https://www.xiaolincoding.com/redis/base/redis_interview.html#redis-%E4%BD%BF%E7%94%A8%E7%9A%84%E8%BF%87%E6%9C%9F%E5%88%A0%E9%99%A4%E7%AD%96%E7%95%A5%E6%98%AF%E4%BB%80%E4%B9%88
引言
Redis是一个高性能、分布式 kv数据库。
Redis高性能的原因:
- Redis是内存数据库,大多数命令处理基于内存操作,读写快。
- Redis是单进程线程处理网络请求,大多数命令是读和写io,不涉及计算,不需要多线程。这样避免了上下文的切换和锁。
- Redis使用多路IO复用模型(select、poll、epoll),可以高效处理并发连接。
- Redis的底层数据结构很巧妙,包括简单冬动态字符串sds、跳表skip list、整数集合intset、压缩列表ziplist、快速链表quicklist、字典dict。
Redis被广泛应用的原因:
- 速度快。
- value支持复杂数据结构:字符串、链表、集合、哈希、有序集合。
- 支持数据持久化。RDB、AOF、RDB&AOF。
- 稳定。分布式主从结构。
Redis的底层数据结构
简单动态字符串 SDS
简单动态字符串(Simple Dynamic Strings,SDS) 是Redis的基本数据结构,我们从以下几个设计目标来理解这个数据结构。
设计目标一: “实现一个扩容方便且二进制安全的字符串”。
[!NOTE]
非二进制安全指的是在C语言中,如果字符串本身包含
\0字符,会被识别为字符串末尾,导致字符串被截断。二进制安全反之。
通过设置单独的统计变量len 和 free 来保证二进制安全和得到字符串长度,内容通过柔性数组buf存放并暴露数组指针,让上层可以像C字符串一样读取和处理sds。
struct sds {
int len;
int free;
char buf[];
}
所谓柔性数组,指的是他的地址和结构体是连续的,可以快速查找内存。

设计目标二:不同长度的字符串其实没必要占用相同大小的头部,
每个字符串的元信息都占用4字节太浪费了。因此,针对不同大小的字符串,衍生了多种sds。
sdshdr5
对于最短的字符串,使用1个flag字节来表示元信息,低3位存储类型,高5位存储长度。短字符串不需要free,因为修改后大概率也在32($2^5$ )字节内,无需扩容;即使扩容,也会直接修改类型,不会再使用sdshdr5 。
struct sdshdr5 {
unsigned char flags;
char buf[];
}

sdshdr8
struct sdshdr8 {
uint8_t len; // 当前长度
uint8_t alloc; // 总长度,空闲长度free = alloc - len
unsigned char flags; // 低3位类型,高5位预留
}
SDS的扩容

跳跃表 skiplist
对于有序数据,数组不便于元素插入和删除,链表查询效率低,平衡树或红黑树实现复杂,因此Redis采用了跳跃表。
分层有序链表思想
首先,为了解决链表查找效率问题,我们可以参考分层有序链表,从高到低进行查找。


基于这种思想,跳跃表具有以下几个关键性质:
- 多层结构:跳跃表由多层有序链表构成;底层(L0)包含全部数据节点,上层是底层的子集;一个节点出现在上层,则必然出现在下层。
- 头结点header:专属头节点,不作为实际数据节点;每层存储指向本层下一个节点的指针,同时记录跨度
span,span代表该指针跨越的底层节点数量。 - 表高度level:排除头节点,节点最大层数就是跳跃表高度,图中示例高度为3。
- 有序存储:每一层链表内数据按分数递增有序排列;每层链表末尾节点指向
NULL,标记本层链表结束。 - 尾指针tail:维护
tail指针,直接指向跳跃表最末尾的数据节点。 - 表长度length:最底层L0的节点总数(不计header头节点),代表跳跃表整体长度,示例中length=7。
- 后退指针backward:每个数据节点带有后退指针;头节点、第一个数据节点后退指针为
NULL;其余节点的后退指针,指向最底层链表的前一个节点,只在底层实现反向遍历。 - 节点信息:每个数据节点存储score分值,作为排序依据。
补充:上层链表用来加速查找,底层完整链表保存全部元素;span跨度用于计算元素在表中的排位。
跳跃表数据结构
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
}
typedef struct zskiplistNode {
sds ele; // 存储字符串类型数据
double score; // 存储本层分数
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned int span;
} level[];
}
每个节点层数确定
跳跃表通过一个random函数随机生成一个1-64之间的数,来确认自身高度。
#define ZSKIPLIST_P 0.25
int zslrandomLevel(void) {
int level = 1;
while ((random()&0xFFFF) < (ZSKIPLIST_P*0xFFFF)) ++level;
return (level < ZSKIPLIST_MAXLEVEL) ?level : ZSKIPLIST_MAXLEVEL;
}
节点层高为n的概率为 $p^{n-1}(1 - p)$
插入节点的步骤
- 查找插入位置
- 调整跳跃表高度
- 插入节点
- 调整backward
删除节点的步骤
- 查找涉及更新的节点
- 设置span和forward
跳跃表的应用
当使用压缩列表的配置(zset-max-ziplist-entries 和 zset-max-ziplist-value)超限时,跳跃表主要应用于有序集合的底层实现。
压缩列表 ziplist
压缩列表的本质是一个字节数组。Redis的有序集合、散列表和列表都使用了压缩列表。
- 有序集合和散列表在元素少且是短字符串时使用压缩列表。
- 列表依赖快速链表实现,快速链表是双向链表与压缩列表的组合。
压缩列表数据结构
整体结构

- zlbytes :压缩列表长度
- zltail :尾元素地址偏移量 (元素个数*元素大小)
- zllen :元素个数
- entryX :压缩列表存储的元素
- zlend : 压缩列表结尾,值恒为0xFF
每个节点的结构

普通双向链表每个节点都要维护 prev、next 两个指针,每个指针占用 8 字节(64 位系统),元数据开销很大。
- ziplist不使用指针,全部数据紧凑排布在一块连续内存;
- 节点通过
prevlen记录上一个节点的长度实现双向遍历,替代链表指针; - 利用
encoding和content支持可变长度编码:小整数直接用整数编码,短字符串用字节编码,不同内容选择最小占用的编码格式,最大限度压缩存储空间。
字典 dict
基本同其他编程语言的map。
整数集合 intset
快速链表 quicklist
stream
Redis面试题
一、认识 Redis
1. 什么是 Redis?
Redis 是一个开源、基于内存、支持持久化的键值对NoSQL数据库,C语言开发。支持多种丰富数据结构;支持主从、哨兵、集群;单线程模型,性能极高;可以做缓存、分布式锁、延迟队列、计数器等。数据主要放在内存,也可以RDB/AOF落盘持久化。
2. Redis 和 Memcached 有什么区别?
- 数据结构:Memcached只有简单k‑v字符串;Redis支持String、List、Hash、Set、ZSet等多种数据结构。
- 持久化:Memcached不支持持久化;Redis支持RDB、AOF持久化,宕机数据可恢复。
- 线程模型:Memcached多线程;Redis6.0前单线程处理命令,6.0引入多线程IO。
- 集群:Memcached原生不支持集群;Redis支持主从、哨兵、Redis‑Cluster集群。
- 内存管理:Memcached使用预分配内存池;Redis按需申请内存,支持内存淘汰策略。
- 其他:Redis支持发布订阅、Lua脚本、事务;Memcached功能简单。
3. 为什么用 Redis 作为 MySQL 的缓存?
- Redis内存访问速度远快于磁盘MySQL,降低数据库压力,提升接口响应速度。
- 支持丰富数据结构,能直接满足业务的集合、排序、列表类缓存场景。
- 支持过期时间,天然适合缓存过期失效。
- 支持持久化,缓存节点重启后数据可以恢复。
- 支持集群,可横向扩容,承载高并发读请求。
注意:需要处理缓存穿透、击穿、雪崩,以及缓存与数据库一致性问题。
二、Redis 数据结构
1. Redis数据类型以及使用场景分别是什么?
| 类型 | 作用 | 典型场景 |
|---|---|---|
| String | 字符串/数字 | 计数器、分布式session、简单KV、限流计数 |
| List | 双向列表 | 消息队列、最新列表、时间轴 |
| Hash | 哈希表 | 存储对象,用户信息,商品属性 |
| Set | 无序集合 | 去重、交集并集、好友共同关注 |
| ZSet | 有序集合 | 排行榜、延时队列、带权重排序 |
2. 五种常见的 Redis 数据类型是怎么实现?
- String:底层 SDS(简单动态字符串)。
- List:底层 quicklist(3.2版本后),双向链表,每个节点是ziplist压缩列表。
- Hash:数据少时用ziplist压缩列表;数据量大转为dict哈希表。
- Set:整数集合intset(元素全是整数、数量少);否则dict哈希表。
- ZSet:数据少时ziplist;数据量大使用跳表(zskiplist)+dict字典。
三、Redis 线程模型
1. Redis是单线程吗?
Redis 执行命令的主线程是单线程,但Redis整体并不是只有一个线程。
- Redis6.0之前:网络IO、命令执行全部单线程;持久化RDB/AOF会fork子进程做后台工作。
- Redis6.0之后:命令执行依旧单线程;网络IO处理支持多线程,读写socket可以多线程分担。
2. Redis单线程模式是怎样的?
采用IO多路复用 + 单线程事件循环:
- 一个主线程,通过epoll/kqueue监听大量socket文件描述符。
- 当客户端socket就绪,主线程读取请求,执行Redis命令,返回结果。
- 所有命令串行执行,不会出现线程安全问题,不需要锁。
- 耗时操作(RDB、AOF刷盘)交给子进程/后台线程,不阻塞主线程。
3. Redis采用单线程为什么还这么快?
- 绝大部分操作是内存操作,内存访问速度极快。
- IO多路复用,可以处理大量客户端连接。
- 避免多线程频繁上下文切换、锁竞争开销。
- 耗时任务交给子进程/后台线程,不阻塞主线程。
- C语言实现,底层内存处理高效。
注意:单线程怕慢命令,keys、flushall、大集合操作会阻塞整个服务。
4. Redis 6.0之前为什么使用单线程?
- 内存数据库瓶颈大多不在CPU,而在网络IO;单线程足以跑满网络。
- 多线程会带来锁、竞争、上下文切换的额外开销,增加代码复杂度。
- 命令串行执行,天然线程安全,不需要处理并发竞争。
- fork子进程处理持久化,已经可以把耗时操作剥离主线程。
5. Redis 6.0 之后为什么引入了多线程?
当网络IO成为瓶颈时,单线程处理大量网络读写会打满CPU。
- 多线程只用来做socket的IO读写,命令执行仍然是单线程。
- 将网络数据包解析、读写交给IO线程,主线程专心执行命令,提升高并发下网络吞吐。
- 默认关闭,需要配置开启。
四、Redis持久化
1. Redis如何实现数据不丢失?
依靠两种持久化:RDB快照、AOF日志,也可以开启混合持久化。
- RDB:某时间点全量二进制快照,fork子进程把内存数据写入磁盘。
- AOF:记录每一条写命令,追加写入日志文件;宕机后重放AOF恢复数据。
- 混合持久化:AOF文件头部带上RDB快照,后面跟着增量AOF日志,兼顾恢复速度与数据安全性。
2. AOF日志是如何实现的?
- 命令追加:写命令追加到aof_buf内存缓冲区,不是直接写磁盘。对于有过期时间的key,AOF会改写命令为设置过期的绝对时间戳;对于过期的key,AOF会追加del命令。
- 文件刷盘策略:
- always:每次写都fsync刷盘,最安全,性能差。
- everysec:每秒刷盘(默认),最多丢失1秒数据。
- no:交给操作系统自行刷盘,不可控。
- AOF重写bgrewriteaof:日志会越来越大,fork子进程,遍历内存数据生成新AOF文件,丢弃冗余旧命令,完成后替换旧文件,减小文件体积。
3. RDB快照是如何实现的呢?
-
执行bgsave,主线程调用
fork()生成子进程。 -
子进程拥有内存数据副本(写时复制COW),把内存全量数据dump成二进制rdb文件。
-
子进程完成后通知主进程,替换旧RDB文件。
-
触发方式:手动save/bgsave;配置定时自动触发。
save命令在主线程执行,会阻塞,生产环境禁止使用。
4. 为什么会有混合持久化?
AOF纯模式:恢复数据需要重放全部历史命令,文件大、恢复慢。 纯RDB:只能全量快照,两次快照之间宕机会丢失大量数据。
混合持久化:AOF重写时,把当前内存数据以RDB二进制写到AOF文件头部,后续继续追加AOF增量命令。
- 恢复:先加载RDB部分,再重放后面增量AOF,兼顾恢复速度和少丢数据。Redis4.0+支持。
五、Redis集群
1. Redis如何实现服务高可用?
- 主从复制:主节点写,从节点同步数据,从节点提供读服务;主挂掉无法自动切换。
- Sentinel哨兵:监控主从节点;主节点故障,哨兵自动选出新主节点,自动故障转移,实现高可用。
- Redis‑Cluster集群:分片,把数据分散到多个master节点,水平扩容存储;每个master搭配slave实现故障转移。
2. 集群脑裂导致数据丢失怎么办?
先复现脑裂整个过程
- 原本:A 是主节点,B、C 是从节点,Sentinel 哨兵正常监控。
- 网络分区发生:主节点 A 和哨兵、从节点之间网络断了。
- 哨兵收不到 A 的心跳,认为 A 故障,把从 B 升级成新主节点。客户端慢慢切换连到新主 B 写数据。
- ⚠️但是!旧主 A 本身还活着,只是和哨兵隔离。一部分客户端因为网络路由,依旧还能连上旧主 A,继续向旧主 A 写入新数据。
- 一段时间后网络恢复。
- 哨兵会把旧主 A 降级为新主 B 的从节点。
- A 上自己接收的那部分新写入的数据,会被直接清空,同步新主 B 的数据,这部分旧主 A 的写入就永久丢失了。
问题根源:网络断开后,旧主依然认为自己是 master,还在接受写请求。
两个配置参数含义
min‑replicas‑to‑write 1
min‑replicas‑max‑lag 5
翻译:主节点只有满足:至少有 1 个从节点,且这个从节点的复制延迟不超过 5 秒,主节点才允许执行写命令;否则拒绝写操作。
min‑replicas‑to‑write 1主节点要求,在线、正常复制的从节点数量至少达到 N 个,才允许接收写。这里 N=1。min‑replicas‑max‑lag 5判断从节点的repl_lag复制延迟(从节点上报给主的延迟秒数)。 只有从节点复制滞后时间 ≤5 秒,才算有效可用从节点。
发生网络分区,旧主 A 与哨兵、从节点网络断开:
- 旧主 A 再也收不到从节点 B、C 的 repl ack,从节点复制延迟会越来越大,超过 5 秒。
- 此时旧主 A 检测:有效从节点数量为 0,不满足
min‑replicas‑to‑write 1条件。 - 旧主 A 直接拒绝所有写请求(写命令返回错误)。
- 哨兵那边依旧正常把 B 提升为新主,客户端切换到 B 写入。
- 网络恢复之后:旧主 A 降为从,同步 B 的数据。
因为脑裂期间旧主 A 根本没有新数据写入,就不会出现数据丢失。
六、Redis过期删除与内存淘汰
1. Redis使用的过期删除策略是什么?
-
惰性删除:访问key的时候才检查是否过期,过期直接删除;CPU友好,但过期key占内存。
-
定期删除:主线程定时随机抽取一批带过期时间的key,检查删除过期key;采样式,不会扫描全部key。
Redis是惰性删除+定期删除配合,不会一次性扫描全部key避免阻塞。
2. Redis持久化时,对过期键会如何处理的?
- RDB:生成RDB快照时,过期key不会写入RDB文件;加载RDB的时候,忽略已经过期的key。
- AOF:key过期不会立刻删除,当key真正被删除时,才向AOF追加del命令;AOF重写的时候,过期key不会写入新AOF。
3. Redis主从模式中,对过期键会如何处理?
Redis主从不会把过期时间同步给从库。
过期删除逻辑全部在主节点执行;主节点删除过期key,生成del命令同步给从节点。从库不会主动删除key,依靠主节点下发del。 旧版本从库即使key过期,依旧可以读到,直到主库发送del。
4. Redis内存满了,会发生什么?
达到maxmemory最大内存限制,触发内存淘汰策略,按照策略删除key释放内存。 如果策略设置为noeviction,不会删除key,写命令直接返回OOM错误。
5. Redis内存淘汰策略有哪些?
noeviction:不淘汰,写报错(默认)allkeys‑lru:所有key,淘汰最近最少使用volatile‑lru:只对带过期时间key,LRU淘汰allkeys‑lfu:所有key,淘汰访问频次最低volatile‑lfu:过期key范围内LFUallkeys‑random:全部key随机删volatile‑random:过期key随机删volatile‑ttl:删除剩余ttl时间最短的key
Redis LRU不是真实LRU,是采样近似LRU,节省内存。
6. LRU算法和LFU算法有什么区别?
- LRU(Least Recently Used):淘汰最近最少访问的key,关心时间维度;适合访问时间差异大的场景。缺点:偶尔访问的冷数据会把长期不访问但是马上要访问的热点挤掉。
- LFU(Least Frequently Used):淘汰访问频次最低的key,关心访问次数;适合看访问热度。缺点:历史高频key,后续不再访问,还会驻留内存。
七、Redis缓存设计
1. 如何避免缓存雪崩、缓存击穿、缓存穿透?
- 缓存雪崩:大量key同时过期,或者Redis宕机,大量请求打到数据库。
- 方案:过期时间加随机偏移;Redis高可用;限流熔断;多级缓存。
- 缓存击穿:某一个热点key过期,大量并发请求打数据库。
- 方案:热点key永不过期;互斥锁。
- 缓存穿透:查询不存在的数据,缓存不命中,直接穿透到数据库。
- 方案:参数校验;布隆过滤器;空值缓存短期过期。
2. 如何设计一个缓存策略,可以动态缓存热点数据呢?
- 设置内存淘汰策略使用
allkeys‑lfu,Redis自动统计访问频次,保留热点key,冷key淘汰。 - 不设置过期时间,依靠LFU内存淘汰自动剔除冷数据。
- 也可以业务层统计访问量,主动把热点数据写入Redis。
3. 说说常见的缓存更新策略?
- Cache Aside(旁路缓存,业务最常用):更新数据库,删除缓存;读:先读缓存,miss读DB再写缓存。
- Read/Write Through:读写全部交给缓存,缓存负责同步更新数据库,业务代码简单。
- Write Back写回:写只更新缓存,后台异步批量刷数据库,性能高,宕机容易丢数据。
工业界主流:Cache‑Aside。
4. 如何保证缓存和数据库数据的一致性?
- 优先:更新数据库,删除缓存,而不是更新缓存。
- 延迟双删:更新DB,删缓存;短暂sleep,再删一次缓存,解决读请求旧数据回写缓存。
- 给缓存设置过期时间,作为兜底最终一致性。
- 高一致性场景,可以使用Canal监听binlog,异步更新/失效缓存。
- 强一致性场景,不要用缓存。
缓存更多追求最终一致性,很难做到强一致。
八、Redis实战
1. Redis如何实现延迟队列?
延迟队列:消息 / 任务不会立刻被消费,等待指定一段时间之后才被处理。
Redis 实现延迟队列,常用 ZSet 方案,score 存放到期时间戳。生产者写入 zset;消费者轮询查询 score 小于等于当前时间的任务。多实例部署下,查询和删除要通过 Lua 脚本保证原子,防止重复消费。
2. Redis的大key如何处理?
大key:value很大,或者集合元素极多;删除会阻塞主线程;迁移慢。
拆 key 一般会给分片加后缀编号。 一种方式业务通过元素 hash / 取模算出分片下标,直接定位分片 key,不需要额外元数据; 另一种用额外元数据 key 保存全部分片 key 列表。
拆 key 会带来代价:聚合查询、计数需要业务层遍历多个分片结果;同时丢失单 key 的原子性。 注意区分拆 key 和 scan 渐进删除:scan 不改变 key 名称,只是分批删里面元素。
3. Redis管道有什么用?
pipeline管道:把多条命令打包,一次性发给Redis,减少多次网络往返RTT;服务端批量执行,一次性返回结果。
注意:不是事务,中间命令失败不会回滚;适合批量操作提升性能。
4. Redis事务支持回滚吗?
Redis事务:MULTI开启,EXEC执行,DISCARD放弃。
不支持回滚。
-
语法错误:全部命令不执行。
-
运行时错误(比如对字符串执行hget),错误命令报错,其他命令依旧执行,不会回滚。
如果需要原子性,优先使用Lua脚本。
5. 如何用 Redis 实现分布式锁的?
- 基础:
SET key value NX EX;NX不存在才设置,EX设置过期时间,防止死锁。 - 锁value设置唯一标识(UUID),释放锁的时候,判断是自己加的锁才删除,避免误删别人锁。
- 释放锁用Lua脚本保证查询+del原子操作。
- 问题:锁过期业务没执行完,需要锁续期(Redisson看门狗机制)。
- Redis集群需要Redlock红锁算法解决主从切换锁丢失风险。
Agent限流
使用ZSET实现,member为标识,score为时间戳。
用户级别限流
每个用户任意连续 10 秒最多请求 3 次。使用ZSET保存用户的请求记录。可以把 ZSET 想象成一条自动按时间排列的请求时间轴:
1s 4s 9s
A-----------B---------------------C
用于判断允许还是拒绝。
调用前
- 清理超时成员
- 检查当前数量
- 未超过上限则 ZADD
调用后
ZREM 调用 ID
任务去重、消息重复消费问题
通过 SETNX 判断是否已经执行,SET NX 是 SET 命令配合 NX(Not eXists)参数的一种用法。它的核心作用是:只有当指定的键(Key, 这里指的是唯一任务id)不存在时,才执行设置操作;如果键已经存在,则不执行任何操作*。
异步 Agent 通过 MQ 返回结果时,使用Redis防止消息重复消费,保证幂等姓。
熔断器
{agentId}:window- 作用:滑动窗口计数器。
- 理解:用来记录最近一段时间内(比如最近10秒)该 Agent 的成功和失败调用次数。Redis 的 List 或 ZSet 结构非常适合做这种时间窗口统计。当失败次数超过阈值(比如50%的请求都失败了),就会触发状态变更。
{agentId}:openuntil- 作用:熔断截止时间戳。
- 理解:这是一个“禁令”标记。一旦进入 OPEN 状态,系统会写入一个未来的时间戳。在这个时间点之前,所有请求直接拒绝,不再调用下游 Agent。
{agentId}:halfopen_probe- 作用:半开状态的探针锁。
- 理解:这是为了解决“集群惊群效应”。当熔断时间结束进入 HALF_OPEN 状态时,如果集群有100个节点,不能让100个节点同时去试探下游是否恢复。利用 Redis 的原子性(如 SETNX),只允许抢到锁的那 1 个节点去发探测请求。
熔断器生命周期
- CLOSED (闭合/正常态)
- 含义:一切正常,请求正常转发给 Agent。
- 触发条件:在
window窗口期内,统计到的失败率超过了设定的阈值。 - 动作:系统判定 Agent 不可用,状态切换为 OPEN。
- OPEN (开启/熔断态)
- 含义:Agent 已“挂掉”,停止发送请求,直接返回错误或默认值(快速失败)。
- 触发条件:预设的熔断冷却时间(
openuntil=30)结束。 - 动作:状态切换为 HALF_OPEN,准备尝试恢复。
- HALF_OPEN (半开/探测态)
- 含义:小心翼翼地尝试恢复。
- 触发条件:放行的探测请求调用成功。
- 动作:确认 Agent 已恢复,状态切回 CLOSED;如果探测失败,则重新回到 OPEN 状态并延长冷却时间。
分布式锁
为什么需要分布式锁
假设 Octopus 部署三个节点:
节点 A:原来执行任务 T1,后来宕机
节点 B:SchedulerX 扫描到 T1
节点 C:Callback 到达,也发现 T1 需要恢复
此时 B 和 C 都可能尝试恢复同一个任务。
没有锁时可能发生:
B 恢复 T1 → 调用 Agent C 恢复 T1 → 再次调用 Agent
后果包括:
- 外部 Agent 被调用两次;
- 最终消息重复推送;
- Checkpoint 相互覆盖;
- TaskCenter 重复上报;
- 产生重复业务副作用。
获取锁的方法
SET lockKey ownerToken NX PX 60000
- NX:Key 不存在时才写入;
- PX:设置毫秒级过期时间;
- ownerToken:当前持有者的唯一身份。
callback bridge
外部 Agent 的结果是异步返回的,而且可能回调到任意 Octopus 节点,如何把结果交给原来的执行流程,不能使用内存,因此需要借助Redis。
Callback Bridge 的核心需求是低延迟、按 correlationId 动态唤醒;消息已经存入 Redis,Pub/Sub 只做可丢失的通知。若改用 MQ,仍需共享存储和节点路由,复杂度更高、收益有限。
callback bridge传递三类消息:
中间事件
对于中间结果,它需要
- 多条事件排队;
- 保持到达顺序;
- 被等待线程逐条消费;
- 新事件到达时立即唤醒线程;
- 消费后及时清理。
最终结果
短期保留,供等待/恢复读取
回调到达
↓
写 Redis
↓
Pub/Sub 通知
↓
等待线程立即醒来
唤醒通知
项目会先把数据写进 Redis,再执行 Publish。如果pub失败,模型仍可以通过定期扫描获取数据;如果写入失败,模型会通过agent run进行重试。
Redis 心跳
Agent执行过程中,要通过Redis 心跳告诉Scheduler巡检员自己没有宕机。