seata的JacksonUndoLogParser回滚SerialArray类型的序列化和反序列化解析

引言

大家好,今天继续给大家带来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.Timestamp
  • java.sql.Blob / SerialBlob
  • java.sql.Clob / SerialClob
  • java.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 会根据 baseTypebaseTypeName 决定如何将 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类型的序列化和反序列化解析了。只有正确序列化和反序列化,前后镜像(也可以称为快照)才能保证事务回滚。在回滚的时候,前镜像是回滚的目标,后镜像可以是回滚的标准(可以看成乐观锁),如果有人修改了数据导致与后镜像不一致,则先不回滚,会抛出相应异常来处理。

相关推荐
武子康2 小时前
大数据-153 Apache Druid 实时接入 Kafka:从摄取到查询的完整实战
大数据·后端·nosql
草莓熊Lotso2 小时前
Git 本地操作入门:版本控制基础、跨平台部署与仓库核心流程
开发语言·人工智能·经验分享·git·后端·架构·gitee
q***06472 小时前
开源模型应用落地-FastAPI-助力模型交互-进阶篇-中间件(四)
开源·交互·fastapi
百锦再2 小时前
大话Rust的前生今世
开发语言·后端·rust·go·内存·时间·抽象
洛克大航海3 小时前
PyCharm 软件关联 GitHub 账户
ide·pycharm·github
q***71853 小时前
开源数据同步中间件(Dbsyncer)简单玩一下 mysql to mysql 的增量,全量配置
mysql·中间件·开源
隐形喷火龙3 小时前
Springboot集成OnlyOffice
java·spring boot·后端
晨非辰3 小时前
【数据结构】排序详解:从快速排序分区逻辑,到携手冒泡排序的算法效率深度评测
运维·数据结构·c++·人工智能·后端·深度学习·排序算法
5pace3 小时前
【SSM|第一篇】MyBatisPlus
java·spring boot·后端·mybatis