定义
乐观锁 (Optimistic Lock) 是一种并发控制策略,它假设多个事务同时修改同一数据的概率很低,因此在读取数据时不加锁,只在提交或更新时检查数据是否被其他事务修改过。如果检测到冲突,则回滚并重试;如果没有冲突,则提交成功。
乐观锁的核心思想是:先操作,后检查,与悲观锁的"先加锁,后操作"形成对比。
详细笔记
核心原理
乐观锁 vs 悲观锁
悲观锁 (Pessimistic Lock):
sql
-- 先加锁,再操作
START TRANSACTION;
SELECT stock FROM products WHERE id = 1 FOR UPDATE; -- 加排他锁
UPDATE products SET stock = stock - 1 WHERE id = 1;
COMMIT;
-- 特点:
-- ✅ 保证不会冲突
-- ❌ 并发度低(其他事务需等待)
-- ❌ 可能死锁乐观锁 (Optimistic Lock):
sql
-- 先读取,不加锁
SELECT stock, version FROM products WHERE id = 1;
-- 读到: stock = 100, version = 5
-- 应用层计算
new_stock = stock - 1;
-- 提交时检查版本号
UPDATE products
SET stock = new_stock, version = version + 1
WHERE id = 1 AND version = 5;
-- 检查影响行数
IF ROW_COUNT() = 0 THEN
-- 版本已变化,冲突!重试
ROLLBACK;
retry();
ELSE
COMMIT;
END IF;
-- 特点:
-- ✅ 并发度高(读不加锁)
-- ✅ 无死锁
-- ❌ 高冲突场景下重试率高乐观锁的实现方式
方式一:版本号机制(最常用)
sql
-- 表结构添加 version 字段
CREATE TABLE products (
id INT PRIMARY KEY,
name VARCHAR(100),
stock INT,
version INT DEFAULT 0, -- 版本号
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 初始化
INSERT INTO products VALUES (1, 'iPhone', 100, 0, NOW());完整实现:
sql
DELIMITER $$
CREATE PROCEDURE update_stock_optimistic(
IN p_product_id INT,
IN p_quantity INT
)
BEGIN
DECLARE v_stock INT;
DECLARE v_version INT;
DECLARE v_attempts INT DEFAULT 0;
DECLARE v_max_attempts INT DEFAULT 3;
DECLARE v_success BOOLEAN DEFAULT FALSE;
WHILE v_attempts < v_max_attempts AND NOT v_success DO
SET v_attempts = v_attempts + 1;
START TRANSACTION;
-- 步骤1:读取当前值和版本号
SELECT stock, version
INTO v_stock, v_version
FROM products
WHERE id = p_product_id;
-- 步骤2:业务逻辑判断
IF v_stock >= p_quantity THEN
-- 步骤3:更新时检查版本号
UPDATE products
SET stock = stock - p_quantity,
version = version + 1
WHERE id = p_product_id
AND version = v_version;
-- 步骤4:检查是否成功
IF ROW_COUNT() > 0 THEN
SET v_success = TRUE;
COMMIT;
ELSE
-- 版本冲突,回滚重试
ROLLBACK;
-- 短暂等待,避免频繁重试
DO SLEEP(0.01 * v_attempts);
END IF;
ELSE
ROLLBACK;
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '库存不足';
END IF;
END WHILE;
IF NOT v_success THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '更新失败,请重试';
END IF;
END$$
DELIMITER ;
-- 调用
CALL update_stock_optimistic(1, 1);执行流程:
事务 A 事务 B
| |
|-- 读取: stock=100, v=5 ------|
| |-- 读取: stock=100, v=5
| |
|-- 计算: new_stock=99 --------|
| |-- 计算: new_stock=99
| |
|-- UPDATE ... WHERE v=5 ------|
| → 成功,v变为6 |
| |-- UPDATE ... WHERE v=5
| | → 失败!(v已是6)
| | → 回滚,重试
| |
| |-- 重试:读取 stock=99, v=6
| |-- UPDATE ... WHERE v=6
| | → 成功,v变为7
↓ ↓方式二:时间戳机制
sql
CREATE TABLE articles (
id INT PRIMARY KEY,
title VARCHAR(200),
content TEXT,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 更新时检查时间戳
UPDATE articles
SET title = '新标题',
updated_at = NOW()
WHERE id = 1
AND updated_at = '2024-01-15 10:30:00';
-- 如果 updated_at 已变化,说明被其他人修改过缺点:
- 时间戳精度问题(可能毫秒级冲突)
- 时钟同步问题(分布式系统)
- 不如版本号可靠
方式三:全值比较
sql
-- 更新时检查所有字段的原值
UPDATE accounts
SET balance = 900
WHERE id = 1
AND balance = 1000 -- 检查原值
AND status = 1 -- 检查其他关键字段
AND ...;
-- 如果任何字段被修改,UPDATE 失败适用场景:
- 字段较少
- 需要精确控制哪些字段不能变化
源码视角:应用层实现
Java Spring Boot 示例
java
@Service
public class ProductService {
@Autowired
private ProductMapper productMapper;
/**
* 乐观锁扣减库存
*/
@Transactional
public boolean decreaseStock(Long productId, int quantity) {
int maxRetries = 3;
for (int attempt = 1; attempt <= maxRetries; attempt++) {
// 1. 查询当前值和版本号
Product product = productMapper.selectById(productId);
if (product.getStock() < quantity) {
throw new BusinessException("库存不足");
}
// 2. 尝试更新(带版本号条件)
int affectedRows = productMapper.updateStockWithVersion(
productId,
quantity,
product.getVersion()
);
// 3. 检查是否成功
if (affectedRows > 0) {
return true; // 成功
}
// 4. 版本冲突,重试
log.warn("版本冲突,第{}次重试,productId={}", attempt, productId);
if (attempt < maxRetries) {
try {
// 指数退避:10ms, 20ms, 40ms
Thread.sleep(10 * (long) Math.pow(2, attempt - 1));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new BusinessException("更新中断");
}
}
}
throw new BusinessException("更新失败,请稍后重试");
}
}
// Mapper
@Mapper
public interface ProductMapper {
@Select("SELECT * FROM products WHERE id = #{id}")
Product selectById(Long id);
@Update("UPDATE products SET stock = stock - #{quantity}, " +
"version = version + 1 " +
"WHERE id = #{id} AND version = #{version}")
int updateStockWithVersion(@Param("id") Long id,
@Param("quantity") int quantity,
@Param("version") int version);
}Python Django 示例
python
from django.db import transaction
from django.utils import timezone
import time
def decrease_stock_optimistic(product_id, quantity):
"""
乐观锁扣减库存
"""
max_retries = 3
for attempt in range(1, max_retries + 1):
with transaction.atomic():
# 1. 查询当前值和版本号
product = Product.objects.select_for_update().get(id=product_id)
old_version = product.version
if product.stock < quantity:
raise ValueError("库存不足")
# 2. 修改数据
product.stock -= quantity
product.version += 1
product.updated_at = timezone.now()
# 3. 保存时检查版本号
updated = Product.objects.filter(
id=product_id,
version=old_version
).update(
stock=product.stock,
version=product.version,
updated_at=product.updated_at
)
if updated > 0:
return True # 成功
# 4. 版本冲突,重试
if attempt < max_retries:
time.sleep(0.01 * (2 ** (attempt - 1))) # 指数退避
raise Exception("更新失败,请稍后重试")乐观锁的性能分析
优势
1. 高并发度
悲观锁:
事务 A: SELECT ... FOR UPDATE → 锁定
事务 B: SELECT ... FOR UPDATE → 等待
事务 C: SELECT ... FOR UPDATE → 等待
→ 串行执行,TPS: 1000
乐观锁:
事务 A: SELECT(无锁) → 计算 → UPDATE
事务 B: SELECT(无锁) → 计算 → UPDATE
事务 C: SELECT(无锁) → 计算 → UPDATE
→ 并行执行,TPS: 10000
性能提升: 10 倍2. 无死锁
悲观锁:
事务 A 锁定资源 1,等待资源 2
事务 B 锁定资源 2,等待资源 1
→ 死锁!
乐观锁:
读操作不加锁
写操作原子性检查
→ 无死锁风险3. 适合读多写少场景
场景:商品详情页
- 读操作: 10000 QPS
- 写操作: 10 QPS(下单)
悲观锁:
- 每次读都加锁(即使只是 SELECT)
- 锁开销大
乐观锁:
- 读操作无锁
- 只有写操作检查版本
- 性能优异劣势
1. 高冲突场景下重试率高
场景:秒杀活动
- 1000 个事务同时抢购 10 个库存
乐观锁:
- 第 1 轮: 1000 个事务读取 stock=10
- 第 1 轮: 只有 1 个事务成功,999 个失败
- 第 2 轮: 999 个事务重试,只有 1 个成功
- ...
- 重试次数: 大量
悲观锁:
- 事务排队执行
- 无重试
- 虽然慢,但确定性强2. ABA 问题
时间点 T1:
事务 A 读取: value = 100, version = 1
时间点 T2:
事务 B 修改: value = 200, version = 2
事务 C 修改: value = 100, version = 3
时间点 T3:
事务 A 更新: UPDATE ... WHERE version = 1
→ 失败!(version 已是 3)
但如果只检查 value:
事务 A 更新: UPDATE ... WHERE value = 100
→ 成功!❌ 但这是错误的,因为 value 曾被改为 200
解决方案:
- 使用版本号(而非只检查值)
- 或使用 CAS(Compare-And-Swap)3. 应用层复杂度高
悲观锁:
START TRANSACTION;
SELECT ... FOR UPDATE;
UPDATE ...;
COMMIT;
→ 简单直接
乐观锁:
- 需要维护 version 字段
- 需要重试逻辑
- 需要处理最大重试次数
- 需要指数退避策略
→ 代码复杂乐观锁的适用场景
适合使用乐观锁的场景
场景一:读多写少
sql
-- 文章系统
CREATE TABLE articles (
id INT PRIMARY KEY,
title VARCHAR(200),
content TEXT,
view_count INT DEFAULT 0,
version INT DEFAULT 0
);
-- 读操作(无锁): 10000 QPS
SELECT * FROM articles WHERE id = 1;
-- 写操作(乐观锁): 10 QPS
UPDATE articles
SET view_count = view_count + 1, version = version + 1
WHERE id = 1 AND version = :old_version;场景二:低冲突率
sql
-- 用户资料更新
CREATE TABLE user_profiles (
user_id INT PRIMARY KEY,
nickname VARCHAR(50),
avatar VARCHAR(200),
version INT DEFAULT 0
);
-- 不同用户更新自己的资料,冲突率低
UPDATE user_profiles
SET nickname = '新昵称', version = version + 1
WHERE user_id = :user_id AND version = :old_version;场景三:长时间运行的事务
悲观锁:
- 长时间持有锁
- 阻塞其他事务
- 资源浪费
乐观锁:
- 读时不加锁
- 提交时才检查
- 不阻塞其他事务不适合使用乐观锁的场景
❌ 高冲突场景
秒杀活动:
- 10000 人抢 100 个库存
- 冲突率: 99%
- 乐观锁重试次数: 极高
- 性能反而更差
→ 应使用悲观锁或队列❌ 写密集型
日志系统:
- 每秒 10000 次写入
- 乐观锁版本冲突频繁
- 重试开销大
→ 应使用批量插入或异步队列❌ 强一致性要求
银行转账:
- 不能接受失败重试
- 需要确定性结果
→ 应使用悲观锁优化策略
策略一:分段乐观锁
sql
-- 将热点数据分段,降低冲突
CREATE TABLE product_stock_segments (
product_id INT,
segment_id INT,
stock INT,
version INT DEFAULT 0,
PRIMARY KEY (product_id, segment_id)
);
-- 初始化:100 个段,每段 10 个库存
INSERT INTO product_stock_segments
SELECT 1, n, 10, 0
FROM generate_series(1, 100) AS n;
-- 扣减时随机选择一段
UPDATE product_stock_segments
SET stock = stock - 1, version = version + 1
WHERE product_id = 1
AND segment_id = FLOOR(1 + RAND() * 100)
AND stock > 0
AND version = :old_version;
-- 效果:
-- - 冲突分散到 100 个段
-- - 重试率降低 100 倍策略二:批量合并更新
sql
-- 累积多次更新,一次性提交
-- 应用层缓存
local_stock_change = 0
FOR each order:
local_stock_change += 1
IF local_stock_change >= 10 THEN
-- 批量更新
UPDATE products
SET stock = stock - 10, version = version + 1
WHERE id = 1 AND version = :old_version;
local_stock_change = 0
END IF
END FOR
-- 效果:
-- - 减少更新次数
-- - 降低版本冲突概率策略三:混合锁策略
sql
-- 根据冲突率动态选择锁策略
DELIMITER $$
CREATE PROCEDURE update_stock_hybrid(
IN p_product_id INT,
IN p_quantity INT
)
BEGIN
DECLARE conflict_rate DECIMAL(5,2);
-- 获取历史冲突率
SELECT AVG(retry_count) INTO conflict_rate
FROM update_statistics
WHERE product_id = p_product_id
AND updated_at > NOW() - INTERVAL 1 HOUR;
IF conflict_rate > 0.5 THEN
-- 高冲突:使用悲观锁
START TRANSACTION;
SELECT stock FROM products WHERE id = p_product_id FOR UPDATE;
UPDATE products SET stock = stock - p_quantity WHERE id = p_product_id;
COMMIT;
ELSE
-- 低冲突:使用乐观锁
CALL update_stock_optimistic(p_product_id, p_quantity);
END IF;
END$$
DELIMITER ;实际案例
案例一:电商库存扣减优化
sql
-- 问题:秒杀活动,乐观锁重试率 90%
-- 原始实现
UPDATE products
SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = :old_version AND stock > 0;
-- 重试 3 次,成功率仅 10%
-- 优化方案:预扣减 + 异步确认
-- 步骤1:预扣减(Redis)
redis DECR stock:1
-- 步骤2:异步写入数据库
INSERT INTO stock_changes (product_id, quantity, status)
VALUES (1, -1, 'pending');
-- 步骤3:定时任务批量确认
UPDATE products p
INNER JOIN (
SELECT product_id, SUM(quantity) as total_change
FROM stock_changes
WHERE status = 'pending'
GROUP BY product_id
) sc ON p.id = sc.product_id
SET p.stock = p.stock + sc.total_change,
p.version = p.version + 1;
DELETE FROM stock_changes WHERE status = 'pending';
-- 效果:
-- - 数据库压力降低 90%
-- - 无版本冲突
-- - TPS: 从 1000 提升至 10000案例二:协同编辑系统
sql
-- Google Docs 类似的协同编辑
CREATE TABLE documents (
doc_id INT PRIMARY KEY,
content TEXT,
version INT DEFAULT 0,
last_editor INT
);
-- 用户编辑
UPDATE documents
SET content = :new_content,
version = version + 1,
last_editor = :user_id
WHERE doc_id = :doc_id
AND version = :old_version;
-- 如果失败,返回冲突信息
-- 前端显示差异,让用户手动合并
-- 效果:
-- - 支持多人同时编辑
-- - 冲突时友好提示
-- - 用户体验好最佳实践
评估冲突率:冲突率 < 10% 时使用乐观锁
设置合理的重试次数:通常 3-5 次
使用指数退避:避免频繁重试
添加监控:统计重试率和成功率
分段降低冲突:热点数据分段处理
混合策略:根据冲突率动态选择锁类型
版本号优于时间戳:更可靠,无精度问题
记录冲突日志:便于分析和优化
关联术语
- [[悲观锁]]
- [[死锁]]
- [[MVCC]]
- [[事务隔离级别]]
参考资料
- 《高性能 MySQL》第 7 章:事务与锁
- Martin Kleppmann: "Designing Data-Intensive Applications"
- Oracle Docs: Optimistic Locking
- Hibernate Docs: Optimistic Locking