Redis-更新策略
目录
缓存更新策略
主动更新策
Cache Aside patten
缓存穿透
缓存空对象
缓存雪崩
缓存击穿编
互斥锁
逻辑过期
缓存更新策略
淘汰方式 触发者 时机 数据是否还新鲜 典型用法 内存淘汰(Eviction) Redis 自己 内存达到 maxmemory 上限 与新鲜度无关,只问“谁最不常用” 所有 key 通用,兜底 超时剔除(Expiration) 时间轮+Redis 到达 TTL 可能已过期,但还没被删除 常规缓存,SET 时 EX/PX 主动更新(Proactive Refresh) 业务/后台任务 数据库一变更就立刻推/刷新缓存 永远最新 高并发读,0 击穿方案
主动更新策略
# | 模式 | 读链路 | 写链路 | 一致性 | 性能 | 代码侵入 | 典型场景 |
---|---|---|---|---|---|---|---|
01 | Cache Aside 旁路缓存 | 未命中→查库→回填 | 先写库→再删缓存 | 最终一致 | 读快写慢 | 中 | 互联网读多写少,标配 |
02 | Read/Write Through 直写 | 未命中→由缓存层自己回源 | 由缓存层同步写库 | 强一致 | 读写均衡 | 低(靠中间层) | 本地缓存、嵌入式 KV |
03 | Write Behind 回写 | 只读缓存,命中即可 | 只写缓存→异步刷库 | 弱一致(可能丢) | 写极快 | 最低 | 计数器、日志、点击流 |
Cache Aside patten
时序 先删缓存再写库(错误示范) 先写库再删缓存(推荐) T1 线程 A 删掉缓存 线程 A 更新数据库 T2 线程 B 发现缓存为空 线程 B 读到旧缓存(仍有效) T3 线程 B 把旧库值重新刷进缓存 → 永久脏数据 线程 A 删除缓存 → 下次读会重新加载新值 结果 100% 出现旧值回填,一定不一致 只有 T2-T3 之间极短的脏读,且可二次删兜底
先更新数据库 = 先锁定“真相”;再删缓存 = 让缓存“重新问真相”。
反过来先删缓存,会让缓存有机会把“过期的真相”再次当成“真相”写回去,必现脏数据;而先写库再删缓存,只会出现毫秒级脏读,且可用延迟二次删把风险压到几乎为零。
缓存穿透
缓存穿透(Cache Penetration)
定义:查询一个在缓存和数据库里都必然不存在的数据,请求每次都会穿透缓存直接打到数据库,导致缓存形同虚设。解决方案:
缓存空对象(Null Cache)
发现数据库返回null
时,仍然把 null 写进 Redis,并给一个短 TTL(30~300 s)。布隆过滤器(Bloom Filter)
预先把所有合法 ID 哈希进一个位数组(百万级 ≈ 1 MB)。由于存在哈希碰撞,可能导致布隆放行后查询不存在的情况。
缓存空对象
在未命中Redis的时候,如果数据库中也不存在,则在Redis中存入一个空值。
在Redis判断命中的时候,如果命中的是空值则之间返回错误信息。
shopJson !=null 本质就是shopJson 为空。
public Result queryById(Long id) {String key=CACHE_SHOP_KEY+id;//1. 从redis查询商铺缓存String shopJson = stringRedisTemplate.opsForValue().get(key);//2. 判断是否存在if (StrUtil.isNotBlank(shopJson)) {//3. 存在,直接返回Shop shop = JSONUtil.toBean(shopJson, Shop.class);return Result.ok(shop);}//判断是否为空值if (shopJson != null) {return Result.fail("店铺不存在");}//4. 不存在,根据id查询数据库Shop shop = getById(id);//5.不存在,返回错误if (shop == null) {//将空值写入RedisstringRedisTemplate.opsForValue().set(key, "",CACHE_NULL_TTL,TimeUnit.MINUTES);//返回错误信息return Result.fail("店铺不存在");}//6. 存在,写入redisstringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL,TimeUnit.MINUTES);//7. 返回return Result.ok(shop);}
缓存雪崩
缓存击穿

互斥锁
public Shop queryWithMutex(Long id){String key=CACHE_SHOP_KEY+id;//1. 从redis查询商铺缓存String shopJson = stringRedisTemplate.opsForValue().get(key);//2. 判断是否存在if (StrUtil.isNotBlank(shopJson)) {//3. 存在,直接返回return JSONUtil.toBean(shopJson, Shop.class);}//判断是否为空值if (shopJson != null) {return null;}//4. 建立缓存重建//4.1 获取互斥锁String locakKey="lock:shop"+id;Shop shop=null;try {boolean isLock = tryLock(locakKey);//4.2 判断锁是否获取成功if(!isLock){//4.3 失败,返回错误或者重试Thread.sleep(50);return queryWithMutex(id);}//4.4 成功,根据id查询数据库shop = getById(id);//模拟耗时Thread.sleep(200);//5. 不存在,返回错误if (shop == null) {//将空值写入redisstringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);//返回错误信息return null;}//6. 存在,写入redisstringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL,TimeUnit.MINUTES);} catch (InterruptedException e) {throw new RuntimeException(e);}finally {//7. 释放互斥锁unLock(locakKey);}//8. 返回return shop;}
逻辑过期
并发不高、允许短暂等待 → 互斥锁(简单、强一致)
超高并发、体验优先、可接受几秒旧值 → 逻辑过期(异步重建,零阻塞)