MySQL 锁机制
乐观锁
程序实现
- 版本号
- 时间戳
悲观锁
全局锁
- Flush tables with read lock (FTWRL)
表级锁
表锁(MySQL layer)
- 加锁:lock tables … read/write
- 解锁:unlock tables
当线程A lock tables t1 read,线程A和其他线程都只能读 t1,不能写t1
当线程A lock tables t1 write,线程A可以读写t1,其他线程不能读写t1
元数据锁(MySQL layer)
元数据锁MDL(metadata lock),元数据:指表结构
- CRUD(DML):加 MDL 读锁
- 表做结构变更(DDL):加 MDL 写锁
兼容性
读锁之间不互斥;读锁和写锁,写锁和写锁间互斥
意向锁(InnoDB)
共享读锁(IS)
- 在给行记录加S锁时,会先自动给数据所在表加IS锁
排他写锁(IX)
- 在给行记录加X锁时,会先自动给数据所在表加IX锁
意向锁的作用
事务A锁住了表中的一行,让这一行只能读(数据库自动给该表增加意向共享锁),不能写。
事务B申请整个表的写锁。
- 判断表是否已被其他事务用表锁锁表
- 发现表上有意向共享锁,说明表中有些行被共享行锁锁住了,因此,事务B申请表的写锁会被阻塞。
如果没有意向锁则是这样的:
- 判断表是否已被其他事务用表锁锁表
- 判断表中的每一行是否已被行锁锁住。
如果没有意向锁的话,则需要遍历所有整个表判断是否有行锁的存在,以免发生冲突。
如果有了意向锁,只需要判断该意向锁与即将添加的表级锁是否兼容即可。因为意向锁的存在代表了,有行级锁的存在或者即将有行级锁的存在。因而无需遍历整个表,即可获取结果。
兼容性
是否兼容(表级) | IS | IX | S | X |
---|---|---|---|---|
IS | 是 | 是 | 是 | 是 |
IX | 是 | 是 | 否 | 否 |
S | 是 | 否 | 是 | 否 |
X | 否 | 否 | 否 | 否 |
- 意向锁相互兼容,因为IX、IS只是表明申请更低层次级别元素(比如 page、记录)的X、S操作。
- 表级S锁和X、IX锁不兼容:因为上了表级S锁后,不允许其他事务再加X锁。
- 表级X锁和 IS、IX、S、X不兼容:因为上了表级X锁后,会修改数据,所以即使是行级排他锁,因为表级锁定的行肯定包括行级锁定的行,所以表级X和IX、X都不兼容。
- 上了行级X锁后,行级X锁不会因为有别的事务上了IX而堵塞,MySQL是允许多个行级X锁同时存在的,只要他们不是针对相同的数据行。
行级锁(InnoDB)
重点:InnoDB行级锁基于索引实现
两阶段锁协议
- 定义:在 InnoDB 事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。
- 可重复读隔离级别下遵守两阶段提交,事务结束才释放锁。read-commited 语句执行完就释放“不满足条件的行”的行锁,而不是在事务结束的时候才释放。
普通select语句默认不会加任何锁类型,是一致性读(MVCC),所以加锁不会阻塞普通select
共享读锁(S)
- 手动加锁:select … lock in share mode(当前读)
使用索引时,为行锁;
未使用索引时,行锁会升级为表锁。
排他写锁(X)
- 自动加锁:select for update (当前读)
- 自动加锁:DML(insert、update、delete)
兼容性
- 读锁写锁间互斥,写锁写锁间互斥
- 读锁读锁间兼容
行锁的具体实现:
Record Locks
记录锁,专门对索引项加锁
- 记录锁, 仅仅锁住索引记录的一行,在单条索引记录上加锁。
- record lock锁住的永远是索引,而非记录本身,即使该表上没有任何索引,那么innodb会在后台创建一个隐藏的聚集主键索引,那么锁住的就是这个隐藏的聚集主键索引。
所以说当一条sql没有走任何索引时,那么将会在每一条聚合索引后面加X锁,类似于表锁。
Gap Locks
间隙锁,对索引项之间间隙加锁
- 区间锁, 仅仅锁住一个索引区间,开区间:()。
- 在索引记录之间的间隙中加锁,或者是在某一条索引记录之前或者之后加锁,并不包括该索引记录本身。比如在 1、2、3中,间隙锁的可能值有 (∞, 1),(1, 2),(2, ∞),
- 间隙锁可用于防止幻读,保证索引间的不会被插入数据
- 间隙锁在可重复读隔离级别下才会生效的。如果把隔离级别设置为读提交的话,就没有间隙锁了。
- 间隙锁的引入,可能会导致同样的语句锁住更大的范围,影响了并发度,如果不需要可重复读的隔离级别,可以使用读提交隔离级别 + binlog_format=row
兼容性
- 跟间隙锁存在冲突关系的,是“往这个间隙中插入一个记录”这个操作。间隙锁之间都不存在冲突关系。
Next-Key Locks
临键锁,是前面两种的组合,对索引项以及之间的间隙加锁
- record lock + gap lock, 左开右闭区间:(]。
- 默认情况下,innodb使用next-key locks来锁定记录。
- Next-Key Lock 的加锁过程是拆分为:加间隙锁和加行锁两段来执行的
- 但当查询的索引含有唯一属性的时候,Next-Key Lock 会进行优化,将其降级为Record Lock,即仅锁住索引本身,不是范围。
- Next-Key Lock在不同的场景中会降级:
场景 | 降级成的锁类型 |
---|---|
使用唯一索引,精确匹配(=),且记录存在 | Record Lock |
使用唯一索引,精确匹配(=),且记录不存在 | Gap Lock |
使用唯一索引范围匹配(< 和 >) | Record Lock + Gap Lock |
幻读相关
- 幻读指的是一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行。
- 在可重复读隔离级别下,普通的查询是快照读,是不会看到别的事务插入的数据的。因此,幻读在“当前读”下才会出现。
- Gap 锁可以避免出现幻读
加锁规则
- 加锁的基本单位是 next-key lock。next-key lock 是前开后闭区间。
- 查找过程中访问到的对象才会加锁。
- 索引上的等值查询,给唯一索引加锁的时候,next-key lock 退化为行锁。
- 索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock 退化为间隙锁。
- 唯一索引上的范围查询会访问到不满足条件的第一个值为止。
(可能是一个BUG,据说8.0.18以后版本已修复)