引言
在技术领域,许多中间件之所以获得巨大成功,部分原因在于它们所采用的思想之先进。这些思想解决了一个个世纪难题,接下来我将讲述一个我学习到的思想,并将其应用至工作中的案例。
惰性策略在日常编码中随处可见,但究竟什么是惰性策略呢?简而言之,惰性策略是一种优化方法,即在不需要进行计算或操作时,不会真正进行相关的处理,而是仅仅记录相关信息或轨迹。只有在需要执行行动操作时,才会触发从头到尾的真正计算。这种机制大大减少了不必要的资源消耗,提高了程序的效率。惰性策略的使用有很多,其中比较常见的便是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;}