news 2026/5/1 6:11:25

Redis 缓存

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Redis 缓存

Redis 缓存

缓存的通用模型

缓存与数据库的协同工作有三种经典模式:

  • Cache Aside(旁路缓存):由应用层负责维护缓存与数据库的一致性

    • 查询:先查缓存,命中则返回;未命中则查数据库,并将结果写入缓存
    • 更新:先更新数据库,再删除缓存(而非更新缓存)
    • 优点:实现简单、灵活性高
    • ⚠️缺点:无法保证强一致性,存在短暂不一致窗口
    • 📌当前最主流的方案
  • Read/Write Through(读写穿透):由缓存层代理数据库读写

    • 查询:缓存命中返回;未命中由缓存自动查 DB 并回填
    • 更新:直接写缓存,缓存同步更新 DB
    • ✅ 优点:对应用透明
    • ⚠️ 缺点:缓存需实现复杂逻辑,Redis 本身不支持,需自研中间件
  • Write Behind Caching(写回缓存)写操作只更新缓存,由后台异步线程批量将变更写入数据库

    • 读操作:优先读缓存
    • ✅ 优点:极大提升写性能(适用于日志、计数器等场景)
    • ⚠️ 缺点:一致性最弱,系统崩溃可能丢数据;实现复杂(需处理顺序、重试)
    • 📌注意:不是“先写 DB 再同步缓存”,而是先写缓存,异步刷 DB

💡 目前绝大多数系统采用Cache Aside模型,因其简单、可控、易于调试。


缓存一致性

Cache Aside模型中,更新操作通常有两种顺序,但都存在并发风险:

方案一:先删除缓存,再更新数据库 ❌(不推荐)

  • 线程1 删除缓存 → 正在更新 DB
  • 线程2 查询:缓存空 → 查 DB(此时 DB 还是旧值)→ 将旧值写入缓存
  • 后续请求全部读到脏数据,且长期不一致

方案二:先更新数据库,再删除缓存 ✅(推荐)

  • 线程1 更新 DB → 删除缓存
  • 线程2 查询:若在删缓存前,会读到旧缓存(短暂不一致,但数据最终正确)
  • 若在删缓存后,会查 DB 获取最新值并重建缓存

为什么选方案二?
虽然仍存在“短暂旧数据返回”的可能,但不会将脏数据写回缓存,最终一致性可保障。

⚠️ 仍需注意的问题

  1. 删除缓存失败

    • 若 DB 更新成功,但删缓存失败 → 长期不一致
    • 解决方案
      • 异步重试(如通过消息队列)
      • 监控告警 + 人工介入
  2. 极端场景下的不一致

    • 可采用“延迟双删”
      删除缓存 → 更新数据库 → sleep(100ms) → 再次删除缓存
    • 目的:防止在更新 DB 期间有旧请求重建缓存

✅ Java 示例:先更新 DB,再删除缓存

@ServicepublicclassUserService{@AutowiredprivateUserMapperuserMapper;@AutowiredprivateRedisTemplate<String,Object>redisTemplate;privatestaticfinalStringUSER_CACHE_KEY="user:";// 查询用户publicUsergetUserById(Longid){Stringkey=USER_CACHE_KEY+id;Useruser=(User)redisTemplate.opsForValue().get(key);if(user!=null){returnuser;}// 缓存未命中,查数据库user=userMapper.selectById(id);if(user!=null){// 设置随机 TTL(防雪崩)longttl=3600+newRandom().nextInt(300);// 1h ~ 1h5minredisTemplate.opsForValue().set(key,user,ttl,TimeUnit.SECONDS);}else{// 防穿透:缓存空值redisTemplate.opsForValue().set(key,"",60,TimeUnit.SECONDS);}returnuser;}// 更新用户(先更新 DB,再删缓存)@TransactionalpublicvoidupdateUser(Useruser){userMapper.updateById(user);// 1. 更新数据库Stringkey=USER_CACHE_KEY+user.getId();redisTemplate.delete(key);// 2. 删除缓存// ✅ 生产建议:若删除失败,可发消息到 MQ 重试}}

三大缓存异常问题

即使采用正确的一致性策略,仍可能遭遇以下三类高并发场景下的缓存危机:

1. 缓存穿透(Cache Penetration)

  • 定义:查询一个根本不存在的数据(缓存无,DB 也无)
  • 特点:key 不存在于任何存储层
  • 危害
    • 数据库承受大量无效查询
    • 可被恶意利用进行 DoS 攻击
✅ 解决方案
  • 空值缓存(Null Cache)
  • 布隆过滤器(Bloom Filter)
✅ Java 示例:布隆过滤器(Guava 单机版)
@ComponentpublicclassBloomFilterService{privateBloomFilter<Long>userIdBloomFilter=BloomFilter.create(Funnels.longFunnel(),1_000_000,0.01);@PostConstructpublicvoidinit(){// 启动时加载所有合法用户 IDList<Long>allUserIds=userMapper.selectAllIds();allUserIds.forEach(userIdBloomFilter::put);}publicbooleanmightExist(LonguserId){returnuserIdBloomFilter.mightContain(userId);}publicvoidaddUserToBloom(LonguserId){userIdBloomFilter.put(userId);}}// 使用示例@ServicepublicclassSafeUserService{@AutowiredprivateBloomFilterServicebloomFilterService;publicUsersafeGetUser(Longid){if(!bloomFilterService.mightExist(id)){returnnull;// 一定不存在,直接返回}returnuserService.getUserById(id);// 走正常缓存流程}}

⚠️ 注意:Guava 是单机内存版。分布式环境建议使用RedisBloom 模块或自研分片布隆过滤器。


2. 缓存雪崩(Cache Avalanche)

  • 定义大量缓存 key 在同一时间失效,导致瞬时所有请求打到数据库
  • 特点:多 key 集体失效,缓存层“崩塌”
  • 危害
    • 数据库 QPS 瞬间飙升,可能被打挂
    • 整体服务不可用
✅ 解决方案
  • 设置随机 TTL
  • 热点数据永不过期(逻辑过期)
  • 多级缓存
✅ Java 示例:随机 TTL + 逻辑过期
// 随机 TTL(通用)longbaseTTL=3600;longrandomTTL=baseTTL+newRandom().nextInt(300);redisTemplate.opsForValue().set(key,data,randomTTL,TimeUnit.SECONDS);// 逻辑过期封装类publicstaticclassLogicalCache<T>{privateTdata;privatelongexpireTime;// 毫秒时间戳// getter/setter}// 写入逻辑过期缓存LogicalCache<User>cache=newLogicalCache<>();cache.setData(user);cache.setExpireTime(System.currentTimeMillis()+3600_000);redisTemplate.opsForValue().set("hot:user:"+id,cache);// 读取(配合后台刷新线程)publicUsergetUserWithLogicalExpire(Longid){Stringkey="hot:user:"+id;LogicalCache<User>cache=(LogicalCache<User>)redisTemplate.opsForValue().get(key);if(cache!=null){if(System.currentTimeMillis()>cache.getExpireTime()){refreshUserCacheAsync(id);// 异步刷新}returncache.getData();// 即使过期也返回旧值}returnloadFromDBAndSetCache(id);}

3. 缓存击穿(Cache Breakdown)

  • 定义某个热点 key 在过期瞬间,大量并发请求同时发现缓存失效,全部查 DB
  • 特点:单个 key 失效 + 高并发 → DB 瞬时压力
✅ 解决方案
  • 互斥锁(Mutex Lock)
  • 热点 key 永不过期
✅ Java 示例:分布式互斥锁重建缓存
publicUsergetUserWithMutex(Longid){Stringkey="user:"+id;Useruser=(User)redisTemplate.opsForValue().get(key);if(user!=null)returnuser;StringlockKey="lock:user:"+id;BooleanisLocked=redisTemplate.opsForValue().setIfAbsent(lockKey,"1",Duration.ofMillis(500));// 原子加锁,500ms超时if(Boolean.TRUE.equals(isLocked)){try{// 双重检查user=(User)redisTemplate.opsForValue().get(key);if(user!=null)returnuser;user=userMapper.selectById(id);if(user!=null){redisTemplate.opsForValue().set(key,user,3600,TimeUnit.SECONDS);}else{redisTemplate.opsForValue().set(key,"",60,TimeUnit.SECONDS);}returnuser;}finally{redisTemplate.delete(lockKey);// 释放锁}}else{// 未获取锁,短暂等待后重试try{Thread.sleep(50);returngetUserWithMutex(id);}catch(InterruptedExceptione){Thread.currentThread().interrupt();returnnull;}}}

✅ 关键:SET key value NX EX实现原子锁,必须设超时防死锁。


🛡️ 附加:多级缓存(本地 + Redis)

privatefinalCache<Long,User>localCache=Caffeine.newBuilder().maximumSize(10_000).expireAfterWrite(10,TimeUnit.MINUTES).build();publicUsergetUserMultiLevel(Longid){// 1. 本地缓存Useruser=localCache.getIfPresent(id);if(user!=null&&!"".equals(user))returnuser;// 2. RedisStringredisKey="user:"+id;user=(User)redisTemplate.opsForValue().get(redisKey);if(user!=null){localCache.put(id,user);returnuser;}// 3. DBuser=userMapper.selectById(id);if(user!=null){redisTemplate.opsForValue().set(redisKey,user,3600+newRandom().nextInt(300),SECONDS);localCache.put(id,user);}else{redisTemplate.opsForValue().set(redisKey,"",60,SECONDS);localCache.put(id,newUser());// 空对象标记}returnuser;}

✅ 最佳实践总结

问题推荐方案Java 实现要点
缓存模型Cache Aside先 update DB → delete cache
缓存穿透空值缓存 + 布隆过滤器Guava BloomFilter(单机)或 RedisBloom
缓存雪崩随机 TTL / 逻辑过期new Random().nextInt()+LogicalCache
缓存击穿互斥锁setIfAbsent(..., Duration)+ 双重检查
高可用多级缓存Caffeine + Redis

💡核心思想
缓存不是银弹,没有 100% 一致性
所有方案都是在一致性、可用性、性能之间做权衡。
根据业务容忍度选择合适策略,才是工程之道。


作者:不会写程序的未来程序员
首发于 CSDN
版权声明:本文为原创文章,转载请注明出处。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/1 5:05:16

MySQL索引优化实战指南:SOAR与SQLAdvisor在Archery平台的应用对比

MySQL索引优化实战指南&#xff1a;SOAR与SQLAdvisor在Archery平台的应用对比 【免费下载链接】Archery hhyo/Archery: 这是一个用于辅助MySQL数据库管理和开发的Web工具。适合用于需要管理和开发MySQL数据库的场景。特点&#xff1a;易于使用&#xff0c;具有多种数据库管理功…

作者头像 李华
网站建设 2026/4/28 22:37:21

Meiam System 企业级权限框架终极指南:构建现代化前后端分离应用

Meiam System 企业级权限框架终极指南&#xff1a;构建现代化前后端分离应用 【免费下载链接】Meiam.System .NET 7 / .NET 5 WebAPI Vue 2.0 RBAC 企业级前后端分离权限框架 项目地址: https://gitcode.com/gh_mirrors/me/Meiam.System 项目魅力展示 在数字化转型浪…

作者头像 李华
网站建设 2026/4/21 1:00:29

3分钟快速集成eventpp:C++事件处理库的终极入门指南

3分钟快速集成eventpp&#xff1a;C事件处理库的终极入门指南 【免费下载链接】eventpp eventpp - 一个为C提供的事件分派器和回调列表库。 项目地址: https://gitcode.com/gh_mirrors/ev/eventpp eventpp是一个功能强大的C事件处理库&#xff0c;提供事件分发器、回调列…

作者头像 李华
网站建设 2026/4/29 17:04:01

320亿参数对标GPT-4o:GLM-4-32B系列开源模型重塑企业级AI应用格局

320亿参数对标GPT-4o&#xff1a;GLM-4-32B系列开源模型重塑企业级AI应用格局 【免费下载链接】GLM-4-32B-Base-0414 项目地址: https://ai.gitcode.com/zai-org/GLM-4-32B-Base-0414 导语 智谱AI最新发布的GLM-4-32B-0414系列开源大模型&#xff0c;以320亿参数实现媲…

作者头像 李华
网站建设 2026/5/1 4:54:46

STM32 DS18B20温度传感器库:5个关键技巧让嵌入式开发更高效

STM32 DS18B20温度传感器库&#xff1a;5个关键技巧让嵌入式开发更高效 【免费下载链接】ds18b20 ds18b20 library for stm32 hal 项目地址: https://gitcode.com/gh_mirrors/ds1/ds18b20 STM32 DS18B20温度传感器库是一个专为STM32微控制器设计的轻量级非阻塞驱动库&am…

作者头像 李华
网站建设 2026/5/1 6:02:29

llama.vim:让Vim编辑体验如虎添翼的智能补全神器

llama.vim&#xff1a;让Vim编辑体验如虎添翼的智能补全神器 【免费下载链接】llama.vim Vim plugin for LLM-assisted code/text completion 项目地址: https://gitcode.com/gh_mirrors/ll/llama.vim 还在为重复敲打相似的代码片段而烦恼吗&#xff1f;是否经常在编辑文…

作者头像 李华