MySQL 的默认隔离级别是 RR - 可重复读,可以通过命令来查看 MySQL 中的默认隔离级别。
RR - 可重复读是基于多版本并发控制(Multi-Version Concurrency Control,MVCC )实现的。MVCC,在读取数据时通过一种类似快照的方式将数据保存下来,不同事务的 session 会看到自己特定版本的数据,这样读锁和写锁就不冲突了。
在 InnoDB 存储引擎里,在有聚簇索引的情况下,每行数据都包含两个必要的隐藏列:
- DB_TRX_ID:记录某条数据的上次修改它的事务ID(trx_id)
- DB_ROLL_PTR:回滚指针,指向这条记录的上一个版本。我们每次对聚簇索引行进行修改时,都会把老版本写入到undo日志里,这个指针就指向了老版本的位置,当需要进行回滚操作时,事务就通过回滚指针以获取上一个版本的数据(注意:插入操作的undo日志没有回滚指针,因为它是新增的数据,没有老版本;而已删除的信息会在undo日志记录的头信息中存一个delete flag标记,当该标记为true时,表示已删除,则不返回数据)。
下图就是一个简洁的版本链概念,InnoDB 中的 undo 日志保存的就是一个版本链:
除了版本链,我们在实现 MVCC 还用到了另一个概念:read-view,一致性试图。我们在查询数据,当使用 select 语句时,InnoDB 会自动生成一个当前活动的(即未提交的)事务 ID 数组,这个 read-view 就是由查询时所有未提交事务 ID 组成的数组。数组中最小的事务 ID 为 min_id 和已创建的最大事务 ID 为 max_id 组成,查询的数据结果需要跟 read-view 做比较从而得到快照结果。
我们做查询时,会查询出当前 session 的 trx_id,通过和 read-view 比对:
- 若 trx_id 比 read-view 中的 min_id 小,则该版本是已经提交的事务生成,一定可见;
- 若 trx_id 比 read view 中的 max_id 大,则该版本是还未提交的事务生成,一定不可见;
- 当 trx_id 在 read-view 列表中,即 min_id <= trx_id <= max_id时,如果 trx_id 在 read-view 的数组中,则还未提交,不可见,但是当前事务是可见的;如果 trx_id 不在数组中,表明是已经提交的事务,则该版本可见。
当版本不可见时,需要通过 DB_ROLL_PTR 获取上一版本的 trx_id,再次比对,直到版本数据可见时,返回结果。
就以上比对的三种情况,用图示说明下:
transaction 100 | transaction 101 | select |
---|---|---|
update user set name = 'zhangsan' where id = 1 | ||
commit | ||
update test set age = 18 where id = 2 | ||
select name from use where id=1 |
1)select 语句执行时,上次更新的 trx_id 为 100,read-view 中未提交的事务为 [101]。此时 read-view 的 min_id 为 101,trx_id 比它小,则该版本是已经提交的事务生成,所以返回 zhangsan。
2)假设当前 select 的 trx_id 为 102,read-view 中未提交的事务为 [101],则需要通过 DB_ROLL_PTR 获取上一版本的 trx_id 100,注意 trx_id 为 101 的事务是改变了另一张表的数据,所以 undo 日志里版本链指向的上一条数据 trx_id 为 100,还是会返回 zhangsan。
3)当 trx_id 在 read-view 中间时:
transaction 100 | transaction 101 | select |
---|---|---|
update user set name = 'zhangsan' where id = 1 | ||
commit | ||
update test set name='wangwu' where id = 1 | ||
select name from use where id=1 |
此时 trx_id 为 101,read-view 为 [101],当前事务 ID 在数组中,所以不可见。需要用 DB_ROLL_PTR 找到上一条版本的位置 trx_id 为 100,还是会返回 zhangsan。
RC 隔离级别在查询时,同一个事务多次查询,每次会生成独立的 read-view。而 RR - 可重复读只在第一次查询时生成统一的 read view,之后的读取都复用之前的 read view。而 RU - 读未提交是可以读取还没提交的数据,没有 undo 版本的概念;可串行化隔离级别在每次读取时都需要加锁控制,没法并发,所以通过版本的概念去控制并发也就没有意义。
transaction 100 | transaction 101 | select |
---|---|---|
update user set name = 'zhangsan' where id = 1 | ||
commit | ||
update test set age = 18 where id = 2 | ||
select name from use where id=1 | ||
update user set name = '666' where id = 1 | ||
select name from use where id=1 |
当使用 RC 级别时,两次 select 的 read-view 不一样,第一次查询时是 [101],第二次是 [100, 101]。而用 RR 级别时,会复用第一次查询的 read-view,故多次查询的结果是一样的。这也是 MySQL 的隔离级别默认用 RR - 可重复读的原因之一,不用重复生成 read-view,提升数据库的操作性能。
总结
每次 select 数据时生成 read view 列表,再配合 undo 日志中的版本链,让不同的事务读-写,写-读操作可以并发执行,进而实现 MVCC。