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
问题根源
- 重复锁持有:单个事务中,每创建一条子记录就触发一次父记录的
UPDATE,Postgres行锁会在事务结束后才释放,导致后续更新请求排队等待,最终形成锁堆积。 - 冗余更新:手动维护
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
相关产品推荐
相关产品推荐

