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