深入挖掘 FST 快速序列化压缩内存的利器的特性和原理

本文详细阐述了FST(Fastestserializationtechnology)的序列化和反序列化机制,涉及版本控制、@Version注解的应用以及如何处理Bean定义变化带来的兼容问题,通过排序和比较规则保证了前后版本间的兼容性。
摘要由CSDN通过智能技术生成
  • 0000:字节数组类型:00 标识 OBJECT

  • 0001:类名编码,00 标识 UTF 编码,01 表示 ASCII 编码

  • 0002:Length of class name (1Byte) = 15

  • 0003~0011:Class name string (15Byte)

  • 0012:Integer 类型标识 0xf7

  • 0013:Integer 的值=1

  • 0014:String 类型标识 0xfc

  • 0015:String 的长度=5

  • 0016~001a:String 的值"v0str"

  • 001b~001c:END

从上面可以看到 Integer 类型序列化后只占用了一个字节(值等于 1),并不像在内存中占用 4Byte,所以可以看出是根据一定规则做了压缩,具体代码看FSTObjectInput#instantiateSpecialTag中对不同类型的读取,FSTObjectInput 也定义不同类型对应的枚举值:

public class FSTObjectOutput implements ObjectOutput {

private static final FSTLogger LOGGER = FSTLogger.getLogger(FSTObjectOutput.class);

public static Object NULL_PLACEHOLDER = new Object() {

public String toString() { return “NULL_PLACEHOLDER”; }};

public static final byte SPECIAL_COMPATIBILITY_OBJECT_TAG = -19; // see issue 52

public static final byte ONE_OF = -18;

public static final byte BIG_BOOLEAN_FALSE = -17;

public static final byte BIG_BOOLEAN_TRUE = -16;

public static final byte BIG_LONG = -10;

public static final byte BIG_INT = -9;

public static final byte DIRECT_ARRAY_OBJECT = -8;

public static final byte HANDLE = -7;

public static final byte ENUM = -6;

public static final byte ARRAY = -5;

public static final byte STRING = -4;

public static final byte TYPED = -3; // var class == object written class

public static final byte DIRECT_OBJECT = -2;

public static final byte NULL = -1;

public static final byte OBJECT = 0;

protected FSTEncoder codec;

}

FST 序列化和反序列化原理

对 Object 进行 Byte 序列化,相当于做了持久化的存储,在反序列的时候,如果 Bean 的定义发生了改变,那么反序列化器就要做兼容的解决方案,我们知道对于 JDK 的序列化和反序列,serialVersionUID 对版本控制起了很重要的作用。FST 对这个问题的解决方案是通过 @Version 注解进行排序。

在进行反序列操作的时候,FST 会先反射或者对象 Class 的所有成员,并对这些成员进行了排序,这个排序对兼容起了关键作用,也就是 @Version 的原理。在 FSTClazzInfo 中定义了一个 defFieldComparator 比较器,用于对 Bean 的所有 Field 进行排序:

public final class FSTClazzInfo {

public static final Comparator defFieldComparator = new Comparator() {

@Override

public int compare(FSTFieldInfo o1, FSTFieldInfo o2) {

int res = 0;

if ( o1.getVersion() != o2.getVersion() ) {

return o1.getVersion() < o2.getVersion() ? -1 : 1;

}

// order: version, boolean, primitives, conditionals, object references

if (o1.getType() == boolean.class && o2.getType() != boolean.class) {

return -1;

}

if (o1.getType() != boolean.class && o2.getType() == boolean.class) {

return 1;

}

if (o1.isConditional() && !o2.isConditional()) {

res = 1;

} else if (!o1.isConditional() && o2.isConditional()) {                res = -1;

} else if (o1.isPrimitive() && !o2.isPrimitive()) {                                 res = -1;

} else if (!o1.isPrimitive() && o2.isPrimitive())                                   res = 1;

//             if (res == 0) // 64 bit / 32 bit issues

//                 res = (int) (o1.getMemOffset() - o2.getMemOffset());

if (res == 0)

res = o1.getType().getSimpleName().compareTo(o2.getType().getSimpleName());            if (res == 0)

res = o1.getName().compareTo(o2.getName());

if (res == 0) {    return o1.getField().getDeclaringClass().getName().compareTo(o2.getField().getDeclaringClass().getName());

}

return res;

}

};

}

从代码实现上可以看到,比较的优先级是 Field 的 Version 大小,然后是 Field 类型,所以总的来说 Version 越大排序越靠后,至于为什么要排序,看下 FSTObjectInput#instantiateAndReadNoSer 方法

public class FSTObjectInput implements ObjectInput {

protected Object instantiateAndReadNoSer(Class c, FSTClazzInfo clzSerInfo, FSTClazzInfo.FSTFieldInfo referencee, int readPos) throws Exception {                   Object newObj;                                                                 newObj = clzSerInfo.newInstance(getCodec().isMapBased());

} else {

FSTClazzInfo.FSTFieldInfo[] fieldInfo = clzSerInfo.getFieldInfo();             readObjectFields(referencee, clzSerInfo, fieldInfo, newObj,0,0);           }

return newObj;

}

protected void readObjectFields(FSTClazzInfo.FSTFieldInfo referencee, FSTClazzInfo serializationInfo, FSTClazzInfo.FSTFieldInfo[] fieldInfo, Object newObj, int startIndex, int version) throws Exception {

if ( getCodec().isMapBased() ) {

readFieldsMapBased(referencee, serializationInfo, newObj);                     if ( version >= 0 && newObj instanceof Unknown == false)                           getCodec().readObjectEnd();

return;

}

if ( version < 0 )

version = 0;

int booleanMask = 0;

int boolcount = 8;

final int length = fieldInfo.length;

int conditional = 0;

for (int i = startIndex; i < length; i++) {  // 注意这里的循环

try {

FSTClazzInfo.FSTFieldInfo subInfo = fieldInfo[i];                            if (subInfo.getVersion() > version ) {   // 需要进入下一个版本的迭代

int nextVersion = getCodec().readVersionTag();  // 对象流的下一个版本

if ( nextVersion == 0 ) // old object read

{

oldVersionRead(newObj);

return;

}

if ( nextVersion != subInfo.getVersion() ) {  // 同一个Field的版本不允许变,并且版本变更和流的版本保持同步

throw new RuntimeException("read version tag “+nextVersion+” fieldInfo has "+subInfo.getVersion());

}          readObjectFields(referencee,serializationInfo,fieldInfo,newObj,i,nextVersion);  // 开始下一个Version的递归

return;

}

if (subInfo.isPrimitive()) {

} else {

if ( subInfo.isConditional() ) {

} // object 把读出来的值保存到FSTFieldInfo中                   Object subObject = readObjectWithHeader(subInfo);                   subInfo.setObjectValue(newObj, subObject);

}

从这段代码的逻辑基本就可以知道 FST 的序列化和反序列化兼容的原理了,注意里面的循环,正是按照排序后的 Filed 进行循环,而每个 FSTFieldInfo 都记录自己在对象流中的位置、类型等详细信息:

序列化:

  • 按照 Version 对 Bean 的所有 Field 进行排序(不包括 static 和 transient 修饰的 member),没有 @Version 注解的 Field 默认 version=0;如果 version 相同,按照 version, boolean, primitives, conditionals, object references 排序

  • 按照排序的 Field 把 Bean 的 Field 逐个写到输出流

  • @Version 的版本只能加不能减小,如果相等的话,有可能因为默认的排序规则,导致流中的 Filed 顺序和内存中的 FSTFieldInfo[]数组的顺序不一致,而注入错误

反序列化:

  • 反序列化按照对象流的格式进行解析,对象流中保存的 Field 顺序和内存中的 FSTFieldInfo 顺序保持一致

  • 相同版本的 Field 在对象流中存在,在内存 Bean 中缺失:可能抛异常(会有后向兼容问题)

  • 对象流中包含内存 Bean 中没有的高版本 Field:正常(老版本兼容新)

  • 相同版本的 Field 在对象流中缺失,在内存 Bean 中存在:抛出异常

  • 相同的 Field 在对象流和内存 Bean 中的版本不一致:抛出异常

  • 内存 Bean 增加了不高于最大版本的 Field:抛出异常

所以从上面的代码逻辑就可以分析出这个使用规则:@Version 的使用原则就是,每新增一个 Field,就对应的加上 @Version 注解,并且把 version 的值设置为当前版本的最大值加一,不允许删除 Field

另外再看一下 @Version 注解的注释:明确说明了用于后向兼容

package org.nustaq.serialization.annotations;

import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)

@Target({ElementType.FIELD})

/**

* support for adding fields without breaking compatibility to old streams.

* For each release of your app increment the version value. No Version annotation means version=0.

* Note that each added field needs to be annotated.

*

* e.g.

*

* class MyClass implements Serializable {

*

*     // fields on initial release 1.0

*     int x;

*     String y;

*

*     // fields added with release 1.5

*     @Version(1) String added;

*     @Version(1) String alsoAdded;

*

*     // fields added with release 2.0

*     @Version(2) String addedv2;

*     @Version(2) String alsoAddedv2;

*

* }

*

* If an old class is read, new fields will be set to default values. You can register a VersionConflictListener

* at FSTObjectInput in order to fill in defaults for new fields.

*

* Notes/Limits:

* - Removing fields will break backward compatibility. You can only Add new fields.

* - Can slow down serialization over time (if many versions)

* - does not work for Externalizable or Classes which make use of JDK-special features such as readObject/writeObject

*   (AKA does not work if fst has to fall back to ‘compatible mode’ for an object).

* - in case you use custom serializers, your custom serializer has to handle versioning

*

*/public @interface Version {

byte value();

}

public class FSTBean implements Serializable {

/** serialVersionUID */

private static final long serialVersionUID = -2708653783151699375L;             private Integer v0in

private String v0str;

}

准备序列化和反序列化方法

public class FSTSerial {

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

惊喜

最后还准备了一套上面资料对应的面试题(有答案哦)和面试时的高频面试算法题(如果面试准备时间不够,那么集中把这些算法题做完即可,命中率高达85%+)

image.png

image.png

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
频,并且会持续更新!**

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

惊喜

最后还准备了一套上面资料对应的面试题(有答案哦)和面试时的高频面试算法题(如果面试准备时间不够,那么集中把这些算法题做完即可,命中率高达85%+)

[外链图片转存中…(img-j5PIGWSr-1713747467864)]

[外链图片转存中…(img-IQosOXYJ-1713747467864)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 20
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值