关于缓存的设计
一、背景
随着流量的增加,DB压力增大,读写能力也随之下降,甚至造成DB故障,服务不可用,缓存应运而生。
二、内存缓存框架的比较
比较项 | ConcurrentHashMap | LRUMap | Ehcache | Guava Cache | Caffeine |
---|---|---|---|---|---|
读写性能 | 很好,分段锁 | 一般,全局加锁 | 好 | 好,需要做淘汰操作 | 很好 |
淘汰算法 | 无 | LRU,一般 | 支持多种淘汰算法,LRU,LFU,FIFO | LRU,一般 | W-TinyLFU, 很好 |
功能丰富程度 | 功能比较简单 | 功能比较单一 | 功能很丰富 | 功能很丰富,支持刷新和虚引用等 | 功能和Guava Cache类似 |
工具大小 | jdk自带类,很小 | 基于LinkedHashMap,较小 | 很大,最新版本1.4MB | 是Guava工具类中的一个小部分,较小 | 一般,最新版本644KB |
是否持久化 | 否 | 否 | 是 | 否 | 否 |
是否支持集群 | 否 | 否 | 是 | 否 | 否 |
三、选择合适的内存缓存
- 对于ConcurrentHashMap来说,比较适合缓存比较固定不变的元素,且缓存的数量较小的。虽然从上面表格中比起来有点逊色,但是其由于是jdk自带的类,在各种框架中依然有大量的使用,比如我们可以用来缓存我们反射的Method,Field等等;也可以缓存一些链接,防止其重复建立。在Caffeine中也是使用的ConcurrentHashMap来存储元素。
- 对于LRUMap来说,如果不想引入第三方包,又想使用淘汰算法淘汰数据,可以使用这个。
- 对于Ehcache来说,由于其jar包很大,较重量级。对于需要持久化和集群的一些功能的,可以选择Ehcache。笔者没怎么使用过这个缓存,如果要选择的话,可以选择分布式缓存来替代Ehcache。
- 对于Guava Cache来说,Guava这个jar包在很多Java应用程序中都有大量的引入,所以很多时候其实是直接用就好了,并且其本身是轻量级的而且功能较为丰富,在不了解Caffeine的情况下可以选择Guava Cache。
- 对于Caffeine来说,笔者是非常推荐的,其在命中率,读写性能上都比Guava Cache好很多,并且其API和Guava cache基本一致,甚至会多一点。在真实环境中使用Caffeine,取得过不错的效果。
- 总结一下: 如果不需要淘汰算法则选择ConcurrentHashMap,如果需要淘汰算法和一些丰富的API,这里推荐选择Caffeine。
四、缓存的写入以及更新
1、缓存的写入
内存缓存是如何写入的呢,tomcat在启动时spring容器会进行依赖注入,@PostConstruct注解修饰的方法可以依赖注入后执行,我们需要在这个时候把数据加载到缓存种。不过这个时候要做到接口没有对外提供服务,否则流量大的例如秒杀活动会直接打到数据库,造成故障。
public class CaffeineConfiguration {
@PostConstruct
public void init() {
Cache<Long, Object> cache = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.MINUTES)
.maximumSize(100)
.build();
List<Order> result = listFromDb();
result.forEach(order -> cache.put(order.getOrderNo(), order));
}
}
2、缓存的更新
缓存的更新涉及3方面:
基于大小,基于时间和基于引用自动删除
监听自生产自消费的mq消息(广播模式)进行删除
监听MySQL binlog进行删除
- 基于大小,基于时间和基于引用自动删除
// 基于大小回收
LoadingCache<Long, Order> cache = Caffeine.newBuilder()
.maximumSize(1)
.build(k -> new Order());
// 基于时间回收
LoadingCache<Long, Order> cache = Caffeine.newBuilder()
.expireAfterAccess(2, TimeUnit.SECONDS)
.build(Order::getKey);
// 基于引用回收
LoadingCache<Long, Order> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.SECONDS)
.weakKeys()
.weakValues()
.build(Order::getKey);
- 监听mq消息
监听mq消息实现自发自消来删除内存缓存 - 监听MySQL binlog
监听binlog实现自发自消来删除内存缓存,删除失败则使用mq进行重试删除。