在讨论Redis内存压缩的时候,我们需要知道几个Redis相关的知识。压缩列表ziplistRedis的ziplist是一种数据结构,使用连续的内存来存储列表数据。其结构示例如下图所示。压缩列表的组成示例--截图自《Redis设计与实现》zlbytes:记录整个压缩列表使用的内存大小zltail:记录压缩列表的末尾到起始位置有多少字节?zllen:记录压缩列表中的节点数。值得注意的是,因为只占2个字节,所以最大值只能达到65535,也就是压缩列表的长度,当大于65535时,只能遍历整个列表计算长度zleng:结束flagofthecompressedlist,固定值为0xFFentry1-N:压缩列表的节点,具体结构如下图所示。其中previous_entry_length:上一个节点的长度encoding:内容的编码和长度content:节点数据我们在搜索节点的时候,主要进行以下操作:根据zltail得到的最后一个节点的位置,判断当前节点是否为目标节点,如果是,则返回数据;如果不是,则根据previous_entry_length计算出上一个节点的起始位置,然后重新判断step2通过上面的描述,我们可以知道ziplist每次更新数据的复杂度大概是O(N),因为需要重新分配N个节点的内存,在查找一条数据时,复杂度为O(N),最坏情况下需要遍历整个链表。什么情况下会用到ziplist?Redis将用于ziplist的数据结构是Hash和List。Hash结构使用ziplist作为底层存储的两个条件是:当所有key和value字符串的长度小于64字节时,当key和value对数据小于512时,只要满足以上任一条件条件不满足,Redis会自动保存这个从ziplist转换为hashtable的Hash对象。但是这两个阈值可以通过修改配置文件中的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系列面试题和答案都整理好了。微信搜索Java技术栈,后台发送:面试,网上可以看。在传统使用Redis的场景中,我们可以使用标准的KV结构来存储定向包数据,存储方式如下:{crowdpacketID}_{deviceID_1}=>true{crowdpacketID}_{deviceID_2}=>true如果要使用ziplist继续进行内存压缩,必须保证Hash对象的长度小于512,key值的长度小于64字节。我们可以将KV结构的数据存储在一个预先分配好的bucket中。我们先估算整个Redis集群预计容纳10亿条数据,那么Bucket数量的计算公式如下:bucket_count=10亿/512=195W,那么我们需要大约200万个Bucket(估算数量Buckets的数量需要多估计一点,以防触发临界值问题)我们先计算BucketID,公式如下:bucket_id=CRC32(crowdpackageID+"_"+deviceID)%200W,然后是存储结构Redis中的数据变为bucket_id=>{{crowdPackageID}_{deviceID_1}=>true{crowdpackageID}_{deviceID_2}=>true}这样我们就保证了每个bucket中的数据项小于512,长度小于64字节。点此刷Redis系列面试题。我们用2000W数据进行测试,前后两者的内存使用情况如下:数据集大小存储方式Bucket数内存碎片率Redis占用内存2000W压缩列表200W928M1.381.25G2000W压缩列表5W785M1.481.14G2000W直接存储-1.44G1.031.48G这里需要引入一个额外的概念——内存碎片率。内存碎片率=操作系统分配给Redis的内存/Redis存储对象占用的内存因为压缩列表在更新节点时经常需要进行内存重新分配,所以导致内存碎片率比较高。我们在比较技术方案时,内存碎片率也是需要关注的指标之一。但是降低内存碎片率的方法有很多,比如内存对齐,甚至更极端的直接重做整个Redis内存(使用快照或者slave节点重做内存)都可以有效降低内存碎片率。在这个实验中,由于存储的值比较大(单个KEY大概34字节左右),实际节省的内存并不多,但还是可以节省35%-50%的内存占用。在实际生产环境中,我们根据应用场景合理设计压缩存储结构,部分业务甚至可以达到节省70%内存使用的效果。压缩列表可以节省多少内存?我们现在知道压缩列表通过在内存中紧凑地排列节点来节省内存。但是他节省了什么样的内存才能达到惊人的压缩率呢?首先,为了理解这个细节,我们需要知道常见的Key-Value结构在Redis中是如何存储的。typedefstructredisObject{unsignedtype:4;//对象类型unsignedencoding:4;//对象编码unsignedlru:LRU_BITS;//LRU类型intrefcount;//引用计数void*ptr;//指向底层数据结构的指针}robj;Redis所有对象通过上面的结构进行存储。假设我在Redis中存储了一个像Hello=>World这样的键值对。除了存储键值数据外,还需要额外的24个字节来存储redisObject对象。Redis用于存储字符串的SDS数据结构structsdshdr8{uint8_tlen;//存储字符串的长度uint8_talloc;//分配的内存量unsignedcharflags;//标志位,用于判断sdshdr类型charbuf[];//byte数组,用户保存的字符串};如果字符串的长度不能用unsignedint8来表示,Redis会使用可以表达更大长度的sdshdr16结构来存储字符串。而且,为了减少修改字符串带来的内存重分类问题,Redis会预先分配内存,所以也许你只想保存5个字符,但Redis会为你预先分配10字节的内存。这意味着当我们存储字符串Hello时,需要额外的3个或更多字节。哦~~~,我只是想保存Hello=>World这十个字符的数据,但是我需要30~40个字节的数据来存储额外的信息,这比存储数据本身的大小还多。这还不包括Redis维护字典表所需的额外内存空间。那么假设我们使用ziplist来存储这个数据,我们只需要额外的2个字节来存储previous_entry_length和编码。具体的计算方法可以参考Redis源码或者第一部分第七章的压缩列表。总结从上面的对比我们可以看出,当存储较小的数据时,使用ziplist进行数据压缩可以获得更好的压缩比。但是副作用也很明显。ziplist的更新效率远低于普通的K-V模式,并且会造成额外的内存碎片率。在Redis存储大量数据的实践中,我们经常会做一些小技巧,尽可能地压榨Redis的存储能力。另外,关注公众号Java技术栈,后台回复:面试,可以拿到我整理的Redis系列面试题及答案,很全。
