强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 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 选择

维度RCRR
一致性语句级一致性事务级一致性
更少的锁(无 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 LockRecord + 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 悲观锁

维度乐观锁悲观锁
实现方式版本号/CASSELECT … 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 ;

扩展阅读