KeyDB项目是redisfork的一个分支。众所周知,redis是一个单线程的kv内存存储系统,而KeyDB在100%兼容redisAPI的情况下,将redis改造为多线程。线程模型KeyDB将redis原有的主线程拆分为主线程和工作线程。每个工作线程都是一个io线程,负责监听端口、接受请求、读取数据和解析协议。如图:KeyDB使用了SO_REUSEPORT特性,多个线程可以绑定监听同一个端口。每个工作线程都有一个cpu-boundcore,使用SO_INCOMING_CPU特性读取数据,指定接收数据的cpu。解析协议后,每个线程都会对内存中的数据进行操作,使用一个全局锁来控制多线程对内存数据的访问。主线程其实就是一个工作线程,包括工作线程的工作内容,也包括只有主线程才能完成的工作内容。工作线程数组中下标0为主线程。主线程主要工作是实现serverCron,包括:处理统计客户端连接管理resize和resharddb数据处理aof复制任务redis主备同步集群模式连接管理所有连接管理在一个线程中完成。在KeyDB的设计中,每个工作线程负责一组链接,所有链接都插入到本线程的链接列表中进行维护。链接的产生、工作和销毁必须在同一个线程中。每个链接添加一个新的字段intiel;/*我们注册的事件循环索引*/用于指示该链接属于哪个线程接管。KeyDB维护了链接管理的三个关键数据结构:clients_pending_write:线程专有链表,维护了一个队列,用于向客户端链接同步发送数据clients_pending_asyncwrite:线程专有链表,维护了一个队列,用于向客户端链接异步发送数据clients_to_close:一个全局链表,维护需要异步关闭的客户链接,分为同步和异步两个队列,因为redis有一些联动API,比如pub/sub。pub之后,需要给sub的客户端发送消息。pub执行的线程和sub的client的线程不在同一个线程中,为了处理这种情况,KeyDB会需要将数据发送给这个线程以外的client,并维护在一个异步队列中。同步发送的逻辑比较简单,都是在这个线程中完成的。下图说明了如何同步向客户端发送数据:上面说了,一个链接的创建、接收、发送、释放必须在同一个线程中执行。异步发送涉及两个线程之间的交互。KeyDB通过管道分两个线程传输消息:intfdCmdWrite;//写入管道intfdCmdRead;//读取管道本地线程需要异步发送数据,首先检查客户端是否属于本地线程,非本地线程获取客户端ID的独占线程,然后发送发送AE_ASYNC_OP::CreateFileEvent的操作到专用线程,请求添加写套接字事件。专用线程在处理pipeline消息的时候,在write事件中加入相应的request,如图:有些redis请求关闭client,在link所在的线程上并没有完全关闭,所以一个全局的异步closelist是维护在这里。锁机制KeyDB实现了一种类似于spinlock的锁机制,称为fastlock。fastlock的主要数据结构有:structicket{uint16_tm_active;//unlock+1uint16_tm_avail;//lock+1};structfastlock{volatilestructticketm_ticket;volatileintm_pidOwner;//当前解锁的线程idvolatileintm_depth;//当前线程重复加锁的次数};使用原子操作__atomic_load_2、__atomic_fetch_add、__atomic_compare_exchange通过比较m_active=m_avail来判断是否可以获取到锁。fastlock提供了两种获取锁的方式:try_lock:一旦获取失败,直接返回lock:busywait,每隔1024*1024个busywait后使用sched_yield主动交出cpu,移到cpu任务的尾部等待执行.在KeyDB中结合try_lock和events,避免忙等待。每个客户都有一把专用锁。在读取客户端数据之前,它会先尝试锁定它。如果失败,它将退出。因为还没有读取到数据,所以可以在下一个epoll_wait处理事件循环中再次处理。Active-ReplicaKeyDB实现了多活机制。每个副本都可以设置为可写但不可只读,副本之间可以同步数据。主要特点是:每个replica都有一个uuidflag,用于去掉环复制和新增rreplayAPI,将增量命令打包成rreplay命令,以本地uuidkey,value加上timestamp版本号,作为冲突检查,如果本地有相同key且时间戳版本号大于同步数据,新写入会失败。当前时间戳左移20位,最后44位递增得到key的时间戳版本号。
