# scaffold 项目之本地缓存

# 简介

为什么项目中有 redis缓存 还需要 本地缓存 呢?

Redis 缓存:适用于需要分布式、高并发、大容量数据缓存的场景,尤其是数据需要共享的场景。

本地缓存:适用于单个服务节点内部需要高速访问、低延迟的小数据量缓存场景。还有一点是: 对于 有状态的 java 对象无法存到 Redis 中。

关于 有状态无状态 的区别:

  • 有状态对象:具有内部状态,行为和结果依赖于该状态,适用于需要跟踪对象行为和数据变化的场景。
  • 无状态对象:没有内部状态,行为和结果仅取决于输入参数,适用于需要提供一致行为和结果的场景。

# 实现原理

本地缓存的实现,一共有两步:

  1. 项目启动 -> 初始化到本地缓存(内存)中,从数据库中读取数据,写入到本地缓存(例如说一个 Map 对象)
  2. 数据变化 -> 实时刷新缓存 (例如说通过管理后台修改数据)重新从数据库中读取数据,重新写入到本地缓存

# 开始使用

本项目的 敏感词 模块举例子,我们项目中需要隐藏或者过滤某些敏感的内容,那么就需要给定哪些是敏感的内容,在通过查询出来的结果过滤, 那么敏感词这个基本不会经常变动,程序启动的时候就可以加载到缓存中,就可以减少数据库的查询

# 初始化缓存

  1. 定义一个 initLocalCache () 方法:

    public void initLocalCache() {
        if (!ENABLED) {
            return;
        }
        // 第一步:查询数据
        List<SensitiveWordDO> sensitiveWords = sensitiveWordMapper.selectList();
        log.info("[initLocalCache][缓存敏感词,数量为:{}]", sensitiveWords.size());
        // 第二步:构建缓存
        // 写入 sensitiveWordTagsCache 缓存
        Set<String> tags = new HashSet<>();
        sensitiveWords.forEach(word -> tags.addAll(word.getTags()));
        sensitiveWordTagsCache = tags;
        sensitiveWordCache = sensitiveWords;
        // 写入 defaultSensitiveWordTrie、tagSensitiveWordTries 缓存
        initSensitiveWordTrie(sensitiveWords);
    }
  2. 通过 @PostConstruct 注解,在项目启动时进行本地缓存的初始化。代码如下:

    /**
     * 敏感词列表缓存
    */
    @Getter
    private volatile List<SensitiveWordDO> sensitiveWordCache = Collections.emptyList();
    @PostConstruct
    public void initLocalCache() {
        if (!ENABLED) {
            return;
        }
        // 第一步:查询数据
        List<SensitiveWordDO> sensitiveWords = sensitiveWordMapper.selectList();
        log.info("[initLocalCache][缓存敏感词,数量为:{}]", sensitiveWords.size());
        // 第二步:构建缓存
        // 写入 sensitiveWordTagsCache 缓存
        Set<String> tags = new HashSet<>();
        sensitiveWords.forEach(word -> tags.addAll(word.getTags()));
        sensitiveWordTagsCache = tags;
        sensitiveWordCache = sensitiveWords;
        // 写入 defaultSensitiveWordTrie、tagSensitiveWordTries 缓存
        initSensitiveWordTrie(sensitiveWords);
    }

    @PostConstruct 这个注解的作用是:

    1. 初始化方法@PostConstruct 注解的方法会在类的实例被创建后立即执行,通常用于初始化资源、配置或状态。
    2. 确保初始化顺序:确保在类的其他方法被调用之前,某些初始化逻辑已经完成。
    3. 依赖注入后执行:在依赖注入(Dependency Injection)完成后执行,确保所有依赖项已经注入。

# 实时刷新缓存

为什么需要使用 Redis Pub/Sub 来实时刷新缓存?考虑到高可用,线上会部署多个 JVM 实例,需要通过 Redis 广播到所有实例,实现本地缓存的刷新。

具体流程就是:

  1. 修改数据成功后生产消息放入广播

    // 发送消息要使用这种方式:
    // 放在 insert 或者 update 后 必须在事务提交后,在发起任务,否则 数据 还没入库,就提前回调接入的业务
    TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
        @Override
        public void afterCommit() {
            sendMessage();
        }
    });
  2. 广播出去后所有实例都能收到,都进行消费

    @Override
    public void onMessage(Message message) {
        log.info("[onMessage][收到缓存刷新消息]");
        service.initLocalCache();
    }