← Назад к вопросам
В каких случаях транзакции обязательны
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 COMMITTED | ✗ | ✓ | ✓ | Default в 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 — если что-то не так, откатить все изменения гораздо лучше, чем иметь половину выполненных операций.