文: 孙成
本文原创,转载请注明作者及出处
我所在的公共业务团队为沪江提供底层业务服务,为了保证系统有能力抗住高并发大流量的压力,我们保证包括数据库在内的系统各个节点的性能,而了解实现原理则会让优化更加精准有效。沪江在去年完成了主要系统的去Windows化,数据库也从SQL Server切换到了MySQL。作为一款被业界广泛使用的数据库,MySQL早已是当今世界上最流行的关系型数据库之一,但是对于曾经比较熟悉微软生态的开发人员来说,可能更加熟悉SQL Server。虽然MySQL和SQL Server使用起来很相似,但是具体实现细节上它们仍然有很大的差异。下面我将分享MySQL InnoDB存储引擎中关于一致性读的相关实现原理。
一致性读(consistend read)
InnoDB中的一致性读(consistend read)指的是利用多版本查询数据库在某个时间点的快照。此查询可以看到该时间点之前提交的事务所做的更改并且不会被之后的修改或者未提交事务所影响。但是对于同一事务中的较早语句的修改则不适用此规则,这种情况会产生以下异常:如果你更新表中的某些行,一次SELECT可能看到更新行的最新版本也可能看到任一行的旧版本;如果其它会话同时更新到同一个表,则可能会看到该表处于数据库中从未存在过的状态。
当事务隔离级别为REPEATABLE READ时,同一个事务中的一致性读都是读取的是该事务下第一次查询所建立的快照。
当事务隔离级别为READ COMMITTED时,同一事务下的一致性读都会建立和读取此查询自己的最新的快照。
一致性读是InnoDB在REPEATABLE READ和READ COMMITTED事务隔离中处理SELECT语句的默认模式。一致性读不会在表上设置任何锁,所以其它会话可以对表进行读写操作。
数据库状态的快照适用于事务中的SELECT语句,而不一定适用于DML语句。如果执行INSERT或者UPDATE某些行然后提交该事务,则从另一个并发REPEATABLE READ事务发出的DELETE或UPDATE语句则会影响那些刚刚提交的数据行。
下面这个示例展示了这种场景:
一致的读取不适用于某些DDL语句,如:
1) 一致性读不适用于DROP TABLE,因为表已经被InnoDB销毁了。2) 一致性读不适用于ALTER TABLE,因为ALTER TABLE实际是生成一张原始表的临时表,并在构建完成后删除原始表。在事务中进行一致的读取时,新表中的行不可见,这种情况下事务会返回ERTABLEDEF_CHANGED错误(表定义已更改,请重试事务)。
在没有指定FOR UPDATE或者LOCK IN SHARE MODE的情况下INSERT INTO ... SELECT,UPDATE ...(SELECT)和CREATE TABLE...等语句中的的读取会有以下差异:
默认情况下,就像READ COMMITTED一样,即使在同一事务中,每个一致性读都会建立和读取自己的快照。
如果将innodblocksunsafeforbinlog设置为了enable并且事务隔离级别不是SERIALIZABLE,则读操作不会再行上加锁。
多版本并发控制
上面说的一致性读(consistend read)的主要是基于MVCC实现,而 MySQL 中大多数事务型(如:InnoDB、Falcon等)存储引擎都同时实现了MVCC(Multi-Version Concurrency Control)。
当前不仅仅是MySQL,其它数据库系统(如:Oracle、PostgreSQL)也都实现了MVCC。值得注意的是MVCC并没有一个统一的实现标准,所以不同的数据库,不同的存储引擎的实现都不尽相同。
多版本控制的核心是数据快照,而InnoDB则是通过undo log来存储数据快照。
下面展示了在不考虑redo log的情况下利用undo log工作的简化过程:
1)为了保证数据的持久性数据要在事务提交之前持久化。2)undo log的持久化必须在在数据持久化之前,这样才能保证系统崩溃时,可以用undo log来回滚事务。
Innodb中的隐藏列
InnoDB通过undo log保存了已更改行的旧版本的信息的快照。InnoDB的内部实现中为每一行数据增加了三个隐藏列用于实现MVCC。
MVCC只在READ COMMITED和REPEATABLE READ两个隔离级别下工作。READ UNCOMMITTED总是读取最新的数据行,而不是符合当前事务版本的数据行。而SERIALIZABLE则会对所有读取的行都加锁。
SELECT
InnoDB会根据两个条件来检查每行记录:
1)InnoDB只查找版本(DBTRXID)早于当前事务版本的数据行(行的系统版本号
INSERT
InnoDB为新插入的每一行保存当前系统版本号作为行版本号。
DELETE
InnoDB为删除的每一行保存当前的系统版本号作为行删除标识。
UPDATE
InnoDB为插入一行新记录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行删除标识。
Undo log
在InnoDB存储引擎中,它的主要作用除了实现MVCC和数据回滚,undo log是一种逻辑日志,它只是将数据库逻辑地回滚到原来的样子,所有的修改都被逻辑地取消,但是数据结构和数据页本身在回滚之后与之前可能已经发生的变化,这样做的目的是因为数据库可能会同时存在多个并发事务,他们可能同时修改一个页上的其它数据行,如果因为一个事务的回滚而将数据页回滚到该事务开始时的状态,则会影响其它正在执行的事务。
undo log存在于undo log segments中,undo log segments位在于rollback segments中,而rollback segments则可能存在于系统系统表空间(system tablespace)、临时表空间(temporary tablespace)、撤销表空间(undo tablespaces)中。
回滚段(rollback segment)
InnoDB最多支持128个回滚段,每个回滚段最多可以支持1023个数据修改事务,回滚段最多可以设置128。
具体的分配策略如下:
必然有一个回滚段是被分配到系统表空间中的
当innodbrollbacksegments的值小于等于32时,InnoDB会将一个回滚段分配给系统表空间,将32个回滚段分配给临时表空间。
当innodbrollbacksegments的值大于32时,InnoDB会将一个回滚段分配给系统表空间,将32个回滚段分配给临时表空间,并将剩下的回滚段分配给撤销表空间,如果不存在撤销表空间则,则会将剩下的回滚段分配给系统表空间(5.7默认)。
撤销表空间在5.7.21版本之后被标记为弃用未来可能被删除,目前(MySQL5.7)innodbundo_tablespaces默认值为即不启用。
事务提交
根据行为的不同undo log分为两种insert undo log/update undo log。
insert undo log是在insert操作中产生的undo log。因为insert操作的记录只对事务本身可见,对于其它事务此记录是不可见的,所以insert undo log可以在事务提交后直接删除而不需要进行purge操作。
update undo log是update或delete操作中产生的undo log,因为会对已经存在的记录产生影响,为了提供MVCC机制,因此update undo log不能在事务提交时就进行删除,而是将事务提交时放到入history list上,等待purge线程进行最后的删除操作。
当事务commit时,需要将事务状态设置为COMMIT状态,并将事务包含的Undo都设置为完成状态
如果当前undo log只占一个page,且占用的header page大小不足其3/4时,将其加入到undo cache list上,以便分配给下个事务使用,并将状态设置为TRXUNDOCACHED
如果当前undo log是insertundo,则状态设置为TRXUNDOTOFREE
如果不满足1和2,则表明该undo可能需要purge线程去执行清理操作,状态设置为TRXUNDOTO_PURG。
MySQL 5.7对临时表undo和普通表undo分别做了处理,前者在写undo日志时总是不需要记录redo,后者则需要记录。
清理
delete和update操作可能并不会直接删除原有数据,delete操作只是将聚集索引列的delete flag置为1,记录仍然存在于B+树中,最终的删除在purge线程中完成(这样的设计是因为其它事务可能引用这行,所以不能立刻删除)。
值得注意的是innodb对于update的处理其实是分两步处理的:1.将原聚集索引记录标记为已经删除 。2.插入一条新记录,所以purge操作只需要针对delete flag为1的记录即可。
history list
history list根据事务提交的顺序将undo log进行链接,先提交的事务总是在history list的尾部,同一undo page中的undo log也总是按照顺序排列的。
具体清理过程为innoDB会默认从history list中找到第一个需要被清理的数据tx1,清理成功之后清理线程会继续在tx1所在页中继续查找需要被清理的 __undo log(即tx3,注意这里并不会从history list继续查找tx2),之后继续向后查找,找到tx5,此时发现tx5被其它事务引用不能清理(trx no比当前purge到的位置更大),所以再次去history list中查找尾部记录,此时为tx2重复以上步骤。
简单来说InnoDB的一致性读主要依赖MVCC实现,而MVCC的核心则是依赖undo log来保存事务快照,使得InnoDB在不使用锁的前提下依然能保证事务中数据的一致性,减少了锁的开销,大大提高了查询性能。
参考文档:
MySQL 5.7 Reference Manual:
https://dev.mysql.com/doc/refman/5.7/en/
MySQL · 引擎特性 · InnoDB undo log 漫游:
http://mysql.taobao.org/monthly/2015/04/01/
《高性能Mysql》(第三版)
《Myql技术内幕-Innodb存储引擎》
领取专属 10元无门槛券
私享最新 技术干货