商品只有库存1,多个用户同时下单仅其中一个能成功。
多线程请求实现模拟并发下单,如上图所示,实现同1秒进入创建订单接口。
从上图可以看到,三个请求均抵达数据库,且均调用了mybatis-plus乐观锁字段version
2失败1成功,与我们预期结果一致,测试通过。
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:已取消'
)
这里展示依赖部分
<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>
@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无法计算,乐观锁失效。
@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;
}
public interface GoodsMapper extends BaseMapper<Goods> {
}
public interface OrderInfoMapper extends BaseMapper<OrderInfo> {
}
@Slf4j
@Service
public class GoodsService extends ServiceImpl<GoodsMapper, Goods> {
}
提示:本案例为了简化,所有service均只有类没有接口,实际情况根据你的项目来。
@Slf4j
@Service
public class OrderInfoService extends ServiceImpl<OrderInfoMapper, OrderInfo> {
}
@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.java
optimisticLockerInnerInterceptor
部分- 注意哪些方法/情况会触发mybatis-plus的乐观锁
@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;
}
}
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
提示:数据信息,请根据自身情况修改
并发测试代码
@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);
}
}
}
}
@Version
注解@Version
注解的字段在数据库中有默认值;optimisticLockerInnerInterceptor
部分配置;//查
Goods goods = goodsService.getById(1);
//更新
boolean update = goodsService.updateById(goods);
//判断更新结果
if(!update){
//更新失败
throw new RuntimeException('更新失败...')
}
提示:上方代码中查部分非常重要。
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 批量更新方法新版多数都是不会触发的。
⚠只有很老版本,有人发现批量里面是循环。那种可能会。
https://blog.xqlee.com/article/2509161224089456.html