文章中如果有图看不到,可以点这里去 csdn 看看。从那边导过来的,文章太多,没法一篇篇修改好。

Redis 底层数据结构解析(七):Stream 类型与消息队列的完美实现

1. 引言:Redis Stream 的设计目标

Redis Stream 是 Redis 5.0 版本引入的全新数据类型,旨在提供完整的消息队列功能,同时保持 Redis 的高性能和简洁性。与之前的 List 类型实现的简单消息队列相比,Stream 类型提供了更丰富的功能:

  • 消息持久化:支持消息的持久化存储和回溯
  • 消费者组:支持多消费者负载均衡和消息确认机制
  • 阻塞操作:支持客户端阻塞等待新消息
  • 范围查询:支持按消息ID范围查询历史消息

Stream 的底层实现结合了多种数据结构,包括 listpackRax 树(基数树)哈希表,形成了一个高效且功能完备的消息流系统。

2. Stream 类型的核心概念

2.1 消息ID的结构

Stream 中的每条消息都有一个唯一的ID,格式为 <millisecondsTime>-<sequenceNumber>,例如 1640995200000-0。这种设计确保了ID的时间有序性和唯一性。

2.2 消费者组机制

消费者组是 Stream 的核心特性之一,允许多个消费者协同处理消息流:

  • 每个消息只会被投递给组内的一个消费者
  • 支持消息确认机制,确保至少处理一次语义
  • 支持消费者故障转移和负载均衡

3. Stream 的底层存储结构

3.1 Stream 的整体结构

Redis Stream 的底层实现主要包含两个部分:

  1. 消息存储:使用 listpack 存储实际的消息内容
  2. 索引结构:使用 Rax 树(基数树)进行消息ID的快速查找

Stream 结构定义(stream.h):

typedef struct stream {
    rax *rax;               // 基数树,存储消息ID到listpack的映射
    uint64_t length;        // 消息数量
    streamID last_id;       // 最后一条消息的ID
    rax *cgroups;           // 消费者组列表
} stream;

3.2 listpack 中的消息存储

每个 listpack 存储多条消息,采用紧凑的二进制格式:

+----------+----------+----------+-----+----------+----------+
| 消息1ID | 字段数 | 字段1值 | ... | 字段N值 | 消息2ID | ...
+----------+----------+----------+-----+----------+----------+

消息存储格式:

typedef struct streamID {
    uint64_t ms;        // 时间戳部分
    uint64_t seq;       // 序列号部分
} streamID;

// listpack中的消息存储格式
+-------------------+----------------+---------------+-----+---------------+
| delta_ms (varint) | delta_seq (varint) | num_fields (varint) | field1 value | ...
+-------------------+----------------+---------------+-----+---------------+

这种增量编码方式大大减少了存储空间。

3.3 Rax 树索引结构

Rax 树(基数树)为消息提供了高效的索引能力,支持前缀查找和范围扫描。

Rax 树节点结构:

typedef struct raxNode {
    uint32_t iskey:1;     // 是否包含key
    uint32_t isnull:1;    // 是否关联NULL值
    uint32_t iscompr:1;   // 是否压缩节点
    uint32_t size:29;     // 子节点数量或压缩字符串长度
    unsigned char data[]; // 柔性数组,存储实际数据
} raxNode;

4. Stream 操作的实现原理

4.1 消息添加(XADD)

XADD 命令的实现涉及多个步骤:

void xaddCommand(client *c) {
    // 解析消息ID和字段
    streamID id;
    int id_given = 0;
    // ... 参数解析逻辑
    
    // 获取或创建Stream对象
    robj *o = streamTypeLookupWriteOrCreate(c, c->argv[1]);
    if (o == NULL) return;
    
    // 生成消息ID
    if (!id_given) {
        id.ms = mstime();
        id.seq = streamIncrID(o->ptr, id.ms);
    }
    
    // 将消息添加到Stream
    int added = streamAppendItem(o->ptr, &id, c->argv+2, (c->argc-2)/2, &id);
    
    if (added) {
        // 通知阻塞的消费者
        signalKeyAsReady(c, c->argv[1], OBJ_STREAM);
        
        // 向消费者组投递消息
        streamPropagateXADD(c, c->argv[1], &id, c->argv+2, (c->argc-2)/2);
    }
    
    addReplyStreamID(c, &id);
}

4.2 消息消费(XREADGROUP)

消费者组消息消费的实现:

void xreadGroupCommand(client *c) {
    // 解析消费者组参数
    streamCG *group = NULL;
    streamConsumer *consumer = NULL;
    
    // 查找或创建消费者组
    group = streamLookupCG(s, groupname);
    if (group == NULL) {
        // 创建新消费者组
        group = streamCreateCG(s, groupname, &id, 0);
    }
    
    // 查找或创建消费者
    consumer = streamLookupConsumer(group, consumername, 1);
    consumer->seen_time = mstime();
    
    // 获取待处理消息
    streamID start_id;
    if (!strcmp(start, ">")) {
        // 获取新消息
        start_id = group->last_id;
    } else {
        // 解析特定ID
        streamParseIDOrReply(c, start, &start_id, 0);
    }
    
    // 读取消息并更新PEL(待处理条目列表)
    raxIterator ri;
    streamStartIterID(s, &start_id, 0, &ri);
    
    while(streamIteratorGetID(&ri, &id, &numfields)) {
        // 将消息添加到PEL
        streamNACK *nack = streamCreateNACK(consumer);
        raxInsert(group->pel, (unsigned char*)&id, sizeof(id), nack, NULL);
        
        // 将消息发送给客户端
        addReplyArrayLen(c, 2);
        addReplyStreamID(c, &id);
        addReplyArrayLen(c, numfields*2);
        // ... 添加字段值
    }
}

5. 内存管理与优化策略

5.1 内存回收机制

Stream 实现了自动的内存回收机制,防止无限增长:

void streamFreeListpacks(stream *s, streamID *max_id, size_t max_items) {
    raxIterator ri;
    streamStartIterID(s, &s->first_id, 1, &ri);
    
    size_t deleted = 0;
    while(streamIteratorGetID(&ri, &id, NULL)) {
        if (streamCompareID(&id, max_id) > 0) {
            break;
        }
        
        // 删除过期的listpack节点
        raxRemove(s->rax, (unsigned char*)&id, sizeof(id), NULL);
        deleted++;
        
        if (max_items && deleted >= max_items) {
            break;
        }
    }
}

5.2 消费者组状态维护

消费者组的状态维护涉及复杂的内存管理:

void streamCleanupConsumers(stream *s, streamCG *group, long long min_idle) {
    raxIterator ri;
    raxStart(&ri, group->consumers);
    
    long long now = mstime();
    while(raxNext(&ri)) {
        streamConsumer *consumer = ri.data;
        
        // 清理空闲时间过长的消费者
        if (now - consumer->seen_time > min_idle) {
            // 转移PEL中的消息到其他消费者
            streamTransferNACKs(consumer, group);
            
            // 删除消费者
            raxRemove(group->consumers, ri.key, ri.key_len, NULL);
            streamFreeConsumer(consumer);
        }
    }
    raxStop(&ri);
}

6. 性能分析与实战应用

6.1 性能特征

操作类型时间复杂度说明
XADDO(log N)使用Rax树索引
XREADO(log N + M)M为返回的消息数
XRANGEO(log N + M)范围查询
XACKO(log N)消息确认
XGROUPO(1)消费者组操作

6.2 实战应用示例

实现可靠的消息队列:

class RedisStreamQueue:
    def __init__(self, stream_name, group_name, consumer_name):
        self.r = redis.Redis()
        self.stream = stream_name
        self.group = group_name
        self.consumer = consumer_name
        
        # 确保消费者组存在
        try:
            self.r.xgroup_create(stream_name, group_name, id='0', mkstream=True)
        except redis.ResponseError:
            pass  # 组已存在
    
    def produce(self, message, max_length=1000):
        # 生产消息,限制流长度
        return self.r.xadd(self.stream, message, maxlen=max_length)
    
    def consume(self, block_time=1000, count=1):
        # 消费消息
        messages = self.r.xreadgroup(
            self.group, self.consumer,
            {self.stream: '>'},
            count=count, block=block_time
        )
        
        results = []
        for stream, message_list in messages:
            for message_id, message_data in message_list:
                results.append((message_id, message_data))
                # 处理成功后确认消息
                self.r.xack(self.stream, self.group, message_id)
        
        return results
    
    def pending_stats(self):
        # 获取待处理消息统计
        return self.r.xpending(self.stream, self.group)

实现事件溯源系统:

class EventSourcingSystem:
    def __init__(self, stream_base):
        self.r = redis.Redis()
        self.stream_base = stream_base
    
    def append_event(self, entity_type, entity_id, event_type, payload):
        stream_name = f"{self.stream_base}:{entity_type}:{entity_id}"
        event = {
            'timestamp': time.time(),
            'type': event_type,
            'payload': json.dumps(payload)
        }
        return self.r.xadd(stream_name, event)
    
    def get_entity_history(self, entity_type, entity_id):
        stream_name = f"{self.stream_base}:{entity_type}:{entity_id}"
        # 读取所有事件
        events = self.r.xrange(stream_name, '-', '+')
        return [self._parse_event(e) for e in events]
    
    def project_entity_state(self, entity_type, entity_id):
        # 基于事件流计算当前状态
        events = self.get_entity_history(entity_type, entity_id)
        state = {}
        for event in events:
            state = self._apply_event(state, event)
        return state

7. 高级特性与最佳实践

7.1 监控与告警

class StreamMonitor:
    def __init__(self, redis_client):
        self.r = redis_client
    
    def monitor_stream_growth(self, pattern="*", max_length=10000):
        # 监控Stream长度
        problematic_streams = []
        cursor = 0
        while True:
            cursor, keys = self.r.scan(cursor, match=pattern, count=100)
            for key in keys:
                if self.r.type(key) == b'stream':
                    length = self.r.xlen(key)
                    if length > max_length:
                        problematic_streams.append((key, length))
            if cursor == 0:
                break
        return problematic_streams
    
    def monitor_consumer_lag(self, group_pattern="*"):
        # 监控消费者滞后
        lagging_consumers = []
        cursor = 0
        while True:
            cursor, keys = self.r.scan(cursor, match="*", count=100)
            for key in keys:
                if self.r.type(key) == b'stream':
                    groups = self.r.xinfo_groups(key)
                    for group in groups:
                        if group['pending'] > 1000:  # 滞后阈值
                            lagging_consumers.append({
                                'stream': key,
                                'group': group['name'],
                                'pending': group['pending']
                            })
            if cursor == 0:
                break
        return lagging_consumers

7.2 性能优化策略

Stream 分片策略:

class ShardedStream:
    def __init__(self, base_name, num_shards=10):
        self.r = redis.Redis()
        self.base_name = base_name
        self.num_shards = num_shards
    
    def get_shard_name(self, key):
        # 根据key计算分片
        shard_id = hash(key) % self.num_shards
        return f"{self.base_name}:shard:{shard_id}"
    
    def add_message(self, key, message):
        shard_name = self.get_shard_name(key)
        return self.r.xadd(shard_name, message)
    
    def read_messages(self, key, count=10):
        shard_name = self.get_shard_name(key)
        return self.r.xrange(shard_name, '-', '+', count=count)

批量消息处理:

class BatchStreamProcessor:
    def __init__(self, stream_name, group_name, batch_size=100):
        self.r = redis.Redis()
        self.stream = stream_name
        self.group = group_name
        self.batch_size = batch_size
    
    def process_batch(self, consumer_name, processor_func):
        # 批量获取消息
        messages = self.r.xreadgroup(
            self.group, consumer_name,
            {self.stream: '>'},
            count=self.batch_size, block=0
        )
        
        if not messages:
            return 0
        
        processed_count = 0
        message_ids = []
        
        for stream, message_list in messages:
            for message_id, message_data in message_list:
                try:
                    # 处理消息
                    processor_func(message_data)
                    message_ids.append(message_id)
                    processed_count += 1
                except Exception as e:
                    print(f"Failed to process message {message_id}: {e}")
        
        # 批量确认消息
        if message_ids:
            self.r.xack(self.stream, self.group, *message_ids)
        
        return processed_count

8. 总结

Redis Stream 类型提供了一个完整、高效的消息队列解决方案,其底层实现巧妙地结合了多种数据结构:

  1. listpack:用于紧凑存储消息内容
  2. Rax 树:用于高效的消息ID索引和范围查询
  3. 哈希表:用于管理消费者组和消费者状态

核心优势:

  • 高性能:所有操作都保持对数时间复杂度
  • 可靠性:支持消息持久化和至少一次投递语义
  • 可扩展性:支持多消费者和消费者组
  • 灵活性:支持消息回溯和范围查询

适用场景:

  • 消息队列和事件驱动架构
  • 事件溯源和审计日志
  • 实时数据流处理
  • 操作日志和活动追踪

Redis Stream 的设计体现了 Redis 一贯的哲学:通过简单的基础构件组合出强大的功能,同时在性能和资源使用上保持高效。理解 Stream 的底层实现有助于开发者更好地利用这一强大工具,构建可靠且高性能的分布式系统。


Redis Stream 类型底层结构图示

1. Stream 整体结构图

Redis Stream 整体结构
Rax树索引
Stream对象
最后消息ID
消息数量
消费者组字典
Listpack节点1
Listpack节点2
Listpack节点N
消息1: ID1, 字段值
消息2: ID2, 字段值
消息K: IDk, 字段值
消费者组1
消费者组2
待处理消息列表PEL
消费者列表
消费者A
消费者B

2. Listpack 消息存储格式图

增量编码示例
消息1: 1640995200000-0
消息2: 1640995200001-0
存储格式:
1640995200000-0: 绝对存储
+1-0: 相对增量
Listpack 消息存储格式
Listpack头部
总字节数: 4字节
元素数量: 2字节
消息存储条目
时间戳增量: varint
序列号增量: varint
字段数量: varint
字段1值
字段2值
字段N值
结束标记: 0xFF
下一个消息...

3. 消费者组机制流程图

消息状态流转
已投递
已接收
处理中
已确认
处理超时
重新投递
消费者组工作机制
消费者组内部
XADD添加消息
生产者
Stream存储消息
向消费者组投递
消费者组
待处理消息列表
消费者列表
消费者1
消费者2
消费者N
消息条目1: ID1, 消费者1
消息条目2: ID2, 消费者2
消费者1读取消息
消费者2读取消息
处理消息
处理消息
发送XACK确认
发送XACK确认
从PEL移除消息
从PEL移除消息

4. Rax 树索引结构图

Rax 节点结构
iskey: 是否包含key
Rax节点结构
isnull: 是否关联空值
iscompr: 是否压缩节点
size: 子节点数量
data: 存储的键片段
子节点指针数组
Rax 树索引结构
节点1: 1640
根节点
节点2: 1641
节点3: 1642
叶子节点1
指向listpack1
叶子节点2
指向listpack2
叶子节点3
指向listpack3
叶子节点4
指向listpack4
叶子节点5
指向listpack5
Listpack1: 存储消息
Listpack2: 存储消息
Listpack3: 存储消息

这些图表从不同角度展示了 Redis Stream 类型的底层实现,包括整体结构、消息存储格式、消费者组机制和索引结构,希望能够帮助您更好地理解 Stream 类型的设计精髓和工作原理。

posted @ 2025-09-07 15:29  NeoLshu  阅读(7)  评论(0)    收藏  举报  来源