Retrofit 风格的 RxCache及其多种缓存替换算法

田园风光.jpg

RxCache 是一个支持 Java 和 Android 的 Local Cache 。

之前的文章《给 Java 和 Android 构建一个简单的响应式Local Cache》《RxCache 整合 Android 的持久层框架 greenDAO、Room》曾详细介绍过它。

目前,对框架增加一些 Annotation 以及 Cache 替换算法。

一. 基于 Annotation 完成缓存操作

类似 Retrofit 风格的方式,支持通过标注 Annotation 来完成缓存的操作。

例如先定义一个接口,用于定义缓存的各种操作。

public interface Provider {

    @CacheKey("user")
    @CacheMethod(methodType = MethodType.GET)
    <T> Record<T> getData(@CacheClass Class<T> clazz);


    @CacheKey("user")
    @CacheMethod(methodType = MethodType.SAVE)
    @CacheLifecycle(duration = 2000)
    void putData(@CacheValue User user);


    @CacheKey("user")
    @CacheMethod(methodType = MethodType.REMOVE)
    void removeUser();

    @CacheKey("test")
    @CacheMethod(methodType = MethodType.GET, observableType = ObservableType.MAYBE)
    <T> Maybe<Record<T>> getMaybe(@CacheClass Class<T> clazz);
}

通过 CacheProvider 创建该接口,然后可以完成各种缓存操作。

public class TestCacheProvider {

    public static void main(String[] args) {


        RxCache.config(new RxCache.Builder());

        RxCache rxCache = RxCache.getRxCache();

        CacheProvider cacheProvider = new CacheProvider.Builder().rxCache(rxCache).build();

        Provider provider = cacheProvider.create(Provider.class);

        User u = new User();
        u.name = "tony";
        u.password = "123456";

        provider.putData(u); // 将u存入缓存中

        Record<User> record = provider.getData(User.class); // 从缓存中获取key="user"的数据

        if (record!=null) {

            System.out.println(record.getData().name);
        }

        provider.removeUser(); // 从缓存中删除key="user"的数据

        record = provider.getData(User.class);

        if (record==null) {

            System.out.println("record is null");
        }

        User u2 = new User();
        u2.name = "tony2";
        u2.password = "000000";
        rxCache.save("test",u2);

        Maybe<Record<User>> maybe = provider.getMaybe(User.class); // 从缓存中获取key="test"的数据,返回的类型为Maybe
        maybe.subscribe(new Consumer<Record<User>>() {
            @Override
            public void accept(Record<User> userRecord) throws Exception {

                User user = userRecord.getData();
                if (user!=null) {

                    System.out.println(user.name);
                    System.out.println(user.password);
                }
            }
        });
    }
}

CacheProvider 核心是 create(),它通过动态代理来创建Provider。

    public <T> T create(Class<T> clazz) {

        CacheProxy cacheProxy = new CacheProxy(rxCache);

        try {
            return (T) Proxy.newProxyInstance(CacheProvider.class.getClassLoader(), new Class[]{clazz}, cacheProxy);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }

其中,CacheProxy 实现了 InvocationHandler 接口,是创建代理类的调用处理器。

package com.safframework.rxcache.proxy;

import com.safframework.rxcache.RxCache;
import com.safframework.rxcache.proxy.annotation.*;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

/**
 * Created by tony on 2018/10/30.
 */
public class CacheProxy implements InvocationHandler {

    RxCache rxCache;

    public CacheProxy(RxCache rxCache) {

        this.rxCache = rxCache;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        CacheMethod cacheMethod = method.getAnnotation(CacheMethod.class);
        CacheKey cacheKey = method.getAnnotation(CacheKey.class);
        CacheLifecycle cacheLifecycle = method.getAnnotation(CacheLifecycle.class);

        Annotation[][] allParamsAnnotations = method.getParameterAnnotations();

        Class cacheClazz = null;
        Object cacheValue = null;

        if (allParamsAnnotations != null) {
            for (int i = 0; i < allParamsAnnotations.length; i++) {
                Annotation[] paramAnnotations = allParamsAnnotations[i];
                if (paramAnnotations != null) {
                    for (Annotation annotation : paramAnnotations) {
                        if (annotation instanceof CacheClass) {
                            cacheClazz = (Class) args[i];
                        }

                        if (annotation instanceof CacheValue) {
                            cacheValue = args[i];
                        }
                    }
                }
            }
        }

        if (cacheMethod!=null) {

            MethodType methodType = cacheMethod.methodType();

            long duration = -1;

            if (cacheLifecycle != null) {
                duration = cacheLifecycle.duration();
            }

            if (methodType == MethodType.GET) {

                ObservableType observableType = cacheMethod.observableType();

                if (observableType==ObservableType.NOUSE) {

                    return  rxCache.get(cacheKey.value(),cacheClazz);
                } else if (observableType == ObservableType.OBSERVABLE){

                    return  rxCache.load2Observable(cacheKey.value(),cacheClazz);
                } else if (observableType==ObservableType.FLOWABLE) {

                    return  rxCache.load2Flowable(cacheKey.value(),cacheClazz);
                } else if (observableType==ObservableType.SINGLE) {

                    return  rxCache.load2Single(cacheKey.value(),cacheClazz);
                } else if (observableType==ObservableType.MAYBE) {

                    return  rxCache.load2Maybe(cacheKey.value(),cacheClazz);
                }

            } else if (methodType == MethodType.SAVE) {

                rxCache.save(cacheKey.value(),cacheValue,duration);

            } else if (methodType == MethodType.REMOVE) {

                rxCache.remove(cacheKey.value());
            }
        }

        return null;
    }
}

CacheProxy 的 invoke() 方法先获取 Method 所使用的 Annotation,包括CacheMethod、CacheKey、CacheLifecycle。

其中,CacheMethod 是最核心的 Annotation,它取决于 rxCache 使用哪个方法。CacheMethod 支持的方法类型包括:获取、保存、删除缓存。当 CacheMethod 的 methodType 是 GET 类型,则可能会返回 RxJava 的各种 Observable 类型,或者还是返回所存储的对象类型。

CacheKey 是任何方法都需要使用的 Annotation。CacheLifecycle 只有保存缓存时才会使用。

二. 支持多种缓存替换算法

RxCache 包含了两级缓存: Memory 和 Persistence 。

Memory 的默认实现 FIFOMemoryImpl、LRUMemoryImpl、LFUMemoryImpl 分别使用 FIFO、LRU、LFU 算法来缓存数据。

2.1 FIFO

通过使用 LinkedList 存放缓存的 keys,ConcurrentHashMap 存放缓存的数据,就可以实现 FIFO。

2.2 LRU

LRU是Least Recently Used的缩写,即最近最少使用,常用于页面置换算法,是为虚拟页式存储管理服务的。

使用 ConcurrentHashMap 和 ConcurrentLinkedQueue 实现该算法。如果某个数据已经存放在缓存中,则从 queue 中删除并添加到 queue 的第一个位置。如果缓存已满,则从 queue 中删除最后面的数据。并把新的数据添加到缓存。

public class LRUCache<K,V> {

    private Map<K,V> cache = null;
    private AbstractQueue<K> queue = null;
    private int size = 0;

    public LRUCache() {

        this(Constant.DEFAULT_CACHE_SIZE);
    }

    public LRUCache(int size) {

        this.size = size;
        cache = new ConcurrentHashMap<K,V>(size);
        queue = new ConcurrentLinkedQueue<K>();
    }

    public boolean containsKey(K key) {

        return cache.containsKey(key);
    }

    public V get(K key) {

        //Recently accessed, hence move it to the tail
        queue.remove(key);
        queue.add(key);
        return cache.get(key);
    }

    public V getSilent(K key) {

        return cache.get(key);
    }

    public void put(K key, V value) {

        //ConcurrentHashMap doesn't allow null key or values
        if(key == null || value == null) throw new RxCacheException("key is null or value is null");

        if(cache.containsKey(key)) {
            queue.remove(key);
        }

        if(queue.size() >= size) {
            K lruKey = queue.poll();
            if(lruKey != null) {
                cache.remove(lruKey);
            }
        }

        queue.add(key);
        cache.put(key,value);
    }

    /**
     * 获取最近最少使用的值
     * @return
     */
    public V getLeastRecentlyUsed() {

        K remove = queue.remove();
        queue.add(remove);
        return cache.get(remove);
    }

    public void remove(K key) {

        cache.remove(key);
        queue.remove(key);
    }

    public void clear() {

        cache.clear();
        queue.clear();
    }
    ......
}

2.3 LFU

LFU是Least Frequently Used的缩写,即最近最不常用使用。

看上去跟 LRU 类似,其实它们并不相同。LRU 是淘汰最长时间未被使用的数据,而 LFU 是淘汰一定时期内被访问次数最少的数据。

LFU 会记录数据在一定时间内的使用次数。稍显复杂感兴趣的可以阅读 RxCache 中相关的源码。

三. 总结

RxCache 大体已经完成,初步可以使用。

RxCache github 地址:https://github.com/fengzhizi715/RxCache
Android 版本的 RxCache github 地址:https://github.com/fengzhizi715/RxCache4a

对于 Android ,除了支持常见的持久层框架之外,还支持 RxCache 转换成 LiveData。如果想要跟 Retrofit 结合,可以通过 RxCache 的 transform 策略。

对于Java 后端,RxCache 只是一个本地缓存,不适合存放大型的数据。但是其内置的 Memory 层包含了多种缓存替换算法,不用内置的 Memory 还可以使用 Guava Cache、Caffeine 。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,864评论 6 494
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,175评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,401评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,170评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,276评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,364评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,401评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,179评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,604评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,902评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,070评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,751评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,380评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,077评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,312评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,924评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,957评论 2 351

推荐阅读更多精彩内容

  • 1我们都听过 cache,当你问他们是什么是缓存的时候,他们会给你一个完美的答案,可是他们不知道缓存是怎么构建的,...
    织田信长阅读 1,504评论 1 20
  • 缓存是最直接有效提升系统性能的手段之一。个人认为用好用对缓存是优秀程序员的必备基本素质。 本文结合实际开发经验,从...
    Java小生阅读 805评论 1 3
  • 理论总结 它要解决什么样的问题? 数据的访问、存取、计算太慢、太不稳定、太消耗资源,同时,这样的操作存在重复性。因...
    jiangmo阅读 2,844评论 0 11
  • 前言 主题是Mybatis一级和二级缓存的应用及源码分析。希望在本场chat结束后,能够帮助读者朋友明白以下三点。...
    余平的余_余平的平阅读 1,325评论 0 12
  • 当夏日的傍晚温暖的风徐徐吹来,夹杂着泥土与花的芬芳,我在回想一天的时光。当我抬头看法国梧桐时,轻轻的瞥一眼天空,灰...
    簟纹灯影阅读 294评论 0 0