Canal 监听 Mysql 数据变化实践记录

背景:

看到 八股里面 有一个如何保证 Redis和数据库的一致性 问题,

一开始说的是从先更新数据库后更新缓存,还是先更新缓存后更新数据库,

再决定应该是删除缓存而不是更新,

那么先更新数据库后删除缓存,还是先删除缓存后更新数据库

对于这个问题,答案是先更新数据库再删除缓存,因为
在这里插入图片描述
数据库更新的耗时通常比缓存重建更长,因此后者发生的情况相对较少。

但这还是存在问题,

更新数据库和删除缓存毕竟是两个不同的操作,没办法保证同时成功,

假如更新数据库成功,删除缓存失败怎么办?

要保证 分布系统的 一致性,可以使用消息队列 MQ 来保证 失败之后有重试的机会。

具体来说,

更新数据库之后,把 已经更新数据的消息 存入 消息队列,之后的 删除业务 从消息队列中取任务,
如果执行删除任务失败,则消息队列重试,重新推送消息给删除业务,直到最大次数maxTry。
如果成功,则删除消息,完成一致性保证。

但还是可以看到 ,

每次更新数据库之后,都需要 增加一个插入MQ消息的操作,


update(a,oldvalue,newvalue);

MQClient.sendMsg(msg);

这会大大增加代码的耦合度,因此

另一种方案就是使用Canal来保证一致性,进入正文。


Canal 组件是一个基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费,将增量数据投递到下游消费者(如 KafkaRocketMQ 等)或者存储(如ElasticsearchHBase 等)的组件。

在这里插入图片描述
Canal会把自己伪装成一个从节点,向Master主节点发送dump请求,Master就会推送Binlog给它,之后解析数据的变动,下发给下游任务,

从这里可以发现,我们不需要像之前那样,在更新数据库的代码里去显式地 插入一条删除任务

到消息队列,canal可以在背后默默地实现这个操作,极大降低代码之间的耦合。


本文主要记录一下 如何实操canal监听 mysql数据库。

我选择的是 docker进行部署,mysqlcanal都是通过容器启动的。

  1. 拉取镜像

docker中添加mysql镜像 和 canal镜像 【我的docker里面有两个版本的mysql
v8和v5的,而canalv1.1.5,经过实践,v1.1.5canal最好还是配合v5版本的mysql.】

docker pull mysql:5.7.36
docker pull canal/canal-server:v1.1.5
  1. 拉取镜像完成后,为了保证canal和mysql在同一个docker网络中可以通信,可以事先创建一个网络
docker network create net1
  1. 运行两个镜像,启动两个容器
#启动canal

docker run -p 11111:11111 \
--name canal \
--network net1 \
-e canal.destinations=canalclu01 \
-e canal.instance.master.address=mysql5:3306  \
-e canal.instance.dbUsername=canal  \
-e canal.instance.dbPassword=canal  \
-e canal.instance.connectionCharset=UTF-8 \
-e canal.instance.tsdb.enable=true \
-e canal.instance.gtidon=false  \
-e canal.instance.filter.regex=.*\\..* \
 -d canal/canal-server:v1.1.5

#启动mysql
docker run -d \
--name mysql5 \
--network net1 \
-p 3306:3306 \
-e TZ=Asia/Shanghai \
-e MYSQL_ROOT_PASSWORD=yourpassword \
-v /root/mysql5/data:/var/lib/mysql \
-v /root/mysql5/init:/docker-entrypoint-initdb.d \
-v /root/mysql5/conf:/etc/mysql/conf.d mysql:5.7.36

这样你就在同一个docker网络net1里启动了两个容器,mysqlcanal.

为了确保canal真的在监听 mysql主节点,可以进入容器中查看确认,

进入mysql容器,确认配置好了内容

#启动容器后进入 容器

docker exec -it mysql5 bash
#进入mysql
mysql -u root -p  之后输入密码

#进入mysql后
SHOW VARIABLES WHERE Variable_name IN ('log_bin', 'binlog_format', 'server_id');

结果需要如下:
log_bin       | ON
binlog_format | ROW
server_id     |0,例如 1

如果不是,需要去mysql挂载的配置目录修改,我们挂载的目录是/root/mysql5/conf,

在/root/mysql5/conf/新建一个my.cnf文件,写入这些内容

[mysqld]
log-bin=mysql-bin
binlog_format=ROW
server-id=1

再次查看
SHOW VARIABLES WHERE Variable_name IN ('log_bin', 'binlog_format', 'server_id');



另外,还需要保证“从节点”canal有权限访问主节点

GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%' IDENTIFIED BY 'canal';
FLUSH PRIVILEGES;

以上就完成了所有的mysql配置

接下来可以去canal容器里看看

docker exec -it canal bash
进入容器后,
我们的canal实例信息在这个目录:
/home/admin/canal-server/conf/canalclu01的instance.properties中
而监听mysql的日志记录在
/home/admin/canal-server/logs/canalclu01的canalclu01.log中

在上面的日志canalclu01.log里可以看到Mysql信息,

c.a.o.c.p.inbound.mysql.rds.RdsBinlogEventParserProxy - ---> find start position successfully, EntryPosition[included=false,journalName=mysql-bin.000001,position=2290,serverId=1,gtid=,timestamp=1752587547000] cost : 3389ms , the next step is binlog dump

说明就已经在监听了.

接下来是如何在Java中得到这些dump解析出的信息

在项目中引入依赖

        <dependency>
            <groupId>com.alibaba.otter</groupId>
            <artifactId>canal.client</artifactId>
            <version>1.1.5</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba.otter</groupId>
            <artifactId>canal.protocol</artifactId>
            <version>1.1.5</version>
        </dependency>
    public static void main(String[] args) throws Exception {
        // 连接 Canal Server,IP 和端口是 Canal 服务所在地址和端口
        CanalConnector connector = CanalConnectors.newSingleConnector(
            new InetSocketAddress("虚拟机ip", 11111),  // 11111 是 Canal 默认端口
            "canalclu01",    // 这里是 Canal 实例名,和你 Canal 配置的 instance.name 保持一致
            "canal",              // 用户名
            "canal"               // 密码
        );

        try {
            connector.connect();
            // 订阅你关心的库和表,比如监听db01库所有表
            connector.subscribe("db01\\..*");
            connector.rollback();  // 回滚上次未确认的位点

            while (true) {
                // 每次拉取最多 100 条消息,不等待
                Message message = connector.getWithoutAck(100);
                long batchId = message.getId();
                int size = message.getEntries().size();

                if (batchId != -1 && size > 0) {
                    printEntries(message.getEntries());
                }

                // 确认消费成功,Canal 才会移动位点
                connector.ack(batchId);

                Thread.sleep(1000);  // 根据业务节奏睡眠,避免空转
            }
        } finally {
            connector.disconnect();
        }
    }

    private static void printEntries(List<Entry> entries) throws Exception {
        for (Entry entry : entries) {
            if (entry.getEntryType() == com.alibaba.otter.canal.protocol.CanalEntry.EntryType.ROWDATA) {
                RowChange rowChange = RowChange.parseFrom(entry.getStoreValue());
                System.out.println("binlog start position : " + entry.getHeader().getLogfileName() + ":" + entry.getHeader().getLogfileOffset());
                System.out.println("event type : " + rowChange.getEventType());

                rowChange.getRowDatasList().forEach(rowData -> {
                    switch (rowChange.getEventType()) {
                        case INSERT:
                            System.out.println("Insert : " + rowData.getAfterColumnsList());
                            break;
                        case UPDATE:
                            System.out.println("Update : Before : " + rowData.getBeforeColumnsList());
                            System.out.println("Update : After : " + rowData.getAfterColumnsList());
                            break;
                        case DELETE:
                            System.out.println("Delete : " + rowData.getBeforeColumnsList());
                            break;
                        default:
                            break;
                    }
                });
            }
        }
    }

这时候只要你去你监听的数据库上执行一些更改操作,上面的程序就会打印信息

binlog start position : mysql-bin.000001:4730
event type : INSERT
Insert : [index: 0
sqlType: 4
name: "id"
isKey: true
updated: true
isNull: false
value: "17"
mysqlType: "int(11)"
, index: 1
sqlType: 12
name: "name"
isKey: false
updated: true
isNull: false
value: "aA"
mysqlType: "varchar(10)"
, index: 2
sqlType: 4
name: "age"
isKey: false
updated: true
isNull: false
value: "24"
mysqlType: "int(11)"
]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值