在最近的计划中,打算看看在不使用google protobuf的情况下,在原有的采用jackson作为json序列化工具的基础上,是否可以实现进一步的性能优化。主要是针对list的情况,在一些包含比较大的对象比如有上百个对象的列表序列化、反序列化的逻辑中,有一个序列化+反序列化操作,他们加起来时间占据了接近1/3,由此可见为了达到高TPS,序列化的性能和大小也是不可忽视的。
测试的时候选择了一个50个字段的对象,采用50条记录的list作为例子。因为大部分还都是可控的系统rpc交互,所以测试的时候选择了将字段用逗号分隔的方式。
在反射机制中,Reflection和BeanInfo两种均作了测试,method/field都做了缓存的前提,结果中与原生jackson序列化、反序列化性能相差在20%以内,并无明显的提升。
同时,在测试中将class缓存后性能提升约10%,将setAccessible设置为true后性能提升也在10%左右。字符串的解析并没有太大的性能消耗,反而是invoke消耗了绝大部分的性能。
所以,至少就原生的Java反射机制而言,性能并没有明显的提升(当然,性能跟字段数还是有一定的关系,如果字段数较少、行数没有那么多,那么class每次加载的比例可能会增加)。
除了json外,还可以考虑kryo序列化方式,其性能比json高出不少,同时没有pb/flatbuffer一样的额外结构维护要求。如下:
// 序列化、反序列化性能测试 UserRole userRole = new UserRole(); userRole.setRoleId(1L); Role role = new Role(); role.setRoleCode("roleCode"); role.setCurrentUserId(1L); role.setSystemRoleFlag(true); role.setTenantCode("ta"); userRole.setRole(role); User user =new User(); user.setPassPercent(10); user.setAddress("wwwfw"); user.setPassModifytime(new Date()); userRole.setUser(user); UserRole newUserRole = null; // kryo序列化 long beg = System.currentTimeMillis(); for (int i=0;i<100000;i++) { newUserRole = deserialize(serialize(userRole)); } System.out.println(System.currentTimeMillis()-beg); // 1443毫秒 // jdk序列化 beg = System.currentTimeMillis(); for (int i=0;i<100000;i++) { ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream os = new ObjectOutputStream(bos); os.writeObject(userRole); os.flush(); os.close(); byte[] b = bos.toByteArray(); bos.close(); ByteArrayInputStream bis = new ByteArrayInputStream(b); ObjectInputStream is = new ObjectInputStream(bis); newUserRole = (UserRole) is.readObject(); } System.out.println(System.currentTimeMillis()-beg); // 8000毫秒 // TODO 自定义序列化,非性能极端场景不要使用,太不够灵活 // json序列化 beg = System.currentTimeMillis(); for (int i=0;i<100000;i++) { newUserRole = JsonUtils.json2Object(JsonUtils.toJson(userRole),UserRole.class); } System.out.println(System.currentTimeMillis()-beg); // 3934毫秒 // 性能测试结束
对于序列化来说,要考虑的是兼容性。在开发中,增减字段是常见的事,由于我们会使用大量的缓存,因此如何在结构变化后保持兼容性非常重要的。对于JDK序列化来说,只要serialVersionUID相同,就可以无缝处理。如下:
@Getter@Setter public class ShareDetail implements Serializable { private static final long serialVersionUID = 1000L; private String tableName; private String status; // 动态改变 private Date date; }
// 结构兼容性测试 // 写 // ShareDetail sh = new ShareDetail(); // sh.setTableName("t"); // sh.setStatus("s"); // FileOutputStream bos = new FileOutputStream("d:\javaserial.dat"); // ObjectOutputStream os = new ObjectOutputStream(bos); // os.writeObject(sh); // os.flush(); // os.close(); // bos.close(); // 读,新增Date字段 FileInputStream bis = new FileInputStream("d:\javaserial.dat"); ObjectInputStream is = new ObjectInputStream(bis); // 没有serialVersionUID的话,报Exception in thread "main" java.io.InvalidClassException: com.hundsun.ta.base.service.ShareDetail; local class incompatible: stream classdesc serialVersionUID = 161953476132204792, local class serialVersionUID = 3990225612413777011 ShareDetail sh = (ShareDetail) is.readObject(); System.out.println(sh.getStatus());
/** * Kryo本身非线程安全,需要使用线程本地变量,也可以线程池,参见https://www.jianshu.com/p/f56c9360936d */ private static ThreadLocal<Kryo> kryo = new ThreadLocal<Kryo>() { @Override protected Kryo initialValue() { Kryo kryo = new Kryo(); kryo.setDefaultSerializer(CompatibleFieldSerializer.class); return kryo; } }; /** * 注意越界 https://m.2cto.com/kf/201612/573016.html、https://www.jianshu.com/p/43008038866c * @param o * @return */ public static byte[] serialize(Object o) { Output output = new Output(4096,65536); kryo.get().writeClassAndObject(output, o); byte[] bytes = output.toBytes(); output.flush(); output.close(); return bytes; } /** * T 定义修改后,默认会报Exception in thread "main" com.esotericsoftware.kryo.KryoException: Encountered unregistered class ID: 13994 * 需使用CompatibleFieldSerializer序列化器。在目标类上增加@DefaultSerializer(CompatibleFieldSerializer.class)也行 * 字段顺序的变化不会导致反序列化失败,因为写入的时候会排序 * 相关其他限制可以参考https://www.cnblogs.com/hntyzgn/p/7122709.html * @param bytes * @param <T> * @return */ public static <T> T deserialize(byte[] bytes) { Input input = new Input(bytes); T o = (T)kryo.get().readClassAndObject(input); return o; }
// 使用CompatibleFieldSerializer序列化支持向前或向后兼容。
// kryo // FileOutputStream bos = new FileOutputStream("d:\kryo.dat"); // ShareDetail sh = new ShareDetail(); // sh.setTableName("t"); // sh.setStatus("s"); // bos.write(serialize(sh)); // bos.flush(); // bos.close(); FileInputStream bis = new FileInputStream("d:\kryo.dat"); byte[] buffer = new byte[74]; int len = bis.read(buffer); ShareDetail sh = deserialize(buffer); System.out.println(sh.getStatus());