使用canal不消耗数据库连接监听数据库变更,异步ACK

本文介绍了canal如何不消耗数据库连接监听MySQL变更,通过流式API进行消息分发,并讨论了如何防止消息丢失和重复处理。在异常情况下,使用mark和offset判断消息是否已被处理,避免重复消息。同时提到了canal作为MQ补偿机制和数据库同步的潜在应用。

 

canal是什么

摘自项目github

基于数据库增量日志解析,提供增量数据订阅&消费,目前主要支持了mysql

原理相对比较简单:
canal模拟mysql slave的交互协议,伪装自己为mysql slave,向mysql master发送dump协议
mysql master收到dump请求,开始推送binary log给slave(也就是canal)
canal解析binary log对象(原始为byte流)

canal解决了什么问题

起源:
早期,阿里巴巴B2B公司因为存在杭州和美国双机房部署,存在跨机房同步的业务需求。不过早期的数据库同步业务,主要是基于trigger的方式获取增量变更,不过从2010年开始,阿里系公司开始逐步的尝试基于数据库的日志解析,获取增量变更进行同步,由此衍生出了增量订阅&消费的业务,从此开启了一段新纪元。

本文在探讨什么

canal的基础操作,本文不再赘述,可参考github上的quick start和client api,包含了demo
本文探讨的是,基于canal的流式api的消息分发,以及如何防止消息丢失和重复处理

什么是流式API

摘自项目github
流式api设计:

11554693-6e1699a0e570d4b6

image

  • 每次get操作都会在meta中产生一个mark,mark标记会递增,保证运行过程中mark的唯一性
  • 每次的get操作,都会在上一次的mark操作记录的cursor继续往后取,如果mark不存在,则在last ack cursor继续往后取
  • 进行ack时,需要按照mark的顺序进行数序ack,不能跳跃ack. ack会删除当前的mark标记,并将对应的mark位置更新为last ack cursor
  • 一旦出现异常情况,客户端可发起rollback情况,重新置位:删除所有的mark, 清理get请求位置,下次请求会从last ack cursor继续往后取

11554693-e309da92a2a8eb14.png

image.png

关注点

异步的ack带来了更好的性能,也带来了一些问题
rollback后,mark会清空,回到上次ack的位置。如果get的速度比ack快,当rollback()后,就会出现重复消息
本文针对这个问题,给出一个较为简单的解决方案

思路

mark清空后,再次get,获取到的batchId会继续递增(保存在服务端),但是消息是已经处理过的,此时我们不希望消息继续被分发或者处理
如何判断消息是否消费过,或者说,该次数据库变更,是否已经解析过
1.在业务上进行判断
2.更好的方式,通过当前处理的消息在binlog中的位置进行判断

String logFileName = entry.getHeader().getLogfileName();
long offset = entry.getHeader().getLogfileOffset();

使用这两行代码,就可以方便的获取当前消息在具体哪个binlog文件的哪个位置,输出类似如下

logfileName = mysql-bin.000001,offset = 41919

代码

测试代码,请勿用于生产

    public static void main(String args[]) {
        new Thread(SimpleCanalClient::receiver).start();
        new Thread(SimpleCanalClient::ack).start();
    }

开局启动两个线程,一个接收,一个确认

private static void receiver() {
        // 创建链接
        CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress(AddressUtils.getHostIp(),
                11111), "example", "", "");
        int batchSize = 1;
        int count = 0;
        try {
            connector.connect();
            connector.subscribe(".*\\..*");
            connector.rollback();
            int total = 20;
            while (count < total) {
                count++;
                Message message = connector.getWithoutAck(batchSize); // 获取指定数量的数据
                long batchId = message.getId();
                int size = message.getEntries().size();
                if (batchId != -1 && size != 0) {
                    printEntry(message.getEntries(),batchId);
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                }
            }
            STOP.set(true);
            System.out.println("receiver exit
### Canal监听MySQL Binlog实现缓存异步更新的实现方案 阿里巴巴的Canal是一个基于MySQL数据库增量日志解析的增量数据订阅和消费工具。通过解析MySQL的binlog日志,Canal可以将数据库的增量数据以消息队列的方式提供给下游系统,从而实现缓存、索引、数据同步等场景的数据异步更新。 #### 1. **Canal的工作原理** Canal通过模拟MySQL的Slave节点,连接到MySQL主库并请求binlog日志。MySQL主库会将binlog事件发送给CanalCanal解析这些事件后,将其转换为统一的数据结构(如`Entry`),并通过客户端订阅机制推送给消费者[^1]。 - **MySQL配置**:需要开启binlog,并设置为`ROW`模式,以确保每行的变更都能被捕获。 - **Canal部署**:Canal可以部署为独立的服务,支持多种部署方式(如单机、HA集群等)。 - **数据消费**:Canal客户端可以订阅特定的数据库表,接收binlog事件,并进行自定义处理。 #### 2. **缓存异步更新的实现** 在实际应用中,缓存通常用于加速数据访问,但缓存与数据库之间的数据一致性是一个关键问题。使用Canal监听MySQL的binlog事件,可以在数据变更异步更新缓存,保证缓存与数据库的一致性。 - **监听binlog事件**:Canal客户端订阅MySQL的binlog事件,当数据库中的数据发生变更(INSERT、UPDATE、DELETE)时,Canal会接收到这些事件。 - **解析事件内容**:从binlog事件中提取出具体的表名、主键、变更后的数据等信息。 - **更新缓存**:根据解析出的数据,构建缓存键(如`user:1001`),并将新的数据写入缓存系统(如Redis、Memcached等)。 以下是一个简单的Canal客户端示例代码,用于监听binlog事件并更新缓存: ```java import com.alibaba.otter.canal.client.CanalConnector; import com.alibaba.otter.canal.client.CanalConnectors; import com.alibaba.otter.canal.protocol.CanalEntry.*; import com.alibaba.otter.canal.protocol.Message; import java.net.InetSocketAddress; import java.util.List; public class CanalClient { public static void main(String[] args) { // 创建Canal连接CanalConnector connector = CanalConnectors.newSingleConnector( new InetSocketAddress("127.0.0.1", 11111), "example", "", ""); try { // 连接Canal服务 connector.connect(); // 订阅所有表 connector.subscribe(".*\\..*"); // 回滚到上一次的位置 connector.rollback(); while (true) { // 获取binlog事件 Message message = connector.getWithoutAck(100L); long batchId = message.getId(); int size = message.getEntries().size(); if (batchId == -1 || size == 0) { // 没有事件,继续等待 continue; } // 处理每个binlog事件 for (Entry entry : message.getEntries()) { if (entry.getEntryType() == EntryType.ROWDATA) { RowChange rowChange = RowChange.parseFrom(entry.getStoreValue()); EventType eventType = rowChange.getEventType(); // 只处理INSERT、UPDATE、DELETE事件 if (eventType == EventType.INSERT || eventType == EventType.UPDATE || eventType == EventType.DELETE) { for (RowData rowData : rowChange.getRowDatasList()) { // 提取主键和变更后的数据 String tableName = entry.getHeader().getTableName(); String primaryKey = getPrimaryKey(rowData.getAfterColumnsList()); String cacheKey = tableName + ":" + primaryKey; // 根据事件类型更新缓存 if (eventType == EventType.INSERT || eventType == EventType.UPDATE) { String cacheValue = buildCacheValue(rowData.getAfterColumnsList()); updateCache(cacheKey, cacheValue); } else if (eventType == EventType.DELETE) { deleteFromCache(cacheKey); } } } } } // 确认处理完成 connector.ack(batchId); } } finally { connector.disconnect(); } } // 提取主键(假设主键为id字段) private static String getPrimaryKey(List<Column> columns) { for (Column column : columns) { if (column.getName().equals("id")) { return column.getValue(); } } return null; } // 构建缓存值(假设缓存值为JSON格式) private static String buildCacheValue(List<Column> columns) { StringBuilder sb = new StringBuilder("{"); for (int i = 0; i < columns.size(); i++) { Column column = columns.get(i); sb.append("\"").append(column.getName()).append("\":\"").append(column.getValue()).append("\""); if (i < columns.size() - 1) { sb.append(","); } } sb.append("}"); return sb.toString(); } // 更新缓存(示例中使用System.out模拟) private static void updateCache(String key, String value) { System.out.println("Updating cache: " + key + " -> " + value); // 实际应用中应调用Redis等缓存系统的API } // 从缓存中删除(示例中使用System.out模拟) private static void deleteFromCache(String key) { System.out.println("Deleting from cache: " + key); // 实际应用中应调用Redis等缓存系统的API } } ``` #### 3. **缓存更新策略** 在实际应用中,缓存更新策略可以根据业务需求进行调整: - **同步更新**:在数据库写操作完成后,立即更新缓存。这种方式简单直接,但在高并发场景下可能会导致缓存与数据库短暂一致。 - **异步更新**:通过Canal监听binlog事件,在数据变更异步更新缓存。这种方式可以减少对数据库的压力,适用于对实时性要求高的场景。 - **TTL(生存时间)控制**:为缓存设置合理的TTL,确保即使缓存未及时更新,也会长期保留过期数据。 - **缓存穿透、击穿、雪崩防护**:可以通过布隆过滤器、缓存空值、热点缓存、随机TTL等方式来防止缓存穿透、击穿和雪崩问题。 #### 4. **性能优化与注意事项** - **批量处理**:为了提高性能,可以批量处理多个binlog事件,减少网络和I/O开销。 - **幂等性处理**:确保每次缓存更新操作是幂等的,避免重复处理导致的数据一致。 - **错误重试机制**:在处理binlog事件时,可能会遇到网络中断、缓存服务可用等问题,因此需要实现重试机制,确保数据最终一致性。 - **监控与报警**:建议对Canal客户端和缓存系统的运行状态进行监控,及时发现并处理异常情况。 #### 5. **扩展性与可靠性** - **多实例部署**:Canal客户端可以部署为多个实例,每个实例处理同的表或数据库,提升系统的并发处理能力。 - **消息队列集成**:可以将Canal与Kafka、RocketMQ等消息队列结合使用,将binlog事件发布到消息队列中,由多个消费者并行处理,进一步提升系统的扩展性和可靠性。 - **数据一致性保障**:虽然Canal可以实现异步更新,但在极端情况下(如网络故障、服务宕机)仍可能导致数据一致。可以通过定期对账、补偿机制等方式来保障数据一致性。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值