【MySQL全面教学】MySQL事务与ACID Day9(2026年)


写在前面

欢迎来到MySQL系列教学第9天!今天我们将深入MySQL最核心的话题之一------事务(Transaction)。事务是保证数据库数据一致性和完整性的基石,理解事务机制对于开发高可靠性的应用系统至关重要。

无论你是刚接触数据库的新手,还是准备面试的求职者,这篇文章都将帮助你全面掌握MySQL事务的核心概念和原理。


文章目录

一、什么是事务

1.1 事务的定义

事务是一组逻辑上的数据库操作,这些操作要么全部成功执行,要么全部不执行。事务是数据库管理系统执行过程中的一个逻辑单位。

1.2 银行转账的例子

最经典的事务例子就是银行转账:

复制代码
张三给李四转账1000元,需要两个操作:
1. 张三账户余额减少1000元
2. 李四账户余额增加1000元

这两个操作必须同时成功或同时失败:
- 如果第1步成功,第2步失败,张三的钱少了,李四没收到,钱凭空消失了
- 如果第1步失败,第2步成功,李四凭空多了1000元

这两种情况都是不可接受的!
sql 复制代码
-- 事务示例
START TRANSACTION;

-- 1. 扣减张三的余额
UPDATE accounts SET balance = balance - 1000 WHERE name = '张三';

-- 2. 增加李四的余额
UPDATE accounts SET balance = balance + 1000 WHERE name = '李四';

-- 检查是否都成功
IF (没有错误) THEN
    COMMIT;  -- 提交事务,永久保存修改
ELSE
    ROLLBACK; -- 回滚事务,撤销所有修改
END IF;

1.3 事务的四个特性(ACID)

特性 英文 含义 说明
原子性 Atomicity 不可再分 事务是最小执行单位,要么全做,要么全不做
一致性 Consistency 数据一致 事务执行前后,数据库从一个一致状态变为另一个一致状态
隔离性 Isolation 互不干扰 并发执行的事务互不干扰
持久性 Durability 永久保存 事务一旦提交,修改永久生效

二、ACID特性详解

2.1 原子性(Atomicity)

定义:事务是一个不可分割的最小工作单元,事务中的所有操作要么全部成功,要么全部失败回滚。

实现机制

  • undo log(回滚日志):记录事务执行前的数据状态,用于回滚
sql 复制代码
-- 原子性示例
START TRANSACTION;
INSERT INTO orders (user_id, amount) VALUES (1, 100);  -- 成功
INSERT INTO order_items (order_id, product_id) VALUES (LAST_INSERT_ID(), 1); -- 成功
UPDATE products SET stock = stock - 1 WHERE id = 1; -- 假设这里库存不足,失败

-- 由于第3步失败,前面两步自动回滚
ROLLBACK;

2.2 一致性(Consistency)

定义:事务执行前后,数据库必须处于一致性状态。所有数据必须满足预定义的完整性约束(外键、唯一约束、CHECK约束等)。

示例

sql 复制代码
-- 转账前后,系统总金额应该不变
-- 转账前:张三5000 + 李四3000 = 8000
-- 转账后:张三4000 + 李四4000 = 8000

-- 一致性检查
CREATE TABLE accounts (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    balance DECIMAL(10,2) CHECK (balance >= 0)  -- 余额不能为负
);

-- 如果执行 UPDATE accounts SET balance = balance - 1000 WHERE balance < 1000;
-- 会违反CHECK约束,事务回滚,保证一致性

2.3 隔离性(Isolation)

定义:多个事务并发执行时,一个事务的执行不应影响其他事务的执行。事务之间是相互隔离的。

实现机制

  • 锁机制:排他锁、共享锁
  • MVCC:多版本并发控制
sql 复制代码
-- 事务A
START TRANSACTION;
UPDATE accounts SET balance = balance - 1000 WHERE id = 1;
-- 此时事务A未提交

-- 事务B(同时执行)
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 1;
-- 根据隔离级别不同,可能读到不同的值

2.4 持久性(Durability)

定义:事务一旦提交,其对数据库的修改就是永久性的,即使系统发生故障也不会丢失。

实现机制

  • redo log(重做日志):记录事务对数据的修改,用于崩溃恢复
  • binlog(二进制日志):用于数据恢复和主从复制

三、事务控制语句

3.1 基本语法

sql 复制代码
-- 开启事务
START TRANSACTION;
-- 或
BEGIN;

-- 提交事务
COMMIT;

-- 回滚事务
ROLLBACK;

3.2 SAVEPOINT保存点

sql 复制代码
START TRANSACTION;

INSERT INTO accounts (name, balance) VALUES ('王五', 1000);
SAVEPOINT sp1;  -- 设置保存点

INSERT INTO accounts (name, balance) VALUES ('赵六', 2000);
SAVEPOINT sp2;

-- 发现第二个插入有问题,回滚到sp1
ROLLBACK TO sp1;

-- 继续其他操作
INSERT INTO accounts (name, balance) VALUES ('孙七', 3000);

COMMIT;
-- 最终只有王五和孙七被插入

3.3 自动提交模式

sql 复制代码
-- 查看自动提交状态
SHOW VARIABLES LIKE 'autocommit';

-- 关闭自动提交
SET autocommit = 0;

-- 此时每个SQL都在事务中,需要手动COMMIT或ROLLBACK
UPDATE accounts SET balance = 1000 WHERE id = 1;
-- 其他会话看不到这个修改,直到执行COMMIT

COMMIT;

-- 开启自动提交(默认)
SET autocommit = 1;

3.4 隐式提交

某些SQL语句会自动提交当前事务:

  • DDL语句(CREATE、DROP、ALTER)
  • 锁定语句(LOCK TABLES、UNLOCK TABLES)
sql 复制代码
START TRANSACTION;
INSERT INTO users (name) VALUES ('张三');

CREATE TABLE test (id INT);  -- 隐式提交!前面的INSERT被提交了

ROLLBACK;  -- 无效,事务已经提交

四、事务隔离级别

4.1 四种隔离级别

隔离级别 脏读 不可重复读 幻读
READ UNCOMMITTED 可能 可能 可能
READ COMMITTED 不可能 可能 可能
REPEATABLE READ 不可能 不可能 可能
SERIALIZABLE 不可能 不可能 不可能

4.2 设置隔离级别

sql 复制代码
-- 查看当前隔离级别
SELECT @@transaction_isolation;

-- 设置会话隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 设置全局隔离级别
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;

4.3 READ UNCOMMITTED(读未提交)

特点:事务可以读取其他事务未提交的数据。

sql 复制代码
-- 事务A
START TRANSACTION;
UPDATE accounts SET balance = 900 WHERE id = 1;
-- 未提交

-- 事务B(READ UNCOMMITTED)
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 1;  -- 读到900(脏读)

-- 事务A回滚
ROLLBACK;

-- 事务B读到的900就是脏数据

问题:脏读(Dirty Read)

4.4 READ COMMITTED(读已提交)

特点:事务只能读取其他事务已提交的数据。

sql 复制代码
-- 事务A
START TRANSACTION;
UPDATE accounts SET balance = 900 WHERE id = 1;
-- 未提交

-- 事务B(READ COMMITTED)
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 1;  -- 读到1000(原值)

-- 事务A提交
COMMIT;

-- 事务B再次查询
SELECT balance FROM accounts WHERE id = 1;  -- 读到900(新值)
-- 同一事务内两次读取结果不同(不可重复读)

问题:不可重复读(Non-repeatable Read)

4.5 REPEATABLE READ(可重复读)

特点:同一事务内多次读取同一数据,结果一致。MySQL默认隔离级别。

sql 复制代码
-- 事务A
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 1;  -- 读到1000

-- 事务B
START TRANSACTION;
UPDATE accounts SET balance = 900 WHERE id = 1;
COMMIT;

-- 事务A再次查询
SELECT balance FROM accounts WHERE id = 1;  -- 仍然读到1000(可重复读)

COMMIT;

实现原理:MVCC机制,使用事务开始时的数据快照。

4.6 SERIALIZABLE(串行化)

特点:最高隔离级别,强制事务串行执行,完全避免并发问题。

sql 复制代码
-- 事务A
START TRANSACTION;
SELECT * FROM accounts WHERE balance > 500;

-- 事务B(SERIALIZABLE)
START TRANSACTION;
INSERT INTO accounts (name, balance) VALUES ('新用户', 1000);
-- 阻塞!等待事务A提交

问题:性能最差,并发度最低。


五、并发问题详解

5.1 脏读(Dirty Read)

定义:事务读取了其他事务未提交的数据,如果那个事务回滚,读取到的数据就是无效的。

sql 复制代码
-- 时间线
-- T1: 事务A开始,修改数据但未提交
-- T2: 事务B读取了A修改后的数据
-- T3: 事务A回滚
-- T4: 事务B使用的数据是无效的

解决方案:使用READ COMMITTED或更高隔离级别。

5.2 不可重复读(Non-repeatable Read)

定义:同一事务内,两次读取同一数据,结果不同(被其他事务修改并提交)。

sql 复制代码
-- 时间线
-- T1: 事务A读取数据X = 100
-- T2: 事务B修改X = 200并提交
-- T3: 事务A再次读取X = 200(与T1不同)

解决方案:使用REPEATABLE READ或SERIALIZABLE。

5.3 幻读(Phantom Read)

定义:同一事务内,两次执行相同条件的查询,返回的行数不同(被其他事务插入或删除)。

sql 复制代码
-- 事务A
START TRANSACTION;
SELECT * FROM accounts WHERE balance > 500;  -- 返回3条

-- 事务B插入新数据并提交
INSERT INTO accounts (name, balance) VALUES ('新用户', 1000);

-- 事务A再次查询
SELECT * FROM accounts WHERE balance > 500;  -- 返回4条(幻读)

解决方案

  • SERIALIZABLE隔离级别
  • InnoDB的间隙锁(Gap Lock)和临键锁(Next-Key Lock)

5.4 三种并发问题对比

问题 描述 发生条件 解决方案
脏读 读到未提交数据 READ UNCOMMITTED READ COMMITTED+
不可重复读 同一事务两次读取结果不同 READ COMMITTED REPEATABLE READ+
幻读 同一事务两次查询行数不同 REPEATABLE READ SERIALIZABLE

六、MVCC多版本并发控制

6.1 什么是MVCC

MVCC(Multi-Version Concurrency Control) 是一种并发控制技术,通过保存数据的历史版本,实现读写不阻塞,提高并发性能。

6.2 核心概念

隐藏字段

  • DB_TRX_ID:最后修改该记录的事务ID
  • DB_ROLL_PTR:回滚指针,指向undo log
  • DB_ROW_ID:隐藏主键(如果没有显式主键)

undo log

  • 记录数据修改前的版本
  • 形成版本链,用于回滚和MVCC

ReadView

  • 事务快照,记录当前活跃事务ID列表
  • 用于判断数据版本对当前事务是否可见

6.3 快照读 vs 当前读

快照读(Snapshot Read)

sql 复制代码
-- 普通SELECT就是快照读
SELECT * FROM accounts WHERE id = 1;
-- 读取的是历史版本,不加锁

当前读(Current Read)

sql 复制代码
-- 读取最新版本,需要加锁
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;  -- 排他锁
SELECT * FROM accounts WHERE id = 1 LOCK IN SHARE MODE;  -- 共享锁

-- DML语句也是当前读
INSERT INTO accounts ...
UPDATE accounts ...
DELETE FROM accounts ...

6.4 ReadView的工作原理

ReadView包含以下信息:

  • creator_trx_id:创建该ReadView的事务ID
  • m_ids:生成ReadView时活跃的事务ID列表
  • min_trx_id:m_ids中的最小值
  • max_trx_id:下一个要分配的事务ID

可见性判断规则

  1. 如果DB_TRX_ID等于creator_trx_id:可见(自己修改的)
  2. 如果DB_TRX_ID小于min_trx_id:可见(事务已提交)
  3. 如果DB_TRX_ID大于等于max_trx_id:不可见(事务未开始)
  4. 如果DB_TRX_IDm_ids中:不可见(事务未提交)
  5. 否则:可见(事务已提交)

6.5 MVCC实现可重复读

sql 复制代码
-- 事务A(事务ID=100)
START TRANSACTION;
-- 生成ReadView: m_ids=[100], min_trx_id=100, max_trx_id=101
SELECT * FROM accounts WHERE id = 1;  -- 读取快照

-- 事务B(事务ID=101)修改并提交
UPDATE accounts SET balance = 900 WHERE id = 1;
COMMIT;

-- 事务A再次查询,使用相同的ReadView
SELECT * FROM accounts WHERE id = 1;  -- 仍然读到原值,实现可重复读

COMMIT;

七、实战:电商下单的事务处理

7.1 业务场景

用户下单需要完成以下操作:

  1. 创建订单记录
  2. 扣减商品库存
  3. 扣减用户余额
  4. 创建订单明细

7.2 事务设计

sql 复制代码
-- 存储过程:创建订单
DELIMITER $$

CREATE PROCEDURE create_order(
    IN p_user_id BIGINT,
    IN p_product_id BIGINT,
    IN p_quantity INT,
    OUT p_order_id BIGINT,
    OUT p_result VARCHAR(100)
)
BEGIN
    DECLARE v_price DECIMAL(10,2);
    DECLARE v_stock INT;
    DECLARE v_balance DECIMAL(12,2);
    DECLARE v_total DECIMAL(12,2);
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        SET p_result = '订单创建失败,已回滚';
        RESIGNAL;
    END;
    
    -- 开启事务
    START TRANSACTION;
    
    -- 1. 查询商品信息(加排他锁,防止并发修改)
    SELECT price, stock INTO v_price, v_stock 
    FROM products 
    WHERE id = p_product_id FOR UPDATE;
    
    -- 检查库存
    IF v_stock < p_quantity THEN
        ROLLBACK;
        SET p_result = '库存不足';
        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '库存不足';
    END IF;
    
    -- 2. 查询用户余额
    SELECT balance INTO v_balance 
    FROM users 
    WHERE id = p_user_id FOR UPDATE;
    
    SET v_total = v_price * p_quantity;
    
    -- 检查余额
    IF v_balance < v_total THEN
        ROLLBACK;
        SET p_result = '余额不足';
        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '余额不足';
    END IF;
    
    -- 3. 扣减库存
    UPDATE products 
    SET stock = stock - p_quantity 
    WHERE id = p_product_id;
    
    -- 4. 扣减余额
    UPDATE users 
    SET balance = balance - v_total 
    WHERE id = p_user_id;
    
    -- 5. 创建订单
    INSERT INTO orders (user_id, total_amount, status, created_at)
    VALUES (p_user_id, v_total, 1, NOW());
    
    SET p_order_id = LAST_INSERT_ID();
    
    -- 6. 创建订单明细
    INSERT INTO order_items (order_id, product_id, quantity, price)
    VALUES (p_order_id, p_product_id, p_quantity, v_price);
    
    -- 提交事务
    COMMIT;
    SET p_result = '订单创建成功';
END$$

DELIMITER ;

7.3 调用示例

sql 复制代码
-- 调用存储过程创建订单
SET @order_id = 0;
SET @result = '';

CALL create_order(1, 100, 2, @order_id, @result);

SELECT @order_id, @result;

八、踩坑提醒

8.1 长事务问题

问题:事务执行时间过长,会导致:

  • 锁持有时间过长,阻塞其他事务
  • undo log堆积,影响性能
  • 可能导致死锁

解决方案

sql 复制代码
-- 1. 尽量缩短事务范围
START TRANSACTION;
-- 只放必要的操作
COMMIT;

-- 2. 避免在事务中执行耗时操作
-- 错误:事务中调用外部API、发送邮件等
START TRANSACTION;
INSERT INTO orders ...;
-- 发送邮件(耗时操作,不应该在事务中)
send_email();
COMMIT;

-- 正确:先提交事务,再发送邮件
START TRANSACTION;
INSERT INTO orders ...;
COMMIT;
send_email();

8.2 事务中避免调用外部接口

sql 复制代码
-- 危险操作
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;

-- 调用支付接口(可能超时、失败)
CALL external_payment_api();

-- 如果接口超时,事务一直不提交,锁一直不释放
COMMIT;

正确做法

  1. 先调用外部接口
  2. 接口成功后再开启事务更新数据库
  3. 或者使用本地消息表实现最终一致性

8.3 注意死锁

sql 复制代码
-- 事务A
START TRANSACTION;
UPDATE accounts SET balance = 900 WHERE id = 1;  -- 锁住id=1
UPDATE accounts SET balance = 1900 WHERE id = 2;  -- 等待id=2

-- 事务B(同时执行)
START TRANSACTION;
UPDATE accounts SET balance = 800 WHERE id = 2;  -- 锁住id=2
UPDATE accounts SET balance = 1800 WHERE id = 1;  -- 等待id=1,死锁!

解决方案

  • 按固定顺序访问资源
  • 设置锁超时时间
  • 捕获死锁异常并重试

九、面试高频考点

Q1:四种隔离级别分别解决什么问题?

  • READ UNCOMMITTED:无,性能最好,问题最多
  • READ COMMITTED:解决脏读
  • REPEATABLE READ:解决脏读、不可重复读(MySQL默认)
  • SERIALIZABLE:解决脏读、不可重复读、幻读,性能最差

Q2:MySQL默认的隔离级别是什么?为什么?

  • 默认隔离级别是REPEATABLE READ(可重复读)
  • 原因:
    1. 相比READ COMMITTED,避免了不可重复读问题
    2. InnoDB通过MVCC和间隙锁,在REPEATABLE READ下也能解决幻读问题
    3. 平衡了数据一致性和并发性能

Q3:幻读是怎么产生的?如何解决?

  • 产生原因:一个事务在两次查询同一范围数据时,由于其他事务插入新数据,导致两次查询结果行数不同。
  • 解决方案
    1. SERIALIZABLE隔离级别(性能差)
    2. InnoDB的间隙锁(Gap Lock)和临键锁(Next-Key Lock)
    3. 查询时加FOR UPDATE锁定范围

Q4:MVCC是如何工作的?

  1. 每行数据有隐藏字段:DB_TRX_ID(事务ID)、DB_ROLL_PTR(回滚指针)
  2. 修改数据时,将旧版本存入undo log,形成版本链
  3. 事务开始时生成ReadView,记录当前活跃事务
  4. 查询时根据ReadView判断数据版本是否可见
  5. 实现读写不阻塞,提高并发性能

Q5:事务的ACID是如何保证的?

  • 原子性:undo log实现回滚
  • 一致性:原子性+隔离性+持久性共同保证,加上约束检查
  • 隔离性:锁机制+MVCC
  • 持久性:redo log+binlog实现崩溃恢复

十、总结

今天我们深入学习了MySQL事务的核心知识:

  1. 事务概念:ACID特性是事务的基石
  2. 事务控制:START TRANSACTION、COMMIT、ROLLBACK、SAVEPOINT
  3. 隔离级别:四种隔离级别及其解决的问题
  4. 并发问题:脏读、不可重复读、幻读的产生和解决
  5. MVCC机制:多版本并发控制实现高效读写分离
  6. 实战应用:电商下单的事务设计

核心要点

  • 理解ACID是理解事务的基础
  • 选择合适的隔离级别,平衡一致性和性能
  • MVCC是InnoDB高并发的核心
  • 避免长事务和事务中调用外部接口

下一步预告

Day10我们将学习MySQL的锁机制与并发控制,深入探讨InnoDB的各种锁类型(记录锁、间隙锁、临键锁),以及如何处理死锁问题。敬请期待!


参考资料

MySQL官方文档 - 事务


互动话题

  1. 你在项目中遇到过哪些事务相关的问题?是如何解决的?
  2. 你们项目使用的是什么隔离级别?为什么选择这个级别?
  3. 对于分布式事务,你有什么实践经验或了解?

欢迎在评论区分享你的经验和见解!如果觉得本文有帮助,别忘了点赞收藏哦~

相关推荐
武子康7 分钟前
Java-07 深入浅出 MyBatis数据库一对多关系模型实战:表结构设计与查询实现
java·后端
花椒技术1 小时前
企业内部 Agent 落地复盘:Gateway、Skill 和二次确认如何串起受控业务执行
后端·agent·ai编程
字节跳动开源2 小时前
Viking AI 搜索 CLI 正式发布:会说话,就能做搜索推荐
数据库·人工智能·开源
TechWJ3 小时前
数据库在公司内网,出差路上想查数据怎么办?
服务器·数据库·mariadb
橙子圆1233 小时前
Redis知识9之集群
数据库·redis·缓存
枕星而眠3 小时前
数据结构八大排序详解(一):四大简单排序
c语言·数据结构·c++·后端
IT_陈寒3 小时前
React useEffect闭包陷阱差点把我整失业了
前端·人工智能·后端
BlackHeart12033 小时前
【SQL】Oracle中序列(Sequence)作为默认值引发的ORA-00979
数据库·sql·oracle
苍何3 小时前
爆肝两周,我把 Codex 最全实战指南开源了
后端