Java Protobuf+Zstd 压缩存储Redis实践&问题解决&对比Gzip压缩的大小和性能

Gzip 压缩存储 和 Protobuf 的介绍可以参考https://blog.youkuaiyun.com/IT_Octopus/article/details/155574230?spm=1001.2014.3001.5502

压缩算法对比
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
6. 总结
gzip:经典、兼容性无敌,但性能已落后。
bzip2/xz:追求极致压缩比,牺牲速度和内存。
lz4:为速度而生,适合延迟敏感场景。
zstd:现代首选,在速度、压缩比、资源消耗之间取得极佳平衡,Facebook、Linux、Docker、Kafka 等广泛采用。

方案选择:
1.对于推荐小数据菜单这类高频、低延迟、结构化数据,Protobuf + LZ4 是最佳平衡点——极致解压速度 + 轻量带宽节省。

2.当菜单数据达到 40MB 规模时,应优先选择 Protobuf + Zstd(level=3~6)。
它在几乎不牺牲解压速度的前提下,显著降低带宽和存储成本,完美契合“中等规模、低频更新、高价值结构化数据”的场景。
在这里插入图片描述
举个生活化的例子
想象你要寄一个包裹:

LZ4:用普通胶带快速打包,包裹体积大一点(比如 30cm × 30cm × 30cm),打包只要 5 秒。
Zstd:用真空压缩袋仔细打包,体积小很多(比如 20cm × 20cm × 20cm),但打包要 10 秒。
现在分两种情况:

情况 1:寄一张纸(<10KB)
原本就很小,压缩后可能还变大(真空袋本身有厚度)。
打包时间反而成了主要成本。 ✅ 结论:别压缩,直接寄!
情况 2:寄一床被子(>100KB,比如 40MB)
快递运费按体积/重量算,压缩后能省几十元。
虽然多花了 5 秒打包,但快递路上省了 1 天时间 + 几十块钱。 ✅ 结论:一定要用真空压缩(Zstd)!

1.Protobuf 集成springboot
需要将原来的实体类转换为.proto
举个例子:Menu 实体 ->.proto -> protobuf 生成的java Menu实体
序列化反序列过程就是从Menu -》protobuf 生成的java Menu实体 .toByteArray () ->Menu

 Protobuf 在 Maven 中的执行流程图(文本版)

┌──────────────────────┐
│   1. Maven 初始化     │
│  - 读取 pom.xml       │
│  - 解析插件配置       │
└─────────┬────────────┘
          │
          ▼
┌──────────────────────┐
│ 2. 加载 os-maven-plugin│
│ (可选,用于检测 OS/ARCH)│
│ → 设置 ${os.detected.classifier} │
└─────────┬────────────┘
          │
          ▼
┌──────────────────────┐
│ 3. 进入 generate-sources 阶段 │
│ - 扫描 src/main/proto/*.proto │
└─────────┬────────────┘
          │
          ▼
┌──────────────────────┐
│ 4. 获取 protoc 编译器        │
│ ├─ 自动下载(通过 protocArtifact)│
│ └─ 或使用系统已安装的 protoc    │
└─────────┬────────────┘
          │
          ▼
┌──────────────────────┐
│ 5. 调用 protoc 编译 .proto 文件 │
│ → 生成 Java 源码              │
│ → 输出至 target/generated-sources/protobuf/java │
└─────────┬────────────┘
          │
          ▼
┌──────────────────────┐
│ 6. 将生成目录加入编译源路径     │
│ (自动或通过 build-helper 插件)│
└─────────┬────────────┘
          │
          ▼
┌──────────────────────┐
│ 7. compile 阶段               │
│ - javac 编译所有 Java 源文件   │
│   (含 Protobuf 生成类)       │
└─────────┬────────────┘
          │
          ▼
┌──────────────────────┐
│ 8. 后续生命周期阶段           │
│ - test → package → install ...│
└──────────────────────┘

pom.xml (版本可变,因为项目有其他的,所以写死)

        <dependency>
            <groupId>com.google.protobuf</groupId>
            <artifactId>protobuf-java</artifactId>
            <version>3.24.0</version>
        </dependency>
        <dependency>
            <groupId>com.google.protobuf</groupId>
            <artifactId>protobuf-java-util</artifactId> <!--protobuf 工具类-->
            <version>3.24.0</version>
        </dependency>
    <build>
        <extensions>
            <extension>
                <groupId>kr.motd.maven</groupId>
                <artifactId>os-maven-plugin</artifactId>
                <version>1.7.1</version>
            </extension>
        </extensions>
        </build>
        <plugins>
 			   <plugin>
                <groupId>org.xolstice.maven.plugins</groupId>
                <artifactId>protobuf-maven-plugin</artifactId>
                <version>0.6.1</version>
                <configuration>
                    <protoSourceRoot>${project.basedir}/src/main/proto</protoSourceRoot>
                    <outputDirectory>${project.build.directory}/generated-sources/protobuf/java</outputDirectory>
                    <clearOutputDirectory>false</clearOutputDirectory>
                    <!--suppress UnresolvedMavenProperty kr.motd.maven 由它注入${os.detected.classifier}-->
                    <protocArtifact>com.google.protobuf:protoc:3.21.12:exe:${os.detected.classifier}</protocArtifact>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>test-compile</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

os-maven-plugin 用于在 Maven/Gradle 中自动检测操作系统和架构
如果不加<!–suppress 会报错,因为maven 在编码阶段没变法动态识别运行注入的参数

            <!--suppress UnresolvedMavenProperty kr.motd.maven 由它注入${os.detected.classifier}-->
            <protocArtifact>com.google.protobuf:protoc:3.21.12:exe:${os.detected.classifier}</protocArtifact>

生成的java 文件在target,idea 本身是识别不了的,需要
在这里插入图片描述
在这里插入图片描述
中文:
将目录标记为(Mark Directory as) → 已生成的源代码根目录(Generated Sources Root)

英文:右键点击 java 文件夹 → 选择 “Mark Directory as” → “Generated Sources Root”
.proto文件和java 数据类型对照

在这里插入图片描述
在这里插入图片描述
支持引用数据类型 NULL
在这里插入图片描述
但是注意!!!Protobuf 不支持Object , 如果List<>是[],会被写成NULL ,后面代码尤其要注意空指针!惨痛经历

ZSTD 集成
pom

        <dependency>
            <groupId>com.github.luben</groupId>
            <artifactId>zstd-jni</artifactId>
            <version>1.5.6-8</version>
        </dependency>

压缩直接调用就行
Zstd.compress(protobufData);
解压
byte[] protobufData = Zstd.decompress(compressedData, (int) decompressedSize);

Redis 序列化反序列

 @Bean
    public RedisTemplate<String, Map<String, Menu>> menuRedisTemplate(RedisConnectionFactory connectionFactory, ObjectMapper objectMapper) {
        RedisTemplate<String, Map<String, Menu>> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        // 设置键的序列化器
        // 设置 key 的序列化
        template.setKeySerializer(RedisSerializer.string());
        template.setHashKeySerializer(RedisSerializer.string());

        template.setValueSerializer(new ProtobufZstdMenuRedisSerializer());
        template.setHashValueSerializer(new ProtobufZstdMenuRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }
public class ProtobufZstdMenuRedisSerializer implements RedisSerializer<Map<String, Menu>> {

    @Override
    public byte[] serialize(Map<String, Menu> menuMap) throws SerializationException {
        if (menuMap == null) {
            return null; // Redis 允许存 null,对应 byte[] 为 null
        }
        try {
            return serializeMenuMapToProtobufZstd(menuMap);
        } catch (Exception e) {
            throw new SerializationException("Cannot serialize menu map", e);
        }
    }

    @Override
    public Map<String, Menu> deserialize(byte[] bytes) throws SerializationException {
        if (bytes == null) {
            return null;
        }
        try {
            return deserializeMenuMapFromProtobufZstd(bytes);
        } catch (Exception e) {
            throw new SerializationException("Cannot deserialize menu map", e);
        }
    }

    // ========== 以下是你的原有方法(保持不变) ==========

    public static byte[] serializeMenuMapToProtobufZstd(Map<String, Menu> menuMap) {
        try {
            MenuProtos.MenuMapProto.Builder builder = MenuProtos.MenuMapProto.newBuilder();
            for (Map.Entry<String, Menu> entry : menuMap.entrySet()) {
                String key = entry.getKey();
                Menu menu = entry.getValue();
                MenuProtos.MenuProto menuProto = convertMenuToProto(menu);
                builder.putEntries(key, menuProto);
            }
            byte[] protobufData = builder.build().toByteArray();
            return Zstd.compress(protobufData);
        } catch (Exception e) {
            throw new RuntimeException("Failed to serialize menu map to Protobuf + Zstd", e);
        }
    }

    public static Map<String, Menu> deserializeMenuMapFromProtobufZstd(byte[] compressedData) {
        try {
            long decompressedSize = Zstd.getFrameContentSize(compressedData);
            if (decompressedSize > Integer.MAX_VALUE) {
                throw new RuntimeException("Decompressed data too large");
            }
            byte[] protobufData = Zstd.decompress(compressedData, (int) decompressedSize);
            MenuProtos.MenuMapProto menuMapProto = MenuProtos.MenuMapProto.parseFrom(protobufData);

            Map<String, Menu> menuMap = new HashMap<>();
            for (Map.Entry<String, MenuProtos.MenuProto> entry : menuMapProto.getEntriesMap().entrySet()) {
                String key = entry.getKey();
                MenuProtos.MenuProto menuProto = entry.getValue();
                Menu menu = convertProtoToMenu(menuProto);
                menuMap.put(key, menu);
            }
            return menuMap;
        } catch (Exception e) {
            throw new RuntimeException("Failed to deserialize menu map from Protobuf + Zstd", e);
        }
    }

根据Gzip 压缩后大小:
在这里插入图片描述
Protobuf+Zstd 压缩后大小:
在这里插入图片描述
Gzip 压缩后存储redis 时长
在这里插入图片描述Gzip 压缩后读取redis 时长
在这里插入图片描述
Protobuf+Zstd 压缩后写入redis 时长
在这里插入图片描述

Protobuf+Zstd 压缩后读取redis 时长
在这里插入图片描述
真实业务压测的性能对比(存在业务差异)
在这里插入图片描述
总结:可以明显看出来
手动测试:
压缩的大小提升了2倍多,数据写入优化了10%,读取优化了1倍,所以从存储和读写优化的很明显,写入的性能实际并没有提升多少。
实际业务压测:
也有明显的提升,存在带宽,网络等因素影响。
这是是单独的protobuf 测试,也是写入的性能提升没有那么明显
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

IT_Octopus

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值