Redis中惰性策略的启发和流量包应用设计

引言

        在技术领域,许多中间件之所以获得巨大成功,部分原因在于它们所采用的思想之先进。这些思想解决了一个个世纪难题,接下来我将讲述一个我学习到的思想,并将其应用至工作中的案例。

        惰性策略在日常编码中随处可见,但究竟什么是惰性策略呢?简而言之,惰性策略是一种优化方法,即在不需要进行计算或操作时,不会真正进行相关的处理,而是仅仅记录相关信息或轨迹。只有在需要执行行动操作时,才会触发从头到尾的真正计算。这种机制大大减少了不必要的资源消耗,提高了程序的效率。惰性策略的使用有很多,其中比较常见的便是Redis了,从中学习这些思想可以在我们日后遇到难题时得到帮助。

中间件设计思想:Redis过期Key淘汰策略

        在早些年作为编程小白的我,在使用Redis时常会想一些问题,例如:Redis的Key配置了过期时间,这个是怎么被删除的?Redis数据明明过期了,怎么还占用着内存?

主动策略和惰性策略

        对于这些问题,曾设想过他们的设计思路,例如对于如何清除过期的 Key ,很自然的可以想到就是可以给每个 key 加一个定时器,这样当时间到达过期时间的时候就自动删除 key,这种定时策略也叫主动策略。

        但从辩证角度来看这种方式使之有过期时间的 Key都需要一个定时器,那么这对 CPU 是极不友好的,会占用较多的CPU资源。后来在不断探究过程中,Redis同样也使用了惰性策略,即不用定时器,采取被动的方式,在访问一个 key 的时候去判断这个 key 是否到达过期时间了,过期了则删除掉。

        这种定期删除+惰性删除的Key过期策略,使得不会立即从内存中删除,当过期key未被客户端调用且未达到执行主动策略的时间,此Key依旧存在内存中。通过配合使用这两种删除策略,服务器可以很好地在合理使用CPU时间和避免浪费内存空间之间取得平衡。如果定期删除漏掉了很多过期 Key的同时也没及时去查,没走惰性删除,就是造成大量过期 key 堆积在内存里,最终会导致 redis 内存块耗尽,那么Redis此时会走内存淘汰机制。

如何淘汰过期的keys

       通过redis命令行运行set name xdclass 3600后,每个设置了过期时间的Key都会放入到一个独立的容器中。

定期删除

        隔一段时间,就随机抽取一些设置了过期时间的 key,检查其是否过期,如果过期就删除,这种定期删除的方式可能会导致很多过期 Key 到了时间并没有被删除掉。

      摘自官方文档:EXPIRE | Redis

Redis 会每秒进行10次过期扫描,过期扫描不会遍历容器中所有的 key,而是采用一种特殊策略

        1)从容器中随机 20 个 key;
        2)删除这 20 个 key 中已经过期的 key;
        3)如果过期的 key 比率超过 1/4,那就重复步骤 1;

惰性删除

        当某个客户端试图访问key时,发现该key已超时会把此key从内存中删除。

主从架构Key删除策略

       从节点不会让key过期,而是主节点的key过期删除后,成为del命令传输到从节点进行删除
主库在 key 到期时,会在 AOF 文件里增加一条 del 指令,同步到所有的从库,从库通过执行这条 del 指令来删除过期的 key。指令同步是异步进行的,所以主库过期的 key 的 del 指令没有及时同步到从库的话,会出现主从数据的不一致,主库没有的数据在从库里还存在。

架构中的启发

        类似于Redis的这种思想其实在主流的中间架构中几乎随处可见,例如Spring中bean创建懒加载(延迟加载)、设计模式中单例创建的懒汉式、Mybatis的懒加载,借助于这种思想在工作中解决了许多数据更新问题,也延伸出了许多方案。例如我再在实际工作中流量包更新维护需求,免费流量包:业务为了拉新,鼓励新用户注册,赠送一个免费流量包,每天允许有一定次免费创建短链的次数。

        采用惰性策略解决方案,不用每天更新全部流量包,用的时候再更新即可。这样使得只要用户有使用,流量包都是可以得到更新,没使用的用户流量包不会去更新,避免了海量数据下更新维护的问题,如果采用定时更新,几千万用户更新记录都是会有不少时间的延迟。
整体步骤如下:

        1)查询用户全部可用流量包
        2)遍历用户可用流量包,判断是否更新-用日期判断(要么都更新过,要么都没更新,根据gmt_modified)。没更新的流量包后加入【待更新集合】中,增加【今天剩余可用总次数】;已经更新的判断是否超过当天使用次数,如果没超过则增加【今天剩余可用总次数】,超过则忽略;
        3)更新用户今日流量包相关数据;
        4)扣减使用的某个流量包使用次数;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class UseTrafficVO {

    /**
     * 天剩余可用总次数 = 总次数-已用
     */
  private   Integer dayTotalLeftTimes;

    /**
     * 当前使用流量包
     */
    private   TrafficDO currentTrafficDO ;

    /**
     * 没过期,且今天没更新的流量包
     */
    private  List<Long> unUpdatedTrafficIds = new ArrayList<>();

}



@Override
    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
    public JsonData useTraffic(UseTrafficRequest trafficRequest) {

        Long accountNo = trafficRequest.getAccountNo();

        //处理流量包,筛选未更新流量包、当前使用流量包
        UseTrafficVO useTrafficVO = processTrafficList(accountNo);

        log.info("今天可用总次数:{}, 当前使用的流量包:{}",useTrafficVO.getDayTotalLeftTimes(),useTrafficVO.getCurrentTrafficDO());
        //如果当前流量包为空,则没有可用流量包
        if(useTrafficVO.getCurrentTrafficDO() == null){
            return JsonData.buildResult(BizCodeEnum.TRAFFIC_REDUCE_FAIL);
        }

        log.info("待更新流量包列表:{}",useTrafficVO.getUnUpdatedTrafficIds());
        if(useTrafficVO.getUnUpdatedTrafficIds().size() >0) {
            //更新今日流量包
            trafficManager.batchUpdateUsedTimes(accountNo, useTrafficVO.getUnUpdatedTrafficIds());
        }

        //先更新,再增加此次流量包扣减
        int rows = trafficManager.addDayUsedTimes( accountNo,  useTrafficVO.getCurrentTrafficDO().getId(),1);
        if(rows !=1){
            throw new BizException(BizCodeEnum.TRAFFIC_REDUCE_FAIL);
        }

        return JsonData.buildSuccess();
    }



/**
     * 处理流量包,筛选未更新流量包、当前使用流量包
     * @param accountNo
     */
    private  UseTrafficVO processTrafficList(Long accountNo){


        //全部流量包
        List<TrafficDO> list = trafficManager.selectAvailableTraffics(accountNo);
        if (list == null || list.size() == 0) { throw new BizException(BizCodeEnum.TRAFFIC_EXCEPTION);}


        //天剩余可用总次数 = 总次数-已用
         Integer dayTotalLeftTimes = 0;

        //当前使用流量包
         TrafficDO currentTrafficDO = null;

        //没过期,且今天没更新的流量包
        List<Long> unUpdatedTrafficIds = new ArrayList<>();

        //今天日期
        String todayStr = TimeUtil.format(new Date(),"yyyy-MM-dd");

        for(TrafficDO trafficDO : list){
            //判断是否更新,用日期判断,不能用时间
            String trafficUpdateDate = TimeUtil.format(trafficDO.getGmtModified(),"yyyy-MM-dd");
            if(todayStr.equalsIgnoreCase(trafficUpdateDate)){
                //已经更新   剩余可用 = 天总次数-已用次数
                int dayLeftTimes = trafficDO.getDayLimit()-trafficDO.getDayUsed();
                dayTotalLeftTimes = dayTotalLeftTimes + dayLeftTimes;

                //选取 当次流量包
                if(dayLeftTimes>0 && currentTrafficDO == null){
                    currentTrafficDO = trafficDO;
                }
            }else {
                //未更新
                dayTotalLeftTimes = dayTotalLeftTimes + trafficDO.getDayLimit();
                //记录未更新流量包  剩余可用 = 天总次数
                unUpdatedTrafficIds.add(trafficDO.getId());

                //选取 当次流量包
                if(currentTrafficDO == null){
                    currentTrafficDO = trafficDO;
                }

            }

        }

        UseTrafficVO useTrafficVO =
                new UseTrafficVO(dayTotalLeftTimes,currentTrafficDO,unUpdatedTrafficIds);

        return useTrafficVO;

    }

  • 49
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值