排他锁 (Exclusive Lock / X Lock)
📋 概述
排他锁(Exclusive Lock),又称写锁(Write Lock)或 X 锁,是一种独占式访问控制机制。当一个事务获得某资源的排他锁后,其他事务不能获得该资源的任何类型的锁(包括共享锁和排他锁)。
核心特点
✅ 优点:
- 数据一致性: 确保修改期间数据不被其他事务访问
- 隔离性强: 完全隔离并发事务
- 防止脏写: 避免多个事务同时修改同一数据
- 简单直观: 易于理解和实现
❌ 缺点:
- 并发度低: 阻塞所有其他事务的访问
- 可能死锁: 多个事务互相等待对方释放锁
- 性能影响: 长事务持有会严重影响并发
- 资源浪费: 即使只修改一个字段也锁定整行
适用场景
- 数据修改: UPDATE、DELETE 操作
- 数据插入: INSERT 操作
- 一致性更新: 读取后立即修改的场景
- 临界区保护: 需要独占访问的代码段
- 事务隔离: 确保事务串行执行
🔧 工作原理
锁兼容性矩阵
排他锁是最严格的锁模式:
| 无锁 | S锁 | X锁
--------|-------|-------|------
X锁 | ✅ | ❌ | ❌解读:
- X 锁与任何锁都不兼容(除了无锁状态)
- 一旦获得 X 锁,其他事务必须等待
示例:
事务A: SELECT ... FOR UPDATE; -- 获得 X 锁
事务B: SELECT ... FOR UPDATE; -- ❌ 必须等待
事务C: SELECT ... LOCK IN SHARE MODE; -- ❌ 必须等待
事务D: UPDATE ...; -- ❌ 必须等待自动获取排他锁
DML 操作会自动获取排他锁:
| 操作 | 锁行为 | 说明 |
|---|---|---|
INSERT | X 锁 | 插入新行时加排他锁 |
UPDATE | X 锁 | 更新行时加排他锁 |
DELETE | X 锁 | 删除行时加排他锁 |
sql
-- 这些操作都会自动加排他锁
UPDATE users SET name = 'test' WHERE id = 1; -- 自动加 X 锁
DELETE FROM users WHERE id = 1; -- 自动加 X 锁
INSERT INTO users VALUES (1, 'john'); -- 自动加 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;💻 各数据库实现
MySQL
基本用法
sql
-- 显式加排他锁
BEGIN;
SELECT * FROM users WHERE id = 1 FOR UPDATE;
UPDATE users SET balance = balance - 100 WHERE id = 1;
COMMIT;
-- 多行排他锁
SELECT * FROM users WHERE age > 18 FOR UPDATE;
-- 不等待立即返回
SELECT * FROM users WHERE id = 1 FOR UPDATE NOWAIT;
-- 跳过被锁定的行
SELECT * FROM users WHERE id IN (1,2,3) FOR UPDATE SKIP LOCKED;锁的查看
sql
-- 查看排他锁
SELECT
lock_id,
lock_trx_id,
lock_mode,
lock_type,
lock_table,
lock_index,
lock_data
FROM performance_schema.data_locks
WHERE lock_mode = 'X'; -- X 表示排他锁
-- 查看锁等待
SELECT * FROM performance_schema.data_lock_waits;
-- InnoDB 状态
SHOW ENGINE INNODB STATUS\G参数配置
ini
# my.cnf
# 排他锁等待超时时间(秒)
innodb_lock_wait_timeout = 50
# 死锁检测
innodb_deadlock_detect = ONPostgreSQL
基本用法
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 NO KEY UPDATE; -- 不影响外键
SELECT * FROM users WHERE id = 1 FOR SHARE; -- 共享锁
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,但写操作仍需加排他锁:
sql
-- 事务 A
BEGIN;
UPDATE users SET name = 'alice' WHERE id = 1;
-- 持有排他锁,但未提交
-- 事务 B
SELECT * FROM users WHERE id = 1;
-- ✅ 不被阻塞,读到旧版本(MVCC)
UPDATE users SET name = 'bob' WHERE id = 1;
-- ❌ 被阻塞,等待事务 A 提交或回滚查看排他锁
sql
-- 查看所有排他锁
SELECT
l.locktype,
l.relation::regclass AS table_name,
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.mode IN ('ExclusiveLock', 'RowExclusiveLock')
ORDER BY l.relation;SQL Server
基本用法
sql
-- 使用排他锁提示
BEGIN TRANSACTION;
SELECT * FROM users WITH (UPDLOCK, ROWLOCK) 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 users WITH (TABLOCKX);查看排他锁
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.request_mode IN ('X', 'IX'); -- X=排他锁, IX=意向排他锁Oracle
基本用法
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;查看排他锁
sql
-- 查看行级排他锁
SELECT
s.sid,
s.serial#,
s.username,
l.type,
l.lmode,
l.request,
o.object_name
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;📊 性能影响分析
并发度评估
| 场景 | 并发度 | 说明 |
|---|---|---|
| 不同行的操作 | ⭐⭐⭐⭐⭐ | 完全并行 |
| 同一行的操作 | ⭐ | 串行执行 |
| 范围查询 | ⭐⭐ | 锁定多行 |
| 全表更新 | ⭐ | 几乎串行 |
性能测试示例
sql
-- 测试场景:100个并发事务更新同一行
-- 使用排他锁
-- 100个事务串行执行,总耗时:~50秒
BEGIN;
SELECT * FROM users WHERE id = 1 FOR UPDATE;
UPDATE users SET balance = balance - 100 WHERE id = 1;
COMMIT;
-- 更新不同行
-- 100个事务并行执行,总耗时:~1秒
BEGIN;
SELECT * FROM users WHERE id = ? FOR UPDATE; -- 不同的 id
UPDATE users SET balance = balance - 100 WHERE id = ?;
COMMIT;🎯 最佳实践
✅ 推荐做法
1. 缩短事务时间
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);2. 固定加锁顺序避免死锁
sql
-- ❌ 可能导致死锁
-- 事务 A
SELECT * FROM users WHERE id = 1 FOR UPDATE;
SELECT * FROM users WHERE id = 2 FOR UPDATE;
-- 事务 B
SELECT * FROM users WHERE id = 2 FOR UPDATE;
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- ✅ 固定顺序
SELECT * FROM users WHERE id IN (1,2) ORDER BY id 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;4. 设置合理的超时
sql
-- MySQL
SET innodb_lock_wait_timeout = 10;
-- PostgreSQL
SET lock_timeout = '5s';
-- SQL Server
SET LOCK_TIMEOUT 5000;❌ 避免陷阱
1. 避免忽略死锁异常
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)) # 指数退避2. 避免在循环中执行带锁查询
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;3. 避免大事务持有排他锁
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;🔍 监控与诊断
MySQL 监控
sql
-- 查看排他锁
SELECT
lock_id,
lock_trx_id,
lock_mode,
lock_type,
lock_table,
lock_data
FROM performance_schema.data_locks
WHERE lock_mode = 'X';
-- 查看锁等待
SELECT
requesting_thread_id,
blocking_thread_id,
wait_age
FROM performance_schema.data_lock_waits;
-- 查看死锁
SHOW ENGINE INNODB STATUS\GPostgreSQL 监控
sql
-- 查看排他锁
SELECT
l.relation::regclass AS table_name,
l.mode,
l.granted,
a.pid,
a.query
FROM pg_locks l
JOIN pg_stat_activity a ON l.pid = a.pid
WHERE l.mode IN ('ExclusiveLock', 'RowExclusiveLock');
-- 查看锁等待
SELECT * FROM pg_stat_activity
WHERE wait_event_type = 'Lock';SQL Server 监控
sql
-- 查看排他锁
SELECT
request_session_id,
OBJECT_NAME(resource_associated_entity_id) AS table_name,
request_mode,
request_status
FROM sys.dm_tran_locks
WHERE request_mode IN ('X', 'IX');
-- 查看阻塞
EXEC sp_who2;🚨 常见问题
Q1: 排他锁和共享锁的区别?
答:
| 特性 | 排他锁 (X) | 共享锁 (S) |
|---|---|---|
| 兼容性 | 与任何锁都不兼容 | 与其他 S 锁兼容 |
| 用途 | 写操作 | 读操作 |
| 并发度 | 低 | 高 |
| 死锁风险 | 有 | 无(纯 S 锁) |
Q2: 如何避免排他锁导致的性能问题?
答:
- 缩短事务时间: 快速提交
- 减少锁粒度: 使用行锁而非表锁
- 批量操作: 减少锁获取次数
- 合理索引: 精确定位需要锁定的行
- 读写分离: 从库承担读负载
Q3: 排他锁会导致死锁吗?如何预防?
答: 会。预防措施:
- 固定加锁顺序: 总是按相同顺序获取锁
- 缩短事务时间: 快速提交
- 设置超时: 避免无限等待
- 重试机制: 捕获死锁异常并重试
📚 相关资源
内部链接
外部资源
最后更新: 2026-04-12
维护状态: ✅ 完整