最近給客戶做了基於SQLServer的發布訂閱的“讀寫分離”功能,但是某些表數據很大,經常發生某幾條數據丟失的問題,導致訂閱無法繼續進行。但是每次發現問題重新做一次發布訂閱又非常消耗時間,所以還得根據“復制監視器”的提示,找到丟失的數據,手工處理。
定位缺失數據
首先,找到出問題的同步語句,在發布服務器的“復制監視器”上事務訂閱的詳細信息里面,找到出錯的信息
嘗試的命令: if @@trancount > 0 rollback tran (事務序列號: 0x0000992600000D09007F00000000,命令 ID: 19) 錯誤消息: 應用復制的命令時在訂閱服務器上找不到該行。 (源: MSSQLServer,錯誤號: 20598) 獲取幫助: http://help/20598 應用復制的命令時在訂閱服務器上找不到該行。 (源: MSSQLServer,錯誤號: 20598)
然后在分發服務器上執行下面的SQL語句,
use distribution go sp_browsereplcmds '0x0000992600000D09007F00000000' ,'0x0000992600000D09007F00000000' go
根據命令ID(如上面的ID:19),找到具體的同步命令(Command列),類似於這樣的:
{CALL [dbo].[sp_MSdel_dboT_TODO] ('697e7cacf5354a36be1ae4cf50dcdaa6')}
這里是 訂閱庫上的 sp_MSdel_dboT_TODO 存儲過程,查看存儲過程定義知道參數是ID的值,這里說找不到要刪除的數據,那么我們在訂閱庫里面模擬增加這個ID的記錄即可。添加數據,
補錄數據
網上提供的解決方案是用一個工具生成差異的SQL數據然后給訂閱庫執行,但看了下覺得不是很方便,想起來SqlServer還提供一個 insert...from....語句,那么是否可以直接從發布數據庫查詢數據然后插入給訂閱數據庫呢?
可以使用同義詞從發布庫查詢過來插入到本地訂閱庫,請看下面具體過程:
先在訂閱庫上建立一個同義詞,比如下面為表 Biz_Customer 建立一個同義詞 Biz_Customer_Master,建立的時候,要求指定同義詞所在的服務器名稱,數據庫名稱,架構,表名稱等信息。
但是此時同義詞還不能直接使用,還需要建立“鏈接服務器”,具體過程如下:
EXEC sp_addlinkedserver @server='192.168.7.4',--被訪問的服務器別名(習慣上直接使用目標服務器IP,或取個別名如:JOY) @srvproduct='', @provider='SQLOLEDB', @datasrc='192.168.7.4' --要訪問的服務器 go EXEC sp_addlinkedsrvlogin '192.168.7.4', --被訪問的服務器別名(如果上面sp_addlinkedserver中使用別名JOY,則這里也是JOY) 'false', NULL, 'sa', --帳號 '1234567890' --密碼 go select * from sys.servers;
然后使用下面的SQL語句插入數據:
insert into [Biz_Customer] select * from Biz_Customer_Master where id='7B210173-7382-43EB-BC5E-0000C3BA564A'
查詢報錯,某個列的數據類型錯誤,打開表一看,原來是 發布庫上的表的字段順序跟訂閱庫上不一樣,因為當初做訂閱的時候,為了解決Timestamp 問題,將訂閱庫的Timestamp字段修改成了binary(8)類型,故訂閱庫上表的字段順序改變了。
此時,只需要在insert 和 select 語句上,指定相同順序的列就可以了。那么如何獲取表所有的列名稱?
很簡單,直接選擇某個表,新建查詢,生成的SQL語句就包含表所有的字段了。
最后正確的語句如下:
insert into [TB_Customer]([Id] ,[CustomerId] ,[Code] ,[Name] ,[BusinessId] ,[CreatedOn] ,[CreatedById] ,[ModifiedOn] ,[ModifiedById] ,[AppraiseTableType] ,[Timestamp] ) SELECT [Id] ,[CustomerId] ,[Code] ,[Name] ,[BusinessId] ,[CreatedOn] ,[CreatedById] ,[ModifiedOn] ,[ModifiedById] ,[AppraiseTableType] ,[Timestamp] FROM dbo.TB_Customer_Master where id='7B210173-7382-43EB-BC5E-0000C3BA564A';
經過這樣的方式,很方便的把發布庫的數據就補充到訂閱庫上了,之后,數據庫的發布訂閱錯誤就解決了。
修改訂閱庫存儲過程
但是,如果這樣的錯誤很多,每次都去靠手工修補數據是不行的,所以我們還需要找到訂閱庫上的系統存儲過程,做相應的修改。
- 修改數據,對應的存儲過程名字是 sp_MSupd_dboTableName ,所以我們可以拿到要操作的表名字:dbo.TableName
- 刪除數據,對應的存儲過程名字是 sp_MSdel_dboTableName,所以我們可以拿到要操作的表名字:dbo.TableName
如果是刪除數據,直接把存儲過程中的下面內容注釋:
if @@rowcount = 0 if @@microsoftversion>0x07320000 exec sp_MSreplraiserror 20598
如果是修改數據,首先也要把上面的內容注釋,然后在存儲過程的最后,添加下面這樣的代碼:
if @@rowcount = 0 begin insert into [TB_Customer]([Id] ,[CustomerId] ,[Code] ,[Name] ,[BusinessId] ,[CreatedOn] ,[CreatedById] ,[ModifiedOn] ,[ModifiedById] ,[AppraiseTableType] ,[Timestamp] ) SELECT [Id] ,[CustomerId] ,[Code] ,[Name] ,[BusinessId] ,[CreatedOn] ,[CreatedById] ,[ModifiedOn] ,[ModifiedById] ,[AppraiseTableType] ,[Timestamp] FROM [192.168.7.4].XXDB.dbo.Biz_Customer where id=@pkc1 end
這里沒有使用同義詞,而是直接使用遠程服務器名字加數據庫名字方式指定遠程表名字,當你要修改的存儲過程比較多,推薦采用這種方式而不是同義詞。
參數 @pkc1 是存儲過程使用的主鍵參數,每個存儲過程都是這樣的。
使用游標生成修改語句
但是,如果要修改從存儲過程很多,這樣一個個的去手工修改存儲過程是非常麻煩的,所以我們可以把上面的過程,寫一個T-SQL來輸出,我們使用游標來便利表所有的列,生成語句:
declare @ObjTbName varchar(100) declare @ColName varchar(100) declare @ColType varchar(50) declare @AllColName varchar(max) declare @SqlText varchar(max) set @ObjTbName='TB_Customer' set @SqlText ='insert into '+@ObjTbName+'(' DECLARE column_cursor CURSOR FOR SELECT COLUMN_NAME,DATA_TYPE FROM INFORMATION_SCHEMA.columns WHERE TABLE_NAME=@ObjTbName OPEN column_cursor FETCH NEXT FROM column_cursor into @ColName,@ColType set @AllColName ='['+ @ColName+']' WHILE @@FETCH_STATUS = 0 BEGIN -- This is executed as long as the previous fetch succeeds. --print 'Col Name:'+ @ColName +',Col Type:' + @ColType FETCH NEXT FROM column_cursor into @ColName,@ColType if @@FETCH_STATUS = 0 --print ' ,'+@ColName set @AllColName = @AllColName +',['+ @ColName+']' END CLOSE column_cursor DEALLOCATE column_cursor --print @AllColName set @SqlText =@SqlText + char(10)+ @AllColName +')' +CHAR(10) set @SqlText =@SqlText +'select '+CHAR(10) + @AllColName + CHAR(10) set @SqlText =@SqlText +' from [192.168.7.4].XXDB.dbo.'+@ObjTbName + ' where id=@pkc1 ' print '--if @@rowcount = 0' print '-- if @@microsoftversion>0x07320000' print '-- exec sp_MSreplraiserror 20598' print 'end ' print 'end ' print 'if @@rowcount = 0' print 'begin' print @SqlText print 'end '
將輸消息復制粘貼在要修改的存儲過程尾部即可。
修改並執行這個存儲過程,等訂閱代理重新執行這個存儲過程后,數據就過去了。
為了方便這個這個過程被程序調用,可以將它封裝成存儲過程,具體內容如下:
/* --創建數據庫復制的時候訂閱庫修改使用的存儲過程 --具體原理和使用,請參考博客文章: -- http://www.cnblogs.com/bluedoctor/p/5680582.html --作者:請參考博客文章作者 --時間:2016.7.20 --調用示例: exec BuildReplUpdateTable 'MainSqlServer','HRDB','TB_AuditOrgBalance',1 */ create procedure BuildReplUpdateTable @LinkServer varchar(100), @ObjDBName varchar(50), @ObjTbName varchar(100), @IsSp_MSupd bit as begin declare @ColName varchar(100) declare @ColType varchar(50) declare @AllColName varchar(max) declare @SqlText varchar(max) declare @TempText varchar(max) set @SqlText ='insert into '+@ObjTbName+'(' DECLARE column_cursor CURSOR FOR SELECT COLUMN_NAME,DATA_TYPE FROM INFORMATION_SCHEMA.columns WHERE TABLE_NAME=@ObjTbName OPEN column_cursor FETCH NEXT FROM column_cursor into @ColName,@ColType set @AllColName ='['+ @ColName+']' WHILE @@FETCH_STATUS = 0 BEGIN --print 'Col Name:'+ @ColName +',Col Type:' + @ColType FETCH NEXT FROM column_cursor into @ColName,@ColType if @@FETCH_STATUS = 0 set @AllColName = @AllColName +',['+ @ColName+']' END CLOSE column_cursor DEALLOCATE column_cursor set @SqlText =@SqlText + char(10)+ @AllColName +')' +CHAR(10) set @SqlText =@SqlText +'select '+CHAR(10) + @AllColName + CHAR(10) set @SqlText =@SqlText +' from ['+@LinkServer+'].['+@ObjDBName +'].[dbo].['+@ObjTbName + '] where id=@pkc1 ' if @IsSp_MSupd = 1 begin set @TempText='--if @@rowcount = 0'+CHAR(10)+ '-- if @@microsoftversion>0x07320000' +CHAR(10)+ '-- exec sp_MSreplraiserror 20598'+CHAR(10)+ 'end '+CHAR(10)+ 'end '+CHAR(10)+ 'if @@rowcount = 0'+CHAR(10)+ 'begin'+CHAR(10)+ @SqlText +CHAR(10)+ 'end ' select @TempText end else begin select @SqlText end end
雖然上面封裝的存儲過程可以很方便的生成修改訂閱存儲過程的部分修改語句,但是如果系統的表很多,目前還沒有做到批量的全部修改這些訂閱存儲過程,如果有一種方法及時通知DBA 哪些訂閱數據出現了問題,然后再按照前面的方法解決問題,就很方便了。這個功能,就是下面說的方法。
SQL郵件監控訂閱錯誤
SQL郵件提供了監視數據庫各種性能,問題,警報,然后發郵件通知管理員的功能,我們也可以利用這個功能,當訂閱庫發生數據同步錯誤,發一封郵件及時通知管理員,而不用實時去盯着“復制監視器”,查看問題了。
- 首先在“服務器”-管理-數據庫郵件節點上,配置一個數據庫郵件賬號,具體過程略,請參考其它相關文章;
- 然后,在Sql Server 代理-操作員功能上,添加一個操作員,填寫上通知該操作員的電子郵件賬號;
- 最后,在Sql Server 代理-作業節點,選擇用於訂閱的作業名稱,然后打開屬性窗口,進行如下設置:
如圖填寫上一個合適的重試次數,默認這是一個很大的數字,所以會重試很久都不會發出問題郵件。該問題我查找了很久才發現,大家不用走彎路了。
經過這樣的配置之后,出現訂閱同步問題,會收到大概如下的郵件內容:
作業運行: “DNXSQL-HRDB-XX發布-DNXSQL1-HRDB-3D57B9A6-207B-486A-8B5D-41125B68A876”已在 2016/7/22 14:00:46 運行 持續時間: 0 小時,8 分鍾,55 秒 狀態: 失敗 消息: 該作業失敗。 用戶 sa 調用了該作業。最后運行的是步驟 1 (運行代理。)。.
收到該郵件后,去服務器按照前面介紹的方法,解決此問題即可。
至此,DBA可以放心去干別的事情了。
(注:本文是一個業余DBA奮戰N多天,不斷嘗試總結,數次修訂本文而成,轉載請注明作者,並歡迎使用 SOD開發框架,它的數據庫工具將會提供自動生成修改的訂閱存儲過程的功能。)
補充:
如果訂閱庫少了某些記錄,可以通過下面類似的查詢解決:
update [MainSqlServer].[XXDB].[dbo].TB_Appropriation set ModifiedOn=GETDATE () where ID in ( SELECT ID FROM [MainSqlServer].[XXDB].[dbo].TB_Appropriation where id not in ( SELECT ID FROM [XXDB].[dbo].TB_Appropriation ) )
其中,MainSqlServer是發布服務器對應的鏈接服務器名稱,假設要補充缺失數據的表有一個ModifiedOn 字段。