本文主要內容:簡單介紹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格式