22.1.1 写-写情况
当一个事务想对这条记录做改动时,首先会看看内存中有没有与这条记录关联的锁结构,当没有的时候就会在内存中生成一个锁结构与之关联
22.1.2 读-写或写-读情况
- 方案1:读操作利用多版本并发控制(MVCC),写操作进行加锁
查询语句只能读到在生成 ReadView 之前已提交事务所做的更改,在生成 ReadView 之前未提交的事务或者之后才开启的事务所做的更改是看不到的
- 方案2:读、写操作都采用加锁的方式
采用 MVCC 方式的话, 读-写操作彼此并不冲突,性能更高,采用加锁方式的话,读-写操作彼此需要排队执行,影响性能
22.1.3 一致性读
事务利用 MVCC 进行的读取操作称之为一致性读,一致性读并不会对表中的任何记录做加锁操作
22.1.4 锁定读
在读取记录前就为该记录加锁的读取方式称为锁定读
- 共享锁:Shared Locks,简称 S 锁。在事务要读取一条记录时,需要先获取该记录的 S 锁
- 独占锁:Exclusive Locks,也称排他锁,简称 X 锁。在事务要改动一条记录时,需要先获取该记录的 X 锁
兼容性 | X锁 | S锁 |
---|---|---|
X锁 | × | × |
S锁 | × | √ |
- 对读取的记录加 S 锁
SELECT ... LOCK IN SHARE MODE;
如果当前事务执行了该语句,那么它会为读取到的记录加 S 锁,这样允许别的事务继续获取这些记录的 S 锁,但是不能获取这些记录的 X 锁。如果别的事务想要获取这些记录的 X 锁,那么它们会阻塞,直到当前事务提交之后将这些记录上的 S 锁释放掉
- 对读取的记录加 X 锁
SELECT ... FOR UPDATE;
为读取到的记录加 X 锁,这样既不允许别的事务获取这些记录的 S 锁,也不允许获取这些记录的 X 锁,如果别的事务想要获取这些记录的 S 锁或者 X 锁,那么它们会阻塞,直到当前事务提交之后将这些记录上的 X 锁释放掉
22.2 多粒度锁
- 意向共享锁:Intention Shared Lock,简称 IS 锁,当事务准备在某条记录上加 S 锁时,需要先在表级别加一个 IS 锁
- 意向独占锁:Intention Exclusive Lock,简称 IX 锁,当事务准备在某条记录上加 X 锁时,需要先在表级别加一个 IX 锁
IS、IX锁是表级锁,它们的提出仅仅为了在之后加表级别的 S 锁和 X 锁时可以快速判断表中的记录是否被上锁,以避免用遍历的方式来查看表中有没有上锁的记录,也就是说其实 IS 锁和 IX 锁是兼容的,IX 锁和 IX 锁是兼容的
兼容性 | X | IX | S | IS |
---|---|---|---|---|
X | × | × | × | × |
IX | × | √ | × | √ |
S | × | × | √ | √ |
IS | × | √ | √ | √ |
Record Lock:在记录上加的锁
Gap Lock:为了防止插入幻影记录而提出的
- Next-Key Lock:本质就是一个记录锁和一个 gap 锁的合体,它既能保护该条记录,又能阻止别的事务将新记录插入被保护记录前边的间隙
22.4 语句加锁分析
22.4.1 普通的SELECT语句
- 在
READ UNCOMMITTED
隔离级别下,不加锁,直接读取记录的最新版本;可能出现脏读、不可重复读、幻读 - 在
READ COMMITTED
隔离级别下,不加锁,在每次执行普通的SELECT
语句时会生成一个 ReadView,这样避免了脏读现象,但没有避免不可重复读和幻读 - 在
REPEATABLE READ
隔离级别下,不加锁,只在第一次执行普通的SELECT
语句时会生成一个 ReadView,这样就避免了脏读和不可重复读,也能在一定程度上避免幻读
之所以不能完全避免幻读,是因为 ReadView 并不能阻止当前事务执行UPDATE
或者DELETE
语句来改动其他事务新插入的记录,这样新纪录的trx_id
就变成了当前事务的id,后面再使用普通的SELECT
就能看到这条记录了,可以认为 InnoDB 中的 MVCC 并不能完全禁止幻读
22.4.2 锁定读语句
- 匹配模式
如果被扫描的区间是一个单点扫描区间,就可以说匹配模式为精确匹配
- 唯一性搜索
如果在扫描某个扫描区间的记录前,就能事先确定该扫描区间内最多只包含一条记录,就把这种情况称作唯一性搜索
需要满足这些条件:
- 匹配模式为精确匹配
- 使用的索引是主键或唯一二级索引
- 如果使用的索引是唯一二级索引,搜索条件不能为“索引 IS NULL”
- 如果索引包含多个列,每一列都要用到
事务在执行过程中所获取的锁一般在事务提交或者回滚时才会释放,但是在隔离级别不大于
READ COMMITTED
时,在某些情况下也会提前将一些不符合搜索条件的记录上的锁释放掉
对于锁定读的语句,在隔离级别不大于READ COMMITTED
时,会为当前记录加记录锁;在隔离级别不小于REPEATABLE READ
时,会为当前记录加next-key
锁
- 隔离级别不大于
READ COMMITTED
,且读取聚簇索引记录
对读取到的记录加 S 型记录锁,如果该记录不满足其他条件则释放锁,如果满足条件则发送到客户端,但不释放锁
- 隔离级别不小于
REPEATABLE READ
,且读取聚簇索引记录
对读取到的记录加 S 型 next-key 锁,如果该记录不满足其他条件也不会释放锁,如果满足条件则发送到客户端,但不释放锁
SELECT ... FOR UPDATE
语句的加锁过程与SELECT ... LOCK IN SHARE MODE
语句类似,只不过为记录加的是 X 锁
- 当隔离级别不大于
READ COMMITTED
时,如果匹配模式为精确匹配,则不会为扫描区间后面的下一条记录加锁 - 当隔离级别不小于
REPEATABLE READ
时,如果匹配模式为精确匹配,则会为扫描区间后面的下一条记录加 gap 锁
22.4.3 半一致性读的语句
半一致性读(Semi-Consistent Read)是一种夹在一致性读和锁定读之间的读取方式。当隔离级别不大于READ COMMITTED
且执行UPDATE
语句时将使用半一致性读。所谓半一致性读,就是当UPDATE
语句读取到已经被其他事务加了 X 锁的记录时,InnoDB 会将该记录的最新提交版本读出来,然后判断该版本是否与UPDATE
语句中的搜索条件相匹配。如果不匹配,则不对该记录加锁,从而跳到一条记录;如果匹配,则再次读取该记录并对其进行加锁。这样处理只是为了让UPDATE
语句尽量少被别的语句阻塞
22.6 死锁
InnoDB 死锁检测机制:当检测到死锁发生时,会选择一个较小的事务进行回滚(是指在事务执行过程中插入、更新或删除的记录条数较少的事务)