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

Для чего нужно пользоваться ACID-принципами в транзакциях?

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

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

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

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

# ACID-принципы в транзакциях

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

Что такое ACID?

ACID — это аббревиатура:

  • Atomicity (Атомарность)
  • Consistency (Консистентность)
  • Isolation (Изоляция)
  • Durability (Долговечность)

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

Транзакция выполняется либо полностью, либо не выполняется вообще. Нет промежуточных состояний.

Пример без атомарности (ПЛОХО):

// Без транзакции - опасно!
public void transferMoney(Account from, Account to, BigDecimal amount) {
    from.setBalance(from.getBalance().subtract(amount));  // Деньги списаны
    // Программа упала здесь!
    to.setBalance(to.getBalance().add(amount));            // Деньги не добавились
    // Результат: деньги потеряны!
}

Пример с атомарностью (ХОРОШО):

@Service
public class BankTransferService {
    
    @Transactional  // Spring гарантирует atomicity
    public void transferMoney(Account from, Account to, BigDecimal amount) {
        from.setBalance(from.getBalance().subtract(amount));
        to.setBalance(to.getBalance().add(amount));
        accountRepository.save(from);
        accountRepository.save(to);
        // Если случится ошибка - всё откатится (rollback)
    }
}

Почему это важно:

  • Гарантирует, что деньги не потеряются
  • Если операция не завершилась — всё вернётся в исходное состояние
  • Невозможны частичные обновления

2. Consistency (Консистентность)

База данных переходит из одного консистентного состояния в другое. Все правила и ограничения соблюдаются.

Пример нарушения консистентности:

-- Правило: сумма всех счетов должна быть постоянной

-- НАЧАЛЬНОЕ СОСТОЯНИЕ:
-- Счет A: 1000
-- Счет B: 2000
-- СУММА: 3000 ✓

-- ПЕРЕВОД 500 со счета A на счет B

-- КОНЕЧНОЕ СОСТОЯНИЕ (без консистентности):
-- Счет A: 500  (1000 - 500)
-- Счет B: 2700 (2000 + 200) -- ОШИБКА! добавлено 200 вместо 500
-- СУММА: 3200 ✗ НАРУШЕНА КОНСИСТЕНТНОСТЬ

Пример с консистентностью (ХОРОШО):

@Entity
public class Account {
    @Id
    private Long id;
    private BigDecimal balance;
    
    // Ограничение целостности: баланс не может быть отрицательным
    @NotNull
    @Min(value = 0)
    private BigDecimal balance;
}

@Service
public class BankService {
    
    @Transactional
    public void transferMoney(Account from, Account to, BigDecimal amount) {
        // Проверяем консистентность
        if (from.getBalance().compareTo(amount) < 0) {
            throw new InsufficientFundsException();
        }
        
        from.setBalance(from.getBalance().subtract(amount));
        to.setBalance(to.getBalance().add(amount));
        
        // База данных проверит все ограничения
        accountRepository.saveAll(Arrays.asList(from, to));
    }
}

Почему это важно:

  • Данные всегда остаются в корректном состоянии
  • Бизнес-правила не нарушаются
  • Можно доверять данным в базе

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

Одновременные транзакции не интерферируют друг с другом. Каждая работает как будто она одна.

Проблема без изоляции (Dirty Read):

// ТРАНЗАКЦИЯ 1
Transaction tx1 = connection.beginTransaction();
Account account = accountRepository.findById(1L);  // balance = 1000
account.setBalance(500);
accountRepository.save(account);
// Транзакция ещё не commit-нулась

// ТРАНЗАКЦИЯ 2 (в это время)
Transaction tx2 = connection.beginTransaction();
Account account2 = accountRepository.findById(1L);  // Видит 500 (незакончённые изменения!)
Double total = account2.getBalance();  // 500 - ГРЯЗНОЕ ЧТЕНИЕ!
// Если TX1 откатится, total будет неправильным

Пример с изоляцией (ХОРОШО):

@Service
public class IsolatedTransactionService {
    
    // SERIALIZABLE - самый высокий уровень изоляции
    @Transactional(isolation = Isolation.SERIALIZABLE)
    public void updateAccountBalance() {
        Account account = accountRepository.findById(1L);
        // Другие транзакции НЕ ВИДЯТ эти изменения
        account.setBalance(500);
        accountRepository.save(account);
        // После commit другие транзакции увидят 500
    }
}

Уровни изоляции в SQL:

  1. READ UNCOMMITTED - низший уровень, опасен
  2. READ COMMITTED - стандарт, защищает от Dirty Reads
  3. REPEATABLE READ - защищает от Phantom Reads
  4. SERIALIZABLE - высший уровень, полная изоляция
@Transactional(isolation = Isolation.READ_COMMITTED)  // По умолчанию
public void process() { ... }

Почему это важно:

  • Никакая транзакция не видит незавершённые изменения других
  • Избегаются race conditions
  • Данные остаются консистентными при параллельном доступе

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

Если транзакция завершилась (commit), данные сохранены навсегда, даже при сбое системы.

Пример без долговечности (ПЛОХО):

// Данные хранятся только в памяти
Map<Integer, BigDecimal> accounts = new HashMap<>();
accounts.put(1, BigDecimal.valueOf(1000));
// Программа упала -> все данные потеряны!

Пример с долговечностью (ХОРОШО):

@Service
public class DurableService {
    
    @Transactional  // Данные записываются на диск
    public void saveTransaction(Transaction transaction) {
        transactionRepository.save(transaction);
        // Сразу после commit данные на диске
        // Даже если сервер упадёт - данные восстановятся
    }
}

Как это работает:

  • СУБД пишет данные на постоянное хранилище (диск)
  • Использует WAL (Write-Ahead Logging)
  • При сбое восстанавливает состояние из логов

Почему это важно:

  • Данные не потеряются при сбое
  • Можно полагаться на вернувшихся в БД
  • Критично для финансовых и критичных систем

Полный пример: ACID в действии

@Service
@RequiredArgsConstructor
public class PaymentService {
    private final OrderRepository orderRepository;
    private final PaymentRepository paymentRepository;
    private final AccountRepository accountRepository;
    
    @Transactional(isolation = Isolation.SERIALIZABLE)
    public void processPayment(Order order, Payment payment) {
        // ATOMICITY: либо все операции выполнятся, либо ни одна
        
        // CONSISTENCY: проверяем бизнес-правила
        if (payment.getAmount().compareTo(order.getTotalAmount()) != 0) {
            throw new PaymentMismatchException();
        }
        
        // ISOLATION: другие транзакции не видят промежуточное состояние
        order.setStatus(OrderStatus.PAID);
        payment.setStatus(PaymentStatus.PROCESSING);
        
        orderRepository.save(order);
        paymentRepository.save(payment);
        
        Account account = accountRepository.findById(payment.getAccountId());
        account.setBalance(account.getBalance().add(payment.getAmount()));
        accountRepository.save(account);
        
        // DURABILITY: все данные на диске после commit
    }
}

Почему ACID критичен?

  1. Финансовые системы - каждый рубль должен быть учтён
  2. E-commerce - заказы и платежи должны быть синхронизированы
  3. Критичные данные - потеря данных недопустима
  4. Параллельный доступ - безопасность при одновременных операциях

Заключение

ACID-принципы — это фундамент надёжных систем. Они гарантируют, что:

  • Данные никогда не потеряются (Atomicity + Durability)
  • Данные всегда корректны (Consistency)
  • Параллельный доступ безопасен (Isolation)

В Java мы опираемся на СУБД (PostgreSQL, MySQL) для гарантий ACID и используем фреймворки вроде Spring @Transactional для управления транзакциями.