1. seata
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
安装使用方法可看《seata官网》
2. 遇到的问题
使用的版本为seata 1.4.2
2.1 序列化异常
2.1.1 异常信息
堆栈信息
Type id handling not implemented for type java.lang.Object (by serializer of type com.fasterxml.jackson.databind.ser.impl.UnsupportedTypeSerializer) (through reference chain: io.seata.rm.datasource.undo.BranchUndoLog["sqlUndoLogs"]->java.util.ArrayList[0]->io.seata.rm.datasource.undo.SQLUndoLog["afterImage"]->io.seata.rm.datasource.sql.struct.TableRecords["rows"]->java.util.ArrayList[0]->io.seata.rm.datasource.sql.struct.Row["fields"]->java.util.ArrayList[16]->io.seata.rm.datasource.sql.struct.Field["value"])
debug调试得知,在undoLog序列化为JSON的时候,LocalDateTime无法序列化,源码在JacksonUndoLogParser
,以下是JacksonUndoLogParser源码
@LoadLevel(name = JacksonUndoLogParser.NAME)
public class JacksonUndoLogParser implements UndoLogParser, Initialize {
public static final String NAME = "jackson";
private static final Logger LOGGER = LoggerFactory.getLogger(JacksonUndoLogParser.class);
private final ObjectMapper mapper = new ObjectMapper();
private final SimpleModule module = new SimpleModule();
/**
* customize serializer for java.sql.Timestamp
*/
private final JsonSerializer timestampSerializer = new TimestampSerializer();
/**
* customize deserializer for java.sql.Timestamp
*/
private final JsonDeserializer timestampDeserializer = new TimestampDeserializer();
/**
* customize serializer of java.sql.Blob
*/
private final JsonSerializer blobSerializer = new BlobSerializer();
/**
* customize deserializer of java.sql.Blob
*/
private final JsonDeserializer blobDeserializer = new BlobDeserializer();
/**
* customize serializer of java.sql.Clob
*/
private final JsonSerializer clobSerializer = new ClobSerializer();
/**
* customize deserializer of java.sql.Clob
*/
private final JsonDeserializer clobDeserializer = new ClobDeserializer();
@Override
public void init() {
try {
List<JacksonSerializer> jacksonSerializers = EnhancedServiceLoader.loadAll(JacksonSerializer.class);
if (CollectionUtils.isNotEmpty(jacksonSerializers)) {
for (JacksonSerializer jacksonSerializer : jacksonSerializers) {
Class type = jacksonSerializer.type();
JsonSerializer ser = jacksonSerializer.ser();
JsonDeserializer deser = jacksonSerializer.deser();
if (type != null) {
if (ser != null) {
module.addSerializer(type, ser);
}
if (deser != null) {
module.addDeserializer(type, deser);
}
LOGGER.info("jackson undo log parser load [{}].", jacksonSerializer.getClass().getName());
}
}
}
} catch (EnhancedServiceNotFoundException e) {
LOGGER.warn("JacksonSerializer not found children class.", e);
}
module.addSerializer(Timestamp.class, timestampSerializer);
module.addDeserializer(Timestamp.class, timestampDeserializer);
module.addSerializer(SerialBlob.class, blobSerializer);
module.addDeserializer(SerialBlob.class, blobDeserializer);
module.addSerializer(SerialClob.class, clobSerializer);
module.addDeserializer(SerialClob.class, clobDeserializer);
mapper.registerModule(module);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
mapper.enable(MapperFeature.PROPAGATE_TRANSIENT_MARKER);
}
@Override
public String getName() {
return NAME;
}
@Override
public byte[] getDefaultContent() {
return "{}".getBytes(Constants.DEFAULT_CHARSET);
}
@Override
public byte[] encode(BranchUndoLog branchUndoLog) {
try {
return mapper.writeValueAsBytes(branchUndoLog);
} catch (JsonProcessingException e) {
LOGGER.error("json encode exception, {}", e.getMessage(), e);
throw new RuntimeException(e);
}
}
@Override
public BranchUndoLog decode(byte[] bytes) {
try {
BranchUndoLog branchUndoLog;
if (Arrays.equals(bytes, getDefaultContent())) {
branchUndoLog = new BranchUndoLog();
} else {
branchUndoLog = mapper.readValue(bytes, BranchUndoLog.class);
}
return branchUndoLog;
} catch (IOException e) {
LOGGER.error("json decode exception, {}", e.getMessage(), e);
throw new RuntimeException(e);
}
}
/**
* if necessary
* extend {@link ArraySerializerBase}
*/
private static class TimestampSerializer extends JsonSerializer<Timestamp> {
@Override
public void serializeWithType(Timestamp timestamp, JsonGenerator gen, SerializerProvider serializers,
TypeSerializer typeSerializer) throws IOException {
WritableTypeId typeId = typeSerializer.writeTypePrefix(gen,
typeSerializer.typeId(timestamp, JsonToken.START_ARRAY));
serialize(timestamp, gen, serializers);
gen.writeTypeSuffix(typeId);
}
@Override
public void serialize(Timestamp timestamp, JsonGenerator gen, SerializerProvider serializers) {
try {
gen.writeNumber(timestamp.getTime());
gen.writeNumber(timestamp.getNanos());
} catch (IOException e) {
LOGGER.error("serialize java.sql.Timestamp error : {}", e.getMessage(), e);
}
}
}
/**
* if necessary
* extend {@link JsonNodeDeserializer}
*/
private static class TimestampDeserializer extends JsonDeserializer<Timestamp> {
@Override
public Timestamp deserialize(JsonParser p, DeserializationContext ctxt) {
if (p.isExpectedStartArrayToken()) {
ArrayNode arrayNode;
try {
arrayNode = p.getCodec().readTree(p);
Timestamp timestamp = new Timestamp(arrayNode.get(0).asLong());
timestamp.setNanos(arrayNode.get(1).asInt());
return timestamp;
} catch (IOException e) {
LOGGER.error("deserialize java.sql.Timestamp error : {}", e.getMessage(), e);
}
}
LOGGER.error("deserialize java.sql.Timestamp type error.");
return null;
}
}
/**
* the class of serialize blob type
*/
private static class BlobSerializer extends JsonSerializer<SerialBlob> {
@Override
public void serializeWithType(SerialBlob blob, JsonGenerator gen, SerializerProvider serializers,
TypeSerializer typeSer) throws IOException {
WritableTypeId typeIdDef = typeSer.writeTypePrefix(gen,
typeSer.typeId(blob, JsonToken.VALUE_EMBEDDED_OBJECT));
serialize(blob, gen, serializers);
typeSer.writeTypeSuffix(gen, typeIdDef);
}
@Override
public void serialize(SerialBlob blob, JsonGenerator gen, SerializerProvider serializers) throws IOException {
try {
gen.writeBinary(blob.getBytes(1, (int)blob.length()));
} catch (SerialException e) {
LOGGER.error("serialize java.sql.Blob error : {}", e.getMessage(), e);
}
}
}
/**
* the class of deserialize blob type
*/
private static class BlobDeserializer extends JsonDeserializer<SerialBlob> {
@Override
public SerialBlob deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
try {
return new SerialBlob(p.getBinaryValue());
} catch (SQLException e) {
LOGGER.error("deserialize java.sql.Blob error : {}", e.getMessage(), e);
}
return null;
}
}
/**
* the class of serialize clob type
*/
private static class ClobSerializer extends JsonSerializer<SerialClob> {
@Override
public void serializeWithType(SerialClob clob, JsonGenerator gen, SerializerProvider serializers,
TypeSerializer typeSer) throws IOException {
WritableTypeId typeIdDef = typeSer.writeTypePrefix(gen,
typeSer.typeId(clob, JsonToken.VALUE_EMBEDDED_OBJECT));
serialize(clob, gen, serializers);
typeSer.writeTypeSuffix(gen, typeIdDef);
}
@Override
public void serialize(SerialClob clob, JsonGenerator gen, SerializerProvider serializers) throws IOException {
try {
gen.writeString(clob.getCharacterStream(), (int)clob.length());
} catch (SerialException e) {
LOGGER.error("serialize java.sql.Blob error : {}", e.getMessage(), e);
}
}
}
private static class ClobDeserializer extends JsonDeserializer<SerialClob> {
@Override
public SerialClob deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
try {
return new SerialClob(p.getValueAsString().toCharArray());
} catch (SQLException e) {
LOGGER.error("deserialize java.sql.Clob error : {}", e.getMessage(), e);
}
return null;
}
}
}
从上面可知,Jackson ObjectMapper对象,初始化的时候没有针对LocalDateTime
进行序列化和反序列化。故而无法序列化为JSON,在1.5.1版本已经修复,
2.1.2 解决办法
1.5.1 处理的源码如下
2.1.2.1 升级seata版本至1.5.1
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.5.1</version>
</dependency>
2.1.2.2 加上LocalDateTime
序列化和反序列化spi
从上面1.4.2源码可知,seata提供了spi机制进行动态的添加序列化和反序列化机制的实现
由此,我们只需要实现JacksonSerializer
接口,即可实现序列化和反序列化
- 新建LocalDateTimeJacksonSerializer类 代码如下:
package io.seata.rm.datasource.undo.parser.spi;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* description: seata LocalDateTime 序列化扩展点
*
* @author zhouxinlei
* @since 2022-07-2022/7/21 11:31:48}
*/
public class LocalDateTimeJacksonSerializer implements JacksonSerializer<LocalDateTime> {
/**
* 标准日期时间格式,精确到毫秒:yyyy-MM-dd HH:mm:ss.SSS
*/
public static final String NORM_DATETIME_MS_PATTERN = "yyyy-MM-dd HH:mm:ss.SSS";
@Override
public Class<LocalDateTime> type() {
return LocalDateTime.class;
}
@Override
public JsonSerializer<LocalDateTime> ser() {
return new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(NORM_DATETIME_MS_PATTERN));
}
@Override
public JsonDeserializer<? extends LocalDateTime> deser() {
return new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(NORM_DATETIME_MS_PATTERN));
}
}
- SPI装配
同时,在resources
META-INF/seata
目录,新建io.seata.rm.datasource.undo.parser.spi.JacksonSerializer
文件 ,加入我们刚刚实现的LocalDateTimeJacksonSerializer
io.seata.rm.datasource.undo.parser.spi.LocalDateTimeJacksonSerializer
至此就支持 LocalDateTime 的序列化和反序列化了。
2.1.2.2 更换序列化方式
- FastjsonUndoLogParser:Fastjson序列化工具
- FstUndoLogParser:Fst序列化工具
- JacksonUndoLogParser:Jackson序列化工具
- KryoUndoLogParser:Kryo序列化工具
- ProtostuffUndoLogParser:Protostuff序列化工具
使用KryoUndoLogParser序列化工具,实测,插入没问题,但是在反序列化的时候又会出现反序列化LocalDateTime
失败,建议不使用。其他的还没有试过
2.2 undo_log needs to contain the primary key
2.2.1 堆栈信息
java.sql.SQLException: io.seata.common.exception.NotSupportYetException: undo_log needs to contain the primary key.
at io.seata.rm.datasource.undo.mysql.MySQLUndoLogManager.insertUndoLog(MySQLUndoLogManager.java:93)
at io.seata.rm.datasource.undo.mysql.MySQLUndoLogManager.insertUndoLogWithNormal(MySQLUndoLogManager.java:74)
at io.seata.rm.datasource.undo.AbstractUndoLogManager.flushUndoLogs(AbstractUndoLogManager.java:242)
at io.seata.rm.datasource.ConnectionProxy.processGlobalTransactionCommit(ConnectionProxy.java:255)
at io.seata.rm.datasource.ConnectionProxy.doCommit(ConnectionProxy.java:230)
at io.seata.rm.datasource.ConnectionProxy.lambda$commit$0(ConnectionProxy.java:188)
at io.seata.rm.datasource.ConnectionProxy$LockRetryPolicy.execute(ConnectionProxy.java:333)
at io.seata.rm.datasource.ConnectionProxy.commit(ConnectionProxy.java:187)
官方提供的seata at 模式的脚本undo_log 是没有设置主键的,所以我们设要设置undo_log日志表的branch_id字段为主键或者定义一个自增id
CREATE TABLE `undo_log`
(
`id` INT(11) NOT NULL AUTO_INCREMENT,
`branch_id` BIGINT(20) NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(100) NOT NULL COMMENT 'global transaction id',
`context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info',
`log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` datetime(6) NOT NULL COMMENT 'create datetime',
`log_modified` datetime(6) NOT NULL COMMENT 'modify datetime',
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = INNODB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8 COMMENT = 'AT transaction mode undo table'