监听binlog构建增量索引 | 广告系统

458 阅读5分钟

简介

一个基于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_百度百科 (baidu.com)

监听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<>();
}

解析完成后tableNamelevelopTypeFieldSetMap应该填充完毕了,opTypeFieldSetMap尤为重要。

注意此时posMap还没有填充。

查询每张表的schema信息

目的是填充JsonTemplateInfoJsonTableInfo中的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 -> BinlogRowDataEventData中字段都是以字段索引(int)的形式存在的,而我们处理表数据时,需要字段的名字(string),所以BinlogRowData中字段是以字段名的形式存在的,这时就需要字段索引字段名的映射这个posMap。

监听binlog

BinlogClient启动类

public class BinlogRunner implements CommandLineRunner

实现CommandLineRunner接口为了在微服务启动时就自动开启对binlog的监听。

BinlogClient

起一个线程运行BinaryLogClient

  1. 设置BinaryLogClient基础参数:host、port、username、password

  2. 设置filename、position(如果有需要)

  3. 注册事件监听器registerEventListener()

  4. client.connect();,连接到复制流(replication stream),此方法会阻塞,直到断开连接

AggregationListener

AggregationListener实现BinaryLogClient.EventListener接口,在BinaryLogClient连接到复制流前,把AggregationListener注册成为BinaryLogClient的事件监听器。

实现EventListener接口就是实现onEvent(Event event)方法。

  1. 获取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。

  1. 获取监听器IListener listener,下面的IncrementListener就是一个IListener ,根据不同的DB和表会有不同的IListener,看第4步,这个IListener是用来处理BinlogRowData
  2. 构造BinlogRowDataBinlogRowData rowData = buildRowData(event.getData());

BinlogRowData 中包含了:操作表的信息JsonTableInfo、操作类型(增加、删除、修改)、修改前表中字段都是什么值、修改后表中字段都是什么值。

表信息JsonTableInfoTemplateHolder中获取。

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)
  1. BinlogRowData转换为MySqlRowData,本质是将BinlogRowData中的List<Map<String, String>> after变成MySqlRowData中的List<Map<String, String>> fieldValueMaps

MySqlRowData中现在有信息:操作的表名、操作类型、修改后表中字段都是什么值。

  1. 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()(索引的处理),实现增量索引的添加。