我用 Lambda Durable Functions 把五个 Lambda 缩成了一个,代码量砍半

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 依赖

xml 复制代码
<dependency>
    <groupId>software.amazon.lambda.durable</groupId>
    <artifactId>aws-durable-execution-sdk-java</artifactId>
    <version>VERSION</version>
</dependency>

Java 17+,这是硬性要求。

核心代码

java 复制代码
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 执行成功了但检查点没存住(运行环境突然被回收),回放时会再执行一次。

扣款这种操作绝对不能重复。解决方案:

java 复制代码
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

java 复制代码
// ❌ 这样不行,回放时反序列化会出错
var orders = ctx.step("fetch-orders", List.class,
    stepCtx -> orderService.getOrders(userId));

Java 类型擦除的老问题。要用 TypeToken

java 复制代码
// ✅ 正确写法
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 导致回放不一致

java 复制代码
// ❌ HashSet 迭代顺序不确定
var items = new HashSet<>(Arrays.asList("a", "b", "c"));
ctx.map("process", items, String.class, fn);
// 抛 IllegalArgumentException

ctx.map() 要求输入集合必须有确定的迭代顺序。用 List 就行:

java 复制代码
// ✅ List 有确定顺序
var items = List.of("a", "b", "c");
ctx.map("process", items, String.class, fn);

批量处理:ctx.map() 实战

真实场景里经常要批量处理数据。比如批量发送通知:

java 复制代码
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 个就停止。每个元素跑在隔离的子上下文里,一个炸了不影响其他。

等外部事件:审批场景

大额订单需要主管审批:

java 复制代码
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 把结果发回来,函数继续跑。

异步并行执行

多个互不依赖的操作可以并行:

java 复制代码
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。开发体验好太多了。

相关推荐
亚马逊云开发者6 小时前
异构 GPU 混合部署 Whisper,我用 HyperPod 一个集群搞定了
aws
亚马逊云开发者19 小时前
模型搜完网页就"脑算"数字?用 Dynamic Filtering 让它老老实实写代码
aws
亚马逊云开发者21 小时前
老板让我迁 Graviton,我用 AI 工具几分钟搞定了迁移评估
aws
亚马逊云开发者1 天前
用 Kiro CLI 做 Agent 后端,1000 行代码搞定飞书 AI 聊天机器人
aws
147API1 天前
从零开始上手 AWS:架构设计、成本优化与避坑指南
云计算·claude·aws
zhojiew1 天前
[INFRA] EMR集群安全配置传输中加密和Kerberos认证配置详解
安全·aws·emr·bigdata
zhojiew1 天前
[INFRA] EMR集群启用HA高可用架构和配置分析
aws·emr·bigdata
亚马逊云开发者1 天前
S3 桶名不用再抢了:Account Regional Namespaces 来了
aws
zhojiew2 天前
[INFRA] EMR集群LogPusher组件功能和运行原理分析
aws·emr·bigdata