RuoYi学习-缓存切换
Base On: RuoYi-Vue-v3.9.2 (若依分离版)
1. 目标
💡 RuoYi框架官方只支持了redis缓存。当部署架构里没有redis服务时,程序就会运行报错。我们需要程序能够切换成本地内存缓存。
在RuoYi框架里添加本地内存缓存的实现。
根据 application.yml 中的配置,切换使用redis缓存,还是本地内存缓存。
2. 方案概述
RuoYi框架中原生的redis缓存,依赖于 RedisCache 实现。见下:
@Autowired
private RedisCache redisCache;
我们需要设计 RedisCacheServiceImpl 代替原生的 RedisCache ,实现redis缓存;设计 CaffeineCacheServiceImpl 来实现内存缓存。
类图见下:
CacheServiceConfig 提供动态注入 CacheService 的能力,它会根据 application.yml 中的 ruoyi.cache.type 确定当前提供的是 RedisCacheServiceImpl 的实现,还是 CaffeineCacheServiceImpl 的实现。
按照此方案,使用缓存的方式变更为:
@Autowired
private CacheService cacheService;
3. 配置设计
application.yml
ruoyi:
cache:
# 缓存类型:redis(Redis缓存)/ caffeine(JVM本地缓存)
type: caffeine
caffeine:
# 初始缓存空间大小
initial-capacity: 100
# 缓存最大条数
maximum-size: 10000
# 最后一次写入后过期时间(秒)
expire-after-write: 3600
当 ruoyi.cache.type 设置为 redis 时,缓存的配置仍然是 spring.data.redis 下的配置。
当 ruoyi.cache.type 设置为 caffeine 时,缓存的配置是 ruoyi.cache.caffeine 下的配置。
4. 实现步骤
4.1 添加Caffeine依赖
在 ruoyi-common 模块的 pom.xml 中添加:
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
[!NOTE]
加到<artifactId>spring-boot-starter-cache</artifactId>依赖的下方就可以了。
其实顺序不重要。
4.2 创建配置属性类
在 ruoyi-common 中新增 CacheProperties.java :
package com.ruoyi.common.config.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 缓存配置属性
*/
@Component
@ConfigurationProperties(prefix = "ruoyi.cache")
public class CacheProperties {
/**
* 缓存类型:redis / caffeine / simple
*/
private String type = "redis";
private Caffeine caffeine = new Caffeine();
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public Caffeine getCaffeine() {
return caffeine;
}
public void setCaffeine(Caffeine caffeine) {
this.caffeine = caffeine;
}
public static class Caffeine {
private int initialCapacity = 100;
private long maximumSize = 10000;
private long expireAfterWrite = 3600;
public int getInitialCapacity() {
return initialCapacity;
}
public void setInitialCapacity(int initialCapacity) {
this.initialCapacity = initialCapacity;
}
public long getMaximumSize() {
return maximumSize;
}
public void setMaximumSize(long maximumSize) {
this.maximumSize = maximumSize;
}
public long getExpireAfterWrite() {
return expireAfterWrite;
}
public void setExpireAfterWrite(long expireAfterWrite) {
this.expireAfterWrite = expireAfterWrite;
}
}
}
[!NOTE]
这个类是为了获取application.yml中的ruoyi.cache下的配置内容。
4.3 定义缓存服务接口
在 ruoyi-common 中新增 CacheService.java :
package com.ruoyi.common.core.cache;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* 缓存服务接口
*
* @author yourname
*/
public interface CacheService {
// ---------- 基本存取操作 ----------
<T> void setCacheObject(final String key, final T value);
<T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit);
<T> T getCacheObject(final String key);
boolean deleteObject(final String key);
boolean deleteObject(final String key, final Object... values);
long deleteObject(final Collection<String> collection);
// ---------- List 操作 ----------
<T> Long setCacheList(final String key, final List<T> dataList);
<T> List<T> getCacheList(final String key);
// ---------- Set 操作 ----------
<T> Long setCacheSet(final String key, final Set<T> dataSet);
<T> Set<T> getCacheSet(final String key);
// ---------- Map 操作 ----------
<T> Long setCacheMap(final String key, final Map<String, T> dataMap);
<T> Map<String, T> getCacheMap(final String key);
<T> T getCacheMapValue(final String key, final String hKey);
// ---------- 分布式锁相关 ----------
Boolean setNX(final String key, final Object value, final Long timeout, final TimeUnit timeUnit);
boolean lock(final String key, final long expire, final TimeUnit unit);
void unlock(final String key);
// ---------- 过期相关 ----------
Boolean expire(final String key, final long timeout, final TimeUnit timeUnit);
Long getExpire(final String key);
boolean hasKey(final String key);
/**
* 获得缓存的基本对象列表(按模式匹配)
*
* @param pattern 字符串前缀/模式,支持 * 通配符
* @return 匹配的键集合
*/
Collection<String> keys(final String pattern);
}
4.4 实现redis缓存
在 ruoyi-common 里新建 RedisCacheServiceImpl.java ,提供 redis 缓存的实现。
package com.ruoyi.common.core.cache.impl;
import com.ruoyi.common.core.cache.CacheService;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.*;
import java.util.concurrent.TimeUnit;
public class RedisCacheServiceImpl implements CacheService {
private final RedisTemplate<Object, Object> redisTemplate;
public RedisCacheServiceImpl(RedisTemplate<Object, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
// ---------- 基本存取操作 ----------
@Override
public <T> void setCacheObject(final String key, final T value) {
redisTemplate.opsForValue().set(key, value);
}
@Override
public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) {
if (timeout == null || timeout <= 0) {
redisTemplate.opsForValue().set(key, value);
} else {
redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
}
}
@Override
public <T> T getCacheObject(final String key) {
return (T) redisTemplate.opsForValue().get(key);
}
@Override
public boolean deleteObject(final String key) {
return Boolean.TRUE.equals(redisTemplate.delete(key));
}
@Override
public boolean deleteObject(final String key, final Object... values) {
return redisTemplate.opsForHash().delete(key, values) > 0;
}
@Override
public long deleteObject(final Collection<String> collection) {
if (collection == null || collection.isEmpty()) {
return 0;
}
// 注意:delete 方法接受 Collection<Object>,这里需要转换
Set<Object> keys = new HashSet<>(collection);
return redisTemplate.delete(keys);
}
// ---------- List 操作 ----------
@Override
public <T> Long setCacheList(final String key, final List<T> dataList) {
if (dataList == null || dataList.isEmpty()) {
return 0L;
}
redisTemplate.delete(key);
return redisTemplate.opsForList().rightPushAll(key, dataList);
}
@Override
public <T> List<T> getCacheList(final String key) {
Long size = redisTemplate.opsForList().size(key);
if (size == null || size == 0) {
return new ArrayList<>();
}
return (List<T>) redisTemplate.opsForList().range(key, 0, size - 1);
}
// ---------- Set 操作 ----------
@Override
public <T> Long setCacheSet(final String key, final Set<T> dataSet) {
if (dataSet == null || dataSet.isEmpty()) {
return 0L;
}
redisTemplate.delete(key);
return redisTemplate.opsForSet().add(key, dataSet.toArray());
}
@Override
public <T> Set<T> getCacheSet(final String key) {
return (Set<T>) redisTemplate.opsForSet().members(key);
}
// ---------- Map 操作 ----------
@Override
public <T> Long setCacheMap(final String key, final Map<String, T> dataMap) {
if (dataMap == null || dataMap.isEmpty()) {
return 0L;
}
redisTemplate.opsForHash().putAll(key, dataMap);
return (long) dataMap.size();
}
@Override
public <T> Map<String, T> getCacheMap(final String key) {
Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);
if (entries == null || entries.isEmpty()) {
return new HashMap<>();
}
Map<String, T> result = new HashMap<>();
for (Map.Entry<Object, Object> entry : entries.entrySet()) {
result.put(String.valueOf(entry.getKey()), (T) entry.getValue());
}
return result;
}
@Override
public <T> T getCacheMapValue(final String key, final String hKey) {
return (T) redisTemplate.opsForHash().get(key, hKey);
}
// ---------- 分布式锁相关 ----------
@Override
public Boolean setNX(final String key, final Object value, final Long timeout, final TimeUnit timeUnit) {
if (timeout == null || timeout <= 0) {
return redisTemplate.opsForValue().setIfAbsent(key, value);
} else {
return redisTemplate.opsForValue().setIfAbsent(key, value, timeout, timeUnit);
}
}
@Override
public boolean lock(final String key, final long expire, final TimeUnit unit) {
Boolean success = redisTemplate.opsForValue().setIfAbsent(key, "LOCKED", expire, unit);
return Boolean.TRUE.equals(success);
}
@Override
public void unlock(final String key) {
redisTemplate.delete(key);
}
// ---------- 过期相关 ----------
@Override
public Boolean expire(final String key, final long timeout, final TimeUnit timeUnit) {
return redisTemplate.expire(key, timeout, timeUnit);
}
@Override
public Long getExpire(final String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
@Override
public boolean hasKey(final String key) {
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
@Override
public Collection<String> keys(final String pattern) {
Set<Object> keys = redisTemplate.keys(pattern);
if (keys == null) {
return Collections.emptySet();
}
Set<String> result = new HashSet<>();
for (Object key : keys) {
result.add(key.toString());
}
return result;
}
}
4.5 实现Caffeine缓存
在 ruoyi-common 里新建 CaffeineCacheServiceImpl.java ,提供 Caffeine 缓存的实现。
package com.ruoyi.common.core.cache.impl;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.ruoyi.common.config.properties.CacheProperties;
import com.ruoyi.common.core.cache.CacheService;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* Caffeine 本地缓存服务实现(无外部注解,构造器初始化)
*/
public class CaffeineCacheServiceImpl implements CacheService {
private final Cache<String, Object> caffeineCache;
private final Map<String, Object> complexMap = new ConcurrentHashMap<>();
public CaffeineCacheServiceImpl(CacheProperties cacheProperties) {
// 在构造方法中直接初始化 Caffeine 实例
Caffeine<Object, Object> builder = Caffeine.newBuilder()
.initialCapacity(cacheProperties.getCaffeine().getInitialCapacity())
.maximumSize(cacheProperties.getCaffeine().getMaximumSize())
.expireAfterWrite(cacheProperties.getCaffeine().getExpireAfterWrite(), TimeUnit.SECONDS)
.recordStats(); // 可选,记录命中率
this.caffeineCache = builder.build();
}
// ========== 基本存取操作 ==========
@Override
public <T> void setCacheObject(final String key, final T value) {
caffeineCache.put(key, value);
}
@Override
public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) {
// Caffeine 不支持动态修改单个 key 的过期时间,此处简化为直接放入
caffeineCache.put(key, value);
// 如需更精确的过期控制,可考虑使用 put 的同时记录过期时间戳,在 get 时判断
}
@Override
public <T> T getCacheObject(final String key) {
return (T) caffeineCache.getIfPresent(key);
}
@Override
public boolean deleteObject(final String key) {
caffeineCache.invalidate(key);
complexMap.remove(key);
return true;
}
@Override
public boolean deleteObject(final String key, final Object... values) {
// Caffeine 不支持 Hash 结构,此处返回 false 表示不支持
return false;
}
@Override
public long deleteObject(final Collection<String> collection) {
if (collection == null || collection.isEmpty()) {
return 0;
}
collection.forEach(key -> {
caffeineCache.invalidate(key);
complexMap.remove(key);
});
return collection.size();
}
// ========== List 操作 ==========
@Override
public <T> Long setCacheList(final String key, final List<T> dataList) {
complexMap.put(key, new ArrayList<>(dataList));
return (long) dataList.size();
}
@Override
public <T> List<T> getCacheList(final String key) {
Object obj = complexMap.get(key);
if (obj instanceof List) {
return (List<T>) obj;
}
return new ArrayList<>();
}
// ========== Set 操作 ==========
@Override
public <T> Long setCacheSet(final String key, final Set<T> dataSet) {
complexMap.put(key, new HashSet<>(dataSet));
return (long) dataSet.size();
}
@Override
public <T> Set<T> getCacheSet(final String key) {
Object obj = complexMap.get(key);
if (obj instanceof Set) {
return (Set<T>) obj;
}
return new HashSet<>();
}
// ========== Map 操作 ==========
@Override
public <T> Long setCacheMap(final String key, final Map<String, T> dataMap) {
complexMap.put(key, new HashMap<>(dataMap));
return (long) dataMap.size();
}
@Override
public <T> Map<String, T> getCacheMap(final String key) {
Object obj = complexMap.get(key);
if (obj instanceof Map) {
return (Map<String, T>) obj;
}
return new HashMap<>();
}
@Override
public <T> T getCacheMapValue(final String key, final String hKey) {
Map<String, T> map = getCacheMap(key);
return map.get(hKey);
}
// ========== 分布式锁相关(Caffeine 单机有效,模拟) ==========
@Override
public Boolean setNX(final String key, final Object value, final Long timeout, final TimeUnit timeUnit) {
Object existing = caffeineCache.asMap().putIfAbsent(key, value);
return existing == null;
}
@Override
public boolean lock(final String key, final long expire, final TimeUnit unit) {
String lockKey = "LOCK:" + key;
Object existing = caffeineCache.asMap().putIfAbsent(lockKey, "locked");
return existing == null;
}
@Override
public void unlock(final String key) {
caffeineCache.invalidate("LOCK:" + key);
}
// ========== 过期相关 ==========
@Override
public Boolean expire(final String key, final long timeout, final TimeUnit timeUnit) {
// Caffeine 不支持动态修改过期时间
return false;
}
@Override
public Long getExpire(final String key) {
// 无法获取剩余过期时间
return -1L;
}
@Override
public boolean hasKey(final String key) {
return caffeineCache.getIfPresent(key) != null;
}
@Override
public Collection<String> keys(final String pattern) {
// 获取当前缓存中所有 key
Set<String> allKeys = caffeineCache.asMap().keySet();
// 如果 pattern 为 null 或 "*",则返回所有 key
if (pattern == null || "*".equals(pattern)) {
return new ArrayList<>(allKeys);
}
// 否则,将 pattern 中的 "*" 转换为正则表达式进行匹配
String regex = pattern
.replace("?", ".?")
.replace("*", ".*");
return allKeys.stream()
.filter(key -> key.matches(regex))
.collect(Collectors.toList());
}
}
4.6 动态注入CacheService
在 ruoyi-framework 里添加 CacheServiceConfig.java :
package com.ruoyi.framework.config;
import com.ruoyi.common.config.properties.CacheProperties;
import com.ruoyi.common.core.cache.CacheService;
import com.ruoyi.common.core.cache.impl.CaffeineCacheServiceImpl;
import com.ruoyi.common.core.cache.impl.RedisCacheServiceImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.core.RedisTemplate;
@Configuration
public class CacheServiceConfig {
private static final Logger log = LoggerFactory.getLogger(CacheServiceConfig.class);
@Autowired
private CacheProperties cacheProperties;
@Autowired
private RedisTemplate<Object, Object> redisTemplate; // 用于 Redis 实现
@Bean
@Primary
public CacheService cacheService() {
// String cacheType = cacheProperties.getType();
// System.out.println("实际读取到的 cacheType = " + cacheType);
if ("caffeine".equalsIgnoreCase(cacheProperties.getType())) {
log.info("===================== 缓存服务激活:Caffeine 本地缓存 =====================");
// 手动创建 Caffeine 实现,传入配置
return new CaffeineCacheServiceImpl(cacheProperties);
} else {
log.info("===================== 缓存服务激活:Redis 分布式缓存 =====================");
// 默认使用 Redis 实现,手动创建并传入 RedisTemplate
return new RedisCacheServiceImpl(redisTemplate);
}
}
}
4.7 修改原有业务代码,适配缓存切换方案
- 将所有的
RedisCache redisCache;替换成CacheService cacheService;。 - 完整注释
RedisCache.java类。
这是个可选项。不注释也不影响程序的运行。如果在切换到 caffeine 缓存时,也仍然想在某些功能上使用 redis,那么就可以保留RedisCache.java类。 - 修改字典工具类
DictUtils.java。
把代码中的RedisCache.class替换成CacheService.class。
5. 测试
在 application.yml 里,把 ruoyi.cache.type 分别设置为 redis 、 caffeine ,测试程序的功能。
6. 附录
6.1 注意事项
- 在切换为 caffeine 缓存后,“系统监控” - “缓存监控” 中并没有实现对 caffeine 缓存的监控。
- 在切换为 caffeine 缓存后,可以不配置
spring.data.redis。
这是因为只有执行了redisTemplate.opsForValue().get()后,才会使用 redis 配置去连接 redis 。在切换为 caffeine 缓存时,程序不会执行redisTemplate.opsForValue().get()。 - caffeine 缓存不支持分布式锁。

浙公网安备 33010602011771号