乐观锁VS悲观锁在电商项目实践

乐观锁 VS 悲观锁在电商项目实践案例

一、核心思想对比

一句话概括

悲观锁 乐观锁
思想 先加锁,再操作 先操作,再验证
假设 冲突一定会发生 冲突很少发生
时机 操作前锁定资源 提交时检查冲突

生活类比

悲观锁:上厕所先敲门,确认没人再进去(先确认安全)
乐观锁:直接推门进去,发现有人再出来(事后处理冲突)

二、技术实现对比

维度 悲观锁 乐观锁
数据库 SELECT ... FOR UPDATE version 字段 + WHERE version=x
Redis SETNX 分布式锁 WATCH + MULTI/EXEC
Java synchronizedReentrantLock CAS(Compare And Swap)
开销 大(持锁期间阻塞其他线程) 小(无锁,冲突时重试)
吞吐量
适用场景 写多读少,冲突频繁 读多写少,冲突较少

三、电商项目实践案例

案例一:库存扣减(秒杀场景)

悲观锁方案

-- 1. 开启事务
BEGIN;

-- 2. 加锁查询库存
SELECT stock FROM product WHERE id = 1001 FOR UPDATE;

-- 3. 判断库存是否充足
-- 应用层判断:if (stock > 0)

-- 4. 扣减库存
UPDATE product SET stock = stock - 1 WHERE id = 1001;

-- 5. 提交事务
COMMIT;

流程图

请求A ──→ BEGIN ──→ SELECT ... FOR UPDATE ──→ 获取锁 ──→ 扣库存 ──→ COMMIT ──→ 释放锁
                                                      ↑
请求B ──→ BEGIN ──→ SELECT ... FOR UPDATE ──→ 阻塞等待 ──┘

特点

  • 串行执行,绝对安全
  • 性能差,高并发时大量阻塞
  • 适合库存极少的秒杀场景

乐观锁方案

-- 表结构增加 version 字段
CREATE TABLE product (
    id INT PRIMARY KEY,
    stock INT NOT NULL,
    version INT DEFAULT 0,
    -- version 字段用于乐观锁
);

-- 扣减库存
UPDATE product 
SET stock = stock - 1, version = version + 1
WHERE id = 1001 AND stock > 0 AND version = #{oldVersion};

-- 如果 affected_rows = 0,说明库存不足或版本冲突,需要重试或报错

Java 代码

public boolean deductStock(Long productId) {
    int retryTimes = 3;
    
    while (retryTimes-- > 0) {
        // 1. 查询当前库存和版本
        Product product = productMapper.selectById(productId);
        
        if (product.getStock() <= 0) {
            return false; // 库存不足
        }
        
        // 2. 尝试更新(乐观锁)
        int affected = productMapper.updateStock(
            productId, 
            product.getVersion()
        );
        
        if (affected > 0) {
            return true; // 扣减成功
        }
        
        // 3. 失败则重试(版本冲突)
    }
    
    return false; // 重试耗尽
}

特点

  • 无锁,性能高
  • 冲突时需要重试
  • 适合读多写少的普通商品

案例二:账户余额扣减

悲观锁方案(资金安全优先)

BEGIN;

-- 加锁查询余额
SELECT balance FROM account WHERE user_id = 1001 FOR UPDATE;

-- 应用层判断余额是否充足
-- UPDATE 扣减
UPDATE account SET balance = balance - 100 WHERE user_id = 1001;

COMMIT;

为什么用悲观锁?

  • 资金操作不能失败重试(用户体验差)
  • 必须保证强一致性
  • 冲突时阻塞比失败更可接受

乐观锁方案(高性能场景)

-- 扣减余额
UPDATE account 
SET balance = balance - 100, version = version + 1
WHERE user_id = 1001 
  AND balance >= 100           -- 余额充足
  AND version = #{oldVersion}; -- 版本匹配

适用场景

  • 内部系统转账
  • 并发量高但冲突少的场景

案例三:订单状态流转

状态条件更新(非锁机制,但思想类似乐观锁)

-- 支付回调更新订单状态
UPDATE orders 
SET status = 'PAID', paid_at = NOW()
WHERE order_no = 'O20240511001' 
  AND status = 'CREATED';  -- 只有CREATED才能变成PAID

-- 如果 affected_rows = 0:
--   - 订单已是 PAID → 重复回调,返回成功(幂等)
--   - 订单是其他状态 → 非法流转,返回失败

与乐观锁的区别

  • 不是用 version,而是用 status 业务状态
  • 目的不是防并发,而是控制状态流转 + 实现幂等

四、方案选型建议

场景 推荐方案 理由
秒杀库存扣减 悲观锁 库存少,冲突极高,必须串行
普通商品下单 乐观锁 库存多,冲突少,性能优先
账户余额扣减 悲观锁 资金安全,不能失败重试
积分扣减 乐观锁 可接受重试,性能优先
订单状态更新 状态条件更新 控制流转 + 幂等
库存预占(下单未支付) 悲观锁 需要精确控制,防止超卖

五、与幂等性的关系

机制 能否实现幂等 说明
悲观锁 ❌ 不能 只解决并发,不解决重复请求
乐观锁 ❌ 不能 只解决并发,不解决重复请求
状态条件更新 ✅ 能 重复执行返回相同结果

幂等性解决的是重复请求问题,乐观锁/悲观锁解决的是并发冲突问题,两者不同但常结合使用。


六、总结

┌─────────────────────────────────────────────────────────┐
│  选型决策树                                              │
│                                                         │
│  冲突概率高? ──是──→ 悲观锁(库存秒杀、资金操作)       │
│       │                                                 │
│       否                                                │
│       ↓                                                 │
│  需要强一致性? ──是──→ 悲观锁                           │
│       │                                                 │
│       否                                                │
│       ↓                                                 │
│  乐观锁(普通商品、积分等)                              │
└─────────────────────────────────────────────────────────┘
posted @ 2026-05-19 11:23  SeiunSky  阅读(45)  评论(0)    收藏  举报