Задать вопрос
szQocks
@szQocks

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

Есть таблица пользователя, и его баланс который равен 100.

Сейчас работает так: 2 транзакции запускаются и выбирают одну и ту же запись пользователя для UPDATE, одна из этих транзакций блокирует её, вторая транзакций видит это и ждет когда первая транзакция завершится. В итоге первая транзакция изменяет баланс пользователю со 100 на 200 и завершается удачно. Вторая транзакция видит что первая завершилась и начинает тоже изменять эту запись, но она работает с балансом 100, а не 200, то есть она видит старый баланс у пользователя, а не новый.

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

Вижу конечно пару вариантов костылей как это можно обойти, блокировать запись не с которую изменяют, а какую-то другую, возможно вышестоящую над пользователем. И ещё вариант вижу заново получать данные и перепроверять действительно ли результат верный, эти оба варианта не очень.
  • Вопрос задан
  • 248 просмотров
Подписаться 1 Простой Комментировать
Помогут разобраться в теме Все курсы
  • Нетология
    DevOps-инженер с нуля
    15 месяцев
    Далее
  • Академия Эдюсон
    Python-разработчик
    9 месяцев
    Далее
  • ProductStar × РБК
    Профессия: Инженер по информационной безопасности
    9 месяцев
    Далее
Решения вопроса 2
opium
@opium
Просто люблю качественно работать
уровни изоляции тут как раз при чём, это классический lost update)) ты SELECT-ом читаешь баланс, потом UPDATE-ом пишешь то что прочитал, вторая транзакция конечно работает со старым значением. Самый простой фикс:
UPDATE users SET balance = balance + 100 WHERE id = 1
, без промежуточного SELECT. А если между чтением и записью нужна логика, используй SELECT ... FOR UPDATE — он заблокирует строку и вторая транзакция при разблокировке перечитает актуальное.
Ответ написан
Daemon23RUS
@Daemon23RUS
Если ты просто увеличиваешь баланс, можно обойтись без чтения:
UPDATE users SET balance = balance + 50 WHERE id = 1;

Это атомарно, не требует блокировок в приложении, и не страдает от lost update.

Но если логику select'а не обойти
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
SELECT balance FROM users WHERE id = 1 FOR UPDATE;
-- вернёт актуальное значение после ожидания
UPDATE users SET balance = ...;
COMMIT;
Ответ написан
Пригласить эксперта
Ответы на вопрос 1
@v__V__v
Разработчик
есть момент, что транзакции начинаются вот так
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
START TRANSACTION

szQocks, у вас же вложенные транзакции, конечно они обе получат один снапшот и будут работать с одними и теми же данными. Не знаю, зачем вам это понадобилось, но просто вытащите вложенную транзакцию наружу и все будет отлично, тем более, что если это происходит в одном участке кода, то конкуренцию запросы друг другу не составят при любом развитии событий, это же один поток, он выполнится последовательно. Т.е. в данном случае даже просто предложенное выше UPDATE users SET balance = balance + 100 WHERE id = 1, выполненное одно за другим сколько угодно раз без транзакций сработает так, как нужно. При отсутствии параллельных обращений, конечно. Транзакции и нужны только чтоб работающие параллельно потоки/процессы не ломились наперегонки в БД, а чинно ждали тех, кто пришел раньше.
Ответ написан
Ваш ответ на вопрос

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

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