一.绪论
前面的六篇文章中,已经给大家深入讲解了MySQLbuffer pool机制、redo log机制和undo log机制,相信大家现在对我们平时执行一些增删改语句的实现原理,都有了一定较为深入的理解了,接下来我会讲解mysql的事务,会站在mysql内核角度,进一步深入理解事务
二.事务
2.1事务的由来
事务的根本原因在于多线程操作
我们操作数据库的时候,一个事务可能会有多条数据,当事务A在执行的时候,事务B也可能在执行,这个时候由于多线程操作数据库,就会引发一些问题,比如在前面我讲解了buffer pool,redolog,undolog几个机制,就会有一些问题
1.多个事务并发执行时候,可能会对同一个缓存页的一行数据进行更新,怎么处理这个冲突?
2.一个事物在对数据处理时候,另外一个事物在查询这行数据,怎么办?
2.2 事务带来的四种问题
如图所示,当多个事务同时对数据库进行crud时候,会产生事物的四种情况
1.脏写
2.脏读
3.不可重复读
4.幻读
下面依次来介绍这四种情况
2.2.1 脏写
脏写是指两个事务对同一个数据进行修改,事务A把它改为A,事务B把它改为B,并且事务B提交了,随后事务A回滚了,导致事务B的更新被覆盖
这种已经不满足事务的基本条件,事务提交后,数据就不会改变,所以脏写是所有数据库都不能允许发生的情况
2.2.2 脏读
脏读是指一个事务读到另外一个未提交的事务修改的值,当修改值的事务回顾时候,导致数据的不一致.
如上图,事务A修改数据为A,事务B这个时候读取的值就是A,此时事务A回滚了, 原始数据的值是xxx,但是此时事务B读取的确是A,导致数据的不一致;
脏写和脏读的本质是一个事务能获取到另外一个事物没有提交的数据,因为另外一个事物并没有提交,所以他随时可以回滚,一旦回滚,就会导致脏写/脏读问题
2.2.3 不可重复读
脏读和脏写是读一个事物未提交的数据,会导致的问题,后面两种不可重复读和幻读的前提是读取别人已经提交的事务,如下图所示:
1.事务B读取数据是xxx
2.事务A修改数据为A
3.事务B再次读取,数据变成了A
同一个事物B读取了多次,但是数据确实不一样的,这是✅还是❎呢?
事务B第一次读取是xxx,第二次读取是A,说正确也算正确,毕竟这个时候事务A已经提交了,事务B多次查询事务提交后的值,也没问题;
说有问题也算有问题,就是希望一个事务从开始到结束,查询的数据希望永远是一个值,希望这条数据是可以重复读的,所以可重复度是否有问题,取决于你想要数据库这么办
2.2.3 幻读
幻读是指事物中用同样的sql查询多次,查询的数据条数不一样
比如select * from users where id >10;
第一次查询出10条,然后另外一个事物查询一条数据,再次执行这个sql,发现查出来11条;
就像是幻觉一样,所以叫做幻读!!😄
2.3 事务的隔离级别
数据库在多线程执行的时候,会导致脏写,脏读,不可重复度,幻读问题,所以sql标准中就规定了事务的几种隔离,用来解决这些问题
注意,隔离级别是sql的标准,是针对所有数据库的,比如oracle,不是单单指mysql,sql标准中规定四种隔离级别,分别是
1.
read uncommitted(读未提交),2.
read committed(读已提交),3.
repeatable read(可重复读),4.
serializable(串行化)
这几中都比较简单,就不做过多赘述,简单讲解
2.3.1 读未提交
这种隔离级别下,能避免脏写,不能避免脏读,不可重复读和幻读,一般来说系统开发中,没人会把隔离级别设置成这个最低的级别
2.3.2 读已提交
这种隔离级别下,能读取到别的事务已经提交的数据,能避免脏写,脏读,不能避免不可重复读和幻读, oracle的默认的隔离级别就是这种
2.3.3 可重复度
这种级别下,不会发生脏写,脏读,和不可重复读的问题,事务中多次读取一条数据,这条数据的值不会发生变化,但是还是会发生幻读情况
2.3.4 序列化
这种级别杜绝所有情况,因为这种情况是不允许多线程操作的,所有的线程必须排队,一个一个的执行,这就大大降低数据库的并发能力,除非脑子瓦特掉了,不然没人会这样搞的
总结 读未提交和序列化,在实际中不会有人这样设置的,重点关注就是RC和RR
2.4 sping 注解事务
如果想要测试mysql的事务,需要设置myslq的事务级别
SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL level;
LEVEL level 可以设置为REPEATABLE READ,READ COMMITTED,READ UNCOMMITTED,SERIALIZABLE四种
另外再开发中,spring开启事务也是可以设置的
@Transactional(isolation=Isolation.DEFAULT) 你也可以设置为
@Transactional(isolation.READ_UNCOMMITTED) 读未提交的事务,但是一般不建议你这样搞,让别的开发知道因为你瞎改事务隔离级别而引起问题,估计后面会被打死的😭😭😭😭
三.msyql如何解决事务带来的问题
mysql的隔离级别是RR,但是它却能完美决绝脏读,不可重复读和幻读问题,mysql可是花费不小心思,用的正是MVCC(多版本并发控制),在讲解这个MVCC之前,首先先介绍下 undo log版本链
3.1 undo log版本链
mysql中的每条数据,其实都有两个隐藏的字段,一个是trx_id(事务id),一个是 roll_pointer,
trx_id 是最近一次更新这个数据的事务id
roll_pointer 就是指向更新这个事务之前生成的undo log
如图所示
事务A插入一条数据,这个时候在undolog 中roll_point为空,事务id=50;
事务B修改这条数据为B,事务id=51,roll_point指向 txr_id=50的那条数据, 同理当事务C执行后,在undo log中会形成一个版本链,版本链连接后,接下来就要来学习大名鼎鼎的MVCC,看看那mysql是如何巧妙的使用mvcc来处理事务的
四.MVCC
MVCC英文全称Multi-Version Concurrency Control,中文翻译多版本并发控制,是一种用来解决
读 -写冲突的无锁并发控制技术,同时也是解决事务中隔离性的关键
📢:mvcc是来处理 读写冲突的,写写冲突还是需要加锁才能处理的
MVCC就是因为大牛们,不满意只让数据库采用悲观锁这样性能不佳的形式去解决读 - 写冲突问题,而提出解决方案。所以在数据库中有了MVCC,可以形成如下两种组合
MVCC + 悲观锁:MVCC解决读写冲突,悲观锁解决写写冲突
MVCC + 乐观锁:MVCC解决读写冲突,乐观锁解决写写冲突
4.1 当前读和快照读
当前读读取的都是记录的最新数据版本,读取时需要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁,像SELECT LOCK IN SHARE MODE(共享锁)、SELECT FOR UPDATE、UPDATE、INSERT、DELETE(排它锁)这些操作都是当前读
快照读就是普通的SELECT,即不加锁的非阻塞读,快照读的前提是事务隔离级别不是串行,在串行级别下快照读会退化成当前读,快照读是为了提高并发度,让读写不冲突
快照读非常依赖于
读操作首次出现的时机,它有决定该事务后续快照读结果的能力
4.2 ReadView 试图
在了解当前读和快照读后,我们还需要知道read view,事务在进行快照读操作的时候产生的读视图,在该事务执行快照读的那一刻,会生成数据库系统当前对于事务的一个快照,记录并维护系统当前活跃事务的id,读视图并不是事务开始后就生成了,而是在进行快照读的时候才会产生,当前读(加排它锁)不会产生读视图;
在不同的隔离级别下,读视图也不相同
1.在可重复读隔离级别下,不管有多少个快照读,使用的都是第一个快照读进行时生成的读视图,读视图从始至终都不会发生变化
2.在读已提交隔离级别下,每进行一次快照读,都会产生一个读视图,也就是说每次快照读,读视图都不相同,读视图随着执行快照读而发生变化
综上所述,在读已提交隔离级别下,即使是相同的SELECT语句,由于读视图可能不同(每次产生新的读视图),读取到的数据可能不同;在可重复读隔离级别下,相同的SELECT语句,读视图一定相同(沿用第一次快照读产生的读视图),读取到的数据一定相同。这也是可重复读的含义,相同的SELECT语句,查询的数据一定相同;
读视图由查询时所有未提交事务id数组(数组里最小的id为min_id)和已提交的最大事务id(max_id)组成。查询的数据结果需要和读视图做对比从而得到快照结果
如上图所示,这些事务会分成三部分,这三部分作为undolog数据能否读取到的依据,规则如下:
1.如果落在绿色部分(trx_id < min_id),表示这个版本是已提交的事务生成的,这个数据是可见的
2.如果落在红色部分(trx_id > max_id),表示这个版本是由将来启动的事务生成的,是肯定不可见的
3.如果落在黄色部分(min_id <= trx_id <= max_id)那就包含两种情况
3.1 若trx_id在数组中,表示这个版本是由还没有提交的事务生成的,对当前事务不可见
3.2 若trx_id不在数组中,表示这个版本是已经提交的事务生成的,对当前事务可见
通过下面两个例子,我们来分析下mvcc读取数据
4.2.1 案例1
第一次快照读:select 1第一次进行快照读生成读视图readview[100, 200], 300。当前活跃事务id为100和200,已提交的最大事务id为300。min_id为100,max_id为300
第一条数据的row的trx_id为300,min_id <= trx_id <= max_id同时trx_id不在未提交事务id的数组[100, 200]中,因此该条数据对当前事务可见,停止查找直接返回数据,查询到的数据就是lilei300
第二次快照读由于是可重复读,读视图沿用第一次快照读的读视图readview[100, 200], 300,
第一条也是最新的一条数据,trx_id为100。100在事务活跃数组[100, 200]中,属于事务未提交的修改,对当前事务不可见,根据回滚指针,找到第二条数据,trx_id为100。100在事务活跃数组[100, 200]中,属于事务未提交的修改,对当前事务不可见,根据回滚指针,找到第三条数据,trx_id为100。min_id <= 100 <= max_id同时trx_id不在未提交事务id的数组[100, 200]中,因此该条数据对当前事务可见,停止查找直接返回数据
第三次快照读:读视图仍然沿用第一次快照读生成的读视图,由于读视图不变,根据版本比对规则,到最后也会找到lilei300这条数据
4.2.2 案例2
大家可以根据这个undolog版本链,自己分析下
四.总结
RC、RR级别下的InnoDB快照读有什么不同?
在RC隔离级别下,是每个快照读都会生成并获取最新的Read View
在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View,之后的快照读获取的都是第一次快照读生成的Read View
正是由于在不同事务隔离级别下,Read View可能不同(活跃事务提交),造成读取的数据可能不同
多个事务对同一条数据进行修改或者删除时,通过undo log形成数据版本链的链表,版本数据中有额外的隐式字段事务id,回滚指针。回滚指针指向老数据,形成链表,链表的头部就是最新的数据,尾部就是最老的数据; 读视图就是对当前系统中所有事务进行一个快照,包含已提交和未提交事务,用来判断数据的可见性。读视图在RC和RR隔离级别生成时机不同;查找数据时,到undo log日志中进行查找,根据读视图,进行版本比对,判断可见性,找到合适的数据,进行返回;MVCC是避免读写冲突的关键技术,同时也是实现事务隔离性的关键技术