06. 锁机制与死锁处理

深入数据库锁机制:行锁、表锁、意向锁、间隙锁、Next-Key Lock,以及死锁检测算法与预防策略。

1. 锁粒度

锁类型粒度并发度开销代表
表锁整张表最低最低MyISAM
页锁一页(通常 4KB/8KB)中等中等BDB
行锁单行记录最高最高InnoDB

InnoDB 默认使用行锁,但在特定场景下会退化为表锁(如未命中索引的全表扫描)。


2. InnoDB 锁类型

2.1 锁模式

锁缩写兼容性适用操作
共享锁S与 S 兼容,与 X 冲突SELECT … LOCK IN SHARE MODE
排他锁X与任何锁冲突INSERT/UPDATE/DELETE/FOR UPDATE
意向共享锁IS表级锁,表示将获取 S 锁自动加
意向排他锁IX表级锁,表示将获取 X 锁自动加

意向锁的作用:避免加行锁时逐行检查表锁冲突。加 S/X 行前,先加 IS/IX 表锁,快速判断是否有表锁冲突。

2.2 锁算法

Record Lock(记录锁):
  锁定单行记录
  SELECT * FROM users WHERE id = 1 FOR UPDATE;
  → 在 id=1 的记录上加 X 锁

Gap Lock(间隙锁):
  锁定一个范围,但不包括记录本身
  SELECT * FROM users WHERE age > 20 AND age < 30 FOR UPDATE;
  → 锁定 (20, 30) 之间的间隙,阻止插入 age=25 的记录
  → 解决幻读问题

Next-Key Lock(临键锁):
  Record Lock + Gap Lock
  锁定记录及其前面的间隙
  → 是 InnoDB RR 级别的默认锁算法
  
  示例:id 值为 10, 20, 30
    对 id=20 加 Next-Key Lock → 锁定 (10, 20](间隙 10-20 + 记录 20)

3. 死锁

3.1 死锁示例

事务 A                    事务 B
  │                        │
  └──→ 获取 lock X(记录 a)  │
                           └──→ 获取 lock X(记录 b)
  └──→ 请求 lock X(记录 b)  │  ← 阻塞
                           └──→ 请求 lock X(记录 a)  ← 阻塞
                           
  循环等待 → 死锁!

3.2 死锁检测与处理

InnoDB 死锁检测(wait-for graph):
  检测事务间的等待关系图,发现环即判定死锁

处理策略:
  1. 选择回滚代价最小的事务(通常修改行数少的)
  2. 回滚该事务,释放锁
  3. 客户端收到 ERROR 1213 (40001): Deadlock found
  4. 应用层应捕获并重试
-- 查看死锁日志
SHOW ENGINE INNODB STATUS;
-- 在 LATEST DETECTED DEADLOCK 段中查看最近死锁详情

-- 查看当前锁等待
SELECT * FROM information_schema.INNODB_TRX;       -- 活跃事务
SELECT * FROM information_schema.INNODB_LOCKS;      -- 当前锁
SELECT * FROM information_schema.INNODB_LOCK_WAITS; -- 锁等待关系

3.3 死锁预防

策略说明
固定加锁顺序所有事务按相同顺序获取锁(如先小 id 后大 id)
降低事务粒度事务内操作尽量少,持有锁时间尽量短
NOWAIT / SKIP LOCKEDFOR UPDATE NOWAIT 锁不住立即失败;SKIP LOCKED 跳过已锁行
应用层重试捕获死锁异常后自动重试(建议指数退避)
// 应用层死锁重试
template.executeWithRetry(() -> {
    jdbcTemplate.update("UPDATE account SET balance = balance - ? WHERE id = ?", amount, fromId);
    jdbcTemplate.update("UPDATE account SET balance = balance + ? WHERE id = ?", amount, toId);
}, 3, Duration.ofMillis(100));  // 最多重试3次,间隔100ms

4. 锁相关的 SQL 提示

-- 显式加读锁
SELECT * FROM orders WHERE id = 1 LOCK IN SHARE MODE;

-- 显式加写锁
SELECT * FROM orders WHERE id = 1 FOR UPDATE;

--  NOWAIT(锁不住立即报错)
SELECT * FROM orders WHERE id = 1 FOR UPDATE NOWAIT;

-- SKIP LOCKED(跳过被锁的行,用于队列场景)
SELECT * FROM orders WHERE status = 'pending' 
  ORDER BY id FOR UPDATE SKIP LOCKED LIMIT 10;

-- 查看锁信息
SHOW STATUS LIKE 'Innodb_row_lock%';
-- Innodb_row_lock_waits:锁等待次数
-- Innodb_row_lock_time_avg:平均锁等待时间

5. MySQL 8.0 新特性

5.1 NOWAIT 与 SKIP LOCKED 的原生支持

MySQL 8.0 对 NOWAIT 和 SKIP LOCKED 的支持更加完善,成为处理高并发队列的标准方案:

-- 任务队列消费:每个 worker 拿独立的任务
BEGIN;
SELECT task_id, payload 
FROM async_tasks 
WHERE status = 'pending' 
ORDER BY priority DESC, created_at ASC
FOR UPDATE SKIP LOCKED
LIMIT 1;
-- worker 处理任务
UPDATE async_tasks SET status = 'processing' WHERE task_id = ?;
COMMIT;

5.2 数据字典锁优化

MySQL 8.0 将系统表改为事务性数据字典,DDL 操作不再需要昂贵的frm文件锁,大幅降低了元数据锁(MDL)的争用。

5.3 死锁信息增强

-- MySQL 8.0 的死锁日志更加详细
SHOW ENGINE INNODB STATUS;
-- 包含完整的事务信息、SQL 语句、锁等待链

-- 通过 performance schema 查看历史死锁
SELECT * FROM performance_schema.data_lock_waits;
SELECT * FROM performance_schema.data_locks;

6. 乐观锁与悲观锁的选型

维度悲观锁乐观锁
冲突概率高并发写低并发写 / 读多写少
实现方式SELECT ... FOR UPDATE版本号 / CAS
重试成本无(直接阻塞)冲突后需要重试
性能影响持锁期间阻塞其他事务不阻塞,冲突时回滚
适用场景金融转账、库存扣减用户信息更新、配置修改

乐观锁实现(版本号)

-- 表结构增加 version 字段
ALTER TABLE products ADD COLUMN version INT DEFAULT 0;

-- 更新时检查版本号
UPDATE products 
SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = 5;
-- 返回 affected_rows = 0 表示冲突,需重试
@Retryable(value = OptimisticLockingFailureException.class, maxAttempts = 3)
public void deductStock(Long productId) {
    Product product = productRepository.findById(productId);
    int updated = productRepository.deductStock(productId, product.getVersion());
    if (updated == 0) {
        throw new OptimisticLockingFailureException("库存并发冲突");
    }
}

7. 锁升级与锁降级

7.1 锁升级(Lock Escalation)

InnoDB 在特殊场景下会发生隐式锁升级:

-- 场景:全表扫描未命中索引
SELECT * FROM large_table WHERE unindexed_column = 'value' FOR UPDATE;
-- 由于无法使用索引定位行,InnoDB 可能退化为锁全表

避免锁升级的策略:

  • 所有高频查询字段建立索引
  • WHERE 条件避免函数操作(导致索引失效)
  • 大事务拆分为小事务

7.2 锁降级

InnoDB 在事务提交时自动释放所有锁,不支持显式锁降级(如将 X 锁降为 S 锁)。如果业务需要分阶段访问控制,应该拆分为多个事务。


8. 锁兼容性矩阵

请求锁 \ 已有锁ISIXSX
IS✅✅✅❌
IX✅✅❌❌
S✅❌✅❌
X❌❌❌❌
  • IS/IX 之间全兼容:意向锁的设计目的是提高加锁效率,而非实际阻塞
  • S 与 IX 冲突:确认读事务无法与即将写数据的事务并发
  • X 与任何锁冲突:写操作独占性最强

9. 常见锁问题诊断

9.1 查询锁等待链

-- 查看当前锁等待关系
SELECT 
    r.trx_id waiting_trx_id,
    r.trx_mysql_thread_id waiting_thread,
    r.trx_query waiting_query,
    b.trx_id blocking_trx_id,
    b.trx_mysql_thread_id blocking_thread,
    b.trx_query blocking_query
FROM information_schema.innodb_lock_waits w
INNER JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id
INNER JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id;

9.2 慢查询与锁等待关联

-- 开启 performance_schema(MySQL 8.0 默认开启)
SELECT 
    event_name,
    sql_text,
    timer_wait / 1000000000 AS wait_ms,
    lock_time / 1000000000 AS lock_ms
FROM performance_schema.events_statements_history_long
WHERE sql_text LIKE '%FOR UPDATE%'
ORDER BY timer_wait DESC
LIMIT 10;

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「database」更多文章

  1. PostgreSQL 锁与阻塞分析
  2. COPY 与批量数据加载优化
  3. pgvector 向量检索与混合查询