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

Какие могут быть проблемы с сериализацией в транзакциях?

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
    )

Лучшие практики

  1. Используй REPEATABLE READ минимум для критичных операций
  2. Избегай READ UNCOMMITTED и READ COMMITTED для financial операций
  3. Используй SELECT FOR UPDATE для явной блокировки
  4. Упорядочивай locks (всегда одинаковый порядок)
  5. Минимизируй время транзакции (меньше конфликтов)
  6. Используй optimistic locking для веб-приложений
  7. Не доверяй pickle — используй JSON для сериализации