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

Приведи пример изолированности одной транзакции от другой

2.3 Middle🔥 181 комментариев
#Другое

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

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

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

Изолированность транзакций (Isolation)

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

Проблемы при отсутствии изоляции

Без изоляции могут возникать три основные проблемы:

  1. Грязное чтение (Dirty Read) — чтение незафиксированных данных
  2. Неповторяемое чтение (Non-Repeatable Read) — изменение данных между чтениями
  3. Фантомное чтение (Phantom Read) — появление новых строк в результате запроса

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

SQL определяет четыре уровня изоляции:

// 1. READ_UNCOMMITTED — минимальная защита
// Допускает грязное чтение, неповторяемое чтение, фантомное чтение
Connection conn = dataSource.getConnection();
conn.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);

// 2. READ_COMMITTED — средняя защита (по умолчанию в PostgreSQL)
// Запрещает грязное чтение, но допускает неповторяемое и фантомное

// 3. REPEATABLE_READ — высокая защита (по умолчанию в MySQL)
// Запрещает грязное и неповторяемое чтение, но допускает фантомное

// 4. SERIALIZABLE — максимальная защита
// Запрещает все три проблемы, но снижает производительность

Практический пример: конфликт счётов

Представим две транзакции, переводящие деньги между счётами:

public class BankService {
    private DataSource dataSource;
    
    // Транзакция 1: Перевод 100 со счёта A на счёт B
    public void transferMoney(String fromAccount, String toAccount, 
                               BigDecimal amount) throws SQLException {
        try (Connection conn = dataSource.getConnection()) {
            conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
            conn.setAutoCommit(false);
            
            try {
                // T1: Читает баланс счета A
                BigDecimal balanceA = getAccountBalance(conn, fromAccount);
                
                // T2 одновременно может начаться здесь
                
                // T1: Проверка и вычитание
                if (balanceA.compareTo(amount) >= 0) {
                    updateBalance(conn, fromAccount, 
                        balanceA.subtract(amount));
                    updateBalance(conn, toAccount, 
                        getAccountBalance(conn, toAccount).add(amount));
                    
                    conn.commit(); // T1 завершена
                }
            } catch (SQLException e) {
                conn.rollback();
                throw e;
            }
        }
    }
    
    private BigDecimal getAccountBalance(Connection conn, 
                                         String account) throws SQLException {
        String sql = "SELECT balance FROM accounts WHERE id = ? FOR UPDATE";
        try (PreparedStatement stmt = conn.prepareStatement(sql)) {
            stmt.setString(1, account);
            ResultSet rs = stmt.executeQuery();
            if (rs.next()) {
                return rs.getBigDecimal("balance");
            }
        }
        return BigDecimal.ZERO;
    }
    
    private void updateBalance(Connection conn, String account, 
                               BigDecimal newBalance) throws SQLException {
        String sql = "UPDATE accounts SET balance = ? WHERE id = ?";
        try (PreparedStatement stmt = conn.prepareStatement(sql)) {
            stmt.setBigDecimal(1, newBalance);
            stmt.setString(2, account);
            stmt.executeUpdate();
        }
    }
}

Сценарий конфликта без надлежащей изоляции

Начальное состояние: Счет A = 1000, Счет B = 500

Время | Транзакция 1 (Перевод 100 A→B) | Транзакция 2 (Перевод 200 A→B)
------|----------------------------------|----------------------------------
T1    | READ balanceA = 1000             |
T2    |                                  | READ balanceA = 1000
T3    | UPDATE A = 900                   |
T4    | COMMIT T1                        |
T5    |                                  | UPDATE A = 800 (WRONG!)
T6    |                                  | COMMIT T2

РЕЗУЛЬТАТ: A = 800 вместо 700 (потеря 100)

Решение с блокировкой

// FOR UPDATE создаёт эксклюзивную блокировку
public BigDecimal getAccountBalance(Connection conn, String account) 
        throws SQLException {
    // Строка заблокирована для других транзакций
    String sql = "SELECT balance FROM accounts WHERE id = ? FOR UPDATE";
    try (PreparedStatement stmt = conn.prepareStatement(sql)) {
        stmt.setString(1, account);
        ResultSet rs = stmt.executeQuery();
        if (rs.next()) {
            return rs.getBigDecimal("balance");
        }
    }
    return BigDecimal.ZERO;
}

Теперь в сценарии выше T2 будет заблокирована на T1 и дождётся её завершения.

Современный подход с JPA/Hibernate

@Service
public class BankService {
    @Autowired
    private AccountRepository accountRepository;
    
    @Transactional(
        isolation = Isolation.READ_COMMITTED,
        propagation = Propagation.REQUIRED
    )
    public void transferMoney(Long fromId, Long toId, 
                              BigDecimal amount) {
        // Hibernate управляет блокировками и изоляцией
        Account from = accountRepository
            .findByIdForUpdate(fromId)
            .orElseThrow();
        Account to = accountRepository
            .findByIdForUpdate(toId)
            .orElseThrow();
        
        from.withdraw(amount);
        to.deposit(amount);
        // auto-commit при выходе из метода
    }
}

// Repository с LockModeType
public interface AccountRepository 
        extends JpaRepository<Account, Long> {
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT a FROM Account a WHERE a.id = :id")
    Optional<Account> findByIdForUpdate(
        @Param("id") Long id);
}

Ключевые моменты

  • READ_COMMITTED подходит для большинства приложений — хороший баланс
  • SERIALIZABLE гарантирует корректность, но может замораживать приложение
  • Optimistic locking (версии) лучше для высоконагруженных систем без конфликтов
  • Pessimistic locking (FOR UPDATE) лучше для частых конфликтов
  • В микросервисной архитектуре используют Event Sourcing для избежания распределённых транзакций
Приведи пример изолированности одной транзакции от другой | PrepBro