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

更新Stripe Connect账户与数据库时webhook竞态问题无法通过并发异常解决

解决方案

1. Stripe.Event可用于判断顺序的属性

Stripe的事件ID(stripeEvent.Id)为K可排序(K-sortable)的有序字符串,同一时间戳生成的事件,ID字典序更大的代表生成时间更晚,完全可以解决同时间戳事件先后判断的问题。

2. 可行的落地方案

方案一:优化现有乐观并发逻辑

在原有的时间戳判断基础上,增加事件ID的比较规则:

  • 数据库的StripeAccount表新增LastProcessedEventId字段,存储上次处理的最新事件ID
  • 收到account.updated事件时的判断逻辑调整为:
    • 若新事件的Created时间早于数据库存储的EventCreatedDate:直接跳过更新
    • 若新事件的Created时间等于数据库存储的EventCreatedDate:仅当新事件ID的字典序大于LastProcessedEventId时才执行更新
    • 若新事件的Created时间晚于数据库存储的EventCreatedDate:直接执行更新
  • 并发冲突处理的逻辑同步调整为同时判断时间戳和事件ID,只有新事件确实晚于数据库中已存事件时才执行覆盖

优化后的核心代码示例:

[HttpPost("stripe")]
public async Task<ActionResult> StripeWebhook()
{
    var json = await new StreamReader(HttpContext.Request.Body).ReadToEndAsync();
    try
    {
        Event stripeEvent = EventUtility.ConstructEvent(json, Request.Headers["Stripe-Signature"], WhSecret);
        if (stripeEvent.Type == Events.AccountUpdated)
        {
            var stripeAccount = stripeEvent.Data.Object as Stripe.Account;
            var spec = new StripeAccountWithTypeSpecification(stripeAccount.Id);
            var dbEntity = await _unitOfWork.Repository<StripeAccount>().GetEntityWithSpec(spec);
            if (dbEntity == null) 
                return BadRequest();
            
            bool canUpdate = false;
            var timeCompareResult = dbEntity.EventCreatedDate.CompareTo(stripeEvent.Created);
            if (timeCompareResult < 0)
            {
                // 新事件时间更晚,允许更新
                canUpdate = true;
            }
            else if (timeCompareResult == 0)
            {
                // 时间相同,比较事件ID字典序,新事件ID更大则允许更新
                if (string.Compare(stripeEvent.Id, dbEntity.LastProcessedEventId, StringComparison.Ordinal) > 0)
                {
                    canUpdate = true;
                }
            }

            if (canUpdate)
            {
                _mapper.Map(stripeAccount, dbEntity);
                dbEntity.EventCreatedDate = stripeEvent.Created;
                dbEntity.LastProcessedEventId = stripeEvent.Id;
                _unitOfWork.Repository<StripeAccount>().Update(dbEntity);
                try
                {
                    await _unitOfWork.Complete();
                }
                catch (DbUpdateConcurrencyException ex)
                {
                    var entry = ex.Entries.Single();
                    var currentValues = entry.CurrentValues;
                    var currentEventDate = currentValues.GetValue<DateTime>("EventCreatedDate");
                    var currentEventId = currentValues.GetValue<string>("LastProcessedEventId");
                    
                    var databaseValues = entry.GetDatabaseValues();
                    var dbEventDate = databaseValues.GetValue<DateTime>("EventCreatedDate");
                    var dbEventId = databaseValues.GetValue<string>("LastProcessedEventId");

                    bool shouldOverride = false;
                    var concurTimeCompare = dbEventDate.CompareTo(currentEventDate);
                    if (concurTimeCompare < 0)
                    {
                        shouldOverride = true;
                    }
                    else if (concurTimeCompare == 0)
                    {
                        if (string.Compare(currentEventId, dbEventId, StringComparison.Ordinal) > 0)
                        {
                            shouldOverride = true;
                        }
                    }

                    if (shouldOverride)
                    {
                        entry.Reload();
                        _mapper.Map(stripeAccount, dbEntity);
                        dbEntity.EventCreatedDate = currentEventDate;
                        dbEntity.LastProcessedEventId = currentEventId;
                        await _unitOfWork.Complete();
                    }
                    _logger.LogInformation("Concurrency Exception Thrown");
                }
            }
        }
        else if (stripeEvent.Type == Events.AccountApplicationDeauthorized)
        {
            var application = stripeEvent.Data.Object as Stripe.Application;
            _logger.LogInformation("Account application deauthorized id: {0}: ", application.Id);
        }
        else
        {
            _logger.LogInformation("Unhandled event type: {0}", stripeEvent.Type);
        }
        return Ok();
    }
    catch (StripeException ex)
    {
        _logger.LogWarning("Stripe Exception: {0}, {1}", ex.Message, ex);
        return BadRequest();
    }
}

方案二:增加主动拉取兜底逻辑

为了避免极端情况下的状态不一致,可在每次处理完account.updated事件后的1~2秒,异步发起一次Stripe账户信息查询,用主动拉取到的最新状态覆盖数据库记录,主动拉取的结果永远是官方最新状态,可完全规避webhook乱序问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.02 00:45:02