关于数据同步的几种实现
概述
关于数据同步主要有两个层面的同步,一是通过后台程序编码实现数据同步,二是直接作用于数据库,在数据库层面实现数据的同步。通过程序编码实现数据同步,其主要的实现思路很容易理解,即有就更新,无则新增,其他情况日志记录,就不做过多的介绍,这里主要讲述的是第二个层面的数据同步,即在数据库层面实现数据同步。
数据库层面的数据库同步主要有三种方式:通过发布/订阅的方式实现同步,通过SQL JOB方式实现数据同步,通过Service Broker 消息队列的方式实现数据同步。
下面分别就这三种数据同步方式,一一详解。
1. 通过发布/订阅的方式实现同步
发布/订阅是Sql Server自带的一种数据库备份的机制,通过该机制可以快速的实现数据的备份同步,不用编写任何的代码。
此种数据同步的方式存在的以下的一些问题:
- 表结构不能更改,同步双方的表结构必须一致,一旦表结构发生更改需要重新生成数据库快照。
- 对于大数据量的同步没有可靠的保证。
- 网络不稳定的情况下同步也不能保证。
总的来说,这种数据备份同步的方式,在表结构一致、数据量不是特别大的情况下还是非常高效的一种同步方式。
网上有很多的关于如何使用发布/订阅的方式实现数据同步的操作示例,这里就不再重复的演示了,有兴趣想要了解的朋友可以参考下面这篇文章:
http://kb.cnblogs.com/page/103975/
2. 通过SQL JOB方式实现数据同步
通过Sql Job定时作业的方式实现同步其基本原理就是通过目标服务器和源服务器的连接,然后通过编写Sql语句,从源服务器中读取数据,再更新到目标服务器。
这种数据同步的方式比较灵活。创建过sql定时作业之后,主要需要执行以下关键的两步。
2.1 创建数据库连接(一般作为定时作业执行的第一步)
不同数据库之间的连接可以通过系统的存储过程实现。下面就直接用一个示例来讲一下如何创建数据库连接。
--添加一个连接
--系统存储过程sp_addlinkedserver 参数:
----------------------1:目标服务器的IP或别名,本例中为:'WIN-S1PO3UA6J7I';----------------------2:'' (srvproduct,默认);
----------------------3:'SQLOLEDB'(provider,默认值);
----------------------4:目标服务器的IP或别名(datasrc),本例中为:'WIN-S1PO3UA6J7I'
exec sp_addlinkedserver 'WIN-S1PO3UA6J7I','','SQLOLEDB','WIN-S1PO3UA6J7I'
--添加登录用户连接
--系统存储过程sp_addlinkedsrvlogin 参数:
----------------------1:目标服务器的IP或别名,本例中为:'WIN-S1PO3UA6J7I';
----------------------2:'false',默认值;
----------------------3:null,默认值;
----------------------4:'sa',登录用户名;
----------------------5:'pass@word1',登录密码;
exec sp_addlinkedsrvlogin 'WIN-S1PO3UA6J7I','false',null,'sa','pass@word1'
创建数据库连接主要用到了以上的两个存储过程,但是在实际操作的过程中可能会遇到“仍有对服务器XXX的远程登录或连接登录问题”这样的问题,如果遇到此类问题,在执行上边的添加连接和登录用户连接之前还需要先删除某个已存在的链接,具体如下:
--系统存储过程sp_droplinkedsrvlogin 参数:
----------------------1:目标服务器的IP或别名,本例中为:'WIN-S1PO3UA6J7I';----------------------2:null
exec sp_droplinkedsrvlogin 'WIN-S1PO3UA6J7I',null
--系统存储过程sp_dropserver 参数:
----------------------1:目标服务器的IP或别名,本例中为:'WIN-S1PO3UA6J7I'
exec sp_dropserver 'WIN-S1PO3UA6J7I'
2.2 使用SQL 语句 实现数据同步
主要的同步思路:
1:在目标数据库中先清空要同步的表的数据
2:使用insert into Table (Cloumn....) select Column..... from 服务器别名或IP.目标数据库名.dbo.TableName 的语法将数据从源数据库读取并插入到目标数据库
Truncate table Org_DepartmentsExt –删除现有系统中已存在的部门表
insert into Org_DepartmentsExt –从名为WIN-S1PO3UA6J7I的服务器上的DBFrom数据库上获取源数据,并同步到目标数据库中
(
[DeptID]
,[DeptStatus]
,[DeptTel]
,[DeptBrief]
,[DeptFunctions]
)
SELECT [DeptID]
,[DeptStatus]
,[DeptTel]
,[DeptBrief]
,[DeptFunctions]
FROM [WIN-S1PO3UA6J7I].[DBFrom].[dbo].[Org_DepartmentsExt]
以上这两步便是通过SQL Job实现数据同步的关键步骤,在完成以上两步之后,如果没有其他的表要进行同步,则可创建同步计划以完善定时作业。带作业创建完后,便可以执行。
这里主要只是演示了通过Sql Job方式实现数据同步的关键步骤。网上有很多具体的实例演示。有兴趣的朋友可以参考以下文章进行练习检验:
http://www.cnblogs.com/tyb1222/archive/2011/05/27/2060075.html
3. 通过SQL Server Service Broker 消息队列的方式实现数据同步
3.1 SQL Server Service Broker概述
SQL Server Service Broker 是数据库引擎的组成部分,为 SQL Server 提供队列和可靠的消息传递。既可用于使用单个 SQL Server 实例的应用程序,也可用于在多个实例间分发工作的应用程序。
在单个 SQL Server 实例内,Service Broker 提供了一个功能强大的异步编程模型。数据库应用程序通常使用异步编程来缩短交互式响应时间,并增加应用程序总吞吐量。
在多个SQL Server实例之间Service Broker 还可以提供可靠的消息传递服务。Service Broker 可帮助开发人员通过称为服务的独立、自包含的组件来编写应用程序。需要使用这些服务中所包含功能的应用程序可以使用消息来与这些服务进行交互。Service Broker 使用 TCP/IP 在实例间交换消息。Service Broker 中所包含的功能有助于防止未经授权的网络访问,并可以对通过网络发送的消息进行加密。
3.2 具体的实现演示
在这一小节里,主要是通过一个完整的数据同步的流程向大家演示,如何实现同一个数据库实例不同数据库的数据同步。关于不同的数据库实例间的数据库的数据同步整体上跟同一个实例的数据库同步是一样的,只不过在不同的数据库实例间同步时还需启用传输安全、对话安全,创建路由、远程服务绑定等额外的操作。
这里边用到了大量的SQL Server XML的东西,如果有不理解的地方可以参考以下链接:http://www.cnblogs.com/Olive116/p/3355840.html
这是我在做技术准备时,自己的一点学习记录。
下面就是具体的实现步骤:
3.2.1为数据库启动Service Broker活动
这一步主要是用来对要进行数据同步的数据启用Service Broker 活动,并且授信。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
USE master GO --如果数据库DBFrom、DBTo不存在,则创建相应的数据库 IF NOT EXISTS (SELECT name FROM sys.databases WHERE name = 'DBFrom' ) CREATE DATABASE DBFrom GO IF NOT EXISTS (SELECT name FROM sys.databases WHERE name = 'DBTo' ) CREATE DATABASE DBTo GO --分别为该数据库启用Service Broker活动并且授权信任 ALTER DATABASE DBFrom SET ENABLE_BROKER GO ALTER DATABASE DBFrom SET TRUSTWORTHY ON GO ALTER AUTHORIZATION ON DATABASE::DBFrom To sa GO ALTER DATABASE DBTo SET ENABLE_BROKER GO ALTER DATABASE DBTo SET TRUSTWORTHY ON GO ALTER AUTHORIZATION ON DATABASE::DBTo TO sa GO |
3.2.2 创建数据库主密匙
这一步主要用来创建数据库主密匙,上边有提到Service Broker可以对要发送的消息进行加密。
1
2
3
4
5
6
7
8
9
10
11
|
Use DBFrom go create master key encryption by password= 'pass@word1' go Use DBTo go create master key encryption by password= 'pass@word1' go |
3.2.3 创建消息类型、协定
这里主要用来创建消息类型和消息协定,源数据库和目标数据库的消息类型和协定都要一致。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
Use DBFrom go --数据同步—消息类型 create message type [http: //oa.founder.com/Data/Sync] validation=well_formed_xml go --数据同步--错误反馈消息类型 create message type [http: //oa.founder.com/Data/Sync/Error] validation=well_formed_xml go --数据同步协议 create contract[http: //oa.founder.com/Data/SyncContract] ( [http: //oa.founder.com/Data/Sync] sent by initiator, [http: //oa.founder.com/Data/Sync/Error] sent by target ) go Use DBTo go --数据同步—消息类型 create message type [http: //oa.founder.com/Data/Sync] validation=well_formed_xml go --数据同步--错误反馈消息类型 create message type [http: //oa.founder.com/Data/Sync/Error] validation=well_formed_xml go --数据同步协议 create contract[http: //oa.founder.com/Data/SyncContract] ( [http: //oa.founder.com/Data/Sync] sent by initiator, [http: //oa.founder.com/Data/Sync/Error] sent by target ) Go |
创建过之后效果如下图:
3.2.4 创建消息队列
这里主要用来创建消息队列,源数据库和目标数据库都要创建,队列名字可以自主命名。
1
2
3
4
5
6
7
8
9
10
11
|
use DBFrom go create queue [DBFrom_DataSyncQueue] with status= on go use DBTo go create queue [DBFrom_DataSyncQueue] with status= on go |
创建之后效果如下图:
3.2.5 创建数据同步服务
这里我们通过利用上边创建的消息协定和消息队列来创建数据同步的服务。
1
2
3
4
5
6
7
8
9
10
11
12
|
use DBFrom go create service [http: //oa.founder.com/DBFrom/Data/SyncService] on queue dbo.[DBFrom_DataSyncQueue]([http: //oa.founder.com/Data/SyncContract]) go --数据同步服务 use DBTo go create service [http: //oa.founder.com/DBTo/Data/SyncService] on queue dbo.[DBFrom_DataSyncQueue]([http: //oa.founder.com/Data/SyncContract]) go |
创建后效果如下图:
3.2.6 在源数据库上创建服务配置列表
这里需要在源数据库上创建一个服务配置列表,主要用来保存之前创建过的服务名称,本例只是用来演示,所以只创建了一个服务,只能是同步一个数据表,如果有多个数据表需要同步,则需创建多个服务,所以这里创建一个服务配置列表,用来存储多个服务的服务名称。
需要注意的是,下面的脚本在执行完创建表的操作之后又插入了一条数据,也就是上边我们创建的服务名,如果有多个服务的话,依次插入该表即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
use DBFrom go --同步数据--目标服务配置 create table SyncDataFarServices ( ServiceID uniqueidentifier, ServiceName nvarchar(256) ) go --将上边创建的服务名,插入此表中 insert into SyncDataFarServices (ServiceID,ServiceName) values go |
效果如下图:
3.2.7 发送数据同步消息
这里创建了一个存储过程主要用来发送同步消息,该消息内容主要包括操作类型、主键、表名、正文内容,分别对应@DMLType,@PrimaryKeyField,@TableName,@XMLData。然后通过创建一个游标来条的读取上边创建的服务列表中的列表信息,向不同的服务发送消息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
Use DBFrom go --发送同步数据消息 Create procedure UP_SyncDataSendMsg ( @PrimaryKeyField nvarchar(128), @TableName nvarchar(128), @DMLType char (1), @XMLData xml ) as begin SET @XMLData.modify( 'insert <DMLType>{sql:variable("@DMLType")}</DMLType> as first into /' ); SET @XMLData.modify( 'insert <PrimaryKeyField>{sql:variable("@PrimaryKeyField")}</PrimaryKeyField> as first into /' ); SET @XMLData.modify( 'insert <Table>{sql:variable("@TableName")}</Table> as first into /' ); DECLARE FarServices CURSOR FOR SELECT ServiceName FROM SyncDataFarServices; open FarServices declare @FarServiceName nvarchar(256); fetch FarServices into @FarServiceName; while @@FETCH_STATUS=0 begin begin Transaction declare @Conv_Handler uniqueidentifier begin DIALOG conversation @Conv_Handler --开始一个会话 from service [http: //oa.founder.com/DBFrom/Data/SyncService] to service @FarServiceName on contract [http: //oa.founder.com/Data/SyncContract]; send on conversation @Conv_Handler Message type [http: //oa.founder.com/Data/Sync](@XMLData); fetch FarServices into @FarServiceName; commit; end close FarServices; deallocate FarServices; end go |
3.2.8 创建数据同步异常信息记录表
这里创建该表主要用来记录在数据同步过程中出现的异常信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
use DBFrom go create Table dbo.SyncException ( ErrorID uniqueidentifier, ConversationHandleID uniqueidentifier, ErrorNumber int , ErrorSeverity int , ErrorState int , ErrorProcedure nvarchar(126), ErrorLine int , ErrorMessage nvarchar(2048), MessageContent nvarchar(max), CreateDate DateTime ) go --修改异常信息记录表 alter table dbo.SyncException add PrimaryKeyField nvarchar(128), TableName nvarchar(128), DMLType char (1), DBName nvarchar(128) Go |
效果如下图:
3.2.9 数据同步反馈
这里主要用来在源数据库中接收队列中的消息,将同时出错的信息,解析一下,然后插入到异常信息记录表里边。
--数据同步回馈
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
|
use DBFrom go create procedure UP_SyncDataFeedback as begin set nocount on --会话变量声明 declare @ConversationHandle uniqueidentifier;--会话句柄 declare @Msg_Body nvarchar(max); declare @Msg_Type_Name sysname; --变量赋值 while (1=1) begin begin transaction --从队列中接收消息 waitfor ( receive top(1) @Msg_Type_Name=message_type_name, @ConversationHandle=[conversation_handle], @Msg_Body=message_body from dbo.[DBFrom_DataSyncQueue] ),timeout 1000 --如果接收到消息处理,否则跳过 if (@@ROWCOUNT<=0) break ; end conversation @ConversationHandle; begin declare @DataSource xml; set @DataSource=Convert(xml,@Msg_Body); insert into dbo.SyncException(ErrorID,ConversationHandleID,ErrorNumber,ErrorSeverity,ErrorState,ErrorProcedure,ErrorLine,ErrorMessage,<br>PrimaryKeyField,TableName,DMLType,MessageContent,DBName,CreateDate) select NEWID(),@ConversationHandle, T.c.value( './@ErrNumber' , 'INT' ), T.c.value( './@ErrSeverity' , 'INT' ), T.c.value( './@ErrState' , 'INT' ), T.c.value( './@ErrProcedure' , 'Nvarchar(126)' ), T.c.value( './@ErrLine' , 'INT' ), T.c.value( './@ErrMessage' , 'nvarchar(2048)' ), T.c.value( './@PrimaryKeyField' , 'nvarchar(128)' ), T.c.value( './@TableName' , 'nvarchar(128)' ), T.c.value( './@DMLType' , 'char(1)' ), T.c.value( './@MessageContent' , 'nvarchar(max)' ), T.c.value( './@DBName' , 'nvarchar(128)' ), GETDATE() from @DataSource.nodes( '/row' ) as T(c); end end conversation @ConversationHandle; commit Transaction; end end commit; go |
3.2.10对Service Broker队列使用内部激活,并指定将调用的存储过程
这里主要用来激活源数据库的消息队列,并为其指定调用的存储过程,即上边3.2.9 中创建的存储过程。
1
2
3
4
5
6
7
8
9
10
11
|
--对Service Broker队列使用内部激活,并指定将调用的存储过程 use DBFrom go alter queue dbo.DBFrom_DataSyncQueue with activation ( status= on , max_queue_Readers=1, procedure_name=UP_SyncDataFeedback, execute as owner ); Go |
3.2.11 在源数据库中为需要同步的数据表创建触发器
这里就以用户表为例,具体操作如下,这里通过查询系统的Inserted和Deleted临时表来判断执行同步的操作类型是更新(U)、新增(A)还是删除(D),最后调用3.2.7 中创建的存储过程来对数据进行处理并发送。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
use DBFrom Go --用户信息同步 Create Trigger UT_DataSync_Users on dbo.Org_Users after insert,update,delete as set nocount on ; --变量声明 declare @PrimaryKeyField nvarchar(128),@TableName nvarchar(128),@DMLType char (1); declare @InsertCount int ,@DeleteCount int ; declare @XMLData xml; --变量赋值 set @PrimaryKeyField= 'ID' --组合主键,多个主键使用 "," 隔开 set @TableName= 'Org_Users' set @InsertCount=( select COUNT(*) from inserted) set @DeleteCount=( select COUNT(*) from deleted) if @InsertCount=@DeleteCount and @InsertCount<>0 ----Update begin select @XMLData=( select * from inserted For xml raw,binary base64,ELEMENTS XSINIL); set @DMLType= 'U' ; end else if (@InsertCount<>0 and @DeleteCount=0) ----Insert begin select @XMLData=( select * from inserted for xml raw ,Binary base64,ELEMENTS XSINIL) set @DMLType= 'A' ; end else ----Delete begin select @XMLData=( select * from deleted for xml raw,binary base64,ELEMENTS XSINIL) set @DMLType= 'D' ; end if (@XMLData is not null ) begin exec UP_SyncDataSendMsg @PrimaryKeyField,@TableName,@DMLType,@XMLData; end go |
3.2.12 目标数据库中创建,字符分割函数
该函数主要是用来进行字符分割,用来处理主键有多个字段的情况。
--目标数据库
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
use DBTo go --转换用‘,'分割的字符串@str create Function dbo.uf_SplitString ( @str nvarchar(max), @Separator nchar(1)= ',' ) returns nvarchar(2000) as begin declare @Fields xml;--结果字段列表 declare @Num int ;-----记录循环次数 declare @Pos int ;-----记录开始搜索位置 declare @NextPos int ;--搜索位置临时变量 declare @FieldValue nvarchar(256);--搜索结果 set @Num=0; set @Pos=1; set @Fields=CONVERT(xml, '<Fields></Fields>' ); while (@Pos<=LEN(@Str)) begin select @NextPos=CHARINDEX(@Separator,@Str,@Pos) if (@NextPos=0 OR @NextPos is null ) select @NextPos=LEN(@Str)+1; select @FieldValue=RTRIM(ltrim(substring(@Str,@Pos,@NextPos-@Pos))) select @Pos=@NextPos+1 set @Num=@Num+1; if @FieldValue<> '' begin set @Fields.modify( 'insert <Field>{sql:variable("@FieldValue")}</Field> as last into /Fields[1]' ); end end return Convert(nvarchar(2000),@Fields); end go |
3.2.13 将解析过的消息信息,根据操作类型的不同同步到数据表中
这是所有的数据同步中最关键也是最复杂的一步了,在整个开发的过程中,大部分时间都花在这上边了,具体的操作都在下面解释的很清楚了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
|
--将XML数据源中的数据同步到数据表中(包括增删改) Use DBTo go create function dbo.UF_XMLDataSourceToSQL ( @DataSource XML,--数据源 @TableName varchar(128),--同步数据表名称 @PrimaryKeyField varchar(128),--需要同步的表的主键,主键为多个时用‘,'隔开 @DMLType char (1) --A:新建;U:编辑;D:删除 ) returns nvarchar(4000) as begin --变量声明及数据初始化 --声明数据表@TableName列Column相关信息变量 declare @ColumnName nvarchar(128),@DataType nvarchar(128),@MaxLength int ; --声明用于拼接SQL的变量 declare @FieldsList nvarchar(4000),@QueryStatement nvarchar(4000); declare @Sql nvarchar(4000); declare @StrLength int ; --变量初始化 set @FieldsList= ' ' ;--初始化变量不为 null ,否则对变量使用 '+=' 操作符无效 set @QueryStatement= ' ' ; --主键信息,根据参数求解如:<Fields><Field>ID1</Field><Field>ID2</Field></Fields> declare @PKs xml; --当前字段是否主键-在‘更新’,‘删除’同步数据时使用 declare @IsPK nvarchar(128); --初始化游标--游标内容包括目标数据表TableName列信息 DECLARE ColumnNameList CURSOR FOR SELECT COLUMN_NAME,DATA_TYPE,CHARACTER_MAXIMUM_LENGTH FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME=@TableName AND <br>DATA_TYPE<> 'xml' ; --数据处理 if @DMLType= 'A' --插入数据 begin open ColumnNameList fetch ColumnNameList into @ColumnName,@DataType,@MaxLength; while @@FETCH_STATUS=0 begin --判断数据源列中是否存在属性:@ColumnName --判断数据源列中是否存在--元素:@ColumnName If @DataSource.exist( '/row/*[local-name()=sql:variable("@ColumnName")]' )=1 begin --拼接SQL set @FieldsList+=(@ColumnName+ ',' ); set @QueryStatement+=( 'T.c.value(' '(./' +@ColumnName+ '[not(@xsi:nil)])[1]' ',' '' +@DataType);--元素读取(包含空值情况) if @MaxLength is not null and @MaxLength<>-1 begin set @QueryStatement+= '(' +CONVERT(nvarchar,@MaxLength)+ ')' ; end else if @MaxLength=-1 and @DataType<> 'xml' --已调整 begin set @QueryStatement+= '(MAX)' ; end set @QueryStatement+=( '' ') as ' +@ColumnName+ ',' ); end fetch ColumnNameList into @ColumnName,@DataType,@MaxLength end close ColumnNameList; deallocate ColumnNameList; set @StrLength=LEN(@FieldsList); --去掉@FieldsList结尾的’,' set @FieldsList=SUBSTRING(@FieldsList,1,@StrLength-1); set @StrLength=LEN(@QueryStatement); --去掉@QueryStatement结尾的’,' set @QueryStatement=SUBSTRING(@QueryStatement,1,@StrLength-1); set @Sql=N 'insert into ' +@TableName+ '(' +@FieldsList+ ') select ' +@QueryStatement+ ' from @DataSource.nodes(' '/row/' ') as T(c)' ; end else if @DMLType= 'U' --更新数据 begin --更新语句 where 后的条件表达式 declare @Condition nvarchar(1000); set @Condition= ' ' ; set @PKs=CONVERT(xml,dbo.uf_SplitString(@PrimaryKeyField, ',' )); Open ColumnNameList fetch ColumnNameList into @ColumnName,@DataType,@MaxLength; while @@FETCH_STATUS=0 begin --判断数据源列中是否存在元素:@ColumnName if @DataSource.exist( '/row/*[local-name()=sql:variable("@ColumnName")]' )=1 begin set @IsPK= null ; SELECT @IsPk=Fs.F FROM (SELECT T.c.value( '.[text()]' , 'Nvarchar(128)' ) AS F FROM @PKs.nodes( '/Fields/Field' ) AS T(c))Fs Where Fs.F=@ColumnName if @IsPK is null or @IsPK= '' begin --非主键,更新字段值 set @FieldsList+=(@ColumnName+ '=Source.' +@ColumnName+ ',' ); end else begin --主键,作为要更新条件 set @Condition+=@TableName+ '.' +@ColumnName+ '=Source.' +@ColumnName+ ' And ' ; end --XML查询 set @QueryStatement+=( 'T.c.value(' '(./' +@ColumnName+ '[not(@xsi:nil)])[1]' ',' '' +@DataType);--元素读取(包含空值情况) if @MaxLength is not null and @MaxLength<>-1 begin set @QueryStatement+= '(' +CONVERT(nvarchar,@MaxLength)+ ')' ; end else if @MaxLength=-1 and @DataType<> 'xml' begin set @QueryStatement+= '(max)' ; end set @QueryStatement+=( '' ') as ' +@ColumnName+ ',' ); end fetch ColumnNameList Into @ColumnName,@DataType,@MaxLength end close ColumnNameList; Deallocate ColumnNameList; --去掉@FieldsList结尾的 ',' set @StrLength=LEN(@FieldsList); set @FieldsList=SUBSTRING(@FieldsList,1,@StrLength-1); --去掉@QueryStatement结尾的 ',' set @StrLength=LEN(@QueryStatement); set @QueryStatement=SUBSTRING(@QueryStatement,1,@StrLength-1); --去掉@Condition结尾的‘and' set @StrLength=LEN(rtrim(@Condition)); set @Condition=SUBSTRING(rtrim(@Condition),1,@StrLength-3); set @Sql=N 'USE DBTo ; update ' +@TableName+ ' set ' +@FieldsList+ ' from (select ' +@QueryStatement+' from @DataSource.nodes( '' /row '' ) as T(c)) Source where '+@Condition; end else if @DMLType= 'D' --删除数据 begin --更新语句 where 后的条件表达式 declare @LinkField nvarchar(1000); set @LinkField= ' ' ; set @PKs=CONVERT(xml,dbo.uf_SplistString(@PrimaryKeyField, ',' )); open ColumnNameList fetch ColumnNameList into @ColumnName,@DataType,@MaxLength; while @@FETCH_STATUS=0 begin if @DataSource.exist( 'row/*[local-name()=sql:variable("@ColumnName")]' )=1 begin set @IsPK= null ;--初始化 --当前字段是否为主键 select @IsPK=Fs.F from ( select T.c.value( '.[text()]' , 'nvarchar(128)' ) as F from @PKs.nodes( '/Fields/Field' ) as T(c))Fs where Fs.F=@ColumnName --主键 if @IsPK is not null and @IsPK<> '' begin --主键删除条件 set @LinkField+= 'Target.' +@ColumnName+ '=Source.' +@ColumnName+ ' And ' ; --XML 查询 set @QueryStatement+=( 'T.c.value(' '(./' +@ColumnName+ '[not(@xsi:nil)])[1]' ',' '' +@DataType);--元素读取(包含空值情况) if (@MaxLength is not null and @MaxLength<>-1) begin set @QueryStatement+= '(' +CONVERT(nvarchar,@MaxLength)+ ')' ; end else if @MaxLength=-1 and @DataType<> 'xml' begin set @QueryStatement+= '(max)' ; end set @QueryStatement+=( '' ') as ' +@ColumnName+ ',' ); end end fetch ColumnNameList into @ColumnName,@DataType,@MaxLength end close ColumnNameList; deallocate ColumnNameList; --去除@QueryStateMent结尾的 ',' set @StrLength=LEN(@QueryStatement); set @QueryStatement=SUBSTRING(@QueryStatement,1,@StrLength-1); --去除@LinkField 结尾的’Add‘ set @StrLength=LEN(rtrim(@LinkField)); set @LinkField=SUBSTRING(rtrim(@LinkField),1,@StrLength-3); set @Sql=N 'Delete from ' +@TableName+ ' from ' +@TableName+ ' as Target inner join (select ' +@QueryStatement+ ' from @DataSource.nodes(' '/row' ') as T(c))<br> Source on ' +@LinkField; end Return @Sql-- 'hello' end go |
3.2.14 解析并处理从队列中读取的消息
这里主要用来读取队列中的消息,并将消息进行处理,最终处理成一定的格式,并调用3.2.13中的存储过程,将数据同步到数据库中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
|
--将数据同步到数据表中 create procedure UP_SyncDataToTable as begin set nocount on --会话变量声明 declare @ConversationHandle uniqueidentifier;--会话句柄 declare @Msg_Body nvarchar(max); declare @Msg_Type_Name sysname; declare @ErrorNumber int ; --变量赋值 while (1=1) begin begin transaction --从队列中接收消息 waitfor ( receive top(1) @Msg_Type_Name=message_type_name, @ConversationHandle=[conversation_handle], @Msg_Body=message_body -- from dbo.[DBTo_DataSyncQueue] from dbo.[DBFrom_DataSyncQueue] ),timeout 500 --如果接收到消息-处理,否则跳过 if @@ROWCOUNT<=0 begin break ; end begin --声明变量 declare @DMLType char (1); declare @PrimaryKeyField nvarchar(128),@TableName nvarchar(128),@Sql nvarchar(4000); declare @DataSource xml --受影响的行数 declare @EffectRowCount int ; declare @ErrMsg xml; begin try --变量赋值 set @DataSource=convert(xml,@Msg_Body);--数据源 set @PrimaryKeyField=@DataSource.value( '(/PrimaryKeyField)[1][text()]' , 'nvarchar(128)' );--主键列表 set @TableName=@DataSource.value( '(/Table)[1][text()]' , 'nvarchar(128)' );--操作数据表 set @DMLType=@DataSource.value( '/DMLType[1][text()]' , 'char(1)' );--操作类型 set @Sql=dbo.UF_XMLDataSourceToSQL(@DataSource,@TableName,@PrimaryKeyField,@DMLType); exec sp_executesql @Sql, N '@DataSource XML' , @DataSource; end try begin catch declare @DBName nvarchar(128) select @DBName=Name from master..SysDataBases where dbid=( select dbid from master..sysprocesses where spid=@@SPID) set @ErrorNumber=ERROR_NUMBER(); set @ErrMsg=( select ERROR_NUMBER() as ErrNumber, ERROR_SEVERITY() as ErrSeverity, ERROR_STATE() as ErrState, ERROR_PROCEDURE() as ErrProcedure, ERROR_LINE() as ErrLine, ERROR_MESSAGE() as ErrMessage, @PrimaryKeyField as PrimaryKeyField, @TableName as TableName, @DMLType as DMLType, @Msg_Body as MessageContent, @DBName as DBName for XML raw); --GOTO 错误处理标签 goto Err_Handle; end catch --结束会话 End Conversation @ConversationHandle if @ErrorNumber is not null begin --错误处理区域 Err_Handle: if @ErrMsg is not null begin declare @test nvarchar(128); --发送失败消息 send on conversation @ConversationHandle message type [http: //oa.founder.com/Data/Sync/Error](@ErrMsg) end --结束会话 end conversation @ConversationHandle -- break ; --回滚--不可回滚,否则将无法发送失败消息 --GoTO Err_Lab; end end commit transaction end end go |
3.2.15 对目标数据库的消息队列进行内部激活
这里主要是用来激活目标数据库的消息队列,主要用来实现数据的同步以及同步出错的错误信息的反馈。
1
2
3
4
5
6
7
8
9
10
11
12
|
--对Service Broker队列使用内部激活,并指定将要调用的存储过程 use DBTo go --alter Queue dbo.[DBTo_DataSyncQueue] with activation alter Queue dbo.[DBFrom_DataSyncQueue] with activation ( status= on , max_queue_readers=1, Procedure_name=UP_SyncDataToTable, Execute as self ) Go |
完成以上这些步骤以后,就可以实现同一数据库实例上两个不同的数据库之间的数据同步。即如果DBFrom数据库中的Org_Users中的某一条信息发生变化,会马上的自动同步到DBTo数据库中的Org_Users 表。如果是想要实现不同的数据库实例间的数据库的表的同步,则可以参考以下链接:
http://www.cnblogs.com/downmoon/archive/2011/05/05/2037830.html
在创建启用传输安全、对话安全,创建路由、远程服务绑定等额外的操作之后,剩下的操作跟在同一数据库实例中的操作是一样的。
此外,本文还参考了如下的链接:
http://www.cnblogs.com/downmoon/archive/2011/04/05/2005900.html
希望可以给大家一些启发和帮助。具体的源码有兴趣的朋友可以留下邮箱。
目录
背景几种方案重点说一下 Assembly.LoadFile备注
背景返回目录
如果某个“功能”需要动态更新?这种动态更新,可能是需求驱动的,也可能是为了修改 BUG,面对这种场景,如何实现“热插拔”呢?先解释一下“热插拔”:在系统运行过程动态替换某些功能,不用重启系统进程。
几种方案返回目录
- 脚本化:采用 Iron 或 集成其它脚本引擎。
- AppDomain:微软的 Add In 框架就是为这个目的设计的。
- 分布式 + 负载平衡 :轮流更新集群中的服务器。
- Assembly.LoadFrom:因为相同标识的程序集在内存中只会加载一次,所以每次功能发生变化,都要重新命名程序集。
- Assembly.Load + GAC 部署程序集:因为相同标识的程序集在内存中只会加载一次,所以每次功能发生变化,都要增加程序集的版本号。
- Assembly.LoadFile:Assembly.LoadFile 可以多次加载相同标识的程序集,只要程序集所在的目录位置不同。
重点说一下 Assembly.LoadFile返回目录
项目结构
测试代码
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 using System.Reflection; 7 using System.IO; 8 using Contracts; 9 10 namespace Test 11 { 12 class Program 13 { 14 static void Main(string[] args) 15 { 16 SetupPlugEnvironment(); 17 18 ExecuteOperator("1.0.0.0"); 19 ExecuteOperator("2.0.0.0"); 20 } 21 22 private static void ExecuteOperator(string version) 23 { 24 var operatorType = Type.GetType("Implements.Operator, Implements, version = " + version + ""); 25 var operatorInstance = Activator.CreateInstance(operatorType) as IOperator; 26 operatorInstance.Operate(); 27 } 28 29 private static void SetupPlugEnvironment() 30 { 31 AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve; 32 } 33 34 static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) 35 { 36 AssemblyName name = new AssemblyName(args.Name); 37 38 var file = Path.Combine( 39 @"E:CodingHappyStudyLoadContextStudyTestinDebugPlugs", 40 name.Name, 41 name.Version.ToString(), 42 name.Name + ".dll"); 43 44 Console.WriteLine("加载插件:" + name.Version); 45 46 return Assembly.LoadFile(file); 47 } 48 } 49 }
输出结果
说明
调用 Type.GetType 会导致 CLR 执行程序集探测过程,在正常的探测路径下没有找到程序集就会触发 AssemblyResolve 事件,为啥会触发两次呢?我还不知道,有知道的兄弟请留言。
备注返回目录
微软不推荐使用 LoadFile(会加载相同标识的程序集多次),Add In 采用的是 AppDomain,MEF 采用的是 LoadFrom(我估计是,还没有看源代码,测试结果是)。