Skip to main content
  1. All Posts/

cache

Tools Shell

项目简介

Cache 用于实现一个可拓展的本地缓存。
有人的地方,就有江湖。
有高性能的地方,就有 cache。

创作目的

  • 为日常开发提供一套简单易用的缓存框架
  • 便于后期多级缓存开发
  • 学以致用,开发一个类似于 redis 的渐进式缓存框架

特性

  • MVP 开发策略
  • fluent 流式编程体验,纵享丝滑
  • 支持 cache 固定大小
  • 支持自定义 map 实现策略
  • 支持 expire 过期特性
  • 支持自定义 evict 驱除策略

内置 FIFO 和 LRU 驱除策略

  • 支持自定义删除监听器
  • 日志整合框架,自适应常见日志
  • 支持 load 初始化和 persist 持久化

RDB 和 AOF 两种模式

变更日志

变更日志

快速开始

准备

JDK1.7 及其以上版本
Maven 3.X 及其以上版本

maven 项目依赖

<dependency>
    <groupId>com.github.houbb</groupId>
    <artifactId>cache-core</artifactId>
    <version>0.0.15</version>
</dependency>

入门测试

ICache<String, String> cache = CacheBs.<String,String>newInstance()
                .size(2)
                .build();

cache.put("1", "1");
cache.put("2", "2");
cache.put("3", "3");
cache.put("4", "4");

Assert.assertEquals(2, cache.size());

默认为先进先出的策略,此时输出 keys,内容如下:

[3, 4]

引导类配置属性

CacheBs 作为缓存的引导类,支持 fluent 写法,编程更加优雅便捷。
上述配置等价于:

ICache<String, String> cache = CacheBs.<String,String>newInstance()
                .map(Maps.<String,String>hashMap())
                .evict(CacheEvicts.<String, String>fifo())
                .size(2)
                .build();

淘汰策略

目前内置了几种淘汰策略,可以直接通过 CacheEvicts 工具类创建。

策略
说明

none
没有任何淘汰策略

fifo
先进先出(默认策略)

lru
最基本的朴素 LRU 策略,性能一般

lruDoubleListMap
基于双向链表+MAP 实现的朴素 LRU,性能优于 lru

lruLinkedHashMap
基于 LinkedHashMap 实现的朴素 LRU,与 lruDoubleListMap 差不多

lru2Q
基于 LRU 2Q 的改进版 LRU 实现,命中率优于朴素LRU

lru2
基于 LRU-2 的改进版 LRU 实现,命中率优于 lru2Q

过期支持

ICache<String, String> cache = CacheBs.<String,String>newInstance()
        .size(3)
        .build();

cache.put("1", "1");
cache.put("2", "2");

cache.expire("1", 10);
Assert.assertEquals(2, cache.size());

TimeUnit.MILLISECONDS.sleep(50);
Assert.assertEquals(1, cache.size());
System.out.println(cache.keySet());

cache.expire("1", 10); 指定对应的 key 在 10ms 后过期。

删除监听器

说明

淘汰和过期,这些都是缓存的内部行为。
如果用户也关心的话,可以自定义删除监听器。

自定义监听器

直接实现 ICacheRemoveListener 接口即可。

public class MyRemoveListener<K,V> implements ICacheRemoveListener<K,V> {

    @Override
    public void listen(ICacheRemoveListenerContext<K, V> context) {
        System.out.println("【删除提示】可恶,我竟然被删除了!" + context.key());
    }

}

使用

ICache<String, String> cache = CacheBs.<String,String>newInstance()
        .size(1)
        .addRemoveListener(new MyRemoveListener<String, String>())
        .build();

cache.put("1", "1");
cache.put("2", "2");
  • 测试日志
【删除提示】可恶,我竟然被删除了!2

添加慢操作监听器

说明

redis 中会存储慢操作的相关日志信息,主要是由两个参数构成:
(1)slowlog-log-slower-than 预设阈值,它的单位是毫秒(1秒=1000000微秒)默认值是10000
(2)slowlog-max-len 最多存储多少条的慢日志记录
不过 redis 是直接存储到内存中,而且有长度限制。
根据实际工作体验,如果我们可以添加慢日志的监听,然后有对应的存储或者报警,这样更加方便问题的分析和快速反馈。
所以我们引入类似于删除的监听器。

自定义监听器

实现接口 ICacheSlowListener
这里每一个监听器都可以指定自己的慢日志阈值,便于分级处理。

public class MySlowListener implements ICacheSlowListener {

    @Override
    public void listen(ICacheSlowListenerContext context) {
        System.out.println("【慢日志】name: " + context.methodName());
    }

    @Override
    public long slowerThanMills() {
        return 0;
    }

}

使用

ICache<String, String> cache = CacheBs.<String,String>newInstance()
        .addSlowListener(new MySlowListener())
        .build();

cache.put("1", "2");
cache.get("1");
  • 测试效果
[DEBUG] [2020-09-30 17:40:11.547] [main] [c.g.h.c.c.s.i.c.CacheInterceptorCost.before] - Cost start, method: put
[DEBUG] [2020-09-30 17:40:11.551] [main] [c.g.h.c.c.s.i.c.CacheInterceptorCost.after] - Cost end, method: put, cost: 10ms
【慢日志】name: put
[DEBUG] [2020-09-30 17:40:11.554] [main] [c.g.h.c.c.s.i.c.CacheInterceptorCost.before] - Cost start, method: get
[DEBUG] [2020-09-30 17:40:11.554] [main] [c.g.h.c.c.s.i.c.CacheInterceptorCost.after] - Cost end, method: get, cost: 1ms
【慢日志】name: get

实际工作中,我们可以针对慢日志数据存储,便于后期分析。
也可以直接接入报警系统,及时反馈问题。

添加 load 加载器

说明

有时候我们需要在 cache 初始化的时候,添加对应的数据初始化。
后期可以从文件等地方加载数据。

实现

实现 ICacheLoad 接口即可。

public class MyCacheLoad implements ICacheLoad<String,String> {

    @Override
    public void load(ICache<String, String> cache) {
        cache.put("1", "1");
        cache.put("2", "2");
    }

}

我们在缓存初始化的时候,放入 2 个元素。

测试效果

ICache<String, String> cache = CacheBs.<String,String>newInstance()
        .load(new MyCacheLoad())
        .build();

Assert.assertEquals(2, cache.size());

添加 persist 持久化类

说明

如果我们只是把文件放在内存中,应用重启信息就丢失了。
有时候我们希望这些 key/value 信息可以持久化,存储到文件或者 database 中。

持久化

CachePersists.<String, String>dbJson("1.rdb") 指定将数据文件持久化到文件中。
定期执行,暂时全量持久化的间隔为 10min,后期考虑支持更多配置。

public void persistTest() throws InterruptedException {
    ICache<String, String> cache = CacheBs.<String,String>newInstance()
            .load(new MyCacheLoad())
            .persist(CachePersists.<String, String>dbJson("1.rdb"))
            .build();

    Assert.assertEquals(2, cache.size());
    TimeUnit.SECONDS.sleep(5);
}
  • 1.rdb

文件内容如下:

{"key":"2","value":"2"}
{"key":"1","value":"1"}

加载器

存储之后,可以使用对应的加载器读取文件内容:

ICache<String, String> cache = CacheBs.<String,String>newInstance()
        .load(CacheLoads.<String, String>dbJson("1.rdb"))
        .build();

Assert.assertEquals(2, cache.size());

后期 Road-MAP

淘汰策略

  • CLOCK 算法
  • SC 二次机会
  • 老化算法
  • 弱引用

过期特性

  • 过期策略添加随机返回
  • expireAfterWrite()
  • expireAfterAccess()

持久化

  • AOF 混合 RDB

统计

  • 命中率
  • keys 数量
  • evict 数量
  • expire 数量
  • 耗时统计

并发

  • 并发安全保障

其他

  • 异步 callable 操作
  • spring 整合

提供 @Cacheable 系列注解

  • 文件压缩
  • 独立服务端

提供类似于 redis-server + redis-client 的拆分,便于独立于应用作为服务存在。

拓展阅读

java从零手写实现redis(一)如何实现固定大小的缓存?
java从零手写实现redis(三)redis expire 过期原理
java从零手写实现redis(三)内存数据如何重启不丢失?
java从零手写实现redis(四)添加监听器
java从零手写实现redis(五)过期策略的另一种实现思路
java从零手写实现redis(六)AOF 持久化原理详解及实现
java从零开始手写redis(七)LRU…