引言
大家好,今天继续给大家带来seata相关代码的解析。今天讲到的是一个seata里面非常重要的一部分--事务的回滚,讲到事务的回滚,就必须讲到相关的数据是如何序列化和反序列化的,只有这部分正确运行,前镜像和后镜像才能正确,事务才能正确回滚。
那么今天就选了JacksonUndoLogParser来做解析,看看他是怎么样正确序列化和反序列化数据库的数组类型的。
什么是 SerialArray
-
SerialArray是 Seata 自定义的一个类(org.apache.seata.rm.datasource.sql.serial.SerialArray),用于封装 JDBC 中的java.sql.Array。 -
因为原生
java.sql.Array无法直接被 Jackson 序列化,且不同数据库对数组的支持差异大,Seata 将其"扁平化"为一个可序列化的对象:baseType:数组元素的 JDBC 类型(如Types.INTEGER = 4)baseTypeName:数组元素的 SQL 类型名(如"INTEGER"或"VARCHAR")elements:实际的数组元素(Object[])
这样就能把数据库中的数组字段纳入 undo log,支持回滚。
简单讲讲JacksonUndoLogParser?
它的核心作用是在分布式事务回滚过程中,将 BranchUndoLog 对象序列化为 JSON 字节数组(用于存储到数据库),以及从字节数组反序列化还原为 Java 对象(用于执行回滚) 。
实现接口
UndoLogParser:定义了encode(BranchUndoLog)和decode(byte[])方法,用于 undo log 的编解码。Initialize:提供初始化逻辑(通过init()方法注册自定义的序列化/反序列化器)
那么为什么要这么多自定义的序列化/反序列化器?我相信第一次看到代码的读者可能都会有这种疑惑 因为 BranchUndoLog 中可能包含 JDBC 特殊类型,如:
java.sql.Timestampjava.sql.Blob/SerialBlobjava.sql.Clob/SerialClobjava.time.LocalDateTime- 达梦数据库的
DmdbTimestamp - 数组类型
SerialArray
这些类型 Jackson 默认无法正确处理 (比如 Timestamp 的纳秒精度、Blob 的二进制内容等),所以必须提供自定义的序列化器与反序列化器且注册在Jackson的mapper里面,这样才可以正确处理这些类型。
java
mapper.registerModule(module);
关键配置
1.enableDefaultTyping用于启用 多态类型支持 (Polymorphic Deserialization)。因为 BranchUndoLog 中的记录(如 TableRecords)可能包含子类(如 SqlUndoLog),需要保留类型信息以便正确反序列化。所以如果观察日志就会发现json格式里面保留了@Class。
2.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)忽略未知字段,保证向前兼容(新版本加字段,旧版本仍能解析)
java
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
mapper.enable(MapperFeature.PROPAGATE_TRANSIENT_MARKER);
如何序列化和反序列化SerialArray数组类型
我们上面已经说了SerialArray,那么现在就可以看看如何序列化和反序列化这种数组类型的
自定义序列化器(Java 对象 → JSON)
先自定义一个序列化器,继承JsonSerializer<T>,并重写里面三个方法。
serializeWithType(...)
- 支持 多态序列化(Polymorphic Typing)
- 当 Jackson 启用了
enableDefaultTyping(Seata 默认开启),会先写入类型标识(如@class字段) - 确保反序列化时能正确识别这是
SerialArray而不是普通 Map
java
@Override
public void serializeWithType(
SerialArray serialArray,
JsonGenerator gen,
SerializerProvider serializers,
TypeSerializer typeSerializer)
throws IOException {
WritableTypeId typeIdDef =
typeSerializer.writeTypePrefix(gen, typeSerializer.typeId(serialArray, JsonToken.START_OBJECT));
serializeValue(serialArray, gen, serializers);
typeSerializer.writeTypeSuffix(gen, typeIdDef);
}
serialize(SerialArray, JsonGenerator, ...)
这里是执行数组类型序列化的关键方法,先写入'{'作为json数据的开始符号,再流式填充写入serialArray的内容,然后再以'}'作为json数据的结束符号。
这样就高效地完成了从对象到json格式的转换
java
@Override
public void serialize(SerialArray serialArray, JsonGenerator gen, SerializerProvider serializers)
throws IOException {
gen.writeStartObject();
serializeValue(serialArray, gen, serializers);
gen.writeEndObject();
}
serializeValue(...)
这里是将核心数据转换为json格式的重要方法。首先得先知道gen是核心实体,它负责生成json格式数据,还有很多通用方法用来生成一些固定格式[]或者{}等等。
在下面的代码核心体现就是写入SerialArray的相关数据,这很简单没啥好说的,值得注意的是,任何字段读取失败都写 null,避免整个 undo log 序列化失败。同时,元素类型自动处理:数字、字符串等由 gen.writeObject() 自动转为 JSON 值。了解这些就够了。
java
private void serializeValue(SerialArray serialArray, JsonGenerator gen, SerializerProvider serializers)
throws IOException {
gen.writeFieldName("baseType");
try {
gen.writeNumber(serialArray.getBaseType());
} catch (SQLException e) {
gen.writeNull();
}
gen.writeFieldName("baseTypeName");
try {
gen.writeString(serialArray.getBaseTypeName());
} catch (SQLException e) {
gen.writeNull();
}
gen.writeFieldName("elements");
try {
Object[] elements = serialArray.getElements();
gen.writeStartArray();
if (elements != null) {
for (Object element : elements) {
gen.writeObject(element);
}
}
gen.writeEndArray();
} catch (Exception e) {
gen.writeNull();
}
}
自定义反序列化器(JSON → Java 对象)
至于反序列化器就很简单了,只需要重写一个重要方法即可deserialize
根据json数据还原成SerialArray对象即可。回滚时,Seata 会根据 baseType 和 baseTypeName 决定如何将 elements 转回数据库原生数组类型(通过 JDBC 驱动)
java
private static class SerialArrayDeserializer extends JsonDeserializer<SerialArray> {
@Override
public SerialArray deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
try {
JsonNode node = p.getCodec().readTree(p);
SerialArray serialArray = new SerialArray();
if (node.has("baseType") && !node.get("baseType").isNull()) {
serialArray.setBaseType(node.get("baseType").asInt());
}
if (node.has("baseTypeName") && !node.get("baseTypeName").isNull()) {
serialArray.setBaseTypeName(node.get("baseTypeName").asText());
}
if (node.has("elements") && node.get("elements").isArray()) {
JsonNode elementsNode = node.get("elements");
Object[] elements = new Object[elementsNode.size()];
for (int i = 0; i < elementsNode.size(); i++) {
JsonNode elementNode = elementsNode.get(i);
if (elementNode.isNull()) {
elements[i] = null;
} else if (elementNode.isNumber()) {
elements[i] = elementNode.asLong();
} else if (elementNode.isTextual()) {
elements[i] = elementNode.asText();
} else {
elements[i] = elementNode;
}
}
serialArray.setElements(elements);
}
return serialArray;
} catch (Exception e) {
LOGGER.error("deserialize SerialArray error: {}", e.getMessage(), e);
return null;
}
}
}
总结
上面就是JacksonUndoLogParser回滚SerialArray类型的序列化和反序列化解析了。只有正确序列化和反序列化,前后镜像(也可以称为快照)才能保证事务回滚。在回滚的时候,前镜像是回滚的目标,后镜像可以是回滚的标准(可以看成乐观锁),如果有人修改了数据导致与后镜像不一致,则先不回滚,会抛出相应异常来处理。