MyBatis-Plus乐观锁插件:优雅解决并发更新问题

轻松应对并发更新冲突,保证数据一致性,无需加锁就能提高系统性能!

目录

🤔 什么是乐观锁?

核心思想:先修改,更新时再检查

⚙️ 快速实现乐观锁

1. 添加版本号字段

2. 配置乐观锁插件

3. 实体类添加@Version注解

🔄 乐观锁的工作原理

🔄 并发更新场景演示

🛠️ 处理更新失败的策略

1. 放弃更新

2. 重新查询后重试

3. 合并更新

🎯 乐观锁的适用场景

适合场景

不适合场景

⚠️ 使用注意事项

🔄 乐观锁与悲观锁对比

🎯 实际应用示例

库存扣减场景

秒杀场景

📝 小结

⏭️ 下一步学习


🤔 什么是乐观锁?

乐观锁是一种并发控制策略,它假设多用户操作同一数据的场景下,冲突是很少发生的。因此,它不会在查询时加锁,而是在更新时检查数据是否被其他事务修改过。

核心思想:先修改,更新时再检查

乐观锁的工作流程:

  1. 查询数据时记录数据版本号

  2. 更新时检查版本号是否一致

  3. 如果一致则更新成功并将版本号+1

  4. 如果不一致则更新失败(说明数据已被他人修改)

💡 提示:乐观锁不是真正的"锁",而是一种并发控制策略,它不会阻塞其他事务的执行。

⚙️ 快速实现乐观锁

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;
}

⚠️ 注意:乐观锁仅支持updateByIdupdate(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);
}

🎯 乐观锁的适用场景

适合场景

  • ✅ 读多写少的业务

  • ✅ 并发冲突较少的场景

  • ✅ 对响应速度要求较高的场景

不适合场景

  • ❌ 写入频繁的业务

  • ❌ 冲突概率高的场景

  • ❌ 无法容忍更新失败的场景

⚠️ 使用注意事项

  1. 仅适用于更新操作:乐观锁只对updateByIdupdate方法有效

  2. 必须使用数据库的版本号:不能手动设置版本号,必须先查询再更新

  3. 批量更新无效:乐观锁对批量更新无效,因为无法精确控制版本号

  4. 注意重试逻辑:实现重试时要避免无限循环

🔄 乐观锁与悲观锁对比

特性乐观锁悲观锁
加锁时机更新时检查查询时加锁
并发性能🚀 高🐢 低
实现复杂度😊 简单😣 复杂
死锁风险🟢 低🔴 高
适用场景读多写少写多读少
冲突处理需要额外处理自动等待

🎯 实际应用示例

库存扣减场景

@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的乐观锁插件是一个轻量级的并发控制工具,只需简单配置即可实现。它通过版本号机制确保数据一致性,适用于读多写少的业务场景。

🔥 最佳实践

  1. 对并发更新敏感的数据使用乐观锁(如库存、余额等)

  2. 合理设计乐观锁更新失败的处理策略

  3. 在高并发场景下,考虑结合缓存或队列减少冲突概率

  4. 对于极高并发的场景,可能需要考虑悲观锁或分布式锁

⏭️ 下一步学习

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值