数据库内核月报-2017年05期-最佳实践 MSSQLServer2008数据增量同步

最佳实践 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的用法。利用这两个特性,我们可以很方便的实现两个库之间的增量同步操作,节省工期,提高效率。

你可能感兴趣的:(数据库,经验分享)