内存是Redis最宝贵的资源,同样的一份数据,存储方式不同,占用的内存可能相差好几倍。当缓存的数据量从几个GB涨到几十GB时,不少团队会面临两难:加机器意味着成本上升,不压缩又扛不住内存压力。其实Redis在多个层面都提供了压缩的思路,既有内置的紧凑数据结构,也支持在客户端对大value做压缩后再写入。本文从Redis自身的内存结构讲起,逐步展开各种压缩手段的原理、适用场景与取舍。

Redis内置的紧凑结构本身就是一种压缩
很多人一提到压缩就想到gzip、zstd这些通用算法,却忽略了Redis在数据结构层面的设计。Redis从很早的版本开始就引入了ziplist这种紧凑编码:当list、hash、zset的元素数量较少且每个元素较小时,底层数据会以一块连续内存存储,而不是维护完整的哈希表或跳表。以hash为例,ziplist模式下每个entry只记录前一个entry的长度、本entry的长度和实际数据,省去了大量指针开销。一个64位系统上的普通哈希表节点,光指针和元数据就可能占用几十字节,而小对象场景下紧凑结构往往能节省一半以上的内存。
可以通过config get hash-max-ziplist-entries和config get hash-max-ziplist-value查看阈值配置,默认情况下hash在元素不超过128个、单个值不超过64字节时会使用紧凑编码。Redis 7.0之后ziplist被listpack彻底取代,listpack去掉了ziplist中记录前一项长度的字段,解决了著名的连锁更新问题,内存布局更加精简。如果业务中的hash字段数不多且值偏短,适当调大阈值可以换取更低的内存占用,但要注意紧凑结构的查找是线性遍历,阈值调得过大反而会拖慢读写性能,一般不建议把entries调到上千。
除了hash和zset,String类型的OBJ_ENCODING_EMBSTR也在做类似的事:小于44字节的字符串对象会把redisObject头和SDS数据分配在同一块内存中,只调用一次内存分配函数。理解这些内置机制的意义在于,做压缩优化之前先用object encoding key命令确认数据的实际编码,很多时候不用动一行业务代码,调整几个参数就能有明显收益。
客户端压缩大value:算法怎么选
当缓存的value本身很大,比如一篇几万字的文档、一段序列化后的JSON、一张图片的二进制数据,内置结构帮不上忙,这时候就需要在客户端压缩后以二进制String写入Redis。主流的可选算法有LZ4、Snappy、Zstandard(zstd)和gzip,它们的核心差异在于压缩率和速度的权衡。gzip压缩率不错但速度偏慢;LZ4和Snappy走的是极速路线,压缩率一般在50%左右但压缩解压速度极快;Zstandard则提供了一个可调节的压缩级别,在中低级别下速度接近LZ4,压缩率却能达到或超过gzip的水平。
下面是一段Java中使用Zstandard压缩并写入Redis的示例,其他语言思路相同:
import com.github.luben.zstd.Zstd;
public class RedisCompressUtil {
// 压缩数据后写入Redis
public static void setCompressed(Jedis jedis, String key, String value) {
byte[] raw = value.getBytes();
byte[] compressed = Zstd.compress(raw, 3); // 级别3,速度与压缩率均衡
jedis.set(key.getBytes(), compressed);
}
// 读取时解压
public static String getDecompressed(Jedis jedis, String key) {
byte[] compressed = jedis.get(key.getBytes());
if (compressed == null) {
return null;
}
byte[] raw = Zstd.decompress(compressed, estimateSize(compressed));
return new String(raw);
}
// zstd压缩帧头部记录了原始大小,可直接读取
private static long estimateSize(byte[] compressed) {
return Zstd.getFrameContentSize(compressed);
}
}
选型上有一个简单的判断标准:如果value是GB级别、读取频率极低,比如离线计算的结果缓存,选高压缩率的zstd高级别甚至gzip都划算;如果是高并发的热点key,value在几KB到几百KB之间,压缩解压的CPU开销会被放大,此时更适合用LZ4或zstd的低级别,甚至不压缩。另外一个容易被忽略的细节是压缩后的数据失去了可读性,redis-cli里直接get只能看到乱码,运维排查问题时最好保留一份未压缩写入的开关或独立的诊断key。
压缩之外的内存优化:碎片、过期与数据结构选择
压缩降低了逻辑数据的大小,但Redis实际占用的内存还受分配器和碎片率影响。通过info memory可以看到mem_fragmentation_ratio指标,它是操作系统分配的内存与Redis使用内存的比值。频繁地对大小不一的value进行写入、修改和删除,会让jemalloc产生碎片,极端情况下碎片率超过1.5,压缩省下来的内存可能被碎片吃掉。对于长期碎片偏高的实例,可以考虑开启activedefrag主动碎片整理,它会在业务低峰期搬移数据整理内存,代价是带来一定的CPU消耗。
数据结构的选择同样影响巨大。同样的字段映射关系,用多个String key存储时,每个key都要承担SDS头、redisObject头、键空间的字典开销,而合并成一个小hash利用紧凑编码,内存占用可能只有前者的几分之一。这在缓存用户资料这类场景中非常典型。此外,如果业务允许,使用client-side caching或者合理设置TTL避免冷数据长期驻留,配合压缩才能把内存水位控制住。
最后需要提醒的是,压缩没有免费的午餐。压缩会增加序列化反序列化的CPU压力,还会让memory usage的观测、大key扫描工具的判断变得更复杂。一个稳妥的落地路径是:先通过RDB分析工具或redis-cli --bigkeys找出内存大头,确认数据编码是否已经利用紧凑结构,再针对确实巨大的value做客户端压缩压测,对比压测前后的RT和CPU,最后再考虑调整阈值和碎片整理。按这个顺序推进,才能在内存和性能之间找到最适合自己业务的平衡点。