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

Что такое схема транзакции базы данных?

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

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

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

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

# Схема транзакции базы данных

Определение

Схема транзакции (Transaction Schema) — это набор правил и механизмов, которые определяют, как база данных обрабатывает группу операций (операции чтения/записи) для обеспечения целостности и консистентности данных. Схема описывает порядок выполнения операций, изоляцию между конкурирующими транзакциями и гарантии, которые БД предоставляет.

Основной концепт

Транзакция — это логическая единица работы, которая состоит из одной или нескольких SQL операций. Все операции должны выполниться успешно (COMMIT) или все откатиться (ROLLBACK). Промежуточное состояние недопустимо.

-- Пример транзакции: перевод денег
BEGIN TRANSACTION;

UPDATE accounts SET balance = balance - 100 WHERE id = 1;  -- Счёт 1 минус 100
UPDATE accounts SET balance = balance + 100 WHERE id = 2;  -- Счёт 2 плюс 100

COMMIT;  -- Если обе операции успешны, сохранить
-- или ROLLBACK; если была ошибка, откатить обе операции

Свойства ACID

Схема транзакций основана на четырёх принципах ACID:

1. Atomicity (Атомарность)

Транзакция либо полностью выполняется, либо полностью откатывается. Промежуточные состояния невозможны.

# Python пример
import sqlite3

conn = sqlite3.connect(":memory:")
cursor = conn.cursor()

# Создаём таблицу счетов
cursor.execute("""
    CREATE TABLE accounts (
        id INTEGER PRIMARY KEY,
        name TEXT,
        balance DECIMAL
    )
""")

cursor.execute("INSERT INTO accounts VALUES (1, 'Alice', 1000)")
cursor.execute("INSERT INTO accounts VALUES (2, 'Bob', 500)")
conn.commit()

# Неатомарная операция (ОПАСНО)
try:
    cursor.execute("UPDATE accounts SET balance = balance - 100 WHERE id = 1")
    # Если здесь произойдёт ошибка...
    raise Exception("Что-то пошло не так!")
    cursor.execute("UPDATE accounts SET balance = balance + 100 WHERE id = 2")
    conn.commit()
except Exception as e:
    # В этом случае у Alice денги уходят, но Bob не получает
    print(f"Ошибка: {e}")
    conn.rollback()

# Кортлось откатить, но Alice потеряла 100!

Правильный способ с SQLAlchemy:

from sqlalchemy import create_engine, Column, Integer, String, Numeric
from sqlalchemy.orm import sessionmaker, Session

engine = create_engine("sqlite:///:memory:")
Session = sessionmaker(bind=engine)

# Модель
class Account:
    __tablename__ = "accounts"
    id = Column(Integer, primary_key=True)
    name = Column(String)
    balance = Column(Numeric)

# Транзакция (атомарная)
session = Session()
try:
    alice = session.query(Account).filter_by(id=1).first()
    bob = session.query(Account).filter_by(id=2).first()
    
    alice.balance -= 100
    bob.balance += 100
    
    session.commit()  # ВСЕ изменения применяются одновременно
except Exception as e:
    session.rollback()  # ВСЕ изменения откатываются одновременно
    print(f"Ошибка: {e}")
finally:
    session.close()

2. Consistency (Консистентность)

БД переходит из одного консистентного состояния в другое. Все ограничения целостности (constraints) выполняются.

-- Ограничение: balance не может быть отрицательным
CREATE TABLE accounts (
    id INTEGER PRIMARY KEY,
    balance DECIMAL CHECK (balance >= 0)
);

BEGIN TRANSACTION;
UPDATE accounts SET balance = -50 WHERE id = 1;
-- БД ОТКЛОНИТ этот UPDATE, т.к. нарушит constraint
ROLLBACK;

3. Isolation (Изоляция)

Конкурирующие транзакции не видят незакомленные изменения друг друга. Это предотвращает "грязные" чтения (dirty reads).

# Без изоляции (ПРОБЛЕМА)
# Сессия A:
cursor.execute("UPDATE accounts SET balance = 1000 WHERE id = 1")
# Сессия B может увидеть balance = 1000, пока A ещё не закоммитила

# С изоляцией (ХОРОШО)
# Сессия A:
session.begin()
alice = session.query(Account).filter_by(id=1).first()
alice.balance = 1000
# Сессия B не видит это изменение, пока A не закоммитит
session.commit()

4. Durability (Долговечность)

После COMMIT, данные сохраняются постоянно, даже при сбое системы.

# После COMMIT, данные безопасны
session.add(new_user)
session.commit()  # Данные записаны на диск
# Даже если сервер упадёт сейчас, данные остаются

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

БД предоставляет разные уровни изоляции, которые контролируют компромисс между скоростью и безопасностью:

1. Read Uncommitted (Чтение незакомленных данных)

Самый низкий уровень безопасности, высокая производительность.

# Проблема: Dirty Read (грязное чтение)
# Сессия A:
cursor.execute("UPDATE accounts SET balance = 0 WHERE id = 1")

# Сессия B (в режиме Read Uncommitted):
cursor.execute("SELECT balance FROM accounts WHERE id = 1")
# B видит balance = 0, хотя A ещё не закоммитила

# А потом A откатилась (ROLLBACK)
# Теперь B имеет данные о несуществующем состоянии!

2. Read Committed (Чтение закомленных данных)

Стандартный уровень. Видишь только закомленные данные.

# В режиме Read Committed
# Сессия A:
cursor.execute("BEGIN")
cursor.execute("UPDATE accounts SET balance = 500 WHERE id = 1")

# Сессия B:
cursor.execute("SELECT balance FROM accounts WHERE id = 1")
# B видит старое значение (1000), т.к. A ещё не закоммитила

# Потом A коммитит:
cursor.execute("COMMIT")

# Теперь B видит новое значение (500)

3. Repeatable Read (Повторяемое чтение)

Предотвращает грязные чтения и non-repeatable reads.

# В режиме Repeatable Read
# Сессия A:
cursor.execute("BEGIN")
cursor.execute("SELECT balance FROM accounts WHERE id = 1")  # Результат: 1000

# Сессия B коммитит UPDATE:
cursor.execute("UPDATE accounts SET balance = 500 WHERE id = 1")
cursor.execute("COMMIT")

# Сессия A:
cursor.execute("SELECT balance FROM accounts WHERE id = 1")  # РЕЗУЛЬТАТ: всё ещё 1000!
# A видит снимок на начало транзакции, невзирая на изменения B

4. Serializable (Сериализируемость)

Наивысший уровень безопасности. Транзакции выполняются как если бы они были последовательны (одна за другой).

# Режим Serializable
# Сессия A:
cursor.execute("BEGIN ISOLATION LEVEL SERIALIZABLE")
cursor.execute("SELECT SUM(balance) FROM accounts")  # Результат: 2000

# Сессия B пытается UPDATE:
cursor.execute("UPDATE accounts SET balance = 300 WHERE id = 2")
# БД может заблокировать B, т.к. A уже читает данные

# Когда A закоммитится, B может продолжить

Практический пример: Django ORM

from django.db import transaction

# Простая транзакция
with transaction.atomic():
    user = User.objects.create(username="john", email="john@example.com")
    Profile.objects.create(user=user, age=30)
    # Если ошибка — ОБА CREATE откатятся

# Вложенные транзакции (savepoints)
with transaction.atomic():
    user = User.objects.create(username="jane")
    
    try:
        with transaction.atomic():
            user.email = "jane@example.com"
            user.save()
            # Может быть ошибка здесь
            raise ValueError("Неправильный email!")
    except ValueError:
        # Только этот блок откатится, user останется созданным
        pass

# Явный control
from django.db import connection

connection.set_autocommit(False)
try:
    user = User.objects.create(username="mike")
    # Ещё не в БД
    connection.commit()
except Exception as e:
    connection.rollback()
finally:
    connection.set_autocommit(True)

Практический пример: SQLAlchemy

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

engine = create_engine("postgresql://user:password@localhost/db")
Session = sessionmaker(bind=engine)

# Автоматическая транзакция
session = Session()
try:
    user = User(username="alice", email="alice@example.com")
    session.add(user)
    session.commit()
except Exception as e:
    session.rollback()
    print(f"Ошибка: {e}")
finally:
    session.close()

# С контекстным менеджером (лучше)
from contextlib import contextmanager

@contextmanager
def get_session():
    session = Session()
    try:
        yield session
        session.commit()
    except:
        session.rollback()
        raise
    finally:
        session.close()

# Использование
with get_session() as session:
    user = User(username="bob")
    session.add(user)
    # Автоматический commit в конце

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

Dirty Read

Сессия A видит незакомленные изменения сессии B.

Non-Repeatable Read

Сессия A читает одни данные дважды и получает разные результаты, т.к. сессия B их обновила.

Phantom Read

Сессия A читает набор строк дважды, получает разные результаты, т.к. сессия B вставила новые строки.

# Пример Phantom Read
# Сессия A:
SELECT COUNT(*) FROM orders WHERE status = 'pending';  # Результат: 5

# Сессия B:
INSERT INTO orders (status) VALUES ('pending');
COMMIT;

# Сессия A:
SELECT COUNT(*) FROM orders WHERE status = 'pending';  # Результат: 6 (Фантом!)

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

# DEADLOCK сценарий:
# Сессия A:
UPDATE accounts SET balance = 500 WHERE id = 1;  -- Блокирует строку 1

# Сессия B:
UPDATE accounts SET balance = 600 WHERE id = 2;  -- Блокирует строку 2

# Сессия A:
UPDATE accounts SET balance = 700 WHERE id = 2;  -- Ждёт разблокировки строки 2 (ждёт B)

# Сессия B:
UPDATE accounts SET balance = 800 WHERE id = 1;  -- Ждёт разблокировки строки 1 (ждёт A)

# DEADLOCK! A ждёт B, B ждёт A
# БД обнаружит это и откатит одну из транзакций

Как избежать deadlock'ов:

# ✅ ПРАВИЛЬНО: всегда обновляй в одинаковом порядке
def transfer(from_id, to_id, amount):
    # Сортируем ID, чтобы всегда обновлять в одном порядке
    first_id, second_id = sorted([from_id, to_id])
    
    with transaction.atomic():
        first = Account.objects.select_for_update().get(id=first_id)
        second = Account.objects.select_for_update().get(id=second_id)
        
        if from_id == first_id:
            first.balance -= amount
            second.balance += amount
        else:
            first.balance += amount
            second.balance -= amount
        
        first.save()
        second.save()

Заключение

Схема транзакций в БД — это фундамент для надежной работы приложений. Ключевые моменты:

  1. ACID гарантирует целостность и надёжность
  2. Уровни изоляции контролируют компромисс между скоростью и безопасностью
  3. Atomicity предотвращает полусостояния
  4. Durability гарантирует, что данные сохранены после COMMIT
  5. Параллельность требует понимания deadlock'ов и блокировок

В Python используй встроенные механизмы (transaction.atomic в Django, session.begin в SQLAlchemy) вместо ручного управления.

Что такое схема транзакции базы данных? | PrepBro