如何优雅地使用本地缓存?

在实际项目中,我们经常会用到本地缓存,但是往往缺少对缓存使用情况的观测。今天推荐一种用法,让你更优雅地使用本地缓存。

项目地址:github.com/studeyang/t...

一、管理本地缓存

缓存管理首页如下:

  • 【显示详情】:查看缓存下的所有key,value
  • 【清空缓存】:清空本地缓存,再次访问会重新加载新的缓存数据

接下来介绍一下如何让项目接入上图的缓存管理页。

1.1 接入本地缓存组件

1、添加依赖

xml 复制代码
<dependency>
    <groupId>io.github.studeyang</groupId>
    <artifactId>toolkit-spring-boot-starter</artifactId>
</dependency>
<dependency>
    <groupId>io.github.studeyang</groupId>
    <artifactId>toolkit-cache</artifactId>
</dependency>

2、开启功能

less 复制代码
@SpringBootApplication
@EnableCache
public class WebApplication {
    public static void main(String[] args) {
        SpringApplication.run(WebApplication.class, args);
    }
}

3、应用配置

yaml 复制代码
################################################
# application.yml 配置Redis以便刷新所有节点的缓存
################################################
spring:
  redis:
    client-name: example
    cluster.nodes: ${redis.nodes}
    password: ${redis.password}
    cluster.max-redirects: 3
    jedis.pool.maxIdle: 50
    jedis.pool.maxActive: 50
    jedis.pool.minIdle: 10
    jedis.pool.maxWait: 3000
    timeout: 3000

1.2 实现缓存类

缓存实现类需要继承AbstractLoadingCache,具体代码实现如下:

typescript 复制代码
@Service
public class UserCacheImpl extends AbstractLoadingCache<String, UserEntity> {
​
    public UserCacheImpl() {
        // 最大缓存条数
        setMaximumSize(5);
        setTimeUnit(TimeUnit.DAYS);
        setExpireAfterWriteDuration(37);
    }
​
    @Override
    public UserEntity get(String key) {
        try {
            return super.getValue(key);
        } catch (Exception e) {
            return null;
        }
    }
​
    @Override
    public UserEntity loadData(String key) {
        // 模拟从数据库读取
        UserEntity user = new UserEntity();
        user.setId(key);
        user.setUserName("人员" + key);
        return user;
    }
}

由于缓存采用的是懒加载策略,我们在程序启动时加载一下缓存:

kotlin 复制代码
@Component
public class CacheLoader implements ApplicationRunner {
​
    @Autowired
    private UserCacheImpl userCacheImpl;
​
    @Override
    public void run(ApplicationArguments args) {
        System.out.println("userCacheImpl: " + userCacheImpl.get("01"));
        System.out.println("userCacheImpl: " + userCacheImpl.get("02"));
    }
}

程序启动后,访问:http://localhost:8080/cache/getAllCacheStats

  • 在缓存首页可以看到 Redis 的发布订单的 channel(下文会详细说明);
  • 点击【显示详情】,可以看到缓存的详情页,支持查询key。

二、刷新缓存

2.1 单节点刷新

本文是基于 Guava 实现缓存管理的,Guava 提供了让缓存失效的接口 com.google.common.cache.Cache,接口如下:

csharp 复制代码
public interface Cache<K, V> {
    // 省略其它方法....
    void invalidateAll();
}

我们也可以刷新缓存,Guava 提供了刷新缓存的接口 com.google.common.cache.LoadingCache#refresh,接口如下:

csharp 复制代码
public interface LoadingCache<K, V> extends Cache<K, V>, Function<K, V> {
    // 省略其它方法....
    void refresh(K key);
}

以上接口只能刷新单个节点的缓存,对于分布式应用,我们该如何处理呢?

2.2 分布式刷新

Redis 提供了发布订单的功能,图示如下:

本文基于这个功能,实现分布式缓存刷新。

Redis 发布订阅更多细节可参考:www.runoob.com/redis/redis...

首先,发布者实现如下:

typescript 复制代码
import io.github.toolkit.cache.dto.GuavaCacheSubscribeDto;
import io.github.toolkit.cache.pubsub.IGuavaCachePublisher;
import org.springframework.data.redis.core.RedisTemplate;
​
public class RedisClusterCachePublisher implements IGuavaCachePublisher {
    private final RedisTemplate<Object, Object> redisTemplate;
    private final String channel;
​
    public RedisClusterCachePublisher(RedisTemplate<Object, Object> redisTemplate, String channel) {
        this.redisTemplate = redisTemplate;
        this.channel = channel;
    }
​
    @Override
    public void publish(String cacheName, Object cacheKey) {
        GuavaCacheSubscribeDto dto = new GuavaCacheSubscribeDto();
        dto.setCacheName(cacheName);
        dto.setCacheKey(JSON.toJSONString(cacheKey));
        redisTemplate.convertAndSend(channel, FastJSONHelper.serialize(dto));
    }
}

订阅者实现如下:

ini 复制代码
import io.github.toolkit.cache.dto.GuavaCacheSubscribeDto;
import io.github.toolkit.cache.guava.GuavaCacheManager;
import io.github.toolkit.cache.pubsub.ISubscribeListener;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;

public class RedisClusterCacheListener implements ISubscribeListener<GuavaCacheSubscribeDto>, InitializingBean {

    private RedisMessageListenerContainer listenerContainer;
    private final RedisTemplate<Object, Object> redisTemplate;
    private final String channel;

    public RedisClusterCacheListener(RedisTemplate<Object, Object> redisTemplate, String channel) {
        this.redisTemplate = redisTemplate;
        this.channel = channel;
    }

    @Override
    public void afterPropertiesSet() {
        ChannelTopic topic = new ChannelTopic(channel);
        MessageListener messageListener = (message, pattern) -> {
            String body = (String) redisTemplate.getDefaultSerializer().deserialize(message.getBody());
            GuavaCacheSubscribeDto dto = FastJSONHelper.deserialize(body, GuavaCacheSubscribeDto.class);
            this.onMessage(dto);
        };
        listenerContainer = new RedisMessageListenerContainer();
        listenerContainer.setConnectionFactory(redisTemplate.getConnectionFactory());
        listenerContainer.addMessageListener(messageListener, topic);
        listenerContainer.afterPropertiesSet();
    }

    @Override
    public void onMessage(GuavaCacheSubscribeDto message) {
        GuavaCacheManager.resetCache(message.getCacheName());
    }
}

重置缓存:

typescript 复制代码
import io.github.toolkit.cache.util.SpringContextUtil;

import java.util.Date;
import java.util.Map;

public class GuavaCacheManager {
    private static Map<String, AbstractLoadingCache> cacheNameToObjectMap = null;

    private static Map<String, AbstractLoadingCache> getCacheMap() {
        if (cacheNameToObjectMap == null) {
            cacheNameToObjectMap = SpringContextUtil.getBeanOfType(AbstractLoadingCache.class);
        }
        return cacheNameToObjectMap;
    }

    private static AbstractLoadingCache<Object, Object> getCacheByName(String cacheName) {
        return getCacheMap().get(cacheName);
    }

    public static void resetCache(String cacheName) {
        AbstractLoadingCache<Object, Object> cache = getCacheByName(cacheName);
        cache.getCache().invalidateAll();
        cache.setResetTime(new Date());
    }
}

这里为了以便演示,单独提供一个接口出来:

less 复制代码
@RestController
public class ExampleController {
    @Autowired
    private IGuavaCachePublisher guavaCachePublisher;
    
    @GetMapping("/example/cache")
    public String refreshCache(@RequestParam String cacheName, @RequestParam String cacheKey) {
        guavaCachePublisher.publish(cacheName, cacheKey);
        return "success";
    }
}

接口调用接口:

vbnet 复制代码
curl --location --request GET 'http://localhost:8080/example/cache?cacheName=sendDictionaryCacheService&cacheKey=01' \
--header 'Accept: */*' \
--header 'Host: localhost:8080' \
--header 'Connection: keep-alive'

这样就完成了缓存的刷新。

封面

更多文章

相关推荐
神奇的程序员31 分钟前
从已损坏的备份中拯救数据
运维·后端·前端工程化
oden1 小时前
AI服务商切换太麻烦?一个AI Gateway搞定监控、缓存和故障转移(成本降40%)
后端·openai·api
李慕婉学姐2 小时前
【开题答辩过程】以《基于Android的出租车运行监测系统设计与实现》为例,不知道这个选题怎么做的,不知道这个选题怎么开题答辩的可以进来看看
java·后端·vue
m0_740043732 小时前
SpringBoot05-配置文件-热加载/日志框架slf4j/接口文档工具Swagger/Knife4j
java·spring boot·后端·log4j
招风的黑耳3 小时前
我用SpringBoot撸了一个智慧水务监控平台
java·spring boot·后端
Miss_Chenzr3 小时前
Springboot优卖电商系统s7zmj(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库·spring boot·后端
期待のcode3 小时前
Springboot核心构建插件
java·spring boot·后端
2501_921649493 小时前
如何获取美股实时行情:Python 量化交易指南
开发语言·后端·python·websocket·金融
serendipity_hky4 小时前
【SpringCloud | 第5篇】Seata分布式事务
分布式·后端·spring·spring cloud·seata·openfeign
五阿哥永琪4 小时前
Spring Boot 中自定义线程池的正确使用姿势:定义、注入与最佳实践
spring boot·后端·python