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

Что такое гонка значений БД?

2.0 Middle🔥 161 комментариев
#Базы данных (NoSQL)

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

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

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

Гонка значений в базе данных

Гонка значений (Race Condition) в контексте БД — это ситуация, когда два или более потока или процесса попытаются одновременно прочитать и изменить одно и то же значение в базе данных, приводя к непредсказуемому и ошибочному результату. Проблема возникает, когда операции чтения и записи не являются атомарными (неделимыми) и не защищены синхронизацией.

Классический пример: счётчик

Представьте счётчик с текущим значением 100. Два параллельных процесса хотят увеличить его на 1:

Процесс 1:                    Процесс 2:
1. ПРОЧИТАТЬ: value = 100     
2.                            1. ПРОЧИТАТЬ: value = 100
3. ВЫЧИСЛИТЬ: 100 + 1         
4.                            2. ВЫЧИСЛИТЬ: 100 + 1
5. ЗАПИСАТЬ: value = 101      
6.                            3. ЗАПИСАТЬ: value = 101

Ожидается: value = 102 (два увеличения). Результат: value = 101 (обновление потеряно!).

Пример на Python с SQLAlchemy

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

Base = declarative_base()

class Counter(Base):
    __tablename__ = counters
    id = Column(Integer, primary_key=True)
    value = Column(Integer, default=0)

def increment_counter(session: Session, counter_id: int):
    # Опасная операция — гонка!
    counter = session.query(Counter).filter(Counter.id == counter_id).first()
    counter.value += 1
    session.commit()

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

# Создаём счётчик
with Session(engine) as session:
    counter = Counter(id=1, value=0)
    session.add(counter)
    session.commit()

# Запускаем 10 потоков, каждый добавляет 1
threads = []
for i in range(10):
    t = threading.Thread(target=increment_counter, args=(Session(engine), 1))
    threads.append(t)
    t.start()

for t in threads:
    t.join()

# Проверяем
with Session(engine) as session:
    counter = session.query(Counter).filter(Counter.id == 1).first()
    print(f"Ожидаем: 10, получили: {counter.value}")  # Скорее всего < 10!

Решение 1: SELECT FOR UPDATE (блокировка)

from sqlalchemy import select, update

def increment_counter_safe(session: Session, counter_id: int):
    # Блокируем строку на время транзакции
    counter = session.query(Counter).with_for_update().filter(Counter.id == counter_id).first()
    counter.value += 1
    session.commit()

SELECT FOR UPDATE блокирует строку, предотвращая одновременный доступ других процессов.

Решение 2: Атомарная операция в БД

from sqlalchemy import update, func

def increment_counter_atomic(session: Session, counter_id: int):
    # Инкрементирование происходит целиком в БД
    session.execute(
        update(Counter)
        .where(Counter.id == counter_id)
        .values(value=Counter.value + 1)
    )
    session.commit()

Это гарантирует атомарность на уровне БД — операция неделима.

Решение 3: SKIP LOCKED (для высоконагруженных систем)

from sqlalchemy import select

def process_queue(session: Session):
    # Получаем только строки, которые не заблокированы
    items = session.query(Task).with_for_update(skip_locked=True).limit(1).all()
    # Обрабатываем...
    session.commit()

Удобно для очередей задач — потоки не ждут блокировку, а берут следующий элемент.

Типы гонок в БД

  • Lost Update — одно обновление перезаписывает другое
  • Dirty Read — чтение незафиксированных данных
  • Non-repeatable Read — повторное чтение возвращает другое значение
  • Phantom Read — новые строки появляются при повторном запросе

Уровни изоляции транзакций

Уровень                      Lost Update    Dirty Read    Non-rep. Read    Phantom
─────────────────────────────────────────────────────────────────────────────────
READ UNCOMMITTED             Да             Да            Да               Да
READ COMMITTED               Нет            Нет           Да               Да
REPEATABLE READ              Нет            Нет           Нет              Да
SERIALIZABLE                 Нет            Нет           Нет              Нет

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

  • Используй SELECT FOR UPDATE для критичных операций
  • Предпочитай атомарные операции БД (UPDATE ... SET value = value + 1)
  • Для высоконагруженных систем применяй SKIP LOCKED
  • Минимизируй время блокировки — делай только необходимые операции в транзакции
  • Тестируй с многопоточностью и нагрузочными тестами

Гонка значений — серьёзная проблема, которая может привести к потере данных и непредсказуемому поведению. Правильный выбор уровня изоляции и механизмов блокировки критичен для надёжности приложения.

Что такое гонка значений БД? | PrepBro