需求
好久没写文章啦,之前写的文章到现在也没有收尾,没办法,时间不多啊,旧坑没有填完就开始开新坑,最近项目组长说实现一个多级缓存,通常我们喜欢把cache放到redis里,可以把访问速度提升,但是redis也算是远程服务器,会有IO时间的开销,如果我们把缓存放在本地内存,性能能进一步提升,这也就带出了二级缓存概念。有人说为什么不把cache直接放到本地,如果是单机没问题,但是集群环境下还是需要两级缓存的配合。
缓存的获取与更新
随便画的,简单来说,工具先从 一级缓存取起,也就是本地缓存,如果缓存命中,就可以直接返回;如果一级缓存没有,就会去redis找,再不行就走传统业务逻辑。这种缓存比单一的缓存工具比起来具有以下特点:
- 适应集群环境
- 比单一Redis缓存性能更高
- 设计了三级数据层(包括业务直接取数据)分摊了请求量,降低数据库压力
但跟所有缓存框架一样,缓存只适合非关键数据,因为缓存更新多少具有延迟性。
缓存的更新比获取更复杂一点,它存在多种情况:
- 当一级缓存失效时(获取不到),得益于Caffeine本身提供的功能,你能指定方法去redis获取并更新到一级缓存中。
- 当业务数据发生改变,调用delete方法直接清除一/二级缓存。(这种方法现在比较暴力,后期可以完善)
- 当新建缓存时,先Redis 存入缓存,再通过Redis 的消息订阅机制 让本地每台机器接收最新的cache。
代码实现
环境的话比较通用的 spring + redission + Caffeine
- redission 要先在spring配置一下,里面的配置文件根据实际情况自己生成填入:
@Bean
RedissonClient redissonClient() {
Config config = new Config();
SingleServerConfig serverConfig = config.useSingleServer()
.setAddress("redis://" + redissonProperties.getHost() + ":" + redissonProperties.getPort())
.setTimeout(redissonProperties.getTimeout())
.setConnectionPoolSize(redissonProperties.getConnectionPoolSize())
.setConnectionMinimumIdleSize(redissonProperties.getConnectionMinimumIdleSize());
if (StringUtils.isNotBlank(redissonProperties.getPassword())) {
serverConfig.setPassword(redissonProperties.getPassword());
}
return Redisson.create(config);
}
- 缓存工具类
@Component
@Slf4j
public class SecondLevelCacheUtil {
//为避免key冲突,key的设置应该规范
public static final String BRAINTRAIN = "braintrain:";
public static final String REDIS_TOPIC_PUT = BRAINTRAIN + "putTopic:";
public static final String REDIS_TOPIC_DELETE = BRAINTRAIN + "deleteTopic:";
@Autowired
CustomsProperties customsProperties;
@Autowired
RedissonClient redissonClient;
private Cache<String, String> cache;
@PostConstruct
void init() {
log.info("SecondLevelCacheUtil init");
cache = Caffeine.newBuilder()
.expireAfterWrite(customsProperties.getRedisFirstCacheTime(), TimeUnit.MILLISECONDS)
.build();
// 监听删除缓存事件,及时清除本地一级缓存
RTopic<String> deleteTopic = redissonClient.getTopic(REDIS_TOPIC_DELETE + customsProperties.getAppName());
deleteTopic.addListener((channel, message) -> {
log.info("first cache delete {}", message);
cache.invalidate(message);
});
// 监听新增缓存事件,及时新增本地一级缓存
RTopic<String> putTopic = redissonClient.getTopic(REDIS_TOPIC_PUT + customsProperties.getAppName());
putTopic.addListener((channel, message) -> {
if (StringUtils.isNotBlank(message)) {
log.info("first cache put {}", message);
String[] split = message.split("\\|\\|");
cache.put(split[0], split[1]);
}
});
log.info("SecondLevelCacheUtil done");
}
public <T> T get(String key, Class<T> clazz) {
try {
if (StringUtils.isBlank(key) || !key.startsWith(BRAINTRAIN)) {
return null;
}
//一级缓存取不到时,调用getByRedis()取二级缓存,由Caffeine原生提供机制
String json = cache.get(key, k -> getByRedis(k));
if (StringUtils.isNotBlank(json)) {
return JSON.parseObject(json, clazz);
}
} catch (Exception e) {
log.warn("SecondLevelCacheUtil get e={}", e);
}
return null;
}
public void delete(String key) {
try {
if (StringUtils.isBlank(key) || !key.startsWith(BRAINTRAIN)) {
return;
}
RBucket<Object> bucket = redissonClient.getBucket(key);
bucket.deleteAsync();
// 分发"删除"主题,让本地一级缓存接收通知
RTopic<String> topic = redissonClient.getTopic(REDIS_TOPIC_DELETE + customsProperties.getAppName());
long clientsReceivedMessage = topic.publish(key);
log.info("delete first/second cache ,key{}, {}个实例接收到信息", key, clientsReceivedMessage);
} catch (Exception e) {
log.warn("SecondLevelCacheUtil delete e={}", e);
}
}
public void set(String key, Object value) {
try {
if (StringUtils.isNotBlank(key) && !Objects.isNull(value)) {
if (!key.startsWith(BRAINTRAIN)) {
return;
}
RBucket<String> bucket = redissonClient.getBucket(key);
String valueStr = JSONObject.toJSONString(value);
bucket.setAsync(valueStr, customsProperties.getRedisSecondCacheTime(), TimeUnit.MILLISECONDS);
RTopic<String> topic = redissonClient.getTopic(REDIS_TOPIC_PUT + customsProperties.getAppName());
// 分发"新增"主题,让本地一级缓存接收通知
long clientsReceivedMessage = topic.publish(key + "||" + valueStr);
log.info("after set , key: {} value: {}, {}个实例接收到信息", key, valueStr, clientsReceivedMessage);
}
} catch (Exception e) {
log.warn("SecondLevelCacheUtil set e={}", e);
}
}
public void setIfAbsent(String key, Object value, Class clazz) {
if (null == get(key, clazz)) {
set(key, value);
}
}
//取二级缓存的方法
private String getByRedis(String key) {
try {
log.info("缓存不存在或过期,调用了redis获取缓存key的值");
if (StringUtils.isNotBlank(key)) {
RBucket<String> bucket = redissonClient.getBucket(key);
String result = bucket.get();
RTopic<String> topic = redissonClient.getTopic(REDIS_TOPIC_PUT + customsProperties.getAppName());
long clientsReceivedMessage = topic.publish(key + "||" + result);
log.info("first cache null, key: {} value: {}, {}个实例接收到信息", key, result, clientsReceivedMessage);
return result;
}
} catch (Exception e) {
log.warn("SecondLevelCacheUtil getByRedis e={}", e);
}
return null;
}
}
3.部分设置项
@Component
@Data
public class CustomsProperties {
//一级缓存失效时间
@Value("${braintrain.redisFirstCacheTime: 180000}")
Long redisFirstCacheTime;
//二级缓存失效时间
@Value("${braintrain.redisSecondCacheTime: 2592000000}")
Long redisSecondCacheTime;
}
不足与改进
- 因为刚开始写,这个工具类能满足基本的二级缓存需求,但其实改进的地方还有很多,比如根据实际情况利用Caffeine本身的淘汰策略进行cache更新与删除,而不是直接设置失效时间,但这种改进要考虑一/二级缓存的一致性,以免缓存出现问题
- 应用重启后一级缓存处于全部失效状态,如果全部从redis取会有读取压力;现有一级缓存也是被动接收新cache,一级缓存的命中率较低,这里可以考虑redis的空间消息通知。