Lambda 15 分钟超时,搞不了长流程——这事儿我相信不少人都遇到过。
之前接了个需求:订单处理流水线,要过库存校验、支付扣款、等仓库拣货(两小时起步)、发物流。我一开始用 SQS + DynamoDB + 五个 Lambda 拼了一套状态机。能跑,但五个函数之间传状态、处理异常、保证幂等……折腾半天,光胶水代码就写了几百行。
现在亚马逊云科技出了 Lambda Durable Functions,我重写了一版——一个函数搞定,代码量直接砍半。这篇文章分享下实战过程和踩的坑。
啥是 Durable Functions
说白了,就是让 Lambda 支持断点续跑。
核心机制叫检查点(Checkpoint)。你把业务逻辑拆成一个个 step,每个 step 执行完 SDK 自动存结果。如果中间函数被打断(超时、OOM、平台回收),下次唤醒时 SDK 会"回放"——已经完成的 step 直接返回存好的结果,没完成的接着跑。
几个亮点:
ctx.wait()可以挂起函数等几小时甚至几天,不产生计算费用- 内置重试,支持指数退避 + 随机抖动
ctx.map()并发处理集合,单个元素失败不影响其他ctx.createCallback()等外部事件,比如人工审批- 函数可以跑到一年
SDK 是 Apache 2.0 开源的:github.com/aws/aws-dur…
实战:订单处理工作流
Maven 依赖
<dependency>
<groupId>software.amazon.lambda.durable</groupId>
<artifactId>aws-durable-execution-sdk-java</artifactId>
<version>VERSION</version>
</dependency>
Java 17+,这是硬性要求。
核心代码
public class OrderProcessor extends DurableHandler<Order, OrderResult> {
private final InventoryService inventoryService = new InventoryService();
private final PaymentService paymentService = new PaymentService();
private final ShippingService shippingService = new ShippingService();
@Override
protected OrderResult handleRequest(Order order, DurableContext ctx) {
// 扣库存
var reservation = ctx.step("reserve-inventory", Reservation.class,
stepCtx -> inventoryService.reserve(order.getItems()));
// 扣款(带重试 + 至多一次语义)
var payment = ctx.step("process-payment", Payment.class,
stepCtx -> paymentService.charge(
order.getPaymentMethod(), order.getTotal()),
StepConfig.builder()
.retryStrategy(RetryStrategies.exponentialBackoff(
3, Duration.ofSeconds(2),
Duration.ofSeconds(15), 2.0,
JitterStrategy.FULL))
.semantics(StepSemantics.AT_MOST_ONCE_PER_RETRY)
.build());
// 等仓库处理 2 小时(零成本挂起)
ctx.wait("wait-for-warehouse", Duration.ofHours(2));
// 发货
var shipment = ctx.step("confirm-shipment", Shipment.class,
stepCtx -> shippingService.ship(
reservation, order.getAddress()));
return new OrderResult(order.getId(), shipment.getTrackingNumber());
}
}
这代码看着就是普通的顺序执行逻辑,但背后 SDK 帮你做了检查点、回放、重试、挂起恢复。跟之前五个 Lambda + SQS 的方案比,维护成本不在一个量级。
踩坑记录
坑 1:扣款被执行了两次
这个坑比较隐蔽。默认的执行语义是 AT_LEAST_ONCE_PER_RETRY——如果 step 执行成功了但检查点没存住(运行环境突然被回收),回放时会再执行一次。
扣款这种操作绝对不能重复。解决方案:
var payment = ctx.step("charge-payment", Payment.class,
stepCtx -> paymentService.charge(amount),
StepConfig.builder()
.semantics(StepSemantics.AT_MOST_ONCE_PER_RETRY)
.retryStrategy(RetryStrategies.Presets.NO_RETRY)
.build());
AT_MOST_ONCE_PER_RETRY 会在执行前先记录一个检查点。如果执行完但结果没存上,回放时不会重新执行,而是抛 StepInterruptedException。你需要自己查外部状态来处理。
坑 2:泛型返回值反序列化炸了
我有一个 step 返回 List<Order>,直接写 List.class:
// ❌ 这样不行,回放时反序列化会出错
var orders = ctx.step("fetch-orders", List.class,
stepCtx -> orderService.getOrders(userId));
Java 类型擦除的老问题。要用 TypeToken:
// ✅ 正确写法
var orders = ctx.step("fetch-orders", new TypeToken<List<Order>>() {},
stepCtx -> orderService.getOrders(userId));
坑 3:改了 step 顺序导致回放失败
有一次我重构代码,把两个 step 的顺序调了一下。结果线上正在运行的 durable function 回放时直接报 NonDeterministicExecutionException。
原因:回放是从头开始跑的,它按顺序匹配 step 名称。你改了顺序,它就对不上了。
教训:对已经在运行的 durable function,不要改 step 的名称和顺序。 要改就等所有运行中的实例跑完再改。
坑 4:map 用了 HashSet 导致回放不一致
// ❌ HashSet 迭代顺序不确定
var items = new HashSet<>(Arrays.asList("a", "b", "c"));
ctx.map("process", items, String.class, fn);
// 抛 IllegalArgumentException
ctx.map() 要求输入集合必须有确定的迭代顺序。用 List 就行:
// ✅ List 有确定顺序
var items = List.of("a", "b", "c");
ctx.map("process", items, String.class, fn);
批量处理:ctx.map() 实战
真实场景里经常要批量处理数据。比如批量发送通知:
var userIds = List.of("user-1", "user-2", "user-3",
"user-4", "user-5");
var result = ctx.map("send-notifications", userIds,
NotifyResult.class,
(userId, index, childCtx) -> {
return childCtx.step("notify-" + index,
NotifyResult.class,
stepCtx -> notificationService.send(userId));
},
MapConfig.builder()
.maxConcurrency(3)
.completionConfig(
CompletionConfig.toleratedFailureCount(2))
.build());
System.out.println("成功: " + result.succeeded().size());
System.out.println("失败: " + result.failed().size());
toleratedFailureCount(2) 表示最多容忍 2 个失败。超过 2 个就停止。每个元素跑在隔离的子上下文里,一个炸了不影响其他。
等外部事件:审批场景
大额订单需要主管审批:
DurableCallbackFuture<String> callback = ctx.createCallback(
"manager-approval", String.class,
CallbackConfig.builder()
.timeout(Duration.ofHours(24))
.build());
ctx.step("request-approval", String.class,
stepCtx -> {
approvalService.requestApproval(
callback.callbackId(), orderDetails);
return "requested";
});
try {
String decision = callback.get();
if ("rejected".equals(decision)) {
// 走拒绝逻辑
}
} catch (CallbackTimeoutException e) {
// 24 小时没审批,自动取涊
}
函数在 callback.get() 处挂起,不占计算资源。审批系统通过 API 把结果发回来,函数继续跑。
异步并行执行
多个互不依赖的操作可以并行:
DurableFuture<User> userFuture = ctx.stepAsync(
"fetch-user", User.class,
stepCtx -> userService.getUser(userId));
DurableFuture<List<Order>> ordersFuture = ctx.stepAsync(
"fetch-orders", new TypeToken<List<Order>>() {},
stepCtx -> orderService.getOrders(userId));
// 并行执行,这里等结果
User user = userFuture.get();
List<Order> orders = ordersFuture.get();
和之前方案的对比
| SQS + 多 Lambda 状态机 | Lambda Durable Functions | |
|---|---|---|
| 代码分布 | 5-6 个函数 + 胶水代码 | 1 个函数,顺序写 |
| 状态管理 | 自己写 DynamoDB 读写 | SDK 自动检查点 |
| 长时间等待 | SQS 延迟消息(上限 15 分钟) | ctx.wait() 挂起,等多久都行 |
| 重试逻辑 | 自己实现 | 内置指数退避 |
| 错误处理 | 每个函数单独处理 | 统一 try-catch |
参考资源
如果你也在用多个 Lambda 拼状态机,建议试试 Durable Functions。开发体验好太多了。