Canal 详解 + Canal+Redis 缓存一致性完整方案
本文会先全面介绍 Canal ,再给出工业级的 Canal+Redis 缓存一致性架构图 ,最后提供可直接运行的 SpringBoot 代码示例,彻底解决 MySQL 与 Redis 的缓存一致性问题。
一、Canal 核心介绍
1. 什么是 Canal?
Canal 是阿里巴巴开源的基于 MySQL Binlog 的增量数据订阅 & 消费组件 (Java 开发),核心能力是实时捕获 MySQL 的增删改数据变更,并将结构化的变更数据推送给下游系统(Redis、ES、Kafka 等)。
2. 核心工作原理
基于 MySQL 主从复制原理实现:
- MySQL Master 写入数据 → 生成 Binlog 日志(记录数据变更);
- Canal 伪装成 MySQL 的 Slave 节点,向 Master 发送 dump 协议;
- Canal 拉取 Binlog 并解析为结构化数据;
- 对外提供订阅接口,供客户端消费变更数据。
3. 核心优势
- ✅ 无侵入:业务代码零修改,不耦合缓存逻辑
- ✅ 实时性:毫秒级捕获数据变更
- ✅ 可靠性:支持断点续传,数据不丢失
- ✅ 统一管理:所有表的缓存同步集中处理
4. 核心适用场景
- MySQL + Redis 缓存一致性
- 异构数据同步(MySQL→ES/Redis/Hive)
- 分库分表数据同步
- 实时数仓、数据审计
二、为什么用 Canal+Redis 解决缓存一致性?
1. 传统缓存方案的缺陷
| 方案 | 问题 |
|---|---|
| 更新 DB → 更新 Redis | 并发场景下会产生脏数据 |
| 更新 DB → 删除 Redis | 删缓存失败会导致缓存永久不一致,业务耦合严重 |
2. Canal 方案的优势
基于 Binlog 监听实现最终一致性(工业界标准方案):
- 业务只操作 MySQL,缓存同步由 Canal 独立处理;
- 数据变更不丢失,删缓存失败可重试;
- 零业务侵入,统一维护缓存规则。
三、Canal + Redis 缓存一致性 完整架构
1. 架构组件分层
| 层级 | 组件 | 核心作用 |
|---|---|---|
| 业务层 | SpringBoot 应用 | 读写 MySQL、查询 Redis |
| 数据源层 | MySQL | 存储核心数据,开启 ROW 模式 Binlog |
| 中间件层 | Canal Server | 拉取 + 解析 Binlog |
| 消费层 | Canal Client | 订阅 Canal 数据,操作 Redis |
| 缓存层 | Redis | 存储热点缓存 |
2. 完整数据流转流程
-
业务应用执行
INSERT/UPDATE/DELETE→ MySQL -
MySQL 生成 ROW 模式 Binlog
-
Canal Server 伪装 Slave,拉取并解析 Binlog
-
Canal Client 订阅变更数据(表名、主键、变更类型)
-
Canal Client 删除 Redis 对应缓存 Key
-
读请求:先查 Redis → 未命中则查 DB → 回写 Redis
缓存层
同步中间件
数据持久层
业务应用
缓存命中
缓存未命中
写入/更新/删除
生成ROW模式Binlog
解析推送变更数据
删除对应缓存Key
用户请求
业务Service
Redis缓存查询
直接返回数据
查询MySQL
回写Redis缓存
MySQL数据库
Canal Server
Canal Client
Redis
四、环境搭建前置步骤
1. MySQL 配置(必须开启 Binlog)
-
修改 MySQL 配置文件
my.cnf[mysqld]
开启binlog
log-bin=mysql-bin
binlog模式:必须用ROW(获取具体数据)
binlog-format=ROW
服务id(唯一)
server-id=1
需要监听的数据库(可选)
binlog-do-db=test_db
-
重启 MySQL,创建 Canal 专用账号并授权
CREATE USER 'canal'@'%' IDENTIFIED BY 'canal123';
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON . TO 'canal'@'%';
FLUSH PRIVILEGES;
2. Canal Server 部署配置
-
下载 Canal 1.1.6
-
修改配置:
conf/example/instance.propertiesMySQL 地址
canal.instance.master.address=127.0.0.1:3306
MySQL 账号密码
canal.instance.dbUsername=canal
canal.instance.dbPassword=canal123监听的库表(正则匹配)
canal.instance.filter.regex=test_db\..*
-
启动 Canal Server:
sh bin/startup.sh
五、完整代码示例(SpringBoot)
1. 项目依赖(pom.xml)
xml
<dependencies>
<!-- SpringBoot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Canal Client -->
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.client</artifactId>
<version>1.1.6</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
2. 配置文件(application.yml)
yaml
spring:
redis:
host: 127.0.0.1
port: 6379
password: # 无密码留空
database: 0
# Canal配置
canal:
server:
ip: 127.0.0.1
port: 11111
destination: example # 默认实例名
batchSize: 1000 # 批量拉取数量
3. Redis 配置类
java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// Key序列化
template.setKeySerializer(new StringRedisSerializer());
// Value序列化
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
4. Canal 客户端核心配置
java
import com.alibaba.otter.canal.client.CanalConnector;
import com.alibaba.otter.canal.client.CanalConnectors;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.net.InetSocketAddress;
@Configuration
public class CanalClientConfig {
@Value("${canal.server.ip}")
private String canalIp;
@Value("${canal.server.port}")
private int canalPort;
@Value("${canal.server.destination}")
private String destination;
/**
* 创建Canal连接(连接Canal Server)
*/
@Bean
public CanalConnector canalConnector() {
CanalConnector connector = CanalConnectors.newSingleConnector(
new InetSocketAddress(canalIp, canalPort),
destination,
"", // 用户名(默认空)
"" // 密码(默认空)
);
// 建立连接
connector.connect();
// 订阅所有表
connector.subscribe(".*\\..*");
// 回滚到未消费的位置
connector.rollback();
return connector;
}
}
5. Binlog 监听核心类(同步 Redis)
核心逻辑:监听 MySQL 变更 → 根据表名 + 主键生成 Redis Key → 删除缓存
java
import com.alibaba.otter.canal.client.CanalConnector;
import com.alibaba.otter.canal.protocol.CanalEntry;
import com.alibaba.otter.canal.protocol.Message;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.List;
@Component
@RequiredArgsConstructor
@EnableAsync
public class CanalBinlogListener {
private final CanalConnector canalConnector;
private final RedisTemplate<String, Object> redisTemplate;
private static final int BATCH_SIZE = 1000;
/**
* 启动时开启异步监听Binlog
*/
@PostConstruct
public void startListen() {
asyncListenBinlog();
}
@Async
public void asyncListenBinlog() {
while (true) {
try {
// 拉取消息(超时时间:5s)
Message message = canalConnector.getWithoutAck(BATCH_SIZE);
long batchId = message.getId();
int size = message.getEntries().size();
if (batchId == -1 || size == 0) {
Thread.sleep(1000);
continue;
}
// 处理变更数据
handleEntries(message.getEntries());
// 确认消费成功
canalConnector.ack(batchId);
} catch (Exception e) {
e.printStackTrace();
// 消费失败,回滚重新消费
canalConnector.rollback();
}
}
}
/**
* 处理Binlog变更数据
*/
private void handleEntries(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());
String tableName = entry.getHeader().getTableName(); // 表名
CanalEntry.EventType eventType = rowChange.getEventType(); // 变更类型
// 只处理 增/删/改 事件
if (eventType == CanalEntry.EventType.INSERT ||
eventType == CanalEntry.EventType.UPDATE ||
eventType == CanalEntry.EventType.DELETE) {
// 遍历变更的行数据
for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {
// 获取主键ID(默认第一个列为主键)
String id = getPrimaryKey(rowData.getAfterColumnsList() != null && !rowData.getAfterColumnsList().isEmpty()
? rowData.getAfterColumnsList()
: rowData.getBeforeColumnsList());
// 生成Redis Key:表名:主键(如 user:1)
String redisKey = tableName + ":" + id;
// 删除Redis缓存(核心:保证缓存一致性)
redisTemplate.delete(redisKey);
System.out.println("删除缓存成功,Key:" + redisKey);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 获取主键(默认第一列为主键)
*/
private String getPrimaryKey(List<CanalEntry.Column> columns) {
for (CanalEntry.Column column : columns) {
if (column.getIsKey()) {
return column.getValue();
}
}
return columns.get(0).getValue();
}
}
6. 业务层代码(读写缓存)
java
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class UserService {
private final RedisTemplate<String, Object> redisTemplate;
private final JdbcTemplate jdbcTemplate;
/**
* 查询用户(缓存流程:查Redis→未命中查DB→回写Redis)
*/
public Object getUserById(Long id) {
String redisKey = "user:" + id;
// 1. 查Redis
Object user = redisTemplate.opsForValue().get(redisKey);
if (user != null) {
System.out.println("缓存命中:" + redisKey);
return user;
}
// 2. 查MySQL
user = jdbcTemplate.queryForMap("SELECT * FROM user WHERE id = ?", id);
System.out.println("缓存未命中,查询DB:" + redisKey);
// 3. 回写Redis(设置过期时间)
redisTemplate.opsForValue().set(redisKey, user, 30, java.util.concurrent.TimeUnit.MINUTES);
return user;
}
}
六、测试验证
-
启动 Redis、MySQL、Canal Server
-
启动 SpringBoot 应用
-
MySQL 中创建测试表:
CREATE TABLE test_db.user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20),
age INT
);
INSERT INTO user (name, age) VALUES ('张三', 20); -
调用查询接口 → 缓存未命中,回写 Redis
-
手动修改 MySQL 数据 → Canal 自动删除 Redis 缓存
-
再次查询 → 缓存未命中,加载最新数据
总结
- Canal 核心:伪装 MySQL Slave,基于 Binlog 实时捕获数据变更;
- 架构核心:业务零侵入,DB 变更 → Canal 监听 → 删除 Redis 缓存 → 读请求回写最新数据;
- 一致性保证:最终一致性(工业界标准),无脏数据、无业务耦合;
- 代码可直接落地:适配 SpringBoot,支持批量消费、失败重试。