背景
在业务系统中,将数据放到缓存里面,业务流量直接查询缓存,一方面减少了db查询,提高了系统的容量和稳定性;另一方面,缓存通常是放在内存里面,内存访问速度比磁盘访问速度快很多,通过缓存降低了系统访问时间,降低系统延迟。但这又带来一个问题,缓存是放在内存中,内存价格相比磁盘昂贵许多,实际中业务数据是比缓存容量要大,为了节约成本,只能将部分数据放到缓存中。为了提高缓存的利用率,一种方案是将热点数据放到缓存中,将不经常访问的数据淘汰下来,业界产生了各种淘汰算法,比如redis系统中针对缓存就有LRU,FIFO,LFU等等;还有另外一种方案,对数据按照一定的算法进行压缩,减少单条数据的内存占用空间,从而在有限的容量下缓存更多的数据。
编码压缩
位图编码
位图(BitMap)是一种比较常见的编码格式,Bit位有0和1两种状态,能够表示两种状态。如果待编码数据只有两种状态,比如常见布尔类型只有true和false两种状态。
举个例子,需要存储的数据的key为整型,value为该key的有效状态。直接存储,一条数据至少需要4个字节来存储整型和一个字节来存储布尔型的状态值。如果使用位图编码技术,我们只需要一个bit就能存储这条数据。因此,使用一个字节就能够存储8条数据的状态信息,内存压缩率为1/(5*8), 优化非常明显。
另外一个场景业务数据比较特殊刚好只有两种字符组成。比如字符串”aaaabbbb“, 只有a和b组成,如果1表示a,0表示b, 则可以编码为11110000,内存压缩率1/8≈12.5%。
游程编码
游程编码(Run-length encoding,RLE)使用当前数据元素以及该元素连续出现的次数来取代字符串中连续出现的部分。游程编码适合字符串中有大量连续的场景。比如"aaaaaaaabbbbbbcc"使用游程编码为"a8b6c2", 内存压缩率6/16。
再来看另外一个例子。字符串"aaaaaaaaaabbbaxxxxyyyzyx"使用游程编码为"a10b3a1x4y3z1y1x1"。此时内存压缩率为17/24≈70%。如果约定出现一次不需要添加次数,可以进一步压缩为"a10b3ax4y3zyx", 此时内存压缩率为13/24≈54%.
游程编码还有一种方式是对位置进行计数编码。如"aaaaaaaaaabbbaxxxxyyyzyx"使用位置计数为"a0b10a13x14y18z21y22x23"。
字典编码
字典编码是把整体重复性高的数据进行去重后建立字典,把原来存放数据的地方变为指向实体字典引用的编码方式。因为引用指针依然占存,因此适合单个的实例数据字段较多的数据缓存。
下例为原始数据为整型Key查询长字符串Value的场景。首先,将重复的字符串实体数据提取出来,将其单独作为一个实体字典进行存储。该字典Key为一个指针,Value则为提取出的不重复的字符串数据。然后,原始字典的Value就可以变为一个指针,指向新实体字典的Key。当需要查询Key1内实际数据的时候,先在原始字典中查询到引用Ref1,再在实体字典查询Ref1即可获得其Value值aaaa。
差值编码
差值编码是对于非连续的数据Key通过差值计算的方式转化为连续的Key,让字典可以转化为数组的编码方式。
下例中的数据Key为日期,Value为一个整型。在日期相对连续的情况下,取所有日期的最小值为开始日期,以数据生效日期到开始日期的差值为新字典的Key。那么编码前旧数据字典的Key为Date类型,而编码后的新数据字典的类型则可以转化为更小更泛用的int型。
参考文章
[1].Horowitz E , Sahni S , Rajasekaran S . Computer Algorithms[M]. Silicon Press, 2007.
[2]. Kleppmann M . Designing Data-Intensive Applications: The Big Ideas Behind Reliable, Scalable, and Maintainable Systems[M]. 2017.
[3].携程百亿级缓存系统探索之路——本地缓存结构选型与内存压缩