个人信息网站建设方案书框架栏目北京做网站男生工资
个人信息网站建设方案书框架栏目,北京做网站男生工资,网页图片下载工具,十大广告设计公司简介#x1f343; 予枫#xff1a;个人主页#x1f4da; 个人专栏: 《Java 从入门到起飞》《读研码农的干货日常》#x1f4bb; Debug 这个世界#xff0c;Return 更好的自己#xff01; 引言 做后端开发的同学#xff0c;几乎都逃不开MySQL与Redis的组合使用——用Redis做缓… 予枫个人主页 个人专栏: 《Java 从入门到起飞》《读研码农的干货日常》 Debug 这个世界Return 更好的自己引言做后端开发的同学几乎都逃不开MySQL与Redis的组合使用——用Redis做缓存减轻数据库压力却常被缓存与数据库数据不一致的问题搞得头大读缓存命中脏数据、写操作后缓存同步不及时、并发场景下数据错乱… 这篇文章就聚焦两个经典解决方案Cache Aside Pattern旁路缓存模式和延迟双删策略从原理拆解到实战落地帮你彻底搞定缓存一致性难题面试和工作都能用文章目录引言一、缓存一致性核心问题剖析1.1 常见不一致场景1.2 核心诉求最终一致性二、CACHE ASIDE PATTERN旁路缓存模式2.1 核心原理读/写分离2.1.1 读操作流程3步走2.1.2 写操作流程3步走2.2 适用场景与优势适用场景核心优势2.3 存在的问题三、延迟双删策略解决更新场景的一致性难题3.1 核心原理4步走3.2 关键解析为什么要“双删延迟”第一次删缓存延迟一段时间第二次删缓存3.3 适用场景与优势适用场景核心优势3.4 关键注意事项四、实战代码示例JavaSpring Boot4.1 环境依赖pom.xml核心依赖4.2 Cache Aside Pattern实现商品查询更新4.3 延迟双删策略优化基于RabbitMQ延迟队列4.3.1 延迟队列配置4.3.2 延迟双删实现优化写操作五、常见问题与避坑指南5.1 缓存删除失败怎么办5.2 如何避免缓存穿透5.3 高并发场景下的极致优化六、总结一、缓存一致性核心问题剖析在聊解决方案前我们得先搞懂为什么MySQL和Redis会出现数据不一致核心原因就3点尤其是并发场景下更突出核心矛盾缓存与数据库是两个独立存储写操作无法同时原子性完成读操作可能读取到未同步的旧数据。1.1 常见不一致场景更新场景先更数据库再删缓存删缓存失败导致缓存存旧数据先删缓存再更数据库更新期间读请求缓存未命中查数据库旧数据写入缓存造成脏数据。并发场景一个写操作删缓存更数据库和一个读操作同时执行读操作在写操作更新数据库前读取旧数据并写入缓存。缓存过期/失效缓存过期后大量请求穿透到数据库同时有写操作导致部分请求读取到中间态数据。1.2 核心诉求最终一致性这里要明确分布式系统中很难做到强一致性成本极高我们追求的是最终一致性——即经过短暂时间后缓存数据与数据库数据完全同步满足业务需求。 小提示如果你的业务对数据一致性要求极高如金融交易可能需要引入分布式锁等更复杂的机制本文聚焦大多数业务场景的经典方案~二、CACHE ASIDE PATTERN旁路缓存模式Cache Aside Pattern是最经典、最常用的缓存策略核心思路是“读走缓存、写走数据库”缓存只作为“旁路”辅助不参与核心写流程。2.1 核心原理读/写分离2.1.1 读操作流程3步走客户端请求数据时先查询Redis缓存若缓存命中存在且未过期直接返回缓存数据结束流程若缓存未命中查询MySQL数据库将查询结果写入Redis缓存设置合理过期时间再返回结果给客户端。是否客户端请求数据缓存命中返回缓存数据查询MySQL数据库将结果写入Redis2.1.2 写操作流程3步走客户端发起写请求先更新MySQL数据库数据库更新成功后删除Redis缓存而非更新缓存返回写操作成功结果给客户端。❌ 为什么是“删缓存”而非“更缓存”避免更新缓存成本过高如复杂计算后的结果减少并发场景下的一致性问题更新缓存和更新数据库无法原子化下次读请求自然会从数据库加载最新数据并重建缓存。成功失败客户端写请求更新MySQL数据库删除Redis缓存返回写成功结果返回写失败结果2.2 适用场景与优势适用场景读多写少的业务如商品详情、用户信息查询对一致性要求为“最终一致”的场景缓存数据可通过数据库快速重建的场景。核心优势逻辑简单开发成本低易于落地缓存利用率高读请求优先走缓存减轻数据库压力减少缓存与数据库的同步冲突降低脏数据概率。2.3 存在的问题Cache Aside Pattern并非完美核心问题出在“写后删缓存”的时序上若更新数据库成功但删除缓存失败如Redis宕机、网络波动则缓存中仍存旧数据后续读请求会命中脏数据并发场景下“读未命中→查旧数据→写缓存”与“更数据库→删缓存”重叠可能导致缓存写入旧数据。✨ 小互动你在项目中遇到过Cache Aside Pattern的坑吗欢迎在评论区留言分享 记得点赞收藏后续持续更新架构实战技巧三、延迟双删策略解决更新场景的一致性难题延迟双删策略是对Cache Aside Pattern的优化专门解决“写操作后缓存删除失败”和“并发读写导致脏数据”的问题核心思路是“两次删除缓存中间加延迟”。3.1 核心原理4步走客户端发起写请求先删除Redis缓存更新MySQL数据库延迟一段时间如500ms、1s根据业务耗时调整再次删除Redis缓存。成功失败客户端写请求第一次删除Redis缓存更新MySQL数据库延迟N毫秒第二次删除Redis缓存返回写成功结果返回写失败结果3.2 关键解析为什么要“双删延迟”第一次删缓存目的提前清空旧缓存避免后续读请求在“数据库更新前”读取到旧缓存数据。延迟一段时间目的等待数据库更新完成后再执行第二次删除确保期间可能写入缓存的旧数据并发读导致被清除延迟时间选择需大于“数据库更新耗时 并发读请求从数据库查数据并写入缓存的耗时”一般建议500ms~3s可通过压测调整。第二次删缓存目的兜底操作即使第一次删缓存失败、或并发读请求在数据库更新后写入了旧数据第二次删除也能将脏数据清空保证后续读请求加载最新数据。3.3 适用场景与优势适用场景写操作较多、并发场景复杂的业务对缓存一致性要求较高不允许长期存在脏数据的场景已使用Cache Aside Pattern但并发问题无法解决的场景。核心优势解决了Cache Aside Pattern的并发读写脏数据问题通过二次删除兜底降低缓存删除失败导致的不一致风险兼容性强可在原有Cache Aside架构上快速改造。3.4 关键注意事项延迟时间的合理性太短无法覆盖数据库更新和缓存写入耗时太长会导致短暂的缓存空窗期读请求频繁穿透数据库第二次删缓存的可靠性建议通过消息队列如RabbitMQ、RocketMQ实现延迟删除避免服务宕机导致第二次删除失败数据库事务一致性更新数据库需保证事务完整性避免数据库更新一半失败导致缓存已删、数据未更的情况。 实操建议延迟双删的延迟时间可通过模拟并发场景压测得出比如统计1000次并发读写的平均耗时在此基础上增加20%的冗余时间。四、实战代码示例JavaSpring Boot下面以“商品信息管理”为例基于Spring BootMySQLRedis实现Cache Aside Pattern和延迟双删策略的实战代码直接抄作业4.1 环境依赖pom.xml核心依赖dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-data-redis/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-jdbc/artifactId/dependencydependencygroupIdmysql/groupIdartifactIdmysql-connector-java/artifactIdscoperuntime/scope/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-amqplt;/artifactIdgt;!-- 用于延迟双删的消息队列 --/dependency4.2 Cache Aside Pattern实现商品查询更新ServicepublicclassProductServiceImplimplementsProductService{AutowiredprivateProductMapperproductMapper;// MyBatis Mapper接口AutowiredprivateStringRedisTemplateredisTemplate;// 缓存key前缀privatestaticfinalStringCACHE_KEY_PRODUCTproduct:info:;// 读操作Cache Aside PatternOverridepublicProductgetProductById(Longid){// 1. 查询缓存StringkeyCACHE_KEY_PRODUCTid;StringproductJsonredisTemplate.opsForValue().get(key);if(StrUtil.isNotBlank(productJson)){// 缓存命中返回数据returnJSONUtil.toBean(productJson,Product.class);}// 2. 缓存未命中查询数据库ProductproductproductMapper.selectById(id);if(productnull){// 数据库无数据可设置空缓存避免缓存穿透redisTemplate.opsForValue().set(key,,5,TimeUnit.MINUTES);returnnull;}// 3. 将数据库数据写入缓存设置1小时过期redisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(product),1,TimeUnit.HOURS);returnproduct;}// 写操作Cache Aside Pattern写后删缓存OverrideTransactional// 保证数据库事务一致性publicBooleanupdateProduct(Productproduct){// 1. 更新数据库booleanupdateSuccessproductMapper.updateById(product)0;if(!updateSuccess){returnfalse;}// 2. 删除缓存StringkeyCACHE_KEY_PRODUCTproduct.getId();redisTemplate.delete(key);returntrue;}}4.3 延迟双删策略优化基于RabbitMQ延迟队列4.3.1 延迟队列配置ConfigurationpublicclassRabbitMQDelayConfig{// 延迟交换机publicstaticfinalStringDELAY_EXCHANGEdelay_exchange;// 延迟队列删除缓存publicstaticfinalStringDELAY_QUEUE_CACHE_DELETEdelay_queue_cache_delete;// 路由键publicstaticfinalStringDELAY_ROUTING_KEYdelay_routing_key;// 延迟交换机基于死信交换机实现BeanpublicDirectExchangedelayExchange(){returnnewDirectExchange(DELAY_EXCHANGE);}// 延迟队列BeanpublicQueuedelayCacheDeleteQueue(){MapString,ObjectargsnewHashMap();// 设置死信交换机与延迟交换机同名简化配置args.put(x-dead-letter-exchange,DELAY_EXCHANGE);// 设置死信路由键args.put(x-dead-letter-routing-key,DELAY_ROUTING_KEY);returnQueueBuilder.durable(DELAY_QUEUE_CACHE_DELETE).withArguments(args).build();}// 队列绑定BeanpublicBindingdelayCacheDeleteBinding(){returnBindingBuilder.bind(delayCacheDeleteQueue()).to(delayExchange()).with(DELAY_ROUTING_KEY);}}4.3.2 延迟双删实现优化写操作ServicepublicclassProductServiceImplimplementsProductService{AutowiredprivateProductMapperproductMapper;AutowiredprivateStringRedisTemplateredisTemplate;AutowiredprivateRabbitTemplaterabbitTemplate;privatestaticfinalStringCACHE_KEY_PRODUCTproduct:info:;// 写操作延迟双删策略OverrideTransactionalpublicBooleanupdateProductWithDelayDoubleDelete(Productproduct){StringkeyCACHE_KEY_PRODUCTproduct.getId();// 1. 第一次删除缓存redisTemplate.delete(key);// 2. 更新数据库booleanupdateSuccessproductMapper.updateById(product)0;if(!updateSuccess){returnfalse;}// 3. 发送延迟消息执行第二次删除延迟1秒rabbitTemplate.convertAndSend(RabbitMQDelayConfig.DELAY_EXCHANGE,RabbitMQDelayConfig.DELAY_ROUTING_KEY,key,message-{// 设置消息过期时间1秒message.getMessageProperties().setExpiration(1000);returnmessage;});returntrue;}// 延迟消息消费者执行第二次缓存删除RabbitListener(queuesRabbitMQDelayConfig.DELAY_QUEUE_CACHE_DELETE)publicvoidhandleDelayCacheDelete(Stringkey){try{// 第二次删除缓存redisTemplate.delete(key);log.info(延迟双删第二次删除缓存成功key:{},key);}catch(Exceptione){log.error(延迟双删第二次删除缓存失败key:{},key,e);// 可加入重试机制如定时任务兜底}}// 读操作与上文Cache Aside Pattern一致省略...}五、常见问题与避坑指南5.1 缓存删除失败怎么办方案1引入消息队列重试机制如上述延迟双删中的RabbitMQ失败后重试3次方案2定时任务兜底定期对比数据库与缓存数据清理脏数据方案3使用Redis的持久化机制AOFRDB确保Redis重启后缓存数据可恢复减少删除失败影响。5.2 如何避免缓存穿透缓存空值数据库查询无结果时写入空缓存设置较短过期时间布隆过滤器在缓存前增加布隆过滤器过滤不存在的key避免无效数据库查询。5.3 高并发场景下的极致优化加分布式锁在读未命中→写缓存的流程中加锁如Redisson分布式锁避免同一key并发写入旧数据缓存预热系统启动时将热点数据提前加载到缓存减少缓存未命中场景缓存降级流量峰值时关闭非核心业务的缓存更新优先保证读请求可用性。✨ 面试小贴士被问到MySQL与Redis缓存一致性时先答“最终一致性”的核心诉求再讲Cache Aside Pattern的基础流程最后补充延迟双删的优化方案和避坑点轻松拿下面试分六、总结本文围绕MySQL与Redis缓存一致性问题详细拆解了两个经典解决方案Cache Aside Pattern读走缓存、写走数据库逻辑简单易落地适合读多写少场景延迟双删策略通过“双删延迟”优化解决并发读写脏数据和缓存删除失败问题适合一致性要求较高的场景。核心原则分布式系统中不追求强一致性而是通过合理的策略实现最终一致性同时结合业务场景选择合适的方案必要时引入分布式锁、消息队列等工具兜底。