操作set类型value的redis命令主要包括sadd,srem,sismember,sintersect和smembers.
对于set类型的value, 从宏观上来看,存在2层哈希表结构,第一层哈希表是客户端对象的字段dict
指向的哈希表,第二层哈希表是第一层哈希表中的节点,该节点管理的robj
对象的类型是类型REDIS_SET
.
redis sadd命令
sadd命令格式为sadd key member
, 其含义是将member存入哈希表中,这里的哈希表即前文所说的第二层哈希表.telnet的模拟操作为:
telnet 10.7.7.132 6379
Trying 10.7.7.132...
Connected to 10.7.7.132.
Escape character is '^]'.
sadd mykey 7
member1
+OK
sadd命令对应的处理函数为saddCommand
, 其实现为(redis.c):
1896 static void saddCommand(redisClient *c) {
1897 dictEntry *de;
1898 robj *set, *ele;
1899
1900 de = dictFind(c->dict,c->argv[1]);
1901 if (de == NULL) {
1902 set = createSetObject();
1903 dictAdd(c->dict,c->argv[1],set);
1904 c->argv[1] = 0;
1905 } else {
1906 set = dictGetEntryVal(de);
1907 if (set->type != REDIS_SET) {
1908 addReplySds(c,
1909 sdsnew("-ERR SADD against key not holding a set value\r\n"));
1910 return;
1911 }
1912 }
1913 ele = createObject(REDIS_STRING, c->argv[2]);
1914 if (dictAdd(set->ptr,ele,NULL) == DICT_OK) {
1915 server.dirty++;
1916 } else {
1917 decrRefCount(ele);
1918 }
1919 c->argv[2] = NULL;
1920 addReply(c,shared.ok);
1921 }
Line1900在数据库中查找包含指定key的哈希节点.Line1901:1904如果没有找到指定的节点,调用函数createSetObject
创建一个类型robj
对象,其类型为REDIS_SET
.函数的实现为(redis.c):
1017 static robj *createSetObject(void) {
1018 dict *d = dictCreate(&setDictType,NULL);
1019 if (!d) oom("dictCreate");
1020 return createObject(REDIS_SET,d);
1021 }
该函数先创建类型dict
对象,再创建类型robj
对象,封装创建的dict
对象.
回到函数saddCommand
中,Line1903将添加这个新节点到哈希表数据库中.这个哈希节点的value又是个dict
类型,并且其中是空的.Line1913:1918将member封装为REDIS_STRING
类型的robj
对象,并且添加到第二层哈希表结构中,注意对于第二层哈希表结构,只需关注其key即可,也就是对应命令格式中的member,所以Line1914调用函数dictAdd
时,第三个参数可以传入NULL.如果Line1900查询的哈希节点存在,Line1906:1911只需获得该哈希节点的value即可,然后将member添加到第二层哈希表中.Line1920向客户端返回操作成功提示字符串.
redis scard命令
redis scard命令的格式为scard key
, 该命令返回该key对应的哈希表中的member个数.telnet模拟操作为:
telnet 10.7.7.132 6379
Trying 10.7.7.132...
Connected to 10.7.7.132.
Escape character is '^]'.
sadd mykey 7
member1
+OK
scard mykey
1
scard命令对应的处理函数为scardCommand
,其实现为(redis.c):
1971 static void scardCommand(redisClient *c) {
1972 dictEntry *de;
1973 dict *s;
1974
1975 de = dictFind(c->dict,c->argv[1]);
1976 if (de == NULL) {
1977 addReply(c,shared.zero);
1978 return;
1979 } else {
1980 robj *o = dictGetEntryVal(de);
1981 if (o->type != REDIS_SET) {
1982 addReplySds(c,sdsnew("-1\r\n"));
1983 } else {
1984 s = o->ptr;
1985 addReplySds(c,sdscatprintf(sdsempty(),"%d\r\n",
1986 dictGetHashTableUsed(s)));
1987 }
1988 }
1989 }
Line1975在数据库中查找包含key的哈希节点.Line1976:1978如果没有找到该节点,向客户端返回0.如果找到该哈希节点,但是robj
对象类型错误,向客户端返回-1,否则向客户端返回第二层哈希表中存在的节点数量.
redis srem命令
redis srem命令的格式为srem key member
, 该命令的含义是将member从key对应的哈希表中删除.telnet的模拟操作为:
telnet 10.7.7.132 6379
Trying 10.7.7.132...
Connected to 10.7.7.132.
Escape character is '^]'.
sadd mykey 7
member1
+OK
srem mykey 7
member1
+OK
scard mykey
0
srem命令的对应处理函数为sremCommand
,其实现为(redis.c):
1923 static void sremCommand(redisClient *c) {
1924 dictEntry *de;
1925
1926 de = dictFind(c->dict,c->argv[1]);
1927 if (de == NULL) {
1928 addReplySds(c,sdsnew("-ERR no such key\r\n"));
1929 } else {
1930 robj *set, *ele;
1931
1932 set = dictGetEntryVal(de);
1933 if (set->type != REDIS_SET) {
1934 addReplySds(c,
1935 sdsnew("-ERR SDEL against key not holding a set value\r\n"));
1936 return;
1937 }
1938 ele = createObject(REDIS_STRING,c->argv[2]);
1939 if (dictDelete(set->ptr,ele) == DICT_OK)
1940 server.dirty++;
1941 ele->ptr = NULL;
1942 decrRefCount(ele);
1943 addReply(c,shared.ok);
1944 }
1945 }
Line1926在数据库中查找指定key的哈希节点.Line1927:1928如果指定的哈希节点不存在,向客户端返回错误提示字符串.Line1938创建一个robj
对象封装string类型的member,Line1939调用函数dictDelete
删除member,该函数的实现为(dict.c):
248 int dictDelete(dict *ht, const void *key) {
249 return dictGenericDelete(ht,key,0);
250 }
真正删除的逻辑在函数dictGenericDelete
中, 其的实现为(dict.c):
215 /* Search and remove an element */
216 static int dictGenericDelete(dict *ht, const void *key, int nofree)
217 {
218 unsigned int h;
219 dictEntry *he, *prevHe;
220
221 if (ht->size == 0)
222 return DICT_ERR;
223 h = dictHashKey(ht, key) & ht->sizemask;
224 he = ht->table[h];
225
226 prevHe = NULL;
227 while(he) {
228 if (dictCompareHashKeys(ht, key, he->key)) {
229 /* Unlink the element from the list */
230 if (prevHe)
231 prevHe->next = he->next;
232 else
233 ht->table[h] = he->next;
234 if (!nofree) {
235 dictFreeEntryKey(ht, he);
236 dictFreeEntryVal(ht, he);
237 }
238 _dictFree(he);
239 ht->used--;
240 return DICT_OK;
241 }
242 prevHe = he;
243 he = he->next;
244 }
245 return DICT_ERR; /* not found */
246 }
要删除的member其实是第二层哈希表结构中的key.Line221:222如果哈希表是空的,该函数直接返回.如果哈希表非空,Line223:224获得该key映射的哈希表的BUCKET索引,如果该索引指向的链表为空,说明待删除的member不在哈希表中,Line245返回未找到member.否则Line227:244便利该链表,找到member并删除.注意如果传入的参数nofree为0,表示哈希节点中的key和value需要在释放,Line235调用函数dictFreeEntryKey
释放哈希节点的key,这里的key就是member,哈希节点的key是类型REDIS_STRING
的robj
对象, 即类型sds
的对象,函数dictFreeEntryKey
其实调用的是sdsDictValDestructor
,该函数根据robj
的类型处理释放操作.Line236调用函数dictFreeEntryVal释放哈希节点的value,之前分析过,对于第二层的哈希表结构, 在添加节点时,其value是NULL, 所以value不需要释放,全局对象setDictType
是控制第二层哈希表节点操作的,其value destructor为NULL, 这是和添加value为NULL想对应的.
回到函数dictGenericDelete
, Line238:240释放哈希节点本身,并更新哈希表中节点的个数,然后返回.
回到函数sremCommand
, Line1940更新数据库更新次数,Line1941设置robj
对象的类型sds
字段指向NULL, 因为member已经在函数dictDelete
中释放.Line1942调用函数decrRefCount
将robj
对象记录到全局的空闲链表中,并释放其封装的对象,这里将导致调用库函数free
释放NULL, C语言中向free
函数传入NULL是可以的. 需要注意的是,Line1941的设置是有益的,否则会导致2次释放同一地址上的内存,反而可能有问题.最后Line1943向客户端返回操作成功提示字符串.
redis sismember命令
redis sismember命令的格式为sismember key member
,该命令的含义是判断么member是否在key所对应的哈希表中,如果存在返回1,如果不存在返回0,如果包含key的哈希表不存在或者其他错误,则返回-1.telnet的模拟操作为:
telnet 10.7.7.132 6379
Trying 10.7.7.132...
Connected to 10.7.7.132.
Escape character is '^]'.
sadd mykey 7
member1
+OK
sismember mykey 7
member1
1
sismember命令对应的处理函数为sismemberCommand
,其实现为(redis.c):
1947 static void sismemberCommand(redisClient *c) {
1948 dictEntry *de;
1949
1950 de = dictFind(c->dict,c->argv[1]);
1951 if (de == NULL) {
1952 addReplySds(c,sdsnew("-1\r\n"));
1953 } else {
1954 robj *set, *ele;
1955
1956 set = dictGetEntryVal(de);
1957 if (set->type != REDIS_SET) {
1958 addReplySds(c,sdsnew("-1\r\n"));
1959 return;
1960 }
1961 ele = createObject(REDIS_STRING,c->argv[2]);
1962 if (dictFind(set->ptr,ele))
1963 addReply(c,shared.one);
1964 else
1965 addReply(c,shared.zero);
1966 ele->ptr = NULL;
1967 decrRefCount(ele);
1968 }
1969 }
Line1950在数据库中查找包含指定key的哈希节点.Line1951:1952如果没有指定的节点,向客户端返回-1.Line1961:1965在哈希表中查找member,如果找到,则返回1, 如果没有找到则返回0.Line1967将动态创建的robj
对象记录到全局空闲链表中.
redis sinter命令
redis sinter命令的格式为sinter key1 key2 ... keyN
, 其含义是返回多个哈希表内容的交集.如果只指定了一个哈希表,则返回该哈希表的全部内容.telnet的模拟操作为:
telnet 10.7.7.132 6379
Trying 10.7.7.132...
Connected to 10.7.7.132.
Escape character is '^]'.
sadd key1 7
member1
+OK
sadd key1 7
member2
+OK
sadd key2 7
member2
+OK
sadd key2 7
member3
+OK
sinter key1 key2
1
7
member2
sinter命令对应的处理函数为sinterCommand
,其实现为(redis.c):
1997 static void sinterCommand(redisClient *c) {
1998 dict **dv = malloc(sizeof(dict*)*(c->argc-1));
1999 dictIterator *di;
2000 dictEntry *de;
2001 robj *lenobj;
2002 int j, cardinality = 0;
2003
2004 if (!dv) oom("sinterCommand");
2005 for (j = 0; j < c->argc-1; j++) {
2006 robj *setobj;
2007 dictEntry *de;
2008
2009 de = dictFind(c->dict,c->argv[j+1]);
2010 if (!de) {
2011 free(dv);
2012 char *err = "No such key";
2013 addReplySds(c, sdscatprintf(
2014 sdsempty(),"%d\r\n%s\r\n",-((int)strlen(err)),err));
2015 return;
2016 }
2017 setobj = dictGetEntryVal(de);
2018 if (setobj->type != REDIS_SET) {
2019 free(dv);
2020 char *err = "LINTER against key not holding a set value";
2021 addReplySds(c, sdscatprintf(
2022 sdsempty(),"%d\r\n%s\r\n",-((int)strlen(err)),err));
2023 return;
2024 }
2025 dv[j] = setobj->ptr;
2026 }
2027 /* Sort sets from the smallest to largest, this will improve our
2028 * algorithm's performace */
2029 qsort(dv,c->argc-1,sizeof(dict*),qsortCompareSetsByCardinality);
2030
2031 /* The first thing we should output is the total number of elements...
2032 * since this is a multi-bulk write, but at this stage we don't know
2033 * the intersection set size, so we use a trick, append an empty object
2034 * to the output list and save the pointer to later modify it with the
2035 * right length */
2036 lenobj = createObject(REDIS_STRING,NULL);
2037 addReply(c,lenobj);
2038 decrRefCount(lenobj);
2039
2040 /* Iterate all the elements of the first (smallest) set, and test
2041 * the element against all the other sets, if at least one set does
2042 * not include the element it is discarded */
2043 di = dictGetIterator(dv[0]);
2044 if (!di) oom("dictGetIterator");
2045
2046 while((de = dictNext(di)) != NULL) {
2047 robj *ele;
2048
2049 for (j = 1; j < c->argc-1; j++)
2050 if (dictFind(dv[j],dictGetEntryKey(de)) == NULL) break;
2051 if (j != c->argc-1)
2052 continue; /* at least one set don't contain the member */
2053 ele = dictGetEntryKey(de);
2054 addReplySds(c,sdscatprintf(sdsempty(),"%d\r\n",sdslen(ele->ptr)));
2055 addReply(c,ele);
2056 addReply(c,shared.crlf);
2057 cardinality++;
2058 }
2059 lenobj->ptr = sdscatprintf(sdsempty(),"%d\r\n",cardinality);
2060 dictReleaseIterator(di);
2061 free(dv);
2062 }
Line1998动态创建了一个类型dict的指针数组,数组元素的个数是传入的哈希表的个数.Line2005:2026判断传入的取交集的哈希表是否存在,如果有一个不存在,就可以直接返回了.并向客户端发送错误提示字符串.因为取交集操作,必须是操作的所有集合都存在才有意义.Line2017:2024如果类型错误,也直接返回,并向客户端发送错误提示字符串.sinter命令必须操作的是哈希表,不能是其他类型.Line2025将数组指针指向各个哈希表.Line2029将各个哈希表进行排序,排序的标准是以哈希表中的节点数量,排序函数qsortCompareSetsByCardinality
的实现为(redis.c):
1991 static int qsortCompareSetsByCardinality(const void *s1, const void *s2) {
1992 dict **d1 = (void*) s1, **d2 = (void*) s2;
1993
1994 return dictGetHashTableUsed(*d1)-dictGetHashTableUsed(*d2);
1995 }
之所以按照哈希表节点数量排序,是因为集合取交集后的结果,肯定小于等于其中最小的集合.
回到函数sinterCommand
, Line2031:2038的意思是说,sinter命令的结果是个bulk类型,结果的第一部分是个整数,表示交集中的节点个数,所以先向客户端响应链表中添加这个响应对象,但是这时候还没有计算出交集,不知道交集中节点个数,所以这时候响应对象中包含的节点个数为空,使用了一个局部对象lenobj
来引用这个响应对象,等到交集计算完毕,再更新这个响应对象.Line2040:2058计算哈希表交集,Line2043基于最小的哈希表(经过排序后,索引为0)创建一个dict
类型的遍历器.依次遍历该哈希表中的节点元素, 如果该节点在其他哈希表中也存在,则该节点属于交集中的节点,同时把该节点记录到客户端对象的响应链表中.Line2059更新交集中节点数量.Line2060:2061释放动态创建的遍历器和指针数组.
redis smembers命令
redis smembers命令的格式为smembers key
, 该命令返回指定key对应的哈希表中的所有元素.telnet模拟操作为:
telnet 10.7.7.132 6379
Trying 10.7.7.132...
Connected to 10.7.7.132.
Escape character is '^]'.
sadd mykey 7
member1
+OK
sadd mykey 7
member2
+OK
smembers mykey
2
7
member1
7
member2
smembers命令对应的处理函数为sinterCommand
,和sinter命令是相同的处理函数,因为smembers只传入一个哈希表参数,所以函数sinterCommand
会返回该哈希表中的所有元素.