Redis 内存压缩实战,学习了!

简介: 在讨论Redis内存压缩的时候,我们需要了解一下几个Redis的相关知识。压缩列表 ziplistRedis的ziplist是用一段连续的内存来存储列表数据的一个数据结构,它的结构示例如下图

image.png

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


zlbytes: 记录整个压缩列表使用的内存大小

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

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

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

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

image.png

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


其中


previous_entry_length: 上一个节点的长度

encoding: content的编码以及长度

content: 节点数据

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


根据zltail获取最后一个节点的位置

判断当前节点是否是目标节点

如果是,则返回数据

如果不是,则根据previous_entry_length计算上一个节点的起始位置,然后重新进行步骤2判断

通过上述的描述,我们可以知道,ziplist每次数据更新的复杂度大约是O(N),因为它需要对N个节点进行内存重分配,查找一个数据的时候,复杂度是O(N),最坏情况下需要遍历整个列表。


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

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


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


所有的键与值的字符串长度都小于64字节的时候

键与值对数据小于512个

只要上述条件任何一个不满足,Redis就会自动将这个Hash对象从ziplist转换成hashtable。但这两个阈值可以通过修改配置文件中的hash-max-ziplist-value与hash-max-ziplist-entries来变更。


List结构使用ziplist的条件与Hash结构一样,当条件不满足的时候,会从ziplist转换成linkedlist,同样我们可以修改list-max-ziplist-value与hash-max-ziplist-entries来使用不同的阈值。


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


因为


ziplist会比hashtable与ziplist节省跟多的内存

内存中以连续块方式保存的数据比起hashtable与linkedlist使用的链表可以更快的载入缓存中

当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

1

如果我们想使用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数据进行测试,前后两者的内存使用情况如下:

image.png

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

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

因为压缩列表在更新节点的时候,经常需要进行内存重分配,所以导致比较高的内存碎片率。我们在做技术方案比较的时候,内存碎片率也是非常需要关注的指标之一。


但有很多手段可以减少内存碎片率,比如内存对其,甚至更极端的直接重做整个Redis内存(利用快照或者从节点来重做内存)都能有效的减低内存碎片率。


我们在本次实验中,因为存储的数值比较大(单个KEY约34个字节),所以实际节省内存不是很多,但依然能节约35%-50%的内存使用。


在实际的生产环境中,我们根据应用场景合理的设计压缩存储结构,部分业务甚至能达到节约70%的内存使用的效果。


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

我们现在知道压缩列表是通过将节点紧凑的排列在内存中,从而节省掉内存的。但他究竟节省了哪些内存从而能达到惊人的压缩率呢?


首先为了明白这个细节,我们需要知道普通Key-Value结构在Redis中是如何存储的。

image.png

假如字符串的长度无法用unsigned int8来表示的话,Redis会使用能表达更大长度的sdshdr16结构来存储字符串。


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


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


Oh,我只想保存Hello=>World这十个字符的数据,竟然需要的3040个字节的数据来存储额外的信息,比存储数据本身的大小还多一些。这还没包括Redis维护字典表所需要的额外的内存空间。


那么假设我们用ziplist来存储这个数据,我们仅仅需要额外的2个字节用于存储previous_entry_length与encoding。具体的计算方式可以参考Redis源码或者《Redis设计与实现》第一部分第7章压缩列表。


总结

从以上对比,我们可以看出,在存储越小的数据的时候,使用ziplist来进行数据压缩能得到更好的压缩率。 但副作用也很明显,ziplist的更新效率远远低于普通K-V模式,并且会造成额外的内存碎片率。


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

目录
相关文章
|
运维 NoSQL 测试技术
Redis:内存陡增100%深度复盘
本文深度分析了Redis内存陡增100%的一些细节和解决方案。
408 1
Redis:内存陡增100%深度复盘
|
2月前
|
NoSQL 算法 Redis
【Docker】(3)学习Docker中 镜像与容器数据卷、映射关系!手把手带你安装 MySql主从同步 和 Redis三主三从集群!并且进行主从切换与扩容操作,还有分析 哈希分区 等知识点!
Union文件系统(UnionFS)是一种**分层、轻量级并且高性能的文件系统**,它支持对文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下(unite several directories into a single virtual filesystem) Union 文件系统是 Docker 镜像的基础。 镜像可以通过分层来进行继承,基于基础镜像(没有父镜像),可以制作各种具体的应用镜像。
505 5
|
3月前
|
存储 NoSQL 前端开发
Redis专题-实战篇一-基于Session和Redis实现登录业务
本项目基于SpringBoot实现黑马点评系统,涵盖Session与Redis两种登录方案。通过验证码登录、用户信息存储、拦截器校验等流程,解决集群环境下Session不共享问题,采用Redis替代Session实现数据共享与自动续期,提升系统可扩展性与安全性。
274 3
Redis专题-实战篇一-基于Session和Redis实现登录业务
|
3月前
|
存储 缓存 NoSQL
Redis专题-实战篇二-商户查询缓存
本文介绍了缓存的基本概念、应用场景及实现方式,涵盖Redis缓存设计、缓存更新策略、缓存穿透问题及其解决方案。重点讲解了缓存空对象与布隆过滤器的使用,并通过代码示例演示了商铺查询的缓存优化实践。
219 1
Redis专题-实战篇二-商户查询缓存
|
3月前
|
存储 缓存 NoSQL
工作 10 年!Redis 内存淘汰策略 LRU 和传统 LRU 差异,还傻傻分不清
小富带你深入解析Redis内存淘汰机制:LRU与LFU算法原理、实现方式及核心区别。揭秘Redis为何采用“近似LRU”,LFU如何解决频率老化问题,并结合实际场景教你如何选择合适策略,提升缓存命中率。
457 3
|
6月前
|
缓存 监控 NoSQL
Redis 实操要点:Java 最新技术栈的实战解析
本文介绍了基于Spring Boot 3、Redis 7和Lettuce客户端的Redis高级应用实践。内容包括:1)现代Java项目集成Redis的配置方法;2)使用Redisson实现分布式可重入锁与公平锁;3)缓存模式解决方案,包括布隆过滤器防穿透和随机过期时间防雪崩;4)Redis数据结构的高级应用,如HyperLogLog统计UV和GeoHash处理地理位置。文章提供了详细的代码示例,涵盖Redis在分布式系统中的核心应用场景,特别适合需要处理高并发、分布式锁等问题的开发场景。
444 41
|
6月前
|
存储 监控 NoSQL
流量洪峰应对术:Redis持久化策略与内存压测避坑指南
本文深入解析Redis持久化策略与内存优化技巧,涵盖RDB快照机制、AOF重写原理及混合持久化实践。通过实测数据揭示bgsave内存翻倍风险、Hash结构内存节省方案,并提供高并发场景下的主从复制冲突解决策略。结合压测工具链构建与故障恢复演练,总结出生产环境最佳实践清单。
223 9
|
6月前
|
机器学习/深度学习 存储 NoSQL
基于 Flink + Redis 的实时特征工程实战:电商场景动态分桶计数实现
本文介绍了基于 Flink 与 Redis 构建的电商场景下实时特征工程解决方案,重点实现动态分桶计数等复杂特征计算。通过流处理引擎 Flink 实时加工用户行为数据,结合 Redis 高性能存储,满足推荐系统毫秒级特征更新需求。技术架构涵盖状态管理、窗口计算、Redis 数据模型设计及特征服务集成,有效提升模型预测效果与系统吞吐能力。
661 2
|
存储 缓存 NoSQL
Redis实战之入门进阶到精通
Redis 是一个远程内存数据库,它不仅性能强劲,而且还具有复制特性以及为解决问题而生的独一无二的数据模型。Redis 提供了 5 种不同类型的数据结构,各式各样的问题都可以很自然地映射到这些数据结构上:Redis 的数据结构致力于帮助用户解决问题,而不会像其他数据库那样,要求用户扭曲问题来适应数据库。除此之外,通过复制、持久化(persistence)和客户端分片(client-side sharding)等特性,用户可以很方便地将 Redis 扩展成一个能够包含数百 GB 数据、每秒处理上百万次请求的系统。
Redis实战之入门进阶到精通
|
存储 NoSQL Java
当Java遇到Redis:Jedis实战入门
Redis是一个开源,高级的键值存储和一个适用的解决方案,用于构建高性能,可扩展的Web应用程序。本文将概要介绍Redis的特性和语法,并以实例代码的形式介绍如何通过Jedis在java语言环境下控制Redis,帮助各位读者快速入门。
1787 0