在现代软件架构中,命令处理是一个核心组件,它负责接收、处理和反馈命令的状态。本文将探讨如何设计一个健壮的命令处理系统,以确保命令的高效和可靠执行。
命令处理系统的基本流程相对简单:从队列中取出下一个命令,找到合适的处理器,读取命令参数,将参数传递给处理器,等待命令完成,然后继续处理下一个命令。然而,在现实世界中,资源是有限的,硬件可能会失败,命令可能无法完成,因此需要更复杂的设计和编码。
在Windows Azure等云平台上,存在一些限制,这些限制会影响应用程序的设置。例如,队列存储每秒最多只能读取500次,如果应用程序被认为是负担过重,可能会被更积极地限制。因此,不能在紧密循环中编写队列读取代码,而应该有自己最小的间隔读取时间。
此外,处理不是免费的。在Windows Azure上,即使队列中没有消息,每次访问队列也会收取费用(虽然非常小)。因此,应该批量读取命令,而不是一次读取一个,并且如果多次读取循环中没有命令要处理,还应该动态增加间隔读取时间。
定义了一个名为IPullModeCommandProcessor
的接口,它允许设置每次迭代中要拉取的命令的最大数量,以及两次轮询命令列表之间的等待时间。这些设置有助于调整处理器,以减少新命令的等待时间,同时减少“无事可做”循环的成本。
命令可能无法完成的原因可以分为两类:临时问题(如硬件繁忙)和永久问题(如不可能执行的命令)。在大多数商业环境中,记录和分类所有可能的失败原因并不明智,因此通常会使用折中方案:尝试重新执行命令几次,如果仍然失败,则将其视为无法完成的命令。在偏好设置中,可以根据命令类型设置重试次数,因为某些类型的命令更容易受到可重复的临时错误的影响。
每次从队列中读取命令时,都会检查DequeueCount
属性。如果大于或等于毒消息上限,则将命令添加到“毒消息”表中,并从队列中删除。这可以防止再次尝试。如果消息的出队计数小于这个上限,则在命令成功完成之前不会从队列中删除。这意味着,如果命令由于任何原因未能完成,它将在稍后再次尝试。
然而,有些命令可能会以部分完成的状态失败。例如,如果有一个命令是要将一百个文件从一种格式转换为另一种格式,并且在第30个文件上失败,那么当它再次出现时,就需要做出选择——回滚失败命令的更改,或者从失败命令到达的地方开始。在这两种情况下,都需要通过“步骤完成”事件来跟踪命令的进度。
命令处理器实现为工作角色,这个工作角色可能随时失败。Azure会在检测到任何工作角色失败时启动新实例,但这需要一点时间,这将影响处理。
解决方案是通过水平扩展(弹性)工作角色,至少运行一个比所需更多的工作角色——这样,如果一个节点失败,这个备用节点可以接管。
一个常见的问题是,如何反馈用户发出的命令已完成?建议使用命令的唯一标识符,并有一个查询返回命令的状态。然后可以轮询这个查询——无论是由客户端显式轮询,还是由服务器端的任务轮询——并相应地发送任何状态更改。
这保持了命令和查询之间的分离的纯净性,允许它们独立扩展。
在CQRS系统中实现命令端的一个有趣方式是将每个命令的生命周期实现为事件流,并使用该事件流上的投影来获取命令的当前状态。
例如,可能会为“命令创建”、“命令执行开始”、“致命错误”、“命令完成”等定义命令事件。每个事件都有事件属性,可以详细说明命令发生了什么,通过“播放”命令的事件流,然后可以推导出其当前状态。
这也允许命令处理器通过播放它们的事件流来保留一组“待办”命令,因此如果它们在临时故障后重新排队,就不必显式重新发出任何命令。