简介:
本次想实现的功能:订阅mysql的binlog日志,通过canal中间件将对应日志通过springboot整合的方式打印出来,后面可以根据具体需求实现后续业务功能。之前也看网上有很多的例子,实践过程中发现有些还是不可用的居多,所以综合网上楼主的实例,自己也记录一下搭建成功的详细过程。
详细过程:
1:搭建mysql环境,开启binlog日志
这里是采用的docker的形式搭建。
首先通过docker拉取mysql的镜像
docker pull mysql:5.7
创建并运行容器,指定对应挂载数据目录,默认root账号密码等
docker run -d --name mysql -v /data/mysql:/var/lib/mysql --restart=always -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 mysql:5.7
查看我们mysql容器是否启动成功
docker ps
启动成功后接下来我们要开启我们的binglog日志,
#进入到我们mysql容器内部
docker exec -it mysql sh
修改对应的配置文件
vim /etc/mysql/mysql.conf.d/mysqld.cnf
加入以下内容
#binlong setting
log-bin=/var/lib/mysql/mysql-bin
server-id=123454
docker里面使用vim命令会报错说找不到命令,执行以下操作
apt-get update
apt-get install vim
这一个参数的作用是mysql会根据这个配置自动设置log_bin为on状态,自动设置log_bin_index文件为你指定的文件名后跟.index
第二个参数 ,用的如果是5.7及以上版本的话,重启mysql服务会报错,这个时候我们必须还要指定这样一个参数,随机指定一个不能和其他集群中机器重名的字符串,如果只有一台机器,那就可以随便指定
为了避免后面插入数据为中文字符串出现乱码情况,还可以追加以下内容设置编码格式
[mysqld]
character_set_server=utf8
[client]
default-character_set=utf8
[mysql]
default-character_set=utf8
改完后保存后重新启动mysql容器
docker restart mysql
启动完成后,我们连接mysql,输入以下命令看binlog是否启动
#进入mysql容器
docker exec -it mysql sh
#登录mysql
mysql -u root -p
输入密码
连接mysql后输入以下命令
show variables like '%log_bin%'
可以看到我们binlog变量为on即为开启状态,进入下一个环节
2:搭建canal环境,并连接我们之前的mysql容器
对于canal这里简单介绍以下,canal,译意为水道/管道/沟渠,主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费。了解详情的话可以去看canal的官网,这里不多介绍。
docker先拉取canal对应的镜像
docker pull canal/canal-server:v1.1.4
先启动canal容器
docker run -p 11111:11111 --name canal -d canal/canal-server:v1.1.4
初次启动Canal镜像后,将instance.properties文件复制到宿主机并通过挂载进行加载
docker cp \
canal:/home/admin/canal-server/conf/example/instance.properties \
/root/data
这里/home/admin/canal-server/conf/example/instance.properties为canal对应的配置文件的目录地址
/root/data为楼主linux上指定的文件夹目录,这个根据你自己的环境指定一个目录
#修改instance.properties
vim /root/data/instance.properties
这里的172.18.0.2对应mysql容器的docker对外暴露的ip地址,可以使用以下命令查看
docker inspect mysql| findstr IPAddress
把之前的canal容器移除,重新创建容器启动
docker run -p 11111:11111 \
--name canal \
-v /root/data/instance.properties:/home/admin/canal-server/conf/example/instance.properties \
-d canal/canal-server:v1.1.4
启动成功后记得我们对应的服务器要开启11111端口,不然后面我们项目是访问不了的
3:搭建springboot环境
创建我们的maven项目
引入依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.client</artifactId>
<version>1.1.4</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.17.2</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.17.2</version>
</dependency>
</dependencies>
在resources建一个application.yml配置文件
canal:
# instance 实例所在ip
host: 服务器公网的ip地址
# tcp通信端口对应canal的端口
port: 11111
# 这里用户名密码我们都没指定所以直接为空
username:
# 密码
password:
#实例名称,这里对应之前的instance.properties配置里的不用改,默认为 example
instance: example
新建springboot启动类StartApplication类
package cn.zwh;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class StartApplication {
public static void main(String[] args) {
SpringApplication.run(StartApplication.class,args);
}
}
新建一个canalListener配置类里面写具体的逻辑
package cn.zwh.listener;
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 org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import java.net.InetSocketAddress;
import java.util.List;
@Component
public class CanalListener implements ApplicationRunner {
private static final Logger logger = LogManager.getLogger();
@Value("${canal.host}")
private String host;
@Value("${canal.port}")
private int port;
@Value("${canal.username}")
private String username;
@Value("${canal.password}")
private String password;
@Value("${canal.instance}")
private String instance;
@Override
public void run(ApplicationArguments args) throws Exception {
CanalConnector conn = getConn();
conn.connect();
//订阅实例中所有的数据库和表
conn.subscribe(".*\\..*");
// 回滚到未进行ack的地方
conn.rollback();
try {
while (true)
{
//获取指定数量的数据,如果不做确认标记,下一次取还会取到这些信息
Message message = conn.getWithoutAck(100);
//获取消息id
long batchId = message.getId();
int size = message.getEntries().size();
if (batchId != -1 && size > 0) {
logger.info("msgId -> " + batchId);
//转换sql
analysis(message.getEntries());
//消息确认
conn.ack(batchId);
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭连接
conn.disconnect();
}
}
private static void analysis(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()) {
System.out.println(rowChange.getEventType());
List<CanalEntry.Column> columnList = null;
StringBuffer sql = null;
switch (rowChange.getEventType()) {
//如果希望监听多种事件,可以手动增加case
case INSERT:
columnList = rowData.getAfterColumnsList();
sql = new StringBuffer("insert into " +
entry.getHeader().getTableName() + " (");
for (int i = 0; i < columnList.size(); i++) {
sql.append(columnList.get(i).getName());
if (i != columnList.size() - 1) {
sql.append(",");
}
}
sql.append(") VALUES (");
for (int i = 0; i < columnList.size(); i++) {
sql.append("'" + columnList.get(i).getValue() + "'");
if (i != columnList.size() - 1) {
sql.append(",");
}
}
sql.append(")");
logger.info("插入sql:" + sql);
break;
case UPDATE:
columnList = rowData.getAfterColumnsList();
sql = new StringBuffer("update " +
entry.getHeader().getTableName() + " set ");
String mainKey = "";
boolean firstFlag=true;
for (int i = 0; i < columnList.size(); i++) {
if (columnList.get(i).getUpdated()) {
if (!firstFlag) {
sql.append(",");
}
firstFlag=false;
sql.append(" ")
.append(columnList.get(i).getName())
.append(" = '").append(columnList.get(i).getValue())
.append("'");
} else {
if (columnList.get(i).getIsKey()) {
mainKey = columnList.get(i).getName() + "=" + columnList.get(i).getValue();
}
}
}
sql.append(" where " + mainKey);
logger.info("更新sql:" + sql);
break;
case DELETE:
columnList = rowData.getBeforeColumnsList();
sql = new StringBuffer("delete from " +
entry.getHeader().getTableName() + " where ");
for (CanalEntry.Column column : columnList) {
if (column.getIsKey()) {
sql.append(column.getName()).append("=").append(column.getValue());
break;
}
}
logger.info("删除sql:" + sql);
break;
default:
}
}
} catch (InvalidProtocolBufferException e) {
e.printStackTrace();
}
}
}
/**
* 获取连接
*/
public CanalConnector getConn() {
return CanalConnectors.newSingleConnector(new InetSocketAddress(host, port), instance, username, password);
}
}
启动我们的springboot项目,编写测试sql看控制台打印内容
4:测试打印sql
使用我们的navicate工具连接到我们的mysql或者你也可以直接使用命令行建立我们的测试数据库,测试表
这里我建了一个测试数据库test,里面建了一张测试user表
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL,
`name` varchar(255) DEFAULT NULL,
`remark` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
写sql测试打印效果
insert into user(`name`,`remark`) values ('test','testRemark');
update user set name='testaa' where id=2;
delete from user where id=2;
查看我们idea控制台
可以看到通过canal订阅我们mysql的binlog,我们的日志转换的sql正确打印出来了,至此我们的整个搭建过程就完成了。后续如果涉及到具体业务处理时,可以进行扩展,代码调整,这里是测试demo所以写的比较简单。正式项目时一般会把我们的sql写入的中间件例如rabbitmq中后续再做相关的增量同步操作等。
总结
本次搭建的过程还算比较顺利,主要时间花在springboot整合上,之前也看过网上很多的参考例子,但是经过调试发现有些还是存在问题的,所以此次搭建成功后记录一下过程,后面如果碰到到具体业务需求就可以作为参考了,感兴趣的小伙伴可以一起交流学习呀。