💥高性能本地缓存caffeine最佳实践

1,642 阅读6分钟

一、Caffeine简介

Caffeine是一个功能强大且高效的本地缓存解决方案,适用于需要快速访问和存储数据的场景。其灵活的配置选项和高性能特性使其成为Java开发者的热门选择,Caffeine是基于JDK8的高性能本地缓存库,提供了几乎完美的命中率。它有点类似JDK中的ConcurrentMap,实际上,Caffeine中的LocalCache接口就是实现了JDK中的ConcurrentMap接口,但两者并不完全一样。最根本的区别就是,ConcurrentMap保存所有添加的元素,除非显示删除之(比如调用remove方法)。而本地缓存一般会配置自动剔除策略,为了保护应用程序,限制内存占用情况,防止内存溢出。

  • 高性能:

    • Caffeine使用了基于JavaConcurrentHashMap和一些优化算法,提供低延迟和高吞吐量的缓存操作。
  • 自动过期:

    • 支持基于时间的过期策略,可以设置缓存条目的最大存活时间(TTL)和最大空闲时间(TTE)。
  • 容量限制:

    • 可以设置缓存的最大容量,当达到容量限制时,Caffeine会根据LRU(Least Recently Used)策略自动清除最少使用的条目。
  • 异步加载:

    • 支持异步加载缓存条目,允许在后台线程中加载数据,避免阻塞主线程。
  • 写入策略:

    • 提供多种写入策略,包括写入后失效(Write-Through)、写入后不失效(Write-Behind)等。
  • 统计信息:

    • 内置统计功能,可以监控缓存的命中率、加载时间等性能指标。
  • 多级缓存:

    • 支持多级缓存架构,可以与其他缓存系统(如Redis)结合使用。
  • 灵活的键和值类型:

    • 支持泛型,可以使用任意类型作为缓存的键和值。
  • 事件监听:

    • 提供事件监听机制,可以在缓存条目被加载、更新或移除时触发相应的事件。

特性和详细介绍

bff4c6316c8d49d0898fca9499e37536~tplv-k3u1fbpfcp-jj-mark-v1_0_0_0_0_5o6Y6YeR5oqA5pyv56S-5Yy6IEAgSVRfc3Vuc2hpbmU=_q75.webp

压力测试

用数据说话,Caffeine官方利用最权威的压测工具 「JMH」CaffeineConcurrentMapGuavaCacheehcache等做了详细的压测对比,结果如下:

image.png

更多压测对比请参考:github.com/ben-manes/c…

从官方的压测结果来看,无论是全读场景、全写场景、或者读写混合场景,无论是8个线程,还是16个线程,Caffeine都是完胜、碾压,简直就是拿着望远镜都看不到对手。

二、POM依赖

Caffeine使用还是非常简单的,如果你用过GuavaCache,那就更简单了,因为CaffeineAPI设计大量借鉴了GuavaCache。首先,引入最新版本Maven依赖:

<dependency> 
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>3.1.8</version> 
</dependency>

三、应用实践

3.1 Cache

最普通的一种缓存,无需指定加载方式,需要手动调用put()进行加载。需要注意的是put()方法对于已存在的key将进行覆盖,这点和Map的表现是一致的。在获取缓存值时,如果想要在缓存值不存在时,原子地将值写入缓存,则可以调用get(key, k -> value)方法,该方法将避免写入竞争。调用invalidate()方法,将手动移除缓存。

在多线程情况下,当使用get(key, k -> value)时,如果有另一个线程同时调用本方法进行竞争,则后一线程会被阻塞,直到前一线程更新缓存完成;而若另一线程调用getIfPresent()方法,则会立即返回null,不会被阻塞。

Cache<Object, Object> cache = Caffeine.newBuilder()
          //初始数量
          .initialCapacity(10)
          //最大条数
          .maximumSize(10)
          //expireAfterWrite和expireAfterAccess同时存在时,以expireAfterWrite为准
          //最后一次写操作后经过指定时间过期
          .expireAfterWrite(1, TimeUnit.SECONDS)
          //最后一次读或写操作后经过指定时间过期
          .expireAfterAccess(1, TimeUnit.SECONDS)
          //监听缓存被移除
          .removalListener((key, val, removalCause) -> { })
          //记录命中
          .recordStats()
          .build();
 
  cache.put("1","张三");
  //张三
  System.out.println(cache.getIfPresent("1"));
  //存储的是默认值
  System.out.println(cache.get("2",o -> "默认值"));

3.2 LoadingCache<K,V> 自动创建

LoadingCache是一种自动加载的缓存。其和普通缓存不同的地方在于,当缓存不存在/缓存已过期时,若调用get()方法,则会自动调用CacheLoader.load()方法加载最新值。调用getAll()方法将遍历所有的key调用get(),除非实现了CacheLoader.loadAll()方法。使用LoadingCache时,需要指定CacheLoader,并实现其中的load()方法供缓存缺失时自动加载。

在多线程情况下,当两个线程同时调用get(),则后一线程将被阻塞,直至前一线程更新缓存完成。

3.3 AsyncLoadingCache<K,V> 异步获取

AsyncCacheCache的一个变体,其响应结果均为CompletableFuture,通过这种方式,AsyncCache对异步编程模式进行了适配。默认情况下,缓存计算使用ForkJoinPool.commonPool()作为线程池,如果想要指定线程池,则可以覆盖并实现Caffeine.executor(Executor)方法。synchronous()提供了阻塞直到异步缓存生成完毕的能力,它将以Cache进行返回。

在多线程情况下,当两个线程同时调用get(key, k -> value),则会返回同一个CompletableFuture对象。由于返回结果本身不进行阻塞,可以根据业务设计自行选择阻塞等待或者非阻塞。

四、Caffeine在SpringBoot中运用

@Component
public class AgentCache {

    /**
     * 直播间代理员基本信息(批量)
     */
    private LoadingCache<Long, List<Agent>> agentBaseInfoCache = null;

    /**
     * 直播间代理员信息(单个)
     */    
    private LoadingCache<Long, AgentModel> agentModelCache = null;
    
    @Resource
    private IZbSiteAttributeService siteAttributeService;

    @Autowired
    private IAgentService agentService;

    @PostConstruct
    private void init() {
        agentBaseInfoCache = Caffeine.newBuilder()
                //设置本地缓存容器的初始容量
                .initialCapacity(10)
                //设置本地缓存的最大容量
                .maximumSize(1000)
                //设置缓存后多少秒过期
                .expireAfterWrite(3600, TimeUnit.SECONDS).build(new CacheLoader<Long, List<Agent>>() {
                    @Override
                    public @Nullable List<Agent> load(Long zbId) {
                        return agentService.lambdaQuery()
                                .select(Agent::getUserId, Agent::getId, Agent::getParentCode, Agent::getLevel, Agent::getWeComAccount, Agent::getState, Agent::getStoreId)
                                .eq(Agent::getZbId, zbId)
                                .in(Agent::getState, HealthAgentConstants.AgentState.NORMA, HealthAgentConstants.AgentState.DISABLE)
                                .list();
                    }
                });

        agentModelCache = Caffeine.newBuilder()
                //设置本地缓存容器的初始容量
                .initialCapacity(10)
                //设置本地缓存的最大容量
                .maximumSize(1000)
                //设置缓存后多少秒过期
                .expireAfterWrite(3600, TimeUnit.SECONDS).build(new CacheLoader<Long, AgentModel>() {
                    @Override
                    public AgentModel load(Long zbId) {
                        String attributeVal = siteAttributeService.getAttributeVal(zbId, SiteAttributeEnum.AGENT_SETTING_MODEL.getStatus());
                        return StringUtils.isNotBlank(attributeVal) ? JsonUtil.fromJson(attributeVal, AgentModel.class) : new AgentModel();
                    }
                });
    }

    /**
     * 获取正常代理的用户ID
     * @param zbId
     * @return
     */
    public List<Long> get(Long zbId) {
        List<Long> list = agentBaseInfoCache.get(zbId)
                .stream()
                .filter(a -> HealthAgentConstants.AgentState.NORMA == a.getState())
                .map(Agent::getUserId)
                .collect(Collectors.toList());
        return list;
    }


    /**
     * 获取所有代理员用户ID 包含了正常 & 禁用
     * @param zbId
     * @return
     */
    public List<Long> getAllAgentUserId(Long zbId) {
        return agentBaseInfoCache.get(zbId)
                .stream()
                .map(Agent::getUserId)
                .collect(Collectors.toList());
    }

    /**
     * 获取正常代理的用户代理集合
     * @param zbId
     * @return
     */
    public List<Agent> getAgentBaseInfoList(Long zbId) {
        return agentBaseInfoCache.get(zbId)
                .stream()
                .filter(a -> HealthAgentConstants.AgentState.NORMA == a.getState())
                .collect(Collectors.toList());
    }

    /**
     * 获取代理的用户代理集合-所有
     * @param zbId
     * @return
     */
    public List<Agent> getAllAgentBaseInfoList(Long zbId) {
        return agentBaseInfoCache.get(zbId).stream().collect(Collectors.toList());
    }

    public Agent getAgentBaseInfo(Long zbId, Long userId) {
        return agentBaseInfoCache.get(zbId).stream().filter(w -> w.getUserId().equals(userId)).findFirst().orElse(null);
    }

    public void invalidate(Long zbId) {
        agentBaseInfoCache.invalidate(zbId);
//        agentBaseInfoCache.refresh(zbId);
    }


    /**
     * 获取AgentModel缓存
     * @param zbId
     * @return
     */
    public AgentModel getAgentModel(Long zbId) {
        AgentModel model;
        try {
            model = agentModelCache.get(zbId);
        } catch (Exception e) {
            String attributeVal = siteAttributeService.getAttributeVal(zbId, SiteAttributeEnum.AGENT_SETTING_MODEL.getStatus());
            return StringUtils.isNotBlank(attributeVal) ? JsonUtil.fromJson(attributeVal, AgentModel.class) : new AgentModel();
        }
        if (model == null) {
            model = new AgentModel();
        }
        return model;
    }

    /**清除AgentModel本地缓存*/
    public void refreshAgentModel(Long key) {
        try {
            agentModelCache.refresh(key);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

参考文献: