作者:Xie Zefan

起源:https://xiezefan.me/2017/05/0...

在探讨Redis内存压缩的时候,咱们须要理解一下几个Redis的相干常识。

压缩列表 ziplist

Redis的ziplist是用一段间断的内存来存储列表数据的一个数据结构,它的构造示例如下图

压缩列表组成示例--截图来自《Redis设计与实现》

  1. zlbytes: 记录整个压缩列表应用的内存大小
  2. zltail: 记录压缩列表表尾间隔起始地位有多少字节
  3. zllen: 记录压缩列表节点数量,值得注意的一点是,因为它只占了2个字节,所以最大值只能到65535,这意味着压缩列表长度大于65535的时候,就只能通过遍历整个列表来计算长度了
  4. zleng: 压缩列表末端标记位,固定值为OxFF
  5. entry1-N: 压缩列表节点, 具体构造如下图

压缩列表节点组成示例--截图来自《Redis设计与实现》

其中

  1. previous_entry_length: 上一个节点的长度
  2. encoding: content的编码以及长度
  3. content: 节点数据

当咱们查找一个节点的时候,次要进行一下操作:

  1. 依据zltail获取最初一个节点的地位
  2. 判断以后节点是否是指标节点
  3. 如果是,则返回数据
  4. 如果不是,则依据previous_entry_length计算上一个节点的起始地位,而后从新进行步骤2判断

通过上述的形容,咱们能够晓得,ziplist每次数据更新的复杂度大概是O(N),因为它须要对N个节点进行内存重调配,查找一个数据的时候,复杂度是O(N),最坏状况下须要遍历整个列表。

什么状况下会应用到ziplist呢?

Redis会应用到ziplist的数据结构是Hash与List。

Hash构造应用ziplist作为底层存储的两个条件是:

  1. 所有的键与值的字符串长度都小于64字节的时候
  2. 键与值对数据小于512个

只有上述条件任何一个不满足,Redis就会主动将这个Hash对象从ziplist转换成hashtable。但这两个阈值能够通过批改配置文件中的hash-max-ziplist-valuehash-max-ziplist-entries来变更。

List构造应用ziplist的条件与Hash构造一样,当条件不满足的时候,会从ziplist转换成linkedlist,同样咱们能够批改list-max-ziplist-valuehash-max-ziplist-entries来应用不同的阈值。

为什么Hash与List会应用ziplist来存储数据呢?

因为

  1. ziplist会比hashtable与ziplist节俭跟多的内存
  2. 内存中以间断块形式保留的数据比起hashtable与linkedlist应用的链表能够更快的载入缓存中
  3. 当ziplist的长度比拟小的时候,从ziplist读写数据的效率比hashtable或者linkedlist的差别并不大。

实质上,应用ziplist就是以工夫换空间的一种优化,然而他的工夫损坏小到简直能够忽略不计,但却能带来可观的内存缩小,所以满足条件时,Redis会应用ziplist作为Hash与List的存储构造。

实战

咱们先抛出问题,在广告程序化交易的过程中,咱们常常须要为一个广告投放打算定制人群包,其存储的模式如下:

人群包ID => [设施ID_1, 设施ID_2 ... 设施ID_N]

其中,人群包ID是Long型整数,设施ID是通过MD5解决,长度为32。
在业务场景中,咱们须要判断一个设施ID是否在一个人群包中,来决定是否投放广告。

在传统的应用Redis的场景, 咱们能够应用规范的KV构造来存储定向包数据,则存储形式如下:

{人群包ID}_{设施ID_1} => true{人群包ID}_{设施ID_2} => true

如果咱们想应用ziplist来持续内存压缩的话,咱们必须保障Hash对象的长度小于512,并且键值的长度小于64字节。咱们能够将KV构造的数据,存储到事后调配好的bucket中。

咱们先预估下,整个Redis集群预计包容的数据条数为10亿,那么Bucket的数量的计算公式如下:

bucket_count = 10亿 / 512 = 195W

那么咱们大略须要200W个Bucket(预估Bucket数量须要多预估一点,以防触发临界值问题)
咱们先以下公式计算BucketID:

bucket_id = CRC32(人群包ID + "_" + 设施ID) % 200W

那么数据在Redis的存储构造就变成

bucket_id => {   {人群包ID}_{设施ID_1} => true   {人群包ID}_{设施ID_2} => true}

这样咱们保障每个bucket中的数据项都小于512,并且长度均小于64字节。

咱们以2000W数据进行测试,前后两者的内存应用状况如下:

数据集大小存储模式Bucket数量所用内存碎片率Redis占用的内存
2000W压缩列表200W928M1.381.25G
2000W压缩列表5W785M1.481.14G
2000W间接存储-1.44G1.031.48G

在这里须要额定引入一个概念 – 内存碎片率。

内存碎片率 = 操作系统给Redis调配的内存 / Redis存储对象占用的内存

因为压缩列表在更新节点的时候,常常须要进行内存重调配,所以导致比拟高的内存碎片率。咱们在做技术计划比拟的时候,内存碎片率也是十分须要关注的指标之一。

但有很多伎俩能够缩小内存碎片率,比方内存对其,甚至更极其的间接重做整个Redis内存(利用快照或者从节点来重做内存)都能无效的减低内存碎片率。

咱们在本次试验中,因为存储的数值比拟大(单个KEY约34个字节),所以理论节俭内存不是很多,但仍然能节约35%-50%的内存应用。

在理论的生产环境中,咱们依据利用场景正当的设计压缩存储构造,局部业务甚至能达到节约70%的内存应用的成果。

压缩列表能节俭多少内存?

咱们当初晓得压缩列表是通过将节点紧凑的排列在内存中,从而节俭掉内存的。但他到底节俭了哪些内存从而能达到惊人的压缩率呢?

首先为了明确这个细节,咱们须要晓得一般Key-Value构造在Redis中是如何存储的。

typedef struct redisObject {    unsigned type:4;        // 对象的类型    unsigned encoding:4;    // 对象的编码    unsigned lru:LRU_BITS;  // LRU类型    int refcount;           // 援用计数    void *ptr;              // 指向底层数据结构的指针} robj;

Redis所有的对象都是通过上述构造来存储, 假如我存储Hello=>World这样一个健值对到Redis中,除了存储自身键值的数据外,还须要额定的24个字节来存储redisObject对象。

而Redis存储字符串应用的SDS数据结构

struct sdshdr8 {    uint8_t len;        // 所保留字符串的长度    uint8_t alloc;      // 调配的内存数量    unsigned char flags;// 标记位,用于判断sdshdr类型    char buf[];         // 字节数组,用户保留字符串};

如果字符串的长度无奈用unsigned int8来示意的话,Redis会应用能表白更大长度的sdshdr16构造来存储字符串。

并且,为了缩小批改字符串带来的内存重分类问题,Redis会进行内存预调配,所以可能你仅仅为了保留五个字符,但Redis会为你预调配10 bytes的内存。

这意味着当咱们存储Hello这个字符串的时候,你须要额定的3个以上的字节。

Oh~~~,我只想保留Hello=>World这十个字符的数据,居然须要的30~40个字节的数据来存储额定的信息,比存储数据自身的大小还多一些。这还没包含Redis保护字典表所须要的额定的内存空间。

那么假如咱们用ziplist来存储这个数据,咱们仅仅须要额定的2个字节用于存储previous_entry_length与encoding。具体的计算形式能够参考Redis源码或者《Redis设计与实现》第一局部第7章压缩列表。

总结

从以上比照,咱们能够看出,在存储越小的数据的时候,应用ziplist来进行数据压缩能失去更好的压缩率。
但副作用也很显著,ziplist的更新效率远远低于一般K-V模式,并且会造成额定的内存碎片率。

在Redis中存储大量数据的实际过程中,咱们常常会做一些小技巧来尽可能压迫Redis的存储能力。接下来筹备写一篇Redis内存压缩的小技巧。

近期热文举荐:

1.1,000+ 道 Java面试题及答案整顿(2021最新版)

2.终于靠开源我的项目弄到 IntelliJ IDEA 激活码了,真香!

3.阿里 Mock 工具正式开源,干掉市面上所有 Mock 工具!

4.Spring Cloud 2020.0.0 正式公布,全新颠覆性版本!

5.《Java开发手册(嵩山版)》最新公布,速速下载!

感觉不错,别忘了顺手点赞+转发哦!