一、InnoDB如何解决幻读
- 幻读:在InnoDB的可重复度隔离级别下,使用当前读,一个事务前后两次查询同一个范围,后一次查询会看到期间新插入的行;
- 幻读的影响:会导致一个事务中先产生的锁,无法锁住后加入的行,会产生数据一致性问题;
- 产生幻读的原因:行锁只能锁住一行,不能避免新插入的记录;
- 解决幻读:在两行记录之间加上间隙锁,阻止新纪录的插入,与间隙锁产生冲突的只有“往这个间隙插入记录”这个操作;
- 同时添加间隙锁与行锁称为Next-key lock,注意间隙锁只有在InnoDB的可重复度隔离级别下生效;
- MVCC只实现读取已提交和可重复读,InnoDB在可重复度的隔离级别下,使用MVCC+Next-key lock解决幻读;
二、多版本并发控制MVCC
1.基本思想
- MVCC是InnoDB实现隔离级别的一种方式,用于实现读取已提交和可重复读两种隔离级别;
- 对于读取未提交,直接读取最新版本的数据;
- 对于串行化,使用加锁的方式访问记录;
- 大多数事务型存储引擎实现都不是简单的行锁,基于并发性的考虑,一般会同时实现多版本并发控制(MVCC)处理读写冲突;
- MVCC是乐观锁的一种实现,是通过保存数据在某一个时间点的快照实现的,写操作更新最新的版本,读操作读取旧版本;
- MVCC中事务的修改操作(增删改)会为行记录新增一个版本快照,并把当前事务id写入trx_id;
2.版本号
- 系统版本号sys_id:每开始一个新的事务,系统版本号递增;
- 在InnoDB中,聚簇索引记录中包含两个隐藏列:
- trx_id:对记录进行改动时,trx_id会记录当前事务id,也就是当前系统版本号;
- roll_pointer:对记录进行改动,会把旧版本记录写入undo日志,roll_pointer指向修改之前的版本;
- 对同一条记录的更新,会把旧值放到一条undo日志中,作为一个旧版本的记录,多次更新之后这些版本会被roll_pointer连接成一个链表,称之为版本链;
3.版本读取
- 对于读取已提交和可重复读,就会用到版本链,关键在于怎么判断版本链中哪个版本对当前事务可见;
- 使用ReadView(快照),ReadView是一个包含当前已经开始但是没有提交的事务的列表,记录每个事务的事务id,记最小事务id为min_id,最大事务id为max_id;
- 版本比较规则:
- 如果记录版本的trx_id小于min_id,说明这个记录版本是已经被提交过的,对其他事务可见;
- 如果记录版本的trx_id大于max_id,说明这个记录版本是ReadView生成之后发生的,不能访问;
- 如果记录版本的trx_id在min_id和max_id之间,判断trx_id是否在ReadView中:
- 如果在ReadView中,说明事务还未提交,该记录版本不可访问;
- 如果不在ReadView中,说明该事务已经提交,该记录版本可以访问;
- 如果当前记录版本不可读,就根据回滚指针roll_pointer找到旧版本的记录再进行判断;
- 对于读取已提交,每次查询都会生成一个新的ReadView;
- 对于可重复度,一个事务在第一次SELECT的时候生成一个ReadView,之后的查询复用这个ReadView;
4.快照读与当前读
- 快照读:MVCC中的SELECT操作是读取快照中的数据,不需要进行加锁;
- 当前读:MVCC中修改数据的操作(增删改)需要进行加锁操作,从而读取最新的数据;
5.例子
- 假设当前有一个事务id为100的事务A,修改一个记录的name字段为name2,产生一个版本快照,因此有这样的版本链:
- 假设事务A还没有提交,此时事务B进行SELECT,事务id为120,查询id为1的记录(记为第一次查询),此时生成ReadView为
[100,120]
,根据版本读取规则,先找到trx_id为100的记录版本,发现不可读,于是通过回滚指针找到trx_id为60的记录,读取成功; - 当事务A提交之后,事务B再次进行SELECT查询id为1的记录(第二次查询),在读取已提交和可重复读两种隔离级别下有不同的情况:
- 如果是读取已提交,则会创建一个新的ReadView为
[120]
,此时读取trx_id为100的记录成功,也就是读取到了在事务期间提交的数据; - 如果是可重复读,则会使用第一次查询时的ReadView为
[100,120]
,此时读取的是trx_id为60的记录,从而实现了可重复度;
菜鸡的小疑惑:
三级封锁协议不是可以解决脏读和不可重复度的问题吗,为什么要用MVCC来实现,是性能更好吗?
转自:https://zhuanlan.zhihu.com/p/180350695