Redis 内存压缩实战

点击上方“芋道源码”,选择“置顶公众号”

技术文章第一时间送达!

源码精品专栏

 

来源:http://t.cn/EUNBwLQ

  • 压缩列表 ziplist

  • 什么情况下会使用到ziplist呢?

  • 实战

  • 压缩列表能节省多少内存?

  • 总结


在讨论Redis内存压缩的时候,我们需要了解一下几个Redis的相关知识。

压缩列表 ziplist

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

640
压缩列表组成示例--截图来自《Redis设计与实现》
  1. zlbytes: 记录整个压缩列表使用的内存大小

  2. zltail: 记录压缩列表表尾距离起始位置有多少字节

  3. zllen: 记录压缩列表节点数量,值得注意的一点是,因为它只占了2个字节,所以最大值只能到65535,这意味着压缩列表长度大于65535的时候,就只能通过遍历整个列表来计算长度了

  4. zleng: 压缩列表末端标志位,固定值为OxFF

  5. entry1-N: 压缩列表节点, 具体结构如下图

640
压缩列表节点组成示例--截图来自《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内存压缩的小技巧。




欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢

640

已在知识星球更新源码解析如下:

  • 《精尽 Dubbo 源码解析系列》69 篇。

  • 《精尽 Netty 源码解析系列》61 篇。

  • 《精尽 Spring 源码解析系列》35 篇。

  • 《精尽 Spring MVC 源码解析系列》24 篇。

  • 《精尽 MyBatis 源码解析系列》34 篇。

  • 《数据库实体设计》17 篇。

  • 《精尽面试题》6 篇。持续更新...

  • 《精尽学习指南》6 篇。持续更新...


目前在知识星球更新了《精尽面试题》目录如下:

01. Dubbo 面试题

02. Netty 面试题

03. Spring 面试题

04. Spring MVC 面试题

05. Spring Boot 面试题

06. MyBatis 面试题


目前在知识星球更新了《精尽学习指南》目录如下:

01. Dubbo 学习指南

02. Netty 学习指南

03. Spring 学习指南

04. Spring MVC 学习指南

05. Spring Boot 学习指南

06. MyBatis 学习指南


目前在知识星球更新了《Dubbo 源码解析》目录如下:

01. 调试环境搭建
02. 项目结构一览
03. 配置 Configuration
04. 核心流程一览

05. 拓展机制 SPI

06. 线程池

07. 服务暴露 Export

08. 服务引用 Refer

09. 注册中心 Registry

10. 动态编译 Compile

11. 动态代理 Proxy

12. 服务调用 Invoke

13. 调用特性 

14. 过滤器 Filter

15. NIO 服务器

16. P2P 服务器

17. HTTP 服务器

18. 序列化 Serialization

19. 集群容错 Cluster

20. 优雅停机

21. 日志适配

22. 状态检查

23. 监控中心 Monitor

24. 管理中心 Admin

25. 运维命令 QOS

26. 链路追踪 Tracing

... 一共 69+ 篇

目前在知识星球更新了《Netty 源码解析》目录如下:

01. 调试环境搭建
02. NIO 基础
03. Netty 简介
04. 启动 Bootstrap

05. 事件轮询 EventLoop

06. 通道管道 ChannelPipeline

07. 通道 Channel

08. 字节缓冲区 ByteBuf

09. 通道处理器 ChannelHandler

10. 编解码 Codec

11. 工具类 Util

... 一共 61+ 篇


目前在知识星球更新了《数据库实体设计》目录如下:


01. 商品模块
02. 交易模块
03. 营销模块
04. 公用模块

... 一共 17+ 篇


目前在知识星球更新了《Spring 源码解析》目录如下:


01. 调试环境搭建
02. IoC Resource 定位
03. IoC BeanDefinition 载入

04. IoC BeanDefinition 注册

05. IoC Bean 获取

06. IoC Bean 生命周期

... 一共 35+ 篇


目前在知识星球更新了《Spring MVC 源码解析》目录如下:


01. Spring MVC 面试题
02. Spring MVC 学习指南
03. 调试环境搭建
04. 容器的初始化
05. 组件一览
06. 请求处理一览
07. HandlerMapping 组件
08. HandlerAdapter 组件
09. HandlerExceptionResolver 组件
10. RequestToViewNameTranslator 组件
11. LocaleResolver 组件
12. ThemeResolver 组件
13. ViewResolver 组件

14. MultipartResolver 组件

15. FlashMapManager 组件

... 一共 24+ 篇


目前在知识星球更新了《MyBatis 源码解析》目录如下:


01. 调试环境搭建
02. 项目结构一览
03. MyBatis 面试题合集

04. MyBatis 学习资料合集

05. MyBatis 初始化

06. SQL 初始化

07. SQL 执行

08. 插件体系

09. Spring 集成

... 一共 34+ 篇


源码不易↓↓↓

点赞支持老艿艿↓↓

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值