接上篇 为何要用Redis,今天来聊聊具体的Redis数据类型与命令。本篇是深刻理解Redis的一个重要基础,请坐稳,前方 长文预警。redis
本系列内容基于:redis-3.2.12shell
文中不会介绍全部命令,主要是工做中常常遇到的。缓存
平时咱们看的大部分资料,都是简单粗暴的告诉咱们这个命令干吗,那个命令须要几个参数。这种方式只会知其然不知其因此然,本文从命令的时间复杂度到用途,再到对应类型在Redis低层采用何种结构保存数据,但愿让你们认识的更深入,使用时内心更有底。服务器
这里在阅读中请注意:虽然不少命令的时间复杂度都是O(n),但要注意其n所表明的具体含义。网络
文中会用到 OBJECT ENCODING xxx 来检查Redis的内部编码,它实际上是读取的 redisObject 结构体中 encoding 所表明的值。redisObject 对不一样类型的数据提供了统一的表现形式。数据结构
应该讲这是Redis中使用的最普遍的数据类型。该类型中的一些命令使用场景很是普遍。好比:函数
注:表格中仅仅说明了String中的12个命令,使用场景也仅列举了部分。post
咱们时常被人说教 MSET/MGET 这类命令少用,由于他们的时间复杂度是O(n),但其实这里注意,n表示的是本次设置或读取的key个数,因此若是你批量读取的key并非不少,每一个key的内容也不是很大,那么使用批量操做命令反而可以节省网络请求、传输的时间。性能
String类型的数据最终是如何在Redis中保存的呢?若是要细究的话,得先从 SDS
这个结构提及,不过今天先按下不表这源码部分的细节,只谈其内部保存的数据结构。最终咱们设置的字符串都会以三种形式中的一种被存储下来。测试
结合代码来看看Redis对这三种数据结构是如何决策的。当咱们在客户端使用命令 SET test hello,redis
时,客户端会把命令保存到一个buf中,而后按照收到的命令前后顺序依次执行。这其中有一个函数是:processMultibulkBuffer()
,它内部调用了 createStringObject()
函数:
#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44
robj *createStringObject(const char *ptr, size_t len) {
// 检查保存的字符串长度,选择对应类型
if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT)
return createEmbeddedStringObject(ptr,len);
else
return createRawStringObject(ptr,len);
}
复制代码
不懂C语言没关系,这里就是检查咱们输入的字符串 hello,redis
长度是否超过了 44 ,若是超过了用类型 raw
,没有则选用 embstr
。实验看看:
127.0.0.1:6379> SET test 12345678901234567890123456789012345678901234 // len=44
OK
127.0.0.1:6379> OBJECT encoding test
"embstr"
127.0.0.1:6379> SET test 123456789012345678901234567890123456789012345 // len=45
OK
127.0.0.1:6379> OBJECT encoding test
"raw"
复制代码
能够看到,一旦超过44,底层类型就变成了:raw
。等等,上面咱们不是还提到有一个 int
类型吗?从函数里边彻底看不到它的踪影啊?不急,当咱们输入的这条命令真的要开始执行时,也就是调用函数 setCommand()
时,会触发一个 tryObjectEncoding()
函数,这个函数的做用是试图对输入的字符串进行压缩,继续看看代码:
robj *tryObjectEncoding(robj *o) {
... ...
len = sdslen(s);
// 长度小于等于20,而且可以转成长整形
if(len <= 20 && string2l(s,len,&value)) {
o->encoding = OBJ_ENCODING_INT;
}
... ...
}
复制代码
这个函数被我大幅缩水了,可是简单咱们可以看到它判断长度是否小于等于20,而且尝试转化成整型,看看例子。
9223372036854775807 是8位字节可表示的最大整数,它的16进制形式是:0x7fffffffffffffffL
127.0.0.1:6379> SET test 9223372036854775807
OK
127.0.0.1:6379> OBJECT encoding test
"int"
127.0.0.1:6379> SET test 9223372036854775808 // 比上面大1
OK
127.0.0.1:6379> OBJECT encoding test
"embstr"
复制代码
至此,关于String的类型选择流程完毕了。这对咱们的参考价值是,咱们在使用String类型保存数据时,要考虑到底层对应不一样的类型,不一样的类型在Redis内部会执行不一样的流程,其所对应的执行效率、内存消耗都是不一样的。
咱们常常用它来保存一个结构化的数据,好比与一个用户相关的缓存信息。若是使用普通的String类型,须要对字符串进行序列化与反序列化,无疑增长额外开销,而且每次读取都只能所有读取出来。
Hash类型保存的结构话数据,很是像MySQL中的一条记录,咱们能够方便修改某一个字段,可是它更具灵活性,每一个记录可以含有不一样的字段。
在内部Hash类型数据可能存在两种类型的数据结构:
对于Hash,Redis 首先默认给它设置使用 ZipList
数据结构,后续根据条件进行判断是否须要改变。
void hsetCommand(client *c) {
int update;
robj *o;
if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return;
hashTypeTryConversion(o,c->argv,2,3);// 根据长度决策
... ...
update = hashTypeSet(o,c->argv[2],c->argv[3]);// 根据元素个数决策
addReply(c, update ? shared.czero : shared.cone);
... ...
}
复制代码
hashTypeLookupWriteOrCreate()
内部会调用 createHashObject()
建立Hash对象。
robj *createHashObject(void) {
unsigned char *zl = ziplistNew();
robj *o = createObject(OBJ_HASH, zl);
o->encoding = OBJ_ENCODING_ZIPLIST;// 设置编码 ziplist
return o;
}
复制代码
hashTypeTryConversion()
函数内部根据是否超过 hash_max_ziplist_value
限制的长度(64),来决定低层的数据结构。
void hashTypeTryConversion(robj *o, robj **argv, int start, int end) {
int i;
if (o->encoding != OBJ_ENCODING_ZIPLIST) return;
for (i = start; i <= end; i++) {
// 检查 field 与 value 长度是否超长
if (sdsEncodedObject(argv[i]) &&
sdslen(argv[i]->ptr) > server.hash_max_ziplist_value)
{
hashTypeConvert(o, OBJ_ENCODING_HT);
break;
}
}
}
复制代码
而后在函数 hashTypeSet()
中检查field个数是否超过了 hash_max_ziplist_entries
的限制(512个)。
int hashTypeSet(robj *o, robj *field, robj *value) {
int update = 0;
if (o->encoding == OBJ_ENCODING_ZIPLIST) {
... ...
// 检查field个数是否超过512
if (hashTypeLength(o) > server.hash_max_ziplist_entries)
hashTypeConvert(o, OBJ_ENCODING_HT);
} else if (o->encoding == OBJ_ENCODING_HT) {
... ...
}
... ...
return update;
}
复制代码
来验证一下上面的逻辑:
127.0.0.1:6379> HSET test name qweqweqwkejkksdjfslfldsjfkldjslkfqweqweqwkejkksdjfslfldsjfkldjsl
(integer) 1
127.0.0.1:6379> HSTRLEN test name
(integer) 64
127.0.0.1:6379> OBJECT encoding test
"ziplist"
127.0.0.1:6379> HSET test name qweqweqwkejkksdjfslfldsjfkldjslkfqweqweqwkejkksdjfslfldsjfkldjslq
(integer) 0
127.0.0.1:6379> HSTRLEN test name
(integer) 65
127.0.0.1:6379> OBJECT encoding test
"hashtable"
复制代码
关于key设置超过64,以及field个数超过512的限制状况,你们可自行测试。
List类型的用途也是很是普遍,主要归纳下经常使用场景:
List 的数据类型在低层实现有如下几种:
网络上有些文章说 LinkedList
在 Redis 4.0
以后的版本没有再被使用,实际上我发现 Redis 3.2.12
版本中也没有再使用该结构(不直接作为数据存储结构),包括 ZipList
在 3.2.12
版本中都没有再被直接用来存储数据了。
咱们作个实验来验证下,咱们设置一个List中有 1000 个元素,每一个元素value长度都超过 64 个字符。
127.0.0.1:6379> LLEN test
(integer) 1000
127.0.0.1:6379> OBJECT encoding test
"quicklist"
127.0.0.1:6379> LINDEX test 0
"qweqweqwkejkksdjfslfldsjfkldjslkfqweqweqwkejkksdjfslfldsjfkldjslq" // 65个字符
复制代码
不管咱们是改变列表元素的个数以及元素值的长度,其结构都是 QuickList
。还不信的话,咱们来看看代码:
void pushGenericCommand(client *c, int where) {
int j, waiting = 0, pushed = 0;
robj *lobj = lookupKeyWrite(c->db,c->argv[1]);
... ...
for (j = 2; j < c->argc; j++) {
c->argv[j] = tryObjectEncoding(c->argv[j]);
if (!lobj) {
// 建立 quick list
lobj = createQuicklistObject();
quicklistSetOptions(lobj->ptr, server.list_max_ziplist_size,
server.list_compress_depth);
dbAdd(c->db,c->argv[1],lobj);
}
listTypePush(lobj,c->argv[j],where);
pushed++;
}
... ...
}
复制代码
初始话时,调用 createQuicklistObject()
设置其低层数据结构是:quick list
。后续流程中没有地方再对该结构进行转化。
Set 类型的重要特性之一是能够去重、无序。它集合的性质在社交上能够有普遍的使用。
Set低层实现采用了两种数据结构:
该命令的代码以下,其中重要的两个关于决定类型的调用是:setTypeCreate()
和 setTypeAdd()
。
void saddCommand(client *c) {
robj *set;
... ...
if (set == NULL) {
// 初始化
set = setTypeCreate(c->argv[2]);
} else {
... ...
}
for (j = 2; j < c->argc; j++) {
// 内部会检查元素个数是否扩充到须要改变低层结构
if (setTypeAdd(set,c->argv[j])) added++;
}
... ...
}
复制代码
来看下 Set 结构对象的初始建立代码:
robj *setTypeCreate(robj *value) {
if (isObjectRepresentableAsLongLong(value,NULL) == C_OK)
return createIntsetObject(); // 使用IntSet
return createSetObject(); // 使用HashTable
}
复制代码
isObjectRepresentableAsLongLong()
内部判断其整数范围,若是是整数且没有超过最大整数就会使用 IntSet
来保存。不然使用 HashTable
。接着会检查元素的个数。
int setTypeAdd(robj *subject, robj *value) {
long long llval;
if (subject->encoding == OBJ_ENCODING_HT) {
... ...
} else if (subject->encoding == OBJ_ENCODING_INTSET) {
if (isObjectRepresentableAsLongLong(value,&llval) == C_OK) {
uint8_t success = 0;
subject->ptr = intsetAdd(subject->ptr,llval,&success);
if (success) {
/* Convert to regular set when the intset contains * too many entries. */
if (intsetLen(subject->ptr) > server.set_max_intset_entries)
setTypeConvert(subject,OBJ_ENCODING_HT);
return 1;
}
} else {
/* Failed to get integer from object, convert to regular set. */
setTypeConvert(subject,OBJ_ENCODING_HT);
... ...
return 1;
}
}
... ...
return 0;
}
复制代码
看看例子,这里以最大整数临界值为例:
127.0.0.1:6379> SADD test 9223372036854775807
(integer) 1
127.0.0.1:6379> OBJECT encoding test
"intset"
127.0.0.1:6379> SADD test 9223372036854775808
(integer) 1
127.0.0.1:6379> OBJECT encoding test
"hashtable"
复制代码
关于集合个数的测试,请自行完成观察。
如今的应用,都有一些排行榜之类的功能,好比投资网站显示投资金额排行,购物网站显示消费排行等。SortSet很是适合作这件事。经常使用来解决如下问题:
虽然有序集合也是集合,可是低层的数据结构却与Set不同,它也有两种数据结构,分别是:
这个转变成过程以下:
void zaddGenericCommand(client *c, int flags) {
if (zobj == NULL) {
if (xx) goto reply_to_client; /* No key + XX option: nothing to do. */
if (server.zset_max_ziplist_entries == 0 ||
server.zset_max_ziplist_value < sdslen(c->argv[scoreidx+1]->ptr))
{
zobj = createZsetObject();// skip list
} else {
zobj = createZsetZiplistObject();// zip list
}
dbAdd(c->db,key,zobj);
} else {
... ...
}
... ...
if (zobj->encoding == OBJ_ENCODING_ZIPLIST) {
if (zzlLength(zobj->ptr) > server.zset_max_ziplist_entries)
zsetConvert(zobj,OBJ_ENCODING_SKIPLIST);// 根据个数转化编码
if (sdslen(ele->ptr) > server.zset_max_ziplist_value)
zsetConvert(zobj,OBJ_ENCODING_SKIPLIST);// 根据长度转化编码
}
}
复制代码
这里以member长度超过64举例:
127.0.0.1:6379> ZADD test 77 qwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwer // member长度是 64
(integer) 1
127.0.0.1:6379> OBJECT encoding test
"ziplist"
127.0.0.1:6379> ZADD test 77 qwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwerq // member长度是65
(integer) 1
127.0.0.1:6379> OBJECT encoding test
"skiplist"
复制代码
当咱们member 超过64位长度时,低层的数据结构由 ZipList
转变成了 SkipList
。剩下的元素个数的测试,动动手试试看。
对于全局命令,无论对应的key是什么类型的数据,都是能够进行操做的。其中须要注意 KEYS 这个命令,不能用于线上,由于Redis单线程机制,若是内存中数据太多,会操做严重的阻塞,致使整个Redis服务都没法响应。
第一篇讲了为何要用Redis,本文又讲了绝大部分命令吧,以及Redis源码中对它们的一些实现,后续开始关注具体实践中的一些操做。但愿对你们有帮助,期待任何形式的批评与鼓励。
公众号:dayuTalk