35 角色与进程

用角色和进程封装状态,通过邮箱与消息协作,避免调用方直接共享内部可变数据。

学习目标

  • 能解释消息如何经过邮箱进入角色,并指出状态变化的唯一拥有者
  • 能修改一段消息处理代码,补上重复、超时和角色失败时的拒绝与恢复路径
  • 能回答:移除邮箱后,第一处可观察的差异在哪里,怎样从原始消息重放?

为什么这一单元不可压缩

想象两个柜台同时处理同一笔订单。顾客把要求交给柜台,柜台按顺序收件,只有一位工作人员能改订单,旁边的人负责发现超时和重新安排。任何人都可以直接翻改草稿时,最后留下的结果看似完整,却无法知道谁覆盖了谁。

软件里的并发请求也会遇到这个问题。若发送者直接改动处理者正在使用的数据,重复请求、等待和失败就会互相污染;测试偶尔通过,线上却很难回答“哪一次变化先发生”。

本章把这条路径拆成可观察的站点:一条请求怎样到达、一位拥有者怎样处理、失败怎样升级、响应怎样证明自己对应了哪条请求。

本章验收路线

本单元只改变一个条件:先观察消息链的正常路径,再移除邮箱。预期是首差停在“无法证明到达与排队”的位置,而不是让角色状态悄悄改变。先预测:如果消息没有邮箱,哪一个节点应拒绝继续?

消息不共享状态:只把意图交给拥有者消息:只表达意图,不直接触碰内部状态1消息带身份与意图当前观察点2邮箱排队与背压等待证据3角色状态单一拥有者等待证据4监督超时与重启等待证据5响应契约与结果等待证据验收合同:每条消息都有所有者、处理边界、失败升级与可追踪响应角色内部状态不直接暴露给发送者,状态变化只由消息处理产生先预测首差,再用单步或故障注入验证消息链
专属图示:消息只经过邮箱进入角色,监督负责让失败可见、响应负责让契约可核对。

复核记录至少保存:消息身份、当前节点、状态变化、下游接收者、拒绝原因、恢复动作和消息契约。最终响应存在,不等于整条路径可证明;必须能从原始消息重放同一结论。

知识点清单

知识点本章的可视化是否进入名词解释
消息与消息契约消息链图、响应节点
邮箱与背压邮箱节点、故障注入
角色状态与所有权角色状态节点、单步实验
监督与失败升级监督节点、故障回退
提示59:用角色实现并发性时不必共享状态三步 Stepper 与实验台

五个边界如何互相约束

<Term def="描述意图、输入身份和处理目标的一份不可变请求。">消息</Term> 是发送者交给系统的事实,而不是对内部变量的直接引用。消息应带有唯一身份、目标对象和允许的副作用;重复消息可以被识别,非法消息可以在进入处理前被拒绝。

<Term def="保存待处理消息并定义到达顺序、容量和等待策略的通道。">邮箱</Term> 把到达和处理分开。它可以让角色一次只处理一条消息,也必须暴露容量、超时和拒绝条件;没有邮箱,就没有可复核的排队证据。

<Term def="拥有自己的状态并通过消息与外界协作的独立处理者。">角色</Term> 只允许自己的处理循环改变内部状态。发送者不拿状态引用,接收者也不把中间对象共享出去;每次改变都应能追溯到一条消息。

<Term def="规定谁可以改变状态、何时交接以及失败后谁负责恢复的约定。">所有权</Term> 是并发设计的责任边界。单一拥有者并不意味着没有并发,而是把并发请求变成可排队的意图,避免多个调用方同时写同一份可变数据。

<Term def="观察角色是否存活、是否超时以及是否需要重启或转移工作的外部监督机制。">监督</Term> 不负责偷偷修正状态。它只根据可观察的心跳、超时和失败消息作出升级决定,恢复时仍要从原始消息或持久化日志重放。

提示59:用角色实现并发性时不必共享状态

提示59的关键不是给每个请求都创建一个重量级线程,而是把“谁能写状态”变成清楚的设计选择。发送者只提交消息,角色按自己的顺序处理,监督记录失败;这样读者可以分别检查消息语义、排队行为、状态变化和恢复路径。

下面的 TypeScript 片段只展示边界。receive 不返回状态引用,handle 也只在角色内部执行;真实系统还要把超时、重试上限和幂等键纳入契约。

type Message = {
  id: string;
  kind: "reserve" | "release";
  itemId: string;
};
 
type ActorState = { reserved: Set<string> };
 
function handle(state: ActorState, message: Message) {
  if (message.kind === "reserve") state.reserved.add(message.itemId);
  if (message.kind === "release") state.reserved.delete(message.itemId);
  return state;
}

关键不在函数名,而在责任边界:调用者不能直接拿到 ActorState;同一 id 的消息要么被识别为重复,要么被明确拒绝。发生失败时,监督只能依据契约决定停止、重启或转移,不能把最后一个响应当成完整证据。

从消息到响应的三步观察

每一步都保留同一条输入,只改变观察焦点。先说出预期首差,再看图示中的高亮站点;如果观察结果与预测不符,就保存分叉处并从原始消息重新开始。

分步1 / 3

1. 冻结消息与契约

写下消息身份、目标对象、允许的副作用和拒绝条件。发送者只交付意图,不共享角色内部状态。

消息不共享状态:只把意图交给拥有者消息:只表达意图,不直接触碰内部状态1消息带身份与意图当前观察点2邮箱排队与背压等待证据3角色状态单一拥有者等待证据4监督超时与重启等待证据5响应契约与结果等待证据验收合同:每条消息都有所有者、处理边界、失败升级与可追踪响应角色内部状态不直接暴露给发送者,状态变化只由消息处理产生先预测首差,再用单步或故障注入验证消息链
专属图示:消息只经过邮箱进入角色,监督负责让失败可见、响应负责让契约可核对。

常见误区与回退

可重置实验:故障如何停在第一处

猜一猜:点击“注入单故障”并把进度推进到邮箱后,角色状态应该改变吗?正确的观察是邮箱证据缺失,角色拒绝继续处理;点击重置后,再从同一条消息重新验证正常路径。

消息到响应实验台

通过播放、单步和拖动进度,观察内部状态如何只被拥有者改变。

当前消息处理节点消息:只表达意图,不直接触碰内部状态消息带身份与意图当前节点邮箱排队与背压待观察角色状态单一拥有者待观察监督超时与重启待观察响应契约与结果待观察每一步都保留输入、当前节点、状态变化、下游接收者与拒绝条件当前:消息:只表达意图,不直接触碰内部状态

第 1 / 5 步 · 消息:只表达意图,不直接触碰内部状态

只有观察到消息契约与角色状态都成立,响应才可被接受。

实验记录可以写成如下形式。它描述事实和回退,不把系统压缩成一个“并发可靠度”分数:

actor_run:
  message_id: reserve-204
  current_node: mailbox
  state_change: none
  downstream: actor-state
  rejection: mailbox-unavailable
  recovery: restore-mailbox-and-replay
  contract: idempotent-message-v1

current_node 从邮箱改成响应而不补充中间证据,是典型的“结果看起来成功”陷阱。重放时必须保留原始消息身份和同一个边界,才知道恢复是否真的修复了问题。

如何选择边界

场景首选边界必须验收不要做什么
一个对象有明确写入者角色 + 邮箱所有权、容量、顺序暴露可变状态引用
下游速度不同有界邮箱 + 背压超时、拒绝、重试上限假设邮箱无限容量
角色可能失败监督心跳、升级、重放复制最后一个成功响应

边界的大小由消息契约和状态不变量决定。并发请求可以同时到达,但状态改变仍应落在一个可观察的处理点;监督的职责是让“没有完成”成为明确状态,而不是掩盖它。

本章回顾

  • 消息表达意图,发送者不共享角色内部状态。
  • 邮箱把到达顺序、容量和背压变成可观察边界。
  • 角色拥有状态,所有权让写入责任集中且可追踪。
  • 监督处理超时与失败,恢复必须从原始消息重放。
  • 先定位首差,再判断响应是否满足消息契约。

可验证练习

练习

问题 1:找出首个差异。 两条 reserve 消息同时到达,一个邮箱容量为 1;第一条处理超时,第二条被系统直接标记为完成。请指出不能宣布成功的节点,并写出两项应保存的证据。

问题 2:修改 Demo 代码。 让下面的处理函数拒绝重复消息,并说明角色失败后监督如何恢复。要求不把角色状态返回给调用者。

function receive(state: ActorState, message: Message) {
  handle(state, message);
  return { ok: true, state };
}

问题 3:做边界选型。 三个下游处理速度不同,可能重复收到同一通知;你会让它们共享一个可变对象,还是发送带版本和幂等键的消息?写出一个拒绝条件。

名词解释

本章出现的专业名词,用大白话再讲一遍。

消息

一份只表达意图和输入身份的请求;它可以被排队、拒绝、重试或重放。

邮箱

保存待处理消息的通道;它决定顺序、容量和等待太久时怎么办。

角色

拥有自己状态的处理者;外部只能发消息,不能直接改它的内部数据。

所有权

规定谁有修改权、何时交接以及失败后谁负责恢复的约定。

监督

观察存活、超时和失败并触发升级或重启的机制,不替系统伪造成功。

出处与改写范围

本文为改编重写:目录、版本与提示坐标用于限定范围;正文、代码、双图示、交互实验和练习均为本课程独立设计,不复制原书正文、插图或答案。

前后导航

讨论

评论区加载中…