首先说一下事务并发存在的问题
- 脏读:事务A在执行期间读到了事务B还未提交的数据,就是脏读
- 不可重复读:事务A在执行期间,两条同样的SQL,第一次查询id为1的记录age为18,第二次查询id为1的记录age变成了20。同样的查询,返回了不同的数据就是不可重复读
- 幻读:事务A执行两次同样的SQL,第一次返回了三条记录,第二次返回了四条或者两条记录,结果集不一样。也就是事务A查询一个范围的结果集,另一个并发事务B在这个范围内插入 / 删除了数据并且提交了,然后事务A再次查询,两个得到的结果集不一样了,就是幻读。
顺便再说一下MySQL的四大隔离级别
- 读未提交:事务A能读到别的事务没有提交的数据,就是读未提交,解决了脏读,但是没有解决不可重复读、幻读。
- 读已提交:在读未提交的基础上解决了脏读,但还是有不可重复读、幻读
- 可重复读:解决了脏读、不可重复读(只解决了快照读情况下),但是没有解决幻读
- 串行化:解决了所有并发问题,因为同一时间只有一个线程可以执行
下面画一个经典表格
脏读 | 不可重复读 | 幻读 | |
---|---|---|---|
读未提交 | × | × | × |
读已提交 | √ | × | × |
可重复读 | √ | √ | × |
串行化 | √ | √ | √ |
再分类一下MySQL中的几种锁
- 意向锁
- 共享 / 排他 锁
- 间隙锁
- 记录锁
- 临键锁
意向锁
意向锁是一种不与行级锁冲突的表级锁。未来的某个时刻,事务可能要加共享或者排它锁时,先提前声明一个意向。注意一下,意向锁,是一个表级别的锁。
因为InnoDB是支持表锁和行锁共存的,如果一个事务A获取到某一行的排他锁,并未提交,这时候事务B请求获取同一个表的表共享锁。因为共享锁和排他锁是互斥的,因此事务B想对这个表加共享锁时,需要保证没有其他事务持有这个表的表排他锁,同时还要保证没有其他事务持有表中任意一行的排他锁。
然后问题来了,你要保证没有其他事务持有表中任意一行的排他锁的话,去遍历每一行?这样显然是一个效率很差的做法。为了解决这个问题,InnoDB的设计大叔提出了意向锁。
意向锁是如何解决这个问题的呢? 我们来看下
意向锁分为两类:
-
意向共享锁:简称IS锁,当事务准备在某些记录上加S锁时,需要现在表级别加一个IS锁。
-
意向排他锁:简称IX锁,当事务准备在某条记录上加上X锁时,需要现在表级别加一个IX锁。
比如: -
select … lock in share mode,要给表设置IS锁;
-
select … for update,要给表设置IX锁;
如果一个事务A获取到某一行的排他锁,并未提交,这时候表上就有意向排他锁和这一行的排他锁。这时候事务B想要获取这个表的共享锁,此时因为检测到事务A持有了表的意向排他锁,因此事务A必然持有某些行的排他锁,也就是说事务B对表的加锁请求需要阻塞等待,不再需要去检测表的每一行数据是否存在排他锁啦。这样效率就高很多啦。
意向锁仅仅表明意向的锁,意向锁之间并不会互斥,是可以并行的,整体兼容性如下图所示:
兼容性 | IS | IX | S | X |
---|---|---|---|---|
IS | 兼容 | 兼容 | 兼容 | 不兼容 |
IX | 兼容 | 兼容 | 不兼容 | 不兼容 |
S | 兼容 | 不兼容 | 兼容 | 不兼容 |
X | 不兼容 | 不兼容 | 不兼容 | 不兼容 |
共享 排他 锁
InnoDB呢实现了两种标准的行级锁:共享锁(简称S锁)、排他锁(简称X锁)。
- 共享锁:简称为S锁,在事务要读取一条记录时,需要先获取该记录的S锁。
- 排他锁:简称X锁,在事务需要改动一条记录时,需要先获取该记录的X锁。
如果事务T1持有行R的S锁,那么另一个事务T2请求访问这条记录时,会做如下处理:
- T2 请求S锁立即被允许,结果 T1和T2都持有R行的S锁
- T2 请求X锁不能被立即允许,此操作会阻塞
- 如果T1持有行R的X锁,那么T2请求R的X、S锁都不能被立即允许,T2 必须等待T1释放X锁才可以,因为X锁与任何的锁都不兼容。
S锁和X锁的兼容关系如下图表格:
兼容性 | S | X |
---|---|---|
S | 兼容 | 不兼容 |
X | 不兼容 | 不兼容 |
X锁和S锁是对于行记录来说的话,因此可以称它们为行级锁或者行锁。我们认为行锁的粒度就比较细,其实一个事务也可以在表级别下加锁,对应的,我们称之为表锁。给表加的锁,也是可以分为X锁和S锁的哈。
如果一个事务给表已经加了S锁,则:
- 别的事务可以继续获得该表的S锁,也可以获得该表中某些记录的S锁。
- 别的事务不可以继续获得该表的X锁,也不可以获得该表中某些记录的X锁。
如果一个事务给表加了X锁,那么
- 别的事务不可以获得该表的S锁,也不可以获得该表某些记录的S锁。
- 别的事务不可以获得该表的X锁,也不可以继续获得该表某些记录的X锁。
记录锁(Record Lock)
顾名思义,就是针对于记录的锁,不过MySQL的记录锁是针对于聚簇索引的,也就是主键索引。当加锁条件能匹配到索引就是行锁,否则就是表锁。
- 如果查询条件用了索引/主键,那么select … for update就会进行 行锁。
- 如果是普通字段(没有索引/主键),那么select … for update就会进行 锁表。
间隙锁(Gap Lock)
为了解决幻读问题,InnoDB引入了间隙锁(Gap Lock)。间隙锁是一种加在两个索引之间的锁,或者加在第一个索引之前,或最后一个索引之后的间隙。它锁住的是一个区间,而不仅仅是这个区间中的每一条数据。
临键锁(Next-Key Lock)
Next-key锁是记录锁和间隙锁的组合,它指的是加在某条记录以及这条记录前面间隙上的锁。说得更具体一点就是:临键锁会封锁索引记录本身,以及索引记录之前的区间,即它的锁区间是前开后闭,比如(5,10]。
如果一个会话占有了索引记录R的共享/排他锁,其他会话不能立刻在R之前的区间插入新的索引记录。
所谓的next key lock就是一个行锁(record lock)+范围锁(gap lock),比如某一个辅助索引,如果它有1,3,5这几个值,那么当我们使用next key lock的锁住class_id=1的时候,实际上锁住了(-无穷,1],或者锁住class_id=3的时候,实际上锁住的是(1,3],也就是一个左开右闭的区间。如果此时别的事务要在这个区间内插入数据,就会被阻塞住。这个锁一直到事务提交才会释放。因此,即使出现了特殊情况,也可以保证前后两次去读的内容一致,因为对这个辅助索引上的锁是:“next key lock”,他会锁住一个区间。
但是注意,对于可重复读默认使用的就是next key lock,但是对于“唯一索引”,比如主键的索引,next key lock会降级成行锁,而不会锁住一个区间。因此,如果上面的事务1的update使用的是主键,事务2也使用主键进行插入,那么实际上事务2根本不会被阻塞,可以立即插入并返回。而对于非唯一索引,next key lock则不会降级。