命令模式如何用于命令队列和异步任务?
简化版
命令模式可以把每个任务封装成命令对象,然后放入队列,由工作线程或调度器统一取出执行。这样提交任务的一方不需要关心任务何时执行、由谁执行,也方便实现排队、重试、限流、日志和异步调度。
详细版
命令队列的基本结构是:
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 统一执行。它让任务提交方和执行方解耦,并为排队、重试、限流、日志、监控和补偿提供统一入口。