实现简单的JAVA多级缓存(Caffeine + redis)

时间:2024-06-01 09:56:21

需求


好久没写文章啦,之前写的文章到现在也没有收尾,没办法,时间不多啊,旧坑没有填完就开始开新坑,最近项目组长说实现一个多级缓存,通常我们喜欢把cache放到redis里,可以把访问速度提升,但是redis也算是远程服务器,会有IO时间的开销,如果我们把缓存放在本地内存,性能能进一步提升,这也就带出了二级缓存概念。有人说为什么不把cache直接放到本地,如果是单机没问题,但是集群环境下还是需要两级缓存的配合。

缓存的获取与更新


实现简单的JAVA多级缓存(Caffeine + redis)

随便画的,简单来说,工具先从 一级缓存取起,也就是本地缓存,如果缓存命中,就可以直接返回;如果一级缓存没有,就会去redis找,再不行就走传统业务逻辑。这种缓存比单一的缓存工具比起来具有以下特点:

  • 适应集群环境
  • 比单一Redis缓存性能更高
  • 设计了三级数据层(包括业务直接取数据)分摊了请求量,降低数据库压力

但跟所有缓存框架一样,缓存只适合非关键数据,因为缓存更新多少具有延迟性。

缓存的更新比获取更复杂一点,它存在多种情况:

  • 当一级缓存失效时(获取不到),得益于Caffeine本身提供的功能,你能指定方法去redis获取并更新到一级缓存中。
  • 当业务数据发生改变,调用delete方法直接清除一/二级缓存。(这种方法现在比较暴力,后期可以完善)
  • 当新建缓存时,先Redis 存入缓存,再通过Redis 的消息订阅机制 让本地每台机器接收最新的cache。

代码实现


环境的话比较通用的 spring + redission + Caffeine

  1. 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);
    }
  1. 缓存工具类
@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;
}

不足与改进

  1. 因为刚开始写,这个工具类能满足基本的二级缓存需求,但其实改进的地方还有很多,比如根据实际情况利用Caffeine本身的淘汰策略进行cache更新与删除,而不是直接设置失效时间,但这种改进要考虑一/二级缓存的一致性,以免缓存出现问题
  2. 应用重启后一级缓存处于全部失效状态,如果全部从redis取会有读取压力;现有一级缓存也是被动接收新cache,一级缓存的命中率较低,这里可以考虑redis的空间消息通知。