Транзакция — группа SQL-операций, которые выполняются как единое целое. Это ключевой механизм обеспечения целостности данных.
Основной синтаксис
BEGIN / START TRANSACTION
-- Both are equivalent in PostgreSQL
BEGIN;
-- or
START TRANSACTION;
-- With isolation level
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- With access mode
BEGIN READ ONLY; -- no writes allowed
BEGIN READ WRITE; -- default
COMMIT
Завершает транзакцию, сохраняя все изменения.
BEGIN;
INSERT INTO users (email, name) VALUES ('[email protected]', 'Иван');
UPDATE accounts SET balance = balance + 1000 WHERE user_id = 42;
COMMIT;
-- Changes are now permanent and visible to other transactions
ROLLBACK
Откатывает все изменения транзакции.
BEGIN;
DELETE FROM important_data WHERE created_at < '2020-01-01';
-- Oops, wrong WHERE clause
ROLLBACK;
-- All deletes cancelled, data restored
Autocommit
По умолчанию в большинстве СУБД и клиентских библиотек включён autocommit — каждая инструкция автоматически оборачивается в транзакцию.
-- With autocommit ON (default):
UPDATE users SET name = 'Test'; -- automatically: BEGIN; UPDATE...; COMMIT;
DELETE FROM logs WHERE old = true; -- automatically: BEGIN; DELETE...; COMMIT;
-- To use explicit transaction:
BEGIN;
UPDATE users SET name = 'Test';
DELETE FROM logs WHERE old = true;
COMMIT;
Autocommit в драйверах
// PHP PDO — autocommit is ON by default
$pdo = new PDO($dsn, $user, $pass);
// Manual transaction
$pdo->beginTransaction();
try {
$pdo->exec("UPDATE accounts SET balance = balance - 100 WHERE id = 1");
$pdo->exec("UPDATE accounts SET balance = balance + 100 WHERE id = 2");
$pdo->commit();
} catch (\Exception $e) {
$pdo->rollBack();
throw $e;
}
// Go database/sql
tx, err := db.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelSerializable,
ReadOnly: false,
})
if err != nil {
return err
}
defer func() {
if p := recover(); p != nil {
tx.Rollback()
panic(p)
}
}()
_, err = tx.ExecContext(ctx, "UPDATE accounts SET balance = balance - $1 WHERE id = $2", amount, fromID)
if err != nil {
tx.Rollback()
return err
}
_, err = tx.ExecContext(ctx, "UPDATE accounts SET balance = balance + $1 WHERE id = $2", amount, toID)
if err != nil {
tx.Rollback()
return err
}
return tx.Commit()
SAVEPOINT — точки сохранения
Savepoint позволяет откатить часть транзакции, не отменяя всю транзакцию.
Синтаксис
SAVEPOINT savepoint_name;
ROLLBACK TO SAVEPOINT savepoint_name;
RELEASE SAVEPOINT savepoint_name; -- removes savepoint (keeps changes)
Практический пример
BEGIN;
INSERT INTO orders (user_id, total) VALUES (42, 5000);
-- order_id = 1 (generated)
SAVEPOINT after_order;
INSERT INTO payments (order_id, method) VALUES (1, 'card');
-- If payment processing fails:
ROLLBACK TO SAVEPOINT after_order;
-- order is still here, payment insert is rolled back
INSERT INTO payments (order_id, method) VALUES (1, 'cash');
-- Trying alternative payment method
COMMIT; -- only if everything went well
Вложенные savepoints
BEGIN;
INSERT INTO log (msg) VALUES ('start');
SAVEPOINT sp1;
INSERT INTO temp_data VALUES (1);
SAVEPOINT sp2;
INSERT INTO temp_data VALUES (2);
ROLLBACK TO SAVEPOINT sp2; -- removes temp_data(2), sp1 still active
INSERT INTO temp_data VALUES (3);
ROLLBACK TO SAVEPOINT sp1; -- removes temp_data(1) and temp_data(3)
INSERT INTO log (msg) VALUES ('end');
COMMIT; -- only log rows committed
Savepoints в ОРМ-фреймворках
// Doctrine ORM — uses savepoints for nested transactions
$em->beginTransaction();
try {
$em->beginTransaction(); // creates savepoint internally
try {
$em->persist($order);
$em->flush();
$em->commit(); // releases savepoint
} catch (\Exception $e) {
$em->rollback(); // rollback to savepoint
}
$em->commit(); // real commit
} catch (\Exception $e) {
$em->rollback(); // real rollback
}
Вложенные транзакции (Nested Transactions)
Большинство СУБД не поддерживают истинно вложенные транзакции — только через savepoints.
BEGIN;
-- This is the outer transaction
BEGIN; -- PostgreSQL: WARNING: there is already a transaction in progress
-- Inner BEGIN is ignored, still in the outer transaction
COMMIT;
Совет: если нужны вложенные транзакции в приложении — используйте явные savepoints или механизм, предоставляемый ORM.
Транзакции и DDL
В PostgreSQL DDL (CREATE TABLE, ALTER TABLE) транзакционны — их можно откатить.
BEGIN;
CREATE TABLE test_migration (id SERIAL PRIMARY KEY);
ALTER TABLE users ADD COLUMN last_login TIMESTAMPTZ;
-- If something goes wrong:
ROLLBACK;
-- Both CREATE TABLE and ALTER TABLE are rolled back
MySQL не поддерживает транзакционный DDL — ALTER TABLE неявно делает COMMIT.
Долгие транзакции — антипаттерн
Проблемы долгих транзакций
BEGIN;
SELECT * FROM large_report; -- 30 seconds query
-- During this time:
-- - Rows visible to this transaction cannot be vacuumed
-- - Locks may block other transactions
-- - WAL cannot be truncated
UPDATE settings SET value = 'x' WHERE key = 'y';
COMMIT;
Последствия:
- Table bloat — VACUUM не может убрать мёртвые версии строк
- Lock contention — удерживаемые блокировки тормозят других
- WAL накопление — журнал растёт, replikation lag увеличивается
Как обнаружить долгие транзакции
-- Show transactions running for more than 5 minutes
SELECT pid, now() - pg_stat_activity.query_start AS duration, query, state
FROM pg_stat_activity
WHERE state != 'idle'
AND query_start < now() - interval '5 minutes'
ORDER BY duration DESC;
Паттерн: батчевая обработка
-- WRONG: one huge transaction
BEGIN;
UPDATE orders SET status = 'archived' WHERE created_at < '2020-01-01';
-- 10 million rows — transaction open for minutes
COMMIT;
-- CORRECT: small batches
DO $$
DECLARE
batch_size INT := 1000;
rows_updated INT;
BEGIN
LOOP
UPDATE orders SET status = 'archived'
WHERE id IN (
SELECT id FROM orders
WHERE created_at < '2020-01-01' AND status != 'archived'
LIMIT batch_size
);
GET DIAGNOSTICS rows_updated = ROW_COUNT;
EXIT WHEN rows_updated = 0;
PERFORM pg_sleep(0.1); -- small pause between batches
END LOOP;
END $$;
Обработка ошибок и retry
Retry-логика для SERIALIZABLE
// Retry on serialization error
func withRetry(ctx context.Context, db *sql.DB, fn func(*sql.Tx) error) error {
const maxRetries = 3
for attempt := 0; attempt < maxRetries; attempt++ {
tx, err := db.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelSerializable,
})
if err != nil {
return err
}
err = fn(tx)
if err != nil {
tx.Rollback()
// Check if it's a serialization error (PostgreSQL error code 40001)
if isSerializationError(err) && attempt < maxRetries-1 {
continue // retry
}
return err
}
if err = tx.Commit(); err != nil {
if isSerializationError(err) && attempt < maxRetries-1 {
continue // retry
}
return err
}
return nil
}
return fmt.Errorf("max retries exceeded")
}
Транзакции в распределённых системах
Одна транзакция в нескольких микросервисах — сложная задача. Решения:
2PC (Two-Phase Commit)
Phase 1 (Prepare):
Coordinator → Service A: "Prepare to commit"
Coordinator → Service B: "Prepare to commit"
A, B: "Ready"
Phase 2 (Commit):
Coordinator → A, B: "Commit"
A, B: "Done"
Проблема: coordinator failure между фазами — система зависает.
Saga Pattern
Последовательность локальных транзакций с компенсирующими действиями при отказе.
OrderService: CreateOrder → emit OrderCreated
PaymentService: ProcessPayment → emit PaymentProcessed
(if fails) → emit PaymentFailed → compensate: CancelOrder
InventoryService: ReserveItem → emit ItemReserved
(if fails) → emit ReservationFailed → compensate: RefundPayment + CancelOrder
Saga не гарантирует изоляцию — промежуточные состояния видны другим сервисам.
Типичные вопросы на интервью
Q: «Что произойдёт, если не вызвать COMMIT?»
Транзакция остаётся открытой до закрытия соединения (тогда автоматический ROLLBACK) или истечения таймаута (lock_timeout, idle_in_transaction_session_timeout).
Q: «Можно ли сделать DDL транзакционным в MySQL?»
Нет. В MySQL ALTER TABLE неявно делает COMMIT перед выполнением и после. Это ключевое отличие от PostgreSQL.
Q: «Зачем нужен RELEASE SAVEPOINT?»
Освобождает память, занятую savepoint. После RELEASE savepoint нельзя использовать для ROLLBACK, но все изменения сохраняются. Полезно для оптимизации при большом числе savepoints.