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

Какие знаешь проблемы, которые могут возникать в базе данных при транзакции?

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

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

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

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

Проблемы базы данных при транзакциях

Работая с транзакциями, разработчик сталкивается с различными проблемами, которые могут привести к потере данных, deadlock'ам и нарушениям консистентности. Давайте разберемся в наиболее критичных из них.

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

Dirty Read — это чтение данных, которые были изменены другой транзакцией, но еще не зафиксированы (committed):

// Транзакция 1: начало
Account account = accountRepository.findById(1L); // balance = 1000
account.setBalance(500);
accountRepository.save(account);
// Еще не commit

// Транзакция 2: в этот момент
Account sameAccount = accountRepository.findById(1L); // balance = 500 (dirty read)
// Если Транзакция 1 откатится, мы работаем с несуществующими данными

Проблема решается уровнем изоляции READ_COMMITTED или выше:

@Service
public class AccountService {
    @Transactional(isolation = Isolation.READ_COMMITTED)
    public void safeRead(Long accountId) {
        Account account = accountRepository.findById(accountId);
        // Гарантия: читаем только commit'едные данные
    }
}

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

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

@Transactional
public void problematicRead(Long userId) {
    // Первое чтение
    User user1 = userRepository.findById(userId);
    System.out.println("Первое имя: " + user1.getName()); // "John"

    // В это время другая транзакция обновляет данные
    // UPDATE users SET name = 'Jane' WHERE id = userId;

    // Второе чтение
    User user2 = userRepository.findById(userId);
    System.out.println("Второе имя: " + user2.getName()); // "Jane" — ДРУГОЕ!
}

Решение — уровень изоляции REPEATABLE_READ:

@Service
public class UserService {
    @Transactional(isolation = Isolation.REPEATABLE_READ)
    public void consistentRead(Long userId) {
        User user1 = userRepository.findById(userId);
        // Другие транзакции не смогут изменить эту строку
        User user2 = userRepository.findById(userId);
        // user1 и user2 гарантированно одинаковые
    }
}

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

Изменение количества строк, возвращаемых одним и тем же запросом:

@Transactional
public void phantomProblem(LocalDate date) {
    // Первый запрос: получаем 5 заказов за день
    List<Order> orders1 = orderRepository.findByDate(date);
    System.out.println("Количество: " + orders1.size()); // 5

    // В это время другая транзакция создает новый заказ за этот день
    // INSERT INTO orders VALUES (...);

    // Второй запрос: теперь их 6!
    List<Order> orders2 = orderRepository.findByDate(date);
    System.out.println("Количество: " + orders2.size()); // 6 — ФАНТОМ!
}

Решение — уровень изоляции SERIALIZABLE:

@Service
public class OrderService {
    @Transactional(isolation = Isolation.SERIALIZABLE)
    public void phantomFree(LocalDate date) {
        List<Order> orders1 = orderRepository.findByDate(date);
        List<Order> orders2 = orderRepository.findByDate(date);
        // Гарантия: количество строк не изменится
    }
}

Deadlock (Взаимная блокировка)

Две транзакции ждут друг друга в круговой зависимости:

Транзакция 1:              Транзакция 2:
1. LOCK TABLE A            1. LOCK TABLE B
2. LOCK TABLE B (ждет)     2. LOCK TABLE A (ждет)
   → DEADLOCK

Пример в коде:

@Service
public class TransferService {
    @Autowired
    private AccountRepository accountRepository;

    // ПРОБЛЕМА: Deadlock при параллельных переводах
    @Transactional
    public void unsafeTransfer(Long fromId, Long toId, BigDecimal amount) {
        Account from = accountRepository.findById(fromId).lock(); // Lock 1
        Account to = accountRepository.findById(toId).lock();     // Lock 2 (может ждать)
        from.withdraw(amount);
        to.deposit(amount);
    }
}

Решение — всегда блокировать в одном порядке:

@Service
public class TransferService {
    @Transactional
    public void safeTransfer(Long fromId, Long toId, BigDecimal amount) {
        // Всегда блокируем в порядке ID
        Long lockFirst = Math.min(fromId, toId);
        Long lockSecond = Math.max(fromId, toId);
        
        Account acc1 = accountRepository.findByIdForUpdate(lockFirst);
        Account acc2 = accountRepository.findByIdForUpdate(lockSecond);
        
        if (fromId.equals(lockFirst)) {
            acc1.withdraw(amount);
            acc2.deposit(amount);
        } else {
            acc2.withdraw(amount);
            acc1.deposit(amount);
        }
    }
}

Lost Update (Потерянное обновление)

Одно обновление перезаписывается другим без учета промежуточных изменений:

@Transactional
public void lostUpdate() {
    // Транзакция 1: прочитал balance = 1000
    Account acc = accountRepository.findById(1L);
    acc.setBalance(acc.getBalance() - 100); // 900
    
    // Транзакция 2: в это время прочитала balance = 1000
    // и обновила: balance = 1000 + 50 = 1050
    
    // Транзакция 1: сохраняет 900
    // Результат: 900 вместо 950! Потеря 50.
    accountRepository.save(acc);
}

Решение — использовать version-based optimistic locking:

@Entity
public class Account {
    @Id
    private Long id;
    private BigDecimal balance;
    
    @Version  // Поле версии
    private Long version;
}

@Service
public class AccountService {
    @Transactional
    public void atomicUpdate(Long accountId, BigDecimal amount) {
        Account acc = accountRepository.findById(accountId);
        acc.setBalance(acc.getBalance().add(amount));
        // JPA автоматически добавит WHERE version = старая_версия
        accountRepository.save(acc);
        // При конфликте выбросит OptimisticLockException
    }
}

Lock Timeout (Таймаут блокировки)

Транзакция слишком долго ждет освобождения ресурса:

@Service
public class LongOperationService {
    @Transactional(timeout = 5) // 5 секунд
    public void processData() {
        List<Data> items = dataRepository.findAll(); // Может занять > 5 сек
        // TransactionTimedOutException
        for (Data item : items) {
            heavyProcessing(item); // Долгие операции
        }
    }
}

Решение — правильно настроить timeout и разбить работу:

@Service
public class OptimizedService {
    @Transactional(timeout = 30)
    public void processInChunks() {
        int batchSize = 100;
        int offset = 0;
        
        while (true) {
            List<Data> batch = dataRepository.findAll(
                PageRequest.of(offset / batchSize, batchSize)
            );
            if (batch.isEmpty()) break;
            
            for (Data item : batch) {
                heavyProcessing(item);
            }
            offset += batchSize;
        }
    }
}

Race Condition (Состояние гонки)

Несколько транзакций одновременно читают и обновляют счетчик:

public class ViewCounter {
    private Long viewCount = 0;
    
    @Transactional
    public void incrementViews() {
        Long count = repository.getViewCount(); // Прочитали 100
        // Другой процесс прочитал 100 и инкрементировал до 101
        repository.setViewCount(count + 1);     // Сохранили 101 вместо 102
    }
}

Решение — использовать native UPDATE с инкрементом:

@Repository
public interface ViewRepository extends JpaRepository<View, Long> {
    @Modifying
    @Query("UPDATE View v SET v.count = v.count + 1 WHERE v.id = ?1")
    void incrementViews(Long id);
}

@Service
public class ViewService {
    @Transactional
    public void safeIncrement(Long id) {
        viewRepository.incrementViews(id);
    }
}

Уровни изоляции транзакций

УровеньDirty ReadNon-RepeatablePhantom
READ_UNCOMMITTEDВозможенВозможенВозможен
READ_COMMITTEDНетВозможенВозможен
REPEATABLE_READНетНетВозможен
SERIALIZABLEНетНетНет

Практические рекомендации

  • Выбирай минимально необходимый уровень изоляции
  • Используй optimistic locking для высоконагруженных систем
  • Устанавливай разумные timeout значения
  • Блокируй ресурсы в одном порядке чтобы избежать deadlock'ов
  • Разбивай долгие операции на небольшие транзакции
  • Мониторь lock contention в production