此博客用于个人学习,来源于网上,对知识点进行一个整理。
1. 增量数据投递前的准备工作:
MySQL 增量数据的投递核心目的是构建检索服务中的增量索引,也就是说 IncrementListener 中使用到的 Isender 的方法去投递增量数据。首先我们观察到 AdPlanTable 中有两个特殊的索引——startDate 和 endDate。Long 和 Integer 这种类型的数据对于 MySQL 的表达不会有问题。但对于 Date 这种类型,Binlog 序列化成字符串,所以说我们需要去搞清楚 Date 这种数据类型的表达是什么样的。同样的,我们最好的方式就是看一下 Binlog 怎么样以字符串的形式表达日期。
1.1 设计转化格式:
启动 Binlog 的监听开关,然后去数据库中写入一些内容,比如插入一条数据,然后查看控制台上的打印信息,特别观察其中的日期信息。
Tue Jan 01 08:00:00 CST 2019
根据上面的打印信息设置解析方式。
1.2 实现转化:
在 CommonUtils 类中定义一个方法,实现对数据的解析,将其解析为一个 Date 类型。
@Slf4j
public class CommonUtils {
/**
* 传进来一个 map,如果这个 map 的 key 不存在,返回一个新的 value 对象
* @param key
* @param map
* @param factory
* @param <K>
* @param <V>
* @return
*/
public static <K,V> V getOrCreate(K key, Map<K,V> map, Supplier<V> factory){
return map.computeIfAbsent(key,k -> factory.get());
}
/**
* 拼接字符串
* @param args
* @return
*/
public static String stringConcat(String... args){
StringBuffer result = new StringBuffer();
for (String arg : args) {
result.append(arg);
result.append("-");
}
result.deleteCharAt(result.length() - 1);
return result.toString();
}
/**
* 将 String 类型转化为 Date 类型
* @param dateString
* @return
*/
public static Date parseStringDate(String dateString){
try{
DateFormat dateFormat = new SimpleDateFormat(
"EEE MMM dd HH:mm:ss zzz yyyy",
Locale.US
);
return DateUtils.addHours(
dateFormat.parse(dateString),
-8
);
}catch (ParseException ex){
log.error("parseStringDate error:{}",dateString);
return null;
}
}
}
1.3 定义数据层级:
因为我们构建索引的时候是根据表之间的层级关系去构建的,但是目前我们还没有一个常量定义去定义这样的一个常量关系。因为我们去定义索引的时候,可能要去处理 level2,level3,level4,在代码中写上234也不太好维护,所以我们去定义一个 DataLevel 的一个数据层级,因为数据层级是索引内部的含义,于是我们将其定义在 index 包下。定义一个枚举类 DateLevel。
@Getter
public enum DateLevel {
LEVEL2("2","level 2"),
LEVEL3("3","level 3"),
LEVEL4("4","level 4");
private String level;
private String desc;
DateLevel(String level,String desc){
this.level = level;
this.desc = desc;
}
}
2. 增量数据的投递:
增量数据的投递其实就是去实现 ISender 这个接口,ISender 接口去接收一个参数,是 MySqlRowData 的类型。构建索引的过程是使用我们之前完成的 AdLevelDataHandler,里面分为好几个层级,对于每一个层级都是将表转化为 table 对象,最终我们在增量数据投递的过程中,去实现增量索引的加载。
2.1 第二层级增量数据的投递:
@Slf4j
@Component("indexSender")
public class IndexSender implements ISender {
@Override
public void sender(MySqlRowData rowData) {
String level = rowData.getLevel();
if (DateLevel.LEVEL2.getLevel().equals(level)){
//第二层级增量数据的投递
Level2RowData(rowData);
}else if (DateLevel.LEVEL3.getLevel().equals(level)){
//第三层级增量数据的投递
}else if (DateLevel.LEVEL4.getLevel().equals(level)){
//第四层级增量数据的投递
}else {
log.error("MysqlRowData ERROR:{}", JSON.toJSONString(rowData));
}
}
private void Level2RowData(MySqlRowData rowData){
if (rowData.getTableName().equals(Constant.AD_PLAN_TABLE_INFO.TABLE_NAME)){
List<AdPlanTable> planTables = new ArrayList<>();
for (Map<String,String> fieldValueMap : rowData.getFieldValueMap()){
AdPlanTable planTable = new AdPlanTable();
fieldValueMap.forEach((k,v) -> {
switch (k){
case Constant.AD_PLAN_TABLE_INFO.COLUMN_ID:
planTable.setPlanId(Long.valueOf(v));
break;
case Constant.AD_PLAN_TABLE_INFO.COLUMN_USER_ID:
planTable.setUserId(Long.valueOf(v));
break;
case Constant.AD_PLAN_TABLE_INFO.COLUMN_PLAN_STATUS:
planTable.setPlanStatus(Integer.valueOf(v));
break;
case Constant.AD_PLAN_TABLE_INFO.COLUMN_START_DATE:
planTable.setStartDate(CommonUtils.parseStringDate(v));
break;
case Constant.AD_PLAN_TABLE_INFO.COLUMN_END_DATE:
planTable.setEndDate(CommonUtils.parseStringDate(v));
break;
}
});
planTables.add(planTable);
}
planTables.forEach(p -> AdLevelDataHandler.handleLevel2(p,rowData.getOpType()));
}else if (rowData.getTableName().equals(Constant.AD_CREATIVE_TABLE_INFO.TABLE_NAME)){
List<CreativeTable> creativeTables = new ArrayList<>();
for (Map<String,String> filedValueMap : rowData.getFieldValueMap()){
CreativeTable creativeTable = new CreativeTable();
filedValueMap.forEach((k,v) -> {
switch (k){
case Constant.AD_CREATIVE_TABLE_INFO.COLUMN_ID:
creativeTable.setAdId(Long.valueOf(v));
break;
case Constant.AD_CREATIVE_TABLE_INFO.COLUMN_TYPE:
creativeTable.setType(Integer.valueOf(v));
break;
case Constant.AD_CREATIVE_TABLE_INFO.COLUMN_MATERIAL_TYPE:
creativeTable.setMaterialType(Integer.valueOf(v));
break;
case Constant.AD_CREATIVE_TABLE_INFO.COLUMN_HEIGHT:
creativeTable.setHeight(Integer.valueOf(v));
break;
case Constant.AD_CREATIVE_TABLE_INFO.COLUMN_WIDTH:
creativeTable.setWidth(Integer.valueOf(v));
break;
case Constant.AD_CREATIVE_TABLE_INFO.COLUMN_AUDIT_STATUS:
creativeTable.setAuditStatus(Integer.valueOf(v));
break;
case Constant.AD_CREATIVE_TABLE_INFO.COLUMN_URL:
creativeTable.setAdUrl(v);
break;
}
});
creativeTables.add(creativeTable);
}
creativeTables.forEach(c -> AdLevelDataHandler.handleLevel2(c,rowData.getOpType()));
}
}
}
2.2 第三层级增量数据的投递:
第三层级与第二层级有依赖关系,需要增加外键的维护。
private void Level3RowData(MySqlRowData rowData){
if (rowData.getTableName().equals(Constant.AD_UNIT_TABLE_INFO.TABLE_NAME)) {
List<AdUnitTable> unitTables = new ArrayList<>();
for (Map<String, String> fieldValueMap : rowData.getFieldValueMap()) {
AdUnitTable unitTable = new AdUnitTable();
fieldValueMap.forEach((k, v) -> {
switch (k) {
case Constant.AD_UNIT_TABLE_INFO.COLUMN_ID:
unitTable.setUnitId(Long.valueOf(v));
break;
case Constant.AD_UNIT_TABLE_INFO.COLUMN_UNIT_STATUS:
unitTable.setUnitStatus(Integer.valueOf(v));
break;
case Constant.AD_UNIT_TABLE_INFO.COLUMN_POSITION_TYPE:
unitTable.setPositionType(Integer.valueOf(v));
break;
case Constant.AD_UNIT_TABLE_INFO.COLUMN_PLAN_ID:
unitTable.setPlanId(Long.valueOf(v));
break;
}
});
unitTables.add(unitTable);
}
unitTables.forEach(u -> AdLevelDataHandler.handleLevel3(u, rowData.getOpType()));
} else if (rowData.getTableName().equals(Constant.AD_CREATIVE_UNIT_TABLE_INFO.TABLE_NAME)) {
List<CreativeUnitTable> creativeUnitTables = new ArrayList<>();
for (Map<String, String> fieldValueMap : rowData.getFieldValueMap()) {
CreativeUnitTable creativeUnitTable = new CreativeUnitTable();
fieldValueMap.forEach((k, v) -> {
switch (k) {
case Constant.AD_CREATIVE_UNIT_TABLE_INFO.COLUMN_CREATIVE_ID:
creativeUnitTable.setAdId(Long.valueOf(v));
break;
case Constant.AD_CREATIVE_UNIT_TABLE_INFO.COLUMN_UNIT_ID:
creativeUnitTable.setUnitId(Long.valueOf(v));
break;
}
});
creativeUnitTables.add(creativeUnitTable);
}
creativeUnitTables.forEach(u -> AdLevelDataHandler.handleLevel3(u, rowData.getOpType()));
}
}
2.3 第四层级增量数据的投递:
第四层级有三张表,可以采用 switch 的形式。
private void Level4RowData(MySqlRowData rowData) {
switch (rowData.getTableName()) {
case Constant.AD_UNIT_DISTRICT_TABLE_INFO.TABLE_NAME:
List<UnitDistrictTable> districtTables = new ArrayList<>();
for (Map<String, String> fieldValueMap : rowData.getFieldValueMap()) {
UnitDistrictTable districtTable = new UnitDistrictTable();
fieldValueMap.forEach((k, v) -> {
switch (k) {
case Constant.AD_UNIT_DISTRICT_TABLE_INFO.COLUMN_UNIT_ID:
districtTable.setUnitId(Long.valueOf(v));
break;
case Constant.AD_UNIT_DISTRICT_TABLE_INFO.COLUMN_PROVINCE:
districtTable.setProvince(v);
break;
case Constant.AD_UNIT_DISTRICT_TABLE_INFO.COLUMN_CITY:
districtTable.setCity(v);
break;
}
});
districtTables.add(districtTable);
}
districtTables.forEach(d -> AdLevelDataHandler.handleLevel4(d, rowData.getOpType()));
break;
case Constant.AD_UNIT_IT_TABLE_INFO.TABLE_NAME:
List<UnitItTable> itTables = new ArrayList<>();
for (Map<String, String> fieldValueMap : rowData.getFieldValueMap()) {
UnitItTable itTable = new UnitItTable();
fieldValueMap.forEach((k, v) -> {
switch (k) {
case Constant.AD_UNIT_IT_TABLE_INFO.COLUMN_UNIT_ID:
itTable.setUnitId(Long.valueOf(v));
break;
case Constant.AD_UNIT_IT_TABLE_INFO.COLUMN_IT_TAG:
itTable.setItTag(v);
break;
}
});
itTables.add(itTable);
}
itTables.forEach(i -> AdLevelDataHandler.handleLevel4(i, rowData.getOpType()));
break;
case Constant.AD_UNIT_KEYWORD_TABLE_INFO.TABLE_NAME:
List<UnitKeywordTable> keywordTables = new ArrayList<>();
for (Map<String, String> fieldValueMap : rowData.getFieldValueMap()) {
UnitKeywordTable keywordTable = new UnitKeywordTable();
fieldValueMap.forEach((k, v) -> {
switch (k) {
case Constant.AD_UNIT_KEYWORD_TABLE_INFO.COLUMN_UNIT_ID:
keywordTable.setUnitId(Long.valueOf(v));
break;
case Constant.AD_UNIT_KEYWORD_TABLE_INFO.COLUMN_KEYWORD:
keywordTable.setKeyword(v);
break;
}
});
keywordTables.add(keywordTable);
}
keywordTables.forEach(k -> AdLevelDataHandler.handleLevel4(k, rowData.getOpType()));
break;
}
}
3. 将增量数据投递到 Kafka:
对于 MySQL Binlog 可以有多种方式实现分发,对于我们的检索系统,更重要的是构建增量索引,比如说给我们的广告分析服务。接下来去实现一种更加常用的方式,将增量数据投递到 Kafka 里面,可以去监听 Kafka 的一个 topic,然后去获取对应的增量数据。其他服务可以去监听这个类,去获取到相应的服务。
@Component("kafkaSender")
public class KafkaSender implements ISender {
@Value("${adconf.kafka.topic}")
private String topic;
private final KafkaTemplate<String,String> kafkaTemplate;
public KafkaSender(KafkaTemplate<String, String> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}
@Override
public void sender(MySqlRowData rowData) {
kafkaTemplate.send(topic, JSON.toJSONString(rowData));
}
@KafkaListener(topics = {"ad-search-mysql-data"}, groupId = "ad-search")
public void processMysqlRowData(ConsumerRecord<?,?> record){
Optional<?> kafkaMessage = Optional.ofNullable(record.value());
if (kafkaMessage.isPresent()) {
Object message = kafkaMessage.get();
MySqlRowData rowData = JSON.parseObject(
message.toString(),
MySqlRowData.class
);
System.out.println("kafka processMysqlRowData: " + JSON.toJSONString(rowData));
}
}
}