事务概念
小钢同学今天发工资了,赶紧打开招商银行app看看工资到账了没有,查看余额300
嗯,今天心情好,给对象转账50大元买lv包包去,最后的结果肯定是小钢同学的钱包余额是250,小钢同学对象钱包余额+50
转账的整个过程在mysql中涉及到一系列动作,下面仔细来看看
现在来考虑一下这个问题,如果在第6步的时候更新数据库失败了会发生什么情况?
- 小钢同学的钱包余额为250元
- 小钢同学对象的钱包余额为0元
转账的50元不翼而飞了?小钢百思不得其解,为此还和对象吵了一架,明明钱给转过去了呀,对象还说没收到?
还好小钢是个技术高超的程序员大佬,趁着和对象冷战期去研究了一下数据库事务,发现事务可以完美解决这个问题,于是赶紧连夜给平台提了bug
数据库中的「事务(Transaction)」:让多个数据库操作要么全部成功,要么全部失败,不允许出现中间态数据,例如上面情况,如果第6步更新数据库失败,事务会负责回滚,小钢转出去的50元会被继续增加回去,小钢余额还是拥有300元
事务特性
首先得注意的是并不是所有的mysql存储引擎都支持事务,常见的Innodb是支持事务的,而MyISAM则不支持事物,所以mysql默认的存储引擎是Innodb
事务有以下四大特性:
- 原子性:所有操作要么全部成功,要么全部失败,不允许出现中间态
比如文章开头所说的小钢转账问题,如果中间任何一个步骤出现问题,事务需要回滚到最初始状态,就好比没发生这件事情一样,小钢余额还是为300元,小钢对象余额为0元
- 一致性:数据库从一个一致性的状态转移到另一个一致性的状态,满足完整性约束
小钢给对象转账之前,小钢余额300元,对象余额0元,总和300元;
小钢给对象转账50元以后,事务提交完成,小钢余额250,对象余额50元,总和300元
转账前和转账后总和不变,这就是一致性
- 持久性:事务一旦提交,数据的改变就是持久性的,就算磁盘宕机恢复也还是修改后的数据
事物提交后,小钢余额250,对象余额50元,这个数据是一个持久性的状态,除非再次修改
- 隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致
事务并发
mysql事务核心在于隔离性,本文重点讲解一下隔离性
首先还是谈一下事务并发会带来什么问题?
- 脏读:如果一个事务「读到」了另一个「未提交事务修改过的数据」,就意味着发生了「脏读」现象
事务A和事务B同时开始执行,事务A先读取小钢余额是300元,事务B紧接着将小钢余额更新为250元,此时事务B没有提交事务,事务A在这个时候再次查询小钢余额发现变成了250元,此时事务B提交事务失败回滚了,小钢余额变为了初始300元,事务A却还认为小钢余额为250元,此时事务A读到的250元数据就是脏数据
- 不可重复读:在一个事务内多次读取同一个数据,如果出现前后两次读到的数据不一样的情况,就意味着发生了「不可重复读」现象
事务A和事务B同时开始执行,事务A读取小钢余额300元,然后先去干其他事情了,事务B读取小钢余额300元并更新为250元,然后成功提交事务,此时事务A再次读取小钢余额为250元,在同一次事务执行中读取数据不一致,这就是不可重复读问题
- 幻读:在一个事务内多次查询某个符合查询条件的「记录数量」,如果出现前后两次查询到的记录数量不一样的情况,就意味着发生了「幻读」现象
事务A和事务B同时执行,事务A负责将所有工资等于100元的员工工资加薪到200元,比如此时有5个员工需要加薪,事务A完成了更新操作,但是还未提交事务,事务B此时新增一个新员工的数据工资为100元,然后提交事务,然后这个时候事务A再次查询工资等于100元的员工,发现变成了6个员工,就好像发生了幻觉一样,这就是幻读
事务隔离级别
事务四种隔离级别
- 未提交读:一个事务未提交时,他所做的变更可以被其他事务看到
会发生脏读、不可重复读、幻读
- 已提交读:一个事务提交以后,他所做的变更才能被其他事务看到
会发生不可重复读、幻读
- 可重复读:事务开启以后,直到提交事务这个时间段,所能看到的数据就是事务开启时的数据,其他事务变更记录看不到
会发生幻读
- 可串行化:对事务加锁,如果发生了读写冲突,会等拿到锁的事务提交以后才会继续执行其他事务
可以解决所有事务并发问题
那么可能会有一个问题,为什么可串行化可以解决所有事务并发问题,为什么数据库不用可串行化隔离级别?
原因:可串行化需要加锁,太慢了,对于优秀的开源系统怎能容忍这种效率问题
mysql默认的隔离级别是可重复读,虽然有幻读的风险,但是概率并不大
MVCC多版本并发控制
mysql的可重复读就是巧妙的借用了MVCC多版本并发控制的思想
- 针对快照读普通查询(select * from table_name):通过 MVCC 方式解决了幻读,因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好的避免幻读问题
- 针对当前读(select … for update 等语句):通过 next-key lock(记录锁+间隙锁)方式解决了幻读,因为当执行 select … for update 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题
Read View
上面也一直提到MVCC思想,接下来详细介绍一下mysql隔离级别是怎么运用的
Read view其实就是数据快照,快照?难道是像redis中的RDB快照一样吗,将当前数据状态记录下来
先介绍跟Read view相关的两个知识点吧(每个事务都会创建Read View)
活跃事务id:开启事务但是还未提交的事务id
- creator_trx_id:创建该Read view快照的事务的id
- min_trx_id:当前数据库中活跃事务id最小的事务
- max_trx_id:当前数据库中活跃事务id最大的事务 + 1(下一个,所以要+1)
- trx_ids:活跃id数组
聚簇索引记录中的两个隐藏列 - trx_id:事务id,记录着当前对该记录修改的事务id
- roll_poniter:指向undo日志,旧版记录会被记录到该地址
在创建Read View后,可以将聚簇索引中的trx_id记录id分为以下三种情况
- trx_id小于Read View中的min_trx_id:表示当前记录是在之前的事务创建的,并且已经提交,那么该记录对当前事务可见
- trx_id在min_trx_id和max_trx_id之间
1、trx_id在trx_ids活跃事务id列表中:说明生成该记录的事务还未提交,所以该记录对当前事务不可见
2、trx_id不在trx_ids活跃事务列表中:说明生成该记录的事务已提交,所以该记录对当前事务可见 - trx_id大于等于max_trx_id:表示该记录是在当前事务创建Read View后的事务创建的,对当前事务不可见
可重复读工作原理
可重复读在开启事务到提交事务整个生命周期只会创建一个Read View,整个生命周期就只使用这一个Read View
还是拿事务A和事务B以及小钢来举例
事务A、B创建Read View
事务 A 和 事务 B 的 Read View 具体内容如下:
- 在事务 A 的 Read View 中,它的事务 id 是 51,由于它是第一个启动的事务,所以此时活跃事务的事务 id 列表就只有 51,活跃事务的事务 id 列表中最小的事务 id 是事务 A 本身,下一个事务 id 则是 52
- 在事务 B 的 Read View 中,它的事务 id 是 52,由于事务 A 是活跃的,所以此时活跃事务的事务 id 列表是 51 和 52,活跃的事务 id 中最小的事务 id 是事务 A,下一个事务 id 应该是 53
接着,在可重复读隔离级别下,事务 A 和事务 B 按顺序执行了以下五大操作: - 事务 A 读取小钢的余额为300元;
- 事务 B 将小钢的余额修改成 250 元,并没有提交事务;
- 事务 A 读取小钢的余额,读到余额还是 300 元;
- 事务 B 提交事务;
- 事务 A 读取小钢的余额,读到余额依然还是 300 元
按照上述五大操作具体分析一下:
当前记录暂时只有一条,trx_id为50
- trx_id小于事务A的Read View的min_trx_id,所以该记录对于事务A可见,事务A查询小钢余额是300元
- 事务B将小钢余额修改为250元,新增一条记录,新记录trx_id为51
- 事务A此时会先去读最新纪录,trx_id为52,52大于等于事务A的Read View的max_trx_id,说明该记录是在事务A创建完Read View后才创建的,所以该记录对事务A不可见,于是会继续往回寻找下一个记录,即trx_id为50的记录,trx_id为50小于事务A的Read View的min_trx_id,所以该记录对于事务A可见,事务A查询小钢余额是300元
- 事务B提交事务(事务B自身的Read View内容会改变,但是对事务A的Read View不影响)
- 事务A此时会先去读最新纪录,trx_id为52,52大于等于事务A的Read View的max_trx_id,说明该记录是在事务A创建完Read View后才创建的,所以该记录对事务A不可见,于是会继续往回寻找下一个记录,即trx_id为50的记录,trx_id为50小于事务A的Read View的min_trx_id,所以该记录对于事务A可见,事务A查询小钢余额是300元
就是通过这样的方式实现了,「可重复读」隔离级别下在事务期间读到的记录都是事务启动前的记录
已提交读工作原理
已提交读在开启事务到提交事务整个生命周期会创建多个Read View(事务每次读取数据都会创建新的Read View)
还是拿事务A和事务B以及小钢来举例
事务A、B第一次创建Read View
事务 A 和 事务 B 的 Read View 具体内容如下:
- 在事务 A 的 Read View 中,它的事务 id 是 51,由于它是第一个启动的事务,所以此时活跃事务的事务 id 列表就只有 51,活跃事务的事务 id 列表中最小的事务 id 是事务 A 本身,下一个事务 id 则是 52
- 在事务 B 的 Read View 中,它的事务 id 是 52,由于事务 A 是活跃的,所以此时活跃事务的事务 id 列表是 51 和 52,活跃的事务 id 中最小的事务 id 是事务 A,下一个事务 id 应该是 53
接着,在可重复读隔离级别下,事务 A 和事务 B 按顺序执行了以下五大操作:
- 事务 A 读取小钢的余额为300元;
- 事务 B 将小钢的余额修改成 250 元,并没有提交事务;
- 事务 A 读取小钢的余额,读到余额还是 300 元;
- 事务 B 提交事务;
- 事务 A 读取小钢的余额,读到余额为 250 元
按照上述五大操作具体分析一下:
当前记录暂时只有一条,trx_id为50
- trx_id小于事务A的Read View的min_trx_id,所以该记录对于事务A可见,事务A查询小钢余额是300元
- 事务B将小钢余额修改为250元,新增一条记录,新记录trx_id为51
- 事务A继续读取数据,此时新创建Read View,由于新创建了,所以事务A的Read View能够看到事务B的相关信息了,新创建的事务A的Read View如下图,此时先看最新纪录trx_id为52,52是在min_trx_id和max_trx_id之间,且52在trx_ids活跃事务列表中,所以说明52这个事务还未提交,故事务A看不到这条记录,紧接着继续向后遍历下一条记录,trx_id为50的,50小于事务A的Read View中的min_trx_id,所以事务A可以看到该条记录,故事务A读取数据为300元
- 事务B提交事务
- 事务A再次读取数据,此时又会新建Read View,如下图所示,此时先看最新纪录trx_id为52,52是在min_trx_id和max_trx_id之间,且52不在trx_ids活跃事务列表中,所以说明52这个事务已经提交,故事务A能看到这条记录,所以事务A读取数据为250元
以上就是已提交读和可重复读的工作原理了,至于未提交读和可串行化暂时没有必要去详细分析了,工作中对于这两种也基本不会用到
最后来个小总结吧
1、事务是在 MySQL 引擎层实现的,我们常见的 InnoDB 引擎是支持事务的,事务的四大特性是原子性、一致性、隔离性、持久性
2、当多个事务并发执行的时候,会引发脏读、不可重复读、幻读这些问题,为了避免这些问题,SQL 提出了四种隔离级别,分别是未提交读、已提交读、可重复读、可串行化,从左往右隔离级别顺序递增,隔离级别越高,意味着性能越差,InnoDB 引擎的默认隔离级别是可重复读
3、要解决脏读现象,就要将隔离级别升级到已提交读以上的隔离级别,要解决不可重复读现象,就要将隔离级别升级到可重复读以上的隔离级别
4、对于幻读,不建议将隔离级别升级为串行化,因为这会导致数据库并发时性能很差。MySQL InnoDB 引擎的默认隔离级别虽然是「可重复读」,但是它很大程度上避免幻读现象
5、对于「已提交读」和「可重复读」隔离级别的事务来说,它们是通过 Read View 来实现的,它们的区别在于创建 Read View 的时机不同
6、已提交读和可重复读这两个隔离级别实现是通过「事务的 Read View 里的字段」和「记录中的两个隐藏列」的比对,来控制并发事务访问同一个记录时的行为,这就叫 MVCC(多版本并发控制)
7、在可重复读隔离级别中,普通的 select 语句就是基于 MVCC 实现的快照读,也就是不会加锁的;而 select … for update 语句就不是快照读了,而是当前读了,也就是每次读都是拿到最新版本的数据,但是它会对读到的记录加上 next-key lock 锁,有新变更会被阻塞