本文主要内容:简单介绍Canal及其工作原理,并实现了感知数据库敏感操作。
1. 是什么?
canal,中文:管道/运河,主要用途是用于MySQL数据库增量日志数据的订阅、消费和解析。
2. 工作原理
- canal 模拟MySQL salve的交互协议,把自己伪装成MySQL slave,向MySQL master发送dump协议。
- MySQL master收到dump请求,开始推送binary log给slave(即canal);
- canal解析binary log对象(初始数据为byte流)
3. 搭建环境
3.1 Java开发环境配置
(1)下载JDK11
(2)安装Java
运行安装程序,点击下一步即可,若有需求更改路径,则在相应位置进行配置即可。
(3)Java环境配置
首先Java的Home目录设置为jdk的根目录。
然后在Path中设置JDK根目录下的bin文件夹。
(4)测试
在Windows的cmd命令窗口输入java -version
打印出Java的版本信息,即表示安装成功。
3.2 Mysql环境配置
为配置Canal框架,需对Mysql进行必要的配置。查看mysql的log_bin是否开启,可在mysql中执行:show variables like ‘%log_bin%’进行查看。因为canal是通过logbin监听更新的,所以必须开启。注意:Mysql不能低于5.6。
(1)安装并登录Mysql
具体安装过程不再赘述,网上资料很多。
(2)配置Mysql
主要包含mysql根目录下的my.ini文件以下三个项的配置:
[mysqld]
log-bin=mysql-bin #开启binlog
binlog-format=ROW #选择ROW模式
server_id=1 #配置MySQL replaction需要定义,不要和canal的slaveId重复
(3)授权
授权canal连接MySQL账号具有作为MySQL slave的权限,如果已有账户可直接grant授权:
// 创建用户canal,并配置密码。
create user canal identified by 'canal';
// 给用户canal授权。
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
# 刷新并应用
FLUSH PRIVILEGES;
// 刷新,然后重启Mysql
FLUSH PRIVILEGES;
(4)创建数据库表,以待测试。
CREATE TABLE `users` (
`id` int(11) NOT NULL,
`nick` varchar(30) DEFAULT NULL,
`phone` varchar(20) NOT NULL,
`password` varchar(64) DEFAULT NULL,
`email` varchar(50) DEFAULT NULL,
`account` varchar(15) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
3.2 配置Canal服务端
(1)下载并解压canal
(2)修改数据库链接配置文件E:\program-software\canal.deployer-1.1.5-SNAPSHOT\conf\example\instance.properties
canal.instance.defaultDatabaseName = test_repo
4 开始实现感知敏感操作
(1)添加pom依赖
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.client</artifactId>
<version>1.1.4</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
</dependency>
(2)UserDTO
import lombok.Data;
@Data
public class UserDTO {
private Integer id;
private String nick;
private String phone;
private String password;
private String email;
private String account;
}
(3)主实现类
import com.alibaba.otter.canal.client.CanalConnector;
import com.alibaba.otter.canal.client.CanalConnectors;
import com.alibaba.otter.canal.protocol.CanalEntry;
import com.alibaba.otter.canal.protocol.Message;
import com.google.protobuf.InvalidProtocolBufferException;
import com.sky.canal.dto.UserDTO;
import java.net.InetSocketAddress;
import java.util.List;
/**
* canal 监听客户端
*/
public class CanalClient {
private static String SERVER_ADDRESS = "127.0.0.1";
private static Integer PORT = 11111;
private static String DESTINATION = "example";
private static String USERNAME = "";
private static String PASSWORD = "";
public static void main(String[] args) {
// newSingleConnector是简单的ip直连模式,基于CanalServerWithNetty定义的网络协议接口,对于canal数据进行get/rollback/ack等操作
CanalConnector canalConnector = CanalConnectors.newSingleConnector(new InetSocketAddress(SERVER_ADDRESS, PORT), DESTINATION, USERNAME, PASSWORD);
canalConnector.connect();
//订阅
canalConnector.subscribe(".*\\..*");
//恢复到之前同步的数据,避免误操作
canalConnector.rollback();
while (true) {
// 获取指定数量的数据,但是不做确认
Message message = canalConnector.getWithoutAck(100);
// 消息ID
long batchId = message.getId();
if (batchId != -1) {
// System.out.println(message.getEntries());
System.out.println("msgId-->" + batchId);
printEntity(message.getEntries());
// canalConnector.ack(batchId); //提交确认
// canalConnector.rollback(); //处理失败,回滚数据
}
}
}
public static void printEntity(List<CanalEntry.Entry> entries){
for (CanalEntry.Entry entry : entries) {
if (entry.getEntryType() != CanalEntry.EntryType.ROWDATA){
continue;
}
try{
// 数据反序列化
CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
for (CanalEntry.RowData rowData: rowChange.getRowDatasList()) {
switch (rowChange.getEventType()){
case INSERT:
String tableName = entry.getHeader().getTableName();
//测试users表进行映射处理
UserDTO userDTO = CanalDataHadler.convertToBean(rowData.getAfterColumnsList(), UserDTO.class);
System.out.println("执行了一条插入操作,数据为:" + userDTO);
break;
case DELETE:
UserDTO deleteUserDTO = CanalDataHadler.convertToBean(rowData.getAfterColumnsList(), UserDTO.class);
System.out.println("执行了一条删除操作,数据为:" + deleteUserDTO);
break;
case UPDATE:
UserDTO updateDTO = CanalDataHadler.convertToBean(rowData.getAfterColumnsList(), UserDTO.class);
System.out.println("执行了一条更新操作,数据为:" + updateDTO);
break;
default:
break;
}
}
}catch (InvalidProtocolBufferException e){
e.printStackTrace();
}
}
}
}
(3)两个工具类
import com.alibaba.otter.canal.protocol.CanalEntry;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class CanalDataHadler extends TypeConvertHandler{
// 将binlog的记录解析为一个bean对象
public static <T> T convertToBean(List<CanalEntry.Column> columnList, Class<T> clazz){
T bean = null;
try{
bean = clazz.newInstance();
Field[] fields = clazz.getDeclaredFields();
Field.setAccessible(fields, true);
Map<String, Field> fieldMap = new HashMap<>(fields.length);
for (Field field : fields) {
fieldMap.put(field.getName().toLowerCase(), field);
}
if (fieldMap.containsKey("serialVersionUID")) {
fieldMap.remove("serialVersionUID".toLowerCase());
}
for (CanalEntry.Column column : columnList) {
String columnName = column.getName();
String columnValue = column.getValue();
if (fieldMap.containsKey(columnName)) {
Field field = fieldMap.get(columnName);
Class<?> type = field.getType();
if (BEAN_FIELD_TYPE.containsKey(type)) {
switch (BEAN_FIELD_TYPE.get(type)){
case "Integer":
field.set(bean, parseToInteger(columnValue));
break;
case "Long":
field.set(bean, parseToLong(columnValue));
break;
case "Double":
field.set(bean, parseToDouble(columnValue));
case "String":
field.set(bean, columnValue);
break;
case "java.handle.Date":
field.set(bean, parseToDouble(columnValue));
break;
case "java.sql.Date":
field.set(bean, parseToSqlDate(columnValue));
break;
case "java.sql.Timestamp":
field.set(bean, parseToTimestamp(columnValue));
case "java.sql.Time":
field.set(bean, parseToSqlTime(columnValue));
break;
}
}else{
field.set(bean, parseObj(columnValue));
}
}
}
}catch (InstantiationException | IllegalAccessException e){
System.out.println("初始化对象出现异常,对象无法被实例化,异常为:" + e);
}
return bean;
}
static Object parseObj(String str){
if (str == null || str.equals("")) {
return null;
}
return str;
}
}
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* 类型转化器
*/
public class TypeConvertHandler {
static final Map<Class, String> BEAN_FIELD_TYPE;
static {
BEAN_FIELD_TYPE = new HashMap<>(8);
BEAN_FIELD_TYPE.put(Integer.class, "Integer");
BEAN_FIELD_TYPE.put(Long.class, "Long");
BEAN_FIELD_TYPE.put(Double.class, "Double");
BEAN_FIELD_TYPE.put(String.class, "String");
BEAN_FIELD_TYPE.put(Date.class, "Date");
BEAN_FIELD_TYPE.put(java.sql.Date.class, "java.sql.Date");
BEAN_FIELD_TYPE.put(java.sql.Timestamp.class, "java.sql.Timestamp");
BEAN_FIELD_TYPE.put(java.sql.Time.class, "java.sql.Time");
}
static Integer parseToInteger(String source){
if (isSourceNull(source)) {
return null;
}
return Integer.valueOf(source);
}
static Long parseToLong(String source){
if (isSourceNull(source)) {
return null;
}
return Long.valueOf(source);
}
static Double parseToDouble(String source){
if (isSourceNull(source)) {
return null;
}
return Double.valueOf(source);
}
protected static Date parseToDate(String source){
if (isSourceNull(source)) {
return null;
}
if (source.length() == 10) {
source = source + "00:00:00";
}
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date;
try {
date = sdf.parse(source);
}catch (ParseException e){
return null;
}
return date;
}
static java.sql.Date parseToSqlDate(String source){
if (isSourceNull(source)) {
return null;
}
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
java.sql.Date sqlDate;
Date utilDate;
try {
utilDate = sdf.parse(source);
}catch (ParseException e){
return null;
}
sqlDate = new java.sql.Date(utilDate.getTime());
return sqlDate;
}
static java.sql.Timestamp parseToTimestamp(String source){
if (isSourceNull(source)) {
return null;
}
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
java.sql.Timestamp timestamp;
Date date;
try {
date = sdf.parse(source);
}catch (ParseException e){
return null;
}
timestamp = new java.sql.Timestamp(date.getTime());
return timestamp;
}
static java.sql.Time parseToSqlTime(String source){
if (isSourceNull(source)) {
return null;
}
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
java.sql.Time time;
Date date;
try {
date = sdf.parse(source);
}catch (ParseException e){
return null;
}
time = new java.sql.Time(date.getTime());
return time;
}
private static boolean isSourceNull(String source){
return source.equals("");
}
}
(4)测试
开启canal服务
添加一条数据:
insert into users (nick, phone) values ('sky', '18800000000');
修改一条数据:
update users set phone = 13701012323 where nick = 'sky';
删除一条数据:
delete from users where nick = 'sky';
5. 附录 - 相关命令
- 是否启用了日志
- show variables like ‘log_bin’;
- 怎么知道当前的日志
- show master status;
- 查看mysql binlog模式
- show variables like ‘binlog_format’;
- 获取binlog文件列表
- show binary logs;
- 查看当前正在写入的binlog文件
- show master status \G
- 查看指定binlog文件的内容
- show binlog events in ‘mysql-bin.000002’;
查看指定binlog文件的内容 - show binlog events in ‘mysql-bin.000002’;
- show binlog events in ‘mysql-bin.000002’;
- 注意binlog日志格式要求为row格式