Что такое схема транзакции базы данных?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
# Схема транзакции базы данных
Определение
Схема транзакции (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()
Заключение
Схема транзакций в БД — это фундамент для надежной работы приложений. Ключевые моменты:
- ACID гарантирует целостность и надёжность
- Уровни изоляции контролируют компромисс между скоростью и безопасностью
- Atomicity предотвращает полусостояния
- Durability гарантирует, что данные сохранены после COMMIT
- Параллельность требует понимания deadlock'ов и блокировок
В Python используй встроенные механизмы (transaction.atomic в Django, session.begin в SQLAlchemy) вместо ручного управления.