MySQL中的一条条SQL语句,都可以理解成一个个事务,而事务基于MySQL连接,也就是线程,当多个事务并发执行的时候也就是多线程并发执行。数据库的锁就是为了解决并发事务下数据的安全性问题。
锁的分类
锁的粒度划分:
- 表锁
- 行锁
互斥性划分:
- 共享锁
- 排他锁
共享锁和排他锁
- 共享锁(S Lock),允许事务读一行数据
- 排他锁(X Lock), 允许事务删除或更新一行数据
如果一个事务T1获取了行r的共享锁,那么事务T2也可以立即获取行r的共享锁,共享锁之间是兼容的,但这是事务T3想获得行r的排他锁,则必须等事务T1、T2释放行r上的共享锁,共享锁和排他锁之间不兼容。
如下表格:
X | S | |
---|---|---|
X | 不兼容 | 不兼容 |
S | 不兼容 | 兼容 |
意向锁
InnoDB支持多粒度的锁,允许行级上的锁和表级上的锁同时存在。为了支持不同力度上的锁,InnoDB支持一种额外的锁方式,也就是意向锁(Intention Lock)。
意向锁是表级的锁,为了要在一个事务中结实下一行将请求的锁类型
- 意向共享锁(IS Lock), 事务想要获得一张表中某几行的共享锁,即当事务要对行加共享锁前,要先对表加意向共享锁
- 意向排他锁(IX Lock),事务想要获取一张表中某几行的排他锁,即当事务要对行加排他锁前,要先对表加意向排他锁
为什么要有意向锁?当给表上S锁时,行不能有X锁,给表上X锁时,行不能有锁。所以给表上锁时要先知道有没有行锁,但是遍历的效率太低又有并发问题,所以加行锁前先加意向锁。
看个例子:
假设表中有一千万的数据,事务T1对id=1000000的事务加行锁,这时事务T2要对这张表加X锁
X锁也就是排他锁,所以T2加锁时要判断表中是否有其他行锁,那么就需要遍历全表,遍历全表的速度较慢,而且可能遍历到一半又有其他事务来对已遍历过的数据加锁。
意向锁就是为了解决这种行锁和表锁的问题,事务T1加行锁前,要先对表加一个意向锁,这样T2在加表锁前只要判断锁是否冲突而不需要遍历全表。
表级意向锁和行级锁的兼容性
IS | IX | S | X | |
---|---|---|---|---|
IS | 不兼容 | |||
IX | 不兼容 | 不兼容 | ||
S | 不兼容 | 不兼容 | ||
X | 不兼容 | 不兼容 | 不兼容 | 不兼容 |
X锁和其他锁都不兼容,S和IX不兼容
锁的算法
- Record Lock: 单个行记录上的锁
- Gap Lock:间隙锁,锁定一个范围但是不包含记录自身
- Next-Key Lock: Record Lock + Gap Lock,锁定一个范围并包含自身
Record Lock
Record Lock记录锁,实际就是行锁,锁住一行数据, 使用方法:
1 | // 获取行级共享锁 |
Gap Lock
现在有user表
1 | mysql> select * from user; |
那么这里的区间就是(-无穷,1),(1, 3),(3, 10),(10, +无穷)
Next-Key Lock
Next-Key Lock类似Gap Lock,只不过是左开右闭, 如(-无穷,1],(1, 3],(3, 10],(10, +无穷)\
当前查询的索引具有唯一性时,InnoDB会对Next-Key Lock进行优化,降级为Record Lock
1 | // 事务1 |
在事务1中,会对id=9这条数据加上X锁,由于id是主键且唯一,因此只会锁定这一条数据,而不会锁住(3, 10)这个间隙,因此事务2不会阻塞能立即成功。
上述降级仅仅在查询的列是唯一索引并且命中唯一行的情况下,如果没有命中行数据或者不是唯一索引的话,则不会降级。
1 | // 事务1 注意:先删除上个操作插入的id=9的行数据 |
这种情况下事务2会阻塞需要ctrl+c手动退出, 因为事务1没有命中行数据,锁定的是(3,9)区间,所以事务2会失败。
1 | // 事务1 |
这种情况下虽然code=10的只有id=10一条行数据,但是还是会锁住code索引(3,10]区间,所以事务2会被阻塞。如果把code索引类型改成UNIQUE,那么事务2则不会被阻塞。
除此之外需要注意的是,在RR隔离级别下InnoDB会对辅助索引的下一个键值加上Gap Lock
1 | // 事务1 |
这种情况下,事务1除了锁住(3,10)区间外,还会锁住(10,+无穷)区间,所以事务2会阻塞
插入意向锁
在《MySQL技术内幕 InnoDB存储引擎》第2版中关于锁的算法只介绍了三种,并没有特别标明插入意向锁,只有一些关于insert操作的描述:
在InnoDB存储引擎中,对于insert操作,其会检查插入记录的下一条记录是否被锁定,若已经被锁定,则不允许插入。
来自《MySQL技术内幕 InnoDB存储引擎》第2版第6章6.4.1小节末尾
插入意向锁实际是一种间隙锁,在insert操作时会判断要插入的位置有没有间隙锁或者临键锁,有的话则等待持有锁的事务提交。当事务持有插入意向锁后,新来事务也要在改区间插入则不会阻塞。也就是说插入意向锁和间隙锁的区别是: 插入意向锁和已有的间隙锁排斥(无论是lock in share mode 还是 for update锁住的),但是插入意向锁之间不互斥,并且插入意向锁是隐式的无法手动获取。
锁的兼容性
横向是已持有锁,纵向是正在请求的锁
Gap | Insert Intention | Record | Next-Key | |
---|---|---|---|---|
Gap | 冲突 | |||
Insert Intention | 冲突 | 冲突 | ||
Record | 冲突 | 冲突 | 冲突 | |
Next-Key | 冲突 | 冲突 | 冲突 |
这个表格是我看了许多博客都是这样描述的(加粗部分的冲突与看到的许多博客描述不符合,博客上大多是写着不冲突,但是我自己实验的结果是冲突的), 但是我的个人看法是无论是Record、Gap还是Next-Key都是锁的算法中的一种,互斥性则是X锁S锁,这两者是不同维度的。 通过 lock in share mode锁的间隙是S锁,通过for update锁住的间隙是X锁,所以这张表格只用来了解插入意向锁和其他锁的兼容性即可。
其他
元数据锁
基于表的元数据加锁, 更改表结构时加的表锁,避免更改表结构时其它事务进行CRUD操作。
自增锁
是专门为了提升自增ID的并发插入性能而设计的, 改善插入数据时的性能。自增锁也是一种特殊的表锁,但它仅为具备AUTO_INCREMENT 自增字段的表服务
参考
掘金:MySQL锁机制
《MySQL技术内幕 InnoDB引擎》第2版