решение проблемы гонки в рельсах (ограниченное количество X в Y)

Изменить: использование MySQL...

Допустим, у вас есть приложение, которое добавляет учеников в класс, и в этом классе мало места... поэтому вы делаете что-то вроде этого:

def add
  if some_classroom.size < MAX_SIZE
    add_student_to_class
  end
end

Это состояние гонки в многопоточной среде. Хромой.

Предполагая, что мы

  1. не хочу этого и
  2. не хотите блокировать нашу классную таблицу или запись (что приводит к тому, что наше приложение отстой в другом месте)

Что мы делаем?

Я предлагаю это:

class Classroom < ActiveRecord::Base
  has_one :classroom_lock
  after_create :create_lock_record

  def create_lock_record
    c = ClassroomLock.new
    c.classroom = self
    c.save!
  end
end

class ClassroomLock < ActiveRecord::Base
  belongs_to :classroom
end

def add
  c = Classroom.first
  ActiveRecord::Base.transaction do
    c.classroom_lock.lock!
    c = Classroom.first #load this again (it might have changed)
    if c.size < MAX_SIZE
      c.add_new_student(some_student)
    else
      do_stuff_about_not_enough_room
    end
  end
end

Кажется, это должно работать потрясающе. Мой (фиктивный) метод Classroom#show не блокируется, потому что запись класса на самом деле не заблокирована, а метод add фактически является однопоточным, поскольку любые дополнительные процессы будут вынуждены ждать блокировки! линии до тех пор, пока блокировка не будет снята.

Это работает? Может быть? Я думаю так? Я не знаю...

Я довольно много работал над этим с несколькими процессами одновременно, но трудно сказать наверняка (в конце концов, это состояние гонки).

Может ли кто-нибудь дать дополнительную информацию?


person jsharpe    schedule 02.03.2012    source источник
comment
Используйте 1_. Выбросьте свое самодельное решение и посмотрите guides.rubyonrails.org/   -  person meagar    schedule 02.03.2012
comment
Я прокатил это: def rockout! ; ActiveRecord::Base.transaction do ; u = User.lock(LOCK IN SHARE MODE).first ; спать 5 ; u.email = foo#{Time.now.to_i}@foo.com ; у.сохрани! ; конец ; конец   -  person jsharpe    schedule 02.03.2012
comment
... и выполнил его на двух разных консолях. Второй привел к тупику. так что минус :/   -  person jsharpe    schedule 02.03.2012
comment
ActiveRecord::StatementInvalid: Mysql2::Error: Обнаружена взаимоблокировка при попытке получить блокировку; попробуйте перезапустить транзакцию   -  person jsharpe    schedule 02.03.2012
comment
... также, вероятно, лучше вносить предложения в виде ответов, а не комментариев   -  person jsharpe    schedule 02.03.2012
comment
Вы когда-нибудь устанавливали, работает ли это? Я чувствую, что мне нужно что-то очень похожее, чтобы решить (своего рода) состояние гонки find_or_create, которое у меня есть.   -  person Andy    schedule 27.03.2013


Ответы (1)


Hi,

Я представляю довольно простой SQL-подход с одной оговоркой: добавьте дополнительный столбец с именем что-то вроде list_position и добавьте уникальный индекс в таблицу отношений класс-ученик, состоящий из classroom_id, student_id и list_position.

Вставка происходит путем установки list_position на количество учеников в классе плюс один. Код Ruby, вероятно, будет где-то рядом

classroomStudent = ClassroomStudent.new
classroomStudent.list_position = classroom.size + 1
until classroomStudent.save
  classroomStudent.list_position += 1
  do_stuff_about_not_enough_room if classroomStudent.list_position > MAX_SIZE
end

Результат: если две вставки попытаются вставить учащегося в один и тот же класс одновременно, уникальный индекс не позволит одному учащемуся (INSERT не удается). Это означает, что вам, возможно, придется повторять попытку до list_position=MAX_SIZE, но вы гарантированно никогда не наберете в классе слишком много учеников. Вы также можете использовать этот подход для создания очереди ожидания.

Если у вас уже есть данные в таблице отношений, вам придется сначала добавить значение для list_position. я бы предположил что-то вроде

UPDATE ClassroomStudents AS c1 SET c1.list_position = COALESCE((SELECT MAX(c2.list_position) FROM ClassroomStudents AS c2 WHERE c2.classroom_id = c1.classroom_id), 0) + 1;

сделает работу здесь, хотя это может быть немного медленно.

Я знаю, что у решения все еще есть некоторые шероховатости, но, возможно, это поможет.

С Уважением

TC

person TheConstructor    schedule 08.11.2012