zoukankan      html  css  js  c++  java
  • 一个高性能、小而美的序列化工具!

    作者:fredalxin
    地址:https://fredal.xin/kryo-quickstart

    Kryo是一个高性能的序列化/反序列化工具,由于其变长存储特性并使用了字节码生成机制,拥有较高的运行速度和较小的体积,在某些场景中成为了除Json、Protobuf之外的选择。

    依赖

    首先我们引入maven的相关依赖:

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

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

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

    记录类型信息

    这算是kryo的一个特点,可以把对象信息直接写到序列化数据里,反序列化的时候可以精确地找到原始类信息,不会出错,这意味着在写readxxx方法时,无需传入Class或Type类信息。

    相应的,kryo提供两种读写方式。记录类型信息的writeClassAndObject/readClassAndObject方法,以及传统的writeObject/readObject方法。

    线程安全

    kryo的对象本身不是线程安全的,所以我们有两种选择来保障线程安全。

    使用Threadlocal来保障线程安全:

    private static final ThreadLocal<Kryo> kryoLocal = new ThreadLocal<Kryo>() {
    	protected Kryo initialValue() {
    		Kryo kryo = new Kryo();
            kryo.setInstantiatorStrategy(new Kryo.DefaultInstantiatorStrategy(
                        new StdInstantiatorStrategy()));
    		return kryo;
    	};
    };
    

    或者使用kryo提供的pool:

    public KryoPool newKryoPool() {
        return new KryoPool.Builder(() -> {
            final Kryo kryo = new Kryo();
            kryo.setInstantiatorStrategy(new Kryo.DefaultInstantiatorStrategy(
                new StdInstantiatorStrategy()));
            return kryo;
        }).softReferences().build();
    }
    

    实例化器

    在上面注意到kryo.setInstantiatorStrategy(new Kryo.DefaultInstantiatorStrategy(new StdInstantiatorStrategy())); 这句话显示指定了实例化器。

    在一些依赖了kryo的开源软件中,可能由于实例化器指定的问题而抛出空指针异常。例如hive的某些版本中,默认指定了StdInstantiatorStrategy。

    public static ThreadLocal<Kryo> runtimeSerializationKryo = new ThreadLocal<Kryo>() {
        @Override
        protected synchronized Kryo initialValue() {
            Kryo kryo = new Kryo();
            kryo.setClassLoader(Thread.currentThread().getContextClassLoader());
            kryo.register(java.sql.Date.class, new SqlDateSerializer());
            kryo.register(java.sql.Timestamp.class, new TimestampSerializer());
            kryo.register(Path.class, new PathSerializer());
            kryo.setInstantiatorStrategy(new StdInstantiatorStrategy());
            ......
                return kryo;
        };
    };
    

    而StdInstantiatorStrategy在是依据JVM version信息及JVM vendor信息创建对象的,可以不调用对象的任何构造方法创建对象。

    那么例如碰到ArrayList这样的对象时候,就会出问题。观察一下ArrayList的源码:

    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    

    既然没有调用构造器,那么这里elementData会是NULL,那么在调用类似ensureCapacity方法时,就会抛出一个异常。

     public void ensureCapacity(int minCapacity) {
         if (minCapacity > elementData.length
             && !(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
                  && minCapacity <= DEFAULT_CAPACITY)) {
             modCount++;
             grow(minCapacity);
         }
     }
    

    解决方案很简单,就如框架中代码写的一样,显示指定实例化器,首先使用默认无参构造策略DefaultInstantiatorStrategy,若创建对象失败再采用StdInstantiatorStrategy。

    类注册

    当kryo写一个对象的实例的时候,默认需要将类的完全限定名称写入。将类名一同写入序列化数据中是比较低效的,所以kryo支持通过类注册进行优化。

    kryo.register(SomeClassA.class);
    kryo.register(SomeClassB.class);
    kryo.register(SomeClassC.class);
    

    注册会给每一个class一个int类型的Id相关联,这显然比类名称高效,但同时要求反序列化的时候的Id必须与序列化过程中一致。这意味着注册的顺序非常重要。

    但是由于现实原因,同样的代码,同样的Class在不同的机器上注册编号任然不能保证一致,所以多机器部署时候反序列化可能会出现问题。

    所以kryo默认会禁止类注册,当然如果想要打开这个属性,可以通过kryo.setRegistrationRequired(true);打开。

    循环引用

    这是对循环引用的支持,可以有效防止栈内存溢出,kryo默认会打开这个属性。当你确定不会有循环引用发生的时候,可以通过kryo.setReferences(false);关闭循环引用检测,从而提高一些性能。

    可变长存储

    kryo对int和long类型都采用了可变长存储的机制,以int为例,一般需要4个字节去存储,而对kryo来说,可以通过1-5个变长字节去存储,从而避免高位都是0的浪费。

    最多需要5个字节存储是因为,在变长存储int过程中,一个字节的8位用来存储有效数字的只有7位,最高位用于标记是否还需读取下一个字节,1表示需要,0表示不需要。

    在对string的存储中也有变长存储的应用,string序列化的整体结构为length+内容,那么length也会使用变长int写入字符的长度。

    配合缓存使用的场景

    在实际开发中,class增删字段是很常见的事情,但对于kryo来说,确是不支持的,而如果恰好需要使用缓存,那么这个问题会被放得更大。

    例如一个对象使用kryo序列化后,数据放入了缓存中,而这时候如果这个对象增删了一个属性,那么缓存中反序列化的时候就会报错。所以频繁使用缓存的场景,可以尽量避免kryo。

    不过现在的Kryo提供了兼容性的支持,使用CompatibleFieldSerializer.class,在kryo.writeClassAndObject时候写入的信息如下:

    class name|field length|field1 name|field2 name|field1 value| filed2 value
    

    而在读入kryo.readClassAndObject时,会先读入field names,然后匹配当前反序列化类的field和顺序再构造结果。

    当然如果在做好缓存隔离的情况下,这一切都不用在意。

    近期热文推荐:

    1.1,000+ 道 Java面试题及答案整理(2021最新版)

    2.终于靠开源项目弄到 IntelliJ IDEA 激活码了,真香!

    3.阿里 Mock 工具正式开源,干掉市面上所有 Mock 工具!

    4.Spring Cloud 2020.0.0 正式发布,全新颠覆性版本!

    5.《Java开发手册(嵩山版)》最新发布,速速下载!

    觉得不错,别忘了随手点赞+转发哦!

  • 相关阅读:
    如何使用gitbash 把你的代码托管到github
    发送邮件错误常见错误码
    使用snipworks/php-smtp发送邮件
    微信支付demo
    Linux——ps命令
    数组对象互转
    变量名下划线和驼峰互转
    对象数组转换
    curl请求
    百度地图接口的使用
  • 原文地址:https://www.cnblogs.com/javastack/p/14943133.html
Copyright © 2011-2022 走看看