使用SQLServer同义词和SQL邮件,解决发布订阅中订阅库丢失数据的问题
最近给客户做了基于SQLServer的发布订阅的“读写分离”功能,但是某些表数据很大,经常发生某几条数据丢失的问题,导致订阅无法继续进行。但是每次发现问题重新做一次发布订阅又非常消耗时间,所以还得根据“复制监视器”的提示,找到丢失的数据,手工处理。
定位缺失数据
首先,找到出问题的同步语句,在发布服务器的“复制监视器”上事务订阅的详细信息里面,找到出错的信息
尝试的命令:
if @@trancount > rollback tran
(事务序列号: 0x0000992600000D09007F00000000,命令 ID: ) 错误消息:
应用复制的命令时在订阅服务器上找不到该行。 (源: MSSQLServer,错误号: )
获取帮助: http://help/20598
应用复制的命令时在订阅服务器上找不到该行。 (源: MSSQLServer,错误号: )
然后在分发服务器上执行下面的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', --帐号
'' --密码
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”已在 // :: 运行
持续时间: 小时, 分钟, 秒
状态: 失败
消息: 该作业失败。 用户 sa 调用了该作业。最后运行的是步骤 (运行代理。)。.
收到该邮件后,去服务器按照前面介绍的方法,解决此问题即可。
至此,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 字段。
使用SQLServer同义词和SQL邮件,解决发布订阅中订阅库丢失数据的问题的更多相关文章
- SQL语句往Oracle数据库中插入日期型数据(to_date的用法)
Oracle 在操作数据库上相比于其他的 T-sql 有微小的差别,但是在插入时间类型的数据是必须要注意他的 to_date 方法,具体的情况如下: --SQL语句往Oracle数据库中插入日期型数据 ...
- SqlServer执行Insert命令同时判断目标表中是否存在目标数据
针对于已查询出数据结果, 且在程序中执行Sql命令, 而非数据库中的存储过程 INSERT INTO TableName (Column1, Column2, Column3, Column4, Co ...
- 6-03使用SQL语句一次型向表中插入多行数据
通过将现有表中的数据添加到已存在的表中: INSERT INTO <表名><列名> SELECT<列名> FROM<源表名> 将UserInfo的数据添 ...
- sql如何向一个表中批量插入大量数据
--如果是一个表插入另外一个表.insert into tb1 需要的列名 select 按照前面写上需要的列名 from tb2 --如果两表结构一样.insert into tb1 * selec ...
- 一、Sql Server 基础培训《进度1-建库建数据表(实际操作)》
知识点: 1.建数据库示例参考 --创建一个数据库名为‘dbtest’ create database dbtest go --打开数据库 dbtest use dbtest go 2.建表示例参考 ...
- SQL Server批量向表中插入多行数据语句
因自己学习测试需要,需要两个有大量不重复行的表,表中行数越多越好.手动编写SQL语句,通过循环,批量向表中插入数据,考虑到避免一致问题,设置奇偶行不同.个人水平有限,如有错误,还望指正. 语句如下: ...
- sql 同步2个表中的一个字段数据
update PMS.tenant_contract a inner join(select id,home_id from PMS.owner_contract) c on a.id = c.id ...
- SQL 教程数据库包括:Oracle, Sybase, SQL Server, DB2, Access 等等,您将学到如何使用 SQL 访问和处理数据系统中的数据
SQL 基础教程 SQL 教程 SQL 简介 SQL 语法 SQL select SQL distinct SQL where SQL AND & OR SQL Order By SQL in ...
- SQL邮件服务(解决各种疑难杂症)+案例 + 使用SQLserver 邮件系统发送SQL代理作业执行警告
首先你需要知道你要做的几部: 1 每个数据库都有自己的 SERVICE BROKER 很多SQL SERVER内部服务依赖它 2 启动 SERVICE BROKER 需要 1 STOP 你的 SQL ...
随机推荐
- Android 6.0 权限申请辅助 ----PermissionsHelper
Android 6.0 权限申请辅助 ----PermissionsHelper 项目地址:https://github.com/didikee/PermissionsHelper Android 的 ...
- weblogic配置数据源
启动weblogic 管理服务器,使用管理用户登录weblogic管理控制台 打开管理控制台后,在左侧的树形域结构中,选择服务->数据源. 在右侧的窗口中,选择 新建->一般数据源 ...
- html 生成pdf
HTML生成PDF(c#) 最近因为工作需要,小小的研究了一下HTML生成PDF的方法,这方面的内容很多,但要么是不尽如人意的方法,要么就是那种收费的类库!为了广大.neter的福利,把自己的一点小小 ...
- Laravel 5.3 请求处理管道详解
对于一个Web应用来说,在一个请求真正处理前,我们可能会对请求做各种各样的判断,然后才允许后续处理. 我们通常的做法: Script 01.php Script 02.php 优点:直观,容易理解 缺 ...
- show master/slave status求根溯源
show master/slave status分别是查看主数据库以及副数据库的状态,是一种能查看主从复制运行情况的方式. 这里仅仅讨论linux下的nysql5.7.13版本的执行情况 一.show ...
- 谈谈php里的DAO Model AR
这次要谈的3个关键字:DAO.Model.AR,是我们在做web应用时常见的几个概念,也被称作设计模式(design pattern),先简单看看它们的全拼和中文: DAO:Data Access O ...
- SQL Server 日期函数:EOMonth、DateFormat、Format、DatePart、DateName
一,月份的最后一天 函数 EOMonth 返回指定日期的最后一天 EOMONTH ( start_date [, month_to_add ] ) 1,对于start_date 有两种输入方式,能够转 ...
- 推荐21款最佳 HTML5 网页游戏
尽管 HTML5 的完全实现还有很长的路要走,但 HTML5 正在改变 Web,未来 HTML5 将把 Web 带入一个更加成熟和开放的应用平台.现在,越来越多的人尝试用 HTML5 来制作网页游戏等 ...
- Jetstrap 在线构建 Bootstrap 的工具
Jetstrap 是一个 100% 基于 Web 的 Twitter Bootstrap 构建工具,无需下载软件,只需登录并构建即可.并且别人可以访问你构建的产品.
- 深入学习jQuery选择器系列第四篇——过滤选择器之属性选择器
× 目录 [1]简单属性 [2]具体属性 [3]条件属性 前面的话 属性过滤选择器的过滤规则是通过元素的属性来获取相应的元素,对应于CSS中的属性选择器.属性过滤选择器可分为简单属性选择器.具体属性选 ...