Day30:Redis 缓存策略 + 菜单实战缓存 + 三大缓存问题(穿透 / 击穿 / 雪崩)

一、今日学习目标

  1. 掌握字典缓存、分布式缓存概念
  2. 吃透缓存穿透、缓存击穿、缓存雪崩原理 + 解决方案
  3. 实战:把菜单树、角色权限加入 Redis 缓存
  4. 适配 DDD 架构,缓存和业务解耦
  5. 整理高频面试题标准答案

二、基础概念

1. 本地内存缓存 vs 分布式缓存

本地缓存(MemoryCache)

  • 进程内缓存,每个服务独立一份
  • 集群 / 多实例不共享
  • 优点:速度最快、无网络开销
  • 缺点:数据不一致、无法跨服务共享

分布式缓存(Redis)

  • 独立中间件,所有服务共用一份缓存
  • 集群、微服务、多实例数据共享
  • 可设置过期、持久化、支持多种数据结构
  • 后台管理系统必用 Redis

2. 字典缓存

不常变动、全局共用的数据放缓存:

  • 菜单树形列表
  • 角色列表
  • 数据字典
  • 系统配置特点:改动少、查询多、适合全局缓存

三、三大缓存问题 原理 + 企业级解决方案(面试必背)

1. 缓存穿透

现象

请求数据库不存在的数据,缓存也没有,每次都直接打数据库。例如:查不存在的用户 ID、恶意伪造大量不存在 Key。

解决方案

  1. 缓存空值,设置短期过期
  2. 布隆过滤器拦截不存在 Key
  3. 接口参数校验、非法参数直接拦截

2. 缓存击穿

现象

热点 Key 刚好过期,瞬间大量请求同时打到数据库。例如:首页菜单、热门角色配置。

解决方案

  1. 互斥锁(分布式锁)同一时间只放行一个请求查库
  2. 热点 Key 永不过期
  3. 后台定时主动刷新缓存

3. 缓存雪崩

现象

大量缓存 Key 同一时间过期,瞬间全部请求压垮数据库;或 Redis 宕机。

解决方案

  1. 给过期时间加随机偏移,避免同时失效
  2. Redis 集群高可用(主从 + 哨兵)
  3. 多级缓存:本地缓存 + Redis
  4. 服务熔断、降级限流

四、高频面试题 + 标准答案

1. 为什么菜单、角色适合做缓存?

菜单 / 角色改动少、查询频率极高,每次登录、侧边栏渲染都要查库,缓存后大幅减少 DB 压力,提升接口响应速度。

2. 分布式缓存和本地缓存区别?

本地缓存进程独享、不支持多实例共享;Redis 分布式缓存全局共享,适合集群、微服务项目,支持过期、持久化、原子操作。

3. 怎么保证缓存和数据库一致性?

  • 更新 / 删除时:先改库,再删缓存
  • 不做更新缓存,直接删除,下次查询自动重建
  • 强一致性场景可用:队列异步延迟删缓存

4. 为什么不建议缓存永久不过期?

数据变更后缓存不会自动更新,容易脏数据,必须手动删缓存刷新。


五、实战练习:菜单信息接入 Redis 缓存(DDD 架构直接可用)

业务思路

  1. 查询菜单树 → 先查 Redis
  2. 有缓存直接返回
  3. 无缓存查数据库,写入 Redis 再返回
  4. 新增 / 编辑 / 删除菜单、分配角色菜单时 → 清空对应缓存,保证一致性

1. 缓存 Key 规范

复制代码
menu:tree:all        // 全量菜单树
menu:user:{userId}  // 当前用户菜单树

① 获取全量菜单树(加缓存)

cs 复制代码
// 缓存Key
private const string AllMenuTreeKey = "menu:tree:all";

public async Task<R<List<MenuVo>>> GetTreeListAsync()
{
    // 1.先查Redis
    var cacheData = await _redis.GetAsync<List<MenuVo>>(AllMenuTreeKey);
    if (cacheData != null && cacheData.Any())
    {
        return R<List<MenuVo>>.Success(cacheData);
    }

    // 2.缓存没有,查库
    var allMenus = await _uow.MenuRepository.GetAllAsync();
    var voList = _mapper.Map<List<MenuVo>>(allMenus);
    var tree = BuildMenuTree(voList, 0);

    // 3.写入Redis 缓存30分钟
    await _redis.SetAsync(AllMenuTreeKey, tree, 30);

    return R<List<MenuVo>>.Success(tree);
}

② 获取当前用户菜单树(加缓存)

cs 复制代码
public async Task<R<List<MenuVo>>> GetUserMenuTreeAsync(long userId)
{
    string key = $"menu:user:{userId}";

    // 1.读缓存
    var cacheData = await _redis.GetAsync<List<MenuVo>>(key);
    if (cacheData != null && cacheData.Any())
    {
        return R<List<MenuVo>>.Success(cacheData);
    }

    // 2.查库逻辑不变...
    // ...省略原有查角色、查菜单、建树代码

    // 3.写入缓存
    await _redis.SetAsync(key, tree, 30);

    return R<List<MenuVo>>.Success(tree);
}

③ 菜单新增 / 修改 / 删除、角色分配菜单 清空缓存

任意菜单变更、角色分配菜单后,删除所有菜单相关缓存

cs 复制代码
// 清除菜单缓存
await _redis.DeleteAsync(AllMenuTreeKey);
// 可按需清空所有用户菜单缓存,简单项目直接清全量即可

规范:改库 → 删缓存 → 下次访问自动重建缓存


六、今日任务清单

✅ 理解本地缓存 / 分布式缓存区别

✅ 背熟 缓存穿透、击穿、雪崩 原理 + 方案

✅ 自定义菜单缓存 Key 规范

✅ 改造菜单树接口接入 Redis

✅ 菜单变动主动清空缓存,保证数据一致

✅ 整理本周 Redis 面试题,可直接背面试

相关推荐
小七-七牛开发者12 小时前
TokenPilot:让 LLM Agent 长会话成本降 60%+ 的上下文管理
缓存·agent·token·context·上下文·推理成本
ClouGence18 小时前
Oracle 数据同步为什么会出现数据不一致?长事务是常被忽略的原因
数据库·后端·oracle
飞将20 小时前
从零实现数据库(2)——HashIndex + IndexManager
数据库
Nturmoils2 天前
订单列表慢查询,先看 WHERE、ORDER BY 和 LIMIT
数据库
渣波2 天前
拒绝 SQL 焦虑!手把手带你用 NestJS + Prisma + DTO 写出“防弹”级后端代码
javascript·数据库·后端
倔强的石头_3 天前
KingbaseES 新版MySQL 兼容版体验:旧版迁移 + 功能实测
数据库
用户3169353811836 天前
Java连接Redis
redis
倔强的石头_6 天前
《Kingbase护城河》——数据库存储空间全景探测与精细化瘦身实战
数据库
冬奇Lab6 天前
每日一个开源项目(第134篇):Zvec - 阿里开源的嵌入式向量数据库,向量搜索界的 SQLite
数据库·人工智能·llm
ClouGence7 天前
Oracle CDC 架构优化:从主库直连到 DataGuard 备库同步
数据库·后端·oracle