我用 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。开发体验好太多了。

相关推荐
zhojiew19 小时前
在AWS裸金属实例上安装Cubesandbox并集成PydanticAI进行数据分析的实践
数据分析·云计算·aws
yyuuuzz19 小时前
aws亚马逊云上运维常见问题梳理
运维·服务器·网络·云计算·aws
亚林瓜子2 天前
AWS S3日志桶常用过期文件生命周期策略
云计算·生命周期·aws·s3·过期·glacier
yyuuuzz2 天前
企业出海场景下的技术适配小经验
运维·服务器·网络·云计算·aws
yyuuuzz4 天前
国外云服务使用的常见技术问题梳理
运维·服务器·网络·数据库·aws
光于前裕于后5 天前
AWS Redshift 集成Zero-ETL和数据共享 Data sharing
云计算·etl·aws
zhojiew6 天前
在AWS中国区实现EKS跨VPC跨区域实现节点加入集群的实践
云计算·aws
认真的薛薛7 天前
Terraform: AWS VPC+可SSH登录EC2
ssh·aws·terraform
认真的薛薛7 天前
Terraform:AWS VPC
云原生·aws·terraform
yyuuuzz7 天前
境外云服务器使用常见问题梳理
运维·服务器·网络·aws