Ruoyi最小化部署方案 将redis 缓存替换为GuavaCache

发布时间 2023-10-13 21:45:27作者: KwFruit

一、说明

   将redis替换为本地缓存方案,有些特殊的场景需要:

               1 比如微小型项目部署在配置比较低的云服务器上,不需要装其他的中间件,并不需要多大的并发量。

               2 将jar包打成exe的项目类似于客户端,运行在用户电脑上

二、GuavaCache介绍

Guava是Google提供的一套Java工具包,而Guava Cache是一套非常完善的本地缓存机制(JVM缓存)。
Guava cache的设计来源于CurrentHashMap,可以按照多种策略来清理存储在其中的缓存值且保持很高的并发读写性能。

三、Ruoyi项目接入GuavaCache

1 引入依赖

主pom.xml 锁定版本

        <!--引入google guava cache 缓存-->
            <dependency>
                <groupId>com.google.guava</groupId>
                <artifactId>guava</artifactId>
                <version>23.0</version>
            </dependency>

ruiyi-common pom.xml 引入依赖

   <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
        </dependency>

2 GuavaCache工具类编写

GuavaCache

/**
 * Guava 缓存工具类
 */
public class GuavaCache {

    private  Cache<String, Object> cache ;


    public GuavaCache(Cache<String, Object> cache){
     this.cache = cache;
    }


    public Object get(String key) {
        return cache.getIfPresent(key);
    }

    public void put(String key, Object value) {
        cache.put(key, value);
    }

    public void remove(String key) {
        cache.invalidate(key);
    }

    public void removeAll(){
        cache.invalidateAll();
    }

    public ConcurrentMap<String,Object> keys(){
        return cache.asMap();
    }

}

GuavaCacheManager

public class GuavaCacheManager {

    private static final Map<String, GuavaCache> guavaCacheMap = new HashMap<>();

    /**
     * 构造器私有,防止外部实例化该对象
     */
    private GuavaCacheManager(){
    }

    /**
     * 静态代码块 当类加载的时候就注册处理器
     */
    static {
        registerGuavaCacheMap(CacheConstants.CAPTCHA_CODE_KEY, new GuavaCache(GuavaCaches.CAPTCHA_CODE));
        registerGuavaCacheMap(CacheConstants.LOGIN_TOKEN_KEY, new GuavaCache(GuavaCaches.LOGIN_TOKEN));
        registerGuavaCacheMap(CacheConstants.SYS_CONFIG_KEY, new GuavaCache(GuavaCaches.SYS_CONFIG));
        registerGuavaCacheMap(CacheConstants.SYS_DICT_KEY, new GuavaCache(GuavaCaches.SYS_DICT));
        registerGuavaCacheMap(CacheConstants.REPEAT_SUBMIT_KEY, new GuavaCache(GuavaCaches.REPEAT_SUBMIT));
        registerGuavaCacheMap(CacheConstants.PWD_ERR_CNT_KEY, new GuavaCache(GuavaCaches.PWD_ERR_CNT));
    }

    /**
     * 注册处理器,用户可以注册新的登录理器 或者 覆盖内置的处理器
     *
     * @param type      处理器类型
     * @param guavaCache 完成任务处理器
     */
    public static void registerGuavaCacheMap(String type, GuavaCache guavaCache) {
        guavaCacheMap.put(type, guavaCache);
    }

    /**
     * 获取实际缓存对象
     * @param type
     * @return
     */
    public static GuavaCache cache(String type){
        return guavaCacheMap.get(type);
    }


}

GuavaCaches

public class GuavaCaches {


    /**
     *
     * 过期策略:
     *     创建过期(expireAfterWrite): 基于缓存记录的插入时间判断。比如设定10分钟过期,则记录加入缓存之后,不管有没有访问,10分钟时间到则过期
     *     访问过期(expireAfterAccess):基于最后一次的访问时间来判断是否过期。比如设定10分钟过期,如果缓存记录被访问到,则以最后一次访问时间重新计时;
     *                                 只有连续10分钟没有被访问的时候才会过期,否则将一直存在缓存中不会被过期。
     */


    /**
     * 验证码缓存实现:过期时间60s 最多缓存1000个验证码
     *
     */
     static Cache<String, Object> CAPTCHA_CODE = CacheBuilder.newBuilder()
            .expireAfterWrite(1, TimeUnit.MINUTES) // 缓存项在写入1分钟后过期
            .maximumSize(1000) // 缓存最多存储1000个项
            .build();


    /**
     * 登录用户缓存实现:过期时间30m(基于访问的过期:在最后一次访问30分钟后过期)  最多缓存1000个用户
     *
     */
     static Cache<String, Object> LOGIN_TOKEN = CacheBuilder.newBuilder()
            .expireAfterAccess(30, TimeUnit.MINUTES) // 基于访问的过期:在最后一次访问30分钟后过期
            .maximumSize(1000) // 缓存最多存储1000个项
            .build();

    /**
     * 配置缓存实现:过期时间30m(基于访问的过期:在最后一次访问30分钟后过期) 最多缓存1000个配置信息
     *
     */
    static Cache<String, Object> SYS_CONFIG = CacheBuilder.newBuilder()
            .expireAfterAccess(30, TimeUnit.MINUTES) // 基于访问的过期:在最后一次访问30分钟后过期
            .maximumSize(1000) // 缓存最多存储1000个项
            .build();


    /**
     * 数据字典缓存实现:过期时间30m(基于访问的过期:在最后一次访问30分钟后过期) 最多缓存1000个配置信息
     *
     */
    static Cache<String, Object> SYS_DICT = CacheBuilder.newBuilder()
            .expireAfterAccess(30, TimeUnit.MINUTES) // 基于访问的过期:在最后一次访问30分钟后过期
            .maximumSize(1000) // 缓存最多存储1000个项
            .build();


    /**
     * 防重提交缓存实现:过期时间5s(基于创建的过期) 最多缓存1000个配置信息
     * TODO 由于原方案是用redis + lua 脚本实现限流处理的 GuavaCache 暂未找到好的配合Lua脚本实现限流处理的好的方案 所以这里暂未用到此缓存
     */
    static Cache<String, Object> REPEAT_SUBMIT = CacheBuilder.newBuilder()
            .expireAfterWrite(5000, TimeUnit.MILLISECONDS) // 缓存项在写入5s后过期
            .maximumSize(1000) // 缓存最多存储1000个项
            .build();

    /**
     * 登录账户密码错误次数缓存实现:过期时间10分钟(基于创建的过期) 最多缓存1000个配置信息
     *
     */
    static Cache<String, Object> PWD_ERR_CNT = CacheBuilder.newBuilder()
            .expireAfterWrite(10, TimeUnit.MINUTES) // 缓存项在写入5s后过期
            .maximumSize(1000) // 缓存最多存储1000个项
            .build();



}

3 缓存替换

1)验证码缓存替换

生成验证码类

@Slf4j
@RestController
public class CaptchaController
{
    @Resource(name = "captchaProducer")
    private Producer captchaProducer;

    @Resource(name = "captchaProducerMath")
    private Producer captchaProducerMath;
    
    @Autowired
    private ISysConfigService configService;
    /**
     * 生成验证码
     */
    @GetMapping("/captchaImage")
    public AjaxResult getCode(HttpServletResponse response) throws IOException {
        AjaxResult ajax = AjaxResult.success();
        boolean captchaEnabled = configService.selectCaptchaEnabled();
        ajax.put("captchaEnabled", captchaEnabled);
        if (!captchaEnabled)
        {
            return ajax;
        }
        CircleCaptcha captcha = CaptchaUtil.createCircleCaptcha(280, 100, 4, 20);

        // 保存验证码信息
        String uuid = IdUtils.simpleUUID();
        String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid;

        String code = null;

        code = captcha.getCode();

        GuavaCacheManager.cache(CacheConstants.CAPTCHA_CODE_KEY).put(verifyKey,code);

        ajax.put("uuid", uuid);
        ajax.put("img", captcha.getImageBase64());
        return ajax;
    }
}

注意由于ruoyi用Kaptcha生成的验证码实在是太丑了 这里我们改成了用hutool工具类生成的

 

校验验证码类

    /**
     * 校验验证码
     * 
     * @param username 用户名
     * @param code 验证码
     * @param uuid 唯一标识
     * @return 结果
     */
    public void validateCaptcha(String username, String code, String uuid)
    {
        boolean captchaEnabled = configService.selectCaptchaEnabled();
        if (captchaEnabled)
        {
            String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, "");

            String captcha = (String) GuavaCacheManager.cache(CacheConstants.CAPTCHA_CODE_KEY).get(verifyKey);

            GuavaCacheManager.cache(CacheConstants.CAPTCHA_CODE_KEY).remove(verifyKey);

            if (captcha == null)
            {
                AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));
                throw new CaptchaExpireException();
            }
            if (!code.equalsIgnoreCase(captcha))
            {
                AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
                throw new CaptchaException();
            }
        }
    }

最后用 GuavaCacheManager.cache(CacheConstants.CAPTCHA_CODE_KEY)的方法依次修改

 这几个地方的实现。限流处理由于没有找到Guava执行lua脚本方案 这里注释掉限流处理的注解和其切面类

 然后干掉redis配置,项目启动时就不需要redis了

但是这里有一个缺点:这里GuavaCache并没有持久化到本地,所以一旦项目重启 缓存就被清了,用户需要重新登录。