發行項帶篩選的合併複製問題之訂閱者更新導致部份數據丟失

始,下边的内容是我在工作中,碰到的问题,并且拿出来进行分析和解决,是对其过程的描述。由于时间原因没有进行繁体简体的转换。在一些字眼或术语中,简体繁体之间可能存在些差异。 如有不明可以通过文章后面的留言发表意见和建议,或发Email与我联系。

問題描述


在SQL Server 2005(版本:Microsoft SQL Server 2005 - 9.00.4035.00 (Intel X86)),帶篩選的合併複製中,發現有這樣的一個問題,在訂閱者Insert數據,數據上傳到發行者,然後下載至訂閱者,發現訂閱者的部份數據被Delete,如圖描述:

image

下面就真實環境中的問題,創建一個模擬環境來分析說明。這裡列舉一個數據庫名為:ReplicationDB的同步情況:

image

各資料表之間的關係圖:

image

創建資料表結構和初始化數據腳本:

View Code
Use [ReplicationDB]
Go
/* 創建表結構Andy 2011-10-13*/

--(1/5) DataOwner
If object_id('[DataOwner]'Is Null
Begin
    Create Table [DataOwner]
    (
        [ID] smallint Identity(1,1Not Null ,
        [Owner] nvarchar(50Null 
    )
    Alter Table [DataOwner] Add Constraint [PK_DataOwner] Primary Key Clustered ([ID] Asc)
End

--(2/5) Data1
If object_id('[Data1]'Is Null
Begin
    Create Table [Data1]
    (
        [ID] uniqueidentifier Not Null ,
        [OwnerID] smallint Not Null 
    )
    Alter Table [Data1] Add Constraint [PK_Data1] Primary Key Nonclustered ([ID] Asc)
    Alter Table [Data1] Add Constraint [FK_Data1_DataOwner] Foreign Key ([OwnerID]References [DataOwner] ([ID])
End

--(3/5) Data2
If object_id('[Data2]'Is Null
Begin
    Create Table [Data2]
    (
        [ID] uniqueidentifier Not Null ,
        [ParentID] uniqueidentifier Not Null 
    )
    Alter Table [Data2] Add Constraint [PK_Data2] Primary Key Nonclustered ([ID] Asc)
    Alter Table [Data2] Add Constraint [FK_Data2_Data1] Foreign Key ([ParentID]References [Data1] ([ID])
End

--(4/5) SysDataType
If object_id('[SysDataType]'Is Null
Begin
    Create Table [SysDataType]
    (
        [ID] smallint Identity(1,1Not Null ,
        [Name] nvarchar(50Null 
    )
    Alter Table [SysDataType] Add Constraint [PK_SysDataType] Primary Key Nonclustered ([ID] Asc)
End

--(5/5) DataRelation
If object_id('[DataRelation]'Is Null
Begin
    Create Table [DataRelation]
    (
        [ID] uniqueidentifier not null ,
        [ParentID] uniqueidentifier Null ,
        [DataTypeID] smallint Null 
    )
    Alter Table [DataRelation] Add Constraint [PK_DataRelation] Primary Key Nonclustered ([ID] Asc)
    Alter Table [DataRelation] Add Constraint [FK_DataRelation_DataTypeID] Foreign Key ([DataTypeID]References [SysDataType] ([ID])
    Alter Table [DataRelation] Add Constraint [U_DataRelation_ParentID_DataTypeID] Unique Nonclustered ([ParentID],[DataTypeID])
End
Go

if Not Exists(Select 1 From SysDataType)
    Insert into SysDataType (Name)
        Select N'DataType1' Union All
        Select N'DataType2' Union All
        Select N'DataType3' 

if Not Exists(Select 1 From DataOwner)
    Insert into DataOwner ([Owner])
        Select N'PC1' Union All
        Select N'PC2' Union All
        Select N'PC3' 


go
--Procedures
Go

If object_id('sp_InsertData'Is not null  Drop Procedure sp_InsertData
Go
Create Procedure sp_InsertData
(
    @ID uniqueidentifier,
    @ParentID uniqueidentifier,
    @DataType1 Smallint=null,
    @DataType2 Smallint=null,
    @DataType3 Smallint=null
    
)
As
Begin Try
    Begin tran
        
        Insert into Data2(ID,ParentID)
                Values(@ID,@ParentID)
                
        
        ;With CTE_Data As
        (
            Select @DataType1 As DataTypeID Union All
            Select @DataType2 As DataTypeID Union All
            Select @DataType3 As DataTypeID 
        )
        Insert into DataRelation(ID,ParentID,DataTypeID)
              Select newid(),@ID,DataTypeID
                From CTE_Data
                Where DataTypeID is not null
        
              
    Commit Tran
End Try
Begin Catch
    Declare @Error nvarchar(2047)
    Set @Error=Error_message()
    Raiserror 50001 @Error
    Rollback Tran
End Catch
Go

If object_id('sp_DeleteData'Is not null  Drop Procedure sp_DeleteData
Go
Create Procedure sp_DeleteData
(
    @ID uniqueidentifier
    
)
As
Begin Try
    Begin tran
        
        Delete c 
            From Data1 As a
                Inner Join Data2 As b On b.ParentID=a.ID
                Inner Join DataRelation As c On c.ParentID=b.ID
            Where a.ID=@ID
         
         Delete b 
            From Data1 As a
                Inner Join Data2 As b On b.ParentID=a.ID
            Where a.ID=@ID
        
         Delete From Data1 Where ID=@ID
                     
    Commit Tran
End Try
Begin Catch
    Declare @Error nvarchar(2047)
    Set @Error=Error_message()
    Raiserror 50001 @Error
    Rollback Tran
End Catch
Go

腳本中的存儲過程sp_InsertData,sp_DeleteData 只是爲了分析過程應用到。

下面抽取【中央DB】à【分支DB】其中1條ReplicationDB同步例子來看在訂閱者Insert數據,會導致訂閱者部份數據給Delete掉,如圖描述(同步設置過程這裡忽略):

image

上图,在訂閱者向表DataRelation插入了5條記錄,並且成功上載至發行者。最後一行發現下載變更到訂閱者,對表DataRelation刪除了剛上載的5條記錄。導致了訂閱者DataRelation新增的數據丟失。在發行者上,對DataRelation新插入的數據是沒有被刪除的。

下面表格是補充例子中的幾個資料表的篩選條件:

image

在發行者上Insert,會不會發生類似的問題呢?在實際例子中是正常的,如图:

image

可以看出在在發行者向表DataRelation插入數據,下載變更到訂閱者過程中只有插入5條記錄的動作,沒看到在訂閱者的刪除動作。

問題分析


接下來,我們跟蹤合併複製的過程,來分析問題所在。SQL Server提供了一些系統表來為我們跟蹤複製的同步過程,如:

  • Sysmergearticles 描述發行項內容,包含有nickname發現項的暱稱,分析過程會用到這一字段關聯。
  • MSmerge_contents 包含已發行資料表中Inserted/Updated的行(row).
  • MSmerge_tombstone包含已發行資料表中deleted的行(row).
  • MSmerge_genhistory包含傳遞至發行者或訂閱者的變更集合.簡單說就是記錄發行者/訂閱者的Inserted/Updated/deleted歷史。
  • MSmerge_partition_groups資料表會針對給定資料庫中每個預先計算的資料分割,各儲存一個資料列。當前分析,暫不用應用不到。
  • MSmerge_current_partition_mappings 描述Inserted/Updated的分區信息,與MSmerge_contents 相關。
  • MSmerge_past_partition_mappings描述Updated/Deleted的分區信息,與MSmerge_contents 和MSmerge_tombstone 相關。

做個例子,在Microsoft SQL Server Management Studio(MSSMS),連接到訂閱者伺候器上,對ReplicationDB數據庫做個Insert數據的實例,類似于前面的操作:

use ReplicationDB
GO
Begin Tran
Insert into Data1(ID,OwnerID)
values ('80B370BA-446A-48D3-9EAC-2DE7FDC11997',3)


Exec sp_InsertData
@ID = 'ACE3BA3E-C6FE-49B5-8E19-AB2F0F894F09', -- uniqueidentifier
@ParentID = '80B370BA-446A-48D3-9EAC-2DE7FDC11997', -- uniqueidentifier
@DataType1 = 1, -- smallint
@DataType2 = 2, -- smallint
@DataType3 = default -- smallint

Exec sp_InsertData
@ID = 'AFD1B3B5-45D1-4F97-B4DA-AA6BF965C8BD', -- uniqueidentifier
@ParentID = '80B370BA-446A-48D3-9EAC-2DE7FDC11997', -- uniqueidentifier
@DataType1 = 1, -- smallint
@DataType2 = 2, -- smallint
@DataType3 = 3 -- smallint
Commit tran

Commit Tran代碼後面,填寫上跟蹤同步過程的T-SQL語句:

--跟蹤
Select * From sysmergearticles Where name='DataRelation'
Select * From MSmerge_contents Where tablenick=16720001 Order by generation Desc /*tablenick=25999002: OUPartitionAssignmentPlan*/
Select * From MSmerge_tombstone Where tablenick=16720001 Order by generation Desc /*tablenick=25999002: OUPartitionAssignmentPlan*/
Select * From MSmerge_genhistory Where art_nick=16720001 Order by coldate Desc,generation Desc /*art_nick=25999002: OUPartitionAssignmentPlan*/
Select a.*,b.name From MSmerge_current_partition_mappings As a Inner Join sysmergearticles As b On b.nickname=a.tablenick
Select * From MSmerge_past_partition_mappings Where tablenick=16720001 Order by generation Desc

對MSmerge, MSmerge_tombstone, MSmerge_genhistory, MSmerge_past_partition_mappings表,只是查詢出有關DataRelation表的數據,通過sysmergearticles表中的nickname列返回的值作為Where參考值。經過執行上面的兩個連續的腳本,可得到似下面的結果數據:

image

這裡捕捉到訂閱者MSmerge_genhistory表中有一條記錄generation=74,genstatus=1(已開啟狀態)

,來自訂閱者本身的Insert操作,影響記錄行為2,在MSmerge_contents表可以找到兩行generation=74的記錄行與之對應。

等到數據同步OK(連續執行的合併複製,默認訂閱間隔時間是1分鐘),我們再看回MSmerge_genhistory,MSmerge_contents,MSmerge_tombstone表的數據變化:

image

相隔大約1分鐘后,數據從發行者同步回到訂閱者,MSmerge_genhistory 發現多了1行generation=75的歷史記錄,而且在MSmerge_tombstone表中發現新加入5行generation=75的記錄行。說明有資料表DataRelation有數據被刪除了。

在訂閱者,我們發現了DataRelation 表Insert動作,引發同步過程的變化。在此期間,我們先不要對資料表DataRelation進行其他的更新刪除操作,接著來看在發行者,DataRelation 表的Insert 過程如何變化。

image

在發行者,MSmerge_genhistory表新插入有兩條記錄,coldate時間比較少的1條的genstatus=2,表示已關閉,並且已在另一個訂閱者產生,這個訂閱者為subscriber_number=2(可以通過Select subscriber_number,subscriber_server from sysmergesubscriptions查詢到subscriber_number對應的subscriber_serve數據)。是于generation=63,在這裡無法找到對應的關聯數據,可能是在上載到發行者的時候發生,這位置捕捉不到。

MSmerge_genhistory表中,coldate時間比較晚的1條generation=65,可以在MSmerge_contents,MSmerge_past_partition_mappings表中找到對應的數據。問題就是這裡,MSmerge_past_partition_mappings中怎麼會存在generation=65的數據,按正常,Insert數據的時候,在MSmerge_current_partition_mappings表有1條或多條記錄與之對應。出現在MSmerge_past_partition_mappings表,只有Update or Delete的時候才會發生。

跟蹤都這裡,開始的時候,我判斷在SQL Server 合併複製中的觸發器某一環節出現bug導致這樣的問題出現,還在SQL Server 2008 R2上測試沒問題。後來查證了MSDN上的資料,得到一些啓發:

在合併同步處理期間,依預設,發行項將按物件間相依性所需的順序處理,包括在基底資料表上定義的宣告式參考完整性 (DRI) 條件約束。處理包括列舉對資料表所作的變更,然後套用這些變更。如果沒有 DRI,但資料表發行項之間存在聯結篩選或邏輯記錄,發行項將以篩選和邏輯記錄所需的順序處理。透過 DRI、聯結篩選、邏輯記錄或其他相依性與任何其他發行項無關的發行項。

根據這一說明,先從DRI方面考慮,先去瞭解各個表的結構。因為沒有設置過邏輯記錄的順序處理編號,通過下面的T-SQL語句可以查詢:

Select name,processing_order From sysmergearticles

image

當看回表結構的時候,果真發現了問題所在:

Create Table [DataRelation]
(
[ID] uniqueidentifier not null ,
[ParentID] uniqueidentifier Null ,
[DataTypeID] smallint Null
)
Alter Table [DataRelation] Add Constraint [PK_DataRelation] Primary Key Nonclustered ([ID] Asc)
Alter Table [DataRelation] Add Constraint [FK_DataRelation_DataTypeID] Foreign Key ([DataTypeID]) References [SysDataType] ([ID])
Alter Table [DataRelation] Add Constraint [U_DataRelation_ParentID_DataTypeID] Unique Nonclustered ([ParentID],[DataTypeID])

資料表DataRelation,在字段ParentID上沒有創建外鍵約束。這裡沒有外鍵約束,同步過程中無法套用DRI條件約束,而且又沒設置邏輯記錄的順序處理,導致了問題所在。

提示:

如果想瞭解有關SQL Server 合併複製的跟蹤說明,可以參考聯機幫助文檔的《合併式複寫如何追蹤和列舉變更》:

http://msdn.microsoft.com/zh-cn/library/ms151789(v=SQL.90).aspx (簡體說明)

http://msdn.microsoft.com/zh-tw/library/ms151789(v=SQL.90).aspx (繁體說明)

http://msdn.microsoft.com/en-us/library/ms151789(v=SQL.90).aspx (英文說明)

《指定合併發行項的處理順序》:

http://msdn.microsoft.com/zh-cn/library/ms152469(v=SQL.90) (簡體說明)

http://msdn.microsoft.com/zh-tw/library/ms152469(v=SQL.90) (繁體說明)

術語:

DRI: declarative referential integrity 宣告式參考完整性,簡體意思是声明性引用完整性。

解決方法


根據前邊的分析,這裡列出幾個解決方法:

  • 增加外鍵約束,在資料表DataRelation字段ParentID上增加外鍵約束
  • 增加Check約束,在資料表DataRelation創建Check約束,檢查ParentID是否在資料表DATA2存在
  • 設置邏輯記錄的處理順序

增加外鍵約束:

在不重新設置同步複寫的前提下,在發行者實例上執行創建外鍵的腳本:

use ReplicationDB
Go
if object_id('FK_DataRelation_ParentID') Is null
Alter table DataRelation Add COnstraint FK_DataRelation_ParentID foreign Key(ParentID) References Data2(ID)
Go

增加Check約束:

在不重新設置同步複寫的前提下,增加Check約束需要按嚴格的步驟進行。先在發行者和訂閱者兩個實例上執行腳本(檢查資料表DataRelation中的ParentID列數據是否在資料表Data2中存在):

use ReplicationDB
Go
if object_id('Fn_CheckDataRelation_ParentID') Is Not Null
Drop Function Fn_CheckDataRelation_ParentID
GO
Create Function Fn_CheckDataRelation_ParentID
(
@ParentID uniqueidentifier
)
Returns bit
As
begin
Return(Select Case When Exists(Select 1 From Data2 Where ID=@ParentID) Then 1 Else 0 End)
End
Go

再在發行者實例上執行下面的創建Check約束腳本:

use ReplicationDB
Go
if object_id('CK_DataRelation_ParentID') Is null
Alter table DataRelation Add COnstraint CK_DataRelation_ParentID Check(dbo.Fn_CheckDataRelation_ParentID(ParentID)=1)
Go

設置邏輯記錄的處理順序:

在不重新設置同步複寫的前提下,在發行者實例上執行下面的腳本

use ReplicationDB
Go
Exec sys.sp_changemergearticle
@publication = 'Merge-PC3',
@article = 'DataOwner',
@property = 'processing_order',
@value = 10,
@force_invalidate_snapshot =0,
@force_reinit_subscription =0

Exec sys.sp_changemergearticle
@publication = 'Merge-PC3',
@article = 'Data1',
@property = 'processing_order',
@value = 20,
@force_invalidate_snapshot =0,
@force_reinit_subscription =0

Exec sys.sp_changemergearticle
@publication = 'Merge-PC3',
@article = 'Data2',
@property = 'processing_order',
@value = 30,
@force_invalidate_snapshot =0,
@force_reinit_subscription =0

Exec sys.sp_changemergearticle
@publication = 'Merge-PC3',
@article = 'DataRelation',
@property = 'processing_order',
@value = 40,
@force_invalidate_snapshot =0,
@force_reinit_subscription =0


上面腳本是通過SQL Server提供的系統函數sys.sp_changemergearticle 來設置各個表的同步處理順序:

image

當Insert數據的時候,會先處理資料表DataOwner,最後是處理資料表DataRelation。相反,當Delete數據的時候,會先處理資料表DataRelation,最後處理資料表DataOwner。

數值10,20,30,40 這樣的設置,也是有其意義的,當我們日後有新增一張資料表,它在合併複製中的處理順序是在資料表Data1和資料表Data2之間,那麼我們直接設置新增加的資料表處理順序為25,不需要再設置處理順序>20的資料表處理順序。這一點在真實應用環境中,是值得參考的。

小結:


綜上所述,在具有篩選的合併式複製中,在篩選條件中含有多表關聯時候,特別要注意表的結構,檢查篩選的字段是否存在外鍵、Check約束,或者是否有設置了合併複製的處理順序。在一些古老的數據庫機構中,我們無法保證篩選的字段存在外鍵或Check約束,這時候,個人認為優先考慮的是外鍵,無法使用外鍵的時候,考慮是否可以使用Check約束;在不能改變數據庫資料表結構,或者對各表的關係非常的熟悉,最後可以考慮使用合併複製中的處理順序功能。

附件,提供这篇文章的DOC格式文档, 喜欢的朋友下载调式:

/Files/wghao/發行項帶篩選的合併複製問題之----訂閱者更新導致部份數據丟失.doc 

原文地址:https://www.cnblogs.com/wghao/p/2212637.html