You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Stripe Webhook短时间重复发送同一事件的原因与解决方案咨询

Stripe开发环境下Webhook重复发送事件的问题

我正在用Stripe搭建支付系统,支付完成后通过invoice.paid事件完成用户授权。测试时发现Webhook会在短时间内重复发送同一事件:不管是网页测试模式支付,还是执行终端命令stripe events resend evt_xxxxxxxxxxxxxxx都会复现,但只在开发环境(用stripe listen --forward-to http://0.0.0.0:8080/webhook/启动)出现,生产环境还没测。

我试过用保存事件ID的方式去重,收到重复事件就返回200,代码如下:

def webhook_view(request):
    payload = json.loads(request.body)
    event_id = payload.get('id')
    event_type = payload.get('type', '')
    logger.info(f'type: {event_type} event_id: {event_id}')
    if Event.objects.filter(id=event_id).exists():
        logger.info(f'{event_id} already exists')
        return HttpResponse(status=200)
    Event(id=event_id, created_at=timezone.now(), data=json.dumps(payload)).save()

但两次请求间隔太短(日志显示仅5毫秒),第二个事件还是能通过重复校验:

[2022-07-24 22:28:53,677] [webhook_view: 60] type: invoice.paid event_id: evt_1LP4n0BB5Q5Dr2x7QDEfWFak

[2022-07-24 22:28:53,682] [webhook_view: 60] type: invoice.paid event_id: evt_1LP4n0BB5Q5Dr2x7QDEfWFak

想咨询三个问题:

  1. 为何会在短时间内发送两次相同事件?
  2. 是否可以仅发送一次事件,或延迟第二次事件的发送?
  3. 如何解决该重复事件问题?

回答

1. 短时间重复发送事件的原因

这是Stripe CLI在开发环境的特性:用stripe listen转发事件时,CLI会模拟生产环境的重试机制,但开发场景下可能因本地服务响应极快、网络转发的微小延迟,触发短时间内的重复推送。另外执行stripe events resend时,CLI的内部逻辑也可能导致重复推送。生产环境中Stripe的事件重试间隔是逐步拉长的(几秒、几分钟后重试),不会出现这种毫秒级重复。

2. 能否控制只发送一次或延迟第二次?

对于stripe listen的开发环境,没有直接关闭短时间重复推送的配置,因为它是为了模拟生产环境可能出现的重试场景(比如服务临时宕机、超时)。不过可以尝试:

  • 确保本地Webhook服务响应足够快,CLI收到200响应后会停止重试;
  • 执行stripe events resend时添加--no-retry参数(可查看Stripe CLI文档确认支持情况),强制只发送一次事件。

3. 解决重复事件的方案

你的去重逻辑因并发竞态条件失效,需要改成原子操作来避免:

方案一:数据库唯一约束+原子创建

给Event模型的id字段添加唯一约束,用get_or_create原子操作处理事件创建,捕获唯一键冲突异常:

from django.db import IntegrityError, transaction

def webhook_view(request):
    payload = json.loads(request.body)
    event_id = payload.get('id')
    event_type = payload.get('type', '')
    logger.info(f'type: {event_type} event_id: {event_id}')

    try:
        with transaction.atomic():
            event, created = Event.objects.get_or_create(
                id=event_id,
                defaults={'created_at': timezone.now(), 'data': json.dumps(payload)}
            )
            if not created:
                logger.info(f'{event_id} already exists')
                return HttpResponse(status=200)
    except IntegrityError:
        # 并发场景下唯一键冲突,说明事件已存在
        logger.info(f'{event_id} already exists (integrity conflict)')
        return HttpResponse(status=200)
    
    # 此处处理用户授权等业务逻辑
    return HttpResponse(status=200)

get_or_create是数据库层面的原子操作,结合唯一约束,能确保同一事件ID只会被创建一次,并发请求无法绕过校验。

方案二:分布式锁(多实例部署场景)

如果服务是多实例运行,可借助Redis实现分布式锁,确保同一事件只有一个请求在处理:

import redis
from django.conf import settings

r = redis.Redis(host=settings.REDIS_HOST, port=settings.REDIS_PORT, db=0)

def webhook_view(request):
    payload = json.loads(request.body)
    event_id = payload.get('id')
    event_type = payload.get('type', '')
    logger.info(f'type: {event_type} event_id: {event_id}')

    # 用事件ID作为锁键,设置10秒过期时间(足够处理事件)
    lock_key = f"webhook_event:{event_id}"
    if not r.set(lock_key, "processing", nx=True, ex=10):
        logger.info(f'{event_id} is being processed or already exists')
        return HttpResponse(status=200)
    
    try:
        if Event.objects.filter(id=event_id).exists():
            logger.info(f'{event_id} already exists')
            return HttpResponse(status=200)
        
        Event.objects.create(id=event_id, created_at=timezone.now(), data=json.dumps(payload))
        # 处理业务逻辑
    finally:
        # 释放锁
        r.delete(lock_key)
    
    return HttpResponse(status=200)

另外,无论用哪种方案,都要确保Webhook视图尽快返回200响应,不要在视图中阻塞处理业务逻辑(比如用户授权),可以把业务逻辑放到异步任务(如Celery)中执行,视图只负责验证和去重,避免因响应超时触发Stripe重试。

内容的提问来源于stack exchange,提问作者Nori

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.24 19:48:46