并发保存多个数据时如何从数据库中获取值

how to get value from database when concurrent saving multiple data

class Defect < ApplicationRecord
  has_many :work_orders, dependent: :destroy
end
class WorkOrder < ApplicationRecord
  belongs_to :defect
  before_save :default_values

  def default_values
    self.running_number = self.defect.work_orders.maximum(:running_number).to_i + 1 if self.new_record?
  end
end

理想情况下代码是这样工作的

Defect A 
- Work Order running_number 1
- Work Order running_number 2
- Work Order running_number 3

Defect B
- Work Order running_number 1
- Work Order running_number 2
- Work Order running_number 3

然而,当多个用户同时保存属于同一个缺陷的不同 WorkOrder 对象时,running_number 将会失控,因为 maximum_running_number 仅基于保存的数据。 如何正确保存 running_number?

问题是您的并发保存获得了相同数量的工单,因此您获得了重复的工单 running_numbers。

您可以通过两种方式解决:

  • running_numberdefect_id
  • 上设置唯一约束
  • 获取工作订单的锁 table,直到您提交新的工作订单。

要在 rails 迁移中设置唯一约束:add_index :work_orders, [:defect_id, :running_number], unique: true。然后,如果调用 save.

时出现错误,请重试保存

假设您使用的是 Postgres

  begin 
    # .. create the work order
    work_order.save
  rescue PG::UniqueViolation
    retry
  end

使用 retry 将重试该块,直到没有出现唯一违规为止。如果记录中存在其他一些独特的违规错误,这可能会导致死锁,因此请确保该错误是由 running_number 而不是其他原因引起的。

另一种方法是获取锁以防止竞争条件。由于它的数据库 table 是共享资源,您需要一个 table 锁以确保在计算工作订单数量和保存时没有其他进程正在使用工作订单 table记录。

假设您使用的是 Postgres explicit-locking docs

ActiveRecord::Base.transaction do
  # create order

  ActiveRecord::Base.connection.execute('LOCK work_orders IN ACCESS EXCLUSIVE MODE')
  
  work_order.save
end

使用此模式获取 table 锁将阻止其他数据库连接对 table 的所有访问。它会在事务提交时释放,但如果 ruby 进程在它有机会完成事务块之前被终止,则可能再次导致死锁。