最佳实践 MSSQLServer2008数据增量同步
问题来源
项目组要实现两个非同构数据库的数据同步功能,为满足时间要求,最好是支持增量同步。
方案决策
- 使用数据库自带的发布订阅实现
因为源库和目标库不同构,实现起来比较复杂,并且客户环境的网络不好,在以前的使用案例中,容易出现队列阻塞或线程宕掉的情况。 - 利用数据库的特性实现
利用数据库特性,性能上有一定的保障。实现简单,开发效率高。 - 开发独立程序实现
充分利用JAVA读取写入多线程,并发写入步提高性能。开发难度高,多线程编程有一定的难度。 - 使用开源工具实现
采用kettle
,方案成熟,在该客户的其他项目中,有成功案例,未知风险较小。但配置复杂,学习成本高,工期较长。
综上所述,采用利用数据库的特性实现,性能上有保障,开发工期上有保障。
方法与原理
- 统一入口
提供统一入口,创建维护计划定期调用,在该入口中顺序调用业务存储过程 - 拆分业务集
根据业务拆分表(几十张)到不同业务存储过程(可多人同时开发不会冲突,方便管理)
同一个数据库(分别涉及到三个数据库)相关的存储过程使用同一个事物。若无异常则提交事务并记录本数据库增量记录表记录;若出现异常则回滚事物,并记录错误日志到日志表,并直接处理下一个数据库相关过程 - 具体同步操作
每个业务存储过程需要传入开始、截止时间戳,以及传出当前步骤、结果代码、结果信息,共五个参数。开始及截止时间戳用于过滤出本次应该处理的结果集范围;为方便调试,在操作每个表之前先记录当前步骤,遇到异常时,把错误代码及错误明细填充至结果代码和结果信息中,然后退出本过程,外部获取当前步骤及错误代码、错误信息,保存至错误日志表,供后续查询。
每个表根据传入的时间戳过滤出本次需要处理的结果集,再结合MERGE的特性同步到目标表
技术难点及解决
-
如何发现增量
该系统属于已开发上线的系统,未进行增量设计。MSSQL的特性——时间戳,能自动记录变化版本。
TIMESTAMP-数据库中自动生成的唯一二进制数字,与时间和日期无关,通常用作给表行加版本戳的机制。存储大小为 8个字节)。
每个表最多只允许有一列定义为TIMESTAMP类型,并且不允许修改其内容。每个数据库都有一个计数器,当对包含TIMESTAMP列的表执行插入或更新操作时,该计数器值就会增加,并且自动更新对应记录
--创建测试表
CREATE TABLE T1
(
N_ID INT IDENTITY,
C_NAME VARCHAR(300),
B_ROWVERSION TIMESTAMP
)
--插入记录 我们这里只对第二个字段进行处理,看看结果如何
INSERT INTO T1 (C_NAME) VALUES ('张三')
INSERT INTO T1 (C_NAME) VALUES ('李四')
--查看结果
SELECT * FROM T1
--第一列是自增列,第三列是时间戳列,他们都自动生成了数据
N_ID C_NAME B_ROWVERSION
1 张三 0x00000000000DC0D2
2 李四 0x00000000000DC0D3
--查看本数据库当前版本号
SELECT @@DBTS --结果 0x00000000000DC0D3
--我们更新一下张三对应的记录
UPDATE T1 SET C_NAME = '王五' WHERE N_ID = 1
--再次查看结果
SELECT * FROM T1
--每个表的TIMESTAMP类型列是由系统自动维护的,不允许手动更改
N_ID C_NAME B_ROWVERSION
1 王五 0x00000000000DC0D4
2 李四 0x00000000000DC0D3
-
如何匹配更新源库与目标库记录
除了先删再插,以及先查询再确定应该更新或插入,MSSQL还提供了另一个特性:MERGE关键字。该关键字可以根据与源表联接的结果,对目标表执行插入、更新或删除操作
--创建测试表1
CREATE TABLE T1
(
N_ID INT,
C_NAME VARCHAR(300),
B_ROWVERSION TIMESTAMP
)
--创建测试表2
CREATE TABLE T2
(
N_ID INT,
C_NAME VARCHAR(300),
B_ROWVERSION TIMESTAMP
)
--插入记录
INSERT INTO T1 (N_ID, C_NAME) VALUES (1, '张三')
INSERT INTO T1 (N_ID, C_NAME) VALUES (2, '李四')
INSERT INTO T2 (N_ID, C_NAME) VALUES (1, '王五')
INSERT INTO T2 (N_ID, C_NAME) VALUES (3, '马六')
--T1当前结果
N_ID C_NAME B_ROWVERSION
1 张三 0x00000000000DC0F6
2 李四 0x00000000000DC0F7
--T2当前结果
N_ID C_NAME B_ROWVERSION
1 王五 0x00000000000DC0F8
3 马六 0x00000000000DC0F9
MERGE T1 AS T
USING (SELECT N_ID, C_NAME FROM T2) AS S
ON (T.N_ID = S.N_ID)
WHEN MATCHED THEN --两边可以匹配
UPDATE SET C_NAME = S.C_NAME
WHEN NOT MATCHED BY TARGET THEN --目标表中不存在匹配
INSERT (N_ID, C_NAME) VALUES (S.N_ID, S.C_NAME)
WHEN NOT MATCHED BY SOURCE THEN --源表中不存在匹配
DELETE;
--T1当前结果
N_ID C_NAME B_ROWVERSION
1 王五 0x00000000000DC110
3 马六 0x00000000000DC111
--T2当前结果
N_ID C_NAME B_ROWVERSION
1 王五 0x00000000000DC0F8
3 马六 0x00000000000DC0F9
增量同步伪代码
- 过程主入口
CREATE PROCEDURE [dbo].[PR_XXXX_SJSB_0_RK]
AS
BEGIN
DECLARE @nResultCode INT --结果代码
DECLARE @cResultMsg VARCHAR(900) --结果信息
DECLARE @bRowVersionBegin BINARY(8) --开始时间戳
DECLARE @bRowVersionEnd BINARY(8) --结束时间戳
DECLARE @cCurrProName VARCHAR(300) --当前过程名称
DECLARE @cCurrStepName VARCHAR(300) --当前步骤名称
--不显示统计行信息
SET NOCOUNT ON
--1. 创建数据上报所需的表
--1.1 数据上报错误日志表
IF NOT EXISTS (SELECT 1 FROM DB_XXXX.DBO.SYSOBJECTS WHERE TYPE = 'U' AND NAME = 'T_SJSB_CWRZ')
BEGIN
CREATE TABLE DB_XXXX.DBO.T_SJSB_CWRZ(
N_BH INT NOT NULL IDENTITY, --主键
C_GCMC VARCHAR(300) NOT NULL, --过程名称
C_BZMC VARCHAR(300) NOT NULL, --步骤名称
N_CWDM INT NOT NULL, --错误代码
C_CWMX VARCHAR(900), --错误明细
DT_GXSJ DATETIME, --更新时间
PRIMARY KEY (N_BH)
)
END
...
--2. 处理AA库所有表
--2.1 获取AA库时间戳
EXECUTE DB_AA.DBO.PR_AA_SJSB_HQSJC @bRowVersionBegin OUTPUT, @bRowVersionEnd OUTPUT
--2.2 开始调用相关存储过程
--开启事物
SET XACT_ABORT ON
BEGIN DISTRIBUTED TRANSACTION
SET @nResultCode = 0
IF @nResultCode = 0
BEGIN
--2.2.1 AA基本信息
SET @cCurrProName = 'PR_XXXX_SJSB_1_AAJBXX'
EXECUTE DB_XXXX.DBO.PR_XXXX_SJSB_1_AAJBXX @bRowVersionBegin, @bRowVersionEnd, @cCurrStepName OUTPUT, @nResultCode OUTPUT, @cResultMsg OUTPUT
PRINT @nResultCode
END
IF @nResultCode = 0
BEGIN
--2.2.2 AA综合信息
SET @cCurrProName = 'PR_XXXX_SJSB_2_AAZH'
EXECUTE DB_XXXX.DBO.PR_XXXX_SJSB_2_AAZH @bRowVersionBegin, @bRowVersionEnd, @cCurrStepName OUTPUT, @nResultCode OUTPUT, @cResultMsg OUTPUT
PRINT @nResultCode
END
--插入增量记录表记录
IF @nResultCode = 0
INSERT INTO DB_XXXX.DBO.T_SJSB_ZLJL (C_KM, B_SJC, DT_GXSJ) VALUES ('DB_AA', @bRowVersionEnd, GETDATE())
--提交事物
PRINT @nResultCode
IF @nResultCode = 0
COMMIT TRAN
ELSE
BEGIN
ROLLBACK TRAN
INSERT INTO DB_XXXX.DBO.T_SJSB_CWRZ (C_GCMC, C_BZMC, N_CWDM, C_CWMX, DT_GXSJ)
VALUES (@cCurrProName, @cCurrStepName, @nResultCode, @cResultMsg, GETDATE())
END
--3. 处理BB库所有表
...
--4. 处理CC库所有表
...
SET XACT_ABORT OFF
SET NOCOUNT OFF
--过程结束
END
- 业务过程
CREATE PROCEDURE [dbo].[PR_XXXX_SJSB_1_AAJBXX](
@bRowVersionBegin BINARY(8),
@bRowVersionEnd BINARY(8),
@cCurrStepName VARCHAR(300) OUTPUT,
@nResultCode INT OUTPUT,
@cResultMsg VARCHAR(900) OUTPUT
)
AS
BEGIN
SET @nResultCode = 0
BEGIN TRY
--基本信息
SET @cCurrStepName = 'DA_JBXX1'
--禁用自增列
SET IDENTITY_INSERT XXXX.DBO.DA_JBXX ON
EXECUTE DB_XXXX.DBO.PR_XXXX_SJSB_SCBHYS 'DB_ZF.DBO.T_ZF', @bRowVersionBegin, @bRowVersionEnd
MERGE XXXX.dbo.DA_JBXX AS T
USING (SELECT DB_XXXX.DBO.FUN_GET_ID_BY_SOURCE('DB_AA.DBO.T_AA', ZF.C_ID) ID,
ISNULL(AA.C_ZFBH, '') C_ZFBH, ISNULL(AA.C_XM, '') C_XM, ...
ISNULL(AA.DT_CreateTime, '') DT_CreateTime FROM DB_AA.DBO.T_AA AA
WHERE AA.B_RowVersion > @bRowVersionBegin AND ZF.B_RowVersion <= @bRowVersionEnd
) AS S
ON (T.ID = S.ID)
WHEN MATCHED THEN
UPDATE SET XM = S.C_XM, ... LRSJ = S.DT_CreateTime
WHEN NOT MATCHED BY TARGET THEN
INSERT (ID, BH, XM, ...LRSJ) VALUES (S.ID, S.C_ZFBH, S.C_XM, ... S.DT_CreateTime);
--恢复自增列属性
SET IDENTITY_INSERT XXXX.DBO.DA_JBXX OFF
--AA别名化名
SET @cCurrStepName = 'DA_BHM'
--禁用自增列
SET IDENTITY_INSERT XXXX.DBO.DA_BHM ON
EXECUTE DB_XXXX.DBO.PR_XXXX_SJSB_SCBHYS 'DB_AA.dbo.T_AA_BMHM', @bRowVersionBegin, @bRowVersionEnd
MERGE XXXX.dbo.DA_BHM AS T
USING (SELECT DB_XXXX.DBO.FUN_GET_ID_BY_SOURCE('DB_AA.dbo.T_AA_BMHM', BMHM.C_ID) ID,
AA.C_ZFBH, ISNULL(BMHM.C_BHM,'') C_BHM,
ISNULL(BMHM.DT_CreateTime, '') DT_CreateTime
FROM DB_ZF.dbo.T_AA AA
INNER JOIN DB_AA.dbo.T_AA_BMHM BMHM ON AA.C_ID = BMHM.C_AA_ID
WHERE AA.C_ID IS NOT NULL
AND BMHM.B_RowVersion > @bRowVersionBegin AND BMHM.B_RowVersion <= @bRowVersionEnd
) AS S
ON (T.ID = S.ID)
WHEN MATCHED THEN
UPDATE SET BH = S.C_ZFBH, BHM = S.C_BHM, LRSJ = S.DT_CREATETIME
WHEN NOT MATCHED BY TARGET THEN
INSERT (ID, BH, BHM, LRSJ)
VALUES (S.ID, S.C_ZFBH, S.C_BHM, S.DT_CREATETIME);
--恢复自增列属性
SET IDENTITY_INSERT XXXX.DBO.DA_BHM OFF
...
END TRY
BEGIN CATCH
SET @nResultCode = ERROR_NUMBER()
SET @cResultMsg = ERROR_MESSAGE()
END CATCH
END
结语
上面我们简单介绍了MSSQL中的TIMESTAMP以及MERGE的用法。利用这两个特性,我们可以很方便的实现两个库之间的增量同步操作,节省工期,提高效率。