人大金仓增量数据实时同步

 说明

  在分布式应用系统中,数据同步是比较常见的场景,本篇整理分享一下国产数据库“人大金仓”的增量数据同步的实现方案

 

1、数据同步实现方式

  实现数据同步的方式有很多,比如在业务代码中同步、定时任务同步、通过CDC实时同步等。下面先对这些方法进行分析对比了解一下:

  (1)业务代码中同步

  在指定的增加、删除、修改操作后,注入同步方法进行同步。

  优点:实现起来比较简单;实时同步;适用于简单的数据同步场景;

  缺点:代码耦合度很高;夹杂在业务代码中执行,效率不高;

  (2)定时任务同步

  在指定表中增加更新记录时间字段,当数据增加、删除、修改操作时,都会更新记录时间,再通过如Quartz创建定时任务,根据更新记录时间筛选出需要同步的数据,来定时同步。

  优点:与业务代码解耦;适用于对数据时效性不高的场景;

  缺点:需要在每个同步的数据表中添加更新字段,并在操作数据时更新;数据同步的实时性不高;

  (3)  通过CDC实时同步

  通过CDC技术捕捉变更数据,并提取转化同步数据

  优点:与业务代码解耦;做到准实时同步;适用于对数据时效性较高的场景。

  缺点:对于不同类型的数据库,记录日志的实现存在差异,没有统一的抓取解析的标准,实现CDC就需要单独定制,比如MySql使用Binlog、人大金仓使用Logical Decoding、PostgreSQL使用Logical Decoding、MongoDB使用oplog等。

  通过以上分析对比这三种同步方式的优劣,实际应用中可以根据实际场景考虑选择。下面再来介绍一下人大金仓CDC实现方案。

 

2、人大金仓CDC技术原理

  CDC(change data capture)即变更数据捕获,它是一种监测写入数据库的所有数据变更,并将其提取并转换为可以复制到其他系统中的形式过程。

  人大金仓数据库(KingBase)使用的预写式日志(Write-Ahead Logging(WAL))技术,其思想在于当发生操作数据修改时,这些修改会先被记录在WAL日志中,即变化的日志先持久化到存储中,再被应用应用到数据库数据中。这样实现思想的优势很明显,比如当数据库发生崩溃时,可以使用WAL日志来恢复数据库了;还可以在线备份归档WAL日志,达到在线备份和恢复数据的目的。

  CDC则可以通过监测WAL日志中数据的变更来实现。人大金仓通过逻辑复制WAL日志的方式提供了CDC所需的基础数据,还包含了CDC实现的基础功能:

  (1)Logical Decoding(逻辑解码):获取数据库的变更日志,并从WAL日志中解析逻辑变更事件;

  (2)Replication Protocol(复制协议):流复制提供了实时订阅数据库变更的机制;

  (3)Export Snapshot(导出快照):允许导出数据库的一致性快照,如果拥有全量变更日志,可以通过重放日志来重建数据库;

  (4)Replication Slot(复制槽):用于保存读取日志偏移量,跟踪订阅者进度,并及时清理回收不用的变更日志以免磁盘爆满。

   CDC客户端可以实时流式逻辑解码WAL日志中的变更事件,完成自定义的处理逻辑,并及时向数据库汇报自己的日志解码进度,同时,CDC客户端可以通过人大金仓(KingBase)数据库提供的复制槽(Replication Slot)机制来保存自己的读取偏移量,保证不错过、不丢失变更数据。

 

3、人大金仓CDC技术实现

  使用Logical Decoding(逻辑解码),从WAL日志中读取数据库的更新内容,将其解析成预定义格式,让后组合成可执行的SQL语句,可在目标库上执行。下面介绍实现的全过程:

  (1)修改数据库配置文件

kingbase@server11 ~]$ vi /KingbaseES/data/kingbase.conf

wal_level=logical

max_replication_slots=10 #该参数要大于等于 1

  上面两个参数默认#注释的,解除注释后,修改wal_level为logical,适用于逻辑解码;默认是replica,适用于备份恢复。max_replication_slots设置支持的复制槽的最大数量,默认为10。

  (2)创建Logical Replication Slot(复制槽)

--使用'test_decoding'解码插件创建一个名为'regression_slot'的 Logical Replication Slot复制槽
SELECT * FROM sys_create_logical_replication_slot('regression_slot', 'test_decoding');

--查看复制槽
select * from sys_replication_slots where slot_name='regression_slot';

  test_decoding为KingBase支持的默认解码插件,可以将WAL操作日志解析成“table schema.table_name:操作类型: 列名[该列数据类型]:值...”形式。

  (3)操作模拟数据

  此操作可以在数据库中对数据表,进行任意增删改操作。

  (4)查看操作记录

--可以多次查询记录
SELECT * FROM sys_logical_slot_peek_changes('regression_slot', NULL, NULL); 

--在查询过记录后,便会删除记录。
SELECT * FROM sys_logical_slot_get_changes('regression_slot', NULL, NULL); 

  查询出的结果如下:

   分析一下解析的数据结构:[table] [模式].[表名][:][INSERT/UPDATE/DELETE][:][字段名1][数据类型][:][字段值1] [字段名2][数据类型][:][字段值2] ...... 可以分割成三个部分:表、操作命令、字段值;再依次分割获取表名、操作命令关键字、所有字段以及字段值。其中在分割字段值过程中发现,字符串和类型中会存在空格,所以依据空格分割字段时,要过滤掉字段串中和类型中的空格。

  按照上面思路简单实现一下,组合成可执行的SQL语句:

复制代码
List<ChangeData> list = SqlSugarHelper.Db.SqlQueryable<ChangeData>("select cast (lsn as text) as lsn, cast (xid as text), cast (data as text) from sys_logical_slot_get_changes('regression_slot', NULL, NULL)").ToList();//获取变更日志的操作数据,获取后删除记录
foreach (var item in list)
{
   if (item.data.StartsWith("table"))
   {
       string tempStr = item.data.Substring(5, item.data.Length - 5);
       string tableName = tempStr.Split(":")[0].Trim();
       var data = tableMaps.Find(x => x.From == tableName.Split(".")[1]);//匹配出需要同步的表
       if (data == null)
       {
         continue;
       }
       string actionName = tempStr.Split(":")[1].Trim();
       string fileds = item.data.Split(tableName + ": " + actionName + ":")[1].Trim() + " ";

       #region 匹配到字符串
       Regex regex = new Regex("\'[^\']*\'");
       string result = regex.Match(fileds).Value;
       dynamic strIndexs = new { index = 0, length = 0 };
       List<dynamic> strlist = new List<dynamic>();
       var regexStr = regex.Match(fileds);
       bool isSearch = true;
       while (isSearch)
       {
         if (regexStr.Captures.Count > 0)
          {
            strlist.Add(new { index = regexStr.Index, length = regexStr.Length });
             regexStr = regexStr.NextMatch();
          }
         else
         {
             break;
         }
      }
       #endregion
                        
    #region 匹配到中括号 Regex regexEx = new Regex("\\[(.+?)\\]");
    var regexExStr = regexEx.Match(fileds); while (isSearch) {   if (regexExStr.Captures.Count > 0) {   strlist.Add(new { index = regexExStr.Index, length = regexExStr.Length }); regexExStr = regexExStr.NextMatch(); } else {   break; } } #endregion #region 解析成sql语句 StringBuilder sqlBuilder = new StringBuilder(); if (actionName == "UPDATE") {   sqlBuilder.Append(string.Format("update {0} set ", tableName)); } else if (actionName.ToUpper() == "INSERT") {   sqlBuilder.Append(string.Format("insert into {0}", tableName)); } else if (actionName.ToUpper() == "DELETE") {   sqlBuilder.Append(string.Format("delete from {0} ", tableName)); } var isTab = tablePrimaryKeys.Find(x => x.tableName == tableName.Split(".")[1]);//查询表是否存在联合主键 string whereSql = ""; string catStr = fileds; string insertFiled = "("; string insertValue = "("; int findCount = 1; int beforeIndex = 0; while (isSearch) {   int firstIndex = GetIndexOf(fileds, findCount);//找空格所在位置   if (firstIndex == 0)   {   break; } var exitis = strlist.FindAll(x => x.index < firstIndex && (x.length + x.index) > firstIndex);//验证空格是否在字符串中或中括号中 if (exitis.Count == 0)//不在 {   string currentNode = catStr.Substring(0, firstIndex - beforeIndex).Trim();   catStr = catStr.Substring(firstIndex - beforeIndex, catStr.Length - (firstIndex - beforeIndex)).Trim() + " ";   beforeIndex = firstIndex;   findCount = findCount + 1;   string filedNameStr = currentNode.Split("]:")[0].Trim().Split('[')[0].Trim();   string filedValueStr = currentNode.Split("]:")[1].Trim();   if (actionName.ToUpper() == "UPDATE")   {     if (isTab != null)//存在联合主键   {   if (isTab.compositeKeys.ToLower().Contains(filedNameStr.ToLower())) //是配置的联合主键 {   whereSql = whereSql + (string.IsNullOrEmpty(whereSql) ? string.Format("where {0}={1}", filedNameStr, filedValueStr) : string.Format(" and {0}={1}", filedNameStr, filedValueStr)); } } else {   whereSql = whereSql + (string.IsNullOrEmpty(whereSql) ? string.Format("where {0}={1}", filedNameStr, filedValueStr) : string.Format(" and {0}={1}", filedNameStr, filedValueStr));   } } else if (actionName.ToUpper() == "INSERT") {    //字段名,  insertFiled = insertFiled + (string.IsNullOrEmpty(insertFiled) ? string.Format("({0},", filedNameStr) : string.Format("{0},", filedNameStr)); //字段值构建 insertValue = insertValue + (string.IsNullOrEmpty(insertValue) ? string.Format("({0},", filedValueStr) : string.Format("{0},", filedValueStr)); } else if (actionName.ToUpper() == "DELETE") {   whereSql = whereSql + (string.IsNullOrEmpty(whereSql) ? string.Format("where {0}={1}", filedNameStr, filedValueStr) : string.Format(" and {0}={1}", filedNameStr, filedValueStr)); } } else {   findCount = findCount + 1;//存在指定的空格,继续向下找 } } string execSql = string.Empty; if (actionName == "UPDATE") {   execSql = sqlBuilder.ToString().Trim(',') + " " + whereSql; } else if (actionName.ToUpper() == "INSERT") {   sqlBuilder.Append(string.Format(" {0})", insertFiled.Trim(',')));   sqlBuilder.Append(string.Format(" values {0})", insertValue.Trim(',')));   execSql = sqlBuilder.ToString(); } else if (actionName.ToUpper() == "DELETE") {   execSql = sqlBuilder.ToString().Trim() + " " + whereSql; } #endregion Console.WriteLine(execSql);//打印出SQL } }
复制代码

(4)删除Logical Replication Slot复制槽

--drop 掉复制槽
SELECT sys_drop_replication_slot('regression_slot'); 

  已经创建的复制槽,不再使用时,可以进行执行上面的命令进行删除。

 

4、封装数据同步服务

  在实际应用中,可以将上面的过程封装成服务,实现思路如下:

  (1)服务启动时,执行查询判断是否创建复制槽的SQL,如果没有创建,则进行创建;

  (2)轮询查询出变更的数据,选择使用获取后删除的模式;

  (3)使用代码组合成SQL语句;

  (4)将组合好的SQL语句发送到给指定目标,如消息队列、其他类型的数据库等。

 

posted @   Hello牛顿  阅读(5548)  评论(18编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
点击右上角即可分享
微信分享提示