likes
comments
collection
share

面试爽文 :怎么也想不到,一个 MySQL 锁能问出20多个问题(万字长文)

作者站长头像
站长
· 阅读数 28

一. 前言

👉👉👉 本篇确实不易,花了不少心思,感谢点赞收藏 !!!

实践和问题可以帮助我们更深入的学习,这篇文章算是广度问题,对于一些细节点不会太深入

首先要有个宏观概念,整个锁囊括了大量的内容 ,首先我尝试用简单的几句话概括整个过程 :

  • 我们可以从2个角度看锁 : 锁的类型(行锁 ,表锁 ,间隙锁) ,锁的模式 (独占锁,共享锁)。基本上 90% 的功能(包括 死锁 ,隔离级别的部分原理)都是基于上述2个角度进行分析
  • 日常使用中会涉及到的核心点主要是 : 间隙锁 ,死锁 。 再往深入理解可以涉及锁的结构,事务隔离级别的实现原理

二. 从头认识锁

2.1 锁有哪些范围 ? 不同的存储引擎都有哪些锁 ?

  • 行锁 (记录锁) :锁定数据特定行,控制单个数据的读写
  • 间隙锁 : 用于锁定某个范围的间隙 (防止事务插入数据,出现幻读)
  • 下一键锁 (Next-Key Locks):和间隙锁类似,间隙锁不会锁自己,下一键会锁自己
  • 页级锁 :对数据页进行锁定 (当大批量修改数据或者全表扫描时)
  • 表锁 :锁定数据库表,对表的操作都会阻塞(通常用于表修改和索引调整,全量导出或者备份)
  • 全局锁 :用于锁定整个数据库(用于数据备份)

❓ 那么存储引擎里面支持哪些锁的范围呢?

通常我们谈存储引擎主要是 MyISAM 和 InnerDB 。 MyISAM 功能较少,支持的是表锁,不支持比表锁粒度更低的锁类型。

而 InnerDB 支持上述说的的所有类型,所以其中需要注意的就是不同锁的触发场景,粒度越细的锁触发的越容易。如果要理解得更加透彻,就需要理解锁得内存结构原理,然后明白不同锁带来得性能和损耗下文详述

2.2 锁有哪些模式?有什么特性呢?

  • 共享锁 (读锁、S锁):多个事务可以同时持有同一数据的共享锁
  • 独占锁 (写锁、排他锁、X锁):一个数据的排他锁只能被一个事务持有,其他事务不能再持有该数据的任何锁
    • 这里的任何锁包括读锁,也就是说如果一个数据已经有了排他锁,那么其他事务的共享锁再上锁了
  • 共享意向锁(IS锁) : 本质上是一种标记,表示某个事务正准备获取共享锁
    1. 事务A向数据库系统发起请求,请求获取共享锁
    2. 事务B此时向数据获取排他锁时,如果数据库查询到存在共享意向锁,则事务B无法获取到排他锁
  • 排他意向锁 (IX): 同样是一种标记
    1. 事务A向数据库系统发起请求,请求获取排他锁
    2. 事务B获取共享锁或者排他锁的时候,如果数据库判断存在排他意向锁,则事务B不能上任何锁
  • 隐式锁 : 一种非实体的锁结构

一句话解释 :读锁(S锁)只和读锁兼容,但凡有一个写锁(X锁),读锁就不能和他兼容

2.3 乐观锁和悲观锁是什么?

乐观锁和悲观锁单独拿出来说,这两种锁并不是一种物理概念,而是一种业务用法

  • 乐观锁 : 乐观的认定锁的竞争场景较少 , 不需要特定的锁对象
    • 思路 :通过版本号字段 (自行添加),少部分场景可以通过时间戳
    • 实现 :读取时拿到当前对象的版本号,当操作数据时会通过判断版本号从而判断当前数据是否被修改过
    • 优点 : 性能更优秀,不需要额外的锁定,不会阻塞,也不会影响到其他的并发请求
    • 缺点 : 需要自定义版本号字段,且当版本号不一致时,当前事务会失败
  • 悲观锁 : 认为大部分场景都会出现锁竞争,在事务一开始的时候就去获取锁,保证整个过程的锁定
    • 思路 :读取和操作数据时直接加锁,上文说的物理层面的锁都是悲观锁
    • 实现 :直接加锁,略
    • 优点 : 数据一致性更高 , 不会轻易的出现异常回滚
    • 缺点 : 会锁定或者阻塞数据,并发性能低,可能触发死锁

2.4 通常说的隐式锁是什么 ?

  • 隐式锁是 InnerDB 引擎的一种加锁模式 ,通常 Insert 语句都是隐式锁。
  • 隐式锁是一种延迟加锁机制,当判断不会发生锁冲突的时候,实际上会跳过加锁环节
  • 隐式锁有较小的几率转换为显示锁,常见的例如事务1插入数据未提交事务2尝试对事务2加锁

❓那么隐式锁的原理是什么?

在后文中就可以了解到,每条记录都会有个 trx_id 字段存放在聚集索引中 ,用于判断当前处理该数据的事务 :

❓那么隐式锁又是什么场景转换为显示锁的?

PS :这一段有点超前了, 可以把下面的锁结构看了再回头来看

  • 主键索引 : 通过聚簇索引记录的 trx_id 隐藏列实现
    • S1 : 当前事务A插入一条聚簇索引记录,该记录的 trx_id 为当前事务A
    • S2 : 其他事务B想要对记录进行操作 (读 / 写),判断当前的 trx_id 对应的事务是否为活跃事务
    • S3 : 如果是活跃用户,则访问事务B会帮助 持有该对象的 事务A 创建一个is_waiting 为 false的 X 锁
    • S4 : 同时为自己(B)创建一个 is_waitingtrue 的锁结构 ,标识自己等待锁释放
  • 二级索引 : 当发生插入时,会更新所在page的max_trx_id
    • S1 : 当触发二级索引的时候,会在二级索引页面得 page Header 部分设置 PAGE_MAX_TRX_ID 属性
      • 该属性表示对页面做改动最大的事务ID
    • S2 : 如果 PAGE_MAX_TRX_ID 的值小于当前最小的活跃事务ID,说明事务已提交
    • S3 : 如果不是 ,则进行回表后,进行上述主键索引的逻辑

简单点说,二级索引也是在索引当前的处理情况,如果还在处理,同样要回表加锁

具体更细节的涉及到源码逻辑,这里不深入,可以参考这篇文章 : MySQL InnoDB隐式锁功能解析

2.5 意向锁的作用

  • 插入意向锁是一种间隙锁,在 Insert 时触发
  • 此锁表明,插入同一索引间隙的多个事务如果没有插入间隙内的同一位置,则无需互相等待

  • 案例一 : 如果2个事务在数据 4-7 之间插入 5和6 ,此时2个事务都用插入意向锁锁定4-7,则不会发生阻塞
  • 案例二 : 如果此时一个事务获取 4-7 之间的排他锁,在获取插入意向锁的同时,还是会等待排他锁释放

三. 从原理的角度深入了解锁

3.1 锁的内存结构是什么样的 ?

抽象结构:

  • trx:代表这个锁结构是哪个事务生成的。
  • is_waiting:代表当前事务是否在等待

行锁核心结构:

struct lock_rec_struct{
    ulint space;  // 所在表空间
    ulint page_no; // 当前所处页
    ulint n_bits;  // 位图 , 位图中的索引与记录的 head_no 一一对应
}

关键点 :

  • 锁结构是按照进行区分的
  • 行锁会记录 SpaceID(记录所在表空间),Page Number(记录所在页号) ,n_bits(比特集合)
    • n_bits : 通过比特位来区分哪些记录加了锁,每一个比特位

面试爽文 :怎么也想不到,一个 MySQL 锁能问出20多个问题(万字长文)

3.2 和锁有关的表有哪些 ?

  • INNODB_TRX : 存储有关正在运行或曾经运行的事务的信息
    • trx_id : 事务ID
    • trx_requested_lock_id :请求的锁定标识符
    • trx_wait_started : 等待锁定的开始时间(如果事务正在等待锁)
    • trx_mysql_thread_id : 与事务相关联的 MySQL 线程标识符
    • trx_state (事务状态) / trx_started (事务启动时间) / trx_query (事务SQL查询)
    • trx_tables_in_use (正在时使用的表数量)/ trx_tables_locked (已锁定的表的数量)
    • trx_isolation_level : 事务的隔离级别
  • INNODB_LOCKS :存储有关当前正在等待或持有的锁定的信息
    • lock_id : 锁定的唯一标识符
    • lock_trx_id :持有或等待该锁的事务的标识符
    • lock_mode : 锁定的模式 (共享锁/排他锁)
    • lock_type : 锁定的类型 (表锁 / 记录锁)
    • lock_table / lock_index : 锁定的表和索引
    • lock_space 和 lock_page:受锁定的页的标识符(仅适用于页锁)
    • lock_data : 附加数据 (键值,主键)
  • INNODB_LOCK_WAITS : 存储正在等待锁的事务的信息
    • requesting_trx_id : 正在请求锁的事务的标识符
    • requested_lock_id :所请求的锁的唯一标识符
    • blocking_trx_id : 导致锁等待的事务的标识符

PS :这里提一下 ,在不同的版本中表是不同的,在 8.0 里面这两张表叫 data_locks 和 data_lock_waits

官方文档

3.3 当多个事务到来的时候,加锁流程是怎样的呢?

一个简单的操作 :

  • S1 : 当事务改动一条记录时,会生成一个锁结构与记录相连,此时 is_Waiting 属性为 false
  • S2 : 当第二个事务发起改动求得时候,会首先判断锁结构是否存在(即对象是否上锁),如果已经上锁,则生成第二个锁结构关联该条记录 (此时第二个锁结构得 is_waiting 为 true
  • S3 : 当第一个事务处理完成后,会释放自己的锁结构,同时判断是否有其他的事务等待锁
    • S3-1 : 如果有对象等待锁,则唤醒对应事务的线程,同时修改对应事务的锁结构的 is_waiting 属性

如果涉及到隐式锁,可以看上文2.4之隐式锁的处理

👉👉内存结构层面的锁 (可了解):

行锁的具体实现主体是bitmap,每条记录一个bit存储。

维护一个锁的全局hash表,key值由(space_*id,* page *_* no)计算得到,value为一个链表,存储该页锁信息。用于事务上锁时判断相应页是否存在锁冲突。

同时各个事务都会维护一个锁链表,存储该事务的锁结构。不同事务即使是对同一条记录上同样模式的锁,也需要分别创建一个锁对象。用于事务结束时释放锁

👉👉当新的事务来临时 :

  • S1 : 首先查询 Hash 链表,判断某个页面上是否存在锁
  • S2 : 如果不存在,则直接生成锁(或者隐式锁),生成的锁加入Hash链表事务的锁链
  • S3 : 若存在,则判断是否可重用,如果有冲突,则创建等待锁,并挂起等待(全局维护一个等待对象数组
  • S4 : 当拿到锁时,设置对应记录的 bit_map 位,用于后续的锁冲突判断

3.4 锁的算法说的是什么 ?

InnerDB 引擎里面有3种锁的算法 :

  • Record Lock : 单个行记录的锁
  • Gap Lock : 间隙锁,锁定一个范围,但是不包含记录本身
  • Next-Key Lock : (Gap Lock + Record Lock) , 锁定一个范围的同时锁定记录本身
  • Insert Intention Locks (插入意向锁) :当一个事务想向 Gap Lock (间隙锁)插入数据时,会生成该锁

Insert Intention Locks 场景主要是当一个间隙锁产生的时候,如果另外一个事务想往间隙插入数据,就会产生插入意向锁 , 表示有事务想在某个间隙中插入新记录,但是现在在等待

3.5 归根结底锁的原理是什么 ?

锁的本质其实是对索引加上锁结构,以下是几种常见的场景 :

  • 锁会和事务绑定,一个锁对应一个内存中的锁结构
  • 通常说的对索引加锁不是说把索引数据改了,而是锁结构中会绑定这些信息
  • 每个行/页/表都会有对应的全局变量,记录当前数据/范围是否存在锁,但是最终判断还是要走锁结构

等值查询场景 :

  • 主键等值查询 : 对聚簇索引中对应的主键记录进行加锁
    • 正常查询(Serializable 隔离级别) / LOCK IN SHARE MODE :会加上共享锁
    • FOR UPDATE : 会加上独占锁
  • 主键等值更新 : 会加入独占锁
    • 如果更新了二级索引列,则会在对应的二级索引上加上独占锁
  • 主键删除 : 加锁步骤类似于 update ,先删除聚簇索引记录,再删除对应的二级索引

范围查询场景 :

  • 主键范围查询 : 会基于聚簇索引加间隙锁

最终总结 :

  • 通过主键进行加锁的语句,仅对聚集索引记录进行加锁
  • 通过辅助索引记录进行加锁的语句,首先要对辅助索引记录加锁,再对聚集索引记录加锁
  • 通过辅助索引记录加锁的语句,可能会涉及到下一记录锁和间隙锁

  • 当加锁时,会在内存中生成各种锁对象
  • 同时这些锁对象会根据 space + page_no 映射到对应的哈希桶中 (用于逆向的行查锁)

四. 常用的业务场景和问题 (重点)

4.1 那么不同的操作又是怎么加锁的 ?

前置要点回顾 :共享和排他锁的竞争原则 👉👉

  • 同一个事务里面,数据如果已有了排他锁,还是可以被当前事务获取到共享锁 (一个事务里面不冲突)
  • 不同事务里面,在没有排他锁的场景下,可以任意获取共享锁 (读锁不冲突)
  • 不同事务里面,一个事务获取了排他锁,其他的事务还是不能获取数据的共享锁 (不同事务读写冲突)

❓操作是怎么加锁的呢?

  • 读取加锁 , 可以加S锁和 X锁
    • SELECT ... FROM ; == 如果不是 SERIALIZABLE(串行化)则会进行快照读,不会加锁
    • SELECT ... LOCK IN SHARE MODE; == 对数据加 S 锁,此时读请求可以进来,X锁操作不能进来
    • SELECT ... FOR UPDATE; == 对数据加 X 锁 , 此时任何其他事务的操作都会被阻塞
  • 写操作加锁 (唯一索引):
    • DELETE : 先在 B+ 树获取记录 , 然后获取这条记录的 X锁(排他) , 后面再执行 delete mark 操作
    • UPDATE : 分为多种不同的情况
      • 未修改键值 + 更新的列存储空间无变化 : 只获取 X锁
      • 未修改键值 + 更新的列储存空间变化 : 先获取 X 锁 ,然后删除记录 , 再插入新的数据(隐式锁)
      • 修改了键值 : 在原记录上做DELETE操作之后再来一次INSERT操作
    • INSERT : 新插入一条记录的操作并不加 , 通过隐式锁镜像控制
  • 间隙操作 (其他搜索条件或非唯一索引)SELECT使用FOR UPDATE或FOR SHARE 或 UPDATE和 DELETE:
    • InnoDB锁定扫描的索引范围 ,使用间隙锁或 下一个键锁(Next-Key Locks) 来阻止其他会话插入该范围所覆盖的间隙

👉容易误解的点 :

  • 如果有3个事务对同一个数据进行请求的时候,会产生几个锁结构呢 ?
  • 答 : 这种场景下会生成3种锁结构
    • 第一个锁 :获取到资源的事务 ,会是生产一个 is_waiting = false 的锁
    • 第二个锁 / 第三个锁 : 会生成一个 is_waiting = true 的锁 , 标识等待该数据

MySQL :: MySQL 8.0 Reference Manual :: 15.7.1 InnoDB Locking

4.2 什么是锁重用

在 MySQL 的处理逻辑中,为了减少锁的开销,InnerDB 引擎会重用已经创建好的 lock_t 对象。

锁重用有2个前提 :

  • 同一个事务锁住同一个页面中的记录
  • 锁的模式相同

当符合这2个条件后,就可以复用内存锁结构

4.3 间隙锁到底是什么 ?

  • 幻读 : 一个事务在第二次查询时看到了不同的记录数量或不一致的数据
  • 目的 :保证某个范围内的记录不存在或不被插入新的数据,主要是为了防止幻读
  • 解释 :假设一个事务里面对同一个数据查询了2次,要保证2次查询的结果一致
  • 误导点 : 间隙锁通常不是修改时产生,大多数情况下是在查询时产生

👉间隙锁和下一键锁有什么区别 :

  • 间隙锁 : 锁定范围,防止幻读
  • 下一键锁 :不仅锁定指定键的范围,还锁定该范围内的下一条记录 , 主要目的是为了一致性和隔离性
    • 不仅防止幻读,还阻止其他事务在锁定范围内插入新行,同时也阻止其他事务更新范围内的下一行

👉间隙锁的加锁流程:

SELECT * FROM sso_user WHERE id <= 30 LOCK IN SHARE MODE;

  • S1 : 先从聚簇索引中查询 符合查询条件 (id < 30 )的第一条记录
  • S2 : 查询到这条记录后,依次沿着链表向后查询 (这些个过程中会进行索引条件下推等判断)
  • S3 : 当找到 id = 30 的数据后,还会往后面查找一位 ,即查询id = 31 , 并且为其加锁
    • 但是由于 id =31 不符合查询条件,所以这个会立即释放
    • 带来的问题是,如果 id =31 已经被其他的事务获取到锁了,这里就会阻塞

👉间隙锁案例:

篇幅有限,案例写的还不齐全,现在放出来效果不好,在后面我会单独出一篇聊聊。

4.4 自增锁是什么

  • 数据的自增长是一种特殊的锁,用于处理自增长列,叫自增锁
  • 自增锁是表锁,每张表只有一个
  • 自增锁只能和插入意向锁和读取意向锁兼容
  • 自增值在启动时读取,加载到内存对象 (dict_table_struct 的 autoinc)中
  • 这也是为什么高并发的系统中通常不推荐数据库自增,无法解决分库分表是一种原因 , 性能相对低又是一大原因

👉自增锁的处理流程 :

  • S1 : 当事务插入一条新纪录并且分配自增值时,会向 DBMS 的自增器 申请下一个自增值
  • S2 : 事务此时会锁定自增器,防止其他事务请求值,避免重复
  • S3 : 获取到自增值后 ,事务会释放自增器锁

👉哪些操作会影响自增锁的性能:

  • SQL 批量的导入,或者执行时间较长的插入

4.5 一个死锁通常是怎么产生的 ?

  • 👉 常规原因是满足了死锁的四个条件
    • 互斥条件 : 资源是排他的,一个资源一次只能被一个对象获取
    • 请求与保持条件 :当前对象持有这个资源的是时候会请求其他的资源,并且不会放弃持有当前资源
    • 不可剥夺条件 : 不可以强行从一个对象手中剥夺资源
    • 循环等待条件 : 当前对象需要去等待其他对象的资源,其他对象也要等待当前对象的资源

  • 👉常见场景一 : 表死锁
    • 一个操作需要访先问表A , 再访问表B , 另一个对象需要先访问表B , 再访问表A
    • 当两者都完成第一步访问的时候 ,因为互相持有了他方下一个表的锁而陷入死锁过程

  • 👉常见场景二 : 排他锁死锁
    • A 持有对象的共享锁 , 企图修改 , 所以想要获取独占锁
    • B 持有独占锁 ,但是因为A持有共享所以无法释放独占 , 导致A无法获取独占 , 也无法释放共享

  • 👉死锁的案例 :
// SELECT ... FOR UPDATE
- 用于在事务中获取并锁定某些数据行,以确保其他事务不能同时修改这些数据行

// 事务一 : 
begin; // 开启一个事务
select * from t where a = 1  for update;

// 事务二 :
begin; // 开启一个事务
select * from t where a = 2  for update;

// 事务一 : 
select * from t where a = 2  for update;

// 事务二 : 
select * from t where a = 1  for update;


// 解析 : 
- 当事务一 分别对 a1 , a2 进行 for update 时 ,分别对数据进行加锁,同时不会释放锁
    //>  互斥条件 + 不可剥夺条件
- 当此时互相请求时,就触发了死锁
    //>  请求与保持条件 + 循环等待条件
    

一句话来解释 :我们互相拿了对方想要的东西,但是我们都不想放弃当前拿到的,同时想拿到另外一个

4.6 碰到死锁该怎么分析和处理 ?

  • innodb_locks :记录锁信息
    • 事务想要获取某个锁但是未获取到,会放在该表中
    • 事务获取到锁后,该锁阻塞了其他的事务,则会放在该表中
  • innodb_lock_wait : 当前系统中因为等待哪些锁而让事务进入阻塞状态

❓死锁怎么进行分析? ❓如果死锁产生了该怎么处理?

太深入了,下次单独说

4.7 锁的升级是什么?

从锁的层次上来说 ,分为以下几种锁 :

  • 表级锁(Table-Level Locks) :锁定整个表,适用于需要对整个表进行操作的情况。
  • 页级锁(Page-Level Locks) :锁定数据库中的一页数据,适用于大规模数据集。
  • 行级锁(Row-Level Locks) :锁定单独的行,适用于对表中的特定行进行操作的情况

锁升级指由细粒度的锁 (行级锁)升级到 粗粒度的锁。锁的升级意味着锁的范围更大,会导致更多的锁冲突和阻塞,带来更多的复杂性

不过要注意,锁的升级并不一定代表着性能会降低。

4.8 什么情况下锁会升级 ?

锁升级的主要目的是为了减少锁的数量,通常在以下几种场景中会触发锁的升级 :

  • 锁数量超过阈值 :当事务持有的锁的数量超过数据库管理系统设定的阈值时,系统可能会自动触发锁升级
  • 锁占用内存过多 : 锁资源占用的内存超过了激活内存的40% ,会触发锁升级
  • 提高性能 :锁的数量越多意味着锁的开销越大,当锁升级后,在某种意义上保护了系统资源,防止系统使用太多的内存来维护锁,一定程度上提高了效率
  • 锁冲突 : 如果事务持有的锁和其他的事务冲突时,可能触发锁升级,以减低冲突

五. 继续深入代码层面的锁处理 ? (非重点)

5.1 锁模式的代码层面是什么样的 ?

在上文锁的内存结构中,会存在一个字段 type_mode 用于记录锁的各种信息,总共包含3种 :

  • 低4位 = lock_mode , 用于记录锁的模式
    • LOCK_IS(十进制的0):表示共享意向锁,也就是IS锁
    • LOCK_IX(十进制的1):表示独占意向锁,也就是IX锁
    • LOCK_S(十进制的2):表示共享锁,也就是S锁
    • LOCK_X(十进制的3):表示独占锁,也就是X锁
    • LOCK_AUTO_INC(十进制的4):表示AUTO-INC锁
  • 5-8位 = lock_type , 用于记录锁的类型
    • LOCK_TABLE (十进制的16,即第5位):当为1时表示表级锁
    • LOCK_REC(十进制的32,即第6位):当为1时表示行级锁
  • 其他位 = 行锁的具体类型 , 只有行锁 (LOCK_REC)才会定义
    • LOCK_ORDINARY :表示next-key锁
    • LOCK_GAP : 为1时,表示gap锁
    • LOCK_REC_NOT_GAP : 为1时,表示记录锁
    • LOCK_INSERT_INTENTION : 为1时,表示插入意向锁

LOCK_WAIT : 也就是当第9个比特位置为1时,表示is_waitingtrue,也就是当前事务尚未获取到锁,处在等待状态;当这个比特位为0时,表示is_waitingfalse,也就是当前事务获取锁成功

5.2 锁结构 ,事务 ,内存的关系

上文了解到了锁的结构模型,那么衍生出一个问题,这个结构模型在整个过程中所处的位置 :

👉先来看正向关系里面的锁结构关联 :通过锁找行

  • S1 : 每个事务会有一个 trx_t 的内存对象,该对象记录了事务的锁信息链表和正在等待的锁结构
  • S2 : 每个锁信息链表中的锁都对应上文的一个基础锁结构
  • S3 : 基础锁结构再对应想要的行锁结构

面试爽文 :怎么也想不到,一个 MySQL 锁能问出20多个问题(万字长文)

这里的锁结构可以和上文进行对应 ,lock_t 就是通用的基础锁结构,而 lock_rec_t 才是行锁的结构

👉再来看看逆向结构的锁关联 :通过行找锁

  • S1 : 同故宫一个全局变量 lock_system_struct(lock_sys_t) 来进行锁信息的查好像
  • S2 : lock_sys_t 包含了一个 hash_table , 该表的键值通过 页的 space + pageNo 进行计算
    • 所以流程是 ,判断行时,先通过所在页进行hash查询
    • 然后拿到对应的 lock_t
    • 最后查询到 lock_rec_t 进行判断

面试爽文 :怎么也想不到,一个 MySQL 锁能问出20多个问题(万字长文)

六. 锁和其他概念点之间的关系 (广度)

6.1 锁和事务有哪些联系 ?

锁与事务的关系主要有以下几个方面

👉隔离级别与并发问题的对应关系 :

隔离级别脏读不可重复读幻读
读未提交 (Read Uncommitted)
读已提交 (Read Committed)
可重复读 (Repeatable Read)
串行化 (Serializable)

👉锁对并发控制的影响 :

  • 脏读 : 读取了另外一个未提交事务写的记录
    • 锁的解决方式 : 另外一个事务在写数据时加上排他锁,其他事务无法读取就不会出现脏读
  • 不可重复读 : 当前事务先读取一条记录,另外一个事务对该记录做了改动之后并提交之后,当前事务再次读取时会获得不同的值
    • 锁的解决方式 :同样加入排他锁后,另外的事务无法修改数据,则不会发生不可重复读
  • 幻读 : 为了避免幻读 ,存储引擎会通过间隙锁来控制数据的一致性

👉锁与隔离级别的关系:

读未提交(Read Uncommitted) 和 读已提交(Read Committed) 两者加锁的方式基本一致。在对数据进行加锁时,脏读和不可重复读都不会发生

不同隔离级别下查询和插入的这一篇就不放了,这个要聊起来还是有点多的,下一篇来单独补上。

6.2 锁与表 / 页有什么关系?

👉 从锁的级别上来说 :

  • 当给表加了 S (共享) 锁后 :
    • 其他的事务可以获取该表 ,该表中记录的 S 锁
    • 其他的事务不能获取该表 ,该表中记录的 X (排他)锁
  • 当给表加了 X (排他) 锁后 :
    • 其他事务不能获取该表,该表中记录的 S 和 X 锁

👉从业务场景上来说 :

  • 页的合并 / 页的分裂 : 插入操作会导致 B+树索引的分裂,从而导致页中锁的信息发生变化

如果插入数据后导致页分裂了,行锁的信息最终算是基于页添加的 (page_no) ,则会导致 lock_rec_t 发生分裂

其中还涉及到一些范围的,没细看,有兴趣可以看对应的书籍。

6.3 锁与 MVCC 的区别 ?

  • MVCC 是一种数据快照模式,可以理解为读取的是那个时间节点的镜像数据
  • MVCC 的读取叫 一致性读(一致性无锁读、快照读)
  • MVCC 读取时不会加任何的锁

七. 一些可以了解的复杂点

7.1 锁内存结构中 n_bit 计算

面试爽文 :怎么也想不到,一个 MySQL 锁能问出20多个问题(万字长文)

  • 为什么是从第三位才开始标记 1 : 说实话我也没看懂,猜测应该是和 infimun 和 supremum 占用有关

总结

里面有很多东西聊得不是很深。一个是能力有限,本身也是在一边学一边整理。再一个文章得定位上也不需要那么深入。

关于其中锁的结构和一些深入的原理后续会单独的深入,有兴趣的可以关注一下。

  • 如果想全面了解脉络,上面的小册 (MySQL 是怎样运行的)是首选
  • 如果想继续了解代码层面的原理,可以看 MYSQL内核:INNODB存储引擎

都是大佬的作品,细节我就不想继续深入了,毕竟对实际的业务已经没太大用了。

篇幅太长,这里对整个过程进行一个概述 :

  • 锁结构和事务挂钩,行锁和页挂钩,通过行锁结构的 bit_map 来判断这个事务对整个页里面的哪些行加锁
  • 隐式锁是指在特定的情况下,不会为数据生成锁结构,但是隐式锁可以转换为显示锁
  • 锁可以重用,但是一切的原则都是围绕一个事务,通俗是一个页面的记录 (因为 bit_map 是整个页面的位图)

不能再写了,字数多了写一个字就卡一下,后面的再细说

参考

  • MYSQL内核:INNODB存储引擎

  • MySQL 是怎样运行的:从根儿上理解 MySQL

dev.mysql.com/doc/refman/…

learnku.com/articles/39…

cloud.tencent.com/developer/a…