1、字典
字典是Redis最基础的数据结构,一个字典即一个DB,Redis支持多DB
Redis字典采用Hash表实现,针对碰撞问题,其采用的方法为“链地址法”,即将多个哈希值相同的节点串连在一起, 从而解决冲突问题。
“链地址法”的问题在于当碰撞剧烈时,性能退化严重,例如:当有n个数据,m个槽位,如果m=1,则整个Hash表退化为链表,查询复杂度O(n)
为了避免Hash碰撞攻击,Redis随机化了Hash表种子
Redis的方案是“双buffer”,正常流程使用一个buffer,当发现碰撞剧烈(判断依据为当前槽位数和Key数的对比),分配一个更大的buffer,然后逐步将数据从老的buffer迁移到新的buffer。
2、Redis中的哈希表
前面提到Redis是个key/value存储系统,学过数据结构的人都知道,key/value最简单的数据结果就是哈希表(当然,还有其他方式,如B-树,二叉平衡树等),hash表的性能取决于两个因素:hash表的大小和解决冲突的方法。这两个是矛盾的:hash表大,则冲突少,但是用内存过大;而hash表小,则内存使用少,但冲突多,性能低。一个好的hash表会权衡这两个因素,使内存使用量和性能均尽可能低。在Redis中,哈希表是所有其他数据结构的基础,对于其他所有数据结构,如:string,set,sortedset,均是保存到hash表中的value中的,这个可以很容易的通过设置value的类型为void*做到。本文详细介绍了Redis中hash表的设计思想和实现方法。
3、Redis哈希表的设计思想
下图是从淘宝《Redis内存存储结构分析》中摘得的图片,主要描述Redis中hash表的组织方式。
在Redis中,hash表被称为字典(dictionary),采用了典型的链式解决冲突方法,即:当有多个key/value的key的映射值(每对key/value保存之前,会先通过类似HASH(key) MOD N的方法计算一个值,以便确定其对应的hash table的位置)相同时,会将这些value以单链表的形式保存;同时为了控制哈希表所占内存大小,redis采用了双哈希表(ht[2])结构,并逐步扩大哈希表容量(桶的大小)的策略,即:刚开始,哈希表ht[0]的桶大小为4,哈希表ht[1]的桶大小为0,待冲突严重(redis有一定的判断条件)后,ht[1]中桶的大小增为ht[0]的两倍,并逐步(注意这个词:”逐步”)将哈希表ht[0]中元素迁移(称为“再次Hash”)到ht[1],待ht[0]中所有元素全部迁移到ht[1]后,再将ht[1]交给ht[0](这里仅仅是C语言地址交换),之后重复上面的过程。
Redis字典结构如下:
- typedef struct dict {
- dictType *type;
- void *privdata;
- dictht ht[2]; //双buffer
- int rehashidx;
- int iterators;
- } dict;
- typedef struct dictht {
- dictEntry **table; //hash链表
- unsigned long size;
- unsigned long sizemask;
- unsigned long used;
- } dictht;
- //数据节点<K,V>
- typedef struct dictEntry {
- void *key;
- union {
- void *val;
- uint64_t u64;
- int64_t s64;
- } v;
- struct dictEntry *next;
- } dictEntry;
- typedef struct redisObject {
- unsigned type:4; //逻辑类型
- unsigned notused:2; /* Not used */
- unsigned encoding:4; //物理存储类型
- unsigned lru:22; /* lru time (relative to server.lruclock) */
- int refcount;
- void *ptr; //具体数据
- } robj;
- #define REDIS_STRING 0
- #define REDIS_LIST 1
- #define REDIS_SET 2
- #define REDIS_ZSET 3
- #define REDIS_HASH 4
- #define REDIS_ENCODING_RAW 0 /* Raw representation */
- #define REDIS_ENCODING_INT 1 /* Encoded as integer */
- #define REDIS_ENCODING_HT 2 /* Encoded as hash table */
- #define REDIS_ENCODING_ZIPMAP 3 /* Encoded as zipmap */
- #define REDIS_ENCODING_LINKEDLIST 4 /* Encoded as regular linked list */
- #define REDIS_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
- #define REDIS_ENCODING_INTSET 6 /* Encoded as intset */
- #define REDIS_ENCODING_SKIPLIST 7 /* Encoded as skiplist */
字符串
Redis的所有的key都采用字符串保存,另外,Redis也支持字符串类型的value。字符串类型即前文中看到的REDIS_STRING,其物理实现(enconding)可以为 REDIS_ENCODING_INT或REDIS_ENCODING_RAW
REDIS_ENCODING_INT保存为long型,即redis会尝试将一个字符串转化为Long,可以转换的话,即保存为REDIS_ENCODING_INT
否则,Redis会将REDIS_STRING保存为字符串类型,即REDIS_ENCODING_RAW
字符串类型在redis中用sds封装,主要为了解决长度计算和追加效率的问题,其定义如下:
- typedef char *sds;
- struct sdshdr {
- int len; <span style="font-family: Arial, Helvetica, sans-serif;">// buf 已占用长度</span>
- int free; <span style="font-family: Arial, Helvetica, sans-serif;">// buf 剩余可用长度</span>
- char buf[];<span style="font-family: Arial, Helvetica, sans-serif;">// 柔性数组,实际保存字符串数据的地方</span>
- };
- static inline size_t sdslen(const sds s) {
- struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
- return sh->len;
- }
- static inline size_t sdsavail(const sds s) {
- struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
- return sh->free;
- }
Hash表
Redis支持Value为Hash表,其逻辑类型为REDIS_HASH,REDIS_HASH可以有两种encoding方式: REDIS_ENCODING_ZIPLIST 和 REDIS_ENCODING_HTREDIS_ENCODING_HT即前文提到的字典的实现
REDIS_ENCODING_ZIPLIST即ZIPLIST,是一种双端列表,且通过特殊的格式定义,压缩内存适用,以时间换空间。ZIPLIST适合小数据量的读场景,不适合大数据量的多写/删除场景
Hash表默认的编码格式为REDIS_ENCODING_ZIPLIST,在收到来自用户的插入数据的命令时:
1,调用hashTypeTryConversion函数检查键/值的长度大于 配置的hash_max_ziplist_value(默认64)
2,调用hashTypeSet判断节点数量大于 配置的hash_max_ziplist_entries (默认512)
以上任意条件满足则将Hash表的数据结构从REDIS_ENCODING_ZIPLIST转为REDIS_ENCODING_HT
列表
Redis支持Value为一个列表,其逻辑类型为REDIS_SET,REDIS_SET有两种encoding方式,REDIS_ENCODING_ZIPLIST和REDIS_ENCODING_LINKEDLISTREDIS_ENCODING_ZIPLIST同上
REDIS_ENCODING_LINKEDLIST是比较正统双端链接表的实现:
- typedef struct listNode {
- struct listNode *prev;
- struct listNode *next;
- void *value;
- } listNode;
1,元素大小大于list-max-ziplist-value(默认64)
2,元素个数大于 配置的list-max-ziplist-entries(默认512)
集合
Redis支持Value为集合,其逻辑类型为REDIS_SET,REDIS_SET有两种encoding方式: REDIS_ENCODING_INTSET 和 REDIS_ENCODING_HT(同上)集合的元素类型和数量决定了encoding方式,默认采用REDIS_ENCODING_INTSET ,当满足以下条件时,转换为REDIS_ENCODING_HT:
1. 元素类型不是整数
2. 元素个数超过配置的“set-max-intset-entries”(默认512)
REDIS_ENCODING_INTSET是一个有序数组,使用的数据结构如下:
- typedef struct intset {
- uint32_t encoding; //3种类型:INTSET_ENC_INT16、INTSET_ENC_INT32、INTSET_ENC_INT64
- uint32_t length; //元素个数
- int8_t contents[]; //元素实际存放的位置,按序排放
- } intset;
有序集
Redis支持Value为有序集合,其逻辑类型为REDIS_ZSET,REDIS_ZSET有两种encoding方式: REDIS_ENCODING_ZIPLIST(同上)和 REDIS_ENCODING_SKIPLIST
REDIS_ENCODING_SKIPLIST使用的数据结构如下,其同事:
- typedef struct zset {
- dict *dict; //Hash字典(同前文)
- zskiplist *zsl; //跳跃表
- } zset;
字典中使用member作为key,score作为value,从而保证在O(1)时间对member的查找
跳跃表基于score做排序,从而保证在 O(logN) 时间内完成通过score对memer的查询
有续集默认也是采用REDIS_ENCODING_ZIPLIST的实现,当满足以下条件时,转换为REDIS_ENCODING_SKIPLIST
1. 数据元素个数超过配置的zset_max_ziplist_entries 的值(默认值为 128 )
2. 新添加元素的 member 的长度大于配置的 zset_max_ziplist_value 的值(默认值为 64 )
总结
针对同一种数据类型,Redis会根据元素类型/大小/个数采用不同的编码方式,不同的编码方式在内存使用效率/查询效率上差距巨大,在遇到内存问题时,可以尝试下修改相关参数:- hash-max-ziplist-entries 512
- hash-max-ziplist-value 64
- list-max-ziplist-entries 512
- list-max-ziplist-value 64
- set-max-intset-entries 512
- zset-max-ziplist-entries 128
- zset-max-ziplist-value 64