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

首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜
文章合集 : 🎁 juejin.cn/post/694164...
Github : 👉 github.com/black-ant
CASE 备份 : 👉 gitee.com/antblack/ca...

一. 前言

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

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

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

  • 我们可以从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 :代表当前事务是否在等待

行锁核心结构:

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

关键点 :

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

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 归根结底锁的原理是什么 ?

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

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

等值查询场景 :

  • 主键等值查询 : 对聚簇索引中对应的主键记录进行加锁
    • 正常查询 / 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无法获取独占 , 也无法释放共享

  • 👉死锁的案例 :
java 复制代码
// 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 : 基础锁结构再对应想要的行锁结构

这里的锁结构可以和上文进行对应 ,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 进行判断

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

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 是怎样运行的:从根儿上理解 MySQL

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

总结

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

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

另外里面的东西也是这里看一点,那里学一点,可能有遗漏,想了解更多的可以看看大佬的小册,MySQL 是怎样运行的:从根儿上理解 MySQL ,一直在看受益匪浅。

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

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

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

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

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

参考

  • MYSQL内核:INNODB存储引擎

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

dev.mysql.com/doc/refman/...

learnku.com/articles/39...

juejin.cn/post/687888...

cloud.tencent.com/developer/a...

相关推荐
liang_jy3 分钟前
数组(Array)
数据结构·面试·trae
xcya5 分钟前
Java ReentrantLock 核心用法
后端
用户4665370150518 分钟前
如何在 IntelliJ IDEA 中可视化压缩提交到生产分支
后端·github
星空下的曙光21 分钟前
mysql 命令语法操作篇 数据库约束有哪些 怎么使用
数据库·mysql
小楓120124 分钟前
MySQL數據庫開發教學(一) 基本架構
数据库·后端·mysql
天天摸鱼的java工程师26 分钟前
Java 解析 JSON 文件:八年老开发的实战总结(从业务到代码)
java·后端·面试
白仑色27 分钟前
Spring Boot 全局异常处理
java·spring boot·后端·全局异常处理·统一返回格式
之诺33 分钟前
MySQL通信过程字符集转换
后端·mysql
喵手34 分钟前
反射机制:你真的了解它的“能力”吗?
java·后端·java ee
用户4665370150535 分钟前
git代码压缩合并
后端·github