《MySQL是怎样运行的 —— 从跟上理解MySQL》—— 第二十二章
一、解决并发事务带来问题的两种基本方式
并发事务访问相同记录的情况大致可以划分为3种:
读-读
情况:即并发事务相继读取相同的记录。写-写
情况:即并发事务相继对相同的记录做出改动。 => 发生脏写问题读-写
或写-读
情况:也就是一个事务进行读取操作,另一个进行改动操作。 => 发生脏读
、不可重复读
、幻读
的问题。
1.1 写 - 写情况
在这种情况下会发生脏写
的问题,任何一种隔离级别都不允许这种问题的发生。所以在多个未提交事务相继对一条记录做改动时,需要让它们排队执行,这个排队的过程其实是通过锁
来实现的。这个所谓的锁
其实是一个内存中的结构,在事务执行前本来是没有锁的,也就是说一开始是没有锁结构
和记录进行关联的。当一个事务想对这条记录做改动时,首先会看看内存中有没有与这条记录关联的锁结构
,当没有的时候就会在内存中生成一个锁结构
与之关联。比方说事务T1
要对这条记录做改动,就需要生成一个锁结构
与之关联:
其实在锁结构
里有很多信息,两个比较重要的属性:
trx信息
:代表这个锁结构是哪个事务生成的。is_waiting
:代表当前事务是否在等待。
当事务T1
改动了这条记录后,就生成了一个锁结构
与该记录关联,因为之前没有别的事务为这条记录加锁,所以is_waiting
属性就是false
,这个场景就称之为获取锁成功,或者加锁成功,然后就可以继续执行操作了。
在事务T1
提交之前,另一个事务T2
也想对该记录做改动,那么先去看看有没有锁结构
与这条记录关联,发现有一个锁结构
与之关联后,然后也生成了一个锁结构
与这条记录关联,不过锁结构
的is_waiting
属性值为true
,表示当前事务需要等待,这个场景就称为获取锁失败,或者加锁失败,或者没有成功的获取到锁,
在事务T1
提交之后,就会把该事务生成的锁结构
释放掉,然后看看还有没有别的事务在等待获取锁,发现了事务T2
还在等待获取锁,所以把事务T2
对应的锁结构的is_waiting
属性设置为false
,然后把该事务对应的线程唤醒,让它继续执行,此时事务T2
就算获取到锁了。
1.2 读 - 写 或者 写 - 读情况
这种情况下可能发生脏读
、不可重复读
、幻读
的问题。
SQL标准
规定不同隔离级别下可能发生的问题不一样:
- 在
READ UNCOMMITTED
隔离级别下,脏读
、不可重复读
、幻读
都可能发生。 - 在
READ COMMITTED
隔离级别下,不可重复读
、幻读
可能发生,脏读
不可以发生。 - 在
REPEATABLE READ
隔离级别下,幻读
可能发生,脏读
和不可重复读
不可以发生。 - 在
SERIALIZABLE
隔离级别下,上述问题都不可以发生。
不过各个数据库厂商对SQL标准
的支持都可能不一样,与SQL标准
不同的一点就是,MySQL
在REPEATABLE READ
隔离级别实际上就基本解决了幻读
问题。
怎么解决脏读
、不可重复读
、幻读
这些问题呢?其实有两种可选的解决方案:
- 方案一:读操作利用多版本并发控制(
MVCC
),写操作进行加锁
。 - 方案二:读、写操作都采用
加锁
的方式。
采用MVCC
方式的话,读-写
操作彼此并不冲突,性能更高,采用加锁
方式的话,读-写
操作彼此需要排队执行,影响性能。但是也要看实际的场景。
1.3 一致性读
事务利用MVCC
进行的读取操作称之为一致性读
,或者一致性无锁读
,有的地方也称之为快照读
。所有普通的SELECT
语句(plain SELECT
)在READ COMMITTED
、REPEATABLE READ
隔离级别下都算是一致性读
。
一致性读
并不会对表中的任何记录做加锁
操作,其他事务可以自由的对表中的记录做改动。
1.4 锁定读
1.4.1 共享锁和独占锁
在使用加锁
的方式解决问题时,由于既要允许读-读
情况不受影响,又要使写-写
、读-写
或写-读
情况中的操作相互阻塞,MySQL
给锁分了个类:
共享锁
,Shared Locks
,简称S锁
。在事务要读取一条记录时,需要先获取该记录的S锁
(不严谨)。独占锁
,也常称排他锁
,Exclusive Locks
,简称X锁
。在事务要改动一条记录时,需要先获取该记录的X锁
。
兼容性 | X |
S |
---|---|---|
X |
不兼容 | 不兼容 |
S |
不兼容 | 兼容 |
S锁
和S锁
是兼容的,S锁
和X锁
是不兼容的,X锁
和X锁
也是不兼容的。
1.4.2 锁定读
对读取的记录加
S锁
:SELECT ... LOCK IN SHARE MODE;
如果当前事务执行了该语句,那么它会为读取到的记录加
S锁
,这样允许别的事务继续获取这些记录的S锁
(比方说别的事务也使用SELECT ... LOCK IN SHARE MODE
语句来读取这些记录),但是不能获取这些记录的X锁
。对读取的记录加
X锁
:SELECT ... FOR UPDATE;
如果当前事务执行了该语句,那么它会为读取到的记录加
X锁
,这样既不允许别的事务获取这些记录的S锁
(比方说别的事务使用SELECT ... LOCK IN SHARE MODE
语句来读取这些记录),也不允许获取这些记录的X锁
。
1.5 写操作
平常所用到的写操作
无非是DELETE
、UPDATE
、INSERT
这三种:
DELETE
:对一条记录做DELETE
操作的过程其实是先在B+
树中定位到这条记录的位置,然后获取一下这条记录的X锁
,然后再执行delete mark
操作。可以把这个定位待删除记录在B+
树中位置的过程看成是一个获取X锁
的锁定读
。UPDATE
,在对一条记录做UPDATE
操作时分为三种情况:- 如果未修改该记录的键值并且被更新的列占用的存储空间在修改前后未发生变化,则先在
B+
树中定位到这条记录的位置,然后再获取一下记录的X锁
,最后在原记录的位置进行修改操作。可以把这个定位待修改记录在B+
树中位置的过程看成是一个获取X锁
的锁定读
。 - 如果未修改该记录的键值并且至少有一个被更新的列占用的存储空间在修改前后发生变化,则先在
B+
树中定位到这条记录的位置,然后获取一下记录的X锁
,将该记录彻底删除掉(就是把记录彻底移入垃圾链表),最后再插入一条新记录。这个定位待修改记录在B+
树中位置的过程看成是一个获取X锁
的锁定读
,新插入的记录由INSERT
操作提供的隐式锁
进行保护。 - 如果修改了该记录的键值,则相当于在原记录上做
DELETE
操作之后再来一次INSERT
操作,加锁操作就需要按照DELETE
和INSERT
的规则进行了。
- 如果未修改该记录的键值并且被更新的列占用的存储空间在修改前后未发生变化,则先在
INSERT
:一般情况下,新插入一条记录的操作并不加锁,InnoDB
通过隐式锁
来保护这条新插入的记录在本事务提交前不被别的事务访问。
二、多粒度锁
细粒度锁:行锁
粗粒度锁:表锁
给表加的锁也可以分为共享锁
(S锁
)和独占锁
(X锁
):
- 给表加
S锁
,如果一个事务给表加了S锁
:- 别的事务可以继续获得该表的
S锁
- 别的事务可以继续获得该表中的某些记录的
S锁
- 别的事务不可以继续获得该表的
X锁
- 别的事务不可以继续获得该表中的某些记录的
X锁
- 别的事务可以继续获得该表的
- 给表加
X锁
:如果一个事务给表加了X锁
(意味着该事务要独占这个表):- 别的事务不可以继续获得该表的
S锁
- 别的事务不可以继续获得该表中的某些记录的
S锁
- 别的事务不可以继续获得该表的
X锁
- 别的事务不可以继续获得该表中的某些记录的
X锁
- 别的事务不可以继续获得该表的
那么在上表锁的时候,需要知道有没有行锁,如何知道?InnoDB
使用了意向锁
来解决问题:
- 意向共享锁
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 |
不兼容 | 兼容 | 兼容 | 兼容 |
三、InnoDB是如何加锁的
详情见书
[完整版:Innodb到底是怎么加锁的 (qq.com)](