我有一个运行任务的 Sidekiq 项目,该任务创建多个子记录并更新该子对象所属的父记录。数据库是Postgres。
架构如下所示。创建子记录时,有一个 before_create 方法会更新父记录中的标志。父级有一个 before_save 方法来更新时间戳。
# 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
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 作业仅创建一条子记录时,不会出现错误。然而,当作业尝试创建一大批子记录(在一个示例中为 35 条)时,作业会长时间处于繁忙状态。我们可以看到 Postgres 连接正在等待父级更新中的锁定。
更新 Sidekiq 作业在单个事务中更新或创建一批子记录。
def perform(params, options = {})
children = params[:children]
#New transaction
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
以下是数据库中的阻塞语句。
UPDATE "Parent" SET "updated_at" = $1 WHERE "Parent"."id" = $2
创建多个子记录时如何避免这种锁定?有更好的设计吗?
无论如何,一一插入每个子节点都是低效的。您需要一种批量插入子对象的方法。这种批量插入方法只会在最后更新他们的父母一次。
相比 Rails 的 insert_all,我更喜欢 activerecord-import,主要是因为它可以进行模型验证。
# Make the Child models, but do not insert them.
children = [Child.new(...), Child.new(...), ...]
# Validate and insert all the children in bulk
Child.import! children, validate: true
# Get their Parents
parents = children.map(&:parent).uniq
# Update their Parents
parents.each { |parent|
parent.update_flag
parent.save!
}