Как избежать состояния гонки при создании дочернего идентификатора на основе родительского атрибута

Представьте, что у вас есть Site котором есть одна из тех вещей, которые «берут число», где люди берут номер, а затем ждут своей очереди. Допустим, каждый номер является Order . Связь заключается в том, что на Site много Orders

В заданной точке сброса менеджер сайта проходит и заменяет «взять число» новыми наборами чисел, то есть вы можете себе представить, что это происходит ежедневно. Однако вещи типа «взять число» изготавливаются с конечными числами, поэтому каждый бросок состоит, скажем, из 1-100, и затем следующий бросок начинается снова со 100-999.

Я пытаюсь смоделировать вышеупомянутое поведение, как я думал о приближении к нему:

  1. На родительском Site есть атрибут start_number . Нажатие сброса / замены броска приведет к сбросу start_number на 100, т. start_number На первое число броска
  2. У дочернего элемента Order есть обратный вызов, который присваивает номер. Если номер родительского Site равен 100, то это означает, что это первый Order после сброса, как описано в шаге № 1, поэтому тогда это число равно 100. Теперь родительский Site автоматически обновляется, так что он больше не находится в состоянии сброса (например, start_number больше не start_number 100). Для будущих Orders назначенный номер - это просто следующий номер после предыдущего заказа

Вот код:

class Site
  has_many :orders
end

class Order
  belongs_to :site

  before_save :assign_number

  def assign_number
    if site.start_number == 100
      self.number = 100
      self.site.update_column(:start_number, nil)
    else
      self.number = self.site.orders.where.not(number:nil).last.number + 1
    end
  end
end

Но это дерьмо, потому что, в отличие от реальной вещи «возьми число», 2 ордера могут обрабатываться одновременно, нет unique ограничения на Order.number потому что числа снова используются (бросок сбрасывается). Но, очевидно, бесполезно, если при сбросе 2 ордера, которые размещаются близко друг к другу, равны 100 . Вы хотите, чтобы несколько ордеров разделяли number если действительно произошло событие сброса, а не просто случайное время.

Другая проблема с этим подходом состоит в том, что за 1 ордером следует второй. Например, последний присвоенный номер был 415 , 2 заказа выполняются в быстрой последовательности. self.site.orders.where.not(number:nil).last.number присваивается 416 , второе настолько близко, что self.site.orders.where.not(number:nil).last.number прежнему возвращает 415 (т. self.site.orders.where.not(number:nil).last.number 416 еще не сохранен), и поэтому второй порядок Теперь также назначен 416 .

Было бы здорово получить идеи о том, как лучше смоделировать желаемое поведение. Спасибо!

ОБНОВЛЕНИЕ За комментарии @ Фернана я собираюсь пойти с пессимистической блокировкой, которую я реализую согласно примечаниям здесь . Итак, прямо сейчас код выглядит так:

  def assign_number
    site = self.site.lock!
    if site.start_number == 100
      self.number = 100
      site.start_number = nil
    else
      last_order = self.site.orders.where.not(number:nil).last.lock!
      self.number = last_order.number + 1
      last_order.save! # releases lock
    end
    site.save! #releases lock, whether or not call number was updated to nil
  end

Я не совсем уверен, как специфицировать это, хотя ... так как написание спецификации по определению упорядочено ... как заставить 2 ордера сохранить близко друг к другу, чтобы симулировать это поведение?

Всего 1 ответ


Одним из способов является создание другой таблицы, в которой будет храниться последний номер.

#code   :string   not null
#number :bigint   default(0), not null

class Serial < ApplicationRecord
  def self.get_latest_number
    Serial.transaction do
      self.lock.find_or_create_by!(code: 'just_any_identification').increment!(:number).count
    end
  end
end

затем установите номер наиболее вероятно во время обратного вызова.

class Order
  before_create :set_number #or after_create

  private
    def set_number
      self.number=Serial.get_latest_number if self.number.blank?
    end
end

принять к сведению блокировку транзакции .


Есть идеи?

10000