一、前言
关于MySql的数据同步,一般的方法是读取mysql的日志binlog文件,来获取数据的变更,进行同步的。
现在开源的项目中有监听mysql binlog的工具有很多:
在本篇中,小编就向大家介绍一下,mysql-binlog-connector-java 的使用。
二、说说业务
最近的项目中,在对用户系统进行重构,用户系统在重构完成后,发现还是存在一些不稳定的地方,为了减轻用户系统的压力,把一些查询频繁的接口提取到了公共服务中,对用户系统的一些热点数据,做了MEMSQL和redis的缓存。
然后就使用到了数据同步,时刻监听用户系统的RDS,来同步公共服务的redis和MemSql。
三、mysql-binlog-connector-java 介绍
mysql-binlog-connector-java是github上的一个开源项目。是二进制日志监听器。
作为mysql的日志同步,有下面的优点:
- 支持解析binlog文件、解析GTID(全局事务id)
- 支持重连
- 支持设置故障转移策略
- TLS协议安全使用
- JMX-friendly
- 实时监控状态
- 无第三方依赖
三、使用
引入依赖
<dependency>
<groupId>com.github.shyiko</groupId>
<artifactId>mysql-binlog-connector-java</artifactId>
<version>0.13.0</version>
</dependency>
Code
封装数据库连接实体DataConfig
package com.xx.common.component;
public class DataConfig {
private String javaHost;
private int javaPort;
private String javaUserName;
private String javaPassword;
private Long javaPosition;
private String javaFileName;
public String getJavaHost() {
return javaHost;
}
public void setJavaHost(String javaHost) {
this.javaHost = javaHost;
}
public int getJavaPort() {
return javaPort;
}
public void setJavaPort(int javaPort) {
this.javaPort = javaPort;
}
public String getJavaUserName() {
return javaUserName;
}
public void setJavaUserName(String javaUserName) {
this.javaUserName = javaUserName;
}
public String getJavaPassword() {
return javaPassword;
}
public void setJavaPassword(String javaPassword) {
this.javaPassword = javaPassword;
}
public Long getJavaPosition() {
return javaPosition;
}
public void setJavaPosition(Long javaPosition) {
this.javaPosition = javaPosition;
}
public String getJavaFileName() {
return javaFileName;
}
public void setJavaFileName(String javaFileName) {
this.javaFileName = javaFileName;
}
@Override
public String toString() {
return "DataConfig{" +
"javaHost='" + javaHost + '\'' +
", javaPort=" + javaPort +
", javaUserName='" + javaUserName + '\'' +
", javaPassword='" + javaPassword + '\'' +
", javaPosition=" + javaPosition +
", javaFileName='" + javaFileName + '\'' +
'}';
}
}
获取导入前binlog增量位置
package com.xxx.common.service;
import com.github.shyiko.mysql.binlog.network.AuthenticationException;
import com.github.shyiko.mysql.binlog.network.ServerException;
import com.github.shyiko.mysql.binlog.network.SocketFactory;
import com.github.shyiko.mysql.binlog.network.protocol.ErrorPacket;
import com.github.shyiko.mysql.binlog.network.protocol.GreetingPacket;
import com.github.shyiko.mysql.binlog.network.protocol.PacketChannel;
import com.github.shyiko.mysql.binlog.network.protocol.ResultSetRowPacket;
import com.github.shyiko.mysql.binlog.network.protocol.command.AuthenticateCommand;
import com.github.shyiko.mysql.binlog.network.protocol.command.QueryCommand;
import com.xxx.common.logger.DushuLogger;
import com.xxxx.common.component.DataConfig;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Service
public class BinLogService {
@Resource(name="dataConfig")
public DataConfig dataConfig;
public void getBinLogPosition() throws Exception{
//链接数据库服务器
SocketFactory socketFactory = null;
Socket socket = socketFactory != null ? socketFactory.createSocket() : new Socket();
DushuLogger.info("==========dataConfig=======",dataConfig.toString());
socket.connect(new InetSocketAddress((dataConfig.getJavaHost()), dataConfig.getJavaPort()), (int) TimeUnit.SECONDS.toMillis(3));
PacketChannel channel = new PacketChannel(socket);
byte[] initialHandshakePacket = channel.read();
if (initialHandshakePacket[0] == (byte) 0xFF /* error */) {
byte[] bytes = Arrays.copyOfRange(initialHandshakePacket, 1, initialHandshakePacket.length);
ErrorPacket errorPacket = new ErrorPacket(bytes);
throw new ServerException(errorPacket.getErrorMessage(), errorPacket.getErrorCode(),
errorPacket.getSqlState());
}
//认证
GreetingPacket greetingPacket = new GreetingPacket(initialHandshakePacket);
int collation = greetingPacket.getServerCollation();
int packetNumber = 1;
AuthenticateCommand authenticateCommand = new AuthenticateCommand(null, dataConfig.getJavaUserName(), dataConfig.getJavaPassword(),
greetingPacket.getScramble());
authenticateCommand.setCollation(collation);
channel.write(authenticateCommand, packetNumber);
byte[] authenticationResult = channel.read();
if (authenticationResult[0] != (byte) 0x00 /* ok */) {
if (authenticationResult[0] == (byte) 0xFF /* error */) {
byte[] bytes = Arrays.copyOfRange(authenticationResult, 1, authenticationResult.length);
ErrorPacket errorPacket = new ErrorPacket(bytes);
throw new AuthenticationException(errorPacket.getErrorMessage(), errorPacket.getErrorCode(),
errorPacket.getSqlState());
}
throw new AuthenticationException("Unexpected authentication result (" + authenticationResult[0] + ")");
}
//执行mysql命令show master status
ResultSetRowPacket[] result;
channel.write(new QueryCommand("show master status"));
List<ResultSetRowPacket> resultSet = new LinkedList<ResultSetRowPacket>();
byte[] statementResult = channel.read();
if (statementResult[0] == (byte) 0xFF /* error */) {
byte[] bytes = Arrays.copyOfRange(statementResult, 1, statementResult.length);
ErrorPacket errorPacket = new ErrorPacket(bytes);
throw new ServerException(errorPacket.getErrorMessage(), errorPacket.getErrorCode(),
errorPacket.getSqlState());
}
while ((channel.read())[0] != (byte) 0xFE /* eof */) { /* skip */ }
for (byte[] bytes; (bytes = channel.read())[0] != (byte) 0xFE /* eof */; ) {
resultSet.add(new ResultSetRowPacket(bytes));
}
result = resultSet.toArray(new ResultSetRowPacket[resultSet.size()]);
if (result.length == 0) {
throw new IOException("Failed to determine binlog filename/position");
}
ResultSetRowPacket resultSetRow = result[0];
String binlogFilename = resultSetRow.getValue(0);
long binlogPosition = Long.parseLong(resultSetRow.getValue(1));
DushuLogger.info("当前binlog文件名:"+binlogFilename+",位置:"+binlogPosition);
dataConfig.setJavaFileName(binlogFilename);
dataConfig.setJavaPosition(binlogPosition);
if (socket != null && !socket.isClosed()) {
socket.close();
}
}
}
启动监听
这里主要通过监听binlog的各种事件,来做先关的处理
package com.xxx.common.service;
import com.github.shyiko.mysql.binlog.BinaryLogClient;
import com.github.shyiko.mysql.binlog.event.*;
import com.xxxxx.common.component.DataConfig;
import com.xxxx.common.logger.DushuLogger;
import com.xxxxx.common.util.DataMapManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.IOException;
@Service
public class IncrementSyncUserService {
@Resource(name="dataConfig")
public DataConfig dataConfig;
@Autowired
private JavaUserService userService;
@Autowired
private JavaUserInfoService userInfoService;
@Autowired
private JavaUserWealthService userWealthService;
@Autowired
private JavaUserTokenService userTokenService;
public void increaseSyncUserData() {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
DushuLogger.info("增量数据监听启动...");
DataMapManager manager = DataMapManager.getInstance();
//添加对表的监控
manager.register("t_user", userService);
manager.register("t_user_info", userInfoService);
manager.register("t_user_wealth", userWealthService);
manager.register("t_user_token", userTokenService);
BinaryLogClient client = new BinaryLogClient(dataConfig.getJavaHost(), dataConfig.getJavaPort(), dataConfig.getJavaUserName(), dataConfig.getJavaPassword());
if (dataConfig.getJavaPosition() != null && dataConfig.getJavaFileName() != null) {
//增量需要指定binlog信息
client.setBinlogFilename(dataConfig.getJavaFileName());
client.setBinlogPosition(dataConfig.getJavaPosition());
}
DushuLogger.info("将从"+client.getBinlogFilename()+"文件的"+client.getBinlogPosition()+"位置开始注册事件监听");
client.registerEventListener(new BinaryLogClient.EventListener() {
@Override
public void onEvent(Event event) {
EventData data = event.getData();
if(data instanceof WriteRowsEventData){
WriteRowsEventData writerData = (WriteRowsEventData)data;
try {
manager.executeInsert(writerData);
} catch (Exception e) {
DushuLogger.error(e,e);
}
}
if(data instanceof UpdateRowsEventData){
UpdateRowsEventData updateRowsEventData = (UpdateRowsEventData)data;
try {
manager.executeUpdate(updateRowsEventData);
} catch (Exception e) {
DushuLogger.error(e,e);
}
}
//用户信息没有删除逻辑,delete不用实现
if(data instanceof DeleteRowsEventData){
DeleteRowsEventData deleteRowsEventData = (DeleteRowsEventData) data;
try {
manager.executeDelete(deleteRowsEventData);
} catch (Exception e) {
DushuLogger.error(e,e);
}
}
if(data instanceof TableMapEventData){
TableMapEventData tableMapEventData = (TableMapEventData) data;
manager.updateTableMap(tableMapEventData);
}
}
});
try {
client.connect();
} catch (IOException e) {
DushuLogger.error(e,e);
}
}
});
t.start();
}
}
DataMapManager
package com.xxxx.common.util;
import com.github.shyiko.mysql.binlog.event.DeleteRowsEventData;
import com.github.shyiko.mysql.binlog.event.TableMapEventData;
import com.github.shyiko.mysql.binlog.event.UpdateRowsEventData;
import com.github.shyiko.mysql.binlog.event.WriteRowsEventData;
import java.util.HashMap;
import java.util.Map;
public class DataMapManager {
private static final DataMapManager manager = new DataMapManager();
public static DataMapManager getInstance() {
return new DataMapManager();
}
private Map<String, DataMapCallback<?>> registerInfo = new HashMap<>();
private Map<Long, String> tableMap = new HashMap<>();
public void register(String tableName, String[] tableRows, DataMapCallback<?> callback) {
if (registerInfo.containsKey(tableName)) {
throw new RuntimeException("该表已经注册过了");
}
registerInfo.put(tableName, callback);
}
public void register(String tableName, DataMapCallback<?> callback) {
if (registerInfo.containsKey(tableName)) {
throw new RuntimeException("该表已经注册过了");
}
registerInfo.put(tableName, callback);
}
public void executeInsert(WriteRowsEventData data) {
String tableName = tableMap.get(data.getTableId());
if (tableName != null) {
DataMapCallback<?> callback = registerInfo.get(tableName);
if (callback != null) {
callback.doInsertCallback(data.getRows());
}
}
}
public void executeDelete(DeleteRowsEventData data) {
String tableName = tableMap.get(data.getTableId());
if (tableName != null) {
DataMapCallback<?> callback = registerInfo.get(tableName);
if (callback != null) {
callback.doDeleteCallback(data.getRows());
}
}
}
public void executeUpdate(UpdateRowsEventData data) {
String tableName = tableMap.get(data.getTableId());
if (tableName != null) {
DataMapCallback<?> callback = registerInfo.get(tableName);
if (callback != null) {
callback.doUpdateCallback(data.getRows());
}
}
}
public void updateTableMap(TableMapEventData data) {
if (!tableMap.containsKey(data.getTableId())) {
tableMap.put(data.getTableId(), data.getTable());
}
}
}
DataMapCallback
package com.xxxxx.common.util;
import java.io.Serializable;
import java.lang.reflect.ParameterizedType;
import java.util.List;
import java.util.Map;
public abstract class DataMapCallback<T> {
private Class<T> rawType;
private String[] rows;
public abstract void executeInsert(T data);
public abstract void executeUpdate(UpdateBean<T> data);
public abstract void executeDelete(T data);
public String[] getRowNames() {
if (rows == null) {
rows = DataMapUtil.getRowNameList(getRawType());
}
return rows;
}
public void doInsertCallback(List<Serializable[]> rows) {
List<T> list = (List<T>) DataMapUtil.loadInsertData(rows, getRawType(), getRowNames());
for (T t : list) {
executeInsert(t);
}
}
public void doDeleteCallback(List<Serializable[]> rows) {
List<T> list = (List<T>) DataMapUtil.loadInsertData(rows, getRawType(), getRowNames());
for (T t : list) {
executeDelete(t);
}
}
public void doUpdateCallback(List<Map.Entry<Serializable[], Serializable[]>> rows) {
List<UpdateBean<T>> list = DataMapUtil.loadUpdateData(rows, getRawType(), getRowNames());
for (UpdateBean<T> t : list) {
executeUpdate(t);
}
}
public Class<T> getRawType() {
if (rawType == null) {
rawType = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
}
return rawType;
}
}
DataMapUtil
package com.xxxx.common.util;
import com.xxxxx.common.logger.DushuLogger;
import com.xxxxx.common.utils.basic.JsonUtil;
import org.springframework.util.ObjectUtils;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.text.SimpleDateFormat;
import java.util.*;
public class DataMapUtil {
public static <T> List<T> loadInsertData(List<Serializable[]> rows, Class<T> clazz, String[] rowNames) {
List<T> result = new ArrayList<>();
for (Serializable[] data : rows) {
T cee = getMapData(data, clazz, rowNames);
result.add(cee);
}
return result;
}
public static <T> T getMapData(Serializable[] data, Class<T> clazz, String[] rowNames) {
StringBuilder sb = new StringBuilder("{");
int index = 0;
for (int i = 0; i < rowNames.length; i++) {
String name = rowNames[i];
Object d = data[i];
if (d != null) {
if (index++ == 0) {
sb.append("\"" + name + "\":");
} else {
sb.append(",\"" + name + "\":");
}
if (BitSet.class.isAssignableFrom(d.getClass())) {
BitSet bitSet = (BitSet) d;
sb.append("\"" + bitSet.get(0) + "\"");
} else if (Date.class.isAssignableFrom(d.getClass())) {
Date date = (Date) d;
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
simpleDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
String formatDate = simpleDateFormat.format(date);
sb.append("\"" + formatDate + "\"");
} else {
sb.append("\"" + d + "\"");
}
}
}
sb.append("}");
T cee = JsonUtil.fromJSON(sb.toString(), clazz);
return cee;
}
public static <T> T diffUpdateData(Map.Entry<Serializable[], Serializable[]> entry, Class<T> clazz, String[] rowNames) throws InstantiationException, IllegalAccessException {
StringBuilder sb = new StringBuilder("{");
int index = 0;
Serializable[] before = entry.getKey();
Serializable[] after = entry.getValue();
for (int i = 0; i < rowNames.length; i++) {
String name = rowNames[i];
Object beforeData = before[i];
Object afterData = after[i];
if (!ObjectUtils.nullSafeEquals(beforeData, afterData)) {
if (afterData != null) {
if (index++ == 0) {
sb.append("\"" + name + "\":");
} else {
sb.append(",\"" + name + "\":");
}
if (BitSet.class.isAssignableFrom(afterData.getClass())) {
BitSet bitSet = (BitSet) afterData;
sb.append("\"" + bitSet.get(0) + "\"");
} else if (Date.class.isAssignableFrom(afterData.getClass())) {
Date date = (Date) afterData;
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
simpleDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
String formatDate = simpleDateFormat.format(date);
sb.append("\"" + formatDate + "\"");
} else {
sb.append("\"" + afterData + "\"");
}
}
}
}
sb.append("}");
T cee = JsonUtil.fromJSON(sb.toString(), clazz);
return cee;
}
public static <T> List<UpdateBean<T>> loadUpdateData(List<Map.Entry<Serializable[], Serializable[]>> rows, Class<T> clazz, String[] rowNames) {
List<UpdateBean<T>> result = new ArrayList<>();
for (Map.Entry<Serializable[], Serializable[]> entry : rows) {
result.add(getUpdateData(entry, clazz, rowNames));
}
return result;
}
public static <T> UpdateBean<T> getUpdateData(Map.Entry<Serializable[], Serializable[]> entry, Class<T> clazz, String[] rowNames) {
UpdateBean<T> updateBean = new UpdateBean<>();
Serializable[] before = entry.getKey();
Serializable[] after = entry.getValue();
T beforeBean = getMapData(before, clazz, rowNames);
T afterBean = getMapData(after, clazz, rowNames);
try {
T diffBean = diffUpdateData(entry, clazz, rowNames);
updateBean.setDiffValue(diffBean);
} catch (InstantiationException e) {
DushuLogger.error(e,e);
} catch (IllegalAccessException e) {
DushuLogger.error(e,e);
}
updateBean.setOldValue(beforeBean);
updateBean.setNewValue(afterBean);
return updateBean;
}
public static String[] getRowNameList(Class<?> clazz) {
Field[] fieldList = clazz.getDeclaredFields();
String[] names = new String[fieldList.length];
for (int i = 0; i < fieldList.length; i++) {
names[i] = fieldList[i].getName();
}
return names;
}
}
UpdateBean
package com.xxxx.common.util;
public class UpdateBean<T> {
private T oldValue;
private T newValue;
private T diffValue;
public T getOldValue() {
return oldValue;
}
public void setOldValue(T oldValue) {
this.oldValue = oldValue;
}
public T getNewValue() {
return newValue;
}
public void setNewValue(T newValue) {
this.newValue = newValue;
}
public T getDiffValue() {
return diffValue;
}
public void setDiffValue(T diffValue) {
this.diffValue = diffValue;
}
}
userService,执行具体的同步操作,同步redis,同步memsql。
package com.xxx.common.service;
import com.xxxx.common.dto.JavaUserDTO;
import com.xxxx.common.dto.UserDTO;
import com.xxxx.common.logger.DushuLogger;
import com.xxx.common.mapper.UserSystemMapper;
import com.xxxx.common.util.DataMapCallback;
import com.xxxxx.common.util.UpdateBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class JavaUserService extends DataMapCallback<JavaUserDTO> {
@Autowired
private UserSystemMapper userSystemMapper;
// @Autowired
// private UserMapper userMapper;
@Autowired
private UserRedisService userRedisService;
@Override
public void executeInsert(JavaUserDTO data) {
Integer userId = data.getUserNo();
UserDTO userDTO = userSystemMapper.getUserById(userId);
if (userDTO == null) {
return;
}
// userMapper.insertByBatch(Arrays.asList(userDTO));
userRedisService.insertToRedis(userDTO);
DushuLogger.info("Insert事件被触发,新增用户,数据对象[" + userDTO.toString() + "]");
}
@Override
public void executeUpdate(UpdateBean<JavaUserDTO> data) {
JavaUserDTO oldDto = data.getOldValue();
JavaUserDTO newDto = data.getNewValue();
UserDTO userDTO = userSystemMapper.getUserById(newDto.getUserNo());
if (userDTO == null) {
return;
}
// userMapper.updateWithTUser(userDTO);
userRedisService.updateWithUserNo(userDTO);
userRedisService.updateWithMobile(oldDto.getAreaCode(), oldDto.getMobile(), userDTO);
DushuLogger.info("Update事件被触发,更新用户,数据对象[" + userDTO.toStringWithJavaUser() + "]");
}
@Override
public void executeDelete(JavaUserDTO data) {
}
}
四、小结
目前使用还是稳定的,但是当一个事件点,有很多数据来,并发上来了,数据同步就会有延迟。这个bug还是没有优化。在后面来优化。