命令模式在系统异步任务调度与重试中的落地
在做复杂的业务系统开发时,我们经常会遇到长流程、多步骤的后台异步任务,例如“批量跨机构用户开户”、“大额资金清结算对账”、“第三方多渠道批量退款与数据同步”。这类任务通常具备三个显著特征:
- 调用链路长:依赖多个外部不稳定 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 AsyncCommand<T> { /** * 命令唯一标识类型,如 "USER_ACCOUNT_OPEN_CMD" */ String getCommandType(); /** * 核心业务执行逻辑 * @param context 上下文参数 * @return 执行结果 */ CommandResult execute(T context) throws Exception; /** * 补偿/回滚逻辑(当重试达到上限依然失败时触发反向清理) */ default void compensate(T context, Throwable cause) { // 默认空实现,子类按需覆盖 } /** * 参数类型,用于 Jackson 反序列化 */ Class<T> 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 AsyncCommand<OpenAccountContext> { 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 Class<OpenAccountContext> 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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 Map<String, AsyncCommand<?>> commandMap = new ConcurrentHashMap<>(); public CommandFactory(List<AsyncCommand<?>> commands) { for (AsyncCommand<?> cmd : commands) { commandMap.put(cmd.getCommandType(), cmd); } } @SuppressWarnings("unchecked") public <T> AsyncCommand<T> getCommand(String commandType) { AsyncCommand<?> command = commandMap.get(commandType); if (command == null) { throw new IllegalArgumentException("未找到命令处理器: " + commandType); } return (AsyncCommand<T>) 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() { // 乐观锁抓取到期任务 List<AsyncTaskDO> 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),防止多次重试造成下游资金多次扣减。