Community写作与编辑github.com

scary-production/claude-plugins - Конкурентная запись в Postgre…

Набор инструментов и лучших практик для конкурентной записи в PostgreSQL: предотвращение потерянных обновлений, write skew, управление блокировками строк, exclusion-ограничения, изоляция и повтор транзакций, очереди задач с SKIP LOCKED, advisory-блокировки и отладка зависаний. Примеры на Go (pgx, go-transaction-manager).

claude-plugins - Конкурентная запись в Postgre… 是什么?

Этот скилл предоставляет комплексное руководство по конкурентной записи в PostgreSQL. Он охватывает ключевые проблемы многопользовательского доступа, такие как потерянные обновления (lost updates) и аномалии write skew, и предлагает конкретные решения для каждого типа инварианта: от простого UPDATE с условием до serializable-изоляции с повтором при ошибках 40001. Скилл подробно разбирает механизмы блокировок строк (FOR UPDATE, FOR NO KEY UPDATE, FOR KEY SHARE), exclusion-ограничения (с использованием btree_gist), построение очередей задач через SKIP LOCKED и advisory-блокировки. Особое внимание уделяется практическим аспектам: настройке lock_timeout, обработке ошибок (40001, 55P03, 23P01) и правильному проектированию транзакций. Примеры написаны на Go с использованием библиотек pgx и go-transaction-manager, что делает скилл готовым к применению в реальных проектах. Скилл поддерживается на любых платформах, где работает PostgreSQL, и не привязан к конкретным AI-агентам или IDE.

兼容平台✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/scary-production/claude-plugins/tree/HEAD/plugins/go-postgres/skills/postgres-concurrency

Installed? Explore more 写作与编辑 skills: steipete/notion, langchain-ai/langchain, bytedance/podcast-generation · View all 6 →

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

Конкурентная запись в PostgreSQL

Главное. Не делай «прочитал в коде — проверил — записал»: на read committed это теряет чужие правки. Выбирай средство по виду инварианта:

ИнвариантСредство
Изменить строку, если условие ещё верноодин UPDATE ... WHERE <условие> + проверка числа строк
Прочитать, решить в коде, записать ту же строкуSELECT ... FOR NO KEY UPDATE и запись в одной транзакции
Нет дубляуникальный индекс + INSERT ... ON CONFLICT
Интервалы не пересекаютсяEXCLUDE USING gist (btree_gist)
Правило по набору строк, которое не выразить ограничениемserializable у всех участников + повтор
Очередь задачFOR UPDATE SKIP LOCKED
Один обработчик на всехpg_advisory_xact_lock

Ограничение в базе надёжнее кода: оно держит инвариант при любом уровне изоляции и любом клиенте. Go-код транзакций и повтора — скилл pgx-txmanager. Проверка утверждений — CHECKS.md.

Уровни изоляции

  • Read committed (по умолчанию): каждая команда видит свежий снимок. UPDATE на занятой строке ждёт чужую транзакцию, потом перечитывает строку и заново проверяет WHERE.
  • Repeatable read и serializable не перечитывают: при конфликте — ошибка 40001 («could not serialize access»), serializable даёт её чаще. Поднял уровень — сразу пиши повтор всей транзакции, иначе пользователь получит ошибку. 40001 может прийти и на COMMIT.
  • Снимок repeatable read строит первая команда после BEGIN, а не сам BEGIN. Важен момент — первым запросом делай чтение.
  • Serializable гарантирует результат только среди serializable-транзакций. Охраняешь им инвариант — на serializable идут все транзакции, которые его трогают.
  • Внутри serializable смотри EXPLAIN. Seq Scan берёт предикатную блокировку на всю таблицу и поднимает долю 40001; чтение по индексу блокирует только прочитанные строки и страницы индекса. Заведи нужные индексы. На маленькой таблице Seq Scan нормален.

Изменить, если условие верно: условие в UPDATE

Передавай в базу действие, а не готовое число: SET sent = sent + 1, SET left = left - $1 WHERE left >= $1, WHERE id = $1 AND status = 'free'. Ноль затронутых строк (RowsAffected() == 0 или pgx.ErrNoRows у RETURNING) — условие не выполнено или строки нет. Это доменная ошибка, а не успех.

tag, err := q.Exec(ctx,
	`UPDATE seats SET status = 'booked', customer_id = $2
	 WHERE id = $1 AND status = 'free'`,
	seatID, customerID,
)
if err != nil {
	return fmt.Errorf("book seat %d: %w", seatID, err)
}
if tag.RowsAffected() == 0 { // место уже занято или его нет
	return fmt.Errorf("book seat %d: %w", seatID, ErrConflict)
}

На repeatable read и serializable тот же конфликт кончится 40001, и нужен повтор.

Прочитать и записать ту же строку

SELECT ... FOR NO KEY UPDATE (режим — в разделе «Режим блокировки») и запись — в одной транзакции (trManager.Do). В автокоммите блокировка снимется сразу после SELECT. Строку, которой ещё нет, блокировка не защитит: вставку двойника остановит только уникальный индекс или ограничение.

Решение по набору строк (write skew)

«Исполнитель свободен», «бронь влезает в интервал», «в смене остаётся дежурный»: две транзакции читают один снимок, каждая пишет свою строку, и обе коммитятся. На read committed так получаются две пересекающиеся брони. Repeatable read не спасает: он ловит только запись в одну и ту же строку.

  • Решение о существующих строках — заблокируй (FOR NO KEY UPDATE / FOR UPDATE) ровно те строки, по которым решаешь.
  • Решение об отсутствии строки («пересекающейся брони нет») — блокировать нечего. Нужно ограничение:
    • дубль по значению — уникальный индекс;

    • пересечение интервалов — exclusion-ограничение. На read committed вторая транзакция дождётся первой и получит 23P01, без serializable и без повторов:

      CREATE EXTENSION IF NOT EXISTS btree_gist; -- trusted: хватает права CREATE на базу
      ALTER TABLE bookings ADD CONSTRAINT bookings_no_overlap
          EXCLUDE USING gist (resource_id WITH =, during WITH &&); -- during tstzrange
      

      23P01 (exclusion_violation) переводи в доменную ошибку конфликта.

  • Правило не выражается ограничением — serializable для всех участников и повтор при 40001.

Режим блокировки строки

Блокируешь строку, чтобы прочитать и обновить, и не меняешь колонки с уникальным индексом, пригодным для внешнего ключа (PK, уникальные ключи; частичные индексы и индексы по выражению не считаются), — бери FOR NO KEY UPDATE. FOR UPDATE оставь для DELETE и смены таких колонок. Почему: проверка внешнего ключа при вставке дочерней строки берёт на родителе FOR KEY SHARE. Он конфликтует с FOR UPDATE и не конфликтует с FOR NO KEY UPDATE. Лишний FOR UPDATE на родительской строке (клиент, ресурс, заказ) останавливает вставку строк, которые на неё ссылаются.

Очередь задач

Несколько воркеров забирают задачу одним оператором:

UPDATE tasks
SET status = 'running', taken_at = now()
WHERE id = (
	SELECT id FROM tasks
	WHERE status = 'ready'
	ORDER BY id
	LIMIT 1
	FOR UPDATE SKIP LOCKED
)
RETURNING id
  • SKIP LOCKED пропускает занятые строки, и воркеры не ждут друг друга. Пара «SELECT ... FOR UPDATE SKIP LOCKED, потом UPDATE» в одной транзакции тоже корректна. SELECT без блокировки и отдельный UPDATE — нельзя: два воркера возьмут одну задачу.
  • ORDER BY в подзапросе — для предсказуемого порядка.
  • Воркер может упасть с задачей на руках. Практика: срок аренды по taken_at — задачу в running дольше срока снова считают готовой.
  • SKIP LOCKED даёт несогласованный вид данных — только для очередей.

Не висеть на чужой блокировке

По умолчанию запрос ждёт блокировку сколько угодно, и пользователь видит зависшее приложение.

  • SET LOCAL lock_timeout внутри транзакции, а не параметр сессии: соединение из пула потом уйдёт к другим запросам. Таймаут действует на каждое ожидание блокировки, а не на всю транзакцию.
  • У UPDATE и DELETE нет NOWAIT. Нужна мгновенная ошибка — сначала SELECT ... FOR UPDATE NOWAIT по этим строкам в той же транзакции.
  • NOWAIT и SKIP LOCKED касаются только строк. Табличную блокировку запрос ждёт как обычно, например за ALTER TABLE. Поэтому lock_timeout нужен и с ними.
  • Ошибка в этих случаях — 55P03 (lock_not_available). Повторяй всю транзакцию.
return s.trManager.Do(ctx, func(ctx context.Context) error {
	q := s.getter.DefaultTrOrDB(ctx, s.pool)
	// константа без ввода; значение подбери под операцию
	if _, err := q.Exec(ctx, `SET LOCAL lock_timeout = '1s'`); err != nil {
		return err
	}
	return s.repo.MoveBooking(ctx, from, to)
})

Очередь ожидания несправедливая. Новые FOR SHARE / FOR KEY SHARE могут обгонять ждущий UPDATE на горячей строке, и поток таких блокировок держит его сколько угодно; порядок среди ждущих не гарантирован. Для горячих строк (общий счётчик, остаток ходового товара) ставь lock_timeout и повторяй транзакцию.

Разбор зависания. Блокировки строк в pg_locks не видны: ждущий процесс виден там как ожидающий transactionid держателя. Кто блокирует, покажет pg_blocking_pids(pid) (в цикле часто не вызывай). Блокировки строк таблицы показывает расширение pgrowlocks из contrib (ставит суперпользователь).

Advisory-блокировки

«Только один обработчик» (одиночный воркер outbox, мигратор) — pg_advisory_xact_lock внутри транзакции: замок снимется сам на commit или rollback.

const advisoryOutboxRelay int64 = 1 // фиксированный ключ подсистемы

err := s.trManager.Do(ctx, func(ctx context.Context) error {
	q := s.getter.DefaultTrOrDB(ctx, s.pool)
	if _, err := q.Exec(ctx, `SELECT pg_advisory_xact_lock($1)`, advisoryOutboxRelay); err != nil {
		return err
	}
	return s.relayBatch(ctx) // одна пачка; замок уйдёт вместе с транзакцией
})
  • Сеансовый pg_advisory_lock — только на выделенном соединении из pool.Acquire; снимай на том же соединении столько раз, сколько захватывал. Сеансовый замок не снимается ни COMMIT, ни откатом, а тот же сеанс может взять его повторно. В пуле соединение вернётся с замком, unlock уйдёт на другое соединение, и замок «утечёт».
  • Не держи транзакцию открытой ради замка у бесконечного воркера: бери xact-замок на каждую пачку работы. Долгоживущему лидеру — сеансовый замок на выделенном соединении.
  • Ключ — фиксированная числовая константа на подсистему или пара (int, int). hashtext не бери: функция недокументированная, 32-битный хеш даёт коллизии.

Функции в запросах

Функцию на plpgsql или SQL, которая читает таблицы и вызывается в запросе рядом с чтением таблиц, помечай STABLE. VOLATILE берёт новый снимок на каждый запрос внутри себя, и на read committed данные таблицы и функции окажутся из разных моментов. IMMUTABLE — только функциям, которые таблиц не читают; VOLATILE — функциям с побочными эффектами.

Коды ошибок

КодЧтоЧто делать
40001 serialization_failureконфликт на repeatable read / serializableповторить всю транзакцию
40P01 deadlock_detectedвзаимная блокировкаповторить всю транзакцию; брать блокировки в одном порядке
55P03 lock_not_availableNOWAIT, lock_timeoutповторить всю транзакцию или отдать «занято»
23505 unique_violationдубльдоменный конфликт; штатный дубль — ON CONFLICT
23P01 exclusion_violationпересечение по exclusion-ограничениюдоменный конфликт

Individual skills in this repo

This repo contains 4 individual skills — each has its own dedicated page.

scary-production/claude-plugins

Работа с PostgreSQL в Go через pgxpool (pgx v5) и avito-tech/go-transaction-manager: пул и устойчивый старт, структуры с тегами db и CollectRows, репозиторий через DefaultTrOrDB, транзакции через Do, propagation и savepoint, повтор транзакции при 40001/40P01/55P03, защита от SQL-инъекций, классификация ошибок и ON CONFLICT, batch и CopyFrom. Конкурентная запись и блокировки — скилл postgres-concurrency. PostgreSQL in Go via pgxpool and go-transaction-manager: pool, repositories, transactions, retries, errors, bulk loads.

scary-production/claude-plugins

SQL-миграции PostgreSQL: парные up/down-файлы, современные типы и ограничения (identity, text с CHECK, timestamptz, jsonb), откат каждой операции, добавление значения в enum, индексы на живых таблицах через CREATE INDEX CONCURRENTLY без блокировки записи, что требовать от мигратора, разбор «индекс создан, а план — Seq Scan». PostgreSQL migrations: paired up/down files, modern types, reversible changes, non-blocking indexes, migrator requirements.

scary-production/claude-plugins

Как найти, что тормозит в PostgreSQL, и доказать, что правка помогла. Профиль по pg_stat_statements, сравнение вариантов по числу страниц (BUFFERS), а не по секундам, разбор плана от места, где оценка строк впервые разошлась с фактом. N+1 из Go-цикла сворачиваем в один запрос. Правим условия, FK, статистику и память, а не боремся с планировщиком. Postgres query tuning: profile first, compare by buffers, fix row estimates, remove N+1.

scary-production/claude-plugins

Показывает стили строки состояния на примере или включает один из них (full, compact, bars, minimal) в настройках пользователя или проекта.

相关技能