从单机 Redis 扛不住流量那天开始,我就知道必须要上一套真正的分布式缓存系统了。这个项目回头看不复杂,但踩的坑、趟的路一点都不少,今天把它从头到尾拆开讲,从设计思路、核心实现到运维排查,完整还原一套分布式缓存系统是怎么落地并稳定跑在业务里的。
如果你正在做缓存选型、正在从单机缓存向集群演进,或者是想深入了解分布式缓存底层原理的研发同学,这篇文章应该能帮你省掉不少试错成本。我不讲教科书式的概念堆砌,只讲实际干活时真正用得上的东西。
1. 项目背景与整体设计
1.1 从单机缓存到分布式缓存的演进动机
先说业务背景。我们当时有一个用户画像查询服务,高峰期 QPS 能冲到 8 万以上,单机 Redis 主从架构撑到 3 万 QPS 就已经出现了明显的 CPU 和带宽瓶颈。最难受的是,单机模式下内存上限就是瓶颈,想存更多热点数据只能不停扩内存,而内存价格远高于计算资源,这种线性扩容的模式既不经济也不可持续。于是我们需要一个能横向扩展、存储能力和吞吐能力都能跟着节点数量线性增长的方案。
分布式缓存和单机缓存的本质区别,在于它把数据分散到多个节点上,通过分片让每个节点只承担一部分数据的读写。这样带来两个直接好处:第一,容量不再受单机内存限制,加节点就能加容量;第二,请求被打散到多台机器,单节点的吞吐压力大幅下降。但代价也随之而来——数据如何分片、节点挂了怎么办、请求路由怎么做,都需要额外设计,这也是整套系统实现的核心难点。
除了性能瓶颈,我们还需要解决一个更现实的问题:多业务线共用缓存。每个业务线的数据访问模式差异很大,有些是读多写少的高频访问,有些是大批量的离线写入。如果共用一个单机缓存,互相干扰会非常严重,某个业务线的突发流量很可能把整个缓存集群打挂。分布式缓存天然支持按业务线隔离命名空间和资源池,这也成了推进项目的一个重要理由。
1.2 技术选型:自研还是基于开源方案
先确定选型边界。当时 Redis Cluster 已经成熟,Codis 也还在维护期,自研分布式缓存看起来是重复造轮子。但我们的场景有几个特殊要求:一是部分接口对极端延迟敏感,希望客户端能感知节点状态、做本地路由,减少代理层跳数;二是我们有多套机房互备的需求,开源方案在跨机房场景下的支持不够灵活;三是团队成员更希望把底层数据分片逻辑掌握在自己手里,方便后续做定制化开发。综合评估后,我们选择了“基于 Redis 数据引擎 + 自研分片路由层”的半自研路线,而不是直接使用 Redis Cluster。
这个决定的核心思路是:数据引擎用成熟的,分布式编排自己写。Redis 单机的数据结构和持久化能力是经过大规模验证的,没必要重写;但分片、路由、节点管理、故障转移这些分布式系统问题,我们希望通过自研代码来精确控制。Redis Cluster 的槽位分配方案虽然不错,但它把路由逻辑和服务端耦合在一起,客户端实现必须遵循固定的协议,这对我们想在客户端做灵活路由和本地缓存的需求限制了太多。
同时我们也在客户端和服务端之间做了一层轻量代理,代理只做协议转发和连接管理,真正的分片计算放在客户端 SDK 里。这样既保留了直连 Redis 的低延迟优势,又能在需要的时候通过代理做跨语言支持和统一监控。整套系统的核心模块包括:分片路由模块、节点管理模块、序列化模块、故障转移模块和监控模块,后面我会逐个拆开讲。
1.3 系统整体架构与模块划分
系统整体上由四层组成:接入层、路由层、数据层、治理层。接入层封装了各种语言 SDK 和代理,业务方看到的是简单的 get/set 接口,完全不感知底层集群拓扑。路由层是核心,负责根据 key 计算分片位置、维护节点上下线状态、处理异常时的重试和降级。数据层是真实的 Redis 节点,每个节点以主从方式部署,主节点提供服务,从节点负责备份和哨兵监控。治理层包括配置中心、监控告警、数据迁移工具,负责整个集群的运维管理。
在模块划分上,分片路由模块和节点管理模块是紧密配合的。路由模块每次请求都需要知道 key 落在哪个节点,节点管理模块则负责把集群状态实时同步给路由模块。我们引入了一个内存中的集群拓扑视图,由专门的协调者更新,客户端通过订阅机制及时感知变更。这个视图至少包含节点列表、分片映射关系、节点健康状态三部分信息。
code复制// 集群拓扑视图的核心结构
public class ClusterTopology {
private Map<String, PhysicalNode> nodes; // 物理节点列表
private ConsistentHashRing hashRing; // 一致性哈希环
private Map<String, String> virtualToPhysical; // 虚拟节点到物理节点的映射
private volatile long version; // 变更版本号,用于增量更新
}
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心关键技术拆解
2.1 数据分片与一致性哈希
数据分片是分布式缓存的基石。业界主流的两种分片方案是范围分片和哈希分片。范围分片把 key 按字典序分成一段一段的范围,每个节点负责一个区间,优点是顺序遍历友好,但容易出现数据倾斜。哈希分片则通过对 key 做哈希运算,将数据均匀映射到节点上,实现简单、分布均匀,是缓存系统的首选方案。
哈希分片里最经典的算法是取模哈希和一致性哈希。取模哈希实现简单,但节点数量一旦变化,绝大多数 key 的映射关系都会改变,导致大量缓存同时失效,这就是所谓的“缓存雪崩”隐患。一致性哈希通过哈希环解决了这个问题,每个节点在环上占据一个位置,key 哈希后沿环顺时针寻找第一个节点,当节点增减时,只有该节点附近的一段数据需要迁移,影响面大大缩小。
实际实现时,我还加了一层虚拟节点机制。物理节点相对较少时,直接映射到哈希环上容易因为节点位置不均匀导致数据倾斜。解决方法是为每个物理节点生成几十个甚至上百个虚拟节点,分散在环上,这样数据分布就变得非常均匀。比如我们有 10 台物理节点,每个节点分配 100 个虚拟节点,环上就有 1000 个点,分布效果远超直接映射。
code复制// 一致性哈希环的简化实现(Java)
public class ConsistentHashRing {
private final TreeMap<Long, PhysicalNode> ring = new TreeMap<>();
private final int virtualNodeCount = 100;
public void addNode(PhysicalNode node) {
for (int i = 0; i < virtualNodeCount; i++) {
long hash = hash(node.getId() + "#" + i);
ring.put(hash, node);
}
}
public PhysicalNode route(String key) {
long hash = hash(key);
SortedMap<Long, PhysicalNode> tailMap = ring.tailMap(hash);
Long targetHash = tailMap.isEmpty() ? ring.firstKey() : tailMap.firstKey();
return ring.get(targetHash);
}
}
有一点必须提醒:一致性哈希只保证映射稳定,不保证数据完全均匀。如果键空间分布本身有倾斜,或者某些 key 的请求量特别高,仍然会出现热点。我在系统里额外做了一层热点 key 识别和扩散,后面在监控调优部分详细讲。
2.2 节点发现与集群管理
节点管理是分布式缓存里容易被忽视但非常重要的一环。节点上下线、故障剔除、配置变更,这些事件如果处理不好,轻则路由错误,重则整个集群不可用。我采用的方式是引入一个独立的协调组件,让它维护集群状态的权威视图,各客户端和代理定期拉取并增量更新。
协调组件的选型上,我们最开始用 ZooKeeper 做节点注册与发现,后来觉得维护成本偏高,切换到了 etcd。etcd 的 watch 机制能实时推送 key 变更事件,对节点状态感知的延迟可以控制在毫秒级,这对缓存路由来说非常关键。每个 Redis 节点启动时在 etcd 中注册临时节点,包含节点 IP、端口、角色、权重等信息,同时开启心跳续约。协调组件发现临时节点消失,就判定该节点异常,触发故障转移流程。
心跳检测除了依赖 etcd,我们还在每个客户端维持一条异步的探活链路。客户端缓存了全量节点列表,每隔几秒直接向节点发送 PING 命令快速探测。两个机制互相配合:etcd 负责状态权威变更,客户端探测负责本地快速感知。某个节点如果客户端连续 N 次探活失败,即使 etcd 还显示正常,也会先在本地将该节点标记为不可用,请求直接走降级策略。这个设计在真实故障中救过我们很多次。
2.3 数据序列化与协议设计
分布式缓存系统就要考虑网络传输的序列化开销。同样的数据,用 JSON 序列化和用 MessagePack 序列化,占用的带宽和 CPU 可能差好几倍。在高吞吐场景下,这些开销会直接变成延迟。
我们最终采用的序列化方案是:key 统一用二进制字节流,value 采用混合序列化策略。对于简单字符串类型直接透传;对于复杂对象,定义了基于 MessagePack 的结构化格式;对于高频访问的小对象,则使用自定义的二进制编码,去掉字段名,只保留字段值和类型标记。这套方案相比 JSON,整体序列化体积减少了约 45%,CPU 开销下降了 30% 左右。
协议设计上,我们没有完全照搬 Redis 的 RESP 协议,而是在业务 SDK 内部做了一层轻量封装。请求格式很简单:请求头包含服务名、操作类型、key 长度和 value 长度,请求体包含真正的 key 和 value 二进制数据。代理层收到请求后解析请求头,用 key 计算分片位置,再转发给目标节点。这样协议虽然简单,但足够高效,而且方便在客户端和服务端之间增加 traceID 等上下文信息,排查问题非常方便。
2.4 缓存淘汰与内存管理
数据放进分布式缓存后,内存管理就成了必须面对的问题。每台节点内存有限,不可能把所有数据都留在内存里,所以必须要有淘汰策略。Redis 官方提供了多种淘汰策略,包括 LRU、LFU、随机淘汰、TTL 淘汰等。我在系统里主要启用了 two 种策略的组合:对于有明确过期时间的业务数据,依赖过期时间自然淘汰;对于长期有效的热点数据,使用近似 LRU 策略淘汰冷数据。
但这里有个容易踩的坑:Redis 的近似 LRU 并不是真正的 LRU,它采样一部分 key 来近似判断冷热度。这个近似性在高写入场景下,偶尔会把刚写入的 key 误淘汰。我在项目中增加了一个保护机制,在 SDK 层维护一个热 key 白名单,白名单中的 key 即使被 Redis 淘汰,客户端也会主动重新加载,并更新过期时间,保证核心数据不会被误淘汰。
另外,内存碎片也是分布式缓存运维中的隐藏杀手。长时间运行后,Redis 内存碎片率会升高,可能导致明明 used_memory 不大,但 RSS 内存很高。我们设置了 cron 定期对碎片率过高的节点执行 memory purge 操作,同时监控 RSS 和 used_memory 的比值,一旦超过 1.5 就触发告警并及时处理。
code复制# 内存碎片率检查项
redis-cli info memory | grep mem_fragmentation_ratio
# 经验阈值:低于1.0说明使用了swap,高于1.5说明碎片严重
3. 实操过程:从零搭建分布式缓存
3.1 环境准备与基础组件
整个系统虽然看起来复杂,实际搭建时能在一个晚上跑通基础版本。我先说环境准备。我们用的是 3 台物理机加 6 个 Redis 实例,每台机器上分布 2 个 Redis 实例,其中一个主一个从,主从搭配保证高可用。操作系统是 CentOS,Redis 版本选的 6.2,主要看中它的多线程 IO 能力和更好的过期机制。
除了 Redis,还要部署 etcd 集群。etcd 我们用了 3 节点,这个数量是保证 raft 容错的最小推荐配置。每个节点上用 systemd 管理 etcd 服务,数据目录单独挂载 SSD。这块有个经验:etcd 节点时钟一定要做 NTP 同步,否则 raft 选举会因为时钟漂移频繁触发,我们一开始没注意,上线后出现过两次莫名的 leader 切换。
然后是客户端 SDK,我们是 Java 技术栈,基于 Spring Boot 封装了一个 starter,内部集成了一致性哈希路由、连接池管理、监控上报等模块。搭建时先在本地起这个 starter,配置文件指定 etcd 地址和集群前缀,启动后 SDK 会自动从 etcd 拉取集群拓扑并建立连接。整个初始化过程不用人工指定任何节点信息,全部服务发现自动化完成。
3.2 一致性哈希客户端实现
客户端实现是整个系统最关键的编码部分。我以 Java 为例讲核心步骤。第一步是实现一致性哈希环,也就是上一节展示的代码;第二步是接入节点动态变更,监听 etcd 的 watch 事件,热更新哈希环。
热更新这块有个很重要的细节:更新哈希环时必须保证读写操作的一致性。比如一个 key 原来路由到节点 A,节点管理模块把节点 A 下线后,一致性哈希环发生变化,同一个 key 现在路由到了节点 B。如果读请求先去 B 找,找不到再去 A 找,同时写请求已经写到了 B,就可能读到旧数据。我当时的解决方式是在 SDK 内维护了一份“旧拓扑 + 新拓扑”的过渡期视图,在过渡期内读请求先查新拓扑,找不到再查旧拓扑,写请求只写新拓扑,过渡期结束自动释放旧拓扑。
客户端还要实现路由失败时的重试机制。重试不是简单地把请求再发一次,而是首先要区分错误类型。连接超时这类可重试错误,可以换节点重试;但如果是数据不存在或者参数错误,就不能重试。重试时还要注意避免对同一个 key 因为哈希环震荡导致多次跨节点访问,我们在重试逻辑里设置了一个最大重试次数和退避时间,防止故障节点恢复期间造成请求风暴。
3.3 节点通信与故障转移实现
节点间的通信主要靠两组链路:一组是 Redis 主从之间的复制链路,另一组是哨兵或编排器与节点之间的健康检查链路。我在系统里用了 Sentinel 做节点层面的故障发现,同时用我们自己写的故障转移逻辑做节点切换。
Sentinel 配置相对直接。每个主节点配置 3 个 Sentinel 监控,quorum 设为 2,也就是说至少 2 个 Sentinel 判断主节点客观下线,才会触发故障转移。这样设计是为了避免单点误判。在故障转移过程中,Sentinel 会从从节点中选出一个提升为新的主节点,然后通知其他从节点重新复制新主。这个过程通常需要几秒到十几秒,期间这些节点上的缓存读写会失败,所以客户端必须有降级方案。
我实现的降级方案是双读策略。某分片节点不可用时,客户端会自动把读请求降级到分片对应的从节点(从节点在 Sentinel 模式下可以配置 slave-read-only yes),如果从节点也读不到,就回源到数据库,回源成功后异步把数据写回缓存。写请求在故障转移期间直接写本地消息队列,等集群恢复后异步补偿写入。这套机制让业务方在故障转移期间几乎无感知,只是热点数据的延迟偶尔上升几十毫秒。
code复制# Sentinel 核心配置示例
sentinel monitor mymaster 10.0.0.11 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 15000
sentinel parallel-syncs mymaster 1
3.4 缓存穿透、击穿、雪崩的防护方案
分布式缓存上线后,第一个要面对的问题就是缓存穿透、击穿和雪崩这三个经典故障。我先把三者区分清楚:穿透是查询一个一定不存在的数据,每次都会打到数据库;击穿是单个热点 key 过期的瞬间,大量请求同时穿透到数据库;雪崩是大量 key 在同一时间段集中过期,导致数据库压力瞬间暴涨。
针对缓存穿透,我采用了两层防护。第一层是布隆过滤器,在 SDK 初始化时加载全量存在的 key 集合,查询前先用布隆过滤器判断 key 是否存在,如果过滤器说不存在,直接返回空结果,不再访问缓存和数据库。第二层是空值缓存,即使布隆过滤器判断存在,但如果数据库真的没有也查不到,就把空值以很短的 TTL 写入缓存,避免下一次相同 key 再次穿透数据库。
针对缓存击穿,核心是互斥锁重建。在缓存失效时,只允许一个请求去数据库加载数据并写回缓存,其他请求等待并重新读取缓存。我在项目中用 Redis 的 SETNX 命令实现了分布式锁,锁的 TTL 设置为 3 秒,数据库查询超过 3 秒的情况极少,但为了保险,锁的续期线程会检查是否需要延长。针对缓存雪崩,做法是给缓存 TTL 增加随机扰动,比如基础 TTL 为 10 分钟,实际 TTL 在 8 到 12 分钟之间随机分布,这样大量 key 就不会同时过期。同时,对于核心业务数据,我们采用多级缓存策略,本地 Caffeine 缓存一层,Redis 一层,数据库兜底,尽量减少瞬时冲击面。
4. 高可用与数据一致性保障
4.1 主从复制与哨兵机制
分布式缓存的高可用基础是主从架构。每个分片的数据至少存在于两个节点上,主节点负责写入和读取,从节点实时复制主节点数据,并作为主节点故障时的备胎。Redis 主从复制的实现基于 RDB 快照加增量命令传播,从节点首次连接主节点时拉取全量 RDB,之后主节点将写命令打包成 RESP 协议增量推送给从节点。
这里有个容易出错的地方:主从复制的异步性。从节点复制主节点数据有一个时间差,如果主节点刚写入一个 key 就立即宕机,这个 key 可能还没来得及复制到从节点,切换后数据就丢失了。对于大部分缓存场景,这种级别的数据丢失可以接受,毕竟缓存数据的最终来源是数据库。但对于不能丢的强一致数据,我们建议不要放在缓存系统里,直接用数据库或者消息队列来保证。
哨兵机制的作用是自动完成主从切换。多个哨兵之间通过订阅频道相互通信,共同决策是否执行故障转移。这个机制比客户端自研心跳要成熟得多,所以我建议,即使你用自研的分布式缓存路由层,也不要绕开 Sentinel 这套成熟方案,直接配套使用能省下很多稳定性问题。
4.2 集群模式下的一致性模型
分布式缓存系统必须明确自己的一致性模型。我们对外承诺的是最终一致性,而不是强一致性。所谓最终一致性,就是数据写入缓存后,经过短暂的复制延迟,所有副本最终都会达到一致状态。这个语义对大多数缓存场景足够,因为缓存本身就是数据库的加速层,允许读到稍微旧一点的数据,只要最终一致即可。
但在某些边界场景,这个模型也会带来问题。比如用户更新了个人信息,写入数据库成功后立即清除了缓存中的旧数据,但读请求此刻路由到了还保留旧数据的从节点,用户看到的就是旧信息。应对办法是提供“缓存双删”策略,即在更新数据库后,先删除缓存,短暂延迟后再次删除一次缓存,第二次删除是为了避免并发写回旧数据造成的缓存与数据库不一致。这个延迟一般设为 500 毫秒到 1 秒。
另外,我们还在写入链路上引入版本号机制。每次业务更新数据时,在缓存中写入一个递增的版本号,读取时如果发现缓存版本号小于数据库版本号,则主动失效缓存并回源。这个机制把最终一致性的收敛时间压缩到了秒级,对大多数读多写少业务完全够用。
4.3 多级缓存与最终一致性落地
单靠分布式缓存扛不住所有流量,尤其是在极端热点场景下,必须引入多级缓存。我们最终落地的方案是三层缓存:第一层是业务进程内的本地缓存,使用 Caffeine,TTL 设置为几秒,命中率能达到 60% 到 80%;第二层是 Redis 分布式缓存,TTL 设置为几分钟到几十分钟;第三层是数据库,作为最终数据源。
多级缓存最大的难点是数据一致性。如果本地缓存不回源更新,分布在各业务实例上的本地缓存可能出现数据不一样。我们采取的方式是:本地缓存不主动更新,只依赖短 TTL 自然过期;同时,在数据库更新时通过消息队列发送一条缓存失效事件,所有业务实例收到事件后立即清除本地缓存和 Redis 中对应的 key。这种“短 TTL + 主动失效”双管齐下的方式,既保证了本地缓存的高命中率,又控制了数据不一致的时间窗口。
这套多级缓存上线后,我们线上数据库的查询 QPS 从高峰期的 1.5 万降到了不到 500,绝大多数请求都在两层缓存中被消化掉了。需要强调的一点是,本地缓存一定要设置内存上限和淘汰策略,否则 JVM 堆会被撑爆,我们用 Caffeine 的 maximumSize 参数控制单实例本地缓存数量,配合弱引用策略,效果很好。
5. 监控调优与问题排查实录
5.1 关键监控指标与告警规则
分布式缓存系统上线后,监控的重要性甚至超过开发本身。我梳理了几个必须盯的核心指标:命中率、平均延迟、P99 延迟、连接数、内存使用率、内存碎片率、命令执行失败数、分片节点离线状态。这八个指标覆盖了缓存系统健康度的全部关键维度。
命中率可以说是缓存系统最重要的健康指标。如果命中率突然从 95% 掉到 70%,说明有大量请求穿透到数据库,接下来数据库压力必然会上升。我们分别为每一个业务线设置了命中率看板,并配置了多重告警规则:命中率低于 85% 告警,低于 70% 严重告警。平均延迟和 P99 延迟则是反映缓存性能的直接指标,P99 超过 20 毫秒就要开始排查了,可能是网络抖动、Redis 阻塞或者大 key 阻塞了事件循环。
内存和连接数的监控往往能提前发现隐患。Redis 是单线程事件模型,一旦内存过大引发 RDB 持久化或内存碎片整理,会瞬时阻塞服务。连接数过多则可能是客户端连接池泄漏,会造成文件描述符耗尽。我们在告警规则里设置了连接数超过最大值 80% 时预警,内存使用率超过 80% 时触发扩容流程。
5.2 热点Key与大Key的治理
热点 key 和大 key 是分布式缓存系统运行中最难缠的两个问题。热点 key 指的是某个 key 的访问量极大,比如电商大促时爆款商品的库存 key,可能在单台 Redis 节点上集中了全站 80% 的流量。我遇到过一次,一个热点 key 的 QPS 达到了 30 万,直接把那台 Redis 节点的 CPU 打到了 100%,虽然其他节点很空闲,但整体服务已经不可用。
针对热点 key,我的经验是三层降级措施:第一,在客户端 SDK 层给每个 key 记录访问频次,一旦超过阈值,就把这个 key 的内容复制到本地缓存中,后续请求直接本地返回;第二,将热点 key 的访问打散到多个副本上,比如在原 key 后面拼接随机后缀生成多个缓存副本,写请求同时写所有副本,读请求随机读一个副本;第三,如果热点是数据库层面的行锁,热点 key 对应数据建议直接走数据库的分库分表避免缓存单点。
大 key 则是另一种问题。单个 key 的 value 达到几百 KB 甚至几 MB,处理它的时候,Redis 的事件循环会被长时间占用,导致其他请求排队等待。之前我们线上有一个 key 存了某个用户的完整行为日志,value 超过 5 MB,一次读取耗时接近 100 毫秒,直接把 P99 延迟拉爆。解决方法是把大 key 拆分,用哈希结构把大 value 拆成多个小字段,或者拆分成多个 key,让每个 key 的 value 控制在 10 KB 以内。
5.3 实战问题排查与避坑清单
最后整理一些实际排查中积累的高频问题和避坑经验,这些是文档里不会写的。
第一个问题是连接池耗尽。现象是业务接口偶发超时,排查后发现客户端连接池的最大连接数设置太小,而 Redis 服务端的 maxclients 也到达了上限。解决办法不是盲目调大连接池,而是要看清楚每个连接的使用时间。如果连接空闲时间过长,说明连接池容量过剩;如果频繁创建销毁连接,说明连接池空闲回收策略不对,建议调大 minIdle 并启用连接复用。
第二个问题是慢日志。Redis 的 slowlog 是排查延迟问题最直接的工具,但很多同学容易忽略。我们上线后设置 slowlog-log-slower-than 为 5000 微秒,定期抓取慢日志。处理慢日志不要只看单个命令,要看命令的类型和 key 的模式,比如大量使用 KEYS 命令、对大集合做 SORT、在高流量下执行 FLUSHDB,这些都是典型的大坑。生产环境我直接禁用了 KEYS 命令,统一改用 SCAN 游标遍历。
第三个问题是序列化导致的 Redis 数据类型错乱。有次排查发现缓存读到数据后反序列化失败,原因是不同版本 SDK 对 value 加了一个类型前缀,老数据没有这个前缀,导致新版本 SDK 反序列化时直接把老数据当成类型头解析。这也是我看重协议版本管理的原因。项目中所有缓存 value 的第一字节都固定是格式类型标记,从 V1 到 V2 演进时需要兼容读取所有旧版本数据。
第四个问题是时钟跳跃导致 TTL 计算错误。Redis 的过期是懒过期加定时过期,依赖服务器时间。如果服务器时钟向后调整,会导致大量 key 的 TTL 被延长,甚至永不过期。解决方式是统一 NTP 时间同步,并且在集群监控里加入时钟偏移检测,超过 50 毫秒就告警。
还有一个容易被忽略但很重要的细节:缓存 key 的命名规范。分布式缓存系统里,不同业务线共用同一套集群,如果 key 不添加业务前缀,很容易出现互相覆盖。我们强制要求 key 统一格式为 业务线:模块名:实际key,比如 order:stock:123456,这样既方便排查,也能在流量不均衡时按前缀做分片调整。
做分布式缓存系统,最大的体会是:技术方案反而是最简单的一环,真正困难的是对细节的敬畏和对流量模型的深刻理解。分布式缓存不是一锤子买卖,它需要持续的监控、调优和演进。你现在看到的稳定运行,背后是无数个告警夜晚和故障复盘换来的。希望这篇文章能让你在实现自己的分布式缓存系统时,少踩几个坑。
