介绍完Redis的底层数据结构之后, 介绍我们平时使用Redis的时候可以直接看到的数据结构:
- 字符串
- 哈希
- 链表
- 集合和有序集合。
字符串的实现
上一章说过,字符串有3种编码形式
| 类型 | 编码 | 描述 |
|---|---|---|
| REDIS_STRING | REDIS_ENCODING_INT | 使用整数值实现的字符串对象 |
| REDIS_STRING | REDIS_ENCODING_EMBSTR | 使用 embstr 编码的简单动态字符串实现的字符串对象 |
| REDIS_STRING | REDIS_ENCODING_RAW | 使用简单动态字符串实现的字符串对象。 |
这三种类型分别对应的底层数据结构为int,embstr, sds。
编码的选择
字符串具体用哪种方法实现呢?
- REDIS_ENCODING_INT:如果字符串存的是整数,就用整数形式保存
- REDIS_ENCODING_RAW:如果字符串对象保存的是一个字符串值, 并且这个字符串值的长度大于44字节, 那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串值, 并将对象的编码设置为 raw
- REDIS_ENCODING_EMBSTR:如果字符串对象保存的是一个字符串值, 并且这个字符串值的长度小于等于 44 字节, 那么字符串对象将使用 embstr 编码的方式来保存这个字符串值。
embstr是啥
embstr 编码是专门用于保存短字符串的一种优化编码方式, 这种编码和 raw 编码一样, 都使用 redisObject 结构和 sdshdr 结构来表示字符串对象, 但 raw 编码会调用两次内存分配函数来分别创建 redisObject 结构和 sdshdr 结构, 而 embstr 编码则通过调用一次内存分配函数来分配一块连续的空间, 空间中依次包含 redisObject 和 sdshdr 两个结构
编码的转换
int转为RAW
对于 int 编码的字符串对象来说, 如果我们向对象执行了一些命令, 使得这个对象保存的不再是整数值, 而是一个字符串值, 比如在最开始的数字类型后边执行了一个append的操作,加上了一串字符串,那么字符串对象的编码将从 int 变为 raw
embstr编码转换为raw编码
因为 Redis 没有为 embstr 编码的字符串对象编写任何相应的修改程序 (只有 int 编码的字符串对象和 raw 编码的字符串对象有这些程序), 所以 embstr 编码的字符串对象实际上是只读的: 当我们对 embstr 编码的字符串对象执行任何修改命令时, 程序会先将对象的编码从 embstr 转换成 raw , 然后再执行修改命令; 因为这个原因, embstr 编码的字符串对象在执行修改命令之后, 总会变成一个 raw 编码的字符串对象。
字符串的命令
字符串支持的命令如下:
| 命令 | 命令描述 |
|---|---|
| SET key value [ex 秒数][px 毫秒数][nx/xx] | 设置指定key的值 |
| GET key | 获取指定key的值 |
| APPEND key value | 将value追加到指定key的值末尾 |
| INCRBY key increment | 将指定key的值加上增量increment |
| DECRBY key decrement | 将指定key的值减去增量decrement |
| STRLEN key | 返回指定key的值长度 |
| SETRANGE key offset value | 将value覆写到指定key的值上,从offset位开始 |
| GETRANGE key start end | 获取指定key中字符串的子串[start,end] |
| MSET key value [key value …] | 一次设定多个key的值 |
| MGET key1 [key2..] | 一次获取多个key的值 |
SET 命令的实现
set命令是调用setCommand实现的,命令格式如下
set key value [ex 秒数] [px 毫秒数] [nx/xx]
其中,各个选项的含义如下:
- ex 设置指定的到期时间,单位为秒
- px 设置指定的到期时间,单位为毫秒
- nx 只有在key不存在的时候,才设置key的值
- xx 只有key存在时,才对key进行设置操作
void setCommand(client *c) {
robj *expire = NULL;
int unit = UNIT_SECONDS;
int flags = OBJ_NO_FLAGS;
if (parseExtendedStringArgumentsOrReply(c,&flags,&unit,&expire,COMMAND_SET) != C_OK) {
return;
}
c->argv[2] = tryObjectEncoding(c->argv[2]);
setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);
}
parseExtendedStringArgumentsOrReply()函数是解析参数用的,设置对应的flag
tryObjectEncoding() 尝试将字符串编码成整数或者EMB,流流程如下:
robj *tryObjectEncoding(robj *o) {
long value;
sds s = o->ptr;
size_t len;
// 确认是字符串类型
serverAssertWithInfo(NULL,o,o->type == OBJ_STRING);
// 如果已经是INT编码就无需再编码了
// #define sdsEncodedObject(objptr) (objptr->encoding == OBJ_ENCODING_RAW || objptr->encoding == OBJ_ENCODING_EMBSTR)
if (!sdsEncodedObject(o)) return o;
// 只对引用计数为1的进行编码,共享的不编码
if (o->refcount > 1) return o;
/****************************
如果长度小于20就尝试编码成long
这里说是长度小于20能编码成long,大于不行
#define ULLONG_MAX 18446744073709551615ULL
20位,大于20位肯定不行
****************************/
len = sdslen(s);
if (len <= 20 && string2l(s,len,&value)) { // string2l()尝试把s转化成long存在value
/*
[1]这里的if是判断是否使用共享对象,标记为1,后面细说
*/
if ((server.maxmemory == 0 ||
!(server.maxmemory_policy & MAXMEMORY_FLAG_NO_SHARED_INTEGERS)) &&
value >= 0 &&
value < OBJ_SHARED_INTEGERS)
{
decrRefCount(o);
incrRefCount(shared.integers[value]);
return shared.integers[value];
} else {
/* 如果原先是RAW形式,成功用整数编码string,更换o的编码形式 ,并把ptr指向value */
if (o->encoding == OBJ_ENCODING_RAW) {
sdsfree(o->ptr);
o->encoding = OBJ_ENCODING_INT;
o->ptr = (void*) value;
return o;
/* 如果原先是EMBSTR形式,把o直接释放掉,重新创建一个robj */
/* createStringObjectFromLongLongForValue(long) 根据数字创建一个robj */
} else if (o->encoding == OBJ_ENCODING_EMBSTR) {
decrRefCount(o);
return createStringObjectFromLongLongForValue(value);
}
}
}
//************************ 整数编码失败,如果长度小于39,开始编码成EMB形式***********
if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT) {
robj *emb;
if (o->encoding == OBJ_ENCODING_EMBSTR) return o;
//【2】标记一下,稍后详解一下emb
emb = createEmbeddedStringObject(s,sdslen(s));
decrRefCount(o);
return emb;
}
/*********************************************编码EMB也失败了,就尝试删除sds的多余空间
//
trimStringObjectIfNeeded(o);
return o;
}
[1]共享对象
[2]emb编码
上面介绍过emb编码了,emb编码的好处有:
1.embstr 编码将创建字符串对象所需的内存分配次数从 raw 编码的两次降低为一次。
2.释放 embstr 编码的字符串对象只需要调用一次内存释放函数, 而释放 raw 编码的字符串对象需要调用两次内存释放函数。
3.因为 embstr 编码的字符串对象的所有数据都保存在一块连续的内存里面, 所以这种编码的字符串对象比起 raw 编码的字符串对象能够更好地利用缓存带来的优势。

创建emb的函数如下
robj *createEmbeddedStringObject(const char *ptr, size_t len) {
robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr8)+len+1);
struct sdshdr8 *sh = (void*)(o+1);
o->type = OBJ_STRING;
o->encoding = OBJ_ENCODING_EMBSTR;
o->ptr = sh+1;
o->refcount = 1;
if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
o->lru = (LFUGetTimeInMinutes()<<8) | LFU_INIT_VAL;
} else {
o->lru = LRU_CLOCK();
}
sh->len = len;
sh->alloc = len;
sh->flags = SDS_TYPE_8;
if (ptr == SDS_NOINIT)
sh->buf[len] = '\0';
else if (ptr) {
memcpy(sh->buf,ptr,len);
sh->buf[len] = '\0';
} else {
memset(sh->buf,0,len+1);
}
return o;
}
为什么是44?
redis用jemalloc分配内存,一次分配64b,减去各种头,剩下的就是44
下面看执行SET命令的函数
setGenericCommand()函数是以下命令: SET, SETEX, PSETEX, SETNX.的最底层实现
- SET:普通设定
- SETEX:设定定时删除,如果 key 已经存在, SETEX 命令将会替换旧的值
redis 127.0.0.1:6379> SETEX KEY_NAME TIMEOUT VALUE
#example
redis 127.0.0.1:6379> SETEX mykey 60 redis
OK
redis 127.0.0.1:6379> TTL mykey
60
redis 127.0.0.1:6379> GET mykey
"redis
- PSETEX:以毫秒为单位设置 key 的生存时间。
redis 127.0.0.1:6379> PSETEX key1 EXPIRY_IN_MILLISECONDS value1
redis 127.0.0.1:6379> PSETEX mykey 1000 "Hello"
OK
redis 127.0.0.1:6379> PTTL mykey
999
redis 127.0.0.1:6379> GET mykey
1) "Hello"
- SETNX: (SET if Not eXists) 命令在指定的 key 不存在时,为 key 设置指定的值。
//setGenericCommand()函数是以下命令: SET, SETEX, PSETEX, SETNX.的最底层实现
//flags 可以是NX或XX,由上面的宏提供
//expire 定义key的过期时间,格式由unit指定
//ok_reply和abort_reply保存着回复client的内容,NX和XX也会改变回复
//如果ok_reply为空,则使用 "+OK"
//如果abort_reply为空,则使用 "$-1"
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 */ //初始化,避免错误
//如果定义了key的过期时间
if (expire) {
//从expire对象中取出值,保存在milliseconds中,如果出错发送默认的信息给client
if (getLongLongFromObjectOrReply(c, expire, &milliseconds, NULL) != C_OK)
return;
// 如果过期时间小于等于0,则发送错误信息给client
if (milliseconds <= 0) {
addReplyErrorFormat(c,"invalid expire time in %s",c->cmd->name);
return;
}
//如果unit的单位是秒,则需要转换为毫秒保存
if (unit == UNIT_SECONDS) milliseconds *= 1000;
}
//lookupKeyWrite函数是为执行写操作而取出key的值对象
//如果设置了NX(不存在),并且在数据库中 找到 该key,或者
//设置了XX(存在),并且在数据库中 没有找到 该key
//回复abort_reply给client
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;
}
//在当前db设置键为key的值为val
setKey(c->db,key,val);
//设置数据库为脏(dirty),服务器每次修改一个key后,都会对脏键(dirty)增1
server.dirty++;
//设置key的过期时间
//mstime()返回毫秒为单位的格林威治时间
if (expire) setExpire(c->db,key,mstime()+milliseconds);
//发送"set"事件的通知,用于发布订阅模式,通知客户端接受发生的事件
notifyKeyspaceEvent(NOTIFY_STRING,"set",key,c->db->id);
//发送"expire"事件通知
if (expire) notifyKeyspaceEvent(NOTIFY_GENERIC,
"expire",key,c->db->id);
//设置成功,则向客户端发送ok_reply
addReply(c, ok_reply ? ok_reply : shared.ok);
}
GET命令
类似于set命令,get命令也是最终调用一个getGenericcommand的函数实现:
//GET 命令的底层实现
int getGenericCommand(client *c) {
robj *o;
//lookupKeyReadOrReply函数是为执行读操作而返回key的值对象,找到返回该对象,找不到会发送信息给client
//如果key不存在直接,返回0表示GET命令执行成功
if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk)) == NULL)
return C_OK;
//如果key的值的编码类型不是字符串对象
if (o->type != OBJ_STRING) {
addReply(c,shared.wrongtypeerr); //返回类型错误的信息给client,返回-1表示GET命令执行失败
return C_ERR;
} else {
addReplyBulk(c,o); //返回之前找到的对象作为回复给client,返回0表示GET命令执行成功
return C_OK;
}
}