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

Есть ли операции, которые нельзя запустить в транзакциях

3.0 Senior🔥 61 комментариев
#Базы данных и SQL

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

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

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

# Операции, которые нельзя запустить в транзакциях

Основной ответ

ДА, есть операции, которые нельзя выполнять внутри транзакций. Они называют non-transactional или DDL операциями.

Категории запрещённых операций

1. DDL операции (Data Definition Language)

Изменяют структуру БД, а не данные. Большинство БД НЕ позволяют откатывать DDL.

-- ❌ Нельзя откатить в транзакции
CREATE TABLE users (id INT);
DROP TABLE orders;
ALTER TABLE products ADD COLUMN price DECIMAL;
TRUNCATE TABLE logs;
CREATE INDEX idx_name ON users(name);
DROP INDEX idx_name;
ALTER TABLE users RENAME TO customers;

Почему DDL нельзя в транзакции

BEGIN TRANSACTION;
    INSERT INTO users VALUES (1, 'John');
    DROP TABLE products;  -- ❌ Некоторые БД автоматически коммитят
    INSERT INTO users VALUES (2, 'Jane');
ROLLBACK;  -- ROLLBACK может не откатить DROP TABLE

-- Результат:
-- - users имеет 2 строки (INSERT1 и INSERT2 откачены)
-- - products таблица была удалена (DDL не откачена)

Поведение разных БД

-- MySQL (InnoDB)
BEGIN;
    CREATE TABLE test (id INT);  -- AUTO COMMIT!
    INSERT INTO test VALUES (1);
ROLLBACK;  -- Откатит только INSERT, CREATE остаётся

-- PostgreSQL
BEGIN;
    CREATE TABLE test (id INT);  -- В транзакции
    INSERT INTO test VALUES (1);
ROLLBACK;  -- Откатит ВСЁ (CREATE и INSERT)

-- Oracle
BEGIN;
    CREATE TABLE test (id INT);  -- AUTO COMMIT!
    INSERT INTO test VALUES (1);
ROLLBACK;  -- Откатит только INSERT, CREATE остаётся

2. Control Flow операции

Операции управления потоком не транзакционны:

-- ❌ Проблемные операции
BEGIN TRANSACTION;
    UPDATE users SET status = 'active';
    
    -- Различные database control операции
    COMMIT;          -- Прерывает транзакцию!
    ROLLBACK;        -- Прерывает транзакцию!
    SAVEPOINT sp1;   -- Может работать нестабильно
    
    UPDATE orders SET processed = 1;
COMMIT;  -- Может быть проигнорирована

Правильный подход

// ❌ НЕПРАВИЛЬНО
@Transactional
public void processData() {
    userRepository.save(new User("John"));
    
    // ВНУТРИ ТРАНЗАКЦИИ!
    createNewTable();  // Нельзя!
}

// ✅ ПРАВИЛЬНО
@Transactional
public void processData() {
    userRepository.save(new User("John"));
}

@Transactional(propagation = Propagation.NEVER)
public void createNewTable() {
    // Выполняется БЕЗ транзакции
    createTable();
}

3. Долгие блокирующие операции

Технически выполняются в транзакции, но это плохая идея:

// ❌ ПЛОХО
@Transactional
public void downloadAndProcess() throws Exception {
    Order order = orderRepository.findById(1);
    order.setStatus("processing");
    orderRepository.save(order);  // Хранит лок
    
    // Долгая сетевая операция в транзакции!
    // База заблокирована на всё это время
    byte[] data = downloadFromExternalAPI();  // 30 секунд
    
    order.setData(data);
    orderRepository.save(order);  // Долгое удержание лока
}

// ✅ ХОРОШО
public void downloadAndProcess() throws Exception {
    // Получить данные ВНЕ транзакции
    byte[] data = downloadFromExternalAPI();
    
    // Обновить в короткой транзакции
    updateOrderWithData(data);
}

@Transactional
private void updateOrderWithData(byte[] data) {
    Order order = orderRepository.findById(1);
    order.setData(data);
    orderRepository.save(order);
}

4. Операции с побочными эффектами

Операции, которые нельзя откатить:

@Transactional
public void createUser(String name) {
    // Сохраняем в БД
    User user = userRepository.save(new User(name));
    
    // ❌ ПРОБЛЕМА: побочный эффект, не откатываемый
    sendEmailNotification(user);  // Письмо уже отправлено!
    
    // ❌ Если дальше будет ошибка
    throw new RuntimeException("Ошибка!");
    // ROLLBACK откатит сохранение в БД
    // Но письмо уже отправлено!
}

// ✅ ПРАВИЛЬНО
@Transactional
public void createUser(String name) {
    // Только операции с БД в транзакции
    User user = userRepository.save(new User(name));
}

// Отправка в отдельной, non-transactional операции
public void processUserCreated(User user) {
    sendEmailNotification(user);
    sendSlackNotification(user);
    logToAnalytics(user);
}

5. Вызовы системных процедур

// ❌ Некоторые системные процедуры нельзя откатить
@Transactional
public void processAndBackup() {
    // Транзакция
    updateData();
    
    // Системная операция (не откатываемая)
    Runtime.getRuntime().exec("backup.sh");
    
    // Если rollback:
    // - БД откатится
    // - Но backup уже запущен!
}

Практические примеры в Java

Пример 1: Неправильное использование DDL

@Service
public class DatabaseService {
    @Transactional
    public void migrateData() {
        // ❌ ОШИБКА: DDL внутри транзакции
        // В MySQL автоматический COMMIT произойдёт после CREATE
        
        List<User> users = userRepository.findAll();
        
        // Это откатится при ROLLBACK
        for (User user : users) {
            user.setMigrated(true);
            userRepository.save(user);
        }
        
        // Но CREATE TABLE может выполниться вне транзакции!
        // CREATE TABLE IF NOT EXISTS users_backup AS SELECT * FROM users;
    }
}

// ✅ ПРАВИЛЬНО: Разделить на две операции
@Service
public class DatabaseService {
    @Transactional
    public void migrateData() {
        List<User> users = userRepository.findAll();
        for (User user : users) {
            user.setMigrated(true);
            userRepository.save(user);
        }
    }
    
    @Transactional(propagation = Propagation.NEVER)
    public void createBackupTable() {
        // Выполняется БЕЗ транзакции
        jdbcTemplate.execute(
            "CREATE TABLE IF NOT EXISTS users_backup AS SELECT * FROM users"
        );
    }
}

Пример 2: Долгие операции

@Service
public class OrderService {
    @Transactional
    public void processOrder(Long orderId) {
        Order order = orderRepository.findById(orderId).orElseThrow();
        order.setStatus(OrderStatus.PROCESSING);
        orderRepository.save(order);
        
        // ❌ ПРОБЛЕМА: долгая операция, база заблокирована
        // generateReport() может вызвать временный лок на несколько минут
        ReportData report = generateReport(order);
        
        order.setReport(report);
        orderRepository.save(order);
    }
    
    // ✅ ПРАВИЛЬНО: разделить
    @Transactional
    public void processOrder(Long orderId) {
        Order order = orderRepository.findById(orderId).orElseThrow();
        order.setStatus(OrderStatus.PROCESSING);
        orderRepository.save(order);
    }
    
    // Вне транзакции
    public void generateAndAttachReport(Long orderId) {
        Order order = orderRepository.findById(orderId).orElseThrow();
        ReportData report = generateReport(order);
        
        // Обновить в очень короткой транзакции
        updateOrderWithReport(orderId, report);
    }
    
    @Transactional
    private void updateOrderWithReport(Long orderId, ReportData report) {
        Order order = orderRepository.findById(orderId).orElseThrow();
        order.setReport(report);
        orderRepository.save(order);
    }
}

Пример 3: События и побочные эффекты

@Service
public class UserService {
    
    // ❌ НЕПРАВИЛЬНО: побочный эффект в транзакции
    @Transactional
    public User createUser(CreateUserRequest request) {
        User user = new User();
        user.setName(request.getName());
        user.setEmail(request.getEmail());
        userRepository.save(user);
        
        // Отправка уведомления может выполниться,
        // а потом выброситься исключение и откатится БД
        emailService.sendWelcomeEmail(user.getEmail());
        
        return user;
    }
    
    // ✅ ПРАВИЛЬНО: используй Event-driven архитектуру
    @Transactional
    public User createUser(CreateUserRequest request) {
        User user = new User();
        user.setName(request.getName());
        user.setEmail(request.getEmail());
        userRepository.save(user);
        
        // Публикуем событие (выполнится ПОСЛЕ коммита транзакции)
        applicationEventPublisher.publishEvent(
            new UserCreatedEvent(user.getId())
        );
        
        return user;
    }
    
    @EventListener
    public void onUserCreated(UserCreatedEvent event) {
        User user = userRepository.findById(event.getUserId()).orElseThrow();
        emailService.sendWelcomeEmail(user.getEmail());
        analyticsService.trackUserCreation(user);
    }
}

Пример 4: Неправильное управление COMMIT/ROLLBACK

// ❌ НЕПРАВИЛЬНО: явный COMMIT внутри @Transactional
@Transactional
public void processData() {
    userRepository.save(user1);
    connection.commit();  // ❌ Конфликт с Spring TX!
    userRepository.save(user2);
}

// ✅ ПРАВИЛЬНО: Spring управляет транзакциями
@Transactional
public void processData() {
    userRepository.save(user1);
    userRepository.save(user2);
}  // Spring автоматически коммитит

// Если нужна явная точка сохранения
@Transactional
public void processDataWithSavepoints() {
    userRepository.save(user1);
    
    try {
        userRepository.save(user2);
    } catch (Exception e) {
        // Только user2 не сохранится
        // user1 останется в БД
    }
}

Лучшие практики

1. Минимизируй транзакции

// ❌ Большая транзакция
@Transactional
public void processMillionRecords() {
    List<Record> records = fetchAllRecords();  // 1 млн
    for (Record r : records) {
        r.process();
        recordRepository.save(r);  // Обновляет по одной
    }
}  // Одна большая транзакция, много локов

// ✅ Маленькие транзакции
public void processMillionRecords() {
    List<Record> records = fetchAllRecords();
    for (Record r : records) {
        r.process();
        processSingleRecord(r);  // Каждый в своей транзакции
    }
}

@Transactional
private void processSingleRecord(Record r) {
    recordRepository.save(r);
}

2. Вывести I/O операции

// ❌ I/O внутри транзакции
@Transactional
public void fetchAndStore() {
    byte[] data = fetchFromNetwork();  // Долгая сетевая операция
    dataRepository.save(new Data(data));
}

// ✅ I/O вне транзакции
public void fetchAndStore() {
    byte[] data = fetchFromNetwork();
    storeData(data);
}

@Transactional
private void storeData(byte[] data) {
    dataRepository.save(new Data(data));
}

3. Используй Event-driven для побочных эффектов

// ✅ ХОРОШО: события выполняются ПОСЛЕ коммита
@Transactional
public Order createOrder(OrderRequest req) {
    Order order = new Order(req);
    orderRepository.save(order);
    
    // Событие выполнится ПОСЛЕ успешного коммита
    eventPublisher.publishEvent(new OrderCreatedEvent(order.getId()));
    
    return order;
}

@EventListener(OrderCreatedEvent.class)
public void onOrderCreated(OrderCreatedEvent event) {
    // Выполнится ТОЛЬКО если заказ успешно сохранён
    notificationService.sendOrderConfirmation(event.getOrderId());
    inventoryService.reserveStock(event.getOrderId());
}

Резюме

ОперацияМожно ли в транзакцииПочему
DDL (CREATE, DROP, ALTER)❌ НетНе откатываются, конфликт с TX управлением
DML (INSERT, UPDATE, DELETE)✅ ДаПолностью откатываются
Долгие операции (I/O, сеть)❌ ПлохоДолгие локи на БД
Побочные эффекты (почта, API)❌ ПлохоНе откатываются, конфликт логики
COMMIT/ROLLBACK явно❌ КонфликтSpring управляет TX
SELECT, поиск✅ ДаБезопасно
Системные процедуры❌ ПлохоНе откатываются

Главное правило: транзакции должны содержать только операции с БД, которые можно откатить. Всё остальное выполняется вне транзакций или через события.