MySql 笔记(四)Mysql事务的提交及底层实现原理
在写这篇博客的时候,也有在网上翻阅大量的资料,CSDN、知乎等平台,花了一个多星期去理解和掌握这些知识,在网上确实也有些文章写的也有很多不一样的地方,很多文章都是有错误的,我们需要对这些文章有自己的判别,还是要结合自己的看法来。然后包括自己要组织语言写起来,也是要重新去翻阅很多其他的资料。下面算是一些总结,加上自己的一些理解吧。
写在前面
在网上有一个总结的很好的一句话,在这里记下来:
- 事务的原子性是通过 undo log 来实现的。
- 事务的持久性性是通过 redo log 来实现的。
- 事务的隔离性是通过 (读写锁+MVCC)来实现的。
- 事务的一致性是通过原子性,持久性,隔离性来实现的。
事务的提交
说一下二阶段提交
表示redo log分为prepare阶段和commit阶段这样的两个阶段提交。它的流程是这样的:
- redo log在prepare后会写入磁盘,保证日志不丢失
- binlog写入磁盘
- 在commit阶段又会给redo log写入commit的状态。且提交了就会使数据具有可见性。
为什么需要二阶段提交?
用反证法来证明,如果没有二阶段提交:
- 先写redolog为commit状态再写binlog,如果redolog写入磁盘成功后直接挂了,然后重启mysql后通过redolog进行了数据恢复,但是binlog中缺失了该记录日志,那么从库通过binlog日志去复制的时候就会比这个库要少一个数据修改,导致主从不一致。
- 先写binlog再写redolog为commit状态,如果binlog写入磁盘成功后直接挂了,那么,mysql重启后不能通过redolog来恢复数据,但是从库却能通过binlog多一个数据修改出来,导致主从不一致。
数据恢复阶段会使用到binlog和redo log:
- 在mysql故障重启之后,是怎么进行数据恢复的呢?在它的日志系统中,binlog和redolog中有一个公共字段XID是对应起来的,首先要看redo log中有没有prepare,有prepare就会直接去binlog中找,而binlog中有对应的日志则继续提交,没有数据则回滚。
binlog和redolog的组提交:
如果数据的并发量较高的话,即使是顺序的写入磁盘,频繁的IO操作会使得服务器的性能大幅下降。为了提高磁盘的写入效率,可以把多个日志攒到一起再写入磁盘,这就是组提交。
而由于使用了binlog和redo log,所以这两个日志是都要使用组提交的,不然没使用组提交的哪一个一样会成为瓶颈所在。
这个组提交和之前的双一设置我感觉应该是不冲突的,每次写binlog或者redo log的时候都会等待多个事务一起过来成组的写入,依然是符合双一设置的,不过就是事务还会有一个等待的过程。
它主要有三个阶段:
1.flush阶段
2.sync阶段
3.commit阶段
它的每个阶段都分别有一个队列和一把锁,他们的流程如下:
flush阶段:
- 不断的有事务加入到flush队列。
- 将所有事务中redolog中prepare阶段的数据刷盘磁盘
- 生成binlog数据并写入文件,但是这里不是直接写入到磁盘,只是写入缓冲系统
sync阶段:
- flush队列中的事务不断的加入到sync队列
- 将flush队列中所有事务的binlog进行刷盘
- 这里有两个参数可以控制sync阶段队列的事务数量
- 一个是binlog_group_commit_sync_delay参数,意思就是在间隔这个参数值的 us之后,该队列就会停止接收事务,直接开始刷盘。
- 二是binlog_group_commit_sync_no_delay_count参数,意思就是如果队列中的事务数量到达了这个参数值的个数后,即使间隔时间没有到达第一个参数那么长的时间,也能直接停止接收事务,直接开始刷盘。
- sync阶段适当的调整这些参数也是可以提升一定的效率的。适当的增大这两个参数的值,那么每次在队列中的事务也越多,集中起来刷入磁盘,性能就会更高。但是增大这两个参数会导致事务在这里等待的时间更长,所以平均处理的时间也会变长,影响mysql的TPS。
commit阶段:
- sync队列中的事务加入到commit队列中
- 将队列中的所有事务提交,修改可见。
上面是关于mysql事务的提交及binlog、redolog原理的讨论
一些问题:
1.redo log写入磁盘的时机?
redo log是在commit提交的时候才会将其持久化到磁盘,而在prepare阶段也会写入redo log文件一些记录,在网上看到了大量的文章,有些提到prepare阶段是只是写入到文件,有些提到是写入到磁盘。
那么我就说说我的看法把,我认为redo log在二阶段的commit阶段之前就会被持久化到磁盘,而这个持久化就是 prepare阶段做的事情,并且在commit阶段写入redo log时并不会直接写入磁盘,而是以异步的方式写入磁盘。为什么呢?(注意我们这里说的commit阶段说的是二阶段提交中的commit阶段,要和事务的commit提交区分开来,事务的提交就是用的二阶段提交)
在二阶段提交和mysql数据恢复里面说过,也用反证法说明了为什么需要二阶段提交,因为不采用二阶段提交不能保证数据的安全性,采用二阶段提交,即使是prepare完成后,无论 binlog有没有持久化就宕机了,依然可以正确提交或者回滚数据,但是如果prepare阶段并没有持久化redo log,而binlog却被持久化了,只有binlog没有redo log,那么二阶段提交就没有意义了。有binlog存在磁盘中,磁盘中就一定有对应的redo log来恢复数据,继续提交,否则就会导致binlog和实际数据的不一致。
Mysql的隔离级别以及mvcc的原理
mysql一共有四种隔离级别和三种并发读的异常,下面介绍一下:
脏读 | 不可重复读 | 幻读 | |
---|---|---|---|
未提交读 | √ | √ | √ |
已提交读 | × | √ | √ |
可重复读 | × | × | √ |
串行化 | × | × | × |
1.脏读:指的是一个事务读到了另外一个事务未提交的数据,而那个事务可能会将该数据回滚,就会导致该数据不存在,这就是脏读。
2.不可重复读:指的是一个事务的前后几次读同一个数据得到了不同的结果。原因是在两次读中间有其他的事务提交了对该数据的更新或者删除操作。
3.幻读:当一个事务在读取一个范围的记录的时候,另外一个事务又在这个范围插入了新的记录,导致这个事务再次读取这个范围的记录的时候,就会产生幻行。
然后介绍一下四种隔离级别:
1.未提交读:这其实就是毫无隔离可言,即使是一个事务中没有提交的数据,一样能在别的事务中可见,所以上面那三种并发 问题都会发生。
2.已提交读:这个隔离级别可以保证的是一个事务已经提交的数据,才能被其他的事务可见。
3.可重复读:能够保证在一个事务内重复对某一数据的查询一致是同样的记录,即使这个数据行被其他的事务修改了。但是对于其他事务新增数据行,这边有可能会产生幻读。
4.串行化:对同一行记录,写操作会加写锁,读操作会加读锁,这样的安全系数是最高的,但是这也导致Mysql的并发处理能力太低,性能太差。
上面的已提交读和可重复读就是使用MVCC来实现的。
那么我们就来介绍一下MVCC吧
MVCC的全称是Multi Version Concurrency Control ,即多版本并发控制,在Mysql Innodb中,就是使用这个来实现已提交读和可重复读的隔离级别的。因为使用悲观锁来避免并发问题的话,会对性能产生一定的影响,导致其可支撑的并发量不高,那么为了提高性能,就需要一种不加锁的方式来实现并发控制,而MVCC就是这样的一种机制。
MVCC的原理
在MySQL中,MVCC实现已提交读和可重复读是通过undo log日志版本链和快照(read view)来实现的。read view中有以下内容:
- trx_ids,就是创建read view时还未提交的活跃事务id集合。
- low_limit_id,在创建read view时出现过的最大事务id+1。
- up_limit_id,活跃事务列表trx_ids中的最小id值
- creator_trx_id,创建read view的事务ID,也就是自己的事务ID
而在MySQL数据行的记录中,还隐藏着两列数据:
- roll_pointer,它是指向能回滚上个版本数据的undo log的指针。
- trx_id,它是用来记录这个数据行最近是被哪个id的事务所更新的。
当我们开启一个事务去查询某些数据的时候,就会创建一个read view快照,然后查到符合where条件的记录的时候,就会通过roll_pointer访问日志版本链,下面是它对可见性判断的流程:
- 判断这个数据行是否 trx_id<up_limit_id 或者 trx_id == creator_trx_id?由于事务id是随时间递增的,那么如果 trx_id< up_limit_id,就说明这个数据是之前已经被提交的,那么这个数据行就对该事务可见,如果trx_id ==creator_trx_id,那么就说明这个数据行就是被自己这个事务所修改的,自己对自己当然要可见。
- 上面不满足则再判断是否trx_id >= low_limit_id,满足条件则说明这个数据行是在当前事务创建后,又新建了事务把这个数据行给更新的,那就不应该对该事务可见。
- 如果上面条件都不满足,那么判断 trx_id 在trx_ids集合中,如果在这个集合中,就说明这个数据行最近是被创建快照时还未提交的活跃事务所修改的,那也不对当前事务可见。如果不在集合中那就说明这个数据行是被创建快照时已经被提交的事务所修改的,所以对该事务可见。
需要注意的是,对于可重复读级别来说,readView快照是在第一次进行select操作时生成的,而如果后面发生了增删改等操作,又会基于当前读来生成一份新的快照,依据这个新的快照来进行版本可见性的管理。
而对于已提交读来说,它的每次读取操作都会生成一个新的快照来进行版本可见性管理,因此一旦有其他事务提交了对该事物的一些更改,根据最新的快照就能够顺利的读取到这些变更。
Mysql幻读的理解?mvcc到底能不能解决幻读?
首先是对幻读的理解:当一个事务在读取一个范围的记录的时候,另外一个事务又在这个范围插入了新的记录,导致这个事务再次读取这个范围的记录的时候,就会产生幻行。
mvcc可以解决部分幻读问题,在当前读的情况下还是可能会出现幻读问题。
通过上面的描述,加之对mvcc的理解,一般情况下,在只使用快照读的时候,mvcc利用可重复读的快照机制还是可以保证不会出现幻读的。
在使用update/delete/insert的时候,mvcc会使用当前读的机制,意思就是这三条命令一定是基于数据库中的最新的数据进行操作的
,比如事务A查询某个范围的记录,事务B插入一条记录并提交,事务A也在这里插入一条同样id的记录,那么由于事务A在插入前会执行当前读,插入数据重复报错,这里不能插入重复数据是正常的,我觉得不能算是幻读的范畴。所以我并不认为这三个命令基于当前最新的数据进行修改就是幻读(其实我觉得还是理解原理就行了,幻读只是一个人为定义的名词,看你自己对幻读的定义,只要知道目前操作会产生的后果,是不是符合幻读的定义也就没那么重要了)。
我举个我认为是幻读的例子:
事务A | 事务B | |
---|---|---|
时间1 | select * from table where id between 1 and 10; | |
时间2 | insert into table values(1,15); | |
时间3 | update table set age=10 where id between 1 and 10; | |
时间4 | select * from table where id between 1 and 10; |
在上面的例子中:
- 事务A先读取表中id为1到10的记录,没有发现id为5的记录。
- 然后事务B开始在表中插入id为5的记录并提交。
- 事务A再次通过条件把刚才新插入的行修改了,并且把这一行的事务id改为自己的事务id,这样的话,由mvcc对事务id的判断机制看,这一行的数据就会对自己可见了。
- 再次使用select查询一样能查到id为5的记录 。这才是幻读。
那么在Mysql中如何去解决当前读的时候出现幻读的情况呢?
我们可以利用MVCC+锁来解决当前读下的幻读问题。
先看看这两种锁的类型:
- 读锁:是一种共享锁,一个事务持有读锁时,不会阻塞其它的读锁,其他事务都可以对该数据进行读取。
- 写锁:是一种排他锁,一个锁持有写锁会阻塞其他的写锁和读锁。
再看看innoDB支持三种行锁定方式:
- 记录锁(Record Lock):锁直接加在索引记录上面(无索引项时演变成表锁)。
- 间隙锁(Gap Lock):锁定索引记录间隙,确保索引记录的间隙不变。间隙锁是针对事务隔离级别为可重复读或以上级别的。
- Next-Key Lock :行锁和间隙锁组合起来就是 Next-Key Lock。
而在MySQL的默认隔离级别可重复读级别下,
- 通过主键或者唯一索引查询某一条数据行会使用Record Lock。
- 通过主键或者唯一索引查询某一范围数据行会使用Next-Key Lock。
- 通过普通索引查询,都会使用Next-Key Lock。
- 不通过索引查询,那就是使用表锁了。
那么我们是怎么利用行锁来解决幻读的呢?
在上面的例子中我们,在事务A刚开始的时候就使用select * from table where id between 1 and 10 for update;来给这一段范围的数据行加上Next-Key Lock,那么事务B想要去修改它的时候,就会被阻塞,直到事务A被提交释放该锁为止。这样就能够保证在事务的处理期间,其他的事务不会对这个事务锁住的数据行修改,从而导致幻读了。(这里要注意的是,在Innodb中,修改数据行的操作时会默认加上写锁的,所以事务B的写操作才会被阻塞)