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

需求


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

缓存的获取与更新


在这里插入图片描述

随便画的,简单来说,工具先从 一级缓存取起,也就是本地缓存,如果缓存命中,就可以直接返回;如果一级缓存没有,就会去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的空间消息通知。

2019.1.3
我已经将工具组件化,详情看新博文
spring之我见–spring的组件化(以logging日志初始化为例)

### 三级标题:多级缓存数据一致性解决方案 在分布式系统中,使用 CaffeineRedis 构建的多级缓存架构时,需要特别关注本地缓存Caffeine)与远程缓存Redis)之间的数据一致性问题。由于本地缓存是基于服务实例的内存存储,当多个服务节点共享同一个 Redis 缓存时,单个节点更新或删除缓存可能导致其他节点的本地缓存出现不一致的情况。 为了解决这一问题,可以采用以下策略: #### 使用 Redis 发布订阅机制实现缓存同步 一种常见的解决方案是通过 Redis 的发布订阅模式来通知所有服务节点关于缓存更新的消息。每当某个服务节点修改了 Redis 中的数据,它会向 Redis 发送一条消息,所有订阅该频道的服务节点接收到消息后,根据消息内容清除或刷新本地缓存中的相应条目。 ```java // 示例代码 - Redis 发布订阅用于缓存同步 public class CacheSyncService { private final RedisTemplate<String, Object> redisTemplate; public CacheSyncService(RedisTemplate<String, Object> redisTemplate) { this.redisTemplate = redisTemplate; } // 当缓存被更新时,发布消息到指定的频道 public void publishCacheUpdate(String channel, String key) { redisTemplate.convertAndSend(channel, key); } // 订阅者监听频道并处理收到的消息 public void subscribeToChannel(MessageListenerAdapter listenerAdapter, String channel) { redisTemplate.getConnectionFactory().getConnection().subscribe(listenerAdapter, channel.getBytes()); } } ``` 这种方法能够有效确保各个服务节点上的本地缓存保持最新状态,同时减少了不必要的全量缓存刷新操作[^1]。 #### 在注解拦截器中集成缓存同步逻辑 另一种方法是在自定义的缓存注解拦截器中直接集成缓存同步逻辑。例如,在执行 PUT 或 DELETE 操作时,除了更新 Redis 缓存外,还需要主动清理本地缓存,并通过某种方式通知其他服务节点进行相应的缓存清理。 ```java // 示例代码 - 注解拦截器中的缓存同步逻辑 @Aspect @Component public class ZbtechCacheAspect { private final RedisTemplate<String, Object> redisTemplate; private final CaffeineCacheManager caffeineCacheManager; public ZbtechCacheAspect(RedisTemplate<String, Object> redisTemplate, CaffeineCacheManager caffeineCacheManager) { this.redisTemplate = redisTemplate; this.caffeineCacheManager = caffeineCacheManager; } @Around("@annotation(zbtechCache)") public Object handleCacheOperation(ProceedingJoinPoint point, ZbtechCache zbtechCache) throws Throwable { String realKey = generateRealKey(point.getSignature(), zbtechCache.key()); if (zbtechCache.type() == ZbtechCache.CacheType.PUT) { Object object = point.proceed(); redisTemplate.opsForValue().set(realKey, object, zbtechCache.expireTime(), TimeUnit.SECONDS); caffeineCacheManager.getCache("default").put(realKey, object); // 通知其他服务清空本地缓存 caffeineCacheManager.push(realKey); return object; } else if (zbtechCache.type() == ZbtechCache.CacheType.DELETE) { if (StringUtils.isNotBlank(zbtechCache.key())) { redisTemplate.delete(realKey); // 清除本地缓存 caffeineCacheManager.getCache("default").invalidate(realKey); // 通知其他服务清空本地缓存 caffeineCacheManager.push(realKey); } else { // 批量删除 Set<String> keys = findKeysByPattern(zbtechCache.keyPattern()); if (!keys.isEmpty()) { redisTemplate.delete(keys); // 清除本地缓存 caffeineCacheManager.getCache("default").invalidateAll(keys); // 通知其他服务清空本地缓存 caffeineCacheManager.push(keys); } } return point.proceed(); } // 其他类型的操作... } // 辅助方法生成实际键名、查找匹配键等 } ``` 此方案允许开发者以声明式的方式控制缓存行为,并且能够在不影响业务逻辑的情况下自动处理缓存同步问题[^3]。 #### 结合 RabbitMQ 实现异步缓存同步 除了 Redis 的发布订阅功能之外,还可以考虑引入 RabbitMQ 来作为消息中间件实现更加灵活可靠的缓存同步机制。这种方式提供了更好的解耦性和可扩展性,同时也支持更复杂的路由规则和错误处理策略。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值