公众号:Java小咖秀,网站:javaxks.com
作者 :xiaolyuh,链接: my.oschina.net/xiaolyuh/bl…
问题描述:
通过使用redis和Caffeine来做缓存,我们会发现一些问题。
如果只使用redis来做缓存我们会有大量的请求到redis,但是每次请求的数据都是一样的,假如这一部分数据就放在应用服务器本地,那么就省去了请求redis的网络开销,请求速度就会快很多。但是使用redis横向扩展很方便。
如果只使用Caffeine来做本地缓存,我们的应用服务器的内存是有限,并且单独为了缓存去扩展应用服务器是非常不划算。所以,只使用本地缓存也是有很大局限性的。
至此我们是不是有一个想法了,两个一起用。将热点数据放本地缓存(一级缓存),将非热点数据放redis缓存(二级缓存)。
缓存的选择
一级缓存:Caffeine是一个一个高性能的 Java 缓存库;使用 Window TinyLfu 回收策略,提供了一个近乎最佳的命中率。Caffeine 缓存详解
二级缓存:redis是一高性能、高可用的key-value数据库,支持多种数据类型,支持集群,和应用服务器分开部署易于横向扩展。
解决思路
Spring 本来就提供了Cache的支持,最核心的就是实现Cache和CacheManager接口。
Cache接口
主要是实现对缓存的操作,如增删查等。
1 | vbnet复制代码public interface Cache { |
CacheManager接口
根据缓存名称来管理Cache,核心方法就是通过缓存名称获取Cache。
1 | arduino复制代码public interface CacheManager { |
通过上面的两个接口我的大致思路是,写一个LayeringCache来实现Cache接口,LayeringCache类中集成对Caffeine和redis的操作。写一个LayeringCacheManager来管理LayeringCache就行了。
这里的redis缓存使用的是我扩展后的RedisCache详情请看:
LayeringCache
LayeringCache类,因为需要集成对Caffeine和Redis的操作,所以至少需要有name(缓存名称)、CaffeineCache和CustomizedRedisCache三个属性,还增加了一个是否使用一级缓存的开关usedFirstCache。在LayeringCache类的方法里面分别去调用操作一级缓存的和操作二级缓存的方法就可以了。
在这里特别说明一下:
- 在查询方法如get等,先去查询一级缓存,如果没查到再去查二级缓存。
- put方法没有顺序要求,但是建议将一级缓存的操作放在前面。
- 如果是删除方法如evict和clear等,需要先删掉二级缓存的数据,再去删掉一级缓存的数据,否则有并发问题。
- 删除一级缓存需要用到redis的Pub/Sub(订阅发布)模式,否则集群中其他服服务器节点的一级缓存数据无法删除。
- redis的Pub/Sub(订阅发布)模式发送消息是无状态的,如果遇到网络等原因有可能导致一些应用服务器上的一级- - 缓存没办法删除,如果对L1和L2数据同步要求较高的话,这里可以使用MQ来做。
完整代码:
1 | typescript复制代码/** |
LayeringCacheManager
因为我们需要在CacheManager中来管理缓存,所以我们需要在CacheManager定义一个容器来存储缓存。在这里我们新建一个ConcurrentMap<String, Cache> cacheMap来存在缓存,CacheManager的两个方法getCache和getCacheNames都通过操作这个cacheMap来实现。
Map<String, FirstCacheSetting> firstCacheSettings和Map<String, SecondaryCacheSetting> secondaryCacheSettings属性是针对每一个缓存的特殊配置,如一级缓存的过期时间配置,二级缓存的过期时间和自动刷新时间配置。剩下的属性就不一一介绍了,可直接看下面的源码。
getCache 方法
这可以说是CacheManager最核心的方法,所有CacheManager操作都围绕这个方法进行。
1 | kotlin复制代码@Override |
从这段逻辑我们可以看出这个方法就是根据名称获取缓存,如果没有找到并且动态创建缓存的开关dynamic为true的话,就调用createCache方法动态的创建缓存。
createCache 方法
去创建一个LayeringCache
1 | scss复制代码protected Cache createCache(String name) { |
在创建缓存的时候我们会调用getSecondaryCacheExpirationSecondTime、getSecondaryCachePreloadSecondTime和getForceRefresh等方法去获取二级缓存的过期时间、自动刷新时间和是否强制刷新(走数据库)等值,这些都在secondaryCacheSettings属性中获取;调用createNativeCaffeineCache方法去创建一个一级缓存Caffeine的实例。
createNativeCaffeineCache在这个方法里面会调用getCaffeine方法动态的去读取一级缓存的配置,并根据配置创建一级缓存,如果没有找到特殊配置,就使用默认配置,而这里的特殊配置则在firstCacheSettings属性中获取。
getCaffeine
动态的获取一级缓存配置,并创建对应Caffeine对象。
1 | typescript复制代码private Caffeine<Object, Object> getCaffeine(String name) { |
setFirstCacheSettings和setSecondaryCacheSettings
我们借用了RedisCacheManager的setExpires(Map<String, Long> expires)方法的思想。用setFirstCacheSettings和setSecondaryCacheSettings方法对一级缓存和二级缓存的特殊配置进行设值。
1 | typescript复制代码/** |
完整代码:
1 | typescript复制代码/** |
FirstCacheSettings
一级缓存配置类
1 | typescript复制代码public class FirstCacheSetting { |
SecondaryCacheSetting
二级缓存的特殊配置类
1 | java复制代码 |
使用方式
在上面我们定义好了LayeringCacheManager和LayeringCache接下来就是使用了。
新建一个配置类CacheConfig,在这里指定一个LayeringCacheManager的Bean。我那的缓存就生效了。完整代码如下:
1 | typescript复制代码/** |
在cacheManager中指定Bean的时候,我们通过调用LayeringCacheManager 的setFirstCacheSettings和setSecondaryCacheSettings方法为缓存设置一级缓存和二级缓存的特殊配置。
剩下的就是在Service方法上加注解了,如:
1 | less复制代码@Override |
@Cacheable的sync属性建议设置成true。
测试
最后通过jmeter测试,50个线程,使用多级缓存,比只使用redis级缓存性能提升2倍多,只是用redis吞吐量在1243左右,使用多级缓存后在2639左右。
源码
本文转载自: 掘金