Redis缓存策略:从入门到避坑
Redis很快,这个大家都知道。 但快不代表能用好。 缓存穿透、缓存击穿、缓存雪崩...这些坑你踩过几个?
为什么需要缓存?
先说个真实场景。
你的网站首页要展示热门商品列表。这个列表需要关联查询好几张表,还要排序、分页。每次查询要200ms。
一秒钟10个请求还好。100个呢?1000个呢?
数据库扛不住了。
这时候Redis就派上用场了。把查询结果放进Redis,下次直接从内存读,1ms搞定。速度提升200倍。
不过,事情没这么简单。

三种缓存读取策略
用缓存,首先要选对策略。
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都放进布隆过滤器。查询时先问布隆过滤器,如果说不存在,就直接返回,不查缓存和数据库。
布隆过滤器的特点是:说不存在就一定不存在,说存在可能不存在(有误判率)。但这个误判率可以控制得很低。

缓存击穿
这个和穿透不一样。
击穿是指一个热点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挂了,本地缓存还能顶一会儿。

缓存更新的一致性问题
这是个老大难问题。
更新数据时,先更新数据库还是先删缓存?
先删缓存,再更新数据库
看起来合理,但有问题。
假设两个并发请求:
请求A删除缓存
请求B查询,缓存没有,查数据库(旧数据)
请求A更新数据库
请求B把旧数据写入缓存
结果缓存里是旧数据。
先更新数据库,再删缓存
这是推荐的做法。
但也不是完美的。极端情况下还是可能不一致:
缓存刚好过期
请求A查数据库(旧数据)
请求B更新数据库
请求B删除缓存
请求A把旧数据写入缓存
不过这种情况概率很低。因为写数据库比读数据库慢,请求B很难在请求A之前完成。
实战建议:如果对一致性要求很高,可以用延迟双删策略。先删缓存,更新数据库,等几百毫秒,再删一次缓存。这样能覆盖大部分并发场景。
缓存设计的最佳实践
说了这么多理论,来点实际的。
合理设置过期时间
不要所有数据都用同一个过期时间。

控制缓存大小
不要什么都往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企业应用
下一条:没有了!