TL;DR
- 交易系统的并发灾难,本质多来自共享可变状态:订单簿/账户余额在高争用下会被锁竞争、死锁、上下文切换开销拖垮。
- Actor 模型用状态私有化 + 异步消息把一致性“内建”进结构里:单个 Actor 串行处理消息,业务逻辑可按单线程思维写,天然线程安全。
- 在交易系统里,把账户和交易对订单簿建模为 Actor 很顺:同一账户/同一交易对内严格顺序,不同账户/交易对可并行,吞吐随分片水平扩展。
- 落地的关键不在理论,而在工程细节:不要在 Actor 里做阻塞 I/O、合理配置 Dispatcher、消息协议要稳定、必要时用事件溯源做可恢复状态。
1. 为什么“加锁”在高并发交易系统里越来越像饮鸩止渴
订单簿是交易系统的核心状态:新增/撤单/撮合都会修改它;账户余额也是关键状态:下单冻结、成交扣减、解冻回滚都要求绝对准确。
传统“多线程 + 共享内存 + 锁”的常见两条路,都不好走:
- 粗粒度锁:对订单簿加一把大锁,正确但吞吐被单线程上限卡死,多核被浪费在等待/切换上。
- 细粒度锁:锁拆细后,撮合链路会跨多个对象加锁(订单簿买卖侧、买卖账户等),锁顺序一复杂就容易死锁;为了防死锁又引入“锁顺序规约”,心智负担暴涨。
归根结底:共享可变状态迫使你引入外部协调(锁),而锁带来的性能与复杂性,是系统长期演进中的隐性利息。
2. Actor 模型:把并发从“线程/锁”提升到“业务实体/消息流”
三个关键点
- 串行处理:Actor 一次只处理一条消息(Mailbox 里的队列),其内部逻辑天然不并发。
- 状态封装:Actor 的状态只在内部可见,外部无法直接读写;改变状态只能发消息让它自己改。
- 异步通信:Actor 之间通过不可变消息异步交互,发送方不阻塞等待(可选 ask/response),解耦时空依赖。
注意:Actor 不等于 OS 线程。成熟框架(如 Akka)会用线程池 Dispatcher 在用户态调度大量 Actor,让“百万级 Actor”成为可能。
3. 在交易系统里怎么切:一张常见的 Actor 划分图
- Gateway/连接 Actor:解析协议(WebSocket/FIX/HTTP),把外部请求转成内部消息。
- Session Actor:维护登录态、订阅关系(行情订阅等)。
- Account Actor:每个账户一个 Actor,串行处理资金冻结/解冻/扣减,保证资金一致性。
- OrderBook Actor:每个交易对一个 Actor,串行处理该交易对的下单/撤单/撮合,保证撮合确定性。
- MarketData Actor:汇总撮合结果与深度变化,向订阅者广播行情。
一个典型下单链路可以是:Gateway → Account(冻结) → OrderBook(撮合) → Account(结算) + MarketData(发布)。
这条链路的“顺序性”由消息流表达,而不是由锁“碰巧”维持。
4. 关键要点/坑:别把 Actor 当成“万能的线程安全胶水”
- Actor 内禁止阻塞操作:比如同步查 DB、调用慢 HTTP。阻塞会占用 Dispatcher 线程,导致同池上的大量 Actor 被饿死,吞吐断崖式下跌。需要 I/O 时用异步/专用 Dispatcher/边界服务。
- 消息协议要当 API 设计:消息结构(字段、语义、幂等性)一旦上线就是契约;随意改会让分布式调试变噩梦。
- 分片是扩展的核心:单个交易对/账户仍是串行处理,但系统总体吞吐来自“很多个 OrderBook/Account 并行”。
- Dispatcher 调优:计算密集型(撮合)与 I/O 密集型(网关)别混一个线程池,避免互相拖累。
- 状态恢复:Actor 重启会丢内存状态;关键 Actor 需要持久化策略(事件溯源/快照),否则 HA 只是纸面。
5. 适用场景:什么时候值得优先考虑 Actor 模型
- 系统里有大量独立的“有状态实体”:账户、订单簿、仓位、风控规则实例等;
- 你被锁竞争、死锁、共享状态并发 bug 折磨,且业务更像“事件/消息驱动”;
- 你需要从单机演进到多机:Actor 的位置透明性 + 分片/路由会让迁移成本更低。