java 从零开始手写 redis(五)过期策略的另一种实现思路
前言
java从零手写实现redis(一)如何实现固定大小的缓存?
java从零手写实现redis(三)redis expire 过期原理
java从零手写实现redis(三)内存数据如何重启不丢失?
前面实现了 redis 的几个基本特性,其中在 expire 过期原理时,提到了另外一种实现方式。
这里将其记录下来,可以拓展一下自己的思路。
以前的实现方式
核心思路
原来的实现方式见:
https://mp.weixin.qq.com/s/BWfBc98oLqhAPLN2Hgkwow
不足
以前的设计非常简单,符合最基本的思路,就是将过期的信息放在一个 map 中,然后去遍历清空。
为了避免单次操作时间过长,类似 redis,单次操作 100 个元素之后,直接返回。
不过定时任务之心时,其实存在两个不足:
(1)keys 的选择不够随机,可能会导致每次循环 100 个结束时,真正需要过期的没有被遍历到。
不过 map 的随机比较蠢,就是将 map 的 keys 全部转为集合,然后通过 random 返回。
转的过程就是一个时间复杂度为 O(n) 的遍历,所以一开始没有去实现。
还有一种方式,就是用空间换区时间,存储的时候,同时存储在 list 中,然后随机返回处理,这个后续优化。
(2)keys 的遍历可能大部分都是无效的。
我们每次都是根据 keys 从前往后遍历,但是没有关心对应的过期时间,所以导致很多无效遍历。
本文主要提供一种以过期时间为维度的实现方式,仅供参考,因为这种方式也存在缺陷。

基于时间的遍历
思路
我们每次 put 放入过期元素时,根据过期时间对元素进行排序,相同的过期时间的 Keys 放在一起。
优点:定时遍历的时候,如果时间不到当前时间,就可以直接返回了,大大降低无效遍历。
缺点:考虑到惰性删除问题,还是需要存储以删除信息作为 key 的 map 关系,这样内存基本翻倍。
基本属性定义
我们这里使用 TreeMap 帮助我们进行过期时间的排序,这个集合后续有时间可以详细讲解了,我大概看了下 jdk1.8 的源码,主要是通过红黑树实现的。
public class CacheExpireSort<K,V> implements ICacheExpire<K,V> {
/**
* 单次清空的数量限制
* @since 0.0.3
*/
private static final int LIMIT = 100;
/**
* 排序缓存存储
*
* 使用按照时间排序的缓存处理。
* @since 0.0.3
*/
private final Map<Long, List<K>> sortMap = new TreeMap<>(new Comparator<Long>() {
@Override
public int compare(Long o1, Long o2) {
return (int) (o1-o2);
}
});
/**
* 过期 map
*
* 空间换时间
* @since 0.0.3
*/
private final Map<K, Long> expireMap = new HashMap<>();
/**
* 缓存实现
* @since 0.0.3
*/
private final ICache<K,V> cache;
}
放入元素时
每次存入新元素时,同时放入 sortMap 和 expireMap。
@Override
public void expire(K key, long expireAt) {
List<K> keys = sortMap.get(expireAt);
if(keys == null) {
keys = new ArrayList<>();
}
keys.add(key);
// 设置对应的信息
sortMap.put(expireAt, keys);
expireMap.put(key, expireAt);
}
定时任务的执行
定义
我们定义一个定时任务,100ms 执行一次。
/**
* 线程执行类
* @since 0.0.3
*/
private static final ScheduledExecutorService EXECUTOR_SERVICE = Executors.newSingleThreadScheduledExecutor();
public CacheExpireSort(ICache<K, V> cache) {
this.cache = cache;
this.init();
}
/**
* 初始化任务
* @since 0.0.3
*/
private void init() {
EXECUTOR_SERVICE.scheduleAtFixedRate(new ExpireThread(), 100, 100, TimeUnit.MILLISECONDS);
}
执行任务
实现源码如下:
/**
* 定时执行任务
* @since 0.0.3
*/
private class ExpireThread implements Runnable {
@Override
public void run() {
//1.判断是否为空
if(MapUtil.isEmpty(sortMap)) {
return;
}
//2. 获取 key 进行处理
int count = 0;
for(Map.Entry<Long, List<K>> entry : sortMap.entrySet()) {
final Long expireAt = entry.getKey();
List<K> expireKeys = entry.getValue();
// 判断队列是否为空
if(CollectionUtil.isEmpty(expireKeys)) {
sortMap.remove(expireAt);
continue;
}
if(count >= LIMIT) {
return;
}
// 删除的逻辑处理
long currentTime = System.currentTimeMillis();
if(currentTime >= expireAt) {
Iterator<K> iterator = expireKeys.iterator();
while (iterator.hasNext()) {
K key = iterator.next();
// 先移除本身
iterator.remove();
expireMap.remove(key);
// 再移除缓存,后续可以通过惰性删除做补偿
cache.remove(key);
count++;
}
} else {
// 直接跳过,没有过期的信息
return;
}
}
}
}
这里直接遍历 sortMap,对应的 key 就是过期时间,然后和当前时间对比即可。
删除的时候,需要删除 expireMap/sortMap/cache。
惰性删除刷新
惰性删除刷新时,就会用到 expireMap。
因为有时候刷新的 key 就一个,如果没有 expireMap 映射关系,可能要把 sortMap 全部遍历一遍才能找到对应的过期时间。
就是一个时间复杂度与空间复杂度衡量的问题。
@Override
public void refreshExpire(Collection<K> keyList) {
if(CollectionUtil.isEmpty(keyList)) {
return;
}
// 这样维护两套的代价太大,后续优化,暂时不用。
// 判断大小,小的作为外循环
final int expireSize = expireMap.size();
if(expireSize <= keyList.size()) {
// 一般过期的数量都是较少的
for(Map.Entry<K,Long> entry : expireMap.entrySet()) {
K key = entry.getKey();
// 这里直接执行过期处理,不再判断是否存在于集合中。
// 因为基于集合的判断,时间复杂度为 O(n)
this.removeExpireKey(key);
}
} else {
for(K key : keyList) {
this.removeExpireKey(key);
}
}
}
/**
* 移除过期信息
* @param key key
* @since 0.0.10
*/
private void removeExpireKey(final K key) {
Long expireTime = expireMap.get(key);
if(expireTime != null) {
final long currentTime = System.currentTimeMillis();
if(currentTime >= expireTime) {
expireMap.remove(key);
List<K> expireKeys = sortMap.get(expireTime);
expireKeys.remove(key);
sortMap.put(expireTime, expireKeys);
}
}
}
小结
实现过期的方法有很多种,目前我们提供的两种方案,都各有优缺点,我相信会有更加优秀的方式。
程序 = 数据结构 + 算法
redis 之所以性能这么优异,其实和其中的数据结构与算法用的合理是分不开的,优秀的框架值得反复学习和思考。
文中主要讲述了思路,实现部分因为篇幅限制,没有全部贴出来。
觉得本文对你有帮助的话,欢迎点赞评论收藏关注一波~
你的鼓励,是我最大的动力~

java 从零开始手写 redis(五)过期策略的另一种实现思路的更多相关文章
- java 从零开始手写 RPC (03) 如何实现客户端调用服务端?
说明 java 从零开始手写 RPC (01) 基于 socket 实现 java 从零开始手写 RPC (02)-netty4 实现客户端和服务端 写完了客户端和服务端,那么如何实现客户端和服务端的 ...
- java 从零开始手写 RPC (04) -序列化
序列化 java 从零开始手写 RPC (01) 基于 socket 实现 java 从零开始手写 RPC (02)-netty4 实现客户端和服务端 java 从零开始手写 RPC (03) 如何实 ...
- java 从零开始手写 RPC (05) reflect 反射实现通用调用之服务端
通用调用 java 从零开始手写 RPC (01) 基于 socket 实现 java 从零开始手写 RPC (02)-netty4 实现客户端和服务端 java 从零开始手写 RPC (03) 如何 ...
- java 从零开始手写 RPC (07)-timeout 超时处理
<过时不候> 最漫长的莫过于等待 我们不可能永远等一个人 就像请求 永远等待响应 超时处理 java 从零开始手写 RPC (01) 基于 socket 实现 java 从零开始手写 RP ...
- java 从零开始手写 RPC (01) 基于 websocket 实现
RPC 解决的问题 RPC 主要是为了解决的两个问题: 解决分布式系统中,服务之间的调用问题. 远程调用时,要能够像本地调用一样方便,让调用者感知不到远程调用的逻辑. 这一节我们来学习下如何基于 we ...
- 搞定redis面试--Redis的过期策略?手写一个LRU?
1 面试题 Redis的过期策略都有哪些?内存淘汰机制都有哪些?手写一下LRU代码实现? 2 考点分析 1)我往redis里写的数据怎么没了? 我们生产环境的redis怎么经常会丢掉一些数据?写进去了 ...
- 4.redis 的过期策略都有哪些?内存淘汰机制都有哪些?手写一下 LRU 代码实现?
作者:中华石杉 面试题 redis 的过期策略都有哪些?内存淘汰机制都有哪些?手写一下 LRU 代码实现? 面试官心理分析 如果你连这个问题都不知道,上来就懵了,回答不出来,那线上你写代码的时候,想当 ...
- redis的过期策略都有哪些?内存淘汰机制都有哪些?手写一下LRU代码实现?
redis的过期策略都有哪些? 设置过期时间: set key 的时候,使用expire time,就是过期时间.指定这个key比如说只能存活一个小时?10分钟?指定缓存到期就会失效. redis的过 ...
- redis 的过期策略都有哪些?内存淘汰机制都有哪些?
面试题 redis 的过期策略都有哪些?内存淘汰机制都有哪些?手写一下 LRU 代码实现? 面试官心理分析 如果你连这个问题都不知道,上来就懵了,回答不出来,那线上你写代码的时候,想当然的认为写进 r ...
- 面试官:讲讲redis的过期策略如何实现?
时隔多日,小菜鸡终于接到阿里的面试通知,屁颠屁颠的从上海赶到了杭州. 经过半个小时的厮杀: 自我介绍 hashMap和ConcurrentHashMap区别 jdk中锁的实现原理 volatile的使 ...
随机推荐
- Ubuntu Linux下的PDF阅读器推荐——Okular
安装方法 在Ubuntu下直接使用sudo apt-get install okular即可,如果中间遇到依赖项的问题,可以通过运行sudo apt --fix-broken install来自动修复 ...
- Icoding 链表 删除范围内结点
1.题目: 已知线性表中的元素(整数)以值递增有序排列,并以单链表作存储结构.试写一高效算法,删除表中所有大于mink且小于maxk的元素(若表中存在这样的元素),分析你的算法的时间复杂度. 链表结点 ...
- C:\Keil_v5\ARM\ARMCC\include\stdint.h contains an incorrect path.
1.问题 在使用Keil uvison5打开例程代码进行学习时,发现部分.h文件无法读取 2.解决方法 1.找到如图的设置按钮(小锤子) 2.根据自己所用的是C/C++还是ARM选择(我这里是C/C+ ...
- [转帖]查看oracle中表的索引
oracle中表的索引信息存在 user_indexes 和 user_ind_columns 两张表里面,其中 user_indexes 系统视图存放是索引的名称以及该索引是否是唯一索引等信息, u ...
- [转帖]一次ORA-3136的处理
https://oracleblog.org/working-case/deal-with-ora3136/ 最近收到一个告警,用户说数据库无法连接,但是从监控上看,oracle的后台进程已经侦听进程 ...
- idb单副本时-TiKV节点损坏后有损数据恢复的方法
Tidb单副本时-TiKV节点损坏后有损数据恢复的方法 背景 UAT环境下,为了减少存储. 搭建了一套单副本的TiDB集群 但是随着数据量的增多, UAT上面的数据可以丢失,但是表结构等信息是无法接受 ...
- [转帖]TiKV & TiFlash 加速复杂业务查询
https://tidb.net/book/tidb-monthly/2022/2022-07/usercase/tikv-tiflash 背景 在互联网公司或传统公司的 CRM 系统中,最常用的功 ...
- Nginx双层域名时 iframe嵌入/跳转页面的处理过程
Nginx双层域名时 iframe嵌入/跳转页面的处理过程 背景 两年前在上一家公司内遇到一个Nginx的问题 当时的场景是 双层nginx代理时(一层域名侧, 一层拆分微服务的网关层) 程序里面会打 ...
- 【转帖】MySQL 8.0.32如期而至
MySQL 8.0版本计划 MySQL 8.0开始采用快速迭代开发模式,基本上是每隔3个月就发布一个新的小版本.去年1月18日(2022.1.18)发布MySQL 8.0.28,今年1月17日发布My ...
- [转帖]shell脚本之awk命令——按列求平均值、最大值、最小值
文章目录 写在前面 awk求平均值 awk求最大值 awk求最小值 awk求极值.均值的实际应用 写在前面 awk命令求极值和均值需要熟悉该命令的基本用法,如果你不熟悉该命令,请先阅读shell脚本之 ...