MySQL事务学习

Table of contents

  1. 1. MySQL事务学习
    1. 1.1. 事务ACID
    2. 1.2. 事务隔离级别
      1. 1.2.1. 并行的事务会有什么问题?
        1. 1.2.1.1. 脏读
        2. 1.2.1.2. 不可重复读
        3. 1.2.1.3. 幻读
      2. 1.2.2. 有哪些事务的隔离级别?
        1. 1.2.2.1. 四种隔离级别的实现方式
      3. 1.2.3. Read View在MVCC是如何工作的?
        1. 1.2.3.1. MVCC

MySQL事务学习

事务ACID

aba

事务隔离级别

参考文章

事务并发会 产生什么问题?

事务隔离级别有哪些?怎么实现的?

读已提交和可重复度有什么区别?

MVCC是什么?解决了什么问题?实现原理是什么?

可重复读隔离级别彻底解决幻读了吗?

事务的四个特性,以及InnoDB引擎的实现方式:

  • 原子性
    • 通过undo log(回滚日志)来保证
  • 一致性
    • 通过持久性+原子性+隔离性来保证
  • 持久性
    • 通过redo log(重做日志)来保证
  • 隔离性
    • 通过MVCC(多版本并发控制)或锁机制来保证

并行的事务会有什么问题?

MySQL在同时处理多个事务时,可能会出现脏读、不可重复读、幻读的问题

脏读

一个事务读取到了另一个未提交的事务所修改的数据,就意味着发生了脏读

🌰:事务A读取并修改账户余额为200w,但没有立刻提交事务。在这一期间,事务B读取到账户余额为200w,但是呢,由于事务A还未正式提交,仍有可能发生回滚操作,如果事务A发生了回滚,那么事务B读到的数据就是一个没用的、过期的数据,这种现象就是脏读

image-20240921203626317

不可重复读

在一个事务内多次读取同一个数据,如果前后两次读取结果不同,就意味着发生了不可重复读

🌰:事务A有两次读取余额操作,好巧不巧🤪在这两次读取操作之间,事务B快速的修改了余额并进行了提交。结果就是,事务A第一次读取到的是原始数据,而第二次读取到的却是事务B修改过后的数据,两次数据不一致!这种现象就是不可重复读

image-20240921205212723

幻读

在一个事务内多次查询符合条件的记录数量,如果出现前后查询结果不同,这就说明发生了幻读

又是🌰:事务A查询完余额大于100w的记录数量为5条后,事务A打算歇一会😴。在事务A歇息的过程中,事务B悄悄的插入了一条余额大于100w的记录,导致现在共有6条记录符合查询条件。当事务A醒来再次进行查询时,就会惊讶地发现多了一条记录,那么它肯定会认为是自己睡觉睡出幻觉了🤪,这种现象就是幻读

image-20240921205706281

有哪些事务的隔离级别?

由前文可知,多个事务并行处理时可以会出现脏读、不可重复读、幻读现象,这三种现象按严重性排序如下:

脏读 > 不可重复读 > 幻读

针对上述三种现象出现与否,SQL标准提出了四种事务隔离级别,事务隔离级别越高,性能越差🙃,按照隔离级别从低到高一次是:

  • 读未提交:指一个事务还未提交,它所做的数据变更就能被其他事务看到
    • 可能发生脏读、不可重复读、幻读
  • 读提交:指一个事务只有在提交后,它所做的数据变更才能被其他事务看到
    • 可能发生不可重复读、幻读
  • 可重复读:指一个事务执行过程中所看到的数据在事务开始时就被确定(类似于快照📷),是InnoDB引擎的默认事务隔离级别
    • 可能发生幻读
  • 串行化:会对记录加上读写锁,当多个事务发生读写冲突时,后访问数据的事务必须等待前一个事务完成才能继续执行
    • 木有问题,但是性能很差🤪

⚠️:MySQL的InnoDB引擎的默认事务隔离级别是「可重复读」,但它很大程度上避免了幻读现象,解决方案有两种:

  • 针对快照读(普通select语句),通过MVCC方式解决幻读。因为事务执行过程中的数据在事务开始时就已经确定了,即使在A事务执行中程B事务插入了一条数据,这也对A事务是不可见的、透明的,所以就很好的避免了幻读现象
  • 针对当前读(select … for update语句),通过next-key lock(记录锁+间隙锁)方式解决了幻读。因为当A事务执行select ... for update语句时,会加上next-key lock,若事务B在锁的范围内插入了一条数据,那么事务B的插入语句会被阻塞,无法插入成功,所以就很好地解决了幻读问题

四种隔离级别的实现方式

  • 读未提交
    • 直接读就好了
  • 串行化
    • 加读写锁避免并行访问
  • 读提交可重复读都是通过Read View来实现的,区别在于创建Read View的时机
    • 读提交在每一个语句执行之前都会生成一个Read View
    • 可重复读只在启动事务时生成一个Read View,然后整个事务执行过程中都用这一个Read View

🌟区分「开启事务」和「启动事务」

在MySQL中有两种方式开启事务

# 第一种
begin/start transaction

# 第二种
start transaction with consistent snapshot

以第一种方式开启事务不代表启动了一个事务,只有执行了第一条select语句,才是真正启动事务

第二种方式在开启事务的同时也启动了事务(毕竟命令中都带有snapshot😂)

Read View在MVCC是如何工作的?

两个前置知识

  • Read View中四个字段作用

  • 聚簇索引记录中两个跟事务有关的两个隐藏字段

Read View实质是由四个字段构成的

image-20240923152532491

  • m_ids:指在创建Read View时,当前数据库中「活跃事务」的事务id,可能有多个
    • 活跃事务启动了但是还没有提交的事务
  • min_trx_id:活跃事务中最小的事务id
  • max_trx_id数据库下一个将分配给事务的id,即全局事务id的最大值+1
  • creator_trx_id:创建当前Read View的事务id

了解完Read View中的四个字段后,再看看聚簇索引记录中的两个隐藏字段,🌰:

image-20240923153259596

  • trx_id:当一个事务对某条聚簇索引的数据进行改动时,就把该事务的id放入trx_id字段中
  • roll_pointer:当对某条聚簇索引记录进行修改时,都会把旧版本的数据记录在undo日志中,roll_pointer就指向每一个旧版本的数据,因此可以通过roll_pointer隐藏列找到历史记录

MVCC

在了解Read View四个字段以及聚簇索引记录的两个隐藏列后,接下来就可以弄清楚MVCC是如何控制数据快照的

假设现在有一个「活跃事务」事务A,那么可以根据事务A的Read View创建时刻将其他事务划分为四类:

  • 事务id小于min_trx_id
  • 事务id大于等于max_trx_id
  • 事务id在min_trx_idmax_trx_id之间且位于m_ids
  • 事务id在min_trx_idmax_trx_id之间且不位于m_ids

image-20240923154216650

  • 如果记录的隐藏列trx_id值小于min_trx_id,那么这个版本的记录是在Read View创建之前完成的,所以该版本记录是对当前事务可见的
  • 如果trx_id值大于等于max_trx_id,说明这个版本的记录是在Read View创建之后生成的,故对于当前事务是不可见的
  • 如果trx_id值在min_trx_idmax_trx_id之间,则需要进一步判断记录的trx_id是否在m_ids列表中(即对于的事务是否是活跃事务)
    • 是活跃事务,那么该该版本的记录对当前事务不可见
    • 不是活跃事务,那么该版本的记录是可见的
    • 🌰:创建了1、2、3、4这四个事务,其中事务3以迅雷不及掩耳之势提交了,那么随后启动的事务5是可以看见事务3所修改的记录版本的

这种通过「版本链」来控制并发事务不冲突地访问同一条记录的操作就叫MVCC(多版本并发控制)