缓存不是加个Redis就完事了
很多人觉得,做网站优化就是上Redis,数据一扔进去,访问快了就完。可真到了高并发场景,比如双11抢购、春节红包活动,光靠一个Redis实例,分分钟被打爆。大型网站的缓存架构远比这复杂,它得考虑命中率、一致性、容灾、扩展性,甚至业务特性。
举个例子,某电商平台的商品详情页,每秒几十万次访问,如果每次都穿透到数据库,数据库早就瘫痪了。这时候缓存不只是“加速器”,而是整个系统的“减压阀”。
多级缓存:从浏览器到数据库
真正扛得住流量的架构,用的是多级缓存策略。用户请求先走浏览器缓存,再经过CDN、反向代理、应用本地缓存,最后才是分布式缓存和数据库。
比如静态资源如图片、JS、CSS,直接由CDN缓存,离用户最近的节点返回,连源站都不用进。动态内容比如商品价格,可以在Nginx层做本地共享内存缓存(比如用lua_shared_dict),减少后端压力。
在应用层,除了Redis集群,还会加一层本地缓存,比如用Caffeine或Guava Cache。虽然容量小,但访问速度是微秒级。对于高频低变的数据,比如城市列表、分类目录,本地缓存命中率能到90%以上。
缓存穿透、击穿、雪崩怎么防?
缓存穿透是指查一个不存在的数据,每次都会打到数据库。常见做法是用布隆过滤器(Bloom Filter)提前拦截无效请求。比如用户查一个根本不存在的商品ID,布隆过滤器能快速告诉你“肯定没有”,避免查缓存再查库。
缓存击穿是某个热点key过期瞬间,大量请求同时击穿到数据库。可以给热点数据设置永不过期,后台异步更新;或者用互斥锁,只让一个线程去加载数据,其他等待。
缓存雪崩是大量key同时失效,整个系统瞬间压力倍增。解决办法是过期时间加随机值,比如原本设600秒,实际在500~700秒之间随机,避免集体失效。
数据一致性怎么处理?
缓存和数据库不可能完全实时一致。常见的策略是“先更库,再删缓存”。比如更新商品库存,先写数据库,然后删除缓存中的对应key,下次读取时自动回源加载新数据。
但这也可能出问题,比如删缓存失败,或者中间有并发读写。这时候可以用“延迟双删”:先删缓存,等数据更新完,再休眠几毫秒,再次删除缓存,降低脏读概率。
更复杂的场景会引入消息队列,把数据变更发到MQ,由消费者负责清理或更新缓存,实现最终一致性。
代码示例:简单的缓存读取逻辑
public String getProductPrice(Long productId) { // 先查本地缓存 String price = localCache.get(productId); if (price != null) { return price; } // 本地没命中,查Redis price = redis.get("product:price:" + productId); if (price != null) { // 回填本地缓存,设置较短过期时间 localCache.put(productId, price, 60); return price; } // 缓存都没命中,查数据库 price = database.queryPrice(productId); if (price != null) { // 写入Redis,设置随机过期时间,防止雪崩 int expireTime = 600 + new Random().nextInt(600); redis.setex("product:price:" + productId, expireTime, price); localCache.put(productId, price, 60); } return price;}这个流程看着简单,但在实际部署中,每个环节都要监控和调优。比如本地缓存大小要控制,避免OOM;Redis访问要加超时和降级,防止拖垮整个服务。
缓存只是手段,目标是稳定和体验
设计缓存架构时,不能只盯着技术参数。用户打开页面的速度、下单是否卡顿、促销活动能不能扛住流量,这些才是最终衡量标准。有时候少写点代码,多加一级缓存,反而能让系统更稳。
就像修路,不是车多了就拼命拓宽车道,而是要有立交、有红绿灯、有应急出口。缓存架构也一样,层次分明,各司其职,才能撑起真正的大型网站。