教妹子学序列化kryo的经历

原创
10/27 08:00
阅读数 4.1K

女同事的疑问

女同事:老哥,为什么你的接口那么快呢?

我故作镇定的回答:妹子,因为咱俩序列化的方式不同。

女同事:流弊篓子,你用的什么方式呀?

我撩了一下稀疏而飘逸的头发说:我用的kryo啊,大妹砸。

女同事一脸通红的说:老哥可以教教我吗?(应该是被我帅气的外表迷倒了)

我装做很不情愿的样子回答:那行吧,就这一次啊。来,哥手把手教你

。。。诶,别走啊,我还没开始讲呢!

何为序列化 / 反序列化

序列化

序列化就是一种处理对象流的机制。

即将对象的内容流化,将数据转化成字节流,以便存储在文件中或用于在网络中传输,当然用的最多的肯定就是网络传输

反序列化

文件中或网络上获得序列化后的对象字节流后,根据字节流中所保存的对象状态及描述信息,通过反序列化重建对象

大家所常见的序列化/反序列化的方式:Serializable

谁敢说没见过Serializable,给我拖出的吊树上打

JDK提供的Serializable速度较慢,原因比如:因为加入了序列化版本号,类名等信息,所以导致码流变大,速度变慢等等。

所以我们要来学一些速度快的序列化方式。不仅速度快,占用的空间还小。

这么好的技术,那个妹子没学到,真是可惜了。

序列化选型标准

  • 通用性:是否只能用于java间序列化/反序列化,是否跨语言、跨平台

  • 性能:分为空间开销和时间开销,序列化后的数据一般用于存储或网络传输,其大小是很重要的一个参数;

当然解析的时间也影响了序列化协议的选择

  • 易用性:API使用是否复杂,是否影响开发效率

  • 可扩展性:实体类的属性变更会不会导致反序列化异常,这通常会在系统升级时会产生,参考性不是很大

Kryo序列化入门

Kryo 是一个快速序列化/反序列化工具,其使用了字节码生成机制(底层依赖了 ASM 库),因此具有比较好的运行速度。

Kryo 序列化出来的结果,是其自定义的、独有的一种格式,不再是 JSON 或者其他现有的通用格式;

而且,其序列化出来的结果是二进制的(即 byte[];而 JSON 本质上是字符串 String);

二进制数据显然体积更小,序列化、反序列化时的速度也更快。

Kryo 一般只用来进行序列化(然后作为缓存,或者落地到存储设备之中)、反序列化,而不用于在多个系统、甚至多种语言间进行数据交换 —— 目前 kryo 也只有 java 实现。

像Redis这样的存储工具,是可以安全的存储二进制数据,所以一般项目中可使用Kryo来替代JDK序列化进行存储。

使用场景:(数据交换或数据持久化)比如使用kryo把对象序列化成字节数组发送给消息队列或者放到redis等nosql中等等应用场景。

pom配置

<dependency>
    <groupId>com.esotericsoftware</groupId>
    <artifactId>kryo</artifactId>
    <version>4.0.1</version>
</dependency>

需要注意的是,由于kryo使用了较高版本的asm,可能会与业务现有依赖的asm产生冲突,这是一个比较常见的问题。只需将依赖改成:

<dependency>
    <groupId>com.esotericsoftware</groupId>
    <artifactId>kryo-shaded</artifactId>
    <version>4.0.2</version>
</dependency>

代码实现

注意:由于kryo不是线程安全的,针对多线程情况下的使用,要对kryo进行一个简单的封装设计,从而可以多线程安全的使用序列化和反序列化

接口

/**
 * 序列化工具(程序调用该接口来实现obj<->byte[]之间的序列化/反序列化)
 */

public interface Serializer{
 
 /**
  * 序列化
  */

 public void serialize(Object t,byte[] bytes);
 
 /**
  * 序列化
  */

 public void serialize(Object obj, byte[] bytes, int offset, int count);
 
 /**
  * 反序列化
  */

 public <T>deserialize(byte[] bytes);

 /**
  * 反序列化
  */

 public <T>deserialize(byte[] bytes, int offset, int count);
}

实现类

/**
 * 基于kyro的序列化/反序列化工具
 */

public class kryoSerializer implements Serializer {

 // 由于kryo不是线程安全的,所以每个线程都使用独立的kryo
 final ThreadLocal<Kryo> kryoLocal = new ThreadLocal<Kryo>() {
  @Override
  protected Kryo initialValue() {
   Kryo kryo = new Kryo();
   kryo.register(ct, new BeanSerializer<>(kryo, ct));
   return kryo;
  }
 };
  
  // 序列化threadlocal
 final ThreadLocal<Output> outputLocal = new ThreadLocal<Output>();
  
  // 反序列化threadlocal
 final ThreadLocal<Input> inputLocal = new ThreadLocal<Input>();
  
 private Class<?> ct = null;

 public kryoSerializer(Class<?> ct) {
  this.ct = ct;
 }
  
  /**
  * 序列化
  */

 @Override
 public void serialize(Object obj, byte[] bytes) {
  Kryo kryo = getKryo();
  Output output = getOutput(bytes);
  kryo.writeObjectOrNull(output, obj, obj.getClass());
  output.flush();
 }

  /**
  * 序列化
  */

 @Override
 public void serialize(Object obj, byte[] bytes, int offset, int count) {
  Kryo kryo = getKryo();
  Output output = getOutput(bytes, offset, count);
  kryo.writeObjectOrNull(output, obj, obj.getClass());
  output.flush();
 }
  
  /**
  * 反序列化
  */

 @SuppressWarnings("unchecked")
 @Override
 public <T> deserialize(byte[] bytes, int offset, int count) {
  Kryo kryo = getKryo();
  Input input = getInput(bytes, offset, count);
  return (T) kryo.readObjectOrNull(input, ct);
 }

 /**
  * 反序列化
  */

 @Override
 public <T> deserialize(byte[] bytes) {
  return deserialize(bytes, 0, bytes.length);
 }

 /**
  * 获取kryo
  */

 private Kryo getKryo() {
  return kryoLocal.get();
 }

 /**
  * 获取Output并设置初始数组
  */

 private Output getOutput(byte[] bytes) {
  Output output = null;
  if ((output = outputLocal.get()) == null) {
   output = new Output();
   outputLocal.set(output);
  }
  if (bytes != null) {
   output.setBuffer(bytes);
  }
  return output;
 }

 /**
  * 获取Output
  */

 private Output getOutput(byte[] bytes, int offset, int count) {
  Output output = null;
  if ((output = outputLocal.get()) == null) {
   output = new Output();
   outputLocal.set(output);
  }
  if (bytes != null) {
   output.writeBytes(bytes, offset, count);
  }
  return output;
 }

 /**
  * 获取Input
  */

 private Input getInput(byte[] bytes, int offset, int count) {
  Input input = null;
  if ((input = inputLocal.get()) == null) {
   input = new Input();
   inputLocal.set(input);
  }
  if (bytes != null) {
   input.setBuffer(bytes, offset, count);
  }
  return input;
 }
  
  public Class<?> getCt() {
  return ct;
 }

 public void setCt(Class<?> ct) {
  this.ct = ct;
 }

Kryo的IO

Kryo致力以简单易用的API,序列化过程中主要核心有KryoOutputInput

Output和Input是Kryo的IO,他们支持以byte array或者stream的形式为序列化的dest和反序列化的source。

当使用stream形式进行写出写入时,需要close这些Output和Input。

写出时,当OutputDe buffer是满的时候,就会flush bytes到stream中。

写入时,会从stream中获取bytes到Input buffer中,当填充满时,进行反序列化。

Kryo的注册

和很多其他的序列化框架一样,Kryo为了提供性能和减小序列化结果体积,提供注册的序列化对象类的方式。

在注册时,会为该序列化类生成int ID,后续在序列化时使用int ID唯一标识该类型。

注册的方式如下:

kryo.register(SomeClass.class);

或者可以明确指定注册类的int ID,但是该ID必须大于等于0。如果不提供,内部将会使用int++的方式维护一个有序的int ID生成。

kryo.register(SomeClass.class, 1);

对象引用方面

这是对循环引用的支持,可以有效防止栈内存溢出,kryo默认会打开这个属性。

当你确定不会有循环引用发生的时候,可以通过以下代码关闭循环引用检测,从而提高一些性能,但不是很推荐

Kryo kryo = new Kryo();
kryo.setReferences(false);

Kryo的读和写的方式

  • 如果序列化的对象类型未知并且可能为空:

    kryo.writeClassAndObject(output, object);
    // ...
    Object object = kryo.readClassAndObject(input);
    if (object instanceof SomeClass) {
       // ...
    }
  • 如果对象类型已知并且可能为空:

    kryo.writeObjectOrNull(output, someObject);
    // ...
    SomeClass someObject = kryo.readObjectOrNull(input, SomeClass.class);
  • 如果对象类型已知并且不可能为空:

    kryo.writeObject(output, someObject);
    // ...
    SomeClass someObject = kryo.readObject(input, SomeClass.class);

序列化线程安全

Kryo默认是线程不安全的,有两种解决方式:

  • 一个是通过Threadlocal的方式为某个线程存储一个实例:

    private static final ThreadLocal<Kryo> kryoThreadLocal = new ThreadLocal<Kryo>() {
        protected Kryo initialValue() {
            Kryo kryo = new Kryo();
            // 这里可以增加一系列配置信息
            return kryo;
        }
    };
  • 另外一种是通过KryoPool的方式,该方式在性能上也好于ThreadLocal:

    public KryoPool createPool() {
        return new KryoPool.Builder(() -> {
            Kryo kryo = new Kryo();
            // 此处也可以进行一系列配置,可通过实现KryoFactory接口来满足动态注册,抽象该类
            return kryo;
        }).softReferences().build();
    }

kryo支持序列化的类型

boolean Boolean byte Byte char
Character short Short int Integer
long Long float Float double
Double byte[] String BigInteger BigDecimal
Collection Date Collections.emptyList Collections.singleton Map
StringBuilder TreeMap Collections.emptyMap Collections.emptySet KryoSerializable
StringBuffer Class Collections.singletonList Collections.singletonMap Currency
Calendar TimeZone Enum EnumSet

kryo优缺点总结

优点

  • 序列化的性能非常高
  • 序列化结果体积较小
  • 提供了简单易用的API

缺点

  • 跨语言支持较复杂

  • 不支持对象字段的增加/删除/修改

    如果更改了对象的字段,然后再从更改前序列化的bytes中反序列化,将会出错。

    当然如果想得到Add、Remove等操作的支持,可以使用FieldSerializer的其他扩展,如TaggedFieldSerializerVersionFieldSerializer等等。

kryo 与 JDK性能对比

Simple类

import java.io.Serializable;
import java.util.Map;

public class Simple implements Serializable {  
   private static final long serialVersionUID = -4914434736682797743L;  
   private String name;  
   private int age;  
   private Map<String,Integer> map;  

   public Simple(){  

   }  

   public Simple(String name,int age,Map<String,Integer> map){  
       this.name = name;  
       this.age = age;  
       this.map = map;  
   }  

   public String getName() {  
     return name;  
   }  

   public void setName(String name) {  
      this.name = name;  
   }  

   public int getAge() {  
      return age;  
   }  

   public void setAge(int age) {  
      this.age = age;  
   }  

   public Map<String, Integer> getMap() {  
      return map;  
   }  

   public void setMap(Map<String, Integer> map) {  
      this.map = map;  
   }
}

JDK性能测试

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.HashMap;
import java.util.Map;

public class OriginalSerializable {
      
  public static void main(String[] args) throws IOException, ClassNotFoundException {  
      long start =  System.currentTimeMillis();  
      setSerializableObject();  
      System.out.println("java原生序列化时间:" + (System.currentTimeMillis() - start) + " ms" );    
      start =  System.currentTimeMillis();  
      getSerializableObject();  
      System.out.println("java原生反序列化时间:" + (System.currentTimeMillis() - start) + " ms");  
  }
  
  public static void setSerializableObject() throws IOException{  

    FileOutputStream fo = new FileOutputStream("D:/file2.bin");  

    ObjectOutputStream so = new ObjectOutputStream(fo);  

    for (int i = 0; i < 100000; i++) {  
        Map<String,Integer> map = new HashMap<String, Integer>(2);  
        map.put("zhang0", i);  
        map.put("zhang1", i);  
        so.writeObject(new Simple("zhang"+i,(i+1),map));  
    }  
    so.flush();  
    so.close();  
  } 
  
  public static void getSerializableObject(){  
      FileInputStream fi;  
      try {  
        fi = new FileInputStream("D:/file2.bin");  
        ObjectInputStream si = new ObjectInputStream(fi);  

        Simple simple =null;  
        while((simple=(Simple)si.readObject()) != null){  
            //System.out.println(simple.getAge() + "  " + simple.getName());  
        }  
        fi.close();  
        si.close();  
      } catch (FileNotFoundException e) {  
          e.printStackTrace();  
      } catch (IOException e) {  
          //e.printStackTrace();  
      } catch (ClassNotFoundException e) {  
          e.printStackTrace();  
      }
  }
}

kryo性能测试

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import org.objenesis.strategy.StdInstantiatorStrategy;

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.KryoException;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;

public class KyroSerializable {  
      
    public static void main(String[] args) throws IOException {  
        long start =  System.currentTimeMillis();  
        setSerializableObject();  
        System.out.println("Kryo 序列化时间:" + (System.currentTimeMillis() - start) + " ms" );  
        start =  System.currentTimeMillis();  
        getSerializableObject();  
        System.out.println("Kryo 反序列化时间:" + (System.currentTimeMillis() - start) + " ms");  
  
    }  
  
    public static void setSerializableObject() throws FileNotFoundException{  
  
        Kryo kryo = new Kryo();  
        kryo.setReferences(false);  
        kryo.setRegistrationRequired(false);  
        kryo.setInstantiatorStrategy(new StdInstantiatorStrategy());  
        kryo.register(Simple.class);  
        Output output = new Output(new FileOutputStream("D:/file1.bin"));  
        for (int i = 0; i < 100000; i++) {  
            Map<String,Integer> map = new HashMap<String, Integer>(2);  
            map.put("zhang0", i);  
            map.put("zhang1", i);  
            kryo.writeObject(output, new Simple("zhang"+i,(i+1),map));  
        }  
        output.flush();  
        output.close();  
    }  
  
  
    public static void getSerializableObject(){  
        Kryo kryo = new Kryo();  
        kryo.setReferences(false);  
        kryo.setRegistrationRequired(false);  
        kryo.setInstantiatorStrategy(new StdInstantiatorStrategy());  
        Input input;  
        try {  
            input = new Input(new FileInputStream("D:/file1.bin"));  
            Simple simple =null;  
            while((simple=kryo.readObject(input, Simple.class)) !null){  
                //System.out.println(simple.getAge() + "  " + simple.getName() + "  " + simple.getMap().toString());  
            }  
  
            input.close();  
        } catch (FileNotFoundException e) {  
            e.printStackTrace();  
        } catch(KryoException e){  
  
        }  
    }
}

测试结果

JDK

  • java原生序列化时间:6614 ms
  • java原生反序列化时间:8609 ms

kryo

  • Kryo 序列化时间:757 ms
  • Kryo 反序列化时间:307 ms


              
              
给个[在看],是对IT老哥最大的支持


本文分享自微信公众号 - IT老哥(dys_family)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

展开阅读全文
打赏
1
8 收藏
分享
加载中
更多评论
打赏
0 评论
8 收藏
1
分享
返回顶部
顶部