人大金仓增量数据实时同步
说明
在分布式应用系统中,数据同步是比较常见的场景,本篇整理分享一下国产数据库“人大金仓”的增量数据同步的实现方案
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语句发送到给指定目标,如消息队列、其他类型的数据库等。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)