黑马点评-商户查询缓存-03_cache_consistency_and_avalanche
黑马点评商户查询缓存三为什么更新商铺后要删缓存而不是改缓存本文继续整理黑马点评 Redis 实战篇第 2 章「商户查询缓存」。前两篇讲了普通商户缓存查询和缓存穿透。缓存能提升查询速度但也带来一个新问题数据源在 MySQL查询却可能从 Redis 返回那商铺更新后Redis 里的旧数据怎么办这一篇重点讲缓存一致性为什么常见做法是“先更新数据库再删除缓存”以及缓存雪崩为什么也需要提前考虑。1. 问题背景商铺查询接口加缓存后读请求大致是查 Redis Redis 有直接返回 Redis 没有查 MySQL 查到后写 Redis这条链路提升了查询性能。但商铺数据不是永远不变的。比如商铺可能会更新商铺名称 商铺地址 营业时间 评分 图片如果数据库更新了但 Redis 里还是旧数据那么用户查询时命中 Redis就会看到旧商铺信息。这就是缓存和数据库不一致问题。2. 我当时的困惑我一开始最自然的想法是更新数据库之后顺手把 Redis 也更新成新数据不就一致了吗看起来确实合理更新 MySQL 更新 Redis但实际项目里更常见的是更新 MySQL 删除 Redis这就有点反直觉。为什么不是“更新缓存”而是“删除缓存”3. 正确理解缓存不是主数据源商户缓存这一章使用的是 Cache Aside Pattern。在这个模式里MySQL 是主数据源。 Redis 是旁路缓存。查询时应用先查缓存缓存没有再查数据库。更新时应用先更新数据库再处理缓存。这里的关键是缓存只是为了加速读不应该承担主数据源职责。所以更新时没必要努力维护 Redis 中每一份缓存为最新。更简单、更常用的做法是数据库更新后让缓存失效。 下一次查询时再从数据库加载最新数据重建缓存。4. 更新缓存 vs 删除缓存更新数据库后有两种常见处理方式。第一种更新缓存。更新 MySQL 更新 Redis问题是如果一个商铺短时间内被多次更新但没人查询那么每次更新 Redis 都是无效写。第二种删除缓存。更新 MySQL 删除 Redis下次有人查询时Redis 没有 查 MySQL 写入最新数据这更符合缓存的定位有人读时才重建缓存没人读就不浪费 Redis 写操作。5. 为什么通常先更新数据库再删除缓存这里有一个经典顺序问题先删缓存再更新数据库 还是先更新数据库再删缓存黑马点评讲义推荐的是先更新数据库再删除缓存。如果先删除缓存再更新数据库可能出现这样的并发问题MySQLRedis线程2 查询商铺线程1 更新商铺MySQLRedis线程2 查询商铺线程1 更新商铺删除缓存查询缓存未命中查询数据库读到旧数据把旧数据写回缓存更新数据库为新数据最后的结果是MySQL 是新数据 Redis 却被写回了旧数据而先更新数据库再删除缓存出现旧数据写回缓存的概率更低。它不是绝对没有并发问题但在常见业务里更合理也更简单。6. 项目里的更新代码项目里更新商铺的核心代码大致是TransactionalOverridepublicResultupdate(Shopshop){Longidshop.getId();if(idnull){returnResult.fail(店铺id不能为空...);}updateById(shop);stringRedisTemplate.delete(CACHE_SHOP_KEYid);stringRedisTemplate.delete(CACHE_HOT_SHOP_KEYid);returnResult.ok();}这段代码有三个关键点。第一先校验 id。没有 id 就不知道要更新哪个商铺也不知道要删除哪个缓存。第二先更新数据库updateById(shop);第三再删除缓存stringRedisTemplate.delete(CACHE_SHOP_KEYid);stringRedisTemplate.delete(CACHE_HOT_SHOP_KEYid);这里删除了两套缓存是因为当前项目里已经做了“普通数据走 pass-through热点数据走逻辑过期”的改造。普通缓存 keycache:shop:{id}热点缓存 keycache:shop:hot:{id}如果只删普通缓存不删热点缓存热点商铺可能仍然返回旧数据。7. 事务注解能解决 Redis 和 MySQL 的强一致吗代码上有Transactional初学时容易以为加了事务数据库更新和 Redis 删除就一定同时成功或失败。这个理解不准确。Transactional主要管理的是数据库事务。Redis 删除操作不天然纳入 MySQL 本地事务。所以这里更准确的理解是数据库更新本身需要事务保护。 缓存删除是更新数据库后的配套动作。在真实分布式系统中如果要更严格保证数据库和缓存一致可能会引入消息队列、订阅 binlog、重试机制等方案。但在这一章里我们先掌握最常用、最简单的 Cache Aside 做法。8. 更新后的查询流程删除缓存后下一次查询会重新走缓存重建。更新商铺请求更新 MySQL删除 Redis 普通缓存删除 Redis 热点缓存后续查询商铺Redis 未命中查询 MySQL 最新数据写入 Redis返回最新商铺信息这就是删除缓存的价值不主动维护缓存最新而是让下一次查询自然重建最新缓存。9. 顺带理解缓存雪崩这一章还讲了缓存雪崩。缓存雪崩是指同一时间大量缓存 key 失效或者 Redis 服务不可用导致大量请求同时打到数据库。比如我们给大量商铺设置了完全相同的 TTLcache:shop:1 30分钟后过期 cache:shop:2 30分钟后过期 cache:shop:3 30分钟后过期 ...如果它们在同一时刻过期大量请求就可能同时回源 MySQL。流程如下大量缓存 key 同时过期大量请求查 Redis 未命中请求集中打到 MySQL数据库压力剧增接口变慢甚至不可用常见解决方式包括给 TTL 增加随机值避免同一时间失效 提高 Redis 可用性比如集群或主从 业务层限流降级 使用多级缓存10. 易错点第一个易错点以为缓存必须和数据库实时完全一致。大多数缓存场景追求的是最终一致而不是每一毫秒都强一致。第二个易错点更新数据库后更新缓存。不是不能做但对于读多写少的缓存场景删除缓存通常更简单避免无效写。第三个易错点先删缓存再更新数据库。这种顺序在并发下更容易把旧数据重新写回缓存。第四个易错点以为Transactional能把 Redis 删除也纳入 MySQL 本地事务。它主要管数据库事务Redis 操作仍然需要额外考虑失败风险。第五个易错点忽略热点缓存。如果系统里同时有普通缓存和热点缓存更新时必须都处理。11. 面试怎么回答如果面试官问缓存和数据库不一致怎么解决可以回答常见做法是 Cache Aside Pattern。 查询时先查缓存未命中再查数据库并写入缓存。 更新时先更新数据库再删除缓存。 这样下一次查询缓存未命中时会从数据库读取最新数据并重建缓存。如果问为什么删除缓存而不是更新缓存可以回答因为缓存是为了加速读不是主数据源。 如果每次更新数据库都同步更新缓存可能产生很多无效写。 删除缓存更简单后续真正有人查询时再从数据库加载最新数据写入缓存。如果问什么是缓存雪崩可以回答缓存雪崩是指同一时间大量缓存 key 失效或者 Redis 整体不可用导致大量请求绕过缓存直接访问数据库。 常见解决方案包括给 TTL 添加随机值、提高 Redis 高可用、限流降级和多级缓存。12. 总结这一篇最重要的是记住在 Cache Aside 模式下更新商铺时通常先更新数据库再删除缓存下一次查询再重建缓存。删除缓存不是偷懒而是符合缓存的定位MySQL 负责保存真实数据。 Redis 负责加速读取。 缓存失效后由下一次查询重建。到这里普通缓存查询、缓存穿透、缓存一致性、缓存雪崩的基本思路已经串起来了。但还有一个更危险的问题如果某个热门商铺 key 过期大量请求同时来查会发生什么这就是下一篇的缓存击穿和互斥锁。