Skip to content

行级锁 (Row-Level Lock) ​

📋 概述 ​

行级锁是数据库锁机制中粒度最细的一种锁类型(不考虑页级锁),它锁定的是表中的单个数据行。当一个事务获得某行的行级锁后,其他事务对该行的某些操作会被阻塞,但不影响其他行的访问。

核心特点 ​

✅ 优点:

  • 并发度高: 不同事务可以同时操作不同的行
  • 冲突概率低: 只锁定需要的行,减少锁竞争
  • 灵活性好: 精确控制锁定范围
  • 扩展性强: 适合高并发的 OLTP 系统

❌ 缺点:

  • 开销大: 需要维护大量锁对象,内存占用多
  • 加锁慢: 需要通过索引定位到具体行
  • 可能死锁: 多个事务互相等待对方持有的行锁
  • 实现复杂: 数据库引擎实现难度高
  • 可能升级: 锁数量过多时可能升级为表锁

适用场景 ​

  • 高并发 OLTP: 电商订单、银行转账等
  • 点对点操作: 更新特定用户信息
  • 短事务: 快速完成的操作
  • 索引良好的查询: 可以通过索引精确定位
  • 读写混合: 需要同时支持读写操作

🔧 工作原理 ​

基于索引的锁定 ​

行级锁的实现依赖于索引。数据库通过索引定位到具体的行,然后在该行上加锁。

sql
-- 假设 id 是主键
SELECT * FROM users WHERE id = 1 FOR UPDATE;

锁定过程:

  1. 通过主键索引找到 id = 1 的记录
  2. 在该记录的聚簇索引上加 X 锁(排他锁)
  3. 其他事务尝试锁定同一行时被阻塞

⚠️ 重要:无索引时的锁升级 ​

如果查询条件没有使用索引,行级锁会退化为表级锁:

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 语句时,数据库会自动添加行级锁:

操作锁模式说明
INSERTX 锁插入新行时加排他锁
UPDATEX 锁更新行时加排他锁
DELETEX 锁删除行时加排他锁
SELECT ... FOR UPDATEX 锁显式加排他锁
SELECT ... LOCK IN SHARE MODES 锁显式加共享锁
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: 如何避免行锁升级为表锁? ​

答:

  1. 确保查询使用索引
  2. 控制单次锁定的行数
  3. SQL Server 中禁用锁升级
sql
-- SQL Server 禁用锁升级
ALTER TABLE users SET (LOCK_ESCALATION = DISABLE);

Q3: 行级锁会导致死锁吗?如何预防? ​

答: 会。预防措施:

  1. 固定加锁顺序: 总是按相同顺序获取锁
  2. 缩短事务时间: 快速提交
  3. 设置超时: 避免无限等待
  4. 重试机制: 捕获死锁异常并重试
sql
-- 设置超时
SET innodb_lock_wait_timeout = 10;

Q4: 如何选择行锁还是表锁? ​

答:

场景推荐
高并发 OLTP行锁
批量导入表锁
更新少量行行锁
全表更新表锁
有点查询行锁
全表扫描表锁

📚 相关资源 ​

内部链接 ​

外部资源 ​


最后更新: 2026-04-12
维护状态: ✅ 完整

Released under MIT License.