记录锁 (Record Lock)
📋 概述
记录锁(Record Lock)是 MySQL InnoDB 引擎中最基础的锁算法,它锁定的是索引记录本身。记录锁是行级锁的核心实现方式,通过锁定聚簇索引或二级索引的记录来实现对数据行的保护。
核心特点
✅ 优点:
- 精确锁定: 只锁定需要的索引记录
- 高并发: 不同记录可以并行访问
- 基础性强: 其他锁算法的基础
- 实现成熟: InnoDB 核心功能,稳定可靠
❌ 缺点:
- 依赖索引: 必须通过索引访问才能生效
- 单记录限制: 无法防止幻读
- 开销较大: 每个锁记录都需要维护
- 可能升级: 大量记录锁可能升级为表锁
适用场景
- 主键查询: 基于主键的精确查询和更新
- 唯一索引查询: 基于唯一索引的操作
- 单行更新: UPDATE/DELETE 单行记录
- 点查询: WHERE id = ? 类型的查询
- 所有行级锁场景: 记录锁是行级锁的基础
🔧 工作原理
基于聚簇索引的锁定
InnoDB 的数据存储在聚簇索引中,记录锁实际上是锁定聚簇索引的记录:
聚簇索引结构:
┌─────────────────┐
│ 主键值: 1 │ ← 锁定这条索引记录
│ 数据: {...} │
│ 下一个指针: ... │
└─────────────────┘sql
-- 基于主键的查询会锁定聚簇索引记录
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- 锁定主键值为 1 的聚簇索引记录基于二级索引的锁定
如果查询使用二级索引,会同时锁定二级索引记录和对应的聚簇索引记录:
sql
-- 假设 email 是二级索引
SELECT * FROM users WHERE email = 'john@example.com' FOR UPDATE;锁定过程:
- 在二级索引上找到
email = 'john@example.com'的记录并加锁 - 通过二级索引记录的主键值,找到对应的聚簇索引记录
- 在聚簇索引记录上也加锁
结果: 两个索引记录都被锁定
锁的模式
记录锁可以是共享锁或排他锁:
1. 记录共享锁 (Record S Lock)
sql
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;
-- 在 id=1 的聚簇索引记录上加 S 锁2. 记录排他锁 (Record X Lock)
sql
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- 在 id=1 的聚簇索引记录上加 X 锁
UPDATE users SET name = 'test' WHERE id = 1;
-- 自动在 id=1 的聚簇索引记录上加 X 锁💻 MySQL InnoDB 实现
基本用法
sql
-- 主键查询加锁
BEGIN;
SELECT * FROM users WHERE id = 1 FOR UPDATE; -- 记录锁
UPDATE users SET name = 'test' WHERE id = 1;
COMMIT;
-- 二级索引查询加锁
SELECT * FROM users WHERE email = 'john@example.com' FOR UPDATE;
-- 锁定二级索引记录 + 聚簇索引记录
-- 多行记录锁
SELECT * FROM users WHERE id IN (1, 2, 3) FOR UPDATE;
-- 锁定 3 条记录查看记录锁
sql
-- 查看当前的记录锁
SELECT
lock_id,
lock_trx_id,
lock_mode,
lock_type,
lock_table,
lock_index,
lock_space,
lock_page,
lock_rec,
lock_data
FROM performance_schema.data_locks
WHERE lock_type = 'RECORD';输出示例:
lock_id | lock_mode | lock_index | lock_data
----------|-----------|------------|----------
12345:100 | X | PRIMARY | 1解读:
lock_mode = X: 排他锁lock_index = PRIMARY: 聚簇索引lock_data = 1: 主键值为 1 的记录
唯一索引 vs 普通索引
唯一索引的记录锁
sql
-- 假设 username 是唯一索引
SELECT * FROM users WHERE username = 'john' FOR UPDATE;锁定行为:
- 只锁定匹配的唯一索引记录
- 不会锁定间隙
- 其他事务可以插入新的 username
普通索引的记录锁
sql
-- 假设 age 是普通索引(非唯一)
SELECT * FROM users WHERE age = 18 FOR UPDATE;锁定行为:
- 锁定所有 age=18 的索引记录
- 在 REPEATABLE READ 级别下,还会锁定间隙(Next-Key Lock)
- 防止其他事务插入 age=18 的新记录
📊 性能影响分析
并发度评估
| 场景 | 并发度 | 说明 |
|---|---|---|
| 不同记录的查询 | ⭐⭐⭐⭐⭐ | 完全并行 |
| 同一记录的查询 | ⭐ | 串行执行 |
| 范围查询 | ⭐⭐⭐ | 锁定多记录 |
| 无索引查询 | ⭐ | 退化为表锁 |
性能测试
sql
-- 测试场景:100个并发事务更新不同记录
-- 使用记录锁
-- 100个事务并行执行,总耗时:~1秒
BEGIN;
SELECT * FROM users WHERE id = ? FOR UPDATE; -- 不同的 id
UPDATE users SET balance = balance - 100 WHERE id = ?;
COMMIT;
-- 更新同一记录
-- 100个事务串行执行,总耗时:~50秒
BEGIN;
SELECT * FROM users WHERE id = 1 FOR UPDATE; -- 相同的 id
UPDATE users SET balance = balance - 100 WHERE id = 1;
COMMIT;🎯 最佳实践
✅ 推荐做法
1. 确保查询使用索引
sql
-- ✅ 添加索引
CREATE INDEX idx_email ON users(email);
-- 使用索引查询
SELECT * FROM users WHERE email = 'john@example.com' FOR UPDATE;2. 精确锁定需要的记录
sql
-- ✅ 使用主键精确定位
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- ❌ 避免范围过大
SELECT * FROM users WHERE age > 18 FOR UPDATE; -- 锁定太多记录3. 批量操作优化
sql
-- ❌ 逐行处理
FOR i IN 1..1000 LOOP
SELECT * FROM users WHERE id = i FOR UPDATE;
UPDATE users SET ... WHERE id = i;
END LOOP;
-- ✅ 批量处理
SELECT * FROM users WHERE id BETWEEN 1 AND 1000 FOR UPDATE;
UPDATE users SET ... WHERE id BETWEEN 1 AND 1000;❌ 避免陷阱
1. 避免无索引的记录锁
sql
-- ❌ 危险:全表扫描,退化为表锁
SELECT * FROM users WHERE age > 18 FOR UPDATE; -- age 无索引
-- ✅ 添加索引
CREATE INDEX idx_age ON users(age);2. 避免锁定过多记录
sql
-- ❌ 不推荐:锁定整个表
SELECT * FROM users FOR UPDATE;
-- ✅ 推荐:只锁定需要的记录
SELECT * FROM users WHERE id = 1 FOR UPDATE;🔍 监控与诊断
sql
-- 查看记录锁
SELECT
lock_id,
lock_mode,
lock_index,
lock_data,
trx.trx_id,
trx.trx_state,
trx.trx_started
FROM performance_schema.data_locks locks
JOIN information_schema.innodb_trx trx ON locks.lock_trx_id = trx.trx_id
WHERE locks.lock_type = 'RECORD'
ORDER BY trx.trx_started;
-- 查看记录锁等待
SELECT * FROM performance_schema.data_lock_waits;🚨 常见问题
Q1: 记录锁和行级锁的关系?
答: 记录锁是行级锁的实现方式。行级锁是概念,记录锁是具体算法。
Q2: 记录锁能防止幻读吗?
答: 不能。需要配合间隙锁(Gap Lock)或临键锁(Next-Key Lock)才能防止幻读。
📚 相关资源
内部链接
外部资源
最后更新: 2026-04-12
维护状态: ✅ 完整