tigh 发表于 2017-12-20 21:32:47

redis内部数据结构深入浅出

redis 1.2以后的协议中部分命令已经开始使用新的协议格式了(比如MSET),总之目前还是把包含边界字符当成非法的key,另外关于key的一个格式约定介绍下,object-type:id:field。比如user:1000:password,blog:xxidxx:title
2、string
  string是redis最基本的类型,而且string类型是二进制安全的。意思是redis的string可以包含任何数据,比如jpg图片或者序列化的对象。从内部实现来看其实string可以看作byte数组,最大上限是1G字节。
  

struct sdshdr {  
long len;
  
long free;
  
char buf[];
  
};
  

  buf是个char数组用于存贮实际的字符串内容。其实char和c#中的byte是等价的,都是一个字节 ,len是buf数组的长度,free是数组中剩余可用字节数。 由此可以理解为什么string类型是二进制安全的了。因为它本质上就是个byte数组。当然可以包含任何数据了。 另外string类型可以被部分命令按int处理,比如incr等命令,redis的其他类型像list,set,sorted set ,hash它们包含的元素与都只能是string类型。

编码
  字符串对象的编码可以是 INT、RAW 或 EMBSTR。如果保存的是整数值并且可以用long表示,那么编码会设置为INT。当字符串值得长度大于44字节使用RAW,小于等于44字节使用EMBSTR。
  Redis在3.0引入EMBSTR编码,这是一种专门用于保存短字符串的一种优化编码方式,这种编码和RAW编码都是用sdshdr简单动态字符串结构来表示。RAW编码会调用两次内存分配函数来分别创建redisObject和sdshdr结构,而EMBSTR只调用一次内存分配函数来分配一块连续的空间保存数据,比起RAW编码的字符串更能节省内存,以及能提升获取数据的速度。
  不过要注意!EMBSTR是不可修改的,当对EMBSTR编码的字符串执行任何修改命令,总会先将其转换成RAW编码再进行修改;而INT编码在条件满足的情况下也会被转换成RAW编码。

两种字符串对象编码方式的区别
  

/* Create a string object with EMBSTR encoding if it is smaller than  
* REIDS_ENCODING_EMBSTR_SIZE_LIMIT, otherwise the RAW encoding is
  
* used.
  
*
  
* The current limit of 39 is chosen so that the biggest string object
  
* we allocate as EMBSTR will still fit into the 64 byte arena of jemalloc. */
  

  
//sdshdr8的大小为3个字节,加上1个结束符共4个字节
  
//redisObject的大小为16个字节
  
//redis使用jemalloc内存分配器,且jemalloc会分配8,16,32,64等字节的内存
  
//一个embstr固定的大小为16+3+1 = 20个字节,因此一个最大的embstr字符串为64-20 = 44字节
  
#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44
  

  
// 创建字符串对象,根据长度使用不同的编码类型
  
// createRawStringObject和createEmbeddedStringObject的区别是:
  
// createRawStringObject是当字符串长度大于44字节时,robj结构和sdshdr结构在内存上是分开的
  
// createEmbeddedStringObject是当字符串长度小于等于44字节时,robj结构和sdshdr结构在内存上是连续的

  
robj *createStringObject(const char *ptr,>  
if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT)
  
return createEmbeddedStringObject(ptr,len);
  
else
  
return createRawStringObject(ptr,len);
  
}
  


字符串对象编码的优化
  

/* Try to encode a string object in order to save space */  
//尝试优化字符串对象的编码方式以节约空间
  
robj *tryObjectEncoding(robj *o) {
  
long value;
  
sds s = o->ptr;
  
size_t len;
  

  
/* Make sure this is a string object, the only type we encode
  
* in this function. Other types use encoded memory efficient
  
* representations but are handled by the commands implementing
  
* the type. */
  
serverAssertWithInfo(NULL,o,o->type == OBJ_STRING);
  

  
/* We try some specialized encoding only for objects that are
  
* RAW or EMBSTR encoded, in other words objects that are still
  
* in represented by an actually array of chars. */
  
//如果字符串对象的编码类型为RAW或EMBSTR时,才对其重新编码
  
if (!sdsEncodedObject(o)) return o;
  

  
/* It's not safe to encode shared objects: shared objects can be shared
  
* everywhere in the "object space" of Redis and may end in places where
  
* they are not handled. We handle them only as values in the keyspace. */
  
//如果refcount大于1,则说明对象的ptr指向的值是共享的,不对共享对象进行编码
  
if (o->refcount > 1) return o;
  

  
/* Check if we can represent this string as a long integer.
  
* Note that we are sure that a string larger than 20 chars is not
  
* representable as a 32 nor 64 bit integer. */
  
len = sdslen(s);            //获得字符串s的长度
  

  
//如果len小于等于20,表示符合long long可以表示的范围,且可以转换为long类型的字符串进行编码
  
if (len <= 20 && string2l(s,len,&value)) {
  
/* This object is encodable as a long. Try to use a shared object.
  
* Note that we avoid using shared integers when maxmemory is used
  
* because every object needs to have a private LRU field for the LRU
  
* algorithm to work well. */
  
if ((server.maxmemory == 0 ||
  
(server.maxmemory_policy != MAXMEMORY_VOLATILE_LRU &&
  
server.maxmemory_policy != MAXMEMORY_ALLKEYS_LRU)) &&
  
value >= 0 &&
  
value < OBJ_SHARED_INTEGERS)    //如果value处于共享整数的范围内
  
{
  
decrRefCount(o);                //原对象的引用计数减1,释放对象
  
incrRefCount(shared.integers); //增加共享对象的引用计数
  
return shared.integers;      //返回一个编码为整数的字符串对象
  
} else {      //如果不处于共享整数的范围
  
if (o->encoding == OBJ_ENCODING_RAW) sdsfree(o->ptr);   //释放编码为OBJ_ENCODING_RAW的对象
  
o->encoding = OBJ_ENCODING_INT;   //转换为OBJ_ENCODING_INT编码
  
o->ptr = (void*) value;             //指针ptr指向value对象
  
return o;
  
}
  
}
  

  
/* If the string is small and is still RAW encoded,
  
* try the EMBSTR encoding which is more efficient.
  
* In this representation the object and the SDS string are allocated
  
* in the same chunk of memory to save space and cache misses. */
  
//如果len小于44,44是最大的编码为EMBSTR类型的字符串对象长度
  
if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT) {
  
robj *emb;
  

  
if (o->encoding == OBJ_ENCODING_EMBSTR) return o;   //将RAW对象转换为OBJ_ENCODING_EMBSTR编码类型
  
emb = createEmbeddedStringObject(s,sdslen(s)); //创建一个编码类型为OBJ_ENCODING_EMBSTR的字符串对象
  
decrRefCount(o);    //释放之前的对象
  
return emb;
  
}
  

  
/* We can't encode the object...
  
*
  
* Do the last try, and at least optimize the SDS string inside
  
* the string object to require little space, in case there
  
* is more than 10% of free space at the end of the SDS string.
  
*

  
* We do that only for>  
* is only entered if the length of the string is greater than
  
* OBJ_ENCODING_EMBSTR_SIZE_LIMIT. */
  
//无法进行编码,但是如果s的未使用的空间大于使用空间的10分之1
  
if (o->encoding == OBJ_ENCODING_RAW &&
  
sdsavail(s) > len/10)
  
{
  
o->ptr = sdsRemoveFreeSpace(o->ptr);    //释放所有的未使用空间
  
}
  

  
/* Return the original object. */
  
return o;
  
}
  


3、list
  list类型其实就是一个每个子元素都是string类型的双向链表。所以push和pop命令的算法时间复杂度都是O(n),另外list会记录链表的长度。所以llen操作也是O(n).链表的最大长度是(2的32次方-1)。
  我们可以通过push,pop操作从链表的头部或者尾部添加删除元素。这使得list既可以用作栈,也可以用作队列。 有意思的是list的pop操作还有阻塞版本的。当我们pop一个list对象,如果list是空,或者不存在,会立即返回nil。但是阻塞版本的bpop可以则可以阻塞, 当然可以加超时时间,超时后也会返回nil。为什么要阻塞版本的pop呢,主要是为了避免轮询。 如果我们用list来实现一个工作队列。执行任务的thread可以调用阻塞版本的pop去 ,获取任务这样就可以避免轮询去检查是否有任务存在。当任务来时候工作线程可以立即返回,也可以避免轮询带来的延迟。

编码
  Redis3.0之前的列表对象的编码可以是ziplist或者linkedlist。当列表对象保存的字符串元素的长度都小于64字节并且保存的元素数量小于512个,使用ziplist编码,可以通过修改配置list-max-ziplist-value和list-max-ziplist-entries来修改这两个条件的上限值、两个条件任意一个不满足时,ziplist会变为linkedlist。
  从3.2开始Redis只使用quicklist作为列表的编码,quicklist是ziplist和双向链表的结合体,quicklist的每个节点都是一个ziplist。可以通过修改list-max-ziplist-size来设置一个quicklist节点上的ziplist的长度,取正值表示通过元素数量来限定ziplist的长度;负数表示按照占用字节数来限定,并且Redis规定只能取-1到-5这五个负值
  

-5: 每个quicklist节点上的ziplist大小不能超过64 Kb。(注:1kb => 1024 bytes)  
-4: 每个quicklist节点上的ziplist大小不能超过32 Kb。
  
-3: 每个quicklist节点上的ziplist大小不能超过16 Kb。
  
-2: 每个quicklist节点上的ziplist大小不能超过8 Kb。(默认值)
  
-1: 每个quicklist节点上的ziplist大小不能超过4 Kb。
  

  另外配置参数list-compress-depth表示一个quicklist两端不被压缩的节点个数
  

0: 表示都不压缩。默认值。  
1: 表示quicklist两端各有1个节点不压缩,中间的节点压缩。
  
2: 表示quicklist两端各有2个节点不压缩,中间的节点压缩。
  
3: 表示quicklist两端各有3个节点不压缩,中间的节点压缩。
  
依此类推…
  

  这里采用的是一种叫LZF的无损压缩算法

4、hash
  哈希对象的编码可以是ziplist或者hashtable。使用ziplist 编码时,保存同一键值对的两个节点总是紧挨在一起,键节点在前,值节点在后,同时满足以下两个条件将使用ziplist编码:


[*]  所有键和值的字符串长度小于64字节

[*]  键值对的数量小于512个

  不能满足这两个条件的都需要使用hashtable编码。以上两个上限值可以通过hash-max-ziplist-value和hash-max-ziplist-entries来修改
  hash是一个string类型的field和value的映射表,它的添加,删除操作都是O(1),hash特别适合用于存储对象。 相较于将对象的每个字段存成单个string类型,将一个对象存储在hash类型中会占用更少的内存,并且可以更方便的存取整个对象。
  省内存的原因是新建一个hash对象时开始是用zipmap(又称为small hash)来存储的。 这个zipmap其实并不是hash table,但是zipmap相比正常的hash实现可以节省不少hash本身需要的一些元数据存储开销。 尽管zipmap的添加,删除,查找都是O(n),但是由于一般对象的field数量都不太多。 所以使用zipmap也是很快的,也就是说添加删除平均还是O(1)。 如果field或者value的大小超出一定限制后,redis会在内部自动将zipmap替换成正常的hash实现,这个限制可以在配置文件中指定
  

hash-max-zipmap-entries 64 #配置字段最多64个  
hash-max-zipmap-value 512 #配置value最大为512字节
  


5、set
  集合对象的编码可以是intset或者hashtable。当满足以下两个条件时使用intset编码:


[*]  所有元素都是整数值

[*]  元素数量不超过512个

  可以修改set-max-intset-entries设置元素数量的上限。使用hashtable编码时,字典的每一个键都是字符串对象,每个字符串对象包含一个集合元素,字典的值全部设置为null。
  redis的set是string类型的无序集合。set元素最大可以包含(2的32次方-1)个元素。 set的是通过hash table实现的,所以添加,删除,查找的复杂度都是O(1)。hash table会随着添加或者删除自动的调整大小。 需要注意的是调整hash table大小时候需要同步(获取写锁)会阻塞其他读写操作。 可能不久后就会改用跳表(skip list)来实现跳表已经在sorted set中使用了 关于set集合类型除了基本的添加删除操作,其他有用的操作还包含集合的取并集(union),交集(intersection), 差集(difference)。

6、sorted set
  有序集合对象的编码可以是ziplist或者skiplist。同时满足以下条件时使用ziplist编码:


[*]  元素数量小于128个

[*]  所有member的长度都小于64字节

  以上两个条件的上限值可通过zset-max-ziplist-entries和zset-max-ziplist-value来修改。
  ziplist编码的有序集合使用紧挨在一起的压缩列表节点来保存,第一个节点保存member,第二个保存score。ziplist内的集合元素按score从小到大排序,score较小的排在表头位置。
  skiplist编码的有序集合底层是一个命名为zset的结构体,而一个zset结构同时包含一个字典和一个跳跃表。跳跃表按score从小到大保存所有集合元素。而字典则保存着从member到score的映射,这样就可以用O(1)的复杂度来查找member对应的score值。虽然同时使用两种结构,但它们会通过指针来共享相同元素的member和score,因此不会浪费额外的内存。
  和set一样sorted set也是string类型元素的集合,不同的是每个元素都会关联一个double类型的score。sorted set的实现是skip list和hash table的混合体,当元素被添加到集合中时,一个元素到score的映射被添加到hash table中,所以给定一个元素获取score的开销是O(1),另一个score到元素的映射被添加到skip list并按照score排序,所以就可以有序的获取集合中的元素。 添加,删除操作开销都是O(1)和skip list的开销一致,redis的skip list实现用的是双向链表,这样就可以逆序从尾部取元素。 sorted set最经常的使用方式应该是作为索引来使用,我们可以把要排序的字段作为score存储,对象的id当元素存储。
  参考:
  http://weixin.niurenqushi.com/article/2017-05-07/4842721.html
  https://mp.weixin.qq.com/s?__biz=MzA5ODM5MDU3MA==&mid=2650862682&idx=1&sn=41ea245ac0a9dbfc943dd1d03228a14e&chksm=8b66131fbc119a09ec27e70dca884425c5c1b54deca2f1fde471b54fe723a23b1b347752b7f8&scene=21#wechat_redirect
  https://mp.weixin.qq.com/s?__biz=MzA5ODM5MDU3MA==&mid=2650862680&idx=1&sn=978a6ea4971b6b98f266fa34bf1b49d8&chksm=8b66131dbc119a0bc82165c67dd6b13c1621b7ea289da6dd23e93adc6a86c43790c41d47da13&scene=21#wechat_redirect
  http://zhangtielei.com/posts/blog-redis-dict.html
  http://crazyjvm.iteye.com/blog/1720289
  http://blog.csdn.net/men_wen/article/details/70257207
页: [1]
查看完整版本: redis内部数据结构深入浅出