ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

命令模式在系统异步任务调度与重试中的落地

命令模式在系统异步任务调度与重试中的落地 命令模式在系统异步任务调度与重试中的落地在做复杂的业务系统开发时我们经常会遇到长流程、多步骤的后台异步任务例如“批量跨机构用户开户”、“大额资金清结算对账”、“第三方多渠道批量退款与数据同步”。这类任务通常具备三个显著特征调用链路长依赖多个外部不稳定 RPC 接口或第三方 HTTP API耗时不可控整套流程执行完毕可能需要数秒乃至数分钟故障易发性中途任何一步遭遇网络抖动、限流或下游瞬时宕机都必须能够安全重试或自动补偿。许多团队在早期开发时习惯写一个上千行的过程式方法在一个巨大的try-catch循环里把每个步骤依次调用一遍并在各个步骤硬编码重试计数器和状态判断。随着业务类型不断增加代码迅速沦为难以维护的“意大利面条”一旦服务在任务执行中途因发版重启内存中正在执行的任务状态瞬间蒸发导致数据处于既未完成也未回滚的中间悬挂状态。引入设计模式中的命令模式Command Pattern将请求封装为包含元数据、入参、执行逻辑与补偿钩子的独立对象配合任务持久化与调度器解耦是解决这一架构难题的标准方案。命令模式在异步调度中的架构设计GoF 命令模式的核心是解耦命令的发出者Invoker / 调度器与命令的执行者Receiver / 业务逻辑。在异步重试体系中整体架构分为四层命令契约层Command Contract定义通用的AsyncCommand接口规范执行、补偿、序列化与重试策略任务持久化层Persistence将命令类型、序列化入参、当前状态INIT / RUNNING / SUCCESS / FAILED、重试次数、下次重试时间持久化到数据库命令工厂与分发器Command Factory根据数据库中存储的命令类型利用 Spring 容器动态反序列化并装配对应的命令实例调度执行器Invoker / Executor基于线程池拉取到期任务统一执行重试退避算法、事务包裹、异常捕获与死信流转。核心接口与命令模型设计1. 异步命令统一抽象接口package com.example.task.command; public interface AsyncCommandT { /** * 命令唯一标识类型如 USER_ACCOUNT_OPEN_CMD */ String getCommandType(); /** * 核心业务执行逻辑 * param context 上下文参数 * return 执行结果 */ CommandResult execute(T context) throws Exception; /** * 补偿/回滚逻辑当重试达到上限依然失败时触发反向清理 */ default void compensate(T context, Throwable cause) { // 默认空实现子类按需覆盖 } /** * 参数类型用于 Jackson 反序列化 */ ClassT getContextType(); /** * 最大允许重试次数 */ default int getMaxRetryCount() { return 5; } }2. 具体的业务命令实现以跨行开户为例命令实现类交由 Spring 容器管理可直接注入下游 Service 或 Feign Client。package com.example.task.command.impl; import com.example.task.command.AsyncCommand; import com.example.task.command.CommandResult; import com.example.task.rpc.BankRpcClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; Component public class BankOpenAccountCommand implements AsyncCommandOpenAccountContext { private static final Logger log LoggerFactory.getLogger(BankOpenAccountCommand.class); private final BankRpcClient bankRpcClient; public BankOpenAccountCommand(BankRpcClient bankRpcClient) { this.bankRpcClient bankRpcClient; } Override public String getCommandType() { return BANK_OPEN_ACCOUNT_CMD; } Override public CommandResult execute(OpenAccountContext context) throws Exception { log.info(执行开户命令: UserNo{}, BankCode{}, context.userNo(), context.bankCode()); // 调用外部不稳定接口 String remoteAccountNo bankRpcClient.openAccount(context.userNo(), context.idCard()); return CommandResult.success(开户成功账号: remoteAccountNo); } Override public void compensate(OpenAccountContext context, Throwable cause) { log.warn(开户达到最大重试失败触发冲正反向注销流程: UserNo{}, context.userNo()); bankRpcClient.cancelPendingAccount(context.userNo()); } Override public ClassOpenAccountContext getContextType() { return OpenAccountContext.class; } }任务持久化与命令工厂注册表1. 数据库任务表结构设计CREATE TABLE t_async_task ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键ID, task_no VARCHAR(64) NOT NULL COMMENT 业务任务唯一流水号, command_type VARCHAR(64) NOT NULL COMMENT 命令类型标识, payload TEXT NOT NULL COMMENT 命令入参 JSON 字符串, status VARCHAR(32) NOT NULL DEFAULT INIT COMMENT 状态: INIT, RUNNING, SUCCESS, FAILED, DEAD, retry_count INT NOT NULL DEFAULT 0 COMMENT 已重试次数, max_retry INT NOT NULL DEFAULT 5 COMMENT 最大重试次数, next_retry_time DATETIME NOT NULL COMMENT 下次触发时间, error_msg VARCHAR(1024) DEFAULT NULL COMMENT 最后一次失败原因, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_task_no (task_no), KEY idx_status_next_time (status, next_retry_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT通用异步任务调度表;2. 命令工厂基于 Spring 容器自动装配package com.example.task.command; import org.springframework.stereotype.Component; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; Component public class CommandFactory { private final MapString, AsyncCommand? commandMap new ConcurrentHashMap(); public CommandFactory(ListAsyncCommand? commands) { for (AsyncCommand? cmd : commands) { commandMap.put(cmd.getCommandType(), cmd); } } SuppressWarnings(unchecked) public T AsyncCommandT getCommand(String commandType) { AsyncCommand? command commandMap.get(commandType); if (command null) { throw new IllegalArgumentException(未找到命令处理器: commandType); } return (AsyncCommandT) command; } }调度执行器与指数退避重试落地调度器负责定时扫描到期的任务并采用**指数退避Exponential Backoff with Jitter**计算下一次重试时间避免失败后立刻高频重试形成雪崩。package com.example.task.executor; import com.example.task.command.AsyncCommand; import com.example.task.command.CommandFactory; import com.example.task.command.CommandResult; import com.example.task.entity.AsyncTaskDO; import com.example.task.mapper.AsyncTaskMapper; import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.time.LocalDateTime; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; Component public class AsyncTaskScheduler { private static final Logger log LoggerFactory.getLogger(AsyncTaskScheduler.class); private final AsyncTaskMapper taskMapper; private final CommandFactory commandFactory; private final ObjectMapper objectMapper new ObjectMapper(); private final ExecutorService workerPool Executors.newFixedThreadPool(8); public AsyncTaskScheduler(AsyncTaskMapper taskMapper, CommandFactory commandFactory) { this.taskMapper taskMapper; this.commandFactory commandFactory; } Scheduled(fixedDelay 3000) // 每 3 秒拉取一轮待执行任务 public void schedulePendingTasks() { // 乐观锁抓取到期任务 ListAsyncTaskDO tasks taskMapper.selectExecutableTasks(LocalDateTime.now(), 20); for (AsyncTaskDO task : tasks) { workerPool.submit(() - processSingleTask(task)); } } SuppressWarnings({rawtypes, unchecked}) private void processSingleTask(AsyncTaskDO task) { // 1. CAS 锁定任务状态为 RUNNING int updated taskMapper.updateStatus(task.getId(), INIT, RUNNING); if (updated 0) { return; // 抢锁失败被其他节点处理 } try { AsyncCommand command commandFactory.getCommand(task.getCommandType()); Object context objectMapper.readValue(task.getPayload(), command.getContextType()); // 2. 执行命令 CommandResult result command.execute(context); // 3. 标记成功 taskMapper.markSuccess(task.getId(), result.message()); log.info(异步任务执行成功: TaskNo{}, task.getTaskNo()); } catch (Throwable ex) { handleTaskFailure(task, ex); } } SuppressWarnings({rawtypes, unchecked}) private void handleTaskFailure(AsyncTaskDO task, Throwable ex) { int currentRetry task.getRetryCount() 1; log.warn(异步任务执行失败: TaskNo{}, RetryCount{}, Error{}, task.getTaskNo(), currentRetry, ex.getMessage()); if (currentRetry task.getMaxRetry()) { // 达到最大重试标记为 DEAD 并执行补偿 taskMapper.markDead(task.getId(), ex.getMessage()); try { AsyncCommand command commandFactory.getCommand(task.getCommandType()); Object context objectMapper.readValue(task.getPayload(), command.getContextType()); command.compensate(context, ex); } catch (Exception e) { log.error(执行任务补偿失败: TaskNo{}, task.getTaskNo(), e); } } else { // 计算指数退避时间间隔 2^(retry) * 5 秒 (5s, 10s, 20s, 40s...) long delaySeconds (long) Math.pow(2, currentRetry) * 5; LocalDateTime nextRetryTime LocalDateTime.now().plusSeconds(delaySeconds); taskMapper.scheduleRetry(task.getId(), currentRetry, nextRetryTime, ex.getMessage()); } } }生产级收益与避坑指南彻底与业务解耦未来新增任何长耗时异步任务如优惠券批量作废、PDF 合同生成只需编写一个实现AsyncCommand的 Spring Bean完全不需要重写状态机、线程池和重试逻辑。宕机自愈与断点恢复所有任务在入库时刻即持久化。即使生产集群遭遇整体重启新实例启动后调度器依然会按next_retry_time无缝拉取未完成的任务继续执行彻底消除了任务丢失风险。幂等性保障是先决条件命令模式虽然解决了调度和重试但被调用的下游接口必须原生支持幂等如传递基于task_no衍生的唯一业务流水号biz_request_id防止多次重试造成下游资金多次扣减。
返回列表