【并发实战】拒绝数据乱套!乐观锁与悲观锁的落地应用指南

简介: 在分布式场景下,Java锁无法解决数据并发问题。本文详解悲观锁(SELECT FOR UPDATE)与乐观锁(版本号机制),结合MySQL与MyBatis-Plus实战,助你应对秒杀超卖等高并发难题,提升系统一致性与性能。

前言

在单体应用开发中,我们习惯了用 synchronized 或 ReentrantLock 来解决线程安全问题。但在分布式系统或多个人同时操作数据库的场景下,Java 级别的锁就鞭长莫及了。

最经典的场景就是**“秒杀扣库存”**:

假设库存只有 1 件。

  • 用户 A 读取库存:1
  • 用户 B 读取库存:1
  • 用户 A 扣减库存:0,写入数据库。
  • 用户 B 扣减库存:0,写入数据库。
  • 结果: 卖出了 2 件商品,库存变成 0(甚至 -1),这就是严重的超卖事故。

为了解决这个问题,我们需要借助数据库层面的锁机制:悲观锁与乐观锁。

1. 悲观锁 (Pessimistic Lock)

心态: “总有刁民想害朕”。

悲观锁总是假设最坏的情况,认为自己在使用数据的时候,一定会有别人来修改数据。所以,在获取数据时就直接加锁,直到我用完提交事务,别人才能用。

实现方式:SELECT ... FOR UPDATE

在 MySQL InnoDB 引擎中,我们可以使用 FOR UPDATE 来触发行锁。

SQL 示例:

SQL

-- 开启事务
BEGIN;
-- 1. 查询并锁定这条记录(注意:必须在事务中才生效)
-- 此时,其他事务如果想修改这条 id=1 的数据,会被阻塞等待
SELECT stock FROM goods WHERE id = 1 FOR UPDATE;
-- 2. 判断库存 > 0,然后执行扣减
UPDATE goods SET stock = stock - 1 WHERE id = 1;
-- 3. 提交事务,释放锁
COMMIT;

优缺点分析

  • 优点: 简单粗暴,数据安全性极高,完全杜绝了并发冲突。
  • 缺点: 性能较差。因为是排他锁,并发高时会造成大量线程阻塞(Waiting),甚至引发死锁。
  • 适用场景: 写多读少,且并发量不是特别大的强一致性场景(如银行转账)。

2. 乐观锁 (Optimistic Lock)

心态: “世界充满爱”。

乐观锁假设数据一般情况下不会造成冲突,所以查询时不上锁。只在更新数据的那一刻,去检查一下在此期间有没有人修改过这条数据。

实现方式:版本号机制 (Version)

这是最通用的实现方案。我们在表中增加一个 version 字段。

  1. 取数据: 查询出商品信息,同时获取当前版本号 version = 1。
  2. 改数据: 做业务逻辑。
  3. 存数据: 更新时,带上刚才查出来的版本号作为条件。

SQL 示例:

SQL

-- 1. 查询库存和版本号
SELECT stock, version FROM goods WHERE id = 1;
-- 假设查出来 version = 1
-- 2. 更新时,检查 version 是否还是 1
-- 如果这期间没人修改,version 还是 1,更新成功,version 变 2
-- 如果有人修改过,version 变成了 2,条件不满足,更新失败 (受影响行数 0)
UPDATE goods SET stock = stock - 1, version = version + 1 
WHERE id = 1 AND version = 1;

Java 实战(MyBatis-Plus)

在 Java 开发中,MyBatis-Plus 提供了开箱即用的乐观锁插件,甚至不需要你写 SQL。

  1. 实体类加注解:
    Java
public class Goods {
    private Long id;
    private Integer stock;
    @Version // 标记这是版本号字段
    private Integer version;
}
  1. 配置拦截器:
    Java
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    // 添加乐观锁插件
    interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
    return interceptor;
}
  1. 业务代码:
    Java
// 1. 先查
Goods goods = goodsMapper.selectById(1L);
// 2. 修改
goods.setStock(goods.getStock() - 1);
// 3. 更新 (MP会自动带上 version 条件并自增)
int result = goodsMapper.updateById(goods);
if (result == 0) {
    // 更新失败,说明被人抢先了,可以抛异常或重试
    throw new RuntimeException("手慢了,请重试!");
}

优缺点分析

  • 优点: 吞吐量高,不需要数据库加锁,不会死锁。
  • 缺点: 存在 ABA 问题(不过用 Version 机制可解);在高并发写的时候,失败率高,需要业务代码实现“重试机制”。
  • 适用场景: 读多写少的场景(如抢优惠券、编辑个人信息)。

3. 总结与选型

维度 悲观锁 乐观锁
侧重点 宁可排队,也不能错 只要没人跟我抢,我就能跑得快
开销 数据库锁开销,高并发易阻塞 CPU 开销(因为可能需要重试循环)
安全性 高 中(需要业务层配合处理失败)
推荐场景 强一致性、写冲突极高 互联网高并发、读多写少

一句话口诀:

冲突多,用悲观,加上排他保平安;

冲突少,用乐观,版本控制效率翻。

相关文章
|
11月前
|
负载均衡 Java Nacos
Gateway服务网关
网关是微服务的统一入口,实现请求路由、权限控制、限流与负载均衡。基于SpringCloud Gateway可构建高性能响应式网关,支持断言与过滤器工厂,灵活实现路由匹配及请求处理,并可通过全局过滤器扩展功能,解决跨域等问题。
|
XML 数据可视化 Java
|
存储 缓存 Java
Spring缓存注解【@Cacheable、@CachePut、@CacheEvict、@Caching、@CacheConfig】使用及注意事项
Spring缓存注解【@Cacheable、@CachePut、@CacheEvict、@Caching、@CacheConfig】使用及注意事项
5666 2
springboot 集成 swagger 2.x 和 3.0 以及 Failed to start bean ‘documentationPluginsBootstrapper‘问题的解决
本文介绍了如何在Spring Boot项目中集成Swagger 2.x和3.0版本,并提供了解决Swagger在Spring Boot中启动失败问题“Failed to start bean ‘documentationPluginsBootstrapper’; nested exception is java.lang.NullPointerEx”的方法,包括配置yml文件和Spring Boot版本的降级。
springboot 集成 swagger 2.x 和 3.0 以及 Failed to start bean ‘documentationPluginsBootstrapper‘问题的解决
|
Java Spring
mybatisplus的typeAliasesPackage 配置
【6月更文挑战第20天】mybatisplus的typeAliasesPackage 配置
2236 3