Redis数据结构之——ziplist

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: Redis数据结构之——ziplist

写在前面

以下内容是基于Redis 6.2.6 版本整理总结

一、压缩列表(ziplist)

当一个哈希键只包含少量键值对,并且每个键值对的键和值要么是小整数,要么是短字符串,Redis就会采用压缩列表作为哈希键的底层实现。

1.1 压缩列表的构成

压缩列表是Redis为节约内存而开发的,是由一系列特殊编码的连续内存组成。一个压缩列表可以包含任意多个节点,每个节点可以保存一个小整数或者一个短的字符串

ziplist 的结构如下

各字段说明:

  • zlbytes(4 字节): ziplist占用总的字节数
  • zltail(4 字节): ziplist 最后一个 entry 距离起始位置偏移的字节数
  • zllen(2 字节): ziplist 中 entry 的个数
  • zlend(1 字节):结束符(ziplist 以0xFF作为结束)

举个栗子:

说明:ziplist 的总长度为96字节(0x60的十进制),最后一个entry距离ziplist起始位置偏移了75字节(0x4B的十进制),ziplist中此时有3(0x03的十进制)个entry。

entry的结构如下:

各字段说明:

pre_entry_len:上一个entry的长度。占用的字节数取决于上一个节点的长度,如果上一个节点的长度小于254字节,pre_entry_len就占1个字节;如果大于等于254,pre_entry_len就占5个字节,而且,第一个字节会被设置为0xFE(十进制的254)

encoding:记录该节点content属性保存数据的类型及长度。开头说了 ziplist 用来存储小整数或者短的字符串。encoding规则如下:

存储字符串:

encoding的长度有1字节、2字节、和5字节:主要根据高位的00/01/10来区分不同的编码。

存储小整数:

1.2 ziplist源码分析
1.2.1 创建ziplist

redis用ziplistNew() 函数来创建ziplist。采用zmalloc分配空间,初始化ziplist的 header 和 end,共 11 个字节。

#define ZIPLIST_HEADER_SIZE     (sizeof(uint32_t)*2+sizeof(uint16_t))
#define ZIPLIST_END_SIZE        (sizeof(uint8_t))
/* Create a new empty ziplist. */
unsigned char *ziplistNew(void) {
    // bytes = 4 + 4 + 2 + 1 = 11 字节
    unsigned int bytes = ZIPLIST_HEADER_SIZE+ZIPLIST_END_SIZE;
    unsigned char *zl = zmalloc(bytes);
    // 初始化 zlbytes = 11
    ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);
    // 初始化 zltail = 10,此时还没有entry
    ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);
    // 初始化 zllen = 10
    ZIPLIST_LENGTH(zl) = 0;
    // 初始化 zlend = 255
    zl[bytes-1] = ZIP_END;
    return zl;
}

1.2.2 ziplist的插入

__ziplistInsert() 函数,将新节点查到给定节点之后。

/* Insert item at "p". */
unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {
    size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), reqlen, newlen;
    unsigned int prevlensize, prevlen = 0;
    size_t offset;
    int nextdiff = 0;
    unsigned char encoding = 0;
    long long value = 123456789; /* initialized to avoid warning. Using a value
                                    that is easy to see if for some reason
                                    we use it uninitialized. */
    zlentry tail;
    /* Find out prevlen for the entry that is inserted. */
    if (p[0] != ZIP_END) {
        ZIP_DECODE_PREVLEN(p, prevlensize, prevlen);
    } else {
        unsigned char *ptail = ZIPLIST_ENTRY_TAIL(zl);
        if (ptail[0] != ZIP_END) {
            prevlen = zipRawEntryLengthSafe(zl, curlen, ptail);
        }
    }
    /* See if the entry can be encoded */
    if (zipTryEncoding(s,slen,&value,&encoding)) {
        /* 'encoding' is set to the appropriate integer encoding */
        reqlen = zipIntSize(encoding);
    } else {
        /* 'encoding' is untouched, however zipStoreEntryEncoding will use the
         * string length to figure out how to encode it. */
        reqlen = slen;
    }
    /* We need space for both the length of the previous entry and
     * the length of the payload. */
    reqlen += zipStorePrevEntryLength(NULL,prevlen);
    reqlen += zipStoreEntryEncoding(NULL,encoding,slen);
    /* When the insert position is not equal to the tail, we need to
     * make sure that the next entry can hold this entry's length in
     * its prevlen field. */
    int forcelarge = 0;
    nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0;
    if (nextdiff == -4 && reqlen < 4) {
        nextdiff = 0;
        forcelarge = 1;
    }
    /* Store offset because a realloc may change the address of zl. */
    offset = p-zl;
    newlen = curlen+reqlen+nextdiff;
    zl = ziplistResize(zl,newlen);
    p = zl+offset;
    /* Apply memory move when necessary and update tail offset. */
    if (p[0] != ZIP_END) {
        /* Subtract one because of the ZIP_END bytes */
        memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff);
        /* Encode this entry's raw length in the next entry. */
        if (forcelarge)
            zipStorePrevEntryLengthLarge(p+reqlen,reqlen);
        else
            zipStorePrevEntryLength(p+reqlen,reqlen);
        /* Update offset for tail */
        ZIPLIST_TAIL_OFFSET(zl) =
            intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+reqlen);
        /* When the tail contains more than one entry, we need to take
         * "nextdiff" in account as well. Otherwise, a change in the
         * size of prevlen doesn't have an effect on the *tail* offset. */
        assert(zipEntrySafe(zl, newlen, p+reqlen, &tail, 1));
        if (p[reqlen+tail.headersize+tail.len] != ZIP_END) {
            ZIPLIST_TAIL_OFFSET(zl) =
                intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+nextdiff);
        }
    } else {
        /* This element will be the new tail. */
        ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(p-zl);
    }
    /* When nextdiff != 0, the raw length of the next entry has changed, so
     * we need to cascade the update throughout the ziplist */
    if (nextdiff != 0) {
        offset = p-zl;
        zl = __ziplistCascadeUpdate(zl,p+reqlen);
        p = zl+offset;
    }
    /* Write the entry */
    p += zipStorePrevEntryLength(p,prevlen);
    p += zipStoreEntryEncoding(p,encoding,slen);
    if (ZIP_IS_STR(encoding)) {
        memcpy(p,s,slen);
    } else {
        zipSaveInteger(p,value,encoding);
    }
    ZIPLIST_INCR_LENGTH(zl,1);
    return zl;
}

二、总结

  1. ziplist是Redis为了节约内存而实现的一种顺序型数据结构
  2. 压缩列表中的节点用来存储较短的字符串或小整数
  3. 添加和删除节点可能会引发连锁更新,但这种概率很低

文章参考与<零声教育>的C/C++linux服务期高级架构系统教程学习

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
8天前
|
存储 NoSQL 关系型数据库
redis数据结构与应用场景
Redis 是一款开源、免费的内存数据库,常用于处理高并发和大数据场景下的热点数据访问,以提升性能。它支持 key-value 存储及多种数据结构,如字符串、列表、集合和哈希表。数据可持久化到磁盘,与 MySQL 等传统数据库相比,Redis 作为缓存能提供更快的读写速度。Redis 应用场景包括:使用字符串进行计数(如商品库存、点赞数)、利用列表实现消息队列或展示最新商品、使用集合去重和计算交集等,以及通过有序集合进行自动排序(如商品热度榜)。
|
13天前
|
存储 NoSQL 算法
Redis入门到通关之Redis数据结构-Hash篇
Redis入门到通关之Redis数据结构-Hash篇
19 1
|
13天前
|
存储 NoSQL Redis
Redis入门到通关之Redis数据结构-Set篇
Redis入门到通关之Redis数据结构-Set篇
20 1
|
13天前
|
存储 NoSQL Redis
Redis入门到通关之Redis数据结构-ZSet篇
Redis入门到通关之Redis数据结构-ZSet篇
21 1
|
13天前
|
存储 NoSQL Redis
Redis入门到通关之Redis数据结构-List篇
Redis入门到通关之Redis数据结构-List篇
33 1
|
13天前
|
存储 JavaScript 前端开发
什么是堆?什么是栈?他们之间从区别和联系
什么是堆?什么是栈?他们之间从区别和联系
30 0
|
2天前
|
算法 测试技术 C++
【栈 最小公倍数 最大公约数】2197. 替换数组中的非互质数
【栈 最小公倍数 最大公约数】2197. 替换数组中的非互质数
【栈 最小公倍数 最大公约数】2197. 替换数组中的非互质数
|
2天前
|
机器学习/深度学习 算法 测试技术
【单调栈】3113. 边界元素是最大值的子数组数目
【单调栈】3113. 边界元素是最大值的子数组数目
|
3天前
|
C语言
猿创征文|栈和队列OJ刷题
猿创征文|栈和队列OJ刷题
[数据结构]~栈和队列(0-1)
[数据结构]~栈和队列(0-1)