Redis源码阅读笔记-各命令的执行(编写中)

由上两篇Redis源码阅读笔记-命令的接收和执行过程(一) Redis源码阅读笔记-命令的接收和执行过程(二) 可以知道,大部分命令是从server.c文件中的int processCommand(client *c)函数,通过lookupCommand()获的结构体为redisCommand的命令的。其实这是通过命令名在server.commands中来查看的。

Redis的命令列表是保存在全局变量server.commands中的,而其初始化是在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// server.c
void initServerConfig(void) {
......
server.commands = dictCreate(&commandTableDictType,NULL);
server.orig_commands = dictCreate(&commandTableDictType,NULL);
populateCommandTable();
server.delCommand = lookupCommandByCString("del");
server.multiCommand = lookupCommandByCString("multi");
server.lpushCommand = lookupCommandByCString("lpush");
server.lpopCommand = lookupCommandByCString("lpop");
server.rpopCommand = lookupCommandByCString("rpop");
server.sremCommand = lookupCommandByCString("srem");
server.execCommand = lookupCommandByCString("exec");
server.expireCommand = lookupCommandByCString("expire");
server.pexpireCommand = lookupCommandByCString("pexpire");
......
}

而其中,populateCommandTable()是给server.commandsserver.orig_commands初始化值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// server.c
/* Populates the Redis Command Table starting from the hard coded list
* we have on top of redis.c file. */
void populateCommandTable(void) {
int j;
int numcommands = sizeof(redisCommandTable)/sizeof(struct redisCommand);

for (j = 0; j < numcommands; j++) {
struct redisCommand *c = redisCommandTable+j;
char *f = c->sflags;
int retval1, retval2;

// 区分命令的类型
while(*f != '\0') {
switch(*f) {
case 'w': c->flags |= CMD_WRITE; break;
case 'r': c->flags |= CMD_READONLY; break;
case 'm': c->flags |= CMD_DENYOOM; break;
case 'a': c->flags |= CMD_ADMIN; break;
case 'p': c->flags |= CMD_PUBSUB; break;
case 's': c->flags |= CMD_NOSCRIPT; break;
case 'R': c->flags |= CMD_RANDOM; break;
case 'S': c->flags |= CMD_SORT_FOR_SCRIPT; break;
case 'l': c->flags |= CMD_LOADING; break;
case 't': c->flags |= CMD_STALE; break;
case 'M': c->flags |= CMD_SKIP_MONITOR; break;
case 'k': c->flags |= CMD_ASKING; break;
case 'F': c->flags |= CMD_FAST; break;
default: serverPanic("Unsupported command flag"); break;
}
f++;
}

retval1 = dictAdd(server.commands, sdsnew(c->name), c);
/* Populate an additional dictionary that will be unaffected
* by rename-command statements in redis.conf. */
retval2 = dictAdd(server.orig_commands, sdsnew(c->name), c);
serverAssert(retval1 == DICT_OK && retval2 == DICT_OK);
}
}

redisCommandTable则是在server.c文件中的一个全局变量,保存着命令字符串和操作函数。

redisCommand结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//server.h 

struct redisCommand {
char *name;
redisCommandProc *proc;
int arity;
char *sflags; /* Flags as string representation, one char per flag. */
int flags; /* The actual flags, obtained from the 'sflags' field. */
/* Use a function to determine keys arguments in a command line.
* Used for Redis Cluster redirect. */
redisGetKeysProc *getkeys_proc;
/* What keys should be loaded in background when calling this command? */
int firstkey; /* The first argument that's a key (0 = no keys) */
int lastkey; /* The last argument that's a key */
int keystep; /* The step between first and last key */
long long microseconds, calls;
};
  • name: 是命令的名字。
  • proc: 是命令的操作函数的指针,指向这个命令的具体操作。
  • arity: 命令所需参数的数量。
  • sflags: 命令标识的字符串形式。
    • w: 写命令。
    • r: 读命令。
    • m: 当调用时可能会增加内存的使用。(当超过内存限制时不允许执行)
    • a: 管理员权限的命令,etc: SAVESHUTDOWN
    • p: Pub/Sub相关的命令。
    • f: 无论server.dirty的值如何,都强制执行。
    • s: 不允许在Lua脚本中执行此函数。
    • R: 随机类型的命令,这表示命令是不幂等的。意思是,同一个命令,具有相同的参数和键空间,可能会有不同的结果。比如SPOPRANDOMKEY
    • S: 表示如果命令从脚本调用,会将输出的数组排序,使得结果具有幂等性。
    • l: 表示允许该命令在加载数据库时执行。
    • t: 表示允许命令在“从”服务器中有过时数据时执行。这个命令类型是很少的。
    • M: 表示命令不会自动在MONITOR上传播。
    • k: 表示会对该命令隐式得执行ASKING这会使得在集群模式下,slot会被标记为importing
    • F: 快速执行的命令(一般是时间复杂度为O(n)O(log(n))的命令)。只要内核调度给予时间则都不应该被延迟执行。PS:对于SET来说,由于可能会触发DEL命令,所以不是快速命令。
  • flags: 命令表示的比特位形式。
  • getkeys_proc: 指向一个帮助在命令行中获取”键”参数的函数。(可选的,仅当下面3个参数不足以指定哪些是“键”)。
  • firstkey: 表明第一个参数是“键”。
  • lastkey: 表明最后一个参数是”键”。
  • keystep: “键”参数的间隔数,比如MSET中参数形式为key, val, key, val

命令的执行

MODULE模块化命令

Reids 4.0 中加入的功能,可以通过外部模块对Redis进行功能扩展。

  • sflagsas,表示其为管理员权限的命令,而且不允许被Lua脚本调用。

命令形式

  • MODULE LOAD /path/to/mymodule.so表示加载模块。
  • MODULE LIST表示列出已经加载的模块。
  • MODULE UNLOAD mymodule表示卸载模块。

内部实现函数

打算迟点写一下module模块的实现,所以更详细的迟点再写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// module.c

/* Redis MODULE command.
*
* MODULE LOAD <path> [args...] */
void moduleCommand(client *c) {
char *subcmd = c->argv[1]->ptr;

if (!strcasecmp(subcmd,"load") && c->argc >= 3) {
// 调用 `MODULE LOAD /path/to/mymodule.so`
robj **argv = NULL;
int argc = 0;

if (c->argc > 3) {
argc = c->argc - 3;
argv = &c->argv[3];
}

// 真正加载模块的函数,该函数工作顺序:
// 1. 通过`dlopen()`加载动态链接库
// 2. 通过`dlsym()`获得 RedisModule_OnLoad 的加载函数 onload
// 3. 调用 onload 加载
// 4. 加载成功,以模块名字为Key,保存入全局字典变量modules中
if (moduleLoad(c->argv[2]->ptr,(void **)argv,argc) == C_OK)
addReply(c,shared.ok);
else
addReplyError(c,
"Error loading the extension. Please check the server logs.");
} else if (!strcasecmp(subcmd,"unload") && c->argc == 3) {
// 调用 `MODULE UNLOAD mymodule`
// 真正卸载模块的函数,该函数工作顺序:
// 1. 在注销模块注册的命令
// 2. 注销所有此模块的消息订阅
// 3. 通过`dlclose()`卸载模块
// 4. 在全局字典变量modules删除模块
if (moduleUnload(c->argv[2]->ptr) == C_OK)
addReply(c,shared.ok);
else {
char *errmsg;
switch(errno) {
case ENOENT:
errmsg = "no such module with that name";
break;
case EBUSY:
errmsg = "the module exports one or more module-side data types, can't unload";
break;
default:
errmsg = "operation not possible.";
break;
}
addReplyErrorFormat(c,"Error unloading module: %s",errmsg);
}
} else if (!strcasecmp(subcmd,"list") && c->argc == 2) {
// 调用`MODULE LIST`
dictIterator *di = dictGetIterator(modules);
dictEntry *de;

// 遍历全局字典表量modules, 获得其中的模块名
addReplyMultiBulkLen(c,dictSize(modules));
while ((de = dictNext(di)) != NULL) {
sds name = dictGetKey(de);
struct RedisModule *module = dictGetVal(de);
addReplyMultiBulkLen(c,4);
addReplyBulkCString(c,"name");
addReplyBulkCBuffer(c,name,sdslen(name));
addReplyBulkCString(c,"ver");
addReplyLongLong(c,module->ver);
}
dictReleaseIterator(di);
} else {
addReply(c,shared.syntaxerr);
}
}

GET命令

返回与键 key 相关联的字符串值。

如果键key不存在,那么返回特殊值nil;否则,返回键key的值。

如果键key的值并非字符串类型,那么返回一个错误,因为GET命令只能用于字符串值。

  • sflagsrF,表示是读命令,且快速执行,不应该被延迟。

命令形式

1
GET {key name}

内部实现函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// t_string.c
void getCommand(client *c) {
getGenericCommand(c);
}

int getGenericCommand(client *c) {
robj *o;

// c->argv[1]为命令中的 key,`shared.nullbulk`是返回的默认值 `nil`
if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk)) == NULL)
return C_OK;

// 判断返回的值得格式,只允许字符串格式
if (o->type != OBJ_STRING) {
addReply(c,shared.wrongtypeerr);
return C_ERR;
} else {
addReplyBulk(c,o);
return C_OK;
}
}

// db.c
robj *lookupKeyReadOrReply(client *c, robj *key, robj *reply) {
robj *o = lookupKeyRead(c->db, key);
if (!o) addReply(c,reply);
return o;
}

调用流程:

  1. getCommand()在函数内调用getGenericCommand()
  2. getGenericCommand(),在函数内调用lookupKeyReadOrReply(),并设置null为默认值。
  3. lookupKeyReadOrReply()client->db中查找key,如果没有找到就返回默认值。

SET命令

将字符串值 value 关联到 key

如果 key 已经持有其他值, SET 就覆写旧值, 无视类型。

SET 命令对一个带有生存时间(TTL)的键进行设置之后, 该键原有的 TTL 将被清除。

  • sflagswm,写命令,并可能导致增加内存的使用。

命令形式

1
SET key value [NX] [XX] [EX <seconds>] [PX <milliseconds>]
  • EX seconds : 将键的过期时间设置为 seconds 秒。 执行 SET key value EX seconds 的效果等同于执行 SETEX key seconds value
  • PX milliseconds : 将键的过期时间设置为 milliseconds 毫秒。 执行 SET key value PX milliseconds 的效果等同于执行 PSETEX key milliseconds value
  • NX : 只在键不存在时, 才对键进行设置操作。 执行 SET key value NX 的效果等同于执行 SETNX key value
  • XX : 只在键已经存在时, 才对键进行设置操作。

内部实现函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
// t_string.c

/* SET key value [NX] [XX] [EX <seconds>] [PX <milliseconds>] */
void setCommand(client *c) {
int j;
robj *expire = NULL;
int unit = UNIT_SECONDS;
int flags = OBJ_SET_NO_FLAGS;

// 判断是否有 `EX` `PX` `NX` `XX`
// 并将其标志入flags中
// expire是保存过期时间
// unit 是保存过期时间的单位
for (j = 3; j < c->argc; j++) {
char *a = c->argv[j]->ptr;
robj *next = (j == c->argc-1) ? NULL : c->argv[j+1];

if ((a[0] == 'n' || a[0] == 'N') &&
(a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
!(flags & OBJ_SET_XX))
{
flags |= OBJ_SET_NX;
} else if ((a[0] == 'x' || a[0] == 'X') &&
(a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
!(flags & OBJ_SET_NX))
{
flags |= OBJ_SET_XX;
} else if ((a[0] == 'e' || a[0] == 'E') &&
(a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
!(flags & OBJ_SET_PX) && next)
{
flags |= OBJ_SET_EX;
unit = UNIT_SECONDS;
expire = next;
j++;
} else if ((a[0] == 'p' || a[0] == 'P') &&
(a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
!(flags & OBJ_SET_EX) && next)
{
flags |= OBJ_SET_PX;
unit = UNIT_MILLISECONDS;
expire = next;
j++;
} else {
addReply(c,shared.syntaxerr);
return;
}
}

// 将 value 编码
c->argv[2] = tryObjectEncoding(c->argv[2]);
setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);
}

void setGenericCommand(client *c, int flags, robj *key, robj *val, robj *expire, int unit, robj *ok_reply, robj *abort_reply) {
long long milliseconds = 0; /* initialized to avoid any harmness warning */

// 判断是否需要设置过期时间
if (expire) {
// 如果是需要设置过期时间,
// 则从expire中获取,
// 并将其转成 long long 类型保存入变量milliseconds中
if (getLongLongFromObjectOrReply(c, expire, &milliseconds, NULL) != C_OK)
return;
if (milliseconds <= 0) {
addReplyErrorFormat(c,"invalid expire time in %s",c->cmd->name);
return;
}
// 如果传入的单位是秒,则需要将变量milliseconds中的值转成毫秒
if (unit == UNIT_SECONDS) milliseconds *= 1000;
}

// 满足条件
// 1. 带NX参数,且DB中的key已经存在
// 或
// 2. 带XX参数,且DB中的key不存在
// 则返回空
if ((flags & OBJ_SET_NX && lookupKeyWrite(c->db,key) != NULL) ||
(flags & OBJ_SET_XX && lookupKeyWrite(c->db,key) == NULL))
{
addReply(c, abort_reply ? abort_reply : shared.nullbulk);
return;
}

// 将键值对添加进c->db中
setKey(c->db,key,val);
server.dirty++;
// 如果有过期时间,则会将过期时间和键值,保存到`c->db->expires`中
if (expire) setExpire(c,c->db,key,mstime()+milliseconds);
// 触发键空间的事件
notifyKeyspaceEvent(NOTIFY_STRING,"set",key,c->db->id);
if (expire) notifyKeyspaceEvent(NOTIFY_GENERIC,
"expire",key,c->db->id);
// 返回响应
addReply(c, ok_reply ? ok_reply : shared.ok);
}

SETNX命令

只在键 key 不存在的情况下, 将键 key 的值设置为 value

若键 key 已经存在, 则 SETNX 命令不做任何动作。

SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。

  • sflagswmF,写命令,并可能导致增加内存的使用。快速执行,不应该被延迟。

PS: 可以用来实现分布式锁。

命令形式

1
SETNX key value

内部实现函数

1
2
3
4
5
// t_string.c
void setnxCommand(client *c) {
c->argv[2] = tryObjectEncoding(c->argv[2]);
setGenericCommand(c,OBJ_SET_NX,c->argv[1],c->argv[2],NULL,0,shared.cone,shared.czero);
}

实际上是调用setGenericCommand(),详见SET的代码解析。

SETEX命令

将键 key 的值设置为 value , 并将键 key 的生存时间设置为 seconds 秒钟。

如果键 key 已经存在, 那么 SETEX 命令将覆盖已有的值。

  • sflagswm,写命令,并可能导致增加内存的使用。

命令形式

1
SETEX key value seconds

内部实现函数

1
2
3
4
5
// t_string.c
void setexCommand(client *c) {
c->argv[3] = tryObjectEncoding(c->argv[3]);
setGenericCommand(c,OBJ_SET_NO_FLAGS,c->argv[1],c->argv[3],c->argv[2],UNIT_SECONDS,NULL,NULL);
}

实际上是调用setGenericCommand(),详见SET的代码解析。

PSETEX命令

SETEX 类似,差别在时间单位是毫秒。

  • sflagswm,写命令,并可能导致增加内存的使用。

命令形式

1
SETEX key value milliseconds

内部实现函数

1
2
3
4
5
// t_string.c
void psetexCommand(client *c) {
c->argv[3] = tryObjectEncoding(c->argv[3]);
setGenericCommand(c,OBJ_SET_NO_FLAGS,c->argv[1],c->argv[3],c->argv[2],UNIT_MILLISECONDS,NULL,NULL);
}

实际上是调用setGenericCommand(),详见SET的代码解析。

APPEND命令

如果键 key 已经存在并且它的值是一个字符串, APPEND 命令将把 value 追加到键 key 现有值的末尾。

如果 key 不存在, APPEND 就简单地将键 key 的值设为 value , 就像执行 SET key value 一样。

  • sflagswm,写命令,并可能导致增加内存的使用。

命令形式

1
APPEND key value

内部实现函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// t_string.c
void appendCommand(client *c) {
// totlen 是返回的新值的长度
size_t totlen;
robj *o, *append;


// 根据键获取值
// lookupKeyWrite() 会检查键是否已经过期
o = lookupKeyWrite(c->db,c->argv[1]);
if (o == NULL) {
// 没有获取到对应键的值,则创建一个新键值对
/* Create the key */
c->argv[2] = tryObjectEncoding(c->argv[2]);
dbAdd(c->db,c->argv[1],c->argv[2]);
incrRefCount(c->argv[2]);
totlen = stringObjectLen(c->argv[2]);
} else {
/* Key exists, check type */
// 键存在,并检查值得类型
if (checkType(c,o,OBJ_STRING))
return;

/* "append" is an argument, so always an sds */
append = c->argv[2];
// 计算新字符串的长度值,并检查是否超过最大限制,默认为512MB
totlen = stringObjectLen(o)+sdslen(append->ptr);
if (checkStringLength(c,totlen) != C_OK)
return;

/* Append the value */
// 调用dbUnshareStringValue()
// dbUnshareStringValue() 会检查字符串的引用,
// 如果引用大于1,则函数会创建一个新的sds字符串返回,
// 并将引用减1
o = dbUnshareStringValue(c->db,c->argv[1],o);
// 追加新值
o->ptr = sdscatlen(o->ptr,append->ptr,sdslen(append->ptr));
totlen = sdslen(o->ptr);
}
signalModifiedKey(c->db,c->argv[1]);
// 触发事件
notifyKeyspaceEvent(NOTIFY_STRING,"append",c->argv[1],c->db->id);
server.dirty++;
addReplyLongLong(c,totlen);
}

STRLEN命令

返回键 key 储存的字符串值的长度。

当键 key 不存在时, 命令返回 0

key 储存的不是字符串值时, 返回一个错误。

  • sflagsrF,只读,快速执行的命令,不应该被延迟。

命令形式

1
STRLEN key

内部实现函数

1
2
3
4
5
6
7
8
9
// t_string.c
void strlenCommand(client *c) {
robj *o;
// 使用key(c->argv[1])以只读模式获取值
if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.czero)) == NULL ||
checkType(c,o,OBJ_STRING)) return;
// stringObjectLen()是sds获取字符串长度的方法
addReplyLongLong(c,stringObjectLen(o));
}

DEL命令

删除给定的一个或多个 key

  • sflagsw,写命令。

命令形式

1
DEL key [key ...]

内部实现函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// db.c

void delCommand(client *c) {
// 0表示非延迟删除
delGenericCommand(c,0);
}

/* This command implements DEL and LAZYDEL. */
void delGenericCommand(client *c, int lazy) {
int numdel = 0, j;

// DEL 命令可以接受多个key
for (j = 1; j < c->argc; j++) {
// 检查键是否过期
expireIfNeeded(c->db,c->argv[j]);
// 通过lazy判断删除是否是延迟删除
// 如果是延迟删除,则会调用异步删除
// 如果不是,则会同步删除
int deleted = lazy ? dbAsyncDelete(c->db,c->argv[j]) :
dbSyncDelete(c->db,c->argv[j]);
if (deleted) {
signalModifiedKey(c->db,c->argv[j]);
notifyKeyspaceEvent(NOTIFY_GENERIC,
"del",c->argv[j],c->db->id);
server.dirty++;
numdel++;
}
}
addReplyLongLong(c,numdel);
}
  • 将删除操作,封装进一个BIO任务(多线程操作)中。在EventLoop的阻塞期间执行(可以参考[事件和事件循环](http://blog.mingforpc.me/2018/12/18/Redis%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB%E7%AC%94%E8%AE%B0-%E4%BA%8B%E4%BB%B6%E5%92%8C%E4%BA%8B%E4%BB%B6%E5%BE%AA%E7%8E%AF/))
    1
    2
    3
    4
    5
    6
    7
    8
    9
    * ```dbSyncDelete()```: 同步,直接`db`中删除键值对。

    ### `UNLINK`命令

    延迟删除给定的一个或多个 `key` 。4.0 中加入,解决大KEY删除的问题。

    * `sflags`:`wF`,写命令,快速执行,不应该被延迟。

    #### 命令形式

DEL key [key …]

1
2
3
4
5
6
7
8
9

#### 内部实现函数

```c
// db.c
void unlinkCommand(client *c) {
// 1 表示延迟删除
delGenericCommand(c,1);
}

详见DEL中的详解。

EXISTS命令

检查给定 key 是否存在。

  • sflagsrF,只读,快速执行命令,不应该被延迟。

命令形式

1
EXISTS key [key2... keyN]

内部实现函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// db.c
/* EXISTS key1 key2 ... key_N.
* Return value is the number of keys existing. */
void existsCommand(client *c) {
long long count = 0;
int j;

for (j = 1; j < c->argc; j++) {
// 在db(hash map)中检查key是否存在,
// 并且会检查key是否过期的
if (lookupKeyRead(c->db,c->argv[j])) count++;
}
addReplyLongLong(c,count);
}

SETBIT命令

key 所储存的字符串值,设置或清除指定偏移量上的位(bit)。

位的设置或清除取决于 value 参数,可以是 0 也可以是 1

key 不存在时,自动生成一个新的字符串值。

字符串会进行伸展(grown)以确保它可以将 value 保存在指定的偏移量上。当字符串值进行伸展时,空白位置以 0 填充。

offset 参数必须大于或等于 0 ,小于 2^32 (bit 映射被限制在 512 MB 之内)。

  • sflagswm,写命令,并且可能增加内存的使用。

命令形式

1
SETBIT key offset bitvalue

内部实现函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// bitops.c

/* SETBIT key offset bitvalue */
void setbitCommand(client *c) {
robj *o;
char *err = "bit is not an integer or out of range";
size_t bitoffset;
ssize_t byte, bit;
int byteval, bitval;
long on;

// 获取offset,并赋值到变量bitoffset中
if (getBitOffsetFromArgument(c,c->argv[2],&bitoffset,0,0) != C_OK)
return;

// 获取bitvalue,并赋值到变量on中
if (getLongFromObjectOrReply(c,c->argv[3],&on,err) != C_OK)
return;


/* Bits can only be set or cleared... */
// ~1 = 0xF...FFE
// 所以, 如果 on & ~1 > 0
// 表示 on 不等于(1或者0)
if (on & ~1) {
addReplyError(c,err);
return;
}
// 专门给位运算实现的函数
// 当 key 保存的值 不是 字符串类型,则会返回NULL
// 如果 key 不存在,则会生产指定长度butoffset的的字符串
// 如果 key 已经存在,且字符串长度不够,则会扩展至指定长度butoffset
if ((o = lookupStringForBitCommand(c,bitoffset)) == NULL) return;

/* Get current values */
// bitoffset >> 3 表示左移3位,既整除8,按照byte大小运算
byte = bitoffset >> 3;
byteval = ((uint8_t*)o->ptr)[byte];
bit = 7 - (bitoffset & 0x7);
// bitval 是原本的值
bitval = byteval & (1 << bit);

/* Update byte with new bit value and return original value */
byteval &= ~(1 << bit);
byteval |= ((on & 0x1) << bit);
// 赋新值
((uint8_t*)o->ptr)[byte] = byteval;
// 消息通知
signalModifiedKey(c->db,c->argv[1]);
notifyKeyspaceEvent(NOTIFY_STRING,"setbit",c->argv[1],c->db->id);
server.dirty++;
addReply(c, bitval ? shared.cone : shared.czero);
}

GETBIT命令

key 所储存的字符串值,获取指定偏移量上的位(bit)。

offset 比字符串值的长度大,或者 key 不存在时,返回 0 。

  • sflagsrF,只读,快速执行命令,不应该被延迟。

命令形式

1
GETBIT key offset

内部实现函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// bitops.c

/* GETBIT key offset */
void getbitCommand(client *c) {
robj *o;
char llbuf[32];
size_t bitoffset;
size_t byte, bit;
size_t bitval = 0;

// 获取offset,并赋值到变量bitoffset中
if (getBitOffsetFromArgument(c,c->argv[2],&bitoffset,0,0) != C_OK)
return;

// 获取值并检查是否是sds字符串格式
if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.czero)) == NULL ||
checkType(c,o,OBJ_STRING)) return;

// 按byte计算偏移量
byte = bitoffset >> 3;
bit = 7 - (bitoffset & 0x7);
// 判断值o的sds保存格式,因为sds会有字符串格式和整数格式
if (sdsEncodedObject(o)) {
// 字符串格式
if (byte < sdslen(o->ptr))

bitval = ((uint8_t*)o->ptr)[byte] & (1 << bit);
} else {
// 整数格式
// 通过ll2string()将long long格式转为字节大小的数值表示
if (byte < (size_t)ll2string(llbuf,sizeof(llbuf),(long)o->ptr))
bitval = llbuf[byte] & (1 << bit);
}

addReply(c, bitval ? shared.cone : shared.czero);
}

关于Redis中字符串的数据结构和编码可以看对象及其类型和编码

BITFIELD命令

BITFIELD 命令可以将一个 Redis 字符串看作是一个由二进制位组成的数组, 并对这个数组中储存的长度不同的整数进行访问 (被储存的整数无需进行对齐)。 换句话说, 通过这个命令, 用户可以执行诸如 “对偏移量 1234 上的 5 位长有符号整数进行设置”、 “获取偏移量 4567 上的 31 位长无符号整数”等操作。 此外, BITFIELD 命令还可以对指定的整数执行加法操作和减法操作, 并且这些操作可以通过设置妥善地处理计算时出现的溢出情况。

BITFIELD 命令可以在一次调用中同时对多个位范围进行操作: 它接受一系列待执行的操作作为参数, 并返回一个数组作为回复, 数组中的每个元素就是对应操作的执行结果。

  • sflagswm,写命令,并且可能增加内存的使用。

命令形式

1
BITFIELD key subcommmand-1 arg ... subcommand-2 arg ... subcommand-N ...

子命令支持:

1
2
3
4
GET <type> <offset>
SET <type> <offset> <value>
INCRBY <type> <offset> <increment>
OVERFLOW [WRAP|SAT|FAIL]

type为指定的二进制范围,i表示有符号整数,u表示无符号整数。例如u8表示8位长的无符号整数,i16表示16位长的有符号整数。

  • GET <type> <offset>: 返回指定的二进制位范围。
  • SET <type> <offset> <value>: 对指定的二进制位范围进行设置,并返回旧值。
  • INCRBY <type> <offset> <increment>: 对指定的二进制位范围进行加法操作,并返回旧值。
  • OVERFLOW [WRAP|SAT|FAIL]: 溢出控制:
    • WRAP: 使用回绕(wrap around)方法处理有符号整数和无符号整数的溢出情况。 对于无符号整数来说, 回绕就像使用数值本身与能够被储存的最大无符号整数执行取模计算, 这也是 C 语言的标准行为。 对于有符号整数来说, 上溢将导致数字重新从最小的负数开始计算, 而下溢将导致数字重新从最大的正数开始计算。 比如说, 如果我们对一个值为 127i8 整数执行加1操作, 那么将得到结果 -128
    • SAT: 使用饱和计算(saturation arithmetic)方法处理溢出, 也即是说, 下溢计算的结果为最小的整数值, 而上溢计算的结果为最大的整数值。 举个例子, 如果我们对一个值为 120i8 整数执行加 10 计算, 那么命令的结果将为 i8 类型所能储存的最大整数值 127 。 与此相反, 如果一个针对 i8 值的计算造成了下溢, 那么这个 i8 值将被设置为 -127
    • FAIL : 在这一模式下, 命令将拒绝执行那些会导致上溢或者下溢情况出现的计算, 并向用户返回空值表示计算未被执行。

内部实现函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
// bittops.c

/* BITFIELD key subcommmand-1 arg ... subcommand-2 arg ... subcommand-N ...
*
* Supported subcommands:
*
* GET <type> <offset>
* SET <type> <offset> <value>
* INCRBY <type> <offset> <increment>
* OVERFLOW [WRAP|SAT|FAIL]
*/

// 子命令的结构体
struct bitfieldOp {
uint64_t offset; /* Bitfield offset. */
int64_t i64; /* Increment amount (INCRBY) or SET value */
int opcode; /* Operation id. */
int owtype; /* Overflow type to use. */
int bits; /* Integer bitfield bits width. */
int sign; /* True if signed, otherwise unsigned op. */
};

void bitfieldCommand(client *c) {
robj *o;
size_t bitoffset;
int j, numops = 0, changes = 0;
// 保存操作的数组
struct bitfieldOp *ops = NULL; /* Array of ops to execute at end. */
int owtype = BFOVERFLOW_WRAP; /* Overflow type. */
int readonly = 1;
size_t higest_write_offset = 0;

// 从命令中解析操作,保存到`ops`中
for (j = 2; j < c->argc; j++) {
// `remargs` 是用来判断参数是否足够的
int remargs = c->argc-j-1; /* Remaining args other than current. */
char *subcmd = c->argv[j]->ptr; /* Current command name. */
int opcode; /* Current operation code. */
long long i64 = 0; /* Signed SET value. */
int sign = 0; /* Signed or unsigned type? */
int bits = 0; /* Bitfield width in bits. */

if (!strcasecmp(subcmd,"get") && remargs >= 2)
// GET的子命令
opcode = BITFIELDOP_GET;
else if (!strcasecmp(subcmd,"set") && remargs >= 3)
// SET的子命令
opcode = BITFIELDOP_SET;
else if (!strcasecmp(subcmd,"incrby") && remargs >= 3)
// INCRBY的子命令
opcode = BITFIELDOP_INCRBY;
else if (!strcasecmp(subcmd,"overflow") && remargs >= 1) {
// 设置溢出处理
char *owtypename = c->argv[j+1]->ptr;
j++;
if (!strcasecmp(owtypename,"wrap"))
owtype = BFOVERFLOW_WRAP;
else if (!strcasecmp(owtypename,"sat"))
owtype = BFOVERFLOW_SAT;
else if (!strcasecmp(owtypename,"fail"))
owtype = BFOVERFLOW_FAIL;
else {
addReplyError(c,"Invalid OVERFLOW type specified");
zfree(ops);
return;
}
continue;
} else {
addReply(c,shared.syntaxerr);
zfree(ops);
return;
}

/* Get the type and offset arguments, common to all the ops. */
// getBitfieldTypeFromArgument() 获取命令行中的, `type`
// type 是有取值范围的
// 当是有符号整数时,支持 i1 < x < i64
// 当是无符号整数时,支持 u1 < x < u63
if (getBitfieldTypeFromArgument(c,c->argv[j+1],&sign,&bits) != C_OK) {
zfree(ops);
return;
}

// getBitOffsetFromArgument() 获取命令行中的, `offset`
// offset 存在大小限制,必须是 -1 < offset
// 而且,offset通过bit转换后,大小是要先知道 512*1024*1024 bytes(512MB)以内
if (getBitOffsetFromArgument(c,c->argv[j+2],&bitoffset,1,bits) != C_OK){
zfree(ops);
return;
}

// 除了 Get以外, SET 和 INCRBY 都有第3个参数
if (opcode != BITFIELDOP_GET) {
readonly = 0;
if (higest_write_offset < bitoffset + bits - 1)
higest_write_offset = bitoffset + bits - 1;
/* INCRBY and SET require another argument. */
if (getLongLongFromObjectOrReply(c,c->argv[j+3],&i64,NULL) != C_OK){
zfree(ops);
return;
}
}

/* Populate the array of operations we'll process. */
// 构成bitfieldOp结构体,保存入ops中
ops = zrealloc(ops,sizeof(*ops)*(numops+1));
ops[numops].offset = bitoffset;
ops[numops].i64 = i64;
ops[numops].opcode = opcode;
ops[numops].owtype = owtype;
ops[numops].bits = bits;
ops[numops].sign = sign;
numops++;

j += 3 - (opcode == BITFIELDOP_GET);
}

// readonly 标记为记录子命令中是否存在写的命令
if (readonly) {
/* Lookup for read is ok if key doesn't exit, but errors
* if it's not a string. */
o = lookupKeyRead(c->db,c->argv[1]);
if (o != NULL && checkType(c,o,OBJ_STRING)) return;
} else {
/* Lookup by making room up to the farest bit reached by
* this operation. */
if ((o = lookupStringForBitCommand(c,
higest_write_offset)) == NULL) return;
}

addReplyMultiBulkLen(c,numops);

/* Actually process the operations. */
for (j = 0; j < numops; j++) {
// 执行各个命令

struct bitfieldOp *thisop = ops+j;

/* Execute the operation. */
if (thisop->opcode == BITFIELDOP_SET ||
thisop->opcode == BITFIELDOP_INCRBY)
{
/* SET and INCRBY: We handle both with the same code path
* for simplicity. SET return value is the previous value so
* we need fetch & store as well. */

/* We need two different but very similar code paths for signed
* and unsigned operations, since the set of functions to get/set
* the integers and the used variables types are different. */
if (thisop->sign) {
// 有符号整数
int64_t oldval, newval, wrapped, retval;
int overflow;

// 获取旧值
oldval = getSignedBitfield(o->ptr,thisop->offset,
thisop->bits);

if (thisop->opcode == BITFIELDOP_INCRBY) {
// INCRBY
newval = oldval + thisop->i64;
// 检查Overflow
overflow = checkSignedBitfieldOverflow(oldval,
thisop->i64,thisop->bits,thisop->owtype,&wrapped);
if (overflow) newval = wrapped;
retval = newval;
} else {
// SET
newval = thisop->i64;
// 检查Overflow
overflow = checkSignedBitfieldOverflow(newval,
0,thisop->bits,thisop->owtype,&wrapped);
if (overflow) newval = wrapped;
retval = oldval;
}

/* On overflow of type is "FAIL", don't write and return
* NULL to signal the condition. */
if (!(overflow && thisop->owtype == BFOVERFLOW_FAIL)) {
addReplyLongLong(c,retval);
setSignedBitfield(o->ptr,thisop->offset,
thisop->bits,newval);
} else {
addReply(c,shared.nullbulk);
}
} else {
// 无符号整数
uint64_t oldval, newval, wrapped, retval;
int overflow;

// 获取旧值
oldval = getUnsignedBitfield(o->ptr,thisop->offset,
thisop->bits);

if (thisop->opcode == BITFIELDOP_INCRBY) {
// INCRBY
newval = oldval + thisop->i64;
overflow = checkUnsignedBitfieldOverflow(oldval,
thisop->i64,thisop->bits,thisop->owtype,&wrapped);
if (overflow) newval = wrapped;
retval = newval;
} else {
// SET
newval = thisop->i64;
overflow = checkUnsignedBitfieldOverflow(newval,
0,thisop->bits,thisop->owtype,&wrapped);
if (overflow) newval = wrapped;
retval = oldval;
}
/* On overflow of type is "FAIL", don't write and return
* NULL to signal the condition. */
if (!(overflow && thisop->owtype == BFOVERFLOW_FAIL)) {
addReplyLongLong(c,retval);
setUnsignedBitfield(o->ptr,thisop->offset,
thisop->bits,newval);
} else {
addReply(c,shared.nullbulk);
}
}
changes++;
} else {
/* GET */
unsigned char buf[9];
long strlen = 0;
unsigned char *src = NULL;
char llbuf[LONG_STR_SIZE];

if (o != NULL)
src = getObjectReadOnlyString(o,&strlen,llbuf);

/* For GET we use a trick: before executing the operation
* copy up to 9 bytes to a local buffer, so that we can easily
* execute up to 64 bit operations that are at actual string
* object boundaries. */
// 因为之前看,取值范围是有限制的,所以使用最多9个字节处理
memset(buf,0,9);
int i;
size_t byte = thisop->offset >> 3;
for (i = 0; i < 9; i++) {
if (src == NULL || i+byte >= (size_t)strlen) break;
buf[i] = src[i+byte];
}

/* Now operate on the copied buffer which is guaranteed
* to be zero-padded. */
if (thisop->sign) {
// 有符号整数
int64_t val = getSignedBitfield(buf,thisop->offset-(byte*8),
thisop->bits);
addReplyLongLong(c,val);
} else {
// 无符号整数
uint64_t val = getUnsignedBitfield(buf,thisop->offset-(byte*8),
thisop->bits);
addReplyLongLong(c,val);
}
}
}

if (changes) {
// 如果值改变了,则发送消息
signalModifiedKey(c->db,c->argv[1]);
notifyKeyspaceEvent(NOTIFY_STRING,"setbit",c->argv[1],c->db->id);
server.dirty += changes;
}
zfree(ops);
}

SETRANGE命令

从偏移量 offset 开始, 用 value 参数覆写(overwrite)键 key 储存的字符串值。

不存在的键 key 当作空白字符串处理。

SETRANGE 命令会确保字符串足够长以便将 value 设置到指定的偏移量上, 如果键 key 原来储存的字符串长度比偏移量小(比如字符串只有 5 个字符长,但你设置的 offset 是 10 ), 那么原字符和偏移量之间的空白将用零字节(zerobytes, "\x00" )进行填充。

  • sflagswm,写命令,并且可能增加内存的使用。

命令形式

1
SETRANGE key offset value

内部实现函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// t_string.c
void setrangeCommand(client *c) {
robj *o;
long offset;
sds value = c->argv[3]->ptr;

// 获取offset
if (getLongFromObjectOrReply(c,c->argv[2],&offset,NULL) != C_OK)
return;

// offset必须大于等于0
if (offset < 0) {
addReplyError(c,"offset is out of range");
return;
}

// 在db中根据键寻找值
o = lookupKeyWrite(c->db,c->argv[1]);
if (o == NULL) {
/* Return 0 when setting nothing on a non-existing string */
if (sdslen(value) == 0) {
// 当键的值不存在,且value为空,则直接返回0
addReply(c,shared.czero);
return;
}

/* Return when the resulting string exceeds allowed size */
// 判断设置偏移后,总长度是否大于512MB(由于这个键不存在,所以这里的其实检查了offset的大小)
if (checkStringLength(c,offset+sdslen(value)) != C_OK)
return;

// 直接创建一个sds字符串,并加入db
o = createObject(OBJ_STRING,sdsnewlen(NULL, offset+sdslen(value)));
dbAdd(c->db,c->argv[1],o);
} else {
size_t olen;

// 检查值得蕾西,需要时string
/* Key exists, check type */
if (checkType(c,o,OBJ_STRING))
return;

/* Return existing string length when setting nothing */
olen = stringObjectLen(o);
if (sdslen(value) == 0) {
// value为空,表示不设置值,直接返回
addReplyLongLong(c,olen);
return;
}

/* Return when the resulting string exceeds allowed size */
// 判断设置偏移后,总长度是否大于512MB
if (checkStringLength(c,offset+sdslen(value)) != C_OK)
return;

/* Create a copy when the object is shared or encoded. */
// 保存值,dbUnshareStringValue()会处理redisObject的引用问题
o = dbUnshareStringValue(c->db,c->argv[1],o);
}

if (sdslen(value) > 0) {
// 发小键值变化的消息
o->ptr = sdsgrowzero(o->ptr,offset+sdslen(value));
memcpy((char*)o->ptr+offset,value,sdslen(value));
signalModifiedKey(c->db,c->argv[1]);
notifyKeyspaceEvent(NOTIFY_STRING,
"setrange",c->argv[1],c->db->id);
server.dirty++;
}
addReplyLongLong(c,sdslen(o->ptr));
}

GETRANGE命令

返回键 key 储存的字符串值的指定部分, 字符串的截取范围由 startend 两个偏移量决定 (包括 startend 在内)。

负数偏移量表示从字符串的末尾开始计数, -1 表示最后一个字符, -2 表示倒数第二个字符, 以此类推。

GETRANGE 通过保证子字符串的值域(range)不超过实际字符串的值域来处理超出范围的值域请求。

  • sflagsr,只读命令。

命令形式

1
GETRANGE key start end

内部实现函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// t_string.c

void getrangeCommand(client *c) {
robj *o;
long long start, end;
char *str, llbuf[32];
size_t strlen;

// 从参数中获取 start
if (getLongLongFromObjectOrReply(c,c->argv[2],&start,NULL) != C_OK)
return;
// 从参数中获取 end
if (getLongLongFromObjectOrReply(c,c->argv[3],&end,NULL) != C_OK)
return;
// 根据 key 获取其值 o, 并检查数据类型是否为redis字符串类型
if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.emptybulk)) == NULL ||
checkType(c,o,OBJ_STRING)) return;

// 如果字符串中保存的是整型,则转成字符串形式
if (o->encoding == OBJ_ENCODING_INT) {
str = llbuf;
strlen = ll2string(llbuf,sizeof(llbuf),(long)o->ptr);
} else {
str = o->ptr;
strlen = sdslen(str);
}

// 对不正确的start,end进行处理
/* Convert negative indexes */
if (start < 0 && end < 0 && start > end) {
addReply(c,shared.emptybulk);
return;
}
// 对负数的(start, end)和越界的end进行处理
if (start < 0) start = strlen+start;
if (end < 0) end = strlen+end;
if (start < 0) start = 0;
if (end < 0) end = 0;
if ((unsigned long long)end >= strlen) end = strlen-1;

/* Precondition: end >= 0 && end < strlen, so the only condition where
* nothing can be returned is: start > end. */
if (start > end || strlen == 0) {
// start > end 的错误情况
// strlen == 0 的情况(PS:为什么这里不提前判断?提前判断了就不用做start和end的运算了。不过可能考虑的是代码的可读性,毕竟start和end的运行应该也花不了多少时间)
addReply(c,shared.emptybulk);
} else {
addReplyBulkCBuffer(c,(char*)str+start,end-start+1);
}
}

SUBSTR命令

在Redis 2.0 之前,SUBSTR其实是GETRANGE。现在的实现也是一样的,所以参照GETRANGE

  • sflagsr,只读命令。

INCR命令

为键 key 储存的数字值加上1

如果键 key 不存在, 那么它的值会先被初始化为 0 , 然后再执行 INCR 命令。

如果键 key 储存的值不能被解释为数字, 那么 INCR 命令将返回一个错误。

本操作的值限制在 64 位(bit)有符号数字表示之内。

  • sflagswmF,写命令,并可能导致增加内存的使用。快速执行,不应该被延迟。

命令形式

1
INCR key

内部实现函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// t_string.c
void incrCommand(client *c) {
// 其实`INCR`,`INCRBY`,`DECR`,`DECRBY`都是调用的incrDecrCommand()函数
incrDecrCommand(c,1);
}

void incrDecrCommand(client *c, long long incr) {
long long value, oldvalue;
robj *o, *new;

// 查询键对应的值o
o = lookupKeyWrite(c->db,c->argv[1]);
// 检查值o的类型
if (o != NULL && checkType(c,o,OBJ_STRING)) return;
// 从o中获取整数值value(可能是sds的)
if (getLongLongFromObjectOrReply(c,o,&value,NULL) != C_OK) return;

// 判断相加后,是否会造成越界,不能超过LLONG_MIN,也不能小于LLONG_MAX
oldvalue = value;
if ((incr < 0 && oldvalue < 0 && incr < (LLONG_MIN-oldvalue)) ||
(incr > 0 && oldvalue > 0 && incr > (LLONG_MAX-oldvalue))) {
addReplyError(c,"increment or decrement would overflow");
return;
}
value += incr;

if (o && o->refcount == 1 && o->encoding == OBJ_ENCODING_INT &&
(value < 0 || value >= OBJ_SHARED_INTEGERS) &&
value >= LONG_MIN && value <= LONG_MAX)
{
// 这是判断 值 o 是存在的,而且不在 OBJ_SHARED_INTEGERS (Redis初始化的共享整数)范围内
// 而且没有其他键引用到o(o->refcount == 1)
// 所以可以直接修改o中的值
new = o;
o->ptr = (void*)((long)value);
} else {
// createStringObjectFromLongLong() 会判断值value是否在 OBJ_SHARED_INTEGERS 的范围内
// 如果在范围内,则直接引用共享的整数对象
// 如果不是,则创建新的对象
new = createStringObjectFromLongLong(value);
if (o) {
// 原本键值对已经存在,则覆盖
dbOverwrite(c->db,c->argv[1],new);
} else {
// 原本键值不存在,则添加
dbAdd(c->db,c->argv[1],new);
}
}
// 发送消息
signalModifiedKey(c->db,c->argv[1]);
notifyKeyspaceEvent(NOTIFY_STRING,"incrby",c->argv[1],c->db->id);
server.dirty++;
addReply(c,shared.colon);
addReply(c,new);
addReply(c,shared.crlf);
}

DECR命令

为键 key 储存的数字值减去1

如果键 key 不存在, 那么键 key 的值会先被初始化为 0 , 然后再执行 DECR 操作。

如果键 key 储存的值不能被解释为数字, 那么 DECR 命令将返回一个错误。

本操作的值限制在 64 位(bit)有符号数字表示之内。

  • sflagswmF,写命令,并可能导致增加内存的使用。快速执行,不应该被延迟。

命令形式

1
DECR key

内部实现函数

1
2
3
4
// t_string.c
void decrCommand(client *c) {
incrDecrCommand(c,-1);
}

incrDecrCommand()的解析参照INCR