← Назад к вопросам
Как будешь решать конфликт в базе данных при обращении к ней двух клиентов?
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) # Выполнится после первого
Практический выбор метода
| Сценарий | Решение | Плюсы | Минусы |
|---|---|---|---|
| Один сервер, простая app | SELECT FOR UPDATE | Надёжно | Может быть узким местом |
| Распределённая система | Оптимистичная блокировка | Масштабируется | Нужна логика retry |
| Очень высокая нагрузка | Очередь + асинхронность | Не блокирует | Сложнее в отладке |
| Финансовые операции | SERIALIZABLE уровень | Максимальная безопасность | Медленнее |
Итог
Для решения конфликтов используй:
- Транзакции как основу — всегда
- SELECT FOR UPDATE — когда один сервер
- Оптимистичную блокировку — для распределённых систем
- Асинхронные очереди — для микросервисов
- Правильный уровень изоляции — выбирай в зависимости от требований
Без этих подходов гарантированы потери данных и несогласованность.