悲观锁 (Pessimistic Lock)
📋 概述
悲观锁(Pessimistic Lock)是一种并发控制策略,它基于"悲观"的假设:认为数据冲突必然会发生。因此,在操作数据之前,先获取锁,确保在整个操作过程中数据不会被其他事务修改。
核心特点
✅ 优点:
- 数据安全: 完全避免数据冲突
- 实现简单: 逻辑直观,易于理解
- 强一致性: 确保数据的严格一致性
- 适用高冲突: 在冲突频繁的场景表现好
❌ 缺点:
- 性能较低: 加锁和释放锁有开销
- 并发度低: 阻塞其他事务的访问
- 可能死锁: 多个事务互相等待
- 资源浪费: 即使没有冲突也加锁
适用场景
- 高冲突场景: 多个事务频繁修改同一数据
- 金融交易: 银行转账、支付等关键业务
- 库存扣减: 电商秒杀、库存管理
- 临界区保护: 需要独占访问的代码段
- 短事务: 操作时间短,锁持有时间短
🔧 工作原理
基本流程
1. 开始事务
↓
2. 获取锁(SELECT ... FOR UPDATE)
↓
3. 读取数据
↓
4. 处理业务逻辑
↓
5. 更新数据
↓
6. 提交事务,释放锁代码示例
sql
-- 银行转账示例
BEGIN;
-- 步骤1: 锁定转出账户
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
-- 检查余额是否充足
-- 步骤2: 锁定转入账户
SELECT * FROM accounts WHERE id = 2 FOR UPDATE;
-- 步骤3: 执行转账
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- 步骤4: 记录日志
INSERT INTO transfer_logs VALUES (...);
-- 步骤5: 提交事务,释放锁
COMMIT;💻 各数据库实现
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 id = 1 FOR UPDATE NOWAIT;
-- 跳过被锁定的行
SELECT * FROM users WHERE id IN (1,2,3) FOR UPDATE SKIP LOCKED;PostgreSQL
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;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;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;🎯 最佳实践
✅ 推荐做法
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
-- ✅ 总是按相同顺序加锁
SELECT * FROM users WHERE id = 1 FOR UPDATE;
SELECT * FROM users WHERE id = 2 FOR UPDATE;3. 设置超时
sql
-- MySQL
SET innodb_lock_wait_timeout = 10;
-- PostgreSQL
SET lock_timeout = '5s';❌ 避免陷阱
1. 避免死锁
python
# ✅ 重试机制
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
-- ❌ 不推荐:长时间持有锁
BEGIN;
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- 大量无关操作
COMMIT;
-- ✅ 推荐:快速完成
BEGIN;
SELECT * FROM users WHERE id = 1 FOR UPDATE;
UPDATE users SET name = 'test' WHERE id = 1;
COMMIT;📊 性能影响
适用场景评估
| 场景 | 适合度 | 说明 |
|---|---|---|
| 高冲突 | ⭐⭐⭐⭐⭐ | 完美适用 |
| 低冲突 | ⭐⭐ | 过度设计 |
| 短事务 | ⭐⭐⭐⭐⭐ | 性能好 |
| 长事务 | ⭐⭐ | 阻塞严重 |
| 高并发读 | ⭐ | 不适合 |
| 高并发写 | ⭐⭐⭐⭐ | 需要优化 |
🚨 常见问题
Q1: 悲观锁和乐观锁如何选择?
答:
| 因素 | 选择悲观锁 | 选择乐观锁 |
|---|---|---|
| 冲突频率 | 高 | 低 |
| 事务长度 | 短 | 长 |
| 并发度要求 | 低 | 高 |
| 实现复杂度 | 简单 | 复杂 |
Q2: 如何预防死锁?
答:
- 固定加锁顺序
- 缩短事务时间
- 设置超时
- 使用重试机制
📚 相关资源
内部链接
外部资源
最后更新: 2026-04-12
维护状态: ✅ 完整