在《如果可以,我想并行消费Kafka拉取的数据库Binlog》这篇文章中,笔者介绍如何实现并行消费Binlog,具体实现就是让同一张表的Binlog放到同一个线程去消费,用局部顺序消费换取消费速度,避免消息堆积。但在某些场景下,使用这种方式还是出现了问题,出现在关联表更新数据同步的先后顺序上。
在此分享下我们的解决方案:新增分组概念,将关联表放到同一分组,将同一分组的Binlog分配到同一线程消费。限制一个表只能分配到一个分组下,如果没有为表配置分组,则该表就是一个独立的分组,分组名称就是表名称。
在一次调试过程中笔者发现,日记打印的Binlog显示某些字段更新之前和更新之后都有值,可是到消费的时候获取字段的值却是null,如下图所示。
在此之前消费都是正常的,突然获取不到Binlog修改的字段值,而此次代码只是加了一条日记打印。添加打印消费到的每条Binlog记录的详细信息之后,消费就不正常了,并且现象很奇怪,数值类型与日期类型的字段依然能正常获取到值,只是字符串类型的字段获取不到值了。
排查此问题的思路:从Kafka拉取到消息到实际消费,这期间都做了什么,导致获取字段值为null?
使用kafka拉取Binlog需要使用Avro反序列化消息,消息反序列化后生成com.alibaba.dts.formats.avro.Record对象,该对象记录了Binlog的操作类型、操作的库和表、字段、字段修改之前的镜像值与修改之后的镜像值。
从Record对象获取字段的镜像值类型为"com.alibaba.dts.formats.avro"包下对应的类型,这些类型都提供有getValue方法获取值,但不同类型getValue方法返回值类型不同。
com.alibaba.dts.formats.avro.Integer的getValue方法返回值类型为java.lang.String;com.alibaba.dts.formats.avro.Float的getValue方法返回值类型为java.lang.Double;- ...
为便于使用,我们封装了获取镜像值的步骤,不管数据库字段是什么类型,统一将镜像值解析为字符串,并提供getAsString、getAsInteger、getAsFloat之类的API,不必再关心数据库该字段的值是什么类型,只需要关心我想要获取的值应该是什么类型。
例如,将字段的镜像值都解析为FieldValue实例,FieldValue类定义如下:
public class FieldValue {
private String encoding;
private byte[] bytes;
public String getAsString() {
return new String(bytes, encoding);
}
public Integer getAsInteger() {
return Integer.parseInt(getAsString());
}
public Long getAsLong() {
return Long.parseLong(getAsString());
}
public BigDecimal getAsBigDecimal() {
return new BigDecimal(getAsString());
}
// ......
}
以将com.alibaba.dts.formats.avro.Integer类型的字段值解析为FieldValue对象为例,实现解析代码如下。
static class NumberStringAdapter implements DataAdapter {
@Override
public FieldValue getFieldValue(Object data) {
FieldValue fieldValue = new FieldValue();
if (null != data) {
com.alibaba.dts.formats.avro.Integer integer = (com.alibaba.dts.formats.avro.Integer) data;
// 调用getValue获取字符串数值,并将字符串转为字节数组
fieldValue.setValue(integer.getValue().getBytes(US_ASCII));
}
fieldValue.setEncoding("ASCII");
return fieldValue;
}
}
为便于使用,我们还可以将字段、字段修改之前的镜像值、字段修改之后的镜像值解析为一个个FieldHolder实例,FieldHolder的定义如下。
public abstract class FieldHolder {
protected Field field;
// 当操作为插入时,此字段没有值
protected FieldValue beforeImage;
// 当操作为删除时,此操作没有值
protected FieldValue afterImage;
// 省略get方法
public FieldHolder(Field field, Object beforeImage, Object afterImage) {
//....
}
// 比较该字段的值是否修改了
public boolean isModify() {
if (beforeImage == null && afterImage == null) {
return false;
}
if (beforeImage == null) {
return true;
}
if (afterImage == null) {
return true;
}
return !getBeforeFieldValue().equals(getAfterFieldValue());
}
}
以Integer、Float(com.alibaba.dts.formats.avro包下的类)不同的是,Character、BinaryObject的getValue方法返回值类型为java.nio.ByteBuffer。将Character类型的字段值解析为FieldValue对象的实现代码如下。
static class CharacterAdapter implements DataAdapter {
@Override
public FieldValue getFieldValue(Object data) {
FieldValue fieldValue = new FieldValue();
if (null != data) {
com.alibaba.dts.formats.avro.Character character = (com.alibaba.dts.formats.avro.Character) data;
ByteBuffer buffer = character.getValue();
byte[] ret = new byte[buffer.remaining()];
buffer.get(ret);
fieldValue.setValue(ret);
fieldValue.setEncoding(character.getCharset());
} else {
fieldValue.setEncoding("ASCII");
}
return fieldValue;
}
}
可以看出,产生此次bug的原因在于,调用ByteBuffer的get方法读取数据后,ByteBuffer的读偏移量(position)等于limit,由于没有调用flip重置读指针为0,导致后续再调用getFieldValue解析同一个镜像值时,解析后的FieldValue对象的bytes字段值是空的。
应将代码改为如下:
static class CharacterAdapter implements DataAdapter {
@Override
public FieldValue getFieldValue(Object data) {
FieldValue fieldValue = new FieldValue();
if (null != data) {
com.alibaba.dts.formats.avro.Character character = (com.alibaba.dts.formats.avro.Character) data;
ByteBuffer buffer = character.getValue();
byte[] ret = new byte[buffer.remaining()];
buffer.get(ret);
fieldValue.setValue(ret);
fieldValue.setEncoding(character.getCharset());
character.getValue().flip();
} else {
fieldValue.setEncoding("ASCII");
}
return fieldValue;
}
}
当然,我们可以让整个Record记录只解析一次,后续就不会出现反复读取字段值的情况。
定义Record解析器接口:
public interface RecordResolver<T extends FieldHolder> {
String getDdl();
String getDatabase();
String getTable();
Operation getOperation();
FieldHolderMap<T> getFields();
}
根据Record对象创建Record解析器,在解析器构造方法中立即解析Record,外部每次调用Record解析器获取字段信息都是获取到已经解析好的。代码如下。
public abstract class BaseRecordResolver<T extends FieldHolder> implements RecordResolver<T> {
private String db;
private String table;
private String ddl;
private Operation operation;
private FieldHolderMap<T> fieldHolderMap;
public BaseRecordResolver(Record record) {
this.operation = record.getOperation();
String[] dbPair = uncompressionObjectName(record.getObjectName());
if (null != dbPair) {
this.db = dbPair[0];
if (dbPair.length == 2) {
table = dbPair[1];
} else if (dbPair.length == 3) {
table = dbPair[2];
} else if (dbPair.length == 1) {
table = "";
}
}
if (record.getOperation() == Operation.DDL) {
ddl = (String) record.getAfterImages();
} else if (record.getFields() != null) {
this.fieldHolderMap = readFieldInfo(record);
}
}
private FieldHolderMap<T> readFieldInfo(Record record) {
Iterator<Field> fields = ((List<Field>) record.getFields()).iterator();
Iterator<Object> beforeImages = null;
Iterator<Object> afterImages = null;
// update操作没有BeforeImages
if (record.getOperation() == Operation.UPDATE || record.getOperation() == Operation.DELETE) {
beforeImages = ((List<Object>) record.getBeforeImages()).iterator();
}
// delete操作没有AfterImages
if (record.getOperation() == Operation.INSERT || record.getOperation() == Operation.UPDATE) {
afterImages = ((List<Object>) record.getAfterImages()).iterator();
}
List<T> fieldHolders = new ArrayList<>(((List<Field>) record.getFields()).size());
while (fields.hasNext()
&& (beforeImages == null || beforeImages.hasNext())
&& (afterImages == null || afterImages.hasNext())) {
Field field = fields.next();
Object before = beforeImages == null ? null : beforeImages.next();
Object after = afterImages == null ? null : afterImages.next();
fieldHolders.add(resolverField(field, before, after));
}
return new FieldHolderMap<>(fieldHolders);
}
@Override
public String getDdl() {
if (operation == Operation.DDL) {
return this.ddl;
}
throw new IllegalArgumentException("not found ddl error");
}
@Override
public String getDatabase() {
return this.db;
}
@Override
public String getTable() {
return this.table;
}
@Override
public Operation getOperation() {
return this.operation;
}
@Override
public FieldHolderMap<T> getFields() {
if (getOperation() == Operation.DDL) {
throw new IllegalArgumentException("ddl not fields");
}
return this.fieldHolderMap;
}
//
protected abstract T resolverField(Field field, Object before, Object after);
}