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