你有没有遇到过这种情况:在电商App里刚下单成功,刷新一下页面,订单却不见了?等了几秒再刷,又突然冒出来。这背后很可能就是缓存和数据库之间的“沟通”出了问题。
缓存为啥会“过期”
缓存就像你家楼下的便利店,离得近、拿东西快。数据库则是城郊的大仓库,货全但路远。为了提升访问速度,系统通常把常用数据先放进缓存。可一旦数据库里的数据变了,比如用户改了手机号,缓存里的旧数据就成了“过期食品”,得及时处理。
这时候就得靠缓存失效策略——告诉系统什么时候该扔掉旧缓存,下次请求时重新从数据库加载新数据。
常见的失效方式
最简单的办法是设置过期时间(TTL)。比如商品详情页缓存10分钟,10分钟后自动失效。这种方式简单粗暴,适合对数据实时性要求不高的场景,比如博客文章。
但有些场景等不了这么久。比如银行余额,必须尽可能准确。这时就会用到“主动失效”:每当数据库更新,立刻删除或更新对应的缓存。
// 伪代码示例:更新数据库后删除缓存
updateUserEmail(userId, newEmail);
delCache("user:profile:" + userId);
删了缓存,就万事大吉了?
别高兴太早。设想这个顺序:
- 线程A要更新数据库,先把缓存删了;
- 还没来得及改数据库,线程B进来查数据;
- B发现缓存没了,就去数据库读了个旧值,又塞回缓存;
- A终于改完数据库,结果缓存里又被塞了旧数据。
这就叫“缓存不一致”,而且特别难查,因为问题只在特定时序下出现。
怎么减少“不一致”的尴尬
一种做法是“先更新数据库,再删缓存”。这样即使中间有并发读,顶多是短暂读到旧缓存,等缓存删了,下一次就读到最新的了。虽然不能完全避免,但概率大大降低。
还有一种更狠的:双删机制。更新前先删一次缓存,更新完再删一次。虽然多了一次操作,但能有效应对上面那种“B插队读”的情况。
// 双删伪代码
delCache("user:data:" + userId);
updateInDB(userId, newData);
delCache("user:data:" + userId);
异步消息来帮忙
如果怕直接操作影响性能,可以用消息队列。数据库一更新,就往MQ发个通知,由下游消费者负责清理缓存。这样主流程更快,也实现了“最终一致”。
当然,消息可能延迟,甚至丢失。所以得配合重试机制和监控,确保每条“删缓存”的指令都落地。
缓存重建别忘了加锁
当缓存失效,多个请求同时发现“没数据”,都去查数据库,可能导致数据库瞬间压力飙升。这就是“缓存击穿”。
解决办法是在查数据库前加个本地锁或分布式锁,只让一个请求去重建缓存,其他等着用新的就行。
if (!getFromCache(key)) {
if (tryLock()) {
data = queryFromDB();
setCache(key, data);
unlock();
} else {
// 等一小会儿,重新查缓存
sleep(50);
return getFromCache(key);
}
}
没有银弹,只有权衡
没有哪种策略能通吃所有场景。电商库存可能用延迟双删+消息队列,社交App的点赞数也许直接用缓存过期就够了。关键看你的业务能不能容忍短暂不一致,以及对性能的要求有多高。
有时候,用户根本不在乎数据是不是差了一两秒。与其花大力气追求强一致,不如把资源用在刀刃上。