多个事务同事执行的场景?
一般是写一个业务系统,然后业务系统会去对数据库执行增删改查,业务系统是执行一个个事务,每个事务里面可能是一个或者多个增删查改的SQL 语句。 这个事务的概念想必不用我多说了,其实就是一个事务里的SQL要不然一起成功就提交了,要不然有一 个SQL失败,那么事务就回滚了,所有SQL做的修改都撤销了 
但是业务系统也是个多线程的系统,面相的终端用户有很多人,可能会同事发起请求,所以也需要多个线程并发来处理多个请求。
所以这个业务系统很可能是基于多线程并发的对MySQL数据库去执行多个事务的!


如果事务提交之后,redo log刷入磁盘,结果MySQL宕机了,是可以根据redo log恢复事务修改过的缓 存数据的。 如果要回滚事务,那么就基于undo log来回滚就可以了,把之前对缓存页做的修改都给回滚了就可以 了。
由事务回滚的原理 引出 事务的概念
- 多个事务并发执行的时候,可能会同时对缓存页里的一行数据进行更新,这个冲突怎么处理?是否 要加锁?
- 可能有的事务在对一行数据做更新,有的事务在查询这行数据,这里的冲突怎么处理?
其实对于我们的业务系统去访问数据库而言,他往往都是多个线程并发执行多个 事务的,对于数据库而言,他会有多个事务同时执行,可能这多个事务还会同时更新和查询同一条数 据,所以这里会有一些问题需要数据库来解决。
如果多个事务要是对缓存页里的同一条数据同时进行更新或者查询,此时会 产生哪些问题呢?
- 脏写

事务A 和 事务B ,同时在更新一条数据,事务A 先更新为A值,事务B紧接着更新为B值。
此时事务A 写了一条 undo log 日志,在事务A 更新之前 值为 NULL ,主键为xx,那么此时事务A 回滚,把值改 为NULL。对事务B 来说,这是不可接受的,我更新的值为什么没了?这就是脏写, 刚才明明写了一个数据值,结果过了一会儿却没了, 本质就是事务B去修改了事务A修改过的值,但是此时事务A还没提交,所以事务A随时会回滚, 导致事务B修改的值也没了。
- 脏读
事务A 更新了一行数据,再事务未提交的时候,事务B读取了这条数据,去做各种业务处理,此时事务A 回滚了操作,恢复成原 值,事务B再次查询这行数据,发现值为NULL。
本质其实就是事务B去查询了事务A修改过的数据,但是此时事务A还没提 交,所以事务A随时会回滚导致事务B再次查询就读不到刚才事务A修改的数据了
无论是脏写还是脏读,都是因为一个事务B去更新或者查询了另外一个还没提交的事务A 更新过的数据,然后未提交的事务A进行了回滚,导致事务B更新或者查询到错误的数据。
不可重复度
事务A、B、C ,未提交的事务 可以读取到已提交的事务的值。
事务A 第一次 读取 id = 1 的值为A,进行业务操作;
此时事务B 将id = 1 的值改为 B ,进行提交;
事务A 第二次 读取 id= 1 的值,变成了B,两次读取的值就不一样了;
此时事务C 将 id = 1 的值改为C ,进行提交;
事务A 第三次读取id = 1的值,变成了C ,第三次读取的值又不一样。
说明这行数据时不可以重复读的。
这样有没有问题,因为这取决于你自己想要数据库是什么样子的,如果你希望看到的场景就是不可重复读,也就是事务A 在执行期间多次查询一条数据,每次都可以查到其他已经提交的事务修改过的值,也没有问题,那么就是不可重复读 的,如果你希望这样子,那也没问题。 但是如果你希望的是,假设你事务A刚开始执行,第一次查询读到的是值A,然后后续你希望事务执行期 间,读到的一直都是这个值A,不管其他事务如何更新这个值,哪怕他们都提交了,你就希望你读到的 一直是第一次查询到的值A,那么你就是希望可重复读的。
简单来说就是一个事务多次查询一条数据,结果每次读取到的值都是不一样的。原因是因为这个过程中,会有别的事务修改这个值且提交事务导致的。
- 幻读— 数据库的并发问题
场景:
- 事务A ,先执行一条SQL语句,查询一批数据,select * from table where id > 10,查出来10条数据
- 此时,事务B 往id > 10 的地方插入2条数据。
- 事务A 第二次查询,id>10的数据查询到12条。
- 事务A :是我的双眼出现幻觉了吗,就是幻读的由来。
幻读,就是一个事务用同样的SQL多次查询,结果每次查询都会发现一些之前没有看到过的数据。
说实在的,大家看完最近几篇文章,应该都有一个感觉,就是脏写、脏读、不可重复读、幻读,都是因 为业务系统会多线程并发执行,每个线程可能都会开启一个事务,每个事务都会执行增删改查操作。
然后数据库会并发执行多个事务,多个事务可能会并发的对缓存页里的同一批数据进行增删改查操作, 于是这个并发增删改查同一批数据的问题,可能就会导致我们说的脏写、脏读、不可重复读、幻读,这 些问题。
上面4种问题的本质就是都是数据库的多事务并发问题,那么为了解决多事务并发问题,数据库才设计了 事务隔离机制、MVCC多版本隔离机制、锁机制,用一整套机制来解决多事务并发问题
SQL标准中的4种事务隔离级别
说多个事务并发运行的时候,互相是如何隔离的, 不同的隔离级别是可以避免不同的事务并发问题的,所以大家一定要对这个事务隔离级别有一个深刻的 理解。
read uncommitted -读未提交
顾名思义,可以读取到其他事物未提交的值,可能发 生脏读,不可重复读,幻读。
- read committed - 读已提交
顾名思义,可以读取到其他事务已提交的值,不可能发生脏写 和 脏读, 可能会发生不可重复读和 幻读问 题,因为一旦人家事务修改了值然后提交了,你事务是会读到的,所以可能你多次读到的值是不 同的!
repeatable read - 可重复读
不会发生脏写、脏读和不可重复读的问题,但是会幻读 ,一个事务A多次查询一个数据的值都是相同的,哪 怕别的事务 B、C多次修改值且提交了,当前事务A也不会读取到其他事务修改的值, 保证对同一行数据的多次查询,你不会读到不一样的值, 不对范围进行保证。
serializable - 串行化
脑残才设置这个。
MySQL 是如何支持4种事务的隔离级别,Spring事务注解是如何设置的。
RR - 可重复读
RC- 读已提交
MySQL 默认设置的事务隔离级别都是RR的,而且MySQL 的RR可以避免幻读的产生。
这点是MySQL的RR级别的语义跟SQL标准的RR级别不同的,毕竟SQL标准里规定RR级别是可以发生幻 读的,但是MySQL的RR级别避免了!
也就是说,MySQL里执行的事务,默认情况下不会发生脏写、脏读、不可重复读和幻读的问题, 事务的 执行都是并行的,大家互相不会影响,我不会读到你没提交事务修改的值,即使你修改了值还提交了, 我也不会读到的,即使你插入了一行值还提交了,我也不会读到的,总之,事务之间互相都完全不影 响!
MVCC -多版本并发控制隔离机制
依托MVCC 能让RR 级别避免不可重复读和幻读的问题。
要修改MySQL的默认事务隔离级别 , 可以设置级别为不同的 level,level的值可以是REPEATABLE READ,READ COMMITTED,READ UNCOMMITTED, SERIALIZABLE几种级别。
SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL level
Spring 支持的事务机制
在@Transactional注解里是有一个isolation参数的,里面是可以设置事务隔离级别的
@Transactional(isolation=Isolation.DEFAULT),然后默认的就是DEFAULT值,这个就是MySQL默认支 持什么隔离级别就是什么隔离级别, 也可以改成Isolation.READ_COMMITTED,Isolation.REPEATABLE_READ,Isolation.SERIALIZABLE几 个级别
再次提醒,其实默认的RR隔离机制挺好的,真的没必要去修改,除非你一定要在你的事务执行期间 多次查询的时候,必须要查到别的已提交事务修改过的最新值,那么此时你的业务有这个要求,你就把 Spring的事务注解里的隔离级别设置为Isolation.READ_COMMITTED级别,偶尔可能也是有这种需求的。
MVCC - 多版本并发控制机制
undo log 版本链是什么东西 - MVCC 的版本记录
其实每条数据都有两个隐藏的字段,一个是trx_id, 一个是roll_pointer
trx_id 就是最近一次更新这条数据的事务id
roll_pointer 指向你更新事务之前生成的undo log
我们知道MySQL 的默认隔离级别 RR 可以避免 脏写、脏读、不可重复、幻读,跟SQL 标准级别RR 不一样。那么如何做到的呢?

接着假设有一个事务B跑来修改了一下这条数据,把值改成了值B,事务B的id是58,那么此时更新之前 会生成一个undo log记录之前的值,然后会让roll_pointer指向这个实际的undo log回滚日志

事务B修改了值为值B,此时表里的那行数据的值就是值B了;
接着假设事务C又来修改了一下这个值为值C,他的事务id是69,此时会把数据行里的txr_id改成69,然 后生成一条undo log,记录之前事务B修改的那个值 ;

不管多个事务并发执行是如何执行的,起码先搞清楚一点, 就是多个事务串行执行的时候,每个人修改了一行数据,都会更新隐藏字段txr_id和roll_pointer,同时之前多个数据快照对应的undo log,会通过roll_pinter指针串联起来,形成一个重要的版本链!
基于undo log多版本链条实现的ReadView 机制
ReadView 时,就是你执行一个事务的时候,给你生成一个ReadView,关键的四个东西
- m_ids, 此时有哪些事务在MySQL 里执行,还没提交
- min_trx_id,就是m_ids里最小的值
- max_trx_id ,就是mySQL 下一个要生成的事务id,就是最大事务id
- creator_trx_id, 就是你这个事务的id
ReadView 怎么用
接着呢,此时两个事务并发过来执行了,一个是事务A(id=45),一个是事务B(id=59),事务B是要 去更新这行数据的,事务A是要去读取这行数据的值的,此时两个事务如下图所示。 
现在事务A 直接开启一个ReadView,ReadView里的 m_ids 包含了事务A 和 事务B 的两个id
45、59。min_trx_id = 45 ,max_trx_id = 60, creator_trx_id = 45(A自己)
这个时候 A 第一次查询这行数据,判断修改当前这行数据事务Id,即 trx_id 是否小于ReadView中的min_trx_id,发现 trx_id =32, 小于min_trx_id = 45。说明事务开始之前,这行数据就已经提交了,所以可以查询到这条数据。
事务B开始,改数据值为B,这行数据的 trx_id = 59,同时 roll_point 指向 修改之前生成的Undo log , 接着事务B 提交了。
此时A查询, 发现这行数据的trx_id = 59 ,大于 ReadView 的 min_trx_id = 45 ,同时小于 Read View 的max_trx_id = 60 的,说明在更新这条数据的事务 跟我的事务时差不多时间开始的,此时要去看一下m_ids 事务列表。
此时,在ReadView 的m_ids 列表里,有45 和 59 两个事务id ,直接证实了,这个修改数据的事务是跟自己同一时段并发执行然后提交的,所以这行数据 是不能查询的。 ——不可重复读
那么查什么呢?
顺着这条数据的 roll_pointer 顺着undo log 日志链条往下找,找到最近的一条undo log,此时 trx_id = 32, 小于min_trx_id = 45 的,说明修改这行数据的事务,在我事务A开始之前就提交了( 说明这个undo log版本 必然是在事务A开启之前就执行且提交的。 ),即这行数据对事务A 是可见的,那么查询到最近的undo log 里面的值就好了,这是undo log 多版本链条的作用, 可以保存一 个快照链条,让你可以读到之前的快照值 。
多个事务并发执行的时候,事务B更新的值,通过这套 ReadView+undo log日志链条的机制,可以保证事务A不会读到并发执行的事务B更新的值,只会读到之前最早的值。
此时假设事务A更新了这行数据的值为A,trx_id = 45
此时事务A查询这行数据,发现trx_id = 45, 发现个 自己ReadView 的 creator_trx_id = 45 是一致的,说明这行数据就是自己改的,那么可以读取到。
此时事务A 在执行中,开始了事务C,id=78,更新值为C 且提交了。
这个时候事务A再去查询,发现trx_id = 78, 大于 ReadView 的max_trx_id = 59说明修改这行数据的事务,是在我这个事务开启之后,才更新的数据,事务A 也就看不到 事务C 更新的数据了。
此时再顺着undo log 版本链条往下找,自然找到的是事务A 自己修改的版本,因为那个 trx_id=45跟自己的ReadView里的creator_trx_id是一样的,所以此时直接读取自己之前修改的那个版本。
通过undo log 版本链 和 事务开启时的 ReadView ,然后再有一个查询的时候,根据ReadView判断是否可以读取该版本数据的机制。 可以保证你只能读到你事务开启前,别的提交事务更新的值,还有就是你自己事务更新的值。
假 如说是你事务开启之前,就有别的事务正在运行,然后你事务开启之后 ,别的事务更新了值,你是绝对 读不到的!或者是你事务开启之后,比你晚开启的事务更新了值,你也是读不到的!
Read committd 的隔离级别是如何基于ReadView 实现的?
事务运行期间,只要别的事务修改数据还提交了,那么就可以读取 到人家修改的数据的,所以是会发生不可重复读的问题,包括幻读的问题,都会有的。
如果是你生成ReadView的时候,就已经活跃的事务,在你生成ReadView之后修改了数据,接着提 交了,此时你是读不到的,或者是你生成ReadView以后再开启的事务修改了数据,还提交了,此时也 是读不到的, 实际上就是ReadView机制的一个原理。
如何基于ReadView机制来实现RC隔离级别呢 ?
当事务的隔离级别设置为RC 时,每次发起查询时,都可以重新生成一个ReadView
首先假设我们的数据库里有一行数据,是事务id=50的一个事务之前就插入进去的,然后现在呢,活跃 着两个事务,一个是事务A(id=60),一个是事务B(id=70)
事务B 发起了一次update 操作,更新了这条数据,把这条数据的值修改为B
所以此时数据的trx_id = 70,同时生成一条undo log ,由roll _point 来指向 之前的undo log
此时, 事务A要发起一次查询操作,此时他一发起查询操作,就会生成一个ReadView
ReadView里的min_trx_id=60,max_trx_id=71,creator_trx_id=60 
这个时候事务A发起查询,发现当前这条数据的trx_id是70, 属于ReadView的事务id范围之 间,说明是他生成ReadView之前就有这个活跃的事务,是这个事务修改了这条数据的值,但是此时这 个事务B还没提交,所以ReadView的m_ids活跃事务列表里, 是有[60, 70]两个id的,所以此时根据 ReadView的机制,此时事务A是无法查到事务B修改的值B的。 事务B 还没有提交。
接着就顺着undo log版本链条往下查找,就会找到一个原始值,发现他的trx_id是50,小于当前 ReadView里的min_trx_id, 说明是他生成ReadView之前 ,就有一个事务插入了这个值并且早就提交 了,因此可以查到这个原始值 
接着,咱们假设事务B此时就提交了,好了,那么提交了就说明事务B不会活跃于数据库里了,是不是?
可以的,大家一定记住,事务B现在提交了。那么按照RC隔离级别的定义,事务B此时一旦提交了,说 明事务A下次再查询,就可以读到事务B修改过的值了,因为事务B提交了。
那么到底怎么让事务A能够独到提交的事务B修改过的值呢?
很简单,就是让事务A下次发起查询,再次生成一个ReadView。 此时再次生成ReadView,数据库内活 跃的事务只有事务A了 , 因此min_trx_id是60,max_trx_id是71, 但是m_ids这个活跃事务列表里,只 会有一个60了,事务B的id=70不会出现在m_ids活跃事务列表里了, 
此时事务A再次基于这个ReadView去查询,会发现这条数据的trx_id=70,虽然在ReadView的 min_trx_id和max_trx_id范围之间,但是此时并不在m_ids列表内,说明事务B在生成本次ReadView之 前就已经提交了。
那么既然在生成本次ReadView之前,事务B就已经提交了,就说明这次你查询就可以查到事务B修改过 的这个值了,此时事务A就会查到值B, 
RC隔离级别如何实现的,大家应该就理解了,他的关键点在于每次查询都生成新的 ReadView,那么如果在你这次查询之前,有事务修改了数据还提交了,你这次查询生成的ReadView 里,那个m_ids列表当然不包含这个已经提交的事务了,既然不包含已经提交的事务了,那么当然可以 读到人家修改过的值了。
基于undo log多版 本链条以及ReadView机制实现的多事务并发执行的RC隔离级别、RR隔离级别,就是数据库的MVCC多 版本并发控制机制。 他本质是协调你多个事务并发运行的时候,并发的读写同一批数据,此时应该如何协调互相的可见性 。
RR 如何基于ReadView 避免不可重复读 和 幻读
MySQL 中让多个事务并发运行的时候能够互相隔离,避免同时读写一条数据的时候有影响,是依托undo log版本链条 和 ReadView机制来实现的。
RR 级别下,一个事务无论一条数据读多少次,都是一个值,别的事务的修改且提交对该事务都是不可见的。同时,如果别的事务插入一些数据,也是读不到的,这样就避免了幻读问题。
此时假设一条数据是事务id = 5 的一个事务插入的,同时有事务A 和 事务B在运行,A的事务id = 60,B 的事务id = 70

此时事务 A 发起了一个查询,第一次查询就会生成一个ReadView,ReadView 的 creator_id = 60,min_trx_id = 60,max_trx_id = 71, m_ids = [60,70]
事务A 发起查询,发现这条数据的 trx_id = 50 ,小于min_trx_id 即说明事务A 开启的时候,该事务已经提交了,则这条数据对该事务可见。





上面解决了不可重复读的问题


因此事务A本次查询,还是只能查到原始值一条数据,如下图

锁机制 解决的是多个事务同时更新一行数据。
