一、事务并发执行的问题
事务并发的问题有四种分别是脏写、脏读、不可重复读、幻读。其中脏读、脏写是在事务并发中读取到了未提交事务修改过的数据,这会导致比较严重的问题。
问题 | 描述 |
---|---|
脏写 | 一个事务修改了另一个未提交事务修改过的数据,那就意味着发生了脏写 |
脏读 | 一个事务读到了另一个未提交事务修改过的数据,那就意味着发生了脏读 |
不可重复读 | 一个事务只能读到另一个已经提交的事务修改过的数据,并且其他事务每对该数据进行一次修改并提交后,该事务都能查询得到最新值,那就意味着发生了不可重复读 |
幻读 | 一个事务先根据某些条件查询出一些记录,之后另一个事务又向表中插入了符合这些条件的记录,原先的事务再次按照该条件查询时,能把另一个事务插入的记录也读出来,那就意味着发生了幻读 |
二、MySQL支持的隔离级别
MySQL的默认隔离级别为REPEATABLE READ。
- READ UNCOMMITTED:未提交读,会导致脏读、不可重复读、幻读
- READ COMMITTED:已提交读,会导致不可重复读、幻读
- REPEATABLE READ:可重复读,会导致幻读
- SERIALIZABLE:可串行化,可以杜绝并发出现的问题,但是效率较低。
三、MVCC 版本控制
1. 基础知识
MySQL设计实现版本控制,主要通过InnoDB page记录中的字段和Undo Log实现版本链记录,结合ReadView 事务id的比较实现版本控制。
- InnoDB聚族索引中存在两个必要的隐藏列
- row_id 可以是主键,如果没有主键则是自动生成隐藏列。
- trx_id 事务对聚族索引改动时将当前修改的事务id赋值给trx_id。
- roll_pointer 对应undo log日志位置,可以回溯到历史版本。
- 版本链
在多个事务对同一条记录修改时,将修改前的记录保存在Undo Log中,多次的修改将会生成一个版本链。其中tri_id就是不同事务修改赋值给聚族索引的事务id。
trx_200需要等待trx_100更完毕后才能更新是因为:InnoDB使用锁来保证不会有脏写情况的发生,也就是在第一个事务更新了某条记录后,就会给这条记录加锁,另一个事务再次更新时就需要等待第一个事务提交了,把锁释放之后才可以继续更新。
有了版本链的记录,MySQL就可以通过当前事务id判断是在提交前读取记录,还是提交之后读取记录了。所以就有了ReadView的概念,实现版本控制
2. ReadView实现版本控制
ReadView的概念
这个ReadView中主要包含4个比较重要的内容:
- m_ids:表示在生成ReadView时当前系统中活跃的读写事务的事务id列表。
- min_trx_id:表示在生成ReadView时当前系统中活跃的读写事务中最小的事务id,也就是m_ids中的最小值。
- max_trx_id:表示生成ReadView时系统中应该分配给下一个事务的id值,是一个自增策略的ID。
- creator_trx_id:表示生成该ReadView的事务的事务id。
在读事务中有了ReadView记录,MySQL就可以根据当前活跃的事务存在的ids,以及ids中的最大id和最小id判断undo log中trx_id是否已经提交,是否在当前活跃事务中,从而实现版本的控制。
记录trx_id和ReadView字段关系总结
- trx_id = creator_trx_id,当前事务访问自己修改过的记录,可访问
- trx_id < min_tri_id,生成该版本的事务在当前事务生产ReadView已经提交,可访问
- trx_id >= max_trx_id,生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。
- min_trx_id < trx_id < max_trx_id ,需要判断trx_id是否在m_ids中,存在则表示是当前正在活跃的事务不可访问,不存在则表示在生成ReadView的时候已经提交,可以访问。
ReadView进行版本控制主要是对可重复读和读已提交的隔离级别设计的,因为在读未提交直接读取最新版本即可,可串行化则是直接加锁访问。
而在READ COMMITTED和REPEATABLE READ隔离级别的事务来说,都必须保证读到已经提交了的事务修改过的记录,也就是说假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的。
所以在判断READ COMMITTED 和REPEATABLE READ 实现版本控制,只要读事务在生成ReadView的记录活跃ids不同即可。
在 READ COMMITTED中,开启两个事务trx_100的事务先修改记录,查询记录则ids=[100,200],然后提交trx_100事务。然后trx_200开始修改记录,查询记录生成ReadView的ids=[200],所以可以读取到trx_100提交的记录。前后读事务生成了两次ReadView。
在REPEATABLE READ 中,开启两个事务trx_100的事务先修改记录,但只是在第一次生成ReadView。第二次读事务则不再生成新的ReadView。所以两次查询的接口都是一致的。