关于Redis不同序列化压缩性能的对比

一、背景介绍

为了核心业务更好的安全性,部分业务服务和核心业务做了redis分离,项目中初始化多个redis的链接,负责的一个业务系统,就存在redis的配置不太够,内存比较吃紧,于是乎,就想到了通过引入更加优秀的序列化方式,在牺牲redis中value的可读性,获取更低的内存占用(如果切换不同的redis序列化方式,同一个key切换前后会有不兼容的情况)。

二、解决方案

有了初期的思路,解决这个问题,相对也比较简单,通过调研,选择了几种比较常用的序列化方式,采用简单直接的方式,通过不同序列化将对象设置到redis中,对比一下他们的value占用空间大小。

三、环境声明:

JDK:17

Maven配置:

XML 复制代码
        <!--redisson-->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.45.1</version>
        </dependency>

        <!--JSON序列化-->
        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
            <version>2.18.4</version>
        </dependency>

        <!--fury序列化,可能会和项目中的guava包有依赖冲突,需要排除一下-->
        <dependency>
            <groupId>org.apache.fury</groupId>
            <artifactId>fury-core</artifactId>
            <version>0.9.0</version>
            <exclusions>
                <exclusion>
                    <artifactId>guava</artifactId>
                    <groupId>com.google.guava</groupId>
                </exclusion>
            </exclusions>
        </dependency>

Redisson配置:

java 复制代码
    public RedissonAutoConfigurationCustomizer redissonCustomizer() {
        return config -> {
// 指定序列化输入的类型,类必须是非final修饰的。序列化时将对象全类名一起保存下来
//            JavaTimeModule javaTimeModule = new JavaTimeModule();
//            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
//            javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(formatter));
//            javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(formatter));
//            ObjectMapper om = new ObjectMapper();
//            om.registerModule(javaTimeModule);
//            om.setTimeZone(TimeZone.getDefault());
//            om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//            om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
//            LoggerFactory.useSlf4jLogging(true);
//            TypedJsonJacksonCodec jsonCodec = new TypedJsonJacksonCodec(Object.class, om);
            // 组合序列化 key 使用 String 内容使用通用 json 格式
//            CompositeCodec codec = new CompositeCodec(StringCodec.INSTANCE, jsonCodec, jsonCodec);
//            CompositeCodec codec = new CompositeCodec(StringCodec.INSTANCE, StringCodec.INSTANCE, StringCodec.INSTANCE);
//            CompositeCodec codec = new CompositeCodec(StringCodec.INSTANCE, new Kryo5Codec(), new Kryo5Codec());

            CustomFuryCodec furyCodec = new CustomFuryCodec();
            CompositeCodec codec = new CompositeCodec(StringCodec.INSTANCE, furyCodec, furyCodec);
            config.setThreads(redissonProperties.getThreads())
                .setNettyThreads(redissonProperties.getNettyThreads())
                // 缓存 Lua 脚本 减少网络传输(redisson 大部分的功能都是基于 Lua 脚本实现)
                .setUseScriptCache(true)
                .setCodec(codec);
            if (SpringUtils.isVirtual()) {
                config.setNettyExecutor(new VirtualThreadTaskExecutor("redisson-"));
            }
            RedissonProperties.SingleServerConfig singleServerConfig = redissonProperties.getSingleServerConfig();
            if (ObjectUtil.isNotNull(singleServerConfig)) {
                // 使用单机模式
                config.useSingleServer()
                    //设置redis key前缀
                    .setNameMapper(new KeyPrefixHandler(redissonProperties.getKeyPrefix()))
                    .setTimeout(singleServerConfig.getTimeout())
                    .setClientName(singleServerConfig.getClientName())
                    .setIdleConnectionTimeout(singleServerConfig.getIdleConnectionTimeout())
                    .setSubscriptionConnectionPoolSize(singleServerConfig.getSubscriptionConnectionPoolSize())
                    .setConnectionMinimumIdleSize(singleServerConfig.getConnectionMinimumIdleSize())
                    .setConnectionPoolSize(singleServerConfig.getConnectionPoolSize());
            }
            // 集群配置方式 参考下方注释
            RedissonProperties.ClusterServersConfig clusterServersConfig = redissonProperties.getClusterServersConfig();
            if (ObjectUtil.isNotNull(clusterServersConfig)) {
                config.useClusterServers()
                    //设置redis key前缀
                    .setNameMapper(new KeyPrefixHandler(redissonProperties.getKeyPrefix()))
                    .setTimeout(clusterServersConfig.getTimeout())
                    .setClientName(clusterServersConfig.getClientName())
                    .setIdleConnectionTimeout(clusterServersConfig.getIdleConnectionTimeout())
                    .setSubscriptionConnectionPoolSize(clusterServersConfig.getSubscriptionConnectionPoolSize())
                    .setMasterConnectionMinimumIdleSize(clusterServersConfig.getMasterConnectionMinimumIdleSize())
                    .setMasterConnectionPoolSize(clusterServersConfig.getMasterConnectionPoolSize())
                    .setSlaveConnectionMinimumIdleSize(clusterServersConfig.getSlaveConnectionMinimumIdleSize())
                    .setSlaveConnectionPoolSize(clusterServersConfig.getSlaveConnectionPoolSize())
                    .setReadMode(clusterServersConfig.getReadMode())
                    .setSubscriptionMode(clusterServersConfig.getSubscriptionMode());
            }
            log.info("初始化 redis 配置");
        };
    }

FuryCodec配置类:

java 复制代码
public class CustomFuryCodec extends BaseCodec {


    // 使用 ThreadLocal 确保每个线程有独立的 Fury 实例
    private final ThreadLocal<Fury> furyThreadLocal;

    public CustomFuryCodec() {
        this.furyThreadLocal = ThreadLocal.withInitial(() ->
                Fury.builder()
                        .withLanguage(Language.JAVA)  // 纯 Java 模式,性能最佳
                        .requireClassRegistration(false)  // 强制注册类,避免写入类名(前提:预注册所有类)
                        .withMetaShare(false)  // 启用 MetaContext 共享,减少 schema 重复(如果需要;否则 false 以进一步提速)
                        .withRefTracking(false)  // 无循环引用时关闭,提升性能
                        .withCodegen(true)  // 启用 ASM 代码生成,接近手写代码速度
                        .withAsyncCompilation(true)  // 启用异步编译,长期使用下更快
                        .withCompatibleMode(CompatibleMode.SCHEMA_CONSISTENT)  // 非兼容模式,纯性能优化
                        .withNumberCompressed(true)  // 启用 int/long 压缩,减少大小
                        .withStringCompressed(true)  // 启用字符串压缩,适合大对象
                        .build()
        );
    }

    // 供 Redisson 复制 codec 时调用的构造方法
    public CustomFuryCodec(ClassLoader classLoader, CustomFuryCodec codec) {
        this(); // 调用默认构造,重新创建 ThreadLocal<Fury>
    }

    private Fury getFury() {
        return furyThreadLocal.get();
    }

    @Override
    public Decoder<Object> getValueDecoder() {
        return (ByteBuf buf, State state) -> {
            byte[] bytes = new byte[buf.readableBytes()];
            buf.readBytes(bytes);
            return getFury().deserialize(MemoryBuffer.fromByteArray(bytes));
        };
    }

    @Override
    public Encoder getValueEncoder() {
        return in -> {
            byte[] bytes = getFury().serialize(in);
            return Unpooled.wrappedBuffer(bytes);
        };
    }
}
四、结果对比

Kryo5Codec:

Java/JVM 上很成熟的二进制序列化库,用于对象图(object graph)序列化,高效但需要配置(注册类、Serializer等);快速、紧凑、支持复杂对象图,但在某些场景下(比如跨语言、零拷贝、元数据开销)会有劣势;

字节码序列化,通常比 Java Serialization/JSON 那些格式小很多,相对的字节码也不可直接阅读。通过注册类可以进一步减少类名、类型 tag 等元数据开销,可变长编码也有助于压缩整型等,对于某些类型(字符串、多余空值、nulls、共享引用等)大小可能较大。kryo5-gitHub链接https://github.com/EsotericSoftware/kryo最终的Redis Key Size(1):

JSONCodec:

较为常见的序列化方式,拥有较好的可读性;

最终的Redis Key Size(2):

FuryCodec:

较新的序列化框架/项目,目标是"多语言支持 + 零拷贝 + JIT 代码生成 + 高吞吐 + 简单易用";较新的序列化框架/项目,目标是"多语言支持 + 零拷贝 + JIT 代码生成 + 高吞吐 + 简单易用";

动态生成序列化代码、支持跨语言、兼容 JDK 序列化 API、支持零拷贝、大量优化(元数据共享、长整型压缩等);多语言支持"------Java+Python+CPP+Golang+Rust+JavaScript 等。

Fury官方也有和其他序列化方式的性能对比,链接如下:

fury-gitHub官方对比各方序列化https://github.com/chaokunyang/fory-benchmarks最终的Redis Key Size(3):

StringCodec:

作为基础对比项;

最终的Redis Key Size(4):

五、总结:

Fury序列化对比与kryo5的压缩比例,对于序列化和反序列化性能有较大提升,但是本身的配置可选项较多,学习成本和使用成本较高,内存大小压缩没有特别机制,对于速度要求比较极致的可以选择。

Kryo5在压缩比,以及性能方面都有不错的表现,综合来说还是非常不错的选择。

相关推荐
是多巴胺不是尼古丁14 小时前
期末java复习--string
java·开发语言·python
Survivor00114 小时前
高并发系统流量治理的底层算法
java·开发语言
凡人叶枫14 小时前
Effective C++ 条款35:考虑 virtual 函数以外的其他选择
java·c++·spring
garmin Chen14 小时前
从 Transformer 到 Agent:大模型技术全景解析
java·人工智能·python·深度学习·transformer
愚公移码14 小时前
蓝凌EKP18产品:流程引擎技术篇之流程核心概念模型
java·人工智能·流程引擎·蓝凌
Full Stack Developme15 小时前
Apache Tika 教程
java·开发语言·python·apache
鹅城剑仙15 小时前
Java线程池完全指南
java
李白的天不白15 小时前
SmartAdmin(基于 Spring Boot 框架)中配置跨域请求 VUE3 设置请求头
java·前端
橙子进阶之路15 小时前
Java线程(CompletableFuture)
java·开发语言
鹅城剑仙15 小时前
Java CompletableFuture 异步编程完全指南
java