Canal 详解 + Canal+Redis 缓存一致性完整方案

Canal 详解 + Canal+Redis 缓存一致性完整方案

本文会先全面介绍 Canal ,再给出工业级的 Canal+Redis 缓存一致性架构图 ,最后提供可直接运行的 SpringBoot 代码示例,彻底解决 MySQL 与 Redis 的缓存一致性问题。

一、Canal 核心介绍

1. 什么是 Canal?

Canal 是阿里巴巴开源的基于 MySQL Binlog 的增量数据订阅 & 消费组件 (Java 开发),核心能力是实时捕获 MySQL 的增删改数据变更,并将结构化的变更数据推送给下游系统(Redis、ES、Kafka 等)。

2. 核心工作原理

基于 MySQL 主从复制原理实现:

  1. MySQL Master 写入数据 → 生成 Binlog 日志(记录数据变更);
  2. Canal 伪装成 MySQL 的 Slave 节点,向 Master 发送 dump 协议;
  3. Canal 拉取 Binlog 并解析为结构化数据;
  4. 对外提供订阅接口,供客户端消费变更数据。

3. 核心优势

  • 无侵入:业务代码零修改,不耦合缓存逻辑
  • 实时性:毫秒级捕获数据变更
  • 可靠性:支持断点续传,数据不丢失
  • 统一管理:所有表的缓存同步集中处理

4. 核心适用场景

  • MySQL + Redis 缓存一致性
  • 异构数据同步(MySQL→ES/Redis/Hive)
  • 分库分表数据同步
  • 实时数仓、数据审计

二、为什么用 Canal+Redis 解决缓存一致性?

1. 传统缓存方案的缺陷

方案 问题
更新 DB → 更新 Redis 并发场景下会产生脏数据
更新 DB → 删除 Redis 删缓存失败会导致缓存永久不一致,业务耦合严重

2. Canal 方案的优势

基于 Binlog 监听实现最终一致性(工业界标准方案):

  1. 业务只操作 MySQL,缓存同步由 Canal 独立处理;
  2. 数据变更不丢失,删缓存失败可重试;
  3. 零业务侵入,统一维护缓存规则。

三、Canal + Redis 缓存一致性 完整架构

1. 架构组件分层

层级 组件 核心作用
业务层 SpringBoot 应用 读写 MySQL、查询 Redis
数据源层 MySQL 存储核心数据,开启 ROW 模式 Binlog
中间件层 Canal Server 拉取 + 解析 Binlog
消费层 Canal Client 订阅 Canal 数据,操作 Redis
缓存层 Redis 存储热点缓存

2. 完整数据流转流程

  1. 业务应用执行 INSERT/UPDATE/DELETE → MySQL

  2. MySQL 生成 ROW 模式 Binlog

  3. Canal Server 伪装 Slave,拉取并解析 Binlog

  4. Canal Client 订阅变更数据(表名、主键、变更类型)

  5. Canal Client 删除 Redis 对应缓存 Key

  6. 读请求:先查 Redis → 未命中则查 DB → 回写 Redis

缓存层
同步中间件
数据持久层
业务应用
缓存命中
缓存未命中
写入/更新/删除
生成ROW模式Binlog
解析推送变更数据
删除对应缓存Key
用户请求
业务Service
Redis缓存查询
直接返回数据
查询MySQL
回写Redis缓存
MySQL数据库
Canal Server
Canal Client
Redis

四、环境搭建前置步骤

1. MySQL 配置(必须开启 Binlog)

  1. 修改 MySQL 配置文件 my.cnf

    [mysqld]

    开启binlog

    log-bin=mysql-bin

    binlog模式:必须用ROW(获取具体数据)

    binlog-format=ROW

    服务id(唯一)

    server-id=1

    需要监听的数据库(可选)

    binlog-do-db=test_db

  2. 重启 MySQL,创建 Canal 专用账号并授权

    CREATE USER 'canal'@'%' IDENTIFIED BY 'canal123';
    GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON . TO 'canal'@'%';
    FLUSH PRIVILEGES;

2. Canal Server 部署配置

  1. 下载 Canal 1.1.6

  2. 修改配置:conf/example/instance.properties

    MySQL 地址

    canal.instance.master.address=127.0.0.1:3306

    MySQL 账号密码

    canal.instance.dbUsername=canal
    canal.instance.dbPassword=canal123

    监听的库表(正则匹配)

    canal.instance.filter.regex=test_db\..*

  3. 启动 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;
    }
}

六、测试验证

  1. 启动 Redis、MySQL、Canal Server

  2. 启动 SpringBoot 应用

  3. MySQL 中创建测试表:

    CREATE TABLE test_db.user (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(20),
    age INT
    );
    INSERT INTO user (name, age) VALUES ('张三', 20);

  4. 调用查询接口 → 缓存未命中,回写 Redis

  5. 手动修改 MySQL 数据 → Canal 自动删除 Redis 缓存

  6. 再次查询 → 缓存未命中,加载最新数据


总结

  1. Canal 核心:伪装 MySQL Slave,基于 Binlog 实时捕获数据变更;
  2. 架构核心:业务零侵入,DB 变更 → Canal 监听 → 删除 Redis 缓存 → 读请求回写最新数据;
  3. 一致性保证:最终一致性(工业界标准),无脏数据、无业务耦合;
  4. 代码可直接落地:适配 SpringBoot,支持批量消费、失败重试。
相关推荐
qq_392807952 小时前
Qt 注册 C++ 给 QML 调用的几种方式
数据库·c++·qt
程序员夏末2 小时前
【MySQL | 第二篇】 MVCC的底层实现(多版本并发控制)
数据库·sql·mysql
庞轩px2 小时前
线程池核心参数与拒绝策略深度解析
java·jvm·数据库
油丶酸萝卜别吃2 小时前
MySQL 事务机制深度解析:从 ACID 到底层实现
数据库·mysql
xcLeigh2 小时前
Oracle 迁移深度复盘:多数据库选型决策全解析
大数据·数据库·sql·oracle·数据迁移·数据管理
王仲肖2 小时前
PostgreSQL pageinspect 插件深度解析
数据库·postgresql
云边有个稻草人2 小时前
【MySQL】第十四节—事务:从基础概念到隔离性理论与实践 | 详解
数据库·mysql·事务·隔离级别·事务的隔离性·事务提交方式
干啥啥不行,秃头第一名2 小时前
Python深度学习入门:TensorFlow 2.0/Keras实战
jvm·数据库·python