序列化技术简单介绍与比较

本文探讨了序列化和反序列化在Java中的应用,介绍了JDK自带、Kryo、Protobuf、FST、Hessian和Fury等常见序列化协议,分析了它们的优缺点,特别关注了性能、跨语言支持和字节数。
一、什么是序列化和反序列化

如果需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。简单来说:

  • 序列化:将数据结构或对象转换成二进制字节流的过程

  • 反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程

常见的序列化场景

下面是序列化和反序列化常见应用场景:

  • 对象在进行网络传输(比如远程方法调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化;

  • 将对象存储到文件之前需要进行序列化,将对象从文件中读取出来需要进行反序列化;

  • 将对象存储到数据库(如 Redis)之前需要用到序列化,将对象从缓存数据库中读取出来需要反序列化;

  • 将对象存储到内存之前需要进行序列化,从内存中读取出来之后需要进行反序列化。

综上所述:序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。

img

二、常见的序列化协议

JDK 自带的序列化方式一般不会用 ,因为序列化效率低并且存在安全问题。比较常用的序列化协议有 Hessian、Kryo、Protobuf、ProtoStuff,这些都是基于二进制的序列化协议。

像 JSON 和 XML 这种属于文本类序列化方式。虽然可读性比较好,但是性能较差,一般不会选择。

2.1、JDK自带的序列化
@AllArgsConstructor
@NoArgsConstructor
public class RpcRequest implements Serializable {
    private static final long serialVersionUID = 1905122041950251207L;
    private String requestId;
    private String interfaceName;
    private String methodName;
    private Object[] parameters;
    private Class<?>[] paramTypes;
    private RpcMessageTypeEnum rpcMessageTypeEnum;
}
    ObjectOutputStream outStream = new ObjectOutputStream(new FileOutputStream("test.out"));
    outStream.writeObject(objList.get(i));
     //反序列化
     ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("test.out"));
     System.out.println(new File("test.out").length());
     Object o = inputStream.readObject();

serialVersionUID 有什么作用?

序列化号 serialVersionUID 属于版本控制的作用。反序列化时,会检查 serialVersionUID 是否和当前类的 serialVersionUID 一致。如果 serialVersionUID 不一致则会抛出 InvalidClassException 异常。强烈推荐每个序列化类都手动指定其 serialVersionUID,如果不手动指定,那么编译器会动态生成默认的 serialVersionUID

如果有些字段不想进行序列化怎么办?

对于不想进行序列化的变量,可以使用 transient 关键字修饰。transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复(恢复成默认值)。

为什么不推荐使用 JDK 自带的序列化?

我们很少或者说几乎不会直接使用 JDK 自带的序列化方式,主要原因有下面这些原因:

  • 不支持跨语言调用 : 如果调用的是其他语言开发的服务的时候就不支持了。

  • 性能差:相比于其他序列化框架性能更低,主要原因是序列化之后的字节数组体积较大,导致传输成本加大。

  • 存在安全问题:序列化和反序列化本身并不存在问题。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码。

2.2、Kryo序列化

Kryo 是专门针对 Java 语言序列化方式并且性能非常好,如果你的应用是专门针对 Java 语言的话可以考虑使用。

   //注册
        Registration register = kroy.register(testFury02.class);
        ArrayList<testFury02> objList = new ArrayList<testFury02>();
        for (int i = 0; i < 1000; i++) {
            objList.add(new testFury02("wangcheng"+i, "man", 123, "学习", "121212991"));
        }
        long start = System.currentTimeMillis();
        for (int i = 0; i < 1000; i++) {
            //序列化
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            Output output = new Output(outputStream);
            kroy.writeObject(output,objList.get(i));
            System.out.println("length"+output.toBytes().length);
            //反序列化
            ByteArrayInputStream in = new ByteArrayInputStream(output.toBytes());
            Input input = new Input(in);
            testFury02 resule = kroy.readObject(input, testFury02.class);
        }
        long end = System.currentTimeMillis();
        System.out.println(end-start);//39,34 Byte
---------------------------------------------------------------------------------------------------
    //ThreadLocal解决fryo非线程安全问题
 private final ThreadLocal<Kryo> kryoThreadLocal = ThreadLocal.withInitial(() -> {
        Kryo kryo = new Kryo();
        kryo.register(RpcResponse.class);
        kryo.register(RpcRequest.class);
        return kryo;
    });
​
    @Override
    public byte[] serialize(Object obj) {
        try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
             Output output = new Output(byteArrayOutputStream)) {
            Kryo kryo = kryoThreadLocal.get();
            // Object->byte:将对象序列化为byte数组
            kryo.writeObject(output, obj);
            kryoThreadLocal.remove();
            return output.toBytes();
        } catch (Exception e) {
            throw new SerializeException("Serialization failed");
        }
    }
​
    @Override
    public <T> T deserialize(byte[] bytes, Class<T> clazz) {
        try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
             Input input = new Input(byteArrayInputStream)) {
            Kryo kryo = kryoThreadLocal.get();
            // byte->Object:从byte数组中反序列化出对象
            Object o = kryo.readObject(input, clazz);
            kryoThreadLocal.remove();
            return clazz.cast(o);
        } catch (Exception e) {
            throw new SerializeException("Deserialization failed");
        }
    }
   
2.3、FST序列化

FST序列化全称是Fast Serialization Tool,它是对Java序列化的替换实现。既然前文中提到Java序列化的两点严重不足,在FST中得到了较大的改善,FST的特征如下:

  • JDK提供的序列化提升了10倍,体积也减少3-4倍多

  • 支持堆外Maps,和堆外Maps的持久化

  • 支持序列化为JSON 

public static void serialSample() {
    FSTConfiguration conf = FSTConfiguration.createAndroidDefaultConfiguration();
    User object = new User();
    object.setName("huaijin");
    object.setAge(30);
    System.out.println("serialization, " + object);
    byte[] bytes = conf.asByteArray(object);
    User newObject = (User) conf.asObject(bytes);
    System.out.println("deSerialization, " + newObject);
}
2.4、protobuf序列化

Protocol buffer是一种语言中立、平台无关、可扩展的序列化框架。Protocol 需要预先定义Schema的,支持Python、C++、java、go等语言。

protobuf的核心内容包括:、

  • 定义消息:消息的结构体,以message标识。

  • 定义接口:接口路径和参数,以service标识。

通过protobuf提供的机制,服务端与服务端之间只需要关注接口方法名(service)和参数(message)即可通信,而不需关注繁琐的链路协议和字段解析,极大降低了服务端的设计开发成本。

//step01:定义message
//使用 proto3 语法 ,未指定则使用proto2
syntax = "proto3";//声明proto语法版本
package com.cmcc;//定义该类生成的包位置
option java_multiple_files = true;//是否再使用protobuf编译工具的时候将该proto文件编译成一个类;
message PosInfo{
   string name=1;
    string sex=2;
}
//step02:使用Protobuf生成对应代码
//step03:序列化与反序列化
 PosInfo build = builder.setAge(121).setName("wangcheng"+i).setSex("nan").setDescribe("shu").build();
 byte[] bytes = build.toByteArray();
 PosInfo posInfo = PosInfo.parseFrom(bytes);
2.5、Hessian2.0序列化

Hessian是一种动态类型的二进制序列化和Web服务协议,专为面向对象的传输而设计。支持跨语言、高效的二进制序列化协议,被经常用于序列化框架使用。

public static <T> byte[] serialize(T javaBean) throws Exception {
        Hessian2Output ho = null;
        ByteArrayOutputStream baos = null;
        try {
            baos = new ByteArrayOutputStream();
            ho = new Hessian2Output(baos);
            ho.writeObject(javaBean);
            ho.flush();
            return baos.toByteArray();
        } catch (Exception ex) {
            System.out.println("[模拟日志记录]HessianUtils.serialize.异常." + ex.getMessage());
            throw new Exception("HessianUtils.serialize.异常.", ex);
        } finally {
            if (null != ho) {
                ho.close();
            }
        }
    }
    public static <T> T deserialize(byte[] serializeData) throws Exception {
        T javaBean = null;
        Hessian2Input hi = null;
        ByteArrayInputStream bais = null;
​
        try {
            bais = new ByteArrayInputStream(serializeData);
            hi = new Hessian2Input(bais);
            javaBean = (T) hi.readObject();
            return javaBean;
        } catch (Exception ex) {
            throw new Exception("HessianUtils.deserialize.异常.", ex);
        } finally {
            if (null != hi) {
                hi.close();
            }
        }
    }
2.6、fury序列化

Fury是一个基于JIT的高性能多语言原生序列化框架,专注于提供极致的序列化性能和易用性:

  • 支持主流编程语言如Java/Python/C++/Golang,其它语言可轻易扩展;

  • 多语言/跨语言自动序列化任意对象,无需创建IDL文件、手动编译schema生成代码以及将对象转换为中间格式;

  • 多语言/跨语言自动序列化共享引用和循环引用,用户只需要关心对象,不需要关心数据重复或者递归错误;

  • 基于JIT动态编译技术在运行时自动生成序列化代码优化性能,增加方法内联、代码缓存和死代码消除,减少虚方法调用/条件分支/Hash查找/元数据写入等,提供相比其它序列化框架20~200倍以上的性能;

  • Zero-Copy序列化支持,支持Out of band序列化协议,支持堆外内存读写;

  • 提供缓存友好的二进制随机访问行存格式,支持跳过序列化和部分序列化,并能和列存自动互转;

除了跨语言能力,Fury还具备以下能力:

  • 无缝替代JDK/Kryo/Hessian等Java序列化框架,无需修改任何代码,同时提供相比Kryo 20倍以上的性能,相比Hessian100倍以上的性能,相比JDK自带序列化200倍以上的性能,可以大幅提升高性能场景RPC调用和对象持久化效率;

  • 支持共享引用和循环引用的Golang序列化框架;

  • 支持对象自动序列化的Golang序列化框架;

    image.png

Fury fury = Fury.builder().withLanguage(Language.JAVA).withRefTracking(false).requireClassRegistration(false)
                .build();
byte[] bytes = fury.serialize(obj);//序列化
Object o = fury.deserialize(bytes);//反序列化
三、对比

本次实验分别使用2.1至2.6中的序列化技术分别序列化和反序列化java对象,记录序列化后的byte数组长度及序列化和反序列化过程所耗用的时间,序列化对象如下所示;序列化过程中会依次循环10次,每次循环都会序列化和反序列化100000次java对象;JDK、Kryo及FST序列化方法只能序列化java对象,而protobuf、Hessian及Fury能跨语言序列化,但本次序列化不涉及跨语言序列化。具体过程如下所示:

 long ser = 0;//序列化总时间
        long unser = 0;//反序列化时间
        long total = 0;//总的时间
        for (int j = 10; j > 0; j--) {
            long start = System.currentTimeMillis();
            for (int i = 100000; i > 0; i--) {
                long l = System.currentTimeMillis();
               //序列化过程,省略 
                long l1 = System.currentTimeMillis();
                ser += (l1 - l);
                long l3 = System.currentTimeMillis();
                //反序列化过程,省略
                long l2 = System.currentTimeMillis();
                unser += (l2 - l3);
            }
            long end = System.currentTimeMillis();
            total += end - start;
        }
        System.out.println("total" + total);
        System.out.println("ser: " + ser);
        System.out.println("unser: " + unser);
​
  List< Object> list = Arrays.asList("str", new byte[1000], new int[100], new double[100]);
    Collection<BufferObject> bufferObjects = new ArrayList<>();
    byte[] bytes = fury.serialize(list, e -> !bufferObjects.add(e));
    List<MemoryBuffer> buffers =
      bufferObjects.stream().map(BufferObject::toBuffer).collect(Collectors.toList());
    System.out.println(fury.deserialize(bytes, buffers));
说明:序列化大概过程
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class uniObject implements Serializable {
    private static final long serialVersionUID = -2708653783151699375L;
    private String yydOrderId;
    private String cloudOrderId;
    private String cloudUserId;
    private Integer appNums;
    private Integer employees;
    private Date startTime;
    private Date expireTime;
    private String orgId;
    private Integer usedAppNums;
    private Integer authorizedEmployees;
    private String orgName;
    private byte byten;
    private short shot;
    private float flon;
    private double doun;
    private int age2;
    private int age3;
    private int age4;
    private int age5;
    private int age6;
    private int age7;
    private int age8;
    private int age9;
    private String name1;
    private String name2;
    private String name3;
    private String name4;
    private String name5;
    private String name6;
    private String name7;
    private String name8;
    private String name9;
    private float flo1;
    private float flo2;
    private float flo3;
    private float flo4;
    private float flo5;
    private float flo6;
    private double dou1;
    private double dou2;
    private double dou3;
    private double dou4;
    private boolean bo1;
    private boolean bo2;
    private boolean bo3;
    private boolean bo4;
    private boolean bo5;
    private boolean bo6;
    private boolean bo7;
    private boolean bo8;
    private boolean bo9;
}
​说明:序列化java对象 uniObject
3.1、序列化字节数

图3-1为各序列化方法序列化uniObject生成结果的字节数,可以很明显观察到JDK和Hessian方法相比其他方法在字节数这一维度上表现较差,序列化后的字节数远高于其他方法。

图3-1 各序列化方法生成的字节数大小

3.2、序列化及反序列化时间

图3-2为个序列化方法序列化及反序列化uniObject对象所用时间,只针对java序列化的方法中FST和Kroy相比于JDK有明显优势,跨语言序列化的方法中Proto和Fury强于Hessian。

图3-2 各方法序列化及反序列化所用时间

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值