续Canal数据过滤场景下,解决并发丢数据问题

2,066 阅读8分钟

作者:正则

前景回顾

之前写过一篇关于canal监听Mysql数据变更时,同一表下重复的变更数据过滤处理的方案,用于解决瞬时区间内重复消费相同ID,导致ES集群CPU过度浪费的问题。

详见:《Canal同步ES瞬时重复数据过滤方案》juejin.cn/post/700283…

思考?

基于重复数据过滤的解决方案,细心的同学会发现,这里存在数据并发场景下的原子性问题。根据伪代码的时序图,可以发现如下:

“程序执行时序图”

如图,程序分Dump服务和定时任务服务两部分并行执行,Dump服务在每次数据库数据变更后触发执行任务,定时任务按每秒定时触发执行(集群中,只有一个线程)。

1)定时任务,从【收集容器】内获取当前这1s需要处理的数据,其中包括id = 1的数据。

2)定时任务,将获取到要处理的数据,放入【收集容器】内。

3)此时,Dump服务收到数据库变更的通知,正好也是“id = 1”的数据需要变更,需将其放入【收集容器】内。

4)定时任务,已经拿到需要处理的数据,故可以将这些数据从【收集容器】内移出。此时,因为并发顺序问题,导致T2需要存入的“id=1”的数据,也被_T1的程序删除_了,引发了数据丢失的问题。

发生概率分析

按照上述描述定义下_问题发生概率_,如下:

1)只有当 id一致的数据且在1s瞬时内发生在两个不同的事务(这里讲到事务的定义,可查看前一篇文章回顾)下,才会出现上述问题。

2)且执行线程拿到ID后的数据处理又是异步执行的,从侧面减缓了数据dump的时效性,也因此降低了瞬时内两个变更事务,引发的最终数据不一致的读取概率

所以,单据事务处理,往往有事务机制保障,前后脚同一条单据1s没变更的人为可能性不大,该问题的产生往往是在_极端情况_下。

实际上,我们在处理重复数据过滤的时候,并未特意去解决这个问题,因为在业务场景下是可以避规这个现象的,其次,在整体数据同步的方案中,我们也用了一些兜底的策略保障数据的最终一致。这里我们可以作为一种普适的解决方案来探讨这个问题。

问题剖析

重新定义

1)dump服务数据处理线程,会生产需处理的数据,我们可以把这个过程称为“生产者”在“生产过程”中产生“生产数据”;

2)定时任务服务,每隔1s会处理“生产数据”,我们可以把这个过程称为“消费者”在“消费过程”中处理“消费数据”;

3)“生产数据”会_放入_【收集容器】,“消费者”会从【收集容器】_取出_数据,并放入【执行容器】内。最后运行异步执行任务,处理【执行容器】的数据。

逐步分析

1)“生产者”,在集群中只有一个线程在处理数据,因为canal同步来的是binlog数据变更通知,这一套解决方案是顺序消息来完成的。(MQ的顺序消息原理,可见:juejin.cn/post/699981…

2)“消费者”,为了实现1s数据的获取,在这里也是通过一个线程执行的。之后的数据处理是异步多线程方案。(要看定时任务执行设置,可能会多线程并发,这里用单线程表示,下一个任务下来需要等上一个任务完成后再执行) 从分析得知,实际这段操作是由两个线程来并发竞争Redis资源,这里的竞争强调不会太高。如图:

“消费者和生产者两个线程竞争Redis资源”

隐含问题

从分析来看,我们是要解决【收集容器】在并发时的资源竞争问题,但是实际情况不是【收集容器】单点资源的问题,我们把整个链路拉长,看下全景,并对关键步骤标记步骤数字,其中T1、T2、T3分别表示3个不同的异步任务,

  • (1)T1:canal的数据变更通知,将“id=1”的数据实时写入【收集容器
  • (2)T2:定时任务,分4步进行每隔1s的数据处理。
    • (3)第一步,获取【收集容器】的数据(包括”id=1“);
    • (4)第二步,将获取到的数据(包括”id=1“)存入【执行容器】;
    • (5)第三步,从【收集容器】中删除获取到的数据(包括”id=1“);
    • (6)第四步,异步执行【执行容器】的数据处理工作;
  • T3:【执行容器】异步执行线程。
    • (7)第一步,获取【执行容器】的数据;
    • 第二步,将获取到的数据,进行处理Dump操作;
    • (8)第三步,确认dump成功后,从【执行容器】中删除获取到的数据;

从全景链路发现,如果因【收集容器】资源竞争,导致”id=1“的数据后置加入时被删除而丢失的问题,在这里会经历如下环节才会发生:

  • (3)先执行,将获取的数据,放入【执行容器】内。
  • (7)执行容器因异步任务调度,直接获取到刚放入的数据,并开始处理数据。
  • (1)canal数据变更通知,又将”id=1“数据写入【收集容器】。
  • (5)定时任务获取到1s内要处理的数据后,进行【收集容器】的清除。

数据丢失发生的过程为(3)-(7)-(1)-(5),且是在第>2次执行定时任务时才会发生,因为首次执行,一定是后异步执行执行容器】。如果是(3)-(1)-(5)-(7),即使因【收集容器】资源竞争导致”id=1“的数据丢失,但是在执行容器内还是会被执行到,所以是并发安全的。 所以要保证该场景的并发安全问题,可以按两个思路去考虑:

  1. 要么确保执行顺序为(5)-(1),就是前面提到的问题根因。不让【收集容器】内的数据丢失。
  2. 要么确保执行顺序为(5)-(7),来保障即使【收集容器】数据丢失,【执行容器】最终还是会对丢失数据进行处理。(其实,这里也会发生【执行容器】的丢数据问题)

解决方案

问题描述

问题剖析结果来看,我们重新定义下问题的描述, 条件:

  • 单线程“生产者”,产生数据并_放入_【收集容器】(Redis资源)。
  • 单线程“消费者”,_获取_消费数据后,删除【收集容器】内此次消费的数据。
  • “消费过程”为每隔1s消费一次。
  • “生产过程”为实时生产。(即生产频次 > 消费频次,并_非生产数据量 > 消费数据量_) 需要解决的问题**:如何保障,(5)-(1)的顺序或者**(5)-(7)的顺序**?

隐含条件:

  • (1)和(5)的步骤是有可能在不同机器上(不同线程)执行的,即【收集容器】在不同机器上资源竞争问题。
  • (5)和(7)的步骤是一定在同一机器上,不同线程执行的。
  • (4)和(8)会产生【执行容器】在同一机器上,资源竞争问题。即【执行容器】丢数据问题。

问题转变

  • 不同机器上的资源竞争和同一机器上资源竞争,处理成本高低问题?(同一机器资源竞争成本低)。假设一下,保障(5)-(7)的执行顺序,保障(4)、(8)资源不竞争,即T2、T3两个线程串行化的问题(因为考虑到需要并行任务处理,串行化解决不可行)。
  • 或者引入版本号,就可以解决这些问题,保证了T3线程处理的是T2当前版本的数据,不会处理其他T2版本数据,同时保证了T3线程并行dump数据的能力。

伪代码

在定时任务执行过程中,获取当前执行任务的时间戳作为该任务版本号,将获取到要执行的数据打上版本号标记,调用异步执行任务时,传入该版本号。 image.png “定时任务每1秒执行的伪代码”

当异步执行任务运行时,根据传入的版本号获取指定版本的数据进行处理,从而隔离不同版本间的id一致的竞争数据。 image.png “异步执行任务的伪代码”

最后,思考问题

  • 多线程消费者的场景,上述解决思路能否满足?
  • 瞬时数据量过多,导致【执行容器】或【收集容器】无法短时间内处理完时怎么办?
  • 将方案能否改成【收集容器】内消费数据版本号的处理方式,可以解决丢数据问题吗?
  • 如果【执行容器】失败,那么该任务版本下的数据如何去补偿处理?

去语雀浏览更好的文本样式www.yuque.com/docs/share/… 《续Canal数据过滤场景下,解决并发丢数据问题》