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

В каких случаях транзакции обязательны

2.2 Middle🔥 161 комментариев
#Архитектура и паттерны#Базы данных (SQL)

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

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

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

В каких случаях транзакции обязательны?

Транзакции — это механизм БД для гарантии ACID свойств (Atomicity, Consistency, Isolation, Durability). Это критично для целостности данных. Давайте разберём, когда они ОБЯЗАТЕЛЬНЫ.

Что такое транзакция?

Транзакция — это группа операций, которые:

  • Либо все выполняются (COMMIT)
  • Либо все отменяются (ROLLBACK)

Проблема БЕЗ транзакций:

# ❌ БЕЗ транзакции — опасно!
def transfer_money(from_account_id: int, to_account_id: int, amount: float):
    # Шаг 1: Вычитаем деньги со счёта A
    from_account = db.query(Account).get(from_account_id)
    from_account.balance -= amount
    db.commit()  # ← Деньги уже списали!
    
    # Шаг 2: Если здесь упадёт сервер или БД...
    # Деньги исчезли из системы!
    
    # Шаг 3: Добавляем деньги на счёт B
    to_account = db.query(Account).get(to_account_id)
    to_account.balance += amount
    db.commit()

Что может пойти не так:

  • Сервер упал после шага 1 → деньги потеряны
  • БД упала → несогласованное состояние
  • Сетевая ошибка → часть данных сохранилась, часть нет

Правильный подход: Транзакции

# ✅ С транзакцией — безопасно!
from sqlalchemy.orm import Session
from contextlib import contextmanager

@contextmanager
def transaction(db: Session):
    """Context manager для ACID транзакции"""
    try:
        yield db
        db.commit()  # Коммитим только если всё успешно
    except Exception as e:
        db.rollback()  # Откатываем все изменения
        raise e

def transfer_money(db: Session, from_id: int, to_id: int, amount: float):
    """Транзакция всё или ничего"""
    with transaction(db):
        from_account = db.query(Account).filter_by(id=from_id).first()
        to_account = db.query(Account).filter_by(id=to_id).first()
        
        if not from_account or not to_account:
            raise AccountNotFoundError()
        
        if from_account.balance < amount:
            raise InsufficientFundsError()
        
        # Оба запроса в одной транзакции
        from_account.balance -= amount
        to_account.balance += amount
        # COMMIT произойдёт только если не было ошибок

ОБЯЗАТЕЛЬНЫЕ случаи для транзакций

1. Денежные операции (критично!)

from decimal import Decimal
from datetime import datetime, timezone

def process_payment(db: Session, order_id: int, amount: Decimal):
    """Платёж требует ACID гарантий"""
    try:
        with db.begin():  # Автоматическая транзакция
            # Шаг 1: Проверяем баланс
            customer_account = db.query(CustomerAccount).filter_by(
                id=order_id
            ).with_for_update().first()  # SELECT FOR UPDATE (lock)
            
            if customer_account.balance < amount:
                raise InsufficientFundsError("Not enough money")
            
            # Шаг 2: Уменьшаем баланс
            customer_account.balance -= amount
            
            # Шаг 3: Записываем транзакцию
            transaction = Payment(
                customer_id=customer_account.customer_id,
                amount=amount,
                status="completed",
                created_at=datetime.now(timezone.utc)
            )
            db.add(transaction)
            
            # Шаг 4: Обновляем статус заказа
            order = db.query(Order).get(order_id)
            order.status = "paid"
            order.paid_at = datetime.now(timezone.utc)
            
            # Если не было исключений — автоматический COMMIT
    except Exception as e:
        # Автоматический ROLLBACK
        logger.error(f"Payment failed: {e}")
        raise PaymentError(str(e))

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

  • Если деньги списались, но заказ не обновился → система в беде
  • Если платёж записался, но баланс не обновился → деньги потеряны
  • Если всё упало на середине → система должна восстановить консистентное состояние

2. Счётчики и аналитика (очень важно)

def increment_view_counter(db: Session, post_id: int):
    """Без транзакции потеряем views"""
    
    # ❌ БЕЗ транзакции (Race condition)
    post = db.query(Post).get(post_id)  # Читаем: views = 100
    # Если два запроса одновременно...
    # Оба прочитают 100
    # Оба напишут 101
    # Вместо 102 будет 101!
    post.view_count += 1
    db.commit()
    
    # ✅ С транзакцией (SERIALIZABLE isolation)
    with db.begin():
        post = db.query(Post).filter_by(id=post_id).with_for_update().first()
        post.view_count += 1
        # Гарантирует, что никто другой не изменит счётчик

# Или используй атомарное увеличение
from sqlalchemy import func

def increment_view_counter_atomic(db: Session, post_id: int):
    """Атомарное увеличение (лучший способ)"""
    db.query(Post).filter_by(id=post_id).update(
        {Post.view_count: Post.view_count + 1}
    )
    db.commit()

3. Удаление с каскадом

def delete_user_with_cascade(db: Session, user_id: int):
    """Удаление пользователя со всеми связанными данными"""
    
    with db.begin():
        user = db.query(User).get(user_id)
        
        # Каскадное удаление (определяется в моделях)
        # 1. Удаляем все посты пользователя
        # 2. Удаляем все комментарии пользователя
        # 3. Удаляем сам пользователя
        
        db.delete(user)  # Всё удалится в одной транзакции

# Model definition
class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True)
    name = Column(String)
    
    # Каскадное удаление
    posts = relationship("Post", cascade="all, delete-orphan")
    comments = relationship("Comment", cascade="all, delete-orphan")

class Post(Base):
    __tablename__ = "posts"
    id = Column(Integer, primary_key=True)
    user_id = Column(Integer, ForeignKey("users.id"))

4. Обновление связанных таблиц

def move_items_between_folders(db: Session, from_folder_id: int, to_folder_id: int):
    """Переместить все файлы из одной папки в другую"""
    
    with db.begin():
        # Проверяем, что обе папки существуют
        from_folder = db.query(Folder).filter_by(id=from_folder_id).with_for_update().first()
        to_folder = db.query(Folder).filter_by(id=to_folder_id).with_for_update().first()
        
        if not from_folder or not to_folder:
            raise FolderNotFoundError()
        
        # Обновляем путь для всех файлов
        files = db.query(File).filter_by(folder_id=from_folder_id).all()
        for file in files:
            file.folder_id = to_folder_id
            file.updated_at = datetime.now(timezone.utc)
        
        # Обновляем счётчики
        from_folder.file_count -= len(files)
        to_folder.file_count += len(files)
        
        # Логируем событие
        event = FileMovementEvent(
            from_folder_id=from_folder_id,
            to_folder_id=to_folder_id,
            count=len(files),
            created_at=datetime.now(timezone.utc)
        )
        db.add(event)
        # Всё в одной транзакции

5. Saga паттерн (распределённые транзакции)

def complete_order(db: Session, order_id: int):
    """Сложный процесс с несколькими шагами"""
    
    try:
        with db.begin():
            # Шаг 1: Обновляем заказ
            order = db.query(Order).get(order_id)
            order.status = "processing"
            
            # Шаг 2: Уменьшаем инвентарь
            for item in order.items:
                product = item.product
                if product.stock < item.quantity:
                    raise OutOfStockError(f"{product.name} is out of stock")
                product.stock -= item.quantity
            
            # Шаг 3: Создаём счёт
            invoice = Invoice(
                order_id=order_id,
                amount=order.total_price,
                status="pending"
            )
            db.add(invoice)
            
            # Шаг 4: Резервируем доставку
            shipment = Shipment(
                order_id=order_id,
                status="reserved"
            )
            db.add(shipment)
            
            # Все шаги или ничего
    
    except OutOfStockError as e:
        # Rollback произойдёт автоматически
        logger.error(f"Order {order_id} failed: {e}")
        raise OrderProcessingError(str(e))

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

УровеньГрязное чтениеНереп. чтениеPhantomИспользование
READ UNCOMMITTEDНе использовать!
READ COMMITTEDDefault в PostgreSQL
REPEATABLE READБольшинство случаев
SERIALIZABLEФинансовые операции

Правильная конфигурация (PostgreSQL)

from sqlalchemy import create_engine
from sqlalchemy.pool import QueuePool

engine = create_engine(
    "postgresql://user:password@localhost/db",
    # Пулинг подключений
    poolclass=QueuePool,
    pool_size=10,
    max_overflow=20,
    pool_pre_ping=True,  # Проверяем подключения перед использованием
    # Уровень изоляции
    connect_args={
        "isolation_level": "READ_COMMITTED"  # Default
    }
)

# Для финансовых операций
finance_engine = create_engine(
    "postgresql://user:password@localhost/db",
    connect_args={
        "isolation_level": "SERIALIZABLE"  # Максимум безопасности
    }
)

Чеклист: когда нужна транзакция?

  • Несколько операций, которые должны быть атомарными?
  • Денежные операции или счётчики?
  • Обновление связанных таблиц?
  • Возможны race conditions?
  • Нужна гарантия целостности данных?

Если хотя бы один ответ "да" → нужна транзакция!

Вывод

Транзакции обязательны в:

  • Денежных системах (платежи, переводы, отчёты)
  • Критичных данных (пользователи, заказы, инвентарь)
  • Любых многошаговых операциях, где консистентность критична

Отсутствие транзакций приводит к:

  • Потере денег
  • Потере данных
  • Несогласованности в системе
  • Невозможности восстановления после сбоев

Помни: Fail Fast with Transactions — если что-то не так, откатить все изменения гораздо лучше, чем иметь половину выполненных операций.