行级锁 (Row-Level Lock)
📋 概述
行级锁是数据库锁机制中粒度最细的一种锁类型(不考虑页级锁),它锁定的是表中的单个数据行。当一个事务获得某行的行级锁后,其他事务对该行的某些操作会被阻塞,但不影响其他行的访问。
核心特点
✅ 优点:
- 并发度高: 不同事务可以同时操作不同的行
- 冲突概率低: 只锁定需要的行,减少锁竞争
- 灵活性好: 精确控制锁定范围
- 扩展性强: 适合高并发的 OLTP 系统
❌ 缺点:
- 开销大: 需要维护大量锁对象,内存占用多
- 加锁慢: 需要通过索引定位到具体行
- 可能死锁: 多个事务互相等待对方持有的行锁
- 实现复杂: 数据库引擎实现难度高
- 可能升级: 锁数量过多时可能升级为表锁
适用场景
- 高并发 OLTP: 电商订单、银行转账等
- 点对点操作: 更新特定用户信息
- 短事务: 快速完成的操作
- 索引良好的查询: 可以通过索引精确定位
- 读写混合: 需要同时支持读写操作
🔧 工作原理
基于索引的锁定
行级锁的实现依赖于索引。数据库通过索引定位到具体的行,然后在该行上加锁。
sql
-- 假设 id 是主键
SELECT * FROM users WHERE id = 1 FOR UPDATE;锁定过程:
- 通过主键索引找到
id = 1的记录 - 在该记录的聚簇索引上加 X 锁(排他锁)
- 其他事务尝试锁定同一行时被阻塞
⚠️ 重要:无索引时的锁升级
如果查询条件没有使用索引,行级锁会退化为表级锁:
sql
-- name 字段没有索引
SELECT * FROM users WHERE name = 'john' FOR UPDATE;后果:
- InnoDB 会扫描所有行
- 每行都加上锁
- 实际效果等同于表锁
- 严重影响并发性能
解决: 为查询条件添加合适的索引
sql
CREATE INDEX idx_name ON users(name);锁的模式
行级锁主要有两种模式:
1. 共享锁 (S锁 / 读锁)
- 多个事务可以同时持有同一行的共享锁
- 允许读取该行数据
- 阻止其他事务获取排他锁
sql
-- MySQL
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;
-- PostgreSQL
SELECT * FROM users WHERE id = 1 FOR SHARE;
-- SQL Server
SELECT * FROM users WITH (HOLDLOCK, ROWLOCK) WHERE id = 1;2. 排他锁 (X锁 / 写锁)
- 只有一个事务可以持有某行的排他锁
- 允许读取和修改该行数据
- 阻止其他事务获取任何类型的锁
sql
-- MySQL
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- PostgreSQL
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- SQL Server
SELECT * FROM users WITH (UPDLOCK, ROWLOCK) WHERE id = 1;
-- Oracle
SELECT * FROM users WHERE id = 1 FOR UPDATE;DML 操作的自动加锁
执行 DML 语句时,数据库会自动添加行级锁:
| 操作 | 锁模式 | 说明 |
|---|---|---|
INSERT | X 锁 | 插入新行时加排他锁 |
UPDATE | X 锁 | 更新行时加排他锁 |
DELETE | X 锁 | 删除行时加排他锁 |
SELECT ... FOR UPDATE | X 锁 | 显式加排他锁 |
SELECT ... LOCK IN SHARE MODE | S 锁 | 显式加共享锁 |
sql
-- 这些操作都会自动加行级锁
UPDATE users SET name = 'test' WHERE id = 1; -- 自动加 X 锁
DELETE FROM users WHERE id = 1; -- 自动加 X 锁
INSERT INTO users VALUES (1, 'john'); -- 自动加 X 锁💻 各数据库实现
MySQL (InnoDB)
InnoDB 引擎默认使用行级锁,是其核心特性之一。
基本用法
sql
-- 显式加排他锁
BEGIN;
SELECT * FROM users WHERE id = 1 FOR UPDATE;
UPDATE users SET balance = balance - 100 WHERE id = 1;
COMMIT;
-- 显式加共享锁
BEGIN;
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;
-- 其他事务也可以加共享锁读取
COMMIT;
-- 不等待立即返回
SELECT * FROM users WHERE id = 1 FOR UPDATE NOWAIT;
-- 跳过被锁定的行
SELECT * FROM users WHERE id IN (1,2,3) FOR UPDATE SKIP LOCKED;锁的查看
sql
-- MySQL 8.0+ 查看行锁
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;
-- 查看锁等待
SELECT * FROM performance_schema.data_lock_waits;
-- 传统方式
SHOW ENGINE INNODB STATUS\G参数配置
ini
# my.cnf
# 行锁等待超时时间(秒),默认50秒
innodb_lock_wait_timeout = 50
# 死锁检测(默认开启)
innodb_deadlock_detect = ON
# 监控启用
performance_schema = ON注意事项
1. 必须通过索引访问才能使用行锁
sql
-- ✅ 使用索引,行锁
SELECT * FROM users WHERE id = 1 FOR UPDATE; -- id 是主键
-- ❌ 全表扫描,退化为表锁
SELECT * FROM users WHERE age > 18 FOR UPDATE; -- age 无索引2. 范围查询会锁定多行
sql
-- 锁定 id 在 1-10 之间的所有行
SELECT * FROM users WHERE id BETWEEN 1 AND 10 FOR UPDATE;3. 注意 Next-Key Lock 的影响
在 REPEATABLE READ 隔离级别下,InnoDB 使用临键锁(记录锁 + 间隙锁),可能锁定比预期更多的行。
PostgreSQL
PostgreSQL 的行级锁实现基于 MVCC,读写不冲突。
基本用法
sql
-- 显式加排他锁
BEGIN;
SELECT * FROM users WHERE id = 1 FOR UPDATE;
UPDATE users SET balance = balance - 100 WHERE id = 1;
COMMIT;
-- 显式加共享锁
SELECT * FROM users WHERE id = 1 FOR SHARE;
-- 更强的锁模式
SELECT * FROM users WHERE id = 1 FOR NO KEY UPDATE; -- 不影响外键检查
SELECT * FROM users WHERE id = 1 FOR KEY SHARE; -- 最弱的锁
-- 不等待
SELECT * FROM users WHERE id = 1 FOR UPDATE NOWAIT;
-- 跳过被锁定的行
SELECT * FROM users WHERE id IN (1,2,3) FOR UPDATE SKIP LOCKED;MVCC 与行锁
PostgreSQL 使用多版本并发控制(MVCC):
- 读操作不加锁: SELECT 不会阻塞其他事务
- 写操作加锁: UPDATE/DELETE 会在行上加排他锁
- 快照隔离: 每个事务看到的数据快照
sql
-- 事务 A
BEGIN;
UPDATE users SET name = 'alice' WHERE id = 1;
-- 持有行锁,但未提交
-- 事务 B
SELECT * FROM users WHERE id = 1;
-- ✅ 不被阻塞,读到旧版本数据
-- 事务 B 尝试更新
UPDATE users SET name = 'bob' WHERE id = 1;
-- ❌ 被阻塞,等待事务 A 提交或回滚查看锁信息
sql
-- 查看所有行锁
SELECT
l.locktype,
l.relation::regclass AS table_name,
l.page,
l.tuple,
l.mode,
l.granted,
a.pid,
a.usename,
a.query
FROM pg_locks l
JOIN pg_stat_activity a ON l.pid = a.pid
WHERE l.locktype = 'tuple' -- tuple 表示行锁
ORDER BY l.relation;
-- 查看锁等待
SELECT
blocked_locks.pid AS blocked_pid,
blocking_locks.pid AS blocking_pid,
blocked_activity.query AS blocked_statement,
blocking_activity.query AS blocking_statement
FROM pg_catalog.pg_locks blocked_locks
JOIN pg_catalog.pg_stat_activity blocked_activity ON blocked_activity.pid = blocked_locks.pid
JOIN pg_catalog.pg_locks blocking_locks
ON blocking_locks.locktype = blocked_locks.locktype
AND blocking_locks.relation = blocked_locks.relation
AND blocking_locks.page = blocked_locks.page
AND blocking_locks.tuple = blocked_locks.tuple
AND blocking_locks.granted
JOIN pg_catalog.pg_stat_activity blocking_activity ON blocking_activity.pid = blocking_locks.pid
WHERE NOT blocked_locks.granted;SQL Server
SQL Server 默认使用行级锁,并有锁升级机制。
基本用法
sql
-- 使用行锁提示
BEGIN TRANSACTION;
SELECT * FROM users WITH (ROWLOCK, UPDLOCK) WHERE id = 1;
UPDATE users SET balance = balance - 100 WHERE id = 1;
COMMIT TRANSACTION;
-- 强制使用行锁(避免升级)
UPDATE users WITH (ROWLOCK) SET name = 'test' WHERE id = 1;
-- 查看使用的锁
SELECT * FROM sys.dm_tran_locks
WHERE resource_type = 'KEY'; -- KEY 表示行锁锁升级控制
当行锁数量超过阈值时,SQL Server 会自动升级为表锁:
sql
-- 禁用锁升级
ALTER TABLE users SET (LOCK_ESCALATION = DISABLE);
-- 设置升级阈值
ALTER TABLE users SET (LOCK_ESCALATION_THRESHOLD = 1000);查看行锁
sql
-- 查看当前的行锁
SELECT
tl.request_session_id,
OBJECT_NAME(p.object_id) AS table_name,
i.name AS index_name,
tl.resource_type,
tl.request_mode,
tl.request_status,
es.host_name,
es.program_name
FROM sys.dm_tran_locks tl
JOIN sys.partitions p ON tl.resource_associated_entity_id = p.hobt_id
JOIN sys.indexes i ON p.object_id = i.object_id AND p.index_id = i.index_id
JOIN sys.dm_exec_sessions es ON tl.request_session_id = es.session_id
WHERE tl.resource_type = 'KEY'
ORDER BY tl.request_session_id;Oracle
Oracle 的行级锁实现也基于 MVCC,是其核心特性。
基本用法
sql
-- 显式加行锁
BEGIN
SELECT * INTO user_record FROM users WHERE id = 1 FOR UPDATE;
UPDATE users SET balance = balance - 100 WHERE id = 1;
COMMIT;
END;
-- 不等待
SELECT * FROM users WHERE id = 1 FOR UPDATE NOWAIT;
-- 指定等待时间
SELECT * FROM users WHERE id = 1 FOR UPDATE WAIT 10;
-- 跳过已锁定的行
SELECT * FROM users WHERE id IN (1,2,3) FOR UPDATE SKIP LOCKED;MVCC 特性
Oracle 是最早实现 MVCC 的数据库之一:
- 读不加锁: SELECT 从不阻塞其他操作
- 写加锁: UPDATE/DELETE 在行上加排他锁
- ** undo 日志**: 保留旧版本数据供一致性读
sql
-- 查看行锁
SELECT
s.sid,
s.serial#,
s.username,
l.type,
l.lmode,
l.request,
o.object_name,
s.row_wait_obj#,
s.row_wait_file#,
s.row_wait_block#,
s.row_wait_row#
FROM v$session s
JOIN v$lock l ON s.sid = l.sid
JOIN dba_objects o ON l.id1 = o.object_id
WHERE l.type = 'TX' -- TX = 事务锁(行锁)
ORDER BY o.object_name;📊 性能影响分析
并发度评估
| 场景 | 并发度 | 说明 |
|---|---|---|
| 不同行的操作 | ⭐⭐⭐⭐⭐ | 完全并行 |
| 同一行的读操作 | ⭐⭐⭐⭐⭐ | 共享锁兼容 |
| 同一行的写操作 | ⭐ | 串行执行 |
| 范围查询 | ⭐⭐⭐ | 锁定多行 |
| 无索引查询 | ⭐ | 退化为表锁 |
开销对比
| 指标 | 行级锁 | 表级锁 |
|---|---|---|
| 内存占用 | 高(N个锁对象) | 低(1个锁对象) |
| 加锁速度 | 慢(需索引查找) | 快 |
| 锁冲突概率 | 低 | 高 |
| 并发度 | 高 | 低 |
| 死锁风险 | 有 | 无 |
性能测试示例
sql
-- 测试场景:100个并发事务更新不同行
-- 使用行级锁
-- 100个事务并行执行,总耗时:~1秒
BEGIN;
UPDATE users SET balance = balance - 100 WHERE id = ?;
COMMIT;
-- 使用表级锁
-- 100个事务串行执行,总耗时:~50秒
LOCK TABLES users WRITE;
UPDATE users SET balance = balance - 100 WHERE id = ?;
UNLOCK TABLES;🎯 最佳实践
✅ 推荐做法
1. 确保查询使用索引
sql
-- ✅ 添加索引
CREATE INDEX idx_user_email ON users(email);
-- 使用索引查询
SELECT * FROM users WHERE email = 'john@example.com' FOR UPDATE;2. 缩短事务时间
sql
-- ❌ 不推荐:长时间持有行锁
BEGIN;
SELECT * FROM orders WHERE id = 1 FOR UPDATE;
-- 调用外部 API(耗时操作)
CALL process_order(1);
UPDATE orders SET status = 'processed' WHERE id = 1;
COMMIT;
-- ✅ 推荐:快速完成
BEGIN;
SELECT * FROM orders WHERE id = 1 FOR UPDATE;
UPDATE orders SET status = 'processed' WHERE id = 1;
COMMIT;
-- 之后再调用外部 API
CALL process_order(1);3. 固定加锁顺序避免死锁
sql
-- ❌ 可能导致死锁
-- 事务 A: 先锁用户1,再锁用户2
SELECT * FROM users WHERE id = 1 FOR UPDATE;
SELECT * FROM users WHERE id = 2 FOR UPDATE;
-- 事务 B: 先锁用户2,再锁用户1
SELECT * FROM users WHERE id = 2 FOR UPDATE;
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- ✅ 固定顺序:总是按 id 升序加锁
SELECT * FROM users WHERE id IN (1,2) ORDER BY id FOR UPDATE;4. 使用合适的隔离级别
sql
-- 读已提交(大多数场景足够)
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 可重复读(需要更强一致性)
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;5. 批量操作优化
sql
-- ❌ 逐行处理
FOR i IN 1..1000 LOOP
UPDATE users SET status = 'active' WHERE id = i;
END LOOP;
-- ✅ 批量更新
UPDATE users SET status = 'active' 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
-- ❌ 不推荐
BEGIN;
SELECT * FROM users WHERE id = 1 FOR UPDATE;
INSERT INTO logs ...; -- 大量日志插入
UPDATE users SET ... WHERE id = 1;
COMMIT;
-- ✅ 拆分事务
BEGIN;
SELECT * FROM users WHERE id = 1 FOR UPDATE;
UPDATE users SET ... WHERE id = 1;
COMMIT;
BEGIN;
INSERT INTO logs ...;
COMMIT;3. 避免忽略死锁异常
python
# ❌ 不处理死锁
cursor.execute("SELECT * FROM users WHERE id = 1 FOR UPDATE")
# ✅ 重试机制
import time
max_retries = 3
for attempt in range(max_retries):
try:
cursor.execute("SELECT * FROM users WHERE id = 1 FOR UPDATE")
break
except DeadlockException:
if attempt == max_retries - 1:
raise
time.sleep(0.1 * (2 ** attempt)) # 指数退避4. 避免在循环中执行带锁查询
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;🔍 监控与诊断
MySQL 监控
sql
-- 查看当前行锁
SELECT
lock_id,
lock_trx_id,
lock_mode,
lock_type,
lock_table,
lock_index,
lock_data
FROM performance_schema.data_locks
WHERE lock_type = 'RECORD';
-- 查看锁等待
SELECT
requesting_thread_id,
blocking_thread_id,
wait_age,
sql_text
FROM performance_schema.data_lock_waits;
-- 查看 InnoDB 状态
SHOW ENGINE INNODB STATUS\G
-- 查看锁等待超时
SELECT * FROM information_schema.innodb_trx
WHERE trx_state = 'LOCK WAIT';PostgreSQL 监控
sql
-- 查看行锁
SELECT
l.relation::regclass AS table_name,
l.mode,
l.granted,
a.pid,
a.usename,
a.query,
now() - a.query_start AS duration
FROM pg_locks l
JOIN pg_stat_activity a ON l.pid = a.pid
WHERE l.locktype = 'tuple'
ORDER BY duration DESC;
-- 查看锁等待
SELECT * FROM pg_stat_activity
WHERE wait_event_type = 'Lock';SQL Server 监控
sql
-- 查看行锁
SELECT
tl.request_session_id,
OBJECT_NAME(p.object_id) AS table_name,
tl.resource_type,
tl.request_mode,
tl.request_status
FROM sys.dm_tran_locks tl
JOIN sys.partitions p ON tl.resource_associated_entity_id = p.hobt_id
WHERE tl.resource_type = 'KEY';
-- 查看阻塞
EXEC sp_who2;🚨 常见问题
Q1: 行级锁一定会使用索引吗?
答: 不一定。只有查询条件使用了索引,才会真正使用行级锁。否则可能退化为表锁。
sql
-- 检查是否使用索引
EXPLAIN SELECT * FROM users WHERE id = 1 FOR UPDATE;Q2: 如何避免行锁升级为表锁?
答:
- 确保查询使用索引
- 控制单次锁定的行数
- SQL Server 中禁用锁升级
sql
-- SQL Server 禁用锁升级
ALTER TABLE users SET (LOCK_ESCALATION = DISABLE);Q3: 行级锁会导致死锁吗?如何预防?
答: 会。预防措施:
- 固定加锁顺序: 总是按相同顺序获取锁
- 缩短事务时间: 快速提交
- 设置超时: 避免无限等待
- 重试机制: 捕获死锁异常并重试
sql
-- 设置超时
SET innodb_lock_wait_timeout = 10;Q4: 如何选择行锁还是表锁?
答:
| 场景 | 推荐 |
|---|---|
| 高并发 OLTP | 行锁 |
| 批量导入 | 表锁 |
| 更新少量行 | 行锁 |
| 全表更新 | 表锁 |
| 有点查询 | 行锁 |
| 全表扫描 | 表锁 |
📚 相关资源
内部链接
外部资源
最后更新: 2026-04-12
维护状态: ✅ 完整