背景
-
Caffeine 简介
-
相对于Guava Cache优化点
-
整合
-
缓存配置
-
缓存使用
-
自定义缓存删除注解
-
reids 事件监听删除缓存
-
测试
-
测试类
-
测试结果
-
总结
-
源码下载
背景
为什么我们明明有了分布式缓存redis,还要将本地缓存多此一举整合为分布式缓存呢。原因很简单,==性能==。不管redis多块,都需要网络请求,io耗时,如果使用本地缓存基本没有耗时。
Caffeine 简介
官方文档Caffeine 是基于 JAVA 8 的高性能缓存库。并且在 spring5 (springboo 2.x) 后,spring 官方放弃了 Guava,而使用了性能更优秀的 Caffeine 作为默认缓存组件
关于性能测试,Caffine 官方也给了一份图片这里只给出了一个性能测试的图片,可以看到Caffeine的性能是最好的,更多详细数据可以参考官网
相对于Guava Cache优化点
Guava Cache 本质上还是使用了LRU淘汰算法 在了解LRU算法前。我们先了解几个常用的缓存淘汰算法
-
FIFO(First-In, First-Out):先进先出,在这种淘汰算法中,先进入缓存的会先被淘汰,会导致命中率很低。 -
LRU(Least Recently Used):最近最少使用算法,每次访问数据都会将其放在我们的队尾,如果需要淘汰数据,就只需要淘汰队首即可。仍然有个问题,如果有个数据在 1 分钟访问了 1000次,再后 1 分钟没有访问这个数据,但是有其他的数据访问,就导致了我们这个热点数据被淘汰。
像Mysql就使用了LRU算法,不同的是MySql对LRU算法做了一定的优化,对数据进行冷热分离,将 LRU 链表分成两部分,一部分用来存放冷数据,也就是刚从磁盘读进来的数据,另一部分用来存放热点数据。当从磁盘读取数据页后,会先将数据页存放到 LRU 链表冷数据区的头部,如果这些缓存页在 1 秒之后被访问,那么就将缓存页移动到热数据区的头部;如果是 1 秒之内被访问,则不会移动,缓存页仍然处于冷数据区中。因为Mysql读书数据是按数据页读取,还有预读操作,所以作了如下优化,感兴趣可以自己去详细了解
-
LFU(Least Frequently Used):最近最少频率使用,利用额外的空间记录每个数据的使用频率,然后选出频率最低进行淘汰。这样就避免了 LRU 不能处理时间段的问题。但是可能存在缓存前期缓存次数过多,后期没人访问了,因为前期访问次数太多,导致一直不被淘汰。
总之以上三种淘汰算法有利有弊。但是后续由前Google工程师发明的==W-TinyLFU==淘汰算法,提供了一个近乎最佳的命中率。Caffine Cache就是基于此算法而研发。==W-TinyLFU==是如何解决LFU算法的缺点的呢?感兴趣可以看这里 https://jishuin.proginn.com/p/763bfbd358a0
这里限于篇幅就不作过多使用
整合
这里我们首先加入需要用到的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.2</version>
</dependency>
版本自行选择
缓存配置
-
CacheConfig
@EnableCaching
@Configuration
public class CacheConfig {
/**
* 节点同步缓存的name和key的分隔符
*/
public static final String DOUBLE_COLON = "::";
/**
* 默认缓存时间
*/
private static final long DEFAULT_TTL = 30;
/**
* 默认最大条数
*/
private static final long MAXIMUM_SIZE = 10000;
/**
* 默认分钟
*/
private static final TimeUnit DEFAULT_TIME_UNIT = TimeUnit.MINUTES;
@Bean
@Primary
public CacheManager caffeineCacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
List<CaffeineCache> cacheBeans = getCacheBeans();
if (DataUtils.isNotEmpty(cacheBeans)) {
cacheManager.setCaches(cacheBeans);
}
return cacheManager;
}
/**
* 把 缓存 CaffeineCache 配置到添加到这个list中
* @return
*/
private List<CaffeineCache> getCacheBeans() {
List<CaffeineCache> list = new ArrayList<>();
list.add(CacheConfig.builderCaffeineCache(CacheConstants.TEST_CACHE));
return list;
}
// 构造缓存
public static CaffeineCache builderCaffeineCache(String name, long ttl, TimeUnit unit, long maxSize) {
return new CaffeineCache(name, Caffeine.newBuilder().expireAfterWrite(ttl, unit).maximumSize(maxSize).build());
}
public static CaffeineCache builderCaffeineCache(String name, long ttl) {
return new CaffeineCache(name, Caffeine.newBuilder().expireAfterWrite(ttl, DEFAULT_TIME_UNIT).maximumSize(MAXIMUM_SIZE).build());
}
public static CaffeineCache builderCaffeineCache(String name) {
return new CaffeineCache(name, Caffeine.newBuilder().expireAfterWrite(DEFAULT_TTL, DEFAULT_TIME_UNIT).maximumSize(MAXIMUM_SIZE).build());
}
}
@EnableCaching
代表开启Spring Cache,这里缓存管理器我们使用SimpleCacheManager
而不是CaffeineCacheManager
,因为SimpleCacheManager
配置更为灵活,可以为每个缓存配置相应的失效时间、策略等
spring cache对缓存的管理定义了一个接口CacheManager
总共有如下实现类其中
AbstractCacheManager
又是一个抽象类,继承了AbstractCacheManager
有如下类由于
CacheManager
众多,我就不一一说明了,感兴趣可以去官网自己了解。我这里偷一张网图说明下
图片来源:https://juejin.cn/post/6844903966615011335
对于缓存的配置,后续就直接加到我们设置的一个list中
List<CaffeineCache> list = new ArrayList<>();
缓存使用
对于缓存的使用,spring提供了如下注解
-
@Cacheable
: 根据方法对其返回结果进行缓存,下次请求时,如果缓存存在,则直接读取缓存数据返回;如果缓存不存在,则执行方法,并把返回的结果存入缓存中。==一般用在查询方法上==。 -
@CachePut
:使用该注解标志的方法,每次都会执行,并将结果存入指定的缓存中。其他方法可以直接从响应的缓存中读取缓存数据,而不需要再去查询数据库。==一般用在新增方法上== -
@CacheEvict
: 使用该注解标志的方法,会清空指定的缓存。==一般用在更新或者删除方法上== -
@Caching
:组合注解,可以组合上面的多个注解 其源码:
public @interface Caching {
Cacheable[] cacheable() default {};
CachePut[] put() default {};
CacheEvict[] evict() default {};
}
这里我们主要使用
@Cacheable
注解,由于需要改造成分布式缓存。我们这里自定义一个注解
自定义缓存删除注解
-
DeleteCache
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DeleteCache {
String value();
String key() default "";
}
-
CacheAspect 然后定义一个切面去删除缓存
@Aspect
@Component
@Slf4j
public class CacheAspect {
@Autowired
private RedissonClient redissonClient;
@Autowired
FeishuRobot feishuRobot;
@Pointcut("@annotation(com.shopcider.plutus.component.annotation.DeleteCache)")
public void fun() {}
/**
* 主要是做节点同步。若没有cacheKey将会把整个缓存Map给清除掉
*/
@AfterReturning(value = "fun()", returning = "object")
public void doAfter(JoinPoint joinPoint, Object object) {
RTopic topic = redissonClient.getTopic(RedisCache.CACHE_TOPIC);
CacheManager cacheManager = SpringUtils.getBean("caffeineCacheManager", CacheManager.class);
MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
Method method = methodSignature.getMethod();
DeleteCache deleteCache = method.getAnnotation(DeleteCache.class);
String value = deleteCache.value();
String key = deleteCache.key();
Object cacheKey = ExpressionUtil.isEl(key) ? ExpressionUtil.parse(deleteCache.key(), method, joinPoint.getArgs()) : key;
String pushMsg;
Cache cache = cacheManager.getCache(value);
if (DataUtils.isEmpty(cache)) return;
if (DataUtils.isEmpty(cacheKey)) {
cache.clear();
pushMsg = value + CacheConfig.DOUBLE_COLON;
} else {
cache.evict(cacheKey);
pushMsg = value + CacheConfig.DOUBLE_COLON + cacheKey;
}
try {
// todo 后续是否对多节点返回数据监控
topic.publish(pushMsg);
} catch (Exception e) {
log.info("分布式缓存刷新通知异常,缓存 {}", value, e);
// 缓存删除失败监控
}
}
}
这里首先aop自已删除一次缓存,然后基于redis发布订阅发布消息多节点删除缓存。
-
ExpressionUtil ExpressionUtil主要是对el表达式作解析使用,不是本文的重点
public class ExpressionUtil {
/**
* el表达式解析
*
* @param expressionString 解析值
* @param method 方法
* @param args 参数
* @return
*/
public static Object parse(String expressionString, Method method, Object[] args) {
if (DataUtils.isEmpty(expressionString)) {
return null;
}
//获取被拦截方法参数名列表
LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
String[] paramNameArr = discoverer.getParameterNames(method);
//SPEL解析
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
for (int i = 0; i < Objects.requireNonNull(paramNameArr).length; i++) {
context.setVariable(paramNameArr[i], args[i]);
}
return parser.parseExpression(expressionString).getValue(context);
}
public static boolean isEl(String param) {
return Objects.equals(param.substring(0, 1), "#");
}
}
reids 事件监听删除缓存
-
RedisCacheListener
@Component
@Slf4j
public class RedisCacheListener implements ApplicationRunner, Ordered {
@Autowired
private RedissonClient redisson;
@Override
public void run(ApplicationArguments applicationArguments){
RTopic topic = redisson.getTopic(RedisCache.CACHE_TOPIC);
topic.addListener(String.class, (channel, msg) -> {
CacheManager cacheManager = SpringUtils.getBean("caffeineCacheManager", CacheManager.class);
String[] split = msg.split(CacheConfig.DOUBLE_COLON);
Cache cache = cacheManager.getCache(split[0]);
evictOrClear(cache, split);
log.info("{} 缓存清除完成", msg);
});
}
private void evictOrClear(Cache cache, String[] split) {
Objects.requireNonNull(cache);
if (split.length > 1) {
cache.evict(split[1]);
} else {
cache.clear();
}
}
@Override
public int getOrder() {
return 1;
}
}
至此就整合完成
测试
@Service
public class UserCacheService {
/**
* 查找
* 先查缓存,如果查不到,会查数据库并存入缓存
*/
@Cacheable(value = CacheConstants.TEST_CACHE, key = "#id", sync = true)
public User getUser(Long id){
System.out.println("查询数据库:" + id);
User user = new User();
user.setId(id);
user.setName("test");
return user;
}
/**
* 更新/保存
*/
@DeleteCache(value = CacheConstants.TEST_CACHE, key = "#user.id")
public void saveOrUpdateUser(User user){
System.out.println("保存或更新数据库" + user.getId());
}
/**
* 删除
*/
@DeleteCache(value = CacheConstants.TEST_CACHE, key = "#user.id")
public void delUser(User user){
System.out.println("删除数据库");
}
/**
* 删除
*/
@DeleteCache(value = CacheConstants.TEST_CACHE, key = "#id")
public void delUser(Long id){
System.out.println("删除数据库");
}
}
测试类
@Test
public void test() throws Exception{
Long id = 1L;
User user = userCacheService.getUser(id);
User user2 = userCacheService.getUser(id);
System.out.println("查询其他数据");
User user3 = userCacheService.getUser(2l);
User user4 = userCacheService.getUser(2l);
System.out.println("进行删除");
// 删除 id为1的数据
userCacheService.delUser(user);
User user7 = userCacheService.getUser(1l);
User user5 = userCacheService.getUser(2l);
User user6 = userCacheService.getUser(2l);
}
测试结果

总结
这种分布式缓存各个节点之间缓存同步没有作强一致性,所以如果有强一致性的场景还是推荐使用redis。本地缓存唯一优点就是比redis快,其次自定义注解改为了分布式删除,分布式通知采用redis发布订阅。可能存在redis异常导致缓存不正确,这种情况暂时不处理,只是加了简单的监控。
源码下载
后续有需要代码会上传到github
原文始发于微信公众号(小奏技术):SpringCache + Caffeine + Redis整合本地缓存为分布式缓存
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/30377.html