一、什么是乐观锁?为什么需要它?

想象一下多人同时编辑同一份文档的场景。如果直接用最后保存的版本覆盖之前的修改,肯定会丢失其他人的编辑内容。数据库中的并发更新也是类似的道理。

乐观锁就像给数据加了个"版本号标签"。每次修改前先核对标签,如果标签没变就允许修改并更新标签;如果标签变了,说明有人抢先修改了,当前操作就会被拒绝。这种机制特别适合读多写少的场景,因为它不需要真正"锁住"数据,避免了性能损耗。

二、MyBatis-Plus的版本号实现

MyBatis-Plus通过@Version注解轻松实现乐观锁。来看个完整的用户余额更新示例:

// 技术栈:SpringBoot + MyBatis-Plus + MySQL

// 1. 实体类添加版本号字段
@Data
public class User {
    private Long id;
    private String name;
    private BigDecimal balance;
    
    @Version  // 关键注解
    private Integer version; 
}

// 2. 配置乐观锁插件
@Configuration
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return interceptor;
    }
}

// 3. 更新操作示例
@Service
public class UserService {
    @Transactional
    public boolean updateBalance(Long userId, BigDecimal amount) {
        User user = userMapper.selectById(userId);
        user.setBalance(user.getBalance().add(amount));
        return userMapper.updateById(user) > 0;
    }
}

当两个线程同时执行这段代码时:

  • 线程A先查询得到version=1
  • 线程B也查询得到version=1
  • 线程A先完成更新,version自动变为2
  • 线程B提交时发现version已经不是1,就会抛出OptimisticLockException

三、实际开发中的完整案例

让我们看个电商库存管理的完整流程:

// 技术栈同上

// 实体类
@Data
@TableName("product")
public class Product {
    private Long id;
    private String name;
    private Integer stock;
    
    @Version
    private Integer version;
}

// 服务层
@Service
@RequiredArgsConstructor
public class ProductService {
    private final ProductMapper productMapper;

    /**
     * 扣减库存
     * @param productId 商品ID
     * @param quantity 扣减数量
     * @return 是否成功
     */
    @Transactional(rollbackFor = Exception.class)
    public boolean reduceStock(Long productId, Integer quantity) {
        // 重试3次机制
        int retry = 0;
        while (retry < 3) {
            Product product = productMapper.selectById(productId);
            if (product.getStock() < quantity) {
                throw new RuntimeException("库存不足");
            }
            
            product.setStock(product.getStock() - quantity);
            try {
                if (productMapper.updateById(product) > 0) {
                    return true;
                }
            } catch (OptimisticLockException e) {
                retry++;
                if (retry == 3) {
                    throw new RuntimeException("并发更新冲突,请重试");
                }
            }
        }
        return false;
    }
}

这个例子展示了:

  1. 标准的乐观锁使用方式
  2. 添加了业务校验(库存检查)
  3. 实现了重试机制
  4. 完整的异常处理流程

四、技术细节与注意事项

4.1 版本号字段的规则

  • 必须使用数值类型(int/long)或时间戳
  • 初始值建议设为0或1
  • 每次更新自动+1(不可手动修改)

4.2 常见问题排查

  1. 更新不生效:检查是否忘记添加乐观锁插件
  2. 版本号不变化:确认字段有@Version注解
  3. 意外冲突:考虑增加重试机制

4.3 性能优化建议

  • 对高并发场景可配合Redis缓存版本号
  • 长时间操作不适合用乐观锁(考虑悲观锁)
  • 批量更新时需要特殊处理

五、与其他方案的对比

5.1 悲观锁方案

SELECT * FROM account WHERE id=1 FOR UPDATE
  • 优点:保证强一致性
  • 缺点:并发性能差,容易死锁

5.2 CAS原子操作

UPDATE product SET stock=stock-1 WHERE id=1 AND stock>=1
  • 优点:轻量级
  • 缺点:不能记录冲突次数

5.3 分布式锁

// 基于Redis的实现
RLock lock = redisson.getLock("product:1");
try {
    lock.lock();
    // 业务逻辑
} finally {
    lock.unlock();
}
  • 优点:适合分布式系统
  • 缺点:实现复杂,性能开销大

六、最佳实践总结

  1. 适合场景

    • 读多写少的业务(用户信息、商品库存)
    • 冲突概率较低的场景
    • 需要保证数据最终一致性
  2. 不适合场景

    • 财务系统等需要强一致性的场景
    • 频繁冲突的业务流程
    • 需要记录修改历史的场景
  3. 实施建议

    • 配合@Transactional使用
    • 前端可添加修改时间戳提示
    • 重要操作建议添加操作日志

通过合理使用乐观锁,我们可以在保证数据一致性的同时,获得更好的系统性能。MyBatis-Plus的版本号机制让实现变得非常简单,是处理并发问题的利器。