InnoDB 隔离级别的实现总结

736 阅读4分钟

一、隔离级别以及并发事务带来的问题

1、READ UNCOMMITTED(读未提交)

在该隔离级别的事务会读到其它未提交事务的数据,会导致脏读

2、READ COMMITTED(读提交)

一个事务可以读取另一个已提交的事务,多次读取同一条数据会造成不一样的结果,此现象称为不可重复读问题(针对于delete和update操作)

3、REPEATABLE READ(可重复读)

mysql默认的隔离级别;在同一个事务里,不同时刻读取同一批数据,读取的结果是一样的。但是会产生幻读(针对于insert操作)

4、SERIALIZABLE(序列化)

在该隔离级别下事务都是串行顺序执行的(读写串行化),MySQL 数据库的 InnoDB 引擎会给读操作隐式加一把读共享锁,从而避免了脏读、不可重读复读和幻读问题。

如下图
隔离级别脏读不可重复读幻读
读未提交
读已提交
可重复读
序列化

二、RC、RR的实现原理

MVCC

MVCC的全称是Multi-Version Concurrency Control,通常用于数据库等场景中,实现多版本的并发控制。他只作用于RC以及RR两种隔离级别;

实现原理

相关依赖

MVCC.png

1、数据隐藏字段

  •  DB_TRX_ID(6字节):表示最近一次对本记录行作修改(insert | update)的事务ID。至于delete操作,InnoDB认为是一个update操作,不过会更新一个另外的删除位,将行表示为deleted。并非真正删除。
  •  DB_ROLL_PTR(7字节):回滚指针,指向当前记录行的undo log信息 
  •  DB_ROW_ID(6字节):随着新行插入而单调递增的行ID。理解:当表没有主键或唯一非空索引时,innodb就会使用这个行ID自动产生聚簇索引。如果表有主键或唯一非空索引,聚簇索引就不会包含这个行ID了。这个DB_ROW_ID跟MVCC关系不大。

数据库行.png

2、Undo Log

  • 存储的是每一行数据老版本,每次执行insert/update/delete 操作时会新增一条记录;当一个事务需要读取记录行时,如果当前记录行不可见,可以顺着undo log链找到满足其可见性条件的记录行版本。
  • insert undo log : insert 的时候会生成,只在事务回滚的时候需要,所以事务提交之后会立即删除
  • update undo log:delete和update时生成;用于事务回滚以及下面即将提到的快照读。

如下:

  1. 原始数据为 name = 张三

未命名文件 (1).png

  1. 修改 name = 李四 那么就会形成类似与这样的版本链

未命名文件 (2).png

  1. 修改 name = 王五 那么就会最终 就会形成类似与这样的版本链

未命名文件 (3).png

总结:undo log 中记录着数据的历史版本链,如上面第二幅图中的版本1、第三幅图中的版本2与版本3。

3、Read View

主要由下面三个部分组成:

  • low_limit_id:下一个将被分配的事务ID
  • trx_ids:目前已经开启事务但是未提交的事务id集合快照(不包含自身事务id和已经提交的事务id)。
  • up_limit_id:活跃事务列表trx_ids中最小的事务ID。

ps:如果trx_ids为空,则up_limit_id = low_limit_id。

所以一个read View 大概的样子如下图(注意事务id这里是有序的):

read view (1).png

数据读取原理
  1. 当mysql innodb ,开启一个新事务,然后第一次 执行select 时就会创建一个read view。
  2. 当要读取某行数据时,就会拿到该条数据的最新的事务ID与read view 中的数据作比较,判断出当前的事务应该读取该数据的哪一个版本内容。
比较算法

先设定当前行最新操作的事务id为trx_id;

1、如果trx_id < up_limit_id,则说明“最新修改该行的事务”在“当前事务”创建read_view之前就提交了,所以该记录行的值对当前事务是可见的,所以就直接返回该行数据。

2、如果up_limit_id <= trx_id < low_limit_id,则说明在创建read_view时,trx_id可能是已经开启了但是没有提交,也有可能是已经提交了;所以这里需要分2种情况进行区分:

1.`trx_id`不存在`trx_ids`中,说明创建read_view时已经提交了,所以是可见的;直接返回该行数据。
2. `trx_id`存在`trx_ids`中,说明创建read_view时未提交,所以这个版本的数据是不可见的,这时候就需要通过该数据的undo Log进行查找,直到查找到`trx_id < up_limit_id`的版本数据进行返回。

3、如果trx_id >= up_limit_id,则说明创建read_view时未提交,所以这个版本的数据是不可见的,这时候就需要通过该数据的undo Log进行查找,直到查找到trx_id < up_limit_id的版本数据进行返回。

流程图如下:

MVCC 可见算法 (2).png

读取数据的两种方式
  1. 快照读:普通的 select 语句(不包括 select ... lock in share mode, select ... for update)
  2. 当前读:select ... lock in share mode,select ... for update,insert,update,delete 语句。并不会生成read view ,也就是每次读取的都是最新的数据。
RR与RC的实现原理

前提:事务中使用的是快照读。

  1. RR: 只有事务在begin之后,执行第一条select(读操作)时, 才会创建一个快照(read view),在这个事务未提交之前一直用这个read view。
  2. RC:在事务执行期间,每执行一条select 就会重新创建一个快照(read view),所以在事务中会读取到其他事务已经提交的数据。

总结

从上面事务中读取的方式可知,如果只是仅仅的靠MVCC是解决不了幻读的;因为可以使用类似于select ... for update当前读的方式,还是会读取到最新的数据,所以Inno DB 在这基础之上还通过对当前读的数据加上锁(记录锁、间隙锁),来防止幻读。

参考

MySQL中MVCC的正确打开方式(源码佐证)

mysql 官方文档

其他

如有不对,欢迎指正。