我想把记忆缓存起来, 等再次见到你, 就能够很快认出你.
能够说出这么有哲理的话, 得益于我对缓存的理解, 以及对它的看重. 没有了缓存, 我的人生就没有了意义.
缓存是非常重要的, 工作中大部分工作可以说是和缓存打交道. 由于使用广泛, 所以针对缓存系统的任何优化, 如果能够提高一丁点儿性能, 就会让人无比兴奋.
很长一段时间, 我都在用 Guava 的 LoadingCache . 它和 ConcurrentHashMap 是非常像的, 但在其上封装了一些好用的逐出策略和并发优化, 就显得好用的多.
今天主要说的是 Caffeine , 中文名就是咖啡因, 一种容易让人精神亢奋的物质. 它可以说是 Guava 的重写, 但是效率却非常的高, 青出于蓝而胜于蓝.
下图是 Caffeine 的一张性能测试图. 可以看到它的性能, 甩了 GuavaCache 老远. 这是为什么呢?
首先要从它的作者开始说起. 作者的 GitHub 是 ( https://github.com/ben-manes ), 曾经写了 ConcurrentLinkedHashMap 这个类, 而这个类又是 GuavaCache 的基础. Ben Manes 一拍脑袋, 决定更上层楼.
为什么说 Caffeine 好?
后浪 Caffeine 一来, GuavaCache 就已经 OUT 了.
Caffeine 支持异步加载方式, 直接返回 CompletableFutures , 相对于 GuavaCache 的同步方式, 它不用阻塞等待数据的载入. 另外, 它的编程模型是友好的, 省去了很多重复的工作.
GuavaCache 是基于 LRU 的, 而 Caffeine 是基于 LRU 和 LFU 的, 结合了两者的优点. 对这两个算法不太清楚的同学, 可以参考 xjjdog 之前的文章: 《3 种堆内缓存算法, 赠源码和设计思路》
两者合体之后, 变成了新的 W-TinyLFU 算法, 它的命中率非常高, 内存占用更加的小, 这是主要原因所在.
Caffeine 另外一个比较快的原因, 就是很多操作都使用了异步, 把这些事件提交到队列里. 队列使用的 RingBuffer , 看到这个名词, 我不自觉的想到了 lmax 的 Disruptor , 它已经成了无锁高并发的代名词.
测试命中率
我们决定拿线上的数据进行验证一下. 事实上, 大部分比较重要的 Cache, 我都已经使用 Caffeine 替换了, 完成了骚气的升级.
由于它们的 API 长得非常像, 这个过程是无痛的, 连麻药都不需要打.
其中有个业务, 有一个大的堆内缓存, 缓存了用户数据. 里面包含用户名, 性别, 地址, 积分等属性, 形成了一个 JSON 对象, 但大小不超过 1KB. 通过灰度, 根据不同的策略, 我们测试了它的实际命中率.
策略 1
最大缓存 1w 用户
数据进入缓存后, 5 分钟失效 (需要重新读取)
命中率:
- Caffeine 29.22 %
- Guava 21.95%
策略 2
加大缓存数据量到 6w 用户
数据进入缓存后, 20 分钟失效, 这个和 Session 有的一拼了
命中率 (依然是高一筹):
- Caffeine 56.04 %
- Guava 50.01%
策略 3
直接加大缓存到 15w 用户
数据进入缓存后, 30 分钟失效
此时的命中率:
- Caffeine 71.10 %
- Guava 62.76%
Caffeine 的命中率一直是领先的. 命中率高, 效率自然也就高. 调整到 50% 以上, 我们的缓存作用就很大了.
异步载入
再放上官方的两张测试图:
(1) Read (75%) / Write (25%)
(2) Write (100%)
(3) Read (100%)
我们一直在提 Caffeine 的异步加载. 那代码到底长什么样子呢? 异步加载缓存使用了响应式编程模型, 返回的是 CompletableFuture 对象. 说实话, 代码长得和 Guava 很像.
- public static void main(String[] args) {
- AsyncLoadingCache<String, String> asyncLoadingCache = Caffeine.newBuilder()
- .maximumSize(1000)
- .buildAsync(key -> slowMethod(key));
- CompletableFuture<String> g = loadingCache.get("test");
- String value = g.get();
- }
- static String slowMethod(String key) throws Exception {
- Thread.sleep(1000);
- return key + ".result";
- }
我记得前段时间翻 Spring 的源码时, 也看到过它.
在 SpringBoot 里, 通过提供一个 CacheManager 的 Bean, 即可与 Springboot-cache 进行集成, 可以说是很方便了.
关键代码.
- //bean 生成
- @Bean("caffeineCacheManager")
- public CacheManager cacheManager() {
- CaffeineCacheManager cacheManager = new CaffeineCacheManager();
- cacheManager.setCaffeine(Caffeine.newBuilder() .maximumSize(1000));
- return cacheManager;
- }
- // 使用注入
- @CacheConfig(cacheNames = "caffeineCacheManager")
- // 信息缓存
- @Cacheable(key = "#id")
技术框架这么多, 何时是尽头.
作者简介: 小姐姐味道 (xjjdog), 一个不允许程序员走弯路的公众号. 聚焦基础架构和 Linux. 十年架构, 日百亿流量, 与你探讨高并发世界, 给你不一样的味道. 我的个人微信 xjjdog0, 欢迎添加好友, 进一步交流. 交流.
来源: http://www.tuicool.com/articles/Z3Yn6ja