限购问题
2026年2月22日大约 4 分钟
限购问题
限购问题是指对每个用户购买的数量进行限制。
- 同样限购问题也会出现线程安全问题,但是这里不能再像上一章使用乐观锁来解决了。
- 因为乐观锁是对库存进行更新时,去比较库存的值是否被修改了。
- 而限购问题是通过查询,对用户购买的数量进行限制的,所以无法直接使用乐观锁来解决。
使用悲观锁解决限购问题
Java中悲观锁可以通过Synchronized或者Lock来实现,这里以Synchronized为例,介绍一下如何使用Synchronized来解决限购问题:
注意
在上一章我提到过
悲观锁在操作数据之前先获取锁,确保线程串行执行。
因此如果我们对整个原始方法加锁,那么在高并发的情况下,性能会非常差,因为每个请求都需要等待前一个请求完成才能执行。
为了解决上面的问题,我们可以减少锁的粒度:
- 即对每个用户进行加锁,确保同一个用户的请求是串行执行的,而不同用户的请求是并行执行的。
- 为了方便阅读,我们这里把需要上锁的代码抽取出来,放在一个单独的方法中,这样就可以只对这个方法加锁,减少锁的粒度,提高性能。
- 示例:
@Transactional
public Result createVoucherOrder(Long voucherId) {
Long userId = UserHolder.getUser().getId();
long count = lambdaQuery().eq(VoucherOrder::getVoucherId, voucherId)
.eq(VoucherOrder::getUserId, userId)
.count();
if(count != 0){
return Result.fail("同种优惠券一人只允许抢购一张");
}
// 4. 扣库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId).gt("stock", 0)
.update();
if(!success) {
return Result.fail("抢购失败");
}
// 5. 创建订单
VoucherOrder voucherOrder = new VoucherOrder();
// 订单id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId)
.setUserId(userId) // 用户id
.setVoucherId(voucherId); // 代金券id
save(voucherOrder);
// 6. 返回订单ID
return Result.ok(orderId);
}问题1-事务范围问题
注意
可以看到我已经抽离出来了需要加锁的方法,但是为什么现在我还没有加锁呢?
- 这是因为如果我们写在这个方法里面,锁会在事务提交前就被释放,仍然会有线程安全问题。
因此我们需要在调用这个方法的地方加锁,确保整个方法的执行过程都是在锁的保护下进行的。
问题2-传入锁对象问题
注意
在加锁传入锁对象要注意,可能会使用到包装类的.toString()方法。
- 该方法会返回一个字符串对象,每次调用都会返回一个新的字符串对象,因此无法保证锁的唯一性。
解决方法是使用toString().intern()方法,该方法会返回字符串常量池中的字符串对象,保证锁的唯一性。
问题3-事务失效问题
注意
此外在调用这个方法的时候要注意,直接调用这个方法会导致事务失效。
- 因为事务是通过AOP实现的,而直接调用方法会绕过AOP代理,导致事务失效。
- 原理是因为,这个方法是在同一个类里面的,默认会
this.createVoucherOrder(voucherId)来调用这个方法,而不是通过Spring的代理对象来调用这个方法,所以事务会失效。
因此我们需要通过Spring的代理对象来调用这个方法,确保事务能够正常工作。
- 这里需要使用
AopContext.currentProxy()来获取当前的代理对象,然后通过代理对象来调用 这个方法。 - 在使用前需要:
- 添加
aspectj依赖。<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> </dependency> - 在启动类上添加
@EnableAspectJAutoProxy(exposeProxy = true)注解暴露代理对象。
- 添加
示例
@Override
public Result secKillVoucher(Long voucherId) {
// 1. 查询优惠券信息
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
// 2. 判断是否在秒杀时间内
// 2.1 不在则直接返回
if(voucher.getBeginTime().isAfter(LocalDateTime.now()) ) {
return Result.fail("秒杀还未开始");
}
if(voucher.getEndTime().isBefore(LocalDateTime.now())) {
return Result.fail("秒杀已经结束");
}
// 3. 判断库存是否充足
if(voucher.getStock()< 1){
return Result.fail("库存不足");
}
// 核心:解决限购问题
Long userId = UserHolder.getUser().getId();
synchronized (userId.toString().intern()) {
VoucherOrderServiceImpl proxy = (VoucherOrderServiceImpl) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}
}