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

Rails唯一性校验因时序问题失效重复创建记录,求替代sleep的合规解决方案

解决方案

你遇到的是Rails应用层唯一性校验的竞态问题 + webhook递归调用产生重复请求的组合问题。你加sleep 5能临时生效是因为人为拉大了多个请求的执行时间差,让第一个请求先完成入库,后续请求的应用层校验就能查到已有记录从而拒绝,但这个方案不可靠,只要并发升高或者数据库响应变慢,依然会出现重复写入的问题。

以下是可落地的非hack方案:

1. 数据库层加唯一约束(最核心,彻底解决竞态问题)

Rails的validates_uniqueness_of是纯应用层校验,本质逻辑是先查库判断有没有对应记录,没有才执行写入。两个同时到达的请求都会查到空结果,最终就会都写入成功。必须在数据库加唯一索引才能从根源上避免重复写入:

# 生成对应迁移文件执行即可,注意where条件要匹配你原有校验的unless规则
add_index :bookings, [:experience_id, :time, :pipedrive_deal_id], 
  unique: true, 
  where: "pipedrive_deal_id != 0 AND pipedrive_deal_id IS NOT NULL"

加完索引后,重复写入请求会直接抛出ActiveRecord::RecordNotUnique异常,你只需要在写入逻辑里捕获这个异常,转而查询已存在的记录执行更新逻辑即可,也就是实现标准的upsert逻辑。

2. 避免内部操作递归触发webhook(减少重复请求根源)

你现有代码里多次触发webhook是因为模型的更新/创建操作绑定了webhook回调,内部操作时可以临时跳过回调,避免产生不必要的重复请求:
第一步给Booking模型加跳过标识:

class Booking < ApplicationRecord
  attr_accessor :skip_pipedrive_webhook
  # 假设你触发webhook的是after_commit回调,加unless条件跳过
  after_commit :send_pipedrive_webhook, on: [:create, :update], unless: :skip_pipedrive_webhook
  
  # 原有校验逻辑保持不变
  validates_uniqueness_of :pipedrive_deal_id, :allow_blank => true, scope: [:experience_id, :time], unless: -> { pipedrive_deal_id.zero? }
end

第二步修改你内部更新的代码,更新时带上跳过标识:

# 把原来的booking.update(booking_params)改成
booking.update(booking_params.merge(skip_pipedrive_webhook: true))

这样内部执行的更新操作就不会再触发webhook回调,从根源上避免自己调用自己产生的重复请求。

3. 给webhook接口加幂等校验(兜底兼容所有重复调用场景)

不管是Pipedrive的官方重试还是你内部逻辑产生的重复请求,都可以通过幂等校验直接拦截:
Pipedrive的每一个webhook事件都自带唯一的event_id参数,你可以新建一张webhook_events表,每次收到请求先把event_id存进去(同样加唯一索引),如果已经存在就直接返回200成功,不执行业务逻辑。也可以直接用你业务里的pipedrive_deal_id + 事件类型作为唯一标识做校验。

4. 异步任务解耦(可选,适合复杂业务逻辑)

如果你的业务逻辑本身耗时很长,不要在webhook同步流程里执行,把创建更新、调用第三方服务的逻辑丢到Sidekiq这类后台任务队列里执行,还可以给任务加唯一锁,保证同一个业务标识的任务同时只能跑一个,完全避免竞态。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.02 01:27:05