架构系列十一(编解码或者说序列化反序列化框架选择思考)

118 阅读5分钟

1.引子

我们在开发一些网络应用,客户端服务器模式应用场景,需要考虑数据如何在网络中进行传递;应用微服务化以后,需要考虑请求报文、响应报文如何在服务之间传递。

像这种跨进程的相互协作,那么客户端与服务器之间,服务与服务之间需要有一套彼此都能够理解的协议。比如说我们从小要学习普通话,只要大家都讲普通话,那么不管你来自东南西北哪个省,相互交流起来畅通无阻,这里的普通话就是协议。

有了协议,自然还需要考虑发起请求一方如何把方言,转换成普通话,这就是编码接收请求一方如何把普通话,转换成方言,这就是解码。你看编解码我们也就很好理解了。

对于计算机来说,它老人家只认识0和1组成的二进制数据。于是我们在开发应用的时候,需要将编程语言表达的对象,转换成二进制数据,这就是序列化将二进制数据,再转换回对象,这就是反序列化。你看序列化反序列化理解起来也不难。

业界常见的编解码框架有protobuf、thrift等,今天这篇文章我们不讨论protobuf、thrift框架,我想给你分享的是

  • java编程语言,有提供序列化机制吗?
  • 为什么jdk明明提供了序列化机制(实现Serializable接口),我们却不推荐使用它?
  • 实际应用中,我们都是基于什么考虑,来选择编解码框架的?

2.案例

2.1.如何衡量选择编解码框架

在引子部分我们知道了,编解码,与序列化反序列化本质上讲的是一个事情。并且留下了3个问题

  • java编程语言,有提供序列化机制吗?
  • 为什么jdk明明提供了序列化机制(实现Serializable接口),我们却不推荐使用它?
  • 实际应用中,我们都是基于什么考虑,来选择编解码框架的?

其实3个问题,归纳起来本质上是一个问题。我们一起来尝试找到答案。首先毫无疑问,jdk有提供序列化机制,java.io.Serializable接口我们都难以忘怀。在需要将对象持久化到文件,或者网络上进行传输,都会情不自禁的让目标对象,实现Serializable接口

来看第二个问题,既然jdk提供了序列化机制,为什么不推荐使用它呢?要回答这个问题,我们可以通过回答第三个问题,从而一并得到第二个问题的答案。

第三个问题是:实际应用中,如何衡量选择合适的编解码框架?我们需要考虑这么几个点

  • 跨语言,既然是跨进程的应用,服务与服务之间完全有可能采用不同的编程语言实现
  • 编码码流精简,要在网络上进行传输,编码以后的码流要精简(越小越好),节省带宽
  • 高性能,要求编码解码速度要够快

对于跨语言、编码码流精简、编解码性能,jdk序列化机制都不能很好的支持,这就是为什么我们说不推荐使用jdk序列化机制的原因和理由。下面我们通过一个实际的案例,来进一步验证

2.2.jdk序列化机制有什么问题

我将通过通用的二进制机制,将对象编码为字节数组;与通过jdk序列化机制,将对象编码为字节数组。进行二者在

  • 码流大小
  • 编码性能

方面的对比,从而验证我们说jdk序列化机制存在的问题。案例代码非常简单,我直接贴出相关的代码,你一看应该就能明白了。其中你需要重点关注codeC()jdkCodeC()方法的实现

2.2.1.编码案例实现代码

/**
 * java序列化缺点分析
 * 1.不能跨语言
 * 2.序列化后码流太大
 * 3.序列化性能低
 * @author ThinkPad
 * @version 1.0
 * @date 2021/4/4 12:07
 */
public class User implements java.io.Serializable{
​
    private int userId;
    private String userName;
​
    public int getUserId() {
        return userId;
    }
​
    public void setUserId(int userId) {
        this.userId = userId;
    }
​
    public String getUserName() {
        return userName;
    }
​
    public void setUserName(String userName) {
        this.userName = userName;
    }
​
    //==================================================
​
    /**
     * 通用二进制机制,将User对象转换成字节数组
     * @return
     */
    public byte[] codeC(){
        // 创建字节缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
​
        // id序列化
        buffer.putInt(this.userId);
​
        // 名称序列化
        byte[] nameBytes = this.userName.getBytes();
        buffer.putInt(nameBytes.length);
        buffer.put(nameBytes);
        buffer.flip();
​
        // 将buffer转换成字节数组
        byte[] result = new byte[buffer.remaining()];
        buffer.get(result);
​
        return result;
    }
​
    /**
     * jdk序列化机制,将User对象转换成字节数组
     * @return
     */
    public byte[] jdkCodeC(){
        byte[] result = null;
        ByteArrayOutputStream bos = null;
        ObjectOutputStream oos = null;
        try {
            bos = new ByteArrayOutputStream();
            oos = new ObjectOutputStream(bos);
​
            // jdk序列化
            oos.writeObject(this);
            oos.flush();
​
            // 获取序列化后的字节数组
            result = bos.toByteArray();
​
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                oos.close();
                bos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
​
        return result;
    }
​
}

2.2.2.测试代码

2.2.2.1.码流大小测试
public static void main(String[] args) {
   // 1.创建用户对象
   User user = new User();
   user.setUserId(1);
   user.setUserName("Hello World!");
​
   // 2.通用二级制机制,将User对象转换字节数组
   byte[] codeC = user.codeC();
   log.info("通用二级制机制,将User对象转换字节数组,数组长度:{}",codeC.length);
​
   log.info("---------------------华丽丽分割线----------------------");
   // 3.jdk对象序列化机制,将User对象转换成字节数组
   byte[] jdkCodeC = user.jdkCodeC();
   log.info("jdk对象序列化机制,将User对象转换成字节数组,数组长度:{}", jdkCodeC.length);  
}
​
通用二级制机制,将User对象转换字节数组,数组长度:20
----------------------华丽丽分割线----------------------
jdk对象序列化机制,将User对象转换成字节数组,数组长度:111
2.2.2.2.编码性能测试
public static void main(String[] args) {
  // 1.创建用户对象
  User user = new User();
  user.setUserId(1);
  user.setUserName("Hello World!");
  // 性能测试对比
  int loop = 1000000;
  long start = System.currentTimeMillis();
  for(int i=0; i<loop; i++){
    user.codeC();
  }
  long end = System.currentTimeMillis();
  log.info("通用二进制机制,执行{}次数,共耗时{}毫秒",loop,(end - start));
​
  log.info("---------------------华丽丽分割线----------------------");
  long start1 = System.currentTimeMillis();
  for(int i=0; i<loop; i++){
       user.jdkCodeC();
  }
  long end1 = System.currentTimeMillis();
  log.info("jdk对象序列化机制,执行{}次数,共耗时{}毫秒",loop,(end1 - start1));
}
​
通用二进制机制,执行1000000次数,共耗时287毫秒
---------------------华丽丽分割线----------------------
jdk对象序列化机制,执行1000000次数,共耗时1372毫秒