← 返回题目列表

命令模式如何用于命令队列和异步任务?

高频 中等 第 6 / 25 题 更新于 2026/07/28
命令模式命令队列异步任务任务调度设计模式

简化版

命令模式可以把每个任务封装成命令对象,然后放入队列,由工作线程或调度器统一取出执行。这样提交任务的一方不需要关心任务何时执行、由谁执行,也方便实现排队、重试、限流、日志和异步调度。

详细版

命令队列的基本结构是:

Producer 创建 Command
Queue 保存 Command
Worker 取出 Command 并调用 execute()
Receiver 执行真实业务

示例:

interface Command {
    void execute();
}

BlockingQueue<Command> queue = new LinkedBlockingQueue<>();

// 生产者
queue.put(new SendEmailCommand(emailService, message));

// 消费者
Command command = queue.take();
command.execute();

命令模式让异步任务具有统一抽象。所有任务都实现 Command,调度器不需要知道具体任务类型。

真实项目里还要补充幂等、失败重试、超时控制、死信队列、持久化、监控告警等能力。

完整版教学

一、异步任务为什么适合命令模式

异步系统里,提交任务和执行任务通常不是同一个线程,甚至不是同一台机器。

如果提交方直接调用业务方法:

emailService.send(message);

它就是同步执行。

命令模式可以把这次操作封装起来:

class SendEmailCommand implements Command {
    private final EmailService emailService;
    private final Message message;

    SendEmailCommand(EmailService emailService, Message message) {
        this.emailService = emailService;
        this.message = message;
    }

    public void execute() {
        emailService.send(message);
    }
}

提交方只负责把命令放入队列,执行时机交给调度器。

二、命令队列的基本代码

class CommandQueue {
    private final BlockingQueue<Command> queue = new LinkedBlockingQueue<>();

    void submit(Command command) throws InterruptedException {
        queue.put(command);
    }

    void start() {
        new Thread(() -> {
            while (true) {
                try {
                    Command command = queue.take();
                    command.execute();
                } catch (Exception e) {
                    // 记录日志、重试或转入失败队列
                }
            }
        }).start();
    }
}

这个队列不关心命令类型。发邮件、生成报表、刷新缓存都可以作为命令提交。

三、命令模式带来的统一调度能力

当所有任务都抽象成 Command 后,调度器可以统一处理:

  • 任务排队;
  • 并发执行;
  • 延迟执行;
  • 优先级调度;
  • 超时控制;
  • 失败重试;
  • 执行日志;
  • 监控指标。

这些能力不需要散落在每个业务调用点。

四、内存队列和消息队列的区别

本地 BlockingQueue 适合进程内任务,简单但不可靠:

  • 进程重启任务会丢;
  • 不能跨机器消费;
  • 容量和可靠性有限。

真实系统可能用消息队列:

Command DTO -> Kafka/RocketMQ/RabbitMQ -> Consumer -> execute

这时命令对象通常不能直接序列化完整 Java 对象,而是把命令类型和参数保存成消息,再由消费者恢复执行逻辑。

五、异步命令必须考虑幂等

消息队列环境里,命令可能重复投递。

如果 SendCouponCommand 执行两次,用户可能收到两张券。工程里要为命令设计幂等键:

commandId
businessKey
deduplication table
unique constraint

消费者执行前先判断是否已经处理过,避免重复执行导致数据错误。

六、失败处理决定命令队列是否可用

异步命令执行失败时,需要明确策略:

  • 立即重试;
  • 延迟重试;
  • 达到次数后进入死信队列;
  • 记录失败原因;
  • 告警人工介入;
  • 设计补偿命令。

命令模式提供了任务抽象,但可靠执行还需要队列、中间件和工程治理配合。

七、常见误区与追问

异步命令的验收重点是交付语义,而不是“放进队列就结束”。例如订单扣减命令因超时被投递2次,处理端必须借助业务幂等键只生效1次,同时记录每次尝试的结果。内存队列适合进程内削峰;需要宕机恢复时,应选择具有持久化、确认与重投能力的消息设施。

检查维度判定依据
内存队列低延迟,但进程退出可能丢任务
持久消息队列可恢复,但要处理重复与乱序

易错点:消息“至少一次”通常意味着业务处理必须能安全地重复执行。

  • 误区:消费者返回成功就代表业务一定完成。 应在业务提交后再确认消息,否则确认与事务之间的窗口会造成丢失或重复。
  • 追问:命令参数能直接携带完整领域对象吗? 跨进程时更适合传稳定标识和必要快照,避免序列化内部对象结构与陈旧状态。
  • 误区:重试次数越多可靠性越高。 永久性错误会被反复放大,应区分可重试错误并设置退避和死信处理。
  • 追问:如何保证同一订单内的命令顺序? 可按订单 ID 分区到同一有序队列,同时防止多个消费者并行处理同一键。
  • 追问:异步命令如何观测? 至少记录 commandId、业务键、尝试次数、耗时、最终状态和失败原因。

八、加强记忆

命令模式用于异步任务时,就是把每次任务请求封装成 Command,先放入队列,再由 Worker 统一执行。它让任务提交方和执行方解耦,并为排队、重试、限流、日志、监控和补偿提供统一入口。