MySQL事务:如何保证ACID?MVCC到底如何工作?

文章目录

一、事务的概念

什么是事务?事务应用于那些场景? 简单说,事务就是把数据库里一组相关的操作(比如改数据、删数据的操作)捆成一个"整体",这个整体要么完完全全做完,要么一点都不做,绝不会出现"做了一半卡住"的情况------就像你网购付款,要么钱扣了、订单成了,要么钱没扣、订单也没成,不会有"钱扣了但订单没生成"这种尴尬情况。

举两个例子,特别好理解:

  1. 火车票售票 :本来只剩1张票,要是不搞事务,A查到有票准备卖(还没改数据库),B又查到有票也卖,最后就会把同1张票卖给两个人。但用事务把"查票数+改票数(减1)"捆成一个整体后,要么A的这整套操作全做完(查完直接改,B再查就没票了),要么A操作失败(比如网络断了),票数回到原来的样子,绝不会出现"卖重复"的问题。

  2. 删学生信息 :学校要删一个毕业学生的所有数据,不只是删他的姓名、电话这些基本信息,还要删他的成绩、在校表现、论坛发帖记录。这一堆删数据的操作要是分开做,万一删完基本信息后数据库崩了,就会剩下"没删的成绩"------这就乱了。但用事务把这些删操作捆一起,要么所有信息全删干净,要么数据库崩了之后,之前删的基本信息也会恢复(就像没删过一样),不会留下半截数据

总结:事务 (Transaction) 是数据库中一组不可分割的操作集合,这组操作要么全部成功执行,要么全部失败回滚,最终保证数据从一个一致性状态切换到另一个一致性状态,而不会出现其他未被定义的状态。

二、事务的定义与核心需求

1. CURD需满足的核心属性

  • 原子性(Atomicity):事务中的所有操作要么全做,要么全不做(例如转账时 "扣钱" 和 "加钱" 必须同时成功或同时失败)。
  • 一致性(Consistency):事务执行前后,数据必须满足预设的业务规则(例如转账后总金额不变)。
  • 隔离性(Isolation):多个事务并发执行时,彼此的操作应相互隔离,避免干扰(例如防止 "脏读""不可重复读" 等问题)。
  • 持久性(Durability):事务一旦提交,其结果会永久保存到数据库(即使宕机也不会丢失)。

2. 一致性的理解

一致性的核心定义是"状态的合法跃迁":事务执行的结果,必须让数据库从一个合法的一致性状态,转变为另一个合法的一致性状态。

  • 一致性与 "业务逻辑强相关"
    一致性的 "合法性" 是由用户的业务规则定义的,数据库本身无法凭空判断业务是否合理
    比如:业务规则要求 "用户年龄不能为负数",MySQL 可以通过字段类型(如INT)或约束(CHECK age >= 0)提供技术支持,但 "为什么年龄不能为负" 是业务逻辑决定的,数据库只是工具,最终由用户的业务需求来定义 "什么样的状态是一致的"
  • 技术上通过 AID 保障 C(原子性、隔离性、持久性共同支撑一致性)
    原子性(A):避免事务 "做一半" 导致数据残缺,确保状态跃迁的 "完整性"。
    隔离性(I):避免多事务并发时互相干扰(如脏读、幻读),确保每个事务看到的是 "合法的中间状态"。
    持久性(D):确保事务提交后数据不会丢失,保证 "合法状态的长期有效性"。

三、事务的ACID属性

事务需满足ACID四大属性,确保数据安全与一致性:

属性 定义与说明
原子性(A) 事务中的所有操作要么全完成,要么全不完成;执行出错时会回滚到事务开始前状态,如同未执行过。
一致性(C) 事务开始前和结束后,数据库完整性未被破坏(如数据精度、关联性符合预设规则),需结合业务逻辑与原子性保障。
隔离性(I) 允许多个并发事务读写数据,防止交叉执行导致数据不一致;分为读未提交、读提交、可重复读、串行化4个隔离级别
持久性(D) 事务处理结束后,数据修改永久有效,即使系统故障也不会丢失(如commit后的数据)。

四、事务的版本支持

  • 仅InnoDB引擎支持事务MyISAM、MEMORY、CSV等引擎不支持事务。

  • 查看数据库引擎命令:

    sql 复制代码
    mysql> show engines; -- 表格显示
    mysql> show engines \G -- 行显示(更详细)
  • 关键引擎支持情况:
    • InnoDB:DEFAULT引擎,支持事务、行级锁、外键、事务保存点(Savepoints: YES)。
    • MyISAM:不支持事务(Transactions: NO),适用于只读或低频更新场景。

五、事务的提交方式

1. 两种提交方式

  • 自动提交:默认模式,每条SQL(除select特殊情况)自动封装为事务并提交。
  • 手动提交:需通过commit提交事务,rollback回滚事务。

2. 查看与修改提交方式

  • 查看自动提交状态:

    sql 复制代码
    mysql> show variables like 'autocommit'; -- 结果为ON(自动)/OFF(手动)
  • 修改自动提交模式:

    sql 复制代码
    mysql> SET AUTOCOMMIT=0; -- 关闭自动提交(手动模式)
    mysql> SET AUTOCOMMIT=1; -- 开启自动提交(默认模式)

六、事务的常见操作演示

1. 准备工作

  • 创建测试表(InnoDB引擎):

    sql 复制代码
    create table if not exists account(
      id int primary key, 
      name varchar(50) not null default '', 
      blance decimal(10,2) not null default 0.0
    )ENGINE=InnoDB DEFAULT CHARSET=UTF8;
  • 远程连接与隔离级别设置(示例:设为读未提交用于测试,后续会讲解这一状态):

    sql 复制代码
    mysql> set global transaction isolation level READ UNCOMMITTED; -- 全局设置
    mysql> quit; -- 重启客户端生效
    mysql> select @@transaction_isolation; -- 验证事务隔离级别(结果:READ-UNCOMMITTED)

2. 事务的开始与回滚

sql 复制代码
-- 1. 查看自动提交状态(设为ON)
mysql> show variables like 'autocommit'; -- 结果:ON

-- 2. 开始事务(begin/start transaction均可)
mysql> begin;

-- 3. 创建保存点(用于部分回滚)
mysql> savepoint s1;

-- 4. 插入数据
mysql> insert into account values(1,'小明',59.99); -- 插入1条

mysql> savepoint s2; -- 新建保存点

mysql> insert into account values(2,'小张',69.99); -- 再插入1条

-- 5. 查看数据(2条记录)
mysql> select * from account; -- 结果:小明和小张

-- 6. 回滚到save2(删除第2条记录)
mysql> rollback to s2;
mysql> select * from account; -- 结果:仅小明

-- 7. 全量回滚(删除所有记录)
mysql> rollback;
mysql> select * from account; -- 结果:空集

3. 非正常演示:事务的自动回滚与持久性

演示1:未commit时客户端崩溃,MySQL自动回滚
  • 终端A(未commit):

    sql 复制代码
    mysql> begin;
    mysql> insert into account values (1, '张三', 100); -- 未commit
    mysql> Aborted; -- 按ctrl+\异常终止
  • 终端B(查看结果):

    sql 复制代码
    mysql> select * from account; -- 终端A崩溃前:有张三(100)
    mysql> select * from account; -- 终端A崩溃后:空集(自动回滚)
演示2:commit后客户端崩溃,数据持久化
  • 终端A(已commit):

    sql 复制代码
    mysql> begin;
    mysql> insert into account values(1,'小张',69.99);
    mysql> commit; -- 提交事务
    mysql> Aborted; -- 异常终止
  • 终端B(查看结果):

    sql 复制代码
    mysql> select * from account; -- 结果:小张(69.99)

4. 关键结论

  • 只要用begin/start transaction开启事务,必须通过commit提交才持久化,与autocommit设置无关。
  • 事务可手动rollback,操作异常时MySQL自动回滚。
  • InnoDB中,单条SQL默认封装为事务并自动提交
  • 从上面的例子,我们能看到事务本身的原子性(回滚),持久性(commit)。

七、事务隔离级别

1. 隔离级别的核心作用

解决多事务并发时的问题(如脏读、不可重复读、幻读),不同级别平衡"安全性"与"并发性能"(级别越严,性能越低)。

2. 四种隔离级别详情

(1)读未提交(Read Uncommitted)
  • 特点:所有事务可看到其他事务未提交的结果,无隔离性。
  • 问题:存在脏读(读取未提交的临时数据),一个事务在执行中,读到另一个执行中事务的更新(或其他操作)但是未commit的数据就是脏读。
  • 实际生产中不可能使用这种隔离级别,相当于没有任何隔离性,也会有很多并发问题!
(2)读提交(Read Committed)
  • 特点:事务仅能看到其他事务已提交的结果(大多数数据库默认级别,非MySQL默认),它满足了隔离的简单定义:一个事务只能看到其他的已经提交的事务所做的改变
  • 问题:这种隔离级别会引起不可重复读,即一个事务执行时,如果多次 select, 可能得到不同的结果。
(3)可重复读(Repeatable Read)
  • 特点:这是 MySQL 默认的隔离级别,它确保同一个事务,在执行中,多次读取操作数据时,会看到同样的数据行。(两个事务都进行提交后才可以看到改变的结果)。
  • 优势:解决不可重复读,但可能存在幻读:同一事务内多次执行相同的查询语句,得到的结果集行数不一致,就像 "出现了幻觉" 一样。
  • 可重复读:其他事务对范围外的行执行了 INSERTDELETE 并提交,不可重复读:其他事务对已有行执行了 UPDATE 并提交,这样可以简单区别开可重复读与不可重复读。
  • MySQL通过Next-Key锁解决幻读(其他数据库可能仍有幻读)。
(4)串行化(Serializable)
  • 特点:这是事务的最高隔离级别,它通过强制事务排序,使之不可能相互冲突,在每个读的数据行上面加上共享锁,从而解决了幻读的问题,。
  • 问题:效率极低,易超时和锁竞争,生产环境基本不使用。

3. 隔离级别的查看与设置

  • 查看隔离级别:

    sql 复制代码
    mysql> SELECT @@global.transaction_isolation;  -- 全局
    mysql> SELECT @@session.transaction_isolation;  
    -- 或简写为 SELECT @@transaction_isolation;	   -- 会话级别(当前终端)
    mysql> SELECT @@tx_isolation; -- 默认同会话级别
  • 设置隔离级别:

    sql 复制代码
    -- 设置会话/全局级别,可选值:READ UNCOMMITTED/READ COMMITTED/REPEATABLE READ/SERIALIZABLE
    SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL 隔离级别;
  • 注意:设置全局级别后,需重启客户端生效。

总结

  • 隔离级别越严格,安全性越高,但数据库的并发性能也就越低,往往需要在两者之间找一个平衡点。
  • 不可重复读的核心是数据的 "修改 / 删除" 操作导致同一行数据在事务内多次读取时内容不一致;幻读的核心是数据的 "新增" 操作导致同一查询在事务内多次执行时,结果集的行数不一致。
  • mysql默认的隔离级别是可重复读,一般情况下不要修改。
  • 事务也有长短事务的概念。事务间互相影响,指的是事务在并行执行且都未commit的时候,影响会比较大。

隔离级别对比表

隔离级别 脏读 不可重复读 幻读 加锁读
读未提交 (read uncommitted) 不加锁
读已提交 (read committed) 不加锁
可重复读 (repeatable read) 不加锁
可串行化 (serializable) 加锁

八、多版本并发控制(MVCC)

数据库并发的场景有三种:

  • 读-读:不存在任何问题,也不需要并发控制
  • 读-写 :有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读 (重点)
  • 写-写:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失(由于大多数是读写并发,这类情况不做重点讲解)

下面用读写并发的场景来讲解MVCC的机制:

多版本并发控制( MVCC )是一种用来解决读-写冲突的无锁并发控制,为事务分配单向增长的事务ID,为每个修改保存一个版本,版本与事务ID关联,读操作只读该事务开始前的数据库的快照

所以MVCC 可以为数据库解决以下问题:在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能,同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题,下面从三个角度来理解MVCC:

事务ID

事务拥有ID决定先后顺序,并用结构体组织,数据结构管理,其中3个记录隐藏列字段:

  • DB_TRX_ID :6 byte,最近修改( 修改/插入)事务ID,记录创建这条记录/最后一次修改该记录的事务ID
  • DB_ROLL_PTR : 7 byte,回滚指针,指向这条记录的上一个版本(简单理解成,指向历史版本就行,这些数据一般在undo log 中)
  • DB_ROW_ID : 6 byte,隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID 产生一个聚簇索引,这就是隐藏主键
  • 补充:实际还有一个删除flag隐藏字段, 记录是否被删除

undo log

undo log 是数据库 (如 MySQL InnoDB) 中用于事务回滚MVCC(多版本并发控制) 的核心日志,记录数据修改前的原始状态。

  • 问题 1:undo 储存在哪里?

    在 MySQL 的InnoDB存储引擎中,undo 日志存储在 "回滚段(Rollback Segment)" 中。InnoDB 会为每个数据库实例维护若干个回滚段,每个回滚段又包含多个 undo 日志段,用于管理和存储 undo 日志的内容。(初步理解为存储在mysql开辟的内存中)

  • 问题 2:undo 的作用是什么?

    • 事务回滚(保障原子性):当事务执行失败(如触发约束、系统崩溃)时,数据库可通过 undo 日志反向执行操作,将数据恢复到事务开始前的状态。
    • 支持多版本并发控制(MVCC):为事务提供数据的历史版本,让不同事务能在并发时读取到 "一致性快照",避免锁竞争,提升并发性能。

模拟MVCC:

插入数据时版本链 的形成:(undo log中的数据可以称为快照)

tips:

  • 如果当前只有一个事务修改后提交,undo log会被清空
  • 如果有多个事务,某个事务执行插入并且commit了,那么undo log会被清空 ,但如果是update或者delete则不一定,上面模拟的是update的版本链,如果是delete,那么修改的是flag仍然有版本链,如果是insert,那么不存在版本链,但是undo log中会插入delete 这条数据供事务回滚时删除这条记录!
  • 来介绍当前读快照读
    • 当前读 :读取数据的 "最新版本",并且会对读取的记录加锁(共享锁或排他锁) ,确保其他事务无法同时修改该记录,从而保证读取到的数据是 "当前时刻最新的、未被其他事务干扰的",增删改都是当前读,select也可以当前读。
    • 触发当前读的场景:
      带锁的查询语句:
      SELECT ... FOR UPDATE(加排他锁,阻止其他事务修改或加排他锁)
      SELECT ... LOCK IN SHARE MODE(加共享锁,阻止其他事务加排他锁,但允许读)

      写操作(本质是 "先读后写",读取阶段需要当前读):
      INSERT、UPDATE、DELETE(执行时会先读取最新数据,再进行修改,读取阶段加锁)
    • 快照读 :读取数据的历史版本,通过数据库的多版本控制(MVCC)机制实现,不加锁,因此不会阻塞其他事务的读写操作,能显著提升并发效率
    • 触发快照读的场景:
      普通的 SELECT 语句(不包含 FOR UPDATE 或 LOCK IN SHARE MODE)
    • 读写并发就是因为读写根据的是不同版本,隔离级别可以决定具体可以看到哪些版本,看到不同版本就体现出来隔离性。

Read View

事务进行快照读操作的时候会生产读视图(Read View) ,在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID (当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大 ),Read View 在源码中就是一个类,本质是用来进行可见性判断的。 即当我们某个事务执行快照读的时候,对该记录创建一个Read View 读视图,把它比作条件,用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undo log 里面的某个版本的数据。 其中重要数据项包括:

  • m_ids:是一个列表,用于维护Read View 生成时刻,系统中正在活跃的事务 ID。换句话说,就是在创建这个 Read View 时,所有还没提交的事务的 ID 都会被记录在这里
  • min_trx_id或者up_limit_id:记录m_ids列表中事务 ID 最小的值。它用于界定 "活跃事务中最早启动的那个",是判断数据版本可见性的边界之一
  • max_trx_id或者low_limit_id:表示Read View 生成时刻系统尚未分配的下一个事务 ID ,也就是 "目前已出现过的事务 ID 的最大值 + 1"。可以理解为 "未来事务 ID 的起始值",用于区分 "生成Read View 之后才启动的事务"。
  • creator_trx_id:指创建该 Read View 的事务自身的 ID。用于判断当前事务修改的数据对自身的可见性(事务内的修改对自己是可见的)

以 "时间轴" 为核心,将事务分为三类:

  • 已经提交的事务:在 Read View 生成前就已提交的事务,其修改对当前事务可能可见
  • 正在操作的事务(活跃事务):Read View 生成时还在执行、未提交的事务,其修改对当前事务不可见
  • 快照后新来的事务:Read View 生成后才启动的事务,其修改对当前事务不可见

可见性判断规则:

通过数据版本的事务 ID(DB_TRX_ID)Read View 字段对比,确定是否可见:

  • 已经提交的事务:
    creator_trx_id == DB_TRX_ID(自身修改),或 DB_TRX_ID < up_limit_id(快照前已提交的事务)则数据可见。
  • 正在操作的事务:
    所有活跃事务的 ID 都在m_ids中,若DB_TRX_ID在m_ids里,说明事务未提交,数据不可见
    (思维误区:m_ids里的事务 ID 不一定连续,比如 11、13、15 号事务活跃,12、14 号已提交,以m_ids是 {11,13,15}。)
  • 快照后新来的事务:
    DB_TRX_ID >= low_limit_id,说明是快照后才启动的事务,数据不可见。

RR(可重复读)与 RC(读已提交)隔离级别的本质区别,核心在于快照读时 Read View 的生成时机不同,这直接导致了两者快照读结果的差异,具体如下:

RR 级别下的快照读特性:

事务中首次执行快照读时 ,会创建一个快照及对应的 Read View,该Read View会记录此时系统中所有活跃(未提交)的其他事务信息。

此后,该事务内的所有快照读都会复用这个 Read View ,不会重新生成。

因此:

  • 若当前事务在其他事务提交修改前已执行过快照读,后续快照读仍会基于首次生成的 Read View 判断可见性,其他事务的后续修改(即使提交)也无法被当前事务的快照读看到,从而保证了"可重复读"。
  • 只有在 Read View 生成前已提交的事务所做的修改,才对当前事务可见。

RC 级别下的快照读特性:

事务中每次执行快照读时,都会重新生成一个新的 Read View ,每次都获取当前最新的活跃事务状态。

因此:

  • 每次快照读都能看到**"最新已提交事务"**的修改,这会导致同一事务内多次快照读可能返回不同结果(即"不可重复读")。
相关推荐
小猪咪piggy2 小时前
【项目】小型支付商城 MVC/DDD
java·jvm·数据库
向阳而生,一路生花2 小时前
redis离线安装
java·数据库·redis
·云扬·2 小时前
使用pt-archiver实现MySQL数据归档与清理的完整实践
数据库·mysql
黄焖鸡能干四碗2 小时前
信息安全管理制度(Word)
大数据·数据库·人工智能·智慧城市·规格说明书
betazhou2 小时前
基于Linux环境使用ogg19版本从oracle 19c ADG备库远程同步数据
linux·运维·oracle·goldengate·adg·远程抽取
zhangyifang_0092 小时前
PostgreSQL一些概念特性
数据库·postgresql
weixin_46682 小时前
安装Zabbix7
数据库·mysql·zabbix
数据库生产实战2 小时前
Oracle 19C实测:重命名分区表后又重命名分区索引,分区索引会失效吗?DBA必看避坑指南!
数据库·oracle·dba