数据库锁机制
数据库锁机制
简介
数据库锁是并发控制的核心手段,用于保证多事务同时访问共享数据时的一致性和完整性。在单机单线程时代,数据修改不存在冲突问题;但在高并发 OLTP 系统中,数百个事务可能同时读写同一行数据,如果没有锁机制,就会出现脏读、丢失更新、幻读等严重的数据一致性问题。
从粒度上看,数据库锁分为行锁、表锁、页锁、意向锁;从模式上看,有共享锁(S Lock)、排他锁(X Lock)、更新锁(U Lock);从 InnoDB 实现细节看,还有记录锁(Record Lock)、间隙锁(Gap Lock)、Next-Key Lock 和插入意向锁(Insert Intention Lock)。理解锁机制不仅是为了面试,更是排查生产环境死锁、锁等待超时、性能抖动的必备技能。
为什么锁机制如此重要
在实际生产中,锁问题通常以以下几种形式出现:
- 接口超时:前端请求返回 504,根因是某个长事务持有行锁,其他事务全部排队等待
- 死锁报错:MySQL 返回
Error: 1213 Deadlock found when trying to get lock,业务逻辑中断 - 吞吐下降:并发写入时 TPS 突然下降,可能因为锁升级(行锁退化为表锁)
- 数据不一致:并发扣减库存出现超卖,因为锁粒度或隔离级别选择不当
理解锁的底层原理,才能快速定位并解决这些问题。
InnoDB 锁的分类体系
按粒度分类
| 锁类型 | 粒度 | 开销 | 并发度 | 说明 |
|---|---|---|---|---|
| 行锁(Row Lock) | 单行记录 | 高 | 高 | InnoDB 默认,只锁定匹配的索引记录 |
| 间隙锁(Gap Lock) | 索引间隙 | 中 | 中 | 锁定两行记录之间的间隙,防止插入 |
| Next-Key Lock | 记录 + 间隙 | 中 | 中 | 行锁 + 间隙锁的组合,RR 级别默认 |
| 表锁(Table Lock) | 整张表 | 低 | 低 | 通常由 MyISAM 使用,InnoDB 特定场景退化 |
| 意向锁(IS/IX) | 表级 | 低 | 高 | 快速判断表中是否有行级锁 |
按模式分类
| 锁模式 | 标记 | 兼容性 | 说明 |
|---|---|---|---|
| 共享锁(S Lock) | LOCK_S | S 与 S 兼容,S 与 X 互斥 | 也称读锁,允许其他事务读但不允许写 |
| 排他锁(X Lock) | LOCK_X | X 与所有锁互斥 | 也称写锁,不允许其他事务读写 |
| 意向共享锁(IS) | LOCK_IS | 与 IS/IX 兼容 | 表示事务打算对某些行加 S 锁 |
| 意向排他锁(IX) | LOCK_IX | 与 IS/IX 兼容 | 表示事务打算对某些行加 X 锁 |
InnoDB 特有锁类型
InnoDB 在 RR(可重复读)隔离级别下实现了一套精细的锁机制:
- 记录锁(Record Lock):锁定索引上的单条记录,不包括间隙
- 间隙锁(Gap Lock):锁定索引记录之间的间隙(开区间),防止其他事务在此间隙插入新记录。间隙锁之间不互斥,多个事务可以同时持有同一间隙的间隙锁
- Next-Key Lock:记录锁 + 间隙锁,锁定记录及其前面的间隙(左开右闭区间)。这是 InnoDB 在 RR 级别下的默认行锁算法
- 插入意向锁(Insert Intention Lock):特殊的间隙锁,在 INSERT 前获取,表示插入意向。多个事务在同一间隙插入不同位置时不会互相阻塞
实战示例
示例 1:共享锁与排他锁的使用
-- 加共享锁(读锁):其他事务可读但不可写
SELECT * FROM orders WHERE order_id = 1001 LOCK IN SHARE MODE;
-- 加排他锁(写锁):其他事务不可读也不可写(阻塞)
SELECT * FROM orders WHERE order_id = 1001 FOR UPDATE;
-- MySQL 8.0+ 语法增强
-- NOWAIT:如果锁被占用,立即返回错误而非等待
SELECT * FROM orders WHERE order_id = 1001 FOR UPDATE NOWAIT;
-- 错误:ERROR 3572 (HY000): Statement aborted because lock(s) could not be acquired immediately and NOWAIT is set.
-- SKIP LOCKED:跳过已被锁定的行,返回未被锁定的行
SELECT * FROM orders WHERE status = 'pending' FOR UPDATE SKIP LOCKED;
-- 适合任务队列的并发消费,避免多个消费者抢同一行应用场景对比:
| 场景 | 推荐锁 | 原因 |
|---|---|---|
| 任务队列消费 | FOR UPDATE SKIP LOCKED | 多个 worker 并行消费,避免争抢 |
| 库存扣减 | FOR UPDATE | 必须独占,防止超卖 |
| 报表快照读取 | LOCK IN SHARE MODE | 允许并发读,阻止写入 |
| 非阻塞获取资源 | FOR UPDATE NOWAIT | 获取失败立即返回,适合用户交互 |
示例 2:查看当前锁信息
-- MySQL 8.0 使用 performance_schema 查看锁等待链
SELECT
r.OBJECT_SCHEMA AS lock_schema,
r.OBJECT_NAME AS lock_table,
r.LOCK_TYPE AS lock_type,
r.LOCK_MODE AS lock_mode,
r.LOCK_STATUS AS lock_status,
w.REQUESTING_ENGINE_TRANSACTION_ID AS waiting_trx_id,
w.BLOCKING_ENGINE_TRANSACTION_ID AS blocking_trx_id,
t1.THREAD_ID AS waiting_thread,
t2.THREAD_ID AS blocking_thread
FROM performance_schema.data_lock_waits w
JOIN performance_schema.data_locks r ON w.OBJECT_INSTANCE_BEGIN = r.OBJECT_INSTANCE_BEGIN
JOIN performance_schema.threads t1 ON w.REQUESTING_THREAD_ID = t1.THREAD_ID
JOIN performance_schema.threads t2 ON w.BLOCKING_THREAD_ID = t2.THREAD_ID;
-- 查看当前所有持有的锁
SELECT
OBJECT_SCHEMA,
OBJECT_NAME,
LOCK_TYPE,
LOCK_MODE,
LOCK_STATUS,
INDEX_NAME
FROM performance_schema.data_locks
WHERE OBJECT_SCHEMA = 'your_db';
-- 查看死锁日志(最近一次检测到的死锁)
SHOW ENGINE INNODB STATUS;
-- 在输出中搜索 "LATEST DETECTED DEADLOCK" 部分
-- 会显示两个事务分别持有什么锁、等待什么锁
-- 查看锁等待超时设置(默认 50 秒)
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';
-- 查看死锁检测开关(默认 ON)
SHOW VARIABLES LIKE 'innodb_deadlock_detect';
-- 查看锁相关性能计数器
SHOW STATUS LIKE 'Innodb_row_lock%';
-- Innodb_row_lock_current_waits: 当前等待行锁的事务数
-- Innodb_row_lock_time: 行锁总等待时间(毫秒)
-- Innodb_row_lock_time_avg: 行锁平均等待时间
-- Innodb_row_lock_time_max: 行锁最大等待时间
-- Innodb_row_lock_waits: 行锁等待总次数示例 3:间隙锁与死锁场景复现
-- 准备测试数据
CREATE TABLE items (
id INT PRIMARY KEY,
name VARCHAR(100),
stock INT
) ENGINE=InnoDB;
INSERT INTO items VALUES (5, '商品A', 100), (10, '商品B', 200), (15, '商品C', 300);
-- 会话 A:锁定 id > 10 的记录和间隙
START TRANSACTION;
SELECT * FROM items WHERE id > 10 FOR UPDATE;
-- InnoDB 加锁分析(RR 级别):
-- 对 id=15 加记录锁
-- 对 (15, +∞) 加间隙锁
-- 对 (10, 15) 加间隙锁
-- 会话 B:锁定 id < 10 的记录和间隙
START TRANSACTION;
SELECT * FROM items WHERE id < 10 FOR UPDATE;
-- 对 id=5 加记录锁
-- 对 (-∞, 5) 加间隙锁
-- 对 (5, 10) 加间隙锁
-- 会话 A 尝试插入 id=8 → 阻塞(在会话 B 的间隙 (5, 10) 内)
INSERT INTO items (id, name, stock) VALUES (8, '测试', 0);
-- 会话 B 尝试插入 id=12 → 死锁!
-- 会话 B 等待 (10, 15) 间隙锁,但该间隙被会话 A 持有
-- 会话 A 等待 (5, 10) 间隙锁,被会话 B 持有
INSERT INTO items (id, name, stock) VALUES (12, '测试', 0);
-- ERROR 1213 (40001): Deadlock found when trying to get lock;
-- try restarting transaction死锁产生的四个必要条件(Coffman 条件):
- 互斥条件:锁一次只能被一个事务持有
- 持有并等待:事务持有锁的同时等待其他锁
- 不可抢占:锁不能被强制回收
- 循环等待:事务之间形成等待环路
示例 4:乐观锁实现(应用层)
-- 使用版本号实现乐观锁
CREATE TABLE products (
id BIGINT PRIMARY KEY,
name VARCHAR(200),
stock INT NOT NULL,
price DECIMAL(10, 2) NOT NULL,
version INT NOT NULL DEFAULT 0
);
-- 扣减库存时检查版本号
UPDATE products
SET stock = stock - 1,
version = version + 1
WHERE id = 1001
AND version = 5
AND stock > 0;
-- 应用层检查影响行数
-- 影响行数 = 1:更新成功
-- 影响行数 = 0:版本号不匹配(并发冲突)或库存不足,需重试
-- C# 乐观锁实现示例
/*
public async Task<bool> DeductStockAsync(long productId, int quantity)
{
const int maxRetries = 3;
for (int i = 0; i < maxRetries; i++)
{
var product = await _context.Products
.Where(p => p.Id == productId)
.FirstOrDefaultAsync();
if (product.Stock < quantity)
return false; // 库存不足
product.Stock -= quantity;
try
{
var rows = await _context.SaveChangesAsync();
if (rows > 0) return true; // 成功
}
catch (DbUpdateConcurrencyException)
{
// 并发冲突,重试
continue;
}
}
return false; // 重试次数用尽
}
*/示例 5:锁升级问题排查
-- 锁升级场景:WHERE 条件没有走索引,行锁退化为表锁
CREATE TABLE user_actions (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
action_type VARCHAR(50) NOT NULL,
action_time DATETIME NOT NULL,
detail TEXT,
INDEX idx_user_time (user_id, action_time)
) ENGINE=InnoDB;
-- 场景 1:走索引,行锁(正常)
-- id 上有主键索引
SELECT * FROM user_actions WHERE id = 1001 FOR UPDATE;
-- 只锁定 id=1001 这一行
-- 场景 2:走索引,行锁(正常)
-- user_id + action_time 上有联合索引
SELECT * FROM user_actions
WHERE user_id = 1001 AND action_time > '2024-01-01'
FOR UPDATE;
-- 只锁定匹配的索引记录
-- 场景 3:没走索引,表锁(危险!)
-- action_type 上没有索引
SELECT * FROM user_actions
WHERE action_type = 'login'
FOR UPDATE;
-- InnoDB 对所有记录加锁(实际上退化为表锁)
-- 其他事务对这张表的任何写入都会被阻塞!
-- 验证:EXPLAIN 确认是否走索引
EXPLAIN SELECT * FROM user_actions WHERE action_type = 'login' FOR UPDATE;
-- type: ALL → 全表扫描 → 锁升级锁与隔离级别的关系
InnoDB 支持四种隔离级别,不同级别下的加锁行为差异很大:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 加锁策略 |
|---|---|---|---|---|
| READ UNCOMMITTED | 可能 | 可能 | 可能 | 不加锁,读不加锁 |
| READ COMMITTED | 不可能 | 可能 | 可能 | 只加记录锁,不加间隙锁 |
| REPEATABLE READ | 不可能 | 不可能 | 不可能* | Next-Key Lock(记录锁 + 间隙锁) |
| SERIALIZABLE | 不可能 | 不可能 | 不可能 | 所有读加共享锁,所有写加排他锁 |
*注:InnoDB 在 RR 级别通过 MVCC + Next-Key Lock 在很大程度上防止了幻读,但在某些特殊场景(如先 UPDATE 再 SELECT)仍可能出现幻读。
RC 与 RR 的加锁差异
-- RC 级别(READ COMMITTED)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
SELECT * FROM orders WHERE id BETWEEN 5 AND 15 FOR UPDATE;
-- 只对 id=5, 10, 15 加记录锁
-- 间隙 (5,10) 和 (10,15) 不加锁
-- 其他事务可以插入 id=7, 8, 12 等记录
-- RR 级别(REPEATABLE READ)
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT * FROM orders WHERE id BETWEEN 5 AND 15 FOR UPDATE;
-- 对 id=5, 10, 15 加 Next-Key Lock
-- 锁定范围:(-∞, 5], (5, 10], (10, 15]
-- 其他事务不能插入 id=7, 8, 12 等记录优点
SQL Server 锁机制
锁粒度与模式
-- SQL Server 锁粒度(从粗到细)
-- RID — 行标识符锁(行级)
-- KEY — 键锁(索引行级)
-- PAGE — 页锁(8KB 数据页)
-- OBJECT — 对象锁(表级)
-- DATABASE — 数据库锁
-- SQL Server 锁模式
-- S (Shared) — 共享锁(读)
-- U (Update) — 更新锁(可升级为排他锁)
-- X (Exclusive) — 排他锁(写)
-- IS — 意向共享锁
-- IX — 意向排他锁
-- BU — 大容量更新锁
-- SCH-M — 架构修改锁(DDL)
-- SCH-S — 架构稳定性锁
-- 查看当前锁信息
SELECT
resource_type,
resource_database_id,
resource_associated_entity_id,
request_mode,
request_session_id,
request_status,
blocking_session_id
FROM sys.dm_tran_locks
WHERE resource_database_id = DB_ID();
-- 锁升级
-- SQL Server 当单条语句锁超过 5000 行时触发锁升级
-- 可以配置锁升级阈值
ALTER TABLE orders SET (LOCK_ESCALATION = DISABLE); -- 禁用锁升级
ALTER TABLE orders SET (LOCK_ESCALATION = AUTO); -- 自动(默认)
ALTER TABLE orders SET (LOCK_ESCALATION = TABLE); -- 表级锁升级
-- 查看锁升级事件
SELECT * FROM sys.dm_tran_locks
WHERE resource_type = 'OBJECT'
AND request_mode = 'X';
-- SQL Server 事务隔离级别
SET TRANSACTION ISOLATION LEVEL READ COMMITTED; -- 默认
SET TRANSACTION ISOLATION LEVEL READ COMMITTED SNAPSHOT; -- RCSI(推荐,减少锁争用)
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SET TRANSACTION ISOLATION LEVEL SNAPSHOT; -- 基于行版本的快照隔离
-- RCSI(READ COMMITTED SNAPSHOT ISOLATION)
-- 每个 UPDATE 都在 tempdb 中维护行版本
-- 读操作不阻塞写操作,写操作不阻塞读操作
-- 需要先开启数据库选项
ALTER DATABASE mydb SET READ_COMMITTED_SNAPSHOT ON;SQL Server 死锁排查
-- 开启死锁跟踪(扩展事件)
CREATE EVENT SESSION [DeadlockCapture] ON SERVER
ADD EVENT sqlserver.xml_deadlock_report
ADD TARGET package0.event_file(
SET filename = N'C:\SQLData\XEvents\Deadlock.xel'
);
ALTER EVENT SESSION [DeadlockCapture] ON SERVER STATE = START;
-- 查看死锁图
SELECT
CAST(event_data AS XML).value('(//deadlock/victim-list/victimProcess/@id)[1]', 'VARCHAR(50)') AS victim_id,
CAST(event_data AS XML) AS deadlock_graph
FROM sys.fn_xe_file_target_read_file(
'C:\SQLData\XEvents\Deadlock*.xel', NULL, NULL, NULL
);
-- 使用 Trace Flag 1222 记录死锁到错误日志
DBCC TRACEON(1222, -1);
-- 查看阻塞链
SELECT
blocking.session_id AS blocking_session,
blocked.session_id AS blocked_session,
blocking_sql.text AS blocking_query,
blocked_sql.text AS blocked_query,
blocked.wait_type,
blocked.wait_time,
blocked.wait_resource
FROM sys.dm_exec_requests blocked
INNER JOIN sys.dm_exec_sessions blocked_sessions ON blocked.session_id = blocked_sessions.session_id
INNER JOIN sys.dm_exec_sessions blocking ON blocked_sessions.blocking_session_id = blocking.session_id
CROSS APPLY sys.dm_exec_sql_text(blocked.sql_handle) blocked_sql
CROSS APPLY sys.dm_exec_sql_text(
(SELECT TOP 1 sql_handle FROM sys.dm_exec_requests WHERE session_id = blocking.session_id)
) blocking_sql
WHERE blocked.blocking_session_id > 0;
-- SQL Server 死锁避免策略:
-- 1. 按相同顺序访问表和行
-- 2. 保持事务简短
-- 3. 使用 RCSI 减少读写冲突
-- 4. 使用 READ COMMITTED 隔离级别(而非 SERIALIZABLE)
-- 5. 对批处理操作使用 TABLOCKX 或考虑 WITH (NOLOCK)(脏读可接受时)PostgreSQL 锁机制
PostgreSQL MVCC 与锁
-- PostgreSQL 的锁机制与 MySQL InnoDB 有本质区别
-- PostgreSQL 的 MVCC 通过多版本存储实现(旧版本保留在表中)
-- 不是通过 Undo Log 实现
-- PostgreSQL 锁类型
-- ACCESS SHARE — SELECT(最弱的锁)
-- ROW SHARE — SELECT FOR UPDATE/SHARE
-- ROW EXCLUSIVE — UPDATE/DELETE/INSERT
-- SHARE UPDATE EXCLUSIVE — VACUUM, CREATE INDEX CONCURRENTLY
-- SHARE — CREATE INDEX(不带 CONCURRENTLY)
-- SHARE ROW EXCLUSIVE
-- EXCLUSIVE — DELETE/UPDATE 全表
-- ACCESS EXCLUSIVE — ALTER TABLE, DROP TABLE, TRUNCATE(最强的锁)
-- 查看当前锁
SELECT
locktype,
relation::regclass AS table_name,
mode,
granted,
pid,
query
FROM pg_locks l
JOIN pg_stat_activity a ON l.pid = a.pid
WHERE NOT granted -- 只看等待的锁
ORDER BY pid;
-- 行级锁
SELECT * FROM accounts WHERE id = 1 FOR UPDATE; -- 排他锁
SELECT * FROM accounts WHERE id = 1 FOR NO KEY UPDATE; -- 不锁外键
SELECT * FROM accounts WHERE id = 1 FOR SHARE; -- 共享锁
SELECT * FROM accounts WHERE id = 1 FOR UPDATE NOWAIT; -- 不等待,立即报错
SELECT * FROM accounts WHERE id = 1 FOR UPDATE SKIP LOCKED; -- 跳过已锁行
-- 死锁检测
-- PostgreSQL 自动检测死锁并回滚其中一个事务
-- 查看死锁日志
-- postgresql.conf: log_lock_waits = on
-- deadlock_timeout = 1s -- 等待超过 1 秒记录日志
-- 顾问锁(Advisory Lock)
-- 应用层自定义锁,不与 SQL 锁冲突
SELECT pg_advisory_lock(12345); -- 获取锁(等待)
SELECT pg_advisory_lock(12345, 67890); -- 获取 64 位锁
SELECT pg_try_advisory_lock(12345); -- 尝试获取(不等待,返回 boolean)
SELECT pg_advisory_unlock(12345); -- 释放锁
SELECT pg_advisory_unlock_all(); -- 释放当前会话所有顾问锁
-- 顾问锁适用场景:
-- 1. 分布式任务调度(防止重复执行)
-- 2. 外部资源互斥访问
-- 3. 应用级别的业务锁分布式锁实现
Redis 分布式锁
# Redis SETNX 实现分布式锁
SET lock:resource:1 "unique_value" NX EX 30
# NX — 只有 key 不存在时才设置(原子性)
# EX — 自动过期时间 30 秒
# 释放锁(Lua 脚本保证原子性)
EVAL "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 lock:resource:1 unique_value
# Redisson 实现(推荐)
# 支持自动续期(看门狗)、可重入锁、公平锁、联锁数据库实现分布式锁
-- 基于数据库表实现分布式锁
CREATE TABLE distributed_lock (
lock_key VARCHAR(100) PRIMARY KEY,
lock_value VARCHAR(100) NOT NULL,
locked_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
expired_at DATETIME NOT NULL,
owner VARCHAR(100) NOT NULL
);
-- 获取锁(INSERT,利用主键唯一性)
INSERT INTO distributed_lock (lock_key, lock_value, expired_at, owner)
VALUES ('order:1001', 'uuid-xxx', DATE_ADD(NOW(), INTERVAL 30 SECOND), 'server-1');
-- 成功 = 获取锁,失败(主键冲突)= 锁已被占用
-- 释放锁(删除 + 校验 owner)
DELETE FROM distributed_lock
WHERE lock_key = 'order:1001'
AND lock_value = 'uuid-xxx';
-- 续期(更新过期时间)
UPDATE distributed_lock
SET expired_at = DATE_ADD(NOW(), INTERVAL 30 SECOND)
WHERE lock_key = 'order:1001'
AND lock_value = 'uuid-xxx';
-- 清理过期锁
DELETE FROM distributed_lock WHERE expired_at < NOW();锁监控指标
-- MySQL 锁监控
SHOW STATUS LIKE 'Innodb_row_lock%';
-- Innodb_row_lock_current_waits — 当前等待锁的线程数
-- Innodb_row_lock_time — 总锁等待时间(毫秒)
-- Innodb_row_lock_time_avg — 平均锁等待时间(毫秒)
-- Innodb_row_lock_waits — 总锁等待次数
-- SQL Server 锁监控
SELECT
wait_type,
waiting_tasks_count,
wait_time_ms,
avg_wait_time_ms = wait_time_ms / waiting_tasks_count
FROM sys.dm_os_wait_stats
WHERE wait_type LIKE 'LCK%'
ORDER BY wait_time_ms DESC;
-- PostgreSQL 锁监控
SELECT
locktype,
mode,
COUNT(*) AS lock_count,
COUNT(*) FILTER (WHERE NOT granted) AS waiting_count
FROM pg_locks
GROUP BY locktype, mode
ORDER BY waiting_count DESC;
-- 告警阈值建议:
-- InnoDB 行锁平均等待时间 > 50ms
-- InnoDB 行锁等待次数 > 100/分钟
-- 锁等待超时次数 > 10/分钟
-- 死锁次数 > 5/分钟缺点
总结
数据库锁机制是并发控制的底层实现,核心原则是"尽量少锁、尽量短锁"。在实际项目中,通过合理的索引设计避免锁升级、控制事务粒度减少锁持有时间、使用乐观锁替代悲观锁处理低冲突场景,是有效管理锁的三大策略。
锁机制的选择没有银弹:乐观锁适合读多写少、冲突概率低的场景,实现简单但需要重试逻辑;悲观锁适合写多冲突高的场景,一致性有保证但吞吐量受限。关键是要根据业务特征选择合适的策略,并建立完善的监控和告警体系。
关键知识点
- InnoDB 的行锁是加在索引记录上的,不走索引的查询会锁住全部记录(锁升级)
- REPEATABLE READ 级别下 InnoDB 使用 Next-Key Lock 防止幻读,RC 级别只用记录锁
- 死锁检测由
innodb_deadlock_detect控制,innodb_lock_wait_timeout设置等待超时 - MySQL 8.0 的
NOWAIT和SKIP LOCKED可以实现非阻塞读,适合队列处理场景 - 间隙锁之间不互斥,但间隙锁与插入意向锁互斥
项目落地视角
- 所有 UPDATE/DELETE 的 WHERE 条件必须走索引,否则行锁变表锁,严重影响并发
- 批量操作分批提交,每批 500-1000 条,避免长事务持有大量锁
- 使用
SELECT ... FOR UPDATE SKIP LOCKED实现任务队列的并发消费 - 监控
innodb_row_lock_waits和innodb_row_lock_time_avg指标,设置告警阈值 - 为关键业务设计死锁重试机制,使用指数退避策略(如 100ms → 200ms → 400ms)
常见误区
- 认为 InnoDB 只用行锁:实际通过索引加锁,无索引则退化为表锁,这是最常见的线上锁问题
- 认为加了 FOR UPDATE 就一定安全:还需要确认 SQL 是否真正锁定了目标行(检查 EXPLAIN)
- 忽略死锁是正常现象:在高并发系统中死锁不可避免,关键是设计好重试机制和事务边界
- 混淆乐观锁和悲观锁的适用场景:高冲突用悲观锁(如库存秒杀),低冲突用乐观锁(如用户资料更新)
- 认为事务越短越好:事务确实应该尽量短,但不能为了缩短事务而牺牲业务一致性
进阶路线
- 深入理解 InnoDB 锁的数据结构:
LOCK_REC和LOCK_TABLE的内部实现(hash 表 + 位图) - 学习分布式锁方案:Redis RedLock、Zookeeper 临时节点、etcd Lease,理解各自的优缺点
- 研究 MySQL 8.0 的
performance_schema.data_locks替代information_schema.innodb_locks的改进 - 了解其他数据库的锁机制差异:PostgreSQL 的 MVCC 实现(无 undo log,多版本存在堆中)、SQL Server 的锁升级策略
- 学习 LMAX Disruptor 等无锁并发框架的设计思想
适用场景
- 高并发库存扣减、秒杀等需要精确控制并发写入的场景
- 任务队列的并发消费,使用 SKIP LOCKED 避免重复处理
- 报表生成、数据导出等需要锁定数据快照的场景
- 金融转账等需要严格串行化的关键业务
落地建议
- 建立 SQL 审查规范:所有 DML 语句必须检查执行计划确认走索引
- 为死锁场景设计自动重试机制,指数退避最多 3 次,超过 3 次返回错误
- 使用
SHOW ENGINE INNODB STATUS定期检查死锁日志,分析死锁模式 - 对长事务设置监控告警,
innodb_kill_idle_transaction或应用层超时 - 编写锁问题排查 SOP 文档,缩短故障恢复时间
排错清单
- 检查涉及锁的 SQL 是否都走了索引(EXPLAIN 确认 type 不是 ALL)
- 查看
performance_schema.data_lock_waits找到阻塞源头和被阻塞的事务 - 确认
innodb_lock_wait_timeout设置是否合理(默认 50 秒,可根据业务调整) - 检查是否存在长事务(
information_schema.innodb_trx中 trx_started 久远的事务) - 确认锁的顺序是否一致(不同业务流程操作相同表时,加锁顺序应该相同)
复盘问题
- 最近一次死锁的根因是什么?SQL 加锁顺序是否一致?是否可以通过调整索引解决?
- 是否存在没有索引的 UPDATE/DELETE 导致锁升级?
- 长事务监控是否到位?最长事务执行时间是多少?是否包含了外部 RPC 调用?
- 行锁平均等待时间
Innodb_row_lock_time_avg是多少?是否需要优化?
