具体的中间件私有化背景在上文 SaaS` 电商设计 (二) 私有化部署-缓存中间件适配 已有做相关介绍.这里具体讨论的场景是通过解析mysql binlog 来实现mysql到其他数据源的同步.具体比如:在电商的解决方案业务流中经常有 ES 的使用场景,用以解决一些复杂的查询和搜索商品的支持以及某些数据分析的场景.那就需要做到 mysql 数据库到 ES 的数据同步.在支持 mysql 到 ES 数据同步的过程中,常用的技术方案有这样几种.
方案1: 业务代码成功应答后操作目标数据源写入(本文用ES举例)
如上第一种方案在业务代码操作数据库, 异步执行 ES 数据同步写入.如:完成商品后写入数据,异步线程开启执行写入 ES 索引录入.
方案2:业务代码成功应答后,发送MQ,利用MQ来保证 ES 写入的最终一致
在第一种的方案中写入 ES 步骤中可能出现ES 写入失败case. 在方案一基础上为了保证可靠性引入 MQ ,保证在ES操作时出现异常抖动能够通过重试来保证数据的最终一致性.在业务代码中实际操作数据库后发送 MQ ,这边消费 MQ 执行 ES 数据同步.如:完成商品写入数据,发送消息 MQ , MQ consumer 消费写入 ES 索引录入.
方案 3.通过binlog 来实现数据库监听,保证数据同步脱离业务代码控制
在大部分的场景下方案二完全能够满足业务诉求. 这样的一个方案在具体实施过程中存在两个点.
业务开发的同时需要同步关心数据的同步
在某种意义上来说,数据的同步并不是业务代码需要去关心的.业务代码永远关心的只是自身的逻辑实现,关注的是产品迭代过程中如何保证业务模型的可持续演进和领域资产沉淀.基于这个原则我的理解是需要把数据的同步从业务代码里进行剥离的.
方案4:完美终极方案(抽离技术细节的实现,做到binlog解析的接口和数据同步的接口化.)
对于第三种方式来说的话,接下来引入了第二个讨论的点.
step1:抽象MessageListener 实现 BinlogListener 完成 binlog 中间件解析发送的 MQ msg 得到反序列化的表数据.内含本次选取的反序列化类型.如:是canal 还是 DRC .
step2:抽象 BinlogClientAdapter 完成反序列化和处理msg接口定义.具体可以有 CanalBinlogAdapter,DrcBinlogClientAdapter实现.
step3:抽象BinlogDataHandler 完成具体表具体操作**(insert,delete,update,query)** 接口定义.具体在接入方进行实现MultiCloundBinLogDataHandler,这样在进行注入时得到具体的实现类,进行具体的实现操作.如:CategoryBinlogDataHandler.
BinlogHandlerAdapter 完成 binlog client 接口定义.
package com.baixiu.middleware.binlog.adapter;
import com.baixiu.middleware.binlog.model.BinlogData;
import com.baixiu.middleware.mq.model.CommonMessage;
/**
* binlog 适配器接口
* 适配中间件list:canal,jingwei,drc等。
* function1:完成不同中间件解析能力
* function2:完成不同中间件handlerMsg能力
* @author baixiu
* @date 2023年12月11日
*/
public interface BinlogHandlerAdapter {
/**
* 反序列MQMsg To binlogMsg
* @param mqMsg mqMsg
* @return
*/
BinlogData deserializationMQMsg(CommonMessage mqMsg);
/**
* 反序列MQMsg To binlogMsg
* @param mqMsg mqMsg
* @return
*/
void handleBinLogData(BinlogData binLogData) throws Exception;
}
CanalBinlogHandlerAdapter 完成 canal 解析
package com.baixiu.middleware.binlog.adapter;
import com.alibaba.fastjson.JSON;
import com.alibaba.otter.canal.protocol.FlatMessage;
import com.baixiu.middleware.binlog.consts.CommonConsts;
import com.baixiu.middleware.binlog.core.AbstractBinlogHandler;
import com.baixiu.middleware.binlog.core.BinlogTableHandlerRouter;
import com.baixiu.middleware.binlog.enums.CommonRowTypeEnum;
import com.baixiu.middleware.binlog.model.BinlogData;
import com.baixiu.middleware.binlog.model.BinlogDataToDiffModel;
import com.baixiu.middleware.binlog.model.BinlogTableRowDiffModel;
import com.baixiu.middleware.mq.model.CommonMessage;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* canal binlog handler adapter
* 当property配置的clientType=canal时进行注入bean
* canal client 用以解析 mq -starter 发送过来的消费消息
* @author baixiu
* @date 创建时间 2023/12/11 8:39 PM
*/
@Slf4j
public class CanalBinlogHandlerAdapter implements BinlogHandlerAdapter{
@Autowired
private BinlogTableHandlerRouter binlogTableHandlerRouter;
@Override
public BinlogData deserializationMQMsg(CommonMessage mqMsg) {
FlatMessage flatMessage = JSON.parseObject(mqMsg.getText(),FlatMessage.class);
BinlogData binLogData=new BinlogData ();
if(flatMessage!=null){
binLogData.setBinlogDataObject(flatMessage);
}
return binLogData;
}
@Override
public void handleBinLogData(BinlogData binLogData) throws Exception {
if(binLogData==null || binLogData.getBinlogDataObject()==null){
return;
}
FlatMessage flatMessage= (FlatMessage) binLogData.getBinlogDataObject ();
List
AbstractBinlogHandler 抽象binloghandler 处理类.
package com.baixiu.middleware.binlog.core;
import com.baixiu.middleware.binlog.model.BinlogTableRowDiffModel;
import java.util.List;
import java.util.Map;
/**
* @author baixiu
* @date 创建时间 2023/12/12 11:31 AM
*/
public interface AbstractBinlogHandler {
/**
* 需要关心的字段。实现后将仅实现的字段值放置于 fieldValues 中
* @return 监控字段
*/
String[] getFields();
/**
* 需要关心的变更字段。实现后将仅实现的字段值放置于 changeList 中
* @return 更新字段
*/
String[] getUpdateFields();
/**
* 新增时触发
* @param fieldValues 唯一字段,用于确定一条数据
* @param changeList 字段的值发生变化的
* @throws Exception 业务exception
*/
void insert(Map fieldValues, List changeList) throws Exception;
/**
* 数据修改时触发
* @param fieldValues 实现了getFields接口里得到的字段里的字段以及字段的值
* @param changeList 字段的值发生变化的
* @throws Exception 业务exception
*/
void update(Map fieldValues, List changeList) throws Exception;
/**
* 删除时触发
* @param fieldValues 唯一字段,用于确定一条数据
* @throws Exception 业务exception
*/
void delete(Map fieldValues) throws Exception;
}