多个事务同事执行的场景?

一般是写一个业务系统,然后业务系统会去对数据库执行增删改查,业务系统是执行一个个事务,每个事务里面可能是一个或者多个增删查改的SQL 语句。 这个事务的概念想必不用我多说了,其实就是一个事务里的SQL要不然一起成功就提交了,要不然有一 个SQL失败,那么事务就回滚了,所有SQL做的修改都撤销了
image.png

但是业务系统也是个多线程的系统,面相的终端用户有很多人,可能会同事发起请求,所以也需要多个线程并发来处理多个请求。
所以这个业务系统很可能是基于多线程并发的对MySQL数据库去执行多个事务的!

image.png
image.png
如果事务提交之后,redo log刷入磁盘,结果MySQL宕机了,是可以根据redo log恢复事务修改过的缓 存数据的。 如果要回滚事务,那么就基于undo log来回滚就可以了,把之前对缓存页做的修改都给回滚了就可以 了。

由事务回滚的原理 引出 事务的概念

  • 多个事务并发执行的时候,可能会同时对缓存页里的一行数据进行更新,这个冲突怎么处理?是否 要加锁?
  • 可能有的事务在对一行数据做更新,有的事务在查询这行数据,这里的冲突怎么处理?

其实对于我们的业务系统去访问数据库而言,他往往都是多个线程并发执行多个 事务的,对于数据库而言,他会有多个事务同时执行,可能这多个事务还会同时更新和查询同一条数 据,所以这里会有一些问题需要数据库来解决。

如果多个事务要是对缓存页里的同一条数据同时进行更新或者查询,此时会 产生哪些问题呢?

  1. 脏写

image.png
事务A 和 事务B ,同时在更新一条数据,事务A 先更新为A值,事务B紧接着更新为B值。
此时事务A 写了一条 undo log 日志,在事务A 更新之前 值为 NULL ,主键为xx,那么此时事务A 回滚,把值改 为NULL。对事务B 来说,这是不可接受的,我更新的值为什么没了?这就是脏写, 刚才明明写了一个数据值,结果过了一会儿却没了, 本质就是事务B去修改了事务A修改过的值,但是此时事务A还没提交,所以事务A随时会回滚, 导致事务B修改的值也没了。
image.png

  1. 脏读

事务A 更新了一行数据,再事务未提交的时候,事务B读取了这条数据,去做各种业务处理,此时事务A 回滚了操作,恢复成原 值,事务B再次查询这行数据,发现值为NULL。
image.png
本质其实就是事务B去查询了事务A修改过的数据,但是此时事务A还没提 交,所以事务A随时会回滚导致事务B再次查询就读不到刚才事务A修改的数据了
无论是脏写还是脏读,都是因为一个事务B去更新或者查询了另外一个还没提交的事务A 更新过的数据,然后未提交的事务A进行了回滚,导致事务B更新或者查询到错误的数据。

  1. 不可重复度

    事务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,那么你就是希望可重复读的。

简单来说就是一个事务多次查询一条数据,结果每次读取到的值都是不一样的。原因是因为这个过程中,会有别的事务修改这个值且提交事务导致的。

  1. 幻读— 数据库的并发问题

场景:

  1. 事务A ,先执行一条SQL语句,查询一批数据,select * from table where id > 10,查出来10条数据
  2. 此时,事务B 往id > 10 的地方插入2条数据。
  3. 事务A 第二次查询,id>10的数据查询到12条。
  4. 事务A :是我的双眼出现幻觉了吗,就是幻读的由来。

幻读,就是一个事务用同样的SQL多次查询,结果每次查询都会发现一些之前没有看到过的数据

说实在的,大家看完最近几篇文章,应该都有一个感觉,就是脏写、脏读、不可重复读、幻读,都是因 为业务系统会多线程并发执行每个线程可能都会开启一个事务,每个事务都会执行增删改查操作
然后数据库会并发执行多个事务,多个事务可能会并发的对缓存页里的同一批数据进行增删改查操作, 于是这个并发增删改查同一批数据的问题,可能就会导致我们说的脏写、脏读、不可重复读、幻读,这 些问题。
上面4种问题的本质就是都是数据库的多事务并发问题,那么为了解决多事务并发问题,数据库才设计了 事务隔离机制、MVCC多版本隔离机制、锁机制,用一整套机制来解决多事务并发问题

SQL标准中的4种事务隔离级别

说多个事务并发运行的时候,互相是如何隔离的, 不同的隔离级别是可以避免不同的事务并发问题的,所以大家一定要对这个事务隔离级别有一个深刻的 理解。

  1. read uncommitted -读未提交

    顾名思义,可以读取到其他事物未提交的值,可能发 生脏读,不可重复读,幻读。

  1. read committed - 读已提交

顾名思义,可以读取到其他事务已提交的值,不可能发生脏写 和 脏读, 可能会发生不可重复读和 幻读问 题,因为一旦人家事务修改了值然后提交了,你事务是会读到的,所以可能你多次读到的值是不 同的!

  1. repeatable read - 可重复读

    不会发生脏写、脏读和不可重复读的问题,但是会幻读 ,一个事务A多次查询一个数据的值都是相同的,哪 怕别的事务 B、C多次修改值且提交了,当前事务A也不会读取到其他事务修改的值, 保证对同一行数据的多次查询,你不会读到不一样的值, 不对范围进行保证。

  2. 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 不一样。那么如何做到的呢?

image.png

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

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

image.png

不管多个事务并发执行是如何执行的,起码先搞清楚一点, 就是多个事务串行执行的时候,每个人修改了一行数据,都会更新隐藏字段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 怎么用
image.png

接着呢,此时两个事务并发过来执行了,一个是事务A(id=45),一个是事务B(id=59),事务B是要 去更新这行数据的,事务A是要去读取这行数据的值的,此时两个事务如下图所示。
image.png

现在事务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。说明事务开始之前,这行数据就已经提交了,所以可以查询到这条数据。
image.png
事务B开始,改数据值为B,这行数据的 trx_id = 59,同时 roll_point 指向 修改之前生成的Undo log , 接着事务B 提交了。
image.png
此时A查询, 发现这行数据的trx_id = 59 ,大于 ReadView 的 min_trx_id = 45 ,同时小于 Read View 的max_trx_id = 60 的,说明在更新这条数据的事务 跟我的事务时差不多时间开始的,此时要去看一下m_ids 事务列表。
此时,在ReadView 的m_ids 列表里,有45 和 59 两个事务id ,直接证实了,这个修改数据的事务是跟自己同一时段并发执行然后提交的,所以这行数据 是不能查询的。 ——不可重复读image.png
那么查什么呢?
顺着这条数据的 roll_pointer 顺着undo log 日志链条往下找,找到最近的一条undo log,此时 trx_id = 32, 小于min_trx_id = 45 的,说明修改这行数据的事务,在我事务A开始之前就提交了( 说明这个undo log版本 必然是在事务A开启之前就执行且提交的。 ),即这行数据对事务A 是可见的,那么查询到最近的undo log 里面的值就好了,这是undo log 多版本链条的作用, 可以保存一 个快照链条,让你可以读到之前的快照值 。
image.png
多个事务并发执行的时候,事务B更新的值,通过这套 ReadView+undo log日志链条的机制,可以保证事务A不会读到并发执行的事务B更新的值,只会读到之前最早的值。

此时假设事务A更新了这行数据的值为A,trx_id = 45
image.png
此时事务A查询这行数据,发现trx_id = 45, 发现个 自己ReadView 的 creator_trx_id = 45 是一致的,说明这行数据就是自己改的,那么可以读取到。
image.png
此时事务A 在执行中,开始了事务C,id=78,更新值为C 且提交了。
image.png
这个时候事务A再去查询,发现trx_id = 78, 大于 ReadView 的max_trx_id = 59说明修改这行数据的事务,是在我这个事务开启之后,才更新的数据,事务A 也就看不到 事务C 更新的数据了。
image.png
此时再顺着undo log 版本链条往下找,自然找到的是事务A 自己修改的版本,因为那个 trx_id=45跟自己的ReadView里的creator_trx_id是一样的,所以此时直接读取自己之前修改的那个版本。
image.png

通过undo log 版本链 和 事务开启时的 ReadView ,然后再有一个查询的时候,根据ReadView判断是否可以读取该版本数据的机制。 可以保证你只能读到你事务开启前,别的提交事务更新的值,还有就是你自己事务更新的值。
假 如说是你事务开启之前,就有别的事务正在运行,然后你事务开启之后 ,别的事务更新了值,你是绝对 读不到的!或者是你事务开启之后,比你晚开启的事务更新了值,你也是读不到的!

Read committd 的隔离级别是如何基于ReadView 实现的?

事务运行期间,只要别的事务修改数据还提交了,那么就可以读取 到人家修改的数据的,所以是会发生不可重复读的问题,包括幻读的问题,都会有的。

如果是你生成ReadView的时候,就已经活跃的事务,在你生成ReadView之后修改了数据,接着提 交了,此时你是读不到的,或者是你生成ReadView以后再开启的事务修改了数据,还提交了,此时也 是读不到的, 实际上就是ReadView机制的一个原理。

如何基于ReadView机制来实现RC隔离级别呢 ?

当事务的隔离级别设置为RC 时,每次发起查询时,都可以重新生成一个ReadView
首先假设我们的数据库里有一行数据,是事务id=50的一个事务之前就插入进去的,然后现在呢,活跃 着两个事务,一个是事务A(id=60),一个是事务B(id=70)
image.png
事务B 发起了一次update 操作,更新了这条数据,把这条数据的值修改为B
所以此时数据的trx_id = 70,同时生成一条undo log ,由roll _point 来指向 之前的undo log
image.png
此时, 事务A要发起一次查询操作,此时他一发起查询操作,就会生成一个ReadView
ReadView里的min_trx_id=60,max_trx_id=71,creator_trx_id=60
image.png
这个时候事务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之前 ,就有一个事务插入了这个值并且早就提交 了,因此可以查到这个原始值
image.png
接着,咱们假设事务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活跃事务列表里了,
image.png
此时事务A再次基于这个ReadView去查询,会发现这条数据的trx_id=70,虽然在ReadView的 min_trx_id和max_trx_id范围之间,但是此时并不在m_ids列表内,说明事务B在生成本次ReadView之 前就已经提交了。
那么既然在生成本次ReadView之前,事务B就已经提交了,就说明这次你查询就可以查到事务B修改过 的这个值了,此时事务A就会查到值B,
image.png
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

image.png

此时事务 A 发起了一个查询,第一次查询就会生成一个ReadView,ReadView 的 creator_id = 60,min_trx_id = 60,max_trx_id = 71, m_ids = [60,70]
image.png

事务A 发起查询,发现这条数据的 trx_id = 50 ,小于min_trx_id 即说明事务A 开启的时候,该事务已经提交了,则这条数据对该事务可见。
image.png
image.png

image.png
image.png
image.png

image.png
上面解决了不可重复读的问题
image.png
image.png
image.png
因此事务A本次查询,还是只能查到原始值一条数据,如下图
image.png

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