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

Какой использовал уровень изолированной транзакции в предыдущем проекте?

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

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

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

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

Уровни изоляции транзакций в моих проектах

Вопрос касается важного аспекта работы с базами данных в Java — уровней изоляции транзакций (Transaction Isolation Levels). В зависимости от требований проекта, я использовал разные уровни, и вот как я подходил к их выбору на практике.

Четыре уровня изоляции ACID

SQL стандарт определяет четыре уровня изоляции транзакций, от самого слабого к самому строгому:

// В Spring Data JPA и Hibernate
@Transactional(isolation = Isolation.READ_UNCOMMITTED)  // Уровень 0
@Transactional(isolation = Isolation.READ_COMMITTED)    // Уровень 1 (default в большинстве БД)
@Transactional(isolation = Isolation.REPEATABLE_READ)   // Уровень 2
@Transactional(isolation = Isolation.SERIALIZABLE)      // Уровень 3

READ_COMMITTED — основной выбор

В большинстве моих enterprise проектов использовал READ_COMMITTED — это стандартный уровень по умолчанию:

@Service
public class OrderService {
    
    @Transactional(isolation = Isolation.READ_COMMITTED)
    public Order createOrder(OrderRequest request) {
        // Читаем данные, которые были закоммичены другими транзакциями
        // Не видим грязные (uncommitted) данные
        
        Order order = new Order();
        order.setCustomerId(request.getCustomerId());
        order.setTotalAmount(request.getAmount());
        
        return orderRepository.save(order);
    }
    
    @Transactional(isolation = Isolation.READ_COMMITTED)
    public void updateOrderStatus(Long orderId, String status) {
        Order order = orderRepository.findById(orderId).orElseThrow();
        order.setStatus(status);
        orderRepository.save(order);
    }
}

Преимущества READ_COMMITTED:

  • Предотвращает dirty reads (чтение грязных данных)
  • Хороший баланс между производительностью и безопасностью
  • По умолчанию в PostgreSQL и SQL Server
  • Минимальные блокировки

Проблемы READ_COMMITTED:

  • Возможны non-repeatable reads — если вторая транзакция изменила данные
  • Возможны phantom reads — новые строки могут появиться при повторном SELECT

REPEATABLE_READ — для критичной бизнес-логики

Для операций, требующих конкурентной безопасности, использовал REPEATABLE_READ:

@Service
public class PaymentService {
    
    // Финансовые операции требуют REPEATABLE_READ
    @Transactional(isolation = Isolation.REPEATABLE_READ)
    public void processRefund(Long orderId) {
        // Гарантия: все SELECT'ы в этой транзакции вернут одни и те же данные
        // даже если другие транзакции их меняют
        
        Order order = orderRepository.findById(orderId).orElseThrow();
        
        // Даже если другая транзакция изменит order.amount здесь,
        // мы увидим старое значение — гарантия повторяемости
        BigDecimal amount = order.getTotalAmount();
        
        Account account = accountRepository.findById(order.getAccountId()).orElseThrow();
        account.addBalance(amount);
        
        order.setStatus("REFUNDED");
        accountRepository.save(account);
        orderRepository.save(order);
    }
}

Когда использовал REPEATABLE_READ:

  • Финансовые транзакции
  • Операции с инвентарём
  • Критичные расчёты где нужна консистентность

SERIALIZABLE — редко, только для очень критичного

SERIALIZABLE использовал крайне редко, т.к. это убивает производительность:

@Service
public class InventoryService {
    
    // Только для самых критичных операций
    @Transactional(isolation = Isolation.SERIALIZABLE)
    public void allocateStock(Long productId, int quantity) throws OutOfStockException {
        // Это как если бы все транзакции выполнялись одна за другой
        // Полная безопасность, но медленно
        
        Product product = productRepository.findById(productId).orElseThrow();
        if (product.getStock() < quantity) {
            throw new OutOfStockException();
        }
        product.setStock(product.getStock() - quantity);
        productRepository.save(product);
    }
}

Практический пример — выбор уровня

@Service
public class MixedTransactionService {
    
    // Чтение пользовательского профиля — READ_COMMITTED достаточно
    @Transactional(isolation = Isolation.READ_COMMITTED, readOnly = true)
    public UserProfile getUserProfile(Long userId) {
        return userRepository.findById(userId).orElseThrow();
    }
    
    // Изменение баланса счёта — нужен REPEATABLE_READ
    @Transactional(isolation = Isolation.REPEATABLE_READ)
    public void transferMoney(Long fromAccountId, Long toAccountId, BigDecimal amount) {
        Account from = accountRepository.findById(fromAccountId).orElseThrow();
        Account to = accountRepository.findById(toAccountId).orElseThrow();
        
        if (from.getBalance().compareTo(amount) < 0) {
            throw new InsufficientFundsException();
        }
        
        from.setBalance(from.getBalance().subtract(amount));
        to.setBalance(to.getBalance().add(amount));
        
        accountRepository.save(from);
        accountRepository.save(to);
    }
    
    // Биржевая операция — максимальная безопасность
    @Transactional(isolation = Isolation.SERIALIZABLE)
    public void executeTrade(String symbol, int quantity, BigDecimal price) {
        StockPosition position = stockRepository.findBySymbol(symbol);
        // Гарантия отсутствия race conditions
        position.addQuantity(quantity);
        stockRepository.save(position);
    }
}

Проблемы и их решение

// Проблема: phantom reads на READ_COMMITTED
@Transactional(isolation = Isolation.READ_COMMITTED)
public void processOrders() {
    List<Order> orders = orderRepository.findByStatus("PENDING");
    // Берём 5 заказов
    
    // Другая транзакция добавляет новый заказ со статусом PENDING
    
    List<Order> ordersAgain = orderRepository.findByStatus("PENDING");
    // Теперь здесь может быть 6 заказов! — phantom read
}

// Решение: использовать REPEATABLE_READ или SELECT FOR UPDATE
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void processOrdersFixed() {
    List<Order> orders = orderRepository.findByStatus("PENDING");
    // Гарантия: при повторном SELECT будут те же строки
}

Таблица сравнения

УровеньDirty ReadNon-repeatable ReadPhantom ReadПроизводительность
READ_UNCOMMITTEDЛучше
READ_COMMITTEDХорошо
REPEATABLE_READСреднее
SERIALIZABLEМедленно

Заключение

В своих проектах я использовал:

  1. READ_COMMITTED — по умолчанию для 80% операций (хороший баланс)
  2. REPEATABLE_READ — для финансовых операций и критичного бизнес-логики
  3. SERIALIZABLE — редко, только когда действительно нужна полная изоляция

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

Какой использовал уровень изолированной транзакции в предыдущем проекте? | PrepBro