使用SQLServer同义词和SQL邮件,解决发布订阅中订阅库丢失数据的问题
最近给客户做了基于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 字段。