KeyDB:多线程redis就绪!!

今天给大家介绍的是KeyDB,KeyDB项目是从redis fork出来的分支。众所周知redis是一个单线程的kv内存存储系统,而KeyDB在100%兼容redis API的情况下将redis改造成多线程。

项目地址是 https://github.com/EQ-Alpha/KeyDB

线程模型

KeyDB将redis原来的主线程拆分成了主线程和worker线程。每个worker线程都是io线程,负责监听端口,accept请求,读取数据和解析协议。如图所示:

图片

KeyDB使用了SO_REUSEPORT特性,多个线程可以绑定监听同个端口。

每个worker线程做了cpu绑核,读取数据也使用了SO_INCOMING_CPU特性,指定cpu接收数据。

解析协议之后每个线程都会去操作内存中的数据,由一把全局锁来控制多线程访问内存数据。

主线程其实也是一个worker线程,包括了worker线程的工作内容,同时也包括只有主线程才可以完成的工作内容。在worker线程数组中下标为0的就是主线程。

主线程的主要工作在实现serverCron,包括:

  • 处理统计

  • 客户端链接管理

  • db数据的resize和reshard

  • 处理aof

  • replication主备同步

  • cluster模式下的任务

链接管理

在redis中所有链接管理都是在一个线程中完成的。在KeyDB的设计中,每个worker线程负责一组链接,所有的链接插入到本线程的链接列表中维护。链接的产生、工作、销毁必须在同个线程中。每个链接新增一个字段

int iel; /* the event loop index we're registered with */

用来表示链接属于哪个线程接管。

KeyDB维护了三个关键的数据结构做链接管理:

  • clients_pending_write:线程专属的链表,维护同步给客户链接发送数据的队列

  • clients_pending_asyncwrite:线程专属的链表,维护异步给客户链接发送数据的队列

  • clients_to_close:全局链表,维护需要异步关闭的客户链接

分成同步和异步两个队列,是因为redis有些联动api,比如pub/sub,pub之后需要给sub的客户端发送消息,pub执行的线程和sub的客户端所在线程不是同一个线程,为了处理这种情况,KeyDB将需要给非本线程的客户端发送数据维护在异步队列中。

同步发送的逻辑比较简单,都是在本线程中完成,以下图来说明如何同步给客户端发送数据:

图片

如上文所提到的,一个链接的创建、接收数据、发送数据、释放链接都必须在同个线程执行。异步发送涉及到两个线程之间的交互。KeyDB通过管道在两个线程中传递消息:

int fdCmdWrite; //写管道
int fdCmdRead; //读管道

本地线程需要异步发送数据时,先检查client是否属于本地线程,非本地线程获取到client专属的线程ID,之后给专属的线程管到发送AE_ASYNC_OP::CreateFileEvent的操作,要求添加写socket事件。专属线程在处理管道消息时将对应的请求添加到写事件中,如图所示:

图片

redis有些关闭客户端的请求并非完全是在链接所在的线程执行关闭,所以在这里维护了一个全局的异步关闭链表。

图片

锁机制

KeyDB实现了一套类似spinlock的锁机制,称之为fastlock。

fastlock的主要数据结构有:

struct ticket
{
 uint16_t m_active; //解锁+1
 uint16_t m_avail; //加锁+1
};
struct fastlock
{
 volatile struct ticket m_ticket;
 volatile int m_pidOwner; //当前解锁的线程id
 volatile int m_depth; //当前线程重复加锁的次数
};

使用原子操作__atomic_load_2,__atomic_fetch_add,__atomic_compare_exchange来通过比较m_active=m_avail判断是否可以获取锁。

fastlock提供了两种获取锁的方式:

  • try_lock:一次获取失败,直接返回

  • lock:忙等,每1024 * 1024次忙等后使用sched_yield 主动交出cpu,挪到cpu的任务末尾等待执行。

在KeyDB中将try_lock和事件结合起来,来避免忙等的情况发生。每个客户端有一个专属的lock,在读取客户端数据之前会先尝试加锁,如果失败,则退出,因为数据还未读取,所以在下个epoll_wait处理事件循环中可以再次处理。

图片

Active-Replica

KeyDB实现了多活的机制,每个replica可设置成可写非只读,replica之间互相同步数据。主要特性有:

  • 每个replica有个uuid标志,用来去除环形复制

  • 新增加rreplay API,将增量命令打包成rreplay命令,带上本地的uuid

  • key,value加上时间戳版本号,作为冲突校验,如果本地有相同的key且时间戳版本号大于同步过来的数据,新写入失败。采用当前时间戳向左移20位,再加上后44位自增的方式来获取key的时间戳版本号。

<think> 我们正在讨论Redis中的阻塞行为和队列管理。用户的问题集中在三个方面: 1. Redis中"阻塞"的含义 2. 每个key是否拥有自己的队列 3. 为什么串行化的Redis需要多个队列 结合之前的引用和Redis的工作原理,我们来逐一解答。 1. **Redis中阻塞的含义** - 在Redis中,"阻塞"通常指的是客户端在执行某些命令时被挂起,直到特定条件满足(如数据到达或超时)才返回。 - 典型的阻塞命令如`BLPOP`、`BRPOP`等,这些命令允许客户端等待列表中的元素。当客户端调用`BLPOP key timeout`时,如果key对应的列表为空,则客户端连接会被阻塞,直到有元素被推入列表或超时[^4]。 - 阻塞期间,Redis不会处理该客户端的其他命令,但其他客户端连接仍然可以正常执行命令(因为Redis是单线程,但可以处理多个连接的I/O多路复用)。 2. **每个key是否有自己的队列** - Redis并没有为每个key维护一个独立的命令执行队列。Redis是单线程的,所有命令都进入一个全局队列,按顺序执行。 - 但是,对于阻塞操作,Redis内部会为每个被阻塞的key维护一个等待队列(阻塞队列)。例如,当多个客户端同时使用`BLPOP`等待同一个列表时,这些客户端都会被阻塞,并且按照请求顺序形成一个队列。当有数据推入该列表时,Redis会按照队列顺序唤醒第一个客户端[^4]。 - 因此,我们可以说:在阻塞操作中,每个被阻塞的key都有一个等待该key的客户端队列。 3. **串行化的Redis为何需要多个队列** - 全局命令队列:为了保证命令的串行执行,Redis将所有命令放入一个全局队列,由单线程按顺序执行。 - 阻塞队列:为了高效处理阻塞命令,Redis为每个被阻塞的key维护一个独立的阻塞队列。这样,当数据到达某个key时,只需唤醒等待该key的客户端,而不需要遍历所有阻塞的客户端,提升了效率。 - 发布/订阅频道队列:在发布/订阅模式中,Redis为每个频道维护了订阅者队列。 - 多个队列的好处: - **提高效率**:避免在全局阻塞队列中遍历所有阻塞的客户端,通过key的阻塞队列可以快速定位需要唤醒的客户端。 - **功能分离**:不同类型的操作(如命令执行、阻塞等待、发布订阅)使用不同的队列,逻辑更清晰。 - **公平性**:每个阻塞队列按照FIFO顺序处理,保证了客户端的公平性。 下面用一个例子说明阻塞队列的工作流程: - 假设有两个客户端A和B同时执行`BLPOP list1 0`(即无限等待list1的元素),此时list1为空。 - Redis会将A和B分别加入list1的阻塞队列(按请求顺序,假设A在前,B在后)。 - 当客户端C执行`LPUSH list1 value`时,Redis会将value放入list1,然后检查list1的阻塞队列,发现A在等待,于是唤醒A,并将value弹出返回给A。此时B仍然在阻塞队列中等待。 - 如果没有其他客户端再推入数据,B将一直阻塞直到超时或有新数据。 引用[4]中提到Redis的List虽然不支持消费组,但其阻塞特性可以实现轻量级消息队列。而Redis 5.0引入的Streams数据类型则提供了更完善的消息队列功能,支持消费组等特性。 总结: - Redis的阻塞操作通过为每个key维护阻塞队列来实现高效唤醒。 - 尽管Redis命令执行是全局串行的,但为了支持不同的功能(阻塞、发布订阅等)以及提高效率,内部使用了多种队列结构。 现在,根据用户的问题,我们给出以下回答: </think> ### Redis阻塞机制与队列结构解析 #### 一、Redis阻塞的含义 在Redis中,"阻塞"特指**客户端连接在特定命令执行期间被挂起等待**的现象,主要发生在两类场景: 1. **阻塞命令调用** 如`BRPOP`、`BLPOP`等命令会使客户端连接暂停执行,直到目标列表有新元素或超时[^4]: ```shell BRPOP order_queue 30 # 阻塞等待30秒 ``` 此时连接处于"休眠"状态,不占用CPU资源。 2. **全局命令队列等待** 所有Redis命令都进入**全局命令队列**(单线程处理队列),当队列积压时,后续命令需等待: $$ \text{命令延迟} = \text{队列长度} \times \text{平均命令处理时间} $$ > 阻塞 ≠ 性能问题:合理阻塞可降低轮询开销,如订单系统使用`BRPOP`比循环`RPOP`更高效[^4] #### 二、Key与队列的关系 Redis **没有为单个Key建立独立队列**,但存在三类核心队列结构: | 队列类型 | 存储位置 | 作用域 | 数据结构 | 特点 | |-------------------|--------------|--------------|-----------------|--------------------| | 全局命令队列 | 内存 | 实例级别 | 双向链表 | 所有命令必经此队列 | | 阻塞客户端队列 | 内存 | Key级别 | 哈希表+链表 | 按Key分组存储 | | 持久化缓冲队列 | AOF缓冲区 | 实例级别 | 内存缓冲区 | 写操作异步持久化 | **Key级阻塞队列实现机制**: 1. 使用哈希表存储阻塞Key与等待客户端的映射: ```c // Redis源码结构简化 dict *blocking_keys; // Key->客户端链表 ``` 2. 当客户端执行阻塞命令时: ```mermaid graph TD A[客户端调用BRPOP list1] --> B{list1是否为空?} B -->|是| C[将客户端加入blocking_keys[list1]链表] B -->|否| D[立即返回数据] ``` #### 三、串行化Redis使用多队列的原因 尽管Redis命令处理是单线程串行的,但多队列设计解决了关键问题: 1. **解耦读写性能** - **写入队列**:主线程快速接收命令存入内存队列 - **处理队列**:单线程顺序执行命令 $$ \text{吞吐量} = \frac{1}{\text{平均命令耗时}} $$ - **阻塞队列**:独立存储休眠连接,避免遍历主队列 2. **事件优先级管理** Redis使用**多路复用器**(如epoll)管理三类事件: ```c while(true) { 处理已就绪的文件事件(网络请求); 处理时间事件(定时任务); 处理阻塞队列就绪事件; } ``` 3. **避免全局遍历** 当新元素推入列表时,只需唤醒对应Key的阻塞队列: ```mermaid graph LR LPUSH[LPUSH list1] --> 查找blocking_keys[list1] --> 唤醒队列首个客户端 --> 从全局队列移除 ``` 时间复杂度:$O(1)$(无需遍历所有连接) #### 四、队列交互示例 秒杀场景下的队列协作: ```mermaid sequenceDiagram participant C as 客户端 participant GQ as 全局命令队列 participant BK as 阻塞Key队列 participant DB as 数据库 C->>GQ: DECR stock_001 (库存扣减) GQ-->>C: 返回库存值 alt 库存>0 C->>GQ: LPUSH order_queue C->>BK: BRPOP order_queue BK-->>C: 30秒后超时 else 库存=0 C->>DB: 异步更新数据库[^1] end ``` > 这种设计保证单线程下: > - 命令执行的原子性 > - 阻塞连接的高效唤醒 > - 10万+ QPS的处理能力 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值