一:事务
概念:一组原子性的sql查询语句,也可以看作是一个工作单元
特点:要么全部执行成功,要么全部执行失败
一个有效的事务需满足的条件(ACID)
原子性(Atomicity)
一个事务必须被视为一个单独的内部最小的,”不可分“的工作单元,以确保事务要么全部执行,要么全部执行失败,当一个事务具有原子性的时候,该事务绝对不会部分执行,要么全部执行,要么不执行。
一致性(Consistency)
数据库总是从一种一致性状态转换到另一种一致性状态,例如银行转账业务中的俩者金额总和的是不变的。这种一致性包括:实体的完整性(字段的类型、大小等),外键约束
隔离性(Isolation)
各个事务中的是互不干扰的,但是严格的隔离性,只有事务隔离级别中的Serializable(可串行化)可以达到
持久性(Durability)
一但一个事务提交,事务所造成的数据改变是永久的,即使系统的崩溃数据也不会因此丢失
二:事务的类别
1. 读未提交(幻读,不可重复读,脏读都可能发生)
定义:可以读到另一个事务未提交的数据
2. 可重复读(mysql默认的隔离级别,可能发生幻读)
注意:
- 不可重复读:读到了已经提交的事务的更改数据(修改或删除)
- 幻读:读到了其他已经提交事务的新增数据
定义:
同一个事务的多个实例在并发处理数据的时候,会看到同样的数据行,但是确可能出现幻读(幻读和不可重复读的区别在于,幻读的注重点在于insert,不可重复读的重点在于select)
那为什么就能这样呢,看到同样的数据行,但是确可能出现幻读,可重复读的内在实现机制是怎样的呢?那这一切就和mvcc机制有关了,以下的所有叙述就拿innodb来讲
可重复读基于mvcc,readview实现的前提:
行数据中的影藏属性(当然不止这几个)
innodb通过为每个数据行增加俩个隐藏值的方式实现了mvcc,这俩个影藏值记录了行的创建时间及结束时间,同时每一行都有一个系统版本号来替代事件的创建时间,实际操作中存的系统版本号就是事务的id,每开启一个事务,这个系统版本号都会递增。
mysql高性能中的原话:
在聚集索引中,每一条数据(每个叶子节点)中都包含主键值、事务ID(tid)、用于事务和MVCC的回滚指针(RB)及剩下的列叶子节点通过RB连接
视图(consistent read view)
这个视图不是查询语句定义的虚拟表。这个一致性视图是为了用于支持隔离级别为《可重复读(repeatable read)和读已提交 (read committed)的实现》
隔离级别为 可重复读 (repeatable read)只有在第一次select的时候才会生成readview,而读已提交(read
committed)在每一次的select的时候都会生成一个readview
mysql高性能中的原话:
mvcc的每一次select操作中干了2件事,1:innodb只查找版本号早于当前事务版本的数据行(也就是只查找这个版本,或者比这个版本更早之前的数据)。2:数据行的删除版本必须是未定义,或者大于事务版本的(保证了读取的行在事务开起前时是未被删除的)
mysql高性能中的原话:mvcc是通过及时保存在某些时刻的数据”快照“实现的(也就是及时保存对应的readview,一个事务内,拿update操作说,innodb会为每个需要更新的行建立一个新的行拷贝,为新的行拷贝记录版本号的同时,修改旧数据的系统版本号,新的行拷贝通过RB与旧数据连接起来形成了一个版本链,之后在select的时候通过比较版本号从而达到readview复用的目的)。
readview遵循的可见性算法**,参考说白了在每次select的时候,会从最新的这条数据开始遍历readview中的每一条数据,直至找到对当前事务可见的这条数据,然后返回
算法过程如下:
1:拿到readview最新的这条数据id,与系统当前活跃事务的id逐个进行比较,如果<最小的活跃事务的id,则这条数据对当前系统所有的活跃事务都具有可见性。
2:如果>=,会接着与**出现过的最大事务id+1**进行比较,如果>=这个版本的数据不可见(说明生成readview之前是没有这个版本的数据的),如果<的话,判断readview中最新的这条数据有无commit(readview中维护着一个活跃事务id的数组),commit了的就是可见,没提交就是不可见,这里窃取[@呵呵一笑](https://www.zhihu.com/question/66320138/answer/241418502)的一张源码图
故每一条数据大概长这样
每一次的修改版本号的操作(update insert delete),就拿updated来说,当有多个事务先后对同一条数据进行修改的时候,通过上面的第6点就得到了这样一个版本链(readview越往上的数据越新)
下面来实操加深理解可重复读的实现原理
在隔离级别为 可重复读 (repeatable read)下,我分别用黄色,紫色,蓝色按照先后顺序开启了三个事务1、2、3。红色为代码的执行顺序。认真阅读后大家会发现这样一个有趣的现象,事务1、3读取到的数据不是一样的,这是为什么呢?下面开始分析
因为每开启一个事务,版本号都是递增的,转换成流程图就是这样的
事务1,根据隔离级别为 可重复读 (repeatable read)只有在第一次select的时候才会生成readview,故事务1第一次select后的readview永远是只有A对应的这个版本的数据这一条。当事务2提交之后,读取到的数据永远是b=666这条。
同理,事务3在select后生成的readview包含了A、B对应的这俩条数据,根据readview可见性算法:
取出readview中最新数据的事务id也就是对应的事务2的tid2=2,
tid=2与活跃事务中id最小的比较,也就是事务1且tid1=1,tid2>=tid1,
接着让tid2=2与出现过的最大事务的id+1比较,也就是tid2与tid3+1比较,tid2=2<tid3=3+1,且tid=2对应的这条是commit的,所以tid=2的这条数据是可见的
故事务3读到的数据一直是b=1.。(所以避免了不可重复读,其实就是对应事务1、2生成的readview不同,加上readview算法实现的)
3:读已提交
定义:一个事务只能读取到另一个事务已经提交的内容
思考:为什么只能读取到另一个事务已经提交的内容,那为什么不能读取到未提交的内容呢,其实这还是和mvcc及readview可见性算法有关
下面看一个请求流程
由于和可重复读的分析过程差不多,就简述以下吧,
这里就和上面提到的mvcc前提3有关(读已提交 在每一次的select的时候都会生成一个readview),
A读到的数据满足readview可见性算法中的第1点
B读不到未提交的数据,是因为不符合readview可见性算法中的第2点中的,未commit的数据读不到
A读得到数据满足readview可见性算法,且readview每select就生成
4:可串行化
定义:其实就是利用悲观锁得原理实现得,数据安全,并发能力差
END总结
以上的所有读都是快照读
简述以下快照读与当前读吧(快照读就是普通的select 语句,当前读就是加select lock in share 或者 for update 这种,读到的数据是最新的数据)当前读在可重复读级别下演示
说白了mvcc就是根据乐观锁实现得,通过加版本号,在每一次select的时候去快照中获取可见性最新的数据,只不过是mysql内部帮我们封装好了这些个判断逻辑。
而串行化呢,就是加悲观锁保证并发安全问题,像我们写秒杀系统中不也是可以通过悲观锁的方式来保证并发嘛