下载网站:www.SyncNavigator.CN
客服QQ1793040
----------------------------------------------------------
关于HKROnline SyncNavigator 注册机价格的问题
HKROnline SyncNavigator 8.4.1 非破解版 注册机 授权激活教程
在上一部分《深入数据库同步技术(1)- 基础篇》中,我们为大家介绍了数据库同步的一些基本概念和在应用层做数据同步时普遍存在的痛点问题,这些问题大家平时工作或多或少也会遇到,读起来有些乏善可陈。
所以,在这一部分,我们将为大家介绍数据同步中的另一个比较新鲜的话题:时序性保障。
一、时序性及其重要意义什么是时序性?时序性就是事件发生的先后顺序。对于数据库来讲,事件的最小单元就是事务(即Transaction,每提交一个事务就产生一个事件,无论事务涉及的数据量有多大或者参与的表有多少)。
作为一个正常的OLTP数据库,可能每时每刻在每张表上都在发生事务性操作。类似于事件溯源(Event Sourcing)机制,我们希望将源库上的事务按照发生的时间先后顺序(也就是事务的提交顺序),依次应用在目的库上,这样就会保证源表和目的表数据状态的一致性和事务的不可撕裂性。
所以,数据库的时序性有两个显著特征:一是以事务为单位,二是保持事务发生的先后顺序。
为什么时序性会如此重要?
普通情况下我们在应用层做数据同步,一个线程负责一张表的同步和数据处理,各个线程间互不干涉,也没有任何联系,这样会产生一些问题。
假设有一个用户表和一个用户投资记录表,很显然用户投资记录表中有一个外键字段指向用户表的用户ID。如果单纯使用每张表/每线程的模式,很可能用户投资记录已经同步到目的表了,但是用户表的记录还没有同步过去,在目的库形成“子先于父存在”的情况。这不但让目的表不能像源表那样强制添加主外键约束,还会对目的端应用层的增量查询和数据汇总造成困扰。如果我们在同步过程中保持了时序性,则不会有此类问题发生。
再举一个例子,出于修正数据的需要,假设源库在一个事务中对多张表做了更新操作,然后提交事务。作为同步工具,如果不以事务为单位进行同步,你会发现目的库上的数据是混乱的:某些表的记录可能已经更新成了最新值,某些表可能还是保留原来的值,这是因为你的同步进程把事务撕裂了。源库在历史上的任何时刻从来没有出现过像现在目的库这样的数据形态,换句话说,你破坏了源库数据的一致性。同步本身可以有延迟,但是,数据不能有不一致。假设你在源库的事务中把钱转给了张三,在目的库的某个时刻进行汇总查询,结果发现你的钱被扣除了,但是张三的账户并没有增加,这就比较尴尬了。
在上一部分“基础篇”中,我们也提到了数据总线(Data Bus)的概念,进而可以引申出数据发布、数据订阅、数据通知。在数据总线中流转的事件必须是按发生顺序排列好的,然后再依次发送给各方系统。所以时序性也是构建数据总线的前提条件。
总之,时序性是衡量一个同步系统好坏的重要指标,接下来我们看看如何实施它。
二、方案选型
关于数据同步时序性,市面上其实有一些现成的解决方案。
如果同步源是MySQL,我们使用了阿里开源的Otter/Canal框架,其基本原理是伪装成MySQL的Slave数据库。
图1:Canal框架原理图(来自官方)
如果同步源是Oracle,则实现时序性有几种流派:
(1)日志挖掘
可以使用免费的Logminer,对在线日志或归档日志进行挖掘,从而解析出Redo SQL在目的库进行重放(Replay),还可以使用商业版的Oracle GoldenGate,这两种方式都是基于日志挖掘方式,其缺点是需要在数据库上进行诸多配置,对数据库不透明。
(2)触发器(Trigger)
可以使用LinkedIn的Databus,它基于trigger + ora_rowscn机制,在源表上添加trigger,需要部署多个存储过程包,侵入数据库更深。同时因为基于触发器,对源表写入有性能影响,也增加了发生死锁的可能性,部署这套东西会造成DBA的反感。
(3)物化视图(Materialized View)
比如阿里的yugong(愚公)开源项目,需要在每张源表上开启物化视图日志,并在目的库中创建物化视图,刷新物化视图读取源表增量更新。物化视图可以看做是变相的trigger,也不是特别好。
对于Oracle,我们的原始需求其实很简单:
(1)实现时序性。
(2)在纯应用层搞定(数据库只要开放select权限就行),对数据库透明,无需在数据库上动刀增加额外配置,更不需要DBA参与。
我们强调纯应用层的解决方案,不提倡在数据库层面有大动作,这是因为:
????数据库是别人的,不是你想加字段就加字段,想加trigger就加trigger,想改配置就改配置的;
????DBA资源是需要跨团队协调的,包括使用选型方案产生的后续维护工作。DBA资源的可用性是在你的掌控范围之外的;
????数据库出问题的时候(比如死锁了),你是有潜力当背锅侠的;
????当项目假手于人,需要太多外部资源扶持,你的项目可能是遥遥无期的;
总起来说,凡事要靠自己,不能一上来就有太多假设因素,项目需要介入扶持的资源越多,就越具有不可控性。
(3)上手容易,配置同步任务简单。
(4)最好在同步中间环节留有扩展,可以进行数据预处理和后处理
可惜市面上没有任何一款开源产品符合我们的需求,我们才自研了da-syncer同步工具。针对Oracle源,在应用层实现时序性,是da-syncer有别于其他同步工具的明显标志,也是它的核心竞争力所在。其绝不仅仅是将数据从一个地方同步到另外一个地方,中间可以做一些数据清洗和ETL工作的常规同步工具。
事实上,da-syncer曾经有两个版本的迭代开发,v1版本就是这样实现常规同步功能的版本,当时只用了两个星期的时间就开发完成了。但为了在v2版本中实现时序性,我们却耗费了大量的精力去调研,反复推敲可行性,不断进行方案选型,最后我们才创新性地借助于Oracle的闪回版本查询(Flashback Version Query)机制实现了时序性,我们应该是第一个这么干的系统。
三、Oracle闪回版本查询简介
前面我们讲到,da-syncer主要是利用了Oracle提供的闪回版本查询机制实现了时序性,现在我们就来聊聊它。
Oracle闪回版本查询是Oracle10g+提供的允许你查询一张表距当前时间之前有限时间段内行记录事务版本的一种机制,它本身基于Oracle的SCN。
SCN(System Change Number)是Oracle的系统变更编号,它是一个长整形数,作为标识Oracle对象发生变化的编号,自动增长,全局使用。这里我们主要讨论SCN作为事务提交编号的场景。
为了让大家有一个感性认识,我们来做一个简单的练习。创建一张表,并执行一些SQL语句(每条SQL单独提交以形成独立事务):
create table test (
id number(38,0) not null,
name varchar2(20) not null,
primary key(id)
);
insert into test(id, name) values(1, ‘a1’);
update test set name = ‘a2’ where id = 1;
delete from test where id = 1;
insert into test(id, name) values(2, ‘b1’);
update test set name = ‘b2’ where id = 2;
由于记录1最终被删除,所以如果执行如下常规查询,只会返回name值为b2的记录2(见图2):
select id, name from test;
图2:执行常规select查询
如果我们执行闪回版本查询,就可以很神奇地追溯发生的事务全部历史版本并按提交顺序查询出来(见图3):
select id, name, rawtohex(versions_xid) versions_xid,
versions_operation, versions_startscn, versions_endscn,
to_char(versions_starttime,’yyyy-mm-dd hh24:mi:ss’) versions_starttime,
to_char(versions_endtime,’yyyy-mm-dd hh24:mi:ss’) versions_endtime
from test
versions between scn minvalue and maxvalue
order by versions_startscn asc;
图3:执行闪回版本查询
观察图3我们可以得出一些基本结论:
????版本这个概念属于记录,随着记录上发生事务提交(插入、更新和删除),新版本不断形成。
????伪列versions_startscn和versions_starttime是版本的开始或者说形成时候的SCN和时间戳,因为只有通过提交事务才能形成版本,所以它们也代表了事务提交时的SCN和时间戳。对于那些参与一个事务的记录版本会有相同的versions_startscn和versions_starttime,不管参与事务的表有多少或记录有多少。
????伪列versions_endscn和versions_endtime是版本的结束SCN和时间戳。
????从以上可知,每个版本都有生命周期,占据记录版本时间线上的某一时间段。所以当前版本的versions_endscn和versions_endtime恰好是下个版本的versions_startscn和versions_starttime。
????伪列versions_xid和versions_operation是形成版本时的事务ID和发生的操作(I=Insert,U=Update,D=Delete,你没看错,Delete也可以被检索出来)。
????形成该版本时的用户字段值以及上述伪列字段可以随闪回版本查询一起被检索出来。
接下来看一下闪回版本查询起关键作用的过滤语句:
versions between scn <START_SCN> and <END_SCN>
<START_SCN>和<END_SCN>分别指明了版本查询时的开始、结束SCN条件值。
如果两个条件分别指定了minvalue和maxvalue,则会把目前Oracle内部回滚段内所有可用的记录版本全部查询出来。
假设表T1的4条记录在时间线上的版本分布图如下,蓝色棱形代表记录插入,黄色短竖线代表版本开始或结束,绿色长竖线代表我们指定的<START_SCN>和<END_SCN>查询条件。
图4:闪回版本查询示意图
需要注意的是,当我们指定<START_SCN>和<END_SCN>查询条件时,其效果相当于在表的所有记录上进行了竖切操作。凡是被竖切接触到的记录的版本以及<START_SCN>、<END_SCN>之间的记录的版本,都会被检索出来。所以可以得到一个结论,无论指定任何的<START_SCN>和<END_SCN>,至少会查询出一条记录的一个版本,因为每条记录都至少被竖切接触到了。
按照这种理解,我们回过头来看图4,很容易得出每条记录的哪些版本会被查询出来:
????记录1会查询出v1,所有伪列字段都为null,因为v1的versions_startscn比 <START_SCN>更靠前,versions_endscn比<END_SCN>更靠后。
????记录2会查询出v1且v1的versions_startscn为null,因为v1的versions_startscn比 <START_SCN>更靠前;会查询出完整v2和v3;会查询出v4且v4的versions_endscn为null,因为v4后续的新版本还未形成。
????记录3会查询出完整v1;会查询出v2且v2的versions_endscn为null,因为v2的versions_endscn比<END_SCN>更靠后。
????记录4比较特殊,因为v1的versions_endscn(也是v2的versions_startscn)正好等于<START_SCN>,v3的versions_endscn(也是v4的versions_startscn)正好等于<END_SCN>。在这种情况下会查询出完整的v2和v3,会查询出v4且v4的versions_endscn为null。
假如现在有一个需求,要求查询出<START_SCN>和<END_SCN>之间形成的版本(即记录在该SCN间隔内有过事务提交行为),我们只需要再添加过滤条件:versions_startscn >= <START_SCN> and versions_startscn != <END_SCN>,则记录2的v2、v3、v4,记录3的v1、v2,记录4的v2、v3这些符合要求的版本都会被筛选出来。其余的都会被去掉。
注意:图4没有引入Delete操作,Delete操作形成的版本表现会稍有不同。
在进行闪回版本查询时,有一些需要特别关注的点:
(1)除了versions between scn <START_SCN> and <END_SCN>语法,还可以使用versions between timestamp <START_TIME> and <END_TIME>,SCN和TIMESTAMP在Oracle内部有映射(在SYS.SMON_SCN_TIME表中有维护),且通过scn_to_timestamp和timestamp_to_scn函数可以相互转换,但因为Oracle内部实际使用的是SCN机制,所以推荐使用SCN进行闪回版本查询。
(2)随着时间的流逝,之前可以查询到某条记录的版本数据可能会逐步消失,这是因为版本数据不会永久留存,它依赖于Oracle多个参数的设置和动态因素,比如:
undo_management:undo管理模式是否为auto。
undo_retention:版本数据在undo表空间的保留时长。
undo_tablespace:使用到的表空间,表空间大小。
可以使用show parameter显示以上参数当前的设置值。
如果在版本查询中undo表空间的回滚段被其他事务覆盖,同样会出现“基础篇”中提到的:
ORA-01555:snapshot too old: rollback segmeng number xxx with name xxx too small
(3)<START_SCN>和<END_SCN>不能是随意的整型数,必须是一个合法的SCN,否则会出错:
ORA-08181:specified number is not a valid system change number
可以使用select timestamp_to_scn(systimestamp) from dual;获取当前时间戳对应的SCN。
(4)即便是合法的SCN,<START_SCN>也不能太旧,必须在undo_retention参数指定的时间范围内,否则会出错:
ORA-30052:invalid lower limit snapshot expression
(5)如果表上发生了DDL操作(比如添加/删除字段,修改字段类型/长度,Truncate,添加约束等),进行闪回版本查询会出现以下错误:
ORA-01466:unable to read data – table definition has changed
(6)伪列ora_rowscn和versions_startscn的关系:
如果建表时指定了rowdependencies,则ora_rowscn是该条记录的最后版本的versions_startscn;如果未指定,则ora_rowscn是Oracle数据块上最后发生事务提交行为的记录的生成版本的versions_startscn。
记录版本的versions_startscn值可能会消失,但是ora_rowscn伪列值不会,因为它已经被持久化下来了(放在记录头或者块头上)。
选项(2)、(3)和(4)提醒我们:我们需要在尽量靠近当前时间范围内进行闪回版本查询,否则就会出现问题。
到这里,我们总结一下:使用闪回版本查询,可以查询出某张表上指定的<START_SCN>和<END_SCN>之间发生事务提交行为的记录的所有事务版本,如果再按versions_startscn升序排列,就会得出这些事务版本的提交顺序。这就是单表上实现时序性的方式。
四、闪回版本查询在da-syncer中的应用
上节已经讲到了在单表上通过指定<START_SCN>和<END_SCN>进行闪回版本查询实现了时序性的方式。如何在多表上实现时序性呢?
图5:多表上的时序性
其实很简单,如图5所示,我们放大来看,可以在多表上进行指定相同<START_SCN>和<END_SCN>的闪回版本查询,黄色短竖线同样代表着对应表上的记录在该段内生成的事务版本。如果把该段内所有表上的记录的事务版本按versions_startscn升序排列,我们同样会得到一个时序性。
我们从左到右沿着时间线,一个接一个地进行<START_SCN>和<END_SCN>的分段查询,就解决了多表的时序性问题。
在进行多表闪回版本查询时,由于一个线程负责一张表的同步,所以最关键的问题是怎么协调各个表的同步线程进入统一的<START_SCN>和<END_SCN>分段闪回版本查询阶段,该阶段我们叫做“全局同步”阶段。在此,我们增加了一个协调线程,由协调线程对各个表的处于全局同步的同步线程下达统一的<START_SCN>和<END_SCN>闪回版本查询位点。
但这里有一些问题,需要思考和探讨:
(1)各个表的同步线程是如何进入全局同步阶段的?
一开始的时候表首先进行全量同步,全量同步完成之后进入增量同步。当增量同步稳定后,就可以向协调线程请求加入全局同步。协调线程使用timestamp_to_scn(systimestamp)函数获取SCN给表同步线程,表同步线程利用ora_rowscn伪列,把ora_rowscn小于这个SCN值的所有记录先同步过去,这意味着表同步线程加入全局同步成功。
接下来进入真正的全局同步过程,协调线程把上面获取的SCN作为<START_SCN>,又把timestamp_to_scn(systimestamp)获取的SCN作为<END_SCN>推给表同步线程,让表同步线程进行<START_SCN>和<END_SCN>间的闪回版本查询。下一步把<END_SCN>作为<START_SCN>,timestamp_to_scn(systimestamp)获取的SCN作为<END_SCN>再推给表同步线程,又形成新的分段,这样不断往复循环,沿着时间线追赶,永不停止。
(2)主外键约束、数据一致性和严格的时序性
当目的表间存在主外键约束时,我们需要实现严格的时序性。假设源表在一个事务中,先在父表中插入了一条记录,然后在子表引用了父表的主键id,在子表中也插入了一条记录。两个表同步线程在<START_SCN>和<END_SCN>段内闪回查询,各自发现一条记录的一个事务版本,但两条记录的两个事务版本具有相同的versions_startscn。在这种情况下,我们还必须分析表之间的依赖关系,在目的表插入时,必须先插入父表,再插入子表,这就是严格的时序性。
当目的表间不存在主外键约束时,我们可以让各个表同步线程把各自在<START_SCN>和<END_SCN>段内查询出来的记录版本,在一个全局事务中,按照顺序依次应用在目的表即可。一般情况下,我们实现这样的数据一致性就能满足需求。
当然,如果实现了严格的时序性,数据一致性一定是得到保障的。
(3)真的需要记录的每一个版本么?
通过<START_SCN>和<END_SCN>间的闪回版本查询,我们可以把表在这个段内的发生事务提交记录的所有版本查询出来,进一步说,如果一条记录在该段内事务提交比较频繁,这条记录会被查询出多个版本。但通常情况下,我们只需要保留这条记录的最后一个版本就行,这样可以节约查询成本、网络传输和降低目的端数据库的负载。
最后,我们总结一下da-syncer整个同步过程的几个阶段:
0:新建
1:全量同步中
2:全量同步结束
3:增量同步中
4:正在请求加入全局同步中
5:全局同步中
整体的大概流程如下:
(1)当通过管理控制台新添加一个同步任务后,任务阶段是“0:新建”。
(2)启动新的同步线程,根据任务配置,进入“1:全量同步中”。这里面包含了对大表进行分段的处理过程。
(3)当全量同步完成后,同步线程将其设置成“2:全量同步结束 ”。
(4)接下来进入增量同步,同步线程将其设置成“3:增量同步中”,可以选择不同的增量同步策略,比如选择last_modified_date字段,对于Oracle,还可以选择ora_rowscn机制,或greatest_function_index机制(通过在多个日期字段上创建greatest函数索引,以代替last_modified_date字段)。
(5)每次增量同步后,系统检测是否已经达到可以加入全局同步的条件,比如连续3次增量同步时间小于1分钟,且最大和最小耗时之差小于5秒,这表示增量同步已经进入稳定态,可以请求加入全局同步了,否则继续增量同步。
(6)如果(5)条件满足,同步线程设置成“4:正在请求加入全局同步中”,向协调线程请求追赶SCN位点,协调线程下达追赶SCN位点给同步线程,如果该位点跟自身已经到达的SCN相等甚至还小,需要等待下一轮请求;否则,通过增量同步过程追赶下达的SCN。
(7)如果追赶位点成功,则加入全局同步成功,同步线程设置成“5:全局同步中”,所有处于全局同步的同步线程接受协调线程统一下达的SCN,然后进行闪回版本查询,如果仅仅实现数据一致性,则由表同步线程在一个事务中将自身的记录的事务版本应用到目的库;如果需要实现严格的时序性,协调线程负责组装、分析所有同步线程的闪回查询结果,然后再应用到目的库。
(8)如果同步线程在同步中遇到异常,比如源表添加新字段产生了ORA-01466错误,则同步线程又降级成增量同步,进入上述的步骤(4),进而再逐步升级成全局同步。