本文主要是实现的原理,因为之前一直想不通怎么同步的,想得十分复杂。
需求
在不同的网络,部署了同一个应用,需要两边网络的数据保持一致,数据同步。
数据库:mysql
思路
问题一:要实现数据库同步,首先想到了什么时候需要进行同步?
答:数据发生改变时,数据库增删改。
问题二:那么怎么知道数据库的数据发生了变化呢?
答:通过触发器实现监听?
问题三:跨网怎么实现同步呢?
答:使用光闸发送数据。
根据问题二,我百度搜索了一下,查找到了canal。
canal
canal主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费
早期阿里巴巴因为杭州和美国双机房部署,存在跨机房同步的业务需求,实现方式主要是基于业务 trigger 获取增量变更。从 2010 年开始,业务逐步尝试数据库日志解析获取增量变更进行同步,由此衍生出了大量的数据库增量订阅和消费业务。
canal 工作原理
canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump 协议
MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal )
canal 解析 binary log 对象(原始为 byte 流)
以上内容来自canal官网https://github.com/alibaba/canal/
好的,到这里,我们的数据同步的难题解决了。
问题
但是后面又遇到一个问题,监听数据库A有数据发生变化了,那么数据就会被传输到数据库B进行入库,那么同样也会被监听到数据发生改变,那么会把数据传递给数据库A。
这么想,数据是不是一直会无线循环下去呢?好像是的。在实现的时候,一直在纠结这个问题,后面想通了,第一步数据进来没有错,第二第三步也没有错,第四步数据传到数据库A这边,怎么避免A又入库呢?其实想清楚就很简单了,在数据来之前进行判断是否有与该条数据变化前一模一样的数据吗,没有就入库,有就不用了。
好的,思路通了。canal可以获取到改变前的数据和改变后的数据。我们根据改变前后的数据生成sql语句即可。
如插入:只有改变后的数据,那么我们直接生成插入语句,需要注意,可能会出现主键冲突,mysql可以在语句insert ignore into,这样就不会报错了,如果已经存在改条数据,就不会再插入数据,那么前面的无限循环同步的问题就可以解决了。
删除:我们可以把所有改变前的数据作为where条件,那么没有被删除掉的数据,就说明数据已经被删除了。那么前面的无限循环同步的问题就可以解决了。
更新:我们可以把改变前的所有数据作为where条件,改变后的数据作为更新的内容值,没有被更新,就说明没有符合条件的数据,那么前面的无限循环同步的问题就可以解决了。
在后面又遇到一个问题,如果两边都插入数据,那么主键id就会重复。将两边的自增id改为一个使用偶数,一个使用奇数即可。
偶数修改vim /ect/my.cnf文件
auto_increment_increment=2 // 自增设置步长
auto_increment_offset=2 // 设置自增从2开始
奇数修改vim /ect/my.cnf文件
auto_increment_increment=2
auto_increment_offset=1
注意:修改配置文件后需要重新启动mysql
service mysqld restart
注意:应用背景是不存在统一时间,对同一条数据进行操作。
实现
安装canal
首先是安装配置我们的canal,两边网络下的都需要进行安装。可以参考官网:https://github.com/alibaba/canal/wiki/QuickStart
需要注意安装时配置自己对应的数据库和监听的数据库表和不监听的表
可以参考https://blog.csdn.net/qq_26502245/article/details/90445323
安装完成后,就可以开始我们的代码了。
代码实现
监听数据库变化
/***
* 监听业务表的改变
* @date 2020/7/28 22:34
*/
public void listenerWorkTable() {
CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress(PropertiesUtils.CANAL_ADDRESS,
PropertiesUtils.CANAL_PORT), PropertiesUtils.DESTINATION, "", "");
int batchSize = 1000;
try {
connector.connect();
// 监控表的过滤条件 canal\\..*
connector.subscribe(PropertiesUtils.SUBSCRIBE);
connector.rollback();
while (true) {
//尝试从master那边拉去数据batchSize条记录,有多少取多少
Message message = connector.getWithoutAck(batchSize);
long batchId = message.getId();
int size = message.getEntries().size();
if (batchId == -1 || size == 0) {
Thread.sleep(1000);
} else {
dataHandle(message.getEntries());
}
connector.ack(batchId);
}
} catch (Exception e) {
logger.error("监听业务表的改变失败:{}",e);
} finally {
connector.disconnect();
}
}
对每一类数据进行不同的处理
/**
* 数据处理
*
* @param entrys
*/
private void dataHandle(List<Entry> entrys) throws Exception {
List<RecordBean> list = new ArrayList<>();
for (Entry entry : entrys) {
if (entry.getEntryType() == EntryType.TRANSACTIONBEGIN || entry.getEntryType() == EntryType.TRANSACTIONEND) {
continue;
}
if (EntryType.ROWDATA == entry.getEntryType()) {
RowChange rowChange = RowChange.parseFrom(entry.getStoreValue());
EventType eventType = rowChange.getEventType();
if (eventType == EventType.DELETE) {
// 删除数据
saveDeleteSql(entry, list);
} else if (eventType == EventType.UPDATE) {
// 更新数据
saveUpdateSql(entry, list);
} else if (eventType == EventType.INSERT) {
// 插入数据
saveInsertSql(entry, list);
}
}
}
// 每次日志发送一次
// 生成文件发生到光闸
// .....
}
对每个操作,处理生成不同的sql
/**
* 保存更新语句
*
* @param entry
*/
private void saveUpdateSql(Entry entry, List<RecordBean> list) {
try {
RowChange rowChange = RowChange.parseFrom(entry.getStoreValue());
List<RowData> rowDatasList = rowChange.getRowDatasList();
// 记录行改变的数据
RecordBean recordBean;
// 遍历每一行
for (RowData rowData : rowDatasList) {
recordBean = new RecordBean();
// 设置操作类型
recordBean.setHandleType(HandleTypeEnum.UPDATE.getType());
StringBuffer sql = new StringBuffer("update ignore " + entry.getHeader().getTableName() + " set ");
List<String> beforeList = new ArrayList<>();
List<String> afterList = new ArrayList<>();
for (Column column : rowData.getAfterColumnsList()) {
if (column.getUpdated()) {
// 只修改改变的值
if (column.getIsNull()) {
afterList.add(column.getName() + "= null ");
} else {
afterList.add(column.getName() + "='" + column.getValue() + "'");
}
}
}
sql.append(StringUtils.collectionToDelimitedString(afterList,","));
for (Column column : rowData.getBeforeColumnsList()) {
if (column.getIsNull()) {
beforeList.add(column.getName() + " is null ");
} else {
beforeList.add(column.getName() + "='" + column.getValue() + "'");
}
}
sql.append(" where ");
sql.append(StringUtils.collectionToDelimitedString(beforeList," and "));
recordBean.setSqlStr(sql.toString().replace(SysConstant.SQL_RESOURCE, SysConstant.SQL_NEW));
recordBean.setSender(PropertiesUtils.SENDER);
list.add(recordBean);
}
} catch (Exception e) {
logger.error("保存更新语句失败:{}",e);
}
}
/**
* 保存删除语句
*
* @param entry
*/
private void saveDeleteSql(Entry entry, List<RecordBean> list) {
try {
RowChange rowChange = RowChange.parseFrom(entry.getStoreValue());
List<RowData> rowDatasList = rowChange.getRowDatasList();
RecordBean recordBean;
for (RowData rowData : rowDatasList) {
recordBean = new RecordBean();
// 删除前的数据
List<Column> columnList = rowData.getBeforeColumnsList();
StringBuffer sql = new StringBuffer("delete from " + entry.getHeader().getTableName() + " where ");
List<String> fieldStr = new ArrayList<>();
for (Column column : columnList) {
if (column.getIsNull()) {
fieldStr.add(column.getName() + " is null ");
} else {
fieldStr.add(column.getName() + "='" + column.getValue() + "'");
}
}
sql.append(StringUtils.collectionToDelimitedString(fieldStr," and "));
recordBean.setHandleType(HandleTypeEnum.DELETE.getType());
recordBean.setSqlStr(sql.toString().replace(SysConstant.SQL_RESOURCE, SysConstant.SQL_NEW));
recordBean.setSender(PropertiesUtils.SENDER);
list.add(recordBean);
}
} catch (Exception e) {
logger.error("保存删除语句失败:{}",e);
}
}
/**
* 保存插入语句
*
* @param entry
*/
private void saveInsertSql(Entry entry, List<RecordBean> list) {
try {
RowChange rowChange = RowChange.parseFrom(entry.getStoreValue());
List<RowData> rowDatasList = rowChange.getRowDatasList();
// 保存每个行的内容
RecordBean recordBean;
// 遍历每一行数据
for (RowData rowData : rowDatasList) {
recordBean = new RecordBean();
StringBuffer sql = new StringBuffer("insert ignore into " + entry.getHeader().getTableName() + " (");
List<String> fieldList = new ArrayList<>();
List<String> valueList = new ArrayList<>();
for (Column column : rowData.getAfterColumnsList()) {
fieldList.add(column.getName());
if (column.getIsNull()) {
valueList.add(null);
} else {
valueList.add("'" + column.getValue() + "'");
}
sql.append(StringUtils.collectionToCommaDelimitedString(fieldList));
sql.append(") values(");
sql.append(StringUtils.collectionToCommaDelimitedString(valueList));
sql.append(")");
recordBean.setHandleType(HandleTypeEnum.INSERT.getType());
recordBean.setSqlStr(sql.toString().replace(SysConstant.SQL_RESOURCE, SysConstant.SQL_NEW));
recordBean.setSender(PropertiesUtils.SENDER);
list.add(recordBean);
}
} catch (Exception e) {
logger.error("保存插入语句失败:{}",e);
}
}
接收端值需要接收文件,解析文件的内容获取sql执行即可。