Mysql事务
事务隔离级别
Mysql中一共有四种事务隔离级别
读未提交
该事务未提交时做出的修改能被其它事务看到,不能解决 脏读 的问题,如:
# 脏读:读到其它事务未提交的数据
事务A set a = 10;
事务B set a = 20;
事务A get a; # 20 ?!
读已提交(Oracle默认)
该事务已提交的修改才能被其它事务看到,不能解决 不可重复读 的问题,如:
# 不可重复读:前后读到的数据不一致
事务A set a = 10;
事务B set a = 20;
事务A get a; # 10 √
事务B commit;
事务A get a; # 20 ?!
可重复读(Mysql默认)
事务执行过程读到的所有数据与启动时一致,不能解决 幻读 问题,如:
# 幻读:前后读到的记录数不一致
事务A set a = 10;
事务B set a = 20;
事务A get a; # 10 √
事务B commit;
事务A get a; # 10 √
事务A select count(*); # 1 √
事务B insert a = 10;
事务B commit;
事务A select count(*); # 1 √
事务A select count(*) for update; # 当前读 2 ?!
串行化
对记录上读写锁,多个事务读写数据时,若发生读写冲突,需要等前一个事务执行完成,能解决所有问题,但效率低;
实现原理
读未提交:直接读取最新数据即可
读已提交、可重复读:使用ReadView快照,即MVCC多版本并发控制实现
可串行化:对记录加读写锁
MVCC多版本并发控制
undo log版本链
这是本篇的重点,先来回顾undo log
此外后面还跟着两个隐藏字段:
- trx_id:当前事务的id,当一个事务对某条聚族索引记录进行改动时,就会把该事务的事务 id 记录在 trx_id 隐藏列里
- rol_pointer:这个隐藏列是个指针,指向每一个旧版本记录,于是就可以通过它找到修改前的记录
许多undo log会通过roll_pointer形成undo log版本链:
ReadView
再来看看一个重要的数据结构 ReadView ,它和undo log版本链结合起来就可以实现多版本并发控制(MVCC),它长这样:
一个ReadView的构成:
- creator_trx_id:创建该 ReadView 的事务id
- m_ids:创建ReadView时活跃且未提交的事务id列表
- min_trx_id:创建ReadView时 活跃且未提交 的事务中最小事务id,即m_ids里的最小值 ⭐
- max_trx_id:创建ReadView时数据库该分配给下一个事务的id的值
实现可重复读
可重复读在事务创建时创建ReadView
假设初始记录为:
事务A和事务B被同时创建:
现在事务B更新了blance:
我们来看下事务A在可重复读的情况下读取会发生什么:
- 先走到id = 2 的记录,一看trx_id在m_ids里(意味着这个事务还未提交)直接不用比较,读取后面undo log
- 假设52不在m_ids里,那我过去一看trx_id比我的create_id大,而且还比max_trx_id小,说明添加这个undo log记录的事务是在我创建事务之后修改了数据,不读,往下看
- 走到id=1的记录,一看trx_id不在m_ids里,而且trx_id比我的create_id小,嗯是在我创建事务前就提交的数据,可读!
实现读已提交
实现读已提交,和可重复读的区别在于创建ReadView 的时机,读已提交在每次执行select语句都会创建(更新)ReadView:
还是上个例子:事务A读取数据
- 先走到id = 2 的记录,一看trx_id在m_ids里(意味着这个事务还未提交)直接不用比较,读取后面undo log
- 假设52不在m_ids里,那我过去一看trx_id比我的create_id大,而且还比max_trx_id小,说明添加这个undo log记录的事务是在我创建事务之后修改了数据,不读,往下看
- 走到id=1的记录,一看trx_id不在m_ids里,而且trx_id比我的create_id小,嗯是在我创建事务前就提交的数据,可读!
- 一段时间后,事务B提交,且事务A再次读取数据(select),这时更新ReadView:
- 先走到id = 2 的记录,一看trx_id不在m_ids里(意味着这个事务已经提交),直接读取就好
到这里我们已经看完了MVCC实现可重复读和读已提交的流程,总结来说就是根据生成的ReadView,去对比查找undo log版本链中的一条合适的版本数据。
参考:
Q.E.D.