理解MySQL事务隔离级别和MVCC机制
1 什么是事务
事务,由一个有限的数据库操作序列构成,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位,看一个用烂了,但比较能说清事务的经典案例。
假如A转账给B 100 元,先从A的账户里扣除 100 元,再在 B 的账户上加上 100 元。如果扣完A的100元后,还没来得及给B加上,银行系统异常了,最后导致A的余额减少了,B的余额却没有增加。所以就需要事务,将A的钱回滚回去,就是这么简单。
2 事务的四大特性
事务的四大特性包含ACID
原子性:事务作为一个整体被执行,包含在其中的对数据库的操作要么全部都执行,要么都不执行。
一致性:指在事务开始之前和事务结束以后,数据不会被破坏(个人理解:数据总是处于事务执行前或执行后的状态,不会出现事务中间数据),假如A账户给B账户转10块钱,不管成功与否,A和B的总金额是不变的。
隔离性:多个事务并发访问时,事务之间是相互隔离的,一个事务不应该被其他事务干扰,多个并发事务之间要相互隔离。
持久性:表示事务完成提交后,该事务对数据库所作的操作更改,将持久地保存在数据库之中。
3 事务并发存在的问题
脏读:脏读指的是读到了其他事务未提交的数据,未提交意味着这些数据可能会回滚,也就是可能最终不会存到数据库中,也就是不存在的数据。读到了并一定最终存在的数据,这就是脏读。
可重复读:可重复读指的是在一个事务在开启事务到提交事务期间,最开始读到的数据和事务结束前的任意时刻读到的同一批数据都是一致的。通常针对数据更新(UPDATE)操作。
不可重复读:对比可重复读,不可重复读指的是在同一事务内,不同的时刻读到的同一批数据可能是不一样的,可能会受到其他事务的影响,比如其他事务改了这批数据并提交了。通常针对数据更新(UPDATE)操作。
幻读:幻读是针对数据插入(INSERT)操作来说的。假设事务A对某些行的内容作了更改,但是还未提交,此时事务B插入了与事务A更改前的记录相同的记录行,并且在事务A提交之前先提交了,而这时,在事务A中查询,会发现好像刚刚的更改对于某些数据未起作用,但其实是事务B刚插入进来的,让用户感觉很魔幻,感觉出现了幻觉,这就叫幻读。
4 事务的隔离级别
为了解决事务并发执行引起的各种问题,SQL标准定义了四种隔离级别,且Mysql全部支持。
读未提交(READ UNCOMMITTED)
读提交 (READ COMMITTED)
可重复读 (REPEATABLE READ)【mysql的默认级别】
串行化(SERIALIZABLE)
4.1 读未提交
mysql事务隔离级别其实是依靠锁来实现的,加锁,解锁会带来性能的损失,而读未提交隔离级别是不加锁的,所以其有很好的性能,但会产生脏读、不可重复读,幻读问题。
事务A在修改了age字段的值,在没有提交之前,事务B读取的值就是脏数据。
4.2 读已提交
读未提交会产生脏数据,那么就有了读提交。读提交就是一个事务只能读到其他事务已经提交过的数据,也就是其他事务调用 commit 命令之后的数据。那脏数据问题迎刃而解了。
事务A修改了age字段的值,在没有提交之前事务B不会读到事务A修改的结果,这样就解决了脏数据的问题,但无法解决可重复读(事务B在自己的生命周期内,不同时刻读到的值不一致,事务A提交后,读取就是事务A修改后的结果,这就是不可重复读),和幻读,
4.3 可重复读
可重复是对比不可重复而言的,上面说不可重复读是指同一事物不同时刻读到的数据值可能不一致。而可重复读是指,事务不会读到其他事务对已有数据的修改,即使其他事务已提交,也就是说,事务开始时读到的已有数据是什么,在事务提交前的任意时刻,这些数据的值都是一样的。但是,对于其他事务新插入的数据是可以读到的,这也就引发了幻读问题。
事务A在不同时刻查询的时候出现事务B插入的新记录,这就是幻读 要说明的是,当你在 MySQL 中测试幻读的时候,并不会出现上图的结果,幻读并没有发生,MySQL 的可重复读隔离级别其实解决了幻读问题,这会在后面的内容说明
4.4 串行化
串行化是4种事务隔离级别中隔离效果最好的,解决了脏读、可重复读、幻读的问题,但是效果最差,它将事务的执行变为顺序执行,与其他三个隔离级别相比,它就相当于单线程,后一个事务的执行必须等待前一个事务结束。
5 事务的隔离级别实现原理(MVCC机制)
首先说读未提交,它是性能最好,也可以说它是最野蛮的方式,因为它压根儿就不加锁,所以根本谈不上什么隔离效果,可以理解为没有隔离。
再来说串行化。读的时候加共享锁,也就是其他事务可以并发读,但是不能写。写的时候加排它锁,其他事务不能并发写也不能并发读。
最后说读提交和可重复读。这两种隔离级别是比较复杂的,既要允许一定的并发,又想要兼顾的解决问题。
5.1实现可重复读
为了解决不可重复读,或者为了实现可重复读和提高数据库在并发环境下保证读写能力,MySQL 采用了 MVVC (多版本并发控制) 的方式。
先了解几个MVCC机制先看几个基本概念?
当前读:读取的是数据的最新版本,
select …… lock in share mode
select …… for update
update
insert
delete
快照读:读取的是历史版本的记录,当一个事务第一次读取数据的时候,生成一个快照,在同一个事务的生命周期中,每次都会undolog日志快照中读取数据
select ……
5.2 MVCC包含三部分
第一部分:隐藏字段
第二部分:undolog 回滚日志
❝事务未提交的时候,修改数据的镜像(修改前的旧版本),存到undo日志里。以便事务回滚时,恢复旧版本数据,撤销未提交事务数据对数据库的影响。
undo日志是逻辑日志。可以这样认为,当delete一条记录时,undo log中会记录一条对应的insert记录,当update一条记录时,它记录一条对应相反的update记录。
❞
第三部分:基于undo-log日志链实现的读视图机制(read view)
❝Read View就是事务执行快照读时,产生的读视图。
事务执行快照读时,会生成数据库系统当前的一个快照,记录当前系统中还有哪些活跃的读写事务,把它们放到一个列表里。
Read View主要是用来做可见性判断的,即判断当前事务可见哪个版本的数据~
❞
Read View可见性规则
(1).m_ids:当前系统中那些活跃(当前没有提交的事务id集合)的读写事务ID,它的数据结构是一个列表。
(2).min_trx_id:m_ids事务列表中,最小的事务ID
(3).max_trx_id: mysql要生成的下一个事务id,就是最大的事务ID
(4).txr_id:当前事务的id
Read View 如何使用
(1)假设原来数据库里就有一行数据,很早以前就有事务插入过了,事务id是100,他的值就是初始值,如下图所示。
(2)一个是事务A(id=200),一个是事务B(id=300),事务B是要去更新这行数据的,事务A是要去读取这行数据的值的,此时两个事务如下图所示。
现在事务A直接开启一个ReadView,m_ids = {200, 300} min_trx_id = 200, max_trx_id = 301 trx_id = 200。
(3)判断:这个时候事务A第一次查询这行数据,会走一个判断,就是判断一下当前这行数据的txr_id是否小于ReadView中的min_trx_id,此时发现txr_id=100,是小于ReadView里的min_trx_id就是200的,说明你事务开启之前,修改这行数据的事务早就提交了,所以此时可以查到这行数据。
接着事务B开始动手了,他把这行数据的age值修改为了值30,然后这行数据的txr_id设置为自己的id,也就是300,同时roll_pointer指向了修改之前生成的一个undo log,接着这个事务B就提交了,
这个时候事务A再次查询,此时查询的时候,会发现一个问题,那就是此时数据行里的txr_id=300,那么这个txr_id是大于ReadView里的min_txr_id(200),同时小于ReadView里的max_trx_id(301)的,说明更新这条数据的事务很可能就跟自己差不多同时开启的,于是会看一下这个txr_id=300,是否在ReadView的m_ids列表里?果然,在ReadView的m_ids列表里,有200和300两个事务id,直接证实了,这个修改数据的事务是跟自己同一时段并发执行然后提交的,所以对这行数据是不能查询的!如下图所示。
读取不到数据,那该怎么办呢,简单,顺着这条数据的roll_pointer顺着undo log日志链条往下找,就会找到最近的一条undo log,trx_id是100,此时发现trx_id=100,是小于ReadView里的min_trx_id(200)的,说明这个undo log版本必然是在事务A开启之前就执行且提交的。
多个事务并发执行的时候,事务B更新的值,通过这套ReadView+undo log日志链条的机制,就可以保证事务A不会读到并发执行的事务B更新的值,只会读到之前最早的值。
(4)接着假设事务A自己更新了这行数据的值,改成值A,trx_id修改为200,同时保存之前事务B修改的值的快照.
此时事务A来查询这条数据的值,会发现这个trx_id=200,居然跟自己的ReadView里的 trx_id(45)是一样的,说明什么?说明这行数据就是自己修改的啊!自己修改的值当然是可以看到的了!如下图。
接着在事务A执行的过程中,突然开启了一个事务C,这个事务的id是400,然后他更新了那行数据的age值50,还提交了,如下图所示
这个时候事务A再去查询,会发现当前数据的trx_id=400,大于了自己的ReadView中的 max_trx_id(300),此时说明什么?说明是这个事务A开启之后,然后有一个事务更新了数据,自己当然是不能看到的了
此时就会顺着undo log多版本链条往下找,自然先找到A自己之前修改的过的那个版本,因为那个trx_id=200跟自己的ReadView里的trx_id是一样的,所以此时直接读取自己之前修改的那个版本,如下图。
通过undo log多版本链条,加上你开启事务时候生产的一个ReadView,然后再有一个查询的时候,根据ReadView进行判断的机制,你就知道你应该读取哪个版本的数据。而且他可以保证你只能读到你事务开启前,别的提交事务更新的值,还有就是你自己事务更新的值。假如说是你事务开启之前,就有别的事务正在运行,然后你事务开启之后 ,别的事务更新了值,你是绝对读不到的!或者是你事务开启之后,比你晚开启的事务更新了值,你也是读不到的!
5 分析读已提交RC和可重复读RR两种隔离级别
明白了MVCC机制,理解RC和RR级别两种隔离级别之间的不同就非常简单了,一句话。
在MySQL中,READ COMMITTED和REPEATABLE READ隔离级别的的一个非常大的区别就是它们生成ReadView的时机不同。READ COMMITTED在每次读取数据前都会生成一个ReadView,这样就能保证每次都能读到其它事务已提交的数据。REPEATABLE READ 只在第一次读取数据时生成一个ReadView,这样就能保证后续读取的结果完全一致。