@websiller

Почему две параллельные транзакции не блокируют друг друга?

Есть простой контроллер:
public function actionIndex()
    {
	$model = Counter::model();
	$transaction = $model->dbConnection->beginTransaction();
	$Counter = $model->findByPk(1);

	$Counter->value++;
	sleep(10);
	$Counter->save();
	$transaction->commit();

	$this->render('index');
    }


Изначально поле "value" в таблице Counter равно 0. Так вот если запустить такой контроллер два раза и одновременно, то по завершению выполнения обоих контроллеров в поле "value" будет стоять значение 1. Получается, транзакции при одновременном выполнении двух контроллеров не блокируют друг друга, а выполняются параллельно. В итоге они обе получают ноль, ждут 10 секунд, а потом записывают обновленное значение, то есть 1. Но мне нужно, чтобы в результате получалась двойка.То есть, чтобы когда начиналась первая транзакция, то вторая блокировалась бы и уже после завершения первой, запускалась вторая, получая обновленное значение (1), увеличивала бы его еще на 1 и уже записывала в поле значение "2".
  • Вопрос задан
  • 398 просмотров
Пригласить эксперта
Ответы на вопрос 3
Decadal
@Decadal
думаю, идею можно подсмотреть здесь: dr-magic.blogspot.com/2010/01/3.html
и ещё один ресурс: https://msdn.microsoft.com/ru-ru/library/aa0416cz(...
то есть, при сохранении нужно сначала проверить, совпадает ли версия записи, которая бралась транзакцией в начале, с версией, которая на сервере сейчас. Если нет - повторно совершать транзакцию с обновлёнными данными.
в Gii, генераторе для yii2, есть даже поле под такие потребности - lock называется.

Также, если говорить конкретно про вашу ситуацию, можно переопределить ActiveQuery - чтобы он выполнял апдейт в таком стиле -
UPDATE Table1 Set Col1 = :new_val1, lock = :old_lock + 1
WHERE (lock = :old_lock)
Ответ написан
bitver
@bitver
Допустим вы юзаете MySQL....
  1. У вас MyISAM? Там нет транзакций (не было, но в новых версиях появились?)
  2. (Универсальное решение) То, о чем вы пишете, решается мьютексами. О них написано много, искать даж не надо.
  3. stackoverflow.com/questions/29575826/mysql-innodb-... вообще ваш пример, гуглится спокойно.


Если в чем-то не прав - поправьте, самому интересно.
Ответ написан
Комментировать
mitaichik
@mitaichik
У вас все работает правильно и ожидаемо, и транзакции здесь вообще не при чем: фактическое изменение значения в БД у вас происходит спустя 10 секунд (save + commit). Запуская 2 процесса, они оба одновременно считают 0, изменят его на 1, и оба через 10 секунд сохранят эту единицу в БД - естественно, там будет 1.

Вы можете сохранять (save) изменения сразу после изменения на 1, но проблему это не решит, так как фактически другой транзакции изменения буду доступны только после коммита (Советую почитать про уровни изоляции транзакций https://habrahabr.ru/post/135217/ чтоб понять почему так происходит)

БД с контролем версий могла бы помочь - при попытке перезаписать устаревшие данные такая БД вернула бы ошибку. Но MySQL - не такая БД (впрочем, это можно сделать самому).

То что тебе надо - это чтоб обновление шло последовательно, не параллельно. Это очередь. Можешь сделать ее подобие сам, можешь юзать кучу готовых инструментов, можешь юзать мьютексы (хотя, имхо, в контексте php это сомнительный совет).

Если речь идет чисто о счетчиках - я бы порекомендовал тебе Redis с его incr (он однопоточный и подобных проблем там нет в принципе).

Если же это просто пример и вопрос про то как не запароть важные данные при одновременном изменении - читай про уровни изоляции транзакций и делай версионность данных.
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

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