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

Как будешь решать конфликт в базе данных при обращении к ней двух клиентов?

3.0 Senior🔥 111 комментариев
#Базы данных (SQL)

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

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

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

Разрешение конфликтов при одновременном доступе к БД

Это один из ключевых вопросов в системах с параллельным доступом. Я расскажу про основные подходы и их применение в Python.

Типы конфликтов

Два клиента хотят изменить один ресурс одновременно:

# Сценарий: два потока пытаются снять деньги со счёта
# Баланс: 1000 рублей

# Поток 1: читает баланс (1000) → снимает 500 → пишет 500
# Поток 2: читает баланс (1000) → снимает 700 → пишет 300 (ОШИБКА!)
# Итог: 300 рублей, хотя должно быть отрицательное значение или ошибка

Это race condition — оба клиента конкурируют за доступ к данным.

Решение 1: Транзакции с ACID свойствами

Лучший и основной подход — использовать транзакции:

from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import Session, declarative_base
from sqlalchemy.exc import SQLAlchemyError

Base = declarative_base()

class Account(Base):
    __tablename__ = "accounts"
    id = Column(Integer, primary_key=True)
    balance = Column(Integer, default=0)

engine = create_engine('postgresql://user:pass@localhost/db')
Base.metadata.create_all(engine)

def withdraw_money(account_id: int, amount: int):
    """Снимает деньги со счёта с гарантией консистентности"""
    session = Session(engine)
    try:
        # Начинаем транзакцию
        account = session.query(Account).filter(
            Account.id == account_id
        ).with_for_update().first()  # LOCK для этого счёта
        
        if account.balance < amount:
            raise ValueError("Недостаточно средств")
        
        account.balance -= amount
        session.commit()  # ACID гарантирует: все или ничего
        print(f"Снято {amount}, баланс: {account.balance}")
    except Exception as e:
        session.rollback()  # Откатываем всё в случае ошибки
        print(f"Ошибка: {e}")
    finally:
        session.close()

ACID свойства:

  • A (Atomicity) — операция либо полностью выполнена, либо откачена
  • C (Consistency) — БД переходит из одного консистентного состояния в другое
  • I (Isolation) — одновременные транзакции не мешают друг другу
  • D (Durability) — данные сохраняются после коммита

Решение 2: Блокировки (Locks)

SELECT FOR UPDATE — явная блокировка строки:

# Поток 1
session1 = Session(engine)
account = session1.query(Account).filter(
    Account.id == 1
).with_for_update().first()  # Блокируем эту строку

print(f"Поток 1: Блокировка получена, баланс = {account.balance}")
time.sleep(2)  # Симулируем долгую операцию

account.balance -= 500
session1.commit()
session1.close()
print("Поток 1: Коммит выполнен")

# Параллельно: Поток 2 ждёт, пока Поток 1 отпустит блокировку
session2 = Session(engine)
account2 = session2.query(Account).filter(
    Account.id == 1
).with_for_update(timeout=5).first()  # Ждёт до 5 секунд
# Если Поток 1 не отпустит блокировку, будет TimeoutError

print(f"Поток 2: Получена строка после разблокировки")
account2.balance -= 300
session2.commit()
session2.close()

Типы блокировок:

  • Shared Lock (READ) — много читателей могут одновременно, но никто не пишет
  • Exclusive Lock (WRITE) — только один писатель, никого больше нет

Решение 3: Уровни изоляции транзакций

Разные уровни изоляции обеспечивают разный уровень защиты:

from sqlalchemy.sql import text

engine = create_engine(
    'postgresql://user:pass@localhost/db',
    isolation_level="SERIALIZABLE"  # Максимальная изоляция
)

session = Session(engine)

# Уровни изоляции в PostgreSQL (от слабого к сильному):
# 1. READ UNCOMMITTED — может читать неподтверждённые данные
# 2. READ COMMITTED (по умолчанию) — видит только подтверждённые данные
# 3. REPEATABLE READ — одна транзакция видит снимок данных
# 4. SERIALIZABLE — полная изоляция, как будто транзакции выполняются последовательно

session.execute(text("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE"))

Решение 4: Оптимистичная блокировка (версионирование)

Когда блокировки невозможны (распределённые системы):

from sqlalchemy import Column, Integer, String, DateTime
import datetime

class Account(Base):
    __tablename__ = "accounts"
    id = Column(Integer, primary_key=True)
    balance = Column(Integer, default=0)
    version = Column(Integer, default=0)  # Версия для оптимистичной блокировки

def optimistic_withdraw(account_id: int, amount: int, expected_version: int):
    """Предполагаем, что конфликт редкий"""
    session = Session(engine)
    try:
        account = session.query(Account).filter(
            Account.id == account_id
        ).first()
        
        if account.version != expected_version:
            raise ValueError("Данные изменились, попробуй снова")
        
        account.balance -= amount
        account.version += 1  # Инкрементируем версию
        session.commit()
    except ValueError as e:
        session.rollback()
        print(f"Конфликт: {e}")
    finally:
        session.close()

# Использование:
account_data = get_account(1)  # {id: 1, balance: 1000, version: 5}
optimistic_withdraw(1, 500, account_data['version'])

Решение 5: Очереди и асинхронность

Для высоконагруженных систем:

import asyncio
from asyncio import Lock, Queue

class AccountManager:
    def __init__(self):
        self.locks = {}  # account_id → Lock
        self.queue = Queue()
    
    async def withdraw(self, account_id: int, amount: int):
        """Выполняет операции последовательно через очередь"""
        if account_id not in self.locks:
            self.locks[account_id] = Lock()
        
        async with self.locks[account_id]:  # Мьютекс в памяти
            account = await self.get_account(account_id)
            if account.balance < amount:
                raise ValueError("Недостаточно средств")
            
            account.balance -= amount
            await self.save_account(account)
            return account

# Использование
manager = AccountManager()
await manager.withdraw(1, 500)
await manager.withdraw(1, 300)  # Выполнится после первого

Практический выбор метода

СценарийРешениеПлюсыМинусы
Один сервер, простая appSELECT FOR UPDATEНадёжноМожет быть узким местом
Распределённая системаОптимистичная блокировкаМасштабируетсяНужна логика retry
Очень высокая нагрузкаОчередь + асинхронностьНе блокируетСложнее в отладке
Финансовые операцииSERIALIZABLE уровеньМаксимальная безопасностьМедленнее

Итог

Для решения конфликтов используй:

  1. Транзакции как основу — всегда
  2. SELECT FOR UPDATE — когда один сервер
  3. Оптимистичную блокировку — для распределённых систем
  4. Асинхронные очереди — для микросервисов
  5. Правильный уровень изоляции — выбирай в зависимости от требований

Без этих подходов гарантированы потери данных и несогласованность.

Как будешь решать конфликт в базе данных при обращении к ней двух клиентов? | PrepBro