一、什么是乐观锁?为什么需要它?
想象一下多人同时编辑同一份文档的场景。如果直接用最后保存的版本覆盖之前的修改,肯定会丢失其他人的编辑内容。数据库中的并发更新也是类似的道理。
乐观锁就像给数据加了个"版本号标签"。每次修改前先核对标签,如果标签没变就允许修改并更新标签;如果标签变了,说明有人抢先修改了,当前操作就会被拒绝。这种机制特别适合读多写少的场景,因为它不需要真正"锁住"数据,避免了性能损耗。
二、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;
}
}
这个例子展示了:
- 标准的乐观锁使用方式
- 添加了业务校验(库存检查)
- 实现了重试机制
- 完整的异常处理流程
四、技术细节与注意事项
4.1 版本号字段的规则
- 必须使用数值类型(int/long)或时间戳
- 初始值建议设为0或1
- 每次更新自动+1(不可手动修改)
4.2 常见问题排查
- 更新不生效:检查是否忘记添加乐观锁插件
- 版本号不变化:确认字段有@Version注解
- 意外冲突:考虑增加重试机制
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();
}
- 优点:适合分布式系统
- 缺点:实现复杂,性能开销大
六、最佳实践总结
适合场景:
- 读多写少的业务(用户信息、商品库存)
- 冲突概率较低的场景
- 需要保证数据最终一致性
不适合场景:
- 财务系统等需要强一致性的场景
- 频繁冲突的业务流程
- 需要记录修改历史的场景
实施建议:
- 配合@Transactional使用
- 前端可添加修改时间戳提示
- 重要操作建议添加操作日志
通过合理使用乐观锁,我们可以在保证数据一致性的同时,获得更好的系统性能。MyBatis-Plus的版本号机制让实现变得非常简单,是处理并发问题的利器。
评论