乐观锁 (Optimistic Lock)
📋 概述
乐观锁(Optimistic Lock)是一种并发控制策略,它基于"乐观"的假设:认为数据冲突很少发生。因此,在操作数据时不加锁,而是在提交时检测是否发生冲突,如果冲突则重试。
核心特点
✅ 优点:
- 高性能: 无需加锁和释放锁的开销
- 高并发: 不阻塞其他事务
- 无死锁: 不存在锁等待,不会死锁
- 适合读多写少: 读操作完全不受影响
❌ 缺点:
- 需要重试: 冲突时需要重试机制
- 实现复杂: 需要在应用层实现
- 不适合高冲突: 冲突频繁时性能反而更差
- ABA问题: 可能存在 ABA 问题
适用场景
- 低冲突场景: 多个事务很少修改同一数据
- 读多写少: 大部分是读操作
- 长事务: 操作时间长,但不想阻塞其他事务
- 高并发读: 需要支持大量并发读取
- 最终一致性: 不要求强一致性
🔧 工作原理
版本号机制
最常见的乐观锁实现方式是使用版本号:
sql
-- 表结构添加版本号
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(100),
balance DECIMAL(10,2),
version INT DEFAULT 0 -- 版本号
);
-- 更新时使用版本号检查
UPDATE users
SET balance = balance - 100, version = version + 1
WHERE id = 1 AND version = 5;
-- 检查影响行数
-- 如果为 1,更新成功
-- 如果为 0,说明版本号已变化,需要重试CAS (Compare And Swap)
另一种方式是使用 CAS 操作:
sql
-- 读取当前值
SELECT balance FROM users WHERE id = 1;
-- 假设读到 balance = 1000
-- 业务处理
new_balance = 1000 - 100;
-- CAS 更新
UPDATE users
SET balance = 900
WHERE id = 1 AND balance = 1000;
-- 如果影响行数为 0,说明 balance 已被其他事务修改,需要重试💻 各数据库实现
MySQL
版本号机制
sql
-- 创建带版本号的表
CREATE TABLE products (
id INT PRIMARY KEY,
stock INT,
version INT DEFAULT 0
);
-- 扣减库存(乐观锁)
UPDATE products
SET stock = stock - 1, version = version + 1
WHERE id = 1 AND stock > 0 AND version = 5;
-- 应用层检查影响行数
-- 如果为 0,重试MVCC 实现
MySQL InnoDB 的 MVCC 本质上也是一种乐观锁:
sql
-- 普通 SELECT 使用 MVCC,不加锁
SELECT * FROM users WHERE id = 1;
-- 读取历史版本,不阻塞其他事务
-- UPDATE 时才检测冲突
UPDATE users SET name = 'test' WHERE id = 1;
-- 如果其他事务已修改,会等待或报错PostgreSQL
PostgreSQL 的 MVCC 实现更加完善:
sql
-- PostgreSQL 自动使用 MVCC
-- SELECT 不加锁
SELECT * FROM users WHERE id = 1;
-- UPDATE 时检测冲突
UPDATE users SET name = 'test' WHERE id = 1;
-- 如果其他事务已修改但未提交,会等待
-- 如果其他事务已回滚,继续执行
-- 应用层实现版本号
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
version INT DEFAULT 0
);
UPDATE users
SET name = 'test', version = version + 1
WHERE id = 1 AND version = 5;Oracle
Oracle 是最早实现 MVCC 的数据库:
sql
-- Oracle 的 SELECT 从不加锁
SELECT * FROM users WHERE id = 1;
-- 通过 undo 日志读取一致性版本
-- UPDATE 时检测冲突
UPDATE users SET name = 'test' WHERE id = 1;
-- 如果检测到冲突,抛出 ORA-08177 错误🎯 最佳实践
✅ 推荐做法
1. 实现重试机制
python
import time
def update_with_optimistic_lock(user_id, new_balance, max_retries=3):
"""使用乐观锁更新余额"""
for attempt in range(max_retries):
try:
# 步骤1: 读取当前值和版本号
cursor.execute(
"SELECT balance, version FROM users WHERE id = %s",
(user_id,)
)
row = cursor.fetchone()
if not row:
raise Exception("用户不存在")
current_balance, version = row
# 步骤2: 业务逻辑处理
if current_balance < 100:
raise Exception("余额不足")
# 步骤3: 尝试更新(带版本号检查)
cursor.execute(
"""UPDATE users
SET balance = %s, version = version + 1
WHERE id = %s AND version = %s""",
(new_balance, user_id, version)
)
if cursor.rowcount == 1:
connection.commit()
return True # 更新成功
else:
# 版本号不匹配,重试
connection.rollback()
time.sleep(0.1 * (2 ** attempt)) # 指数退避
except Exception as e:
connection.rollback()
if attempt == max_retries - 1:
raise
time.sleep(0.1 * (2 ** attempt))
raise Exception("更新失败:超过最大重试次数")2. 使用合适的数据类型
sql
-- ✅ 推荐:使用 INT 或 BIGINT 作为版本号
version INT UNSIGNED NOT NULL DEFAULT 0
-- ❌ 避免:使用字符串
version VARCHAR(10) -- 性能差3. 添加索引优化查询
sql
-- ✅ 为查询条件添加索引
CREATE INDEX idx_user_id ON users(id);
-- 版本号不需要索引,因为它总是随主键一起使用❌ 避免陷阱
1. 避免无限重试
python
# ❌ 危险:可能无限循环
while True:
result = try_update()
if result:
break
# ✅ 推荐:限制重试次数
max_retries = 3
for attempt in range(max_retries):
result = try_update()
if result:
break
time.sleep(0.1 * (2 ** attempt))
else:
raise Exception("超过最大重试次数")2. 避免在高冲突场景使用
python
# ❌ 不推荐:秒杀场景使用乐观锁
# 大量请求同时更新同一商品库存,冲突率极高
# 导致大量重试,性能反而更差
# ✅ 推荐:秒杀场景使用悲观锁或 Redis
SELECT * FROM products WHERE id = 1 FOR UPDATE;
UPDATE products SET stock = stock - 1 WHERE id = 1;3. 注意 ABA 问题
时间线:
T1: 事务A读取 balance=1000, version=1
T2: 事务B更新 balance=900, version=2
T3: 事务C更新 balance=1000, version=3 (又改回1000)
T4: 事务A尝试更新,检查 version=1
❌ 虽然 balance 还是 1000,但 version 已变为 3
解决:只检查版本号,不检查具体值📊 性能对比
乐观锁 vs 悲观锁
| 指标 | 乐观锁 | 悲观锁 |
|---|---|---|
| 读性能 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| 写性能(低冲突) | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| 写性能(高冲突) | ⭐⭐ | ⭐⭐⭐⭐ |
| 并发度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| 实现复杂度 | 高 | 低 |
| 死锁风险 | 无 | 有 |
适用场景
| 场景 | 推荐策略 | 原因 |
|---|---|---|
| 电商浏览 | 乐观锁 | 读多写少 |
| 银行转账 | 悲观锁 | 高冲突,强一致 |
| 社交点赞 | 乐观锁 | 低价值,可重试 |
| 库存扣减 | 悲观锁 | 高冲突 |
| 用户资料 | 乐观锁 | 低冲突 |
| 订单状态 | 悲观锁 | 关键业务 |
🚨 常见问题
Q1: 乐观锁和 MVCC 的关系?
答: MVCC 是乐观锁的一种实现方式。
- 乐观锁: 一种并发控制策略
- MVCC: 多版本并发控制,是实现乐观锁的技术
Q2: 如何处理乐观锁的冲突?
答:
- 重试: 最常见的方式
- 合并: 智能合并冲突(如 CRDT)
- 拒绝: 直接报错,让用户决定
- 最后写入胜出: 接受最后一次修改
Q3: 版本号会溢出吗?
答:
使用 INT 类型可以存储约 42 亿次更新,几乎不会溢出。
如果担心溢出:
- 使用
BIGINT - 定期重置版本号(需谨慎)
📚 相关资源
内部链接
外部资源
最后更新: 2026-04-12
维护状态: ✅ 完整