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

Ruby ActiveRecord批量创建子记录时如何避免父记录数据库锁/延迟

批量创建子记录Postgres锁等待问题解决

问题背景

基于Sidekiq的Rails项目,使用Postgres数据库。批量创建子记录时任务长时间处于busy状态,Postgres出现锁等待;单条创建则无问题。

核心逻辑

  • 子记录创建时,before_create回调会更新父记录的is_updated标志位
  • 父记录通过before_save手动更新updated_at时间戳
  • Sidekiq任务在单个事务中批量处理子记录创建/更新

代码实现

Parent模型

# 数据库字段:
# t.boolean "is_updated", default: false
# id                          :integer          not null, primary key
# t.datetime "updated_at"
class Parent < ActiveRecord::Base
  has_many :child

  def update_flag
    self.is_updated = true
  end

  before_save :set_updated_at

  def set_updated_at
    self.updated_at = Time.current 
  end
end

ChildRecord模型

class ChildRecord < ActiveRecord::Base
  belongs_to :parent
  before_create :update_parent_flag

  def update_parent_flag
    if self.parent.try(:update_flag)
      self.parent.save!
    end
  end
end

Sidekiq任务代码

def perform(params, options = {})
  children = params[:children] 
  # 开启单个事务
  ActiveRecord::Base.transaction do
    children.each do |child|
      return_code, error = create_or_update_child_record(child)
      if return_code != :created
        Rails.logger.info error
        return
      end
    end
  end
end

问题现象

批量创建(如35条)时,Postgres出现锁等待,阻塞语句为:

UPDATE "Parent" SET "updated_at" = $1 WHERE "Parent"."id" = $2

问题根源

  1. 重复锁持有:单个事务中,每创建一条子记录就触发一次父记录的UPDATE,Postgres行锁会在事务结束后才释放,导致后续更新请求排队等待,最终形成锁堆积。
  2. 冗余更新:手动维护updated_at是多余操作——Rails默认会自动更新该字段,额外的手动赋值增加了不必要的更新次数,延长锁持有时间。

优化方案

方案1:批量创建后统一更新父记录(最优)

既然所有子记录属于同一父记录,无需每条创建时都更新父记录,批量创建完成后一次性更新即可,彻底避免重复锁。

修改Sidekiq任务

def perform(params, options = {})
  children = params[:children]
  # 假设子记录参数包含parent_id,提前获取父记录
  parent = Parent.find(children.first[:parent_id])

  ActiveRecord::Base.transaction do
    children.each do |child|
      return_code, error = create_or_update_child_record(child)
      if return_code != :created
        Rails.logger.info error
        raise ActiveRecord::Rollback # 触发事务回滚
      end
    end

    # 批量完成后统一更新父记录
    parent.update_flag
    parent.save!
  end
end

移除子记录的回调

class ChildRecord < ActiveRecord::Base
  belongs_to :parent
  # 删除before_create :update_parent_flag
end

方案2:使用原子更新减少锁冲突

若必须在子记录创建时更新父记录,改用原子更新SQL,避免加载父对象触发全量更新,减少锁持有时间。

修改ChildRecord回调

class ChildRecord < ActiveRecord::Base
  belongs_to :parent
  before_create :update_parent_flag

  def update_parent_flag
    # 直接执行原子更新,仅修改is_updated字段
    Parent.where(id: parent_id).update_all(is_updated: true)
  end
end

移除父记录的手动updated_at维护

class Parent < ActiveRecord::Base
  has_many :child

  def update_flag
    self.is_updated = true
  end

  # 删除before_save :set_updated_at及对应方法
end

方案3:拆分事务(备选,不推荐)

若无法合并父记录更新,可将每条子记录的创建放入独立事务,释放锁后再处理下一条,但会牺牲原子性,需权衡数据一致性:

def perform(params, options = {})
  children = params[:children] 
  children.each do |child|
    # 每条子记录单独开启事务
    ActiveRecord::Base.transaction do
      return_code, error = create_or_update_child_record(child)
      if return_code != :created
        Rails.logger.info error
        return
      end
    end
  end
end

关键优化总结

  • 避免循环中重复更新同一行记录,尽量合并为单次操作
  • 不要手动覆盖Rails自动维护的updated_at字段,减少冗余更新
  • 使用原子更新(update_all)可避免加载对象,缩短锁持有时间

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 20:23:25