文章目录
- 多服务节点数据修正方案设计与实现
-
- 一、背景与问题
- 二、核心设计
-
- [2.1 整体架构图](#2.1 整体架构图)
- [2.2 表结构设计](#2.2 表结构设计)
-
- [2.2.1 订正任务表 (data_revised_task)](#2.2.1 订正任务表 (data_revised_task))
- [2.2.2 订正任务流程表 (data_revised_task_process)](#2.2.2 订正任务流程表 (data_revised_task_process))
- [2.2.3 订正节点配置表 (data_revised_node)](#2.2.3 订正节点配置表 (data_revised_node))
- [2.3 消息设计](#2.3 消息设计)
- 三、核心流程实现
-
- [3.1 任务创建与编排](#3.1 任务创建与编排)
- [3.2 节点执行与推进](#3.2 节点执行与推进)
- [3.3 结果回执与下一节点触发](#3.3 结果回执与下一节点触发)
- [3.4 延迟检查机制](#3.4 延迟检查机制)
- 四、可视化设计
-
- [4.1 任务状态展示](#4.1 任务状态展示)
- [4.2 节点进度展示](#4.2 节点进度展示)
- [4.3 页面展示示例](#4.3 页面展示示例)
- 五、失败重试机制
-
- [5.1 重试策略](#5.1 重试策略)
- [5.2 重试API](#5.2 重试API)
- 六、事务性发件箱集成
-
- [6.1 消息幂等设计](#6.1 消息幂等设计)
- 七、总结
-
- [7.1 核心能力](#7.1 核心能力)
- [7.2 方案优势](#7.2 方案优势)
- [7.3 适用场景](#7.3 适用场景)
多服务节点数据修正方案设计与实现
一、背景与问题
在 ERP 系统中,业务人员在录入产品成本时可能发生误操作,导致产品成本数据错误。这类错误数据往往不会立即被发现,而是在最终的统计和核算环节才会暴露问题。然而此时,错误数据可能已经在多个业务领域节点中保存了副本:
- 成本基础数据节点:存储产品成本原始数据
- 定价计算节点:基于成本计算销售定价
- 财务报表节点:基于定价生成财务统计报表
- 库存核算节点:基于成本进行库存价值核算
这种场景下,我们需要设计一套多服务节点协作的数据修正方案,实现:
- 多节点顺序修正:按照节点依赖关系依次修正各节点数据
- 可视化进度追踪:实时查看各节点修正进度和状态
- 中断可重试:支持节点级别失败重试,保证最终一致性
- 事务性保证 :利用事务性发件箱确保消息可靠投递
二、核心设计
在深入实现细节之前,我们首先需要了解系统的整体架构设计。本节将从架构图、核心实体、消息设计三个维度展开说明。
2.1 整体架构图
事务性发件箱
事务性发件箱
事务性发件箱
触发下一节点
失败重试
业务人员创建修正任务
任务编排服务
任务进度表
data_revised_task_process
消息队列层 RocketMQ
节点1 Topic:A
节点2 Topic:B
节点3 Topic:C
节点4 Topic:D
任务完成
成本基础节点
定价计算节点
财务报表节点
库存核算节点
结果回执
更新流程状态
延迟检查机制
2.2 表结构设计
基于上述架构,我们需要设计三张核心数据表来支撑整个修正流程。以下是各表的详细设计:
2.2.1 订正任务表 (data_revised_task)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | BIGINT | 主键ID |
| task_identifier | VARCHAR(64) | 任务标识 (雪花算法生成) |
| revised_type_id | BIGINT | 订正类型ID |
| revised_type_identifier | VARCHAR(64) | 订正类型标识 |
| revised_type_desc | VARCHAR(256) | 订正类型描述 |
| status | INT | 任务状态:1-初始化 2-进行中 3-完成 4-暂停 |
| main_task_id | BIGINT | 主任务ID (多任务关联) |
| creator_name | VARCHAR(64) | 创建人 |
| create_time | DATETIME | 创建时间 |
2.2.2 订正任务流程表 (data_revised_task_process)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | BIGINT | 主键ID |
| revised_task_id | BIGINT | 订正任务ID |
| revised_task_identifier | VARCHAR(64) | 任务标识 |
| revised_node_id | BIGINT | 订正节点ID |
| revised_node_name | VARCHAR(64) | 节点名称 |
| revised_node_sort | INT | 节点排序 (确保顺序执行) |
| revised_topic | VARCHAR(128) | 订正topic |
| revised_group | VARCHAR(128) | 订正group |
| revised_tag | VARCHAR(64) | 订正tag |
| revised_param | TEXT | 订正参数 (JSON) |
| process_status | INT | 流程状态:1-初始化 2-处理中 3-成功 4-失败 5-延迟处理 |
| delay_check_mark | INT | 延迟检查标记 |
| create_time | DATETIME | 创建时间 |
| update_time | DATETIME | 更新时间 |
2.2.3 订正节点配置表 (data_revised_node)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | BIGINT | 主键ID |
| node_name | VARCHAR(64) | 节点名称 |
| node_sort | INT | 节点排序 |
| revised_topic | VARCHAR(128) | 订正topic |
| revised_group | VARCHAR(128) | 订正group |
| revised_tag | VARCHAR(64) | 订正tag |
| revised_type_id | BIGINT | 订正类型ID |
| delay_check_mark | INT | 延迟检查标记 |
| delay_check_topic | VARCHAR(128) | 延迟检查topic |
| delay_check_group | VARCHAR(128) | 延迟检查group |
| delay_check_tag | VARCHAR(64) | 延迟检查tag |
| create_time | DATETIME | 创建时间 |
2.3 消息设计
消息是驱动整个修正流程的核心载体。我们定义了两种关键消息类型:
任务执行消息 (DataRevisedTaskMessage)
| 字段名 | 类型 | 说明 |
|---|---|---|
| taskId | Long | 订正任务ID |
| taskIdentifier | String | 任务标识 |
| revisedTypeId | Long | 订正类型ID |
| revisedTypeIdentifier | String | 订正类型标识 |
| revisedTypeDesc | String | 订正类型描述 |
| dataRevisedTaskProcessId | Long | 订正任务流程ID |
| revisedNodeId | Long | 订正节点ID |
| revisedNodeName | String | 节点名称 |
| revisedParam | String | 订正参数 (JSON) |
| uniqueKey | String | 幂等键 |
执行结果消息 (DataRevisedTaskProcessExecutionResultMessage)
| 字段名 | 类型 | 说明 |
|---|---|---|
| taskId | Long | 订正任务ID |
| taskIdentifier | String | 任务标识 |
| dataRevisedTaskProcessId | Long | 订正任务流程ID |
| enforcementResult | String | 执行结果:SUCCESS/FAIL |
| revisedParam | String | 传递给下一节点的参数 |
| executeDesc | String | 执行描述 |
| uniqueKey | String | 幂等键 |
三、核心流程实现
基于前述的实体和消息设计,本节将详细讲解四个核心流程的实现逻辑。
3.1 任务创建与编排
任务创建是整个流程的起点,需要完成订正类型查询、节点列表获取、任务实体创建、流程记录初始化等工作:
java
@Transactional(rollbackFor = Exception.class)
public DataRevisedTask addTask(DataRevisedTaskAddRequest request, Long mainTaskId) {
// 1. 查询订正类型配置
DataRevisedType revisedType = revisedTypeService.getByTypeIdentifier(request.getTypeIdentifier());
// 2. 查询订正类型关联的节点列表 (按nodeSort排序)
List<DataRevisedNode> nodeList = revisedNodeService.getByTypeIdentifier(request.getTypeIdentifier());
// 3. 创建订正任务
DataRevisedTask task = createTaskEntity(revisedType, request.getOperatorName(), mainTaskId);
this.save(task);
// 4. 创建任务流程记录 (每个节点一条记录)
List<DataRevisedTaskProcess> processList = new ArrayList<>();
for (DataRevisedNode node : nodeList) {
DataRevisedTaskProcess process = new DataRevisedTaskProcess();
process.setRevisedTaskId(task.getId());
process.setRevisedTaskIdentifier(task.getTaskIdentifier());
process.setRevisedNodeId(node.getId());
process.setRevisedNodeName(node.getNodeName());
process.setRevisedNodeSort(node.getNodeSort());
process.setRevisedTopic(node.getRevisedTopic());
process.setRevisedGroup(node.getRevisedGroup());
process.setRevisedTag(node.getRevisedTag());
// 头节点设置修正参数
if (node.getNodeSort() == 1) {
process.setRevisedParam(request.getRevisedParam());
}
process.setProcessStatus(ProcessStatusEnum.INITIALIZATION.getStatus());
process.setDelayCheckMark(node.getDelayCheckMark());
processList.add(process);
}
processService.saveBatch(processList);
return task;
}
3.2 节点执行与推进
节点执行是驱动工作流流程运转的关键步骤,需要处理状态变更和消息发送:
java
@Transactional(rollbackFor = Exception.class)
public void commitTask(String taskIdentifier) {
// 1. 获取头节点 (node_sort = 1)
DataRevisedTaskProcess headNode = processService.getTaskProcessHeadNode(taskIdentifier);
// 2. 更新任务状态为执行中
updateTaskStatus(taskIdentifier, TaskStatusEnum.PROCESSING);
// 3. 执行头节点
executeNode(headNode);
}
private void executeNode(DataRevisedTaskProcess process) {
Long processId = process.getId();
Integer currentStatus = process.getProcessStatus();
// 状态更新逻辑
if (ProcessStatusEnum.INITIALIZATION.getStatus().equals(currentStatus)) {
// 正常发起:初始化 -> 处理中
processService.modifyStatus(processId, ProcessStatusEnum.PROCESSING,
ProcessStatusEnum.INITIALIZATION);
} else if (ProcessStatusEnum.PROCESSING_FAILED.getStatus().equals(currentStatus)) {
// 失败重试:失败 -> 处理中
processService.modifyStatus(processId, ProcessStatusEnum.PROCESSING,
ProcessStatusEnum.PROCESSING_FAILED);
} else if (ProcessStatusEnum.PROCESSING_SUCCEEDED.getStatus().equals(currentStatus)) {
// 延迟重试:成功 -> 延迟处理中
processService.modifyStatus(processId, ProcessStatusEnum.DELAY_PROCESSING,
ProcessStatusEnum.PROCESSING_SUCCEEDED);
}
// 发送执行消息
sendExecutionMessage(process);
}
3.3 结果回执与下一节点触发
每个节点执行完成后,需要处理执行结果并触发下一节点:
java
@Transactional(rollbackFor = Exception.class)
public void handleExecutionResult(DataRevisedTaskProcessExecutionResultMessage result) {
Long processId = result.getDataRevisedTaskProcessId();
if (EnforcementResultEnum.SUCCESS.getResult().equals(result.getEnforcementResult())) {
// 执行成功
processService.modifyStatus(processId, ProcessStatusEnum.PROCESSING_SUCCEEDED,
ProcessStatusEnum.PROCESSING, ProcessStatusEnum.DELAY_PROCESSING);
// 更新下一节点修正参数
processService.updateNextProcessParam(result.getTaskIdentifier(), processId,
result.getRevisedParam());
// 触发下一节点执行
executeNextNode(result.getTaskIdentifier(), processId);
} else {
// 执行失败
processService.modifyStatus(processId, ProcessStatusEnum.PROCESSING_FAILED,
ProcessStatusEnum.PROCESSING, ProcessStatusEnum.DELAY_PROCESSING);
}
}
private void executeNextNode(String taskIdentifier, Long currentProcessId) {
// 查询下一节点
DataRevisedTaskProcess nextNode = processService.getTaskProcessNextNode(taskIdentifier, currentProcessId);
if (nextNode == null) {
// 无下一节点,任务完成
updateTaskStatus(taskIdentifier, TaskStatusEnum.COMPLETE);
checkAssociatedTask(taskIdentifier);
return;
}
// 执行下一节点
executeNode(nextNode);
}
3.4 延迟检查机制
为了处理节点执行超时但未明确失败的情况,我们引入了延迟检查机制:
java
private void sendDelayCheckMessage(DataRevisedTaskMessage message, DataRevisedTaskProcess process) {
String uniqueKey = message.getRevisedTypeIdentifier() + "-DELAY-CHECK-" +
message.getTaskIdentifier() + "-" + process.getId();
message.setUniqueKey(uniqueKey);
// 发送延迟消息 (30分钟后触发)
RocketMQProducerLog log = RocketMQProducerLog.newBuilder()
.uniqueMsg(uniqueKey) // 事务性发件箱的方法名仍是 uniqueMsg
.topic(process.getDelayCheckTopic())
.groupId(process.getDelayCheckGroup())
.tag(process.getDelayCheckTag())
.body(JSONObject.toJSONString(message))
.type(MQMessageTypeEnum.DELAY_MESSAGE)
.delayTime(DateUtil.offsetMinute(new Date(), 30).getTime())
.build();
rocketMQProducerLogService.addProducerLog(log);
}
四、可视化设计
为了让业务人员直观地了解修正任务的执行状态,我们设计了多维度的可视化展示方案。
4.1 任务状态展示
| 状态 | 说明 | 颜色标识 |
|---|---|---|
| 初始化 | 任务刚创建,待执行 | 灰色 |
| 进行中 | 正在执行修正 | 蓝色 |
| 完成 | 所有节点修正完成 | 绿色 |
| 暂停 | 被手动暂停 | 橙色 |
4.2 节点进度展示
| 节点状态 | 说明 | 颜色标识 |
|---|---|---|
| 初始化 | 待执行 | 灰色 |
| 处理中 | 正在执行 | 蓝色 |
| 处理成功 | 修正完成 | 绿色 |
| 处理失败 | 执行失败,可重试 | 红色 |
| 延迟处理中 | 等待延迟检查 | 黄色 |
4.3 页面展示示例
页面设计采用卡片式布局,主要包含任务详情信息和节点进度两个部分。
任务详情
|------|----------------------------|
| 任务标识 | 20241102103055001234567890 |
| 订正类型 | 产品成本修正 |
| 创建人 | 张三 |
| 创建时间 | 2024-11-02 10:30:55 |
| 任务状态 | 进行中 |
节点进度
| 排序 | 节点名称 | 处理状态 | 更新时间 |
|---|---|---|---|
| 1 | 成本基础 | ✅ 完成 | 2024-11-02 10:31:05 |
| 2 | 定价计算 | 🔄 处理中 | 2024-11-02 10:31:10 |
| 3 | 财务报表 | ⏳ 等待 | - |
| 4 | 库存核算 | ⏳ 等待 | - |
⚠️ 当前节点: 定价计算 (处理失败 - 可重试)
错误信息: 数据库连接超时
🔄 重试当前节点 ⏸ 暂停任务 🔍 查看详情
五、失败重试机制
分布式环境下,节点执行失败是不可避免的情况。本方案设计了多层次的重试策略来保证最终一致性。
5.1 重试策略
节点执行失败
重试机制
手动重试
点击重试按钮
手动触发
延迟检查重试
delayCheck=1
30分钟后检查
关联任务重试
主任务触发
5.2 重试API
java
@PutMapping("/retryNode")
public Result<Void> retryNode(Long nodeId) {
DataRevisedTaskProcess process = processService.getById(nodeId);
DataRevisedTask task = taskService.getById(process.getRevisedTaskId());
taskService.executeNode(task, process);
return Result.success();
}
六、事务性发件箱集成
本方案复用事务性发件箱 模式确保消息可靠投递,详见 《事务性发件箱模式设计与实现》。
6.1 消息幂等设计
消息幂等的核心在于幂等键的设计。由于消息会因重试、网络波动等导致重复发送,幂等键必须满足:同一个业务上下文下,相同消息的幂等键应保持一致。
幂等键设计原则
| 消息类型 | 幂等键组成 | 说明 |
|---|---|---|
| 任务执行消息 | TASK_EXECUTE-{taskIdentifier}-{processId} |
头节点首次执行触发 |
| 执行结果消息 | TASK_RESULT-{taskIdentifier}-{processId} |
各节点执行完成结果回执 |
| 延迟检查消息 | TASK_DELAY_CHECK-{taskIdentifier}-{processId} |
延迟检查触发 |
七、总结
7.1 核心能力
| 能力 | 实现方式 |
|---|---|
| 多节点顺序推进 | nodeSort排序 + 结果回执驱动 |
| 可视化进度追踪 | 任务状态 + 节点状态多维度展示 |
| 失败可重试 | 手动重试 + 延迟检查 + 状态机 |
| 事务性保证 | 事务性发件箱模式 |
7.2 方案优势
- 节点解耦:各节点通过消息队列通信,互不依赖
- 进度可视化:实时查看各节点修正状态
- 失败无感:节点级别失败重试,不影响其他节点
- 可靠投递:事务性发件箱确保消息不丢失
- 可扩展:新增节点只需配置,无需修改代码
7.3 适用场景
- 多服务节点数据修正
- 分布式数据同步
- 跨服务数据对账
- 批量数据修复任务