一 序
上一篇按照书上的第9章的内容整理了redis数据库, 主要侧重于键值的过期及删除策略。限于偏于本篇整理数据的命令。
数据库管理的命令如下表所示:
命令 | 描述 |
FLUSHDB | 清空当前数据库的所有key |
FLUSHALL | 清空整个Redis服务器的所有key |
DBSIZE | 返回当前数据库的key的个数 |
DEL key [key …] | 删除一个或多个键 |
EXISTS key | 检查给定key是否存在 |
SELECT id | 切换到指定的数据库 |
RANDOMKEY | 从当前数据库中随机返回(不删除)一个 key 。 |
KEYS pattern | 查找所有符合给定模式pattern的key |
SCAN cursor [MATCH pattern] [COUNT count] | 增量式迭代当前数据库键 |
LASTSAVE | 返回最近一次成功将数据保存到磁盘上的时间,以 UNIX 时间戳格式表示。 |
TYPE key | 返回指定键的对象类型 |
SHUTDOWN | 停止所有客户端,关闭 redis 服务器(server) |
RENAME key newkey | 重命名指定的key,newkey存在时覆盖 |
RENAMENX key newkey | 重命名指定的key,当且仅当newkey不存在时操作 |
MOVE key db | 移动key到指定数据库 |
EXPIREAT key timestamp | 为 key 设置生存时间,EXPIREAT 命令接受的时间参数是 UNIX 时间戳 |
EXPIRE key seconds | 以秒为单位设置 key 的生存时间 |
PEXPIRE key milliseconds | 以毫秒为单位设置 key 的生存时间 |
PEXPIREAT key milliseconds-timestamp | 以毫秒为单位设置 key 的过期 unix 时间戳 |
TTL key | 以秒为单位返回 key 的剩余生存时间 |
PTTL key | 以毫秒为单位返回 key 的剩余生存时间 |
二 move
上一篇整理过超时相关,所以从move开始看。
MOVE key db
将当前数据库的 key
移动到给定的数据库 db
当中。如果当前数据库(源数据库)和给定数据库(目标数据库)有相同名字的给定 key
,或者 key
不存在于当前数据库,那么 MOVE
没有任何效果。源码在db.c
void moveCommand(client *c) {
robj *o;
redisDb *src, *dst;
int srcid;
long long dbid, expire;
//集群模式禁用move
if (server.cluster_enabled) {
addReplyError(c,"MOVE is not allowed in cluster mode");
return;
}
/* Obtain source and target DB pointers */
src = c->db; // 源数据库
srcid = c->db->id; // 源数据库的 id
// 切换到目标数据库
if (getLongLongFromObject(c->argv[2],&dbid) == C_ERR ||
dbid < INT_MIN || dbid > INT_MAX ||
selectDb(c,dbid) == C_ERR)
{
addReply(c,shared.outofrangeerr);
return;
}
dst = c->db;// 目标数据库
// 切换回源数据库
selectDb(c,srcid); /* Back to the source DB */
/* If the user is moving using as target the same
* DB as the source DB it is probably an error. */
// 如果源数据库和目标数据库相等,那么返回错误
if (src == dst) {
addReply(c,shared.sameobjecterr);
return;
}
/* Check if the element exists and get a reference */
// 取出要移动的对象
o = lookupKeyWrite(c->db,c->argv[1]);
if (!o) {
addReply(c,shared.czero);
return;
}
expire = getExpire(c->db,c->argv[1]);
/* Return zero if the key already exists in the target DB */
// 如果键已经存在于目标数据库,那么返回
if (lookupKeyWrite(dst,c->argv[1]) != NULL) {
addReply(c,shared.czero);
return;
}
// 将键添加到目标数据库中
dbAdd(dst,c->argv[1],o);
//设置过期时间
if (expire != -1) setExpire(dst,c->argv[1],expire);
// 增加对对象的引用计数,避免接下来在源数据库中删除时 o 被清理
incrRefCount(o);
/* OK! key moved, free the entry in the source DB */
// 将键从源数据库中返回
dbDelete(src,c->argv[1]);
server.dirty++;//设脏
addReply(c,shared.cone);
}
三 rename
RENAME key newkey
将 key
改名为 newkey
。
当 key
和 newkey
相同,或者 key
不存在时,返回一个错误。
当 newkey
已经存在时, RENAME 命令将覆盖旧值。
RENAMENX key newkey
当且仅当 newkey
不存在时,将 key
改名为 newkey
。
当 key
不存在时,返回一个错误。
void renameCommand(client *c) {
renameGenericCommand(c,0);
}
void renamenxCommand(client *c) {
renameGenericCommand(c,1);
}
底层的函数是一样的,入参不同
void renameGenericCommand(client *c, int nx) {
robj *o;
long long expire;
int samekey = 0;
/* When source and dest key is the same, no operation is performed,
* if the key exists, however we still return an error on unexisting key. */
// key和newkey相同的话,设置samekey标志
if (sdscmp(c->argv[1]->ptr,c->argv[2]->ptr) == 0) samekey = 1;
// 以写操作读取key的值对象
if ((o = lookupKeyWriteOrReply(c,c->argv[1],shared.nokeyerr)) == NULL)
return;
// 如果key和newkey相同,nx为1发送0,否则为ok,就是结合入参来看
if (samekey) {
addReply(c,nx ? shared.czero : shared.ok);
return;
}
// 增加引用计数,因为后面目标键也会引用这个对象
// 如果不增加的话,当来源键被删除时,这个值对象也会被删除
incrRefCount(o);
// 取出来源键的过期时间,将来作为newkey的过期时间
expire = getExpire(c->db,c->argv[1]);
// 判断newkey的值对象是否存在
if (lookupKeyWrite(c->db,c->argv[2]) != NULL) {
// 设置nx标志,RENAMENX 则不符合已存在的条件,发送0
if (nx) {
decrRefCount(o);
addReply(c,shared.czero);
return;
}
/* Overwrite: delete the old key before creating the new one
* with the same name. */
// 如果执行的是 RENAME ,那么删除已有的目标键
dbDelete(c->db,c->argv[2]);
}
// 将newkey和key的值对象关联
dbAdd(c->db,c->argv[2],o);
// 如果有过期时间,那么为目标键设置过期时间
if (expire != -1) setExpire(c->db,c->argv[2],expire);
// 删除来源键
dbDelete(c->db,c->argv[1]);
// 发送这两个键被修改的信号
signalModifiedKey(c->db,c->argv[1]);
signalModifiedKey(c->db,c->argv[2]);
// 发送不同命令的事件通知
notifyKeyspaceEvent(NOTIFY_GENERIC,"rename_from",
c->argv[1],c->db->id);
notifyKeyspaceEvent(NOTIFY_GENERIC,"rename_to",
c->argv[2],c->db->id);
server.dirty++;//设脏
addReply(c,nx ? shared.cone : shared.ok);
}
四 SCAN
SCAN 命令及其相关的 SSCAN 命令、 HSCAN 命令和 ZSCAN 命令都用于增量地迭代(incrementally iterate)一集元素(a collection of elements):
以上列出的四个命令都支持增量式迭代, 它们每次执行都只会返回少量元素, 所以这些命令可以用于生产环境, 而不会出现像 KEYS 命令、 SMEMBERS 命令带来的问题 —— 当 KEYS 命令被用于处理一个大的数据库时, 又或者 SMEMBERS 命令被用于处理一个大的集合键时, 它们可能会阻塞服务器达数秒之久。
增量式迭代命令缺点: 因为在对键进行增量式迭代的过程中, 键可能会被修改, 所以增量式迭代命令只能对被返回的元素提供有限的保证 (offer limited guarantees about the returned elements)。
看下源码:
/* The SCAN command completely relies on scanGenericCommand. */
void scanCommand(client *c) {
unsigned long cursor;
if (parseScanCursorOrReply(c,c->argv[1],&cursor) == C_ERR) return;
scanGenericCommand(c,NULL,cursor);
}
/* This command implements SCAN, HSCAN and SSCAN commands.
* If object 'o' is passed, then it must be a Hash or Set object, otherwise
* if 'o' is NULL the command will operate on the dictionary associated with
* the current database.
*
* When 'o' is not NULL the function assumes that the first argument in
* the client arguments vector is a key so it skips it before iterating
* in order to parse options.
*
* In the case of a Hash object the function returns both the field and value
* of every element on the Hash. */
// SCAN cursor [MATCH pattern] [COUNT count]
// SCAN、HSCAN、SSCAN、ZSCAN一类命令底层实现
// o对象必须是哈希对象或集合对象,否则命令将操作当前数据库
// 如果o不是NULL,那么说明他是一个哈希或集合对象,函数将跳过这些键对象,对参数进行分析
// 如果是哈希对象,返回返回的是键值对
void scanGenericCommand(client *c, robj *o, unsigned long cursor) {
int i, j;
list *keys = listCreate(); //创建一个列表
listNode *node, *nextnode;
long count = 10;
sds pat = NULL;
int patlen = 0, use_pattern = 0;
dict *ht;
/* Object must be NULL (to iterate keys names), or the type of the object
* must be Set, Sorted Set, or Hash. */
// 输入类型检查
serverAssert(o == NULL || o->type == OBJ_SET || o->type == OBJ_HASH ||
o->type == OBJ_ZSET);
/* Set i to the first option argument. The previous one is the cursor. */
// 设置第一个选项参数的索引位置
// 0 1 2 3
// SCAN OPTION <op_arg> SCAN 命令的选项值从索引 2 开始
// HSCAN <key> OPTION <op_arg> 而其他 *SCAN 命令的选项值从索引 3 开始
i = (o == NULL) ? 2 : 3; /* Skip the key argument if needed. */
/* Step 1: Parse options. */
// 1. 解析选项参数
while (i < c->argc) {
j = c->argc - i;
// 设定COUNT参数,COUNT 选项的作用就是让用户告知迭代命令, 在每次迭代中应该返回多少元素。
if (!strcasecmp(c->argv[i]->ptr, "count") && j >= 2) {
//保存个数到count
if (getLongFromObjectOrReply(c, c->argv[i+1], &count, NULL)
!= C_OK)
{
goto cleanup;
}
// 如果个数小于1,语法错误
if (count < 1) {
addReply(c,shared.syntaxerr);
goto cleanup;
}
i += 2;//参数跳过两个已经解析过的
//匹配match,让命令只返回和给定模式相匹配的元素。
} else if (!strcasecmp(c->argv[i]->ptr, "match") && j >= 2) {
pat = c->argv[i+1]->ptr;//pattern字符串
patlen = sdslen(pat); //pattern字符串长度
/* The pattern always matches if it is exactly "*", so it is
* equivalent to disabling it. */
// 如果pattern是"*",就不用匹配,全部返回,设置为0
use_pattern = !(pat[0] == '*' && patlen == 1);
i += 2;
} else {
addReply(c,shared.syntaxerr);
goto cleanup;
}
}
/* Step 2: Iterate the collection.
*
* Note that if the object is encoded with a ziplist, intset, or any other
* representation that is not a hash table, we are sure that it is also
* composed of a small number of elements. So to avoid taking state we
* just return everything inside the object in a single call, setting the
* cursor to zero to signal the end of the iteration. */
// 如果对象的底层实现为 ziplist 、intset 而不是哈希表,
// 那么这些对象应该只包含了少量元素,
// 为了保持不让服务器记录迭代状态的设计
// 我们将 ziplist 或者 intset 里面的所有元素都一次返回给调用者
// 并向调用者返回游标(cursor) 0
/* Handle the case of a hash table. */
ht = NULL;
if (o == NULL) { // 迭代目标为数据库
ht = c->db->dict;
} else if (o->type == OBJ_SET && o->encoding == OBJ_ENCODING_HT) {
// 迭代目标为 HT 编码的集合
ht = o->ptr;
} else if (o->type == OBJ_HASH && o->encoding == OBJ_ENCODING_HT) {
// 迭代目标是HT编码的哈希对象
ht = o->ptr;
count *= 2; /* We return key / value for this type. */
} else if (o->type == OBJ_ZSET && o->encoding == OBJ_ENCODING_SKIPLIST) {
// 迭代目标是skiplist编码的有序集合对象
zset *zs = o->ptr;
ht = zs->dict;
count *= 2; /* We return key / value for this type. */
}
if (ht) {
void *privdata[2];
/* We set the max number of iterations to ten times the specified
* COUNT, so if the hash table is in a pathological state (very
* sparsely populated) we avoid to block too much time at the cost
* of returning no or very few elements. */
// 设置最大的迭代长度为10*count次
long maxiterations = count*10;
/* We pass two pointers to the callback: the list to which it will
* add new elements, and the object containing the dictionary so that
* it is possible to fetch more data in a type-dependent way. */
// 我们向回调函数传入两个指针:
// 一个是用于记录被迭代元素的列表
// 另一个是字典对象
// 从而实现类型无关的数据提取操作
privdata[0] = keys;
privdata[1] = o;
do {
// 循环扫描ht,从游标cursor开始,调用指定的scanCallback函数,提出ht中的数据到刚开始创建的列表keys中
cursor = dictScan(ht, cursor, scanCallback, privdata);
} while (cursor &&
maxiterations-- &&
listLength(keys) < (unsigned long)count);
//没迭代完,或没迭代够count,就继续循环
} else if (o->type == OBJ_SET) {
// 如果是集合对象但编码不是HT是整数集合
int pos = 0;
int64_t ll;
// 将整数值取出来,构建成字符串对象加入到keys列表中,游标设置为0,表示迭代完成
while(intsetGet(o->ptr,pos++,&ll))
listAddNodeTail(keys,createStringObjectFromLongLong(ll));
cursor = 0;
} else if (o->type == OBJ_HASH || o->type == OBJ_ZSET) {
// 如果是哈希对象,或有序集合对象,但是编码都不是HT,是ziplist
unsigned char *p = ziplistIndex(o->ptr,0);
unsigned char *vstr;
unsigned int vlen;
long long vll;
while(p) {
// 将值取出来,根据不同类型的值,构建成相同的字符串对象,加入到keys列表中
ziplistGet(p,&vstr,&vlen,&vll);
listAddNodeTail(keys,
(vstr != NULL) ? createStringObject((char*)vstr,vlen) :
createStringObjectFromLongLong(vll));
p = ziplistNext(o->ptr,p);
}
cursor = 0;
} else {
serverPanic("Not handled encoding in SCAN.");
}
/* Step 3: Filter elements. */
// 3. 如果设置MATCH参数,要进行过滤
node = listFirst(keys); //链表首节点地址
while (node) {
robj *kobj = listNodeValue(node); //key对象
nextnode = listNextNode(node); //下一个节点地址
int filter = 0; //默认为不过滤
/* Filter element if it does not match the pattern. */
//pattern不是"*"因此要过滤
if (!filter && use_pattern) {
// 如果kobj是字符串对象
if (sdsEncodedObject(kobj)) {
// kobj的值不匹配pattern,设置过滤标志
if (!stringmatchlen(pat, patlen, kobj->ptr, sdslen(kobj->ptr), 0))
filter = 1;
} else {
// 如果kobj是整数对象
char buf[LONG_STR_SIZE];
int len;
serverAssert(kobj->encoding == OBJ_ENCODING_INT);
// 将整数转换为字符串类型,保存到buf中
len = ll2string(buf,sizeof(buf),(long)kobj->ptr);
//buf的值不匹配pattern,设置过滤标志
if (!stringmatchlen(pat, patlen, buf, len, 0)) filter = 1;
}
}
/* Filter element if it is an expired key. */
// 迭代目标是数据库,如果kobj是过期键,则过滤
if (!filter && o == NULL && expireIfNeeded(c->db, kobj)) filter = 1;
/* Remove the element and its associted value if needed. */
// 如果该键满足了上述的过滤条件,那么将其从keys列表删除并释放
if (filter) {
decrRefCount(kobj);
listDelNode(keys, node);
}
/* If this is a hash or a sorted set, we have a flat list of
* key-value elements, so if this element was filtered, remove the
* value, or skip it if it was not filtered: we only match keys. */
//如果是hash 或者 zset 有序集合,因此keys列表中保存的是键值对,如果key键对象被过滤,值对象也应当被过滤
if (o && (o->type == OBJ_ZSET || o->type == OBJ_HASH)) {
node = nextnode;
nextnode = listNextNode(node);//值对象的节点地址
if (filter) { // 如果该键满足了上述的过滤条件,那么将其从keys列表删除并释放
kobj = listNodeValue(node); //取出值对象
decrRefCount(kobj);
listDelNode(keys, node); //删除
}
}
node = nextnode;
}
/* Step 4: Reply to the client. */
// 4. 回复信息给client
addReplyMultiBulkLen(c, 2); //2部分,一个是游标,一个是列表
addReplyBulkLongLong(c,cursor); //回复游标
addReplyMultiBulkLen(c, listLength(keys)); //回复列表长度
//循环回复列表中的元素,并释放
while ((node = listFirst(keys)) != NULL) {
robj *kobj = listNodeValue(node);
addReplyBulk(c, kobj);
decrRefCount(kobj);
listDelNode(keys, node);
}
cleanup:// 清理代码
listSetFreeMethod(keys,decrRefCountVoid);//设置特定的释放列表的方式decrRefCountVoid
listRelease(keys);//释放
}
主要步骤如下:
- 解析count和match参数.如果没有指定count,默认返回10条数据
- 开始迭代集合,如果是key保存为ziplist或者intset,则一次性返回所有数据,没有游标(游标值直接返回0).由于redis设计只有数据量比较小的时候才会保存为ziplist或者intset,所以此处不会影响性能.
游标在保存为hash的时候发挥作用,具体入口函数为dictScan,下文详细描述。 - 根据match参数过滤返回值,并且如果这个键已经过期也会直接过滤掉(redis中键过期之后并不会立即删除)
- 返回结果到客户端,是一个数组,第一个值是游标,第二个值是具体的键值对
有的细节我还没太看明白。
参考: