技术大佬 : 佩琪看你一脸的闷闷不乐,最近的面试又闹心了?
佩琪: 哎,大佬,别提了,伤心。前面的10道 8股文,我回答的那是真叫一个丝滑,一点都不带阻塞的,可是咋不是做业务系统的CRUD居多嘛,最后 面试官问了我一个真实的场景题,我感觉没回答好。他问我: 你们系统从其他业务系统同步数据的时候,做了哪些技术上的考虑?
技术大佬 : 哦,那你怎么回答的呢?
佩琪: 一个小小的数据同步功能,有啥技术上的考虑,我按照 实际工程里的做法实话实说,就定时每隔一段时间从别的系统同步数据然后存储到我们系统里就完事了,没啥技术上的考虑点。
技术大佬 : 哦,然后了?
佩琪: 然后面试官,就让我回家等通知,然后就没有然后了。。。。。。
佩琪: 大佬,一个小小的业务系统间的数据同步功能,有啥技术上的考虑点了。我看我们上次做的从外部系统同步促销信息就是一个定时任务,然后调用下游系统的拉数接口,定时增量同步数据就完了,没发现里面有什么设计呀?就像下面这幅图
技术大佬 : 额,这个嘛,还真是有些设计和考虑在里面的。比如在进行双方数据同步时:数据防重 ;数据防乱序 ;如何保证数据不丢失 ;数据同步及时性上的考虑 ;以及 数据完整性 ;还有如何保证拉数服务的高可用 等。
佩琪: (丢,你是不是在装B;就一个小小的数据同步功能,那里来的那么多的考虑。)啊?还有这么多的考虑点和设计在里面呀,大佬弱弱的问一句,这个数据防重,是什么意思?
技术大佬 : 数据防重其实就是我们拉取过来的同一条数据,在我们系统里不能变成2条吧。
佩琪: 摸了摸头,能详细说说吗?
技术大佬 : 比如在我们同步上游促销系统的信息;在拉取增量数据时,历史数据也在同步;很有可能在同步历史数据时,这条历史数据又变更了,然后做为增量更新数据过来;此时同一个id的历史数据和更新数据都来了?怎么办?直接往库里插入,是会出现重复数据的;
技术大佬 : 又比如:在同步历史数据时,同步的过程中,突然出现了异常,导致应用停止了;然后需要重新同步,但是历史数据又同步了一小部分;如果再次同步,直接往库里插入数据;则也会出现重复数据;比如下面这幅图
佩琪: 哦哦,大佬 那我们是如何去解决这个数据重复的问题呢?
技术大佬 : 解决数据重复的思路也很简单:比如要求数据有唯一id;然后把此唯一ID 作为表的主键或者唯一索引; 在往数据库写入时;查询有没有,没有则插入;有则带条件的更新;如果并发插入,则底层数据库的唯一索引可保证,只会有一个写入成功;写失败的可以再次重试上面的过程。
佩琪: 哦哦,原来数据的唯一ID还有这个作用。那数据乱序又是什么了?
技术大佬 : 比如上面说的,程序在同时处理具有相同ID的两条数据时,以哪条为准了?同一条促销信息,在拉取历史数据里是未删除状态;增量更新里拉取过来是删除状态;而我们处理增量更新的线程和处理历史数据的线程不是同一个线程,在并发处理的时候,有可能导致先做了删除,随后又做了更新;这样数据就和上游不一样了嘛
佩琪: 哦哦,那怎么去解决这个并发更新,导致的数据乱序问题了?
技术大佬 : 其实我们在进行处理的时候,除了要求上游传过来的数据,必须要有ID防数据的唯一性以外;还要求上游的每一条数据都带一个版本号;通常版本号用时间戳代替;在数据更新的时候,必须要求拿到的数据比库里的数据新,才会去库里更新;即在更新的条件里除了带ID做为条件,还需要带时间戳做为条件
佩琪: 哦哦,我大概明白了,这就是我们常说的使用数据库乐观锁的思想,解决数据并发更新问题。弱弱的问问大佬,我们是如何保证数据不丢失了?
技术大佬 : 这个数据不丢,主要还是看我们使用的是哪种数据同步方式,如果用MQ方式,可以看看《kafka 消息"零丢失"的配方》这篇文章。但咱们使用的是定时拉取增量数据方式,主要是通过记录数据最后更新时间来保证的。
佩琪: 摸了摸猪头,大佬你说的我不明白呀,能详细解说下吗?
技术大佬 : 大概过程是这样的。每次查询增量数据时,都会指定一个时间范围做为条件进行查询;查询完数据并处理成功后,都会记录当前增量数据的最后更新时间;下次同步数据时,以上一次数据最后更新时间做为起始时间,当前时间做为最后时间,拉取这段时间内的增量数据;
技术大佬 : 如果在同步的过程中,发生了任何的错误,都不会去更新这个查询开始时间;这样就保证了,即使某次数据同步失败了,当定时时间到了再次拉取增量数据时,数据更新开始时间也是没变的,只是查询时间范围变大了;但这样至少是保障了数据最终是不会丢失的,即保证了数据最终是一致。再加上前面说的防重+防并发更新机制,基本也能保证数据的准确性了。
佩琪: 哦哦,我大概明白了,原来是这样,那大佬 我们是如何考虑数据同步及时性上的了?
技术大佬 : 双方系统间数据同步问题,在我们的认知里,一般都是通过引入MQ来解决;因为MQ有很多的好处;比如系统间解耦,削峰填谷,异步调用等;但是用了MQ上面提到的数据重复,数据乱序,数据防漏问题,还是会存在的,也是需要我们去解决的,请看《MQ防重》《MQ防乱序》《MQ防丢失》
佩琪: 那我们系统里,为什么没有去用MQ进行双方系统间数据同步了?
技术大佬 : 这个,咳咳。主要原因是:第一:小公司,没有专门的MQ中间件;
技术大佬 : 第二:部门里,某些业务有MQ中间件,但是不一定会给这个业务使用
技术大佬 : 第三:我们业务每天数据量并不是很大,大概一天就2W条;所以MQ的很多好处也用不到
技术大佬 : 第四:如果为这个业务单独引入MQ,还需要后续进行MQ的维护,小公司没那么多运维资源 和机器资源
佩琪: (此时心里嘀咕,说白了,就是没钱,没资源,技术也不咋的呗,属于买的起,伺候不起)那大佬我们选用定时是如何实现和如何考虑的了?
技术大佬 : 我们主要是 采取了 定时 高频率 批量同步 这样方式:即 系统B 每隔5秒钟,查询一次5秒前到当前时间 这段时间范围内已更新数据;批量查询其实提高了数据同步的效率;而较小的间隔时间,保证了数据尽快的同步到B系统;另外引入本地定时在技术和架构上,比引入一个MQ中间件轻量多了。但是数据同步的延迟是在秒级内;比起MQ这种数据同步在毫秒内的速度,确实差一个数量级;而我们能用这种方式主要是业务上能接受秒级的延迟。
佩琪: 哦哦,那我们是如何考虑数据完整性的呢?
技术大佬 : 通过MQ或者接口增量查询,一般同步过来的都是增量更新的数据;历史数据的同步,上游系统可以提供一个分页接口+时间范围 查询接口给到系统B;
佩琪: 为什么不通过线下导数据这种 简单,又快捷的方式同步历史数据了?
技术大佬 : 主要是因为:第一: 系统A的数据接口,不是原表接口;是多个表聚合后,并且还和其他系统关联 聚合后才吐出去的接口数据;这种方式不适合直接给到我们系统,或者给到了我们系统,系统还要加工处理下,我们肯定也不乐意,业务数据同步本来事儿就挺多了,还给加一个 加工数据的任务,肯定不乐意,谁都希望用现成的
技术大佬 : 第二 :为了省时间和人力,因为增量接口和历史数据查询接口,可以是同一个接口。开发完数据增量查询接口,历史数据查询接口也就研发好了,这样两全其美,省时又省力事情,为什么不去做了?
技术大佬 : 第三: 用接口的方式,还可以加快历史数据同步的这个过程;比如采用多线程的方式,指定某些日期是某个线程处理的,这样通过多线程分日期 并发的进行数据查询和处理的过程,是加快了整体历史数据同步的过程的。比如 如下图所示
佩琪: 哦哦,原来如此。那这个高可用需要考虑啥了?
技术大佬 : 一般解决服务高可用,是通过部署多台无状态的实例来完成,一个实例有问题宕机了,还有其它实例可继续提供服务;
技术大佬 : 如果是用MQ的方式进行数据同步,数据同步业务高可用的问题,其实由底层MQ解决了。比如kafka 在发现一个消费者实例没有存活的时候,会把分配给该实例的parition(分区),重平衡给另外一个消费者;这样这个分区上的数据还能继续被消费;而且这种自动重平衡,是由底层kafka自动完成的,上层消费者应用是不用关心的;如下图所示
消费者实例2宕机了,kafka会自动把分区1重新分配给消费实例1,继续消费分区1上数据
佩琪: 但是我们用的定时增量查询这种方式,是如何保证拉数服务的高可用了?
技术大佬 : 采用定时高频率 批量拉取数据的方式,这种高可用是需要程序自动处理的。因为我们的应用一般都会部署两个实例,这样一个实例宕机了,还有另外一个实例是可用的;但是因为采取了本地定时的方式去同步数据;这样就造成了两台机器在定时时间到了都会去查询 相同时间范围的增量数据,然后进行相同的处理,你不觉得很奇怪嘛?。(如果有分布式定时任务中间件,那就不存在这个问题了)
佩琪: 摸了摸头,是挺奇怪的,能详细说说吗?
技术大佬 : 虽然数据更新的底层逻辑,有类似于幂等性的效果;不会造成数据重复,乱序等,但是这种本地定时拉取数据方式无形中加重了机器负载:比如两台机器同时去拉取数据,那么有一台机器在做无用功,浪费算力,浪费网络带宽,加重咋们接口方系统的压力,还有自身存储中间件的压力;要是3台机器,这种浪费机器资源情况,不是更加严重了嘛,而且这种情况还是和机器数成正比。
佩琪: 那咱们是怎么去解决这个又想要多机运行保证高可用,又不想浪费资源,加重机器负载的这个矛盾的了?
技术大佬 : 解决这种多实例排他性问题也很简单,用分布式锁来控制多台机器同时拉取数据问题。即:在拉数的时候,申请到分布式锁成功的机器,才真正的去进行数据同步和处理;如果没有拿到分布式锁的就返回吧。
佩琪: 大佬,没看到我们代码里用了类似redis,或者zk 这种中间件,咱们分布式锁用什么实现的?
技术大佬 : 我们主要是用mongo来实现了个简单分布式锁功能,利用数据的唯一索引+mongo数据自动过期功能来实现的;即先往集合里添加一条数据,能插入成功 表明获得了锁,(因为用了唯一索引,其它机器在插入时会报错的)然后在集合维度设置了数据的过期时间为10秒钟,这样即使获得锁的机器中途宕机了,该条数据也会被mongo自动删除;其他机器还有机会插入成功,能获得锁,继续进行数据的同步。
佩琪: 哦哦,大佬好厉害,收下我的打火机
技术大佬 : 通过上面设计和实现,我们系统已经同步了上游系统的业务数据,并且还能和上游系统数据保持一致;可是此时还不敢说,我的数据和上游系统是完全一样的。因为双方数据还没有经过数据完整性对比。
佩琪: 那数据对比如何去做呢?
技术大佬 : 数据完整性对比:一般包括两部分
技术大佬 : 第一 是每一条数据的准确性,即A系统和B系统每条数据的里的每个字段是相同的;(非严格意义 可以对比重点业务字段)
技术大佬 : 第二 是系统A的总数据和系统B的总数据是能对的上。即总条数一样。 总条数的对比还比较的容易进行实现,可能一条带条件的SQL就搞定了,可是每条数据的每个字段对比;大多数时候只能通过写代码进行比较了。这块还没有发现什么好的数据对比工具,如果小伙伴知道,还请告知下。
总结
在一个小小的数据同步功能里;居然有这么多需要考虑的技术点。
通过数据唯一ID,保证同步过来的数据不重复;
通过数据唯一ID+时间戳;保证并发同步过来的数据无乱序问题;
通过记录数据最后更新时间方式,保证拉数服务不丢失数据;
通过一次全量历史数据同步+多次增量更新 保证双方系统间数据的完整性;
通过定时 + 高频率 + 批量的方式,加快增量数据同步速度;
通过多线程指定日期方式,加快历史数据同步速度;
通过多实例部署解决数据同步服务高可用问题,分布锁解决多实例同步调度问题;
最终通过双方数据对比,完成数据准确性和完整性的验证,从而保证双方数据的一致性。