目录
前言
数据库脚本与测试用例在文末
背景
在抢购商品或优惠券的时候,如果是大量用户同时抢购,容易出现多抢购的情况,订单的数量超过库存。
本文章主要使用redis模拟演示秒杀优惠券过程出现的超卖现象与解决。
表
使用到的三张表。
voucher 优惠券表
vouchers_seckill 优惠券秒杀活动列表
voucher_order 优惠券订单表
说明
优惠券秒杀过程:
①新建优惠券信息;
②选择优惠券添加到秒杀活动列表,并保存到redis;
③抢购优惠券,生产优惠券订单。
本文主要演示将秒杀活动缓存到redis后的抢购演示。
超卖问题
展示主要代码
说明:模拟用户登录后抢购的过程。因为redis的递减操作不为increment原子性,容易出现超卖问题。
1.新建controller
@PostMapping("/{userId}/{voucherId}")
@ApiOperation("用户秒杀优惠券-redis-有超卖问题")
public Result seckillVoucherByredisNo(@PathVariable("userId") Integer userId, @PathVariable("voucherId") Integer voucherId){
Result result = seckillService.seckillVoucherByredis(userId,voucherId);
return Result.success("成功");
}
2.新建serviceimpl
@Override
public Result seckillVoucherByredis(Integer userId, Integer voucherId) {
//1.判断用户是否登录
Assert.isFalse(userId == null,"用户未登录","");
//2.优惠券是否有效 表达式为true抛出异常
Assert.isFalse(voucherId < 0 && voucherId == null,"优惠券无效","");
//3.活动是否开始
String seckillKey = RedisContants.VOUCHER_SECKill.getKey()+voucherId;
Map<String,Object> map = redisTemplate.opsForHash().entries(seckillKey);
VouchersSeckill vouchersSeckill = BeanUtil.mapToBean(map, VouchersSeckill.class, true, CopyOptions.create());
Date startTime = vouchersSeckill.getStartTime();
Date endTime = vouchersSeckill.getEndTime();
Assert.isFalse(new Date().getTime() < startTime.getTime(),"活动未开始");
//4.活动是否结束
Assert.isFalse(new Date().getTime() > endTime.getTime(),"活动已结束");
//5.数量是否抢购完
Assert.isFalse(vouchersSeckill.getAmount()<=0,"已经抢购一空");
//7.扣减库存
//递减操作不是原子性,并发情况下,操作优惠券库存会不正确。
redisTemplate.opsForHash().increment(seckillKey,"amount",-1);
//8.新增订单
VoucherOrder voucherOrder = new VoucherOrder();
voucherOrder.setFkUserId(userId);
voucherOrder.setFkSeckillId(vouchersSeckill.getId());
voucherOrder.setFkVoucherId(voucherId);
voucherOrder.setOrderNo(IdUtil.simpleUUID());
voucherOrderService.save(voucherOrder);
return Result.success();
}
jmeter测试
使用jmeter模拟1000个用户抢购优惠券。配置如下:
演示过程:
1.访问http:localhost:8084/doc.html.往redis添加优惠券id为2,库存100的秒杀活动。在redis中查到如下数据,说明添加成功。
2.使用jmeter测试1000用户并发
点击超卖问题脚本后,待脚本执行结束,查看redis,发现库存为-100,生成的订单数量为200.超出了100的库存
使用lua脚本解决超卖问题
展示主要代码
在原先有超卖问题的代码上进行了稍微的改动,从redis查询库存再到修改使用了lua脚本保证原子性。
controller层
@PostMapping("/lua/{userId}/{voucherId}")
@ApiOperation("用户秒杀优惠券-redis-使用lua脚本解决超卖问题")
public Result seckillVoucherByredisLua(@PathVariable("userId") Integer userId, @PathVariable("voucherId") Integer voucherId){
Result result = seckillService.seckillVoucherByredisLua(userId,voucherId);
return Result.success("成功");
}
serviceImpl层
@Override
public Result seckillVoucherByredisLua(Integer userId, Integer voucherId) {
//1.判断用户是否登录
Assert.isFalse(userId == null,"用户未登录","");
//2.优惠券是否有效 表达式为true抛出异常
Assert.isFalse(voucherId < 0 && voucherId == null,"优惠券无效","");
//3.活动是否开始
String seckillKey = RedisContants.VOUCHER_SECKill.getKey()+voucherId;
Map<String,Object> map = redisTemplate.opsForHash().entries(seckillKey);
VouchersSeckill vouchersSeckill = BeanUtil.mapToBean(map, VouchersSeckill.class, true, CopyOptions.create());
Date startTime = vouchersSeckill.getStartTime();
Date endTime = vouchersSeckill.getEndTime();
Assert.isFalse(new Date().getTime() < startTime.getTime(),"活动未开始");
//4.活动是否结束
Assert.isFalse(new Date().getTime() > endTime.getTime(),"活动已结束");
//5.数量是否抢购完
//使用lua脚本保证原子性
List<String> keys = new ArrayList<>();
keys.add(seckillKey);
keys.add("amount");
Long stockOversold = (Long) redisTemplate.execute(stockOversoldScript, keys);
Assert.isFalse(stockOversold == null || stockOversold < 1,"已经抢购一空");
//8.新增订单
VoucherOrder voucherOrder = new VoucherOrder();
voucherOrder.setFkUserId(userId);
voucherOrder.setFkSeckillId(vouchersSeckill.getId());
voucherOrder.setFkVoucherId(voucherId);
voucherOrder.setOrderNo(IdUtil.simpleUUID());
voucherOrderService.save(voucherOrder);
return Result.success();
}
lua脚本
config配置
jmeter测试
模拟1000用户并发操作,除了接口其他配置与前面相同。
1.清空优惠券订单表数据
2.往redis添加优惠券id为2,库存100的秒杀活动。在redis中查到如下数据,说明添加成功。
3.启动jmeter测试
脚本执行完毕后,可以看到redis中库存为0,优惠券订单表生成100个订单。成功解决超卖问题。