简介
一个基于springcloud的分布式广告系统。
监听binlog文件,将信息写入kafka,微服务订阅kafka中的binlog信息构建增量索引。
binlog
MySQL :: MySQL 8.0 Reference Manual :: 5.4.4 The Binary Log
腾讯工程师带你深入解析 MySQL binlog - 知乎 (zhihu.com)
Mysql Binlog是二进制格式的日志文件,但是不能把binlog文件等同于OS系统某目录下的具体文件,这是狭隘的。Binlog是用来记录Mysql内部对数据库的改动(只记录对数据的修改操作),主要用于数据库的主从复制以及增量恢复。
监听MySQL的binlog文件构造增量数据。为什么会选用binlog作为增量数据的收集方案?为什么不在广告投放系统里直接将广告数据加入到广告检索系统中,而是要监听MySQL的binlog文件,实现广告检索系统中增量索引的构建?
- 核心是降低耦合
使用shyiko/mysql-binlog-connector-java: MySQL Binary Log connector (github.com)实现对binlog文件的监听。
关于mysql-binlog日志解析框架 - 知乎 (zhihu.com)
通过mysql-binlog-connector-java监控 MySQL 的 binlog_不负好时光的博客-CSDN博客_mysql-binlog-connector
ad-binlog-kafka
这是一个微服务,负责监听binlog,处理binlog数据,将处理后的数据MySqlRowData 投递到kafka的topic中。
为什么要把binlog的增量数据发送的kafka上,微服务再去订阅kafka的消息?
-
如果不使用kafka,就需要所有的
ad-search检索服务实例都去监听binlog,每一个数据库连接实例都会占用一个数据库连接线程,数据传输也会消耗网络IO -
加入了kafka之后,只需要一个实例去监听binlog数据,并投递到kafka中,每个
ad-search检索服务从kafka中读取就行了。
TemplateHolder
@PostConstruct
private void init() {
log.info("TemplateHolder Init");
loadJson("template.json");//加载template.json配置文件
}
类TemplateHolder中有一个JsonTemplateInfo类型的参数,TemplateHolder的作用就是填充JsonTemplateInfo。
JsonTemplateInfo中有一个Map,key是表的名称,value是表的属性JsonTableInfo。
加载配置文件
template.json配置文件中存放了:某个表,在某个操作下(增、删、改),需要哪些字段。
比如ad_plan表在insert和update时都需要id、user_id、plan_status、start_date、end_date这5个字段,但是delete时只需要id这一个字段。每个表都有一份这样的数据在template.json文件中。
将文件内容加载到类JsonTemplate.class中
解析JsonTemplate
将JsonTemplate解析为JsonTemplateInfo,本质是将JsonTemplate中的JsonTable里的数据转到JsonTemplateInfo中的JsonTableInfo。
public class JsonTableInfo {
private String tableName;
private String level;
public JsonTableInfo(String tableName, String level){
this.tableName=tableName;
this.level=level;
}
//操作类型 -> 字段名列表
private Map<OpType, List<String>> opTypeFieldSetMap=new HashMap<>();
/**
* 字段索引 -> 字段名
*/
private Map<Integer,String> posMap=new HashMap<>();
}
解析完成后tableName、level、opTypeFieldSetMap应该填充完毕了,opTypeFieldSetMap尤为重要。
注意此时posMap还没有填充。
查询每张表的schema信息
目的是填充JsonTemplateInfo中JsonTableInfo中的posMap。
private final String SQL_SCHEMA = "select table_schema, table_name, column_name, ordinal_position " +
"from information_schema.columns " +
"where table_schema = ? and table_name = ?";
private void loadMeta() {
for (Map.Entry<String, JsonTableInfo> entry : this.jsonTemplateInfo.getTableTemplateMap().entrySet()) {
JsonTableInfo jsonTableInfo = entry.getValue();
List<String> updateFields = jsonTableInfo.getOpTypeFieldSetMap().get(OpType.UPDATE);
List<String> insertFields = jsonTableInfo.getOpTypeFieldSetMap().get(OpType.ADD);
List<String> deleteFields = jsonTableInfo.getOpTypeFieldSetMap().get(OpType.DELETE);
Object[] paramValues = new Object[]{this.jsonTemplateInfo.getDBName(), jsonTableInfo.getTableName()};
jdbcTemplate.query(
SQL_SCHEMA,
paramValues,
(rs, i) -> {
int pos = rs.getInt("ORDINAL_POSITION");
String colName = rs.getString("COLUMN_NAME");
if ((null != updateFields && updateFields.contains(colName))//包含这个字段才需要记录
|| (null != insertFields && insertFields.contains(colName))
|| (null != deleteFields && deleteFields.contains(colName))) {
jsonTableInfo.getPosMap().put(pos - 1, colName);
}
return null;
}
);
}
}
为什么需要这这一步,来填充posMap呢?
/**
* 字段索引 -> 字段名
*/
private Map<Integer,String> posMap=new HashMap<>();
下面介绍AggregationListener时,有一步EventData -> BinlogRowData。EventData中字段都是以字段索引(int)的形式存在的,而我们处理表数据时,需要字段的名字(string),所以BinlogRowData中字段是以字段名的形式存在的,这时就需要字段索引到字段名的映射这个posMap。
监听binlog
BinlogClient启动类
public class BinlogRunner implements CommandLineRunner
实现CommandLineRunner接口为了在微服务启动时就自动开启对binlog的监听。
BinlogClient
起一个线程运行BinaryLogClient。
-
设置
BinaryLogClient基础参数:host、port、username、password -
设置filename、position(如果有需要)
-
注册事件监听器
registerEventListener() -
client.connect();,连接到复制流(replication stream),此方法会阻塞,直到断开连接
AggregationListener
AggregationListener实现BinaryLogClient.EventListener接口,在BinaryLogClient连接到复制流前,把AggregationListener注册成为BinaryLogClient的事件监听器。
实现EventListener接口就是实现onEvent(Event event)方法。
- 获取Event事件类型
EventType type = event.getHeader().getEventType();
- TABLE_MAP_EVENT:用于描述表的内部ID和结构定义
- ROW_EVENT:每个ROW_EVENT之前都有一个TABLE_MAP_EVENT
- EXT_UPDATE_ROWS:在DB中修改数据的操作信息
- EXT_WRITE_ROWS:向DB中写入数据的操作信息
- EXT_DELETE_ROWS:从DB中删除数据的操作信息
如果是TABLE_MAP_EVENT,获取DBname和tableName。
- 获取监听器
IListener listener,下面的IncrementListener就是一个IListener,根据不同的DB和表会有不同的IListener,看第4步,这个IListener是用来处理BinlogRowData的 - 构造
BinlogRowData。BinlogRowData rowData = buildRowData(event.getData());
BinlogRowData 中包含了:操作表的信息JsonTableInfo、操作类型(增加、删除、修改)、修改前表中字段都是什么值、修改后表中字段都是什么值。
表信息JsonTableInfo从TemplateHolder中获取。
JsonTableInfo jsonTableInfo = templateHolder.getTable(tableName);
4.listener.onEvent(rowData);
AggregationListener中额外实现了一个register()函数,用来注册IListener,实现不同的数据库不同的表有不同的IListener进行处理。
IncrementListener
public class IncrementListener implements IListener
public interface IListener {
//我们可以对不同的表,定义不同的更新方法,所以我们需要注册不同的监听器
void register();
void onEvent(BinlogRowData eventData);
}
//@PostConstruct是Java自带的注解,在方法上加该注解会在项目启动的时候执行该方法,也可以理解为在spring容器初始化的时候执行该方法。
@Override
@PostConstruct
public void register() {
log.info("IncrementListener register db and table info");
//k是tableName,v是DBName
Constant.table2Db.forEach((k, v) -> aggregationListener.register(v, k, this));
}
把IncrementListener注册到AggregationListener中,这个项目中所有的表都用这个一个IListener处理。
处理收到的BinlogRowData数据。
//将BinlogRowData转换为MySqlRowData,将MySqlRowData投递
@Override
public void onEvent(BinlogRowData binlogRowData)
- 将
BinlogRowData转换为MySqlRowData,本质是将BinlogRowData中的List<Map<String, String>> after变成MySqlRowData中的List<Map<String, String>> fieldValueMaps。
MySqlRowData中现在有信息:操作的表名、操作类型、修改后表中字段都是什么值。
sender.sender(mySqlRowData);,把MySqlRowData投递出去。sender是ISender。
KafkaSender
public class KafkaSender implements ISender
public void sender(MySqlRowData rowData) {
kafkaTemplate.send(topic, JSON.toJSONString(rowData));
}
把MySqlRowData序列化为JSON格式,投递到kafka的topic中。
ad-search
此处介绍ad-search中的增量索引构造,ad-search微服务是广告检索系统,增量索引的构造只是其中的一个部分。
数据路径
binlog文件 -> Event -> EventData -> BinlogRowData -> MySqlRowData -> kafka -> MySqlRowData -> AdPlanTable -> AdPlanObject
通过kafka将前半部分与后半部分解耦。
构造增量索引
从kafka中接收到MySqlRowData数据,然后sender.sender(rowData)
ISender
public class IndexSender implements ISender
public interface ISender {
void sender(MySqlRowData rowData);
}
sender会将MySqlRowData变为AdPlanTable(以这个为例),交给AdDataHandler.handle()(索引的处理),实现增量索引的添加。