架构师必备:巧用Canal实现异步、解耦的架构

本文介绍如何应用Canal实现异步、解耦的架构,后续有空再写文章分析Canal原理和源代码。

Canal简介

Canal是用来获取数据库变更的中间件。
伪装自己为MySQL从库,拉取主库binlog并解析、处理。处理结果可发送给MQ,方便其他服务获取数据库变更消息,这一点非常有用。下面介绍一些典型用途。

image.png

其中,Canal+MQ作为一个整体,从外界看来就是一个数据管道服务服务,如下图。


image.png

Canal典型用途

异构数据(如ES、HBase、不同路由key的DB)

通过Canal自带的adapter,同步异构数据至ES、HBase,而不用自行实现繁琐的数据转换、同步操作。这里的adapter就是典型的适配器模式,把数据转成相应格式,并写入异构的存储系统。

image.png

当然,也可以同步数据至DB,甚至构建一份按不同字段分片路由的数据库。
比如:下单时按用户id分库分表订单记录,然后借助Canal数据通道,构建一份按商家id分库分表的订单记录,用于B端业务(如商家查询自己接到哪些订单)。

image.png

缓存刷新

缓存刷新的常规做法是,先更新DB,再删除缓存,再延迟删除(即cache-aside pattern+延迟双删),这种多步操作可能失败,而且实现相对复杂。借助Canal刷新缓存,使主服务、主流程无需关心缓存更新等一致性问题,保证最终一致性。


image.png

价格变化等重要业务消息

下游服务可立即感知价格变化。
常规做法是,先修改价格,再发出消息,此处的难点是要保证消息一定发送成功,以及如果发送不成功时如何处理。借助Canal,不用在业务层面担心消息丢失的问题。

数据库迁移

  • 多机房数据同步
  • 拆库
    虽然可以自己在代码中实现双写逻辑,然后对历史数据做处理,但是历史数据也可能被更新,需要不断迭代对比、更新,总之很复杂。

实时对账

常规做法是定时任务跑对账逻辑,时效性低,不能及时发现不一致问题。借助Canal,可实时触发对账逻辑。
大致流程如下:

  • 接收数据变更消息
  • 写入hbase作为流水记录
  • 一段窗口时间过后,触发比较与对端数据做比较

Canal客户端demo代码分析

以下示例是客户端连接Canal的例子,修改自官方github示例,楼主做了一些优化,并且在关键代码行中加入了注释。如果Canal把数据变更消息发送至MQ,写法有所不同,不同之处只是一个是订阅Canal,一个是订阅MQ,但是解析和处理逻辑基本相同。

`

public void process() {
    // 每批次处理的条数
    int batchSize = 1024;
    while (running) {
        try {
            // 连上Canal服务
            connector.connect();
            // 订阅数据(比如某个表)
            connector.subscribe("table_xxx");
            while (running) {
                // 批量获取数据变更记录
                Message message = connector.getWithoutAck(batchSize);
                long batchId = message.getId();
                int size = message.getEntries().size();
                if (batchId == -1 || size == 0) {
                    // 非预期情况,需做异常处理
                } else {
                    // 打印数据变更明细
                    printEntry(message.getEntries());
                }

                if (batchId != -1) {
                    // 使用batchId做ack操作:表明该批次处理完成,更新Canal侧消费进度
                    connector.ack(batchId);
                }
            }
        } catch (Throwable e) {
            logger.error("process error!", e);
            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e1) {
                // ignore
            }

            // 处理失败, 回滚进度
            connector.rollback();
        } finally {
            // 断开连接
            connector.disconnect();
        }
    }
}

private void printEntry(List<Entry> entrys) {
    for (Entry entry : entrys) {
        long executeTime = entry.getHeader().getExecuteTime();
        long delayTime = new Date().getTime() - executeTime;
        Date date = new Date(entry.getHeader().getExecuteTime());
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        // 只关心数据变更的类型
        if (entry.getEntryType() == EntryType.ROWDATA) {
            RowChange rowChange = null;
            try {
                // 解析数据变更对象
                rowChange = RowChange.parseFrom(entry.getStoreValue());
            } catch (Exception e) {
                throw new RuntimeException("parse event has an error , data:" + entry.toString(), e);
            }

            EventType eventType = rowChange.getEventType();

            logger.info(row_format,
                new Object[] { entry.getHeader().getLogfileName(),
                        String.valueOf(entry.getHeader().getLogfileOffset()), entry.getHeader().getSchemaName(),
                        entry.getHeader().getTableName(), eventType,
                        String.valueOf(entry.getHeader().getExecuteTime()), simpleDateFormat.format(date),
                        entry.getHeader().getGtid(), String.valueOf(delayTime) });

            // 不关心查询,和DDL变更
            if (eventType == EventType.QUERY || rowChange.getIsDdl()) {
                logger.info("ddl : " + rowChange.getIsDdl() + " ,  sql ----> " + rowChange.getSql() + SEP);
                continue;
            }

            for (RowData rowData : rowChange.getRowDatasList()) {
                if (eventType == EventType.DELETE) {
                    // 数据变更类型为 删除 时,打印变化前的列值
                    printColumn(rowData.getBeforeColumnsList());
                } else if (eventType == EventType.INSERT) {
                    // 数据变更类型为 插入 时,打印变化后的列值
                    printColumn(rowData.getAfterColumnsList());
                } else {
                    // 数据变更类型为 其他(即更新) 时,打印变化前后的列值
                    printColumn(rowData.getBeforeColumnsList());
                    printColumn(rowData.getAfterColumnsList());
                }
            }
        }
    }
}

// 打印列值
private void printColumn(List<Column> columns) {
    for (Column column : columns) {
        StringBuilder builder = new StringBuilder();
        try {
            if (StringUtils.containsIgnoreCase(column.getMysqlType(), "BLOB")
                || StringUtils.containsIgnoreCase(column.getMysqlType(), "BINARY")) {
                // get value bytes
                builder.append(column.getName() + " : "
                               + new String(column.getValue().getBytes("ISO-8859-1"), "UTF-8"));
            } else {
                builder.append(column.getName() + " : " + column.getValue());
            }
        } catch (UnsupportedEncodingException e) {
        }
        builder.append("    type=" + column.getMysqlType());
        if (column.getUpdated()) {
            builder.append("    update=" + column.getUpdated());
        }
        builder.append(SEP);
        logger.info(builder.toString());
    }
}

`

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,588评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,456评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,146评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,387评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,481评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,510评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,522评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,296评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,745评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,039评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,202评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,901评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,538评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,165评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,415评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,081评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,085评论 2 352

推荐阅读更多精彩内容