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

Ruby on Rails钱包交易API并发请求下transaction_id重复问题求助

Rails钱包交易API并发重复记录问题解决思路

问题场景

基于Ruby on Rails开发的钱包交易创建API,因对接多家银行、交易创建逻辑复杂,最初未在模型层给transaction_id/reference_no加唯一性验证。原本通过before_action检查同transaction_id的记录是否存在,存在则返回错误,但生产环境中遇到银行并发请求的情况,导致同一transaction_id生成了两条created_at完全相同的重复记录。

本地用Ruby脚本复现了该问题:Rails会同时处理两个请求,即便添加Active Record唯一性验证,两个请求的exists?查询会同时执行且都返回nil,最终还是创建了重复记录。

现有约束

理论上数据库级唯一性约束是根治方案,但当前有近15个生产数据库,部分库中可能已存在重复记录,且该模块属于系统敏感部分,直接加约束风险较高,因此需要寻找更稳妥的并发处理方案。

已尝试无效方案

  • 添加模型层唯一性验证
  • 使用Ruby的Mutex锁(进程内有效,多进程部署下失效)
  • 采用MySQL的GET_LOCK锁(仅单实例有效,多数据库/多实例场景下失效)

可行解决方案探讨

1. 先清理重复数据,逐步添加数据库约束

这是最彻底的方案,步骤如下:

  • 编写批量脚本,遍历所有生产数据库,找出transaction_id重复的记录,结合业务规则(比如保留状态正常、最新创建的记录)清理重复数据
  • 清理完成后,分批给数据库添加唯一性约束(可先通过ALTER TABLE ... ADD UNIQUE INDEX ... IGNORE的方式创建,避免因遗留数据报错)
  • 同时在模型层添加validates :transaction_id, uniqueness: true,形成模型验证+数据库约束的双重保障,从根源避免重复

2. 事务+悲观锁控制并发

在创建交易的逻辑中,用数据库事务包裹,并配合悲观锁让并发请求排队执行:

WalletTransaction.transaction do
  # 通过FOR UPDATE锁定该transaction_id的查询范围,即使无记录也会锁定,阻止其他请求的同条件查询
  WalletTransaction.where(transaction_id: params[:transaction_id]).lock(true).first
  
  # 再次检查是否存在记录
  if WalletTransaction.exists?(transaction_id: params[:transaction_id])
    raise ActiveRecord::Rollback, "重复交易请求"
  end
  
  WalletTransaction.create!(transaction_id: params[:transaction_id], ...)
end

这种方式利用数据库事务的隔离性,确保同一transaction_id的创建请求串行执行,避免重复。

3. 分布式锁(多实例/多数据库场景)

如果是多服务器部署或多数据库实例的情况,进程内锁和单数据库锁都无效,可采用Redis分布式锁:

lock_key = "wallet_transaction_lock:#{params[:transaction_id]}"
# 设置锁超时时间,避免死锁
lock = Redis.current.lock(lock_key, expires_in: 10.seconds)

begin
  if lock.acquire
    # 锁获取成功,执行创建逻辑
    unless WalletTransaction.exists?(transaction_id: params[:transaction_id])
      WalletTransaction.create!(...)
    else
      # 返回重复错误
    end
  else
    # 锁获取失败,说明有并发请求在处理,直接返回重复提示
  end
ensure
  # 确保锁被释放
  lock.release if lock
end

分布式锁可以跨进程、跨服务器控制并发,适合复杂部署场景。

4. 基于幂等性的请求处理优化

在API层面强化幂等性设计:

  • 要求对接的银行端以transaction_id作为幂等键,确保同一交易的重复请求不会触发重复创建
  • 服务端收到请求后,先在Redis中记录该transaction_id的处理状态(比如"处理中"),并发请求过来时,若发现状态为"处理中"则等待或直接返回已有结果;处理完成后更新状态为"完成"
  • 结合数据库查询,双重验证避免重复

内容的提问来源于stack exchange,提问作者M Ibtehaj Nasar

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 02:05:55