效果参考
需求
商品只有库存1,多个用户同时下单仅其中一个能成功。
并发下单
 
多线程请求实现模拟并发下单,如上图所示,实现同1秒进入创建订单接口。
mybatis-plus 乐观锁使用
 
从上图可以看到,三个请求均抵达数据库,且均调用了mybatis-plus乐观锁字段version
执行结果
 
2失败1成功,与我们预期结果一致,测试通过。
项目环境
- spring boot 3.5.5
- mybatis-plus-spring-boot3-starter 3.5.7
项目
项目结构图
 
相关代码
初始化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;
    }
}提示:
- 模拟中间业务的1s所有订单均相等,会导致谁先进
createOrder方法谁就抢到。实际业务耗时因素很多不确定,有可能是后来抢到(比如第一个进来做中间其他业务就刚好cpu没抢到满了一丢丢就会导致后面的抢到)。- 可能发生并发业务的方法内所有数据库操作均需要接收和处理结果,结果不成功需要抛异常事务才会回滚,保证业务数据一致性。
- 需要使用mybatis-plus乐观锁逻辑注意配置乐观锁插件,本教程参考配置类
MybatisPlusConfig.javaoptimisticLockerInnerInterceptor部分- 注意哪些方法/情况会触发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乐观锁触发情况
前置条件
- 操作表包含乐观锁字段及实体类有mybatis-plus的@Version注解
- @Version注解的字段在数据库中有默认值;
- 项目启用了mybatis-plus乐观锁插件,即:optimisticLockerInnerInterceptor部分配置;
- 查询到当前的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也能实现相同效果。但是不是触发的mybatis plus插件,属于手动事件 | 
| 如何确保乐观锁生效? | ✔️ 先查数据 → 获取 version→ 调用.update(entity) | 
updateBatchById【不触发】
❌ 不能!updateBatchById 批量更新方法新版多数都是不会触发的。
⚠只有很老版本,有人发现批量里面是循环。那种可能会。
https://blog.xqlee.com/article/2509161224089456.html
 
                 
                      
评论