数据结构

动态字符串

1、Redis 中保存的 Key 是字符串,value 往往是字符串或字符串的集合,字符串是 Redis 中最常用的一种数据结构

2、Redis 没有直接使用 C 语言中的字符串,因为 C 语言字符串存在很多问题

(1)获取字符串长度的需要通过运算

(2)非二进制安全

(3)不可修改

3、Redis 构建一种新的字符串结构,称为简单动态字符串(Simple Dynamic String),简称 SDS

4、Redis 由 C 语言实现,其中 SDS 是一个结构体

5、SDS 之所以叫做动态字符串,是因为它具备动态扩容的能力

6、假如要给 SDS 追加一段字符串,这里首先会申请新内存空间

(1)如果新字符串小于 1M,则新空间 = 原字符串长度 * 2

(2)如果新字符串大于 1M,则新空间 = 原字符串长度 + 1MB,称为内存预分配

7、优点

(1)获取字符串长度的时间复杂度为 O(1)

(2)支持动态扩容

(3)减少内存分配次数

(4)二进制安全

 

IntSet

1、Redis 中 Set 集合的一种实现方式,基于整数数组实现,并且具备长度可变、有序等特征

2、结构

3、encoding 包含三种模式,表示存储的整数大小不同

4、为了方便查找,Redis 会将 IntSet 中所有的整数,按照升序依次保存在 contents 数组中

(1)结构(例)

(2)数组中每个数字都在 int16_t 范围内,因此采用的编码方式是 INTSET_ENC_INT16

(3)contents:2 字节 * 3 = 6 字节

(4)采用统一编码方式:因为数组在内存中为连续空间,已知起始地址、其编码方式,可以迅速寻址下一个元素

(5)寻址公式:起始地址 + 编码格式 * 角标(index)

5、升级流程

(1)当数字超出范围,IntSet 自动升级编码方式到合适的大小

(2)按照新的编码方式、元素个数,扩容数组

(3)倒序依次将数组中的元素拷贝到扩容后的正确位置

(4)将待添加的元素放入数组末尾

(5)修改 InSet 的 encoding 属性、length 属性

6、源码

(1)新增

intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
    uint8_t valenc = _intsetValueEncoding(value);// 获取当前值编码
    uint32_t pos; // 要插入的位置
    if (success) *success = 1;
    // 判断编码是不是超过了当前intset的编码
    if (valenc > intrev32ifbe(is->encoding)) {
        // 超出编码,需要升级
        return intsetUpgradeAndAdd(is,value);
    } else {
        // 在当前intset中查找值与value一样的元素的角标pos
        if (intsetSearch(is,value,&pos)) {
            if (success) *success = 0; //如果找到了,则无需插入,直接结束并返回失败
            return is;
        }
        // 数组扩容
        is = intsetResize(is,intrev32ifbe(is->length)+1);
        // 移动数组中pos之后的元素到pos+1,给新元素腾出空间
        if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
    }
    // 插入新元素
    _intsetSet(is,pos,value);
    // 重置元素长度
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
    return is;
}

(2)升级

static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
    // 获取当前intset编码
    uint8_t curenc = intrev32ifbe(is->encoding);
    // 获取新编码
    uint8_t newenc = _intsetValueEncoding(value);
    // 获取元素个数
    int length = intrev32ifbe(is->length); 
    // 判断新元素是大于0还是小于0 ,小于0插入队首、大于0插入队尾
    int prepend = value < 0 ? 1 : 0;
    // 重置编码为新编码
    is->encoding = intrev32ifbe(newenc);
    // 重置数组大小
    is = intsetResize(is,intrev32ifbe(is->length)+1);
    // 倒序遍历,逐个搬运元素到新的位置,_intsetGetEncoded按照旧编码方式查找旧元素
    while(length--) // _intsetSet按照新编码方式插入新元素
        _intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));
    /* 插入新元素,prepend决定是队首还是队尾*/
    if (prepend)
        _intsetSet(is,0,value);
    else
        _intsetSet(is,intrev32ifbe(is->length),value);
    // 修改数组长度
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
    return is;
}

7、Intset 可以看做是特殊的整数数组

(1)Redis 确保 Intset 中的元素唯一、有序

(2)具备类型升级机制,可以节省内存空间

(3)底层采用二分查找方式来查询

 

Dict

1、Redis 是一个键值型(Key-Value Pair)的数据库,可以根据键实现快速的增删改查,而键与值的映射关系通过 Dict 来实现

2、三部分组成

(1)哈希表(DictHashTable)

typedef struct dictht {
    // entry数组
    // 数组中保存的是指向entry的指针
    dictEntry **table; 
    // 哈希表大小
    unsigned long size;     
    // 哈希表大小的掩码,总等于size - 1
    unsigned long sizemask;     
    // entry个数
    unsigned long used; 
} dictht;

(2)哈希节点(DictEntry)

typedef struct dictEntry {
    void *key; // 键
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v; // 值
    // 下一个Entry的指针
    struct dictEntry *next; 
} dictEntry;

(3)字典(Dict)

typedef struct dict {
    dictType *type; // dict类型,内置不同的hash函数
    void *privdata;     // 私有数据,在做特殊hash运算时用
    dictht ht[2]; // 一个Dict包含两个哈希表,其中一个是当前数据,另一个一般是空,rehash时使用
    long rehashidx;   // rehash的进度,-1表示未进行
    int16_t pauserehash; // rehash是否暂停,1则暂停,0则继续
} dict;

3、向 Dict 添加键值对时

(1)Redis 首先根据 key 计算出 hash 值(h)

(2)然后利用 h & sizemask,计算元素应该存储到数组中的哪个索引位置

4、扩容

(1)Dict 中 HashTable 是数组结合单向链表的实现,当集合中元素较多时,必然导致哈希冲突增多,链表过长,则查询效率会大大降低

(2)Dict在每次新增键值对时,都会检查负载因子(LoadFactor = used / size) ,满足以下两种情况时会触发哈希表扩容

(3)哈希表的 LoadFactor >= 1,并且服务器没有执行 BGSAVE 或 BGREWRITEAOF 等后台进程

(4)哈希表的 LoadFactor > 5

static int _dictExpandIfNeeded(dict *d){
    // 如果正在rehash,则返回ok
    if (dictIsRehashing(d)) return DICT_OK;    // 如果哈希表为空,则初始化哈希表为默认大小:4
    if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);
    // 当负载因子(used/size)达到1以上,并且当前没有进行bgrewrite等子进程操作
    // 或者负载因子超过5,则进行 dictExpand ,也就是扩容
    if (d->ht[0].used >= d->ht[0].size &&
        (dict_can_resize || d->ht[0].used/d->ht[0].size > dict_force_resize_ratio){
            // 扩容大小为used + 1,底层会对扩容大小做判断,实际上找的是第一个大于等于 used+1 的 2^n
            return dictExpand(d, d->ht[0].used + 1);
        }
        return DICT_OK;
}

5、收缩

(1)Dict 除了扩容以外,每次删除元素时,也会对负载因子做检查

(2)当 LoadFactor < 0.1 时,会做哈希表收缩

// t_hash.c # hashTypeDeleted() 
if (dictDelete((dict*)o->ptr, field) == C_OK) {
    deleted = 1;
    // 删除成功后,检查是否需要重置Dict大小,如果需要则调用dictResize重置    /* Always check if the dictionary needs a resize after a delete. */
    if (htNeedsResize(o->ptr)) dictResize(o->ptr);
}
// server.c 文件
int htNeedsResize(dict *dict) {
    long long size, used;
    // 哈希表大小
    size = dictSlots(dict);
    // entry数量
    used = dictSize(dict);
    // size > 4(哈希表初识大小)并且 负载因子低于0.1
    return (size > DICT_HT_INITIAL_SIZE && (used*100/size < HASHTABLE_MIN_FILL));
}
int dictResize(dict *d){
    unsigned long minimal;
    // 如果正在做bgsave或bgrewriteof或rehash,则返回错误
    if (!dict_can_resize || dictIsRehashing(d)) 
        return DICT_ERR;
    // 获取used,也就是entry个数
    minimal = d->ht[0].used;
    // 如果used小于4,则重置为4
    if (minimal < DICT_HT_INITIAL_SIZE)
        minimal = DICT_HT_INITIAL_SIZE;
    // 重置大小为minimal,其实是第一个大于等于minimal的2^n
    return dictExpand(d, minimal);
}

6、rehash

(1)不管是扩容还是收缩,必定会创建新的哈希表,导致哈希表的 size 和 sizemask 变化,而 key 的查询与 sizemask 有关,因此必须对哈希表中的每一个 key 重新计算索引,插入新的哈希表,这个过程称为 rehash

(2)Dict 的 rehash 并不是一次性完成的,如果 Dict 中包含数百万 entry,要在一次 rehash 完成,极有可能导致主线程阻塞,因此 Dict 的 rehash 分多次、渐进式的完成,因此称为渐进式 rehash

(3)计算新 hash 表的 realeSize,值取决于当前要做的是扩容还是收缩

(4)如果是扩容,则新 size 为第一个大于等于 dict.ht[0].used + 1 的 2n

(5)如果是收缩,则新 size 为第一个大于等于 dict.ht[0].used 的 2n(不得小于 4)

(6)按照新的 realeSize 申请内存空间,创建 dictht,并赋值给 dict.ht[1]

(7)设置 dict.rehashidx = 0,标示开始 rehash

(8)每次执行新增、查询、修改、删除操作时,都检查 dict.rehashidx 是否大于 -1,如果是,则将 dict.ht[0].table[rehashidx] 的 entry 链表 rehash 到 dict.ht[1],并且将 rehashidx++,直至 dict.ht[0] 所有数据都 rehash 到 dict.ht[1]

(9)将 dict.ht[1] 赋值给 dict.ht[0],给 dict.ht[1] 初始化为空哈希表,释放原来 dict.ht[0] 内存

(10)将 rehashidx 赋值为 -1,代表 rehash 结束

(11)在 rehash 过程中,新增操作,则直接写入 ht[1],查询、修改、删除则会在 dict.ht[0] 和 dict.ht[1] 依次查找并执行,这样可以确保 ht[0] 数据只减不增,随着 rehash 最终为空

 

ZipList

1、一种特殊的双端链表,由一系列特殊编码的连续内存块组成,可以在任意一端进行压入 / 弹出操作,并且该操作的时间复杂度为 O(1)

2、ZipListEntry

(1)ZipList 中的 Entry 不如普通链表记录前后节点的指针,因为记录两个指针要占用 16 个字节,浪费内存

(2)结构

(3)previous_entry_length:前一节点的长度,占 1 个或 5 个字节

(4)如果前一节点的长度小于 254 字节,则采用 1 个字节来保存这个长度值

(5)如果前一节点的长度大于 254 字节,则采用 5 个字节来保存这个长度值,第一个字节为 0xfe,后四个字节才是真实长度数据

(6)encoding:编码属性,记录 content 数据类型(字符串 / 整数)、长度,占用 1 个 或 2 个或 5 个字节

(7)contents:负责保存节点的数据,可以是字符串或整数

(8)ZipList 中所有存储长度的数值均采用小端字节序,即低位字节在前,高位字节在后,例如:数值 0x1234,采用小端字节序后实际存储值为:0x3412

3、ZipListEntry 中的 encoding 编码分为字符串、整数

(1)字符串:如果 encoding 是以 00、01、10 开头,则证明 content 是字符串

(2)整数:如果 encoding 是以 11 开始,则证明 content 是整数,且 encoding 固定只占用 1 个字节

4、ZipList 连锁更新(Cascade Update)

(1)产生的连续多次空间扩展操作

(2)新增、删除较大数据时,都可能导致连锁更新的发生

 

QuickList

1、Redis 3.2 引入,一个双端链表,链表中的每个节点都是一个 ZipList

2、Redis 提供一个配置项:list-max-ziplist-size,避免 QuickList 中的每个 ZipList 中 entry 过多

(1)如果值为正,则代表 ZipList 允许的 entry 个数的最大值

(2)如果值为负,则代表 ZipList 的最大内存,分 5 种情况

(3)-1:每个 ZipList 的内存占用不能超过 4kb

(4)-2:每个 ZipList 的内存占用不能超过 8kb

(5)-3:每个 ZipList 的内存占用不能超过 16kb

(6)-4:每个 ZipList 的内存占用不能超过 32kb

(7)-5:每个 ZipList 的内存占用不能超过 64kb

(8)默认值为 -2

3、QuickList 对节点的 ZipList 压缩

(1)list-compress-depth:控制首尾不压缩的节点个数,因为链表一般都是从首尾访问较多,所以首尾不压缩

(2)0:默认值,代表不压缩

(3)1:表示 QuickList 首尾各有 1 个节点不压缩,中间节点压缩

(4)2:表示 QuickList 首尾各有 2 个节点不压缩,中间节点压缩

4、结构源码

(1)quicklist

typedef struct quicklist {
    // 头节点指针
    quicklistNode *head; 
    // 尾节点指针
    quicklistNode *tail; 
    // 所有ziplist的entry的数量
    unsigned long count;    
    // ziplists总数量
    unsigned long len;
    // ziplist的entry上限,默认值 -2 
    int fill : QL_FILL_BITS;         // 首尾不压缩的节点数量
    unsigned int compress : QL_COMP_BITS;
    // 内存重分配时的书签数量及数组,一般用不到
    unsigned int bookmark_count: QL_BM_BITS;
    quicklistBookmark bookmarks[];
} quicklist;

(2)quicklistNode

typedef struct quicklistNode {
    // 前一个节点指针
    struct quicklistNode *prev;
    // 下一个节点指针
    struct quicklistNode *next;
    // 当前节点的ZipList指针
    unsigned char *zl;
    // 当前节点的ZipList的字节大小
    unsigned int sz;
    // 当前节点的ZipList的entry个数
    unsigned int count : 16;  
    // 编码方式:1,ZipList; 2,lzf压缩模式
    unsigned int encoding : 2;
    // 数据容器类型(预留):1,其它;2,ZipList
    unsigned int container : 2;
    // 是否被解压缩。1:则说明被解压了,将来要重新压缩
    unsigned int recompress : 1;
    unsigned int attempted_compress : 1; //测试用
    unsigned int extra : 10; /*预留字段*/
} quicklistNode;

 

SkipList(跳表)

1、链表,但与传统链表相比有差异

(1)元素按照升序排列存储

(2)节点可能包含多个指针,指针跨度不同

2、源码

// t_zset.c
typedef struct zskiplist {
    // 头尾节点指针
    struct zskiplistNode *header, *tail;
    // 节点数量
    unsigned long length;
    // 最大的索引层级,默认是1
    int level;
} zskiplist;
// t_zset.c
typedef struct zskiplistNode {
    sds ele; // 节点存储的值
    double score;// 节点分数,排序、查找用
    struct zskiplistNode *backward; // 前一个节点指针
    struct zskiplistLevel {
        struct zskiplistNode *forward; // 下一个节点指针
        unsigned long span; // 索引跨度
    } level[]; // 多级索引数组
} zskiplistNode;

3、结构

4、特点

(1)跳跃表是一个双向链表,每个节点都包含 score 和 ele 值

(2)节点按照 score 值排序,score 值则按照 ele 字典排序

(3)每个节点都可以包含多层指针,层数是 1 到 32 之间的随机数

(4)不同层指针到下一个节点的跨度不同,层级越高,跨度越大

(5)增删改查效率与红黑树基本一致,实现却更简单

 

Redis对象

1、Redis 中的任意数据类型的键和值,都会被封装为一个 RedisObject

2、源码

2、Redis 中会根据存储的数据类型不同,选择不同的编码方式,共包含 11 种不同类型

 

String

1、基本编码方式:RAW,基于简单动态字符串(SDS)实现,存储上限为 512MB

2、如果存储的 SDS 长度小于 44 字节,则会采用 EMBSTR 编码,此时 object head 与 SDS 是一段连续空间,申请内存时只需要调用一次内存分配函数,效率更高

3、如果存储的字符串是整数值,并且大小在 LONG_MAX 范围内,则会采用 INT 编码:直接将数据保存在 RedisObject 的 ptr 指针位置(刚好 8 字节),不再需要 SDS

 

 

List

1、类似一个双端链表,可以从首、尾操作列表中的元素

2、在 3.2 之前,Redis 采用 ZipList、LinkedList 实现 List,当元素数量小于 512 并且元素大小小于 64 字节时,采用 ZipList 编码,超过则采用 LinkedList 编码

3、在 3.2 之后,Redis 统一采用 QuickList 实现 List

 

Set

1、Redis 中的单列集合

2、特点

(1)不保证有序性

(2)保证元素唯一

(3)可求交集、并集、差集

3、为了查询效率和唯一性,Set 采用 HT 编码(Dict)

(1)Dict 中的 key 存储元素,value 统一为 null

(2)当存储的所有数据都是整数,并且元素数量不超过 set-max-intset-entries(默认值 512)时,Set 采用 IntSet 编码,以节省内存

4、源码

(1)创建

robj *setTypeCreate(sds value) {
    // 判断value是否是数值类型 long long 
    if (isSdsRepresentableAsLongLong(value,NULL) == C_OK)
        // 如果是数值类型,则采用IntSet编码
        return createIntsetObject();
    // 否则采用默认编码,也就是HT
    return createSetObject();
}
robj *createIntsetObject(void) {
    // 初始化INTSET并申请内存空间
    intset *is = intsetNew();
    // 创建RedisObject
    robj *o = createObject(OBJ_SET,is);
    // 指定编码为INTSET
    o->encoding = OBJ_ENCODING_INTSET;
    return o;
}
robj *createSetObject(void) {
    // 初始化Dict类型,并申请内存
    dict *d = dictCreate(&setDictType,NULL);
    // 创建RedisObject
    robj *o = createObject(OBJ_SET,d);
    // 设置encoding为HT
    o->encoding = OBJ_ENCODING_HT;
    return o;
}

(2)添加

 

ZSet / SortedSet

1、其中每一个元素都需要指定一个 score 值、member 值

(1)根据 score 值排序后

(2)member 必须唯一

(3)根据 member 查询分数

2、编码结构

(1)SkipList:可以排序,并且可以同时存储 score 和 ele 值(member)

(2)HT(Dict):可以键值存储,并且可以根据 key 查找 value

// zset结构
typedef struct zset {
    // Dict指针
    dict *dict;
    // SkipList指针
    zskiplist *zsl;
} zset;
robj *createZsetObject(void) {
    zset *zs = zmalloc(sizeof(*zs));
    robj *o;
    // 创建Dict
    zs->dict = dictCreate(&zsetDictType,NULL);
    // 创建SkipList
    zs->zsl = zslCreate(); 
    o = createObject(OBJ_ZSET,zs);
    o->encoding = OBJ_ENCODING_SKIPLIST;
    return o;
}

3、当元素数量不多时,HT 和 SkipList 优势不明显,而且更耗内存,因此 ZSet 还会采用 ZipList 结构来节省内存,不过需要同时满足两个条件

(1)元素数量小于 zset_max_ziplist_entries,默认值 128

(2)每个元素都小于 zset_max_ziplist_value 字节,默认值 64

// zadd添加元素时,先根据key找到zset,不存在则创建新的zset
zobj = lookupKeyWrite(c->db,key);
if (checkType(c,zobj,OBJ_ZSET)) goto cleanup;
// 判断是否存在
if (zobj == NULL) { // zset不存在
    if (server.zset_max_ziplist_entries == 0 ||
        server.zset_max_ziplist_value < sdslen(c->argv[scoreidx+1]->ptr))
    { // zset_max_ziplist_entries设置为0就是禁用了ZipList,
        // 或者value大小超过了zset_max_ziplist_value,采用HT + SkipList
        zobj = createZsetObject();
    } else { // 否则,采用 ZipList
        zobj = createZsetZiplistObject();
    }
    dbAdd(c->db,key,zobj); 
}
// ....
zsetAdd(zobj, score, ele, flags, &retflags, &newscore);
robj *createZsetObject(void) {
    // 申请内存
    zset *zs = zmalloc(sizeof(*zs));
    robj *o;
    // 创建Dict
    zs->dict = dictCreate(&zsetDictType,NULL);
    // 创建SkipList
    zs->zsl = zslCreate();
    o = createObject(OBJ_ZSET,zs);
    o->encoding = OBJ_ENCODING_SKIPLIST;
    return o;
}
robj *createZsetZiplistObject(void) {
    // 创建ZipList
    unsigned char *zl = ziplistNew();
    robj *o = createObject(OBJ_ZSET,zl);
    o->encoding = OBJ_ENCODING_ZIPLIST;
    return o;
}
int zsetAdd(robj *zobj, double score, sds ele, int in_flags, int *out_flags, double *newscore) {
    /* 判断编码方式*/
    if (zobj->encoding == OBJ_ENCODING_ZIPLIST) {// 是ZipList编码
        unsigned char *eptr;
        // 判断当前元素是否已经存在,已经存在则更新score即可        if ((eptr = zzlFind(zobj->ptr,ele,&curscore)) != NULL) {
            //...略
            return 1;
        } else if (!xx) {
            // 元素不存在,需要新增,则判断ziplist长度有没有超、元素的大小有没有超
            if (zzlLength(zobj->ptr)+1 > server.zset_max_ziplist_entries
 		|| sdslen(ele) > server.zset_max_ziplist_value 
 		|| !ziplistSafeToAdd(zobj->ptr, sdslen(ele)))
            { // 如果超出,则需要转为SkipList编码
                zsetConvert(zobj,OBJ_ENCODING_SKIPLIST);
            } else {
                zobj->ptr = zzlInsert(zobj->ptr,ele,score);
                if (newscore) *newscore = score;
                *out_flags |= ZADD_OUT_ADDED;
                return 1;
            }
        } else {
            *out_flags |= ZADD_OUT_NOP;
            return 1;
        }
    }    // 本身就是SKIPLIST编码,无需转换
    if (zobj->encoding == OBJ_ENCODING_SKIPLIST) {
       // ...略
    } else {
        serverPanic("Unknown sorted set encoding");
    }
    return 0; /* Never reached. */
}

4、ZipList 本身没有排序功能,而且没有键值对的概念,因此需要有 ZSet 通过编码实现

(1)ZipList 是连续内存,因此 score、element 是连续的两个 entry, element 在前,score 在后

(2)score 越小越接近队首,score 越大越接近队尾,按照 score 值升序排列

 

Hash

1、结构与 ZSet 类似

(1)都是键值存储

(2)都需求根据键获取值

(3)键必须唯一

2、与 ZSet 区别

(1)ZSet 键是 member,值是 score;Hash 键、值都是任意值

(2)ZSet 根据 score 排序;Hash 则无需排序

3、Hash 底层采用的编码与 ZSet 基本一致,只需要把排序有关的 SkipList 去掉即可

(1)Hash 结构默认采用 ZipList 编码,用以节省内存,ZipList 中相邻的两个 entry 分别保存 field 和 value

(2)当数据量较大时,Hash 结构会转为 HT 编码,即 Dict,触发条件有两个

(3)ZipList 中的元素数量超过 hash-max-ziplist-entries(默认 512)

(4)ZipList 中的任意 entry 大小超过 hash-max-ziplist-value(默认 64 字节)

void hsetCommand(client *c) {// hset user1 name Jack age 21
    int i, created = 0;
    robj *o; // 略 ...    // 判断hash的key是否存在,不存在则创建一个新的,默认采用ZipList编码
    if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return;
    // 判断是否需要把ZipList转为Dict
    hashTypeTryConversion(o,c->argv,2,c->argc-1);
    // 循环遍历每一对field和value,并执行hset命令
    for (i = 2; i < c->argc; i += 2)
        created += !hashTypeSet(o,c->argv[i]->ptr,c->argv[i+1]->ptr,HASH_SET_COPY);    // 略 ...
}
robj *hashTypeLookupWriteOrCreate(client *c, robj *key) {
    // 查找key
    robj *o = lookupKeyWrite(c->db,key);
    if (checkType(c,o,OBJ_HASH)) return NULL;
    // 不存在,则创建新的
    if (o == NULL) {
        o = createHashObject();
        dbAdd(c->db,key,o);
    }
    return o;
}
robj *createHashObject(void) {
    // 默认采用ZipList编码,申请ZipList内存空间
    unsigned char *zl = ziplistNew();
    robj *o = createObject(OBJ_HASH, zl);
    // 设置编码
    o->encoding = OBJ_ENCODING_ZIPLIST;
    return o;
}
void hashTypeTryConversion(robj *o, robj **argv, int start, int end) {
    int i;
    size_t sum = 0;
    // 本来就不是ZipList编码,什么都不用做了
    if (o->encoding != OBJ_ENCODING_ZIPLIST) return;
    // 依次遍历命令中的field、value参数
    for (i = start; i <= end; i++) {
        if (!sdsEncodedObject(argv[i]))
            continue;
        size_t len = sdslen(argv[i]->ptr);
        // 如果field或value超过hash_max_ziplist_value,则转为HT
        if (len > server.hash_max_ziplist_value) {
            hashTypeConvert(o, OBJ_ENCODING_HT);
            return;
        }
        sum += len;
    }// ziplist大小超过1G,也转为HT
    if (!ziplistSafeToAdd(o->ptr, sum))
        hashTypeConvert(o, OBJ_ENCODING_HT);
}
int hashTypeSet(robj *o, sds field, sds value, int flags) {
    int update = 0;
    // 判断是否为ZipList编码
    if (o->encoding == OBJ_ENCODING_ZIPLIST) {
        unsigned char *zl, *fptr, *vptr;
        zl = o->ptr;
        // 查询head指针
        fptr = ziplistIndex(zl, ZIPLIST_HEAD);
        if (fptr != NULL) { // head不为空,说明ZipList不为空,开始查找key
            fptr = ziplistFind(zl, fptr, (unsigned char*)field, sdslen(field), 1);
            if (fptr != NULL) {// 判断是否存在,如果已经存在则更新
                update = 1;
                zl = ziplistReplace(zl, vptr, (unsigned char*)value,
                                    sdslen(value));
            }
        }
        // 不存在,则直接push
        if (!update) { // 依次push新的field和value到ZipList的尾部
            zl = ziplistPush(zl, (unsigned char*)field, sdslen(field),
                             ZIPLIST_TAIL);
            zl = ziplistPush(zl, (unsigned char*)value, sdslen(value),
                             ZIPLIST_TAIL);
        }
        o->ptr = zl;
        /* 插入了新元素,检查list长度是否超出,超出则转为HT */
        if (hashTypeLength(o) > server.hash_max_ziplist_entries)
            hashTypeConvert(o, OBJ_ENCODING_HT);
    } else if (o->encoding == OBJ_ENCODING_HT) {
        // HT编码,直接插入或覆盖
    } else {
        serverPanic("Unknown hash encoding");
    }
    return update;
}
posted @   半条咸鱼  阅读(88)  评论(0编辑  收藏  举报
(评论功能已被禁用)
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
点击右上角即可分享
微信分享提示