Как иметь дело с параллельными обновлениями в базах данных?

Удалите toArray(), так что красноречиво верните по умолчанию коллекцию, тогда вы можете применить:

$newArray= $sentBy->unique('sentTo');
29
задан Madara Uchiha 27 July 2012 в 12:35
поделиться

8 ответов

Использование транзакций:

BEGIN WORK;
SELECT creds FROM credits WHERE userid = 1;
-- do your work
UPDATE credits SET creds = 150 WHERE userid = 1;
COMMIT;

Некоторые важные примечания:

  • Не все типы баз данных поддерживают транзакции. В частности, старый движок базы данных mysql по умолчанию (по умолчанию до версии 5.5.5), MyISAM, этого не делает. Используйте InnoDB (новое значение по умолчанию), если вы используете MySQL.
  • Транзакции могут быть прерваны по независящим от вас причинам. Если это произойдет, ваша заявка должна быть готова начать все заново, с НАЧАЛА РАБОТЫ.
  • Вам нужно будет установить уровень изоляции SERIALIZABLE, иначе первый выбор может считывать данные, которые другие транзакции еще не зафиксировали (транзакции не похожи на мьютексы в языках программирования). Некоторые базы данных выдают ошибку, если есть параллельные текущие транзакции SERIALIZABLE, и вам придется перезапустить транзакцию.
  • Некоторые СУБД предоставляют SELECT .. FOR UPDATE, которая блокирует строки, полученные с помощью select, до завершения транзакции.

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

26
ответ дан piojo 28 November 2019 в 01:19
поделиться

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

Допустим, у вас есть эти шаги и 2 потока параллелизма:

1) open a transaction
2) fetch the data (SELECT creds FROM credits WHERE userid = 1;)
3) do your work (credits + amount)
4) update the data (UPDATE credits SET creds = ? WHERE userid = 1;)
5) commit

И на этой временной шкале:

Time =  0; creds = 100
Time =  1; ThreadA executes (1) and creates Txn1
Time =  2; ThreadB executes (1) and creates Txn2
Time =  3; ThreadA executes (2) and fetches 100
Time =  4; ThreadB executes (2) and fetches 100
Time =  5; ThreadA executes (3) and adds 100 + 50
Time =  6; ThreadB executes (3) and adds 100 + 50
Time =  7; ThreadA executes (4) and updates creds to 150
Time =  8; ThreadB tries to executes (4) but in the best scenario the transaction
          (depending of isolation level) won't allow it and you get an error

Транзакция не позволяет переопределить значение кредитов с помощью неправильное значение, но этого недостаточно, потому что я не хочу ошибаться.

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

Есть несколько способов сделать в SQL Server, и это один из них:

SELECT creds FROM credits WITH (UPDLOCK) WHERE userid = 1;

Если я воссоздаю предыдущую временную шкалу с этим улучшением, вы получите что-то вроде этого:

Time =  0; creds = 100
Time =  1; ThreadA executes (1) and creates Txn1
Time =  2; ThreadB executes (1) and creates Txn2
Time =  3; ThreadA executes (2) with lock and fetches 100
Time =  4; ThreadB tries executes (2) but the row is locked and 
                   it's has to wait...

Time =  5; ThreadA executes (3) and adds 100 + 50
Time =  6; ThreadA executes (4) and updates creds to 150
Time =  7; ThreadA executes (5) and commits the Txn1

Time =  8; ThreadB was waiting up to this point and now is able to execute (2) 
                   with lock and fetches 150
Time =  9; ThreadB executes (3) and adds 150 + 50
Time = 10; ThreadB executes (4) and updates creds to 200
Time = 11; ThreadB executes (5) and commits the Txn2
11
ответ дан Abel ANEIROS 28 November 2019 в 01:19
поделиться

Оптимистическая блокировка с использованием нового столбца timestamp может решить эту проблему параллелизма.

UPDATE credits SET creds = 150 WHERE userid = 1 and modified_data = old_modified_date
3
ответ дан YCF_L 28 November 2019 в 01:19
поделиться

В вашем случае есть один критический момент, когда вы уменьшаете текущее кредитное поле пользователя на запрошенную сумму , и если оно успешно уменьшилось, вы выполняете другие операции, и проблема в теории может может быть много параллельных запросов на операцию уменьшения , когда, например, у пользователя есть 1 кредит на балансе и с 5 параллельными запросами на 1 кредит, он может купить 5 вещей, если запрос будет отправлен точно в одно и то же время, и в итоге вы получите -4 кредита на баланс пользователя.

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

ОБНОВЛЕНИЯ кредитов УСТАНОВЛЕНО creds = creds-1 ГДЕ creds-1> = 0 и ИД пользователя = 1

Это гарантирует этому пользователю никогда не купит много вещей за несколько кредитов, если он будет работать с вашей системой.

После этого запроса вы должны запустить ROW_COUNT (), который сообщает, были ли обновлены критерии кредитования текущего пользователя, и была ли строка обновлена:

UPDATE credits SET creds = creds-1 WHERE creds-1>=0 and userid = 1
IF (ROW_COUNT()>0) THEN 
   --IF WE ARE HERE MEANS USER HAD SURELY ENOUGH CREDITS TO PURCHASE THINGS    
END IF;

Подобное в PHP можно сделать так:

mysqli_query ("UPDATE credits SET creds = creds-$amount WHERE creds-$amount>=0 and userid = $user");
if (mysqli_affected_rows())
{
   \\do good things here
}

Здесь мы не использовали ни SELECT ... FOR UPDATE, ни TRANSACTION, но если вы поместили этот код в транзакцию, просто убедитесь, что уровень транзакции всегда предоставляет самые последние данные из строки (включая те, которые уже зафиксированы в других транзакциях). Вы также можете использовать ROLLBACK, если ROW_COUNT () = 0

Недостатком WHERE credit- $ amount> = 0 без блокировки строк являются:

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

Внимание:

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

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

Просто попробуйте полагаться на то, что кредит был успешно списан, не опустившись ниже нуля.

1
ответ дан BIOHAZARD 28 November 2019 в 01:19
поделиться

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

0
ответ дан Mark Sherretta 28 November 2019 в 01:19
поделиться

Для первого сценария вы могли бы добавить еще одно условие в предложение where, чтобы гарантировать, что вы не перезапишете изменения, сделанные одновременным пользователем. Например,

update credits set creds= 150 where userid = 1 AND creds = 0;
2
ответ дан 28 November 2019 в 01:19
поделиться

Вы можете настроить механизм организации очередей, при котором добавления или вычитания из значения типа ранга будут ставиться в очередь для периодической обработки LIFO некоторым заданием. Если требуется информация в реальном времени о «балансе» ранга, это не подходит, потому что баланс не будет вычисляться до тех пор, пока не будут согласованы невыполненные записи очереди, но если это что-то, что не требует немедленного согласования, оно может служить.

Это, кажется, отражает, по крайней мере, со стороны, как игры, подобные старой серии Panzer General, обрабатывают индивидуальные ходы. Подходит очередь одного игрока, и он объявляет свои ходы. Каждый ход, в свою очередь, обрабатывается последовательно, и конфликтов нет, потому что каждый ход занимает свое место в очереди.

1
ответ дан 28 November 2019 в 01:19
поделиться

Для таблиц MySQL InnoDB это действительно зависит от установленного вами уровня изоляции.

Если вы используете уровень по умолчанию 3 (ПОВТОРНОЕ ЧТЕНИЕ), вам потребуется заблокировать любую строку, которая влияет на последующие записи, даже если вы находитесь в транзакции. В вашем примере вам потребуется:

SELECT FOR UPDATE creds FROM credits WHERE userid = 1;
-- calculate --
UPDATE credits SET creds = 150 WHERE userid = 1;

Если вы используете уровень 4 (SERIALIZABLE), то достаточно простого SELECT с последующим обновлением. Уровень 4 в InnoDB реализуется путем блокировки чтения каждой строки, которую вы читаете.

SELECT creds FROM credits WHERE userid = 1;
-- calculate --
UPDATE credits SET creds = 150 WHERE userid = 1;

Однако в этом конкретном примере, поскольку вычисление (добавление кредитов) достаточно просто, чтобы выполняться в SQL, простое:

UPDATE credits set creds = creds - 150 where userid=1;

будет эквивалентно на SELECT FOR UPDATE с последующим UPDATE.

17
ответ дан 28 November 2019 в 01:19
поделиться
Другие вопросы по тегам:

Похожие вопросы: