后端_本地缓存:零抖动的极速缓冲层
为什么要“本地缓存”?
把“不会变”或“缓慢变”的热数据拉到 JVM,性能直接提升 1~2 个数量级,同时 零网络、零序列化、零 GC 压力。
2. 架构全景图
┌-----------------┐ ┌-----------------┐
│ Admin 后台改数据 │ ----► │ 写操作触发刷新 -> XXXProducer.send()
└-----------------┘ └-----------------┘│ │▼ Redis Pub/Sub 广播 ▼
┌-----------------┐ ┌-----------------┐
│ JVM-1 本地缓存 │◄-------┤ XXXRefreshConsumer
└-----------------┘ └-----------------┘▲│----┐│ 各实例并行刷新▼----┘
┌-----------------┐
│ JVM-n 本地缓存 │
└-----------------┘
关键约定
- 只缓存“读多写少”数据(角色、部门、字典)
- 缓存对象 不可变,杜绝并发修改隐患
- 刷新动作 幂等(全量 Map 替换,无并发锁)
3. 编码三步曲
3.1 定义接口:把“刷新”暴露出去
// 目的:Consumer 与 Service 解耦,方便单元测试 Mock。
public interface XxxService {/*** 全量刷新本地缓存,幂等*/void initLocalCache();
}
3.2 启动时一次性预热
@Service
public class XxxServiceImpl implements XxxService {// Map 替换是原子操作/** volatile 保证可见性,刷新即“指向替换” */private volatile Map<Long, XXXDto> xxxCache = Collections.emptyMap();@PostConstruct // ← 项目启动完成即执行public void initLocalCache() {List<XXXDto> list = roleMapper.selectList(); // ① 读库xxxCache = list.stream().collect(Collectors.toMap(XXXDto::getId, Function.identity()));log.info("[initLocalCache] 角色缓存预热完成,size={}", xxxCache.size());}/** 对外查询接口,O(1) */public RoleDO getXxxFromCache(Long xxxId) {return xxxCache.get(xxxId); // 直接内存命中}
}
3.3 Redis 广播刷新(多实例一致)
为什么需要使用 Redis Pub/Sub 来实时刷新缓存?考虑到高可用,线上会部署多个 JVM 实例,需要通过 Redis 广播到所有实例,实现本地缓存的刷新。
① 消息实体
// AbstractRedisChannelMessage 自行实现或者参考网上开源源码
@Data
public class XxxRefreshMessage extends AbstractRedisChannelMessage {// 空即可,只做信号
}
② 生产者(写操作后发送)
@Component
@RequiredArgsConstructor
public class XxxProducer {private final RedisMQTemplate redisMQTemplate;public void sendRoleRefreshMessage() {redisMQTemplate.send(new XxxRefreshMessage());}
}
在 Service 的 save/update/delete 方法最后一行调用即可。
③ 消费者(所有实例同时刷新)
@Component
@Slf4j
public class XxxRefreshConsumer extends AbstractRedisChannelMessageListener<XxxRefreshMessage> {private final XxxService xxxService;@Overridepublic void onMessage(XxxRefreshMessage msg) {log.info("<<Redis<< 收到Xx刷新信号");xxxService.initLocalCache(); // 全量重新加载}
}
4. 高级增强(按需取用)
能力 | 实现思路 | 收益 |
---|---|---|
局部刷新 | 消息体带上 changedXxxIds ,只替换指定条目 | 大表场景减少 GC |
延迟刷新 | 发送端使用 RedissonDelayedQueue 聚合 3 s 内的变更 | 削峰填谷 |
灰度刷新 | 在消息附加 grayFlag ,结合 SpringProfile 判断 | 预发环境先验证 |
5. 常见问题
- “我改了数据库,但别的实例没生效”
→ 检查 Redis 订阅是否成功MONITOR
频道。 - “偶尔出现旧数据”
→ Map 未加volatile
或返回的是原对象引用(需深拷贝)。 - “刷新瞬间 CPU 飙高”
→ 数据量过大,改为分页加载 + 增量更新。 - “项目启动报 NPE”
→@PostConstruct
里调用了其他未初始化完的 Bean,改用ApplicationRunner
。
6. 结语
本地缓存是投入产出比最高的性能优化手段,但务必遵守两条铁律:
- 只缓存“读多写少”数据
- 刷新必须“幂等 + 广播”
照本指南落地,平均接口 RT 从 30 ms 降至 1 ms 以内,数据库连接数下降 60 % 以上——实打实的“老板看得见”的优化。