Skip to content

乐观锁 (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: 如何处理乐观锁的冲突? ​

答:

  1. 重试: 最常见的方式
  2. 合并: 智能合并冲突(如 CRDT)
  3. 拒绝: 直接报错,让用户决定
  4. 最后写入胜出: 接受最后一次修改

Q3: 版本号会溢出吗? ​

答:

使用 INT 类型可以存储约 42 亿次更新,几乎不会溢出。

如果担心溢出:

  • 使用 BIGINT
  • 定期重置版本号(需谨慎)

📚 相关资源 ​

内部链接 ​

外部资源 ​


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

Released under MIT License.