在业务系统中处理并发数据更新是个经典难题。上周我刚处理了一个电商平台的库存超卖问题,核心矛盾在于:既要保证数据一致性,又要避免传统数据库锁带来的性能损耗。这正是MyBatis-Plus乐观锁的用武之地。
乐观锁的本质是通过版本号机制实现无锁并发控制。与悲观锁不同,它不会阻塞其他事务,而是在提交时检查数据是否被修改过。这种机制特别适合读多写少、冲突频率低的场景,比如用户余额变更、商品库存扣减、订单状态更新等业务场景。
MyBatis-Plus通过@Version注解实现乐观锁。其工作流程如下:
UPDATE table SET ..., version=version+1 WHERE id=1 AND version=1java复制// 实体类配置示例
public class Account {
@Version
private Integer version;
// 其他字段...
}
| 对比维度 | 乐观锁 | 悲观锁 |
|---|---|---|
| 实现方式 | 版本号检测 | SELECT FOR UPDATE |
| 并发性能 | 无阻塞,高吞吐 | 串行执行,低吞吐 |
| 适用场景 | 低冲突频率 | 高冲突频率 |
| 失败处理 | 需重试机制 | 自动阻塞等待 |
| 死锁风险 | 无 | 有 |
xml复制<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
java复制@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}
库存扣减示例代码:
java复制@Service
@RequiredArgsConstructor
public class InventoryService {
private final InventoryMapper inventoryMapper;
@Transactional(rollbackFor = Exception.class)
public boolean deductInventory(Long productId, int quantity) {
Inventory inventory = inventoryMapper.selectById(productId);
if (inventory.getStock() < quantity) {
throw new RuntimeException("库存不足");
}
inventory.setStock(inventory.getStock() - quantity);
int updated = inventoryMapper.updateById(inventory);
if (updated == 0) {
throw new OptimisticLockingFailureException("并发修改冲突");
}
return true;
}
}
对于高并发场景,建议增加重试策略:
java复制@Retryable(value = OptimisticLockingFailureException.class,
maxAttempts = 3,
backoff = @Backoff(delay = 100))
public boolean deductInventoryWithRetry(Long productId, int quantity) {
return deductInventory(productId, quantity);
}
sql复制UPDATE inventory SET stock=stock-1, version=version+1
WHERE id=1 AND version=1 AND stock>=1
java复制// 根据用户ID哈希选择子库存
int segment = userId.hashCode() % 10;
inventoryMapper.deductStock(productId, segment, quantity);
建议监控以下指标:
问题现象:version从A→B→A,导致误判无冲突
解决方案:
java复制@Version
private Long version; // 使用永不重复的雪花ID
// 或增加时间戳
@Version
private LocalDateTime updateTime;
批量操作时需要特殊处理:
java复制List<Inventory> inventories = inventoryMapper.selectBatchIds(ids);
inventories.forEach(inv -> inv.setStock(inv.getStock() - 1));
int updated = inventoryMapper.updateBatchById(inventories);
if (updated != inventories.size()) {
// 部分失败处理
}
跨服务场景下的解决方案:
redis复制WATCH inventory:1:version
GET inventory:1:version
MULTI
SET inventory:1:version 2
EXEC
使用JMeter测试对比(单商品库存,100并发):
| 方案 | TPS | 平均响应时间 | 错误率 |
|---|---|---|---|
| 无锁 | 1250 | 80ms | 8.7% |
| 乐观锁 | 980 | 102ms | 0% |
| 悲观锁 | 320 | 312ms | 0% |
| 乐观锁+重试 | 850 | 118ms | 0% |
测试结论:乐观锁在保证数据一致性的同时,性能损耗在可接受范围内。
java复制public boolean transfer(Long fromId, Long toId, BigDecimal amount) {
Account from = accountMapper.selectById(fromId);
Account to = accountMapper.selectById(toId);
if (from.getBalance().compareTo(amount) < 0) {
throw new RuntimeException("余额不足");
}
from.setBalance(from.getBalance().subtract(amount));
to.setBalance(to.getBalance().add(amount));
int affected = accountMapper.updateById(from)
+ accountMapper.updateById(to);
if (affected != 2) {
throw new OptimisticLockingFailureException("并发冲突");
}
return true;
}
java复制public boolean cancelOrder(Long orderId) {
Order order = orderMapper.selectById(orderId);
if (!OrderStatus.PENDING.equals(order.getStatus())) {
return false;
}
order.setStatus(OrderStatus.CANCELLED);
return orderMapper.updateById(order) > 0;
}
java复制@Scheduled(fixedRate = 60000)
public void refreshConfig() {
List<SystemConfig> configs = configMapper.selectList(null);
configs.forEach(config -> {
String newValue = loadFromRemote(config.getKey());
config.setValue(newValue);
configMapper.updateById(config);
});
}
大事务问题:在事务中包含耗时操作会导致version长时间不释放
解决方案:将事务拆分为小粒度操作
字段更新遗漏:忘记给version字段加@Version注解
检查项:确保实体类和数据库都有version字段
监控缺失:未及时发现冲突率上升
建议:配置冲突率超过5%的报警阈值
重试风暴:无限重试导致系统负载激增
最佳实践:设置最大重试次数和退避时间
批量操作:部分成功部分失败时的补偿机制
方案:记录失败记录进行异步修复
实际项目中,我们通过乐观锁将库存系统的并发处理能力提升了3倍,同时将超卖问题发生率降为零。关键点在于:合理设置重试策略、避免长事务、做好监控报警。对于特别热点的商品,最终采用了库存分段+乐观锁的组合方案。