更新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
相关产品推荐
相关产品推荐

