ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

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

分布式缓存详解:从原理到高并发实践 1. 分布式缓存1.1 什么是分布式缓存分布式缓存是指独立于业务服务和数据库之外的缓存系统多个应用实例可以通过网络共同访问它。常见的分布式缓存中间件有 Redis、Memcached 等。在高并发系统中如果每次请求都直接访问数据库数据库很容易成为性能瓶颈。分布式缓存的作用就是把高频访问、变化不那么频繁的数据提前放到缓存中让应用优先读取缓存从而减少数据库压力。典型读取流程如下客户端请求 ↓ 业务服务 ↓ 先查询缓存 ↓ 缓存命中直接返回 缓存未命中查询数据库 ↓ 数据库返回数据 ↓ 写入缓存 ↓ 返回客户端这种模式通常叫做 Cache Aside也叫旁路缓存模式。1.2 为什么需要分布式缓存分布式缓存主要解决以下问题降低数据库访问压力提升接口响应速度提高系统吞吐量承接突发流量减少重复计算降低复杂查询成本例如商品详情页、用户信息、首页配置、热门榜单、权限信息、字典数据等都适合放入缓存。1.3 本地缓存和分布式缓存的区别类型数据位置优点缺点本地缓存应用进程内速度快无网络开销多实例数据不一致容量有限分布式缓存独立缓存服务多实例共享容量更大有网络开销依赖缓存服务稳定性实际项目中经常会把本地缓存和分布式缓存结合使用应用服务 ↓ 本地缓存 ↓ Redis 分布式缓存 ↓ 数据库1.4 常见缓存数据适合缓存的数据通常有这些特点访问频率高数据变化频率低查询数据库成本高对实时一致性要求不是特别强可以接受短时间旧数据常见缓存对象包括用户基础信息商品详情商品类目首页配置活动配置权限菜单系统字典热门排行榜1.5 常见缓存读取代码publicUserDTOgetUser(LonguserId){Stringkeyuser:profile:userId;UserDTOcacheUserredisTemplate.opsForValue().get(key);if(cacheUser!null){returncacheUser;}UserDTOdbUseruserMapper.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 上增加随机时间。longbaseTtl30*60;longrandomTtlThreadLocalRandom.current().nextLong(5*60);longfinalTtlbaseTtlrandomTtl;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){Stringkeyproduct:detail:productId;ProductDTOcacheProductredisTemplate.opsForValue().get(key);if(cacheProduct!null){returncacheProduct.isEmpty()?null:cacheProduct;}ProductDTOdbProductproductMapper.selectById(productId);if(dbProductnull){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(){ListSystemConfigconfigsconfigMapper.selectAllEnabled();for(SystemConfigconfig:configs){Stringkeyconfig:config.getCode();redisTemplate.opsForValue().set(key,config,1,TimeUnit.HOURS);}}这种方式适合数据量小、加载速度快的场景。4.5 预热方式二定时任务预热通过定时任务定期刷新缓存。Scheduled(cron0 */10 * * * ?)publicvoidrefreshHotProductCache(){ListProductDTOproductsproductMapper.selectHotProducts();for(ProductDTOproduct:products){Stringkeyproduct: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());Stringkeyuser:profile:request.getUserId();redisTemplate.delete(key);}为什么是删除缓存而不是更新缓存因为数据库是最终数据源。删除缓存后下一次读取会重新查询数据库并写入缓存。这样可以减少缓存对象组装复杂、字段遗漏、并发覆盖等问题。5.3 常见策略二延迟双删在高并发场景下可能出现旧数据重新写入缓存的问题。典型流程请求 A 更新数据库 请求 B 查询缓存未命中 请求 B 查询数据库旧数据 请求 A 删除缓存 请求 B 把旧数据写入缓存可以使用延迟双删降低风险publicvoidupdateProduct(ProductUpdateRequestrequest){productMapper.updateById(request.toEntity());Stringkeyproduct: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{ConfigDTOconfigredisTemplate.opsForValue().get(config:global);if(config!null){localCache.put(config:global,config);returnconfig;}}catch(Exceptione){ConfigDTOlocalConfiglocalCache.getIfPresent(config:global);if(localConfig!null){returnlocalConfig;}}returndefaultConfig();}6.5 降级方式三限制数据库回源缓存异常时最危险的是所有请求直接打到数据库。可以对数据库回源做限流只允许少量请求访问数据库其他请求返回兜底数据非核心请求快速失败热点接口返回旧数据这样可以保护数据库不被瞬间打满。6.6 降级方式四关闭非核心功能在极端情况下可以临时关闭部分非核心能力。例如关闭个性化推荐关闭实时排行榜关闭复杂筛选关闭非核心统计关闭部分营销模块降低页面展示模块数量6.7 降级方式五配置开关控制降级能力应该通过配置中心动态控制而不是临时改代码发布。常见开关包括是否启用本地缓存兜底是否关闭某个页面模块是否限制数据库回源是否返回默认配置是否启用只读模式是否缩短接口超时时间6.8 缓存降级原则缓存降级需要提前设计不能等故障发生后再临时补。核心原则核心链路优先非核心功能可牺牲读请求可以返回旧数据写请求要谨慎降级资金、订单、权限类数据不能随意兜底降级行为必须有日志降级开关必须可快速恢复6.9 总结分布式缓存可以显著提升系统性能但它不是简单地加一个 Redis。真正稳定的缓存系统需要同时考虑缓存雪崩、缓存穿透、缓存预热、缓存更新和缓存降级。整体来看章节核心问题关键方案1. 分布式缓存如何用缓存提升系统性能Cache Aside、多级缓存、合理 TTL2. 缓存雪崩大量缓存同时失效TTL 随机化、逻辑过期、限流熔断3. 缓存穿透查询不存在的数据参数校验、空值缓存、布隆过滤器4. 缓存预热冷启动时缓存命中率低启动预热、定时预热、活动前预热5. 缓存更新数据库和缓存不一致更新数据库后删除缓存、MQ、CDC6. 缓存降级缓存异常时保护核心链路兜底数据、本地缓存、限流、开关控制一个成熟的分布式缓存方案应该具备高命中率、可控一致性、故障保护能力、清晰的更新策略和可快速启停的降级能力。缓存设计得好系统会更快、更稳缓存设计不好它也可能成为放大故障的关键因素。
返回列表