Оптимистическая или пессимистическая блокировка в аукционе / банковском приложении (Rails / MySQL) - PullRequest
3 голосов
/ 30 декабря 2011

Я нахожусь в процессе разработки аукциона, такого как веб-приложение, с использованием Rails 3.1 и MySQL 5.1. Пользователи будут иметь остатки на счетах, поэтому важно, чтобы кто-то не предлагал цену за аукцион, если у него недостаточно средств.

Очевидно, что я буду упаковывать «выигрыш» аукциона в транзакцию, что-то вроде этого:

Транзакция 1:

ActiveRecord::Base.transaction do
    a = Account.where(:id=>session[:user_id]).first
    # now comes a long part of code with various calculations and other table updates, i.e. time pases
    a.balance -= the_price_of_the_item
    a.save!
end

Кстати, я использую оптимистическую блокировку, поэтому во всех моих таблицах есть столбец lock_version.

В то время как такая транзакция выполняется, пользователь может с помощью другого ввода разместить другие заявки, следовательно, всякий раз, когда они делают ставку, фрагмент кода проверяет, достаточен ли текущий доступный баланс

То же самое и здесь:

Транзакция 2:

ActiveRecord::Base.transaction do
    a = Account.where(:id=>session[:user_id]).first
    raise ActiveRecord::Rollback if a.balance < the_price_of_the_bid + Bids.get_total_bid_value_for_user(session[:user_id])
    # now process the bid saving
end

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

Следует отметить, что транзакция 2 не вносит никаких изменений в Учетную запись, она просто читает учетную запись. Я предполагаю, что это сводится к вопросу: как предотвратить любые чтения для выбранных операторов SELECT при выполнении транзакции 1.

Как заставить транзакцию 2 ждать завершения транзакции 1? Возможно ли это с оптимистической блокировкой и одним из доступных уровней изоляции транзакций MySQL или мне нужно использовать здесь пессимистическую блокировку? Если пессимистическая блокировка является единственным ответом, добавление замок! после прочтения записи счета в каждой из двух транзакций будет достаточно?

Критерии дизайна, конечно,

  • Я ищу наиболее эффективное решение, даже если оно означает больше кодирование.
  • согласованность данных имеет первостепенное значение

Ответы [ 2 ]

2 голосов
/ 31 декабря 2011

Потратив сейчас почти 10 часов без перерыва на чтение различных постов и документов, а также проб и ошибок с помощью консоли Rails, я хочу обобщить мои выводы:

Оптимистическая блокировка: бесполезна для удовлетворения моих требований, блокировка срабатывает только тогда, когда я действительно сохраняю запись баланса счета. Но размещение ставки не обновляет запись аккаунта, поэтому она не вызовет оптимистическую блокировку, если только я не сохраню поле в записи аккаунта, которое отслеживает мои текущие зафиксированные средства для всех открытых ставок и, следовательно, будет обновлять запись аккаунта после размещения ставки (что я не хочу делать, так как для сохранения ставки потребуется другое обновление БД).

Так что это оставляет меня только с пессимистической блокировкой. Для простоты я решил заблокировать запись пользователя, поэтому код для моей транзакции 1 изменяется на:

Транзакция 1:

ActiveRecord::Base.transaction do     
    u = User.find(session[:user_id],:lock=>true)
    a = Account.where(:id=>session[:user_id]).first 
    a.balance -= the_price_of_the_item
    ... some more code here ...
    a.save!      
end      

и транзакция 2:

ActiveRecord::Base.transaction do
    u = User.find(session[:user_id],:lock=>true)
    raise ActiveRecord::Rollback if a.balance < the_price_of_the_bid + Bids.get_total_bid_value_for_user(session[:user_id])          
    # now process the bid saving
    ....
end

Кроме того, я решил установить уровень изоляции транзакции MySQL на SERIALIZABLE.

1 голос
/ 31 декабря 2011

Я интерпретирую ваш вопрос следующим образом.

  1. На счету есть баланс
  2. У учетной записи много ставок
  3. Ставка имеет значение
  4. Заявка, которая не выиграла и не проиграла, является "открытой"
  5. Когда Большой выигран, его стоимость вычитается из баланса Счета.
  6. Ставка может быть подана, только если сумма значений для «открытых» ставок меньше баланса.

Таким образом, вы определили важные транзакции.

  1. Размещение ставки
  2. Выигрышная ставка

Вот как я это сделаю.

1

account = Account.find_by_id(session[:user_id])
# maybe do some stuff here

transaction do
  account.lock!

  bid_amount = account.bids.open.sum(:value)
  if bid_amount + this_value > account.balance
    raise "you're broke, mate"
  end

  account.bid.create!(:value => this_value)
end

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

Если предположить, что первый бит был верным, то следующий намного проще

2

class Bid
  def win!
    transaction do
      account.lock!
      account.decrement(:balance, self.value)
      account.save!
      close!
    end
  end
end

В частности, если вы обновили SQL SET balance = balance - ?, вам не нужно было бы блокировать 2-е.

Но, как правило, блокируйте минимальный фрагмент кода.

Реально, у вас не должно быть блокировки более 100 мс.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...