最近给客户做了基于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邮件,解决发布订阅中订阅库丢失数据的问题的更多相关文章

  1. SQL语句往Oracle数据库中插入日期型数据(to_date的用法)

    Oracle 在操作数据库上相比于其他的 T-sql 有微小的差别,但是在插入时间类型的数据是必须要注意他的 to_date 方法,具体的情况如下: --SQL语句往Oracle数据库中插入日期型数据 ...

  2. SqlServer执行Insert命令同时判断目标表中是否存在目标数据

    针对于已查询出数据结果, 且在程序中执行Sql命令, 而非数据库中的存储过程 INSERT INTO TableName (Column1, Column2, Column3, Column4, Co ...

  3. 6-03使用SQL语句一次型向表中插入多行数据

    通过将现有表中的数据添加到已存在的表中: INSERT INTO <表名><列名> SELECT<列名> FROM<源表名> 将UserInfo的数据添 ...

  4. sql如何向一个表中批量插入大量数据

    --如果是一个表插入另外一个表.insert into tb1 需要的列名 select 按照前面写上需要的列名 from tb2 --如果两表结构一样.insert into tb1 * selec ...

  5. 一、Sql Server 基础培训《进度1-建库建数据表(实际操作)》

    知识点: 1.建数据库示例参考 --创建一个数据库名为‘dbtest’ create database dbtest go --打开数据库 dbtest use dbtest go 2.建表示例参考 ...

  6. SQL Server批量向表中插入多行数据语句

    因自己学习测试需要,需要两个有大量不重复行的表,表中行数越多越好.手动编写SQL语句,通过循环,批量向表中插入数据,考虑到避免一致问题,设置奇偶行不同.个人水平有限,如有错误,还望指正. 语句如下: ...

  7. sql 同步2个表中的一个字段数据

    update PMS.tenant_contract a inner join(select id,home_id from PMS.owner_contract) c on a.id = c.id ...

  8. 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 ...

  9. SQL邮件服务(解决各种疑难杂症)+案例 + 使用SQLserver 邮件系统发送SQL代理作业执行警告

    首先你需要知道你要做的几部: 1 每个数据库都有自己的 SERVICE BROKER 很多SQL SERVER内部服务依赖它 2 启动 SERVICE BROKER 需要 1 STOP 你的 SQL  ...

随机推荐

  1. 模拟淘宝购物,运用cookie,记录登录账号信息,并且记住购物车内所选的商品

    1.登录界面 <%@ page language="java" contentType="text/html; charset=UTF-8" pageEn ...

  2. [大数据之Spark]——Actions算子操作入门实例

    Actions reduce(func) Aggregate the elements of the dataset using a function func (which takes two ar ...

  3. Android开发学习之路-Android6.0运行时权限

    在Android6.0以后开始,对于部分敏感的“危险”权限,需要在应用运行时向用户申请,只有用户允许的情况下这个权限才会被授予给应用.这对于用户来说,无疑是一个提升安全性的做法.那么对于开发者,应该怎 ...

  4. API调试工具推荐 - httpie

    API调试工具推荐 - httpie <HelloGitHub>第07期上面看到这个python项目,好东西 文档地址 但是安装的时候报错,google之后发现是个已知的bug,直接使用p ...

  5. ASP.NET Core 1.0 静态文件、路由、自定义中间件、身份验证简介

    概述 ASP.NET Core 1.0是ASP.NET的一个重要的重新设计. 例如,在ASP.NET Core中,使用Middleware编写请求管道. ASP.NET Core中间件对HttpCon ...

  6. OleDb Source component 用法

    OleDb Source component 主要是从DB中获取数据,传递给下游组件,OleDb Source component的强大之处在于 query data 的mode有四种,如图 Tabl ...

  7. JS 原型,检索,更新,引用等

    <script type="text/javascript"> var myObject=maker({ first:f, last:1, state:s, city: ...

  8. 深入理解DOM事件类型系列第二篇——键盘事件

    × 目录 [1]类型 [2]顺序 [3]按键信息[4]应用 前面的话 鼠标和键盘是电脑端主要的输入设备,上篇介绍了鼠标事件,本文将详细介绍键盘事件 类型 键盘事件用来描述键盘行为,主要有keydown ...

  9. How to write perfect C code

    Several days ago, I was involved in an argument about choice of C or C++. What I ignored was "l ...

  10. 【原生态】Http请求数据 与 发送数据

    今天项目组小弟居然问我怎么用java访问特定的地址获取数据和发送请求 Http请求都是通过输入输出流来进行操作的,首先要制定GET或者POST,默认是GET,在安全和数据量较大情况下请使用post 根 ...