SpringBoot整合Canal实现数据库实时同步

前言

在微服务架构盛行的今天,数据一致性成为了一个关键挑战。当业务数据在MySQL中发生变化时,如何实时同步到其他服务或缓存中?阿里巴巴开源的Canal组件为我们提供了完美的解决方案。今天,我将带你深入探索SpringBoot整合Canal的技术内幕,让你轻松掌握这一核心技术。

一、什么是Canal?

Canal是阿里巴巴开源的一个基于MySQL数据库增量日志解析的组件,它模拟MySQL主从复制的交互协议,伪装成MySQL的从节点,向MySQL主节点发送dump协议,获取到MySQL的二进制日志(binlog)后,再解析为便于理解和使用的数据格式。

1.1 Canal工作原理

复制代码
MySQL主库(master)
    ↓ (binlog日志)
Canal Server (模拟slave)
    ↓ (解析后的数据变更事件)
Canal Client/SpringBoot应用
    ↓ (业务处理)
下游系统(Redis/ES/MQ等)

Canal的核心原理就是:

  1. 伪装成MySQL从库:Canal模拟MySQL slave的交互协议
  2. 获取binlog:向MySQL master发送dump协议,获取binlog日志
  3. 解析数据变更:解析binlog中的INSERT、UPDATE、DELETE等事件
  4. 推送到下游:将解析后的数据变更推送给应用处理

二、环境准备

2.1 MySQL配置

首先,确保MySQL已开启binlog功能,并设置为ROW模式:

sql 复制代码
-- 查看binlog配置
SHOW VARIABLES LIKE 'log_bin';
SHOW VARIABLES LIKE 'binlog_format';

-- 如果未开启,需在my.cnf中添加以下配置
[mysqld]
# 开启binlog
log-bin=mysql-bin
# binlog格式必须为ROW
binlog-format=ROW
# 服务器ID,唯一标识
server-id=1
# binlog过期时间(天)
expire_logs_days=7
# 单个binlog文件大小
max_binlog_size=500M

2.2 创建Canal专用用户

创建Canal用户并授权:

sql 复制代码
-- 创建canal用户
CREATE USER 'canal'@'%' IDENTIFIED BY 'canal';

-- 授予权限
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';

-- 刷新权限
FLUSH PRIVILEGES;

注意 :必须授予REPLICATION SLAVEREPLICATION CLIENT权限,Canal才能读取binlog。

2.3 部署Canal Server

下载Canal Server

从GitHub下载最新稳定版(推荐1.1.7版本):

bash 复制代码
wget https://github.com/alibaba/canal/releases/download/canal-1.1.7/canal.deployer-1.1.7.tar.gz

# 解压
mkdir -p /opt/canal
tar -zxvf canal.deployer-1.1.7.tar.gz -C /opt/canal

配置Canal Server

修改conf/example/instance.properties配置文件:

properties 复制代码
# MySQL连接信息
canal.instance.master.address=127.0.0.1:3306
canal.instance.dbUsername=canal
canal.instance.dbPassword=canal
canal.instance.connectionCharset=UTF-8

# 监听的表(正则表达式)
# .*\\..* 表示监听所有库的所有表
# 可以指定为具体的表,如:test_db\\.user_info
canal.instance.filter.regex=.*\\..*

# 表黑名单
canal.instance.filter.black.regex=mysql\\.information_schema.*

# slaveId(需要唯一)
canal.instance.mysql.slaveId=1234

修改conf/canal.properties全局配置:

properties 复制代码
# Canal服务端口
canal.port=11111
canal.metrics.pull.port=11112

# 实例列表
canal.destinations=example

# 管理端口
canal.admin.manager.port=8089

启动Canal Server

bash 复制代码
# 启动
sh bin/startup.sh

# 查看日志
tail -f logs/canal/canal.log
tail -f logs/example/example.log

# 停止
sh bin/stop.sh

启动成功后,你会看到类似以下日志:

复制代码
2026-02-02 17:00:00.123 [main] INFO  com.alibaba.otter.canal.instance.core.CanalInstanceWithManager - [example] init successful

三、SpringBoot集成Canal

3.1 创建SpringBoot项目

创建一个新的SpringBoot项目,添加必要的依赖:

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.18</version>
        <relativePath/>
    </parent>

    <groupId>com.imooc</groupId>
    <artifactId>canal-sync-demo</artifactId>
    <version>1.0.0</version>

    <properties>
        <java.version>8</java.version>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <!-- Spring Boot Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- Canal Spring Boot Starter -->
        <dependency>
            <groupId>top.javatool</groupId>
            <artifactId>canal-spring-boot-starter</artifactId>
            <version>1.2.1-RELEASE</version>
        </dependency>

        <!-- JPA注解支持 -->
        <dependency>
            <groupId>javax.persistence</groupId>
            <artifactId>persistence-api</artifactId>
            <version>1.0</version>
        </dependency>

        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- Redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- MyBatis Plus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.5</version>
        </dependency>

        <!-- MySQL驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!-- fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.83</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

3.2 配置文件

创建application.yml配置文件:

yaml 复制代码
server:
  port: 8080

spring:
  application:
    name: canal-sync-demo

  # MySQL配置
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/test_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver

  # Redis配置
  redis:
    host: 127.0.0.1
    port: 6379
    password:
    database: 0

# Canal配置
canal:
  # Canal Server地址
  server: 127.0.0.1:11111
  # 实例名称(对应Canal Server配置的destination)
  destination: example
  # Canal Server用户名和密码(如果Canal Server配置了认证)
  user-name: canal
  password: canal

# 日志配置
logging:
  level:
    top.javatool.canal.client: warn

注意top.javatool.canal.client的日志级别设置为warn,避免Canal客户端的日志过多。

3.3 创建实体类

假设我们有一个数据字典表data_dictionary,对应的实体类如下:

java 复制代码
package com.xxx.pojo;

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.Column;
import javax.persistence.Id;

/**
 * 数据字典实体类
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("data_dictionary")
public class DataDictionary {

    /**
     * 主键ID
     */
    @Id
    @TableId
    @Column(name = "id")
    private String id;

    /**
     * 类型编码
     */
    @Column(name = "type_code")
    private String typeCode;

    /**
     * 类型名称
     */
    @Column(name = "type_name")
    private String typeName;

    /**
     * 字典项键
     */
    @Column(name = "item_key")
    private String itemKey;

    /**
     * 字典项值
     */
    @Column(name = "item_value")
    private String itemValue;

    /**
     * 排序
     */
    @Column(name = "sort")
    private Integer sort;

    /**
     * 图标
     */
    @Column(name = "icon")
    private String icon;

    /**
     * 是否启用
     */
    @Column(name = "enable")
    private Boolean enable;
}

关键点说明

  • @TableName("data_dictionary"):指定对应的数据库表名
  • @Id@Column:这些注解来自JPA,Canal会根据这些注解将数据库字段映射到实体类属性
  • 类名、属性名可以与表名、字段名不同,通过注解映射

3.4 创建Canal监听处理器

这是最核心的部分!我们需要创建一个类来实现EntryHandler<T>接口:

java 复制代码
package com.xxx.canal;

import com.alibaba.fastjson.JSON;
import com.xxx.pojo.DataDictionary;
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;

import javax.annotation.Resource;

/**
 * 数据字典同步处理器
 * 监听data_dictionary表的数据变更,实时同步到Redis
 */
@Slf4j
@Component
@CanalTable("data_dictionary")  // 指定监听的表名
public class DataDictSyncHandler implements EntryHandler<DataDictionary> {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 监听新增操作
     * 当MySQL中执行INSERT时,此方法会被回调
     *
     * @param dataDictionary 新增的数据
     */
    @Override
    public void insert(DataDictionary dataDictionary) {
        log.info("=== Canal监听到新增操作 ===");
        log.info("数据内容:{}", JSON.toJSONString(dataDictionary));

        // 业务逻辑:将数据同步到Redis
        syncToRedis(dataDictionary);

        log.info("新增操作处理完成");
    }

    /**
     * 监听更新操作
     * 当MySQL中执行UPDATE时,此方法会被回调
     *
     * @param before 更新前的数据(老数据)
     * @param after  更新后的数据(新数据)
     */
    @Override
    public void update(DataDictionary before, DataDictionary after) {
        log.info("=== Canal监听到更新操作 ===");
        log.info("更新前数据:{}", JSON.toJSONString(before));
        log.info("更新后数据:{}", JSON.toJSONString(after));

        // 业务逻辑:删除旧缓存,写入新缓存
        deleteFromRedis(before.getId());
        syncToRedis(after);

        log.info("更新操作处理完成");
    }

    /**
     * 监听删除操作
     * 当MySQL中执行DELETE时,此方法会被回调
     *
     * @param dataDictionary 被删除的数据
     */
    @Override
    public void delete(DataDictionary dataDictionary) {
        log.info("=== Canal监听到删除操作 ===");
        log.info("被删除数据:{}", JSON.toJSONString(dataDictionary));

        // 业务逻辑:从Redis中删除对应数据
        deleteFromRedis(dataDictionary.getId());

        log.info("删除操作处理完成");
    }

    /**
     * 将数据同步到Redis
     *
     * @param dataDictionary 数据字典对象
     */
    private void syncToRedis(DataDictionary dataDictionary) {
        try {
            String key = "data_dict:" + dataDictionary.getId();
            redisTemplate.opsForValue().set(key, dataDictionary);
            log.info("数据已同步到Redis,key:{}", key);
        } catch (Exception e) {
            log.error("同步数据到Redis失败:", e);
        }
    }

    /**
     * 从Redis中删除数据
     *
     * @param id 数据ID
     */
    private void deleteFromRedis(String id) {
        try {
            String key = "data_dict:" + id;
            redisTemplate.delete(key);
            log.info("已从Redis删除数据,key:{}", key);
        } catch (Exception e) {
            log.error("从Redis删除数据失败:", e);
        }
    }
}

核心要点

  1. @CanalTable("data_dictionary"):指定要监听的数据库表名
  2. implements EntryHandler<DataDictionary>:泛型指定要处理的实体类型
  3. 三个核心方法
    • insert(T t):监听INSERT操作
    • update(T before, T after):监听UPDATE操作,可以同时拿到更新前后的数据
    • delete(T t):监听DELETE操作
  4. 自动回调机制:Canal会根据数据库变更类型自动调用对应的方法,并将数据封装成实体对象传入

3.5 启动类

创建SpringBoot启动类:

java 复制代码
package com.xxx;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * Canal同步演示启动类
 */
@SpringBootApplication
public class CanalSyncApplication {

    public static void main(String[] args) {
        SpringApplication.run(CanalSyncApplication.class, args);
    }
}

四、测试验证

4.1 准备测试数据

在MySQL中创建测试表并插入数据:

sql 复制代码
-- 创建数据库
CREATE DATABASE IF NOT EXISTS test_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

USE test_db;

-- 创建数据字典表
CREATE TABLE IF NOT EXISTS `data_dictionary` (
  `id` varchar(64) NOT NULL COMMENT '主键ID',
  `type_code` varchar(50) NOT NULL COMMENT '类型编码',
  `type_name` varchar(100) DEFAULT NULL COMMENT '类型名称',
  `item_key` varchar(50) DEFAULT NULL COMMENT '字典项键',
  `item_value` varchar(200) DEFAULT NULL COMMENT '字典项值',
  `sort` int(11) DEFAULT '0' COMMENT '排序',
  `icon` varchar(100) DEFAULT NULL COMMENT '图标',
  `enable` tinyint(1) DEFAULT '1' COMMENT '是否启用',
  PRIMARY KEY (`id`),
  KEY `idx_type_code` (`type_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据字典表';

-- 插入测试数据
INSERT INTO `data_dictionary`
VALUES ('1', 'gender', '性别', 'male', '男', 1, 'icon-male', 1),
       ('2', 'gender', '性别', 'female', '女', 2, 'icon-female', 1);

4.2 启动应用

  1. 确保Canal Server已启动
  2. 启动SpringBoot应用

4.3 测试数据同步

测试新增

在MySQL中执行INSERT:

sql 复制代码
INSERT INTO `data_dictionary`
VALUES ('3', 'status', '状态', 'active', '激活', 1, NULL, 1);

观察应用日志:

复制代码
=== Canal监听到新增操作 ===
数据内容:{"id":"3","typeCode":"status","typeName":"状态","itemKey":"active","itemValue":"激活","sort":1,"icon":null,"enable":true}
数据已同步到Redis,key:data_dict:3
新增操作处理完成

检查Redis中是否有对应数据:

bash 复制代码
redis-cli
127.0.0.1:6379> GET data_dict:3
{"id":"3","typeCode":"status","typeName":"状态","itemKey":"active","itemValue":"激活","sort":1,"icon":null,"enable":true}

测试更新

在MySQL中执行UPDATE:

sql 复制代码
UPDATE `data_dictionary`
SET `item_value`='启用中', `sort`=2
WHERE `id`='3';

观察应用日志:

复制代码
=== Canal监听到更新操作 ===
更新前数据:{"id":"3","typeCode":"status","typeName":"状态","itemKey":"active","itemValue":"激活","sort":1,"icon":null,"enable":true}
更新后数据:{"id":"3","typeCode":"status","typeName":"状态","itemKey":"active","itemValue":"启用中","sort":2,"icon":null,"enable":true}
已从Redis删除数据,key:data_dict:3
数据已同步到Redis,key:data_dict:3
更新操作处理完成

测试删除

在MySQL中执行DELETE:

sql 复制代码
DELETE FROM `data_dictionary` WHERE `id`='3';

观察应用日志:

复制代码
=== Canal监听到删除操作 ===
被删除数据:{"id":"3","typeCode":"status","typeName":"状态","itemKey":"active","itemValue":"启用中","sort":2,"icon":null,"enable":true}
已从Redis删除数据,key:data_dict:3
删除操作处理完成

检查Redis中数据是否已被删除:

bash 复制代码
127.0.0.1:6379> GET data_dict:3
(nil)

五、高级应用场景

5.1 多表监听

如果需要监听多张表,可以创建多个Handler:

java 复制代码
// 用户表监听器
@CanalTable("t_user")
@Component
public class UserHandler implements EntryHandler<User> {
    @Override
    public void insert(User user) {
        // 处理用户新增
    }

    @Override
    public void update(User before, User after) {
        // 处理用户更新
    }

    @Override
    public void delete(User user) {
        // 处理用户删除
    }
}

// 订单表监听器
@CanalTable("t_order")
@Component
public class OrderHandler implements EntryHandler<Order> {
    @Override
    public void insert(Order order) {
        // 处理订单新增
    }

    @Override
    public void update(Order before, Order after) {
        // 处理订单更新
    }

    @Override
    public void delete(Order order) {
        // 处理订单删除
    }
}

5.2 同步到Elasticsearch

java 复制代码
@CanalTable("t_product")
@Component
@Slf4j
public class ProductEsHandler implements EntryHandler<Product> {

    @Autowired
    private ElasticsearchRestTemplate esTemplate;

    @Override
    public void insert(Product product) {
        // 同步到ES
        esTemplate.save(product);
    }

    @Override
    public void update(Product before, Product after) {
        // 更新ES文档
        esTemplate.save(after);
    }

    @Override
    public void delete(Product product) {
        // 从ES删除文档
        esTemplate.delete(product.getId(), "product");
    }
}

5.3 发送消息到MQ

java 复制代码
@CanalTable("t_order")
@Component
@Slf4j
public class OrderMqHandler implements EntryHandler<Order> {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Override
    public void insert(Order order) {
        // 发送订单创建消息
        rabbitTemplate.convertAndSend("order.exchange", "order.created", order);
    }

    @Override
    public void update(Order before, Order after) {
        // 判断订单状态变化,发送相应消息
        if (!before.getStatus().equals(after.getStatus())) {
            rabbitTemplate.convertAndSend("order.exchange", "order.status.changed", after);
        }
    }

    @Override
    public void delete(Order order) {
        // 发送订单删除消息
        rabbitTemplate.convertAndSend("order.exchange", "order.deleted", order);
    }
}

六、生产环境优化建议

6.1 性能优化

  1. 批量处理 :Canal支持批量拉取数据,可以在配置中设置batch-size
yaml 复制代码
canal:
  batch-size: 1000  # 每次拉取1000条
  1. 异步处理:在Handler中使用异步方式处理业务逻辑,避免阻塞Canal消费线程
java 复制代码
@Async
@Override
public void insert(DataDictionary dataDictionary) {
    // 异步处理
}
  1. 精简监听:只监听必要的表,避免监听所有库表
properties 复制代码
# 在Canal Server配置中精简监听表
canal.instance.filter.regex=test_db\\.data_dictionary,test_db\\.t_user

6.2 高可用方案

  1. Canal Server集群:部署多个Canal Server实例,通过Zookeeper进行协调

  2. 主从切换:MySQL主从切换时,Canal需要自动切换到新的master

  3. 消费位点管理:确保消费位点正常提交,避免重复消费或数据丢失

6.3 监控告警

  1. 监控Canal延迟:定期检查Canal消费延迟,确保实时性

  2. 监控Handler执行时间:记录Handler执行耗时,及时发现性能问题

  3. 异常告警:当同步出现异常时,及时发送告警通知

七、常见问题与解决方案

7.1 监听不到数据变更

可能原因

  1. MySQL的binlog未开启或格式不是ROW
  2. Canal用户权限不足
  3. canal.instance.filter.regex配置错误
  4. 实体类的@Column注解配置不正确

解决方案

sql 复制代码
-- 检查binlog
SHOW VARIABLES LIKE 'log_bin';
SHOW VARIABLES LIKE 'binlog_format';

-- 检查用户权限
SHOW GRANTS FOR 'canal'@'%';

7.2 字段映射失败

问题描述:某些字段无法映射到实体类

解决方案

  1. 确保@Column注解的name属性与数据库字段名一致
  2. 对于下划线转驼峰的情况,Canal会自动转换,但建议显式指定
java 复制代码
@Column(name = "type_code")  // 显式指定字段名
private String typeCode;

7.3 数据同步延迟

可能原因

  1. Handler中的业务逻辑执行时间过长
  2. Canal消费线程阻塞
  3. 网络延迟

解决方案

  1. 将耗时的业务逻辑异步化
  2. 增加Canal消费线程数
  3. 优化网络环境

7.4 重复消费数据

问题描述:重启应用后,部分数据被重复处理

解决方案

  1. 确保业务逻辑具备幂等性
  2. 在Redis或其他存储中记录已处理的ID
  3. 使用数据库唯一索引防止重复插入
java 复制代码
@Override
public void insert(DataDictionary dataDictionary) {
    // 幂等性校验
    String key = "processed:" + dataDictionary.getId();
    if (Boolean.TRUE.equals(redisTemplate.hasKey(key))) {
        log.info("数据已处理过,跳过:{}", dataDictionary.getId());
        return;
    }
    
    // 处理业务逻辑
    syncToRedis(dataDictionary);
    
    // 标记已处理(设置过期时间)
    redisTemplate.opsForValue().set(key, "1", 24, TimeUnit.HOURS);
}

八、总结

通过本文的详细介绍,相信你已经掌握了SpringBoot整合Canal的核心技术。让我们总结一下关键要点:

核心优势

  1. 零侵入:不需要修改业务代码,基于binlog监听
  2. 实时性强:基于binlog解析,延迟可控制在毫秒级
  3. 易于集成 :使用canal-spring-boot-starter,几行注解即可实现
  4. 应用场景广:缓存同步、搜索索引、数据归档等

最佳实践

  1. 合理配置binlog格式为ROW
  2. 为Canal创建专用用户并授权
  3. 实体类使用@Id@Column注解精确映射
  4. Handler中的业务逻辑要保持轻量,耗时操作异步化
  5. 注意幂等性处理,避免数据重复
  6. 监控Canal消费延迟和Handler执行时间

适用场景

  • 缓存实时更新(Redis、Memcached)
  • 搜索引擎数据同步(Elasticsearch)
  • 数据归档与备份
  • 多数据源同步
  • 业务解耦(通过MQ发送变更事件)
相关推荐
yinyan131418 小时前
一起学springAI系列一:使用多种聊天模型
java·人工智能·spring boot·后端·spring·springai
de_wizard18 小时前
Spring Boot 项目开发流程全解析
java·spring boot·log4j
dreamxian18 小时前
苍穹外卖day07
java·spring boot·后端·spring·mybatis
流水武qin18 小时前
SpringAI 使用 RAG
java·spring boot·spring·ai
Fang fan18 小时前
Redis基础数据结构
数据结构·数据库·redis·缓存·bootstrap·sentinel
星空露珠18 小时前
迷你世界UGC3.0脚本Wiki对象模块管理接口 GameObject
开发语言·数据库·算法·游戏·lua
或与且与或非18 小时前
Redis-Lua脚本调试教程
数据库·redis·lua
陈随易18 小时前
MoonBit访谈:MoonBit开发moonclaw实现“养虾”自由
前端·后端·程序员
码界奇点18 小时前
基于Java GUI和Access数据库的图书馆管理系统设计与实现
java·开发语言·数据库·毕业设计·源代码管理
kiku181818 小时前
Mysql数据库
数据库·mysql·oracle