记一次高并发下热点key的解决方案

背景:我们的优惠券系统发券是采用异步发券的模式。当优惠券发全量太大的时,因为要不断的去更新库存,这时候系统会出现热key问题,而且量越大速度越慢,同时还会导致mq消息大量堆积。大致数据如下,发放40万的券需要4个小时,近期我们要上线一个活动,预计同时需要发放100万的券,我们估算需要12个小时。

问题分析:目前业务上一个批次的库存量有上百万,而且还可以不断的追加库存,库存量越来越大,我们每次发券都会update库存,在并发较大的情况下,一旦开启事务,就会导致同批次的更新串行化,服务线程阻塞在该处,造成超时。cat上反馈的接口信息 

上图表面,一旦出现该sql阻塞的情况,就会造成,请求越大,sql执行效率越低,接口耗时越长,影响服务正常吞吐。
解决方案:我们采用Redis预扣库存的方式:第一次发券请求时申请固定步长的量放入Redis同时进行数据库扣减,后续每次请求都从Redis扣减,直到扣减完步长的量然后再次申请,我们假定步长是1万,那么1万次发券我们才进行一次数据库扣减库存操作,这样就解决了热key问题。

具体调用逻辑图如下:

    /**
     * 更新库存接口
     * @param batchId
     */
public Integer updateOutCountFromRedis(Integer batchId, String batchCountKey, int addOutCount)
            throws Exception {
        //1.判断redis是否存在该批次库存
        if (!redisTemplate.exist(batchCountKey)){
            //1.1 不存在执行增加预扣库存原子操作
            Integer result = addWithholdingInventory(batchId, batchCountKey, addOutCount);
            if (UPDATE_FAIL.equals(result)){
                return result;
            }
        }

        //2.判断redis 该批次库存是否足够
        Long redisOutCount = redisTemplate.getCount(batchCountKey);
        if (redisOutCount == null || redisOutCount < addOutCount){
            // 2.1 该批次库存不足时,进行增加预扣库存原子操作
            Integer result = addWithholdingInventory(batchId, batchCountKey, addOutCount);
            if (UPDATE_FAIL.equals(result)){
                return result;
            }
        }


        //3.执行redis扣减库存
        Long remainRedisCount = redisTemplate.increment(batchCountKey,-addOutCount);
        Cat.logEvent("postCoupon", batchId + "increment");
        if (remainRedisCount < 0){
            //如果发现不够扣减的,回退库存,且再次执行扣减
            Long returnCount = redisTemplate.increment(batchCountKey, addOutCount);
            Cat.logEvent("postCoupon", batchId + "getAndDecrement");
            return updateOutCountFromRedis(batchId, batchCountKey, addOutCount);
        } else {
            Cat.logEvent("postCoupon", batchId + "getAndIncrementsuccess");
            return UPDATE_SUCCESS;
        }
    }


    /**
     * 增加预扣库存原子操作
     * @param batchId
     */
    public Integer addWithholdingInventory(Integer batchId, String batchCountKey, int addOutCount)
            throws Exception {
        String lock = "redisOutCount_lock_" + batchId + "_lock";
        try {
            //分布式锁 + 双重检查
            if (redisTemplate.lock(lock,120, TimeUnit.SECONDS)){
                if (redisTemplate.exist(batchCountKey)){
                    Long redisOutCount = redisTemplate.getCount(batchCountKey);
                    if (redisOutCount != null && redisOutCount > addOutCount){
                        return UPDATE_SUCCESS;
                    }
                }
                Integer redisOutCountSegment = apolloConfigData.getIntValue("withholding.inventory.segment", 1000);
                CouponBatch couponBatch = getById(batchId);
                Integer remainCount = couponBatch.getTotalCount() - couponBatch.getOutCount();
                if (remainCount <= 0){
                    return UPDATE_FAIL;
                }
                if (remainCount < redisOutCountSegment){
                    redisOutCountSegment = remainCount;
                }
                Integer updateMysqlResult = couponBatchMapper.updateOutcount(batchId, redisOutCountSegment);
                if (updateMysqlResult == null || updateMysqlResult == 0){
                    return UPDATE_FAIL;
                }
                redisTemplate.increment(batchCountKey,redisOutCountSegment);
                logger.info(batchCountKey + "增加预扣库存原子操作成功,增加值:" + redisOutCountSegment);
                return UPDATE_SUCCESS;
            } else {
                Thread.sleep(10);
                return addWithholdingInventory(batchId, batchCountKey, addOutCount);
            }
        } catch (Exception e){
            logger.error("增加预扣库存原子操作失败!" , e);
            throw e;
        } finally {
            redisTemplate.unlock(lock);
        }
    }

优化前后发券性能对比:

操作消费组发券数发券耗时速度
优化前4个消费组15万35分钟 4285/分钟 
优化前4个消费组42万  250分钟1680/分钟 
优化后4个消费组62万 53分钟11698/分钟

效果:可以看出优化后发券速度提升了7倍。
 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值