最近公司新接了一个医院项目,主要是对接医院内营养处方数据,院内his部署在内网环境,因此仅提供一个外网数据库给我们(他们自己应该做了类似主从的方式将我们需要的数据同步一份到外网数据库),又因为落地的数据我们需要立马进行处理并在公众号推送给病人进行付款等一系列其他操作,所以我们需要实时监听数据库的变化。以下是当时能想到的方案:
方案一:轮询数据库
第一个想到的也第一个被pass。原因也不必多说
方案二:数据库触发器
数据库触发器公司内部的小伙伴使用的都极其的少,确实是我们自己学艺不精,而且维护起来不方便,对后面来的开发小伙伴也不友好,最终也被pass
方案三:消息队列
这应该是在没有提供外网数据库的时候想出的方案,也是最符合业务场景的方案之一,但由于甲方不想增加开发成本、业务代码被入侵以及使用第三方插件增加系统风险和不稳定性等一系列原因,这个方案也被pass
方案四:Canal
讨论很久以后才决定使用canal,主要原因第一个是我们需要实时监听院内处方的数据变化,第二个是推过来的处方数据我们需要展示给用户,每个人只能看到自己的处方数据,并且处方有时效性,利用redis的过期时间也能完美代替数据库条件查询。
这里简单介绍,具体就不从官网贴图了,啥都没有官网说的清楚,点击进入Canal官网
1、了解什么是Canal之前我们应该首先了解以下MySql的主从复制原理
MySQL master 将数据变更写入二进制日志( binary log, 其中记录叫做二进制日志事件binary log events,可以通过 show binlog events 进行查看)
MySQL slave 将 master 的 binary log events 拷贝到它的中继日志(relay log)
MySQL slave 重放 relay log 中事件,将数据变更反映它自己的数据。
上述中涉及到两个重要的MySql事务日志 binlog和relaylog,简单介绍一下两种日志
BinLog
即binary log,二进制日志文件,也叫作变更日志(update log)。它记录了数据库所有执行的
DDL 和 DML 等数据库更新事件的语句,但是不包含没有修改任何数据的语句(如数据查询语句select、 show等)。
说白了就是MySql记录了除了查询语句之外的几乎任何增删改操作。MySql8以后默认开启。开启会损失约百分之一的性能。
RelayLog
中继日志只在主从服务器架构的从服务器上存在。从服务器为了与主服务器保持一致,要从主服务器读
取二进制日志(binary log)的内容,并且把读取到的信息写入 本地的日志文件 中,这个从服务器本地的日志文件就叫
中继日志 。然后,从服务器读取中继日志,并根据中继日志的内容对从服务器的数据进行更新,完成主
从服务器的 数据同步 。
2、了解了主从复制的原理以后,就把Canal当成一个Slave,原理也就清晰了
SELECT VERSION(); -- 查看数据库版本
SHOW VARIABLES LIKE 'log_bin%';
-- 如果log_bin的value为on则已开启,显示off则未开启。
[mysqld]
log-bin=mysql-bin #添加这一行就ok
binlog-format=ROW #选择row模式
server_id=1 #配置mysql replaction需要定义,不能和canal的slaveId重复
CREATE USER canal IDENTIFIED BY 'canal';
-- 授权部分操作
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
-- 也可以使用下面的命令授权所有操作
GRANT ALL PRIVILEGES ON *.* TO 'canal'@'%' ;
-- 需要刷新权限生效
FLUSH PRIVILEGES;
------------注意踩坑点------------
注意点一: log_bin 为只读变量,不能通过命令set global log_bin = on;临时生效。
注意点二: 这是我刚开始学习Canal踩的坑,也是自己对binlog不够了解犯的错误。也是很多博主没有提到过的问题,所以自己照葫芦画瓢就容易出错。执行命令如下:
SHOW BINARY LOGS;
针对于已经运行的或者重启过很多次的MySql可以看到很多binlog日志文件(也有可能是几十几百个),这和binlog的配置有很大关系,同学们自己感兴趣的了解一下。因此canal在配置的时候要配置监听的文件名称,很多博主照搬照抄都设置为binlog.000001。导致后面监听不到数据便变化。所以后面设置的时候需要注意文件名称为当前MySql正在写入的binlog,如何确定具体是哪个呢?执行如下命令:
SHOW MASTER STATUS;
正对于一些不重要的数据库可以直接使用下面命令删除所有binlog日志文件(慎用!!!)
RESET MASTER;
这样后面canal的配置文件名称就可以用binlog.000001,否则只能用上述的binlog.000054
下载地址:
解压:tar -zxvf canal.deployer-1.1.4.tar.gz
解压后如图四个文件夹
进入conf/example下
修改instance.properties,主要配置参考如下
#################################################
#需要配置的地方都进行了中文描述,其他都为默认信息
canal.instance.gtidon=false
# 这里是数据库地址信息,数据库账号密码在下面
canal.instance.master.address=124.111.11.111:3306
#这里就是上面强调的binlog日志文件名称即mysql主库链接时起始的binlog文件
canal.instance.master.journal.name=binlog.000001
#mysql主库链接时起始的binlog偏移量
canal.instance.master.position=157
#mysql主库链接时起始的binlog的时间戳
canal.instance.master.timestamp=
canal.instance.master.gtid=
# rds oss binlog
canal.instance.rds.accesskey=
canal.instance.rds.secretkey=
canal.instance.rds.instanceId=
# table meta tsdb info
canal.instance.tsdb.enable=false
#这里为数据库账号
canal.instance.dbUsername=canal
#这里为数据库密码
canal.instance.dbPassword=canal
canal.instance.connectionCharset = UTF-8
# enable druid Decrypt database password
canal.instance.enableDruid=false
#canal.instance.pwdPublicKey=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALK4BUxdDltRRE5/zXpVEVPUgunvscYFtEip3pmLlhrWpacX7y7GCMo2/JM6LeHmiiNdH1FWgGCpUfircSwlWKUCAwEAAQ==
# table regex
canal.instance.filter.regex=.*\\..*
# table black regex
canal.instance.filter.black.regex=mysql\\.slave_.*
# table field filter(format: schema1.tableName1:field1/field2,schema2.tableName2:field1/field2)
#canal.instance.filter.field=test1.t_product:id/subject/keywords,test2.t_company:id/name/contact/ch
# table field black filter(format: schema1.tableName1:field1/field2,schema2.tableName2:field1/field2)
#canal.instance.filter.black.field=test1.t_product:subject/product_image,test2.t_company:id/name/contact/ch
# mq config
canal.mq.topic=example
# dynamic topic route by schema or table regex
#canal.mq.dynamicTopic=mytest1.user,mytest2\\..*,.*\\..*
canal.mq.partition=0
# hash partition config
#canal.mq.partitionsNum=3
#canal.mq.partitionHash=test.table:id^name,.*\\..*
#################################################
进入canal1.1.4/bin可以看以下脚本。
直接命令启动:sh startup.sh
;
因为canal-server本质上也是jar包在运行,直接通过jps命令查看运行状态
发现已经初步启动
接下来进入/canal1.1.4/logs/example/查看日志观察是否报错tail -f example.log
查看日志信息
------------注意踩坑点------------
注意点一:linux启动完成后,会在bin目录下生成canal.pid,stop.sh会读取canal.pid进行进程关闭。如果直接通过kill命令可能会导致canal不能完全关闭,这样下次执行./startup.sh
命令可能会启动失败。
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<groupId>com.groupId>
<artifactId>springcloud-alibabaartifactId>
<version>1.0-SNAPSHOTversion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.3.4.RELEASEversion>
<relativePath/>
parent>
<properties>
<maven.compiler.source>8maven.compiler.source>
<maven.compiler.target>8maven.compiler.target>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>com.alibaba.ottergroupId>
<artifactId>canal.clientartifactId>
<version>1.1.4version>
dependency>
dependencies>
project>
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 org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.net.InetSocketAddress;
import java.util.List;
/**
* @author zqf
* @desc
*/
@Component
public class CanalListener {
/**
* 解析数据
*
* @param beforeColumns 修改、删除后的数据
* @param afterColumns 新增、修改、删除前的数据
* @param dbName 数据库名字
* @param tableName 表大的名字
* @param eventType 操作类型(INSERT,UPDATE,DELETE)
* @param timestamp 消耗时间
*/
private static void dataDetails(List<CanalEntry.Column> beforeColumns, List<CanalEntry.Column> afterColumns, String dbName, String tableName, CanalEntry.EventType eventType, long timestamp) {
System.out.println("数据库:" + dbName);
System.out.println("表名:" + tableName);
System.out.println("操作类型:" + eventType);
if (CanalEntry.EventType.INSERT.equals(eventType)) {
System.out.println("这是一条新增的数据");
} else if (CanalEntry.EventType.DELETE.equals(eventType)) {
System.out.println("删除数据:" + afterColumns);
} else {
System.out.println("更新数据:更新前数据--" + afterColumns);
System.out.println("更新数据:更新后数据--" + beforeColumns);
}
System.out.println("操作时间:" + timestamp);
}
@PostConstruct
public void run() throws Exception {
CanalConnector conn = CanalConnectors.newSingleConnector(new InetSocketAddress("124.111.11.111", 11111), "example", null, null);
while (true) {
conn.connect();
conn.subscribe(".*\\..*");
// 回滚到未进行ack的地方
conn.rollback();
// 获取数据 每次获取一百条改变数据
Message message = conn.getWithoutAck(100);
//获取这条消息的id
long id = message.getId();
int size = message.getEntries().size();
if (id != -1 && size > 0) {
// 数据解析
analysis(message.getEntries());
} else {
//暂停1秒防止重复链接数据库
Thread.sleep(1000);
}
// 确认消费完成这条消息
conn.ack(message.getId());
// 关闭连接
conn.disconnect();
}
}
/**
* 数据解析
*/
private void analysis(List<CanalEntry.Entry> entries) {
for (CanalEntry.Entry entry : entries) {
// 解析binlog
CanalEntry.RowChange rowChange = null;
try {
rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
} catch (Exception e) {
throw new RuntimeException("解析出现异常 data:" + entry.toString(), e);
}
if (rowChange != null) {
// 获取操作类型
CanalEntry.EventType eventType = rowChange.getEventType();
// 获取当前操作所属的数据库
String dbName = entry.getHeader().getSchemaName();
// 获取当前操作所属的表
String tableName = entry.getHeader().getTableName();
// 事务提交时间
long timestamp = entry.getHeader().getExecuteTime();
for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {
dataDetails(rowData.getBeforeColumnsList(), rowData.getAfterColumnsList(), dbName, tableName, eventType, timestamp);
}
}
}
}
}
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author zqf
* @desc
*/
@SpringBootApplication
public class CanalApplication {
public static void main(String[] args) {
SpringApplication.run(CanalApplication.class, args);
}
}
如此即可,启动项目,并随意在监听的数据库增删改,查看控制台信息输出
数据库:linyi
表名:tb_answer
操作类型:UPDATE
更新数据:更新前数据--[index: 0
sqlType: -5
name: "answer_no"
isKey: true
updated: false
isNull: false
value: "1638010372783263744"
mysqlType: "bigint"
, index: 1
sqlType: 12
name: "questionnaire_name"
isKey: false
updated: true
isNull: false
value: "\351\227\256\345\215\267\350\260\203\346\237\245\350\241\2501"
mysqlType: "varchar(255)"
, index: 2
sqlType: -5
name: "questionnaire_no"
isKey: false
updated: false
isNull: false
value: "1637737551628750848"
mysqlType: "bigint"
, index: 3
sqlType: -5
name: "goods_no"
isKey: false
updated: false
isNull: false
value: "1"
mysqlType: "bigint"
, index: 4
sqlType: -5
name: "patient_no"
isKey: false
updated: false
isNull: false
value: "1635614014201823232"
mysqlType: "bigint"
, index: 5
sqlType: 12
name: "answer_url"
isKey: false
updated: false
isNull: false
value: "questionnaire_answer/2023-03-21/1638010372783263745.jpg"
mysqlType: "varchar(255)"
, index: 6
sqlType: 4
name: "assess_score"
isKey: false
updated: false
isNull: false
value: "2"
mysqlType: "int"
, index: 7
sqlType: 12
name: "assess_result"
isKey: false
updated: false
isNull: true
mysqlType: "varchar(500)"
, index: 8
sqlType: 4
name: "status"
isKey: false
updated: false
isNull: false
value: "0"
mysqlType: "int"
, index: 9
sqlType: 93
name: "create_time"
isKey: false
updated: false
isNull: false
value: "2023-03-21 10:51:20"
mysqlType: "datetime"
, index: 10
sqlType: 93
name: "update_time"
isKey: false
updated: false
isNull: false
value: "2023-03-21 10:51:20"
mysqlType: "datetime"
, index: 11
sqlType: 4
name: "is_del"
isKey: false
updated: false
isNull: false
value: "0"
mysqlType: "int"
]
更新数据:更新后数据--[index: 0
sqlType: -5
name: "answer_no"
isKey: true
updated: false
isNull: false
value: "1638010372783263744"
mysqlType: "bigint"
, index: 1
sqlType: 12
name: "questionnaire_name"
isKey: false
updated: false
isNull: false
value: "\351\227\256\345\215\267\350\260\203\346\237\245\350\241\250"
mysqlType: "varchar(255)"
, index: 2
sqlType: -5
name: "questionnaire_no"
isKey: false
updated: false
isNull: false
value: "1637737551628750848"
mysqlType: "bigint"
, index: 3
sqlType: -5
name: "goods_no"
isKey: false
updated: false
isNull: false
value: "1"
mysqlType: "bigint"
, index: 4
sqlType: -5
name: "patient_no"
isKey: false
updated: false
isNull: false
value: "1635614014201823232"
mysqlType: "bigint"
, index: 5
sqlType: 12
name: "answer_url"
isKey: false
updated: false
isNull: false
value: "questionnaire_answer/2023-03-21/1638010372783263745.jpg"
mysqlType: "varchar(255)"
, index: 6
sqlType: 4
name: "assess_score"
isKey: false
updated: false
isNull: false
value: "2"
mysqlType: "int"
, index: 7
sqlType: 12
name: "assess_result"
isKey: false
updated: false
isNull: true
mysqlType: "varchar(500)"
, index: 8
sqlType: 4
name: "status"
isKey: false
updated: false
isNull: false
value: "0"
mysqlType: "int"
, index: 9
sqlType: 93
name: "create_time"
isKey: false
updated: false
isNull: false
value: "2023-03-21 10:51:20"
mysqlType: "datetime"
, index: 10
sqlType: 93
name: "update_time"
isKey: false
updated: false
isNull: false
value: "2023-03-21 10:51:20"
mysqlType: "datetime"
, index: 11
sqlType: 4
name: "is_del"
isKey: false
updated: false
isNull: false
value: "0"
mysqlType: "int"
]
操作时间:1681747515000
至此,Canal整合完成