缓存不是简单的存和取
打开一个电商网站,商品列表秒开;刷新页面,数据还是那么快。这背后不一定是服务器有多强,更多时候是缓存策略设计得好。缓存不是把数据往内存或Redis里一扔就完事了,怎么存、何时存、如何失效,这些才是关键。
在高并发的网络应用中,数据库往往是最容易成为瓶颈的一环。通过合理使用缓存,可以大幅降低数据库压力,提升响应速度。但用不好,反而会带来数据不一致、内存爆满、雪崩等问题。这时候,就需要借助成熟的缓存策略设计模式来规范行为。
常见的缓存策略模式
Cache-Aside(旁路缓存) 是最常用的模式。读请求先查缓存,命中就直接返回;没命中则查数据库,再写回缓存。写操作时,先更新数据库,然后让缓存失效。
// 伪代码示例:Cache-Aside 读操作
function getProduct(id) {
data = redis.get("product:" + id);
if (data == null) {
data = db.query("SELECT * FROM products WHERE id = ?", id);
redis.setex("product:" + id, 3600, data); // 缓存1小时
}
return data;
}这种模式灵活,开发者控制力强,但也容易出错。比如忘记清除缓存,就会导致用户看到旧数据。
Write-Through 和 Write-Behind
Write-Through(直写模式) 是在更新数据库的同时,也同步更新缓存。这样下次读取时,缓存里的数据总是最新的。适合读多写少且对一致性要求较高的场景。
而 Write-Behind(回写模式) 更进一步,写操作只更新缓存,由缓存异步批量写入数据库。好处是写得快,但实现复杂,有数据丢失风险,一般用于日志类或非核心数据。
这两种模式通常需要自定义缓存层支持,不像Cache-Aside那样可以直接用Redis+代码组合实现。
应对缓存穿透、击穿、雪崩
真实业务中,光有策略还不够。比如有人恶意查询不存在的商品ID,每次都会打到数据库——这就是缓存穿透。解决办法是用布隆过滤器提前拦截无效请求,或者对查不到的结果也缓存一个空值,设短过期时间。
某个热点商品缓存刚好过期,瞬间大量请求涌进来,压垮数据库,这是缓存击穿。可以用互斥锁控制,只让一个线程去加载数据,其他等待。
// 使用Redis实现简单互斥锁防止击穿
function getProductWithLock(id) {
data = redis.get("product:" + id);
if (data == null) {
// 尝试获取锁
if (redis.setnx("lock:product:" + id, 1, 10)) {
data = db.query("SELECT * FROM products WHERE id = ?", id);
redis.setex("product:" + id, 3600, data);
redis.del("lock:product:" + id);
} else {
// 等待一会儿重试(实际中可用sleep或递归)
sleep(10);
return getProductWithLock(id);
}
}
return data;
}而缓存雪崩是指大量缓存同时失效,流量全涌向数据库。避免方式是设置随机过期时间,比如基础时间加一个随机偏移,不让它们集体“下班”。
选择合适的策略取决于业务场景
新闻首页可以容忍几分钟延迟,用Cache-Aside加固定TTL就行;用户账户余额这种数据,就得考虑Write-Through甚至结合消息队列保证最终一致。没有银弹,只有权衡。
实际开发中,很多人一开始只做简单缓存,等出问题了才补救。不如在设计阶段就把缓存当作系统的一部分,像设计数据库一样去规划它的读写路径、失效机制和容错能力。
好的缓存策略,不只是技术实现,更是一种架构思维。它让系统在面对流量高峰时依然从容,也让用户感觉“这个App真快”。