发布时间:2026/6/15 21:57:58
Redis 缓存一致性方案:从缓存穿透到数据同步,分布式系统的缓存治理
Redis 缓存一致性方案从缓存穿透到数据同步分布式系统的缓存治理一、缓存一致性的本质矛盾性能与一致性的不可能三角Redis 缓存的核心价值是提升读取性能但引入缓存后数据存储在两个位置数据库和 Redis。任何写操作都需要同时更新两份数据而两步操作无法在分布式环境中原子执行。这就产生了缓存一致性问题——数据库和 Redis 中的数据可能不一致。更具体地一致性问题的场景包括写操作先更新数据库再删缓存在删除缓存前读请求可能读到旧缓存写操作先删缓存再更新数据库在更新数据库前读请求可能将旧数据重新写入缓存并发写场景下后发的写请求可能先完成数据库更新先发的写请求后完成导致缓存与数据库顺序不一致。不存在完美的缓存一致性方案每种方案都是在性能、一致性、复杂度之间的权衡。理解这些权衡才能为不同业务场景选择合适的方案。二、缓存一致性方案对比与架构设计常见的缓存一致性方案有四种Cache Aside、Read/Write Through、Write Behind、Refresh Ahead。每种方案的一致性保证和适用场景不同。flowchart TD A[缓存一致性方案] -- B[Cache Aside: 旁路缓存] A -- C[Read/Write Through: 读写穿透] A -- D[Write Behind: 异步写入] A -- E[Refresh Ahead: 预刷新] B -- B1[读: 先缓存→未命中查DB→写缓存] B -- B2[写: 先更新DB→删缓存] B -- B3[一致性: 最终一致, 延迟秒级] C -- C1[读: 缓存层代理读] C -- C2[写: 缓存层代理写] C -- C3[一致性: 强一致, 性能低] D -- D1[写: 先写缓存→异步写DB] D -- D2[风险: 宕机丢数据] D -- D3[一致性: 弱一致, 吞吐高] E -- E1[读: 预加载即将过期数据] E -- E2[写: 同 Cache Aside] E -- E3[一致性: 最终一致, 读延迟低] style B fill:#e8f5e9 style D fill:#ffcdd22.1 Cache Aside 方案的延迟双删// CacheAsideManager.java — Cache Aside 延迟双删方案 // 设计意图通过先删缓存→更新DB→延迟再删缓存的三步操作 // 解决并发读写导致的缓存不一致问题 import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; public class CacheAsideManager { private final JedisPool jedisPool; private final DatabaseAccessor dbAccessor; private final long delayDeleteMs; // 延迟删除的等待时间 public CacheAsideManager(JedisPool jedisPool, DatabaseAccessor dbAccessor, long delayDeleteMs) { this.jedisPool jedisPool; this.dbAccessor dbAccessor; this.delayDeleteMs delayDeleteMs; } // 读操作先查缓存未命中查数据库并回填 public String get(String key) { try (Jedis jedis jedisPool.getResource()) { // 1. 查询缓存 String cachedValue jedis.get(key); if (cachedValue ! null) { return cachedValue; } // 2. 缓存未命中查询数据库 String dbValue dbAccessor.query(key); if (dbValue null) { // 防止缓存穿透空值也缓存设置短过期时间 jedis.setex(key, 60, NULL); return null; } // 3. 回填缓存设置过期时间作为兜底 jedis.setex(key, 3600, dbValue); return dbValue; } } // 写操作延迟双删 public void put(String key, String value) { // 第 1 步先删除缓存 try (Jedis jedis jedisPool.getResource()) { jedis.del(key); } // 第 2 步更新数据库 dbAccessor.update(key, value); // 第 3 步延迟再次删除缓存 // 解决步骤 1 删缓存后、步骤 2 更新 DB 前 // 并发读请求将旧数据重新写入缓存的问题 scheduleDelayedDelete(key); } private void scheduleDelayedDelete(String key) { Thread.ofVirtual().start(() - { try { Thread.sleep(delayDeleteMs); try (Jedis jedis jedisPool.getResource()) { jedis.del(key); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); } }2.2 基于 Canal 的数据库变更监听// CanalCacheSync.java — 基于 Canal 的缓存同步 // 设计意图监听 MySQL Binlog 变更自动删除或更新对应缓存 // 实现数据库与缓存的最终一致性无需在业务代码中处理缓存删除 import com.alibaba.otter.canal.client.*; import com.alibaba.otter.canal.protocol.*; import com.alibaba.otter.canal.protocol.exception.CanalClientException; public class CanalCacheSync { private final CanalConnector connector; private final JedisPool jedisPool; private volatile boolean running true; public CanalCacheSync(String canalHost, int canalPort, String destination, JedisPool jedisPool) { this.connector CanalConnectors.newSingleConnector( new InetSocketAddress(canalHost, canalPort), destination, , ); this.jedisPool jedisPool; } public void start() { Thread.ofPlatform().start(() - { connector.connect(); connector.subscribe(.*\\..*); // 订阅所有库所有表 connector.rollback(); while (running) { try { Message message connector.getWithoutAck(100); long batchId message.getId(); ListRowData rows message.getEntries().stream() .filter(e - e.getEntryType() EntryType.ROWDATA) .flatMap(e - e.getRowDatas().stream()) .toList(); for (RowData row : rows) { processRowChange(row); } connector.ack(batchId); } catch (CanalClientException e) { // Canal 连接异常重试 try { Thread.sleep(1000); connector.reconnect(); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); break; } } } connector.disconnect(); }); } public void stop() { running false; } private void processRowChange(RowData row) { // 根据变更的表和主键构建缓存 key 并删除 // 这里需要建立 表名主键 → 缓存 key 的映射规则 String tableName extractTableName(row); String primaryKey extractPrimaryKey(row); String cacheKey buildCacheKey(tableName, primaryKey); try (Jedis jedis jedisPool.getResource()) { jedis.del(cacheKey); } } private String extractTableName(RowData row) { // 从 RowData 中提取表名 return table; // 简化 } private String extractPrimaryKey(RowData row) { // 从 RowData 中提取主键值 return pk; // 简化 } private String buildCacheKey(String tableName, String primaryKey) { return String.format(cache:%s:%s, tableName, primaryKey); } }三、缓存异常场景处理3.1 缓存穿透防护// PenetrationGuard.java — 缓存穿透防护 // 设计意图防止大量请求查询不存在的数据绕过缓存直接打到数据库 import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import java.util.*; public class PenetrationGuard { private final JedisPool jedisPool; private final BloomFilterString bloomFilter; // 布隆过滤器 private final String nullValueMarker NULL_CACHE; public PenetrationGuard(JedisPool jedisPool, int expectedInsertions) { this.jedisPool jedisPool; // 使用布隆过滤器预判数据是否存在 this.bloomFilter BloomFilter.create( Funnels.stringFunnel(), expectedInsertions, 0.01 ); } public String getWithGuard(String key, DatabaseAccessor dbAccessor) { // 第 1 层防护布隆过滤器判断数据是否可能存在 if (!bloomFilter.mightContain(key)) { return null; // 数据一定不存在直接返回 } try (Jedis jedis jedisPool.getResource()) { // 第 2 层防护查询缓存 String cached jedis.get(key); if (cached ! null) { if (nullValueMarker.equals(cached)) { return null; // 空值缓存命中 } return cached; } // 第 3 层查询数据库 String dbValue dbAccessor.query(key); if (dbValue null) { // 空值缓存防止重复穿透 jedis.setex(key, 60, nullValueMarker); return null; } // 正常回填缓存 jedis.setex(key, 3600, dbValue); return dbValue; } } // 新数据写入时同步更新布隆过滤器 public void addToBloomFilter(String key) { bloomFilter.put(key); } } // 简化的布隆过滤器接口 interface BloomFilterT { static T BloomFilterT create(com.google.common.hash.FunnelT funnel, int expectedInsertions, double fpp) { return null; // 实际使用 Guava BloomFilter } boolean mightContain(T key); void put(T key); }3.2 缓存雪崩防护// AvalancheGuard.java — 缓存雪崩防护 // 设计意图防止大量缓存同时过期导致请求全部穿透到数据库 import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import java.util.Random; public class AvalancheGuard { private final JedisPool jedisPool; private final Random random new Random(); private final int baseExpireSeconds; private final int expireJitterSeconds; // 过期时间抖动范围 public AvalancheGuard(JedisPool jedisPool, int baseExpireSeconds, int expireJitterSeconds) { this.jedisPool jedisPool; this.baseExpireSeconds baseExpireSeconds; this.expireJitterSeconds expireJitterSeconds; } // 设置带随机抖动的过期时间 public void setWithJitter(String key, String value) { int expireSeconds baseExpireSeconds random.nextInt(expireJitterSeconds); try (Jedis jedis jedisPool.getResource()) { jedis.setex(key, expireSeconds, value); } } // 互斥锁重建缓存防止并发请求同时重建 public String getWithMutex(String key, DatabaseAccessor dbAccessor) { try (Jedis jedis jedisPool.getResource()) { String cached jedis.get(key); if (cached ! null) { return cached; } // 尝试获取互斥锁 String lockKey lock: key; String lockValue String.valueOf(System.currentTimeMillis()); String result jedis.set(lockKey, lockValue, NX, EX, 10); if (OK.equals(result)) { try { // 获取锁成功查询数据库并重建缓存 String dbValue dbAccessor.query(key); if (dbValue ! null) { setWithJitter(key, dbValue); } return dbValue; } finally { // 释放锁使用 Lua 脚本保证原子性 releaseLock(jedis, lockKey, lockValue); } } else { // 获取锁失败短暂等待后重试 try { Thread.sleep(50); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return jedis.get(key); // 再次查询缓存 } } } private void releaseLock(Jedis jedis, String lockKey, String lockValue) { String script if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; jedis.eval(script, 1, lockKey, lockValue); } }四、边界分析与架构权衡延迟双删的时间窗口延迟删除的等待时间需要大于一次读请求的完整耗时查缓存→查DB→写缓存否则无法覆盖并发读场景。但等待时间过长会增加不一致窗口。通常设置为 500ms-1s需要根据 P99 读延迟调整。Canal 的可靠性Canal 监听 Binlog 的方式依赖 MySQL 的 Binlog 配置。如果 Binlog 被清理或 Canal 消费延迟缓存删除可能丢失。必须监控 Canal 的消费延迟设置延迟告警。同时Canal 本身是单点需要部署高可用集群。布隆过滤器的误判率布隆过滤器判断可能存在时有一定概率是误判数据实际不存在。误判率与过滤器大小和插入数量有关。对于缓存穿透防护误判意味着少量请求仍会穿透到数据库这是可接受的。互斥锁的竞争缓存重建时的互斥锁会导致同一时刻只有一个请求查询数据库其他请求等待。如果数据库查询耗时较长等待的请求可能超时。需要设置合理的锁超时时间和等待重试次数。五、总结Redis 缓存一致性方案的核心是在性能与一致性之间找到平衡。Cache Aside 延迟双删是最常用的方案适用于大多数业务场景Canal 监听 Binlog 实现自动缓存同步减少业务代码的侵入性布隆过滤器和空值缓存防止穿透随机过期时间和互斥锁防止雪崩。落地建议读多写少的场景使用 Cache Aside 延迟双删对一致性要求高的场景引入 Canal 监听缓存过期时间加随机抖动避免雪崩空值缓存设置短过期时间防止穿透同时避免内存浪费。

相关新闻

PXS20微控制器ADC中断机制详解:从架构到实战配置
2026/6/15 21:57:58

PXS20微控制器ADC中断机制详解:从架构到实战配置

1. 项目概述与核心价值在嵌入式开发,尤其是汽车电子和工业控制领域,模数转换器(ADC)扮演着连接物理世界与数字系统的桥梁角色。我们常常需要实时监控电池电压、采集温度传感器数据或检测电机电流,这些场景对数据的及时…

阅读更多
ZC706P+ADRV9009连接RADIOVERSE踩坑实录:从SD卡镜像制作到软件联调的全流程避坑指南
2026/6/15 20:57:58

ZC706P+ADRV9009连接RADIOVERSE踩坑实录:从SD卡镜像制作到软件联调的全流程避坑指南

ZC706PADRV9009连接RADIOVERSE实战避坑指南:从镜像制作到系统联调的深度解析当硬件工程师第一次将ZC706P开发板与ADRV9009射频收发器组合使用时,往往会遇到一系列令人困惑的技术障碍。本文将以实战视角,剖析从SD卡镜像制作到软件联调全流程中…

阅读更多
Colab或Kaggle跑Hugging Face代码总报错?可能是transformers库版本与PyTorch环境不兼容了
2026/6/15 20:57:58

Colab或Kaggle跑Hugging Face代码总报错?可能是transformers库版本与PyTorch环境不兼容了

Colab与Kaggle环境下的transformers库版本冲突实战指南 当你兴奋地在Colab或Kaggle上打开一个新笔记本,准备运行最新的Hugging Face代码时,突然遭遇 ImportError: Using the Trainer with PyTorch requires accelerate>0.20.1 这样的错误提示&…

阅读更多
避开这些坑!Simulink连接CCS生成DSP代码的环境配置全记录
2026/6/15 22:57:58

避开这些坑!Simulink连接CCS生成DSP代码的环境配置全记录

Simulink与CCS代码生成环境搭建的深度避坑指南 当Simulink遇上TI Code Composer Studio(CCS),理论上应该是一段美好的技术联姻——模型驱动开发直接生成可部署的DSP代码。但现实中,这个环境搭建过程往往成为开发者噩梦的开始。本文…

阅读更多
Windows 10也能畅享Android应用?3分钟搞定原生级体验
2026/6/15 22:57:58

Windows 10也能畅享Android应用?3分钟搞定原生级体验

Windows 10也能畅享Android应用?3分钟搞定原生级体验 【免费下载链接】WSA-Windows-10 This is a backport of Windows Subsystem for Android to Windows 10. 项目地址: https://gitcode.com/gh_mirrors/ws/WSA-Windows-10 还在为Windows 10无法运行Androi…

阅读更多
Hi9103:150V耐压内置2.5A MOS,恒压恒流降压芯片
2026/6/15 22:57:58

Hi9103:150V耐压内置2.5A MOS,恒压恒流降压芯片

一、产品背景在84V电动车、110V工业母线、太阳能板串联等高压应用场景中,普通降压芯片耐压不足(常见60V或100V),往往需要外置高压MOS或采用两级变换,导致电路复杂、成本增加。Hi9103是Hi910X系列中耐压最高且内置大电流…

阅读更多
手把手教你为DSP28335配置Simulink代码生成环境(含TI软件下载与MATLAB编译器安装)
2026/6/15 22:57:58

手把手教你为DSP28335配置Simulink代码生成环境(含TI软件下载与MATLAB编译器安装)

从零搭建DSP28335的Simulink代码生成环境:TI工具链与MATLAB深度整合指南第一次接触德州仪器C2000系列DSP的开发时,最令人头疼的莫过于各种开发环境的配置。特别是当需要将Simulink模型直接生成可部署代码时,软件工具链的安装与配置往往成为拦…

阅读更多
【共创季稿事节】鸿蒙ArkTS布局实战_Column交叉轴对齐
2026/6/15 22:57:58

【共创季稿事节】鸿蒙ArkTS布局实战_Column交叉轴对齐

鸿蒙原生ArkTS布局实战:Column 交叉轴对齐 HorizontalAlign.Start / Center / End 一、引言 HarmonyOS NEXT(API 24)全面采用 ArkTS 声明式 UI 范式,开发者通过 Component 组合 Column、Row、Flex 等布局容器构建页面。 Column …

阅读更多
从一次LabelImg闪退报错,聊聊Python GUI开发中那些‘坑爹’的数据类型转换
2026/6/15 21:57:58

从一次LabelImg闪退报错,聊聊Python GUI开发中那些‘坑爹’的数据类型转换

从LabelImg闪崩溃看Python GUI开发中的类型陷阱:防御性编程实战指南当你在LabelImg中精心标注到第87张图片时,程序突然闪退并抛出TypeError: argument 1 has unexpected type float——这个看似简单的类型错误背后,隐藏着Python GUI开发中一系…

阅读更多
别再只用BERT了!用Transformers库的AutoModel,5分钟搞定文本相似度计算(附代码对比)
2026/6/14 0:57:30

别再只用BERT了!用Transformers库的AutoModel,5分钟搞定文本相似度计算(附代码对比)

超越BERT:用Transformers库高效实现文本相似度计算的三种实战方案在自然语言处理领域,文本相似度计算是信息检索、问答系统和推荐系统等应用的核心技术。传统方法如TF-IDF或Word2Vec已逐渐被基于Transformer的预训练模型所取代。Hugging Face的Transform…

阅读更多
Prompt Engineering:重构人机协作的工程化方法论
2026/6/14 0:57:30

Prompt Engineering:重构人机协作的工程化方法论

1. 项目概述:这不是“写提示词”,而是重构人机协作的底层逻辑“Prompt Engineering”这个词,这两年被讲得太多,也太轻飘。很多人把它理解成“给AI发指令的技巧”,甚至简化为“多加几个形容词”“换种说法再试一次”。我…

阅读更多
Anthropic提示层归零:模型即协议的工程实践
2026/6/14 0:57:30

Anthropic提示层归零:模型即协议的工程实践

1. 项目概述:这不是一次普通更新,而是一次架构级“蒸发”“Anthropic Just Shipped the Layer That’s Already Going to Zero”——这个标题一出来,我正在调试一个Claude调用链的终端前停了三秒。不是因为震惊,而是因为熟悉&…

阅读更多
TEKLauncher:终极ARK模组管理与性能优化解决方案
2026/6/15 0:57:55

TEKLauncher:终极ARK模组管理与性能优化解决方案

TEKLauncher:终极ARK模组管理与性能优化解决方案 【免费下载链接】TEKLauncher Launcher for ARK: Survival Evolved 项目地址: https://gitcode.com/gh_mirrors/te/TEKLauncher 你是否为ARK: Survival Evolved复杂的模组管理和服务器连接问题而烦恼&#xf…

阅读更多
如何3分钟免费解锁Cursor Pro:终极AI编程助手破解方案
2026/6/15 0:57:55

如何3分钟免费解锁Cursor Pro:终极AI编程助手破解方案

如何3分钟免费解锁Cursor Pro:终极AI编程助手破解方案 【免费下载链接】cursor-free-vip [Support 0.45](Multi Language 多语言)自动注册 Cursor Ai ,自动重置机器ID , 免费升级使用Pro 功能: Youve reached your tri…

阅读更多
21.2 mcp-server-chart 图表化作用
2026/6/15 0:57:55

21.2 mcp-server-chart 图表化作用

如何检查 langchain_mcp_adapters 版本和 antv/mcp-server-chart 安装 1. 检查 langchain_mcp_adapters 版本 在终端(确保已激活虚拟环境)中运行: pip show langchain_mcp_adapters输出示例: Name: langchain-mcp-adapters Ve…

阅读更多
GIT修改用户名
2026/6/14 11:53:59

GIT修改用户名

在GIT中修改用户名可按以下步骤操作: 查看当前git的用户名,使用命令git config --list或git config user.name。修改git用户名,使用命令git config --global user.name "xxx(新的用户名)",将其中…

阅读更多
Win11Debloat:让你的Windows系统重获新生的终极优化工具
2026/6/15 2:21:34

Win11Debloat:让你的Windows系统重获新生的终极优化工具

Win11Debloat:让你的Windows系统重获新生的终极优化工具 【免费下载链接】Win11Debloat A simple, lightweight PowerShell script that allows you to remove pre-installed apps, disable telemetry, as well as perform various other changes to declutter and …

阅读更多
技术深度解析:m4s-converter实现原理与B站缓存视频转换最佳实践
2026/6/15 21:13:35

技术深度解析:m4s-converter实现原理与B站缓存视频转换最佳实践

技术深度解析:m4s-converter实现原理与B站缓存视频转换最佳实践 【免费下载链接】m4s-converter 一个跨平台小工具,将bilibili缓存的m4s格式音视频文件合并成mp4 项目地址: https://gitcode.com/gh_mirrors/m4/m4s-converter m4s-converter是一个…

阅读更多