2014-01-23 2 views
1

Мне интересно, как бороться с параллелизмом, учитывая общую книгу. Рассмотрим схему, как это:Общая паровая игра (django atomic operations)

id | account_id | credit | debit | balance | 
1 | 123  | 0  | 100 | 200  | 
2 | 456  | 100 | 0  | 100  | 

Чтобы добавить новую запись в бухгалтерской книге, я бы сделал (псевдо-код):

last_entry = get last account entry 
is_credit = figure out if it is debit or credit entry 
is_liability = figure out type of account 

new_entry = Entry(
    foo='bar' 
    # etc 
) 

if is_liability and is_credit 
    new_entry.balance = last_entry.balance + amount 

if is_liability and !is_credit 
    new_entry.balance = last_entry.balance - amount 

if !is_liability and is_credit 
    new_entry.balance = last_entry.balance - amount 

if !is_liability and !is_credit 
    new_entry.balance = last_entry.balance + amount 

new_entry.save() 

Проблема, которую я вижу при таком подходе является:

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

Что делать, если в середине запуска вышеуказанного кода (скажем, после получения последней записи) есть еще один запрос, который снова увеличит весы.

Так баланс будет увеличен один раз, другой запрос будет сохранить новую запись с тем же балансом, как он будет просто использовать что-то вроде:

new_balance = last_entry.balance + amount 

Но last_entry был устаревшим другим запросом так баланс сейчас выше.

Любые идеи о том, как убедиться, что подобной ситуации не происходит (я знаю, что это было бы маловероятно).

UPDATE:

После некоторых ответов, я пришел к этому решению с помощью SELECT FOR UPDATE:

with transaction.atomic(): 
     new_entries = prepare_entries() 
     for new_entry in new_entries: 
      new_entry.save() 

Это хороший способ, чтобы обойти потенциальные проблемы параллелизма?

ответ

2

Вы можете использовать select_for_update (возвращает QuerySet, который будет блокировать строки до конца сделки):

with transaction.atomic(): # or commit_on_success/commit_manually in django < 1.6 
    new_entries = prepare_entries() 
    new_entries.select_for_update() # lock to update only in current transaction 
    for new_entry in new_entries: 
     #change new_entry somehow 
     new_entry.save() 

или F выражение:

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

Например:

last_entry.update(balance=F('balance')+amount) 
+0

Можете взглянуть на мой код? Я добавил несколько пример кода на мой вопрос. Вы видите какие-либо проблемы с этим? –

+0

Спасибо. Я вижу сейчас. Таким образом, изменения, сделанные одной открытой транзакцией, видны другим работающим транзакциям. Я обновил свой пример кода. Сейчас это намного проще. –

+0

Обратите внимание, что для работы 'select_for_update' вы должны находиться внутри транзакции. –

1

Предполагая, что ваша база данных поддерживает ее (и для этого она должна), заверните всю операцию в транзакции. I. Начните с вызова «начать транзакцию» и закончите с фиксацией.

Это гарантирует выполнение всей транзакции или ее отсутствие. Вам также может потребоваться заблокировать таблицу, пока вы это сделаете, чтобы обеспечить постоянный внешний вид для других процессов.

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

1

Вычислить общее различие, которое будет применяться к balance и использовать update запрос:

Model.objects.filter(pk=entry.pk).update(balance=F('balance') + difference) 
+0

В этом вызове, можно также атомарно проверить, если баланс остается положительным? или я должен проверить его потом и откат, если он стал отрицательным? – EralpB

Смежные вопросы