Redis数据结构之ziplist

news2025/7/21 13:31:12

前言

Redis 为了提高内存效率,设计了一种特殊的数据结构 ziplist(压缩列表)。ziplist 本质是一段字节数组,采用了一种紧凑的、连续存储的格式,可以有效地压缩数据,提高内存效率。

hash、zset 在数据量比较小时,会优先用 ziplist 存储数据,否则会分配大量指针,指针本身会占用空间,而且会增加内存的碎片化率。以 hash 为例,可以通过下列配置当 Entry 数量小于 512,且 数据长度小于 64 时使用压缩列表存储,否则采用 哈希表 存储。

hash-max-ziplist-entries=512
hash-max-ziplist-value=64

ZipList

ziplist 的优点:

  • 内存效率高,无需额外分配指针,数据紧凑的存储在一起
  • 高效的顺序访问,因为是一块连续的内存空间
  • 减少内存碎片

当然,ziplist 也有一些缺点,否则也不会数据量大了以后,Redis 就放弃它了。

  • 查找时间复杂度 O(N)
  • 内存重分配,ziplist 长度是固定的,无法动态扩展,只能重新申请一块内存
  • 连锁更新的问题

ziplist 的缺点还是很明显的,但是不能因为它有缺点就不用它,在数据量小,写入操作不多的时候,它确实可以节省内存。根据数据的特点使用不同的数据结构来存储,不正是 Redis 的设计哲学吗?

源码里对 ziplist 的布局描述如下,所有数据都是紧凑排列的:

<zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>
属性类型长度描述
zlbytesuint32_t4 字节ziplist 占用字节数
zltailuint32_t4 字节尾节点距离起始地址的距离
zllenuint16_t2 字节节点数量
entry变长节点
zlenduint8_t1 字节结尾符 0xff

因为记录了尾节点的位置,所以 ziplist 在查找头尾节点时很快就能找到,但是对于中间节点就需要遍历了。

Entry 代表 ziplist 里的元素,由三部分组成:

<previous_entry_length> <encoding> <content>
  • previous_entry_length:前一个 Entry 的长度,采用变长设计,占用 1 或 5 字节。如果前一个 Entry 长度小于 254 就用 1 字节存储,否则用 5 字节存储
  • encoding:数据的编码类型,记录了数据的类型和长度。高位 1 开头代表是数字,高位 0 开头代表是字节数组
  • content:存储节点的值

image.png
Redis 会根据值的类型来编码 encoding 和 content,举例:

  • encoding 1111 开头,后 4 位表示整型值,没有 content 部分
  • encoding 11111110 开头,代表 content 存储的是 8 位整型值
  • encoding 11000000 开头,代表 content 存储的是 16 位整型值
  • encoding 00 开头,后 6 位表示长度,content 是一个最大长度 63 的字节数组
  • encoding 01 开头,后 6 位 + 1 字节表示长度,content 是一个最大长度 2^14-1 的字节数组

Redis 定义了 encoding 的各种类型:

/* Different encoding/length possibilities */
#define ZIP_STR_MASK 0xc0
#define ZIP_INT_MASK 0x30
#define ZIP_STR_06B (0 << 6)
#define ZIP_STR_14B (1 << 6)
#define ZIP_STR_32B (2 << 6)
#define ZIP_INT_16B (0xc0 | 0<<4)
#define ZIP_INT_32B (0xc0 | 1<<4)
#define ZIP_INT_64B (0xc0 | 2<<4)
#define ZIP_INT_24B (0xc0 | 3<<4)
#define ZIP_INT_8B 0xfe

源码

ziplist 对应的源码文件是src/ziplist.csrc/ziplist.h

  • ziplistNew

创建一个空的 ziplist,因为本质是字节数组,所以没有结构体,返回的是一个指针。

unsigned char *ziplistNew(void) {
    // ziplist空间 header(4+4+2)+end(1)
    unsigned int bytes = ZIPLIST_HEADER_SIZE+ZIPLIST_END_SIZE;
    // 分配内存
    unsigned char *zl = zmalloc(bytes);
    // 写入总长度
    ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);
    // 写入zltail 因为还没元素,指向header结束位置
    ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);
    // 写入zllen=0
    ZIPLIST_LENGTH(zl) = 0;
    // 写入结尾符
    zl[bytes-1] = ZIP_END;
    return zl;
}
  • zlentry

Redis 定义了结构体 zlentry 来承载节点 Entry 数据,但这不代表 Entry 数据实际的存储方式。

typedef struct zlentry {
    unsigned int prevrawlensize; // 记录前一个节点长度占用的字节数 1或5
    unsigned int prevrawlen; // 前一个节点的长度
    unsigned int lensize; // 当前节点的长度占用的字节数 1或5
    unsigned int len; // 当前节点的长度
    unsigned int headersize; // 节点头部长度
    unsigned char encoding; // 编码方式 整形/字符串
    unsigned char *p; // 存储值的指针
} zlentry;
  • zipEntry

zlentry 结构比较复杂,Redis 提供了 zipEntry()方法对其编码:

static inline void zipEntry(unsigned char *p, zlentry *e) {
    // 解码前节点长度
    ZIP_DECODE_PREVLEN(p, e->prevrawlensize, e->prevrawlen);
    // 设置编码方式
    ZIP_ENTRY_ENCODING(p + e->prevrawlensize, e->encoding);
    ZIP_DECODE_LENGTH(p + e->prevrawlensize, e->encoding, e->lensize, e->len);
    assert(e->lensize != 0);
    e->headersize = e->prevrawlensize + e->lensize;
    e->p = p;
}
  • ziplistPush

用来向 ziplist 头部或尾部插入新的节点。

unsigned char *ziplistPush(unsigned char *zl, unsigned char *s, unsigned int slen, int where) {
    unsigned char *p;
    // 判断向头部还是尾部插入
    p = (where == ZIPLIST_HEAD) ? ZIPLIST_ENTRY_HEAD(zl) : ZIPLIST_ENTRY_END(zl);
    return __ziplistInsert(zl,p,s,slen);
}
  • __ziplistInsert

向 ziplist 指定位置插入新的节点,主要步骤:

  • 对元素的内容进行编码
  • 重新申请一块内存空间
  • 数据拷贝
/**
 * ziplist插入节点
 * @param zl ziplist指针
 * @param p 插入的位置 p的前面
 * @param s 内容
 * @param slen 长度
 * @return
 */
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;
    zlentry tail;
    if (p[0] != ZIP_END) { // 压缩列表不为空,根据p查找要插入的位置
        ZIP_DECODE_PREVLEN(p, prevlensize, prevlen); // 解码 prevlen 长度
    } else {
        unsigned char *ptail = ZIPLIST_ENTRY_TAIL(zl);
        if (ptail[0] != ZIP_END) {
            prevlen = zipRawEntryLengthSafe(zl, curlen, ptail);
        }
    }

    // 尝试编码为整型,如果失败则直接保存字符数组
    if (zipTryEncoding(s,slen,&value,&encoding)) {
        reqlen = zipIntSize(encoding);
    } else {
        reqlen = slen;
    }
    // reqlen代表当前节点长度 = 前置节点长度+encoding长度+content长度
    reqlen += zipStorePrevEntryLength(NULL,prevlen);
    reqlen += zipStoreEntryEncoding(NULL,encoding,slen);
    int forcelarge = 0;
    nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0;
    if (nextdiff == -4 && reqlen < 4) {
        nextdiff = 0;
        forcelarge = 1;
    }
    offset = p-zl;
    newlen = curlen+reqlen+nextdiff;
    zl = ziplistResize(zl,newlen);
    p = zl+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);
    }
    if (nextdiff != 0) {
        offset = p-zl;
        zl = __ziplistCascadeUpdate(zl,p+reqlen);
        p = zl+offset;
    }
    p += zipStorePrevEntryLength(p,prevlen);
    p += zipStoreEntryEncoding(p,encoding,slen);
    if (ZIP_IS_STR(encoding)) {
        memcpy(p,s,slen);
    } else {
        zipSaveInteger(p,value,encoding);
    }
    // 更新 zllen 字段
    ZIPLIST_INCR_LENGTH(zl,1);
    return zl;
}

通过源码发现,ziplist 插入元素的开销还是很大的,其实删除和更新也是一样,因为数据是紧密排列的,一旦要写入新数据,或者更新后的数据比之前大,就会出现没有存储空间的尴尬局面,此时不得不重新分配一块新的内存空间。

  • ziplistFind

ziplist 元素的查找,时间复杂度是 O(N),从头到尾遍历元素对比,直到遇到结尾符。

unsigned char *ziplistFind(unsigned char *zl, unsigned char *p, unsigned char *vstr, unsigned int vlen, unsigned int skip) {
    int skipcnt = 0;
    unsigned char vencoding = 0;
    long long vll = 0;
    size_t zlbytes = ziplistBlobLen(zl);
    while (p[0] != ZIP_END) { // 从头到尾遍历,直到遇到结尾符
        struct zlentry e;
        unsigned char *q;
        assert(zipEntrySafe(zl, zlbytes, p, &e, 1));
        q = p + e.prevrawlensize + e.lensize;
        if (skipcnt == 0) {
            if (ZIP_IS_STR(e.encoding)) {
                // 字符串的比较方法,逐个字符对比
                if (e.len == vlen && memcmp(q, vstr, vlen) == 0) {
                    return p;
                }
            } else {
                // 整型的比较
                if (vencoding == 0) {
                    if (!zipTryEncoding(vstr, vlen, &vll, &vencoding)) {
                        vencoding = UCHAR_MAX;
                    }
                    assert(vencoding);
                }
                if (vencoding != UCHAR_MAX) {
                    long long ll = zipLoadInteger(q, e.encoding);
                    if (ll == vll) {
                        return p;
                    }
                }
            }
            skipcnt = skip;
        } else {
            skipcnt--;
        }
        // 移动到下一个节点
        p = q + e.len;
    }
    return NULL;
}

连锁更新

除了增删改的开销大外,ziplist 还有一个风险点,就是连锁更新。
假设现在 ziplist 存储了 100 个元素,长度都是 253,此时每个元素刚好都可以用 1 字节来存储前一个元素的大小,大家相安无事。这个时候,突然有一个更新操作,把表头的数据长度给改了,超过了254。此时影响就不只是它自己了,因为它的长度超过了254,导致它的后一个元素不能再用 1 字节存储长度了,而要改成 5 字节,后一个元素改成 5 字节存储前一个元素的长度后,又导致自己的长度超了 254,又会导致 后后一个元素要使用 5 字节来存储它的长度,以此类推,产生连锁反应。
连锁更新带来的影响非常大,不过好在发生的概率不高。

尾巴

Redis 为了提高内存效率,当 hash、list、zset 存储的数据量较小时,会优先使用一个叫 ziplist 的数据结构存储数据。它本质是一个字节数组,把所有元素按照特定的编码格式编码后紧凑的排列在一起,提高内存效率。缺点是数据更新需要重新分配内存。
总体而言,ziplist 适用于存储较小的有序集合或哈希表数据。但对于大型数据、需要频繁扩展或需要高效迭代的情况,常规的有序集合或哈希表可能更合适。在使用ziplist时,需要权衡利弊,并根据实际需求选择合适的数据结构。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1102299.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CSS 基础知识-01

CSS 基础知识 1.CSS概述2. CSS引入方式3. 选择器4.文字控制属性5. 复合选择器6. CSS 特性7.背景属性8.显示模式9.选择器10.盒子模型 1.CSS概述 2. CSS引入方式 3. 选择器 4.文字控制属性 5. 复合选择器 6. CSS 特性 7.背景属性 8.显示模式 9.选择器 <!DOCTYPE html> <…

AutoGPT:自动化GPT原理及应用实践

一、AutoGPT介绍 想象一下&#xff0c;生活在这样一个世界里&#xff0c;你有一个人工智能助手&#xff0c;它不仅能够理解你的需求&#xff0c;而且还能够与你一起学习与成长。人工智能已无缝融入我们工作、生活&#xff0c;并帮助我们有效完成各种目标。大模型技术的发展与应…

【数据分享】2022年我国30米分辨率的地形粗糙度(起伏度)数据(免费获取)

地形数据&#xff0c;也叫DEM数据&#xff0c;是我们在各项研究中最常使用的数据之一。之前我们分享过2022年哥白尼30米分辨率的DEM数据&#xff0c;该数据被公认为是全球最佳的开源DEM数据之一&#xff0c;甚至没有之一&#xff08;可查看之前的文章获悉详情&#xff09;&…

Jmeter的性能测试

性能测试的概念 定义&#xff1a;软件的性能是软件的一种非功能特性&#xff0c;它关注的不是软件是否能够完成特定的功能&#xff0c;而是在完成该功能时展示出来的及时性。 由定义可知性能关注的是软件的非功能特性&#xff0c;所以一般来说性能测试介入的时机是在功能测试…

particles 粒子背景插件在vue3中的使用

particles 粒子背景插件在vue3中的使用 概述使用完整代码概述 npm 链接 https://www.npmjs.com/package/particles.vue3 GitHub地址 https://github.com/tsparticles/vue3 配置参数说明: color: String类型 默认’#dedede’。粒子颜色。particleOpacity: Number类型 默认0.7。…

【Linux】线程互斥与同步

文章目录 一.Linux线程互斥1.进程线程间的互斥相关背景概念2互斥量mutex3.互斥量的接口4.互斥量实现原理探究 二.可重入VS线程安全1.概念2.常见的线程不安全的情况3.常见的线程安全的情况4.常见的不可重入的情况5.常见的可重入的情况6.可重入与线程安全联系7.可重入与线程安全区…

【halcon】halcon轮廓总结之select_contours_xld

前言 select_contours_xld 我认为是一个非常常用且实用的算子&#xff0c;用于对轮廓进行筛选。 简介 这段文档描述了一个名为"SelectContoursXld"的操作&#xff0c;用于根据不同特征选择XLD&#xff08;XLD是一种图像数据表示形式&#xff0c;表示轮廓线&#x…

使用 Bard 的 Google Hotel 插件查询酒店

使用 Bard 的 Google Hotel 插件&#xff0c;您可以通过以下步骤找到符合您需求的酒店&#xff1a; 在 Google 搜索中打开 Bard 插件。输入您要搜索的城市或酒店名称。选择您要搜索的日期和入住人数。选择您要搜索的酒店类型和价格范围。单击“搜索”按钮。 Find hotels for a…

OpenCV实战完美实现眨眼疲劳检测!!

目录 1&#xff0c;项目流程 2&#xff0c;代码实现 3&#xff0c;结果展示 应用场景主要是在监控系统和驾驶员安全监测中&#xff1a; 监控系统&#xff1a;可以将该项目应用于监控摄像头的视频流中&#xff0c;实时检测闭眼行为。通过实时计算闭眼次数和眼睛长宽比&#x…

ubuntu20.04安装FTP服务

安装 sudo apt-get install vsftpd# 设置开机启动并启动ftp服务 systemctl enable vsftpd systemctl start vsftpd#查看其运行状态 systemctl status vsftpd #重启服务 systemctl restart vsftpdftp用户 sudo useradd -d /home/ftp/ftptest -m ftptest sudo passwd ftptest…

数字签名 及 数字证书 原理笔记

这里是对 数字签名 及 数字证书 原理该视频做的一个笔记&#xff0c;链接 前言 如果对一些加密算法不懂可以参考这篇文章 数字签名 小明发送文件给小红时对文件做出签名 将文件进行hash算法加密得到hash值&#xff0c;并且对该hash值使用私钥进行加密&#xff08;私钥加密的…

接口加密解决方案:Python的各种加密实现!

01、前言 在现代软件开发中&#xff0c;接口测试已经成为了不可或缺的一部分。随着互联网的普及&#xff0c;越来越多的应用程序都采用了接口作为数据传输的方式。接口测试的目的是确保接口的正确性、稳定性和安全性&#xff0c;从而保障系统的正常运行。 在接口测试中&…

【python海洋专题二十三】共用坐标轴

点击蓝字,关注我们 共用坐标轴上期画出subplot 但是坐标轴觉得多余 本期内容 优化坐标轴 1:未优化 优化 关键语句 # % 不显示坐标刻度 plt.xticks([])往期推荐 图片 【python海洋专题一】查看数据nc文件的属性并输出属性到txt文件 【python海洋专题二】读取水…

关于 Invalid bound statement (not found): 错误的解决

关于 Invalid bound statement not found: 错误的解决 前言错误原因解决方法1. 检查SQL映射文件2. 检查MyBatis配置3. 检查SQL语句4. 检查命名约定5. 清除缓存6. 启用日志记录 重点注意 结语 我是将军我一直都在&#xff0c;。&#xff01; 前言 当开发Java Spring Boot应用程…

手把手教你随机合并全部视频添加同一个文案的方法

今天&#xff0c;我将为你介绍一个简单易行的方法&#xff0c;只需两个步骤&#xff0c;让你轻松实现批量合并视频。 1. 在浏览器中搜索并下载“固乔智剪软件”&#xff0c;然后打开软件。这款软件是一款专业的视频剪辑工具&#xff0c;它提供了多种视频剪辑功能&#xff0c;包…

OpenCV模板匹配实现银行卡数字识别

目录 1&#xff0c;项目流程 2&#xff0c;代码流程解读 2.1 导入工具包 2.2 设置参数 2.3 指定信用卡类型 2.4 展示图像 ​编辑 2.5 读取一个模板图像 2.6 转化为灰度图--------->再转化为二值图像 2.7 计算轮廓 ​编辑 2.8 导入我们要识别的图像&…

设计海报都有哪些好用的软件推荐

在新媒体时代&#xff0c;设计在各个方面都是不可分割的。它最初是设计师的工作&#xff0c;并逐渐成为新媒体编辑的必要技能。 网页内容需要图片和文字&#xff0c;应用程序需要独特的风格基调&#xff0c;人们更喜欢分享视频和图片&#xff0c;而不是简单的文本。因此&#…

HTX 与 Zebec Protocol 深度合作,并将以质押者的身份参与 ZBC Staking

自 2023 年下半年以来&#xff0c;加密市场始终处于低迷的状态&#xff0c;在刚刚结束的 9 月&#xff0c;加密行业总融资额创下 2021 年以来的新低&#xff0c;同时在 DeFi 领域 DEX 交易额为 318.9 亿美元&#xff0c;同样创下 2021 年 1 月以来的新低。 对于投资者而言&…

jmeter接口测试断言

一、响应断言&#xff1a;对服务器的响应接口进行断言校验&#xff0c;来判断接口测试得到的接口返回值是否正确。 二、添加断言&#xff1a; 1、apply to&#xff1a;通常发出一个请求只触发一个请求&#xff0c;所以勾选“main sampie only”就可以&#xff1b;若发一个请求可…

基于Mediapipe的对象分类任务,CPU平台毫秒级别延迟

计算机视觉任务一直是GPU的天下,由于GPU超强的算力,也把计算机视觉任务提高了很多水平。但是在移动终端平台,如何来运行大型的模型,一直是大家关注的话题。Mediapipe是Google开源的可以直接运行在移动终端设备上的多任务模型,不仅在计算机视觉任务上,还是NLP自然语言处理…