Redis 字符串对象
字符串对象的编码可以是 int、raw 或者 embstr.
如果一个字符串对象保存的是整数值, 并且这个整数值可以用 long
类型来表示, 那么字符串对象会将整数值保存在字符串对象结构的 ptr
属性里面 (将 void*
转换成 long
), 并将字符串对象的编码设置为 int
.
int
举个例子, 如果我们执行以下 SET
命令, 那么服务器将创建一个如图所示的 int
编码的字符串对象作为 number
键的值:
redis> SET number 10086
OK
redis> OBJECT ENCODING number
"int"
值得注意的是:
long
类型表示的是 C 语言中的 8 个字节的长整型.
编码int
则指的是 Redis 中的REDIS_ENCODING_INT
编码.
在 redis redisObject 数据结构 中有说过, ptr
是一个指针, 指向实际保存值的数据结构, 这个数据结构由 type
属性和 encoding
属性决定的.
我的系统是 Ubuntu 16.04 64 位, 用的是 Redis 3.0.7 版本, 可以存下 -9223372036854775808~9223372036854775807
范围的值. 也就是说, 这个范围内的值都会被编码为 int
.
raw
如果字符串对象保存的是一个字符串值, 并且这个字符串值的长度大于 39 字节, 那么字符串对象将使用一个简单动态字符串 (SDS) 来保存这个字符串值, 并将对象的编码设置为 raw
。
举个例子, 如果我们执行以下命令, 那么服务器将创建一个如图所示的 raw
编码的字符串对象作为 story
键的值.
redis> SET story "Long, long, long ago there lived a king ..."
OK
redis> STRLEN story
(integer) 43
redis> OBJECT ENCODING story
"raw"
embstr
如果字符串对象保存的是一个字符串值,并且这个字符串值的长度小于等于 39 字节,那么字符串对象将使用 embstr
编码的方式来保存这个字符串值.
embstr
编码是专门用于保存短字符串的一种优化编码方式, 这种编码和 raw 编码一样, 都使用 redisObject
结构和 sdshdr
结构来表示字符串对象, 但 raw
编码会调用两次内存分配函数来分别创建 redisObject
结构和 sdshdr
结构.
而 embstr
编码则通过调用一次内存分配函数来分配一块连续的空间, 空间中依次包含 redisObject
和 sdshdr
两个结构.
使用 embstr
编码的字符串对象来保存短字符串值有以下好处:
-
embstr
编码将创建字符串对象所需的内存分配次数从raw
编码的两次降低为一次. - 释放
embstr
编码的字符串对象只需要调用一次内存释放函数, 而释放raw
编码的字符串对象需要调用两次内存释放函数. - 因为
embstr
编码的字符串对象的所有数据都保存在一块连续的内存里面, 所以这种编码的字符串对象比起raw
编码的字符串对象能够更好地利用缓存带来的优势.
那么为什么小于等于 39 字节才使用 embstr
编码呢?
首先 embstr
是一块连续的内存区域, 由 redisObject
和 sdshdr
组成.
其中 redisObject
占 16 个字节, 当 buf 内的字符串长度是 39 时, sdshdr
的大小为 8+39+1=48
, 那一个字节是 '\0'
. 加起来刚好 64.
通过下面这种方式可以知道 redisObject
是占 16 个字节.
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:24; /* lru time (relative to server.lruclock) */
int refcount;
void *ptr;
} robj;
sizeof(redisObject);
下面这个代码是告诉你为啥是 48 个字节.
struct sdshdr {
unsigned int len; // 字符串长度
unsigned int free; // 剩余可使用的字节数
char buf[];};
unsigned int
占 4 个字节, 两个那么当然是 8 个字节了.buf
就是内容, 例子是 39 字节.
最后的 +1
指的是 '\0'
, 字符串可以用一个 '\0'
结尾的 char 数组来表示.
值得注意的是:
Redis 5.0.5 中, 39 变成了 44 字节.
这是因为使用的unsigned int
可以表示很大的范围, 但是对于很短的 sds 有很多的空间被浪费了 (两个unsigned int
8 个字节 ).
而将原来的sdshdr
改成了sdshdr8
,sdshdr16
,sdshdr32
,sdshdr64
, 里面的unsigned int
变成了uint8_t
,uint16_t
. 这样更加优化小 sds 的内存使用.
sds
与 sds 实现有关的数据类型有两个, 一个是 sds:
// 字符串类型的别名
typedef char *sds;
另一个是 sdshdr:
// 持有 sds 的结构
struct sdshdr {
// buf 中已被使用的字符串空间数量
int len;
// buf 中预留字符串空间数量
int free;
// 实际储存字符串的地方
char buf[];};
sds
只是字符数组类型 char*
的别名, 而 sdshdr
则用于持有和保存 sds
的信息.
如何追加字符串的?
// 扩展 sds 的预留空间,确保在调用这个函数之后,// sds 字符串后的 addlen + 1 bytes(for NULL)可写
sds sdsMakeRoomFor(sds s, size_t addlen) {
struct sdshdr *sh, *newsh;
size_t free = sdsavail(s);
size_t len, newlen;
// 预留空间可以满足本次拼接
if (free >= addlen) return s;
len = sdslen(s);
sh = (void*) (s-(sizeof(struct sdshdr)));
// 设置新 sds 的字符串长度
// 这个长度比完成本次拼接实际所需的长度要大
// 通过预留空间优化下次拼接操作
newlen = (len+addlen);
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
// 重分配 sdshdr
newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);
if (newsh == NULL) return NULL;
newsh->free = newlen - len;
// 只返回字符串部分
return newsh->buf;
}
从 newlen
变量的设置可以看出, 如果 newlen
小于 SDS_MAX_PREALLOC
, 那么 newlen
的实际值会比所需的长度多出一倍.
如果 newlen
的值大于 SDS_MAX_PREALLOC
, 那么 newlen
的实际值会加上 SDS_MAX_PREALLOC
(目前 3.0.7 版本的 SDS_MAX_PREALLOC 默认值为 1024 * 1024).
这种内存分配策略表明, 在对 sds 值进行扩展(expand)时, 总会预留额外的空间, 通过花费更多的内存, 减少了对内存进行重分配(reallocate)的次数, 并优化下次扩展操作的处理速度.
优化扩展操作的一个例子就是 APPEND 命令: APPEND
命令在执行时会调用 sdsMakeRoomFor, 多预留一部分空间. 当下次再执行 APPEND 的时候, 如果要拼接的字符串长度 addlen
不超过 sdshdr.free
(上次 APPEND
时预留的空间 ), 那么就可以略过内存重分配操作, 直接进行字符串拼接操作.
相反, 如果不使用这种策略, 那么每次进行 APPEND
都要对内存进行重分配.
注意, 初次创建 sds 值时并不会预留多余的空间 (查看前面给出的 sdsnewlen 定义), 只有在调用 sdsMakeRoomFor
起码一次之后, sds 才会有预留空间, 而且 sds 模块中也有相应的紧缩空间函数 sdsRemoveFreeSpace
.
因此, Redis 对 sds 值的这种扩展策略实际上不会浪费多少内存, 但它对一些需要多次执行字符串拼接的 Redis 模式来说, 却会获得不错的优化效果 (因为频繁的内存重分配是一种比较昂贵的工作).
sds VS c 语言字符串