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

Всегда ли будет запрещен доступ к данным, прочитанным в другой транзакции?

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

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

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

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

Всегда ли будет запрещен доступ к данным, прочитанным в другой транзакции?

Это отличный вопрос о транзакциях и уровнях изоляции. Ответ НЕТ — доступ будет разрешен, но ЭТО МОЖЕТ ПРИВЕСТИ К ПРОБЛЕМАМ. Все зависит от уровня изоляции транзакции.

Что происходит без уровня изоляции

По умолчанию в большинстве БД используется уровень изоляции, который ПОЗВОЛЯЕТ читать данные из других транзакций. Это может привести к нескольким проблемам:

Основные проблемы (Concurrency Issues)

1. Dirty Read (Грязное чтение)

Одна транзакция читает незавершенные (не-коммитованные) изменения другой транзакции.

// Транзакция 1
public class TransactionExample1 {
    @Transactional
    public void transfer() {
        Account from = accountRepository.findById(1L);
        from.setBalance(from.getBalance() - 100);  // Вычитаем 100
        accountRepository.save(from);
        // БЕЗ КОММИТА!
        Thread.sleep(5000);  // Ждем 5 секунд
    }
}

// Транзакция 2 (параллельно)
public class TransactionExample2 {
    @Transactional
    public void checkBalance() {
        // ГРЯЗНОЕ ЧТЕНИЕ - читаем незакоммитованные данные!
        Account account = accountRepository.findById(1L);
        System.out.println("Balance: " + account.getBalance());  // Видим 900
        // Если Транзакция 1 откатится - видели неправильный баланс!
    }
}

2. Non-Repeatable Read (Неповторяемое чтение)

Транзакция дважды читает одни и те же данные, но получает разные значения.

public class NonRepeatableReadExample {
    @Transactional
    public void processOrder(Long orderId) {
        // Первое чтение
        Order order1 = orderRepository.findById(orderId);
        System.out.println("Price 1: " + order1.getPrice());  // 100
        
        // В это время другая транзакция обновила цену
        Thread.sleep(2000);
        
        // Второе чтение ТОГоВЫХ ЖЕ ДАННЫХ
        Order order2 = orderRepository.findById(orderId);
        System.out.println("Price 2: " + order2.getPrice());  // 150 - ДРУГое значение!
        
        // Проблема: у нас непостоянный снимок данных
    }
}

3. Phantom Read (Фантомное чтение)

Транзакция выполняет запрос дважды, но получает разное количество строк.

public class PhantomReadExample {
    @Transactional
    public void processOrders() {
        // Первый запрос
        List<Order> orders1 = orderRepository.findByStatus("PENDING");
        System.out.println("Count 1: " + orders1.size());  // 5 заказов
        
        // Другая транзакция вставляет новый заказ
        Thread.sleep(2000);
        
        // Второй запрос ТЕХ ЖЕ УСЛОВИЯХ
        List<Order> orders2 = orderRepository.findByStatus("PENDING");
        System.out.println("Count 2: " + orders2.size());  // 6 заказов - НОВЫЙ появился!
    }
}

Уровни изоляции (Isolation Levels)

В SQL стандарте есть 4 уровня изоляции транзакций:

public class IsolationLevels {
    // УРОВЕНЬ 1: READ_UNCOMMITTED (самый низкий)
    // - Позволяет Dirty Reads
    // - Позволяет Non-Repeatable Reads
    // - Позволяет Phantom Reads
    // - Быстро, но ОПАСНО!
    @Transactional(isolation = Isolation.READ_UNCOMMITTED)
    public void unsafeTransaction() {
        // Практически БЕЗ защиты
    }
    
    // УРОВЕНЬ 2: READ_COMMITTED (по умолчанию в большинстве БД)
    // - БЛОКИРУЕТ Dirty Reads ✓
    // - Позволяет Non-Repeatable Reads
    // - Позволяет Phantom Reads
    // - Баланс между скоростью и безопасностью
    @Transactional(isolation = Isolation.READ_COMMITTED)
    public void defaultTransaction() {
        // По умолчанию в PostgreSQL, SQL Server
    }
    
    // УРОВЕНЬ 3: REPEATABLE_READ (по умолчанию в MySQL)
    // - БЛОКИРУЕТ Dirty Reads ✓
    // - БЛОКИРУЕТ Non-Repeatable Reads ✓
    // - Позволяет Phantom Reads
    // - Хороший баланс
    @Transactional(isolation = Isolation.REPEATABLE_READ)
    public void mysqlDefault() {
        // По умолчанию в MySQL
    }
    
    // УРОВЕНЬ 4: SERIALIZABLE (самый высокий)
    // - БЛОКИРУЕТ Dirty Reads ✓
    // - БЛОКИРУЕТ Non-Repeatable Reads ✓
    // - БЛОКИРУЕТ Phantom Reads ✓
    // - МЕДЛЕННО, полная изоляция
    @Transactional(isolation = Isolation.SERIALIZABLE)
    public void criticalOperation() {
        // Максимальная безопасность
    }
}

Таблица уровней изоляции

УровеньDirty ReadNon-RepeatablePhantomСкорость
READ_UNCOMMITTED❌ Возможен❌ Возможен❌ Возможен⚡⚡⚡
READ_COMMITTED✓ Нет❌ Возможен❌ Возможен⚡⚡
REPEATABLE_READ✓ Нет✓ Нет❌ Возможен
SERIALIZABLE✓ Нет✓ Нет✓ Нет🐢

Пример в Spring Data JPA

@Service
public class BankingService {
    
    // Рискованно: может быть грязное чтение
    @Transactional(isolation = Isolation.READ_UNCOMMITTED)
    public void unsafeTransfer(Long from, Long to, BigDecimal amount) {
        // ❌ НИКОГДА не используй для финансовых операций!
    }
    
    // По умолчанию в Spring (зависит от БД)
    @Transactional
    public void safeTransfer(Long from, Long to, BigDecimal amount) {
        Account fromAccount = accountRepository.findById(from).orElseThrow();
        Account toAccount = accountRepository.findById(to).orElseThrow();
        
        fromAccount.setBalance(fromAccount.getBalance().subtract(amount));
        toAccount.setBalance(toAccount.getBalance().add(amount));
        
        accountRepository.save(fromAccount);
        accountRepository.save(toAccount);
    }
    
    // Максимальная безопасность для критичных операций
    @Transactional(isolation = Isolation.SERIALIZABLE)
    public void criticalTransfer(Long from, Long to, BigDecimal amount) {
        // Гарантирует, что никакие другие транзакции не вмешаются
    }
}

Оптимистичная блокировка (Optimistic Locking)

Вместо блокирования можно использовать версионирование:

@Entity
public class Account {
    @Id
    private Long id;
    
    private BigDecimal balance;
    
    @Version  // JPA оптимистичная блокировка
    private Long version;
}

@Service
public class OptimisticLockingService {
    @Transactional
    public void transfer(Long from, Long to, BigDecimal amount) {
        Account fromAccount = accountRepository.findById(from).orElseThrow();
        Account toAccount = accountRepository.findById(to).orElseThrow();
        
        // Если версия изменилась - будет OptimisticLockingFailureException
        fromAccount.setBalance(fromAccount.getBalance().subtract(amount));
        toAccount.setBalance(toAccount.getBalance().add(amount));
        
        accountRepository.save(fromAccount);
        accountRepository.save(toAccount);
    }
}

Пессимистичная блокировка (Pessimistic Locking)

Заблокировать данные сразу при чтении:

public interface AccountRepository extends JpaRepository<Account, Long> {
    // SELECT FOR UPDATE - блокирует для других транзакций
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT a FROM Account a WHERE a.id = :id")
    Account findByIdWithLock(@Param("id") Long id);
}

@Service
public class PessimisticLockingService {
    @Transactional
    public void transfer(Long from, Long to, BigDecimal amount) {
        // Блокируем оба счета сразу
        Account fromAccount = accountRepository.findByIdWithLock(from);
        Account toAccount = accountRepository.findByIdWithLock(to);
        
        // Теперь никакая другая транзакция не может изменить эти счета
        fromAccount.setBalance(fromAccount.getBalance().subtract(amount));
        toAccount.setBalance(toAccount.getBalance().add(amount));
    }
}

Рекомендации

  1. Для финансовых операций: используй SERIALIZABLE или пессимистичную блокировку
  2. Для обычных операций: READ_COMMITTED (по умолчанию) обычно достаточно
  3. Для отчетов: REPEATABLE_READ дает снимок данных
  4. Избегай READ_UNCOMMITTED — почти никогда не используется в продакшене
  5. Мониторь deadlocks — они возникают при высокой конкуренции

Ответ на исходный вопрос: Доступ к данным из другой транзакции НЕ всегда запрещен. Это зависит от уровня изоляции. По умолчанию READ_COMMITTED позволяет читать уже закоммитованные данные, что в большинстве случаев безопасно.

Всегда ли будет запрещен доступ к данным, прочитанным в другой транзакции? | PrepBro