Транзакции базы данных позволяют выполнять сложный набор операций записи, не нарушая согласованности данных. Вообще говоря, транзакция блокирует все строки, затронутые ее операциями, поэтому они не могут быть изменены другими транзакциями или атомарными операциями. Он действует как мьютекс для определенного подмножества данных в таблице. Но транзакции работают по-разному в зависимости от уровня изоляции. Иногда необходимо повторить транзакции из-за временных ошибок, вызванных одновременным доступом.

Моделирование случая

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

А вот инструкции SQL для создания нашей базы данных:

CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

CREATE TABLE IF NOT EXISTS users (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    username VARCHAR NOT NULL UNIQUE,
    email VARCHAR NOT NULL UNIQUE,
    registered TIMESTAMP NOT NULL
);

CREATE TABLE IF NOT EXISTS accounts (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    user_id UUID REFERENCES users ON DELETE SET NULL,
    amount DECIMAL NOT NULL,
    currency VARCHAR(3) NOT NULL
);

CREATE TABLE IF NOT EXISTS spendings (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    sender_id UUID REFERENCES accounts(id) ON DELETE SET NULL,
    receiver_id UUID REFERENCES accounts(id) ON DELETE SET NULL,
    amount DECIMAL NOT NULL
);

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

-- insert user
INSERT INTO users (username, email, registered)
VALUES ($1, $2, $3) RETURNING *;

-- insert account
INSERT INTO accounts (user_id, amount, currency)
VALUES ($1, $2, $3) RETURNING *;

-- insert spending
INSERT INTO spendings (sender_id, receiver_id, amount)
VALUES ($1, $2, $3) RETURNING *;

-- get account
SELECT *
FROM accounts
WHERE id = $1
FOR UPDATE;

-- account edit balance
UPDATE accounts
SET amount = $2
WHERE id = $1
RETURNING *;

Теперь заполним базу данными:

Выполнение действий

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

Как видите, строка time.Sleep(time.Minute) закомментирована. Если мы раскомментируем эту строку, эта транзакция займет больше времени для завершения и, таким образом, увеличит временной интервал блокировки строк данных. Итак, давайте раскомментируем эту строку, запустим код, затем снова закомментируем и снова запустим. Будет 2 одновременных транзакции с повторяющимся уровнем изоляции чтения. Тот, который был запущен раньше, завершится корректно, но другой будет отменен и откатится из-за одновременного чтения.

panic: pq: could not serialize access due to concurrent update

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

Повтор транзакций

Вот метод:

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

Теперь все работает нормально.