Canal简介及项目集成(8.24)

一、什么是Canal

​ Canal 是阿里巴巴开源的、基于 MySQL binlog 增量日志解析的中间件,能订阅并消费数据库的增量变更数据。

二、使用场景

场景 一句话说明
数据库同步 作为阿里 OTTER 中间件的核心组件,实现异地数据库同步
缓存更新 监控 MySQL 变更,异步同步到缓存,保证最终一致性
拉链表制作 抽取每日新增和变化的数据,为拉链表提供增量数据源
实时统计 实时获取业务表的新增变化数据,支撑实时报表与统计

三、工作原理

image-20260824190223668

1、MySQL主从复制原理

MySQL主从复制主要依赖三个线程协作完成:

主库(Master): 当主库发生增删改操作时,会将数据变更记录写入binlog(二进制日志)。

从库(Slave): 从库启动两个线程来完成同步:

  • I/O线程: 向主库发送请求,拉取binlog中新增的增量部分,写入本地的relay log(中继日志)。
  • SQL线程: 读取relay log中的变更事件,逐条重放执行,从而实现与主库的数据同步。

image-20260824191356694

2、Canal的工作原理

Canal的核心思路就是伪装成一个MySQL从库:

  • 模拟从库的交互协议,向主库发送Dump请求。
  • 主库收到请求后,会像对待真正的从库一样,将binlog实时推送给Canal。
  • Canal解析binlog(原始byte流),提取出每一条增删改的变更数据,然后通知给下游消费方。

关键区别: 真正的从库拿到binlog后会写入relay log并重放SQL来同步数据;而Canal拿到binlog后不执行任何写入操作,只是解析变更并推送出去。

四、项目集成Canal

背景: 发布攻略文章时,业务代码会发送消息到RabbitMQ,search服务接收消息后同步到ES。

问题: 随着业务扩展,攻略的更新删除、游记的更新删除、用户的注册注销等都需要同步到ES,RabbitMQ发送消息的代码会散落在各个业务点,还需要在Redis中存一份防止丢消息,代码冗余。后续还要同步日志库和缓存,在各个业务点发送消息非常麻烦。

方案: 这些数据变更都源自MySQL,只需要监控MySQL的改动就能统一捕获所有变更并发送消息,所以引入Canal来监听MySQL的binlog,替代各业务点手动发送消息的方式。

1、新建travel-api-canal模块

(1)新建StrategyCanal攻略实体类

@Data
public class StrategyCanal {

    private Long id;

    @Column(name = "dest_id")
    private Long destId;

    @Column(name = "dest_name")
    private String destName;

    @Column(name = "theme_id")
    private Long themeId;

    @Column(name = "theme_name")
    private String themeName;

    @Column(name = "catalog_id")
    private Long catalogId;

    @Column(name = "catalog_name")
    private String catalogName;

    private String title;

    @Column(name = "sub_title")
    private String subTitle;

    private String summary;

    @Column(name = "cover_url")
    private String coverUrl;

    private Long isabroad;

    private Long viewnum;

    private Long replynum;

    private Long favornum;

    private Long sharenum;

    private Long thumbsupnum;

    private Long state;

    @Column(name = "create_time")
    private Date createTime;
}

(2)编写pom.xml

   <dependencies>
        <dependency>
            <groupId>cn.wolfcode.wolf2w</groupId>
            <artifactId>travel-common-core</artifactId>
        </dependency>
        <dependency>
            <groupId>javax.persistence</groupId>
            <artifactId>javax.persistence-api</artifactId>
        </dependency>
    </dependencies>

2、新建travel-modules-canal模块

(1)编写pom.xml

    <dependencies>
        <dependency>
            <groupId>cn.wolfcode.wolf2w</groupId>
            <artifactId>travel-api-canal</artifactId>
            <version>3.6.2</version>
        </dependency>
        <dependency>
            <groupId>top.javatool</groupId>
            <artifactId>canal-spring-boot-starter</artifactId>
            <version>1.2.1-RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>cn.wolfcode.wolf2w</groupId>
            <artifactId>travel-common-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>cn.wolfcode.wolf2w</groupId>
            <artifactId>travel-common-rabbit</artifactId>
            <version>3.6.2</version>
        </dependency>
    </dependencies>

(2)新建application.yml配置文件

canal:
  server: 192.168.222.131:11111
  destination: example
  user-name: canal
  password: Canal_2020
logging:
  level:
    root: info
    top:
      javatool:
        canal:
          client:
            client:
              AbstractCanalClient: error
spring:
  redis:
    host: localhost
    port: 6379
    password: 123456
  rabbitmq:
    addresses: 192.168.222.129:5672,192.168.222.130:5672
    username: root
    password: root
    virtual-host: /
server:
  port: 9095

(3)新建CanalApplication主启动类

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class CanalApplication {
    public static void main(String[] args) {
        SpringApplication.run(CanalApplication.class, args);
    }
}

(4)新建listener.StrategyListener类

/**
 * 攻略表数据变更监听器
 * 通过Canal监听ta_strategy表的增删改操作,将变更数据写入Redis并发送到RabbitMQ,
 * 由下游search服务消费后同步到ES
 */
@Component
@CanalTable("ta_strategy") // 指定监听的数据库表名
public class StrategyListener implements EntryHandler<StrategyCanal> {

    @Autowired
    private RedisService redisService;
    @Autowired
    private AmqpTemplate amqpTemplate;

    /**
     * 监听ta_strategy表的新增操作
     * 当MySQL中执行INSERT时,此方法会被回调
     *
     * @param strategyCanal 新增的攻略数据
     */
    @Override
    public void insert(StrategyCanal strategyCanal) {

        // 将变更数据序列化为JSON字符串
        String message = JSON.toJSONString(strategyCanal);

        // 将消息存入Redis的ZSet中,以当前时间戳作为score,防止消息丢失
        String key = RedisKeys.STRATEGY_RABBIT_MESSAGE.getPrefix();
        redisService.addCacheZset(key, message, System.currentTimeMillis());

        // 将消息发送到RabbitMQ,由下游search服务消费并同步到ES
        amqpTemplate.convertAndSend(RabbitConfig.EXCHANGE_NAME, RabbitConfig.ROUTING_KEY, message);
    }
}

(5)新增攻略 - 移除手动发送MQ消息逻辑

 @Override
    public int insert(Strategy strategy) {
        StrategyTheme theme = strategyThemeMapper.selectById(strategy.getThemeId());
        strategy.setThemeName(theme.getName());

        StrategyCatalog catalog = strategyCatalogMapper.selectById(strategy.getCatalogId());
        strategy.setCatalogName(catalog.getName());
        strategy.setDestId(catalog.getDestId());
        strategy.setDestName(catalog.getDestName());
        strategy.setCreateTime(new Date());

        Long isabroad = remoteDestinationService.isabroad(strategy.getDestId(), "inner").getData();
        strategy.setIsabroad(isabroad);

        strategy.setViewnum(0L);
        strategy.setReplynum(0L);
        strategy.setFavornum(0L);
        strategy.setSharenum(0L);
        strategy.setThumbsupnum(0L);
        baseMapper.insert(strategy);

        Long id = strategy.getId();
        String content = strategy.getContent().getContent();
        StrategyContent strategyContent = new StrategyContent();
        strategyContent.setId(id);
        strategyContent.setContent(content);
        int ret = strategyContentMapper.insert(strategyContent);
        //todo 发消息
//        String message = JSON.toJSONString(strategy);
//
//        // ZSet类型,存储member(消息)和score(存入时间戳),member唯一不可重复,
//        String key = RedisKeys.STRATEGY_RABBIT_MESSAGE.getPrefix();
//        redisService.addCacheZset(key, message,System.currentTimeMillis());
//
//        // 发送消息到MQ
//        amqpTemplate.convertAndSend(RabbitConfig.EXCHANGE_NAME,RabbitConfig.ROUTING_KEY,message);
//
        return ret;
    }
posted on 2026-08-24 19:48  冬冬咚  阅读(33)  评论(0)    收藏  举报