关于创建订单一人一单问题
本文最后更新于:2025年6月25日 晚上
场景
界面上显示秒杀券,用户可以抢购,但是每位用户只能抢购一张。
秒杀券表(tb_seckill_voucher)结构如下:
CREATE TABLE `tb_seckill_voucher` (
`voucher_id` bigint(20) unsigned NOT NULL COMMENT '关联的优惠券的id',
`stock` int(8) NOT NULL COMMENT '库存',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`begin_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '生效时间',
`end_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '失效时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`voucher_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=COMPACT COMMENT='秒杀优惠券表,与优惠券是一对一关系';
优惠券订单表(tb_voucher_order)结构如下
CREATE TABLE `tb_voucher_order` (
`id` bigint(20) NOT NULL COMMENT '主键',
`user_id` bigint(20) unsigned NOT NULL COMMENT '下单的用户id',
`voucher_id` bigint(20) unsigned NOT NULL COMMENT '购买的代金券id',
`pay_type` tinyint(1) unsigned NOT NULL DEFAULT '1' COMMENT '支付方式 1:余额支付;2:支付宝;3:微信',
`status` tinyint(1) unsigned NOT NULL DEFAULT '1' COMMENT '订单状态,1:未支付;2:已支付;3:已核销;4:已取消;5:退款中;6:已退款',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '下单时间',
`pay_time` timestamp NULL DEFAULT NULL COMMENT '支付时间',
`use_time` timestamp NULL DEFAULT NULL COMMENT '核销时间',
`refund_time` timestamp NULL DEFAULT NULL COMMENT '退款时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=COMPACT;
用户抢券的方法(seckillCreate.java)
@Override
@Transactional
public Result seckillCreate(Long voucherId) {
// 查询优惠券信息
SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
// 判断到没到抢券的时间
xxx
return createOrder(voucherId);
}
创建订单(createOrder.java)简单代码如下:
public Result createOrder(Long voucherId){
// 当前登录的用户
UserDTO user = UserHolder.getUser();
// 查库
Integer count = query().eq("user_id", user.getId()).eq("voucher_id", voucherId).count(); //<X>
if (count > 0) {
return Result.fail("每人最多只能持有一张,不要贪心哦!");
}
// 创建订单信息
VoucherOrder voucherOrder = new VoucherOrder();
xxx
this.save(voucherOrder); // <Y>
return Result.ok(orderId);
}
但是这样的话,多线程并发会出现一个用户购买多张券的情况。即在刚开始时,库里还没有任何订单,同一时间可能存在多个线程在
由于在
尝试一:
@Override
@Transactional
public Result seckillCreate(Long voucherId) {
// 查询优惠券信息
SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
// 判断到没到抢券的时间
xxx
return createOrder(voucherId);
}
public synchronized Result createOrder(Long voucherId){
// 当前登录的用户Id
Long userId = UserHolder.getUser().getUserId();
// 查库
Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0) {
return Result.fail("每人最多只能持有一张,不要贪心哦!");
}
// 创建订单信息
VoucherOrder voucherOrder = new VoucherOrder();
xxx
this.save(voucherOrder);
return Result.ok(orderId);
}
在方法上加 synchronized
,但是这样的话锁的范围是整个方法,锁的对象是 this
整个方法都会加上锁,粒度太粗,导致任何一个用户都会加锁,而且是同一把锁,串行执行,严重影响性能。我们想要的是按照同一用户来加锁,即同一个用户才加锁。且真正的事务是在创建订单那里,第一步查券不需要事务,把用户抢券那个方法的事务放到创建订单的方法上。
尝试二:
@Override
public Result seckillCreate(Long voucherId) {
// 查询优惠券信息
SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
// 判断到没到抢券的时间
xxx
return createOrder(voucherId);
}
@Transactional
public Result createOrder(Long voucherId){
// 当前登录的用户Id
Long userId = UserHolder.getUser().getUserId();
// String.toString()
synchronized(userId.toString().intern()) { // <X>
Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0) {
return Result.fail("每人最多只能持有一张,不要贪心哦!");
}
// 创建订单信息
VoucherOrder voucherOrder = new VoucherOrder();
xxx
this.save(voucherOrder);
return Result.ok(orderId);
}
}
注意,
这样还存在一个问题,就是当前的事务是加在方法上的,且交由Spring进行提交的。那么,如果当前方法执行完了,锁已经释放了,还没来的及提交,另外一个线程进来了。然后查询数据库,发现还没数据,又进行了新增订单,就出现了多个订单的情况。
尝试三:
@Override
public Result seckillCreate(Long voucherId) {
// 查询优惠券信息
SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
// 判断到没到抢券的时间
xxx
// 当前登录的用户Id
Long userId = UserHolder.getUser().getUserId();
synchronized(userId){
return createOrder(voucherId); // <Z>
}
}
@Transactional
public Result createOrder(Long voucherId){
Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0) {
return Result.fail("每人最多只能持有一张,不要贪心哦!");
}
// 创建订单信息
VoucherOrder voucherOrder = new VoucherOrder();
xxx
this.save(voucherOrder);
return Result.ok(orderId);
}
至此,应该就没问题了吧。上锁,生成订单,提交事务,最后再释放锁,已经能确保线程安全了。但是。。。。线程安全就能确保业务安全了吗?就能按照我们所想的方式完成了吗?NoNoNo,这里还有一个事务问题。注意 this.createOrder(voucherId)
,也就是说调用的是当前这个实例的对象,并不是它的代理,Spring事务管理用的是当前实例的代理对象来进行事务处理,所以没有事务功能,事务失效的几种可能性之一。
尝试四:
@Override
public Result seckillCreate(Long voucherId) {
// 查询优惠券信息
SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
// 判断到没到抢券的时间
xxx
Long userId = UserHolder.getUser().getId();
String userIdIntern = userId.toString().intern();
synchronized (userIdIntern){
IVoucherOrderService iVoucherOrderService = (IVoucherOrderService) AopContext.currentProxy();
return iVoucherOrderService.createOrder(voucherId, seckillVoucher, userId);
}
}
@Transactional
public Result createOrder(Long voucherId){
Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0) {
return Result.fail("每人最多只能持有一张,不要贪心哦!");
}
// 创建订单信息
VoucherOrder voucherOrder = new VoucherOrder();
xxx
this.save(voucherOrder);
return Result.ok(orderId);
}
使用AopContext.currentProxy()
获取当前代理的对象,使用代理对象的事务方法来执行就可以了。使用AopContext
需要导入依赖:
<!-- aop代理 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>XML
并且需要在启动类上添加注解:@EnableAspectJAutoProxy(exposeProxy = true)
验证
可以看到,只有一个请求成功且返回了订单ID,其他全部失败,