MySQL的事物管理

1:事物基础,为什么我们需要事物

1:并发操作带来的问题

没有事物控制的并发CURD会引发严重的数据一致性问题,最经典的就是火车票超卖问题

sql 复制代码
-- 测试表初始化
CREATE TABLE tickets (
    id INT PRIMARY KEY,
    name VARCHAR(50) NOT NULL,
    nums INT NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO tickets VALUES (10, '西安<->兰州', 1);

问题场景:

  • 客户端 A 执行SELECT nums FROM tickets WHERE id=10,得到nums=1
  • 客户端 B 同时执行SELECT nums FROM tickets WHERE id=10,也得到nums=1
  • 客户端 A 执行UPDATE tickets SET nums=nums-1 WHERE id=10,卖票成功
  • 客户端 B 执行UPDATE tickets SET nums=nums-1 WHERE id=10,导致同一张票被卖两次,最终nums=-1

2:事物的定义与ACID四大特性

事物是一组逻辑相关的DML语句的集合,这组语句要么全部执行成功,要么全部执行失败,是一个不可分割的整体。

事物必须满足ACID四大特性,这是解决并发问题的基础:

特性 英文 核心含义 对应案例
原子性 Atomicity 事务中的所有操作要么全部完成,要么全部回滚,不会停留在中间状态 银行转账:A 扣钱和 B 加钱必须同时成功或失败
一致性 Consistency 事务执行前后,数据库的完整性约束没有被破坏 转账前后,A 和 B 的总金额保持不变
隔离性 Isolation 多个并发事务之间相互隔离,互不干扰 一个事务的未提交修改,不应该被其他事务看到
持久性 Durability 事务一旦提交,对数据的修改就是永久的,即使系统崩溃也不会丢失 买票成功后,即使服务器宕机,购票记录也不会消失

关键补充:

  • 原子性是手段 ,一致性是目的 ,隔离性是保障 ,持久性是结果
  • 一致性分为数据库层面的一致性 (主键、外键、唯一约束等)和业务层面的一致性(如总金额不变),后者需要开发者自己保证
  • MySQL 中只有InnoDB 引擎支持事务,MyISAM、MEMORY 等引擎均不支持

3:验证InnoDB对事物的支持

sql 复制代码
-- 查看所有数据库引擎
SHOW ENGINES;

-- 查看当前表的引擎
SHOW CREATE TABLE tickets;

-- 尝试将表改为MyISAM引擎(不支持事务)
ALTER TABLE tickets ENGINE=MyISAM;

-- 测试MyISAM是否支持回滚
BEGIN;
UPDATE tickets SET nums=0 WHERE id=10;
ROLLBACK; -- 执行后数据不会回滚,MyISAM忽略ROLLBACK语句

-- 改回InnoDB引擎
ALTER TABLE tickets ENGINE=InnoDB;
  • InnoDB 引擎的Transactions列显示为YES,支持事务
  • MyISAM 引擎的Transactions列显示为NO,不支持事务,执行 ROLLBACK 无效

2:事物的基本操作与提交方式

1:事物的提交方式

MySQL 支持两种事务提交方式:

  1. 自动提交(默认):每条 SQL 语句都是一个独立的事务,执行完成后自动提交
  2. 手动提交 :需要显式使用BEGIN/START TRANSACTION开启事务,COMMIT提交,ROLLBACK回滚

2:自动提交和手动提交的区别

sql 复制代码
-- 步骤1:查看当前自动提交状态
SHOW VARIABLES LIKE 'autocommit'; -- 默认值为ON

-- 步骤2:自动提交模式下的修改
UPDATE tickets SET nums=1 WHERE id=10;
-- 新开终端查询,数据已经更新(自动提交生效)

-- 步骤3:关闭自动提交
SET AUTOCOMMIT=0;
SHOW VARIABLES LIKE 'autocommit'; -- 显示OFF

-- 步骤4:关闭自动提交后的修改
UPDATE tickets SET nums=0 WHERE id=10;
-- 新开终端查询,数据还是1(未提交)

-- 步骤5:提交事务
COMMIT;
-- 新开终端查询,数据变为0(提交生效)

-- 步骤6:恢复自动提交
SET AUTOCOMMIT=1;
  • 自动提交模式下,每条 SQL 执行后自动 COMMIT
  • 关闭自动提交后,所有 SQL 都在同一个事务中,直到显式 COMMIT 或 ROLLBACK
  • 注意:SET AUTOCOMMIT=0只对当前会话有效,不影响其他会话

3:事物的保存点与部分回滚

sql 复制代码
-- 初始化测试表
CREATE TABLE IF NOT EXISTS account (
    id INT PRIMARY KEY,
    name VARCHAR(50) NOT NULL DEFAULT '',
    balance DECIMAL(10,2) NOT NULL DEFAULT 0.0
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- 步骤1:开启事务
BEGIN;

-- 步骤2:插入第一条记录,创建保存点save1
INSERT INTO account VALUES (1, '张三', 100.00);
SAVEPOINT save1;

-- 步骤3:插入第二条记录,创建保存点save2
INSERT INTO account VALUES (2, '李四', 10000.00);
SAVEPOINT save2;

-- 步骤4:插入第三条记录
INSERT INTO account VALUES (3, '王五', 5000.00);

-- 步骤5:查询当前数据(3条记录)
SELECT * FROM account;

-- 步骤6:回滚到保存点save2(删除第三条记录)
ROLLBACK TO save2;
SELECT * FROM account; -- 只剩张三和李四

-- 步骤7:回滚到保存点save1(删除第二条记录)
ROLLBACK TO save1;
SELECT * FROM account; -- 只剩张三

-- 步骤8:全回滚(删除所有记录)
ROLLBACK;
SELECT * FROM account; -- 空表
  • 保存点允许我们在事务中设置多个回滚点,实现部分回滚
  • 回滚到某个保存点后,该保存点之后的所有操作都会被撤销
  • 保存点只在当前事务中有效,事务提交或回滚后自动失效

4:异常情况下的自动回滚

1:终端A
sql 复制代码
BEGIN;
INSERT INTO account VALUES (1, '张三', 100.00);
SELECT * FROM account; -- 显示有1条记录
-- 不要COMMIT,直接按Ctrl+\强制终止MySQL客户端
2:终端B
sql 复制代码
-- 终端A崩溃前查询,可能看到未提交的数据(取决于隔离级别)
SELECT * FROM account;
-- 等待几秒后再次查询,数据消失(MySQL自动回滚)
SELECT * FROM account;
  • 事务未提交时,如果客户端异常崩溃,MySQL 会自动回滚该事务
  • 事务一旦提交,即使客户端崩溃,数据也会永久保存(持久性)

3:事物隔离级别

1:事物并发可能引发的三个问题

问题 定义 发生场景
脏读 一个事务读取了另一个事务未提交的修改 事务 A 修改了数据但未提交,事务 B 读取了这个修改,然后事务 A 回滚,导致事务 B 读到的数据无效
不可重复读 同一个事务中,多次读取同一数据,得到的结果不同 事务 A 第一次读取数据后,事务 B 修改并提交了该数据,事务 A 第二次读取得到不同的结果
幻读 同一个事务中,多次执行相同的查询,得到的记录数不同 事务 A 第一次查询得到 N 条记录,事务 B 插入了一条新记录并提交,事务 A 第二次查询得到 N+1 条记录

关键区别

  • 不可重复读的重点是修改和删除
  • 幻读的重点是新增

2:MySQL的四种隔离级别

MySQL 提供了四种隔离级别,从低到高依次为:

  1. 读未提交(Read Uncommitted):最低级别,所有事务都能看到其他事务未提交的修改
  2. 读提交(Read Committed):只能看到其他事务已经提交的修改(大多数数据库的默认级别)
  3. 可重复读(Repeatable Read):MySQL 的默认级别,同一个事务中多次读取同一数据结果一致
  4. 串行化(Serializable):最高级别,强制事务串行执行,完全避免并发问题

不同隔离级别对并发问题的解决程度:

隔离级别 脏读 不可重复读 幻读 性能
读未提交 最高
读提交 较高
可重复读 ❌(MySQL 特殊实现) 一般
串行化 最低

注意:标准 SQL 中,可重复读级别仍然会出现幻读,但 MySQL 的 InnoDB 引擎通过 **Next-Key Lock(间隙锁 + 行锁)** 解决了幻读问题。

3:读未提交级别和脏读

序号为实验步骤

1:设置全局隔离级别为读未提交
sql 复制代码
SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
-- 重启MySQL客户端后验证
SELECT @@tx_isolation; -- MySQL 5.7
-- SELECT @@transaction_isolation; -- MySQL 8.0+
2:终端A
sql 复制代码
BEGIN;
UPDATE account SET balance=123.00 WHERE id=1;
-- 不要COMMIT
3:终端B
sql 复制代码
BEGIN;
SELECT * FROM account WHERE id=1; -- 读到balance=123.00(脏读)
4:终端A
sql 复制代码
ROLLBACK;
5:终端B
sql 复制代码
SELECT * FROM account WHERE id=1; -- 读到balance=100.00,之前的123.00是脏数据
  • 读未提交级别下,事务可以读取到其他事务未提交的修改,出现脏读
  • 该级别几乎没有加锁,性能最高,但实际生产中绝对不会使用

4:读提交级别与不可重复读

1:设置全局隔离级别为读提交
sql 复制代码
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 重启客户端后验证
SELECT @@tx_isolation;
2:终端A
sql 复制代码
BEGIN;
SELECT * FROM account WHERE id=1; -- balance=100.00
3:终端B
sql 复制代码
BEGIN;
UPDATE account SET balance=321.00 WHERE id=1;
COMMIT;
4:终端A
sql 复制代码
SELECT * FROM account WHERE id=1; -- balance=321.00(不可重复读)
COMMIT;
  • 读提交级别下,事务只能看到其他事务已经提交的修改,解决了脏读
  • 同一个事务中,多次读取同一数据可能得到不同的结果,出现不可重复读

5:可重复读级别与幻读验证

1:设置全局隔离级别为可重复读
sql 复制代码
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 重启客户端后验证
SELECT @@tx_isolation;
2:验证不可重复读已解决(懒得敲终端两字了)

A:

sql 复制代码
BEGIN;
SELECT * FROM account WHERE id=1; -- balance=321.00

B:

sql 复制代码
BEGIN;
UPDATE account SET balance=4321.00 WHERE id=1;
COMMIT;

A:

sql 复制代码
SELECT * FROM account WHERE id=1; -- 仍然是321.00(可重复读)
COMMIT;
SELECT * FROM account WHERE id=1; -- 提交后看到4321.00
3:验证幻读已解决

A:

sql 复制代码
BEGIN;
SELECT * FROM account; -- 只有1条记录(id=1)

B:

sql 复制代码
BEGIN;
INSERT INTO account VALUES (2, '李四', 10000.00);
COMMIT;

A:

sql 复制代码
SELECT * FROM account; -- 仍然只有1条记录(没有幻读)
COMMIT;
SELECT * FROM account; -- 提交后看到2条记录
  • MySQL 的可重复读级别解决了不可重复读问题
  • MySQL 通过 Next-Key Lock 解决了幻读问题,这是 MySQL 优于其他数据库的地方

6:串行化级别与锁等待

1:设置全局隔离级别与串行化
sql 复制代码
SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- 重启客户端后验证
SELECT @@tx_isolation;
2:终端A
sql 复制代码
BEGIN;
SELECT * FROM account; -- 加共享锁
3:终端B
sql 复制代码
BEGIN;
UPDATE account SET balance=1.00 WHERE id=1; -- 会被阻塞,直到终端A提交或回滚
4:终端A
sql 复制代码
COMMIT; -- 终端B的UPDATE会立即执行
  • 串行化级别下,所有读操作都会加共享锁,写操作都会加排他锁
  • 读写操作之间会互相阻塞,强制事务串行执行
  • 该级别性能极低,实际生产中几乎不会使用

4:MVCC底层原理:无锁并发控制的核心

1:什么是MVCC

MVCC(多版本并发控制)是InnoDB是西安隔离性的核心机制,他通过保存数据的多个历史版本,让读操作不需要加锁,从而实现读写不阻塞,大大提高了数据库的并发性。

MVCC 主要解决了读 - 写冲突问题:

  • 读操作(快照读)不需要加锁,不会阻塞写操作
  • 写操作也不会阻塞读操作

2:MVCC的三个核心组件

1:隐藏字段

InnoDB 会为每一行数据添加三个隐藏字段:

  1. DB_TRX_ID(6 字节):最近一次修改(插入 / 更新)该行数据的事务 ID
  2. DB_ROLL_PTR(7 字节):回滚指针,指向该行数据的上一个版本(存储在 undo log 中)
  3. DB_ROW_ID(6 字节):隐藏主键,如果表没有定义主键,InnoDB 会自动生成
2:Undo Log

Undo Log 是 InnoDB 用于实现事务回滚和 MVCC 的日志,它保存了数据的历史版本。当执行 UPDATE/DELETE 操作时,InnoDB 会先将原始数据拷贝到 Undo Log 中,形成一个历史版本。

多个历史版本通过DB_ROLL_PTR 指针连接成一个版本链

3:Read VIew

Read View 是事务进行快照读时生成的读视图,它记录了当前系统中活跃的事务 ID,用于判断数据版本的可见性。

Read View 包含四个核心字段:

  • m_ids:当前系统中活跃的事务 ID 列表
  • up_limit_id:活跃事务中最小的 ID
  • low_limit_id:系统下一个要分配的事务 ID(当前最大事务 ID+1)
  • creator_trx_id:创建该 Read View 的事务 ID

3:数据版本可见性判断规则

当事务读取某行数据时,会从版本链的最新版本开始,依次判断每个版本是否可见:

  1. 如果版本的DB_TRX_ID < up_limit_id:该版本的事务已经提交,可见
  2. 如果版本的DB_TRX_ID >= low_limit_id:该版本的事务是在 Read View 生成之后才启动的,不可见
  3. 如果版本的DB_TRX_IDup_limit_idlow_limit_id之间:
    • 如果DB_TRX_IDm_ids列表中:该版本的事务还在活跃,不可见
    • 如果DB_TRX_ID不在m_ids列表中:该版本的事务已经提交,可见
  4. 如果当前版本不可见,就通过DB_ROLL_PTR找到上一个版本,继续判断,直到找到可见的版本或遍历完整个版本链

4:RR与RC的本质区别

RR 和 RC 隔离级别的本质区别在于Read View 的生成时机不同

  • RC 级别:每次快照读都会生成一个新的 Read View,所以能看到其他事务已经提交的最新修改,导致不可重复读
  • RR 级别:同一个事务中,只有第一次快照读会生成 Read View,之后的所有快照读都使用同一个 Read View,所以能保证可重复读

5:当前读与快照读的区别

1:设置隔离级别为RR
sql 复制代码
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 重启客户端
2:初始化数据
sql 复制代码
TRUNCATE TABLE account;
INSERT INTO account VALUES (1, '张三', 100.00);
  • 快照读:普通的 SELECT 语句,读取的是历史版本,不需要加锁
  • 当前读:SELECT ... LOCK IN SHARE MODE、SELECT ... FOR UPDATE、INSERT、UPDATE、DELETE,读取的是最新版本,需要加锁
  • RR 级别下,快照读使用同一个 Read View,当前读总是读取最新版本

5:事务进阶

1:事务的额分类

根据事务的复杂程度,事务可以分为以下几类:

  1. 扁平事务:最基本的事务类型,所有操作都在同一个层次,要么全部成功,要么全部失败
  2. 带保存点的扁平事务:在扁平事务的基础上增加了保存点,支持部分回滚
  3. 链式事务:一个事务提交后自动开启下一个事务,事务之间通过上下文传递
  4. 嵌套事务:事务中包含子事务,子事务的提交依赖于父事务的提交
  5. 分布式事务:涉及多个数据库或资源的事务,需要保证跨资源的原子性

2:InnoDB事务的底层实现:Redo Log与Undo Log

InnoDB 通过Redo LogUndo Log共同实现事务的 ACID 特性:

  • Undo Log:实现原子性和 MVCC,保存数据的历史版本,用于回滚和快照读
  • Redo Log:实现持久性,记录对数据页的修改,当系统崩溃时,通过 Redo Log 恢复未刷盘的数据

两阶段提交:为了保证 Redo Log 和 Binlog 的一致性,InnoDB 采用了两阶段提交机制:

  1. 准备阶段:将 Redo Log 写入磁盘,标记事务为准备状态
  2. 提交阶段:将 Binlog 写入磁盘,然后标记 Redo Log 为提交状态

3:更新丢失的问题与解决方案

MVCC 解决了读 - 写冲突,但无法解决写 - 写冲突,即更新丢失问题。更新丢失分为两类:

  1. 第一类更新丢失:一个事务的回滚覆盖了另一个事务已经提交的修改(已被数据库解决)
  2. 第二类更新丢失:一个事务的提交覆盖了另一个事务已经提交的修改(需要开发者解决)

解决方案

  • 悲观锁:使用 SELECT ... FOR UPDATE 加排他锁,确保同一时间只有一个事务能修改数据
  • 乐观锁:使用版本号或时间戳机制,在更新时检查版本号是否一致,如果不一致则重试

4:事务最佳实践

  • 避免长事务:长事务会占用大量资源,导致锁等待和回滚段膨胀
  • 合理设置隔离级别:大多数场景下使用 MySQL 默认的 RR 级别即可,不需要盲目提高隔离级别
  • 避免大事务:将大事务拆分成多个小事务,减少锁持有时间
  • 正确使用保存点:在复杂事务中使用保存点,避免全回滚带来的性能损失
  • 不要在事务中执行耗时操作:如网络请求、文件 IO 等,会延长事务执行时间
  • 及时提交或回滚事务:事务执行完成后立即提交或回滚,释放资源

5:常见事务坑

  • 自动提交的坑:忘记关闭自动提交,导致每条 SQL 都是一个独立的事务
  • 死锁问题:多个事务互相等待对方持有的锁,导致死锁
  • 幻读的误区:认为所有隔离级别下的幻读都被解决了,实际上只有 MySQL 的 RR 级别解决了快照读的幻读
  • 长事务导致的回滚段膨胀:长事务会导致 Undo Log 无法被清理,占用大量磁盘空间
相关推荐
m0_617493945 小时前
PySide6 数据库操作深度实测:从 SQLite 连接到增删改查避坑指南
jvm·数据库·sqlite
weixin_449290015 小时前
Docker + MySQL 在 Windows 11 上的本地安装部署文档
mysql·docker·容器
OpsEye6 小时前
数据库连接池爆了,这3个命令能救你一次
运维·数据库·后端
csdn小瓯6 小时前
PostgreSQL迁移实战:从SQLite到生产级数据库的平滑演进
数据库·postgresql·sqlite
浪客灿心6 小时前
mysql表的操作和数据类型
mysql
码云骑士7 小时前
Redis 入门实战:从 NoSQL 概念到安装与基础操作详解(一)
数据库·redis·缓存
YL200404267 小时前
MySQL-进阶篇-锁
数据库·mysql
爱喝水的鱼丶7 小时前
SAP-ABAP:数据类型与数据对象(8篇) 第七篇:进阶优化篇——基于类型与对象特征的性能优化技巧
运维·数据库·学习·性能优化·sap·abap·开发交流
SelectDB技术团队7 小时前
PB 级自动驾驶数据秒级检索:Apache Doris 统一多模态数据平台实践
数据库·人工智能·自动驾驶·apache doris·selectdb