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

- 【显示详情】:查看缓存下的所有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'
这样就完成了缓存的刷新。

封面
