# Akka 应用范式
# 1. 先判断是否真的需要 Actor
Akka.NET 不是消息队列、不是 RPC 框架,也不是普通 Service 方法的替代品。
在微服务场景下,Actor 更适合解决“同一个业务实体的并发状态流转、长流程编排、超时补偿和串行决策”问题,而不是为了把消息发到 MQ 再额外加一层模型。
| 场景 | 推荐方案 | 是否建议引入 Actor |
|---|---|---|
| 支付回调只做签名校验、幂等、改订单状态、写 Outbox | Controller/Consumer -> Service -> DB + Outbox -> MQ | 不建议 |
| 订单完成后通知库存、仓储、快递、短信、商家 | Outbox -> MQ -> 下游服务订阅 | 不建议只为通知引入 |
| 同一个订单同时收到支付成功、超时关闭、人工取消、库存失败、风控拒绝 | OrderActor(orderId) -> 状态机 -> DB + Outbox | 建议 |
| 订单生命周期需要延迟任务、重试、补偿、Saga 编排 | Actor + 状态机 + Inbox/Outbox | 建议 |
| 跨微服务直接调用对方 Actor | MQ / HTTP / gRPC | 默认不建议 |
结论很明确:
- 如果 Actor 里只做“改订单状态 + 发 MQ”,那大概率是多余复杂度。
- 如果业务已经需要按
OrderId串行处理多个冲突事件,Actor 才开始有价值。 - 跨服务通知仍然优先使用 MQ,Actor 主要用于服务内部或同业务服务集群内的并发治理。
# 2. 直接模式:简单订单流转
如果支付结果处理逻辑很简单,直接使用 Inbox + Outbox 即可:
flowchart LR
A["支付回调 / MQ Consumer"] --> B["Inbox<br/>幂等记录"]
B --> C["OrderService"]
C --> D["DB Transaction<br/>改订单状态 + 写 Outbox"]
D --> E["Outbox Publisher"]
E --> F["MQ"]
F --> G["快递 / 短信 / 商家服务"]
这种模式已经可以解决:
Inbox记录外部事件,避免重复回调重复处理。Orders.Status通过状态条件更新,避免非法流转。Outbox和订单状态在同一个事务内提交,避免状态成功但消息丢失。OutboxPublisher负责 MQ 失败后的重推。
示例:
public async Task HandlePaymentSucceededAsync(PaymentSucceeded message)
{
await unitOfWork.ExecuteAsync(async () =>
{
if (await inbox.ExistsProcessedAsync(message.EventId))
{
return;
}
var order = await orderRepository.GetAsync(message.OrderId);
if (order is null || order.Status != OrderStatus.PendingPayment)
{
await inbox.MarkProcessedAsync(message.EventId);
return;
}
order.MarkPaid(message.PaymentId, message.Amount, message.PaidAt);
await orderRepository.UpdateAsync(order);
await outbox.WriteAsync(new OrderPaidIntegrationEvent(
message.EventId,
message.OrderId,
message.PaymentId,
message.Amount,
message.PaidAt));
await inbox.MarkProcessedAsync(message.EventId);
});
}
这个场景不需要 Actor。直接模式更少组件、更容易排查,也更符合大多数简单订单链路。
# 3. Actor 模式:并发事件收敛
当同一个订单会同时收到多个事件时,问题不再是“怎么发 MQ”,而是“谁有资格推动订单状态变化”。
例如同一个订单可能同时收到:
PaymentSucceeded
PaymentCanceled
TimeoutClosed
ManualCancel
RiskApproved
InventoryFailed
这时可以让所有事件先进入同一个 OrderActor(orderId):
flowchart LR
A["PaymentSucceeded"] --> O["OrderActor(orderId)"]
B["TimeoutClosed"] --> O
C["ManualCancel"] --> O
D["InventoryFailed"] --> O
O --> S["订单状态机"]
S --> T["DB Transaction<br/>Orders + Inbox + Outbox"]
T --> M["MQ"]
这里的关键原则是:
Actor 把同一个订单的并发事件收敛成“串行执行”,但每个事件是否生效,由订单当前状态机决定。
Actor 只保证同一个 Actor 内部一次处理一条消息,不代表消息天然符合业务优先级。
# 4. 串行不等于业务优先级
Actor 只保证串行,不保证业务优先级。
最终是否生效不能只靠执行顺序,要靠状态机规则。
对支付/超时这种冲突场景,还要结合业务时间、过期时间、支付平台最终结果来判断。
例如订单当前状态是 PendingPayment,同时来了两个事件:
1. PaymentSucceeded
2. TimeoutClosed
如果 PaymentSucceeded 先进入 OrderActor:
PendingPayment -> Paid
TimeoutClosed ignored
如果 TimeoutClosed 先进入 OrderActor:
PendingPayment -> Closed
PaymentSucceeded ignored / refund / manual review
所以不能简单认为“谁先到谁一定正确”。支付成功和超时关闭这种冲突,通常需要引入业务时间:
private async Task HandlePaymentSucceededAsync(PaymentSucceeded message)
{
var order = await orderRepository.GetAsync(message.OrderId);
if (order is null)
{
return;
}
if (order.Status == OrderStatus.PendingPayment &&
message.PaidAt <= order.ExpiredAt)
{
order.MarkPaid(message.PaymentId, message.Amount, message.PaidAt);
await outbox.WriteAsync(new OrderPaidIntegrationEvent(...));
return;
}
if (order.Status == OrderStatus.Closed &&
message.PaidAt <= order.ExpiredAt)
{
order.ReopenAsPaid(message.PaymentId, message.Amount, message.PaidAt);
await outbox.WriteAsync(new OrderPaidIntegrationEvent(...));
return;
}
await outbox.WriteAsync(new PaymentArrivedAfterOrderClosedIntegrationEvent(...));
}
private async Task HandleTimeoutClosedAsync(TimeoutClosed message)
{
var order = await orderRepository.GetAsync(message.OrderId);
if (order is null || order.Status != OrderStatus.PendingPayment)
{
return;
}
if (await paymentGateway.HasSuccessfulPaymentAsync(order.Id))
{
return;
}
order.CloseByTimeout(message.ClosedAt);
await outbox.WriteAsync(new OrderClosedIntegrationEvent(...));
}
这里的判断依据不是 Actor 的 Mailbox 顺序,而是:
- 当前订单状态;
- 订单过期时间;
- 支付成功时间;
- 支付平台最终结果;
- 状态流转规则;
- 数据库状态条件和版本号。
# 5. 推荐落库模型
Actor 不应该承担可靠消息存储职责。可靠性应由数据库中的命令表、事件表和 Outbox 兜底。
flowchart LR
A["外部事件"] --> B["Inbox / OrderCommand<br/>Pending"]
B --> C["OrderActor(orderId)"]
C --> D["DB Transaction"]
D --> E["Orders<br/>业务状态"]
D --> F["Inbox / OrderCommand<br/>Processed"]
D --> G["Outbox<br/>待发布事件"]
G --> H["MQ"]
建议拆分语义:
| 表 | 职责 | 重试对象 |
|---|---|---|
Inbox / OrderCommand | 记录外部输入命令,保证 Actor 失败后可恢复 | 重新投递给 OrderActor(orderId) |
Orders | 保存订单当前业务状态和版本号 | 状态条件更新、乐观锁兜底 |
Outbox | 记录要发布到 MQ 的集成事件 | 重新发布 MQ |
不要把 Inbox 和 Outbox 混成一个概念:
Inbox重试的是“订单命令重新推进”。Outbox重试的是“下游 MQ 消息重新发布”。
# 6. 代码结构示例
消息只传最小必要参数,不传完整订单对象:
public interface IOrderCommand
{
string OrderId { get; }
string EventId { get; }
}
public sealed record PaymentSucceeded(
string OrderId,
string EventId,
string PaymentId,
decimal Amount,
DateTime PaidAt) : IOrderCommand;
public sealed record TimeoutClosed(
string OrderId,
string EventId,
DateTime ClosedAt) : IOrderCommand;
入口先写命令表,再投递 Actor:
public async Task<IActionResult> PaymentSuccess(PaymentSuccessRequest request)
{
var command = new PaymentSucceeded(
request.OrderId,
request.NotifyId,
request.PaymentId,
request.Amount,
request.PaidAt);
await orderCommandStore.AppendPendingAsync(command);
await orderActorGateway.TellAsync(command);
return Ok();
}
Actor 只做串行入口和流程决策,不把所有业务规则硬塞进 Actor:
public sealed class OrderActor : ReceiveActor
{
public OrderActor(
IOrderRepository orderRepository,
IOrderStateMachine orderStateMachine,
IOrderCommandStore commandStore,
IOutboxWriter outbox)
{
ReceiveAsync<PaymentSucceeded>(async message =>
{
await orderStateMachine.ApplyPaymentSucceededAsync(message);
await commandStore.MarkProcessedAsync(message.EventId);
});
ReceiveAsync<TimeoutClosed>(async message =>
{
await orderStateMachine.ApplyTimeoutClosedAsync(message);
await commandStore.MarkProcessedAsync(message.EventId);
});
}
}
状态机负责判断当前事件是否允许生效:
public async Task ApplyTimeoutClosedAsync(TimeoutClosed message)
{
var order = await orderRepository.GetAsync(message.OrderId);
if (order is null || order.Status != OrderStatus.PendingPayment)
{
return;
}
if (await paymentGateway.HasSuccessfulPaymentAsync(order.Id))
{
return;
}
order.CloseByTimeout(message.ClosedAt);
await unitOfWork.ExecuteAsync(async () =>
{
await orderRepository.UpdateAsync(order);
await outbox.WriteAsync(new OrderClosedIntegrationEvent(
message.EventId,
message.OrderId,
"Payment timeout"));
});
}
定时任务只重推未完成命令:
public async Task ReplayPendingOrderCommandsAsync()
{
var commands = await orderCommandStore.GetRetryableAsync();
foreach (var command in commands)
{
await orderActorGateway.TellAsync(command);
}
}
# 7. 使用建议
引入 Actor 前先问三个问题:
- 是否存在同一个业务实体的多个并发事件?
- 是否需要把这些事件收敛成同一个状态机顺序处理?
- 是否有超时、补偿、重试、长流程、Saga 编排等需求?
如果答案是否定的,优先使用 Service + Inbox + Outbox + MQ。
如果答案是肯定的,再使用 Actor + 状态机 + Inbox/Outbox。
框架层面提供 Akka.Cluster 能力,但业务侧不应该为了“看起来更分布式”而强行引入 Actor。Actor 的价值是降低复杂并发流程的不可控性,不是增加调用链层数。