MySQLNotes

第 15 章:事务基础与隔离级别

zjc 于 2026-01-15 发布

这是《MySQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 事务是 MySQL 成为交易系统事实来源的根基。它让一组操作要么全部可见,要么完全不可见,并在并发执行时为业务提供明确的一致性边界。

15.1 事务的 ACID

特性 含义 主要依赖
Atomicity 原子性 事务内操作全部成功或全部失败 Undo Log、事务管理
Consistency 一致性 数据满足约束和业务规则 约束、锁、应用逻辑
Isolation 隔离性 并发事务互不干扰 MVCC、锁
Durability 持久性 提交后的数据崩溃不丢 Redo Log、Binlog、Doublewrite

一致性是目的,原子性、隔离性和持久性是数据库提供的重要机制。应用把非法数据写入合法事务中,数据库无法替你保证业务一致性。

15.2 显式事务

开启事务:

START TRANSACTION;

等价写法:

BEGIN;

提交:

COMMIT;

回滚:

ROLLBACK;

示例:

START TRANSACTION;

UPDATE accounts
SET balance = balance - 100
WHERE user_id = 1
  AND balance >= 100;

UPDATE accounts
SET balance = balance + 100
WHERE user_id = 2;

COMMIT;

如果第二条更新失败,应执行:

ROLLBACK;

不能让一半转账留在数据库里。

15.3 autocommit

MySQL 默认开启自动提交:

SHOW VARIABLES LIKE 'autocommit';

autocommit=1 时,单条 SQL 没有显式开启事务也会自动提交。显式 START TRANSACTION 后,需要执行 COMMITROLLBACK

关闭自动提交:

SET autocommit = 0;

风险:

  1. 忘记提交会形成长事务;
  2. 连接归还连接池后事务状态可能残留;
  3. 锁和 Undo 版本长期保留;
  4. 主从延迟和性能问题难以定位。

应用建议:显式管理事务边界,提交或回滚必须放在 finally 逻辑中;连接归还前确认没有未结束事务。

15.4 SAVEPOINT

保存点允许部分回滚:

START TRANSACTION;

SAVEPOINT after_order;

INSERT INTO orders
  (order_no, user_id, merchant_id, status, total_amount)
VALUES
  ('20260826000001', 1, 1, 'CREATED', 25.50);

SAVEPOINT after_coupon;

UPDATE coupons
SET used = 1
WHERE coupon_id = 100;

ROLLBACK TO SAVEPOINT after_coupon;

COMMIT;

结果:

  1. 订单插入保留;
  2. 优惠券更新被撤销;
  3. 事务仍未结束,直到 COMMIT。

删除保存点:

RELEASE SAVEPOINT after_coupon;

ROLLBACK TO SAVEPOINT 不是提交事务,只是撤销到某个内部位置。

15.5 并发异常

标准 SQL 定义了几个经典并发问题。

异常 描述
Dirty Read 脏读 读到其他事务未提交的数据
Non-repeatable Read 不可重复读 同一事务两次读取同一行值不同
Phantom Read 幻读 同一条件两次读取出现或消失额外行
Lost Update 丢失更新 两个事务基于旧值互相覆盖

示例表:

CREATE TABLE isolation_accounts (
    id INT PRIMARY KEY,
    balance DECIMAL(12, 2) NOT NULL
) ENGINE=InnoDB;

INSERT INTO isolation_accounts VALUES (1, 1000.00);

不可重复读

Session A:

START TRANSACTION;
SELECT balance FROM isolation_accounts WHERE id = 1;

Session B:

START TRANSACTION;
UPDATE isolation_accounts
SET balance = 900
WHERE id = 1;
COMMIT;

Session A 再读:

SELECT balance FROM isolation_accounts WHERE id = 1;
COMMIT;

如果两次结果不同,就是不可重复读。

15.6 四个隔离级别

查看级别:

SELECT @@global.transaction_isolation,
       @@session.transaction_isolation;

设置会话级别:

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

设置全局级别:

SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
级别 脏读 不可重复读 幻读
READ UNCOMMITTED 可能 可能 可能
READ COMMITTED 不可能 可能 可能
REPEATABLE READ 不可能 不可能 InnoDB 常规情况下基本避免
SERIALIZABLE 不可能 不可能 不可能

MySQL InnoDB 默认是 REPEATABLE READ

READ UNCOMMITTED

可能读到未提交数据:

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

生产业务通常不使用。脏读会把中间状态、失败状态甚至回滚前的数据暴露给应用。

READ COMMITTED

每次一致性读都会生成新的 Read View,只能看到已提交数据。很多互联网系统将在线业务设置为 RC,原因是:

  1. 减少间隙锁使用;
  2. 并发写入冲突更少;
  3. 死锁概率下降;
  4. 业务多为单行短事务;
  5. 配合显式条件更新可满足一致性。

代价:

  1. 同一事务内两次读可能不同;
  2. 复杂业务需要显式锁定读;
  3. 不能依赖快照做业务判断。

REPEATABLE READ

事务第一次一致性读时创建 Read View,后续普通 SELECT 复用同一快照。

适合:

  1. 报表内多次读取希望口径一致;
  2. 默认 MySQL 行为;
  3. 需要减少不可重复读;
  4. 配合主键或唯一条件更新。

注意:

  1. 普通 SELECT 是快照读,不锁行;
  2. UPDATE 使用当前读;
  3. 先 SELECT 再 UPDATE 可能基于旧快照;
  4. 需要强一致判断时使用 SELECT ... FOR UPDATE
  5. 幻读不是在所有场景下都绝对不存在。

SERIALIZABLE

普通 SELECT 会被隐式转换为锁定读,并发能力显著下降:

SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;

适合低并发、强隔离需求的任务。在线高并发交易系统很少使用。

15.7 快照读与当前读

快照读

普通 SELECT:

SELECT balance
FROM accounts
WHERE user_id = 1;

InnoDB 使用 MVCC 读取历史版本,不加行锁。

当前读

以下语句读取最新已提交或当前锁定版本,并涉及锁:

SELECT * FROM accounts WHERE user_id = 1 FOR UPDATE;
SELECT * FROM accounts WHERE user_id = 1 LOCK IN SHARE MODE;
UPDATE accounts SET balance = balance - 1 WHERE user_id = 1;
DELETE FROM accounts WHERE user_id = 1;
INSERT INTO accounts VALUES (...);

MySQL 8.0 中 FOR SHARE 替代 LOCK IN SHARE MODE,旧写法仍兼容。

经典问题:

START TRANSACTION;
SELECT stock FROM products WHERE id = 1;
-- 返回 10
UPDATE products SET stock = 5 WHERE id = 1;

如果另一个事务已经把库存改成 8,当前 UPDATE 会覆盖为 5。快照读不能防止丢失更新。

安全写法:

START TRANSACTION;

SELECT stock
FROM products
WHERE id = 1
FOR UPDATE;

UPDATE products
SET stock = 5
WHERE id = 1;

COMMIT;

或直接条件更新:

UPDATE products
SET stock = 5
WHERE id = 1
  AND stock = 10;

15.8 丢失更新实验

创建表:

CREATE TABLE counters (
    id INT PRIMARY KEY,
    value INT NOT NULL
) ENGINE=InnoDB;

INSERT INTO counters VALUES (1, 0);

错误流程:

-- Session A
START TRANSACTION;
SELECT value FROM counters WHERE id = 1; -- 读到 0

-- Session B
START TRANSACTION;
SELECT value FROM counters WHERE id = 1; -- 读到 0
UPDATE counters SET value = 1 WHERE id = 1;
COMMIT;

-- Session A
UPDATE counters SET value = 1 WHERE id = 1;
COMMIT;

B 的更新结果被 A 覆盖,等效丢失。

正确方式:

UPDATE counters
SET value = value + 1
WHERE id = 1;

或:

SELECT value FROM counters WHERE id = 1 FOR UPDATE;

15.9 事务边界设计

推荐:

开启事务
  -> 校验和锁定必要数据
  -> 少量核心写入
  -> 提交

不应放入事务:

  1. 远程 HTTP 调用;
  2. 文件上传下载;
  3. 消息发送等待;
  4. 复杂规则计算;
  5. 用户交互等待;
  6. 大批量数据清洗;
  7. 锁内调用外部服务。

正确拆分:

1. 前置校验;
2. 开启本地事务;
3. 锁定并更新核心数据;
4. 提交事务;
5. 事务外发送消息或调用外部系统;
6. 通过状态机和对账处理外部失败。

15.10 查看事务状态

查看当前事务:

SELECT
  trx_id,
  trx_state,
  trx_started,
  trx_requested_lock_id,
  trx_wait_started,
  trx_rows_locked,
  trx_rows_modified,
  trx_mysql_thread_id
FROM information_schema.INNODB_TRX
ORDER BY trx_started;

查看长事务:

SELECT
  trx_id,
  trx_started,
  TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS trx_age_seconds,
  trx_mysql_thread_id
FROM information_schema.INNODB_TRX
WHERE trx_started < DATE_SUB(NOW(), INTERVAL 60 SECOND);

终止会话:

KILL <thread_id>;

终止前必须确认影响,不要把业务正在执行的正常大事务随意杀掉。

本章小结

事务通过 ACID 机制保证一组操作的可靠性。InnoDB 默认使用 REPEATABLE READ,普通 SELECT 是基于 MVCC 的快照读,UPDATE、DELETE 和锁定读是当前读。隔离级别是并发能力和一致性之间的取舍:RC 更适合高并发短事务,RR 提供更稳定的快照口径,SERIALIZABLE 则明显降低并发。

思考题

  1. ACID 分别解决什么问题?分别依赖哪些机制?
  2. RC 和 RR 的 Read View 创建时机有什么不同?
  3. 快照读和当前读的区别是什么?
  4. 为什么远程调用不应该放在数据库事务内?
  5. 设计一个余额转账事务,说明锁顺序、失败回滚和幂等处理。