← Назад к вопросам
Какие могут быть проблемы с сериализацией в транзакциях?
3.0 Senior🔥 71 комментариев
#Архитектура и паттерны#Базы данных (SQL)
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Проблемы с сериализацией в транзакциях
Сериализация (Serialization) в контексте транзакций — это один из ключевых уровней изоляции в базах данных. Это часто путают с marshalling/pickling данных, но это разные концепции. Рассмотрю обе:
1. Уровни изоляции транзакций (ISO/IEC 27001)
# SQL изолированность определяется ACID свойствами
DATABASE ISOLATION LEVELS:
1. READ UNCOMMITTED (Грязное чтение)
2. READ COMMITTED (Фантомные читтения)
3. REPEATABLE READ (Потеря обновлений)
4. SERIALIZABLE (Полная изоляция)
2. Проблемы с низким уровнем изоляции
import psycopg2
# Проблема: Dirty Read (READ UNCOMMITTED)
# Транзакция читает незакоммиченные изменения другой транзакции
# Сценарий:
# Транзакция A: UPDATE accounts SET balance = 100 WHERE id = 1
# Транзакция B (параллельная): SELECT balance FROM accounts WHERE id = 1
# Результат: B получает 100 (незакоммиченное значение)
# Затем A делает ROLLBACK
# Баланс вернулся к исходному, но B уже использовал неверное значение
conn = psycopg2.connect("dbname=mydb")
conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_READ_UNCOMMITTED)
cursor = conn.cursor()
cursor.execute("SELECT balance FROM accounts WHERE id = %s", (1,))
balance = cursor.fetchone()
# Риск: это может быть грязное чтение
3. Phantom Read (READ COMMITTED)
# Проблема: Фантомные строки
# Сценарий:
# Транзакция A: SELECT COUNT(*) FROM orders WHERE date > '2024-01-01'
# Результат: 100 заказов
# [Параллельная Транзакция B: INSERT INTO orders ... WHERE date = '2024-02-01']
# Транзакция A: SELECT COUNT(*) FROM orders WHERE date > '2024-01-01'
# Результат: 101 заказ (появилась фантомная строка!)
def count_orders(min_date):
cursor = conn.cursor()
cursor.execute(
"SELECT COUNT(*) FROM orders WHERE date > %s",
(min_date,)
)
count = cursor.fetchone()[0]
# Риск: параллельные INSERT'ы добавят строки между запросами
return count
4. Lost Updates (Race Condition)
# Проблема: Потеря обновлений
# Сценарий банка:
# Начальный баланс: 1000
# Транзакция A:
# 1. SELECT balance FROM accounts WHERE id = 1 → 1000
# 2. UPDATE accounts SET balance = 1000 - 100 → 900
# Параллельная Транзакция B:
# 1. SELECT balance FROM accounts WHERE id = 1 → 1000
# 2. UPDATE accounts SET balance = 1000 - 50 → 950
# Результат: обновление B "теряется", финальный баланс 950 вместо 850
def withdraw(account_id, amount):
cursor = conn.cursor()
# ❌ Небезопасно
cursor.execute(
"SELECT balance FROM accounts WHERE id = %s",
(account_id,)
)
balance = cursor.fetchone()[0]
if balance >= amount:
cursor.execute(
"UPDATE accounts SET balance = %s WHERE id = %s",
(balance - amount, account_id)
)
conn.commit()
# ✅ Правильно: используй atomic операции
def withdraw_safe(account_id, amount):
cursor = conn.cursor()
cursor.execute(
"""
UPDATE accounts
SET balance = balance - %s
WHERE id = %s AND balance >= %s
""",
(amount, account_id, amount)
)
if cursor.rowcount == 0:
raise ValueError("Insufficient funds or account not found")
conn.commit()
5. Deadlock в транзакциях
# Проблема: взаимная блокировка (deadlock)
# Сценарий:
# Транзакция A: LOCK table1 → ждёт LOCK table2
# Транзакция B: LOCK table2 → ждёт LOCK table1
# Результат: дедлок, обе транзакции зависают
# ❌ Плохо: разный порядок locks
def transfer_bad(from_account, to_account, amount):
cursor = conn.cursor()
# В транзакции A этот порядок
cursor.execute("SELECT * FROM accounts WHERE id = %s FOR UPDATE", (from_account,))
cursor.execute("SELECT * FROM accounts WHERE id = %s FOR UPDATE", (to_account,))
# В параллельной транзакции B обратный порядок → дедлок!
# ✅ Правильно: последовательный порядок locks
def transfer_safe(from_account, to_account, amount):
cursor = conn.cursor()
# Всегда одинаковый порядок: меньший ID первым
first_id = min(from_account, to_account)
second_id = max(from_account, to_account)
cursor.execute("SELECT * FROM accounts WHERE id = %s FOR UPDATE", (first_id,))
cursor.execute("SELECT * FROM accounts WHERE id = %s FOR UPDATE", (second_id,))
# Безопасно от дедлока
6. Использование SELECT FOR UPDATE
# Правильный способ: явная блокировка на уровне statement
def update_with_lock(account_id, new_balance):
cursor = conn.cursor()
# Блокирует строку для других транзакций
cursor.execute(
"SELECT balance FROM accounts WHERE id = %s FOR UPDATE",
(account_id,)
)
current_balance = cursor.fetchone()[0]
# Теперь никто другой не может читать/писать эту строку
cursor.execute(
"UPDATE accounts SET balance = %s WHERE id = %s",
(new_balance, account_id)
)
conn.commit()
7. Сериализация объектов (pickle/JSON)
# Отдельная проблема: сохранение объектов в БД
import pickle
import json
# ❌ Опасно: pickle может выполнить произвольный код
class Account:
def __init__(self, balance):
self.balance = balance
account = Account(1000)
# Pickle в БД
serialized = pickle.dumps(account)
# Позже при загрузке из ненадёжного источника:
# malicious_object = pickle.loads(untrusted_data) # ОПАСНО!
# ✅ Безопасно: JSON
account_json = json.dumps({"balance": 1000})
# Загрузка: всегда только data structure, никаких executions
loaded = json.loads(account_json)
8. Optimistic vs Pessimistic Locking
# Pessimistic: блокируем сразу
def pessimistic_update(user_id, new_name):
cursor = conn.cursor()
cursor.execute(
"SELECT * FROM users WHERE id = %s FOR UPDATE",
(user_id,)
)
user = cursor.fetchone()
cursor.execute(
"UPDATE users SET name = %s WHERE id = %s",
(new_name, user_id)
)
conn.commit()
# Optimistic: используем version column
def optimistic_update(user_id, new_name, current_version):
cursor = conn.cursor()
cursor.execute(
"""
UPDATE users
SET name = %s, version = version + 1
WHERE id = %s AND version = %s
""",
(new_name, user_id, current_version)
)
if cursor.rowcount == 0:
raise ValueError("Concurrent modification detected")
conn.commit()
9. Правильный уровень изоляции
# Рекомендации для разных сценариев
def critical_financial_operations():
"""Финансовые операции требуют SERIALIZABLE"""
conn.set_isolation_level(
psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE
)
def read_heavy_operations():
"""Отчёты могут использовать READ COMMITTED"""
conn.set_isolation_level(
psycopg2.extensions.ISOLATION_LEVEL_READ_COMMITTED
)
def inventory_operations():
"""Инвентарь требует REPEATABLE READ"""
conn.set_isolation_level(
psycopg2.extensions.ISOLATION_LEVEL_REPEATABLE_READ
)
Лучшие практики
- Используй REPEATABLE READ минимум для критичных операций
- Избегай READ UNCOMMITTED и READ COMMITTED для financial операций
- Используй SELECT FOR UPDATE для явной блокировки
- Упорядочивай locks (всегда одинаковый порядок)
- Минимизируй время транзакции (меньше конфликтов)
- Используй optimistic locking для веб-приложений
- Не доверяй pickle — используй JSON для сериализации