目录
0.引用阅读与说明
贼全!连夜看完Redis常用的数据类型及对应底层数据结构解析
说明:
本文以《Redis 5设计与源码分析》为基础,对键相关命令实现进行探讨,
以redis6.0.8代码为学习版本,在原书基础上有所删减或补充,
是对《Redis 5设计与源码分析》相关章节的阅读笔记.
1.对象结构体和数据库结构体回顾
1.1 对象结构体redisObject
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS;
int refcount;
void *ptr;
} robj;
1.2 数据库结构体redisDb
typedef struct redisDb {
dict *dict; /* The keyspace for this DB */
dict *expires; /* Timeout of keys with a timeout set */
dict *blocking_keys; /* Keys with clients waiting for data (BLPOP)*/
dict *ready_keys; /* Blocked keys that received a PUSH */
dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */
int id; /* Database ID */
long long avg_ttl; /* Average TTL, just for stats */
unsigned long expires_cursor; /* Cursor of the active expire cycle. */
list *defrag_later; /* List of key names to attempt to defrag one by one, gradually. */
} redisDb;
dict --键空间散列表,存放所有键值对;
expires --过期时间散列表,存放键的过期时间,注意dict和expires中的键都指向同一个键的sds;
blocking_keys--处于阻塞状态的键和对应的client;
ready_keys --解除阻塞状态的键和对应的client,与blocking_keys属性相对,为了实现需要阻塞的命令设计;
watched_keys --watch的键和对应的client,主要用于事务;
id --数据库ID;
avg_ttl --数据库内所有键的平均生存时间;
defrag_later --逐渐尝试逐个碎片整理的key列表.
2.查看键信息
2.1 查看键的属性-object
object help 帮助命令,object命令使用手册
object refcount setname 获得指定键关联的值的引用数,即redisObject对象refcount属性
object encoding setname 获得指定键关联的值的内部存储使用的编码,即redisObject对象encoding属性的字符串表达,对象类型与底层编码对应关系见表10-1
object idletime setname 返回键的空闲时间,即自上次读写键以来经过的近似秒数
object idletime freq 返回键的对数访问频率计数器。当maxmemory-policy设置为LFU策略时,此子命令可用
/* The actual Redis Object */
#define OBJ_STRING 0 /* String object. */
#define OBJ_LIST 1 /* List object. */
#define OBJ_SET 2 /* Set object. */
#define OBJ_ZSET 3 /* Sorted set object. */
#define OBJ_HASH 4 /* Hash object. */
#define OBJ_MODULE 5 /* Module object. */
#define OBJ_STREAM 6 /* Stream object. */
#define OBJ_ENCODING_RAW 0 /* Raw representation */
#define OBJ_ENCODING_INT 1 /* Encoded as integer */
#define OBJ_ENCODING_HT 2 /* Encoded as hash table */
#define OBJ_ENCODING_ZIPMAP 3 /* Encoded as zipmap */
#define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */
#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define OBJ_ENCODING_INTSET 6 /* Encoded as intset */
#define OBJ_ENCODING_SKIPLIST 7 /* Encoded as skiplist */
#define OBJ_ENCODING_EMBSTR 8 /* Embedded sds string encoding */
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */
#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */
2.2 查看键的类型-type
格式:
type key
返回key对应存储的值的类型,根据Redis对象type属性,可以是:
none(key不存在),
string(字符串),
list(列表),
set(集合),
zset(有序集),
hash(散列表).
通过读取redisObject对象的type属性实现。
2.3 查看键的过期时间-ttl&pttl
格式:
ttl key
pttl key
说明:ttl返回键剩余的生存时间,单位秒;
pttl返回键剩余的生存时间,单位毫秒;
ttl命令返回key剩余的生存时间,单位秒。
一般用于根据key生存时间进行业务逻辑判断处理等,也可用于排查问题。
类似命令还有pttl返回以毫秒为单位的生存时间,它们调用函数都相同,此处以ttl命令为例。
源码分析:
调用的函数是ttlGenericCommand(client *c, int output_ms){...}
第1个参数是Redis客户端对象,client对象属性很多,这里的db表示选择的数据库,argv表示命令行参数,第2
个参数output_ms表示是否以毫秒为输出单位,首先以不修改查找对象(最后访问时间,LOOKUP_NOTOUCH)方式
查找key,若不存在则返回-2,存在则获取其过期时间,若已过期则返回0,没有过期则返回未过期的时间,其他
情况返回默认值-1。
3.设置键的信息
3.1 设置键过期时间-expire
expire命令用于设置key的过期时间,一般情况下redis的key应该都有一个对应的过期时间。类似命令还有
expireat、pexpire、pexpireat,格式都一样,区别在于时间和单位。它们调用的函数也是同一个,此处
以expire命令为例。
用法:
expire <key> <ttl> 命令用于将键key设置为ttl秒
pexpire <key> <ttl> 命令用于将键key设置为ttl毫秒
expireat <key> <timestamp> 命令用于将键key的过期时间设置为timestamp指定的秒数时间戳
pexpireat <key> <timestamp> 命令用于将键key的过期时间设置为timestamp指定的毫秒数时间戳
说明:为key设定seconds秒的过期时间,命令底层调用函数为expireGenericCommand,其原理是在redisDb
过z期字典里面添加或覆盖对应键值对,如果使用set/getset命令覆写会导致原来的过期时间被移除(参考
set/getset命令源码分析),但incr/rename/lpush等非覆写命令不会修改key的过期时间。仅移除key的过期时
间时可使用persist命令。源码分析:basetime为基准时间,unit为时间单位,首先将秒转化为毫秒,然后再将
毫秒时间转化为毫秒时间戳,统一参数之后进行对比,如果小于当前时间则删除,反之执行setExpire设置过期时
间,setExpire函数将key加入redisDb对象的expires字典,值为该key的过期时间。代码如下:
void expireGenericCommand(client *c, long long basetime, int unit) {
robj *key = c->argv[1], *param = c->argv[2];
long long when; /* unix time in milliseconds when the key will expire. */
if (getLongLongFromObjectOrReply(c, param, &when, NULL) != C_OK)
return;
if (unit == UNIT_SECONDS) when *= 1000;// 单位转换
when += basetime; // 加上基准时间
/* No key, return zero. */
if (lookupKeyWrite(c->db,key) == NULL) {
addReply(c,shared.czero);
return;
}
if (checkAlreadyExpired(when)) {
robj *aux;
int deleted = server.lazyfree_lazy_expire ? dbAsyncDelete(c->db,key) :
dbSyncDelete(c->db,key);
serverAssertWithInfo(c,key,deleted);
server.dirty++;
/* Replicate/AOF this as an explicit DEL or UNLINK. */
aux = server.lazyfree_lazy_expire ? shared.unlink : shared.del;
rewriteClientCommandVector(c,2,aux,key);
signalModifiedKey(c,c->db,key);
notifyKeyspaceEvent(NOTIFY_GENERIC,"del",key,c->db->id);
addReply(c, shared.cone);
return;
} else {
setExpire(c,c->db,key,when);
addReply(c,shared.cone);
signalModifiedKey(c,c->db,key);
notifyKeyspaceEvent(NOTIFY_GENERIC,"expire",key,c->db->id);
server.dirty++;
return;
}
}
3.2 删除键过期时间使键永久生效-persist
1.persist的作用
如果一个key存在过期时间,那么persist命令可以用于移除key的过期时间.
有时候我们需要将临时key变成永久key,那么可以使用persist命令处理.
2.格式:
persist key
3.说明:
persist用于删除key的过期时间,使key永久有效,通过将key从redisDb对象的expires字典里删除实现。
4.源码分析:
调用lookupKeyWrite函数,在查找前先查询过期字典,如果ttl到期则使键过期,如果键存在,则返回键的值对
象,并从数据库的过期字典中删除指定key的对象.
源码如下:
4.1 注册persist命令
struct redisCommand redisCommandTable[] = {
...
{"persist",persistCommand,2,
"write fast @keyspace",
0,NULL,1,1,1,0,0,0},
...
}
4.2 persistCommand的实现如下:
/* PERSIST key */
void persistCommand(client *c) {
if (lookupKeyWrite(c->db,c->argv[1])) {
if (removeExpire(c->db,c->argv[1])) // 从过期字典里删除过期时间
{
signalModifiedKey(c,c->db,c->argv[1]);
notifyKeyspaceEvent(NOTIFY_GENERIC,"persist",c->argv[1],c->db->id);
addReply(c,shared.cone);
server.dirty++;
} else {
addReply(c,shared.czero);
}
} else {
addReply(c,shared.czero);
}
}
4.3 重要步骤lookupKeyWrite的具体实现
robj *lookupKeyWrite(redisDb *db, robj *key) {
return lookupKeyWriteWithFlags(db, key, LOOKUP_NONE);
}
robj *lookupKeyWriteWithFlags(redisDb *db, robj *key, int flags) {
expireIfNeeded(db,key);
return lookupKey(db,key,flags);
}
int expireIfNeeded(redisDb *db, robj *key) {
if (!keyIsExpired(db,key)) return 0;
/* If we are running in the context of a slave, instead of
* evicting the expired key from the database, we return ASAP:
* the slave key expiration is controlled by the master that will
* send us synthesized DEL operations for expired keys.
*
* Still we try to return the right information to the caller,
* that is, 0 if we think the key should be still valid, 1 if
* we think the key is expired at this time. */
if (server.masterhost != NULL) return 1;
/* Delete the key */
server.stat_expiredkeys++;
propagateExpire(db,key,server.lazyfree_lazy_expire);
notifyKeyspaceEvent(NOTIFY_EXPIRED,
"expired",key,db->id);
int retval = server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) :
dbSyncDelete(db,key);
if (retval) signalModifiedKey(NULL,db,key);
return retval;
}
robj *lookupKey(redisDb *db, robj *key, int flags) {
dictEntry *de = dictFind(db->dict,key->ptr);
if (de) {
robj *val = dictGetVal(de);
/* Update the access time for the ageing algorithm.
* Don't do it if we have a saving child, as this will trigger
* a copy on write madness. */
if (!hasActiveChildProcess() && !(flags & LOOKUP_NOTOUCH)){
if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
updateLFU(val);
} else {
val->lru = LRU_CLOCK();
}
}
return val;
} else {
return NULL;
}
}
3.3 重命名键-rename&renamenx
1.介绍:
rename命令将key重命名,使用频率较低。同样类似命令还有renamenx,表示重命名后的key不存在时才能执行成
功,因底层调用同一函数,所以将两个命令合并介绍。
2.格式:
rename key newkey
renamenx key newkey
3.说明:
重命名key, key不存在时返回错误,存在时,将被new_key覆盖.
4.源码逻辑
命令为renamenx时nx参数等于1,先校验旧key名是否相同、是否存在,如果新key也存在,当nx等于1时返回0,反之删除新key。如果旧key有过期时间则给新key也加上过期时间,最后删除旧key。
4.1 注册命令
struct redisCommand redisCommandTable[] = {
...
{"rename",renameCommand,3,
"write @keyspace",
0,NULL,1,2,1,0,0,0},
{"renamenx",renamenxCommand,3,
"write fast @keyspace",
0,NULL,1,2,1,0,0,0},
...
}
4.2 调用函数:
void renameCommand(client *c) {
renameGenericCommand(c,0);
}
void renamenxCommand(client *c) {
renameGenericCommand(c,1);
}
最后两者都是调用renameGenericCommand,只不过第二个参数在rename命令中是0,在renamenx命令中是1.
4.3 renameGenericCommand函数的源码:
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. */
if (sdscmp(c->argv[1]->ptr,c->argv[2]->ptr) == 0) samekey = 1;
if ((o = lookupKeyWriteOrReply(c,c->argv[1],shared.nokeyerr)) == NULL)
return;
if (samekey) {
addReply(c,nx ? shared.czero : shared.ok);
return;
}
incrRefCount(o);
// 将key的过期时间保存到expire变量
expire = getExpire(c->db,c->argv[1]);
// 新key存在则删除
if (lookupKeyWrite(c->db,c->argv[2]) != NULL) {
/*如果1==nx,则不操作直接返回0*/
if (nx) {
decrRefCount(o);
addReply(c,shared.czero);
return;
}
/* Overwrite: delete the old key before creating the new one
* with the same name. */
dbDelete(c->db,c->argv[2]);
}
dbAdd(c->db,c->argv[2],o);// 将旧key的值对象和新的key添加到redis字典中
if (expire != -1) setExpire(c,c->db,c->argv[2],expire);// 如果就key有过期时间则对新key保留
dbDelete(c->db,c->argv[1]);// 删除旧key
signalModifiedKey(c,c->db,c->argv[1]);
signalModifiedKey(c,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);
}
5.使用示例,如下图:
3.4 修改键最后访问-touch
1.介绍
touch命令用于更新key的访问时间,避免被lru策略淘汰,使用频率较低.
2.格式
touch key [key...]
3.说明
改变key的最后访问时间。如果key不存在,则忽略该key。返回成功修改的数量。
4.源码分析
在入口函数touchCommand中,循环调用lookupKeyRead函数去修改key的最后访问时间,当然前提条件是没有rbd
或者aof进程在运行。关键函数为lookupKey,关键代码如下:
4.1 注册命令
struct redisCommand redisCommandTable[] = {
...
{"touch",touchCommand,-2,
"read-only fast @keyspace",
0,NULL,1,-1,1,0,0,0},
...
}
4.2 调用touchCommand函数
/* TOUCH key1 [key2 key3 ... keyN] */
void touchCommand(client *c) {
int touched = 0;
for (int j = 1; j < c->argc; j++)
if (lookupKeyRead(c->db,c->argv[j]) != NULL) touched++;
addReplyLongLong(c,touched);
}
4.3 lookupKeyRead具体实现
robj *lookupKeyRead(redisDb *db, robj *key) {
return lookupKeyReadWithFlags(db,key,LOOKUP_NONE);
}
4.4 lookupKeyReadWithFlags函数的具体实现,关键函数lookupKey
robj *lookupKeyReadWithFlags(redisDb *db, robj *key, int flags) {
robj *val;
if (expireIfNeeded(db,key) == 1) {
if (server.masterhost == NULL) {
server.stat_keyspace_misses++;
notifyKeyspaceEvent(NOTIFY_KEY_MISS, "keymiss", key, db->id);
return NULL;
}
if (server.current_client &&
server.current_client != server.master &&
server.current_client->cmd &&
server.current_client->cmd->flags & CMD_READONLY)
{
server.stat_keyspace_misses++;
notifyKeyspaceEvent(NOTIFY_KEY_MISS, "keymiss", key, db->id);
return NULL;
}
}
val = lookupKey(db,key,flags);// 关键函数
if (val == NULL) {
server.stat_keyspace_misses++;
notifyKeyspaceEvent(NOTIFY_KEY_MISS, "keymiss", key, db->id);
}
else
server.stat_keyspace_hits++;
return val;
}
4.5 函数lookupKey的实现,修改key的最后访问时间
robj *lookupKey(redisDb *db, robj *key, int flags) {
dictEntry *de = dictFind(db->dict,key->ptr);
if (de) {
robj *val = dictGetVal(de);
/* Update the access time for the ageing algorithm.
* Don't do it if we have a saving child, as this will trigger
* a copy on write madness. */
if (!hasActiveChildProcess() && !(flags & LOOKUP_NOTOUCH)){
if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
updateLFU(val);
} else {
val->lru = LRU_CLOCK();
}
}
return val;
} else {
return NULL;
}
}
4.查找键
本节介绍的命令都属于查询一类的,比如exists命令查询键是否存在,keys命令查找符合模式的键,scan命令遍
历键,以及randomkey命令随机取键。其中exists命令和randomkey命令比较常用,keys命令由于其特性,一般
被禁止在线上环境使用,scan命令在遍历过程中数据可以被修改,可能会造成不严谨的返回结果,但都有其合适的
使用场景。
4.1 判断键是否存在-exists
1.介绍
exists命令用于判断指定的key是否存在,并返回key存在的数量。使用频率较高。
2.格式
exists key1 key2 ... key_N
3.说明
检查key是否存在,返回key存在的数量.
4.源码分析
for循环调用expireIfNeeded函数,表示尝试删除已过期的key,
然后调用dbExists,并根据返回结果累加数量。
4.1 注册命令
struct redisCommand redisCommandTable[] = {
...
{"exists",existsCommand,-2,
"read-only fast @keyspace",
0,NULL,1,-1,1,0,0,0},
...
}
4.2 existsCommand的实现
void existsCommand(client *c) {
long long count = 0;
int j;
for (j = 1; j < c->argc; j++) {
if (lookupKeyRead(c->db,c->argv[j])) count++;// 如果找到了这个robj对象(对象不为空),则count++
}
addReplyLongLong(c,count);
}
4.3 lookupKeyRead的实现可以查看上一节的内容.
主要逻辑就是lookupKeyReadWithFlags中的:
{
...
if (expireIfNeeded(db,key) == 1)
...
}
5.示例:
4.2 查找符合模式的键-keys
1.介绍
keys命令匹配合适的key并一次性返回,如果匹配的键较多,则可能阻塞服务器,因此该命令一般禁止在线上使用.
2.格式
keys pattern
3.说明
keys命令的作用是查找所有符合给定模式“pattern”的key,使用该命令处理大数据库时,可能会造成服务器长时间阻塞(秒级).
4.源码分析
初始化安全迭代器(迭代过程中允许修改数据),如果传入pattern为’*'则allkeys等于true,迭代过程中判断
allkeys或者字符串匹配为true并且没有过期则记录该key.
4.1 注册命令
struct redisCommand redisCommandTable[] = {
...
{"keys",keysCommand,2,
"read-only to-sort @keyspace @dangerous",
0,NULL,0,0,0,0,0,0},
...
}
4.2 keysCommand函数
void keysCommand(client *c) {
dictIterator *di;
dictEntry *de;
sds pattern = c->argv[1]->ptr;
int plen = sdslen(pattern), allkeys;
unsigned long numkeys = 0;
void *replylen = addReplyDeferredLen(c);
di = dictGetSafeIterator(c->db->dict);// 将数据库键空间作为参数,初始化安全迭代器
allkeys = (pattern[0] == '*' && plen == 1);
/*遍历数据库键空间*/
while((de = dictNext(di)) != NULL) {
sds key = dictGetKey(de);
robj *keyobj;
/*判断key是否与正则表达式匹配,若匹配且key没有过期则在回复给客户端的内容中记录*/
if (allkeys || stringmatchlen(pattern,plen,key,sdslen(key),0)) {
keyobj = createStringObject(key,sdslen(key));
/*key没有过期,过期则删除*/
if (!keyIsExpired(c->db,keyobj)) {
addReplyBulk(c,keyobj);
numkeys++;
}
decrRefCount(keyobj);
}
}
dictReleaseIterator(di);
setDeferredArrayLen(c,replylen,numkeys);
}
5.示例:
4.3 遍历键-scan
1.简介
scan命令可以遍历数据库中几乎所有的键,并且不用担心阻塞服务器。使用频率较低.
2.格式
scan cursor [MATCH patter] [COUNT count]
scan 游标 MATCH <返回和给定模式相匹配的元素> count 每次迭代所返回的元素数量
3.说明
scan、sscan、hscan、zsan分别有自己的命令入口,入口中会进行参数检测和游标赋值,然后进入统一的入口函
数:dictscan,具体细节详见dict章节。需要注意的是迭代都是以“桶”为单位的,所以有时候因为Hash冲突的原
因,scan会多返回一些数据.
SCAN命令是增量的循环,每次调用只会返回一小部分的元素。SCAN命令返回的是一个游标,从0开始遍历,到0结束遍历。通过scan中的MATCH <pattern> 参数,可以让命令只返回和给定模式相匹配的元素,实现模糊查询的效果
返回值的说明:
SCAN,SSCAN, HSCAN,ZSCAN命令都返回一个包含两个元素的multi-bulk 回复:
回复的第一个元素是字符串表示的无符号64位整数(游标)
SCAN 命令每次被调用之后,都会向用户返回一个新的游标,用户在下次迭代时需要使用这个新游标作为 SCAN 命令的游标参数,以此来延续之前的迭代过程.
当SCAN命令的游标参数被设置为0时,服务器将开始一次新的迭代,而当服务器向用户返回值为0,的游标时,表示迭代已结束.
回复的第二个元素是另一个 multi-bulk 回复
这个 multi-bulk 回复包含了本次被迭代的元素。
注意:SCAN命令不能保证每次返回的值都是有序的,另外同一个key有可能返回多次,不做区分,需要应用程序去处理.
SCAN 命令返回的每个元素都是一个数据库键。
SSCAN 命令返回的每个元素都是一个集合成员。
HSCAN 命令返回的每个元素都是一个键值对,一个键值对由一个键和一个值组成。
ZSCAN 命令返回的每个元素都是一个有序集合元素,一个有序集合元素由一个成员(member)和一个分值(score)组成。
4.源码分析
4.1 注册命令
struct redisCommand redisCommandTable[] = {
...
{"sscan",sscanCommand,-3,
"read-only random @set",
0,NULL,1,1,1,0,0,0},
{"zscan",zscanCommand,-3,
"read-only random @sortedset",
0,NULL,1,1,1,0,0,0},
{"hscan",hscanCommand,-3,
"read-only random @hash",
0,NULL,1,1,1,0,0,0},
{"scan",scanCommand,-2,
"read-only random @keyspace",
0,NULL,0,0,0,0,0,0},
...
}
4.2 函数sscanCommand,zscanCommand,hscanCommand和scanCommand,最终都调用了scanGenericCommand,
源码如下:
void sscanCommand(client *c) {
robj *set;
unsigned long cursor;
if (parseScanCursorOrReply(c,c->argv[2],&cursor) == C_ERR) return;
if ((set = lookupKeyReadOrReply(c,c->argv[1],shared.emptyscan)) == NULL ||
checkType(c,set,OBJ_SET)) return;
scanGenericCommand(c,set,cursor);
}
void zscanCommand(client *c) {
robj *o;
unsigned long cursor;
if (parseScanCursorOrReply(c,c->argv[2],&cursor) == C_ERR) return;
if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.emptyscan)) == NULL ||
checkType(c,o,OBJ_ZSET)) return;
scanGenericCommand(c,o,cursor);
}
void hscanCommand(client *c) {
robj *o;
unsigned long cursor;
if (parseScanCursorOrReply(c,c->argv[2],&cursor) == C_ERR) return;
if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.emptyscan)) == NULL ||
checkType(c,o,OBJ_HASH)) return;
scanGenericCommand(c,o,cursor);
}
/* 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);
}
4.3 scanGenericCommand源码过长,可自行查阅,主要步骤是:
Step 1: Parse options.
Step 2: Iterate the collection.
Step 3: Filter elements.
Step 4: Reply to the client.
即:
(1)解析count和match参数,如果没有指定count,默认返回10条数据;
(2)开始迭代集合,如果key保存为ziplist或者intset,则一次性返回所有数据,游标为0(scan命令的游标参
数为0时表示新一轮迭代开始,命令返回的游标值为0时表示迭代结束).由于Redis设计只有数据量比较小的时候
才会保存为ziplist或者intset,所以此处不会影响性能.游标在保存为Hash的时候发挥作用.具体入口函数为
dictScan,具体细节详见dict相关内容;
(3)根据match参数过滤返回值,并且如果这个键已经过期也会直接过滤掉(Redis中键过期之后并不会立即删除);
(4)返回结果到客户端,是一个数组,第1个值是游标,第2个值是具体的键值对.
5.再作说明
SCAN相关命令包括SSCAN命令,HSCAN命令和ZSCAN命令,分别用于集合,哈希键及有序集合等.
SCAN 命令用于迭代当前数据库中的数据库键。
SSCAN 命令用于迭代集合键中的元素。
HSCAN 命令用于迭代哈希键中的键值对。
ZSCAN 命令用于迭代有序集合中的元素(包括元素成员和元素分值).
因为 SCAN,SSCAN,HSCAN和ZSCAN 四个命令的工作方式都非常相似,要记住:
SSCAN命令,HSCAN命令和ZSCAN命令的第一个参数总是一个数据库键,
而SCAN命令则不需要在第一个参数提供任何数据库键,因为它迭代的是当前数据库中的所有数据库键.
6.操作示例:
4.4 随机取键-randomkey
1.介绍
randomkey命令随机返回数据库中的key,使用频率较低。
2.格式
randomkey
3.说明
在当前数据库中随机返回一个尚未过期的key(不删除)
4.源码分析
命令核心函数为dictGetRandomKey,如果Redis正在rehash,那么将1号散列表也作为随机查找的目标,否则只从0号散列表中查找节点,d->rehashidx表示rehash当前的索引。
4.1 注册命令
struct redisCommand redisCommandTable[] = {
...
{"randomkey",randomkeyCommand,1,
"read-only random @keyspace",
0,NULL,0,0,0,0,0,0},
...
}
4.2 randomkeyCommand函数
void randomkeyCommand(client *c) {
robj *key;
if ((key = dbRandomKey(c->db)) == NULL) {
addReplyNull(c);
return;
}
addReplyBulk(c,key);
decrRefCount(key);
}
4.3 核心函数dictGetRandomKey的调用路径:
randomkeyCommand->dbRandomKey->dictGetFairRandomKey->dictGetRandomKey
5.示例:
5. 操作键
本节讲解删除键、序列化/反序列化键、移动键和键排序操作,其中del是比较常用的删除键命令,unlink是Redis 4.0为了弥补del删除大值时阻塞服务器而加入的异步删除键命令。
5.1 删除键
5.1.1 del命令
1.介绍
该命令以异步方式删除key,这可以避免del删除大key的问题,unlink在删除时会判断删除所需的工作量,以
此决定使用同步还是异步删除(另一个线程中进行内存回收,不会阻塞当前线程),通常建议使用unlink代替
DEL命令,但注意使用unlink命令需Redis版本在4.0及以上.
2.格式
del key [key...]
3.说明
以阻塞方式删除key。
4.源码分析
del调用函数delGenericCommand,再循环调用dbSyncDelete函数,同步删除key、value、过期字典里对应的key(如果有),如果是集群还会删除key与slot(槽位)的对应关系。
4.1 注册命令
struct redisCommand redisCommandTable[] = {
...
{"del",delCommand,-2,
"write @keyspace",
0,NULL,1,-1,1,0,0,0},
...
}
4.2 delCommand函数
void delCommand(client *c) {
delGenericCommand(c,server.lazyfree_lazy_user_del);
}
4.3 函数delGenericCommand(在del中,此处循环调用dbSyncDelete,为同步调用)
void delGenericCommand(client *c, int lazy) {
int numdel = 0, j;
for (j = 1; j < c->argc; j++) {
expireIfNeeded(c->db,c->argv[j]);
int deleted = lazy ? dbAsyncDelete(c->db,c->argv[j]) :
dbSyncDelete(c->db,c->argv[j]);
if (deleted) {
signalModifiedKey(c,c->db,c->argv[j]);
notifyKeyspaceEvent(NOTIFY_GENERIC,
"del",c->argv[j],c->db->id);
server.dirty++;
numdel++;
}
}
addReplyLongLong(c,numdel);
}
4.4 dbSyncDelete函数的具体实现
int dbSyncDelete(redisDb *db, robj *key) {
/* Deleting an entry from the expires dict will not free the sds of
* the key, because it is shared with the main dictionary. */
if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr);
if (dictDelete(db->dict,key->ptr) == DICT_OK) {
if (server.cluster_enabled) slotToKeyDel(key->ptr);
return 1;
} else {
return 0;
}
}
5.示例:
5.1.2 unlink命令
相关链接:Redis源码整理笔记-bio与个人理解
1.介绍
该命令用于同步删除一个或多个key,因为是同步删除,所以在删除大key时可能会阻塞服务器。
2.格式
unlink key [key...]
3.说明
以阻塞方式删除key。
4.源码分析
同del一样,unlink也是调用同一个命令执行函数delGenericCommand,根据传参不同,unlink循环调用
dbAsyncDelete,先删除过期字典里的key(如果有),然后调用dictUnlink从键空间删除key的关联关系
并返回被删除的实例,根据实例计算删除需要的工作量和是否被引用来决定是否使用惰性删除,最后再使用
dictFreeUnlinkedEntry删除dictUnlink返回的实例,同样,如果该Redis是集群模式,还会删除key与
slot的对应关系。
4.1 注册命令
struct redisCommand redisCommandTable[] = {
...
{"unlink",unlinkCommand,-2,
"write fast @keyspace",
0,NULL,1,-1,1,0,0,0},
...
}
4.2 delCommand函数
void unlinkCommand(client *c) {
delGenericCommand(c,1);
}
4.3 函数delGenericCommand(在unlink分支中,会循环调用dbAsyncDelete来实现键的删除,为异步调用)
void delGenericCommand(client *c, int lazy) {
int numdel = 0, j;
for (j = 1; j < c->argc; j++) {
expireIfNeeded(c->db,c->argv[j]);
int deleted = lazy ? dbAsyncDelete(c->db,c->argv[j]) :
dbSyncDelete(c->db,c->argv[j]);
if (deleted) {
signalModifiedKey(c,c->db,c->argv[j]);
notifyKeyspaceEvent(NOTIFY_GENERIC,
"del",c->argv[j],c->db->id);
server.dirty++;
numdel++;
}
}
addReplyLongLong(c,numdel);
}
4.4 函数dbAsyncDelete的实现:
int dbAsyncDelete(redisDb *db, robj *key) {
/* Deleting an entry from the expires dict will not free the sds of
* the key, because it is shared with the main dictionary. */
if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr);
/* If the value is composed of a few allocations, to free in a lazy way
* is actually just slower... So under a certain limit we just free
* the object synchronously. */
dictEntry *de = dictUnlink(db->dict,key->ptr);
if (de) {
robj *val = dictGetVal(de);
/* 返回释放对象需要的工作量,字符串对象始终返回1 */
size_t free_effort = lazyfreeGetFreeEffort(val);
/* 工作量大于阈值并且没有被别的对象引用,LAZYFREE_THRESHOLD的值是64 */
if (free_effort > LAZYFREE_THRESHOLD && val->refcount == 1) {
atomicIncr(lazyfree_objects,1);
/* 创建后台job,将val加入移步删除队列-核心步骤*/
bioCreateBackgroundJob(BIO_LAZY_FREE,val,NULL,NULL);
dictSetVal(db->dict,de,NULL);
}
}
/* Release the key-val pair, or just the key if we set the val
* field to NULL in order to lazy free it later. */
if (de) {
dictFreeUnlinkedEntry(db->dict,de);
if (server.cluster_enabled) slotToKeyDel(key->ptr);
return 1;
} else {
return 0;
}
}
4.5 bioCreateBackgroundJob的具体实现:
void bioCreateBackgroundJob(int type, void *arg1, void *arg2, void *arg3) {
struct bio_job *job = zmalloc(sizeof(*job));
job->time = time(NULL);
job->arg1 = arg1;
job->arg2 = arg2;
job->arg3 = arg3;
pthread_mutex_lock(&bio_mutex[type]);// 线程互斥锁
listAddNodeTail(bio_jobs[type],job); // 追加任务到对应类型的链表尾部
bio_pending[type]++; // 标记未处理的数据量
pthread_cond_signal(&bio_newjob_cond[type]); // 唤醒一个异步处理线程
pthread_mutex_unlock(&bio_mutex[type]); // 解锁
}
type表示任务类型,arg1、arg2、arg3为参数,在处理任务时使用.创建的bio_job结构体包含当前时间和传
入的3个参数,将bio_job结构体追加到对应类型的双向链表尾部,这个过程是线程互斥的. 添加完成后调用
pthread_cond_signal通知异步线程处理.
在使用initServer时会调用bioInit来初始化bio,并生成3个异步处理线程,分别对应3个类型
(BIO_CLOSE_FILE、BIO_AOF_FSYNC、BIO_LAZY_FREE)的双向链表,处理函数为
bioProcessBackgroundJobs,该函数从任务链表头部获取数据,根据类型和参数调用相关释放函数.
4.6 bioProcessBackgroundJobs的具体实现
void *bioProcessBackgroundJobs(void *arg) {
struct bio_job *job;
unsigned long type = (unsigned long) arg;
sigset_t sigset;
/* Check that the type is within the right interval. */
if (type >= BIO_NUM_OPS) {
serverLog(LL_WARNING,
"Warning: bio thread started with wrong type %lu",type);
return NULL;
}
switch (type) {
case BIO_CLOSE_FILE:
redis_set_thread_title("bio_close_file");
break;
case BIO_AOF_FSYNC:
redis_set_thread_title("bio_aof_fsync");
break;
case BIO_LAZY_FREE:
redis_set_thread_title("bio_lazy_free");
break;
}
/* 设置CPU亲缘性 */
redisSetCpuAffinity(server.bio_cpulist);
/* Make the thread killable at any time, so that bioKillThreads()
* can work reliably. */
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);
/* 加上互斥锁 */
pthread_mutex_lock(&bio_mutex[type]);
/* Block SIGALRM so we are sure that only the main thread will
* receive the watchdog signal. */
sigemptyset(&sigset);
sigaddset(&sigset, SIGALRM);
if (pthread_sigmask(SIG_BLOCK, &sigset, NULL))
serverLog(LL_WARNING,
"Warning: can't mask SIGALRM in bio.c thread: %s", strerror(errno));
while(1) {
listNode *ln;
/* 链表为空则继续等待 */
/* The loop always starts with the lock hold. */
if (listLength(bio_jobs[type]) == 0) {
pthread_cond_wait(&bio_newjob_cond[type],&bio_mutex[type]);
continue;
}
/* Pop the job from the queue. */
/* 从链表头部获取元素 */
ln = listFirst(bio_jobs[type]);
job = ln->value;
/* It is now possible to unlock the background system as we know have
* a stand alone job structure to process.*/
/* 解锁 */
pthread_mutex_unlock(&bio_mutex[type]);
/* Process the job accordingly to its type. */
if (type == BIO_CLOSE_FILE) {
close((long)job->arg1);
} else if (type == BIO_AOF_FSYNC) {
redis_fsync((long)job->arg1);
} else if (type == BIO_LAZY_FREE) {
/* What we free changes depending on what arguments are set:
* arg1 -> free the object at pointer.
* arg2 & arg3 -> free two dictionaries (a Redis DB).
* only arg3 -> free the skiplist. */
if (job->arg1) // 释放对象
lazyfreeFreeObjectFromBioThread(job->arg1);
else if (job->arg2 && job->arg3)// 释放数据库
lazyfreeFreeDatabaseFromBioThread(job->arg2,job->arg3);
else if (job->arg3)// 释放slot与key的映射关系
lazyfreeFreeSlotsMapFromBioThread(job->arg3);
} else {
serverPanic("Wrong job type in bioProcessBackgroundJobs().");
}
zfree(job);
/* Lock again before reiterating the loop, if there are no longer
* jobs to process we'll block again in pthread_cond_wait(). */
pthread_mutex_lock(&bio_mutex[type]);// 加锁
listDelNode(bio_jobs[type],ln); // 删除任务
bio_pending[type]--; // 任务计数减1
/* Unblock threads blocked on bioWaitStepOfType() if any. */
pthread_cond_broadcast(&bio_step_cond[type]); // 广播消息
}
}
lazyfreeFreeObjectFromBioThread(job->arg1);--释放对象
lazyfreeFreeDatabaseFromBioThread(job->arg2,job->arg3);--释放数据库
lazyfreeFreeSlotsMapFromBioThread(job->arg3);--释放slot与key的映射关系
这三个函数十分重要,此处就lazyfreeFreeObjectFromBioThread做出说明.
4.7 lazyfreeFreeObjectFromBioThread源码
void lazyfreeFreeObjectFromBioThread(robj *o) {
decrRefCount(o);
atomicDecr(lazyfree_objects,1); // 原子自减惰性释放数量
}
void decrRefCount(robj *o) {
if (o->refcount == 1) {
switch(o->type) {
case OBJ_STRING: freeStringObject(o); break;
case OBJ_LIST: freeListObject(o); break;
case OBJ_SET: freeSetObject(o); break;
case OBJ_ZSET: freeZsetObject(o); break;
case OBJ_HASH: freeHashObject(o); break;
case OBJ_MODULE: freeModuleObject(o); break;
case OBJ_STREAM: freeStreamObject(o); break;
default: serverPanic("Unknown object type"); break;
}
zfree(o);
} else {
if (o->refcount <= 0) serverPanic("decrRefCount against refcount <= 0");
if (o->refcount != OBJ_SHARED_REFCOUNT) o->refcount--;
}
}
在函数decrRefCount中,如果对象的refcount为1表示没有别的引用,可以释放内存,switch里面对应的是各
个类型对象的释放函数。在unlink命令出现之前,Redis对象的refcount是有实际意义的,为了实现具有良好
性能的惰性删除,Redis对象共享只对0到10000(可配置)的整数进行共享(refcount=2147483647),其他
对象都不再共享,以此降低惰性删除时频繁加解锁竞争导致的性能下降.
5.示例:
5.2 序列化/反序列化键
5.2.1 dump命令
1.介绍
randomkey命令随机返回数据库中的key,使用频率较低。
2.格式
randomkey
3.说明
在当前数据库中随机返回一个尚未过期的key(不删除)
4.源码分析
命令核心函数为dictGetRandomKey,如果Redis正在rehash,那么将1号散列表也作为随机查找的目标,否则只从0号散列表中查找节点,d->rehashidx表示rehash当前的索引。
4.1 注册命令
struct redisCommand redisCommandTable[] = {
...
{"randomkey",randomkeyCommand,1,
"read-only random @keyspace",
0,NULL,0,0,0,0,0,0},
...
}
4.2 randomkeyCommand函数
void randomkeyCommand(client *c) {
robj *key;
if ((key = dbRandomKey(c->db)) == NULL) {
addReplyNull(c);
return;
}
addReplyBulk(c,key);
decrRefCount(key);
}
4.3 核心函数dictGetRandomKey的调用路径:
randomkeyCommand->dbRandomKey->dictGetFairRandomKey->dictGetRandomKey
5.示例:
5.2.2 restore命令
1.介绍
randomkey命令随机返回数据库中的key,使用频率较低。
2.格式
randomkey
3.说明
在当前数据库中随机返回一个尚未过期的key(不删除)
4.源码分析
命令核心函数为dictGetRandomKey,如果Redis正在rehash,那么将1号散列表也作为随机查找的目标,否则只从0号散列表中查找节点,d->rehashidx表示rehash当前的索引。
4.1 注册命令
struct redisCommand redisCommandTable[] = {
...
{"randomkey",randomkeyCommand,1,
"read-only random @keyspace",
0,NULL,0,0,0,0,0,0},
...
}
4.2 randomkeyCommand函数
void randomkeyCommand(client *c) {
robj *key;
if ((key = dbRandomKey(c->db)) == NULL) {
addReplyNull(c);
return;
}
addReplyBulk(c,key);
decrRefCount(key);
}
4.3 核心函数dictGetRandomKey的调用路径:
randomkeyCommand->dbRandomKey->dictGetFairRandomKey->dictGetRandomKey
5.示例:
5.3 移动键
5.3.1 move命令
1.介绍
randomkey命令随机返回数据库中的key,使用频率较低。
2.格式
randomkey
3.说明
在当前数据库中随机返回一个尚未过期的key(不删除)
4.源码分析
命令核心函数为dictGetRandomKey,如果Redis正在rehash,那么将1号散列表也作为随机查找的目标,否则只从0号散列表中查找节点,d->rehashidx表示rehash当前的索引。
4.1 注册命令
struct redisCommand redisCommandTable[] = {
...
{"randomkey",randomkeyCommand,1,
"read-only random @keyspace",
0,NULL,0,0,0,0,0,0},
...
}
4.2 randomkeyCommand函数
void randomkeyCommand(client *c) {
robj *key;
if ((key = dbRandomKey(c->db)) == NULL) {
addReplyNull(c);
return;
}
addReplyBulk(c,key);
decrRefCount(key);
}
4.3 核心函数dictGetRandomKey的调用路径:
randomkeyCommand->dbRandomKey->dictGetFairRandomKey->dictGetRandomKey
5.示例:
5.3.2 migrate命令
1.介绍
randomkey命令随机返回数据库中的key,使用频率较低。
2.格式
randomkey
3.说明
在当前数据库中随机返回一个尚未过期的key(不删除)
4.源码分析
命令核心函数为dictGetRandomKey,如果Redis正在rehash,那么将1号散列表也作为随机查找的目标,否则只从0号散列表中查找节点,d->rehashidx表示rehash当前的索引。
4.1 注册命令
struct redisCommand redisCommandTable[] = {
...
{"randomkey",randomkeyCommand,1,
"read-only random @keyspace",
0,NULL,0,0,0,0,0,0},
...
}
4.2 randomkeyCommand函数
void randomkeyCommand(client *c) {
robj *key;
if ((key = dbRandomKey(c->db)) == NULL) {
addReplyNull(c);
return;
}
addReplyBulk(c,key);
decrRefCount(key);
}
4.3 核心函数dictGetRandomKey的调用路径:
randomkeyCommand->dbRandomKey->dictGetFairRandomKey->dictGetRandomKey
5.示例:
5.4 键排序-sort
1.介绍
randomkey命令随机返回数据库中的key,使用频率较低。
2.格式
randomkey
3.说明
在当前数据库中随机返回一个尚未过期的key(不删除)
4.源码分析
命令核心函数为dictGetRandomKey,如果Redis正在rehash,那么将1号散列表也作为随机查找的目标,否则只从0号散列表中查找节点,d->rehashidx表示rehash当前的索引。
4.1 注册命令
struct redisCommand redisCommandTable[] = {
...
{"randomkey",randomkeyCommand,1,
"read-only random @keyspace",
0,NULL,0,0,0,0,0,0},
...
}
4.2 randomkeyCommand函数
void randomkeyCommand(client *c) {
robj *key;
if ((key = dbRandomKey(c->db)) == NULL) {
addReplyNull(c);
return;
}
addReplyBulk(c,key);
decrRefCount(key);
}
4.3 核心函数dictGetRandomKey的调用路径:
randomkeyCommand->dbRandomKey->dictGetFairRandomKey->dictGetRandomKey
5.示例: