首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜
文章合集 : 🎁 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锁) : 本质上是一种标记,
表示某个事务正准备获取共享锁
- 事务A向数据库系统发起请求,请求获取共享锁
- 事务B此时向数据获取排他锁时,如果数据库查询到存在共享意向锁,则事务B无法获取到排他锁
- 排他意向锁 (IX): 同样是一种标记
- 事务A向数据库系统发起请求,请求获取排他锁
- 事务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_waiting
为true
的锁结构 ,标识自己等待锁释放
- S1 : 当前事务A插入一条聚簇索引记录,该记录的
- 二级索引 : 当发生插入时,会更新所在page的max_trx_id
- S1 : 当触发二级索引的时候,会在二级索引页面得 page Header 部分设置
PAGE_MAX_TRX_ID
属性- 该属性表示对页面
做改动
得最大的事务ID
- 该属性表示对页面
- S2 : 如果 PAGE_MAX_TRX_ID 的值
小于当前最小的活跃事务ID
,说明事务已提交
- S3 : 如果
不是
,则进行回表后
,进行上述主键索引
的逻辑
- S1 : 当触发二级索引的时候,会在二级索引页面得 page Header 部分设置
简单点说,二级索引也是在索引当前的处理情况,如果还在处理,同样要回表加锁
具体更细节的涉及到源码逻辑,这里不深入,可以参考这篇文章 : 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_ORDINARY :表示
LOCK_WAIT : 也就是当第9个比特位置为1
时,表示is_waiting
为true
,也就是当前事务尚未获取到锁,处在等待状态;当这个比特位为0
时,表示is_waiting
为false
,也就是当前事务获取锁成功
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 计算
- 为什么是从第三位才开始标记 1 : 说实话我也没看懂,猜测应该是和 infimun 和 supremum 占用有关
总结
里面有很多东西聊得不是很深。一个是能力有限,本身也是在一边学一边整理。再一个文章得定位上也不需要那么深入。
关于其中锁的结构和一些深入的原理后续会单独的深入,有兴趣的可以关注一下。
另外里面的东西也是这里看一点,那里学一点,可能有遗漏,想了解更多的可以看看大佬的小册,MySQL 是怎样运行的:从根儿上理解 MySQL ,一直在看受益匪浅。
- 如果想全面了解脉络,上面的小册 (
MySQL 是怎样运行的
)是首选 - 如果想继续了解代码层面的原理,可以看
MYSQL内核:INNODB存储引擎
都是大佬的作品,细节我就不想继续深入了,毕竟对实际的业务已经没太大用了。
篇幅太长,这里对整个过程进行一个概述 :
- 锁结构和事务挂钩,行锁和页挂钩,通过行锁结构的 bit_map 来判断这个事务对整个页里面的哪些行加锁
- 隐式锁是指在特定的情况下,不会为数据生成锁结构,但是隐式锁可以转换为显示锁
- 锁可以重用,但是一切的原则都是围绕一个事务,通俗是一个页面的记录 (因为 bit_map 是整个页面的位图)
不能再写了,字数多了写一个字就卡一下,后面的再细说
参考
-
MYSQL内核:INNODB存储引擎
-
MySQL 是怎样运行的:从根儿上理解 MySQL