Skip to content

定义 ​

乐观锁 (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;

-- 如果失败,返回冲突信息
-- 前端显示差异,让用户手动合并

-- 效果:
-- - 支持多人同时编辑
-- - 冲突时友好提示
-- - 用户体验好

最佳实践 ​

  1. 评估冲突率:冲突率 < 10% 时使用乐观锁

  2. 设置合理的重试次数:通常 3-5 次

  3. 使用指数退避:避免频繁重试

  4. 添加监控:统计重试率和成功率

  5. 分段降低冲突:热点数据分段处理

  6. 混合策略:根据冲突率动态选择锁类型

  7. 版本号优于时间戳:更可靠,无精度问题

  8. 记录冲突日志:便于分析和优化

关联术语 ​

  • [[悲观锁]]
  • [[死锁]]
  • [[MVCC]]
  • [[事务隔离级别]]

参考资料 ​

Released under MIT License.