前言

java从零手写实现redis(一)如何实现固定大小的缓存?

java从零手写实现redis(三)redis expire 过期原理

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 之所以性能这么优异,其实和其中的数据结构与算法用的合理是分不开的,优秀的框架值得反复学习和思考。

文中主要讲述了思路,实现部分因为篇幅限制,没有全部贴出来。

开源地址:https://github.com/houbb/cache

觉得本文对你有帮助的话,欢迎点赞评论收藏关注一波~

你的鼓励,是我最大的动力~

java 从零开始手写 redis(五)过期策略的另一种实现思路的更多相关文章

  1. java 从零开始手写 RPC (03) 如何实现客户端调用服务端?

    说明 java 从零开始手写 RPC (01) 基于 socket 实现 java 从零开始手写 RPC (02)-netty4 实现客户端和服务端 写完了客户端和服务端,那么如何实现客户端和服务端的 ...

  2. java 从零开始手写 RPC (04) -序列化

    序列化 java 从零开始手写 RPC (01) 基于 socket 实现 java 从零开始手写 RPC (02)-netty4 实现客户端和服务端 java 从零开始手写 RPC (03) 如何实 ...

  3. java 从零开始手写 RPC (05) reflect 反射实现通用调用之服务端

    通用调用 java 从零开始手写 RPC (01) 基于 socket 实现 java 从零开始手写 RPC (02)-netty4 实现客户端和服务端 java 从零开始手写 RPC (03) 如何 ...

  4. java 从零开始手写 RPC (07)-timeout 超时处理

    <过时不候> 最漫长的莫过于等待 我们不可能永远等一个人 就像请求 永远等待响应 超时处理 java 从零开始手写 RPC (01) 基于 socket 实现 java 从零开始手写 RP ...

  5. java 从零开始手写 RPC (01) 基于 websocket 实现

    RPC 解决的问题 RPC 主要是为了解决的两个问题: 解决分布式系统中,服务之间的调用问题. 远程调用时,要能够像本地调用一样方便,让调用者感知不到远程调用的逻辑. 这一节我们来学习下如何基于 we ...

  6. 搞定redis面试--Redis的过期策略?手写一个LRU?

    1 面试题 Redis的过期策略都有哪些?内存淘汰机制都有哪些?手写一下LRU代码实现? 2 考点分析 1)我往redis里写的数据怎么没了? 我们生产环境的redis怎么经常会丢掉一些数据?写进去了 ...

  7. 4.redis 的过期策略都有哪些?内存淘汰机制都有哪些?手写一下 LRU 代码实现?

    作者:中华石杉 面试题 redis 的过期策略都有哪些?内存淘汰机制都有哪些?手写一下 LRU 代码实现? 面试官心理分析 如果你连这个问题都不知道,上来就懵了,回答不出来,那线上你写代码的时候,想当 ...

  8. redis的过期策略都有哪些?内存淘汰机制都有哪些?手写一下LRU代码实现?

    redis的过期策略都有哪些? 设置过期时间: set key 的时候,使用expire time,就是过期时间.指定这个key比如说只能存活一个小时?10分钟?指定缓存到期就会失效. redis的过 ...

  9. redis 的过期策略都有哪些?内存淘汰机制都有哪些?

    面试题 redis 的过期策略都有哪些?内存淘汰机制都有哪些?手写一下 LRU 代码实现? 面试官心理分析 如果你连这个问题都不知道,上来就懵了,回答不出来,那线上你写代码的时候,想当然的认为写进 r ...

  10. 面试官:讲讲redis的过期策略如何实现?

    时隔多日,小菜鸡终于接到阿里的面试通知,屁颠屁颠的从上海赶到了杭州. 经过半个小时的厮杀: 自我介绍 hashMap和ConcurrentHashMap区别 jdk中锁的实现原理 volatile的使 ...

随机推荐

  1. [转帖]shell编程:变量知识进阶(三)

    https://www.cnblogs.com/luoahong/articles/9154309.html 1 Shell特殊位置变量 范例1:$n的实践例子 1 2 3 4 5 6 7 8 9 1 ...

  2. 使用Grafana 监控 minio 的部分改进

    使用Grafana 监控 minio 的部分改进 部署minio开启监控metrics的脚本 mkdir -p /data/minio/data cat << EOF > /etc/ ...

  3. [转帖]002、体系结构之TiDB Server

    TiDB Server 1.TiDB总览 1.1.TiDB Server架构 1.2.TiDB Server 主要功能: 2.SQL语句处理 语句的解析和编译 SQL层 协议层 上下文 解析层 逻辑优 ...

  4. [转帖]如何在Linux系统中使用命令发送邮件

    https://zhuanlan.zhihu.com/p/96897532 Linux系统更多的被用来做服务器系统,在运维的过程中难免我们需要编写脚本监控一些指标并定期发送邮件. 本教程将介绍如何在L ...

  5. [转帖]PowerShell教程 - 日期时间管理(Date & Time Management)

    https://www.cnblogs.com/cqpanda/p/16589991.html 更新记录转载请注明出处.2022年8月25日 发布.2022年8月18日 从笔记迁移到博客. 日期时间管 ...

  6. Ubuntu18.04 设置ip地址

    1. 自己用vCenter安装了一个ubuntu18.04, 结果因为是 vCenter6.7 只有web界面, 发现GUI操作时鼠标位置不对,没办法只能通过cli的方式设置ip地址. 2. 先简单查 ...

  7. 【如何提高IT运维效率】深度解读京东云基于NLP的运维日志异常检测AIOps落地实践

    作者:京东科技  张宪波.张静.李东江 基于NLP技术对运维日志聚类,从日志角度快速发现线上业务问题 日志在IT行业中被广泛使用,日志的异常检测对于识别系统的运行状态至关重要.解决这一问题的传统方法需 ...

  8. iframe 在线预览pdf、word、excel、ppt、txt、图片、视频

    第一种方式通过 iframe 在线预览 pdf,word,excel,ppt,txt,图片,视频 <template> <el-button @click="openHan ...

  9. TypeScript枚举类型

    枚举 简单理解就是将所有的情况列举出来. 枚举不是用来定义类型的哈.就是说枚举不是一种数据类型. enum xxx={ key1=value1, key2=value2, } 通过 xxx.key1的 ...

  10. 利用pearcmd.php本地文件包含(LFI)

    本文主要是为了学习如何用pearcmd进行本地文件包含 0x00  环境准备 首先先在docker中安装一个php环境. docker exec -it [container id] /bin/bas ...