后端_本地缓存:零抖动的极速缓冲层

为什么要“本地缓存”?

把“不会变”或“缓慢变”的热数据拉到 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;

  @Override
  public 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. 结语

本地缓存是投入产出比最高的性能优化手段,但务必遵守两条铁律:

  1. 只缓存“读多写少”数据
  2. 刷新必须“幂等 + 广播”

照本指南落地,平均接口 RT 从 30 ms 降至 1 ms 以内,数据库连接数下降 60 % 以上——实打实的“老板看得见”的优化。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值