Files
loan-pricing/doc/2026-03-28-remove-redis-backend-plan.md

15 KiB

后端移除 Redis Implementation Plan

For agentic workers: REQUIRED: Use superpowers:executing-plans to implement this plan. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 移除后端 Redis 依赖并以单实例进程内缓存替代,保证登录态、验证码、失败次数、防重提交、限流、配置缓存、字典缓存、在线用户和缓存监控行为不变。

Architecture: 保留现有 RedisCache 作为业务侧统一入口,将底层替换为线程安全的进程内缓存存储,统一提供 TTL、前缀检索、删除和统计能力。所有原先依赖 RedisTemplate 或 Lua 脚本的代码改为依赖本地缓存组件,尽量不改接口路径和业务调用方式。

Tech Stack: Java 17, Spring Boot 3.5.x, Spring Security, Maven, JUnit 5, Mockito


文件结构

需要新增

  • ruoyi-common/src/main/java/com/ruoyi/common/core/cache/InMemoryCacheEntry.java
  • ruoyi-common/src/main/java/com/ruoyi/common/core/cache/InMemoryCacheStats.java
  • ruoyi-common/src/main/java/com/ruoyi/common/core/cache/InMemoryCacheStore.java
  • ruoyi-common/src/test/java/com/ruoyi/common/core/cache/InMemoryCacheStoreTest.java
  • ruoyi-framework/src/test/java/com/ruoyi/framework/aspectj/RateLimiterAspectTest.java
  • ruoyi-framework/src/test/java/com/ruoyi/framework/web/service/TokenServiceLocalCacheTest.java
  • ruoyi-admin/src/test/java/com/ruoyi/web/controller/monitor/CacheControllerTest.java

需要修改

  • ruoyi-common/pom.xml
  • ruoyi-framework/pom.xml
  • ruoyi-admin/pom.xml
  • ruoyi-common/src/main/java/com/ruoyi/common/core/redis/RedisCache.java
  • ruoyi-common/src/main/java/com/ruoyi/common/utils/DictUtils.java
  • ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RateLimiterAspect.java
  • ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/impl/SameUrlDataInterceptor.java
  • ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java
  • ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java
  • ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysRegisterService.java
  • ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPasswordService.java
  • ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysConfigServiceImpl.java
  • ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CaptchaController.java
  • ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysUserOnlineController.java
  • ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/CacheController.java
  • ruoyi-admin/src/main/resources/application-dev.yml

需要删除

  • ruoyi-framework/src/main/java/com/ruoyi/framework/config/RedisConfig.java
  • ruoyi-framework/src/main/java/com/ruoyi/framework/config/FastJson2JsonRedisSerializer.java

Task 1: 建立本地缓存基础设施与测试基线

Files:

  • Create: ruoyi-common/src/main/java/com/ruoyi/common/core/cache/InMemoryCacheEntry.java

  • Create: ruoyi-common/src/main/java/com/ruoyi/common/core/cache/InMemoryCacheStats.java

  • Create: ruoyi-common/src/main/java/com/ruoyi/common/core/cache/InMemoryCacheStore.java

  • Create: ruoyi-common/src/test/java/com/ruoyi/common/core/cache/InMemoryCacheStoreTest.java

  • Modify: ruoyi-common/pom.xml

  • Step 1: 为 ruoyi-common 补充测试依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
  • Step 2: 先写 InMemoryCacheStoreTest 失败用例
@Test
void shouldExpireEntryAfterTtl() throws Exception {
    InMemoryCacheStore store = new InMemoryCacheStore();
    store.set("captcha_codes:1", "1234", 20, TimeUnit.MILLISECONDS);
    Thread.sleep(40);
    assertNull(store.get("captcha_codes:1"));
}

@Test
void shouldReturnPrefixKeysInSortedOrder() {
    InMemoryCacheStore store = new InMemoryCacheStore();
    store.set("login_tokens:a", "A");
    store.set("login_tokens:b", "B");
    store.set("sys_dict:x", "X");
    assertEquals(Set.of("login_tokens:a", "login_tokens:b"), store.keys("login_tokens:*"));
}
  • Step 3: 运行测试确认当前失败

Run: mvn -pl ruoyi-common -am -Dtest=InMemoryCacheStoreTest test

Expected: 失败,提示测试类或 InMemoryCacheStore 不存在。

  • Step 4: 实现最小本地缓存存储
public class InMemoryCacheStore {
    private final ConcurrentHashMap<String, InMemoryCacheEntry> entries = new ConcurrentHashMap<>();
    private final InMemoryCacheStats stats = new InMemoryCacheStats();

    public void set(String key, Object value, long timeout, TimeUnit unit) { ... }
    public <T> T get(String key) { ... }
    public boolean hasKey(String key) { ... }
    public boolean delete(String key) { ... }
    public Set<String> keys(String pattern) { ... }
    public InMemoryCacheStats snapshot() { ... }
}
  • Step 5: 实现过期清理与统计快照
public record InMemoryCacheStats(
        String cacheType,
        String mode,
        long keySize,
        long hitCount,
        long missCount,
        long expiredCount,
        long writeCount) {}
  • Step 6: 重新运行测试确认通过

Run: mvn -pl ruoyi-common -am -Dtest=InMemoryCacheStoreTest test

Expected: BUILD SUCCESS

  • Step 7: 提交本任务
git add ruoyi-common/pom.xml \
  ruoyi-common/src/main/java/com/ruoyi/common/core/cache \
  ruoyi-common/src/test/java/com/ruoyi/common/core/cache/InMemoryCacheStoreTest.java
git commit -m "新增本地缓存基础设施"

Task 2: 保留 RedisCache 入口并移除 Redis 专属配置

Files:

  • Modify: ruoyi-common/src/main/java/com/ruoyi/common/core/redis/RedisCache.java

  • Modify: ruoyi-framework/pom.xml

  • Delete: ruoyi-framework/src/main/java/com/ruoyi/framework/config/RedisConfig.java

  • Delete: ruoyi-framework/src/main/java/com/ruoyi/framework/config/FastJson2JsonRedisSerializer.java

  • Step 1: 先为 RedisCache 兼容语义补测试

@Test
void shouldSupportSetGetDeleteAndExpireThroughRedisCacheFacade() {
    RedisCache cache = new RedisCache(new InMemoryCacheStore());
    cache.setCacheObject("login_tokens:abc", "payload", 1, TimeUnit.SECONDS);
    assertEquals("payload", cache.getCacheObject("login_tokens:abc"));
    assertTrue(cache.hasKey("login_tokens:abc"));
    assertTrue(cache.deleteObject("login_tokens:abc"));
}
  • Step 2: 运行 ruoyi-common 测试确认失败

Run: mvn -pl ruoyi-common -am -Dtest=InMemoryCacheStoreTest,RedisCache* test

Expected: 失败,当前 RedisCache 仍依赖 RedisTemplate

  • Step 3: 将 RedisCache 改为委托本地缓存存储
@Component
public class RedisCache {
    private final InMemoryCacheStore cacheStore;

    public <T> void setCacheObject(String key, T value, Integer timeout, TimeUnit timeUnit) {
        cacheStore.set(key, value, timeout.longValue(), timeUnit);
    }
}
  • Step 4: 实现 keys("*")、批量删除、剩余 TTL 查询等兼容方法
public long getExpire(final String key) { ... }
public Collection<String> keys(final String pattern) { ... }
public boolean deleteObject(final Collection collection) { ... }
  • Step 5: 删除 Redis 专属配置与框架依赖

需要完成:

  • 删除 RedisConfig.java

  • 删除 FastJson2JsonRedisSerializer.java

  • ruoyi-common/pom.xml 删除 spring-boot-starter-data-redis

  • ruoyi-common/pom.xml 删除 commons-pool2

  • Step 6: 运行公共模块测试与编译

Run: mvn -pl ruoyi-common,ruoyi-framework -am test

Expected: BUILD SUCCESS

  • Step 7: 提交本任务
git add ruoyi-common/pom.xml \
  ruoyi-framework/pom.xml \
  ruoyi-common/src/main/java/com/ruoyi/common/core/redis/RedisCache.java \
  ruoyi-framework/src/main/java/com/ruoyi/framework/config \
  ruoyi-common/src/test
git commit -m "移除Redis配置并保留缓存入口"

Task 3: 改造认证、验证码、密码错误次数与防重提交

Files:

  • Modify: ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java

  • Modify: ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java

  • Modify: ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysRegisterService.java

  • Modify: ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPasswordService.java

  • Modify: ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/impl/SameUrlDataInterceptor.java

  • Modify: ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CaptchaController.java

  • Modify: ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysUserOnlineController.java

  • Modify: ruoyi-framework/pom.xml

  • Create: ruoyi-framework/src/test/java/com/ruoyi/framework/web/service/TokenServiceLocalCacheTest.java

  • Step 1: 为登录态与验证码写失败测试

@Test
void shouldStoreLoginUserWithTokenTtl() {
    LoginUser loginUser = new LoginUser();
    String jwt = tokenService.createToken(loginUser);
    assertNotNull(jwt);
    assertNotNull(redisCache.getCacheObject("login_tokens:" + loginUser.getToken()));
}

@Test
void shouldDeleteCaptchaAfterValidation() {
    redisCache.setCacheObject("captcha_codes:uuid", "ABCD", 1, TimeUnit.MINUTES);
    loginService.validateCaptcha("admin", "ABCD", "uuid");
    assertNull(redisCache.getCacheObject("captcha_codes:uuid"));
}
  • Step 2: 运行框架测试确认失败

Run: mvn -pl ruoyi-framework -am -Dtest=TokenServiceLocalCacheTest test

Expected: 失败,测试基础设施或本地缓存接线尚未完成。

  • Step 3: 让认证相关服务继续只依赖 RedisCache 抽象

要求:

  • 不改 CacheConstants key 规则

  • 不改 token 刷新时机

  • 不改验证码删除时机

  • 不改密码错误锁定时间计算

  • Step 4: 验证在线用户扫描仍走前缀检索

Collection<String> keys = redisCache.keys(CacheConstants.LOGIN_TOKEN_KEY + "*");

实现时只调整底层,不改控制器接口路径和返回结构。

  • Step 5: 验证防重提交仍支持毫秒级 TTL
redisCache.setCacheObject(cacheRepeatKey, cacheMap, annotation.interval(), TimeUnit.MILLISECONDS);

实现时重点确认本地缓存对毫秒级过期不丢精度。

  • Step 6: 运行框架模块测试

Run: mvn -pl ruoyi-framework -am test

Expected: BUILD SUCCESS

  • Step 7: 提交本任务
git add ruoyi-framework/pom.xml \
  ruoyi-framework/src/main/java/com/ruoyi/framework/web/service \
  ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/impl/SameUrlDataInterceptor.java \
  ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CaptchaController.java \
  ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysUserOnlineController.java \
  ruoyi-framework/src/test/java/com/ruoyi/framework/web/service/TokenServiceLocalCacheTest.java
git commit -m "改造登录态与验证码缓存"

Task 4: 替换限流与缓存监控实现

Files:

  • Modify: ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RateLimiterAspect.java

  • Modify: ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/CacheController.java

  • Modify: ruoyi-admin/pom.xml

  • Create: ruoyi-framework/src/test/java/com/ruoyi/framework/aspectj/RateLimiterAspectTest.java

  • Create: ruoyi-admin/src/test/java/com/ruoyi/web/controller/monitor/CacheControllerTest.java

  • Step 1: 先写限流失败测试

@Test
void shouldRejectThirdRequestWithinWindow() throws Throwable {
    RateLimiter limiter = annotation(count = 2, time = 60);
    aspect.doBefore(joinPoint, limiter);
    aspect.doBefore(joinPoint, limiter);
    assertThrows(ServiceException.class, () -> aspect.doBefore(joinPoint, limiter));
}
  • Step 2: 写缓存监控失败测试
@Test
void shouldReturnInMemoryCacheSummary() throws Exception {
    mockMvc.perform(get("/monitor/cache"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.data.info.cache_type").value("IN_MEMORY"))
        .andExpect(jsonPath("$.data.info.cache_mode").value("single-instance"));
}
  • Step 3: 运行测试确认失败

Run: mvn -pl ruoyi-framework,ruoyi-admin -am -Dtest=RateLimiterAspectTest,CacheControllerTest test

Expected: 失败,当前仍依赖 Redis 脚本与 Redis INFO

  • Step 4: 将限流改为本地窗口计数
long current = redisCache.increment(rateLimiterKey, time, TimeUnit.SECONDS);
if (current > count) {
    throw new ServiceException("访问过于频繁,请稍候再试");
}

如果 RedisCache 还没有原子递增能力,先在本地缓存层补 increment,不要再引入新的限流存储类。

  • Step 5: 将缓存监控接口改为本地统计视图
Map<String, Object> info = Map.of(
    "cache_type", "IN_MEMORY",
    "cache_mode", "single-instance",
    "key_size", stats.keySize(),
    "hit_count", stats.hitCount(),
    "expired_count", stats.expiredCount()
);

同时保持以下接口不变:

  • GET /monitor/cache

  • GET /monitor/cache/getNames

  • GET /monitor/cache/getKeys/{cacheName}

  • GET /monitor/cache/getValue/{cacheName}/{cacheKey}

  • DELETE /monitor/cache/clearCacheName/{cacheName}

  • DELETE /monitor/cache/clearCacheKey/{cacheKey}

  • DELETE /monitor/cache/clearCacheAll

  • Step 6: 运行对应测试

Run: mvn -pl ruoyi-framework,ruoyi-admin -am test

Expected: BUILD SUCCESS

  • Step 7: 提交本任务
git add ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RateLimiterAspect.java \
  ruoyi-admin/pom.xml \
  ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/CacheController.java \
  ruoyi-framework/src/test/java/com/ruoyi/framework/aspectj/RateLimiterAspectTest.java \
  ruoyi-admin/src/test/java/com/ruoyi/web/controller/monitor/CacheControllerTest.java
git commit -m "改造限流与缓存监控实现"

Task 5: 收尾配置、配置缓存/字典缓存验证与无 Redis 启动校验

Files:

  • Modify: ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysConfigServiceImpl.java

  • Modify: ruoyi-common/src/main/java/com/ruoyi/common/utils/DictUtils.java

  • Modify: ruoyi-admin/src/main/resources/application-dev.yml

  • Step 1: 清理 application-dev.yml 中的 Redis 配置

spring:
  datasource:
    ...
  # 删除整个 spring.data.redis 段
  • Step 2: 审核配置缓存和字典缓存调用点

确认以下逻辑不变:

  • loadingConfigCache()

  • clearConfigCache()

  • resetConfigCache()

  • DictUtils.getDictCache()

  • DictUtils.setDictCache()

  • DictUtils.clearDictCache()

  • Step 3: 运行后端全量测试

Run: mvn test

Expected: BUILD SUCCESS

  • Step 4: 本地启动后端,确认无 Redis 也可启动

Run: mvn -pl ruoyi-admin -am spring-boot:run

Expected: 应用启动成功,日志中不再出现 Redis 连接初始化失败。

  • Step 5: 手工验证关键链路

按顺序验证:

  • 登录

  • 验证码

  • 登录失败锁定

  • 在线用户列表

  • 强退用户

  • 配置刷新

  • 字典刷新

  • 缓存监控查看与清理

  • Step 6: 停止后端进程

要求:测试结束后自动结束当前任务拉起的 Java 进程,不保留后台测试进程。

  • Step 7: 提交本任务
git add ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysConfigServiceImpl.java \
  ruoyi-common/src/main/java/com/ruoyi/common/utils/DictUtils.java \
  ruoyi-admin/src/main/resources/application-dev.yml
git commit -m "完成Redis移除后端收尾"