背景:
看到 八股里面 有一个如何保证 Redis和数据库的一致性 问题,
一开始说的是从先更新数据库后更新缓存,还是先更新缓存后更新数据库,
再决定应该是删除缓存而不是更新,
那么先更新数据库后删除缓存,还是先删除缓存后更新数据库 ?
对于这个问题,答案是先更新数据库再删除缓存,因为
数据库更新的耗时通常比缓存重建更长,因此后者发生的情况相对较少。
但这还是存在问题,
更新数据库和删除缓存毕竟是两个不同的操作,没办法保证同时成功,
假如更新数据库成功,删除缓存失败怎么办?
要保证 分布系统
的 一致性,可以使用消息队列 MQ
来保证 失败之后有重试的机会。
具体来说,
更新数据库之后,把 已经更新数据的消息 存入 消息队列,之后的 删除业务 从消息队列中取任务,
如果执行删除任务失败,则消息队列重试,重新推送消息给删除业务,直到最大次数maxTry。
如果成功,则删除消息,完成一致性保证。
但还是可以看到 ,
每次更新数据库之后,都需要 增加一个插入MQ消息的操作,
update(a,oldvalue,newvalue);
MQClient.sendMsg(msg);
这会大大增加代码的耦合度,因此
另一种方案就是使用Canal来保证一致性,进入正文。
Canal
组件是一个基于 MySQL
数据库增量日志解析,提供增量数据订阅和消费,将增量数据投递到下游消费者(如 Kafka
、RocketMQ
等)或者存储(如Elasticsearch
、HBase
等)的组件。
Canal
会把自己伪装成一个从节点,向Master
主节点发送dump
请求,Master
就会推送Binlog
给它,之后解析数据的变动,下发给下游任务,
从这里可以发现,我们不需要像之前那样,在更新数据库的代码里去显式地 插入一条删除任务
到消息队列,canal
可以在背后默默地实现这个操作,极大降低代码之间的耦合。
本文主要记录一下 如何实操canal监听 mysql数据库。
我选择的是 docker
进行部署,mysql
和canal
都是通过容器启动的。
- 拉取镜像
在docker
中添加mysql
镜像 和 canal
镜像 【我的docker
里面有两个版本的mysql
,
v8和v5的,而canal
是v1.1.5
,经过实践,v1.1.5
的cana
l最好还是配合v5版本的mysql
.】
docker pull mysql:5.7.36
docker pull canal/canal-server:v1.1.5
- 拉取镜像完成后,为了保证canal和mysql在同一个docker网络中可以通信,可以事先创建一个网络
docker network create net1
- 运行两个镜像,启动两个容器
#启动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
里启动了两个容器,mysql
和canal
.
为了确保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)"
]