MySQL一致性非锁定读原理以及MVCC简介
1、 一致性读
InnoDB 中的一致性读(consistend read)指的是利用多版本查询数据库在某个时间点的快照。此查询可以看到该时间点之前提交的事务所做的更改并且不会被之后的修改或者未提交事务所影响。但是对于同一事务中的较早语句的修改则不适用此规则,这种情况会产生以下异常:如果你更新表中的某些行,一次 SELECT 可能看到更新行的最新版本也可能看到任一行的旧版本;如果其它会话同时更新到同一个表,则可能会看到该表处于数据库中从未存在过的状态。
- 当事务隔离级别为 REPEATABLE READ 时,同一个事务中的一次性读都说读取该事务第一次查询所建立的快照。
- 当事务隔离级别为 READ COMMITTED 时,同一个事务下的一致性读都会建立和读取此查询自己的最新快照。
一致性读是 InnoDB 在 REPEATABLE READ 和 READ COMMITTED 事务隔离中处理 SELECT 语句的默认模式。一致性读不会在表上设置任何锁,所以其它会话可以对表进行读写操作。
数据库状态的快照适用于事务中的 SELECT 语句,而不一定适用于 DML 语句。如果执行 INSERT 或者 UPDATE某些行然后提交该事务,则从另一个并发 REPEATABLE READ 事务发出的 DELETE 或 UPDATE 语句则会影响那些刚刚提交的数据行。
下面这个示例展示了这种场景:
Session A Session B begin; begin; ------------------------------------------------------------------------------------- SELECT * FROM t; SELECT * FROM t; > empty set > empty set ------------------------------------------------------------------------------------- INSERT INTO t (id) VALUES (1); > 1 row affected SELECT * FROM t; ----------- | id | ----------- | 1 | ----------- ------------------------------------------------------------------------------------- SELECT * FROM t; > empty set ------------------------------------------------------------------------------------- COMMIT; ------------------------------------------------------------------------------------- SELECT * FROM t; > empty set ------------------------------------------------------------------------------------- DELETE FROM t WAERE id = 1; > 1 row affected ------------------------------------------------------------------------------------- SELECT * FROM t; ----------- | id | ----------- | 1 | ----------- ------------------------------------------------------------------------------------- COMMIT; ------------------------------------------------------------------------------------- SELECT * FROM t; > empty set ------------------------------------------------------------------------------------- SELECT * FROM t; ----------- | id | ----------- | 1 | ----------- -------------------------------------------------------------------------------------
一致的读取不适用于某些 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 一样,即使在同一事务中,每个一致性读都会建立和读取自己的快照。
- 如果将 innodblocksunsafe forbinlog 设置为了 enable 并且事务隔离级别不是 SERIALIZABLE,则读操作不会再行上加锁。
2、多版本并发控制
上面说的一致性读(consistend read)的主要是基于MVCC实现,而 MySQL 中大多数事务型(如:InnoDB、Falcon 等)存储引擎都同时实现了 MVCC(Multi-Version Concurrency Control) 。
当前不仅仅是 MySQL,其它数据库系统(如:Oracle、PostgreSQL)也都实现了 MVCC。值得注意的是 MVCC 并没有一个统一的实现标准,所以不同的数据库,不同的存储引擎的实现都不尽相同。
多版本控制的核心是数据快照,而 InnoDB 则是通过 undo log 来存储数据快照。
下面展示了在不考虑 redo log 的情况下利用 undo log工作的简化过程:
序号 动作 开始事务 记录数据行数据快照到undo log 更新数据 将undo log写到磁盘 将数据写到磁盘 提交事务
1)为了保证数据的持久性数据要在事务提交之前持久化。 2)undo log的持久化必须在在数据持久化之前,这样才能保证系统崩溃时,可以用undo log来回滚事务。
2.1、Innodb中的隐藏列
InnoDB 通过 undo log 保存了已更改行的旧版本的信息的快照。InnoDB 的内部实现中为每一行数据增加了三个隐藏列用于实现 MVCC 。
列名 长度(字节) 作用 DBTRXID 6 插入或更新行的最后一个事务的事务标识符。(删除视为更新,将其标记为已删除) DBROLLPTR 7 写入回滚段的撤消日志记录(若行已更新,则撤消日志记录包含在更新行之前重建行内容所需的信息) DBROWID 6 行标识(隐藏单调自增id)
MVCC 只在 READ COMMITED 和 REPEATABLE READ 两个隔离级别下工作。READ UNCOMMITTED 总是读取最新的数据行,而不是符合当前事务版本的数据行。而 SERIALIZABLE 则会对所有读取的行都加锁。
2.2 每个操作所做的内容
SELECT
InnoDB 会根据两个条件来检查每行记录:
1) InnoDB 只查找版本(DBTRXID)早于当前事务版本的数据行(行的系统版本号 <= 事务的系统版本号,这样可以确保数据行要么是在开始之前已经存在了,要么是事务自身插入或修改过的)。
2) 行的删除版本号(DBROLLPTR)要么未定义(未更新过),要么大于当前事务版本号(在当前事务开始之后更新的)。这样可以确保事务读取到的行,在事务开始之前未被删除。
事例
当事务 A 执行查询语句时,其查询数据逻辑图
INSERT
InnoDB 为新插入的每一行保存当前系统版本号作为行版本号。
DELETE
InnoDB 为删除的每一行保存当前的系统版本号作为行删除标识。
UPDATE
InnoDB 为插入一行新记录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行删除标识。