Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
ACID и транзакции в БД
ACID — это четыре фундаментальных свойства, которые гарантируют надёжность и согласованность данных в транзакциях базы данных.
Что такое ACID
A — Atomicity (Атомарность)
C — Consistency (Согласованность)
I — Isolation (Изоляция)
D — Durability (Надёжность)
1. Atomicity (Атомарность)
Определение: Транзакция выполняется полностью или вообще не выполняется (all-or-nothing).
Если что-то пошло не так, все изменения откатываются.
Пример проблемы без Atomicity:
Перевод денег с счета A на счет B:
1. Снять 100 со счёта A <- успешно
2. Добавить 100 на счет B <- ОШИБКА СЕТИ!
Результат: деньги потеряны!
С Atomicity:
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 'A';
UPDATE accounts SET balance = balance + 100 WHERE account_id = 'B';
COMMIT; -- оба изменения применены
-- ИЛИ если ошибка:
ROLLBACK; -- оба изменения откатаны, как если бы транзакции не было
В Java (Spring):
@Transactional
public void transferMoney(String fromAccount, String toAccount, BigDecimal amount) {
accountService.withdraw(fromAccount, amount); // Операция 1
accountService.deposit(toAccount, amount); // Операция 2
// Если любая операция выбросит исключение,
// обе будут откачены (ROLLBACK)
}
2. Consistency (Согласованность)
Определение: Транзакция переводит БД из одного согласованного состояния в другое.
Все бизнес-правила и ограничения остаются в силе.
Пример проблемы без Consistency:
Ограничение: total_income >= 0 (неотрицательный доход)
Транзакция без контроля:
UPDATE users SET total_income = -100 WHERE user_id = 1;
Результат: нарушено правило!
С Consistency:
-- БД имеет constraint (ограничение)
ALTER TABLE users ADD CONSTRAINT check_income CHECK (total_income >= 0);
-- Попытка нарушить constraint не пройдёт
UPDATE users SET total_income = -100 WHERE user_id = 1; -- ОШИБКА!
-- Транзакция откатывается, состояние остаётся согласованным
В Java (бизнес-логика):
@Transactional
public void setUserIncome(Long userId, BigDecimal income) {
if (income.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("Доход не может быть отрицательным");
}
User user = userRepository.findById(userId).orElseThrow();
user.setTotalIncome(income);
userRepository.save(user);
// Если исключение, транзакция откатывается
// БД остаётся в согласованном состоянии
}
3. Isolation (Изоляция)
Определение: Параллельные транзакции не влияют друг на друга до момента коммита.
Каждая транзакция видит консистентный снимок данных.
Пример проблемы без Isolation (Dirty Read):
Транзакция 1: Транзакция 2:
1. Прочитать balance = 100
2. Прочитать balance = 100
3. balance -= 50
4. Написать balance = 50
5. Прочитать balance = 50 (ГРЯЗНОЕ ЧТЕНИЕ!)
6. balance += 25
7. Написать balance = 75
8. ROLLBACK (ошибка)
Результат: Tx2 работала с откатанными данными!
С Isolation (Serializable):
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
Транзакция 1: Транзакция 2:
1. BEGIN
2. SELECT balance (lock)
3. balance -= 50
4. WRITE balance = 50
5. COMMIT (unlock)
6. BEGIN
7. SELECT balance = 50 (не грязное!)
8. COMMIT
Уровни изоляции в Java:
// Spring JPA
@Transactional(isolation = Isolation.SERIALIZABLE)
public void criticalOperation() {
// Максимальная защита, но медлено
}
@Transactional(isolation = Isolation.READ_COMMITTED)
public void normalOperation() {
// Стандартный уровень (по умолчанию)
}
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void readOnlyFast() {
// Быстро, но могут быть грязные чтения
}
Проблемы Isolation без контроля:
1. Dirty Read — чтение некоммиченых данных
2. Non-Repeatable Read — одно значение читается по-разному
3. Phantom Read — появляются новые строки между чтениями
Пример Non-Repeatable Read:
Транзакция 1: Транзакция 2:
1. SELECT price FROM items
WHERE id=5 -> 100
2. UPDATE items SET price = 150 WHERE id=5
3. COMMIT
4. SELECT price FROM items
WHERE id=5 -> 150 (ДРУГОЕ значение!)
5. COMMIT
4. Durability (Надёжность)
Определение: Один раз коммиченые данные остаются в БД даже при сбоях (отключение питания, крах).
Пример проблемы без Durability:
1. Записали данные в оперативную память
2. COMMIT
3. Отключилось питание
Результат: данные потеряны!
С Durability:
1. Записали данные в WAL (Write-Ahead Log) на диск
2. Записали в оперативную память
3. COMMIT (гарантировано на диске)
4. Отключилось питание
5. При перезагрузке БД восстанавливает данные из WAL
Результат: данные сохранены!
В PostgreSQL:
BEGIN TRANSACTION;
INSERT INTO transactions VALUES (1, 'payment', 100);
COMMIT; -- Данные синхронно записаны на диск
-- Даже если сервер упадёт через 1 микросекунду,
-- данные будут восстановлены
Как ACID работает вместе
Пример: транзакция перевода денег
@Transactional
public void transferMoney(String from, String to, BigDecimal amount)
throws InsufficientFundsException {
// ATOMICITY: либо всё, либо ничего
User fromUser = userRepository.findById(from);
// CONSISTENCY: проверяем бизнес-правила
if (fromUser.getBalance().compareTo(amount) < 0) {
throw new InsufficientFundsException("Недостаточно средств");
}
// ISOLATION: блокировка для других транзакций
fromUser.setBalance(fromUser.getBalance().subtract(amount));
User toUser = userRepository.findById(to);
toUser.setBalance(toUser.getBalance().add(amount));
userRepository.save(fromUser);
userRepository.save(toUser);
// DURABILITY: после COMMIT данные на диске
}
Что происходит внутри:
- ATOMICITY — если исключение на userRepository.save(toUser), обе операции откатываются
- CONSISTENCY — проверка баланса защищает инвариант
- ISOLATION — другие транзакции не видят промежуточное состояние
- DURABILITY — после COMMIT метода, данные гарантированно на диске
Производительность vs ACID
Trade-off между безопасностью и скоростью:
// БЕЗОПАСНО, НО МЕДЛЕННО
@Transactional(isolation = Isolation.SERIALIZABLE)
public void criticalBankOp() { /* ... */ }
// БЫСТРО, НО МЕНЕЕ БЕЗОПАСНО
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void statisticsReport() { /* ... */ }
// БАЛАНС (рекомендуется)
@Transactional(isolation = Isolation.READ_COMMITTED)
public void normalOperation() { /* ... */ }
Практический пример
@Service
public class OrderService {
@Transactional
public void placeOrder(Long userId, Long productId, int quantity) {
// ATOMICITY: все шаги или ничего
User user = userRepository.findByIdForUpdate(userId);
// CONSISTENCY: проверяем правила
if (user.getBalance() < getPrice(productId) * quantity) {
throw new PaymentFailedException("Недостаточно средств");
}
Product product = productRepository.findByIdForUpdate(productId);
if (product.getStock() < quantity) {
throw new OutOfStockException("Товара нет в наличии");
}
// ISOLATION: блокировка строк
Order order = new Order(user, product, quantity);
orderRepository.save(order);
user.setBalance(user.getBalance() - calculateTotal(product, quantity));
product.setStock(product.getStock() - quantity);
userRepository.save(user);
productRepository.save(product);
// DURABILITY: данные на диске после метода
}
}
Итог
- Atomicity — транзакция целиком или ничего
- Consistency — БД остаётся в согласованном состоянии
- Isolation — транзакции не влияют друг на друга
- Durability — коммиченые данные сохраняются
Bez ACID могут быть потеря денег, дублирование, инкосинстентность. ACID — основа надёжных систем, особенно финансовых.