Kryo, Hessian y Json en el primer vistazo de la tecnología de serialización

Tabla de contenido

¿Qué es la serialización?

Serialización JDK

Serialización de Kryo

confiar

Inicio rápido

Tres formas de leer y escribir

Registro de clases

A salvo de amenazas

Referencia circular

Comparación del rendimiento de serialización de JDK y serialización de Kryo

Integrar la prueba de RedisTemplate

Serialización de Hesse

confiar

Inicio rápido

Serialización de Fastjson

confiar

Inicio rápido


¿Qué es la serialización?

En resumen, la serialización es un mecanismo para procesar flujos de objetos, es decir, el contenido del objeto se transmite y los datos se convierten en un flujo de bytes para su almacenamiento en un archivo o para su transmisión a través de la red. Por supuesto, es más Se utiliza. Es transmisión de red. RPC se basa en la capa de serialización cuando se realiza la transmisión de datos, y la deserialización es el proceso opuesto. Al elegir un protocolo de serialización, a menudo existen los siguientes indicadores como referencia:

  • Versatilidad: si solo se puede usar para serialización / deserialización entre Java, si es entre idiomas, multiplataforma
  • Rendimiento: divididos en sobrecarga de espacio y sobrecarga de tiempo, los datos serializados se utilizan generalmente para almacenamiento o transmisión de red, y su tamaño es un parámetro muy importante; por supuesto, el tiempo de análisis también afecta la elección del protocolo de serialización
  • Facilidad de uso: si la API es complicada de usar y si afecta la eficiencia del desarrollo
  • Extensibilidad: ¿El cambio de atributo de la clase de entidad causará una excepción de deserialización, que generalmente ocurre cuando se actualiza el sistema y la referencia no es muy grande?

 

Serialización JDK

JDK nos proporciona la serialización de forma predeterminada. Ya sea que haya utilizado varias tecnologías de serialización populares y eficientes como Kyro, hessian o Protobuf, debe haber utilizado la implementación de serialización predeterminada de JDK. Este método solo debe estar en la entidad correspondiente Implementación de la interfaz serializable en la clase puede marcar la clase como serializable. Una demostración simple es la siguiente:

public void test0 () throws Exception {
        // 需实现Serializable接口
        User user = new User("123", "jdks");
        // 将对象写入到文件中
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("user.txt"));
        objectOutputStream.writeObject(user);
        objectOutputStream.close();
        // 将对象从文件中读出来
        ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("user.txt"));
        User newUser = (User) inputStream.readObject();
        inputStream.close();
        assert "jdks".equals(newUser.getName());
    }

El modelo fácil de usar no suele funcionar muy bien. A esto pertenece la serialización JDK. Al mismo tiempo, el protocolo también contiene información de metadatos en el objeto pasado, lo que ocupa mucho espacio. Pero debido a que está integrado en Java, es simple, conveniente y no requiere dependencias de terceros.

 

Serialización de Kryo

Kryo es una herramienta de serialización / deserialización rápida que utiliza un mecanismo de generación de código de bytes (la capa subyacente se basa en la biblioteca ASM), por lo que tiene una velocidad de ejecución relativamente buena.

El resultado de la serialización de Kryo es un formato personalizado y único, que ya no es JSON u otros formatos generales existentes; además, el resultado de la serialización es binario (es decir, byte []; y JSON es esencialmente una cadena (String); los datos binarios son obviamente más pequeño, y la velocidad de serialización y deserialización es más rápida.

Kryo generalmente solo se usa para serialización (y luego como caché o aterrizaje en un dispositivo de almacenamiento) y deserialización, no para intercambio de datos entre múltiples sistemas o incluso múltiples idiomas; actualmente, kryo solo se implementa en Java.

Las herramientas de almacenamiento como Redis pueden almacenar datos binarios de forma segura, por lo que Kryo se puede utilizar en proyectos generales para reemplazar la serialización de JDK para el almacenamiento.

confiar

Introduzca la dependencia de Maven:

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

Cabe señalar que debido a que kryo usa una versión superior de asm, puede entrar en conflicto con el asm existente del que depende la empresa. Este es un problema relativamente común. Simplemente cambie la dependencia a:

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

Inicio rápido

Primero observe un caso de serialización usando Kryo, de la siguiente manera:

public void test1() throws Exception {
        Kryo kryo = new Kryo();
        User user = new User("123", "kryo");
        Output output = new Output(new FileOutputStream("userKryo.txt"));
        kryo.writeObject(output, user);
        output.close();

        Input input = new Input(new FileInputStream("userKryo.txt"));
        User newUser = kryo.readObject(input, User.class);
        input.close();
        assert "kryo".equals(newUser.getName());
    }

Se puede ver que el proceso de serialización es muy similar al JDK, y todo el proceso también es muy claro.

Tres formas de leer y escribir

Kryo admite tres métodos de lectura y escritura. Si conoce el código de bytes de la clase y el objeto no está vacío, puede usar directamente el método en el caso de entrada: writeObject / readObject. Si el objeto puede estar vacío, Kryo también proporciona otro método.: WriteObjectOrNull / readObjectOrNull. Por supuesto, Kryo también admite el almacenamiento de la información del código de bytes directamente en el resultado de la serialización y lee la información del código de bytes por sí misma durante la deserialización: writeClassAndObject / readClassAndObject, en este momento el objeto deserializado es un obj, es necesario juzgar el uso. Para la deserialización de objetos genéricos, el análisis de Kryo es mucho más conveniente, como List <User>, vea el siguiente caso:

public void test4() throws Exception {
        Kryo kryo = new Kryo();
        List<User> list = Lists.newArrayList(new User("123", "kryoR"));
        Output output = new Output(new FileOutputStream("userKryo3.txt"));
        kryo.writeObject(output, list);
        output.close();

        Input input = new Input(new FileInputStream("userKryo3.txt"));
        // 使用Kryo在反序列化自定义对象的list时无需像有些json工具一样透传泛型参数,因为Kryo在序列化结果里记录了泛型参数的实际类型的信息,反序列化时会根据这些信息来实例化对象
        List newList = kryo.readObject(input, ArrayList.class);
        input.close();
        assert newList.get(0) instanceof User;
    }

Esta línea de código en el caso anterior: List newList = kryo.readObject (input, ArrayList.class); Si ArrayList.class se reemplaza con List.class, obtendrá la siguiente excepción:

com.esotericsoftware.kryo.KryoException: Class cannot be created (missing no-arg constructor): java.util.List

Debido a que Kryo no admite la deserialización de clases que contienen constructores sin parámetros , si intenta deserializar una clase que no contiene constructores sin parámetros, obtendrá la excepción anterior. Por supuesto, agregar constructores sin parámetros a cada clase es cada programa Los estándares de programación que todos los empleados deben seguir.

Otro punto importante es que Kryo no admite la adición y eliminación de campos en Beans . Si usa Kryo para serializar una clase, almacenarla en Redis y luego modificar la clase, causará una excepción de deserialización. Por supuesto, podemos detectar la excepción, borrar la caché y luego devolver la información de "falta de caché" al llamador superior.

Registro de clases

Cuando Kryo serializa un objeto, necesita escribir el nombre completo de la clase de forma predeterminada. Es relativamente ineficiente escribir el nombre de la clase juntos en los datos serializados, por lo que Kryo admite la optimización a través del registro de clases:

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

Cuando se registra la clase, cada clase se asociará con un valor de id de tipo int, y el valor de id se utilizará para reemplazar el nombre de la clase en futuras serializaciones y deserializaciones. Esto es obviamente más eficiente que una gran lista de nombres de clases, pero al mismo tiempo se requiere que el id durante la deserialización sea consistente con el proceso de serialización, lo que significa que la asociación entre id y class no se puede cambiar, es decir, el orden de registro es muy importante, pero tiene un gran inconveniente. No puede garantizar que todas las clases de la misma clase Los números registrados son los mismos, y solo están relacionados con el orden de registro. Esto significa que diferentes máquinas o la misma máquina pueden tener diferentes números antes y después de reiniciar. Esto causará problemas con la deserialización, por lo que en proyectos distribuidos Este problema quedará expuesto. Lo he encontrado una vez en el proyecto anterior. El objeto obtenido después de la deserialización siempre es nulo. Por lo tanto, el comportamiento de registro en Kryo está cerrado por defecto. Si el proyecto distribuido tiene para ser utilizado, puede registrarse Al especificar el valor de id, el orden de registro no importa.

Puede mezclar clases registradas y no registradas De forma predeterminada, todos los tipos básicos, envoltorios de clases básicas, String y void se registran con id 0-9. Así que tenga cuidado con la cobertura de registro en este rango.

Cuando Kryo # setRegistrationRequired se establece en verdadero, se puede lanzar una excepción cuando se encuentra cualquier clase no registrada, lo que evita que la aplicación use la cadena de nombre de clase para la serialización.

A salvo de amenazas

Kryo no es seguro para subprocesos de forma predeterminada. Hay dos soluciones. Una es almacenar una instancia de un subproceso a través de Threadlocal:

private static final ThreadLocal<Kryo> kryoThreadLocal = new ThreadLocal<Kryo>() {
        protected Kryo initialValue() {
            Kryo kryo = new Kryo();
            // 这里可以增加一系列配置信息
            return kryo;
        }
    };

El otro es a través de KryoPool, que también es mejor que ThreadLocal en rendimiento:

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

Referencia circular

Este es el soporte para referencias circulares, que pueden prevenir eficazmente el desbordamiento de la memoria de pila. Kryo activará este atributo de forma predeterminada. Cuando estés seguro de que no habrá referencias circulares, puedes pasar kryo.setReferences (false); para desactivar la detección de referencias circulares para mejorar algo de rendimiento, pero no es muy recomendable

 

Comparación del rendimiento de serialización de JDK y serialización de Kryo

Tome 10,000 objetos de prueba como ejemplo:

Tiempo de serialización de Kryo 181

Tiempo de deserialización de Kryo 223

Tiempo de serialización de JDK consumido 458

Tiempo consumido por la deserialización de JDK 563

@Test
    public void test5() throws Exception{
        long time = System.currentTimeMillis();
        Kryo kryo = new Kryo();
        Output output = new Output(new FileOutputStream("kryoPerformance.txt"));
        Map<String, String> map = new HashMap<>();
        map.put("key", "value");
        for (int i = 0;i < 10000; i++) {
            kryo.writeObject(output, new User(String.valueOf(i), "test", false, Lists.newArrayList(String.valueOf(i)), map));
        }
        output.close();
        System.out.println("Kryo序列化消耗的时间" + (System.currentTimeMillis() - time));
    }

    @Test
    public void test6() throws Exception{
        long time = System.currentTimeMillis();
        Kryo kryo = new Kryo();
        Input input = new Input(new FileInputStream("kryoPerformance.txt"));
        User user = null;
        try {
            while (null != (user = kryo.readObject(input, User.class))) {

            }
        } catch (KryoException e) {

        }
        input.close();
        System.out.println("Kryo反序列化消耗的时间" + (System.currentTimeMillis() - time));
    }

    @Test
    public void test7() throws Exception{
        long time = System.currentTimeMillis();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("JDKPerformance.txt"));
        Map<String, String> map = new HashMap<>();
        map.put("key", "value");
        for (int i = 0;i < 10000; i++) {
            oos.writeObject(new User(String.valueOf(i), "test", false, Lists.newArrayList(String.valueOf(i)), map));
        }
        oos.close();
        System.out.println("JDK序列化消耗的时间" + (System.currentTimeMillis() - time));
    }

    @Test
    public void test8() throws Exception{
        long time = System.currentTimeMillis();
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("JDKPerformance.txt"));
        User user = null;
        try {
            while (null != (user = (User) ois.readObject())) {

            }
        } catch (EOFException e) {

        }
        System.out.println("JDK反序列化消耗的时间" + (System.currentTimeMillis() - time));
    }

Debido a la referencia circular y muchos parámetros del objeto Usuario anterior, después de probar 1000 objetos, la velocidad de jdk será más rápida. En términos de objetos ordinarios, Kryo sigue siendo mucho más rápido y más compacto que JDK. Si registra las clases que se utilizará en el programa de antemano, el rendimiento será mejor.

Integrar la prueba de RedisTemplate

Pero, de hecho, Kryo se usa a menudo para serializar objetos personalizados en Redis. Reemplaza el método de serialización JDK. Tomando RedisTemplate como ejemplo, el método de serialización de valor es la serialización JDK por defecto, pero su rendimiento no es tan bueno como Kryo.

    @Bean
    public RedisTemplate<String, Serializable> jdkRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
        return redisTemplate;
    }

Entonces, si cambia a la serialización de Kryo, primero debe personalizar una clase de serialización de Kryo, de la siguiente manera:

public class KryoSerializer implements RedisSerializer<Object> {

    private KryoPool kryoPool;

    private static final Logger logger = LoggerFactory.getLogger(KryoSerializer.class);

    public KryoSerializer() {
        kryoPool = new KryoPool.Builder(Kryo::new).softReferences().build();
    }

    @Override
    public byte[] serialize(Object data) throws SerializationException {
        byte[] result = new byte[0];
        if (null == data)
            return result;
        Kryo kryo = kryoPool.borrow();
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        // 这里采用默认的缓冲字节数组大小即可,若指定的过大效率会非常慢
        Output output = new Output(bos);
        kryo.writeClassAndObject(output, data);
        output.close();
        // 释放当前实例
        kryoPool.release(kryo);
        result = bos.toByteArray();
        try {
            bos.close();
        } catch (IOException e) {
            logger.error("Close IO error:{}", e);
        }
        return result;
    }

    @Override
    public Object deserialize(byte[] bytes) throws SerializationException {
        Object result = null;
        if (null != bytes && bytes.length > 0) {
            Kryo kryo = kryoPool.borrow();
            Input input = new Input(bytes);
            result = kryo.readClassAndObject(input);
            kryoPool.release(kryo);
            input.close();
        }
        return result;
    }
}

Luego, establezca el valor en la instancia de redisTemplate correspondiente:

    @Bean
    public RedisTemplate<String, Object> kryoRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new KryoSerializer());
        return redisTemplate;
    }

Luego probé el siguiente ciclo para establecer el valor en Redis. El tiempo consumido por los dos ciclos es 5000 veces, lo que significa que se deben enviar 5000 comandos a Redis, por lo que la eficiencia es realmente muy baja. El tercer método usa la canalización , que es 5000. Este comando le dice a Redis a la vez, pero Redis no admite cosas, por lo que no puede garantizar todo el éxito. El código de muestra es el siguiente:

if (type == 0) {
            // jdk序列化方式
            long start = System.currentTimeMillis();
            for (int i = 0; i < 5000; i++) {
                String key = String.valueOf(i);
                String keyName = "jdk:user" + key;
                User user = new User(key, keyName, i, "18888888888", "塞外", "[email protected]");
                jdkRedisTemplate.opsForValue().set(keyName, user);
            }
            log.info("Serializable 序列化方式耗时:" + (System.currentTimeMillis() - start));
        } else if (type == 1) {
            // kryo序列化方式
            long start = System.currentTimeMillis();
            for (int i = 0; i < 5000; i++) {
                String key = String.valueOf(i);
                String keyName = "kryo:user" + key;
                User user = new User(key, keyName, i, "18888888888", "塞外", "[email protected]");
                kryoRedisTemplate.opsForValue().set(keyName, user);
            }
            log.info("Kryo 序列化方式耗时:" + (System.currentTimeMillis() - start));
        } else if (type == 2) {
            // 使用管道方式批量增加
            long start = System.currentTimeMillis();
            List<Object> result = kryoRedisTemplate.executePipelined(new RedisCallback<Object>() {
                @Override
                public Object doInRedis(RedisConnection connection) throws DataAccessException {
                    // 打开管道
                    connection.openPipeline();
                    // 然后给本次管道内添加要一次执行的多条命令
                    KryoSerializer kryoSerializer = new KryoSerializer();
                    for (int i = 5000; i < 10001; i++) {
                        String key = String.valueOf(i);
                        String keyName = "kryo:user" + key;
                        User user = new User(key, keyName, i, "18888888888", "塞外", "[email protected]");
                        connection.set(keyName.getBytes(), kryoSerializer.serialize(user));
                    }
                    // 管道不需要手动关闭,否则拿不到返回值
                    return null;
                }
            });

            // 可以对结果集进行获取 result
            log.info("Kryo 批量序列化方式耗时:" + (System.currentTimeMillis() - start));
        }

El tiempo que lleva es el siguiente:

Serializable 序列化方式耗时:8619
Kryo 序列化方式耗时:4478
批量序列化方式耗时:197

 

Serialización de Hesse

confiar

<dependency>
    <groupId>com.caucho</groupId>
    <artifactId>hessian</artifactId>
    <version>4.0.51</version>
</dependency>

Inicio rápido

public byte[] hessianSerialize(Object data) throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        Hessian2Output out = new Hessian2Output(bos);
        out.writeObject(data);
        out.flush();
        return bos.toByteArray();
    }

    public <T> T hessianDeserialize(byte[] bytes, Class<T> clz) throws IOException {
        Hessian2Input input = new Hessian2Input(new ByteArrayInputStream(bytes));
        return (T) input.readObject(clz);
    }

El mecanismo de implementación de la serialización de arpillera es un método que se centra en los datos y adjunta información de tipo simple. Admite varios idiomas, un número moderado de bytes después de la serialización y una API fácil de usar. Es el protocolo de serialización predeterminado de Dubbo y motan, los principales marcos de trabajo de RPC en China, y simplemente omítelo porque no se ha utilizado mucho.

 

Serialización de Fastjson

confiar

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.48</version>
        </dependency>

Inicio rápido

public byte[] jsonSerialize(Object data) throws IOException{
        SerializeWriter out = new SerializeWriter();
        JSONSerializer serializer = new JSONSerializer(out);
        // 注意补充对枚举类型的特殊处理
        serializer.config(SerializerFeature.WriteEnumUsingToString, true);
        // 额外补充类名可以在反序列化时获得更丰富的信息
        serializer.config(SerializerFeature.WriteClassName, true);
        serializer.write(data);
        return out.toBytes("UTF-8");
    }

    public <T> T jsonDeserialize(byte[] data, Class<T> tClass) throws IOException {
        return JSON.parseObject(new String(data), tClass);
    }

Como herramienta json, parece un poco incorrecto ser incluido en el esquema de serialización, pero el marco de trabajo RPC motan de código abierto de Sina admite la serialización de Fastjson además de hessian, por lo que también se puede usar como serialización en varios idiomas Esquema de implementación simple .

Supongo que te gusta

Origin blog.csdn.net/m0_38001814/article/details/103809551
Recomendado
Clasificación