← Назад к вопросам
Зачем нужно выражение SELECT FOR UPDATE в SQL?
2.2 Middle🔥 121 комментариев
#Базы данных (SQL)
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
# SELECT FOR UPDATE в SQL
SELECT FOR UPDATE — это выражение, которое блокирует выбранные строки на время транзакции, предотвращая их изменение другими процессами. Это критично для обеспечения data consistency в конкурентной среде.
Основная проблема
Без блокировок может возникнуть race condition:
# Без FOR UPDATE — НЕПРАВИЛЬНО
connection = get_db_connection()
# Шаг 1: Читаем текущий баланс
user = connection.query("SELECT balance FROM users WHERE id = ?", (user_id,))
current_balance = user['balance'] # 1000
# Между шагом 1 и 2 другой процесс может изменить баланс!
# Шаг 2: Обновляем баланс
new_balance = current_balance - 100 # 900
connection.execute(
"UPDATE users SET balance = ? WHERE id = ?",
(new_balance, user_id)
)
connection.commit()
Сценарий race condition:
- Процесс A читает balance = 1000
- Процесс B читает balance = 1000
- Процесс A снимает 100, записывает balance = 900
- Процесс B снимает 100, записывает balance = 900
- Итог: вместо 800 получилось 900 (потеря 100)
Решение: SELECT FOR UPDATE
# С FOR UPDATE — ПРАВИЛЬНО
from sqlalchemy import text
from sqlalchemy.orm import Session
def transfer_money(user_id: int, amount: float):
session = Session(engine)
try:
# Читаем и блокируем запись
user = session.query(User).with_for_update().filter(
User.id == user_id
).one()
# Теперь эта строка заблокирована
# Другие процессы не могут изменять этого пользователя
if user.balance >= amount:
user.balance -= amount
session.commit()
return True
else:
session.rollback()
return False
finally:
session.close()
Что происходит:
- Процесс A читает и блокирует balance = 1000
- Процесс B попытается прочитать и заблокировать — ждет
- Процесс A обновляет, коммитит, открывает блокировку
- Процесс B теперь читает balance = 900
- Итог: 900 - 100 = 800 (правильно)
Варианты блокировок
1 SELECT FOR UPDATE (эксклюзивная блокировка)
BEGIN;
SELECT * FROM users WHERE id = 123 FOR UPDATE;
UPDATE users SET balance = balance - 100 WHERE id = 123;
COMMIT;
- Эксклюзивная блокировка (EXCLUSIVE LOCK)
- Никто не может читать ИЛИ писать эту строку
- Максимальная защита, но медленнее
2 SELECT FOR UPDATE SKIP LOCKED
BEGIN;
SELECT * FROM orders
WHERE status = 'pending' AND worker_id IS NULL
FOR UPDATE SKIP LOCKED
LIMIT 5;
UPDATE orders
SET worker_id = 42
WHERE id IN (...);
COMMIT;
- Пропускает уже заблокированные строки
- Полезно для распределения работы между воркерами
- Не ждет, если строка заблокирована
3 SELECT FOR SHARE (read-only блокировка)
BEGIN;
SELECT * FROM users WHERE id = 123 FOR SHARE;
COMMIT;
- Несколько процессов могут читать одновременно
- Никто не может писать
- Слабее, чем FOR UPDATE
Практические примеры
Пример 1: Управление финансами
def withdraw_money(user_id: int, amount: float) -> bool:
"""Снять деньги со счета с гарантией консистентности."""
from sqlalchemy import text
with engine.begin() as connection:
# Читаем и блокируем
result = connection.execute(
text("""
SELECT balance FROM users
WHERE id = :user_id
FOR UPDATE
"""),
{"user_id": user_id}
)
user = result.first()
if not user or user[0] < amount:
return False
# Обновляем внутри той же транзакции
connection.execute(
text("UPDATE users SET balance = balance - :amount WHERE id = :user_id"),
{"amount": amount, "user_id": user_id}
)
return True
Пример 2: Обработка очереди задач
def get_next_task() -> Task:
"""Получить следующую задачу для обработки."""
from sqlalchemy import text
with engine.begin() as connection:
# Находим и блокируем свободную задачу
result = connection.execute(
text("""
SELECT id, data FROM tasks
WHERE status = 'pending'
ORDER BY created_at
LIMIT 1
FOR UPDATE SKIP LOCKED
""")
)
task = result.first()
if not task:
return None
# Помечаем как в обработке
connection.execute(
text("UPDATE tasks SET status = 'processing' WHERE id = :task_id"),
{"task_id": task[0]}
)
return Task(id=task[0], data=task[1])
Пример 3: Инвентаризация
def purchase_item(user_id: int, item_id: int, quantity: int) -> bool:
"""Купить товар, проверив наличие с блокировкой."""
from sqlalchemy import text
with engine.begin() as connection:
# Читаем и блокируем товар
result = connection.execute(
text("""
SELECT stock FROM inventory
WHERE item_id = :item_id
FOR UPDATE
"""),
{"item_id": item_id}
)
item = result.first()
if not item or item[0] < quantity:
return False
# Уменьшаем stock
connection.execute(
text("""
UPDATE inventory
SET stock = stock - :qty
WHERE item_id = :item_id
"""),
{"qty": quantity, "item_id": item_id}
)
# Создаем заказ
connection.execute(
text("""
INSERT INTO orders (user_id, item_id, quantity)
VALUES (:user_id, :item_id, :qty)
"""),
{"user_id": user_id, "item_id": item_id, "qty": quantity}
)
return True
Производительность и deadlocks
Потенциальная проблема: deadlock
# Процесс A
BEGIN;
SELECT * FROM users WHERE id = 1 FOR UPDATE; # Блокирует user 1
SELECT * FROM users WHERE id = 2 FOR UPDATE; # Ждет...
# Процесс B (в параллели)
BEGIN;
SELECT * FROM users WHERE id = 2 FOR UPDATE; # Блокирует user 2
SELECT * FROM users WHERE id = 1 FOR UPDATE; # Ждет...
# DEADLOCK!
Решение: всегда блокируй в одном порядке:
# Правильно
BEGIN;
SELECT * FROM users WHERE id IN (1, 2) ORDER BY id FOR UPDATE;
Когда использовать
✅ Используй FOR UPDATE:
- Финансовые операции
- Управление инвентарем
- Бронирование ресурсов
- Обработка конкурирующих запросов
- Критичные бизнес-операции
❌ Избегай FOR UPDATE:
- Простые SELECT запросы без изменений
- Высоконагруженные системы (может быть узким местом)
- Когда можно использовать optimistic locking
Альтернативы
Optimistic Locking (версионирование)
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
balance = Column(Float)
version = Column(Integer, default=0)
def withdraw_money(user_id: int, amount: float):
user = session.query(User).filter(User.id == user_id).one()
if user.balance >= amount:
user.balance -= amount
user.version += 1 # Увеличиваем версию
try:
session.commit()
except StaleDataError:
# Кто-то еще обновил запись
session.rollback()
return False
return True
Итог
SELECT FOR UPDATE — это мощный инструмент для обеспечения консистентности данных в конкурентной среде. Это особенно важно в микросервисной архитектуре, где множество процессов обращаются к одним данным. Правильное использование FOR UPDATE может спасти от критичных ошибок в логике бизнеса.