前言

在生产中已有实践,本组件仅做个人学习交流分享使用。github:https://github.com/axinSoochow/redis-caffeine-cache-starter

个人水平有限,欢迎大家在评论区轻喷。

所谓二级缓存

缓存就是将数据从读取较慢的介质上读取出来放到读取较快的介质上,如磁盘-->内存。

平时我们会将数据存储到磁盘上,如:数据库。如果每次都从数据库里去读取,会因为磁盘本身的IO影响读取速度,所以就有了像redis这种的内存缓存。可以将数据读取出来放到内存里,这样当需要获取数据时,就能够直接从内存中拿到数据返回,能够很大程度的提高速度。

但是一般redis是单独部署成集群,所以会有网络IO上的消耗,虽然与redis集群的链接已经有连接池这种工具,但是数据传输上也还是会有一定消耗。所以就有了进程内缓存,如:caffeine。当应用内缓存有符合条件的数据时,就可以直接使用,而不用通过网络到redis中去获取,这样就形成了两级缓存。应用内缓存叫做一级缓存,远程缓存(如redis)叫做二级缓存。

系统是否需要缓存

  • CPU占用:如果你有某些应用需要消耗大量的cpu去计算获得结果。
  • 数据库IO占用:如果你发现你的数据库连接池比较空闲,那么不应该用缓存。但是如果数据库连接池比较繁忙,甚至经常报出连接不够的报警,那么是时候应该考虑缓存了。

分布式二级缓存的优势

Redis用来存储热点数据,Redis中没有的数据则直接去数据库访问。

已经有Redis了,干嘛还需要了解Guava,Caffeine这些进程缓存呢:

  • Redis如果不可用,这个时候我们只能访问数据库,很容易造成雪崩,但一般不会出现这种情况
  • 访问Redis会有一定的网络I/O以及序列化反序列化开销,虽然性能很高但是其终究没有本地方法快,可以将最热的数据存放在本地,以便进一步加快访问速度。这个思路并不是我们做互联网架构独有的,在计算机系统中使用L1,L2,L3多级缓存,用来减少对内存的直接访问,从而加快访问速度。

所以如果仅仅是使用Redis,能满足我们大部分需求,但是当需要追求更高的性能以及更高的可用性的时候,那就不得不了解多级缓存。

二级缓存操作过程

数据读流程 描述
redis 与本地缓存都查询不到值的时候,会触发更新过程,整个过程是加锁的
缓存失效流程 描述
redis更新与删除缓存key都会触发,清除redis缓存后

如何使用组件?

组件是基于Spring Cache框架上改造的,在项目中使用分布式缓存,仅仅需要在缓存注解上增加:cacheManager ="L2_CacheManager",或者 cacheManager = CacheRedisCaffeineAutoConfiguration.分布式二级缓存

//这个方法会使用分布式二级缓存来提供查询
@Cacheable(cacheNames = CacheNames.CACHE_12HOUR, cacheManager = "L2_CacheManager")
public Config getAllValidateConfig() {
}

如果你想既使用分布式缓存,又想用分布式二级缓存组件,那你需要向Spring注入一个 @Primary 的 CacheManager bean

@Primary
@Bean("deaultCacheManager")
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
// 生成一个默认配置,通过config对象即可对缓存进行自定义配置
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
// 设置缓存的默认过期时间,也是使用Duration设置
config = config.entryTtl(Duration.ofMinutes(2)).disableCachingNullValues(); // 设置一个初始化的缓存空间set集合
Set<String> cacheNames = new HashSet<>();
cacheNames.add(CacheNames.CACHE_15MINS);
cacheNames.add(CacheNames.CACHE_30MINS); // 对每个缓存空间应用不同的配置
Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
configMap.put(CacheNames.CACHE_15MINS, config.entryTtl(Duration.ofMinutes(15)));
configMap.put(CacheNames.CACHE_30MINS, config.entryTtl(Duration.ofMinutes(30))); // 使用自定义的缓存配置初始化一个cacheManager
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.initialCacheNames(cacheNames) // 注意这两句的调用顺序,一定要先调用该方法设置初始化的缓存名,再初始化相关的配置
.withInitialCacheConfigurations(configMap)
.build();
return cacheManager;
}

然后:

//这个方法会使用分布式二级缓存
@Cacheable(cacheNames = CacheNames.CACHE_12HOUR, cacheManager = "L2_CacheManager")
public Config getAllValidateConfig() {
} //这个方法会使用分布式缓存
@Cacheable(cacheNames = CacheNames.CACHE_12HOUR)
public Config getAllValidateConfig2() {
}

核心实现方法

核心其实就是实现 org.springframework.cache.CacheManager接口与继承org.springframework.cache.support.AbstractValueAdaptingCache,在Spring缓存框架下实现缓存的读与写。

RedisCaffeineCacheManager实现CacheManager 接口

RedisCaffeineCacheManager.class 主要来管理缓存实例,根据不同的 CacheNames 生成对应的缓存管理bean,然后放入一个map中。

package com.axin.idea.rediscaffeinecachestarter.support;

import com.axin.idea.rediscaffeinecachestarter.CacheRedisCaffeineProperties;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.stats.CacheStats;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.CollectionUtils; import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit; @Slf4j
public class RedisCaffeineCacheManager implements CacheManager { private final Logger logger = LoggerFactory.getLogger(RedisCaffeineCacheManager.class); private static ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<String, Cache>(); private CacheRedisCaffeineProperties cacheRedisCaffeineProperties; private RedisTemplate<Object, Object> stringKeyRedisTemplate; private boolean dynamic = true; private Set<String> cacheNames;
{
cacheNames = new HashSet<>();
cacheNames.add(CacheNames.CACHE_15MINS);
cacheNames.add(CacheNames.CACHE_30MINS);
cacheNames.add(CacheNames.CACHE_60MINS);
cacheNames.add(CacheNames.CACHE_180MINS);
cacheNames.add(CacheNames.CACHE_12HOUR);
}
public RedisCaffeineCacheManager(CacheRedisCaffeineProperties cacheRedisCaffeineProperties,
RedisTemplate<Object, Object> stringKeyRedisTemplate) {
super();
this.cacheRedisCaffeineProperties = cacheRedisCaffeineProperties;
this.stringKeyRedisTemplate = stringKeyRedisTemplate;
this.dynamic = cacheRedisCaffeineProperties.isDynamic();
} //——————————————————————— 进行缓存工具 ——————————————————————
/**
* 清除所有进程缓存
*/
public void clearAllCache() {
stringKeyRedisTemplate.convertAndSend(cacheRedisCaffeineProperties.getRedis().getTopic(), new CacheMessage(null, null));
} /**
* 返回所有进程缓存(二级缓存)的统计信息
* result:{"缓存名称":统计信息}
* @return
*/
public static Map<String, CacheStats> getCacheStats() {
if (CollectionUtils.isEmpty(cacheMap)) {
return null;
} Map<String, CacheStats> result = new LinkedHashMap<>();
for (Cache cache : cacheMap.values()) {
RedisCaffeineCache caffeineCache = (RedisCaffeineCache) cache;
result.put(caffeineCache.getName(), caffeineCache.getCaffeineCache().stats());
}
return result;
} //—————————————————————————— core —————————————————————————
@Override
public Cache getCache(String name) {
Cache cache = cacheMap.get(name);
if(cache != null) {
return cache;
}
if(!dynamic && !cacheNames.contains(name)) {
return null;
} cache = new RedisCaffeineCache(name, stringKeyRedisTemplate, caffeineCache(name), cacheRedisCaffeineProperties);
Cache oldCache = cacheMap.putIfAbsent(name, cache);
logger.debug("create cache instance, the cache name is : {}", name);
return oldCache == null ? cache : oldCache;
} @Override
public Collection<String> getCacheNames() {
return this.cacheNames;
} public void clearLocal(String cacheName, Object key) {
//cacheName为null 清除所有进程缓存
if (cacheName == null) {
log.info("清除所有本地缓存");
cacheMap = new ConcurrentHashMap<>();
return;
} Cache cache = cacheMap.get(cacheName);
if(cache == null) {
return;
} RedisCaffeineCache redisCaffeineCache = (RedisCaffeineCache) cache;
redisCaffeineCache.clearLocal(key);
} /**
* 实例化本地一级缓存
* @param name
* @return
*/
private com.github.benmanes.caffeine.cache.Cache<Object, Object> caffeineCache(String name) {
Caffeine<Object, Object> cacheBuilder = Caffeine.newBuilder();
CacheRedisCaffeineProperties.CacheDefault cacheConfig;
switch (name) {
case CacheNames.CACHE_15MINS:
cacheConfig = cacheRedisCaffeineProperties.getCache15m();
break;
case CacheNames.CACHE_30MINS:
cacheConfig = cacheRedisCaffeineProperties.getCache30m();
break;
case CacheNames.CACHE_60MINS:
cacheConfig = cacheRedisCaffeineProperties.getCache60m();
break;
case CacheNames.CACHE_180MINS:
cacheConfig = cacheRedisCaffeineProperties.getCache180m();
break;
case CacheNames.CACHE_12HOUR:
cacheConfig = cacheRedisCaffeineProperties.getCache12h();
break;
default:
cacheConfig = cacheRedisCaffeineProperties.getCacheDefault();
}
long expireAfterAccess = cacheConfig.getExpireAfterAccess();
long expireAfterWrite = cacheConfig.getExpireAfterWrite();
int initialCapacity = cacheConfig.getInitialCapacity();
long maximumSize = cacheConfig.getMaximumSize();
long refreshAfterWrite = cacheConfig.getRefreshAfterWrite(); log.debug("本地缓存初始化:");
if (expireAfterAccess > 0) {
log.debug("设置本地缓存访问后过期时间,{}秒", expireAfterAccess);
cacheBuilder.expireAfterAccess(expireAfterAccess, TimeUnit.SECONDS);
}
if (expireAfterWrite > 0) {
log.debug("设置本地缓存写入后过期时间,{}秒", expireAfterWrite);
cacheBuilder.expireAfterWrite(expireAfterWrite, TimeUnit.SECONDS);
}
if (initialCapacity > 0) {
log.debug("设置缓存初始化大小{}", initialCapacity);
cacheBuilder.initialCapacity(initialCapacity);
}
if (maximumSize > 0) {
log.debug("设置本地缓存最大值{}", maximumSize);
cacheBuilder.maximumSize(maximumSize);
}
if (refreshAfterWrite > 0) {
cacheBuilder.refreshAfterWrite(refreshAfterWrite, TimeUnit.SECONDS);
}
cacheBuilder.recordStats();
return cacheBuilder.build();
}
}

RedisCaffeineCache 继承 AbstractValueAdaptingCache

核心是get方法与put方法。

package com.axin.idea.rediscaffeinecachestarter.support;

import com.axin.idea.rediscaffeinecachestarter.CacheRedisCaffeineProperties;
import com.github.benmanes.caffeine.cache.Cache;
import lombok.Getter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.support.AbstractValueAdaptingCache;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.StringUtils; import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock; public class RedisCaffeineCache extends AbstractValueAdaptingCache { private final Logger logger = LoggerFactory.getLogger(RedisCaffeineCache.class); private String name; private RedisTemplate<Object, Object> redisTemplate; @Getter
private Cache<Object, Object> caffeineCache; private String cachePrefix; /**
* 默认key超时时间 3600s
*/
private long defaultExpiration = 3600; private Map<String, Long> defaultExpires = new HashMap<>();
{
defaultExpires.put(CacheNames.CACHE_15MINS, TimeUnit.MINUTES.toSeconds(15));
defaultExpires.put(CacheNames.CACHE_30MINS, TimeUnit.MINUTES.toSeconds(30));
defaultExpires.put(CacheNames.CACHE_60MINS, TimeUnit.MINUTES.toSeconds(60));
defaultExpires.put(CacheNames.CACHE_180MINS, TimeUnit.MINUTES.toSeconds(180));
defaultExpires.put(CacheNames.CACHE_12HOUR, TimeUnit.HOURS.toSeconds(12));
} private String topic;
private Map<String, ReentrantLock> keyLockMap = new ConcurrentHashMap(); protected RedisCaffeineCache(boolean allowNullValues) {
super(allowNullValues);
} public RedisCaffeineCache(String name, RedisTemplate<Object, Object> redisTemplate,
Cache<Object, Object> caffeineCache, CacheRedisCaffeineProperties cacheRedisCaffeineProperties) {
super(cacheRedisCaffeineProperties.isCacheNullValues());
this.name = name;
this.redisTemplate = redisTemplate;
this.caffeineCache = caffeineCache;
this.cachePrefix = cacheRedisCaffeineProperties.getCachePrefix();
this.defaultExpiration = cacheRedisCaffeineProperties.getRedis().getDefaultExpiration();
this.topic = cacheRedisCaffeineProperties.getRedis().getTopic();
defaultExpires.putAll(cacheRedisCaffeineProperties.getRedis().getExpires());
} @Override
public String getName() {
return this.name;
} @Override
public Object getNativeCache() {
return this;
} @Override
public <T> T get(Object key, Callable<T> valueLoader) {
Object value = lookup(key);
if (value != null) {
return (T) value;
}
//key在redis和缓存中均不存在
ReentrantLock lock = keyLockMap.get(key.toString()); if (lock == null) {
logger.debug("create lock for key : {}", key);
keyLockMap.putIfAbsent(key.toString(), new ReentrantLock());
lock = keyLockMap.get(key.toString());
}
try {
lock.lock();
value = lookup(key);
if (value != null) {
return (T) value;
}
//执行原方法获得value
value = valueLoader.call();
Object storeValue = toStoreValue(value);
put(key, storeValue);
return (T) value;
} catch (Exception e) {
throw new ValueRetrievalException(key, valueLoader, e.getCause());
} finally {
lock.unlock();
}
} @Override
public void put(Object key, Object value) {
if (!super.isAllowNullValues() && value == null) {
this.evict(key);
return;
}
long expire = getExpire();
logger.debug("put:{},expire:{}", getKey(key), expire);
redisTemplate.opsForValue().set(getKey(key), toStoreValue(value), expire, TimeUnit.SECONDS); //缓存变更时通知其他节点清理本地缓存
push(new CacheMessage(this.name, key));
//此处put没有意义,会收到自己发送的缓存key失效消息
// caffeineCache.put(key, value);
} @Override
public ValueWrapper putIfAbsent(Object key, Object value) {
Object cacheKey = getKey(key);
// 使用setIfAbsent原子性操作
long expire = getExpire();
boolean setSuccess;
setSuccess = redisTemplate.opsForValue().setIfAbsent(getKey(key), toStoreValue(value), Duration.ofSeconds(expire)); Object hasValue;
//setNx结果
if (setSuccess) {
push(new CacheMessage(this.name, key));
hasValue = value;
}else {
hasValue = redisTemplate.opsForValue().get(cacheKey);
} caffeineCache.put(key, toStoreValue(value));
return toValueWrapper(hasValue);
} @Override
public void evict(Object key) {
// 先清除redis中缓存数据,然后清除caffeine中的缓存,避免短时间内如果先清除caffeine缓存后其他请求会再从redis里加载到caffeine中
redisTemplate.delete(getKey(key)); push(new CacheMessage(this.name, key)); caffeineCache.invalidate(key);
} @Override
public void clear() {
// 先清除redis中缓存数据,然后清除caffeine中的缓存,避免短时间内如果先清除caffeine缓存后其他请求会再从redis里加载到caffeine中
Set<Object> keys = redisTemplate.keys(this.name.concat(":*"));
for (Object key : keys) {
redisTemplate.delete(key);
} push(new CacheMessage(this.name, null));
caffeineCache.invalidateAll();
} /**
* 取值逻辑
* @param key
* @return
*/
@Override
protected Object lookup(Object key) {
Object cacheKey = getKey(key);
Object value = caffeineCache.getIfPresent(key);
if (value != null) {
logger.debug("从本地缓存中获得key, the key is : {}", cacheKey);
return value;
} value = redisTemplate.opsForValue().get(cacheKey); if (value != null) {
logger.debug("从redis中获得值,将值放到本地缓存中, the key is : {}", cacheKey);
caffeineCache.put(key, value);
}
return value;
} /**
* @description 清理本地缓存
*/
public void clearLocal(Object key) {
logger.debug("clear local cache, the key is : {}", key);
if (key == null) {
caffeineCache.invalidateAll();
} else {
caffeineCache.invalidate(key);
}
} //————————————————————————————私有方法—————————————————————————— private Object getKey(Object key) {
String keyStr = this.name.concat(":").concat(key.toString());
return StringUtils.isEmpty(this.cachePrefix) ? keyStr : this.cachePrefix.concat(":").concat(keyStr);
} private long getExpire() {
long expire = defaultExpiration;
Long cacheNameExpire = defaultExpires.get(this.name);
return cacheNameExpire == null ? expire : cacheNameExpire.longValue();
} /**
* @description 缓存变更时通知其他节点清理本地缓存
*/
private void push(CacheMessage message) {
redisTemplate.convertAndSend(topic, message);
} }

关于分布式本地缓存失效

现在的线上生产的都是多个节点,如果本节点的缓存失效了,是需要通过中间件来通知其他节点失效消息的。本组件考虑到学习分享让大家引入的依赖少点,就直接通过 redis 来发送消息了,实际生产过程中换成成熟的消息中间件(kafka、RocketMQ)来做通知更为稳妥。

分布式二级缓存组件实战(Redis+Caffeine实现)的更多相关文章

  1. Mybatis自定义分布式二级缓存实现与遇到的一些问题解决方案!

    先说两句: 我们都知道Mybatis缓存分两类: 一级缓存(同一个Session会话内) & 二级缓存(基于HashMap实现的以 namespace为范围的缓存) 今天呢, 我们不谈一级缓存 ...

  2. Mybatis的二级缓存、使用Redis做二级缓存

    目录 什么是二级缓存? 1. 开启二级缓存 如何使用二级缓存: userCache和flushCache 2. 使用Redis实现二级缓存 如何使用 3. Redis二级缓存源码分析 什么是二级缓存? ...

  3. 分布式限流组件-基于Redis的注解支持的Ratelimiter

    原文:https://juejin.im/entry/5bd491c85188255ac2629bef?utm_source=coffeephp.com 在分布式领域,我们难免会遇到并发量突增,对后端 ...

  4. 基于Spring Cache实现二级缓存(Caffeine+Redis)

    一.聊聊什么是硬编码使用缓存? 在学习Spring Cache之前,笔者经常会硬编码的方式使用缓存. 我们来举个实际中的例子,为了提升用户信息的查询效率,我们对用户信息使用了缓存,示例代码如下: @A ...

  5. Mybatis整合(Redis、Ehcache)实现二级缓存

    目的: Mybatis整合Ehcache实现二级缓存 Mybatis整合Redis实现二级缓存 Mybatis整合ehcache实现二级缓存 ssm中整合ehcache 在POM中导入相关依赖 < ...

  6. Spring Boot 入门(十):集成Redis哨兵模式,实现Mybatis二级缓存

    本片文章续<Spring Boot 入门(九):集成Quartz定时任务>.本文主要基于redis实现了mybatis二级缓存.较redis缓存,mybaits自带缓存存在缺点(自行谷歌) ...

  7. hibernate(九) 二级缓存和事务级别详讲

    序言 这算是hibernate的最后一篇文章了,下一系列会讲解Struts2的东西,然后说完Struts2,在到Spring,然后在写一个SSH如何整合的案例.之后就会在去讲SSM,在之后我自己的个人 ...

  8. hibernate二级缓存ehcache

    与Session相对的是,SessionFactory也提供了相应的缓存机制.SessionFactory缓存可以依据功能和目的的不同而划分为内置缓存和外置缓存. SessionFactory的内置缓 ...

  9. Hibernate二级缓存原理

    缓存:缓存是什么,解决什么问题? 位于速度相差较大的两种硬件/软件之间的,用于协调两者数据传输速度差异的结构,均可称之为缓存Cache.缓存目的:让数据更接近于应用程序,协调速度不匹配,使访问速度更快 ...

  10. Hibernate学习(九)———— 二级缓存和事务级别详讲

    序言 这算是hibernate的最后一篇文章了,下一系列会讲解Struts2的东西,然后说完Struts2,在到Spring,然后在写一个SSH如何整合的案例.之后就会在去讲SSM,在之后我自己的个人 ...

随机推荐

  1. No enclosing instance of type Hello is accessible

    1.static 关键字 修饰的成员被所有对象共享(包括成员变量和方法). 修饰的成员优先于对象存在. 存储于方法区(共享数据区)的静态区中. 静态方法只能访问静态成员. 静态方法中不可以使用this ...

  2. iOS发展- backBarButtonItem 颜色/文字修改

    iOS7之后. 默认返回button字体颜色为蓝色, 在父母的陈列柜VC(老界面)的title 假设做出改变, 通过下面的方法可以: 1. 更改字体颜色 (1) 在plist里面, 加View con ...

  3. NSString之Format

    三点用法总结: 类型转换:基本类型到对象类型 需要调用NSString的fotmat方法 NSString  *location = [NSString  stringWithFormat:@&quo ...

  4. 异步 HttpContext.Current实现取值的方法(解决异步Application,Session,Cache...等失效的问题)

    在一个项目中,为了系统执行效率更快,把一个经常用到的数据库表通过dataset放到Application中,发现在异步实现中每一次都会出现HttpContext.Current为null的异常,后来在 ...

  5. MySQL使用LOAD DATA LOCAL INFILE报错

    在windows系统的MySQL8.0中尝试执行以下语句时报错 mysql> LOAD DATA LOCAL INFILE '/path/filename' INTO TABLE tablena ...

  6. DAX2012 R3安装

    安装程序跟DAX2009大同小异,不过这验证需要的组件也太多了,简直是.NET Framework大阅兵啊,各种版本都需要安装,特别是VC++从2008一直装到2012,有点崩溃... DEMO数据的 ...

  7. 20145311王亦徐《网络对抗技术》MAL_逆向与Bof基础

    20145311王亦徐<网络对抗技术>MAL_逆向与Bof基础 实践目标 运行一个可执行文件,通过逆向或者Bof技术执行原本不应该执行的代码片段采用的两种方法: 1.利用foo函数的Bof ...

  8. p2093 [国家集训队]JZPFAR

    传送门 分析 首先给大家推荐一个非常好的KDTree笔记 here 此题就是y9ong优先队列维护距离最远的k个,最后输出队首元素即可 估价函数就是max和min两点到 询问点的最远距离 代码 #in ...

  9. java多线程-ReadWriteLock

    大纲: ReadWriteLock接口 ReentrantReadWriteLock ReentrantReadWriteLock使用 一.ReadWriteLock public interface ...

  10. 解锁CHM文件

    刚在网上下载了CHM格式的文件,打开之后,右侧部分一片空白. 原因:可能是,系统针对来源不明文件的一种保存措施吧 解决方法: 1.选中文件,右键打开属性对话框,在 “常规”选项卡中 勾选 [解除锁定] ...