secKill项目 --- 限制同一用户只有一个请求生效 + 回补Redis库存如何反馈到内存标识的思考(有库存,买不到)

完成了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设置一个较短的过期时间。笔者认为设置为整个业务能完成的时间就比较合理。

这就是笔者的解决方案。

未解决的问题

上述的key设置了过期时间,那如果确实异常,导致键过期了。

  1. 如何捕获这个消息,进行库存的补充?

这里是Redis的监听过期key的策略在的,可是这个十分损耗性能。没有采用

  1. 依然没有本质的解决内存标识的问题。因为除了上述的"Redis库存被同一用户的多个请求"外,其余地方出现异常,或者更简单的用户退款等,这些对Redis的补充是不能忽略
    那这个补充如何反馈到HashMap上

笔者思路:

  1. 单机环境下,把HashMap的范围改为public,补充库存后,直接重置HashMap为true。

  2. 集群环境下,就把补充的消息推送到MQ,各个节点进行监听,修改本地的HashMap。或者直接依靠集群的同步机制完成。

  3. 直接依靠定时任务,根据redis的库存定时更新HashMap的值。

    这种方法缺点也很明显,定时间隔长了,实时性不强。间隔短,性能低

无论是哪种实现,其实都不用担心是否误覆盖了HashMap。
比如极端情况下的,A补充了redis的库存,B又把该库存消耗掉了,然后设置HashMap为false。
因为HashMap判断后,我们还是会根据Redis的库存数量再进行操作。

笔者没有作任何一种的处理,因为这些实现都是需要代价的。
而且参考相关资料,也说不作处理是主流。


拓:@AccessLimit 能否完成任务?

答案是可行的。但是该注解也是由redis实现的。而且其是直接作用在controller的秒杀接口上

	@AccessLimit(seconds = 10, maxCount = 1)
    @RequestMapping(value = {"/{path}/do_miaosha")
    @ResponseBody
    public Result<Integer> miaosha(xxx)

就意味着每个调用该接口的都会先经拦截器处理,然后交由redis处理。再进入接口。

即:无法先经接口的内存标记筛选,也就受益不到内存标记的作用。而这显然是不合理的。


本文完,有误欢迎指出

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值