HTTP 轮询、SSE、WebSocket 与 Webhook:原理、区别与适用场景
文章摘要
订单支付完成后,页面如何及时显示“支付成功”?AI 生成回答时,为什么文字能够逐段出现?GitHub 收到代码提交后,又如何通知部署系统?这些需求都涉及数据更新通知,但通信双方和交互方式并不相同。本文从普通 HTTP 请求与响应出发,介绍短轮询、长轮询、SSE、WebSocket 和 Webhook 的通信过程,结合简短示例比较它们的适用场景,并说明自动重连、消息补发、Webhook 验签与幂等处理的边界。
本文聚焦通信机制与选型。JavaScript 示例运行于浏览器,HTTP 报文只展示与讲解有关的字段;示例接口需要由服务端实现,不构成一套可直接部署的完整应用。文中不涉及特定 JDK 或 Java 框架版本。
一、从一次普通 HTTP 查询说起
浏览器通过 HTTP 查询数据时,服务端通常返回查询时的结果,本次响应随后结束。后端的数据还可能继续变化,但页面拿到的结果不会自动跟着更新。
例如,订单页面向服务端查询订单1001的支付状态:
GET /api/orders/1001 HTTP/1.1 Host: shop.example.com服务端返回的响应体是:
{"orderId":"1001","status":"PENDING"}PENDING表示待支付,页面据此显示“等待支付”。
如果用户随后完成支付,后端把订单状态更新为PAID,页面仍然显示之前查询到的“等待支付”。因为先前的响应已经结束,服务端不能再往其中追加新的状态。
要让页面显示最新结果,就需要让它获取后续变化。最直接的办法是再次查询:把这个查询过程按一定规则反复执行,就是轮询。
二、HTTP 轮询:客户端不断询问有没有变化
1. 短轮询:每隔一段时间查询一次
短轮询最直观:浏览器每隔几秒查询订单,服务端每次返回当时的状态。
例如,每隔 3 秒查询一次:
| 查询时刻 | 服务端返回 |
|---|---|
| 第 0 秒 | PENDING,待支付 |
| 第 3 秒 | PENDING,待支付 |
| 第 6 秒 | PAID,已支付 |
如果支付发生在第 4 秒,页面要等到第 6 秒的查询才会发现变化。
下面用浏览器代码展示这个过程:
asyncfunctionpollOrder(){try{constresponse=awaitfetch("/api/orders/1001",{cache:"no-store"});if(!response.ok){thrownewError(`查询失败:HTTP${response.status}`);}constorder=awaitresponse.json();console.log("订单状态:",order.status);if(order.status==="PAID"||order.status==="CLOSED"){return;// 到达终态后,停止轮询}}catch(error){console.error(error);}setTimeout(pollOrder,3000);}pollOrder();这里在本次查询结束后再等待 3 秒,可以避免使用固定间隔的setInterval时,前一次请求尚未完成,下一次请求已经发出的情况。因此,这段代码的两次请求起点之间,还包含前一次查询的耗时。
示例只保留轮询主线。实际页面还需要在退出时取消查询,为请求设置超时,并对连续失败采用退避策略。订单状态接口也应合理设置缓存策略,避免读到过期结果。
轮询频率决定了更新延迟与请求量之间的取舍。
如果有N个页面持续查询,每个页面每隔T秒发出一次请求,忽略请求耗时和重试时:
平均查询请求数/秒 ≈ N / T例如,10,000 个页面每 5 秒查询一次,大约产生 2,000 次查询请求/秒。即使订单没有任何变化,这些查询也会继续发生。
在“严格每隔T秒查询、状态变化相对查询时刻均匀分布、忽略网络与处理耗时”的简化条件下,发现变化的等待时间平均约为T / 2,最长接近T。这只是估算,不能直接当作接口的实际延迟指标。
短轮询实现简单,适合数据更新不频繁、允许几秒延迟、在线规模可控的场景。
2. 长轮询:没有变化时,先不结束响应
长轮询仍然由客户端发起 HTTP 请求,但服务端返回响应的时机变了:
- 浏览器请求“订单是否有更新”。
- 如果已经有更新,服务端立即响应。
- 如果没有更新,服务端暂时保留请求,等待变化。
- 出现变化,或达到等待超时时间后,服务端结束响应。
- 浏览器收到响应后,立即发起下一次长轮询。
例如,浏览器在第 0 秒发出请求,订单在第 4 秒变为已支付,服务端便在第 4 秒返回结果。浏览器不必等到固定的下一个查询时刻。
为了表示“从哪条消息之后开始等待”,长轮询接口也可以携带游标,例如?after=42。游标的含义、超时响应和历史保留范围,都由接口自行约定。RFC 6202 讨论了长轮询及 HTTP 流式传输的工作方式与实际问题。
长轮询减少了无更新时的频繁查询,但仍有成本:服务端需要维护等待中的请求、连接资源和超时状态;每次响应结束后,客户端还要重新发起请求。更新非常频繁时,请求与响应也会频繁发生。
“请求一直等待”也不意味着“必须占用一个线程原地睡眠”。采用异步 I/O 的实现可以挂起请求,而不为每个等待请求独占工作线程;具体资源开销取决于服务端实现。
图 1:短轮询定时查询,长轮询等待更新或超时后结束本次响应,再发起下一次请求。时间沿纵向向下。
3. HTTP 长连接与长轮询有什么区别
| 概念 | 关注的问题 |
|---|---|
| HTTP 长连接、持久连接 | 一次响应结束以后,底层连接能否继续复用 |
| HTTP 长轮询 | 没有更新时,本次请求是否等待一段时间再响应 |
HTTP/1.1 支持持久连接,一次响应结束后,底层连接仍然可以继续复用,处理后续请求。响应结束与底层连接关闭是两件事。参见 HTTP/1.1 连接管理规范。
短轮询同样可以复用 HTTP 连接。长轮询的一次响应结束后,也不一定需要重新建立 TCP 连接。
因此,不能用“是否反复建立 TCP 连接”来判断短轮询和长轮询。
三、SSE:在一个 HTTP 响应中持续发送事件
SSE 的全称是 Server-Sent Events,即服务端发送事件。
它的核心过程是:客户端先发起 HTTP 请求,服务端返回一个持续输出的事件流。后续事件继续写入这个响应体,不必每发一条事件就结束响应。
1. SSE 与长轮询的关键区别
假设系统先后产生“订单已支付”和“订单已发货”两条更新:
- 长轮询:响应第一条更新后结束本次响应,客户端再发起请求等待下一条。
- SSE:可以在同一个响应流中先后发送两条更新。
这里的“持续连接”主要描述应用层的事件流。SSE 也可以运行在 HTTP/2 上,此时一个事件流不必独占一条 TCP 连接,多个 HTTP 流可以复用连接。
2. SSE 发送的数据是什么样的
服务端使用Content-Type: text/event-stream,事件流按 UTF-8 解码。下面展示响应头和解码后的响应体结构,省略 HTTP 传输分帧细节:SSE 规范。
HTTP/1.1 200 OK Content-Type: text/event-stream Cache-Control: no-cache retry: 3000 id: 42 event: order-status data: {"orderId":"1001","status":"PAID"} id: 43 event: order-status data: {"orderId":"1001","status":"SHIPPED"}这些字段的含义是:
| 字段 | 含义 |
|---|---|
data | 事件内容,可以约定为 JSON 文本 |
event | 事件类型;未指定时默认是message |
id | 事件标识,可用于重连时报告接收位置 |
retry | 设置重连等待时间,单位为毫秒;浏览器还可以根据网络情况延后重试 |
事件以空行分隔。服务端只写出一行data:,却没有发送事件结束的空行,浏览器可能还不会触发事件回调。多行data:会用换行符拼接为同一个事件的数据。
SSE 的数据格式是文本。二进制内容可以自行编码为文本传输,但会增加体积与处理成本。
3. 浏览器如何接收 SSE
浏览器可以通过原生EventSource接收事件:
constsource=newEventSource("/api/orders/1001/events");source.addEventListener("order-status",(event)=>{constorder=JSON.parse(event.data);console.log("订单状态:",order.status);if(order.status==="PAID"||order.status==="CLOSED"){source.close();// 本页面只等待支付结果,到达终态后主动关闭}});source.onerror=()=>{console.warn("事件流发生错误,请检查连接状态和服务端响应");};这里使用addEventListener("order-status", ...),对应服务端的event: order-status。onmessage用来接收类型为message的事件,不能代替所有自定义事件的监听器。EventSource 使用说明。
这个页面收到支付终态后会关闭事件流,因此不会继续接收后续发货事件。如果页面需要持续展示物流变化,就应调整关闭条件。
对于可恢复的连接中断,原生EventSource会尝试重连;调用close()会停止连接与重连。不能把所有错误都理解为“浏览器一定会一直重试”,例如规范规定服务端可以用 HTTP204告知客户端停止重连。SSE 处理模型。
4. 自动重连不等于自动补齐消息
假设客户端已经接收到id: 42,随后断线。原生EventSource在该对象的重连过程中,可以通过Last-Event-ID: 42报告位置。Last-Event-ID 规范。
但这个请求头本身不会保存事件,也不会让数据库自动补发。
如果业务要求从断点继续,服务端需要保存可回放的事件、根据 ID 查找后续记录,并约定历史已过期时如何恢复。客户端也要考虑重复事件的处理。这个 ID 表示事件流的接收位置,不是业务处理成功的确认;重新打开页面并创建新的EventSource,也不能默认继承上一次页面的进度。
对只关心最终状态的订单页面,一种较简单的恢复方式是:重新连接后先查询最新状态,再继续监听变化。对需要保留每一段输出的文本流,则需要设计更明确的序号和补发机制。
5. 为什么服务端已经发送,浏览器却很久没有显示
除了应用层及时写出、刷新数据,还要检查中间代理是否缓冲响应。事件流若被积攒后成批转发,页面就会失去逐条更新的效果。
例如 Nginx 可以通过proxy_buffering off关闭对应代理位置的响应缓冲,也支持上游用X-Accel-Buffering: no控制,但该响应头的处理还受 Nginx 配置影响。应对照实际代理配置排查,不能只加一个响应头就认为已经解决。Nginx 官方文档。
空闲时可以发送: keepalive这样的注释行作为心跳,并匹配网关的空闲超时配置。注释不触发业务消息回调,心跳也不能保证所有基础设施都允许无限时长的连接。
SSE 适合服务端主要负责持续推送文本、客户端操作可以通过普通 HTTP 提交的场景,例如进度通知和 AI 文本流。
还要注意,SSE 事件格式与原生EventSourceAPI 不是同一个概念。原生构造器不提供设置任意请求头和 POST 请求体的选项。如果业务必须通过 POST 提交问题,或携带自定义Authorization头,可以通过fetch读取流并解析事件,但解析、重连与进度恢复需要由所用代码或库承担。
四、WebSocket:连接建立后,双方都能主动发送消息
SSE 主要解决服务端持续向客户端发送数据的问题。如果客户端也需要频繁发送消息,例如聊天、协同编辑或游戏操作,就可以考虑 WebSocket。
WebSocket 提供全双工通信:连接建立后,客户端与服务端都可以主动发送消息,不需要每条消息都对应一次 HTTP 请求与响应。WebSocket 协议规范。
1. WebSocket 如何建立连接
以常见的 HTTP/1.1 握手为例,客户端先请求升级协议:
GET /ws HTTP/1.1 Host: chat.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSocket-Version: 13 Origin: https://chat.example.com服务端接受连接后,返回类似下面的响应:
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=随后双方交换 WebSocket 帧。上面的 Key 与 Accept 是规范中的配对示例;实际客户端应为每次连接生成随机 Key。这个握手校验也不能代替用户身份认证。RFC 6455。
因此,WebSocket 可以借助 HTTP 建立连接,但建立后传输的是 WebSocket 消息。不能把它理解成“持续发送普通 HTTP 请求”。
这里的Upgrade和101特指 HTTP/1.1 的握手过程。HTTP/2 上的 WebSocket 建立方式由扩展 CONNECT 机制支持,不能把这份 HTTP/1.1 报文套用于所有版本。RFC 8441。
2. 浏览器如何收发消息
constsocket=newWebSocket("wss://example.com/ws");socket.addEventListener("open",()=>{socket.send(JSON.stringify({type:"subscribe-order",orderId:"1001"}));});socket.addEventListener("message",(event)=>{constmessage=JSON.parse(event.data);console.log("收到服务端消息:",message);});socket.addEventListener("close",()=>{console.log("连接已关闭");});wss://表示通过 TLS 保护的 WebSocket 连接。示例中的地址是占位地址,subscribe-order是自行约定的业务消息类型,并不是 WebSocket 内置功能。
示例还约定所有收到的消息都是 JSON 文本。WebSocket 本身支持文本和二进制消息,实际应用应按约定判断消息类型并处理解析错误。
3. 双向通信能力之外,还要设计什么
WebSocket 解决了消息通道问题,但应用仍需处理:
- 连接存活:协议有 Ping/Pong 控制帧。浏览器 JavaScript 原生 API 没有直接发送这类控制帧的方法;用
socket.send("ping")发出的是业务消息,需要双方自行约定处理。浏览器 WebSocket 标准。 - 断线重连:原生
WebSocket不提供自动重连,需要应用或库重新创建连接,并控制退避、重试和订阅恢复。 - 消息确认:
send()返回不代表对方已经完成业务处理。关键消息需要按业务要求增加消息 ID、确认与重试。 - 离线恢复:连接断开期间的历史消息,需要持久化、同步或补拉机制。
- 服务端路由:多实例部署时,要知道目标用户连接在哪个实例上,再将通知送达持有连接的实例。
如果聊天消息需要离线保存,就应把“消息已入库”“对方已收到”“对方已读”区分开来。使用 WebSocket 本身不会自动完成这些业务状态的管理。
五、Webhook:事件发生后,向对方的接口发送请求
Webhook 常用于系统之间的事件通知。接收方提供回调地址,发送方在事件发生后向这个地址发起 HTTP 请求。GitHub 对 Webhook 的说明。
例如,商家在支付平台配置:
https://shop.example.com/webhooks/payment支付完成以后,支付平台向商家后端发送通知:
POST /webhooks/payment HTTP/1.1 Host: shop.example.com Content-Type: application/json请求体示意如下,字段由接口双方约定:
{"eventId":"evt_10001","type":"payment.succeeded","data":{"orderId":"1001","paymentId":"pay_90001"}}这是一种事件回调模式。它没有像 WebSocket 那样统一的消息帧协议;事件结构、鉴权方式、成功响应、重试规则,都应以具体平台的接口约定为准。
1. Webhook 与 SSE 的“服务端主动发送”有什么区别
| 对比项 | SSE | Webhook |
|---|---|---|
| 事先做什么 | 客户端发起请求,建立事件流 | 接收方配置回调接口或订阅 |
| 事件发生时 | 服务端向已有 HTTP 响应流写入事件 | 发送方向回调地址发起新的 HTTP 请求 |
| 接收方的条件 | 能发起请求并持续读取响应 | 有发送方能够访问的 HTTP 接口 |
| 常见接收方 | 浏览器、客户端 | 业务后端、自动化系统 |
浏览器页面通常没有供外部平台直接调用的入站 HTTP 接口。支付平台因此一般通知商家后端,再由商家后端向页面提供订单状态。
2. Webhook 与 WebSocket 的通信方式有什么区别
WebSocket 先建立持续连接,之后双方都可以通过这个通道主动发送消息。Webhook 则在事件发生时,向接收方的回调地址发送一次 HTTP 请求,并等待本次请求的响应。
Webhook 不要求为了等待事件而一直挂起一个请求,但每次通知的 HTTP 请求仍然可以复用底层连接。不要把它简单理解为“每次通知都要新建 TCP 连接”。
图 2:WebSocket 在持续连接中双向发送消息;Webhook 在事件发生时调用接收方接口,并接收本次 HTTP 响应。
3. 验签:如何判断通知来自可信发送方
回调地址能够收到请求,并不代表请求一定来自支付平台。接收方应按照平台约定验证签名,再处理事件。
例如,GitHub 使用共享密钥与请求体计算 HMAC-SHA256,并通过X-Hub-Signature-256传递签名。接收方应基于原始请求体字节验签,避免先解析、重新序列化 JSON 后导致签名不匹配,并采用适当的恒定时间比较方法。GitHub 验签文档。
不同平台也可能采用证书或非对称签名。若协议提供签名时间戳,应按约定检查有效时间范围;同时通过事件标识等方式防止重放造成重复业务操作。不能把某个平台的签名字段当作所有 Webhook 的固定格式。
支付场景还应核对订单归属、支付流水、金额和币种等业务信息,不能只看到payment.succeeded就直接修改任意订单。
4. 重试与重复通知:为什么必须处理幂等
假设商家已经处理成功,但返回的 HTTP 响应在网络中丢失。支付平台可能认为通知失败,再次发送同一个事件。
因此,接收方需要考虑同一个通知被重复投递,以及同一个业务结果通过不同通知到达的情况。
常见做法是:
- 按平台、事件 ID 等建立去重记录,使用数据库唯一约束应对并发重复请求。
- 结合支付流水和订单状态进行业务幂等控制,避免重复记账、重复发货。
- 将去重记录与相关业务变更放在合适的事务边界内,避免标记已处理后,业务操作却失败。
只在内存中放一个“处理过的 ID”,通常不足以应对重启和多实例并发。若业务会进一步调用外部系统,还需要让下游操作支持幂等,或通过可靠任务记录重试。
Webhook 也不保证所有平台都会自动重试。截至本文核验时,GitHub 文档明确说明不会自动重投失败通知;Stripe 则提供自动重试,并说明可能出现重复事件和乱序投递。接入时需要分别核对。GitHub 失败投递说明、Stripe Webhook 文档。
5. 返回成功,是否意味着业务已经完成
这取决于接口约定与处理方式。
如果业务处理很快,可以在事务完成后返回成功。若处理较重,可以先完成验签和必要校验,将事件可靠地保存为待处理任务,或交给具有相应持久化保证的队列,再及时响应,由后台继续处理。GitHub 实践建议。
关键在于:确认接收之前,应确保后续处理不会因为进程重启而无声丢失。不能先返回成功,再把唯一一份事件只放到内存线程池中。
在异步接收方案中,HTTP 成功响应表示接收方已经按约定接受通知,不代表发货、积分发放等后续业务全部完成。后台处理还需要失败重试、告警和对账补偿。
六、放在同一个业务中,应该怎么选
1. 按通信需求比较
| 维度 | 短轮询 | 长轮询 | SSE | WebSocket | Webhook |
|---|---|---|---|---|---|
| 主要触发方式 | 客户端定时查询 | 客户端查询,服务端等待后响应 | 服务端向响应流写入事件 | 双方主动发送消息 | 事件触发一次回调请求 |
| 是否长期等待或保持通道 | 单次查询通常快速结束 | 单次请求等待更新或超时 | 持续保持事件流 | 持续保持双向通道 | 通常不为等待事件保留请求 |
| 更新延迟的主要来源 | 轮询间隔与请求耗时 | 网络、处理和重新发起请求的间隙 | 网络、处理和缓冲 | 网络、处理和排队 | 平台调度、网络和重试 |
| 客户端上行操作 | 普通 HTTP 请求 | 普通 HTTP 请求 | 通常另发 HTTP 请求 | 可复用同一 WebSocket 通道 | 接收方需要反向调用时另行设计 |
| 主要维护成本 | 请求量、频率与停止条件 | 等待请求、超时与游标 | 事件流、代理配置与恢复 | 连接、订阅、重连与消息协议 | 回调接口、验签、幂等与补偿 |
| 常见场景 | 低频状态刷新 | 等待更新、已有系统兼容 | 进度通知、文本流 | 高频双向交互 | 外部系统事件通知 |
这张表比较的是常见使用方式,不能据此给出固定的性能排名。数据频率、在线人数、消息大小、代理行为和服务端实现,都会影响资源开销与实际延迟。
例如,一个访问量较小、每十几秒刷新一次的页面,短轮询可能已经足够;如果为了少量通知引入复杂的连接管理,整体收益未必明显。
2. 支付平台、业务后端与浏览器如何配合
对于前文的支付页面,可以设计下面的链路:
图 3:浏览器提前建立 SSE 订阅;支付平台用 Webhook 通知业务后端,后端提交订单状态后,再向页面发送更新。
具体过程是:
- 浏览器打开订单页面,查询当前状态,并建立 SSE 订阅。
- 用户完成支付,支付平台通过 Webhook 通知业务后端。
- 后端验签、校验业务信息,幂等更新订单并提交事务。
- 后端按约定向支付平台响应成功,同时安排向页面发送状态事件。
- 页面收到事件后显示“支付成功”,并按需要关闭订阅。
图中第 4、5 步展示了一种可能的先后顺序。向支付平台响应与向浏览器推送是两个动作,不要求严格按图中的顺序执行,但都应符合各自的业务约定,避免在状态未提交时就通知成功。
如果订单事务提交与消息发送之间存在故障窗口,可以在事务中一并写入待发送事件,再由后台投递。这是一种应用层可靠性设计,并不是 SSE 或 Webhook 自动提供的能力。
订阅也要校验用户是否有权访问订单。多实例部署时,状态事件需要送到持有目标订阅的实例;不能默认处理 Webhook 的实例恰好持有用户的 SSE 连接。
此外,“先查询,再订阅”之间可能刚好发生状态变化。服务端可以在订阅建立后发送最新状态快照,或结合版本、游标协调快照与增量事件;页面重新进入或重连后也应重新校准状态。通知丢失不能改变数据库中的真实订单结果。
如果页面允许几秒延迟,也可以直接轮询订单状态。支付平台到后端仍然使用 Webhook,页面端不必因此使用 SSE。
3. 几类常见需求的选择思路
- 只需要偶尔刷新状态:先考虑普通查询或短轮询,确认更新延迟与请求量是否可接受。
- 主要是服务端持续推送文本:考虑 SSE,例如任务进度、告警列表、AI 文本输出;用户提交操作仍可走普通 HTTP。
- 双方需要频繁实时发送消息:考虑 WebSocket,并设计连接恢复与业务消息协议。
- 接收外部系统的事件:平台支持时使用 Webhook,并处理验签、重复通知和失败补偿。
- 已有系统采用长轮询:结合基础设施、兼容性与更新频率评估,不必仅因为存在新方案就重写。
七、几个容易混淆的问题
1. SSE 是单向的,浏览器就不能提交操作了吗
可以提交。“单向”描述 SSE 事件流中的数据方向:服务端向客户端发送事件。浏览器仍然可以通过普通 HTTP 请求创建订单、取消任务或发送问题。
所以,“HTTP 提交操作 + SSE 接收结果”是一种可以配合使用的设计。
2. 使用 WebSocket,就一定比轮询更好吗
不一定。WebSocket 适合持续双向交互,但会引入连接管理、重连、订阅恢复等工作。对更新稀疏、延迟要求不高的页面,轮询可能更容易实现和维护。
比较方案时,需要同时考虑业务实时性、在线规模和运维成本。
3. TCP 可靠,业务消息就一定不会丢了吗
TCP 的可靠传输不等于业务已经执行成功。服务端可能收到消息后崩溃,客户端也可能在处理事件前退出;连接重建后,还会出现历史恢复与重复处理的问题。
需要可靠交付的业务,应明确持久化时机、确认语义、重试条件和幂等边界。
4. Webhook 返回 HTTP 200,就可以认为订单处理完成了吗
需要看约定。同步方案可以在事务完成后返回;异步接收方案可能只是确认事件已被可靠接收,后续业务仍在执行。发送方与接收方应明确“接收成功”和“业务完成”的区别。
5. AI 回答逐字出现,就一定用了 SSE 吗
不能只从页面效果判断。SSE、普通 HTTP 流式响应、WebSocket 都可以实现逐步展示,前端也可能把一次性返回的文本做成逐字动画。
要确认实际机制,应查看请求、响应头、响应内容和客户端读取方式。采用 SSE 格式时,也不代表客户端一定使用原生EventSource,它还可能通过fetch读取并解析流。
参考资料
- IETF:RFC 9112 — HTTP/1.1,连接管理与持久连接
- IETF:RFC 6202 — Long Polling 与 HTTP Streaming 的问题和实践
- WHATWG HTML Standard:Server-sent events
- MDN:Using server-sent events
- Nginx:proxy_buffering 与 X-Accel-Buffering
- IETF:RFC 6455 — The WebSocket Protocol
- IETF:RFC 8441 — Bootstrapping WebSockets with HTTP/2
- WHATWG:WebSockets Standard
- GitHub Docs:About webhooks
- GitHub Docs:Best practices for using webhooks
- GitHub Docs:Handling failed webhook deliveries
- GitHub Docs:Validating webhook deliveries
- Stripe Docs:Webhooks,重试、顺序与重复事件处理