目录

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

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

项目地址: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'

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

封面

更多文章

本文是转载文章,点击查看原文
如有侵权,请联系 xyy@jishuzhan.net 删除
相关推荐
京东云开发者2 分钟前
当系统闹脾气:用「因果推断」哄稳技术的心
后端
陈傲翔2 分钟前
新版 NetworkManager 网络配置立即生效
后端
京东云开发者15 分钟前
从 Java 到 Go:面向对象的巨人与云原生的轻骑兵
后端
37手游后端团队17 分钟前
Go性能优化实战:benchstat工具详解与应用
后端
FreeCultureBoy23 分钟前
常用的测速网站
后端
七月丶38 分钟前
❌ 别再用接口文档开发了!我改用“Mock-First”后爽到飞起
前端·javascript·后端
Code哈哈笑43 分钟前
【Spring Boot】深入解析:#{} 和 ${}
java·spring boot·后端·spring·mybatis
315356691344 分钟前
Optional 全解析:Java 最被低估的空值处理利器
java·前端·后端
七月丶44 分钟前
🗑 写了 2000 行代码,结果自己全删了。
前端·javascript·后端
mazhimazhi1 小时前
Java虚拟机代码是如何一步一步变复杂且难以理解的?
java·后端