首页>软件资讯>常见问题

常见问题

Redis缓存策略从入门到避坑

发布时间:2025-11-13 08:39:50人气:2


Redis缓存策略:从入门到避坑

Redis很快,这个大家都知道。 但快不代表能用好。 缓存穿透、缓存击穿、缓存雪崩...这些坑你踩过几个?


为什么需要缓存?

先说个真实场景。


你的网站首页要展示热门商品列表。这个列表需要关联查询好几张表,还要排序、分页。每次查询要200ms。


一秒钟10个请求还好。100个呢?1000个呢?


数据库扛不住了。


这时候Redis就派上用场了。把查询结果放进Redis,下次直接从内存读,1ms搞定。速度提升200倍。


不过,事情没这么简单。

缓存工作流程.png


三种缓存读取策略

用缓存,首先要选对策略。


Cache-Aside(旁路缓存)

这是最常用的模式。


读数据时:


先查Redis

有就直接返回

没有就查数据库

查到后写入Redis

返回数据

写数据时:


先更新数据库

再删除Redis中的缓存



// 读取数据 public User getUser(Long id) { // 先查缓存 String key = "user:" + id; String cached = redis.get(key); if (cached != null) { return JSON.parse(cached); } // 缓存没有,查数据库 User user = db.query(id); if (user != null) { // 写入缓存,设置过期时间 redis.setex(key, 3600, JSON.stringify(user)); } return user; } // 更新数据 public void updateUser(User user) { // 先更新数据库 db.update(user); // 再删除缓存 redis.del("user:" + user.getId()); }




为什么是删除缓存而不是更新缓存?

因为更新缓存有两个问题:一是如果更新很频繁但读取很少,会浪费性能;二是并发更新时可能导致数据不一致。删除缓存更简单,下次读取时自然会重新加载最新数据。


Read-Through / Write-Through

这种模式下,应用只和缓存层交互。


缓存层负责和数据库打交道。听起来很美好,但实际上很少用。


为什么?


因为这需要缓存层有很强的能力,要理解业务逻辑。Redis本身不支持,得自己封装一层。太复杂了。


Write-Behind(异步写入)

更新数据时,只写缓存,不立即写数据库。


缓存层会异步批量写入数据库。


性能很好,但有风险。缓存挂了,数据就丢了。所以只适合对一致性要求不高的场景,比如浏览量、点赞数这种。


三大经典问题

用Redis,这三个问题绕不开。


缓存穿透

什么是穿透?


用户查询一个不存在的数据。缓存里没有,数据库里也没有。但每次请求都会打到数据库。


如果有人恶意攻击,不停查询不存在的ID,数据库就废了。


怎么解决?


方案一:缓存空值


查不到数据时,也往Redis里写一个空值,设置较短的过期时间(比如5分钟)。




public User getUser(Long id) { String key = "user:" + id; String cached = redis.get(key); // 缓存命中 if (cached != null) { if (cached.equals("NULL")) { return null; // 空值 } return JSON.parse(cached); } // 查数据库 User user = db.query(id); if (user != null) { redis.setex(key, 3600, JSON.stringify(user)); } else { // 缓存空值,过期时间短一些 redis.setex(key, 300, "NULL"); } return user; }


方案二:布隆过滤器


在缓存前面加一层布隆过滤器。


所有存在的ID都放进布隆过滤器。查询时先问布隆过滤器,如果说不存在,就直接返回,不查缓存和数据库。


布隆过滤器的特点是:说不存在就一定不存在,说存在可能不存在(有误判率)。但这个误判率可以控制得很低。


布隆过滤器.png

缓存击穿

这个和穿透不一样。


击穿是指一个热点key突然过期了。


比如某个明星的微博,平时有几万QPS。突然缓存过期了,这几万请求同时打到数据库。数据库瞬间压力暴增。


解决办法:


方案一:热点数据永不过期


对于特别热的数据,干脆不设置过期时间。要更新时手动删除。


方案二:互斥锁


缓存失效时,不是所有请求都去查数据库。而是用分布式锁,只让一个请求去查,其他请求等着。




public User getUser(Long id) { String key = "user:" + id; String cached = redis.get(key); if (cached != null) { return JSON.parse(cached); } // 尝试获取锁 String lockKey = "lock:user:" + id; boolean locked = redis.setnx(lockKey, "1", 10); // 10秒超时 if (locked) { try { // 获得锁,查数据库 User user = db.query(id); if (user != null) { redis.setex(key, 3600, JSON.stringify(user)); } return user; } finally { redis.del(lockKey); // 释放锁 } } else { // 没获得锁,等一会儿再查缓存 Thread.sleep(50); return getUser(id); // 递归重试 } }




注意:这个方案会增加响应时间。没拿到锁的请求要等待。所以要权衡。如果数据库能扛住短时间的并发,可能不需要这么做。


缓存雪崩

雪崩比击穿更严重。


大量缓存同时过期,或者Redis直接挂了。所有请求都打到数据库,数据库直接崩溃。


怎么防?


方案一:过期时间加随机值


不要让大量key同时过期。设置过期时间时加个随机值。


// 不好的做法


 redis.setex(key, 3600, value); // 都是1小时后过期 


// 好的做法 int expire = 3600 + random.nextInt(300); // 1小时 + 0-5分钟随机 redis.setex(key, expire, value);


方案二:Redis集群 + 持久化


用Redis集群,主节点挂了还有从节点。


开启持久化(RDB或AOF),重启后能快速恢复数据。


方案三:多级缓存


Redis前面再加一层本地缓存(比如Caffeine)。


即使Redis挂了,本地缓存还能顶一会儿。

防雪崩策略.png


缓存更新的一致性问题

这是个老大难问题。


更新数据时,先更新数据库还是先删缓存?


先删缓存,再更新数据库

看起来合理,但有问题。


假设两个并发请求:


请求A删除缓存

请求B查询,缓存没有,查数据库(旧数据)

请求A更新数据库

请求B把旧数据写入缓存

结果缓存里是旧数据。




先更新数据库,再删缓存

这是推荐的做法。


但也不是完美的。极端情况下还是可能不一致:


缓存刚好过期

请求A查数据库(旧数据)

请求B更新数据库

请求B删除缓存

请求A把旧数据写入缓存

不过这种情况概率很低。因为写数据库比读数据库慢,请求B很难在请求A之前完成。


实战建议:如果对一致性要求很高,可以用延迟双删策略。先删缓存,更新数据库,等几百毫秒,再删一次缓存。这样能覆盖大部分并发场景。


缓存设计的最佳实践

说了这么多理论,来点实际的。


合理设置过期时间

不要所有数据都用同一个过期时间。

对比.png

控制缓存大小

不要什么都往Redis里塞。


Redis的内存是有限的。要缓存的是热点数据,不是全量数据。


可以用LRU策略,让Redis自动淘汰最少使用的数据。


# redis.conf maxmemory 2gb maxmemory-policy allkeys-lru


监控缓存命中率

缓存有没有用,看命中率。


命中率 = 缓存命中次数 / 总请求次数


一般来说,命中率在80%以上算正常。如果太低,说明缓存策略有问题。


// 简单的监控实现 




public class CacheStats { private AtomicLong hits = new AtomicLong(0); private AtomicLong misses = new AtomicLong(0); public void recordHit() { hits.incrementAndGet(); } public void recordMiss() { misses.incrementAndGet(); } public double getHitRate() { long total = hits.get() + misses.get(); return total == 0 ? 0 : (double) hits.get() / total; } }




序列化选择

数据存Redis前要序列化。


JSON简单直观,但占空间大。如果数据量大,可以考虑用更紧凑的格式,比如Protobuf或MessagePack。


JSON

优点:可读性好,调试方便

缺点:体积大,序列化慢


Protobuf

优点:体积小,速度快

缺点:需要定义schema,不直观


一些反直觉的经验

最后说几个容易踩的坑。


不要过度依赖缓存

缓存是优化手段,不是必需品。


如果数据库本身性能就够,加缓存反而增加复杂度。要先优化SQL,加索引,实在不行再上缓存。


缓存不是越多越好

见过有人把整个数据库都缓存到Redis。


结果Redis内存爆了,频繁淘汰数据,命中率反而下降。


记住:缓存热点数据,不是全量数据。


小心缓存的副作用

用了缓存,数据更新就不是实时的了。


这在某些场景下会有问题。比如库存扣减,如果用缓存,可能导致超卖。这种场景就不能用缓存,或者要用分布式锁保证一致性。


关键业务数据要谨慎:涉及金钱、库存、订单状态这些关键数据,要特别小心。宁可慢一点,也不能出错。


总结

Redis很强大,但不是银弹。


用好缓存,要理解业务场景,选对策略,做好监控。


记住几个关键点:


Cache-Aside是最常用的模式

先更新数据库,再删缓存

防止穿透、击穿、雪崩

合理设置过期时间

监控命中率

不要过度依赖缓存

最后,缓存的本质是用空间换时间。


但空间不是无限的,时间也不是越快越好。要在性能、一致性、复杂度之间找平衡。



上一条:redis企业应用

下一条:没有了!