在SeckillVoucherService中,修改addSeckillVouchers()方法将数据存入Redis中,以HashMap的方式存储
/**
* 添加需要抢购的代金券
*
* @param seckillVouchers
*/
@Transactional(rollbackFor = Exception.class)
public void addSeckillVouchers(SeckillVouchers seckillVouchers) {
// 非空校验
AssertUtil.isTrue(seckillVouchers.getFkVoucherId() == null, "请选择需要抢购的代金券");
AssertUtil.isTrue(seckillVouchers.getAmount() == 0, "请输入抢购总数量");
Date now = new Date();
AssertUtil.isNotNull(seckillVouchers.getStartTime(), "请输入开始时间");
// 生产环境下面一行代码需放行,这里注释方便测试
// AssertUtil.isTrue(now.after(seckillVouchers.getStartTime()), "开始时间不能早于当前时间");
AssertUtil.isNotNull(seckillVouchers.getEndTime(), "请输入结束时间");
AssertUtil.isTrue(now.after(seckillVouchers.getEndTime()), "结束时间不能早于当前时间");
AssertUtil.isTrue(seckillVouchers.getStartTime().after(seckillVouchers.getEndTime()), "开始时间不能晚于结束时间");
// ----------注释原始的 关系型数据库 的流程----------
// 验证数据库中是否已经存在该券的秒杀活动
// SeckillVouchers seckillVouchersFromDb = seckillVouchersMapper.selectVoucher(seckillVouchers.getFkVoucherId());
// AssertUtil.isTrue(seckillVouchersFromDb != null, "该券已经拥有了抢购活动");
// 插入数据库
// seckillVouchersMapper.save(seckillVouchers);
// ----------采用 Redis ----------
String redisKey = RedisKeyConstant.seckill_vouchers.getKey() + seckillVouchers.getFkVoucherId();
// 验证 Redis 中是否已经存在该券的秒杀活动
Map<String, Object> seckillVoucherMaps = redisTemplate.opsForHash().entries(redisKey);
AssertUtil.isTrue(!seckillVoucherMaps.isEmpty() && (int) seckillVoucherMaps.get("amount") > 0,
"该券已经拥有了抢购活动");
// 将数量同步到 Redis
seckillVouchers.setIsValid(1);
seckillVouchers.setCreateDate(now);
seckillVouchers.setUpdateDate(now);
seckillVoucherMaps = BeanUtil.beanToMap(seckillVouchers);
redisTemplate.opsForHash().putAll(redisKey, seckillVoucherMaps);
}
.
.
修改抢购业务
/**
* 抢购代金券
*
* @param voucherId 代金券 ID
* @param accessToken 登录token
* @Para path 访问路径
*/
@Transactional(rollbackFor = Exception.class)
public ResultInfo doSeckill(Integer voucherId, String accessToken, String path) {
// 基本参数校验
AssertUtil.isTrue(voucherId == null || voucherId < 0, "请选择需要抢购的代金券");
AssertUtil.isNotEmpty(accessToken, "请登录");
// ----------注释原始的走 关系型数据库 的流程----------
// 判断此代金券是否加入抢购
// SeckillVouchers seckillVouchers = seckillVouchersMapper.selectVoucher(voucherId);
// AssertUtil.isTrue(seckillVouchers == null, "该代金券并未有抢购活动");
// ----------采用 Redis 解问题----------
String redisKey = RedisKeyConstant.seckill_vouchers.getKey() + voucherId;
Map<String, Object> seckillVoucherMaps = redisTemplate.opsForHash().entries(redisKey);
SeckillVouchers seckillVouchers = BeanUtil.mapToBean(seckillVoucherMaps, SeckillVouchers.class, true, null);
// 判断是否有效
AssertUtil.isTrue(seckillVouchers.getIsValid() == 0, "该活动已结束");
// 判断是否开始、结束
Date now = new Date();
AssertUtil.isTrue(now.before(seckillVouchers.getStartTime()), "该抢购还未开始");
AssertUtil.isTrue(now.after(seckillVouchers.getEndTime()), "该抢购已结束");
// 判断是否卖完通过 Lua 脚本扣库存时判断
//AssertUtil.isTrue(seckillVouchers.getAmount() < 1, "该券已经卖完了");
// 获取登录用户信息
String url = oauthServerName + "user/me?access_token={accessToken}";
ResultInfo resultInfo = restTemplate.getForObject(url, ResultInfo.class, accessToken);
if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE) {
resultInfo.setPath(path);
return resultInfo;
}
// 这里的data是一个LinkedHashMap,SignInDinerInfo
SignInDinerInfo dinerInfo = BeanUtil.fillBeanWithMap((LinkedHashMap) resultInfo.getData(),
new SignInDinerInfo(), false);
// 判断登录用户是否已抢到(一个用户针对这次活动只能买一次)
VoucherOrders order = voucherOrdersMapper.findDinerOrder(dinerInfo.getId(),
seckillVouchers.getId());
AssertUtil.isTrue(order != null, "该用户已抢到该代金券,无需再抢");
// ----------注释原始的走 关系型数据库 的流程----------
// 扣库存
// int count = seckillVouchersMapper.stockDecrease(seckillVouchers.getId());
// AssertUtil.isTrue(count == 0, "该券已经卖完了");
// ----------采用 Redis 解问题----------
// 扣库存
long count = redisTemplate.opsForHash().increment(redisKey, "amount", -1);
AssertUtil.isTrue(count < 0, "该券已经卖完了");
// 下单
VoucherOrders voucherOrders = new VoucherOrders();
voucherOrders.setFkDinerId(dinerInfo.getId());
// Redis 中不需要维护外键信息
//voucherOrders.setFkSeckillId(seckillVouchers.getId());
voucherOrders.setFkVoucherId(seckillVouchers.getFkVoucherId());
String orderNo = IdUtil.getSnowflake(1, 1).nextIdStr();
voucherOrders.setOrderNo(orderNo);
voucherOrders.setOrderType(1);
voucherOrders.setStatus(0);
count = voucherOrdersMapper.save(voucherOrders);
AssertUtil.isTrue(count == 0, "用户抢购失败");
return ResultInfoUtil.buildSuccess(path, "抢购成功");
}
.
.
在减库存时,使用的lua脚本操作了Redis,因为减库存时,我们需要判断系统库存够不够,然后才能减掉,这里是两个操作,如果分开独立执行,那么有可能会出现错误(因为客户端是多线程),因此我们采用lua脚本将两步操作放到一起同时在Redis中执行(Redis是单线程操作,故不会出现安全问题)
将 stock.lua 脚本放入resources文件夹下,并在 Redis 配置类中添加以下代码:
@Bean
public DefaultRedisScript<Long> stockScript() {
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
//放在和application.yml 同层目录下
redisScript.setLocation(new ClassPathResource("stock.lua"));
redisScript.setResultType(Long.class);
return redisScript;
}
修改后JMeter测试结果正常
超卖的问题已经解决了,但是此时运行某个用户发起多个抢购请求的测试计划任然会出错,会出现一个人购买多份的情况。下文接着写
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。