MySQL 完全指南 / 第 10 章:事务与锁
第 10 章:事务与锁
事务保证数据一致性,锁保证并发安全。本章深入 ACID、MVCC 和死锁处理。
10.1 事务基础
10.1.1 ACID 特性
| 特性 | 英文 | 说明 | 实现机制 |
|---|
| 原子性 | Atomicity | 事务要么全部成功,要么全部回滚 | Undo Log |
| 一致性 | Consistency | 事务前后数据状态一致 | 由应用+数据库共同保证 |
| 隔离性 | Isolation | 并发事务互不干扰 | MVCC + 锁 |
| 持久性 | Durability | 提交后数据永久保存 | Redo Log |
10.1.2 事务语法
-- 开启事务
START TRANSACTION; -- 或 BEGIN;
-- 执行操作
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- 检查结果
-- 如果成功
COMMIT;
-- 如果失败
ROLLBACK;
-- SAVEPOINT:事务内保存点
START TRANSACTION;
INSERT INTO orders (order_no, user_id, total_amount) VALUES ('ORD001', 1, 100);
SAVEPOINT sp1;
INSERT INTO order_items (order_id, product_id, quantity) VALUES (1, 101, 2);
-- 如果插入失败
ROLLBACK TO SAVEPOINT sp1;
COMMIT; -- 只提交 orders,order_items 被回滚
-- 设置事务自动提交
SET autocommit = 0; -- 关闭自动提交(每条语句都需手动 COMMIT)
SET autocommit = 1; -- 开启自动提交(默认)
10.2 隔离级别
10.2.1 四种隔离级别
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 性能 |
|---|
| READ UNCOMMITTED | ✅ 可能 | ✅ 可能 | ✅ 可能 | 最好 |
| READ COMMITTED (RC) | ❌ 不会 | ✅ 可能 | ✅ 可能 | 较好 |
| REPEATABLE READ (RR) | ❌ 不会 | ❌ 不会 | ⚠️ InnoDB 通过 MVCC+Gap Lock 解决 | MySQL 默认 |
| SERIALIZABLE | ❌ 不会 | ❌ 不会 | ❌ 不会 | 最差 |
-- 查看当前隔离级别
SELECT @@transaction_isolation;
-- 设置会话隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 设置全局隔离级别(需重启连接生效)
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
10.2.2 各隔离级别详解
READ UNCOMMITTED
-- 会话 A
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;
UPDATE accounts SET balance = 500 WHERE id = 1;
-- 尚未 COMMIT
-- 会话 B(同时)
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT balance FROM accounts WHERE id = 1;
-- 读到 500(脏读:会话 A 可能会回滚)
READ COMMITTED
-- 会话 A
START TRANSACTION;
UPDATE accounts SET balance = 500 WHERE id = 1;
COMMIT;
-- 会话 B(RC 隔离级别)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 1; -- 第一次读:可能是旧值或新值
-- 假设会话 A 在这之间 COMMIT
SELECT balance FROM accounts WHERE id = 1; -- 第二次读:读到 500(不可重复读)
COMMIT;
REPEATABLE READ(MySQL 默认)
-- 会话 B(RR 隔离级别)
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 1; -- 读到 300
-- 无论会话 A 怎么修改并 COMMIT
SELECT balance FROM accounts WHERE id = 1; -- 仍然读到 300(一致性读)
COMMIT;
10.2.3 RC vs RR 选择
| 维度 | RC | RR |
|---|
| 一致性 | 语句级一致性 | 事务级一致性 |
| 锁 | 更少的锁(无 Gap Lock) | 更多的锁(Gap Lock) |
| 死锁 | 较少 | 较多 |
| 主从复制 | 可能有问题 | ROW 格式下安全 |
| 适用场景 | 高并发 OLTP | 绝大多数场景 |
💡 阿里等大厂常用 RC:RC 下 InnoDB 的锁更少,并发性能更好,但需要应用层处理不可重复读问题。
10.3 MVCC(多版本并发控制)
10.3.1 MVCC 原理
MVCC 通过保存数据的多个版本,实现非锁定读(一致性读),避免读写冲突。
Undo Log 链
数据行 → 当前版本 → 旧版本1 → 旧版本2 → ...
v3 v2 v1
↑
聚簇索引中存储最新版本
旧版本存储在 Undo Log 中
10.3.2 核心概念
| 概念 | 说明 |
|---|
| DB_TRX_ID | 最近修改该行的事务 ID |
| DB_ROLL_PTR | 指向 Undo Log 中旧版本的指针 |
| Read View | 事务创建时的活跃事务快照 |
| 可见性判断 | 根据 Read View 判断某版本对当前事务是否可见 |
10.3.3 Read View 的生成时机
| 隔离级别 | Read View 生成时机 | 效果 |
|---|
| RC | 每次 SELECT 都生成新的 | 每次读可能看到不同版本 |
| RR | 事务第一次 SELECT 时生成 | 整个事务期间看到一致的快照 |
10.3.4 当前读 vs 快照读
-- 快照读(一致性读,走 MVCC)
SELECT * FROM accounts WHERE id = 1; -- 读取的是 Read View 时的快照
-- 当前读(读取最新已提交数据,加锁)
SELECT * FROM accounts WHERE id = 1 FOR UPDATE; -- 加 X 锁
SELECT * FROM accounts WHERE id = 1 LOCK IN SHARE MODE; -- 加 S 锁
INSERT / UPDATE / DELETE -- 都是当前读
10.4 锁机制
10.4.1 锁类型
| 锁类型 | 说明 | 兼容性 |
|---|
| S 锁(Shared Lock) | 共享锁,读锁 | S 与 S 兼容,S 与 X 互斥 |
| X 锁(Exclusive Lock) | 排他锁,写锁 | X 与所有锁互斥 |
| IS 锁(Intention Shared) | 意向共享锁(表级) | 与 IX 兼容 |
| IX 锁(Intention Exclusive) | 意向排他锁(表级) | 与 IS 兼容 |
-- 加共享锁
SELECT * FROM accounts WHERE id = 1 LOCK IN SHARE MODE;
-- 或 8.0+:
SELECT * FROM accounts WHERE id = 1 FOR SHARE;
-- 加排他锁
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
10.4.2 行锁类型
| 锁类型 | 锁定范围 | 说明 |
|---|
| Record Lock | 索引记录本身 | 锁定已存在的记录 |
| Gap Lock | 索引记录之间的间隙 | RR 隔离级别,防止幻读 |
| Next-Key Lock | Record + Gap | 默认的行锁类型 |
-- Record Lock 示例
-- 假设 id = 1 存在
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
-- 只锁定 id = 1 这条记录
-- Gap Lock 示例
-- 假设 id 有 1, 5, 10 三条记录
SELECT * FROM accounts WHERE id = 7 FOR UPDATE;
-- 锁定 (5, 10) 这个间隙,防止插入 id = 7
-- Next-Key Lock 示例
SELECT * FROM accounts WHERE id = 5 FOR UPDATE;
-- 锁定 (1, 5] 即记录 5 加前面的间隙
10.4.3 表锁
-- 显式加表锁
LOCK TABLES accounts READ; -- 读锁(所有会话可读,不可写)
LOCK TABLES accounts WRITE; -- 写锁(只有当前会话可读写)
-- 释放表锁
UNLOCK TABLES;
-- 查看当前锁
SHOW OPEN TABLES WHERE In_use > 0;
10.4.4 查看锁信息
-- 查看当前锁等待
SELECT * FROM performance_schema.data_lock_waits;
-- 查看所有持有的锁
SELECT * FROM performance_schema.data_locks;
-- 查看 InnoDB 锁信息
SHOW ENGINE INNODB STATUS\G
-- 在 "TRANSACTIONS" 和 "LATEST DETECTED DEADLOCK" 部分
-- MySQL 8.0 查看锁等待
SELECT
w.REQUESTING_ENGINE_TRANSACTION_ID AS waiting_trx,
w.BLOCKING_ENGINE_TRANSACTION_ID AS blocking_trx,
l.LOCK_TYPE,
l.LOCK_MODE,
l.OBJECT_NAME
FROM performance_schema.data_lock_waits w
JOIN performance_schema.data_locks l ON w.REQUESTING_ENGINE_LOCK_ID = l.ENGINE_LOCK_ID;
10.5 死锁
10.5.1 死锁原理
事务 A: 锁定 id=1 → 请求锁定 id=2
事务 B: 锁定 id=2 → 请求锁定 id=1
→ 死锁!
10.5.2 死锁示例与解决
-- 会话 A
START TRANSACTION;
UPDATE accounts SET balance = 100 WHERE id = 1; -- 锁定 id=1
-- 等待...
-- 会话 B(同时)
START TRANSACTION;
UPDATE accounts SET balance = 200 WHERE id = 2; -- 锁定 id=2
UPDATE accounts SET balance = 300 WHERE id = 1; -- 等待 id=1 → 死锁
-- 会话 A 继续
UPDATE accounts SET balance = 400 WHERE id = 2; -- 等待 id=2 → 死锁
-- MySQL 检测到死锁,回滚其中一个事务
-- ERROR 1213 (40001): Deadlock found when trying to get lock
10.5.3 死锁预防
-- 1. 统一加锁顺序(按 ID 从小到大)
UPDATE accounts SET balance = 100 WHERE id = 1; -- 先锁小的
UPDATE accounts SET balance = 200 WHERE id = 2; -- 再锁大的
-- 2. 减少事务持有时间
-- ❌ 事务中做大量计算
START TRANSACTION;
-- ... 大量业务逻辑 ...
UPDATE accounts SET balance = 100 WHERE id = 1;
COMMIT;
-- ✅ 计算在事务外,事务只做数据操作
-- ... 大量业务逻辑 ...
START TRANSACTION;
UPDATE accounts SET balance = 100 WHERE id = 1;
COMMIT;
-- 3. 使用较低隔离级别(RC 无 Gap Lock,死锁更少)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 4. 设置锁等待超时
SET GLOBAL innodb_lock_wait_timeout = 10; -- 默认 50 秒
10.6 乐观锁与悲观锁
10.6.1 悲观锁
-- 先锁定数据,再操作
START TRANSACTION;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE; -- 加 X 锁
-- 业务处理
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;
10.6.2 乐观锁
-- 不加锁,通过版本号检测冲突
-- 表结构(添加 version 字段)
CREATE TABLE products (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(200),
stock INT UNSIGNED NOT NULL DEFAULT 0,
version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号'
);
-- 扣减库存(乐观锁方式)
-- 第 1 步:读取当前版本号
SELECT id, stock, version FROM products WHERE id = 1;
-- 假设返回:stock=100, version=5
-- 第 2 步:更新时检查版本号
UPDATE products
SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = 5;
-- 如果 affected_rows = 1 → 成功
-- 如果 affected_rows = 0 → 版本冲突,需要重试
💡 乐观锁 vs 悲观锁:
| 维度 | 乐观锁 | 悲观锁 |
|---|
| 实现方式 | 版本号/CAS | SELECT … FOR UPDATE |
| 冲突检测 | 更新时检测 | 加锁前阻塞 |
| 适用场景 | 读多写少、冲突少 | 写多、冲突多 |
| 性能 | 冲突少时高 | 冲突少时低 |
业务场景
场景 1:转账事务
DELIMITER //
CREATE PROCEDURE transfer(
IN from_id BIGINT,
IN to_id BIGINT,
IN amount DECIMAL(12,2)
)
BEGIN
DECLARE from_balance DECIMAL(12,2);
START TRANSACTION;
-- 锁定转出账户
SELECT balance INTO from_balance
FROM accounts WHERE id = from_id FOR UPDATE;
-- 检查余额
IF from_balance < amount THEN
ROLLBACK;
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '余额不足';
END IF;
-- 执行转账
UPDATE accounts SET balance = balance - amount WHERE id = from_id;
UPDATE accounts SET balance = balance + amount WHERE id = to_id;
-- 记录流水
INSERT INTO transactions (from_id, to_id, amount, created_at)
VALUES (from_id, to_id, amount, NOW());
COMMIT;
END //
DELIMITER ;
扩展阅读