前言

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. 2023强网拟态crypto-一眼看出

    1.题目信息 一眼看穿 查看代码  from Crypto.Util.number import * from secret import flag import gmpy2 flag=b'' r = ...

  2. [转帖]k8s(1.28.2)部署ingress-nginx-controller(1.9.0)

    1.部署ingress-nginx-controller 继在三台虚拟机部署k8s后,需要部署ingress-nginx-controller,才能使设置的ingress规则生效. 1.1下载yaml ...

  3. Oracle Preinstall 调优参数的学习

    Oracle Preinstall 调优参数的学习 背景 学习是一个痛苦并快乐的过程. 之前自己手工安装过很多套Oracle数据库,也总结过很多 但是很多都是比较皮毛的. 最近遇到了一些问题. 才发现 ...

  4. [转帖]Sysbench - 一种系统性能benchmark

    SysBench是一款开源的.跨平台的.模块化的.多线程的性能测试工具,通过高负载地运行在数据库上,可以执行CPU/内存/线程/IO/数据库等方面的性能测试.用于评估操作系统的性能参数. 1 sysb ...

  5. [转帖]记一次vcsa6修复过程

    一. 某天发现一台vmware  vCenter Server Appliance services 6偶尔能登陆了,但极不稳定,连shell都偶尔能进...... 然后利用各种手段想方设法进到she ...

  6. [转帖]Debian9换源(阿里源)(Linux子系统)

    http://www.taodudu.cc/news/show-5410026.html?action=onClick 默认你已经装好Linux子系统. Step 0: 换源核心就是把/etc/apt ...

  7. Python学习之七_input和print

    Python学习之七_input和print 摘要 python3 之后 函数必须带 () 了 因为我开始学习的比较晚, 所以准备Python3开始学起 前面主要是模仿别人的代码进行学习 后续慢慢学习 ...

  8. Docker 运行 MongoDB的简单办法

    Docker 运行 MongoDB的简单办法 第一步拉取镜像 docker pull mongo 第二步创建自己的目录 地址 10.24.22.240 创建目录 mkdir /mongodb 第三步 ...

  9. git有关commit的命令

    2.更改最近一次(本次) commit 的提交信息: 当我们执行 git add . git commit -m "0-0-1" 之后我们发现自己写的提交信息是不符合项目要求的,这 ...

  10. echarts设置标题样式

    <!DOCTYPE html> <html> <!-- https://blog.csdn.net/weixin_42698255/article/details/892 ...