# 后端移除 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` 补充测试依赖** ```xml org.springframework.boot spring-boot-starter-test test ``` - [ ] **Step 2: 先写 `InMemoryCacheStoreTest` 失败用例** ```java @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: 实现最小本地缓存存储** ```java public class InMemoryCacheStore { private final ConcurrentHashMap entries = new ConcurrentHashMap<>(); private final InMemoryCacheStats stats = new InMemoryCacheStats(); public void set(String key, Object value, long timeout, TimeUnit unit) { ... } public T get(String key) { ... } public boolean hasKey(String key) { ... } public boolean delete(String key) { ... } public Set keys(String pattern) { ... } public InMemoryCacheStats snapshot() { ... } } ``` - [ ] **Step 5: 实现过期清理与统计快照** ```java 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: 提交本任务** ```bash 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` 兼容语义补测试** ```java @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` 改为委托本地缓存存储** ```java @Component public class RedisCache { private final InMemoryCacheStore cacheStore; public void setCacheObject(String key, T value, Integer timeout, TimeUnit timeUnit) { cacheStore.set(key, value, timeout.longValue(), timeUnit); } } ``` - [ ] **Step 4: 实现 `keys("*")`、批量删除、剩余 TTL 查询等兼容方法** ```java public long getExpire(final String key) { ... } public Collection 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: 提交本任务** ```bash 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: 为登录态与验证码写失败测试** ```java @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: 验证在线用户扫描仍走前缀检索** ```java Collection keys = redisCache.keys(CacheConstants.LOGIN_TOKEN_KEY + "*"); ``` 实现时只调整底层,不改控制器接口路径和返回结构。 - [ ] **Step 5: 验证防重提交仍支持毫秒级 TTL** ```java redisCache.setCacheObject(cacheRepeatKey, cacheMap, annotation.interval(), TimeUnit.MILLISECONDS); ``` 实现时重点确认本地缓存对毫秒级过期不丢精度。 - [ ] **Step 6: 运行框架模块测试** Run: `mvn -pl ruoyi-framework -am test` Expected: `BUILD SUCCESS` - [ ] **Step 7: 提交本任务** ```bash 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: 先写限流失败测试** ```java @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: 写缓存监控失败测试** ```java @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: 将限流改为本地窗口计数** ```java long current = redisCache.increment(rateLimiterKey, time, TimeUnit.SECONDS); if (current > count) { throw new ServiceException("访问过于频繁,请稍候再试"); } ``` 如果 `RedisCache` 还没有原子递增能力,先在本地缓存层补 `increment`,不要再引入新的限流存储类。 - [ ] **Step 5: 将缓存监控接口改为本地统计视图** ```java Map 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: 提交本任务** ```bash 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 配置** ```yaml 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: 提交本任务** ```bash 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移除后端收尾" ```