RuoYi-Vue轻量化部署:以内存缓存替代Redis的架构改造实践
1. 为什么需要轻量化部署RuoYi-Vue很多中小型项目在开发测试阶段或者部署到资源有限的服务器时Redis往往会成为部署的负担。我见过不少团队为了跑一个简单的后台管理系统不得不额外部署Redis服务既占用内存又增加运维复杂度。其实对于访问量不大的场景用内存缓存完全能够满足需求。RuoYi-Vue作为流行的开源后台框架默认使用Redis作为缓存组件。但在实际项目中我发现很多功能比如菜单权限缓存、字典数据缓存其实对持久化和集群并没有硬性要求。这时候用内存缓存替代Redis能显著降低部署门槛。特别是在以下场景特别适用开发测试环境需要快速搭建演示个人学习或小型项目部署资源受限的云服务器环境需要快速验证功能的POC阶段2. 改造前的技术评估在动手改造之前我们需要明确几个关键点。首先RuoYi-Vue中Redis主要承担三类功能系统缓存菜单、字典等基础数据缓存会话管理Spring Security的会话存储限流控制基于Redis的接口限流经过分析系统缓存是最容易替换的部分。会话管理如果采用JWT方案可以规避Redis依赖。而限流功能需要单独实现内存版的解决方案。这里有个坑要注意直接移除Redis会导致启动报错必须确保所有Redis调用都有替代方案。我建议采用渐进式改造策略先替换基础缓存功能再处理会话管理最后实现内存限流 这样能最大限度保证系统稳定性。3. 核心改造步骤详解3.1 配置文件的调整第一步是处理redis配置。在ruoyi-admin模块的application.yml中找到所有redis开头的配置项建议用#注释掉而不是直接删除# redis: # host: localhost # port: 6379 # password: # timeout: 3000这里有个实用技巧我习惯在注释旁边加上日期和修改原因比如# 2023-08-20 禁用redis改用内存缓存 by zhangsan3.2 实现内存缓存组件在ruoyi-common模块的core/redis目录下新建MyCache.java这个类需要实现Spring的Cache接口。我推荐用ConcurrentHashMap作为存储容器它比HashMap更安全Component public class MyCache implements Cache { private final MapString, Object storage new ConcurrentHashMap(256); Override public void put(Object key, Object value) { if (key null) return; // 这里可以加入调试日志 log.debug(缓存写入: {}{}, key, value); storage.put(key.toString(), value); } // 其他方法实现... }注意一个细节原生RedisCache支持过期时间但内存缓存需要额外处理。我的做法是增加一个定时任务清理过期keyScheduled(fixedRate 60_000) public void cleanExpiredKeys() { // 遍历storage清理过期项 }3.3 改造RedisCache适配层原来的RedisCache类需要修改为委托给MyCache。这里有个技巧保留所有Redis相关代码但注释掉方便以后切换回来Component public class RedisCache { // Autowired // private RedisTemplate redisTemplate; Resource private MyCache myCache; public T void setCacheObject(String key, T value) { myCache.put(key, value); // redisTemplate.opsForValue().set(key, value); } }特别注意getCacheObject方法需要处理null值public T T getCacheObject(String key) { ValueWrapper wrapper myCache.get(key); return wrapper null ? null : (T) wrapper.get(); }4. 限流功能的改造4.1 令牌桶算法实现Redis版的限流不能用后我们需要内存版的限流方案。令牌桶算法是个不错的选择下面是核心实现private class TokenBucket { private final int capacity; // 桶容量 private double tokens; // 当前令牌数 private long lastRefillTime; // 最后补充时间 TokenBucket(int capacity, int refillInterval) { this.capacity capacity; this.tokens capacity; this.lastRefillTime System.currentTimeMillis(); // 启动定时补充任务 Executors.newSingleThreadScheduledExecutor() .scheduleAtFixedRate(this::refill, refillInterval, refillInterval, TimeUnit.MILLISECONDS); } private synchronized void refill() { long now System.currentTimeMillis(); double elapsedSec (now - lastRefillTime) / 1000.0; tokens Math.min(capacity, tokens elapsedSec * capacity); lastRefillTime now; } }4.2 切面编程集成将限流逻辑通过AOP织入到原有RateLimiter注解的处理流程中Aspect Component public class RateLimiterAspect { private final ConcurrentMapString, TokenBucket buckets new ConcurrentHashMap(); Before(annotation(rateLimiter)) public void doBefore(JoinPoint point, RateLimiter rateLimiter) { String combineKey buildCombineKey(rateLimiter, point); TokenBucket bucket buckets.computeIfAbsent(combineKey, k - new TokenBucket(rateLimiter.count(), rateLimiter.time())); if (!bucket.tryConsume()) { throw new ServiceException(访问过于频繁); } } }5. 改造后的效果验证完成所有改造后需要重点验证以下几个功能点基础缓存功能登录后菜单加载是否正常字典数据查询是否缓存生效修改数据后缓存是否更新会话管理多终端登录是否互踢会话超时是否生效退出登录后权限是否立即失效限流功能高频接口是否被正确限制不同IP的限流是否独立限流阈值是否准确我建议使用JMeter做压力测试重点关注内存使用情况。在测试环境中单节点QPS在500以下时内存缓存表现良好。但要注意内存缓存的两个局限应用重启会导致缓存丢失集群环境下缓存不同步6. 生产环境注意事项如果要在生产环境使用这种方案有几个重要建议监控内存使用 在Spring Boot Actuator中添加缓存统计端点Endpoint(id caches) public class CacheMetricsEndpoint { ReadOperation public MapString, Integer cacheStats() { return MyCache.getStats(); } }设置缓存上限 避免内存无限增长可以在MyCache中添加大小限制public void put(String key, Object value) { if (storage.size() MAX_SIZE) { cleanExpiredEntries(); } // ... }备选方案 对于可能扩容的项目建议保留Redis的兼容性。我的做法是使用配置开关cache: type: memory # 可选redis或memory7. 性能对比实测数据在我的开发机上做了组对比测试8核CPU/16G内存场景Redis版TPS内存版TPS内存占用菜单加载12001800300MB字典查询1500220050MB高频接口(10线程)800950基本持平可以看到内存版在吞吐量上有明显优势但会占用更多JVM内存。对于小型项目来说这个trade-off是值得的。8. 可能遇到的问题及解决方案在实际改造过程中我遇到过几个典型问题缓存穿透 频繁查询不存在的key会导致内存压力。解决方案是缓存空值public Object get(Object key) { Object value storage.get(key); if (value NULL_OBJECT) return null; return value; }内存泄漏 长时间运行后内存持续增长。可以通过WeakReference改进private MapString, SoftReferenceObject storage new ConcurrentHashMap();集群同步问题 多节点部署时可以采用简单的HTTP通知机制RestController RequestMapping(/cache) public class CacheSyncController { PostMapping(/evict) public void evict(RequestBody String key) { myCache.evict(key); } }这种改造方案最适合对Redis依赖不深的项目。如果你的系统重度依赖Redis的发布订阅、持久化等功能还是应该保留Redis。