JFinal使用技巧-Caffeine接近最佳的缓存库

软件简介

Caffeine 是基于Java 8的高性能,接近最佳的缓存库。

本来也没用过,JF内置的EhCache对于我们业务是绰绰有余
前几天有社友反馈说demo,就查了一下资料,网友说是这玩意性能还非常高,比EhCache还高不少。
image.png
今天休息,想着敲点代码放松一下,就来模仿模仿。废话不多说,直接 上 石马 ~


导包加依赖 pom.xml

<!-- caffeine 缓存 -->
<dependency>
   <groupId>com.github.ben-manes.caffeine</groupId>
   <artifactId>caffeine</artifactId>
   <version>3.0.5</version>
</dependency>

3.0及以上版本,Java jdk最低版本是11,如果项目使用低版本jdk,请尝试降低caffeine版本, 如jdk8用2.9.3。


先来个CaffeineKit 模仿复制JF的CacheKit代码

package com.jfinal.plugin.caffeine;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.jfinal.log.Log;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Function;

/**
 * Caffeine 缓存
 * @author 杜福忠
 */
public class CaffeineKit {
    private static final Log log = Log.getLog(CaffeineKit.class);
    private static final ConcurrentHashMap<String, Cache> CACHES = new ConcurrentHashMap();

    public static Cache putCache(String cacheName, Cache cache){
        return CACHES.put(cacheName, cache);
    }

    public static Cache getCache(String cacheName){
        return CACHES.get(cacheName);
    }

    public static Cache removeCache(String cacheName){
        return CACHES.remove(cacheName);
    }

    static Cache getOrAddCache(String cacheName) {
        Cache cache = CACHES.get(cacheName);
        if (cache == null) {
            synchronized(CaffeineKit.class) {
                cache = CACHES.get(cacheName);
                if (cache == null) {
                    log.warn("Could not find cache config [" + cacheName + "], using default.");
                    cache = Caffeine.newBuilder().build();
                    CACHES.put(cacheName, cache);
                    log.debug("Cache [" + cacheName + "] started.");
                }
            }
        }
        return cache;
    }

    public static void put(String cacheName, Object key, Object value) {
        if(value != null){
            getOrAddCache(cacheName).put(key, value);
        } else {
            remove(cacheName, key);
        }
    }

    @SuppressWarnings("unchecked")
    public static <T> T get(String cacheName, Object key) {
        return (T) getOrAddCache(cacheName).getIfPresent(key);
    }

    @SuppressWarnings("rawtypes")
    public static List getKeys(String cacheName) {
        return new ArrayList(asMap(cacheName).keySet());
    }

    public static ConcurrentMap asMap(String cacheName) {
        return getOrAddCache(cacheName).asMap();
    }

    public static void remove(String cacheName, Object key) {
        getOrAddCache(cacheName).invalidate(key);
    }

    public static void removeAll(String cacheName) {
        getOrAddCache(cacheName).invalidateAll();
    }

    @SuppressWarnings("unchecked")
    public static <T> T get(String cacheName, Object key, Function mappingFunction) {
        return (T)getOrAddCache(cacheName).get(key, mappingFunction);
    }

    @SuppressWarnings("unchecked")
    public static <T> T get(String cacheName, Object key, Class<? extends Function> dataLoaderClass) {
        try {
            Function dataLoader = dataLoaderClass.getDeclaredConstructor().newInstance();
            return (T)getOrAddCache(cacheName).get(key, dataLoader);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

}


OK,测试测试

package com.jfinal.plugin.caffeine;

import com.jfinal.kit.Ret;

import java.util.List;

public class TestCaffeine {
    static String cacheName = "man";
    static String key = "a";
    static Object v;

    public static void main(String[] args) {
        put(key);

        remove(key);

        put(key);
        put("b");
        put("c");

        getKeys();

        removeAll();

        getKeys();
    }

    private static void getKeys() {
        System.out.println("getKeys------");
        List list = CaffeineKit.getKeys(cacheName);
        System.out.println(list);
    }

    private static void removeAll() {
        System.out.println("removeAll------");
        CaffeineKit.removeAll(cacheName);
        v = CaffeineKit.get(cacheName, key);
        System.out.println(v);
    }

    private static void remove(String key) {
        System.out.println("remove------" + key);
        CaffeineKit.remove(cacheName, key);
        v = CaffeineKit.get(cacheName, key);
        System.out.println(v);
    }

    private static void put(String key) {
        System.out.println("put------" + key);
        CaffeineKit.put(cacheName, key, Ret.ok("老铁没毛病!"));
        v = CaffeineKit.get(cacheName, key);
        System.out.println(v);
    }
}

测试结果:

put------a

2021-12-05 23:06:57
[WARN]-[Thread: main]-[com.jfinal.plugin.caffeine.CaffeineKit.getOrAddCache()]: Could not find cache config [man], using default.

2021-12-05 23:06:57
[DEBUG]-[Thread: main]-[com.jfinal.plugin.caffeine.CaffeineKit.getOrAddCache()]: Cache [man] started.
{msg=老铁没毛病!, state=ok}
remove------a
null
put------a
{msg=老铁没毛病!, state=ok}
put------b
{msg=老铁没毛病!, state=ok}
put------c
{msg=老铁没毛病!, state=ok}
getKeys------
[a, b, c]
removeAll------
null
getKeys------
[]

进程已结束,退出代码为 0

上面用的是默认配置,也可以在JF启动的时候,手动设置:

CaffeineKit.putCache(cacheName, Caffeine.newBuilder().maximumSize(10_000).build());

那用很简单,网上例子一大把,咋和JF配合使用了?得和内置的EhCache一样方便才行啊。
那继续复制JF里面的源码,改一下:

先说Db.findByCache和Model里面的ByCache等系列方法咋设置吧。
建一个CaffeineCache即可

package com.jfinal.plugin.caffeine;

import com.jfinal.plugin.activerecord.cache.ICache;

/**
 * Caffeine 缓存
 *
 * ActiveRecordPlugin arp = new ActiveRecordPlugin(...);
 * arp.setCache(new CaffeineCache());
 *
 * @author 杜福忠
 */
public class CaffeineCache implements ICache {

    @Override
    public <T> T get(String cacheName, Object key) {
        return CaffeineKit.get(cacheName, key);
    }

    @Override
    public void put(String cacheName, Object key, Object value) {
        CaffeineKit.put(cacheName, key, value);
    }

    @Override
    public void remove(String cacheName, Object key) {
        CaffeineKit.remove(cacheName, key);
    }

    @Override
    public void removeAll(String cacheName) {
        CaffeineKit.removeAll(cacheName);
    }
}

用法注释中写了,ActiveRecordPlugin支持配置的。

 ActiveRecordPlugin arp = new ActiveRecordPlugin(...);
 arp.setCache(new CaffeineCache());

测试:

/**
 * 获取缓存数据
 */
public Ret findByCache() {
   List<Record> list = Db.findByCache("a", "findByCache", "SELECT 1");
   return Ret.ok(list.toString());
}

断点第一次进去了image.png

第二次就走缓存了

image.png

好没问题。

再说下一个,CacheInterceptor 和 EvictInterceptor 代码复制过来吧23333CV大F
用法文档中有说明:
https://jfinal.com/doc/7-4

package com.jfinal.plugin.caffeine;

import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import javax.servlet.http.HttpServletRequest;
import com.jfinal.aop.Interceptor;
import com.jfinal.aop.Invocation;
import com.jfinal.core.Controller;
import com.jfinal.plugin.ehcache.CacheName;
import com.jfinal.plugin.ehcache.RenderInfo;
import com.jfinal.render.Render;

/**
 * CacheInterceptor.
 */
public class CacheCaffeineInterceptor implements Interceptor {

    private static final String renderKey = "_renderKey";
    private static ConcurrentHashMap<String, ReentrantLock> lockMap = new ConcurrentHashMap<String, ReentrantLock>(512);

    private ReentrantLock getLock(String key) {
        ReentrantLock lock = lockMap.get(key);
        if (lock != null) {
            return lock;
        }

        lock = new ReentrantLock();
        ReentrantLock previousLock = lockMap.putIfAbsent(key, lock);
        return previousLock == null ? lock : previousLock;
    }

    final public void intercept(Invocation inv) {
        Controller controller = inv.getController();
        String cacheName = buildCacheName(inv, controller);
        String cacheKey = buildCacheKey(inv, controller);
        Map<String, Object> cacheData = CaffeineKit.get(cacheName, cacheKey);
        if (cacheData == null) {
            Lock lock = getLock(cacheName);
            lock.lock();               // prevent cache snowslide
            try {
                cacheData = CaffeineKit.get(cacheName, cacheKey);
                if (cacheData == null) {
                    inv.invoke();
                    cacheAction(cacheName, cacheKey, controller);
                    return ;
                }
            }
            finally {
                lock.unlock();
            }
        }

        useCacheDataAndRender(cacheData, controller);
    }

    // TODO 考虑与 EvictInterceptor 一样强制使用  @CacheName
    protected String buildCacheName(Invocation inv, Controller controller) {
        CacheName cacheName = inv.getMethod().getAnnotation(CacheName.class);
        if (cacheName != null)
            return cacheName.value();
        cacheName = controller.getClass().getAnnotation(CacheName.class);
        return (cacheName != null) ? cacheName.value() : inv.getActionKey();
    }

    protected String buildCacheKey(Invocation inv, Controller controller) {
        StringBuilder sb = new StringBuilder(inv.getActionKey());
        String urlPara = controller.getPara();
        if (urlPara != null)
            sb.append('/').append(urlPara);

        String queryString = controller.getRequest().getQueryString();
        if (queryString != null)
            sb.append('?').append(queryString);
        return sb.toString();
    }

    /**
     * 通过继承 CacheInterceptor 并覆盖此方法支持更多类型的 Render
     */
    protected RenderInfo createRenderInfo(Render render) {
        return new RenderInfo(render);
    }

    protected void cacheAction(String cacheName, String cacheKey, Controller controller) {
        HttpServletRequest request = controller.getRequest();
        Map<String, Object> cacheData = new HashMap<String, Object>();
        for (Enumeration<String> names=request.getAttributeNames(); names.hasMoreElements();) {
            String name = names.nextElement();
            cacheData.put(name, request.getAttribute(name));
        }

        Render render = controller.getRender();
        if (render != null) {
            cacheData.put(renderKey, createRenderInfo(render));       // cache RenderInfo
        }
        CaffeineKit.put(cacheName, cacheKey, cacheData);
    }

    protected void useCacheDataAndRender(Map<String, Object> cacheData, Controller controller) {
        HttpServletRequest request = controller.getRequest();
        Set<Entry<String, Object>> set = cacheData.entrySet();
        for (Iterator<Entry<String, Object>> it=set.iterator(); it.hasNext();) {
            Entry<String, Object> entry = it.next();
            request.setAttribute(entry.getKey(), entry.getValue());
        }
        request.removeAttribute(renderKey);

        RenderInfo renderInfo = (RenderInfo)cacheData.get(renderKey);
        if (renderInfo != null) {
            controller.render(renderInfo.createRender());     // set render from cacheData
        }
    }
}
package com.jfinal.plugin.caffeine;


import com.jfinal.aop.Interceptor;
import com.jfinal.aop.Invocation;
import com.jfinal.plugin.ehcache.CacheName;

/**
 * EvictInterceptor.
 */
public class EvictCaffeineInterceptor implements Interceptor {

    public void intercept(Invocation inv) {
        inv.invoke();

        // @CacheName 注解中的多个 cacheName 可用逗号分隔
        String[] cacheNames = getCacheName(inv).split(",");
        if (cacheNames.length == 1) {
            CaffeineKit.removeAll(cacheNames[0].trim());
        } else {
            for (String cn : cacheNames) {
                CaffeineKit.removeAll(cn.trim());
            }
        }
    }

    /**
     * 获取 @CacheName 注解配置的 cacheName,注解可配置在方法和类之上
     */
    protected String getCacheName(Invocation inv) {
        CacheName cacheName = inv.getMethod().getAnnotation(CacheName.class);
        if (cacheName != null) {
            return cacheName.value();
        }

        cacheName = inv.getController().getClass().getAnnotation(CacheName.class);
        if (cacheName == null) {
            throw new RuntimeException("EvictInterceptor need CacheName annotation in controller.");
        }

        return cacheName.value();
    }
}

OK,改一行代码即可,再测试测试

/**
 * 清除缓存
 */
@CacheName("a")
@Before(EvictCaffeineInterceptor.class)
public void clearCache() {
   renderJson(srv.clearCache());
}

/**
 * 传递数据
 */
@CacheName("a")
@Before(CacheCaffeineInterceptor.class)
public void passData() {
   renderJson(srv.passData(get("k1"), get("k2")));
}

测试结果:

可以看到第一次访问是进入了Action

image.png

第二次访问是直接返回结果了,确定使用到缓存了
image.png

再清除缓存试试

image.png

看着是清除了,再访问一下刚取值的方法,看是否重新进入了Action
image.png

OK,确实清除了。


那么 EhCachePlugin这个插件的功能,都拿过来了。

image.png

Caffeine比上面少了两个东西,一个是Plugin这个插件接口,因为它不需要加载ehcache.xml所以不需内置,

那么想在JF启动时,配置自己的CaffeineKit.putCache(...) 写在什么位置好了?
我觉得最方便的位置是放在JFinalConfig子类的

public void configConstant(Constants me) {

当然也看自己的业务。
这里,如果putCache的代码比较多,可以单独拆出一个方法,或者类,这样转调即可。

放图:

有用得到的话,点个赞呗~ 

评论区

海哥

2021-12-06 10:28

来一个 ehcache 和 Caffeine 的 benchmark 呗。

杜福忠

2021-12-06 10:42

@海哥 之前没用过,前两天查了一下资料,网上吹的挺牛的。有时间是得自己验证验证这个是不是真🐂

贴下网上一些比较的图:
https://blog.csdn.net/weixin_39622178/article/details/111049224
https://www.cnblogs.com/cnndevelop/p/13429962.html
感兴趣的社友可以搞搞 ,@GXS 来一个性能对比报告咋样?

zzutligang

2021-12-07 03:00

必须赞一个

GXS

2021-12-09 23:51

@杜福忠 我得春节才有时间试试了,年底忙成狗

杜福忠

2021-12-15 14:18

public static List getKeys(String cacheName) {
return new ArrayList(asMap(cacheName).keySet());
}
@山东小木 老师反馈我getKeys这里写错了,确实之前写成了返回值集合了,getKeys应该是返回键集合的,这里更新一下,如果有社友用到的话记得处理一下哈

halason

2021-12-17 15:50

3.0及以上版本,jdk最低版本是11

杜福忠

2021-12-17 16:12

@halason 赞,这个细节忘记补充了,我文章里补充一下,免得有人掉坑了

GXS

2022-03-10 23:16

网上的一份压测对比报告,供大家参考(可惜没记录测试使用的缓存版本号)
https://www.cnblogs.com/cnndevelop/p/13429962.html

GXS

2022-06-04 11:34

参数result是null时,CaffeineKit put会报错。
参考j2cache做法,做了如下调整:传入null值时,生成1个空对象

CaffeineKit:
public static void put(String cacheName, Object key, Object value) {
getOrAddCache(cacheName).put(key, (null == value)? newNullObject() : value);
}

private static NullObject newNullObject() {
return new NullObject();
}

NullObject:
public class NullObject implements Serializable {
}

杜福忠

2022-06-05 09:15

@GXS 有道理!我优化一下帖子。虽然我后来做了if null 判断,但是如果更新的时候,就是想设置为null那么put操作就丢失了,不想放NullObject对象,可以考虑调用remove,我考虑一下

GXS

2022-06-05 15:55

@杜福忠 放NullObject对象有异常,目前还是remove稳一点