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 LOCKED | FOR 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. 锁兼容性矩阵
| 请求锁 \ 已有锁 | IS | IX | S | X |
|---|---|---|---|---|
| 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;
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。