← Назад к вопросам

Почему важно поддерживать транзакцию в БД?

2.0 Middle🔥 181 комментариев
#Базы данных и SQL

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Почему важно поддерживать транзакцию в БД

Транзакции — фундамент надёжной работы с данными. Они обеспечивают ACID свойства, без которых данные теряются и приложение ломается.

Что такое транзакция

Транзакция — это атомарная последовательность операций с БД, которая либо полностью выполняется, либо полностью откатывается (rollback). Никаких частичных состояний.

// Пример: перевод денег между счётами
public void transferMoney(Account from, Account to, BigDecimal amount) 
        throws SQLException {
    try (Connection conn = dataSource.getConnection()) {
        conn.setAutoCommit(false);  // Начало транзакции
        
        try {
            // Операция 1: списать со счёта
            debit(conn, from.getId(), amount);
            
            // Операция 2: положить на счёт
            credit(conn, to.getId(), amount);
            
            conn.commit();  // Всё прошло успешно
        } catch (Exception e) {
            conn.rollback();  // Откат при ошибке
            throw e;
        }
    }
}

ACID свойства

1. Atomicity (Атомарность)

Суть: Транзакция либо целиком выполнится, либо целиком откатится. Частичные результаты невозможны.

// Без транзакции (БЕЗ Atomicity):
BigDecimal balance1 = account1.getBalance();  // Было: 100
BigDecimal balance2 = account2.getBalance();  // Было: 0

// Списание
SQL: UPDATE accounts SET balance = 50 WHERE id = 1
// Успешно: account1 теперь 50

// ⚠️ ОШИБКА ДО ЗАЧИСЛЕНИЯ!
System.out.println(1 / 0);

// Зачисление (не выполнилось)
SQL: UPDATE accounts SET balance = 50 WHERE id = 2

// ИТОГ: money = 50 МБ, потеря 50 руб!
// С транзакцией (Atomicity):
try {
    conn.setAutoCommit(false);
    SQL: UPDATE accounts SET balance = 50 WHERE id = 1
    System.out.println(1 / 0);  // ОШИБКА!
    SQL: UPDATE accounts SET balance = 50 WHERE id = 2
    conn.commit();
} catch (Exception e) {
    conn.rollback();  // Откат обеих операций!
}

// ИТОГ: account1 = 100, account2 = 0 (как было)

2. Consistency (Непротиворечивость)

Суть: БД переходит из одного непротиворечивого состояния в другое. Бизнес-правила соблюдаются.

// Инвариант: сумма всех счётов = const
Было: account1 = 100, account2 = 0, total = 100
После transfer(100): account1 = 0, account2 = 100, total = 100// Без защиты:
// account1 = 0 (списано)
// CRASH! account2 не обновилась
// total = 0 (нарушено!)

Как БД обеспечивает Consistency:

  • Constraints (NOT NULL, UNIQUE, FK)
  • CHECK constraints
  • Triggers
  • Rollback при нарушении правил
CREATE TABLE accounts (
    id INT PRIMARY KEY,
    balance DECIMAL(10,2),
    CHECK (balance >= 0)  -- Баланс не может быть отрицательным
);

-- Это предотвратит:
UPDATE accounts SET balance = -50 WHERE id = 1;  -- ОШИБКА!

3. Isolation (Изоляция)

Суть: Параллельные транзакции не влияют друг на друга. Каждая видит непротиворечивое состояние.

// Сценарий: две параллельные транзакции

// Транзакция A (начало)
SELECT balance FROM accounts WHERE id = 1;  // Видит 100
BEGIN

// Транзакция B (начало)
SELECT balance FROM accounts WHERE id = 1;  // Видит 100
BEGIN

// Транзакция A
UPDATE accounts SET balance = 50 WHERE id = 1;
COMMIT;  // Теперь в БД 50

// Транзакция B всё ещё видит 100 (Isolation)
UPDATE accounts SET balance = 75 WHERE id = 1;
COMMIT;  // Может возникнуть конфликт

Уровни изоляции (от слабого к сильному):

1. READ_UNCOMMITTED
   - Видны даже неподтверждённые изменения (Dirty read)
   - Опасно! Может прочитать данные, которые откатятся

2. READ_COMMITTED (по умолчанию в PostgreSQL)
   - Видны только подтверждённые данные
   - Защита от dirty read

3. REPEATABLE_READ
   - Один SELECT в транзакции всегда вернёт одно значение
   - Защита от non-repeatable read

4. SERIALIZABLE (по умолчанию в SQLite)
   - Максимальная изоляция
   - Транзакции выполняются поочередно
// Java: указание уровня изоляции
conn.setTransactionIsolation(
    Connection.TRANSACTION_REPEATABLE_READ
);

// Spring: через аннотацию
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void transferMoney(Account from, Account to, BigDecimal amount) {
    // ...
}

4. Durability (Долговечность)

Суть: Когда транзакция завершена (commit), данные сохранены и не потеряются при сбое.

conn.commit();  // Сигнал: "запишите на диск!"

// После этого, даже если:
// - Сервер упадёт
// - Отключится электричество
// - Будет техническое обслуживание
//
// Данные уже в БД и не потеряются

Реализация:

  • WAL (Write-Ahead Logging) — логируется ДО записи
  • fsync() — синхронная запись на диск
  • Репликация на несколько серверов

Проблемы без транзакций

1. Потеря данных (Lost Update)

// Два процесса меняют одно значение

// Процесс A
Int counter = db.getCounter();      // 10
counter++;                           // 11
db.update(counter);                  // UPDATE counter = 11

// Процесс B (одновременно)
Int counter = db.getCounter();      // 10 (видит старое)
counter++;                           // 11
db.update(counter);                  // UPDATE counter = 11

// ПРОБЛЕМА: должно быть 12, а стало 11
// Одно увеличение потеряно!

Решение: SELECT FOR UPDATE (пессимистичная блокировка)

BEGIN TRANSACTION;
SELECT counter FROM stats FOR UPDATE;  -- Блокируем строку
UPDATE stats SET counter = counter + 1;
COMMIT;

2. Грязное чтение (Dirty Read)

// Транзакция A начала, но не закончила
UPDATE account SET balance = 50;

// Транзакция B прочитала незавершённые данные
SELECT balance FROM account;  // Видит 50

// Транзакция A откатилась
ROLLBACK;  // balance вернулся в 100

// У Транзакции B неверные данные!

3. Фантомное чтение (Phantom Read)

// Транзакция A
SELECT COUNT(*) FROM orders;  // Видит 100

// Транзакция B (параллельно)
INSERT INTO orders VALUES (...);  // Добавила новую
COMMIT;

// Транзакция A
SELECT COUNT(*) FROM orders;  // Теперь видит 101!
// Один и тот же запрос дал разные результаты

Spring и транзакции

@Service
public class OrderService {
    
    @Transactional
    public Order createOrder(CreateOrderRequest req) {
        // Автоматический BEGIN
        Order order = new Order(req);
        orderRepository.save(order);
        
        // Уменьшение stocks
        for (OrderItem item : req.getItems()) {
            Product product = productRepository.findById(item.getProductId());
            product.decreaseStock(item.getQuantity());
            productRepository.save(product);
        }
        
        // Автоматический COMMIT если нет ошибок
        return order;
        // Автоматический ROLLBACK если исключение
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void sendNotification(Order order) {
        // Новая независимая транзакция
    }
}

Правила хорошей практики

  1. Держи транзакцию коротко: только необходимые операции
  2. Не делай I/O в транзакции: API вызовы, file upload
  3. Обрабатывай исключения правильно: не ловади все Exception
  4. Используй оптимистичные блокировки где возможно (version field)
  5. Мониторь deadlocks: логируй и обрабатывай retry

Итого

Транзакции нужны для:

  1. Atomicity — всё или ничего
  2. Consistency — непротиворечивость данных
  3. Isolation — независимость параллельных операций
  4. Durability — надёжность и восстанавливаемость

Без них: потеря денег, дублирование данных, race conditions, крахи. С ними: надёжная система.

Почему важно поддерживать транзакцию в БД? | PrepBro