当前位置: 首页 > news >正文

分布式缓存详解:从原理到高并发实践

1. 分布式缓存

1.1 什么是分布式缓存

分布式缓存是指独立于业务服务和数据库之外的缓存系统,多个应用实例可以通过网络共同访问它。常见的分布式缓存中间件有 Redis、Memcached 等。

在高并发系统中,如果每次请求都直接访问数据库,数据库很容易成为性能瓶颈。分布式缓存的作用,就是把高频访问、变化不那么频繁的数据提前放到缓存中,让应用优先读取缓存,从而减少数据库压力。

典型读取流程如下:

客户端请求 ↓ 业务服务 ↓ 先查询缓存 ↓ 缓存命中:直接返回 缓存未命中:查询数据库 ↓ 数据库返回数据 ↓ 写入缓存 ↓ 返回客户端

这种模式通常叫做 Cache Aside,也叫旁路缓存模式。

1.2 为什么需要分布式缓存

分布式缓存主要解决以下问题:

  • 降低数据库访问压力
  • 提升接口响应速度
  • 提高系统吞吐量
  • 承接突发流量
  • 减少重复计算
  • 降低复杂查询成本

例如商品详情页、用户信息、首页配置、热门榜单、权限信息、字典数据等,都适合放入缓存。

1.3 本地缓存和分布式缓存的区别

类型数据位置优点缺点
本地缓存应用进程内速度快,无网络开销多实例数据不一致,容量有限
分布式缓存独立缓存服务多实例共享,容量更大有网络开销,依赖缓存服务稳定性

实际项目中,经常会把本地缓存和分布式缓存结合使用:

应用服务 ↓ 本地缓存 ↓ Redis 分布式缓存 ↓ 数据库

1.4 常见缓存数据

适合缓存的数据通常有这些特点:

  • 访问频率高
  • 数据变化频率低
  • 查询数据库成本高
  • 对实时一致性要求不是特别强
  • 可以接受短时间旧数据

常见缓存对象包括:

  • 用户基础信息
  • 商品详情
  • 商品类目
  • 首页配置
  • 活动配置
  • 权限菜单
  • 系统字典
  • 热门排行榜

1.5 常见缓存读取代码

publicUserDTOgetUser(LonguserId){Stringkey="user:profile:"+userId;UserDTOcacheUser=redisTemplate.opsForValue().get(key);if(cacheUser!=null){returncacheUser;}UserDTOdbUser=userMapper.selectById(userId);if(dbUser!=null){redisTemplate.opsForValue().set(key,dbUser,30,TimeUnit.MINUTES);}returndbUser;}

这个逻辑看起来简单,但在高并发场景下,会引出缓存雪崩、缓存穿透、缓存击穿、缓存更新和缓存降级等问题。

2. 缓存雪崩

2.1 什么是缓存雪崩

缓存雪崩是指大量缓存 key 在同一时间失效,或者缓存服务整体不可用,导致大量请求瞬间打到数据库,最终把数据库压垮。

常见场景包括:

  • 大量 key 设置了相同过期时间
  • Redis 集群故障
  • 缓存节点重启后大量缓存丢失
  • 活动开始时缓存还没有预热
  • 定时任务批量删除或刷新缓存

2.2 缓存雪崩的危害

缓存雪崩的危险在于它不是单个请求失败,而是可能引发链路级故障:

  • 数据库 QPS 突然升高
  • 数据库连接池被打满
  • 慢 SQL 增多
  • 应用线程阻塞
  • 上游服务重试加剧压力
  • 接口大量超时
  • 系统整体不可用

2.3 解决方案一:过期时间加随机值

不要让大量缓存 key 在同一时间过期。可以在基础 TTL 上增加随机时间。

longbaseTtl=30*60;longrandomTtl=ThreadLocalRandom.current().nextLong(5*60);longfinalTtl=baseTtl+randomTtl;redisTemplate.opsForValue().set(key,value,finalTtl,TimeUnit.SECONDS);

这样可以让缓存过期时间分散,避免集中失效。

2.4 解决方案二:热点数据逻辑过期

对于核心热点数据,可以不直接依赖 Redis 的物理过期时间,而是在缓存 value 中保存逻辑过期时间。

当请求发现数据逻辑过期时,不立即删除缓存,而是:

  • 先返回旧数据
  • 后台异步刷新缓存
  • 刷新完成后替换旧缓存

这种方式可以避免热点 key 过期瞬间所有请求都回源数据库。

2.5 解决方案三:多级缓存

可以使用本地缓存加 Redis 的方式降低 Redis 故障影响。

请求 ↓ 本地缓存 ↓ Redis ↓ 数据库

当 Redis 出现短暂异常时,本地缓存仍然可以承接一部分读流量。

2.6 解决方案四:限流、熔断和降级

缓存异常时,不能让所有请求都直接访问数据库。可以增加保护措施:

  • 对数据库回源请求限流
  • 对非核心接口熔断
  • 返回默认数据
  • 返回旧缓存数据
  • 临时关闭非核心功能

2.7 缓存雪崩总结

问题解决方式
大量 key 同时过期TTL 加随机值
热点 key 失效逻辑过期、异步刷新
Redis 故障多级缓存、限流、熔断
数据库被打满控制回源流量
活动流量突增提前缓存预热

3. 缓存穿透

3.1 什么是缓存穿透

缓存穿透是指请求查询的数据既不在缓存中,也不在数据库中。

由于数据库查不到数据,所以通常不会写入缓存。这样一来,每次请求都会绕过缓存,直接访问数据库。

例如:

  • 查询不存在的用户 ID
  • 查询不存在的商品 ID
  • 恶意构造随机订单号
  • 爬虫请求大量非法参数

3.2 缓存穿透的危害

缓存穿透会让缓存层失去保护作用。即使 Redis 正常,请求依然会持续打到数据库。

常见表现包括:

  • 缓存命中率下降
  • 数据库空查询增多
  • 数据库 QPS 异常升高
  • 接口响应时间变长
  • 非法请求拖垮正常业务

3.3 解决方案一:参数校验

最基础的方式是在请求入口做参数校验,把明显非法的请求挡在数据库之前。

例如:

  • ID 必须大于 0
  • 分页大小必须有上限
  • 枚举值必须合法
  • 查询时间范围不能过大
  • 业务标识必须符合格式

非法请求不应该继续访问缓存和数据库。

3.4 解决方案二:缓存空值

当数据库查询结果为空时,也把空结果写入缓存,并设置较短过期时间。

publicProductDTOgetProduct(LongproductId){Stringkey="product:detail:"+productId;ProductDTOcacheProduct=redisTemplate.opsForValue().get(key);if(cacheProduct!=null){returncacheProduct.isEmpty()?null:cacheProduct;}ProductDTOdbProduct=productMapper.selectById(productId);if(dbProduct==null){redisTemplate.opsForValue().set(key,ProductDTO.empty(),5,TimeUnit.MINUTES);returnnull;}redisTemplate.opsForValue().set(key,dbProduct,30,TimeUnit.MINUTES);returndbProduct;}

空值缓存要注意:

  • TTL 不要太长
  • 要区分缓存不存在和真实空值
  • 数据创建成功后要删除空值缓存
  • 防止大量随机 key 占用内存

3.5 解决方案三:布隆过滤器

布隆过滤器可以快速判断一个数据是否一定不存在。

请求流程:

请求进入 ↓ 布隆过滤器判断 ↓ 一定不存在:直接返回 可能存在:继续查询缓存 ↓ 缓存未命中再查询数据库

布隆过滤器适合大规模存在性判断,例如:

  • 商品 ID 是否存在
  • 用户 ID 是否存在
  • 订单号是否合法
  • 黑名单、白名单判断

它的特点是:

  • 查询速度快
  • 占用空间小
  • 可以判断一定不存在
  • 不能判断一定存在
  • 存在一定误判率

3.6 缓存穿透总结

问题解决方式
非法参数参数校验
数据不存在缓存空值
大量随机 key布隆过滤器
恶意请求限流、黑名单、风控
空值缓存过多设置短 TTL,控制 key 数量

4. 缓存预热

4.1 什么是缓存预热

缓存预热是指在系统正式承接流量之前,提前把核心热点数据加载到缓存中。

如果没有缓存预热,系统刚启动、刚发布或活动刚开始时,缓存命中率会很低,大量请求会直接访问数据库。

4.2 为什么需要缓存预热

缓存预热可以解决冷启动问题。

常见冷启动场景包括:

  • 服务刚发布
  • Redis 刚重启
  • 活动刚开始
  • 新功能刚上线
  • 缓存被批量清理
  • 大促前流量突然进入

如果不提前加载缓存,数据库可能在缓存建立起来之前就已经被打满。

4.3 适合预热的数据

适合预热的数据通常是访问量高、范围可预测的数据。

例如:

  • 首页配置
  • 热门商品
  • 活动商品
  • 秒杀库存
  • 推荐列表
  • 排行榜
  • 系统字典
  • 权限配置

不适合预热的数据:

  • 访问频率很低的数据
  • 数据量过大的冷数据
  • 变化极其频繁的数据
  • 无法提前预测访问范围的数据

4.4 预热方式一:应用启动时预热

服务启动后加载少量核心配置。

@PostConstructpublicvoidwarmUpCache(){List<SystemConfig>configs=configMapper.selectAllEnabled();for(SystemConfigconfig:configs){Stringkey="config:"+config.getCode();redisTemplate.opsForValue().set(key,config,1,TimeUnit.HOURS);}}

这种方式适合数据量小、加载速度快的场景。

4.5 预热方式二:定时任务预热

通过定时任务定期刷新缓存。

@Scheduled(cron="0 */10 * * * ?")publicvoidrefreshHotProductCache(){List<ProductDTO>products=productMapper.selectHotProducts();for(ProductDTOproduct:products){Stringkey="product:detail:"+product.getId();redisTemplate.opsForValue().set(key,product,30,TimeUnit.MINUTES);}}

适合榜单、热门商品、推荐数据等场景。

4.6 预热方式三:活动前批量预热

在秒杀、大促、直播等场景中,需要在活动开始前提前预热缓存。

预热内容通常包括:

  • 活动信息
  • 商品详情
  • 库存信息
  • 价格信息
  • 优惠信息
  • 限购规则

预热任务要支持重复执行,避免失败后无法重试。

4.7 缓存预热注意事项

缓存预热不是越多越好。

需要注意:

  • 控制预热数据范围
  • 分批加载,避免瞬间打满 Redis
  • 预热失败要有告警
  • 预热任务要支持重试
  • 预热过程要记录日志
  • 避免大量冷数据挤占缓存空间

5. 缓存更新

5.1 为什么缓存更新复杂

缓存更新的核心问题是:数据库和缓存是两个独立系统。

只要存在两个存储系统,就一定会面临一致性问题。例如:

  • 数据库更新成功,缓存删除失败
  • 缓存更新成功,数据库事务回滚
  • 并发读写导致旧数据重新写入缓存
  • 多个服务都能修改同一份数据
  • 消息异步刷新失败

所以缓存更新不能只看单次操作成功,还要考虑并发、失败和补偿。

5.2 常见策略一:先更新数据库,再删除缓存

这是业务系统中最常见的方案。

@TransactionalpublicvoidupdateUser(UserUpdateRequestrequest){userMapper.updateById(request.toEntity());Stringkey="user:profile:"+request.getUserId();redisTemplate.delete(key);}

为什么是删除缓存,而不是更新缓存?

因为数据库是最终数据源。删除缓存后,下一次读取会重新查询数据库并写入缓存。这样可以减少缓存对象组装复杂、字段遗漏、并发覆盖等问题。

5.3 常见策略二:延迟双删

在高并发场景下,可能出现旧数据重新写入缓存的问题。

典型流程:

请求 A 更新数据库 请求 B 查询缓存未命中 请求 B 查询数据库旧数据 请求 A 删除缓存 请求 B 把旧数据写入缓存

可以使用延迟双删降低风险:

publicvoidupdateProduct(ProductUpdateRequestrequest){productMapper.updateById(request.toEntity());Stringkey="product:detail:"+request.getProductId();redisTemplate.delete(key);delayQueue.submit(()->redisTemplate.delete(key),500,TimeUnit.MILLISECONDS);}

延迟双删不是强一致方案,只是降低并发场景下旧缓存残留的概率。

5.4 常见策略三:消息队列异步刷新

数据库更新成功后发送消息,由消费者异步删除或刷新缓存。

优点:

  • 解耦业务逻辑和缓存处理
  • 支持失败重试
  • 适合跨服务缓存同步
  • 可以记录消费状态

缺点:

  • 存在短暂延迟
  • 依赖消息可靠性
  • 需要处理重复消费
  • 需要补偿机制

5.5 常见策略四:监听数据库变更

可以通过 Binlog 或 CDC 监听数据库变更,然后自动删除或刷新缓存。

适合以下场景:

  • 多个服务都能修改同一张表
  • 缓存更新入口很多
  • 很难在业务代码中覆盖所有变更点
  • 需要统一处理缓存失效

5.6 缓存更新建议

场景推荐方案
普通业务数据更新数据库后删除缓存
高并发热点数据删除缓存加互斥锁或逻辑过期
跨服务数据变更消息队列或 CDC
强一致数据不依赖缓存作为最终结果
更新频繁数据缩短 TTL,减少复杂缓存
删除失败重试、补偿、告警

5.7 缓存 Key 设计建议

好的 key 设计可以降低缓存更新难度。

建议:

  • key 中包含业务域
  • key 中包含唯一 ID
  • 命名规则统一
  • 避免超长 key
  • 避免把不稳定参数拼进 key
  • 批量缓存要考虑局部失效问题

示例:

user:profile:{userId} product:detail:{productId} activity:sku:{activityId}:{skuId} config:global:{configKey}

6. 缓存降级

6.1 什么是缓存降级

缓存降级是指当缓存系统异常、数据库压力过高或接口响应变慢时,系统主动降低部分功能或数据实时性要求,优先保证核心链路可用。

降级的本质是取舍:异常情况下,不追求所有功能都完整,而是优先保证系统不被拖垮。

6.2 什么时候需要缓存降级

常见触发场景包括:

  • Redis 连接超时
  • Redis 集群故障
  • 缓存命中率突然下降
  • 数据库连接池接近打满
  • 核心接口响应时间明显升高
  • 活动流量超过预估
  • 下游服务不可用

6.3 降级方式一:返回兜底数据

当缓存和数据库都不可用时,可以返回默认数据或上一次成功的数据。

适合场景:

  • 首页推荐
  • 活动配置
  • 排行榜
  • 商品非核心字段
  • 用户展示信息

不适合场景:

  • 支付金额
  • 账户余额
  • 订单状态
  • 库存扣减
  • 权限核心判断

6.4 降级方式二:本地缓存兜底

业务服务可以保存最近一次成功读取的数据。当 Redis 出现异常时,短时间内返回本地缓存。

publicConfigDTOgetGlobalConfig(){try{ConfigDTOconfig=redisTemplate.opsForValue().get("config:global");if(config!=null){localCache.put("config:global",config);returnconfig;}}catch(Exceptione){ConfigDTOlocalConfig=localCache.getIfPresent("config:global");if(localConfig!=null){returnlocalConfig;}}returndefaultConfig();}

6.5 降级方式三:限制数据库回源

缓存异常时,最危险的是所有请求直接打到数据库。

可以对数据库回源做限流:

  • 只允许少量请求访问数据库
  • 其他请求返回兜底数据
  • 非核心请求快速失败
  • 热点接口返回旧数据

这样可以保护数据库不被瞬间打满。

6.6 降级方式四:关闭非核心功能

在极端情况下,可以临时关闭部分非核心能力。

例如:

  • 关闭个性化推荐
  • 关闭实时排行榜
  • 关闭复杂筛选
  • 关闭非核心统计
  • 关闭部分营销模块
  • 降低页面展示模块数量

6.7 降级方式五:配置开关控制

降级能力应该通过配置中心动态控制,而不是临时改代码发布。

常见开关包括:

  • 是否启用本地缓存兜底
  • 是否关闭某个页面模块
  • 是否限制数据库回源
  • 是否返回默认配置
  • 是否启用只读模式
  • 是否缩短接口超时时间

6.8 缓存降级原则

缓存降级需要提前设计,不能等故障发生后再临时补。

核心原则:

  • 核心链路优先
  • 非核心功能可牺牲
  • 读请求可以返回旧数据
  • 写请求要谨慎降级
  • 资金、订单、权限类数据不能随意兜底
  • 降级行为必须有日志
  • 降级开关必须可快速恢复

6.9 总结

分布式缓存可以显著提升系统性能,但它不是简单地加一个 Redis。真正稳定的缓存系统,需要同时考虑缓存雪崩、缓存穿透、缓存预热、缓存更新和缓存降级。

整体来看:

章节核心问题关键方案
1. 分布式缓存如何用缓存提升系统性能Cache Aside、多级缓存、合理 TTL
2. 缓存雪崩大量缓存同时失效TTL 随机化、逻辑过期、限流熔断
3. 缓存穿透查询不存在的数据参数校验、空值缓存、布隆过滤器
4. 缓存预热冷启动时缓存命中率低启动预热、定时预热、活动前预热
5. 缓存更新数据库和缓存不一致更新数据库后删除缓存、MQ、CDC
6. 缓存降级缓存异常时保护核心链路兜底数据、本地缓存、限流、开关控制

一个成熟的分布式缓存方案,应该具备高命中率、可控一致性、故障保护能力、清晰的更新策略和可快速启停的降级能力。缓存设计得好,系统会更快、更稳;缓存设计不好,它也可能成为放大故障的关键因素。

http://www.jsqmd.com/news/1403298/

相关文章:

  • 巴中市正规防水补漏维修公司口碑实力怎么样_屋顶漏水团队好坏分辨技巧,居民选购参考思路,业主经验 - 雨婺虹修缮
  • HZ-RK3506_MiniEVM开发板-SDK编译
  • Windows下MinGW-w64环境配置与C/C++开发实战指南
  • 2026年8月新发布海参罐头贴牌代加工厂家,五家风格各异的专业机构解析 - 工业推荐榜
  • Claude Code 循环校验把我账单拉爆了:三层熔断才止住血
  • OpenClaw架构解析:构建可控AI Agent的7层管道与4大模块
  • Git远程仓库地址重置与分支管理实战指南
  • 2026上海海参罐头代加工品牌商实力测评,所见即所得避坑指南 - myqiye
  • 银河麒麟V10 SP2服务器版Docker 20.10离线安装指南
  • 5款AI写论文哪个好?书匠策AI凭实力出圈
  • 【Linux】磁盘与文件系统
  • OpenClaw前端技术栈解析:React/Next.js在AI智能体项目中的选型与实践
  • 旅行分装瓶制造商靠谱推荐,价格透明零套路不踩雷 - 工业推荐榜
  • 基于符号链接的 Windows C 盘开发环境迁移实践
  • 安徽企业怎么选专业抖音代运营?痛点破解与定制方案全解析,抖音推广/抖音代运营/GEO优化,抖音代运营服务商口碑推荐 - 企业权威推荐大使
  • Win11 Realtek音频管理器消失?从驱动原理到修复方案全解析
  • 开发辅助工具环境配置与生产集成最佳实践指南
  • AI面试助手:苏格拉底式引导与模拟面试提升算法能力
  • OpenSSH Server与SFTP配置实战指南
  • Altium Designer 20 完整安装与破解指南:从环境准备到实战验证
  • PDF转Word拆分太折腾?律师实测200页合同全程15秒搞定
  • 临汾建房怎么实施?环境、步骤和结果如何验证
  • Windows系统0xc000007b错误全解析:从运行库修复到Xshell启动故障排除
  • 中高端改善大平层装修攻略,设计落地强户型改造储物规划好,实景还原度高,供应链成熟一站式服务,实力测评 - myqiye
  • 飞书CLI开源:AI Agent办公自动化的执行层基础设施
  • 基于MiniMax H3与ComfyUI的低成本AI视频生成实战指南
  • 项目管家跟进进度,西安老牌装修公司设计落地强口碑推荐 - 工业推荐榜
  • 关于pnpm 部分使用(一)
  • Figma源文件导出全攻略:从备份到交付的协作规范与避坑指南
  • 【算法】什么是 DFS 与 BFS 及他们的区别