MyBatis-Plus 乐观锁解决订单并发问题

编程教程 > Java > Spring (7) 2025-09-16 14:29:00

效果参考

需求

商品只有库存1,多个用户同时下单仅其中一个能成功。

并发下单

MyBatis-Plus 乐观锁解决订单并发问题_图示-bf83f3697495410ea589e3a00b9b39f9.png

多线程请求实现模拟并发下单,如上图所示,实现同1秒进入创建订单接口。

mybatis-plus 乐观锁使用

MyBatis-Plus 乐观锁解决订单并发问题_图示-58ad3eaea80643849b57d8bb1ae293f0.png

从上图可以看到,三个请求均抵达数据库,且均调用了mybatis-plus乐观锁字段version

执行结果

MyBatis-Plus 乐观锁解决订单并发问题_图示-ba8067b3ffb542908e67bcbcec0f5a68.png

2失败1成功,与我们预期结果一致,测试通过。

项目环境

  • spring boot 3.5.5
  • mybatis-plus-spring-boot3-starter 3.5.7

项目

项目结构图

MyBatis-Plus 乐观锁解决订单并发问题_图示-59b3e86a849a4b88852d703b840e9b8d.png

相关代码

初始化SQL

drop table if exists `goods`;
create table `goods`(
    `id` int primary key auto_increment,
    `name` varchar(20) not null,
    `price` decimal(10,2) not null,
    `stock` int not null default 0,
    `createTime` timestamp not null default current_timestamp,
    `updateTime` timestamp not null default current_timestamp on update current_timestamp,
    `version` int not null default 0
)comment '商品';
insert into `goods`(`name`,`price`,`stock`) values('iphone 17 PRO',8888.88,1);
drop table if exists `order_info`;
create table `order_info`(
    `id` int primary key auto_increment,
    `goodsId` int not null,
    `userId` int not null,
    `count` int not null default 1,
    `createTime` timestamp not null default current_timestamp,
    `updateTime` timestamp not null default current_timestamp on update current_timestamp,
    `status` tinyint not null default 1 comment '1:待支付 2:待发货 3:待收货 4:待评价 5:已完成 6:已取消'
)

pom.xml

这里展示依赖部分

   <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
            <version>3.5.7</version>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

 

Goods.java

@Data
@TableName("goods")
public class Goods {
    @TableId
    private Integer id;
    @TableField("name")
    private String name;
    @TableField("price")
    private BigDecimal price;
    @TableField("stock")
    private Integer stock;
    @TableField("createTime")
    private Date createTime;
    @TableField("updateTime")
    private Date updateTime;
    @TableField("version")
    @Version
    private Integer version;
}

@Version 标记该字段为记录乐观锁版本字段,一般是int,long,datetime类型,且给一个默认值,建议 0 ;如果未设置默认值会导致null无法计算,乐观锁失效。

OrderInfo.java

@Data
@TableName("order_info")
public class OrderInfo {
    @TableId
    private Integer id;
    @TableField("goodsId")
    private Integer goodsId;
    @TableField("userId")
    private Integer userId;
    @TableField("createTime")
    private Date createTime;
    @TableField("updateTime")
    private Date updateTime;
    @TableField("status")
    private Integer status;
}

GoodsMapper.java

public interface GoodsMapper extends BaseMapper<Goods> {
}

OrderInfoMapper.java

public interface OrderInfoMapper extends BaseMapper<OrderInfo> {
}

GoodsService.java

@Slf4j
@Service
public class GoodsService extends ServiceImpl<GoodsMapper, Goods> {
}

提示:本案例为了简化,所有service均只有类没有接口,实际情况根据你的项目来。

OrderInfoService.java

@Slf4j
@Service
public class OrderInfoService extends ServiceImpl<OrderInfoMapper, OrderInfo> {
}

UserService.java

@RequiredArgsConstructor
@Slf4j
@Service
public class UserService {
    private final OrderInfoService orderInfoService;
    private final GoodsService goodsService;
    @Transactional(rollbackFor = Exception.class)
    public OrderInfo createOrder(Integer userId, Integer goodsId) {
        log.info("用户:{} 下单,商品:{}",userId,goodsId);
        OrderInfo orderInfo = new OrderInfo();
        orderInfo.setUserId(userId);
        orderInfo.setGoodsId(goodsId);
        orderInfo.setStatus(1);
        boolean save = orderInfoService.save(orderInfo);
        if (!save) {
           throw new RuntimeException("创建订单失败");
        }
        //查
        Goods goods = goodsService.getById(goodsId);
        if (goods.getStock() <= 0) {
           throw new RuntimeException("查询库存:库存不足");
        }
        //模拟中间有其他业务耗时1s
        try {
            Thread.sleep(1000);
        }catch (Exception e){
            log.error("中间有其他业务耗时1s");
        }
        //减少库存
        goods.setStock(goods.getStock() - 1);
        boolean updateStock = goodsService.updateById(goods);
        if (!updateStock) {
            throw new RuntimeException("减少库存:库存不足");
        }
        return orderInfo;
    }
}

提示:

  1. 模拟中间业务的1s所有订单均相等,会导致谁先进createOrder方法谁就抢到。实际业务耗时因素很多不确定,有可能是后来抢到(比如第一个进来做中间其他业务就刚好cpu没抢到满了一丢丢就会导致后面的抢到)。
  2. 可能发生并发业务的方法内所有数据库操作均需要接收和处理结果,结果不成功需要抛异常事务才会回滚,保证业务数据一致性。
  3. 需要使用mybatis-plus乐观锁逻辑注意配置乐观锁插件,本教程参考配置类MybatisPlusConfig.java optimisticLockerInnerInterceptor部分
  4. 注意哪些方法/情况会触发mybatis-plus的乐观锁

MybatisPlusConfig.java

@Configuration
@EnableTransactionManagement
@MapperScan("com.example.demomybatisplus.mapper")
public class MybatisPlusConfig {
    /**
     * 添加分页插件
     */
    @Bean
    public MybatisPlusInterceptor paginationInnerInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 如果配置多个插件, 切记分页最后添加
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        // 如果有多数据源可以不配具体类型, 否则都建议配上具体的 DbType
        return interceptor;
    }
    /**
     * 添加乐观锁插件
     */
    @Bean
    public MybatisPlusInterceptor optimisticLockerInnerInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return interceptor;
    }
}

application.yml

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
    username: root
    password: root
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

提示:数据信息,请根据自身情况修改

 

DemoMybatisPlusApplicationTests.java

并发测试代码

@Slf4j
@SpringBootTest
class DemoMybatisPlusApplicationTests {
    @Resource
    UserService userService;
    @Test
    void createOrdersTest() {
        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(10, 10, 0, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(10));
        final int goodsId = 1;
        List<Future<OrderInfo>> futures = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            final int userId = i;
            futures.add(poolExecutor.submit(() -> userService.createOrder(userId, goodsId)));
        }
        for (Future<OrderInfo> future : futures) {
            try {
                OrderInfo orderInfo = future.get();
                log.info("订单信息: {}", orderInfo);
            } catch (Exception e) {
                log.error("创建订单异常", e);
            }
        }
    }
}

 

mybatis-plus乐观锁触发情况

前置条件

  1. 操作表包含乐观锁字段及实体类有mybatis-plus的@Version注解
  2. @Version注解的字段在数据库中有默认值;
  3. 项目启用了mybatis-plus乐观锁插件,即:optimisticLockerInnerInterceptor 部分配置;
  4. 查询到当前的version值好与更新中的where配合

updateById【触发】

//查
Goods goods = goodsService.getById(1);
//更新
boolean update = goodsService.updateById(goods);
//判断更新结果
if(!update){
  //更新失败
  throw new RuntimeException('更新失败...')
}

提示:上方代码中查部分非常重要。

lambdaUpdate() 【条件触发】

触发情况

Entity entity = xxService.getById(id);
//....
xxService.lambdaUpdate()
  .set(x,y)
  .set(r,q)
  .eq(id,1)
  .update(entity);

 

Entity entity = xxService.getById(id);
//....
lambdaUpdate().update(entity);

该方案等效updateById

不触发情况

xxService.lambdaUpdate()
  .set(x,y)
  .set(r,q)
  .eq(id,1)
  .update();

 

手动设置

Entity entity = xxService.getById(id);
//....
xxService.lambdaUpdate()
  .set(Entity::getVersion,entity.getVersion()+1)
  //...其他值设置...
  .set(r,q)
  
  
  //版本条件,手动设置
  .eq(Entity::getVersion,entity.getVersion())
  //...其余条件...
  .eq(id,1)

  .update();

手动设置,效果与触发乐观锁效果一致,但是不是触发的mybatis plus乐观锁插件,是类似手写sql

lambdaUpdate 总结

问题 回答
lambdaUpdate 会触发 @Version 乐观锁吗? 会!但前提是:<br>1. 配置了 OptimisticLockerInnerInterceptor<br>2. 调用了 .update(entity) 并传入了包含 version 的实体对象
只写 .lambdaUpdate().eq(...).set(...).update() 能触发吗?

不能! 因为没有传入实体,无法获取 version 值用于 WHERE 条件

注意:通过手动查询和设置/比对version也能实现相同效果。但是不是触发的mybatis plus插件,属于手动事件

如何确保乐观锁生效? ✔️ 先查数据 → 获取 version → 调用 .update(entity)

 

updateBatchById【不触发】

不能!updateBatchById 批量更新方法新版多数都是不会触发的。

只有很老版本,有人发现批量里面是循环。那种可能会。

 


评论
User Image
提示:请评论与当前内容相关的回复,广告、推广或无关内容将被删除。

相关文章
效果参考需求商品只有库存1,多个用户同时下单仅其中一个能成功。并发下单多线程请求实现模拟并发下单,如上图所示,实现同1秒进入创建订单接口。mybatis-plu
java多线程编程_java多线程安全_java多线程实现安全锁CAS机制,CAS在java多线程中相当于数据库的乐观锁,synchronized相当于数据库的乐观锁。
本文将讲述什么是自旋锁?自旋锁的使用场景,什么情况适合使用自旋锁?Java 怎么使用自旋锁?
本文将讲述CLH锁的使用场景,什么情况适合使用CLH锁?Java 怎么使用CLH锁?
本文将讲述排队锁的使用场景,什么情况适合使用排队锁?Java 怎么使用排队锁?
本文将讲述MCS锁的使用场景,什么情况适合使用MCS锁?Java 怎么使用MCS锁?
mybatis plus 逻辑删除使用说明全局逻辑值配置,application.properties# 逻辑已删除值(默认为 1) mybatis-plus.global-config.db...
接上一篇:mybatis Interceptor拦截器实现自定义扩展查询兼容mybatis plus-xqlee (blog.xqlee.com)这里进行自定义分页查询扩展,基于mybatis ...
mybatis plus starter 3.3.x以内配置分页 @Bean public PaginationInterceptor paginationInterceptor() {...
mybatis plus 自增长主键如何获取注意在model对象里面配置以下注解即可在调用save()方法后通过对象get获取@TableId(type = IdType.AUTO) BigI...
mybatis Interceptor拦截器实现自定义扩展查询兼容mybatis plus @Intercepts({ @Signature(type = Executor.c...
mybatis plus find_in_set 使用wrapper.apply(StrUtil.isNotBlank(clazz)," find_in_set('"+clazz+"',claz...
spring boot mybatis 整合使用讲解介绍,spring boot与MyBatis的使用讲解介绍。spring boot mybatis xml mapper方式的入门和通过一个简...
引言    通过之前spring boot mybatis 整合的讲解: spring boot mybaties整合  (spring boot mybaties 整合 基于Java注解方式写...