(一)并发篇:详解MySQL的事务和MVCC工作机制

详解MySQL的事务和MVCC原理

  1. 什么是事务?事务带来什么问题?如何解决?

  2. MVCC是什么?它的原理是什么?用它解决了什么问题?

事务是什么?

事务 是我们学习MySQL时,永远绕不开的话题。我们知道,当一个系统多线程运行 时,并发 带来的问题永远是最主要考虑解决的。因此,而MySQL用来解决并发问题的关键词即为事务

事务主要将围绕以下几个维度展开,如图:

什么是事务?

事务的基本概念 要先了解,我个人的理解是:要么有始有终,要么其实都没有

有一场景,用户A需要向用户B转账1000块钱 ,我们将步骤细分为四步,如下:

  • 从数据库读取用户A的余额
  • 用户A的余额扣1000元 (如果余额>1000),将扣完的余额更新到数据库
  • 从数据库读取用户B的余额
  • 将用户B的余额加1000元 ,将增加后的余额更新到数据库

试想?如果在第二步,用户A的余额扣完后,服务器断电了 ,这1000元不是直接不翼而飞了?这换做谁都无法接受吧!

因此,便有了事务这一概念 。MySQL通过事务 来解决这一问题。在原先转账操作的基础 上,在转账前后分别开启事务和提交事务

如果开启事务后,中途发生意外(报错了,或是服务器断电),直接将 "这件事" 退回 到事务开启前的状态。如果顺利执行完,就将事务提交,完毕!

总结来说,就是在一开始做事的时候,有个标记在初始位置,如果发现情况不对,赶紧跑回去。 如果一切顺利,就不用管了。

事务四大特性总结

事务肯定是有特点的嘛,分别是原子性,持久性,隔离性,一致性。下面我们分别展开说说!

原子性

如何理解原子性? 我个人的理解是:原子是世界上最小的单位 ,肯定是无法再做切割 的。整个事务可以看成是一个原子 ,无法将事务再拆分开 。因此一次事务内的所有操作 ,肯定是要么全部成功 ,要么直接挫骨扬灰(全部失败)。

事务的原子性由 undo log回滚日志来保证(后续会展开说)

持久性

如何理解持久性?上文提到的转账操作,如果事务提交了,还可以修改,或者是回滚 ,那问题可就麻烦了。那需要事务来做什么?因此,事务的持久性代表了:提交的事务是不可被修改的状态 ,已经彻底保存了下来

事务的持久性由 redo log回滚日志来保证(后续会展开说)

隔离性

如何理解隔离性?如果同时有多个事务在运行 ,事务之间可以互相看到对方的事务 ,并修改对方事务内的信息 ,那事务就会彻底乱套啦。因此事务的隔离性代表:未提交的 事务之间相互不可见不可修改对方事务内的信息 ,保证数据一致性

事务的持久性由 MVCC机制和上锁 来保证(持久性是锁篇的重点

一致性

如何理解一致性?事务的一致性表示,事务的操作前后的数据需要保持完整性约束 。(例如,上述例子提到的1000块转账钱只是从A账户跑到了B账户 ,并没有凭空消失,这就是数据的完整性 )由原子性+持久性+一致性共同守护。

事务的并发问题

如果整个世界只允许单线程的细胞生物存在就好了,哈哈哈!上到各门编程语言,下到操作系统,并发的问题总是会被提到,MySQL的事务也不例外 。肯定是允许多事务并发的,也会带来一些并发问题,总结如下:

脏读

如何理解脏读?假设有两个正在运行的事务,事务A能读取到事务B的未提交的数据 ,表明发生了脏读。这个问题就很严重啦!我画了个图举例,如下:

如果发生了脏读 ,那就表明在t5时刻查到的余额V1为2000,如果这时候是有两个人在做交易:

  • B在t5时刻给A看完余额记录
  • 随后在t6时刻 ,再把事务不是提交,而是直接回滚
  • 在t7时刻 ,查询到的余额V2为1000。这样就涉及到诈骗了!

脏读是绝对不允许发生的!

不可重复读

如何理解不可重复读?不可重复读是在解决了脏读的基础上 ,存在的其它问题。具体为:一个正在活跃的事务,在不同时刻查询到的数据不一致。,还是同样的图:

如果发生了不可重复读,即:

  • 在t4时刻,事务B将余额修改为2000,没有提交
  • 在t5时刻,事务A查到的余额V1为1000,没有出现脏读现象
  • 在t6时刻,事务B把事务提交了
  • 在t7时刻,事务A再次查询余额,此时读到其他提交的事务的值 ,查到的余额V2为2000 ,出现了不可重复读问题

幻读

如何理解幻读?在处理了脏读和不可重复读 后,幻读的具体为:一个正在活跃的事务,在不同时刻查询到的数据的数量不一致 。幻读更多关注的是数量,多次查询某个条件的数量,出现数量不一致的情况,如图:

如果发生了幻读,即:

  • 在t2时刻,查询到符合条件的用户数量为5
  • 在t4时刻,新启动的事务B将插入一条新用户,并随后提交
  • 在t6时刻,事务A再次执行相同的sql,此时查询到符合条件的用户数量为6 ,和一开始的数量对不上,出现了幻读问题

总结下事务的并发问题:

  1. 严重性考虑,脏读 > 不可重复读 > 幻读
  2. 从性质分类,脏读:读到其他事务未提交 的数据;不可重复读:前后读取的数据 不一致;幻读:前后读取的数据数量不一致

事务的隔离级别

既然让事务并发操作,会带来这么多问题,那MySQL就不可能不处理 这些问题,让这些问题破坏我们的数据 。针对这不同的问题,MySQL同样有多种隔离级别来应对。分别是:读未提交,读提交,重复读,串行化

从安全性考虑

从会产生的问题考虑

  1. 读未提交 = 安全裸奔,什么安全性都不顾,只要速度!
  2. 读提交 ,可以解决脏读问题,但仍然存在不可重复读和幻读。是Oracle的默认隔离级别
  3. 重复读 ,可以解决脏读,不可重复读,绝大多数幻读(个例除外),是MySQL的默认隔离级别
  4. 串行化 ,不会存在并发问题,直接让事务变成读写互斥操作,一劳永逸,但性能最低

不同隔离级别下读到的数据

  1. 如果隔离级别为读未提交事务可以读到未提交的事务的数据 ,因此V1为2000,V2为2000
  2. 如果隔离级别为读提交 ,可解决脏读,但会出现不可重复读问题,因此V1为1000,V2为2000
  3. 如果隔离级别为读提交 ,可解决绝大多数并发问题,因此V1为1000,V2也为1000
  4. 如果隔离级别为串行化 ,在事务A执行读操作后,没提交事务;事务B执行写操作时,会发生阻塞,直到提交

隔离级别的原理

我们在写sql时,执行单条insert, update, delete语句 时,MySQL会默认开启事务(在sql的前后默认加上begin(开启事务), commmit(提交事务))。

而执行关键的查询select sql时,有两种写法:

  • 普通的select ... 语句(快照读 ),会通过MVCC的方式来解决事务的并发问题
  • select ... for update(当前读 ),会通过直接给数据加锁的方式来解决事务的并发问题

而根据不同的隔离级别,MVCC和上锁的粒度也有所不同,我会在后续展开详细介绍


MVCC是如何解决事务并发问题的?

在上章节我们提到,普通的select 语句是通过MVCC 的方式来解决事务的并发问题,MVCC(多版本并发控制) 是我们这次介绍的重点,具体从以下三部分展开:

什么是MVCC?

在介绍MVCC之前,我们需要了解几个基础的概念。

一. 普通的MySQL记录,还有几个我们看不到的隐藏字段

  • trx_id: 最近的操作这条数据的事务ID ;(对一条记录做增删改 时,会在这条记录上留下"我是谁"的印记 ,即事务ID

  • roll_pointer: 回滚指针,指向该条记录的上一个版本 :(一条数据修改后旧记录并不是被直接抹去 ,而是有新纪录的指针会指向它)

  • row_id: 虚拟字段,若表中没有主键,起到代替主键的作用

举个例子,数据的变更图如下。

在t1时刻,由id为50的事务创建了id = 1的记录 ,此刻他的trx_id为50 ,由于是新记录roll_pointer为NULL。

在t2时刻,由id为51的事务修改了id = 1的记录 ,将age从20改成18。等于这条记录的版本被更新了 。当前最新版本记录的trx_id为51 ,而roll_pointer则会指向当前记录的上一个版本。

二. Read View (视图),MVCCd的重要组成 ,可以理解为数据的快照相机的快门每生成一个Read View时(按一下相机快门),总会有一些记录(图片)被保存下来。以下是Read View的关键内容:

  • create_trx_id:创建这个read view的事务ID
  • m_ids: 创建这个read view时,当前还活跃的事务ID集合(事务是允许并发的嘛!)
  • min_trx_id: 当前还活跃的事务最小ID ,即m_ids集合中的最小值
  • max_trx_id: 下一个即将创建的事务的ID 。相当于是一种预分配 ,提前告诉这个read view会有哪个事务会来,但是现在还没来!

通过上述的基本概念,我们就可以系统性的学习MVCC是如何工作的,read view, rtx_id, roll_pointer...

如何用MVCC来判定当前的数据是否可见(MVCC工作原理)?

如何理解数据是否可见这句话? 其实就是在查询数据时查到了本不应该让他看到的数据 ,才导致的并发问题。比如说:脏读:查到了其他事务还没提交的数据不可重复读和幻读:自己的数据还没提交,查到了其他事务提交的数据

MVCC是如何利用Read View来判断数据是否可见的?

当前我开启了一个事务,其中有查询语句,并且手里有一个read view,判别步骤如下:

  1. 获取查询到的符合筛选条件的行数据 ,将当前行数据的最新版本的trx_id取出来
  2. 如果trx_id == create_trx_id , trx_id和read view中的create_trx_id相同,表明这条记录的当前版本我这个事务创建的可见,直接返回当前的记录
  3. 如果trx_id < min_trx_id ,trx_id小于当前活跃事务的最小id,表明这条记录的当前版本已经被提交了(事务的id是持续递增的 ),可见,直接返回当前的记录
  4. 如果trx_id ∈ m_ids , trx_id包含在read view中的m_ids中,表明这条记录的当前版本还是活跃 的,如果可见,那就直接发生脏读了,肯定不可见因此MVCC天生就会避免脏读,继续判断
  5. 如果trx_id >= max_trx_id ,trx_id大于等于read view中的max_trx_id,表明这条记录的当前版本对于这个read view来说,还未创建 !肯定不可见,继续判断
  6. 如果没有找到可见记录,则通过隐藏列中的回滚指针 ,找到这条记录的上一个版本重复步骤2-5,直到找到为止。

判别流程图可以总结为如下:

可以看下伪代码

ini 复制代码
row_data = select ...;

while (row_data != null){
    if (row_data.trx_id == read_view.create_trx_id) {
        break;
    }
    if (row_data.trx_id < read_view.min_trx_id) {
        break;
    }
    if (read_view.m_ids contains row_data.trx_id) {
        row_data = row_data.poll_pointer;
        continue;
    }
    if (row_data.trx_id >= read_view.max_trx_id) {
        row_data = row_data.poll_pointer;
        continue;
    }
}
return row_data;

不同隔离级别下的MVCC是如何工作的?

读未提交 不谈(这玩意没有任何防御措施),重点是读提交和重复读两种隔离机制 ,MVCC开启Read View的时机不同,因此安全性也不同

对于重复读隔离级别,整个事务的运行阶段,只会创建一个Read View ,事务内执行的select语句共用 一个read view。(从一而终

对于读提交隔离级别,整个事务的运行阶段,只要执行一次select语句 ,就会马上创建一个Read View ,然后用这个新的Read View做可见性判断。(始乱终弃

那在重复读的隔离级别下,创建唯一的Read View的时机

  1. 如果是通过begin 开启的事务,只有在第一次执行select语句的时候才会创建Read View(类似懒加载的机制)
  2. 如果是通过start transiction with consistent snapshot 开启的事务,在创建的时候马上就会创建Read View(类似饿汉式加载机制)

读提交下的MVCC

举个例子,下面是两个事务在不同时刻对某条相同记录执行的CRUD操作

一. 在T5时刻,事务B对余额要做一次查询操作,此时会创建一个Read View,如下:

根据判断规则,余额的最新记录为2000 ,这是由id为50 的记录创建的,该read_view的活跃事务id集合为[50, 51]50的事务对于当前的read_view是不可见 的。因此,通过回滚指针 找到上一条版本记录 ,即余额为1000 ,而余额为1000的记录则是由id < 50的事务创建 的,小于min_trx_id。因此,该版本记录对于这个read_view是可见的,余额V1=1000

二. 在T7时刻,事务B再次 对余额做一次查询操作,由于是读提交隔离级别,因此会创建新的Read View,如下:

根据判断规则,余额的最新记录为2000 ,这是由id为50 的记录创建的,该read_view的最小活跃事务ID为51 ,小于min_trx_id。因此,该版本记录对于这个read_view是可见的,余额V2=2000 。一个事务内前后时刻读取的数据不一致,出现了不可重复读的并发问题

重复读下的MVCC

还是相同的例子,隔离级别升级为重复读

一. 在T5时刻,事务B对余额要做一次查询操作,此时会创建一个Read View,如下:

根据判断规则,余额的最新记录为2000 ,这是由id为50 的记录创建的,该read_view的活跃事务id集合为[50, 51]50的事务对于当前的read_view是不可见 的。因此,通过回滚指针 找到上一条版本记录 ,即余额为1000 ,而余额为1000的记录则是由id < 50的事务创建 的,小于min_trx_id。因此,该版本记录对于这个read_view是可见的,余额V1=1000

二. 在T7时刻,事务B再次 对余额做一次查询操作,由于是重复读隔离级别,因此不会创建新的Read View复用 T5时刻创建的Read View。因此,时刻T7的判断和T5相同,余额V2仍然为1000不可重复读 的问题解决。从理论上来说,一个事务内,对任何查询都沿用同一个read_view ,在这个期间无法看到其他事务插入的数据 ,也能避免幻读问题(除个例)。

MVCC真能完全解决幻读嘛?

毫无疑问,在重复读隔离级别下,对于普通的幻读MVCC是可以避开的,如下图:

t3时刻 再次执行t2时刻 的select语句,由于t3时刻使用的read view的和t1时刻的一样 。因此,在t2时刻,执行的insert语句对于该read_view是不可见的 , 因此select语句的结果仍然可以和t1时刻的保持一致

但如果是本事务对其他事务insert的数据修改呢?

在讲解之前,需要科普一下快照读和当前读的概念

  • 快照读:读的是数据的版本 ,(读取到的不一定是最新的版本,因为有可能不可见)。普通的select ... 用的就是快照读
  • 当前读:读的是一定是数据的最新版本 。执行insert, update, delete 用的都是当前读(需要知道操作数据的最新值,才能操作); 还有特殊的 select ... for update语句。

特例一:两次select间穿插update

存在一张user表,该表里面没有数据,存在字段id, age, mal5。

一. 在T1时刻,A开启事务,执行select语句,由于表没有数据 ,因此查询结果为NULL

二. 在T2时刻,B事务插入了一条id=10的数据,并马上提交事务

三. 在T3时刻,事务A执行一条update 语句,需要对id = 10的数据做更新 ,此时采用的是当前读 ,由于数据库有一条已经提交了的id = 10的数据 ,因此能读到,并修改。此时,id=10这条记录的最新版本的trx_id为事务A

四.在T4时刻,事务A执行同样的select语句,此时对于id=10这条记录来说,最新版本的记录的trx_id为事务A ,正好满足条件:read_view.create_trx_id == trx_id 。结果为可见,尴尬的幻读问题出现了。

特例二:两次select; 一次select, 一次select...for update

还是同样的表,执行顺序变为如下:

一. 在T1时刻,A开启事务,执行select语句,由于表没有数据 ,因此查询结果为NULL

二. 在T2时刻,B事务插入了一条id=10的数据,并马上提交事务

三. 在T3时刻,事务A执行的是select ... for update ,不同于普通的select,为当前读 ,一定会读取数据最新的版本 ,事务B提交的记录对其是可见 的,尴尬的幻读问题又出现了!

思考题,为何我的数据修改了不生效呢?

看下图,开启事务后,执行第一次select语句的时候,我们可以看到表里的数据,所有的id值均等于age值 ,随后执行了一条update语句 ,再次执行同样的 select语句,为什么数据没有改变? 什么样的场景能复现该操作?

其实问题的关键在于:执行了update语句,受到影响的行数居然为0数据必然是提前被其他事务修改了。答案如下:

一. 在T1时刻,A开启事务,执行select语句,正常返回数据,可以看到所有的id均等于age

二. 在T2时刻,B事务将表里的age全部+1,并马上提交事务

三. 在T3时刻,事务A执行的是update语句 ,将id == age 的数据中的age都修改为0 。此刻会进行当前读 ,读取最新数据。由于最新数据的age全被+1 ,已经不满足id == age 这个条件,因此受影响的行数为0

四. 在T4时刻,事务A再次执行的是普通的select语句 ,为快照读 。这时候会从数据的最新版本开始查找,事务A此时用的read_view是在T1时刻创建的,(此时事务B操作的sql均不可见,trx_id >= read_view.max_trx_id ,对于当时的事务A 来说,事务B还没创建 ,所以肯定是读不到的)。因此会通过行的roll_pointer查找到上个版本的数据 ,因此查找的数据仍和T1时刻相同!!


总结下MVCC的工作原理

对于事务产生的脏读,不可重复读,幻读问题。对于MVCC来说,活跃的事务数据均是不可见的,因此脏读问题自然解决。

MVCC的读提交隔离级别 ,生成的是语句级别的快照Read View(每次select都会创建),不可重复读,幻读问题仍然存在。

MVCC的重复读隔离级别 ,生成的是事务级别 的快照Read View(一个事务只会有一个),从开始到结束都会保持一致 。即使有别的事务插入和修改数据,对于普通的快照读都是不可见的,因此可解决不可重复读和幻读问题

未完待续...........还会讲MySQL的锁,上锁规则,死锁分析...重量级的还没来

相关推荐
我的ID配享太庙呀1 小时前
Django 科普介绍:从入门到了解其核心魅力
数据库·后端·python·mysql·django·sqlite
不辉放弃2 小时前
kafka的消费者负载均衡机制
数据库·分布式·kafka·负载均衡
拉姆哥的小屋2 小时前
用 Flask 打造宠物店线上平台:从 0 到 1 的全栈开发实践
数据库·oracle·flask
liliangcsdn3 小时前
mac neo4j install & verifcation
数据库·neo4j
Cyanto3 小时前
MyBatis-Plus高效开发实战
java·开发语言·数据库
-XWB-4 小时前
【Oracle】套接字异常(SocketException)背后隐藏的Oracle问题:ORA-03137深度排查与解决之道
数据库·oracle
睿思达DBA_WGX4 小时前
由于主库切换归档路径导致的 Oracle DG 无法同步问题的解决过程
运维·数据库·oracle
fengye2071614 小时前
板凳-------Mysql cookbook学习 (十二--------6)
学习·mysql·adb
!chen4 小时前
Oracle 19.20未知BUG导致oraagent进程内存泄漏
数据库·oracle·bug
DarkAthena5 小时前
【GaussDB】构建一个GaussDB的Docker镜像
数据库·docker·gaussdb