如何避免Rails并发请求触发重复的第三方外部API调用
问题解决方案
你遇到的是典型的Rails并发请求下重复执行逻辑的竞态条件问题,可按你的项目技术栈选择以下方案处理:
1. 数据库唯一约束 + 异常捕获(适合创建逻辑先写库再调API的场景)
这是成本最低、可靠性最高的防重复插入方案,首先给profiles表的user_id字段加唯一索引,从数据库底层禁止单用户重复生成档案:
# 执行命令生成迁移文件 rails g migration AddUniqueUserIdIndexToProfiles user_id:uniq # 执行迁移 rails db:migrate
然后修改get_user方法,捕获唯一约束冲突异常即可:
def get_user @user = User.find(params[:id]) return if @user.profile.present? begin @user.create_profile rescue ActiveRecord::RecordNotUnique # 其他请求已经完成创建,重新加载档案即可 @user.reload.profile end end
注意:如果你的
create_profile逻辑是先调用第三方API、再把结果写入数据库,该方案只能避免重复存库,还是会触发两次API调用,这种情况建议用下面两种方案。
2. 数据库行锁(适合中小流量项目)
在查询用户的时候加行锁,让后续同用户的查询请求等待第一个请求的事务完成后再执行,从源头避免重复进入创建逻辑:
def get_user # 加行锁,同ID的用户查询会串行执行 @user = User.lock.find(params[:id]) @user.create_profile if @user.profile.nil? end
该方案不需要额外引入第三方组件,实现成本极低,适合并发量不高的项目。
3. 分布式锁(适合高并发、已接入Redis的项目)
用Redis实现分布式锁,只有拿到锁的请求才能执行档案创建逻辑,完全避免重复调用第三方API:
def get_user @user = User.find(params[:id]) return if @user.profile.present? lock_key = "profile_create:#{@user.id}" # 锁过期时间按第三方API的最大响应时间设置,这里示例为10秒 if Redis.current.set(lock_key, 'locked', nx: true, ex: 10) begin @user.create_profile ensure # 执行完成后主动释放锁 Redis.current.del(lock_key) end else # 未拿到锁的请求轮询等待创建完成,最多等待3秒 6.times do break if @user.reload.profile.present? sleep 0.5 end end end
额外提示:不要直接用Rails自带的find_or_create_by方法处理该场景,它内部是先查询再插入,中间存在竞态窗口,并发场景下依然会出现重复创建的问题。
内容的提问来源于stack exchange,提问作者Prem Anand
相关产品推荐
相关产品推荐

