一、项目里的数据同步难题
在开发项目时,经常会遇到这样的问题:
问题 1:专辑数据不同步
我们把专辑信息存在数据库(MySQL)里,同时为了加快查询速度,还会把这些信息复制一份存在缓存(Redis)里。但是当专辑信息在数据库里修改后,缓存里的数据却不会自动更新,这样用户查到的可能就是过时的数据。
问题 2:用户信息不一致
用户登录后,系统会把用户信息缓存起来。如果用户后来修改了自己的资料,缓存里的信息还是老样子,导致显示的用户信息不准确。
二、解决数据同步问题的方法
解决这些问题有几种常见办法:
-
Redis 延时双删策略:先删除缓存,再更新数据库,等一会儿后再删一次缓存。但这种方法不能保证数据实时一致,而且如果读写操作同时进行,还是可能出现数据不一致的情况。
-
消息队列同步:把数据库里的数据变化打包成消息,通过消息队列(比如 Kafka、RabbitMQ)发送出去,再由另一个程序接收消息并更新缓存。虽然能实现异步更新,但消息队列本身也会有延迟,而且可能出现消息丢失的问题。
-
Canal 同步:Canal 就像一个 "数据监控员",它能实时监控数据库的变化,一旦发现数据有增删改,就马上把变化同步到缓存里。相比前两种方法,Canal 能更及时、可靠地保证数据一致,所以我们选择用它来解决问题。
三、Canal 是什么?
Canal 是阿里巴巴开发的一款工具,专门用来监控 MySQL 数据库的变化。它的名字原意是 "水道、沟渠",就像水渠把水从一个地方引到另一个地方一样,Canal 把数据库里的数据变化引到需要更新的地方。
Canal 的特点
- 速度快:比其他同步方法效率更高
- 灵活:支持多种数据格式,可以通过多种方式同步数据
- 功能强:能精确监控指定的表,支持在单机或集群环境下使用,还可以通过插件扩展功能
Canal 的应用场景
- 制作数据库副本
- 实时备份数据库
- 更新搜索索引
- 刷新业务缓存
四、Canal 是怎么工作的?
Canal 的工作原理基于 MySQL 的主从复制机制:
-
MySQL 主从复制:
- MySQL 主库会把数据变化记录在二进制日志(binlog)里
- 从库会把主库的日志复制过来,然后根据日志更新自己的数据
-
Canal 的工作方式:
- Canal 假装自己是 MySQL 的从库,向主库发送请求
- 主库把 binlog 发给 Canal
- Canal 解析 binlog,获取数据变化信息
- 然后把这些变化同步到需要更新的地方,比如 Redis 缓存
Canal 的结构
Canal 由服务端(canal server)和客户端(canal client)组成:
- 服务端:负责连接 MySQL,获取 binlog 并解析
- 客户端:从服务端获取数据变化信息,然后进行处理
服务端里面又包含几个重要部分:
- eventParser:负责和 MySQL 通信,获取 binlog
- eventSink:对获取到的数据进行过滤、加工
- eventStore:临时存储数据
- metaManager:管理数据订阅和消费的信息
五、如何安装 Canal
安装 Canal 之前,需要先安装好 MySQL,并且要开启 binlog 功能。这里以 MySQL 8.0.29 和 Canal 1.1.4 版本为例:
1. 配置 MySQL
-
检查 MySQL 是否开启 binlog:
SHOW VARIABLES LIKE '%log_bin%';
如果
log_bin
的值是OFF
,需要手动开启。 -
开启 binlog:
编辑 MySQL 配置文件/etc/mysql/mysql.conf.d/mysqld.cnf
,在[mysqld]
下面添加:log-bin=mysql-bin binlog-format=ROW server_id=1
-
创建 Canal 用户并授权:
create user canal@'%' IDENTIFIED by 'canal'; GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT,SUPER ON *.* TO 'canal'@'%'; FLUSH PRIVILEGES;
-
重启 MySQL。
2. 安装 Canal
-
下载 Canal 镜像:
docker pull canal/canal-server:v1.1.4
-
创建并启动 Canal 容器:
docker run -p 11111:11111 --name canal -id canal/canal-server:v1.1.4
-
配置 Canal:
- 创建目录:
mkdir -p /home/canal/conf
- 拷贝配置文件:
docker cp canal:/home/admin/canal-server/conf/canal.properties /mydata/canal/conf docker cp canal:/home/admin/canal-server/conf/test/instance.properties /mydata/canal/conf
- 修改
instance.properties
文件,配置 MySQL 连接信息等。
- 创建目录:
-
重新创建容器,应用配置修改。
-
查看 Canal 启动日志,确认是否启动成功。
六、在 Spring Boot 项目中使用 Canal
1. 创建项目
新建一个 Spring Boot 项目,添加以下依赖:
<dependencies>
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.client</artifactId>
<version>1.1.2</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.5.2</version>
</dependency>
<dependency>
<groupId>top.javatool</groupId>
<artifactId>canal-spring-boot-starter</artifactId>
<version>1.2.1-RELEASE</version>
</dependency>
</dependencies>
2. 编写代码
- 配置文件:在
application.yml
里配置 Canal 和 Redis 连接信息,比如:
canal:
destination: example
server: 192.168.254.156:11111
spring:
redis:
host: 192.168.254.156
port: 6379
- 编写处理器:创建一个类,实现
EntryHandler
接口,处理数据变化事件。比如处理专辑信息同步:
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import top.javatool.canal.client.annotation.CanalTable;
import top.javatool.canal.client.handler.EntryHandler;
@Slf4j
@Component
@CanalTable("album_info")
public class AlbumInfoCdcHandler implements EntryHandler<GenericEntity> {
@Autowired
private RedisTemplate redisTemplate;
@Override
public void insert(GenericEntity t) {
log.info("监听到数据添加,ID:{}", t.getId());
String key = "album:info:" + t.getId();
redisTemplate.delete(key);
}
@Override
public void update(GenericEntity before, GenericEntity after) {
log.info("监听到数据修改,ID:{}", after.getId());
String key = "album:info:" + after.getId();
redisTemplate.delete(key);
}
@Override
public void delete(GenericEntity t) {
log.info("监听到数据删除,ID:{}", t.getId());
String key = "album:info:" + t.getId();
redisTemplate.delete(key);
}
}
class GenericEntity {
private Long id;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}