Canal简介及项目集成(8.24)
一、什么是Canal
Canal 是阿里巴巴开源的、基于 MySQL binlog 增量日志解析的中间件,能订阅并消费数据库的增量变更数据。
二、使用场景
| 场景 | 一句话说明 |
|---|---|
| 数据库同步 | 作为阿里 OTTER 中间件的核心组件,实现异地数据库同步 |
| 缓存更新 | 监控 MySQL 变更,异步同步到缓存,保证最终一致性 |
| 拉链表制作 | 抽取每日新增和变化的数据,为拉链表提供增量数据源 |
| 实时统计 | 实时获取业务表的新增变化数据,支撑实时报表与统计 |
三、工作原理

1、MySQL主从复制原理
MySQL主从复制主要依赖三个线程协作完成:
主库(Master): 当主库发生增删改操作时,会将数据变更记录写入binlog(二进制日志)。
从库(Slave): 从库启动两个线程来完成同步:
- I/O线程: 向主库发送请求,拉取binlog中新增的增量部分,写入本地的relay log(中继日志)。
- SQL线程: 读取relay log中的变更事件,逐条重放执行,从而实现与主库的数据同步。

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;
}
浙公网安备 33010602011771号