【MySQL】什么是事务?MVCC?
文章目录
- 【MySQL】什么是事务?MVCC?
- 1.事务有哪些特性?
- 2.并行事务引发什么问题?
- 3.事务的隔离级别?
- 4.read view在MVCC里如何工作
- 4.1 read view是什么?
- 4.2 聚簇索引的隐藏列?
- 4.3 MVCC如何判断行记录对某一个事务是否可见?
- 其他问题
- 视频理解
【MySQL】什么是事务?MVCC?
1.事务有哪些特性?
原子性、一致性、隔离性、持久性。
- 原子性(Atomicity):原子性就是事务中的所有操作要么全部成功,要么全部不完成,不会结束在中间某个环节,原子性是由undo log日志保证的。
- 一致性(Consistency):一致性就是事务执行前后,数据库的状态必须保持一致性,一致性是由原子性+隔离性+持久性以及业务保证的。
- 隔离性(Isolation):隔离性是防止多个事务并发读写同一个数据的时候,导致数据不一致问题的发生,是由MVCC和锁保证的。
- 持久性(Durability):持久性是保证事务完成后对数据的修改就是永久的,不会因为系统故障而丢失,持久性是由redo log日志保证的。
上述提到了一致性是由原子性、隔离性、持久性保证的,那么其它特性是由什么保证的?
事务的原子性是通过undo log实现,在事务未提交前,历史数据会记录在undo log中,如果事务执行过程中,出现了错误或用户执行了回滚,MySQL可以利用undo log将数据恢复到事务开启前的状态,从而保证事务原子性。
隔离性是由MVCC和锁保证的。可重复读级别下的快照读是通过MVCC来保证事务隔离性的;当前读是通过行级锁来保证事务隔离性的。
持久性是由redo log保证的,因为MySQL通过WAL(先写日志再写数据)机制,在修改数据的时候,会将本次对数据页的修改以redo log的形式记录下来,这个时候更新就算完成了,Buffer Pool的脏页会通过后台线程刷盘,即使在脏页还没刷盘的时候发生了数据库重启,由于修改操作都记录到了redo log,之前已提交的记录都不会丢失,重启后就通过redo log,恢复脏页数据,从而保证事务的持久性。
2.并行事务引发什么问题?
MySQL服务端允许多个客户端连接,那么就会出现同时处理多个事务的情况。这就会导致一些问题,比如脏读、不可重复读、幻读。
- 脏读:一个事务【读到】了另一个【未提交事务修改过的数据】。
就是说,A开启事务并且更新之后,但是还没有提交事务,这时B开启事务并读到了这个更新的数据,但是A很有可能回滚,那么B得到的数据就是过期的,那么这种现象就是脏读。
- 不可重复读:在一个事务内多次读取同一个数据,如果出现了【前后两次读到的数据不一样】的情况,就是不可重复读现象。(关注的是因为修改造成的不一致情况)
就是说,A启动事务后第一次get到这个数据,然后B事务就启动了,并对其更新后就提交事务,这时A未提交事务并进行了再次读取,结果get到的数据不一样,就是不可重复读。
- 幻读:在一个事务内多次查询某个符合查询条件的【记录数量】,如果出现前后两次查询到的记录数量不一样,就意味发生了幻读。(关注的是因为增删造成的不一致情况)
看着上面的不可重复读和幻读,极其混淆,在我看来,不可重复读主要是因修改造成结果的不同,幻读是因增删造成的记录数不同。
那脏读和幻读的区别是什么?
脏读是一个事务读到了另一个未提交事务修改过的数据,如果另一个事务回滚了,那么刚才读到的数据与数据库的就不一致。幻读是前后两次查询的结果集的数量不同,是因为增删造成的。
总结一下
- 脏读:读到其他事务未提交的数据。(最严重)
- 不可重复读:前后读取的数据不一致。(其次)
- 幻读:前后读取的记录数量不一致。(最低)
3.事务的隔离级别?
我们知道了在并发情况下,多个事务会出现麻烦的问题。为了解决这些问题,SQL标准提出了四种隔离级别来规避这些现象。
MySQL默认隔离级别是可重复读,同时还支持读未提交、读提交、串行化。
- 串行化(serializable):会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务完成,才能继续执行。(避免了所有问题,但是事务并发性能最差)
- 可重复读(repeatable read):指一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的。(避免了脏读和不可重复读,大程度上避免了幻读,没有完全避免)
- 读提交(read committed):指一个事务提交后,它的变更才能被其它事务看到。(避免了脏读问题,但是还存在不可重复读和幻读这两个问题)
- 读未提交(read uncommitted):指一个事务还没提交时,它的变更就能被其它事务看到。(什么也没解决)
MySQL的InnoDB存储引擎的默认隔离级别虽然是可重复读,但是它很大程度避免了幻读,但没有完全解决,虽然串行化可以解决,但是性能太差了,所以就有以下两种方式解决:
- 对于快照读(普通select),是通过MVCC方式解决幻读,因为可重复读隔离级别下,事务执行过程看到的数据,一直跟这个事务启动看到的数据是一致的,即使中途有其他事务插入一条数据,也是查询不出来的,所以就避免了幻读。
- 对于当前读(select … For update等),是通过next-key lock(记录锁+间隙锁)方式解决了幻读,因为当执行select … For update语句的时候,会加上next-key lock,如果有其他事务在next-key lock锁范围内插入一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就避免了幻读问题。
我们已经知道了这四种隔离级别了,那么他们又是如何实现的呢?
- 对于【读未提交】隔离级别的事务,直接读取最新的数据即可。
- 对于【串行化】,通过加读写锁来避免并行访问。
- 对于【可重复读】和【读提交】,它们都是通过read view 实现的,主要区别就是它们的创建时机不同,read view可以看做是一个数据快照。【读提交】是在【每个语句执行前】都会重新生成一个read view,而【可重复读】是【启动事务时(START TRANSACTION 或 BEGIN )】生成一个read view,然后整个事务期间都使用这个read view。
4.read view在MVCC里如何工作
4.1 read view是什么?
Read view中有四个重要的字段:
- creator_trx_id:创建该read view的事务id。
- m_ids:指的是在创建read view时,当前数据库中【活跃事务】的事务id列表,就是启动了但还没提交的事务。
- min_trx_id:指的是在创建了read view时,当前数据库中【活跃事务】中事务id最小的事务,也就是m_ids的最小值。
- max_trx_id:这个不是m_ids的最大值,而是创建read view 时当前数据库中应该给下一个事务的id值,也就是全局事务中最大的事务id+1 。
4.2 聚簇索引的隐藏列?
对于使用InnoDB存储引擎的数据库表,它的聚簇索引都有这两个隐藏列:
- trx_id:当一个事务对某条聚簇索引记录进行改动时,就会把事务的id记录到trx_id列里。
- roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写到undo log日志里,然后这个隐藏列执行每一个旧版本记录,于是就可以通过它找到修改前的记录。
4.3 MVCC如何判断行记录对某一个事务是否可见?
Read view创建好之后,所有记录的trx_id就可以划分为这三种情况。
一个事务去访问记录的时候,除了自己更新的记录总是可见之外,也有以下规则:
- 如果记录的trx_id(隐藏列的) < read view 中的 min_trx_id ,表示这个版本的记录是在创建read view前就已经提交的事务生成的,所以对其可见。
- 如果记录的trx_id >= read view 中的 max_trx_id,表示这个版本的记录是在创建read view后才启动的事务生成的,所以不可见。
- 如果记录的trx_id in 【min_trx_id,max_trx_id】,需要判断trx_id是否在 m_ids 列表中;
- 如果在,表示生成这个版本记录的事务依旧活跃(还未提交事务),所以对其不可见。
- 如果不在,表示生成这个版本记录的事务已经提交,所以对其可见。
最后,以上这种通过【版本链】来控制并发事务访问同一个记录的行为就叫MVCC(多版本并发控制)。
其他问题
1、可重复读是如何实现的?
2、读提交是如何工作的?
3、当前读是如何避免幻读的?
4、可重复读隔离级别为什么不能完全避免幻读?
本篇文章借助以下的博客学习记录,膜拜大佬的一天。
事务隔离级别是怎么实现的?
MySQL 可重复读隔离级别,完全解决幻读了吗?
视频理解
1.脏读:166-读未提交隔离性下的演示_哔哩哔哩_bilibili
2.不可重复读:167-读已提交和可重复读的隔离性下的演示_哔哩哔哩_bilibili
3.幻读:168-幻读的演示与解决方案_哔哩哔哩_bilibili
4.MVCC:
184-MVCC三剑客:隐藏字段、UndoLog版本链、ReadView规则_哔哩哔哩_bilibili
186-MVCC在可重复读下解决幻读的流程_哔哩哔哩_bilibili