完成了redis的库存的补充,redis库存和数据库库存,现在已经可以保证最终一致性了。
但这并不等同于项目的正确性。
先来回顾一下秒杀控制器的逻辑(核心部分源代码,其余伪代码。)
此处已实现库存补偿篇所说的优化 : 先检查重复秒杀,再预减库存
@RequestMapping(value="/{path}/do_miaosha",method=RequestMethod.POST)
@ResponseBody
public Result<Integer> doMiaosha() {
//0.判断用户是否非空、秒杀路径是否正确
// 1. 查看内存标记,看是否已结束
// 2.判断这个秒杀订单形成没有,避免重复秒杀
//3 预减少redis的库存
long stock=redisService.decr(GoodsKey.getMiaoshaGoodsStock,""+goodsId);
// 4 判断减数量之后的stock
if (stock < 0) {
// 库存补充
redisService.incr(GoodsKey.getMiaoshaGoodsStock, "" + goodsId);
// 设置 内存标记
localOverMap.put(goodsId, true);
// 返回响应,秒杀已结束
return Result.error(CodeMsg.MIAO_SHA_OVER);
}
//5.正常请求,入队
//返回0代表排队中
return Result.success(0);
}
但是还是有缺陷:
redis的库存还是会被同一用户的多个请求给抢掉,后续虽然能补充成功。但需要注意:
if (stock < 0) {
// 库存补充
redisService.incr(GoodsKey.getMiaoshaGoodsStock, "" + goodsId);
// 设置 内存标记
localOverMap.put(goodsId, true);
// 返回响应,秒杀已结束
return Result.error(CodeMsg.MIAO_SHA_OVER);
}
这里有个问题:
内存标记设置为true
了。那么后续即使把redis库存补充起来了。
对后续用户而言,根本通过不了第一步的内存标记。即:虽然还有库存,但买不到。
简单的解决办法,就是舍弃掉内存标记这一步,根据redis.decr
的结果判断是否入队,这样后续对redis的补充自然就能反馈到此处。
但这就完全抛弃了这里优势:减少对redis的压力。而且这一步对于流量的拦截是相当有力的,省去是很不合理的做法。
那么有没有办法从根源解决问题呢?
即:限制同一个用户的秒杀请求最多只有一个生效。这样就能保证一个人最多只能减掉的redis的一个库存。
如果满足了,其实消费端也不会出现重复订单了。
但安全起见,其余代码笔者也不作修改。
这时要使用的就是redis的 setnx
命令了。
可能人很多第一反应是"分布式锁",感觉性能很低。
确实"分布式锁"是通过
setnx
实现的,但那是自旋导致的等待。跟setnx
本身没啥关系而现在要用的,只是最基本的命令:不存在才设置,返回1。存在返回0。
封装函数如下
public <T> boolean setnx(KeyPrefix prefix, String key, T value) { Jedis jedis = null; try { jedis = jedisPool.getResource(); String str = beanToString(value); if (str == null || str.length() <= 0) { return false; } //生成真正的key String realKey = prefix.getPrefix() + key; Long res = jedis.setnx(realKey,str); return res == 1; } finally { returnToPool(jedis); } }
只要在预减库存前,增加一个setnx
的判断,用户id作为key,value随便放个。
如果设置成功,才放行。否则说明已经请求了,只是还没出结果,需要等待。
伪代码如下:
@RequestMapping(value="/{path}/do_miaosha",method=RequestMethod.POST)
@ResponseBody
public Result<Integer> doMiaosha() {
//0.判断用户是否非空、秒杀路径是否正确
// 1. 查看内存标记,看是否已结束
// 2.判断这个秒杀订单形成没有,避免重复秒杀
2.5 setnx,判断是否请求过
// 注意key 要同时有 userId 和 goodsId
boolean b = redisService.setnx
(MiaoshaKey.robRedisStock, "" + user.getId() + "_" + goodsId, true);
if (!b) {
return Result.error("已下单,请等待结果");
}
//3 预减少redis的库存
// 4 判断减数量之后的stock
if (stock < 0) {
}
return Result.success(0);
}
这里需要注意
setnx
应该放在重复秒杀的判断后。倒不是正确性的问题。只是返回提示的不同。因为能满足重复秒杀
if
的,肯定是满足if(!b)
的,如果已经秒杀到了,却返回了个"请等待结果",会让使用者误解。
这解决方案这么简洁,自然是有相应的问题存在的。
那就是消息出现异常时,需要删除掉key,才能让用户能重新请求。
项目中的"异常"包括:发送消息重发超限、消费出错等,但因为消费者宕机等很多原因,很容易就会漏删。
所以我们需要给该key设置一个较短的过期时间。笔者认为设置为整个业务能完成的时间就比较合理。(我选了10)
至此,最终解决方案就出来了。
@AccessLimit
能否完成任务?答案是可行的。但是该注解也是由redis实现的。而且其是直接作用在controller
的秒杀接口上
@AccessLimit(seconds = 10, maxCount = 1)
@RequestMapping(value = {"/{path}/do_miaosha")
@ResponseBody
public Result<Integer> miaosha(xxx)
就意味着每个调用该接口的都会先经拦截器处理,然后交由redis处理。再进入接口。
即:无法先经接口的内存标记筛选,也就受益不到内存标记的作用。而这显然是不合理的。
本文完,有误欢迎指出