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[];
}

所谓柔性数组,指的是他的地址和结构体是连续的,可以快速查找内存。

image-20260831193324489

设计目标二:不同长度的字符串其实没必要占用相同大小的头部,

每个字符串的元信息都占用4字节太浪费了。因此,针对不同大小的字符串,衍生了多种sds。

sdshdr5

对于最短的字符串,使用1个flag字节来表示元信息,低3位存储类型,高5位存储长度。短字符串不需要free,因为修改后大概率也在32($2^5$ )字节内,无需扩容;即使扩容,也会直接修改类型,不会再使用sdshdr5 。

struct sdshdr5 {
	unsigned char flags;
  char buf[];
}

image-20260831193949495

sdshdr8

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

SDS的扩容

image-20260831195623273

跳跃表 skiplist

对于有序数据,数组不便于元素插入和删除,链表查询效率低,平衡树或红黑树实现复杂,因此Redis采用了跳跃表。

分层有序链表思想

首先,为了解决链表查找效率问题,我们可以参考分层有序链表,从高到低进行查找。

image-20260831195930311

image-20260831200153284

基于这种思想,跳跃表具有以下几个关键性质:

  1. 多层结构:跳跃表由多层有序链表构成;底层(L0)包含全部数据节点,上层是底层的子集;一个节点出现在上层,则必然出现在下层。
  2. 头结点header:专属头节点,不作为实际数据节点;每层存储指向本层下一个节点的指针,同时记录跨度span,span代表该指针跨越的底层节点数量。
  3. 表高度level:排除头节点,节点最大层数就是跳跃表高度,图中示例高度为3。
  4. 有序存储:每一层链表内数据按分数递增有序排列;每层链表末尾节点指向NULL,标记本层链表结束。
  5. 尾指针tail:维护tail指针,直接指向跳跃表最末尾的数据节点。
  6. 表长度length:最底层L0的节点总数(不计header头节点),代表跳跃表整体长度,示例中length=7。
  7. 后退指针backward:每个数据节点带有后退指针;头节点、第一个数据节点后退指针为NULL;其余节点的后退指针,指向最底层链表的前一个节点,只在底层实现反向遍历。
  8. 节点信息:每个数据节点存储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)$

插入节点的步骤

  1. 查找插入位置
  2. 调整跳跃表高度
  3. 插入节点
  4. 调整backward

删除节点的步骤

  1. 查找涉及更新的节点
  2. 设置span和forward

跳跃表的应用

当使用压缩列表的配置(zset-max-ziplist-entries 和 zset-max-ziplist-value)超限时,跳跃表主要应用于有序集合的底层实现。

压缩列表 ziplist

压缩列表的本质是一个字节数组。Redis的有序集合、散列表和列表都使用了压缩列表。

  • 有序集合和散列表在元素少且是短字符串时使用压缩列表。
  • 列表依赖快速链表实现,快速链表是双向链表与压缩列表的组合。

压缩列表数据结构

整体结构

image-20260831222843954

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

每个节点的结构

image-20260831224031223

普通双向链表每个节点都要维护 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 有什么区别?

  1. 数据结构:Memcached只有简单k‑v字符串;Redis支持String、List、Hash、Set、ZSet等多种数据结构。
  2. 持久化:Memcached不支持持久化;Redis支持RDB、AOF持久化,宕机数据可恢复。
  3. 线程模型:Memcached多线程;Redis6.0前单线程处理命令,6.0引入多线程IO。
  4. 集群:Memcached原生不支持集群;Redis支持主从、哨兵、Redis‑Cluster集群。
  5. 内存管理:Memcached使用预分配内存池;Redis按需申请内存,支持内存淘汰策略。
  6. 其他:Redis支持发布订阅、Lua脚本、事务;Memcached功能简单。

3. 为什么用 Redis 作为 MySQL 的缓存?

  1. Redis内存访问速度远快于磁盘MySQL,降低数据库压力,提升接口响应速度。
  2. 支持丰富数据结构,能直接满足业务的集合、排序、列表类缓存场景。
  3. 支持过期时间,天然适合缓存过期失效。
  4. 支持持久化,缓存节点重启后数据可以恢复。
  5. 支持集群,可横向扩容,承载高并发读请求。

注意:需要处理缓存穿透、击穿、雪崩,以及缓存与数据库一致性问题。

二、Redis 数据结构

1. Redis数据类型以及使用场景分别是什么?

类型 作用 典型场景
String 字符串/数字 计数器、分布式session、简单KV、限流计数
List 双向列表 消息队列、最新列表、时间轴
Hash 哈希表 存储对象,用户信息,商品属性
Set 无序集合 去重、交集并集、好友共同关注
ZSet 有序集合 排行榜、延时队列、带权重排序

2. 五种常见的 Redis 数据类型是怎么实现?

  1. String:底层 SDS(简单动态字符串)。
  2. List:底层 quicklist(3.2版本后),双向链表,每个节点是ziplist压缩列表。
  3. Hash:数据少时用ziplist压缩列表;数据量大转为dict哈希表。
  4. Set:整数集合intset(元素全是整数、数量少);否则dict哈希表。
  5. ZSet:数据少时ziplist;数据量大使用跳表(zskiplist)+dict字典。

三、Redis 线程模型

1. Redis是单线程吗?

Redis 执行命令的主线程是单线程,但Redis整体并不是只有一个线程。

  • Redis6.0之前:网络IO、命令执行全部单线程;持久化RDB/AOF会fork子进程做后台工作。
  • Redis6.0之后:命令执行依旧单线程;网络IO处理支持多线程,读写socket可以多线程分担。

2. Redis单线程模式是怎样的?

采用IO多路复用 + 单线程事件循环:

  1. 一个主线程,通过epoll/kqueue监听大量socket文件描述符。
  2. 当客户端socket就绪,主线程读取请求,执行Redis命令,返回结果。
  3. 所有命令串行执行,不会出现线程安全问题,不需要锁。
  4. 耗时操作(RDB、AOF刷盘)交给子进程/后台线程,不阻塞主线程。

3. Redis采用单线程为什么还这么快?

  1. 绝大部分操作是内存操作,内存访问速度极快。
  2. IO多路复用,可以处理大量客户端连接。
  3. 避免多线程频繁上下文切换、锁竞争开销。
  4. 耗时任务交给子进程/后台线程,不阻塞主线程。
  5. C语言实现,底层内存处理高效。

注意:单线程怕慢命令,keys、flushall、大集合操作会阻塞整个服务。

4. Redis 6.0之前为什么使用单线程?

  1. 内存数据库瓶颈大多不在CPU,而在网络IO;单线程足以跑满网络。
  2. 多线程会带来锁、竞争、上下文切换的额外开销,增加代码复杂度。
  3. 命令串行执行,天然线程安全,不需要处理并发竞争。
  4. fork子进程处理持久化,已经可以把耗时操作剥离主线程。

5. Redis 6.0 之后为什么引入了多线程?

当网络IO成为瓶颈时,单线程处理大量网络读写会打满CPU。

  • 多线程只用来做socket的IO读写,命令执行仍然是单线程。
  • 将网络数据包解析、读写交给IO线程,主线程专心执行命令,提升高并发下网络吞吐。
  • 默认关闭,需要配置开启。

四、Redis持久化

1. Redis如何实现数据不丢失?

依靠两种持久化:RDB快照、AOF日志,也可以开启混合持久化。

  1. RDB:某时间点全量二进制快照,fork子进程把内存数据写入磁盘。
  2. AOF:记录每一条写命令,追加写入日志文件;宕机后重放AOF恢复数据。
  3. 混合持久化:AOF文件头部带上RDB快照,后面跟着增量AOF日志,兼顾恢复速度与数据安全性。

2. AOF日志是如何实现的?

  1. 命令追加:写命令追加到aof_buf内存缓冲区,不是直接写磁盘。对于有过期时间的key,AOF会改写命令为设置过期的绝对时间戳;对于过期的key,AOF会追加del命令。
  2. 文件刷盘策略:
    • always:每次写都fsync刷盘,最安全,性能差。
    • everysec:每秒刷盘(默认),最多丢失1秒数据。
    • no:交给操作系统自行刷盘,不可控。
  3. AOF重写bgrewriteaof:日志会越来越大,fork子进程,遍历内存数据生成新AOF文件,丢弃冗余旧命令,完成后替换旧文件,减小文件体积。

3. RDB快照是如何实现的呢?

  1. 执行bgsave,主线程调用fork()生成子进程。

  2. 子进程拥有内存数据副本(写时复制COW),把内存全量数据dump成二进制rdb文件。

  3. 子进程完成后通知主进程,替换旧RDB文件。

  4. 触发方式:手动save/bgsave;配置定时自动触发。

    save命令在主线程执行,会阻塞,生产环境禁止使用。

4. 为什么会有混合持久化?

AOF纯模式:恢复数据需要重放全部历史命令,文件大、恢复慢。 纯RDB:只能全量快照,两次快照之间宕机会丢失大量数据。

混合持久化:AOF重写时,把当前内存数据以RDB二进制写到AOF文件头部,后续继续追加AOF增量命令。

  • 恢复:先加载RDB部分,再重放后面增量AOF,兼顾恢复速度和少丢数据。Redis4.0+支持。

五、Redis集群

1. Redis如何实现服务高可用?

  1. 主从复制:主节点写,从节点同步数据,从节点提供读服务;主挂掉无法自动切换。
  2. Sentinel哨兵:监控主从节点;主节点故障,哨兵自动选出新主节点,自动故障转移,实现高可用。
  3. Redis‑Cluster集群:分片,把数据分散到多个master节点,水平扩容存储;每个master搭配slave实现故障转移。

2. 集群脑裂导致数据丢失怎么办?

先复现脑裂整个过程

  1. 原本:A 是主节点,B、C 是从节点,Sentinel 哨兵正常监控。
  2. 网络分区发生:主节点 A 和哨兵、从节点之间网络断了。
    • 哨兵收不到 A 的心跳,认为 A 故障,把从 B 升级成新主节点。客户端慢慢切换连到新主 B 写数据。
    • ⚠️但是!旧主 A 本身还活着,只是和哨兵隔离。一部分客户端因为网络路由,依旧还能连上旧主 A,继续向旧主 A 写入新数据。
  3. 一段时间后网络恢复。
    • 哨兵会把旧主 A 降级为新主 B 的从节点。
    • A 上自己接收的那部分新写入的数据,会被直接清空,同步新主 B 的数据,这部分旧主 A 的写入就永久丢失了。

问题根源:网络断开后,旧主依然认为自己是 master,还在接受写请求。

两个配置参数含义

min‑replicas‑to‑write 1
min‑replicas‑max‑lag 5

翻译:主节点只有满足:至少有 1 个从节点,且这个从节点的复制延迟不超过 5 秒,主节点才允许执行写命令;否则拒绝写操作。

  1. min‑replicas‑to‑write 1 主节点要求,在线、正常复制的从节点数量至少达到 N 个,才允许接收写。这里 N=1。
  2. min‑replicas‑max‑lag 5 判断从节点的repl_lag复制延迟(从节点上报给主的延迟秒数)。 只有从节点复制滞后时间 ≤5 秒,才算有效可用从节点。

发生网络分区,旧主 A 与哨兵、从节点网络断开:

  1. 旧主 A 再也收不到从节点 B、C 的 repl ack,从节点复制延迟会越来越大,超过 5 秒。
  2. 此时旧主 A 检测:有效从节点数量为 0,不满足min‑replicas‑to‑write 1条件。
  3. 旧主 A 直接拒绝所有写请求(写命令返回错误)。
  4. 哨兵那边依旧正常把 B 提升为新主,客户端切换到 B 写入。
  5. 网络恢复之后:旧主 A 降为从,同步 B 的数据。

因为脑裂期间旧主 A 根本没有新数据写入,就不会出现数据丢失。

六、Redis过期删除与内存淘汰

1. Redis使用的过期删除策略是什么?

  1. 惰性删除:访问key的时候才检查是否过期,过期直接删除;CPU友好,但过期key占内存。

  2. 定期删除:主线程定时随机抽取一批带过期时间的key,检查删除过期key;采样式,不会扫描全部key。

    Redis是惰性删除+定期删除配合,不会一次性扫描全部key避免阻塞。

2. Redis持久化时,对过期键会如何处理的?

  1. RDB:生成RDB快照时,过期key不会写入RDB文件;加载RDB的时候,忽略已经过期的key。
  2. 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内存淘汰策略有哪些?

  1. noeviction:不淘汰,写报错(默认)
  2. allkeys‑lru:所有key,淘汰最近最少使用
  3. volatile‑lru:只对带过期时间key,LRU淘汰
  4. allkeys‑lfu:所有key,淘汰访问频次最低
  5. volatile‑lfu:过期key范围内LFU
  6. allkeys‑random:全部key随机删
  7. volatile‑random:过期key随机删
  8. volatile‑ttl:删除剩余ttl时间最短的key

Redis LRU不是真实LRU,是采样近似LRU,节省内存。

6. LRU算法和LFU算法有什么区别?

  • LRU(Least Recently Used):淘汰最近最少访问的key,关心时间维度;适合访问时间差异大的场景。缺点:偶尔访问的冷数据会把长期不访问但是马上要访问的热点挤掉。
  • LFU(Least Frequently Used):淘汰访问频次最低的key,关心访问次数;适合看访问热度。缺点:历史高频key,后续不再访问,还会驻留内存。

七、Redis缓存设计

1. 如何避免缓存雪崩、缓存击穿、缓存穿透?

  1. 缓存雪崩:大量key同时过期,或者Redis宕机,大量请求打到数据库。
    • 方案:过期时间加随机偏移;Redis高可用;限流熔断;多级缓存。
  2. 缓存击穿:某一个热点key过期,大量并发请求打数据库。
    • 方案:热点key永不过期;互斥锁。
  3. 缓存穿透:查询不存在的数据,缓存不命中,直接穿透到数据库。
    • 方案:参数校验;布隆过滤器;空值缓存短期过期。

2. 如何设计一个缓存策略,可以动态缓存热点数据呢?

  1. 设置内存淘汰策略使用allkeys‑lfu,Redis自动统计访问频次,保留热点key,冷key淘汰。
  2. 不设置过期时间,依靠LFU内存淘汰自动剔除冷数据。
  3. 也可以业务层统计访问量,主动把热点数据写入Redis。

3. 说说常见的缓存更新策略?

  1. Cache Aside(旁路缓存,业务最常用):更新数据库,删除缓存;读:先读缓存,miss读DB再写缓存。
  2. Read/Write Through:读写全部交给缓存,缓存负责同步更新数据库,业务代码简单。
  3. Write Back写回:写只更新缓存,后台异步批量刷数据库,性能高,宕机容易丢数据。

工业界主流:Cache‑Aside。

4. 如何保证缓存和数据库数据的一致性?

  1. 优先:更新数据库,删除缓存,而不是更新缓存。
  2. 延迟双删:更新DB,删缓存;短暂sleep,再删一次缓存,解决读请求旧数据回写缓存。
  3. 给缓存设置过期时间,作为兜底最终一致性。
  4. 高一致性场景,可以使用Canal监听binlog,异步更新/失效缓存。
  5. 强一致性场景,不要用缓存。

缓存更多追求最终一致性,很难做到强一致。

八、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放弃。

不支持回滚。

  1. 语法错误:全部命令不执行。

  2. 运行时错误(比如对字符串执行hget),错误命令报错,其他命令依旧执行,不会回滚。

    如果需要原子性,优先使用Lua脚本。

5. 如何用 Redis 实现分布式锁的?

  1. 基础:SET key value NX EX;NX不存在才设置,EX设置过期时间,防止死锁。
  2. 锁value设置唯一标识(UUID),释放锁的时候,判断是自己加的锁才删除,避免误删别人锁。
  3. 释放锁用Lua脚本保证查询+del原子操作。
  4. 问题:锁过期业务没执行完,需要锁续期(Redisson看门狗机制)。
  5. Redis集群需要Redlock红锁算法解决主从切换锁丢失风险。

Agent限流

使用ZSET实现,member为标识,score为时间戳。

用户级别限流

每个用户任意连续 10 秒最多请求 3 次。使用ZSET保存用户的请求记录。可以把 ZSET 想象成一条自动按时间排列的请求时间轴:

1s          4s                    9s
A-----------B---------------------C

用于判断允许还是拒绝。

调用前

  1. 清理超时成员
  2. 检查当前数量
  3. 未超过上限则 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巡检员自己没有宕机。