一个事务复制的bug--更新丢失

 

有两种情况会造成更新丢失,第一种是不正确的设置,例如外键或触发器的“Not For Replication” (NFR)属性没有开启。详情请参考https://blogs.msdn.com/b/apgcdsd/archive/2012/01/10/10254809.aspx

第二种是产品bug,例如使用了 MaxCmdsInTran https://support.microsoft.com/kb/2648158

 

 

前一阵我在做case的时候遇到了一个新的bug。这个bug在sql server 2005,2008, 2008R2这些版本中都可以重现,并且目前没有推出相应的fix. 需要注意的是,sql server 2012中相关的行为已经重新进行了设计,不会存在这个问题。

 

 

描述

===

环境:事务复制的订阅不是通过快照进行的初始化

条件:Logreader没有运行。

操作:我们向publicaiton添加了一个新的Article.

此时在log reader停止期间到完成添加article之前,publication的article出现了更新/删除/插入,那么这些变更都不会传递到订阅。 假设 Logreader在11:00停止,我们在12:00添加了一个新的artilce(假设瞬间完成),13:00重新启动Logreader。那么11:00~12:00之间的更新将全部丢失。

 

原因

===

当publicationdatabase的article发生更新时, 会产生相应的日志,Log reader会读取这些日志信息,将他们写入到Distribution数据库的msrepl_transactions和msrepl_commands中。具体的技术细节我会在以后的文章里介绍。

 

Msrepl_transactions中的每一条记录都有一个唯一标识xact_seqno,xact_seqno对应日志中的LSN。 所以可以通过xact_seqno推断出他们在publicationdatabase中的生成顺序,编号大的生成时间就晚,编号小的生成时间就早。

Distributionagent包含两个进程,reader和writer。 Reader负责从Distribution数据库中读取数据,Writer负责将reader读取的数据写入到订阅数据库.

reader是通过sp_MSget_repl_commands来读取Distribution数据库中(读取Msrepl_transactions表和Msrepl_Commands表)的数据

下面是sp_MSget_repl_commands的参数定义

CREATEPROCEDUREsys.sp_MSget_repl_commands 

@agent_id int, 

@last_xact_seqno varbinary(16), 

@get_count tinyint= 0,  -- 0 = no count, 1 = cmd and tran (legacy), 2 = cmd only 

@compatibility_level int= 7000000, 

@subdb_version int= 0, 

@read_query_size int=-1 

这个存储过程有6个参数,在Transactionalreplication中,只会使用前4个(并且第三个参数和第四个参数的值是固定不变的.分别为0和10000000)。下面是一个例子:

execsp_MSget_repl_commands 46,0x0010630F000002A900EA00000000,0,10000000

@agent_id表示Distributionagentid,每个订阅都会有一个单独的Distributionagent来处理数据。 带入@agent_id后,就可以找到订阅对应的publication和所有的article。

@last_xact_seqno表示上一次传递到订阅的LSN。

大致逻辑是:Reader读取分发数据库中LSN大于@last_xact_seqno的数据。 Writer将读取到的数据写入订阅,并更新相应的LSN.(subscription数据库的 MSreplication_subscriptions表的 transaction_timestamp列和Distribution数据库的msDistribution_history表的xact_seqno列)。然后Reader会继续用新的LSN来读取后续的数据,再传递给Writer,如此往复。

 

假设现在订阅段的数据已经更新到了0x0010630F000002A900EA00000000, 之后我们认为地向Msrepl_transactions表和Msrepl_Commands表插入了一批数据,xact_seqno对为0x0010630E000002A900EA00000000. 虽然这些数据的格式全部有效, 但Distributionagent是不会读取这些新加入的数据的,因为他们”太旧了”(他们的xact_seqno小于订阅的xact_senqo).

 

当Log reader停止时, 我们是可以添加article的。 并且相应的操作也会向msrepl_transactions和msrepl_commands插入数据,这些数据并不是由Log reader传递的,而是通过linkedserver直接向Distributor直接写入数据。 Distributionagent会读取这些数据,并更新相应的xact_seqno。而“Log reader停止”到“添加新article”这段期间发布产生的数据的xact_seqno是小于“添加新article”的xact_seqno,所以这些跟新会丢失。 说起来比较抽象,下面举个例子。

 

10:00到11:00期间publication database共生成了三条事务,对应的xact_seqno分别为

0x0010630F000006B9001E

0x0010630F000006F10004 

0x0010630F000006F20004

 

 

11:01将log reader停止

 

11:01~12:00期间publication database共生成了4条事务

0x0010630F000006F30004 

0x0010630F000007080005

0x0010630F000007D40205 

0x0010630F0000098C005C

 

但由于log reader没有启动,所以msrepl_transactions表内依然是三条数据. 12:01完成添加article的操作,msrepl_transactions内生成相应的记录0x00106310000000100100。

此时msrepl_transations内共有四条记录

此时订阅端的xact_seqno为0x0010630F000006F20004, distribution agent将读取大于0x0010630F000006F20004的数据,只有0x00106310000000100100符合要求。  订阅段完成的所有的数据同后,相应的xact_seqno为0x00106310000000100100

 

 

此时开启log reader,累计的数据传递完毕后,msrepl_transactions共有8条记录,但distribution agent不会再去读取之前的数据了…

 

影响

===

在有些极端情况下,即使您认为Log reader处于运行状态,还是会出现跟新丢失的情况。原因为在一些极端情况下,Log reader成为了deadlock的牺牲者被kill掉,但Log readeragentjob有自动retry的机制,会在一段时间后自动恢复, 所以您可能无法察觉。 是否出现死锁,您可以查询MSLog reader_history,MSrepl_errors

 

解决办法

===

将Distributor升级至sql servr 2012

如果您暂时没有办法升级,可以采用以下两种方法:

1) 添加一个新的发布,将新的article添加到发布中。

2) 在添加article前将Log reader和Distributionagent停止。添加完成后,启动Log reader,确认Log reader已经将之前的数据传送到了Distribution数据库后(可以使用tracertoken来确认Log reader是否已经完成同步), 启动Distributionagent。这样就可以避免数据丢失了。