Почему важно поддерживать транзакцию в БД?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Почему важно поддерживать транзакцию в БД
Транзакции — фундамент надёжной работы с данными. Они обеспечивают 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) {
// Новая независимая транзакция
}
}
Правила хорошей практики
- Держи транзакцию коротко: только необходимые операции
- Не делай I/O в транзакции: API вызовы, file upload
- Обрабатывай исключения правильно: не ловади все Exception
- Используй оптимистичные блокировки где возможно (version field)
- Мониторь deadlocks: логируй и обрабатывай retry
Итого
Транзакции нужны для:
- Atomicity — всё или ничего
- Consistency — непротиворечивость данных
- Isolation — независимость параллельных операций
- Durability — надёжность и восстанавливаемость
Без них: потеря денег, дублирование данных, race conditions, крахи. С ними: надёжная система.