• Redis字符串占用偏高


    前言

    直接上测试结果, 向redis中写入值(KV相同)为5000001..5100000共10万个字符串类型KV 数据.

    先自以为是的单计算一下需要多少内存: K/V相同, 均为整形, 各占用8字节, 那么一条数据会占用16字节. 10万数据大概占用155kb. 但事实真的是这样么? (如果是这样我也不用写这篇文章了)

    实际测试一下, 这是一个刚刚启动的redis状态, 还没有写入任何数据:

    redis_init

    使用脚本向其中set这10万条数据后:

    image-20221029184723768

    怎么说? 是不是发现比之前计算的数据要大的多? 算一下每条数据占用的内存大小: (7520440-871864)/100000≈66

    也就是说, 只是简单的整形KV, redis写入内存后也会占用66字节. 啊, 这?

    为了解释这个问题, 我研究了字符串类型在redis中的实现.

    字符串实现

    源码

    为了搞懂这个问题, 我去翻了Redis的源码. 以下源码基于Redis5.0

    首先, Redis在内存中使用一个map来记录所有的K/V映射关系. 其实现就是比较常见的数组加链表. 每一个元素使用结构体dictEntry保存, 源码如下:

    typedef struct dictEntry {
        void *key;
        union {
            void *val;
            uint64_t u64;
            int64_t s64;
            double d;
        } v;
        struct dictEntry *next;
    } dictEntry;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    其中三个字段各占用8字节, 用于保存元素的结构体占用16字节. next字段用于链表指向下个元素, k/v均是指向redisObject类型的指针(value 的整形先跳过, 后面会提到), 其源码如下:

    #define LRU_BITS 24
    typedef struct redisObject {
        unsigned type:4; // 4bit
        unsigned encoding:4; // 4bit
        unsigned lru:LRU_BITS; // 3字节
        int refcount; // 4字节
        void *ptr; // 8字节
    } robj;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    RedisObject类型共占用16字节. 很明显, 对应的数据就保存在ptr指针中. Redis中保存字符串有几种情况, 我用一张小图简单说明不同情况下redisObject的数据存放.

    image-20221029194124344

    为了减少内存占用, Redis在存储的时候根据不同的值使用了不同的存储方式. 为什么是44B 呢? 在注释中给出了解释, 是为了令连续内存占用为64B. 至于sds结构, 这里不展开了.

    再次计算

    至此, 我们可以重新计算内存占用了, 每个元素的内存占用大致如下:

    image-20221029195303049

    加起来就是8*7=56B. 但是这跟我们实际测量的66B还是有些差距的呀? 经过查找, Redis使用的内存分配库为jemalloc, 而jemalloc在内存分配的时候申请的内存为2的n 次方. 因此, 虽然dictEntry只需要24B 的内存, 但还是会分配32B.

    这样一来, 每个元素的内存占用就是: 32+8*4=64B, 与我们之前分析的几乎没差.

    value union

    在刚刚看dictEntry的时候, 还有个疑惑, 就是value是一个union, 整形可以直接保存在value变量中, 不需要额外的redisObject, 这样的话, 不就可以节省16B 么?

    是的, 没有错. 如果使用incr key命令将值放进去, 你就会发现内存占用减少了, 每个元素占用大概48B

    思考

    至此, 经过查找, 我们发现, 在向Redis中使用字符串类型保存整形的时候, 虽然我们以为每个元素只需要16B, 但最终还是会占用64B, 这意味着如果需要存储10G 的数据, 需要至少40G 的内存才能放得下, 这着实有些夸张了. 当存储的元素本身不大的时候, 元素所带的额外信息就会是内存占用的大头了.

    那么对于我们遇到的这种情况, 有没有什么办法能够减少内存占用呢?

    通过查找, 发现了压缩列表这个结构, 在Redis注释中对其进行了详细的解释.

    在元素不多的时候, hash/set/list等均使用ziplist, 其优势是可以减少内存占用, 但缺点是时间复杂度较高. 可以使用命令object encoding key查看元素内部类型

    于是, 我尝试着将k=5012345 v=5012345的数据, 改为k=5012 field=345 v=5012345的形式以hash的方式进行存储, 这里为了使用ziplist, 修改了配置文件hash-max-ziplist-entries=1024.当我将这10万条数据以hash的方式放进去后, 浅看结果:

    image-20221030115836367

    image-20221030113837803

    我想到内存占用会降低一些, 但没想到低了这么多, 每个元素占用内存大概: (1824216-871952)/100000≈10B.

    其中必然是存在这一些共享内容的, 比如元素5012345/5112345field均为345.

    但是, 这个内存占用着实有些令我震惊了, 回头抽时间好好研究研究压缩列表这玩意.

    注意, hash-max-ziplist-entries配置不要盲目的调高, 压缩列表虽然会降低内存占用, 但是会将时间复杂度降低到 O(n).

    原文地址: https://hujingnb.com/archives/860

  • 相关阅读:
    vue启动项目,npm run dev出现error:0308010C:digital envelope routines::unsupported
    项目部署上线
    EasyExcel实现文件上传-批量插入 文件下载
    (二)正点原子STM32MP135移植——TF-A移植
    nodejs+express+mysql简单博客搭建
    华为认证 | 2023年全国华为HCIE认证通过率是多少?
    编码踩坑——多线程可能带来意想不到的OOM
    前沿研究|16s+宏基因组binning揭示大型藻类附生微生物群落核心组成
    Celix是什么?能做什么?
    Java面向对象编程三大特征-多态
  • 原文地址:https://blog.csdn.net/qq_31725391/article/details/127597783