Hotdry.
systems

设计无 Webhook 的 Stripe 到 Postgres 数据同步架构

本文介绍如何设计一个不依赖 Webhook 的 Stripe 到 Postgres 数据同步架构,实现最终一致性、幂等更新与故障恢复机制,包含具体参数与监控清单。

在构建基于 Stripe 的支付系统时,将 Stripe 中的事件数据(如付款成功、订阅更新、退款)可靠地同步到自身的 Postgres 数据库是一项核心需求。传统的做法依赖于 Stripe 的 Webhook,由 Stripe 主动向我们的回调端点推送事件。然而,Webhook 架构引入了外部依赖:我们的服务必须保持高可用以接收回调,网络问题可能导致事件丢失,并且调试和重放机制复杂。因此,采用一种无 Webhook(Webhook-less)的主动拉取模式,能赋予系统更高的可控性、可观测性和故障恢复能力。本文将深入探讨如何设计一个不依赖 Webhook、基于轮询 Stripe Events API 并同步至 Postgres 的架构,重点实现最终一致性、幂等更新与健壮的故障恢复机制。

架构核心:轮询器、处理器与游标管理器

无 Webhook 同步架构的核心思想是变被动为主动,由我们的服务定期向 Stripe 发起查询,获取新增或更新的事件,并将其转化为对 Postgres 数据库的操作。该架构主要由三个组件构成:

  1. 轮询器(Poller):负责按计划调用 Stripe 的 Events API。其关键参数包括:

    • created:基于时间范围的过滤,使用 gtelt 参数精确界定每次轮询的时间窗口,这是实现增量同步的基础。
    • limit:单次请求返回的最大事件数,Stripe 允许的最大值为 100。合理的分页策略对于处理大量事件至关重要。
    • type:可选,用于过滤特定类型的事件,如 invoice.paidcustomer.subscription.updated。 轮询器需要维护一个持久化的游标(Cursor),通常记录上一次成功处理事件的最大 created 时间戳或最后一个事件的 ID。这个游标是保证数据不重不漏的关键状态。
  2. 事件处理器(Event Processor):接收轮询器获取到的事件列表,并将其转化为对 Postgres 的业务实体(如 invoicessubscriptions 表)的增删改操作。这里最大的挑战是幂等性。由于网络超时、进程重启或 Stripe 端的重试,同一事件可能被多次投递。处理器必须确保重复应用同一事件不会导致数据错误或重复。

  3. 游标管理器(Cursor Manager):负责安全地更新和持久化游标。最佳实践是在一个数据库事务中,先处理事件(更新业务数据),再更新游标。这确保了 “至少一次” 的处理语义。如果事务失败,游标不会前进,下次轮询会重新处理相同的事件。

实现最终一致性与幂等更新

最终一致性意味着系统保证在有限的时间延迟后,所有副本(此处指 Stripe 的状态和我们的 Postgres 数据库)将达到一致状态。在我们的架构中,一致性通过以下机制保障:

  • 有序轮询窗口:轮询器使用严格递增的时间窗口(例如,每次处理 [last_cursor, last_cursor + 5分钟) 内的事件)。这避免了事件被遗漏,并提供了确定性的处理范围。
  • 幂等更新策略:这是架构中最关键的技术点。针对不同的事件类型和数据模型,可以采用以下一种或多种策略:
    • 唯一约束与 Upsert:为业务表设计由 Stripe 事件 ID (stripe_event_id) 或对象 ID (stripe_invoice_id) 构成的唯一约束。处理事件时,使用 Postgres 的 INSERT ... ON CONFLICT DO UPDATE(Upsert)语句。即使同一事件被处理多次,数据库层面的约束会保证最终状态一致。
    • 乐观锁与版本号:对于更新操作,可以在业务表中增加一个 version 字段或使用 updated_at 时间戳。处理器在更新时附带条件(例如 WHERE stripe_id = ? AND version = ?),如果版本不匹配,则说明数据已被更新,当前操作可安全跳过或记录冲突。
    • 事件溯源模式:将 Stripe 事件本身作为事实源,在 Postgres 中单独存储一个 stripe_events 表,记录所有原始事件。业务状态通过查询和聚合这些事件来派生。这天然具备幂等性,因为重复插入相同事件 ID 会被唯一约束阻止。业务表则作为物化视图,通过后台作业从事件表同步。

故障恢复与监控清单

任何同步系统都必须预设故障并设计恢复路径。以下是关键的故障恢复机制和监控点:

故障恢复机制:

  1. 重试队列:对于处理失败的事件(如临时网络故障、数据库死锁),不应立即丢弃。应将其放入一个重试队列(可用 Postgres 表实现),由后台任务进行指数退避重试。
  2. 死信队列(DLQ):经过多次重试仍失败的事件(如数据格式异常、业务逻辑错误)应移入死信队列。这需要人工介入排查,并提供了修复后重新注入的入口。
  3. 游标修复工具:当游标因 bug 而损坏或丢失时,需要有一个手动或自动的工具能够根据业务数据的最新状态,反向计算出安全的游标位置(例如,查询已处理事件的最大 created 时间),并重置游标。
  4. 批量回滚与补全:在检测到大规模数据不一致时,应能暂停同步,并根据一个指定的时间点,重新拉取该时间点之后的所有事件进行全量重放。这要求 Stripe 事件有足够的保留期(通常为 30 天)。

可落地的监控清单:

  • 延迟指标同步延迟 = 当前时间 - 最新已处理事件的 created 时间。应设置报警阈值(如延迟 > 5 分钟)。
  • 错误率:监控事件处理失败率(失败数 / 总数)。瞬时飙升可能预示 API 故障或业务逻辑 bug。
  • 游标健康度:游标是否在持续、单调递增。游标长时间未更新意味着轮询器已停止工作。
  • Stripe API 用量与限流:监控 Stripe API 的请求速率,确保接近但不超过速率限制
  • 队列深度:监控重试队列和死信队列的长度,及时发现积压。

与 Postgres 逻辑复制的结合

虽然本文主要讨论从 Stripe 到 Postgres 的同步,但此架构生成的数据同样可以成为企业内部数据流的一部分。例如,可以使用 Postgres 的逻辑复制功能,将处理后的业务表(如 subscriptions)的更改实时流式传输到数据仓库(如 Snowflake)或缓存层。这样,Stripe 数据通过无 Webhook 同步进入 Postgres 主库,再通过 Postgres 内置的 CDC 能力流向其他系统,形成了一个可控、可观测的数据管道。

总结

放弃 Webhook 转而采用主动轮询 Stripe Events API 的同步架构,并非简单的技术替换,而是一种架构范式的转变,将数据同步的主动权和控制权收回己方。通过精心设计的轮询策略、幂等更新逻辑和全面的故障恢复机制,该架构能够提供不亚于甚至优于 Webhook 的可靠性和一致性保证。实现此架构的关键在于深刻理解 Stripe API 的约束、Postgres 的事务特性,并投入精力构建强大的监控和运维工具。对于追求系统稳定性和数据自治的团队而言,这是一条值得深入探索的道路。

资料来源

  1. Stripe API 文档 - Events List: https://stripe.com/docs/api/events/list
  2. PostgreSQL 文档 - Logical Replication: https://www.postgresql.org/docs/current/logical-replication.html
查看归档