Part 1 zset
Redis zset 是一个 sorted set 有序集合,其中的每一个元素都会关联一个 double 类型的 score,形成一个key-value pair。既然是集合,那个 key 是唯一的,但是score是可以重复的,但是score的类型必须是double,而key值的类型,必须是string。
命令
ZADD zset_name score member [score2 member2 ...]
这个与一般 k-v pair 存储结构的命令相比,略有不同,其中是 value 前而 key 值在后。如果member存在,该命令则会更新该member的score值
ZREM zset_name member [member2 ...]
删除元素
zset 作为一个集合,其亦支持集合的相关操作,如差交并等。
其他的一些命令,略。
zset 存储结构
ziplist ,前文在 set 中已经介绍了该种数据结构,此处略。
说明:
zset中的ziplist是有序的,按照元素 score 升序排列,对于插入操作,是会遍历的,复杂度是O(N)。
其他按照分数的相关操作,也并没有进行比如二分查找的处理,因为 ziplist 从逻辑上而言,是一个 linked list,难以二分查找,只能顺序查找,所以复杂度也都是O(N)级别(算法操作上,可以采用双游标往中间游,可以一定程度上的加快速度,仍旧是O(N)级别,zset中并没有这么干)。
分数相等的情况下,采用的是元素的key值按照字典序进行排列。
但是,如果是按照元素 key 值的相关操作,那就直接是O(N)级别,比如 ZREM 某个 member。
修改元素的 score, zset的内部实现,可想而知,为了保持 score 有序,那必须进行 delete + re-insert的操作。这样的操作的复杂的实际也是O(N)级别的。
SKIPLIST
zset 内部,对于一些比较简单的zset数据,一般默认采用 ziplist作为底层存储结构,但是对于复杂的,比如key值过长,数量过多,(参见redis配置),便会发送convert,zset内部的编码格式由ziplist转变为skiplist。如下图所示(图片截自Redisbook.com):
首先:左下角就是 zset的skiplist的数据结构:
struct zskiplistLevel
{
struct zskiplistNode *forward;
unsigned long span;
};
typedef struct zskiplistNode
{
sds ele;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel level[];
} zskiplistNode;
typedef struct zskiplist
{
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
level 表示所有节点中,level最高的那个节点的level值。
header: 其实是一个空节点,主要内容是level数组中各个节点指针。如上图所示的level为5的skiplist,header中的L5总是指向第一个level为5的那个元素,从最底层level 1层来看,skiplist其实就是一个单链表,redis中加入了backward指针,就变成了双链表。
各个节点:1. 分数升序,2. 分数相等的,采用ele字典序进行排列。
每个节点的 span 表示的是,跳跃的间隔节点个数。当前这个节点的level数组的该level元素,到forward中间间隔了几个元素,上图中曲线箭头上的数字就是span。
每一个节点,并没有一个单独的成员就记录level值,所以 skiplistLevel 数组,都是最大值的方式申请来的(默认64层,64个元素的数组)。遍历都是从 zskiplist 中的level开始由最高往最低遍历,所以,有些节点层数不高的时候,高层次的位置,指向的forward就是NULL。
另外,每一个节点的level都是随机来的,假设一个插入场景:分数1.5,如果是生成的level是4
那么这个 1.5 节点的 L4 会直接指向3.0的 L4,因为2.0节点的level只有L2,也可说,新增1.5节点的L4从前一个节点继承过来。L4类似,L2,L1同理。然后,前面1.0的节点的所有层的forward都指向了新增的1.5节点。
如果1.5节点层数是3层,那么前面一个1.0节点的L4中的forward的不会更新。
如果1.5节点层数是5层,那么前四层与前面类似,但是L5层,就会继承到1.0前面的有L5层的节点,就是header节点,所以新增的1.5的L5层就会直接指向3.0节点。原先header中的L5层的foward就会断开,指向新增的1.5节点的L5.
Part 2 pubsub & PIPE
Redis的发布订阅其实非常简单,如果熟悉 linux 中的socket tcp + nonblock + epoll,就可以了。
订阅者,需要注意的是,必须保持TCP的连接,redis中记录这个连接(封装为client然后添加CLIENT_PUBSUB flag)。在处理publish 命令的时候,就会遍历检查对应的client,然后逐一去回复。这个内部逻辑与 BLPOP之类的命令比较类似。无非就是缓存连接,有数据可写的时候,检查socket的可写状态,不可写,就注册写事件,挂callback,可写,就直接回复了。完全的nonblock+异步I/O。
PIPE也是类似的,redis的客户端命令主要分为两种类型,一种就是内部称之为multibuk的命令,一个称之为 INLINE的命令,multibulk的命令,用的其实就是epoll 的回射模型。而PIPE命令,如果是自实现客户端,推荐采用异步操作,采用epoll监听,一堆命令发送过去之后,有部分数据,那就回复部分数据,客户端这边进入可读状态,客户端读取我们需要的结果即可。