一、隔离级别以及并发事务带来的问题
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两种隔离级别;
实现原理
相关依赖
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关系不大。
2、Undo Log:
- 存储的是每一行数据老版本,每次执行insert/update/delete 操作时会新增一条记录;当一个事务需要读取记录行时,如果当前记录行不可见,可以顺着undo log链找到满足其可见性条件的记录行版本。
- insert undo log : insert 的时候会生成,只在事务回滚的时候需要,所以事务提交之后会立即删除
- update undo log:delete和update时生成;用于事务回滚以及下面即将提到的快照读。
如下:
- 原始数据为
name = 张三
- 修改
name = 李四
那么就会形成类似与这样的版本链
- 修改
name = 王五
那么就会最终 就会形成类似与这样的版本链
总结: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这里是有序的):
数据读取原理
- 当mysql innodb ,开启一个新事务,然后第一次 执行
select
时就会创建一个read view。 - 当要读取某行数据时,就会拿到该条数据的最新的事务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
的版本数据进行返回。
流程图如下:
读取数据的两种方式
- 快照读:普通的 select 语句(不包括 select … lock in share mode, select … for update)
- 当前读:select … lock in share mode,select … for update,insert,update,delete 语句。并不会生成read view ,也就是每次读取的都是最新的数据。
RR与RC的实现原理
前提:事务中使用的是快照读。
- RR: 只有事务在begin之后,执行第一条select(读操作)时, 才会创建一个快照(read view),在这个事务未提交之前一直用这个read view。
- RC:在事务执行期间,每执行一条select 就会重新创建一个快照(read view),所以在事务中会读取到其他事务已经提交的数据。
总结
从上面事务中读取的方式可知,如果只是仅仅的靠MVCC是解决不了幻读的;因为可以使用类似于select ... for update
当前读的方式,还是会读取到最新的时间,所以Inno DB 在这基础之上还通过对当前读的数据加上锁(记录锁、间隙锁),来防止幻读。
参考
其他
如有不对,欢迎指正。