在使用缓存时,不可避免地会遇到数据库同步问题,即当Mysql数据库中的数据更新之后,需要同步更新Redis缓存。一种做法是在更新数据库时删除Redis中的缓存。但是这种方式无疑会将业务逻辑复杂化,稍微不注意就有可能忘记更新缓存,导致数据不一致的问题。因此,另一种较为常见的做法是订阅Mysql的增加、修改和删除操作,当数据库数据发生变动时,自动监听数据的变化,更新缓存。Canal就是做这个事情的。
1. Canal工作原理
canal是应阿里存在杭州和美国的双机房部署,存在跨机房同步的业务需求而提出的。通过对数据库日志的分析,获取增量变更进行数据同步,实现MySQL数据库的增量订阅&消费的业务。快速通道
canal的工作原理:
- canal模拟mysql slave的交互协议,伪装自己为mysql slave,向mysql master发送dump协议;
- mysql master收到dump请求,开始推送binary log给slave(canal);
- canal解析binary log对象(原始的字节流)
2. Canal安装与配置
2.1 开启binlog模式
canal是基于mysql的主从模式实现的,所以必须先开启binlog。
这里记录一下在docker中安装vim的方法:
apt-get update
apt-get install vim
进入mysql容器,配置/etc/mysql/my.cnf
如下:
#binlog config
log-bin=/var/lib/mysql/mysql-bin
server-id=12097 #mysql数据库的唯一标识
2.2 创建测试账号
使用root用户创建用户账号并授予权限
#使用root账号登录
mysql -uroot -p
#输入密码
#创建用户账号canal,密码为canal
create user canal@'%' identified by 'canal';
grant select, replication slave, replication client, super on *.* to 'canal'@'%';
flush privileges;
#退出mysql和mysql容器
exit
#重启mysql容器
docker restart mysql
2.3 canal容器安装
#下载镜像
docker pull docker.io/canal/canal-server
#容器安装
docker run -p 11111:11111 --name canal -d docker.io/canal/canal-server
#登录canal
docker exec -it canal /bin/bash
#配置canal
vi canal.properties
#canal.id = 10039 #这里需要和之前配置的mysql容器的server-id不同
#配置canal实例,同步配置,具体见下图
vi example/instance.properties
#配置完成后退出并重启canal容器
docker restart canal
3. 搭建Canal微服务
此时,若用户执行数据库操作,binlog将会被canal捕获到,并解析到数据。我们可以将解析出来的数据进行同步到redis中。
下面,我们创建一个独立的程序,并监控canal服务器,获取binlog日志,解析数据,将数据更新到redis中,这样就能保证数据库和缓存中的数据保持一致了。
添加依赖:
<!-- canal依赖 -->
<dependency>
<groupId>com.xpand</groupId>
<artifactId>starter-canal</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
在application.yml
中添加配置:
#canal配置
canal:
client:
instances:
example:
host: 192.168.137.118
port: 11111
在Application类上添加注解@EnableCanalClient
监听代码如下:
@CanalEventListener
public class MyEventListener {
@InsertListenPoint
public void onEvent(CanalEntry.EventType eventType, CanalEntry.RowData rowData) {
//do something...
List<CanalEntry.Column> afterColumnsList = rowData.getAfterColumnsList();
for (CanalEntry.Column column : afterColumnsList) {
System.out.println(column.getName() + ":" + column.getValue());
}
}
@UpdateListenPoint
public void onEvent1(CanalEntry.RowData rowData) {
//do something...
}
@DeleteListenPoint
public void onEvent3(CanalEntry.EventType eventType) {
//do something...
}
@ListenPoint(destination = "example", schema = "canal-test", table = {"t_user", "test_table"}, eventType = CanalEntry.EventType.UPDATE)
public void onEvent4(CanalEntry.EventType eventType, CanalEntry.RowData rowData) {
//do something...
}
}
如图插入数据时,canal微服务可以监听到数据。