← Назад к вопросам
Что такое SKIP LOCKED в SQL?
2.0 Middle🔥 191 комментариев
#SOLID и паттерны проектирования#Spring Boot и Spring Data
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
SKIP LOCKED в SQL: оптимистичное блокирование для высоконагруженных систем
SKIP LOCKED — это режим блокирования в SQL, который позволяет транзакции пропускать строки, которые уже заблокированы другими транзакциями, вместо того чтобы ждать. Это критичный инструмент для высоконагруженных систем (например, очереди обработки задач).
Проблема: обычная блокировка
-- Без SKIP LOCKED
BEGIN;
SELECT * FROM tasks WHERE status = 'pending' LIMIT 10 FOR UPDATE;
-- Если одна из строк уже заблокирована другой транзакцией,
-- эта транзакция ЗАВИСНЕТ, ждя освобождения
-- (может быть долго!)
COMMIT;
Проблема в высоконагруженной системе:
Транзакция A: SELECT ... FOR UPDATE на task_1
↓ (берет lock на task_1)
Транзакция B: SELECT ... FOR UPDATE на task_1
↓ (хочет эту же строку, жидет!!)
⏳ БЛОКИРОВКА (может быть 1 сек, 10 сек, 1 мин...)
Результат: очередь застаивается, throughput падает
Решение: SKIP LOCKED
-- С SKIP LOCKED
BEGIN;
SELECT * FROM tasks
WHERE status = 'pending'
LIMIT 10
FOR UPDATE SKIP LOCKED;
-- Если задача заблокирована — просто пропускаем её
-- Берем следующую доступную задачу
COMMIT;
Результат:
Транзакция A: SELECT ... FOR UPDATE SKIP LOCKED
↓ task_1 заблокирована, пропускаем
↓ task_2 свободна, берем её
Транзакция B: SELECT ... FOR UPDATE SKIP LOCKED
↓ task_3 заблокирована, пропускаем
↓ task_4 свободна, берем её
✅ Обе транзакции работают параллельно, никто не ждет!
Реальный пример: очередь обработки задач
// Задача: есть очередь из 10,000 задач
// 100 workers обрабатывают их параллельно
// Каждый worker должен взять следующую доступную задачу
@Repository
public class TaskRepository {
// ❌ БЕЗ SKIP LOCKED — workers ждут друг друга
@Query("""
SELECT * FROM tasks
WHERE status = 'pending'
ORDER BY created_at ASC
LIMIT 1
FOR UPDATE
""")
public Optional<Task> getNextTask() {
// Если task_1 заблокирована worker A,
// worker B зависнет, ждя её
// = throughput падает в N раз (где N = количество workers)
}
// ✅ СО SKIP LOCKED — workers независимо берут задачи
@Query("""
SELECT * FROM tasks
WHERE status = 'pending'
ORDER BY priority DESC, created_at ASC
LIMIT 1
FOR UPDATE SKIP LOCKED
""")
public Optional<Task> getNextTaskOptimized() {
// Если task_1 заблокирована worker A,
// worker B просто возьмет task_2, task_3 и т.д.
// = все workers работают параллельно на полной скорости!
}
}
@Service
public class TaskProcessingService {
private final TaskRepository taskRepository;
@Transactional
public void processNextTask() {
Optional<Task> task = taskRepository.getNextTaskOptimized();
if (task.isPresent()) {
// Обработка задачи
Task t = task.get();
System.out.println("Processing task: " + t.getId());
// Долгая операция (DB query, API call, etc)
Thread.sleep(1000);
// Отмечаем как завершенную
t.setStatus("completed");
taskRepository.save(t);
} else {
System.out.println("No tasks available");
}
}
}
Пример: система распределенных заказов
-- Таблица: заказы ждут распределения на курьеров
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
customer_id BIGINT,
status VARCHAR(20), -- pending, assigned, completed
created_at TIMESTAMP
);
-- Без SKIP LOCKED: проблема
BEGIN;
-- Курьер 1 берет заказ 1-10
SELECT * FROM orders
WHERE status = 'pending'
LIMIT 10
FOR UPDATE; -- заказы заблокированы
-- Курьер 2 хочет взять заказы
SELECT * FROM orders
WHERE status = 'pending'
LIMIT 10
FOR UPDATE; -- зависает! ждет, пока курьер 1 закончит
COMMIT;
-- Результат: только курьер 1 работает, остальные 99 ждут
-- = throughput: 1 курьер из 100
-- ✅ СО SKIP LOCKED: параллельная обработка
BEGIN;
SELECT * FROM orders
WHERE status = 'pending'
LIMIT 10
FOR UPDATE SKIP LOCKED; -- пропускает заблокированные
COMMIT;
-- Результат: все 100 курьеров работают независимо
-- = throughput: 100x лучше!
Пример: дистрибьютед очередь (Redis-style)
@Service
public class MessageQueueService {
private final jdbcTemplate: JdbcTemplate;
@Transactional
public Message consumeMessage() {
// Вариант 1: без SKIP LOCKED
// Если message_1 обрабатывается consumer A,
// consumer B зависнет, ждя message_1
// = только 1 consumer работает из 100
// Вариант 2: со SKIP LOCKED (правильно!)
List<Message> messages = jdbcTemplate.query(
"""
SELECT * FROM messages
WHERE status = 'pending'
ORDER BY created_at ASC
LIMIT 1
FOR UPDATE SKIP LOCKED
""",
new MessageRowMapper()
);
if (messages.isEmpty()) {
return null; // нет доступных сообщений
}
Message msg = messages.get(0);
// Обновляем статус
jdbcTemplate.update(
"UPDATE messages SET status = 'processing' WHERE id = ?",
msg.getId()
);
return msg;
}
}
SKIP LOCKED vs FOR UPDATE
┌─────────────────────────────────────────────────────────┐
│ │
│ FOR UPDATE (обычное) │
│ │
│ T1: SELECT * WHERE ... FOR UPDATE │
│ ↓ Берет EXCLUSIVE lock на строку │
│ │
│ T2: SELECT * WHERE ... FOR UPDATE │
│ ↓ Хочет тот же lock │
│ ↓ ЖДЕТ (может быть долго) │
│ ✅ Гарантирует, что ВСЕ нужные строки обработаны │
│ ❌ Но может быть медленно (много ожиданий) │
│ │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ │
│ FOR UPDATE SKIP LOCKED │
│ │
│ T1: SELECT * WHERE ... FOR UPDATE SKIP LOCKED │
│ ↓ Берет lock на строку 1 │
│ │
│ T2: SELECT * WHERE ... FOR UPDATE SKIP LOCKED │
│ ↓ Строка 1 занята, пропускаем │
│ ↓ Берем строку 2 (доступна) │
│ ✅ Быстро, параллельно │
│ ⚠️ Может обработать не ВСЕ строки (некоторые │
│ обработает другой процесс) │
│ │
└─────────────────────────────────────────────────────────┘
Важно: SELECT FOR UPDATE vs SELECT FOR UPDATE SKIP LOCKED
// Сценарий: есть 100 задач, 10 workers
// Без SKIP LOCKED
// Worker 1: SELECT ... FOR UPDATE
// -> берет lock на задачи 1-10
// Worker 2: SELECT ... FOR UPDATE
// -> видит, что 1-10 заблокированы
// -> ЖДЕТ (может быть 1 мин, пока worker 1 не завершит)
// Worker 3-10: также ждут
// = кол-во параллельных worker'ов = 1 (остальные idle)
// СО SKIP LOCKED
// Worker 1: SELECT ... FOR UPDATE SKIP LOCKED
// -> берет lock на задачи 1-10
// Worker 2: SELECT ... FOR UPDATE SKIP LOCKED
// -> видит, что 1-10 заблокированы, пропускает
// -> берет 11-20
// Worker 3: берет 21-30
// ... и так далее
// = все 10 workers работают параллельно!
// = throughput в 10x выше!
Примеры в разных СУБД
-- PostgreSQL (SKIP LOCKED)
SELECT * FROM tasks
WHERE status = 'pending'
FOR UPDATE SKIP LOCKED
LIMIT 10;
-- MySQL 8.0+ (SKIP LOCKED)
SELECT * FROM tasks
WHERE status = 'pending'
FOR UPDATE SKIP LOCKED
LIMIT 10;
-- Oracle (SKIP LOCKED)
SELECT * FROM tasks
WHERE status = 'pending'
FOR UPDATE SKIP LOCKED
FETCH FIRST 10 ROWS ONLY;
-- SQL Server (нет SKIP LOCKED, но есть альтернатива)
-- Используй READPAST вместо SKIP LOCKED (аналогично)
SELECT TOP 10 * FROM tasks
WHERE status = 'pending'
WITH (UPDLOCK, READPAST);
Когда использовать SKIP LOCKED
✅ Используй, если:
- Высоконагруженная система (много параллельных worker'ов)
- Очередь обработки задач
- Нужна максимальная throughput
- Не критично, что отдельные элементы обработает другой worker
❌ Не используй, если:
- Нужна гарантия обработки ВСЕХ строк в одной транзакции
- Мало параллельных процессов (нет конкуренции)
- Логика требует ACID консистентность (все или ничего)
Best Practices
@Service
public class TaskQueue {
private final jdbcTemplate: JdbcTemplate;
@Transactional
public void processTask() {
// 1. Используй SKIP LOCKED для взятия задачи
Task task = getNextTaskWithSkipLocked();
// 2. Обработай задачу
processTask(task);
// 3. Обнови статус
task.setStatus("completed");
save(task);
// Transaction commit -> lock освобождается
// Следующий worker может взять следующую задачу
}
private Task getNextTaskWithSkipLocked() {
return jdbcTemplate.queryForObject(
"""
SELECT * FROM tasks
WHERE status = 'pending'
ORDER BY priority DESC, created_at ASC
LIMIT 1
FOR UPDATE SKIP LOCKED
""",
new TaskRowMapper()
);
}
}
Вывод
SKIP LOCKED — это essential паттерн для высоконагруженных систем:
- Проблема: обычный FOR UPDATE блокирует workers, ждающих доступа
- Решение: SKIP LOCKED пропускает заблокированные строки
- Результат: все workers работают параллельно, throughput в 10-100x выше
- Применение: очереди задач, consumer-producer системы, распределенная обработка
Это критично для production систем, обрабатывающих миллионы событий в секунду.