基于 Canal + Elasticsearch 的业务操作日志解决方案

一、问题来源

在日常的业务系统中,操作日志是不可或缺的一部分。它能帮助我们追踪用户的操作行为,记录关键数据的变更,甚至在必要时支持操作回滚。最近,我们接到客户的需求,希望在系统中实现一个业务操作日志管理的功能,具体包括:

  • 记录用户的业务操作行为:包括操作人、操作时间、操作功能、日志类型、操作内容描述、操作前后的数据报文等。

  • 提供可视化的查询页面:方便查询用户的操作记录,对重要操作进行回溯。

  • 支持误操作回滚:在必要时,对用户的误操作进行回滚处理。

这个需求看似简单,但要在不影响现有业务逻辑的情况下,实现高效、通用的操作日志记录,确实需要好好思考一番。

二、问题描述

2.1 日志的类型

在业务系统中,常见的日志类型主要有两种:

  • 系统日志

    • 记录程序执行过程中的关键步骤,用于输出 debuginfowarnerror 等不同级别的信息。

    • 这类日志主要供程序员和运维人员查看,帮助快速排查故障。

  • 操作日志

    • 记录用户的实际业务操作行为,如哪个用户在什么时间点击了某个菜单,修改了哪个配置等。

    • 这类日志一般存储在数据库中,供普通用户或系统管理员查看。

2.2 传统实现方式的局限

2.2.1 业务代码嵌套日志

最直接的方法是在业务代码中手动添加日志记录。

例如,在每个数据库操作的前后,记录操作名称、时间、影响的数据等信息。然而,这种方式需要修改大量的业务代码,增加了编码的复杂度,而且不够通用。

2.2.2 AOP(面向切面编程)

AOP 是一种编程范式,能够将日志记录等通用功能与业务逻辑分离。

在 Spring 框架中,常用 AOP 来实现操作日志的记录。然而,AOP 在处理数据变更前后的值、批量操作、多表关联等复杂场景时,显得力不从心。

举个例子,我之前尝试过一种方案,通过在数据对象中设置 newDataoldData 两个属性来记录数据的前后变化:

@Valid
@NotNull(message = "新值不能为空")
@UpdateNewDataOperationLog
private T newData;@Valid
@NotNull(message = "旧值不能为空")
@UpdateOldDataOperationLog
private T oldData;

这种方式存在以下问题:

  1. 旧值的获取问题:如果不再次查询数据库,就需要前端将旧值封装到 oldData 对象中,但这可能导致数据不一致。

  2. 无法处理批量数据:对于 List 类型的数据,处理起来相当麻烦。

  3. 不支持多表操作:当一个业务操作涉及多个表时,很难完整地记录操作日志。

三、方案探讨

面对上述问题,我们需要一种更高效、更通用的解决方案。经过调研,我们发现了 Canal 这款神器(咱们之前文章也提及和验证过这个方案)。

3.1 Canal 的技术原理

Canal 是阿里巴巴开源的一款基于 MySQL 二进制日志(Binlog)的增量数据订阅和消费组件。它的主要功能是实时监听 MySQL 数据库的变更,包括表结构和数据的变化。

dd64e46ab9e3a6ca8983ef453c383fdb.png

通过捕获 Binlog,Canal 能够获取数据库层面的原始变更事件(如 INSERTUPDATEDELETE),并将其解析为可消费的数据。

3.2 为什么选择 Canal?

  • 解耦业务代码

    • 不需要修改现有的业务代码,降低了系统的耦合度。

  • 支持批量操作和多表关联

    • 由于直接从数据库层面获取变更数据,能够方便地处理复杂的业务场景。

  • 不依赖开发语言

    • Canal 与具体的编程语言无关,适用于各种技术栈的项目。

3.3 Canal 的优缺点

  • 优点

    • 解除了数据新旧变化的耦合。

    • 支持批量操作和多表关联拓展。

    • 不依赖于特定的开发语言。

  • 缺点

    • 数据库表设计需要统一的约定。

    • 对于多表级联保存和更新的数据,可能存在兼容性问题。

    • 需要处理非业务层面的数据变更(如手动修改数据库)。

四、方案实施

4.1 数据解析与转换

首先,Canal 采集并解析业务库的 Binlog 日志,将其投递到 Kafka 中。解析后的数据包括操作类型(如删除、修改、新增)以及新旧值,格式大致如下:

{"data": [{"id": "122158992930664499","goodsName": "新商品名称","update_time": "2020-08-26 13:45:46"}],"old": [{"goodsName": "旧商品名称","update_time": "2020-08-26 09:15:13"}],"database": "db_business","table": "goods","type": "UPDATE","ts": 1587879945698
}

4.2 定义通用接口规则

为了兼容不同业务的字段定义,我们设计了一个通用的接口规范,返回变更前后的数据和字段描述。

33c86352151faaaa94bf8e11954d7b2f.jpeg

以商品修改为例,接口如下:

{"id": "10001","groupID": 1700,"system": "01","newObject": {"goodsName": "商品名称001","goodsCode": "商品编码001"},"oldObject": {"goodsName": "商品名称","goodsCode": "商品编码"},"fieldsDescription": {"goodsID": "商品ID","goodsName": "商品名称","goodsCode": "商品编码"},"action": 2,"description": "修改商品信息","operator": "user001","databaseName": "db_business","tableName": "goods","module": "商品管理","txID": "36aef98585db4e7a98f9694c8ef28b8c","timestamp": 1587879945698
}

字段解释:

  • groupID:集团 ID

  • databaseName:数据库名称

  • tableName:表名称

  • oldObject:变更前的数据

  • newObject:变更后的数据

  • fieldsDescription:字段描述,方便前端展示

  • operator:操作人

  • module:业务模块

  • action:操作类型(0:新增,1:删除,2:修改)

  • description:操作描述

  • txID:事务 ID

通过这个接口,我们可以将变更的数据直观地展示出来,也可以使用 JSONDiff 等工具高亮显示差异。

小提示:如果同一个事务操作了多个表,为了完整地串联相关表的变更并支持回滚,可以使用 txID 将 Binlog 进行聚合处理。

4.3 数据存储

由于业务字段的变更不确定,我们选择使用 NoSQL 数据库来存储这些操作日志。

这里,我们采用了 Elasticsearch,并按照月份对各个业务线的索引进行切割。

4.3.1 Elasticsearch 索引与映射

首先,定义索引和映射:

 
PUT goods-nested
{"mappings": {"properties": {"id": {"type": "integer"},"groupID": {"type": "integer"},"bizSource": {"type": "keyword"},"action": {"type": "integer"},"description": {"type": "keyword"},"operator": {"type": "keyword"},"databaseName": {"type": "keyword"},"tableName": {"type": "keyword"},"bizmodule": {"type": "keyword"},"txId": {"type": "keyword"},"newObject": {"type": "nested","properties": {"goodsID": {"type": "integer"},"goodsName": {"type": "keyword"},"goodsCode": {"type": "keyword"}}},"oldObject": {"type": "nested","properties": {"goodsID": {"type": "integer"},"goodsName": {"type": "keyword"},"goodsCode": {"type": "keyword"}}},"fieldsDescription": {"type": "nested","properties": {"goodsID": {"type": "integer"},"goodsName": {"type": "keyword"},"goodsCode": {"type": "keyword"}}}}}
}

32a7fce31e690f1b7ca6352daa66dbf7.png


4.3.2 数据插入示例

插入操作日志数据:

 
POST goods-nested/_bulk
{"index":{"_index":"goods-nested","_id":"10001"}}
{"id":"10001","groupID":1700,"bizSource":"Scm","newObject":{"goodsID":1001,"goodsName":"商品名称001","goodsCode":"商品编码001"},"oldObject":{"goodsID":1001,"goodsName":"商品名称","goodsCode":"商品编码"},"fieldsDescription":{"goodsName":"商品名称","goodsCode":"商品编码"},"action":2,"description":"修改集团品相","operator":"001","databaseName":"db_supply_chain_basic","tableName":"tbl_chain_distribution","bizmodule":"集团品相","txId":"36aef98585db4e7a98f9694c8ef28b8c"}
{"index":{"_index":"goods-nested","_id":"10002"}}
{"id":"10002","groupID":1700,"bizSource":"Scm","newObject":{"goodsID":1002,"goodsName":"商品名称002","goodsCode":"商品编码002"},"oldObject":{"goodsID":1002,"goodsName":"商品名称","goodsCode":"商品编码"},"fieldsDescription":{"goodsName":"商品名称","goodsCode":"商品编码"},"action":2,"description":"修改集团品相","operator":"001","databaseName":"db_supply_chain_basic","tableName":"tbl_chain_distribution","bizmodule":"集团品相","txId":"36aef98585db4e7a98f9694c8ef28b8c"}
{"index":{"_index":"goods-nested","_id":"10003"}}
{"id":"10003","groupID":1700,"bizSource":"Scm","newObject":{"goodsID":1003,"goodsName":"商品名称003","goodsCode":"商品编码003"},"oldObject":{"goodsID":1003,"goodsName":"商品名称","goodsCode":"商品编码"},"fieldsDescription":{"goodsName":"商品名称","goodsCode":"商品编码"},"action":2,"description":"修改集团品相","operator":"001","databaseName":"db_supply_chain_basic","tableName":"tbl_chain_distribution","bizmodule":"集团品相","txId":"36aef98585db4e7a98f9694c8ef28b8c"}

504debfd70cc884b8ec3533d9f67e8d1.png

4.3.3 数据查询示例

根据商品 ID 查询操作日志:

 
GET goods-nested/_search
{"query": {"nested": {"path": "newObject","query": {"bool": {"must": [{"match": {"newObject.goodsID": "1001"}}]}}}}
}

d6ce5c6ca06a33cbf10a7109b6e6b7dc.png

如下查询是在索引 goods-nested 中查找满足以下条件的文档:groupID 等于 "1700",并且其嵌套字段 newObject 中的 goodsName 是 "商品名称001" 或 "商品名称002",同时 goodsID 是 1001 或 1002:

 
GET /goods-nested/_search
{"query": {"bool": {"must": [{"match": {"groupID": "1700"}},{"nested": {"path": "newObject","query": {"bool": {"must": [{"terms": {"newObject.goodsName": ["商品名称001","商品名称002"]}},{"terms": {"newObject.goodsID": [1001,1002]}}]}}}}]}}
}

bb41634777675576d33a268dd7fbd1cb.png

五、多表关联问题处理

在实际业务中,一个操作可能涉及多个表的级联保存和更新。

然而,Binlog 的数据是无序的,如果上游数据的操作不在同一个事务中,处理起来会有一定困难。

解决方案:

  • 使用事务 ID(txID):通过事务 ID,将同一事务内的操作聚合在一起,便于追踪和回滚。

  • 统一更新操作人:确保系统在进行数据更新时,正确记录操作人信息,方便后续的日志分析。

六、过滤非业务层面的数据变更

需要注意的是,Binlog 中包含的不仅仅是业务系统的操作,还可能包括数据库工单、跑批等产生的数据变更。

为了避免干扰,需要对 Binlog 进行过滤,只保留业务层面的操作日志。

七、小结

通过以上的方案设计和实践,我们成功地实现了对业务操作日志的高效、通用记录。使用 Canal 捕获数据库层面的数据变更,再结合 Elasticsearch 进行存储和查询,不仅解耦了业务逻辑,还满足了客户的需求。

当然,这个方案并非完美,仍存在一些挑战:

  • 多表关联的处理:需要更复杂的逻辑来聚合和关联数据变更。

  • 操作人的准确性:需要业务系统配合,确保每次数据变更都能正确记录操作人。

但在系统架构设计中,没有完美的方案。我们需要在实用性和完美性之间找到平衡,适应业务的需求,不断优化和迭代。

最后,技术的发展是一个不断演进的过程。我们需要拥抱变化,灵活运用各种工具和方法,为业务提供最合适的解决方案。


作者:海鸥

14 年开发经验,现任某互联网 SaaS 公司TL+架构师,目前专注于 ERP 供应链 、新零售业务 、企业架构、中台架构、领域驱动设计、技术领导力等领域。死磕 Elasticsearch 知识星球常驻技术专家。

对于高并发、高可用、高性能、大数据处理有过丰富项目实战经验,乐于技术沟通分享。


d6b5c6562d6a0c198c6ad426b2d64024.jpeg

更短时间更快习得更多干货!

和全球2000+ Elastic 爱好者一起精进!

elastic6.cn——ElasticStack进阶助手

459e1dc134d49d3e5df0a27b24bf3690.gif

抢先一步学习进阶干货!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/464173.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python并发编程库:Asyncio的异步编程实战

Python并发编程库:Asyncio的异步编程实战 在现代应用中,并发和高效的I/O处理是影响系统性能的关键因素之一。Python的asyncio库是专为异步编程设计的模块,提供了一种更加高效、易读的并发编程方式,适用于处理大量的I/O密集型任务…

【Vue项目1】第一篇

Vue项目1学习第一篇 01. 环境配置介绍和项目搭建02. Router路由配置引入03. ElementPlus引入和按需加载04. layout布局和菜单aside组件创建05. aside样式问题和treeMenu组件拆分06. treeMenu组件递归实现 01. 环境配置介绍和项目搭建 (1)安装node.js …

WPF使用Prism框架首页界面

1. 首先确保已经下载了NuGet包MaterialDesignThemes 2.我们通过包的项目URL可以跳转到Github上查看源码 3.找到首页所在的代码位置 4.将代码复制下来&#xff0c;删除掉自己不需要的东西&#xff0c;最终如下 <materialDesign:DialogHostDialogTheme"Inherit"Ide…

Golang | Leetcode Golang题解之第524题通过删除字母匹配到字典里最长单词

题目&#xff1a; 题解&#xff1a; func findLongestWord(s string, dictionary []string) (ans string) {m : len(s)f : make([][26]int, m1)for i : range f[m] {f[m][i] m}for i : m - 1; i > 0; i-- {f[i] f[i1]f[i][s[i]-a] i}outer:for _, t : range dictionary …

无人机的就业前景怎么样?

无人机的就业前景在当前及未来一段时间内都非常广阔。随着低空经济的蓬勃发展&#xff0c;无人机在农业、公安、测绘、交通、应急救援、影视拍摄等多个领域得到了广泛应用&#xff0c;对无人机操控员和相关专业人才的需求也随之急剧增加。 一、无人机操控员的就业前景 1. 高需…

如何将钉钉新收款单数据高效集成到MySQL

钉钉数据集成到MySQL的技术案例分享 在企业信息化管理中&#xff0c;数据的高效流动和处理至关重要。本文将分享一个具体的系统对接集成案例&#xff1a;如何将钉钉平台上的新收款单&#xff08;收款退款单&#xff09;数据集成到MySQL数据库中&#xff0c;方案名称为“dd-新收…

批量修改图片大小+删除空白页+手写签名

插入图片右键设置大小 设置对象格式 高度&#xff0c;宽度同一 最后一张图片拖到最后 alt键一下吸附好 ctrla全选图片 对齐 纵向分布 删除空白页 前面有文字 CTRL删除键 上一页是表格 CTRLd 勾选隐藏文字 手写签名 手机拍摄签名 发到电脑 文档里插入图…

软设师知识点-计算机网络

计算机网络 在一台安装好TCP/IP协议的计算机上&#xff0c;当网络连接不可用时&#xff0c;为了测试编写好的网络程序&#xff0c;通常使用的目的主机IP地址127.0.0.1&#xff08;本地回送地址&#xff09; *网络设备 物理层的互传设备&#xff1a;中继器(用于扩展局域网网段…

40.第二阶段x86游戏实战2-初识lua

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 本次游戏没法给 内容参考于&#xff1a;微尘网络安全 本人写的内容纯属胡编乱造&#xff0c;全都是合成造假&#xff0c;仅仅只是为了娱乐&#xff0c;请不要…

Docker可视化管理面板DPanel的安装

本文软件由网友 rui 推荐&#xff1b; 什么是 DPanel &#xff1f; DPanel 是一款 Docker 可视化管理面板&#xff0c;旨在简化 Docker 容器、镜像和文件的管理。它提供了一系列功能&#xff0c;使用户能够更轻松地管理和部署 Docker 环境。 软件特点&#xff1a; 可视化管理&…

Nature文章《deep learning》文章翻译

这篇文章是对Nature上《deep learning》文章的翻译。原作者 Yann LeCun, Yoshua Bengio& Geoffrey Hinton。 这篇文章的中心思想是深入探讨深度学习在机器学习中的革命性贡献&#xff0c;重点介绍其在特征学习、监督学习、无监督学习等方面的突破&#xff0c;并阐述其在图…

低代码用户中心:简化开发,提升效率的新时代

随着数字化转型的加速&#xff0c;企业对于快速交付高质量应用的需求日益增长。在这个背景下&#xff0c;低代码开发平台应运而生&#xff0c;成为越来越多企业和开发者的首选工具。今天&#xff0c;我们将聚焦于低代码用户中心&#xff0c;探讨其如何帮助开发者简化流程、提升…

leetcode71:简化路径

给你一个字符串 path &#xff0c;表示指向某一文件或目录的 Unix 风格 绝对路径 &#xff08;以 / 开头&#xff09;&#xff0c;请你将其转化为 更加简洁的规范路径。 在 Unix 风格的文件系统中规则如下&#xff1a; 一个点 . 表示当前目录本身。此外&#xff0c;两个点 ..…

2、liunx网络基础

一、TCP/IP协议概述 Linux服务器默认网卡配置文件在/etc/sysconfig/network-scripts/下&#xff0c;命名的名称一般为:ifcfg-eth0 ifcfg-eth1 &#xff0c;eth0表示第一块网卡&#xff0c;eth1表示第二块网卡&#xff0c;依次类推。一般DELL R720标配有4块千兆网卡。 TCP/IP&a…

[neo4j报错]py2neo.errors.ClientError: [Request.Invalid] Not Found解决方案

报错源代码 g Graph(http://localhost:7687, auth("neo4j", "password"))或许这是从网上复制下来的代码&#xff0c;看上去没什么问题&#xff0c;但实际上 要结合具体的浏览器上的地址来看&#xff0c;具体如下&#xff1a; 看到了吗&#xff0c;这里才…

WPF+MVVM案例实战(二十一)- 制作一个侧边弹窗栏(AB类)

文章目录 1、案例效果1、侧边栏分类2、AB类侧边弹窗实现1.文件创建2、样式代码与功能代码实现3、功能代码实现 3 运行效果4、源代码获取 1、案例效果 1、侧边栏分类 A类 &#xff1a;左侧弹出侧边栏B类 &#xff1a;右侧弹出侧边栏C类 &#xff1a;顶部弹出侧边栏D类 &#xf…

基于Multisim数控直流稳压电源电路(含仿真和报告)

【全套资料.zip】数控直流稳压电源电路设计Multisim仿真设计数字电子技术 文章目录 功能一、Multisim仿真源文件二、原理文档报告资料下载【Multisim仿真报告讲解视频.zip】 功能 1.输出直流电压调节范围5-12V。 2.输出电流0-500mA。 3.输出直流电压能步进调节&#xff0c;步…

原来大佬的测试用例都是这样写的...

1、测试点与测试用例 测试点不等于测试用例&#xff0c;这是我们首先需要认识到的。 问题1&#xff1a;这些测试点在内容上有重复&#xff0c;存在冗余。 问题2&#xff1a;一些测试点的测试输入不明确&#xff0c;不知道测试时要测试哪些。 问题3&#xff1a;总是在搭相似…

ubuntu20.04 加固方案-设置SSH是否使用业界认可的加密算法

一、编辑/etc/ssh/sshd_config配置文件 打开终端。 使用文本编辑器&#xff08;如vim&#xff09;编辑/etc/ssh/sshd_config文件。 vi /etc/ssh/sshd_config 二、添加配置参数 在打开的配置文件中&#xff0c;如图位置添加如下参数&#xff1a; 查看支持的算法&#xff1a;h…