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 来实现内存缓存。
类图见下:

classDiagram direction TB %% ==================== 接口层 ==================== class CacheService~T~ { <<interface>> +setCacheObject(String key, T value) void +getCacheObject(String key) T +deleteObject(String key) boolean } %% ==================== 配置层 ==================== class CacheServiceConfig { -Logger log -CacheProperties cacheProperties -RedisTemplate~Object,Object~ redisTemplate +cacheService() CacheService$ } %% ==================== Caffeine 实现 ==================== class CaffeineCacheServiceImpl { -Cache~String,Object~ caffeineCache -Map~String,Object~ complexMap +CaffeineCacheServiceImpl(CacheProperties cacheProperties) +setCacheObject(String key, T value) void +getCacheObject(String key) T +deleteObject(String key) boolean } %% ==================== Redis 实现 ==================== class RedisCacheServiceImpl { -RedisTemplate~Object,Object~ redisTemplate +RedisCacheServiceImpl(RedisTemplate~Object,Object~ redisTemplate) +setCacheObject(String key, T value) void +getCacheObject(String key) T +deleteObject(String key) boolean } %% ==================== 配置属性(推断) ==================== class CacheProperties { +getType() String +getCaffeine() CaffeineProperties } class CaffeineProperties { +getInitialCapacity() int +getMaximumSize() long +getExpireAfterWrite() long } %% ==================== 外部依赖(简化) ==================== class Cache~K,V~ { <<external>> } class RedisTemplate~K,V~ { <<external>> } %% ==================== 关系 ==================== CacheService <|.. CaffeineCacheServiceImpl : implements CacheService <|.. RedisCacheServiceImpl : implements CacheServiceConfig ..> CacheService : creates & returns CacheServiceConfig ..> CaffeineCacheServiceImpl : creates CacheServiceConfig ..> RedisCacheServiceImpl : creates CaffeineCacheServiceImpl --> CacheProperties : uses CaffeineCacheServiceImpl --> Cache : contains RedisCacheServiceImpl --> RedisTemplate : contains CacheProperties --> CaffeineProperties : contains

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 修改原有业务代码,适配缓存切换方案

  1. 将所有的 RedisCache redisCache; 替换成 CacheService cacheService;
  2. 完整注释 RedisCache.java 类。
    这是个可选项。不注释也不影响程序的运行。如果在切换到 caffeine 缓存时,也仍然想在某些功能上使用 redis,那么就可以保留 RedisCache.java 类。
  3. 修改字典工具类 DictUtils.java
    把代码中的 RedisCache.class 替换成 CacheService.class

5. 测试

application.yml 里,把 ruoyi.cache.type 分别设置为 rediscaffeine ,测试程序的功能。


6. 附录

6.1 注意事项

  1. 在切换为 caffeine 缓存后,“系统监控” - “缓存监控” 中并没有实现对 caffeine 缓存的监控。
  2. 在切换为 caffeine 缓存后,可以不配置 spring.data.redis
    这是因为只有执行了 redisTemplate.opsForValue().get() 后,才会使用 redis 配置去连接 redis 。在切换为 caffeine 缓存时,程序不会执行 redisTemplate.opsForValue().get()
  3. caffeine 缓存不支持分布式锁。

posted @ 2026-06-12 13:38  梦魇  阅读(6)  评论(0)    收藏  举报