6.2 Actor模型与消息驱动


6.2 Actor 模型与消息驱动

Actor 是"私有状态 + 消息收件箱"的并发单元:不共享内存、不加锁,所有交互都是异步消息。Scala 生态的实现从 Akka 演化到开源继承者 Pekko,本节讲模型本身与它的适用边界。

为什么锁不是答案

多线程共享一个计数器,经典做法是加锁。锁的问题清单很长:忘锁、锁粒度、死锁、优先级反转,而且每个 bug 都在并发压力下才现身。Actor 的思路是釜底抽薪——让每个状态只被一个执行体触碰

// 概念示意(Pekko 风格 API) class Counter extends Actor: private var count = 0 // 私有,外界摸不到 def receive: Receive = case "inc" => count += 1 case "get" => sender() ! count // 回消息

外界只能发消息(counter ! "inc"),消息在收件箱排队、逐条处理。count 永远只被 Actor 自己的线程读写,锁从设计里消失了——不是被更聪明地使用,而是被取消。

Actor 的消息流转

一个订单簿的例子

限价订单簿是典型的高状态密度场景:买卖单频繁插删、成交配对必须原子。用共享 Map 加锁写,配对逻辑与锁边界纠缠;用 Actor 写,订单簿的整个状态就是 Actor 的私有字段,"撮合一批订单"天然原子:

case class Limit(side: Side, price: BigDecimal, qty: Int) case class Match(side: Side, price: BigDecimal, qty: Int) class OrderBook extends Actor: private var bids = SortedMap.empty[BigDecimal, Int] private var asks = SortedMap.empty[BigDecimal, Int] def receive = case l @ Limit(Bid, p, q) => bids += p -> (bids.getOrElse(p, 0) + q); tryMatch() case l @ Limit(Ask, p, q) => asks += p -> (asks.getOrElse(p, 0) + q); tryMatch() private def tryMatch(): Unit = ??? // 读自己的 bids asks,无锁撮合

模式匹配(3.3)在这里正好做消息分派,样例类(2.1)正好做消息载体——前几章的装备全在肩上。

代价与边界

优点 代价
无锁、状态天然原子 单 Actor 吞吐有上限(消息排队)
天然分布式叙事(消息可跨节点) 调试栈被消息切断,需要因果追踪
背压、监督、容错有成熟模式 学习与运维成本高于 Future 管线

选型经验:无状态并行用 Future(6.1);状态密集、需横向扩展时才上 Actor。大多数业务系统里 Actor 的合理位置是少数几个"热点状态点"(撮合、会话、连接管理),而不是把每个 service 都包成 Actor——后者只是把函数调用换成排队,徒增延迟。

监督策略是 Actor 体系的另一价值:子 Actor 崩溃时由父级决定重启或上报,故障处理本身成为架构的一部分而非散落的 try-catch。

完整案例:用 Actor 做限流闸门

背景:下游接口每秒只吃 10 个请求,直接并发打过去会被熔断。用单个 Actor 做令牌闸门,所有请求先向它领票:

import akka.actor.typed.*, ActorRef enum Gate: case TryPass(replyTo: ActorRef[Boolean]) case Return class GateActor(n: Int) extends Actor[Gate]: private var tokens = n def receive(msg: Gate): Unit = msg match case Gate.TryPass(reply) => if tokens > 0 then { tokens -= 1; reply ! true } else reply ! false case Gate.Return => tokens += 1 // 使用方:请求前问一次,失败走延迟重试;请求完成后归还一张票

操作:请求方发送 TryPass 并等待回执,拿到 true 才放行,处理完发 Return 归还。结果:无论多少并发线程,令牌数只被这一个 Actor 串行修改,不存在检查与扣减之间的竞态窗口。解读:Actor 把"锁保护状态"改写成"状态只归一个线程"——不是消灭竞争,而是让竞争在邮箱里排队。变式:Akka 的 ask 模式、Pekko(Akka 的开源继任者)API 几乎相同,迁移成本低;纯标准库场景可用 本章前面学过的原子引用替代,但状态一多 Actor 仍是最清晰的建模。

消息设计三原则

  • 消息是不可变样例类或枚举,绝不传可变集合与外部句柄
  • 每条消息自带回复地址(replyTo),Actor 不保存对别人的长期引用
  • 协议用 sealed 枚举集中声明,协议即 API 文档

常见事故:再补一句选型结论:需要持久化队列与集群分片时才上 Akka/Pekko 全家桶,单进程内的状态收拢用标准库的 AtomicReference 加消息循环已足够,别为简单问题引入重型运行时。邮箱溢出与死信

Actor 默认邮箱无上限,下游处理慢时消息无限堆积最终 OOM。处置清单:配置 bounded mailbox 让上游收到背压;监控 dead letters 日志——发给已停止 Actor 的消息会进死信队列,批量死信几乎总意味着生命周期管理有 bug;ask 超时要设置,否则一个卡死的下游会拖住整条调用链。这些都是上线后才补课的贵知识,营地阶段先记清单。

本节要点回顾

  • 核心等式:私有状态 + 排队消息 = 无锁并发。
  • 样例类做消息、模式匹配做分派,2、3 章装备在此集成。
  • 适用边界:状态密集与分布式扩展;无状态并行交给 Future。
  • 监督树让容错结构化,是 Akka/Pekko 体系超出"并发工具"的部分。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U