轻松应对并发更新冲突,保证数据一致性,无需加锁就能提高系统性能!
目录
🤔 什么是乐观锁?
乐观锁是一种并发控制策略,它假设多用户操作同一数据的场景下,冲突是很少发生的。因此,它不会在查询时加锁,而是在更新时检查数据是否被其他事务修改过。
核心思想:先修改,更新时再检查
乐观锁的工作流程:
-
查询数据时记录数据版本号
-
更新时检查版本号是否一致
-
如果一致则更新成功并将版本号+1
-
如果不一致则更新失败(说明数据已被他人修改)
💡 提示:乐观锁不是真正的"锁",而是一种并发控制策略,它不会阻塞其他事务的执行。
⚙️ 快速实现乐观锁
1. 添加版本号字段
在数据库表中添加版本号字段:
ALTER TABLE user ADD COLUMN version INT DEFAULT 1 COMMENT '版本号';
2. 配置乐观锁插件
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}
3. 实体类添加@Version注解
@Data
@TableName("user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
private Integer age;
private String email;
@Version // 标识该字段为乐观锁版本号字段
private Integer version;
}
⚠️ 注意:乐观锁仅支持
updateById
和update(entity, wrapper)
方法,且实体类必须带有@Version注解的字段。
🔄 乐观锁的工作原理
当执行更新操作时,MyBatis-Plus会:
操作 | 说明 |
---|---|
1. 自动在SQL语句中添加版本号条件 | 确保只更新版本号匹配的记录 |
2. 同时将版本号+1 | 更新成功后,版本号自动递增 |
例如:
// 查询用户(此时version=1)
User user = userMapper.selectById(1L);
// 修改用户信息
user.setName("张三改名");
// 更新用户
userMapper.updateById(user);
生成的SQL语句:
UPDATE user SET name=?, version=? WHERE id=? AND version=?
-- 参数: 张三改名, 2, 1, 1
-
✅ 如果数据未被修改,版本号仍为1,更新成功
-
❌ 如果数据已被修改,版本号已变,更新失败(返回影响行数为0)
🔄 并发更新场景演示
假设两个线程同时操作同一条数据:
// 线程1查询用户(version=1)
User user1 = userMapper.selectById(1L);
// 线程2查询用户(version=1)
User user2 = userMapper.selectById(1L);
// 线程1修改并更新
user1.setName("线程1修改");
userMapper.updateById(user1); // 成功,version变为2
// 线程2修改并更新
user2.setName("线程2修改");
userMapper.updateById(user2); // 失败,因为此时数据库version=2,而user2的version=1
🛠️ 处理更新失败的策略
当乐观锁更新失败时,常见的处理策略有:
1. 放弃更新
int rows = userMapper.updateById(user);
if (rows == 0) {
throw new BusinessException("数据已被他人修改,请刷新后重试");
}
2. 重新查询后重试
int maxRetries = 3;
int retryCount = 0;
boolean success = false;
while (!success && retryCount < maxRetries) {
// 尝试更新
int rows = userMapper.updateById(user);
if (rows > 0) {
success = true;
} else {
// 更新失败,重新查询最新数据
user = userMapper.selectById(user.getId());
user.setName("新名字"); // 重新设置要修改的值
retryCount++;
}
}
3. 合并更新
// 更新失败时
if (rows == 0) {
// 获取最新数据
User latest = userMapper.selectById(user.getId());
// 合并修改(根据业务逻辑合并)
latest.setName(user.getName());
latest.setRemark(user.getRemark() + "," + latest.getRemark());
// 重新更新
userMapper.updateById(latest);
}
🎯 乐观锁的适用场景
适合场景
-
✅ 读多写少的业务
-
✅ 并发冲突较少的场景
-
✅ 对响应速度要求较高的场景
不适合场景
-
❌ 写入频繁的业务
-
❌ 冲突概率高的场景
-
❌ 无法容忍更新失败的场景
⚠️ 使用注意事项
-
仅适用于更新操作:乐观锁只对
updateById
和update
方法有效 -
必须使用数据库的版本号:不能手动设置版本号,必须先查询再更新
-
批量更新无效:乐观锁对批量更新无效,因为无法精确控制版本号
-
注意重试逻辑:实现重试时要避免无限循环
🔄 乐观锁与悲观锁对比
特性 | 乐观锁 | 悲观锁 |
---|---|---|
加锁时机 | 更新时检查 | 查询时加锁 |
并发性能 | 🚀 高 | 🐢 低 |
实现复杂度 | 😊 简单 | 😣 复杂 |
死锁风险 | 🟢 低 | 🔴 高 |
适用场景 | 读多写少 | 写多读少 |
冲突处理 | 需要额外处理 | 自动等待 |
🎯 实际应用示例
库存扣减场景
@Transactional
public boolean reduceStock(Long productId, int quantity) {
// 1. 查询商品库存
Product product = productMapper.selectById(productId);
if (product.getStock() < quantity) {
throw new BusinessException("库存不足");
}
// 2. 扣减库存
product.setStock(product.getStock() - quantity);
// 3. 更新库存(带乐观锁)
int rows = productMapper.updateById(product);
if (rows == 0) {
// 更新失败,说明有并发冲突
throw new BusinessException("库存扣减失败,请重试");
}
return true;
}
秒杀场景
@Transactional
public boolean seckill(Long userId, Long productId) {
// 1. 查询商品
Product product = productMapper.selectById(productId);
if (product.getStock() <= 0) {
throw new BusinessException("商品已售罄");
}
// 2. 检查是否重复购买
int count = orderMapper.selectCount(
new QueryWrapper<Order>()
.eq("user_id", userId)
.eq("product_id", productId)
);
if (count > 0) {
throw new BusinessException("您已购买过该商品");
}
// 3. 扣减库存
product.setStock(product.getStock() - 1);
int rows = productMapper.updateById(product);
if (rows == 0) {
// 乐观锁更新失败,说明库存已被修改
throw new BusinessException("手慢了,商品被抢走了");
}
// 4. 创建订单
Order order = new Order();
order.setUserId(userId);
order.setProductId(productId);
order.setCreateTime(new Date());
orderMapper.insert(order);
return true;
}
📝 小结
MyBatis-Plus的乐观锁插件是一个轻量级的并发控制工具,只需简单配置即可实现。它通过版本号机制确保数据一致性,适用于读多写少的业务场景。
🔥 最佳实践:
对并发更新敏感的数据使用乐观锁(如库存、余额等)
合理设计乐观锁更新失败的处理策略
在高并发场景下,考虑结合缓存或队列减少冲突概率
对于极高并发的场景,可能需要考虑悲观锁或分布式锁