MySQL 各级别事务的实现机制

MySQL 各级别事务的实现机制
在处理cnctp项目已合包裹状态同步的问题时,发现读包裹状态和对包裹状态的更新不在一个事务内,我提出是否会因为消息并发导致状态一致性问题。在和同事讨论的过程中,我们开始论及事务隔离级别与其实现,以及修改隔离级别是否会对其他会话和事务造成影响等问题。由于学而忘习,对这个问题谈得比较模糊,没有以有力的论据说服其他人,因此整理了一下相关文档记录于此。

谈事务的实现前,先说一下InnoDB(MySQL 5.7)中事务相关的锁:
Shared And Exclusive Locks. InnoDB的标准行锁有共享锁(Shared lock)和排它锁(eXclusive lock)两种,S锁通常用于数据读取,X锁用于读和写。持有S锁的事务允许其他事务获得S锁和X锁;持有X锁的事务不允许其他事务获得S锁或X锁(在事务隔离级别低于Repeatable Read时,X锁允许其他事务在读取数据时获得S锁)。S/X锁作为行级别的锁,也可以通过LOCK TABLE ... READ和LOCK TABLE ... WRITE语句作用于表上。
Intention Lock. Intention Lock同样分 Intention Shared Lock(IS)和 Intention eXclusive Lock(IX)。在事务获取数据的S锁前,需要获得对应表的IS锁或IX锁;在事务获取数据的X锁前,需要获得对应表的IX锁。当事务无法获取作为表锁的意向锁时,就不会尝试获取S/X锁。意向锁的存在意义是告诉请求的事务,在表的级别,已经有事务有对表中某些数据持有S/X锁的意向。如果请求的事务需要获取表级锁(如:ALTER TABLE),且该表级锁与已有的意向锁冲突(如表级X锁与IS锁),则无法获取目标的表锁。这样的设计避免事务再去遍历整个表以判断要获取的锁是否与已存在的S/X锁冲突。
Record Lock. 记录锁是作用于索引记录上的锁,即使表不存在索引,InnoDB也会为查询条件创建隐形的聚簇索引,并对该索引记录加上记录锁。根据读写场景不同,记录锁本身可能是S锁或是X锁。
Gap Lock. 区间锁,在条件的索引判断的开区间内(判断条件为=时,开区间为判等的索引记录和上一条数据索引记录之间),不允许没获得该锁的事务插入数据。若使用的索引为unique索引,则区间锁不生效,但unique字段为组合索引字段之一时,区间锁依然生效。区间锁只是锁住了Insert语句的执行,并不影响Update语句,因此大部分时候,区间锁只是在Repeatable Read的事务级别上起作用,直接解决幻读问题。若事务隔离级别从Repeatable Read降级,或是更改 innodb_locks_unsafe_for_binlog 变量的值为1,区间锁失效。
Next-Key Lock. Next-Key Lock是Record Lock + Gap Lock的结合,即锁住索引记录本身和索引记录的前区间,构成一个前开后闭区间。InnoDB默认事务级别为Repeatable Read,即是使用Next-Key Lock避免其它事务引起前区间的幻影行和记录本身的更新。在标准的事务隔离级别中,顾名思义,RR应该是避免脏读和不可重复读,但MySQL通过Next-Key Lock,在RR级别避免了幻读,因此MySQL中,Serializable相对于RR,并不额外避免幻读,也不会真的让所有事务串行化执行,而是在RR的基础上,对纯SELECT语句加上LOCK IN SHARE MODE。这种行为让事务从MVCC实现变成了基于二段锁的读写,但非串行化执行依然可能出现死锁。
Insert Intention Lock. 插入意向锁,这是一种特殊的区间锁,只在Insert语句生成,它阻止事务向已持有区间锁的区间插入数据。前面我们说区间锁可以阻止其他事务向已加锁的区间插入数据,但执行相关WHERE子句时,相关区间只是获得了区间锁,判断能否插入数据,则是通过插入意向锁。两个同区间的插入意向锁互不影响,允许不同键值数据的插入,就像同区间的区间锁互不影响。

MySQL事务隔离级别
事务的ACID性质:
A: Atomicity 原子性。事务内的操作要么全部执行,要么无一执行,事务像原子一样不可细分为多部分分开执行。redo log和undo log是MySQL实现原子性的保障,事务COMMIT后,形成redo log,这些日志负责将事务的变更写入磁盘,同时undo log将记录对应的反向变更,当用户执行ROLLBACK或redo log执行失败时,undo log负责将已执行到磁盘的变更还原。从实现来说,实现事务的原子性即是实现回滚。
C: Consistency 一致性。事务提交后才对其他事务可见,即事务一旦提交,其他事务查看到的结果应该一致,事务一旦回滚,其他事务也只能看到回滚前的状态。一致性着重强调系统的错误恢复能力,MySQL通过double write buffer来实现这一能力。事务提交后,尚未同步到磁盘的数据称为脏页,它们将先被写入double write buffer中,在积累一定数量后,才以顺序访问的方式写入磁盘,以提高写入效率。如果写入过程崩溃,MySQL在重启后可以通过读取double write buffer中的数据进行数据恢复。
I: Isolation 隔离性。多个事务的执行相互独立,执行期间导致的变更彼此不可见。就隔离性而言,事务有四个通用的标准,即Read Uncommitted, Read Committed, Repeatable Read和Serializable. 通用定义中,RU会造成脏读,RC避免了脏读但可能发生不可重复读,RR避免不可重复读却可能发生幻读,S级别要求所有事务串行化执行,即避免了所有数据冲突和死锁。但对于MySQL来说,它的实现机制使得在RR级别,数据已经避免了幻读;而即使在Serializable级别,事务也并非串行化执行。这点在介绍Next-Key Lock时有解释。
D: Durability 持久性。一旦事务提交,所有变更立即写入磁盘进行持久化。MySQL事务提交后,通过redo log写入double write buffer,在将脏页写入磁盘时,根据磁盘的硬件特性,会选择合适的同步操作执行原子性的数据写入,这里的原子性由磁盘保证,我们也不做深究。所以MySQL对持久性的保障,是基于redo log和double write buffer的容错恢复能力,并不是数据提交即写入磁盘。

ACID是性质,Lock和MVCC是实现方式。从隔离性的描述,我们知道事务的隔离级别分为Read Uncommitted, Read Committed, Repeatable Read和Serializable,InnoDB通过MVCC实现了Consistent Nonlocking Read,避免了对大多数读操作加S锁。MVCC的实现虽然能提高读的效率,但锁并不能被完全替代。
Read Uncommitted 读写互不阻塞,不使用锁,而MVCC也不保证多次读取到的数据一定是一致的,即可能读取到别的事务提交的版本。
Read Committed 通过Consistent Nonlocking Read(MVCC)实现读不阻塞写,每次读取对于行的最新快照,所以可能导致不可重复读;对于加锁读和写操作,InnoDB获取行锁,但不获取行前的Gap Lock,因此可能导致幻读
Repeatable Read 同样通过Consistent Nonlocking Read读取数据,但读数据时检查事务版本与首次读取的版本相同,因此避免了不可重复读;对于加锁读和写操作,InnoDB根据使用的索引情况对数据加锁,如果使用unique key,InnoDB仅获取行锁,如果是其他索引,则获取Gap Lock或Next-Key Lock,故而也能避免幻读。
Serializable 在RR的基础上,对所有SELECT语句隐士地加上LOCK IN SHARE MODE,在autocommit开启时,如果其它事务没有改变目标行,则不会阻塞同获得S锁的读操作。

MySQL通过这些手段实现事务,通过这些简略的描述我们不难发现,理论和实践并不是完全一致的,实践为了追求性能,往往会做一些对理论定义的妥协。例如MySQL实现事务的原子性,作为多步执行的事务并不像真正的原子一样,是一个不可切分的单元,只是在MySQL实现了回滚机制后,我们可以认为事务的多步操作是一个整体,像一个原子一样不可分割;而Redis为了追求更高的性能,甚至在设计层面抛弃了回滚机制,只保证所有多步执行的操作在执行时不被别的事务中断,但如果事务中的一个步骤执行出错或者Redis Server崩溃,事务并不会回滚到执行前的状态。这些性能需求,是为了满足现实世界不同的应用场景而产生的,实践通过采用高性能但可能出错的方案,结合各种容错机制,做到性能和理论间的平衡。

对于文章开头提及的一个问题,即一个事务的隔离级别是否会影响链接数据库的其他会话和事务的隔离级别,我们在解释了事务隔离级别的实现后,没有发现有影响的地方。部分更改数据库配置的行为,如改变innodb_locks_unsafe_for_binlog或autocommit的值,可能影响锁的行为,从而影响其他事务,但这些修改均不是在Java程序中完成的,单纯使用Spring的Transactional注解指定访问事务的隔离级别,不能改变这些配置项。

关于一致性的定义,从不同的角度似乎能给出不同的解释,欢迎讨论。
如有误欢迎指出。

posted @ 2018-07-24 11:26  穆穆兔兔  阅读(631)  评论(0编辑  收藏  举报