Redis扩展命令实现

上周,参加公司后端部门开发的分享。
在期间,该开发吐槽Redis好几个使用不便利的地方。

  1. Redis没有批量设置过期时间的命令
  2. incr 不存在的key的时候,并不会设置过期时间。导致持久化key的存在

作为客户端,解决办法只有使用lua脚本来进行实现:

思考

虽然说使用lua脚本也能够解决这样的问题,但是对用户体验不太友好,同时也增加了编码的复杂度。
而且这样的功能实现起来并不算复杂,为什么不可以在Redis服务端去实现这样的功能呢?

实现

说干咱就干。
利用周末的时间,简单的实现了其中的一个槽点功能:mexpire
可能会存在考虑欠缺的地方,但基本功能还是实现了的。

expire.c

expire.c主要是对过期管理的文件。
一开始只想实现mexpire功能,结果发现还有expiretapexpirepexpirate命令跟expire命令相近,并且底层实现都是同一个函数。
于是就一起实现了。

逻辑其实很简单,就是遍历传过来的参数。

  • 如果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
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
void mexpireGenericCommand(client *c, long long basetime, int unit){

if ((c->argc % 2) == 0){
addReplyErrorFormat(c, "wrong number of arguments for %s", c->cmd->name);
return;
}

int j;
robj *key, *param;
long long when;
int nums = 0;
for (j = 1; j < c->argc; j+=2){
key = c->argv[j];
param = c->argv[j+1];
if ((getLongLongFromObject(param, &when) != C_OK) || lookupKeyWrite(c->db,key) == NULL){
continue;
}

if (unit == UNIT_SECONDS) when *= 1000;
when += basetime;

nums++;
if (when <= mstime() && !server.loading && !server.masterhost) {
int deleted = server.lazyfree_lazy_expire ? dbAsyncDelete(c->db,key) :
dbSyncDelete(c->db,key);
serverAssertWithInfo(c,key,deleted);
server.dirty++;

signalModifiedKey(c->db,key);
notifyKeyspaceEvent(NOTIFY_GENERIC,"del",key,c->db->id);

} else {
setExpire(c,c->db,key,when);
signalModifiedKey(c->db,key);
notifyKeyspaceEvent(NOTIFY_GENERIC,"expire",key,c->db->id);
server.dirty++;
}
}
addReplyLongLong(c, nums);
}

void mexpireCommand(client *c){
mexpireGenericCommand(c, mstime(), UNIT_SECONDS);
}

void mexpireatCommand(client *c) {
mexpireGenericCommand(c,0,UNIT_SECONDS);
}

void mpexpireCommand(client *c) {
mexpireGenericCommand(c,mstime(),UNIT_MILLISECONDS);
}

void mpexpireatCommand(client *c) {
mexpireGenericCommand(c,0,UNIT_MILLISECONDS);
}

server.h

1
2
3
4
void mexpireCommand(client *c);
void mexpireatCommand(client *c);
void mpexpireCommand(client *c);
void mpexpireatCommand(client *c);

server.c

1
2
3
4
{"mexpire",mexpireCommand,-3, "write @keyspace",0,NULL,1,-1,2,0,0,0},
{"mexpireat",mexpireatCommand,-3,"write @keyspace", 0,NULL,1,-1,2,0,0,0},
{"mpexpire",mpexpireCommand,-3, "write @keyspace", 0,NULL,1,-1,2,0,0,0},
{"mpexpireat",mpexpireatCommand,-3, "write @keyspace", 0,NULL,1,-1,2,0,0,0},

这里有必要说明一下上面配置的解释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct redisCommand {
char *name;
redisCommandProc *proc;
int arity;
char *sflags; /* Flags as string representation, one char per flag. */
uint64_t 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;
int id; /* Command ID. This is a progressive ID starting from 0 that
is assigned at runtime, and is used in order to check
ACLs. A connection is able to execute a given command if
the user associated to the connection has this command
bit set in the bitmap of allowed commands. */
};

  • name:命令名称
  • function:指向函数
  • arity:参数限制
  • sflags:命令属性
  • flags:命令属性掩码,一般为0
  • get_keys_proc:在复杂参数下,指定那个才是真正的key。一般为NULL
  • first_key_index:第一个参数所在位置
  • last_key_index:最后一个参数所在位置
  • key_step:命令步长
  • microseconds:命令的度量项,由Redis来设置,并且总是初始化为0。
  • calls:命令的度量项,由Redis来设置,并且总是初始化为0。
  • id:命令的权限,由Redis来设置,并且总是初始化为0。

只需要修改以上几个文件然后启动就可以了。是不是很简单?
PS:前一篇文章中解决哨兵BUG《Redis哨兵client-reconfig-script脚本bug记录一则》时,调试就需要重新进行make && make install操作才行。

使用

命令使用,跟正常使用其他命令并没有什么太大区别。主要会跟mset命令使用比较相似。

issue

本来还想去github提提issue的,结果发现早就有人跟作者提过这些问题了。我还是too yong to simple呀。
但是本着学习的心态,还是提了个issue,问问作者为什么不去实现这些简单又好用的命令。https://github.com/antirez/redis/issues/7263
后面有时间还是会继续实现其他命令的。