직렬화 기술의 첫 번째 엿보는 Kryo, Hessian 및 Json

목차

직렬화 란?

JDK 직렬화

Kryo 직렬화

의지하다

빠른 시작

읽고 쓰는 세 가지 방법

수업 등록

스레드 안전

순환 참조

JDK 직렬화 및 Kryo 직렬화 성능 비교

RedisTemplate 테스트 통합

헤센 직렬화

의지하다

빠른 시작

Fastjson 직렬화

의지하다

빠른 시작


직렬화 란?

간단히 말해 직렬화는 객체 스트림을 처리하는 메커니즘입니다. 즉, 객체의 내용이 스트리밍되고 데이터는 파일에 저장하거나 네트워크를 통해 전송하기 위해 바이트 스트림으로 변환됩니다. 네트워크 전송으로 RPC는 데이터 전송을 실현할 때 직렬화 계층에 의존하며 역 직렬화는 그 반대의 과정입니다. 직렬화 프로토콜을 선택할 때 참조 할 수있는 다음 표시기가있는 경우가 많습니다.

  • 융통성 : 자바 간 직렬화 / 역 직렬화에만 사용할 수 있는지 여부, 언어 간, 플랫폼 간 여부
  • 성능 : 공간 오버 헤드와 시간 오버 헤드로 구분되는 직렬화 된 데이터는 일반적으로 스토리지 또는 네트워크 전송에 사용되며 크기는 매우 중요한 매개 변수입니다. 물론 분석 시간은 직렬화 프로토콜의 선택에도 영향을 미칩니다.
  • 사용 용이성 : API 사용이 복잡하고 개발 효율성에 영향을 미치는지 여부
  • 확장 성 : 엔티티 클래스의 속성 변경으로 인해 일반적으로 시스템이 업그레이드되고 참조가 크지 않을 때 발생하는 역 직렬화 예외가 발생합니까?

 

JDK 직렬화

JDK는 기본적으로 직렬화를 제공합니다. Kyro, hessian 또는 Protobuf와 같은 인기 있고 효율적인 다양한 직렬화 기술을 사용했는지 여부에 관계없이 JDK 기본 직렬화 구현을 사용 했어야합니다.이 메서드는 해당 엔티티에만 있어야합니다. 직렬화 가능 인터페이스 구현 클래스에서 직렬화 가능으로 표시 할 수 있습니다. 간단한 데모는 다음과 같습니다.

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());
    }

사용하기 쉬운 모델은 일반적으로 잘 수행되지 않습니다. JDK 직렬화는 이것에 속합니다. 동시에 프로토콜은 전달 된 객체에 메타 데이터 정보도 포함하여 많은 공간을 차지합니다. 그러나 이것은 내장 Java이기 때문에 간단하고 편리하며 타사 종속성이 필요하지 않습니다.

 

Kryo 직렬화

Kryo는 바이트 코드 생성 메커니즘 (기본 계층은 ASM 라이브러리에 의존)을 사용하는 빠른 직렬화 / 역 직렬화 도구이므로 상대적으로 실행 속도가 좋습니다.

Kryo 직렬화의 결과는 더 이상 JSON 또는 기타 기존 일반 형식이 아닌 사용자 지정 및 고유 한 형식입니다. 또한 직렬화의 결과는 바이너리 (예 : byte [])이고 JSON은 본질적으로 문자열 (String)입니다. 바이너리 데이터는 분명히 더 작고 직렬화 및 역 직렬화 속도가 더 빠릅니다.

Kryo는 일반적으로 직렬화 (그런 다음 캐시 또는 저장 장치에 착륙) 및 역 직렬화에만 사용되며 여러 시스템 또는 심지어 여러 언어 간의 데이터 교환에는 사용되지 않습니다. 현재 kryo는 java에서만 구현됩니다.

Redis와 같은 스토리지 도구는 바이너리 데이터를 안전하게 저장할 수 있으므로 Kryo는 일반 프로젝트에서 스토리지 용 JDK 직렬화를 대체하는 데 사용할 수 있습니다.

의지하다

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를 사용한 직렬화 사례를 살펴보십시오.

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());
    }

직렬화 프로세스가 JDK와 매우 유사하고 전체 프로세스도 매우 명확함을 알 수 있습니다.

읽고 쓰는 세 가지 방법

Kryo는 세 가지 읽기 및 쓰기 방법을 지원합니다. 클래스 바이트 코드를 알고 있고 객체가 비어 있지 않은 경우 입력 사례에서 직접 메소드를 사용할 수 있습니다 : writeObject / readObject. 객체가 비어있을 수있는 경우 Kryo는 다른 방법도 제공합니다. : WriteObjectOrNull / readObjectOrNull. 물론, Kryo는 직렬화 결과에 직접 바이트 코드 정보를 저장하는 것을 지원하고, 역 직렬화 과정에서 바이트 코드 정보를 자체적으로 읽는 것도 지원합니다 : writeClassAndObject / readClassAndObject, 이때 역 직렬화 된 객체는 obj이므로 용도를 판단해야합니다. 일반 개체의 역 직렬화의 경우 Kryo의 구문 분석이 훨씬 더 편리합니다 (예 : List <User>). 다음 사례를 참조하세요.

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;
    }

위의 경우이 코드 줄 : List newList = kryo.readObject (input, ArrayList.class); ArrayList.class가 List.class로 바뀌면 다음 예외가 발생합니다.

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

Kryo는 매개 변수가없는 생성자를 포함하는 클래스의 역 직렬화를 지원하지 않기 때문에 매개 변수 가없는 생성자를 포함하지 않는 클래스를 역 직렬화하려고하면 위의 예외가 발생합니다. 물론 각 클래스에 매개 변수가없는 생성자를 추가합니다. 모든 프로그램은 모든 직원이 따라야하는 프로그래밍 표준입니다.

또 다른 중요한 점은 Kryo가 Beans에서 필드 추가 및 삭제를 지원하지 않는다는 것 입니다. Kryo를 사용하여 클래스를 직렬화하고 Redis에 저장 한 다음 클래스를 수정하면 역 직렬화 예외가 발생합니다. 물론 예외를 포착하고 캐시를 지운 다음 상위 호출자에게 "캐시 미스"정보를 반환 할 수 있습니다.

수업 등록

Kryo가 객체를 직렬화 할 때 기본적으로 클래스의 정규화 된 이름을 작성해야합니다. 직렬화 된 데이터에 클래스 이름을 함께 쓰는 것은 상대적으로 비효율적이므로 Kryo는 클래스 등록을 통한 최적화를 지원합니다.

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

클래스가 등록되면 각 클래스는 int 유형의 id 값과 연결되고 id 값은 향후 직렬화 및 역 직렬화에서 클래스 이름을 대체하는 데 사용됩니다. 이것은 큰 클래스 이름 목록보다 분명히 더 효율적입니다. 그러나 동시에 deserialization 과정에서 id는 serialization 프로세스와 일치해야합니다. 즉, id와 class 간의 연관성을 변경할 수 없습니다. 즉, 등록 순서가 매우 중요하지만 결점 동일한 클래스의 모든 클래스를 보장 할 수 없습니다. 등록 된 번호가 동일하고 등록 순서와 만 관련이 있습니다. 즉, 다른 시스템 또는 동일한 시스템이 재시작 전후에 다른 번호를 가질 수 있습니다. 역 직렬화 문제로 분산 프로젝트에서이 문제가 노출됩니다. 이전 프로젝트에서 한 번 발생했습니다. 역 직렬화 후 얻은 개체는 항상 null이므로 Kryo의 등록 동작은 기본적으로 닫힙니다. 사용하려면 등록 할 수 있습니다. id 값을 지정할 때 등록 순서는 중요하지 않습니다.

등록 된 클래스와 미등록 된 클래스를 혼합 할 수 있으며 기본적으로 모든 기본 유형, 기본 클래스 래퍼, String 및 void는 id 0-9로 등록됩니다. 따라서이 범위의 등록 범위에주의하십시오.

Kryo # setRegistrationRequired가 true로 설정되면 등록되지 않은 클래스가 발견되면 예외가 발생하여 애플리케이션이 직렬화를 위해 클래스 이름 문자열을 사용하지 못하게 할 수 있습니다.

스레드 안전

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는 기본적으로이 속성을 설정합니다. 순환 참조가 없다고 확신하는 경우 kryo.setReferences (false);를 전달하여 일부 성능을 개선하기 위해 순환 참조 감지를 끌 수 있지만 권장되지는 않습니다.

 

JDK 직렬화 및 Kryo 직렬화 성능 비교

10,000 개의 테스트 개체를 예로 들어 보겠습니다.

Kryo 직렬화 시간 181

Kryo 역 직렬화 시간 223

JDK 직렬화 시간 소비 458

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));
    }

위의 User 객체의 순환 참조와 많은 매개 변수로 인해 1000 개의 객체를 테스트 한 후 jdk의 속도가 더 빨라질 것입니다. 일반 객체의 경우 Kryo는 여전히 JDK보다 훨씬 빠르고 컴팩트합니다. 클래스를 등록하면 사전에 프로그램에서 사용되는 것이 성능이 더 좋아질 것입니다.

RedisTemplate 테스트 통합

그러나 실제로 Kryo는 Redis에서 사용자 정의 객체를 직렬화하는 데 자주 사용됩니다. JDK 직렬화 방법을 대체합니다. RedisTemplate을 예로 들어 값 직렬화 방법은 기본적으로 JDK 직렬화이지만 성능은 실제로 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;
    }

따라서 Kryo 직렬화로 전환하는 경우 먼저 다음과 같이 Kryo 직렬화 클래스를 사용자 정의해야합니다.

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;
    }
}

그런 다음 값을 해당 redisTemplate 인스턴스로 설정합니다.

    @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;
    }

그런 다음 Redis에서 값을 설정하기 위해 다음 루프를 테스트했습니다. 두 루프에 소요되는 시간은 5000 번입니다. 즉, 5000 개의 명령을 Redis로 보내야하므로 효율성이 실제로 매우 낮습니다. 세 번째 방법은 파이프 라인을 사용합니다. , 즉 5000입니다.이 명령은 Redis에 한 번에 알려주지 만 Redis는 지원하지 않으므로 모든 성공을 보장 할 수 없습니다. 샘플 코드는 다음과 같습니다.

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));
        }

시간이 많이 걸리는 것은 다음과 같습니다.

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

 

헤센 직렬화

의지하다

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

빠른 시작

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);
    }

헤 시안 직렬화의 구현 메커니즘은 데이터에 중점을두고 간단한 유형 정보를 첨부하는 방법입니다. 교차 언어, 직렬화 후 적당한 바이트 수 및 사용하기 쉬운 API를 지원합니다. 중국의 주류 RPC 프레임 워크 인 Dubbo와 motan의 기본 직렬화 프로토콜이며 많이 사용되지 않았기 때문에 간단히 건너 뜁니다.

 

Fastjson 직렬화

의지하다

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

빠른 시작

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);
    }

json 도구로서 직렬화 체계로 끌어들이는 것은 약간 잘못된 것처럼 보이지만 Sina의 오픈 소스 motan RPC 프레임 워크는 hessian 외에도 Fastjson의 직렬화를 지원하므로 교차 언어 직렬화 간단한 구현 체계로도 사용할 수 있습니다. .

추천

출처blog.csdn.net/m0_38001814/article/details/103809551