什么是一人一单
一人一单是指每个用户只能购买一次商品,这种情况通常发生在限购活动中,比如双十一、618等大促销活动。一人一单的原因主要是为了防止用户恶意刷单,导致库存不足。
如何解决一人一单问题
查询数据库中对应订单记录数量,如果大于0则表示用户已经购买过,不能再次购买。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| @Override @Transactional public Result seckillVoucher(Long voucherId) { SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) { return Result.fail("秒杀未开始"); }
if (voucher.getEndTime().isBefore(LocalDateTime.now())) { return Result.fail("秒杀已结束"); }
if (voucher.getStock() < 1) { return Result.fail("库存不足"); }
Long userId = UserHolder.getUser().getId();
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0) { return Result.fail("用户已经购买过一次!"); }
boolean success = seckillVoucherService.update() .setSql("stock = stock - 1") .eq("voucher_id", voucherId) .gt("stock", 0)
.update(); if (!success) { return Result.fail("库存不足!"); }
VoucherOrder voucherOrder = new VoucherOrder(); long orderId = redisIdWorker.nextId("order"); voucherOrder.setId(orderId); voucherOrder.setUserId(userId); voucherOrder.setVoucherId(voucherId); save(voucherOrder);
return Result.ok(orderId); }
|
使用jmeter进行压测,发现存在并发问题,即多个请求同时进入时,会导致用户购买多次。
为什么会出现这种情况呢?因为query()查询订单记录不会有数据库锁进行隔离,会存在并发问题。
当第一次请求进入时,查询订单记录数量为0,此时其他请求也进入,查询订单记录数量也为0,然后多个请求都执行扣减库存操作,导致用户购买多次。
如何解决并发问题
加锁解决并发问题,保证查询订单记录和扣减库存操作的原子性。
使用悲观锁来处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
| @Override public Result seckillVoucher(Long voucherId) { SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) { return Result.fail("秒杀未开始"); }
if (voucher.getEndTime().isBefore(LocalDateTime.now())) { return Result.fail("秒杀已结束"); }
if (voucher.getStock() < 1) { return Result.fail("库存不足"); }
Long userId = UserHolder.getUser().getId(); synchronized (userId.toString().intern()) { IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy(); return proxy.createVoucherOrder(voucherId); } }
@Transactional public Result createVoucherOrder(Long voucherId) { Long userId = UserHolder.getUser().getId();
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0) { return Result.fail("用户已经购买过一次!"); }
boolean success = seckillVoucherService.update() .setSql("stock = stock - 1") .eq("voucher_id", voucherId) .gt("stock", 0) .update(); if (!success) { return Result.fail("库存不足!"); }
VoucherOrder voucherOrder = new VoucherOrder(); long orderId = redisIdWorker.nextId("order"); voucherOrder.setId(orderId); voucherOrder.setUserId(userId); voucherOrder.setVoucherId(voucherId); save(voucherOrder);
return Result.ok(orderId);
}
|
注意,使用sync锁定用户id,但是不能放在方法上,因为会导致串行化,性能下降。
并且,userId要使用intern来保证锁的唯一性,intern会在常量池中查找是否存在该对象,如果存在则返回常量池中的对象,如果不存在则放入常量池并返回。如果不使用intern,那么每次都会创建新的对象,导致锁失效。
同时,方法使用@Transactional注解,保证事务的一致性。
由于spring通过代理模式来实现事务,如果直接调用this.func,那么就不会走代理,导致事务失效,所以这里使用代理对象来调用createVoucherOrder方法。
如何获取代理对象呢?通过AopContext.currentProxy()来获取代理对象。
注意添加注解让代理对象暴露出来:
1
| @EnableAspectJAutoProxy(exposeProxy = true)
|
同时别忘了修改pom.xml:
1 2 3 4 5
| <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.7</version> </dependency>
|
进行jmeter压测,发现并发问题得到解决,用户只能购买一次商品。

数据库显示:

显示成功,用户只能购买一次商品。