Redis代金卷(优惠卷)秒杀案例-单应用版
优惠卷表:优惠卷基本信息,优惠金额,使用规则 包含普通优惠卷和特价优惠卷(秒杀卷)
优惠卷的库存表:优惠卷的库存,开始抢购时间,结束抢购时间.只有特价优惠卷(秒杀卷)才需要填写这些信息
优惠卷订单表
卷的表里已经有一条普通优惠卷记录
下面首先新增一条秒杀优惠卷记录
{
"shopId": 1,
"title": "100元代金券",
"subTitle": "周一至周五均可使用",
"rules": "全场通用\\n无需预约\\n可无限叠加\\n不兑现、不找零\\n仅限堂食",
"payValue": 8000,
"actualValue": 10000,
"type": 1,
"stock": 100,
"beginTime": "2022-01-25T10:09:17",
"endTime": "2022-01-26T12:09:04"
}
返回一个订单
实现秒杀下单
就是往下面这张表中添加数据 创建订单
这里暂时只需要添加主键id 购买的代金卷id
对应的还需要去扣减库存
关于订单的主键
使用Redis生成全局唯一ID示例-CSDN博客
控制器
业务层
传入的优惠卷id
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Autowired
private ISeckillVoucherService seckillVoucherService;
@Autowired
private RedisIdWorker redisIdWorker;
@Override
@Transactional(rollbackFor = Exception.class)
public Result seckillVoucher(Long voucherId) {
//1.查询优惠券
SeckillVoucher byId = seckillVoucherService.getById(voucherId);
if (byId.getBeginTime().isAfter(LocalDateTime.now())) {
//2.判断秒杀是否开始
return Result.fail("秒杀尚未开始");
}
if (byId.getEndTime().isBefore(LocalDateTime.now())) {
//3.判断秒杀是否结束
return Result.fail("秒杀已经结束");
}
if (byId.getStock() < 1) {
//4.判断库存是否充足
return Result.fail("库存不足");
}
//5.扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId)
.update();
if(!success){
return Result.fail("库存不足");
}
//6.创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//6.1.订单id
long order = redisIdWorker.nextId("order");
voucherOrder.setId(order);
voucherOrder.setUserId(1L);//登录用户先写死
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
return Result.ok(order);
}
}
以上代码,在高并发场景下会出现超卖现象
以下用乐观锁方式解决超卖问题
用乐观锁 CAS法解决超卖问题
就是在更新时候添加个条件
当200个并发线程打进去之后
虽然解决了并发安全问题,但是出现成功率低的问题 200个线程进来 没有卖完 很多都失败了
如何解决??
修改下条件 where id=? and stock>0 即可
这样就可以解决超卖问题
以上是用JMeter并发测试的,从结果看,系统还存在一个问题,例如对方开启并发访问的工具,这样会导致所有订单都是同一个用户购买的情况,或者说一个人购买了好几单
在订单表中出现如图情况
这样的漏洞,就好比黄牛了,那么系统如何实现一人一单,就是说一个用户最多下一个订单的需求
其实很简单 只要满足user_id 和 voucher_id唯一即可
就是做个查询,该用户有没有下单
如果这样写 并发情况下还是会出现问题
因为当你去判断count>0时候可能已经有10个线程都完成了查询 在判断count的时候 都查出来等于0的情况
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Autowired
private ISeckillVoucherService seckillVoucherService;
@Autowired
private RedisIdWorker redisIdWorker;
@Override
public Result seckillVoucher(Long voucherId) {
//1.查询优惠券
SeckillVoucher byId = seckillVoucherService.getById(voucherId);
if (byId.getBeginTime().isAfter(LocalDateTime.now())) {
//2.判断秒杀是否开始
return Result.fail("秒杀尚未开始");
}
if (byId.getEndTime().isBefore(LocalDateTime.now())) {
//3.判断秒杀是否结束
return Result.fail("秒杀已经结束");
}
if (byId.getStock() < 1) {
//4.判断库存是否充足
return Result.fail("库存不足");
}
Long id = UserHolder.getUser().getId();//表示获取用户id
//id.toString()实际是new了一个String对象,然后调用intern()方法,这个方法会去常量池中查找有没有这个对象,如果有就返回这个对象,如果没有就添加到常量池中,然后返回这个对象
synchronized (id.toString().intern()){
//如果这样调用,前面有个this 事务使用代理对象去执行的
//return createVoucherOrder(voucherId);
IVoucherOrderService proxy = (IVoucherOrderService)AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}
}
@Transactional(rollbackFor = Exception.class)
public Result createVoucherOrder(Long voucherId) {
Long id = UserHolder.getUser().getId();//
//一人一单的查询
Integer count = query().eq("user_id", id)
.eq("voucher_id", voucherId)
.count();
if(count > 0){
return Result.fail("您已经购买过一次了");
}
//5.扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId)
.gt("stock", 0)
.update();
if(!success){
return Result.fail("库存不足");
}
//6.创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//6.1.订单id
long order = redisIdWorker.nextId("order");
voucherOrder.setId(order);
voucherOrder.setUserId(id);//登录用户先写死
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
Result.ok(order);
}
}
以上方案仅适用于单机版本