操作string类型value的redis协议

redis 命令不区分大小写,这里均以小写格式说明.

操作string类型value的redis命令主要包括set,get,setnx,incr和decr.
redis set命令
redis set命令的格式为set key value 命令字符串,key和value之间以空格分割, set命令是bulk类型的命令,其value部分由四部分组成,其二进制格式为bytes\r\nxxxxxx\r\n 其中bytes代表value字节数,xxxxxx代表实际的value字节,二者之间以\r\n分割,value最后部分以\r\n结束.

假设客户端已经通过如下telnet命令连接到了redis服务器:

telnet 10.7.7.132 6379
Trying 10.7.7.132...
Connected to 10.7.7.132.
Escape character is '^]'.

接着在控制终端继续输入redis set命令:

set mykey 7
myvalue
+OK

用telnet模拟向redis服务器发送set命令及其参数时, 回车会导致输入,因此value部分将以两部分(两行)输入,字节数和字节数据. “+OK”是redis服务器返回给客户端的响应字符串.当输入myvalue这个字符窜时, 服务端完成了set命令及其参数的所有解析,函数processCommand中将调用set命令处理函数setCommand,该函数的实现为(redis.c):

1337 static void setCommand(redisClient *c) {
1338     return setGenericCommand(c,0);
1339 }

调用函数setGenericCommand,第二个参数为0,表示如果数据库中已经存在该key,则对该key对应的value进行更新. redis还有一个setnx命令, 相当于SET if Not eXists,即数据库中不存在该key时,才进行设置,否则不进行任何操作.

函数setGenericCommand的实现为(redis.c):

1317 static void setGenericCommand(redisClient *c, int nx) {
1318     int retval;
1319     robj *o;
1320 
1321     o = createObject(REDIS_STRING,c->argv[2]);
1322     c->argv[2] = NULL;
1323     retval = dictAdd(c->dict,c->argv[1],o);
1324     if (retval == DICT_ERR) {
1325         if (!nx)
1326             dictReplace(c->dict,c->argv[1],o);
1327         else
1328             decrRefCount(o);
1329     } else {
1330         /* Now the key is in the hash entry, don't free it */
1331         c->argv[1] = NULL;
1332     }
1333     server.dirty++;
1334     addReply(c,shared.ok);
1335 }

客户端对象的argv数组中,argv[0]记录set命令字符串,argv[1]记录key的string,argv[2]记录value的string.Line1321调用函数createObject创建一个类型robj的对象, 来封装value的值.因为set命令的value是string类型,所以创建的robj对象的类型是REDIS_STRING. 函数createObject的实现为(redis.c):

 992 static robj *createObject(int type, void *ptr) {
 993     robj *o;
 994 
 995     if (listLength(server.objfreelist)) {
 996         listNode *head = listFirst(server.objfreelist);
 997         o = listNodeValue(head);
 998         listDelNode(server.objfreelist,head);
 999     } else {
1000         o = malloc(sizeof(*o));
1001     }
1002     if (!o) oom("createObject");
1003     o->type = type;
1004     o->ptr = ptr;
1005     o->refcount = 1;
1006     return o;
1007 }

Line995:1001判断全局的空闲robj链表是否为空,如果非空,则从链表头删除该对象,否则,动态创建一个robj对象.Line1003:1005设置该对象的类型,引用的value,初始的应用计数.Line1322将sds对象设置为NULL, 即不再指向value, 因为value已经被robj对象所管理.调用函数dictAdd将key/value存储在内存数据库中.默认情况下,客户端对象的字段dict指向16个全局数据库中的第一个(索引为0).函数dictAdd的实现为(dict.c):

175 /* Add an element to the target hash table */
176 int dictAdd(dict *ht, void *key, void *val)
177 {
178     int index;
179     dictEntry *entry;
180 
181     /* Get the index of the new element, or -1 if
182      * the element already exists. */
183     if ((index = _dictKeyIndex(ht, key)) == -1)
184         return DICT_ERR;
185 
186     /* Allocates the memory and stores key */
187     entry = _dictAlloc(sizeof(*entry));
188     entry->next = ht->table[index];
189     ht->table[index] = entry;
190 
191     /* Set the hash entry fields. */
192     dictSetHashKey(ht, entry, key);
193     dictSetHashVal(ht, entry, val);
194     ht->used++;
195     return DICT_OK;
196 }

Line183调用函数_dictKeyIndex判断key在该内存数据库中是否存在,如果存在,返回-1,如果不存在则返回该key经哈希算法映射后对应的该数据库的BUCKET的索引.函数_dictKeyIndex的实现为(dict.c):

399 /* Returns the index of a free slot that can be populated with
400  * an hash entry for the given 'key'.
401  * If the key already exists, -1 is returned. */
402 static int _dictKeyIndex(dict *ht, const void *key)
403 {
404     unsigned int h;
405     dictEntry *he;
406 
407     /* Expand the hashtable if needed */
408     if (_dictExpandIfNeeded(ht) == DICT_ERR)
409         return -1;
410     /* Compute the key hash value */
411     h = dictHashKey(ht, key) & ht->sizemask;
412     /* Search if this slot does not already contain the given key */
413     he = ht->table[h];
414     while(he) {
415         if (dictCompareHashKeys(ht, key, he->key))
416             return -1;
417         he = he->next;
418     }
419     return h;
420 }

Line408调用函数_dictExpandIfNeeded尝试扩展内存数据库的容量, 因为redis的内存数据库在逻辑上设计为哈希表结构, 因此这里的容量指的是哈希表的BUCKET的大小.函数_dictExpandIfNeeded的实现为(dict.c):

373 /* Expand the hash table if needed */
374 static int _dictExpandIfNeeded(dict *ht)
375 {
376     /* If the hash table is empty expand it to the intial size,
377      * if the table is "full" dobule its size. */
378     if (ht->size == 0)
379         return dictExpand(ht, DICT_HT_INITIAL_SIZE);
380     if (ht->used == ht->size)
381         return dictExpand(ht, ht->size*2);
382     return DICT_OK;
383 }

如果数据库容量为0或者已经存储的节点个数达到了数据库容量,均需要扩展其容量.对于全局的16个数据库,在redis服务器实例启动后,如果没有从dump.rdb中装载key/value节点,则其数据库容量均为0.数据库容量为0时,将其容量扩展为DICT_HT_INITIAL_SIZE, 否则扩展为当前容量的2倍.需要注意的是函数dictExpand不一定只是扩展容量, 也可能是将容量调整小, 请参考之前定时事件逻辑的分析.

函数dictExpand的实现为(dict.c):

124 /* Expand or create the hashtable */
125 int dictExpand(dict *ht, unsigned int size)
126 {
127     dict n; /* the new hashtable */
128     unsigned int realsize = _dictNextPower(size), i;
129 
130     /* the size is invalid if it is smaller than the number of
131      * elements already inside the hashtable */
132     if (ht->used > size)
133         return DICT_ERR;
134 
135     _dictInit(&n, ht->type, ht->privdata);
136     n.size = realsize;
137     n.sizemask = realsize-1;
138     n.table = _dictAlloc(realsize*sizeof(dictEntry*));
139 
140     /* Initialize all the pointers to NULL */
141     memset(n.table, 0, realsize*sizeof(dictEntry*));
142 
143     /* Copy all the elements from the old to the new table:
144      * note that if the old hash table is empty ht->size is zero,
145      * so dictExpand just creates an hash table. */
146     n.used = ht->used;
147     for (i = 0; i < ht->size && ht->used > 0; i++) {
148         dictEntry *he, *nextHe;
149 
150         if (ht->table[i] == NULL) continue;
151 
152         /* For each hash entry on this slot... */
153         he = ht->table[i];
154         while(he) {
155             unsigned int h;
156 
157             nextHe = he->next;
158             /* Get the new element index */
159             h = dictHashKey(ht, he->key) & n.sizemask;
160             he->next = n.table[h];
161             n.table[h] = he;
162             ht->used--;
163             /* Pass to the next element */
164             he = nextHe;
165         }
166     }
167     assert(ht->used == 0);
168     _dictFree(ht->table);
169 
170     /* Remap the new hashtable in the old */
171     *ht = n;
172     return DICT_OK;
173 }

Line128调用函数_dictNextPower计算实际要扩展的容量,其值是2的幂,大于或者等于输入参数值.因为这个值是哈希表的BUCKET数目,所以存在一个最大值.函数_dictNextPower的实现为(dict.c):

385 /* Our hash table capability is a power of two */
386 static unsigned int _dictNextPower(unsigned int size)
387 {
388     unsigned int i = DICT_HT_INITIAL_SIZE;
389 
390     if (size >= 2147483648U)
391         return 2147483648U;
392     while(1) {
393         if (i >= size)
394             return i;
395         i *= 2;
396     }
397 }

回到函数dictExpand, Line132判断如果希望调整的容量小于当前存储的节点数,则直接返回,不做任何操作.Line135初始化类型dict的一个局部对象,相关调用函数实现包括.

函数_dictInit的实现为(dict.c):

103 /* Initialize the hash table */
104 int _dictInit(dict *ht, dictType *type,
105         void *privDataPtr)
106 {
107     _dictReset(ht);
108     ht->type = type;
109     ht->privdata = privDataPtr;
110     return DICT_OK;
111 }

函数_dictReset的实现为(dict.c):

85 static void _dictReset(dict *ht)
86 {
87     ht->table = NULL;
88     ht->size = 0;
89     ht->sizemask = 0;
90     ht->used = 0;
91 }
92 

Line136为该局部dict对象设置正确的BUCKET容量.Line137设置的掩码值,任意数值与该掩码值进行’&’操作,相当于对哈希表容量取模操作, 也就是获得一个BUCKET索引.Line138的逻辑是为字段table分配指针数组,注意这里的数组元素是指针.Line141初始化指针数组元素,使所有元素均指向NULL.Line147:166如果哈希表数据库之前存储有节点,需要将节点拷贝到新扩容的哈希表数据库中,因此需要遍历之前的哈希表数据库.Line150如果该BUCKET为空,则检查下一个索引BUCKET.Line159:161对节点重新计算哈希并映射到新的哈希表BUCKET索引上.函数dictHashKey就是类型dictType对象中的函数指针hashFunction,对应的函数实现为函数dictGenHashFunction(dict.c):

71 /* Generic hash function (a popular one from Bernstein).
72  * I tested a few and this was the best. */
73 unsigned int dictGenHashFunction(const unsigned char *buf, int len) {
74     unsigned int hash = 5381;
75 
76     while (len--)
77         hash = ((hash << 5) + hash) + (*buf++); /* hash * 33 + c */
78     return hash;
79 }

Line168将之前哈希表的指针数组的内存释放掉,因为这个数组是在堆上动态创建的.Line171将新扩展的哈希表赋值给客户端对象的dict字段,因为该字段是指向的全局的哈希表数据库,所以全局的哈希表相应的得到了更新.

回到函数_dictKeyIndex, Line411计算key在哈希表中的BUCKET索引,函数dictHashKey已经分析过.Line413:419如果该BUCKET中存在节点,则比较输入参数key和节点的key是否一致,如果找到一致key的节点,返回-1,否则返回BUCKET索引.

回到函数dictAdd, Line183如果返回-1,说明该节点已经存储在数据库中,否则,Line187:193将创建一个类型dictEntry的节点对象,将对节点对象插入到该BUCKET的链表头.

回到函数setGenericCommand,Line1324:1329说明该节点之前已经存在于数据库中了, 所以如果是set命令,调用dictReplace替换该节点的value值,如果是setnx命令,调用函数decrRefCount释放创建的robj对象.

函数dictReplace的实现为(dict.c):

198 /* Add an element, discarding the old if the key already exists */
199 int dictReplace(dict *ht, void *key, void *val)
200 {
201     dictEntry *entry;
202 
203     /* Try to add the element. If the key
204      * does not exists dictAdd will suceed. */
205     if (dictAdd(ht, key, val) == DICT_OK)
206         return DICT_OK;
207     /* It already exists, get the entry */
208     entry = dictFind(ht, key);
209     /* Free the old value and set the new one */
210     dictFreeEntryVal(ht, entry);
211     dictSetHashVal(ht, entry, val);
212     return DICT_OK;
213 }

Line205:206调用函数dictAdd将key/value存储到数据库,如果存储成功,说明该key对应的之前节点不存在,不需要替换其value,直接返回.Line207:211为value的替换处理, 首先通过key找到该dictEntry节点对象,然后释放旧的value值,并为该节点设置新的value值.

函数dictFind的实现为(dict.c):

289 dictEntry *dictFind(dict *ht, const void *key)
290 {
291     dictEntry *he;
292     unsigned int h;
293 
294     if (ht->size == 0) return NULL;
295     h = dictHashKey(ht, key) & ht->sizemask;
296     he = ht->table[h];
297     while(he) {
298         if (dictCompareHashKeys(ht, key, he->key))
299             return he;
300         he = he->next;
301     }
302     return NULL;
303 }

搜索节点的逻辑是, 通过哈希函数将key映射到数据库的BUCKET索引,在该BUCKET指向的链表中依次比较节点的key和待查找key,如果相等,返回该节点.如果没有找到匹配的节点,返回NULL.

函数dictFreeEntryVal释放value值,该函数(其实是个宏定义)对应类型dictType中的valDestructor指针,其指向的是函数sdsDictValDestructor, 该函数的实现为(redis.c):

395 static void sdsDictValDestructor(void *privdata, void *val)
396 {
397     DICT_NOTUSED(privdata);
398 
399     decrRefCount(val);
400 }

函数decrRefCount的实现为(redis.c):

1039 static void decrRefCount(void *obj) {
1040     robj *o = obj;
1041     if (--(o->refcount) == 0) {
1042         switch(o->type) {
1043         case REDIS_STRING: freeStringObject(o); break;
1044         case REDIS_LIST: freeListObject(o); break;
1045         case REDIS_SET: freeSetObject(o); break;
1046         default: assert(0 != 0); break;
1047         }
1048         if (!listAddNodeHead(server.objfreelist,o))
1049             free(o);
1050     }
1051 }

首先将类型robj对象引用计数减少一次,如果其值为0, 根据该对象的类型,释放该对象引用的value(由其字段ptr指向).这里先分析REDIS_STRING类型的robj对象,其他两种类型,在后续相应命令中再分析.调用函数freeStringObject 将释放string类型的value,其实现为(redis.c):

1023 static void freeStringObject(robj *o) {
1024     sdsfree(o->ptr);
1025 }

string类型的value由类型sds管理,所以需要释放sds对象.

回到函数decrRefCount, 释放完robj对象管理的value后,Line1048将robj对象插入到全局的空闲链表中.

回到函数dictReplace, Line211调用函数dictSetHashVal将新的value值赋值给节点对象.

回到函数setGenericCommand, set命令操作完成以后,更新全局的字段dirty,以反映内存数据更新次数.需要注意的是如果是setnx命令,可能内存数据并没有更新,所以这个操作并不严密,需要判断是否是setnx命令.Line1334调用函数addReply设置向客户端发送的响应内容.该函数的实现为(redis.c):

957 static void addReply(redisClient *c, robj *obj) {
958     if (listLength(c->reply) == 0 &&
959         aeCreateFileEvent(server.el, c->fd, AE_WRITABLE,
960         sendReplyToClient, c, NULL) == AE_ERR) return;
961     if (!listAddNodeTail(c->reply,obj)) oom("listAddNodeTail");
962     incrRefCount(obj);
963 }

如果客户端对象的响应链表为空,需要为客户端创建一个文件事件对象,监听其可写状态,其触发回调函数为sendReplyToClient.如果客户端对象的响应链表非空,说明已经创建了监听可写状态的文件事件对象.Line961将类型robj对象添加到响应链表尾部.函数setGenericCommand中的逻辑已经全部处理完毕,将返回到事件主循环,系统调用select将监听包括为该客户端创建的监听可写状态的文件事件对象,可写状态的文件事件对象在select系统调用时将立即触发,不会阻塞.函数sendReplyToClient被调用,想客户端发送命令响应.该函数的实现为(redis.c):

716 static void sendReplyToClient(aeEventLoop *el, int fd, void *privdata, int mask) {
717     redisClient *c = privdata;
718     int nwritten = 0, totwritten = 0, objlen;
719     robj *o;
720     REDIS_NOTUSED(el);
721     REDIS_NOTUSED(mask);
722 
723     while(listLength(c->reply)) {
724         o = listNodeValue(listFirst(c->reply));
725         objlen = sdslen(o->ptr);
726 
727         if (objlen == 0) {
728             listDelNode(c->reply,listFirst(c->reply));
729             continue;
730         }
731 
732         nwritten = write(fd, o->ptr+c->sentlen, objlen - c->sentlen);
733         if (nwritten <= 0) break;
734         c->sentlen += nwritten;
735         totwritten += nwritten;
736         /* If we fully sent the object on head go to the next one */
737         if (c->sentlen == objlen) {
738             listDelNode(c->reply,listFirst(c->reply));
739             c->sentlen = 0;
740         }
741     }
742     if (nwritten == -1) {
743         if (errno == EAGAIN) {
744             nwritten = 0;
745         } else {
746             redisLog(REDIS_DEBUG,
747                 "Error writing to client: %s", strerror(errno));
748             freeClient(c);
749             return;
750         }
751     }
752     if (totwritten > 0) c->lastinteraction = time(NULL);
753     if (listLength(c->reply) == 0) {
754         c->sentlen = 0;
755         aeDeleteFileEvent(server.el,c->fd,AE_WRITABLE);
756     }
757 }

当redis服务器向客户端发送命令响应时, 会把客户端对象的响应链表中的所有robj对象都发送给客户端.即一个响应内容可能是由多个robj对象组成的.注意响应链表上的robj对象都是string类型的,其内容都是存储在sds对象里面的.Line724:725从响应链表头获得robj对象,并获得sds中的数据长度.Line732:740系统调用write将数据写入socket,对于robj对象中的数据,可能需要多次write写入,因此客户端对象的字段sentlen记录累计写入的数据量,当一个robj中的数据全部写入socket,则将该robj对象从响应链表中删除,并将客户端对象的字段sentlen清零.接着发送响应链表中下一个robj对象中的数据.Line742:751系统调用write返回错误,如果错误码是EAGAIN, 该函数返回后, 重新进入主事件循环,客户端对象的监听可写状态的文件事件对象再次被触发,重新进入该函数,重试write操作.如果是其他不可重试错误,则释放客户端对象资源, 终止同客户端的连接.Line752如果系统调用write成功,更新交互时间戳.Line753:756如果客户端对象中响应链表中的内容全部写入,即链表为空,调用函数aeDeleteFileEvent删除该文件事件对象,即不再需要监听可写状态,只需继续监听可读状态,继续等待该客户端发送过来的redis命令.

函数aeDeleteFileEvent的实现为(ae.c):

56 void aeDeleteFileEvent(aeEventLoop *eventLoop, int fd, int mask)
57 {
58     aeFileEvent *fe, *prev = NULL;
59 
60     fe = eventLoop->fileEventHead;
61     while(fe) {
62         if (fe->fd == fd && fe->mask == mask) {
63             if (prev == NULL)
64                 eventLoop->fileEventHead = fe->next;
65             else
66                 prev->next = fe->next;
67             if (fe->finalizerProc)
68                 fe->finalizerProc(eventLoop, fe->clientData);
69             free(fe);
70             return;
71         }
72         prev = fe;
73         fe = fe->next;
74     }
75 }

在全局的文件事件对象链表中, 根据文件描述符和掩码查找指定的对象,找到之后,将其从链表中删除,并释放该文件事件对象的内存.

至此, 整个set命令的处理逻辑全部结束.其中主要步骤包括,监听客户端可读状态的文件事件被触发,从客户端socket中读取数据,解析命令及其参数,调用响应命令的处理函数,将set命令中的key/value写入内存数据库,为该客户端创建一个监听可写状态的文件事件,并把封装有响应数据的robj对象插入客户端对象的响应链表.返回主事件循环,监听可写状态的文件事件对象被触发,向客户端发送响应数据.其中有写处理步骤在处理redis其他命令时都是类似的.

redis setnx命令
setnx命令的格式为setnx key value, setnx和set的区别在于,当key已经存在于数据库中时, setnx命令什么也不做.代码中的处理差异在于函数setGenericCommand中,Line1325:1328如果是setnx命令,调用函数decrRefCount释放value中的数据,并不替换之前存在的value.

redis get命令
redis的get命令返回指定key的value值,其命令格式为get key. telnet模拟操作如下:

telnet 10.7.7.132 6379
Trying 10.7.7.132...
Connected to 10.7.7.132.
Escape character is '^]'.
set mykey 7
myvalue
+OK
get mykey
7
myvalue
get nonkey
nil

redis的get命令相应的处理函数为getCommand, 其实现为(redis.c):

1345 static void getCommand(redisClient *c) {
1346     dictEntry *de;
1347    
1348     de = dictFind(c->dict,c->argv[1]);
1349     if (de == NULL) {
1350         addReply(c,shared.nil);
1351     } else {
1352         robj *o = dictGetEntryVal(de);
1353 
1354         if (o->type != REDIS_STRING) {
1355             char *err = "GET against key not holding a string value";
1356             addReplySds(c,
1357                 sdscatprintf(sdsempty(),"%d\r\n%s\r\n",-((int)strlen(err)),err));
1358         } else {
1359             addReplySds(c,sdscatprintf(sdsempty(),"%d\r\n",(int)sdslen(o->ptr)));
1360             addReply(c,o);
1361             addReply(c,shared.crlf);
1362         }
1363     }
1364 }

Line1348在数据库中查找key对应的节点,如果没有找到包含该key的节点,则向客户端返回shared.nil对象,该robj对象包含字符串nil,表示不存在的节点.如果存在包含该key的节点,因为redis的get命令操作的value必须是string类型,Line1354:1358对此进行判断,如果类型错误,向客户端发送错误提示字符串,该字符串已一个负整数起始,其绝对值是Line1355错误提示字符串的字符数.Line1359:1361想客户端发送get命令的结果,即查询key对应的value.可以看到响应由三个robj对象构成,第一个robj对象包含value的字节数和\r\n两个字节,第二个robj对象是value数据,第三个robj对象是shared.crlf,也就是\r\n.

redis incr命令
incr命令将key所在节点的value值增加一次, 也说是说incr命令是计数用的,其value是个数值.incr命令的格式为incr key.如果是对指定key第一次执行incr命令,则将value数值设置为1.telnet的模拟操作为:

telnet 10.7.7.132 6379
Trying 10.7.7.132...
Connected to 10.7.7.132.
Escape character is '^]'.
incr mykey
1
incr mykey
2

incr命令的相应处理函数为incrCommand(redis.c):

1419 static void incrCommand(redisClient *c) {
1420     return incrDecrCommand(c,1);
1421 }

函数incrDecrCommand的实现为(redis.c):

1382 static void incrDecrCommand(redisClient *c, int incr) {
1383     dictEntry *de;
1384     sds newval;
1385     long long value;
1386     int retval;
1387     robj *o;
1388    
1389     de = dictFind(c->dict,c->argv[1]);
1390     if (de == NULL) {
1391         value = 0;
1392     } else {
1393         robj *o = dictGetEntryVal(de);
1394 
1395         if (o->type != REDIS_STRING) {
1396             value = 0;
1397         } else {
1398             char *eptr;
1399 
1400             value = strtoll(o->ptr, &eptr, 10);
1401         }
1402     }
1403 
1404     value += incr;
1405     newval = sdscatprintf(sdsempty(),"%lld",value);
1406     o = createObject(REDIS_STRING,newval);
1407     retval = dictAdd(c->dict,c->argv[1],o);
1408     if (retval == DICT_ERR) {
1409         dictReplace(c->dict,c->argv[1],o);
1410     } else {
1411         /* Now the key is in the hash entry, don't free it */
1412         c->argv[1] = NULL;
1413     }
1414     server.dirty++;
1415     addReply(c,o);
1416     addReply(c,shared.crlf);
1417 }

Line1389:1402查找包括该key的节点,如果找到该节点,获得该节点中存储的value数值,value在节点中是以字符串的形式存储的,因此需要调用strtoll转换为数值.如果没有找到该节点或者节点类型错误(非string类型),则将计数初始化为0.Line1404:1405更新计数值,并将数值以字符串的格式存储在sds对象中.对于incr命令,该函数的第二个参数传入的是1,是步长为1的递增.Line1407:1413创建一个robj对象封装sds对象,并存储在数据库中,如果对该key是第一次调用incr命令,函数dictAdd将返回DICT_OK, 否则DICT_ERR, 如果对该key不是第一次调用incr命令,通过调用函数dictReplace更新节点中value的数值.Line1415:1416向客户端返回更新后的value值.

redis decr命令
decr命令将key所在节点的value值减少一次, 也说是说decr命令是计数用的,其value是个数值.decr命令的格式为decr key.如果是对指定key第一次执行decr命令,则将value数值设置为-1用telnet模拟操作为:

telnet 10.7.7.132 6379
Trying 10.7.7.132...
Connected to 10.7.7.132.
Escape character is '^]'.
incr mykey
1
decr mykey
0

redis decr命令的相应处理函数为decrCommand,其实现为(redis.c):

1423 static void decrCommand(redisClient *c) {
1424     return incrDecrCommand(c,-1);
1425 }

其实现和incr命令的逻辑相同,只是以步长为-1,增加其value值.

猜你喜欢

转载自blog.csdn.net/azurelaker/article/details/81477206
今日推荐