在前面的博客中,我们已经深入探讨了 Redis 的基础知识,包括高并发机制、核心数据结构、持久化策略等。这些基础为我们提供了 Redis 在高负载、高并发环境中的强大能力,但要让 Redis 在实际的生产环境中发挥最大效能,我们还需要更进一步地理解缓存设计策略、数据删除策略、以及如何合理部署 Redis 集群等来保证系统的可扩展性和高可用性。
让我们设想一个典型的业务场景:你正在设计一个用户活动平台,平台的用户数据、活动记录、评论等都依赖 Redis 来提高查询性能。随着用户数量的增长,数据规模不断扩展,系统的响应时间越来越长。为了维持高并发的同时确保系统稳定性,我们不仅要保证缓存数据的合理更新和删除策略,还需要设计合适的 Redis 集群架构来应对突发的流量和数据的快速增长。
这就要求我们必须深入学习:
- 缓存设计策略,合理规划缓存的存活时间、失效策略和更新机制,以保证系统在高并发下的流畅体验;
- 数据删除策略,避免无效或过期数据占用过多内存,导致 Redis 性能下降;
- Redis 集群架构设计,确保数据的高可用性、负载均衡及容灾能力。
本篇博客将帮助你深入理解这些关键技术,并结合实际业务需求,指导你如何设计高效、可靠的 Redis 架构。通过合理配置 Redis,你将能够在高并发、高可用的场景下,充分发挥其强大的缓存能力,同时保证系统的稳定性和可扩展性。
Redis 缓存设计是系统性能优化的核心环节,合理的策略能大幅提升系统吞吐量、降低数据库压力,但设计不当可能导致缓存一致性问题、性能瓶颈甚至系统崩溃。以下从 缓存更新、问题解决、粒度控制、内存管理、高可用 五个维度,详细解析 Redis 缓存设计策略:
一、缓存更新策略:平衡一致性与性能
缓存的核心作用是「代替数据库回答问题」,但数据会变化,因此需要明确「缓存中的数据何时更新 / 删除」
编辑
编辑
在缓存+数据库这样的结构中要首先要保证缓存一致性问题。那么在数据更新时,不仅要考虑更新数据库与缓存的先后问题,还要考虑缓存是要删除还是更新。
先给结论:
Cache Aside策略:
先操作数据库再操作缓存,更新的时候删缓存,查询的时候写缓存
为什么呢?
先DB/Redis
首先解答更新数据时,是先操作数据库还是先操作缓存:
- 先更新缓存,再更新数据库;
- 先更新数据库,再更新缓存;
编辑
先更新缓存,再更新数据库
正常理想情况:
请求A的缓存更新和数据库更新是顺序发生的
请求B的缓存更新和数据库更新是顺序发生的
高并发情况:
请求A的更新缓存后,由于更新数据库的时间较长,中间进入了其他的线程B,B更新完缓存和数据库后,A更新完数据库,出现了缓存与数据库不一致的情况。
编辑
先更新数据库,再更新缓存
正常理想情况:
请求A的数据库更新和缓存更新是顺序发生的
请求B的数据库更新和缓存更新是顺序发生的
高并发情况:
请求A的更新数据库后,中间进入了其他的线程B,B更新完数据库和缓存后,A更新完了缓存,出现了缓存与数据库不一致的情况。
编辑
所以,无论是「先更新数据库,再更新缓存」,还是「先更新缓存,再更新数据库」,这两个方案都存在并发问题,当两个请求并发更新同一条数据的时候,可能都会出现缓存和数据库中的数据不一致的现象。
难道就没有了办法了么
当然有,先不更新缓存,而是删除缓存中的数据。然后,到读取数据时,发现缓存中没了数据之后,再从数据库中读取数据,更新到缓存中。就不会有数据不一致的现象了。这就是Cache Aside策略
更/删
这里解答操作数据库和缓存时为什么要删缓存而不是更新:
在更新数据时需要大量的对数据库进行操作,
- 如果同步更新缓存的话,会有大量的写操作(写操作相对费时)但写进来的数据不一定都会被读到,也就是不一定有用。通常情况是一些热点数据会被重复读到,大部分数据偶尔被读到,也不重常使用。(这估计也符合二八定律)
- 而删缓存则不存在这样的问题,更新数据库时让缓存删掉失效,然后在查询时,再将数据重写到缓存里,这样就能做到读多少写多少,重复读的时候不用写。
由此可使同步更新缓存的话,无效读写操作太多。而删缓存更加的高效
所以,先操作数据库再操作缓存,更新的时候删缓存,查询的时候写缓存的 Cache Aside策略是最优解。
1. Cache Aside(旁路缓存):最常用,适合大部分场景
核心逻辑:读操作先查缓存,缓存没有再查数据库并回写缓存;写操作先更数据库,再删缓存(而非直接更新缓存)。
编辑
-
读流程:
- 请求查询数据时,先查 Redis 缓存;
- 若缓存命中,直接返回;
- 若缓存未命中,查数据库,将结果写入缓存后返回。
-
写流程:
- 先更新数据库中的数据;
- 再删除 Redis 中对应的缓存(而非更新缓存)。
到这里问题似乎已经解决,但我还有一个小疑问:即执行写策略时,能不能先删除缓存再更新数据库,但我估计在「读+写」并发的时候,会出现缓存和数据库的数据不一致性的问题,那么先更新数据库再删缓存就可以避免了吗?
来看一看黑马老师的解答:
现在缓存和数据库一开始都是10,进行数据更新将数据库更新成20,以下是两种情况:
正常理想情况:
编辑
线程1,线程2顺序执行,不会有问题。
线程抢占情况:
编辑
- 线程1,先删除了缓存(10),
- 然后更新数据库,但由于是写操作相对耗时。
- 在此期间线程2进入,完成了查询(由于线程1没有更新完成,此时读到的是旧数据10)并写入缓存(10)
- 之后,线程1也将数据库的数据更新完毕(20)这样就出现了缓存不一致性问题
所以先删除缓存再更新数据库,很有可能发生缓存不一致的问题
那么同样的前提,先更新数据库再删缓存:
正常理想情况:
编辑
线程1,线程2顺序执行,不有问题。
线程抢占,突发缓存失效情况:
编辑
- 线程1进入先查缓存,但是缓存突发失效未命中
- 接着查数据库查到10,然后开始将10写入缓存,虽是写缓存但也需要耗时
- 在此期间,线程二进入执行更新数据库(20)然后删除缓存(由于缓存失效删了就和没删一样)
- 最后线程1完写入缓存(10)出现了不一致性的问题
所以先更新数据库再删除缓存,也有可能发生缓存不一致的问题,但是这种概率相当小,因为:
这种情况要出现的话,需要满足以下条件:
- 是多个线程并行执行
- 查的时候,缓存正好失效
- 写缓存的时候,恰好限其他线程进入更新数据库
注意此处速度:缓存>数据库 读>写
那么缓存的写操作速度一定是远大于数据库的写操作的速度的,所以不太可能在写缓存的过程中插入更新数据库,然后数据库都写完了才写缓存,所以这种情况一般不太可能发生。这种可能性低,如果有则可以缓存数据加上过期时间, 就算这段期间出现问题,也有过期时间来兜底,也能保证一致。
Write Through(写透缓存):强一致性优先,性能稍差
核心逻辑:写操作时,先更新缓存,再更新数据库,两者都成功才算完成。
- 读流程与 Cache Aside 一致;
- 写流程:先更新缓存,再更新数据库,两步都成功则返回,任何一步失败则重试 / 回滚。
优点:缓存与数据库强一致(几乎无延迟);
缺点:写操作需要两次 IO(缓存 + 数据库),性能较低;若缓存是分布式的,可能因网络问题导致更新失败。
适用场景:对一致性要求极高(如金融交易记录),且能接受写性能损耗的场景。
3. Write Back(写回缓存):性能优先,一致性差
核心逻辑:写操作时只更新缓存,不立即更新数据库,等到缓存数据「淘汰 / 过期」时再批量写入数据库(类似操作系统的页缓存机制)。
- 读流程:命中缓存直接返回,未命中则查数据库并回写缓存;
- 写流程:只更新缓存,标记为「脏数据」,后台异步将脏数据批量刷入数据库。
优点:写操作仅需一次缓存 IO,性能极高;
缺点:若缓存宕机,未刷入数据库的「脏数据」会丢失,一致性风险高。
适用场景:对性能要求极高,且能接受数据丢失风险的场景(如日志临时存储)
二、缓存问题解决策略:避开「三大坑」
缓存设计中最常见的问题是 穿透、击穿、雪崩,三者均会导致请求绕过缓存直接冲击数据库,需针对性解决。
项目中的基本缓存模型:客户端先发送请求先进入Redis如果有则命中返回,如果没有再进入数据库查询返回,然后将本次数据库中用到的数据写入Redis中以备下次使用,无需访问数据库直接命中返回。
编辑
在高并发系统中,Redis 作为缓存中间件,可以大大提升访问效率、减轻数据库压力。但同时也会面临缓存穿透、缓存击穿、缓存雪崩等问题。以下结合理论与黑马点评项目,进行系统总结。
缓存穿透
请求的数据在数据库与缓存中都不存在,每次查询都会穿过缓存,打到数据库,如果同时出现大量的请求会全部打在数据库上(可能有坏人专门同时大量发送恶意请求(如查询 ID=-1 的用户)来攻击数据库),导致数据库压力激增。
编辑
应对缓存穿透的方案,常见的方案有三种:
- 非法请求的限制:当有大量恶意请求访问不存在的数据的时候,也会发生缓存穿透,因此在 API 入口处我们要判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果判断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库。
- 设置空值或者默认值:查询数据源无结果时,缓存空值(如
null)并设置短期 TTL(如 1 分钟),避免重复穿透;这种方法实现容易、方便维护,但是可能会造成一定的内存消耗
编辑
//2.判断是否命中 if (StrUtil.isNotBlank(shopJson)) { //3.命中,返回商铺信息 return JSONUtil.toBean(shopJson, Shop.class);} //如果未命中,判断命中的值是否为空 if (shopJson != null) { return null;} //4.如果未命中,通过id查询数据库 Shop shop = getById(id); //5.若不存在,返回404 if (shop == null) { //防止存储穿透,使用存储空对象的方法,将空值写入reids stringRedisTemplate.opsForValue().set(key,"", CACHE_NULL_TTL, TimeUnit.MINUTES); //返回错误信息 return null;}
- 使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在:将数据源中所有有效 Key(如用户 ID、商品 ID)提前存入布隆过滤器,请求先过过滤器,无效 Key 直接拦截(注意:布隆过滤器有小概率误判,需配合空值缓存兜底)。
编辑
缓存击穿
某个热点数据刚好过期,此时大量请求同时访问该 key,导致大量请求穿透缓存,打爆数据库。
编辑
解决办法:
- 互斥锁(分布式锁) :当缓存未命中,先尝试加锁,只有一个线程能去数据库加载,其它线程等待。锁释放后再从缓存中读取。(如用 Redis 的
SET NX实现锁);
//6.1.获取互斥锁 String lockKey = LOCK_SHOP_KEY + id; boolean isLock = tyrlock(lockKey); //再一次检测Redis缓存是否过期,做doublecheck ,若存在无需重建缓存 if(expireTime.isAfter(LocalDateTime.now())){ //.未过期,直接返回店铺信息 return r;} //6.2.判断是否取锁成功 if(isLock){ // 6.3.成功,开启独立线程,实现缓存重建 CACHE_REBUILD_EXECUTOR.submit(() -> { //6.3.1查询数据库 R r1 = dbFallack.apply(id); //6.3.2.写入redis this.setWithLogicalExpire(key,r1, time, unit); } catch (Exception e) { throw new RuntimeException(e); }finally { //释放锁 unlock(lockKey); } });
- 逻辑过期 + 异步更新(黑马点评封装 RedisData 结构):
-
先是提交请求到Redis里查,
-
然后判断逻辑过去时间,
-
如果查到了并且也没有过期说明没问题直接返回。
-
如果查到了,但过期了,就需要获取锁,
- 如果能够获取到锁(说明这是过期后的第一个请求,所以还没有上锁),就开独立线程到数据库里查。
- 如果获取不到锁,说明已经上锁,这是后续的请求,便返回旧的数据并且开始等待,就是由一个后台线程异步去刷新数据,前台仍使用旧缓存。
-
此处注意:Redis 默认不会给你设置 TTL(过期时间) ,你必须自己显式设置,否则 key 会永久存在,直到你手动删除或被覆盖。只要这个 key 设置了过期时间(TTL),即使你一直不访问它,它也会自动过期、被删除。
所以总结来说:Redis 默认支持自动过期机制(Redis 内部判断(TTL)),但在高并发场景下,也可以使用逻辑过期(由代码判断)来更灵活控制缓存刷新时机。
编辑
// 封装逻辑过期时间并写入 Redis public void save2Redis(Long id, long expireSeconds) throws InterruptedException { // 1. 查询店铺数据 Shop shop = getById(id); Thread.sleep(200); // 模拟数据库查询耗时 // 2. 封装逻辑过期时间(核心:设置过期时间) RedisData redisData = new RedisData(); redisData.setData(shop); // 存储实际业务数据(店铺信息) // 设置逻辑过期时间 = 当前时间 + 过期秒数(如20秒) redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds)); // 3. 将封装后的数据写入 Redis(不设置 Redis 自身的 TTL) stringRedisTemplate.opsForValue().set( CACHE_SHOP_KEY + id, // Key:店铺缓存的唯一标识 JSONUtil.toJsonStr(redisData) // Value:包含逻辑过期时间的 JSON 字符串 // 注意:这里没有设置 Redis 自带的过期时间(如 TimeUnit.MINUTES) ); } // 重建缓存时,传入过期秒数为 20L(20秒) this.save2Redis(id,20L);
缓存雪崩
在同一时间大量缓存key同时失效或者Redis服务宕机,导致所有请求都打到数据库,引起数据库压力剧增,甚至崩溃,系统整体不可用。
编辑
解决方案:
- 将缓存失效时间随机打散: 我们可以在原有的失效时间基础上增加一个随机值(比如 1 到 10 分钟)或者让过期时间错开分布,这样会在不同时间点逐步过期,不集中失效,也就降低了缓存集体失效的概率。
- 使用逻辑过期 + 异步缓存重建(黑马点评用法) : 手动控制过期时间, 不让热点 key 自行过期,如果判断逻辑时间已过期,先返回旧数据,再异步重建缓存(防止请求打爆数据库),从而避免因为缓存失效造成的缓存雪崩,也可以在一定程度上避免缓存并发问题。👉 这是黑马点评中解决热点缓存一致性的关键方案。
三、过期策略与内存管理:防止缓存「撑爆」
Redis 内存有限,需合理设置过期策略和内存淘汰机制,避免无用数据占用空间。
1. 过期键删除策略(键设置了 TTL(过期时间))
- 惰性删除:只有当请求查询某个 key 时,才检查它是否过期,过期则删除。优点是节省 CPU,缺点是可能内存中堆积大量过期 key。
- 定期删除:Redis 每隔一段时间(默认 100ms)随机抽查部分过期 key 并删除。优点是平衡 CPU 和内存,缺点是可能有少量过期 key 未及时删除。
实际效果:Redis 同时使用两种策略,既避免 CPU 浪费,又减少过期 key 堆积。
其实还有定时删除,但Redis 中不存在严格意义上的定时删除(即 TTL 一到立即删除),这种删除策略并不高效,所以很少用。
其实还有定时删除:在设置某个key 的过期时间同时,我们创建一个定时器,让定时器在该过期时间到来时,立即执行对其进 行删除的操作,要让服务器创建大量的定时器,从而实现定时删除策略,在现阶段来说并不现实。
2. 内存淘汰机制:内存满了怎么办?
当 Redis 内存达到 maxmemory 阈值时,会触发内存淘汰,需提前配置淘汰策略(maxmemory-policy):
只有当 Redis 写入新数据 且内存达到上限时,才会触发淘汰机制。
如果上面的删除过期键仍无法释放足够内存,就开始启用 内存淘汰策略 来决定清除哪些键:
- LRU(Least Recently Used) :淘汰「最近最少使用」的 key(适合热点数据场景,如商品详情)。
- LFU(Least Frequently Used) :淘汰「最近访问频率最低」的 key(适合长期有访问但频率不均的场景,如用户登录记录)。
- volatile-xxx:只淘汰「设置了过期时间」的 key(优先保留永不过期的核心数据)。
- allkeys-xxx:所有 key 都可能被淘汰(适合缓存全是临时数据的场景)。
建议:根据业务选 LRU 或 LFU,并用 volatile-xxx 保护核心数据(如配置类 key 永不过期)
组合策略:
| 策略名 | 含义 | 说明 |
|---|---|---|
| noeviction | 不删除,直接报错 | 默认策略,写入失败 |
| allkeys-lru | 所有键中淘汰最近最少使用 | 智能 |
| allkeys-lfu | 所有键中淘汰使用频率最低的 | 更先进的推荐策略 |
| allkeys-random | 所有键中随机淘汰 | 简单快速 |
| volatile-lru | 仅 TTL 键中 LRU 淘汰 | 有 TTL 控制 |
| volatile-lfu | 仅 TTL 键中 LFU 淘汰 | 类似上面 |
| volatile-random | 仅 TTL 键中随机淘汰 | 比较极端 |
| volatile-ttl | 淘汰 TTL 时间最短的 | 优先过期键 |
四、Redis都存在哪些集群方案
由于不同的业务对 可用性、扩展性、容错性、操作复杂度 的需求不同。Redis 的原生设计是单线程、高性能的内存数据库,但为了更好地服务于大规模、高并发的分布式系统,它逐渐演化出了多个集群方案。
常见方案包括主从复制、哨兵模式(Sentinel)、Redis Cluster,各自的存在原因与适用场景如下:
主从复制
之前的持久化技术已经可以保证了即使在服务器重启的情况下也不会丢失数据(或少量损失)。
不过,由于数据都是存储在一台服务器上,终究是不安全的,比如:
- 如果服务器发生了宕机,那么数据恢复过程中是无法服务新的请求的;
- 如果这台服务器的硬盘出现了故障,可能数据就都丢失了。
要避免这种单点故障,最好的办法是将数据备份到其他服务器上,让这些服务器也可以对外提供服务,这样即使有一台服务器出现了故障,其他服务器依然可以继续提供服务。
编辑
多台服务器要保存同一份数据,如何做到服务器之间的数据保持一致性呢?
Redis 提供了主从复制模式,来避免上述的问题。
编辑
方案核心:一个主节点(Master)处理写操作,多个从节点(Slave)通过复制主节点数据承担读操作,数据异步同步。从节点通过复制保持与主节点一致。
实现过程:
- 初始同步(全量复制)
当从节点第一次连接主节点时,会进行全量复制:
-
从节点发送
PSYNC请求; -
主节点执行
bgsave创建 RDB 快照,同时将写命令缓存在复制缓冲区中; -
主节点将 RDB 发送给从节点;
-
从节点加载 RDB 文件;
-
主节点将缓冲区中的写命令发送给从节点执行,完成增量补偿。
-
增量同步
- 当从节点已经复制过一次,只是短暂断连时,会尝试继续同步主节点的复制积压缓冲区中的写操作,避免全量复制;
- 使用复制偏移量(replication offset)和主节点的
backlog buffer实现。
编辑
优化实现:
我们知道主从服务器在第一次数据同步的过程中,主服务器会做两件耗时的操作:生成 RDB 文件和传输 RDB 文件。
主服务器是可以有多个从服务器的,如果从服务器数量非常多,而且都与主服务器进行全量同步的话,就会带来两个问题:
- 由于是通过 bgsave 命令来生成 RDB 文件的,那么主服务器就会忙于使用 fork() 创建子进程,如果主服务器的内存数据非大,在执行 fork() 函数时是会阻塞主线程的,从而使得 Redis 无法正常处理请求;
- 传输 RDB 文件会占用主服务器的网络带宽,会对主服务器响应命令请求产生影响。
小林同学说:要解决这个问题,就像公司中的老板设立经理职位,由经理管理多名普通员工,然后老板只需要管理经理的模式。
Redis 也是一样的,从服务器可以有自己的从服务器,我们可以把拥有从服务器的从服务器当作经理角色,它不仅可以接收主服务器的同步数据,自己也可以同时作为主服务器的形式将数据同步给从服务器,组织形式如下图:
编辑
这样的话,主服务器生成 RDB 和传输 RDB 的压力可以分摊到从服务器上。
我们在「从服务器」上执行下面这条命令,使其作为目标服务器的从服务器:
replicaof <目标服务器的IP> 6379
此时如果目标服务器本身也是「从服务器」,那么该目标服务器就会成为「经理」的角色,不仅可以接受主服务器同步的数据,也会把数据同步给自己旗下的从服务器,从而减轻主服务器的负担。
主从复制的核心价值在于低成本实现读扩展和数据备份、 保证高可用性 。实现故障转移需要手动实现,无法实现海量数据存储。但受限于单点故障和写性能瓶颈,更适合对高可用要求不高、读请求主导的业务。若需更高可用性或写扩展能力,需结合哨兵模式或 Redis Cluster 方案。
哨兵模式
虽然 Redis 的主从复制可以实现读写分离、冗余备份,但它有个关键问题: 主节点宕机后,无法自动切换主从,手动介入成本高、风险大。
编辑
哨兵模式弥补主从复制的 “人工干预故障转移” 缺陷,实现自动高可用(如金融交易、电商支付场景需 7×24 小时服务)。可以实现:
- ✅ 主节点故障自动转移(Failover)
- ✅ 自动将某个从节点提升为主节点
- ✅ 通知客户端更新连接地址
- ✅ 监控节点状态
哨兵其实是一个运行在特殊模式下的 Redis 进程,所以它也是一个节点。
哨兵主要负责三件事情:监控、选主、通知。
编辑
监控
一、哨兵是如何监控节点的?
哨兵进程在后台持续向主节点和从节点发送 PING 命令,以检测它们的健康状态。
具体机制:
- 每个哨兵每 1 秒(默认)会向主节点、从节点和其他哨兵节点发送 PING 命令;
- 如果在一定时间(由配置项 down-after-milliseconds 控制)内,某个节点没有回应
PONG,哨兵就认为该节点“可能出问题了”。
二、哨兵是如何判断主节点是否“真的故障”的?
哨兵的判断分两步:
- 主观下线(SDown)
某个哨兵节点自己判断主节点失联。
- 条件:某个哨兵持续 down-after-milliseconds(如 5s)没有收到主节点的 PONG 响应,就认为这个主节点进入了
SDown状态; - 这是单个哨兵的判断,并不能触发真正的主从切换。
编辑
- 客观下线(ODown)
多个哨兵达成共识,共同认为主节点失效。
- Sentinel(哨兵) 之间会互相通信(通过 PUB/SUB(发布 / 订阅机制));
- 如果多数哨兵都判断同一个主节点 SDown,则进入
ODown; - 此时,才会触发 failover 故障转移流程;
- 法定票数由配置项中的哨兵数量决定
编辑
选主
当主节点被判定故障(客观下线 ODOWN)后,会触发一次 故障转移(Failover) ,此时必须从已有的从节点中选出一个新的主节点。根据什么规则选择一个从节点切换为主节点?
Sentinel 选择新主节点的核心规则(按顺序排序)
-
优先级(priority)
- Redis 从节点可以配置一个 replica-priority(默认是 100)。
- Sentinel 会优先选择 优先级数值最小的从节点(priority 越小,优先级越高)。
- 如果设置为 0,表示不参与主节点选举。
-
复制偏移量(offset)
-
如果有多个从节点优先级相同,会选择数据最新的,也就是 复制 offset 最大的那个。
-
复制 offset 越大,说明它与旧主节点的数据越接近,数据越完整。
编辑
-
-
响应延迟时间
- 如果还无法决定,就选择最后一次响应 Sentinel PING 命令最快的那个。
-
名字字典序(ID)
- 实在没法区分(比如 offset 一样、延迟一样),就按 名字的字典序最小(比如
redis-7001比redis-7002小)来选。
- 实在没法区分(比如 offset 一样、延迟一样),就按 名字的字典序最小(比如
编辑
通知
故障转移(Failover)之后, 如何通知所有从节点和客户端,新主节点是谁。
这主要通过 Redis 的PUB/SUB(发布者/订阅者)机制来实现的。每个哨兵节点提供发布者/订阅者机制,客户端可以从哨兵订阅消息。
通知从节点:重新配置复制关系
整体流程
当 Sentinel 成功选出新的主节点后,它会:
- 向其他从节点发送命令:即让原来的从节点取消对旧主节点的复制,转而去同步新主节点的数据。
- 新主节点不再是任何人的 slave,它自己会取消 slave 状态,成为真正的主节点。
- 经过几次重试确认同步成功后,从节点将新的主节点设置为自己的 master。
通知客户端
Redis Sentinel 并不是直接通知客户端新主节点的地址,而是让客户端自己通过 Sentinel 查询主节点信息。
客户端如何感知主节点变化?
客户端应当:
- 不要直接连接某一个 Redis 实例
- 而是连接多个 Sentinel 节点,通过以下命令动态获取最新的主节点:
SENTINEL get-master-addr-by-name <master-name>
这个命令会返回当前有效的主节点的 IP 和端口。
建议做法(客户端层)
开发中一般会借助 Redis 客户端库(如 Jedis、Lettuce、Redisson)提供的支持:
- 设置多个 Sentinel 节点地址(IP:port)
- 设置监控的主节点名称
- 客户端会自动向 Sentinel 获取并维护主节点连接
config.useSentinelServers()
.addSentinelAddress("redis://127.0.0.1:26379", "redis://127.0.0.1:26380")
.setMasterName("mymaster");
Sentinel Leader
在 Redis 哨兵系统中,多个 Sentinel 节点可能几乎同时发现主节点下线。为了避免混乱(比如多个 Sentinel 同时发起故障转移),需要选出一个“领导者”来主导故障转移流程。
这个领导者就是 Leader Sentinel。
一、Leader 是怎么选出来的?
Sentinel 使用一种非常简单的Raft-like 的投票机制来选举 Leader,具体流程如下:
Leader 选举流程:
- 某个 Sentinel 首先判断主节点疑似宕机(Subjectively Down:SDOWN)。
- 它会向其他 Sentinel 发送
is-master-down-by-addr请求,询问“你也觉得主节点宕机了吗?” - 如果超过半数的 Sentinel 都同意主节点确实宕机,主节点就被标记为 Objectively Down(ODOWN) 。
- 接下来,所有 Sentinel 会对某个 Sentinel 提出“我同意它作为故障转移的领导者”。
- 得票最多且达成 quorum(过半票数)的 Sentinel 成为 Leader。
quorum 的默认值是 Sentinel 数量的一半 + 1,也可以手动设置。
二、Leader Sentinel 主要负责什么?
选出来之后,Leader Sentinel 执行以下关键任务:
- 从多个从节点中选出最合适的一个作为新的主节点(考虑优先级、复制偏移量等)。
- 向其他从节点发送
SLAVEOF命令,指向新主节点。 - 更新元信息,并在整个哨兵系统内广播新的主节点信息。
而其他 Sentinel 则作为 Follower Sentinel,不参与实际故障转移操作,只接收结果并同步状态。
三、Leader 是长期固定的吗?
不是。
Leader Sentinel 是一次性角色,仅在每次故障转移时选举一次。
每次发生主节点故障时都会重新选举一次 Leader。这个过程是临时协调角色的分配,而不是固定职责。
举个例子:
你有 3 个 Sentinel:S1、S2、S3。
- 当主节点宕机时,S1 首先发现,发起投票。
- S2、S3 也发现了宕机并响应。
- 投票后,S2 票数最多(或者先到达投票门槛),就成为 Leader。
- S2 主导整个故障转移操作,其它两个 Sentinel 则静观其变。
总结一句话:
Redis Sentinel 的 Leader 是通过哨兵投票临时选出来的唯一协调者,负责整个主节点故障转移的执行,而不是固定角色,每次主节点故障时都会重新选举。
哨兵集群
这里最主要介绍哨兵集群的组成方式
在哨兵信息的配置中,只需要填下面这几个参数,设置主节点名字、主节点的 IP 地址和端口号以及 quorum 值。
sentinel monitor <master-name> <ip> <redis-port> <quorum>
少量参数即可实现功能的背后是哨兵节点之间是的 Redis PUB/SUB(发布者/订阅者)机制。
在主从集群中,主节点上有一个名为__sentinel__:hello的频道,不同哨兵就是通过它来相互发现,实现互相通信的。
在下图中,哨兵 A 把自己的 IP 地址和端口的信息发布到__sentinel__:hello 频道上,哨兵 B 和 C 订阅了该频道。那么此时,哨兵 B 和 C 就可以从这个频道直接获取哨兵 A 的 IP 地址和端口号。然后,哨兵 B、C 可以和哨兵 A 建立网络连接。
编辑
通过这个方式,哨兵 B 和 C 也可以建立网络连接,这样一来,哨兵集群就形成了。
哨兵集群会对「从节点」的运行状态进行监控,那哨兵集群如何知道「从节点」的信息?
主节点知道所有「从节点」的信息,所以哨兵会向主节点发送 INFO 命令(类似像领导申请)来获取所有「从节点」的信息。
如下图所示,哨兵 B 给主节点发送 INFO 命令,主节点接受到这个命令后,就会把从节点列表返回给哨兵。接着,哨兵就可以根据从节点列表中的连接信息,和每个从节点建立连接,并在这个连接上持续地对从节点进行监控。哨兵 A 和 C 可以通过相同的方法和从节点建立连接。
编辑
正式通过 Redis 的发布者/订阅者机制,哨兵之间可以相互感知,然后组成集群,同时,哨兵又通过 INFO 命令,在主节点里获得了所有从节点连接信息,于是就能和从节点建立连接,并进行监控了。
Redis分片集群
主从复制和哨兵集群可以解决高可用、高并发读的问题。但是依然有两个问题没有解决:
- 海量数据存储问题
- 高并发写的问题
使用分片集群(Redis Cluster)可以解决上述问题,分片集群特征:
- 集群中有多个master,每个master保存不同数据每个master都可以有多个slave节点
- master之间通过ping监测彼此健康状态
- 客户端请求可以访问集群任意节点,最终都会被转发到正确节点
编辑
Redis Cluster 的核心设计
1. 数据分片:槽位(Slot)机制
-
槽位划分:Redis Cluster 将整个键空间划分成 16384 个哈希槽(hash slot)
-
分片逻辑:
当客户端写入键(Key)时,集群通过哈希算法计算键的槽位:
Redis 使用 CRC16 算法对 key 进行哈希,然后对 16384 取模:slot = CRC16(key) % 16384例如,键
user:100的 CRC16 值取模后若为 5000,则该键会被分配到槽位 5000 对应的节点。 -
节点与槽位的关系:每个主节点负责部分槽位,例如:
| 节点 | 槽位范围 |
|---|---|
| Master A | 0 - 5460 |
| Master B | 5461 - 10922 |
| Master C | 10923 - 16383 |
2. 集群拓扑:去中心化架构
- 节点角色:集群由多个主节点(Master)和从节点(Slave)构成。
- 去中心化:集群中没有 “中心节点”,每个节点通过Gossip 协议与其他节点通信(交换节点状态、槽位分配等信息),客户端可连接任意节点发送请求。
Redis 分片集群的作用:
一、海量数据存储:通过分片(Sharding)实现数据水平扩展
- Redis Cluster 把整个键空间划分成
16384个哈希槽(Hash Slot) - 每个主节点负责一部分槽位,多个主节点分担存储压力
- 新增节点时,槽位可以重新分配(resharding) ,自动迁移部分数据
结果:
- 数据被打散存储在多个 Redis 实例中,内存容量线性增长
- 你想存 1TB 数据?部署 10 个 128GB 节点即可!
二、高并发写入能力:通过节点分担请求压力
- 客户端通过哈希槽定位到数据所属节点,直接请求目标主节点
- 各个节点独立处理写操作(多核机器部署多个 Redis 实例)
- 写请求被分散到多个节点,每个节点处理自身槽位范围内的写入
结果:
- 高并发被“切片”成多个低并发,系统整体吞吐能力成倍提升
- 没有一个节点会成为性能瓶颈
结束,结束了吗?
本篇文章围绕 Redis 的缓存设计策略、数据删除机制以及集群部署架构进行了系统梳理。从缓存粒度到淘汰策略,从主从复制到哨兵监控,再到分片集群应对海量存储与高并发写入,每一部分都是 Redis 在实际生产中稳定高效运行的关键环节。在理解这些架构背后的设计初衷后,我们也不难发现,缓存并不仅仅是“加快访问”的工具,更是系统稳定性与可扩展性的护城河。
写到这里,Redis的大部分基础知识了解也就差不多了,当然本文中也有很多点并没有展开讲、很多内容也没有实操,只是简单的总结,这既有时间的问题也有博主现在阶段是否有能力讲解的原因。总之,Redis 的世界远不止于此。依然有许多内容需要我们去理解学习,我也会在日后不断地丰富补充,也欢迎看到这里的诸位朋友与我交流反馈博文中”好“与”不好“的地方,我们共同进步,共同学习。
缘至于此,来日方长,未完待续,敬请期待......