# scaffold 项目之本地缓存
# 简介
为什么项目中有 redis缓存 还需要 本地缓存 呢?
Redis 缓存:适用于需要分布式、高并发、大容量数据缓存的场景,尤其是数据需要共享的场景。
本地缓存:适用于单个服务节点内部需要高速访问、低延迟的小数据量缓存场景。还有一点是: 对于 有状态的 java 对象无法存到 Redis 中。
关于 有状态 和 无状态 的区别:
- 有状态对象:具有内部状态,行为和结果依赖于该状态,适用于需要跟踪对象行为和数据变化的场景。
- 无状态对象:没有内部状态,行为和结果仅取决于输入参数,适用于需要提供一致行为和结果的场景。
# 实现原理
本地缓存的实现,一共有两步:
- 项目启动 -> 初始化到本地缓存(内存)中,从数据库中读取数据,写入到本地缓存(例如说一个 Map 对象)
- 数据变化 -> 实时刷新缓存 (例如说通过管理后台修改数据)重新从数据库中读取数据,重新写入到本地缓存
# 开始使用
本项目的 敏感词 模块举例子,我们项目中需要隐藏或者过滤某些敏感的内容,那么就需要给定哪些是敏感的内容,在通过查询出来的结果过滤, 那么敏感词这个基本不会经常变动,程序启动的时候就可以加载到缓存中,就可以减少数据库的查询
# 初始化缓存
定义一个 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);
}通过
@PostConstruct注解,在项目启动时进行本地缓存的初始化。代码如下:/*** 敏感词列表缓存
*/
@Getterprivate volatile List<SensitiveWordDO> sensitiveWordCache = Collections.emptyList();
@PostConstructpublic 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这个注解的作用是:- 初始化方法:
@PostConstruct注解的方法会在类的实例被创建后立即执行,通常用于初始化资源、配置或状态。 - 确保初始化顺序:确保在类的其他方法被调用之前,某些初始化逻辑已经完成。
- 依赖注入后执行:在依赖注入(Dependency Injection)完成后执行,确保所有依赖项已经注入。
- 初始化方法:
# 实时刷新缓存
为什么需要使用 Redis Pub/Sub 来实时刷新缓存?考虑到高可用,线上会部署多个 JVM 实例,需要通过 Redis 广播到所有实例,实现本地缓存的刷新。
具体流程就是:
修改数据成功后生产消息放入广播
// 发送消息要使用这种方式:// 放在 insert 或者 update 后 必须在事务提交后,在发起任务,否则 数据 还没入库,就提前回调接入的业务TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Overridepublic void afterCommit() {
sendMessage();
}});
广播出去后所有实例都能收到,都进行消费
@Overridepublic void onMessage(Message message) {
log.info("[onMessage][收到缓存刷新消息]");
service.initLocalCache();
}