Перейти к содержимому

Транзакции, ACID, уровни изоляции и блокировки

Транзакция — это способ выполнить группу операций так, будто она одна: либо всё, либо ничего, и при этом не сойти с ума от конкурентных клиентов. Аббревиатура ACID описывает четыре гарантии. Atomicity — атомарность: набор изменений применяется целиком или откатывается целиком; в PostgreSQL это обеспечивается тем, что видимость версий строк привязана к номеру транзакции (xmin/xmax), а статус транзакции хранится в отдельной структуре pg_xact (clog) — «откат» это просто пометка «транзакция aborted», а не физический откат данных. Consistency — согласованность: транзакция переводит БД из одного корректного состояния в другое, где «корректность» задаётся ограничениями (PK, FK, CHECK, UNIQUE, триггеры) и инвариантами приложения. Isolation — изоляция: степень, в которой параллельные транзакции не видят «сырых» промежуточных состояний друг друга. Durability — долговечность: после успешного COMMIT данные переживут падение процесса и хоста.

Изоляция стоит денег, поэтому стандарт SQL определяет четыре уровня — Read Uncommitted, Read Committed, Repeatable Read, Serializable — через список аномалий, которые на них допустимы: грязное чтение (dirty read — увидели незакоммиченное), неповторяющееся чтение (non-repeatable read — та же строка при повторном чтении изменилась), фантомное чтение (phantom read — при повторном чтении по условию появились/исчезли строки). Стандарт описывает уровни в терминах блокировок, а реальные СУБД чаще реализуют их через MVCC (multiversion concurrency control), поэтому поведение конкретной СУБД надо знать отдельно. В PostgreSQL реально существует три уровня: Read Uncommitted молча работает как Read Committed (грязного чтения в MVCC-дизайне просто не бывает), по умолчанию — Read Committed, Repeatable Read даёт полноценный снимок данных и потому запрещает и фантомы тоже, а Serializable добавляет поверх снимка алгоритм SSI (Serializable Snapshot Isolation), который ловит аномалию записи (write skew) и откатывает одну из транзакций с ошибкой 40001. В MySQL/InnoDB по умолчанию Repeatable Read, а фантомы там гасятся gap- и next-key-локами.

Механика MVCC в PostgreSQL: UPDATE не меняет строку на месте, а создаёт новую версию кортежа и помечает старую как удалённую текущей транзакцией. Каждая транзакция при старте (Repeatable Read/Serializable) или при старте каждого оператора (Read Committed) получает снимок — фактически «список транзакций, которые на этот момент ещё не завершились». Видимость версии определяется её xmin/xmax и этим снимком. Отсюда два практических следствия, которые любят спрашивать: читатели никогда не блокируют писателей и наоборот (блокируются только писатель с писателем на одной строке), а мусор из старых версий убирает VACUUM, который не может почистить версии старше самой старой живой транзакции — поэтому одна забытая транзакция в состоянии idle in transaction раздувает таблицы и индексы на всей базе.

Блокировки — второй механизм, ортогональный MVCC. Они бывают уровня строки (SELECT ... FOR UPDATE, FOR NO KEY UPDATE, FOR SHARE, FOR KEY SHARE), уровня таблицы (от ACCESS SHARE у обычного SELECT до ACCESS EXCLUSIVE у DROP/большинства ALTER), и рекомендательные (advisory) — произвольные именованные локи по числовому ключу, которые СУБД просто хранит, не связывая с данными. Пессимистичная стратегия — заранее взять лок и держать его до конца транзакции; оптимистичная — не блокировать, а на записи проверить, что версия строки не изменилась, и при конфликте повторить. Пессимистичная выигрывает при высокой конкуренции за одни и те же строки, оптимистичная — при редких конфликтах и длинном «думании» между чтением и записью.

Отдельный крупный сюжет на собеседованиях — граница между транзакцией БД и внешним миром. Транзакция не распространяется на брокер сообщений, HTTP-вызов или кеш, поэтому «записали в БД и отправили в Kafka» (dual write) — это не атомарная операция. Каноническое решение — transactional outbox: событие пишется в таблицу той же транзакцией, что и бизнес-данные, а отдельный процесс (poller или CDC-читатель WAL) доставляет его в брокер минимум один раз.

Скрининг: Были стандартные вопросы по Go и спросили «что такое transaction outbox».

Заголовок раздела «Скрининг: Были стандартные вопросы по Go и спросили «что такое transaction outbox».»

Коротко. Transactional outbox — паттерн против проблемы двойной записи: событие для брокера сохраняется в таблицу outbox в той же транзакции БД, что и бизнес-изменение, а отдельный процесс-релей читает эту таблицу и публикует события в брокер, помечая отправленные. Так атомарность «изменили данные + гарантированно отправим событие» обеспечивается одной локальной транзакцией, без распределённых транзакций.

Глубже. Прямая отправка в брокер внутри транзакции ломается в обе стороны: отправили и упали до коммита — событие о несуществующем факте; закоммитили и упали до отправки — потерянное событие. Outbox переносит проблему в надёжное место: БД. Релей бывает двух видов — polling publisher (SELECT ... FROM outbox WHERE published_at IS NULL ORDER BY id FOR UPDATE SKIP LOCKED LIMIT 100, отправка, пометка) и CDC-вариант, когда события вычитываются из WAL логическим декодированием (Debezium, pgoutput, слоты репликации) — он не создаёт нагрузку опросом и не требует апдейта строк. Доставка получается at-least-once, поэтому потребитель обязан быть идемпотентным: дедуп по event_id в таблице-inbox или естественная идемпотентность операции. Порядок гарантируется только в пределах ключа партиционирования, поэтому в outbox обычно хранят aggregate_id, по которому и партиционируется топик.

BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
INSERT INTO outbox (id, aggregate_id, type, payload, created_at)
VALUES (gen_random_uuid(), 1, 'money.debited', '{"amount":100}'::jsonb, now());
COMMIT;

Что такое уровни изоляции транзакций? Какие проблемы они решают (с примерами)?

Заголовок раздела «Что такое уровни изоляции транзакций? Какие проблемы они решают (с примерами)?»

Коротко. Уровень изоляции — это контракт о том, какие аномалии конкурентного доступа допустимы. Стандарт определяет четыре уровня: Read Uncommitted (разрешены грязные чтения), Read Committed (грязных нет, но возможны неповторяющиеся и фантомные чтения), Repeatable Read (нет неповторяющихся; по стандарту фантомы возможны, в PostgreSQL — нет), Serializable (результат эквивалентен какому-то последовательному выполнению).

Глубже. Примеры аномалий. Грязное чтение: T1 списала деньги, но ещё не закоммитила, T2 видит новый баланс, T1 откатывается — T2 приняла решение по несуществующим данным. Неповторяющееся чтение: T1 читает balance = 100, T2 меняет на 50 и коммитит, T1 читает снова — уже 50, отчёт внутри одной транзакции противоречив. Фантомное чтение: T1 считает SELECT count(*) FROM orders WHERE user_id = 5, получает 3, T2 вставляет четвёртый заказ и коммитит, T1 повторяет запрос — 4. Write skew (не покрывается ничем ниже Serializable): двое дежурных, правило «хотя бы один на смене»; обе транзакции читают «нас двое», обе снимают себя с дежурства, инвариант нарушен, хотя ни одна строка не редактировалась дважды. В PostgreSQL Repeatable Read и Serializable при конфликте не ждут вечно, а падают с 40001 serialization_failure, поэтому приложение обязано уметь ретраить транзакцию целиком.

Коротко. ACID — атомарность, согласованность, изоляция, долговечность. Уровни изоляции (Read Uncommitted / Read Committed / Repeatable Read / Serializable) — это настраиваемая «сила» буквы I, компромисс между корректностью и конкурентностью. Блокировки — низкоуровневый механизм, которым СУБД разруливает конфликты записи; в MVCC-базах они нужны в основном писателям, а читатели работают по снимку.

Глубже. Три вещи полезно уметь связывать. Первое: в PostgreSQL при любом уровне изоляции два UPDATE одной строки сериализуются — второй ждёт на row lock. Отличается только то, что произойдёт после ожидания: в Read Committed оператор перечитает новую версию строки (EvalPlanQual) и применится к ней, в Repeatable Read/Serializable транзакция упадёт с 40001. Второе: явные блокировки (SELECT ... FOR UPDATE) позволяют получить сериализацию на нужных строках, не поднимая уровень изоляции всей транзакции — это самый частый в проде способ. Третье: любая блокировка в PostgreSQL держится до конца транзакции, освободить её раньше нельзя (кроме pg_advisory_unlock для сессионных advisory-локов), поэтому длина транзакции = длительность удержания локов.

Коротко. Чтобы группа изменений применялась как единое целое (при сбое посередине не остаётся полусостояния) и чтобы конкурентные клиенты не видели чужих промежуточных состояний и не затирали друг друга. Классический пример — перевод денег: списание и зачисление должны либо оба произойти, либо оба не произойти.

Глубже. Часто забывают, что транзакция полезна не только при сбоях, но и как единица согласованности для констрейнтов: внутри транзакции можно временно нарушить FK, если он DEFERRABLE INITIALLY DEFERRED, и проверка произойдёт на коммите. Ещё транзакция — единица видимости для читателей: в Repeatable Read длинный отчёт из десяти запросов увидит одно консистентное состояние базы, а не десять разных. Плата — удержание локов, рост числа версий строк и невозможность вакуумировать всё, что старше снимка транзакции.

Транзакции, для чего они нужны, какие есть уровни изоляции?

Заголовок раздела «Транзакции, для чего они нужны, какие есть уровни изоляции?»

Коротко. См. выше «Зачем нужны транзакции?» — атомарность группы изменений и изоляция от конкурентов. Уровней четыре: Read Uncommitted, Read Committed, Repeatable Read, Serializable; в PostgreSQL реально работают три, поскольку Read Uncommitted эквивалентен Read Committed.

Глубже. На таком «двойном» вопросе интервьюер обычно ждёт, что вы сами свяжете половинки: транзакции нужны ради ACID, а уровень изоляции — это ручка, которой регулируется буква I, потому что полная изоляция (Serializable) стоит либо блокировок, либо откатов на конфликтах. Хороший ответ заканчивается практикой: «дефолт Read Committed нас устраивает, точечно берём FOR UPDATE, а Serializable включаем на редких операциях, где важен инвариант по набору строк, и обязательно с ретраями».

Коротко. MVCC — это способ реализовать изоляцию без блокировок на чтение: вместо изменения строки на месте создаётся новая версия, а каждая транзакция видит тот набор версий, который был актуален на момент её снимка. Отсюда правило «читатели не блокируют писателей, писатели не блокируют читателей».

Глубже. В PostgreSQL у каждого кортежа есть системные поля xmin (транзакция-создатель) и xmax (транзакция-удалитель), плюс ctid — физический адрес версии. Снимок — это (xmin_snapshot, xmax_snapshot, xip_list): всё, что старше границы и не в списке активных, считается видимым, если создавшая транзакция закоммичена (статус берётся из pg_xact, для скорости кешируется в hint-битах самого кортежа). DELETE только проставляет xmax, UPDATE = DELETE + INSERT новой версии, при этом HOT-обновление (когда не меняются индексированные колонки и есть место на той же странице) позволяет не трогать индексы. Мусор убирает VACUUM, а autovacuum — по порогам autovacuum_vacuum_scale_factor/threshold. Побочные эффекты MVCC, о которых спрашивают: раздувание таблиц (bloat), необходимость VACUUM FREEZE из-за 32-битных идентификаторов транзакций и риска wraparound, и то, что count(*) в PostgreSQL всегда честно сканирует, потому что видимость зависит от снимка. В MySQL/InnoDB MVCC устроен иначе: строка обновляется на месте, а старые версии восстанавливаются из undo log, поэтому там нет bloat таблиц, зато есть история в undo и «слишком длинные» транзакции упираются в размер undo tablespace.

Запустится ли транзакция при одном запросе UPDATE?

Заголовок раздела «Запустится ли транзакция при одном запросе UPDATE?»

Коротко. Да. В PostgreSQL (как и в большинстве СУБД) любой оператор вне явного BEGIN выполняется в неявной транзакции — режим autocommit: сервер сам открывает транзакцию перед оператором и коммитит после его успешного завершения. Так что одиночный UPDATE атомарен: либо изменит все подходящие строки, либо ни одной.

Глубже. Практические следствия. UPDATE t SET x = x + 1 WHERE id = 1 полностью безопасен при конкуренции: чтение и запись происходят внутри одного оператора под row lock, гонки read-modify-write нет. А вот SELECT в приложении, потом арифметика в Go, потом UPDATE — это уже две транзакции и классический lost update. Отдельная деталь: если оператор упал посередине (например, на пятой строке нарушился CHECK), неявная транзакция откатится целиком — половины изменений не останется. Ещё нюанс: в PostgreSQL нельзя выполнить CREATE INDEX CONCURRENTLY, VACUUM и подобное внутри явного блока транзакции — именно потому, что им нужен autocommit-режим.

Была ли у тебя задача где требовалось работать с разными типами изоляции?

Заголовок раздела «Была ли у тебя задача где требовалось работать с разными типами изоляции?»

Коротко. Это вопрос про опыт — отвечать нужно конкретной историей: какая была бизнес-проблема, какую аномалию поймали, какой уровень выбрали и чем заплатили. Каркас: «дефолт Read Committed, столкнулись с <аномалия>, подняли до <уровень> / взяли FOR UPDATE, добавили ретраи на 40001, померили влияние на латентность».

Глубже. Интервьюер проверяет не факт, а понимание trade-off. Сильные сюжеты, которые почти у всех есть: (1) отчёт из нескольких запросов, где цифры не сходились между собой — лечится Repeatable Read, потому что нужен один снимок; (2) списание баланса — лечится не уровнем изоляции, а SELECT ... FOR UPDATE или атомарным UPDATE ... WHERE balance >= :amount; (3) «не больше N активных подписок на аккаунт» — это write skew, единственные честные решения — Serializable с ретраями либо материализация инварианта в виде строки-счётчика/уникального индекса, которую можно залочить. Типичная ошибка ответа — сказать «везде поставили Serializable»: это провоцирует лавину 40001 под нагрузкой и требует идемпотентных ретраев всей бизнес-операции.

Коротко. Тоже вопрос про опыт. Правильная структура ответа: как обнаружили (ошибка 40P01 deadlock detected в логах, deadlock_timeout по умолчанию 1 секунда, в логе PostgreSQL печатается граф ожидания с обоими запросами), в чём была причина (разный порядок захвата строк или таблиц) и как починили.

Глубже. Механика: PostgreSQL не предотвращает дедлоки, а обнаруживает их — если транзакция прождала лок дольше deadlock_timeout, запускается проверка графа ожиданий, и при найденном цикле одна из транзакций убивается с 40P01. Классические источники: два перевода денег A→B и B→A, где строки лочатся в порядке появления в коде; батчевый UPDATE ... WHERE id IN (...) без ORDER BY, из-за чего порядок захвата строк разный; апдейт родителя и вставка ребёнка с FK в разном порядке (FK берёт FOR KEY SHARE на родителе). Лечение по убыванию эффективности: единый порядок захвата ресурсов (например, всегда по возрастанию idSELECT ... WHERE id IN (...) ORDER BY id FOR UPDATE), укорачивание транзакций, SELECT ... FOR UPDATE NOWAIT/SKIP LOCKED там, где уместно, ретрай на 40P01 с джиттером. Важно уметь отличать дедлок от простого долгого ожидания лока: второе диагностируется через pg_locks + pg_stat_activity и лечится lock_timeout.

Коротко. См. выше «Что такое уровни изоляции транзакций?». Кратко для устного ответа: Read Uncommitted — разрешено грязное чтение; Read Committed — видим только закоммиченное, но каждый оператор берёт свежий снимок; Repeatable Read — один снимок на транзакцию, повторные чтения стабильны; Serializable — результат гарантированно эквивалентен последовательному выполнению.

Глубже. Отвечая устно, стоит сразу назвать матрицу «уровень × аномалия» и добавить две поправки к стандарту: PostgreSQL не имеет настоящего Read Uncommitted, а его Repeatable Read сильнее стандартного, потому что запрещает фантомы (это snapshot isolation). Отдельным пунктом — что цена высоких уровней в PostgreSQL не блокировки, а ошибки сериализации, то есть ретраи в коде приложения.

Коротко. Atomicity — a transaction is all-or-nothing. Consistency — a transaction moves the database from one valid state to another, preserving constraints and invariants. Isolation — concurrent transactions do not observe each other’s intermediate states (the exact guarantee depends on the isolation level). Durability — once committed, changes survive a crash.

Глубже. Полезно добавить, чем каждая буква обеспечена в реальной СУБД: атомарность — записью в WAL и статусом транзакции в pg_xact (в InnoDB — undo log), согласованность — констрейнтами и, честно говоря, самим приложением (это единственная буква, которая не про механику движка), изоляция — MVCC плюс блокировки, долговечность — fsync журнала на коммите и recovery-процедурой при старте. И отдельно: буква C в ACID и C в CAP — разные вещи, это классический уточняющий вопрос.

Describe the transactional outbox pattern and when to apply it. What other patterns do you know?

Заголовок раздела «Describe the transactional outbox pattern and when to apply it. What other patterns do you know?»

Коротко. Outbox применяют, когда одна операция должна атомарно изменить состояние в БД и опубликовать событие во внешнюю систему, а распределённых транзакций не хочется. Событие пишется в таблицу той же локальной транзакцией, релей (polling или CDC) публикует его в брокер как минимум один раз.

Глубже. Смежные паттерны, которые ждут в ответе: inbox (idempotent receiver) — таблица обработанных message_id на стороне потребителя, превращающая at-least-once в effectively-once; saga — длинная бизнес-операция как цепочка локальных транзакций с компенсациями, в хореографической (через события) или оркестрованной (через координатор) форме; 2PC/XA (PREPARE TRANSACTION в PostgreSQL) — настоящая распределённая транзакция, честная, но блокирующая и хрупкая к падению координатора, поэтому в микросервисах почти не применяется; CDC через логический слот WAL — публикация изменений напрямую из журнала, часто как реализация того же outbox; listen to yourself — сервис сначала публикует событие, а состояние меняет, потребив собственное событие; event sourcing — состояние есть свёртка журнала событий, тогда «двойной записи» нет по построению; idempotency key на входе API — защита от повторов на границе системы. Когда outbox не нужен: если потеря или дубль события не критичны, или если БД и брокер — это одно и то же хранилище.

Коротко. Транзакция — логически единая последовательность операций с БД, обладающая свойствами ACID: атомарность, согласованность, изоляция, долговечность. Границы задаются BEGIN и COMMIT/ROLLBACK, а без явного BEGIN каждый оператор — своя транзакция.

Глубже. См. подробности выше в «Explain ACID». Здесь стоит добавить про частичный откат: SAVEPOINT позволяет откатить часть транзакции, не теряя всю (ROLLBACK TO SAVEPOINT); в PostgreSQL это единственный способ продолжить работу после ошибки внутри транзакции, потому что после любой ошибки транзакция переходит в состояние aborted и все последующие операторы отвергаются с 25P02 current transaction is aborted. Именно на савпоинтах построена конструкция BEGIN ... EXCEPTION в PL/pgSQL, и злоупотребление ими стоит дорого — каждый савпоинт это подтранзакция со своим xid, а больше 64 подтранзакций на транзакцию вызывают переполнение subxid-кеша и деградацию (SubtransSLRU).

Назови уровни изолированности транзакций? Какой по умолчанию в Postrges? Что будет если установим Read Uncommitted?

Заголовок раздела «Назови уровни изолированности транзакций? Какой по умолчанию в Postrges? Что будет если установим Read Uncommitted?»

Коротко. Четыре уровня: Read Uncommitted, Read Committed, Repeatable Read, Serializable. По умолчанию в PostgreSQL — Read Committed. Если явно задать Read Uncommitted, команда выполнится без ошибки, но фактическое поведение будет как у Read Committed: грязное чтение в PostgreSQL невозможно в принципе.

Глубже. Причина в дизайне MVCC: видимость версии кортежа определяется статусом создавшей его транзакции, и незакоммиченная версия просто не проходит проверку видимости — реализовать «грязное чтение» пришлось бы отдельным кодом, которого нет. Проверить дефолт можно через SHOW default_transaction_isolation; или SELECT current_setting('transaction_isolation') внутри транзакции; поменять — SET default_transaction_isolation на уровне сессии/БД/пользователя, либо BEGIN ISOLATION LEVEL REPEATABLE READ, либо SET TRANSACTION ISOLATION LEVEL ... первым оператором транзакции. Для сравнения: в MySQL/InnoDB дефолт — Repeatable Read, и там Read Uncommitted работает по-настоящему.

Коротко. См. выше «Зачем нужны транзакции?»: чтобы набор изменений применялся атомарно и чтобы параллельные клиенты не портили друг другу данные и не видели промежуточных состояний.

Глубже. Короткая формулировка, которую удобно говорить вслух: «транзакция даёт две вещи — точку отката при сбое и границу видимости для конкурентов; всё остальное (уровни изоляции, локи) — это уже про то, насколько строго вторая гарантия соблюдается и сколько мы за неё платим».

Коротко. См. выше. Read Uncommitted → допустимо грязное чтение; Read Committed → нет грязных, но есть неповторяющиеся и фантомные; Repeatable Read → нет неповторяющихся (в PostgreSQL и фантомов тоже); Serializable → нет никаких аномалий, результат как при последовательном выполнении.

Базы данных: четвертая задача (транзакции + блокировки и прочие радости).

Заголовок раздела «Базы данных: четвертая задача (транзакции + блокировки и прочие радости).»

Коротко. Формулировка задачи не восстанавливается, но это типовая практическая секция. Каркас разбора любой такой задачи: определить инвариант, который надо сохранить; понять, на каком уровне изоляции он держится сам, а на каком нет; выбрать механизм (атомарный UPDATE, FOR UPDATE, advisory lock, уникальный индекс, Serializable + ретрай); проверить порядок захвата ресурсов на дедлоки; ограничить длительность транзакции таймаутами.

Глубже. Что чаще всего дают в таких задачах: перевод денег между счетами (ответ — лочить строки в детерминированном порядке по id, либо один UPDATE ... SET balance = balance - x WHERE id = ... AND balance >= x на списание и второй на зачисление в одной транзакции); бронирование мест (ответ — уникальный индекс на (event_id, seat_no) или FOR UPDATE на строку места, плюс EXCLUDE USING gist с tstzrange для интервалов); очередь заданий поверх таблицы (ответ — SELECT ... FOR UPDATE SKIP LOCKED LIMIT n); счётчик остатков товара (ответ — атомарный UPDATE ... WHERE qty >= n и проверка RowsAffected). Почти всегда правильный ответ короче, чем кажется: не поднимать уровень изоляции, а выразить инвариант так, чтобы его защищала сама СУБД (констрейнт или один атомарный оператор).

Масштабируемость: система должна легко масштабироваться горизонтально для обработки увеличивающегося количества товаров и транзакций;

Заголовок раздела «Масштабируемость: система должна легко масштабироваться горизонтально для обработки увеличивающегося количества товаров и транзакций;»

Коротко. Это требование из системного дизайна, а не вопрос. Ключ к ответу: горизонтально масштабируется stateless-слой приложения, а узкое место — БД, поэтому масштабируют её через реплики для чтения, партиционирование больших таблиц и шардирование по бизнес-ключу, при этом сознательно отказываясь от кросс-шардовых транзакций в пользу outbox/saga и идемпотентности.

Глубже. Практический порядок шагов: (1) сервисы без состояния, сессии и локи вынесены в БД/Redis, поэтому подов может быть сколько угодно; (2) чтения — на реплики, но помнить про lag и «read your own writes» (после записи читать с primary или через sticky-логику); (3) большие таблицы — декларативное партиционирование по времени или по хешу ключа, чтобы индексы и вакуум оставались управляемыми; (4) шардирование по tenant_id/user_id, когда одна машина перестаёт держать запись, — с этого момента транзакция может охватывать только один шард; (5) для операций через границу шарда — saga с компенсациями и outbox, а не 2PC; (6) горячие ключи (популярный товар) — разбивать счётчик на N подстрок и суммировать, или буферизовать декременты; (7) идемпотентность операций (ключ идемпотентности + inbox), потому что при горизонтальном масштабировании ретраи неизбежны; (8) пул соединений (PgBouncer в transaction pooling), иначе тысяча подов убьёт PostgreSQL количеством backend-процессов.

Коротко. См. выше «Расскажи про уровни изоляции транзакций» — тот же вопрос. Матрица: Read Uncommitted (dirty read возможен), Read Committed (non-repeatable + phantom), Repeatable Read (в PostgreSQL остаётся только write skew), Serializable (аномалий нет).

Как сделать блокировки чтобы записывать мог только один поток, а читать могли все?

Заголовок раздела «Как сделать блокировки чтобы записывать мог только один поток, а читать могли все?»

Коротко. Это shared/exclusive-семантика: множество читателей одновременно либо один писатель. В Go — sync.RWMutex (RLock/RUnlock для чтения, Lock/Unlock для записи). В PostgreSQL — SELECT ... FOR SHARE против FOR UPDATE на уровне строк, LOCK TABLE ... IN SHARE MODE / EXCLUSIVE MODE на уровне таблицы, либо advisory-локи в shared/exclusive варианте (pg_advisory_lock_shared / pg_advisory_lock).

Глубже. В Go важно помнить: RWMutex write-preferring — как только писатель встал в очередь на Lock, новые RLock начинают ждать, чтобы писатель не голодал; RLock не рекурсивен, вложенный RLock в той же горутине может привести к дедлоку по этой же причине. Выигрыш от RWMutex появляется только при по-настоящему читающей нагрузке и достаточно долгих критических секциях: на коротких секциях обычный Mutex часто быстрее, а при высокой конкуренции чтений на многих ядрах RWMutex упирается в атомарный счётчик читателей — тогда лучше atomic.Pointer с copy-on-write снимком или шардирование. В БД отдельно стоит подчеркнуть, что в PostgreSQL для «читать могут все» ничего делать не нужно: обычный SELECT благодаря MVCC не блокируется писателем вообще, FOR SHARE нужен только когда читатель хочет запретить изменение прочитанных строк до конца своей транзакции.

type Cache struct {
mu sync.RWMutex
m map[string]string
}
func (c *Cache) Get(k string) (string, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
v, ok := c.m[k]
return v, ok
}
func (c *Cache) Set(k, v string) {
c.mu.Lock()
defer c.mu.Unlock()
c.m[k] = v
}

Какие есть уровни изоляции и какие проблемы они решают?

Заголовок раздела «Какие есть уровни изоляции и какие проблемы они решают?»

Коротко. См. выше. Read Committed решает грязное чтение; Repeatable Read дополнительно — неповторяющееся чтение (а в PostgreSQL и фантомы); Serializable — все аномалии, включая write skew и аномалию только для чтения.

Глубже. Чтобы ответ не звучал заученно, полезно сказать, что «решает проблему» на разных уровнях означает разное: Read Committed решает грязное чтение бесплатно (само свойство MVCC), Repeatable Read решает неповторяемость ценой возможных ошибок сериализации на записи, а Serializable решает write skew ценой отслеживания предикатных зависимостей (SIReadLock в pg_locks), заметного роста числа откатов и требования писать ретраи.

Коротко. Обрывок исходника, вопрос не восстанавливается. По контексту это пункт условия задачи про сервис, который сначала выполняет транзакцию в БД, а затем отправляет сообщение в брокер, — разбор такой связки см. в следующем вопросе и в ответе про transactional outbox.

Отправляет информация о транзакции в некий абстрактный брокер.

Заголовок раздела «Отправляет информация о транзакции в некий абстрактный брокер.»

Коротко. Обрывок исходника, но узнаваемый: это вторая половина задачи про dual write — «закоммитили в БД, потом отправили в брокер». Правильный ответ на такую задачу: так делать нельзя, потому что между коммитом и отправкой процесс может умереть; нужен transactional outbox (или CDC из WAL) плюс идемпотентный потребитель.

Глубже. Проговорить стоит все четыре комбинации порядка: «отправили до коммита» даёт события о том, чего не случилось; «отправили после коммита» даёт потерянные события; «отправили внутри транзакции, откатились» — то же самое, что первое, так как брокер не участвует в откате; и только запись события в ту же БД той же транзакцией делает пару атомарной. Дальше — доставка релеем с гарантией at-least-once и дедупликация на стороне потребителя по message_id.

Какие есть иные механизмы чтобы уменьшить участок синхронизации помимо транзакционных блокировок?

Заголовок раздела «Какие есть иные механизмы чтобы уменьшить участок синхронизации помимо транзакционных блокировок?»

Коротко. Основные приёмы: выразить изменение одним атомарным оператором (UPDATE ... SET x = x - n WHERE id = ... AND x >= n) вместо read-modify-write; оптимистичная блокировка по версии; шардирование горячего ключа на несколько строк; вынесение всего, что не требует БД (HTTP-вызовы, расчёты, сериализация), за пределы транзакции; батчинг и агрегация изменений; переход на очередь с одним писателем на ключ; append-only журнал вместо обновления агрегата на месте.

Глубже. Каждый приём уменьшает время удержания лока или вероятность конфликта. Атомарный UPDATE сокращает критическую секцию до длительности одного оператора. Оптимистичная схема убирает лок вовсе, перенося цену на ретраи — выгодно при редких конфликтах. Шардирование счётчика (counter_shards(id, shard_no, value), инкремент случайного шарда, чтение как sum(value)) снимает конкуренцию за одну строку ценой более дорогого чтения. Партиционирование потока по ключу (Kafka-партиция по account_id, consumer-группа) даёт естественную сериализацию без локов вообще. Append-only ledger (INSERT проводки вместо UPDATE баланса) превращает конфликт записи в отсутствие конфликта, потому что вставки в разные строки друг другу не мешают; баланс тогда считается свёрткой или поддерживается материализованным снимком. Плюс инфраструктурные меры: lock_timeout, чтобы не копить очередь ожидающих, и SKIP LOCKED для очередей, чтобы воркеры не выстраивались в хвост за одной строкой.

Сталкивался ли с ошибками долгих транзакций (idle in transaction)?

Заголовок раздела «Сталкивался ли с ошибками долгих транзакций (idle in transaction)?»

Коротко. Вопрос про опыт. Суть проблемы: сессия открыла транзакцию и ничего не делает (state = 'idle in transaction' в pg_stat_activity), но её снимок держит горизонт видимости — VACUUM не может удалить мёртвые версии строк во всей базе, таблицы и индексы пухнут, планы деградируют, а в пределе растёт риск wraparound. Плюс транзакция продолжает держать взятые локи, блокируя чужие ALTER TABLE и апдейты.

Глубже. Как ловят: SELECT pid, state, xact_start, now() - xact_start AS age, query FROM pg_stat_activity WHERE state LIKE 'idle in transaction%' ORDER BY age DESC; и мониторинг age(backend_xmin). Как лечат: idle_in_transaction_session_timeout (обрывает такие сессии), statement_timeout, lock_timeout, transaction_timeout (появился в PostgreSQL 17), а на стороне приложения — context.WithTimeout на всю транзакцию и обязательный defer tx.Rollback(). Типичные причины в Go-сервисах: транзакция открыта, а внутри делается HTTP-вызов или ожидание ответа от другого сервиса; забытый Rollback на ветке ошибки; rows не закрыт, из-за чего соединение не возвращается в пул с завершённой транзакцией; и «транзакция на весь HTTP-хендлер» как архитектурный шаблон.

Как может помочь версионирование баланса при блокировках?

Заголовок раздела «Как может помочь версионирование баланса при блокировках?»

Коротко. Версия (или сам баланс как «ожидаемое значение») превращает пессимистичную блокировку в оптимистичную: читаем balance, version без лока, считаем новое значение, пишем UPDATE ... SET balance = :new, version = version + 1 WHERE id = :id AND version = :old. Если затронуто 0 строк — кто-то нас опередил, читаем заново и повторяем. Лока между чтением и записью нет, а lost update невозможен.

Глубже. Это CAS на уровне БД. Выигрыш: между чтением и записью может пройти сколько угодно времени (например, пользователь думает на форме) — никто не держит лок; нет риска дедлока в этом месте; хорошо работает при низкой конкуренции за конкретный счёт. Проигрыш: при высокой конкуренции за одну строку доля неудачных попыток растёт, и суммарно это может быть дороже, чем честная очередь на FOR UPDATE. Версия полезна и вне конкуренции: она даёт ETag для HTTP (If-Match), позволяет обнаружить, что закешированное значение устарело, и делает конфликт видимым бизнес-логике («данные изменились, обновите страницу») вместо тихого затирания. Для чисто денежных операций, впрочем, чаще проще и надёжнее один атомарный UPDATE accounts SET balance = balance - :amt WHERE id = :id AND balance >= :amt — он не требует версии и не даёт ложных конфликтов, потому что не читает значение в приложение.

Что не хватает в методе MakeTransaction чтобы обезопасить от возможных долгих операций?

Заголовок раздела «Что не хватает в методе MakeTransaction чтобы обезопасить от возможных долгих операций?»

Коротко. Кода метода нет, но список ожидаемого стандартный: пробрасываемый context с дедлайном (BeginTx(ctx, ...) и все запросы через ...Context), серверные таймауты statement_timeout и lock_timeout на сессию транзакции, гарантированный defer tx.Rollback(), отсутствие любых внешних вызовов внутри транзакции и ограничение объёма работы (батчи вместо «обновим миллион строк одной транзакцией»).

Глубже. Минимальный безопасный каркас в Go выглядит так: контекст с таймаутом создаётся снаружи транзакции и покрывает её целиком; сразу после BeginTx выставляются серверные таймауты, чтобы клиентский дедлайн не оставил висеть запрос на сервере; defer tx.Rollback() безопасен после Commit (вернёт sql.ErrTxDone, его игнорируют); ретраи делаются вокруг всей функции, а не отдельного запроса, и только для кодов 40001/40P01.

func MakeTransaction(ctx context.Context, db *sql.DB, from, to int64, amount int64) error {
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})
if err != nil {
return err
}
defer tx.Rollback() //nolint:errcheck // no-op после Commit
if _, err = tx.ExecContext(ctx, "SET LOCAL lock_timeout = '2s'"); err != nil {
return err
}
if _, err = tx.ExecContext(ctx, "SET LOCAL statement_timeout = '3s'"); err != nil {
return err
}
res, err := tx.ExecContext(ctx,
`UPDATE accounts SET balance = balance - $1 WHERE id = $2 AND balance >= $1`,
amount, from)
if err != nil {
return err
}
if n, _ := res.RowsAffected(); n == 0 {
return errors.New("insufficient funds")
}
if _, err = tx.ExecContext(ctx,
`UPDATE accounts SET balance = balance + $1 WHERE id = $2`, amount, to); err != nil {
return err
}
return tx.Commit()
}

Коротко. Транзакция — неделимая с точки зрения СУБД последовательность операций, которая либо применяется целиком, либо не применяется вовсе, и выполняется с заданной степенью изоляции от других транзакций.

Глубже. Технически транзакция в PostgreSQL — это идентификатор xid, снимок видимости и набор захваченных локов; её жизненный цикл — BEGIN (в момент первого изменения выделяется настоящий xid), выполнение операторов, COMMIT (запись commit-записи в WAL, fsync, пометка статуса в pg_xact) или ROLLBACK (пометка aborted, данные остаются мусором до VACUUM). Полезная деталь для ответа: read-only транзакция не получает настоящего xid (используется виртуальный), поэтому «пустые» транзакции не расходуют пространство идентификаторов, но снимок всё равно держат.

Есть 3 пода все пишут в таблицу. Как можно надо защитить данные от гонки?

Заголовок раздела «Есть 3 пода все пишут в таблицу. Как можно надо защитить данные от гонки?»

Коротко. Гонку между процессами надо разруливать в БД, а не в приложении: уникальный констрейнт плюс INSERT ... ON CONFLICT для дублей, атомарный UPDATE ... WHERE <условие> вместо read-modify-write, SELECT ... FOR UPDATE при необходимости залочить строку на время транзакции, оптимистичная блокировка по версии, SKIP LOCKED для распределения заданий, advisory lock — если синхронизировать нужно не строку, а логическую операцию.

Глубже. Мьютекс в Go тут бесполезен: он локален для процесса, а подов три (и завтра пять). Выбор механизма зависит от вида гонки. Гонка «оба вставляют одну сущность» — уникальный индекс, это единственная защита, которая работает даже при Serializable без ретраев: INSERT ... ON CONFLICT (key) DO NOTHING/UPDATE. Гонка «оба читают-меняют-пишут» (lost update) — либо один атомарный UPDATE, либо FOR UPDATE, либо версия. Гонка «оба взяли одно и то же задание» — SELECT id FROM jobs WHERE status='new' ORDER BY id FOR UPDATE SKIP LOCKED LIMIT 1. Гонка «должен выполниться ровно один экземпляр периодической задачи» — pg_try_advisory_lock(key) или leader election. Гонка «инвариант по набору строк» (не больше N чего-то) — Serializable с ретраем на 40001 или материализация инварианта в отдельную строку-счётчик, которую можно залочить.

Что такое транзакции, какие у них основные принципы?

Заголовок раздела «Что такое транзакции, какие у них основные принципы?»

Коротко. См. выше «Что такое транзакция?». Основные принципы — ACID: атомарность, согласованность, изоляция, долговечность.

Глубже. Если хочется добавить глубины, стоит упомянуть, что на практике «принципы» уточняются двумя вещами: уровнем изоляции (буква I — это не бинарное свойство, а настраиваемая шкала) и настройками долговечности (synchronous_commit можно выключить и обменять D на пропускную способность). Это показывает, что вы понимаете ACID как набор инженерных компромиссов, а не как заклинание.

Коротко. Read Uncommitted, Read Committed, Repeatable Read, Serializable — в порядке усиления гарантий. В PostgreSQL Read Uncommitted эквивалентен Read Committed, дефолт — Read Committed; в MySQL/InnoDB дефолт — Repeatable Read.

Глубже. Самый частый вопрос темы (встретился в подборке пять раз), поэтому ответ должен быть отточен: назвать четыре уровня, назвать три аномалии, дать матрицу соответствия, указать дефолты и сказать про особенности PostgreSQL (нет реального Read Uncommitted, RR запрещает фантомы, конфликты приводят к 40001, а не к ожиданию).

Коротко. Вопрос-разведка. Отвечать не «да», а коротким конкретным кейсом: где именно применяли, какой уровень изоляции, как обрабатывали ошибки и ретраи, как следили за длительностью транзакций.

Глубже. Хороший ответ на 30–60 секунд: «В сервисе платежей все операции со списанием шли в одной транзакции с атомарным UPDATE ... WHERE balance >= :amt и проверкой RowsAffected; на уровне инфраструктуры — context с дедлайном, statement_timeout, defer Rollback, ретраи на 40001/40P01; события во внешние системы отправляли через outbox, чтобы не делать сетевых вызовов внутри транзакции; мониторили idle in transaction и bloat». Дальше интервьюер обычно цепляется за одну из деталей — и это как раз то, что нужно.

Коротко. См. выше «Что такое транзакция?» — единица работы с БД, обладающая свойствами ACID; границы — BEGIN/COMMIT/ROLLBACK, вне явных границ каждый оператор образует собственную транзакцию (autocommit).

Коротко. Atomicity (всё или ничего), Consistency (переход между корректными состояниями с соблюдением ограничений), Isolation (независимость от параллельных транзакций в рамках выбранного уровня), Durability (закоммиченное не теряется при сбое).

Глубже. Детали реализации каждой буквы см. в ответе «Explain ACID» и в отдельном вопросе про durability ниже. Полезно добавить, что не все хранилища дают ACID целиком: многие NoSQL ограничиваются атомарностью на уровне одного документа/ключа, а «BASE» — это осознанный отказ от строгой согласованности ради доступности.

Коротко. Фантомное чтение — когда транзакция дважды выполняет один и тот же запрос с условием (диапазон, фильтр), и во второй раз в выборку попадают новые строки (или исчезают старые), вставленные/удалённые другой закоммитившейся транзакцией. Отличается от неповторяющегося чтения тем, что меняется не значение существующей строки, а состав множества строк.

Глубже. Пример: транзакция проверяет «у пользователя меньше 5 активных заказов», получает 4, параллельная транзакция вставляет пятый и коммитится, первая вставляет свой — стало 6, инвариант нарушен. По стандарту фантомы разрешены вплоть до Serializable, но в PostgreSQL уже Repeatable Read их не допускает, потому что вся транзакция работает по одному снимку: строка, созданная после снимка, невидима. Однако снимок не спасает от логической проблемы из примера — это уже write skew, и его ловит только Serializable (SSI берёт предикатные SIRead-локи и обнаруживает опасные циклы rw-зависимостей). В MySQL/InnoDB на Repeatable Read фантомы для чтений закрываются снимком, а для блокирующих чтений (SELECT ... FOR UPDATE) — gap- и next-key-локами, которые запрещают вставку в диапазон.

Рассмотрим уровень изоляции транзакций Read Committed. Запущены две параллельные транзакции. Вторая транзакция вносит изменения в данные и успешно коммитится, пока первая еще активна. Увидит ли первая транзакция эти изменения при следующем чтении тех же данных?

Заголовок раздела «Рассмотрим уровень изоляции транзакций Read Committed. Запущены две параллельные транзакции. Вторая транзакция вносит изменения в данные и успешно коммитится, пока первая еще активна. Увидит ли первая транзакция эти изменения при следующем чтении тех же данных?»

Коротко. Да, увидит. На Read Committed снимок берётся заново на каждый оператор, поэтому повторный SELECT внутри той же транзакции покажет уже закоммиченные чужие изменения. Это и есть неповторяющееся чтение — штатное поведение уровня, а не баг.

Глубже. Если такое поведение нежелательно (например, отчёт из нескольких запросов должен быть консистентным), нужен Repeatable Read: снимок фиксируется на момент первого оператора транзакции и не обновляется до конца. Отдельный нюанс Read Committed: при UPDATE, который наткнулся на строку, изменённую параллельной транзакцией, PostgreSQL после её коммита не берёт старую версию, а перечитывает новую и заново проверяет на ней условие WHERE (механизм EvalPlanQual). Из-за этого возможны неинтуитивные результаты в сложных запросах — ещё одна причина не полагаться на read-modify-write через приложение.

Какой уровень изоляции стоит выбрать для того, чтобы гарантированно работать с данными, не позволяя другим транзакциям их изменять?

Заголовок раздела «Какой уровень изоляции стоит выбрать для того, чтобы гарантированно работать с данными, не позволяя другим транзакциям их изменять?»

Коротко. Строго говоря, уровень изоляции сам по себе не запрещает другим транзакциям менять данные — он лишь определяет, что вы увидите. Чтобы реально запретить изменения конкретных строк, нужен явный лок: SELECT ... FOR UPDATE (или FOR NO KEY UPDATE/FOR SHARE, если достаточно слабее). Если нужен инвариант по целому множеству строк, включая ещё не существующие, — Serializable с ретраями на 40001.

Глубже. Разница принципиальная и часто спрашивается «в лоб». Repeatable Read даёт вам стабильную картину, но параллельная транзакция спокойно изменит и закоммитит те же строки; вы просто узнаете об этом при попытке записи, получив could not serialize access due to concurrent update. SELECT ... FOR UPDATE физически ставит других писателей этой строки в очередь до конца вашей транзакции. Serializable не блокирует, а гарантирует, что итог будет эквивалентен какому-то последовательному порядку, откатывая нарушителей. Практический выбор: точечная защита строк — FOR UPDATE; защита сложного инварианта — Serializable; защита логической операции целиком — advisory lock.

Есть ли опыт работы с блокировками? Какие использовали в работе?

Заголовок раздела «Есть ли опыт работы с блокировками? Какие использовали в работе?»

Коротко. Вопрос про опыт. Стоит перечислить конкретику по уровням: строковые (FOR UPDATE, FOR NO KEY UPDATE, FOR UPDATE SKIP LOCKED для очередей), табличные (ловили ACCESS EXCLUSIVE от миграций и лечили lock_timeout + ретраи), advisory (pg_try_advisory_lock для единственного экземпляра фоновой задачи) и клиентские (sync.Mutex/RWMutex внутри процесса, распределённый лок в Redis, когда он допустим).

Глубже. Сильный ответ включает историю про боль. Например: миграция ALTER TABLE ... ADD COLUMN ... DEFAULT на старой версии PostgreSQL переписывала таблицу под ACCESS EXCLUSIVE и уложила прод — с тех пор миграции идут с lock_timeout = '2s', ADD COLUMN без volatile-дефолта (с PostgreSQL 11 добавление колонки с константным дефолтом не переписывает таблицу), индексы создаются CONCURRENTLY, констрейнты добавляются как NOT VALID с последующим VALIDATE CONSTRAINT. Или: очередь заданий на FOR UPDATE без SKIP LOCKED выстраивала воркеров в один хвост — добавили SKIP LOCKED, и пропускная способность выросла линейно по числу воркеров. Стоит явно сказать, что для распределённых локов в Redis без fencing-токена нет строгих гарантий, поэтому критичные вещи (деньги) на них не строятся.

В PostgreSQL уровни изоляции, от чего они защищают?

Заголовок раздела «В PostgreSQL уровни изоляции, от чего они защищают?»

Коротко. Read Committed защищает от грязного чтения; Repeatable Read дополнительно — от неповторяющегося чтения и фантомов (потому что это snapshot isolation); Serializable — ещё и от write skew и аномалии только для чтения, то есть от всех аномалий сериализации. Read Uncommitted в PostgreSQL не отличается от Read Committed.

Глубже. Важное уточнение, которое отличает хороший ответ: ни один уровень изоляции в PostgreSQL не защищает от lost update, если вы делаете read-modify-write через приложение на Read Committed — там просто выиграет последний писатель. На Repeatable Read/Serializable этот же сценарий не потеряет данные, но упадёт с 40001, и без ретрая пользователь получит ошибку. То есть «защита» на верхних уровнях — это не «всё волшебно работает», а «аномалия превращается в явную ошибку, которую обязано обработать приложение».

Примитивы синхронизации виды? атомик это? синк мапа ээ? синк пул это? Mutex/RwMutex отличия? Чем отличаются блокировки на запись от блокировок на чтение, RLock и Lock отличие?

Заголовок раздела «Примитивы синхронизации виды? атомик это? синк мапа ээ? синк пул это? Mutex/RwMutex отличия? Чем отличаются блокировки на запись от блокировок на чтение, RLock и Lock отличие?»

Коротко. В Go основные примитивы: sync.Mutex и sync.RWMutex (взаимное исключение), sync.WaitGroup (дождаться группу горутин), sync.Once (однократная инициализация), sync.Cond (ожидание условия), каналы (передача владения данными), пакет sync/atomic (атомарные операции над словом или указателем), sync.Map и sync.Pool как специализированные структуры, плюс golang.org/x/sync/semaphore и errgroup.

Глубже. atomic — это атомарные операции процессора над одним значением: atomic.Int64.Add, CompareAndSwap, atomic.Pointer[T].Store/Load; с Go 1.19 есть типы-обёртки, которые снимают проблему выравнивания 64-битных полей на 32-битных платформах. Он дешевле мьютекса, но защищает ровно одну ячейку, а не инвариант между несколькими. sync.Map оптимизирована ровно под два сценария из документации: (1) ключ пишется один раз и много раз читается (растущий кеш) и (2) разные горутины работают с непересекающимися наборами ключей; в остальных случаях map + RWMutex или шардированная мапа быстрее и типобезопаснее. sync.Pool — кеш временных объектов для снижения нагрузки на GC (типовое применение — буферы), содержимое может быть выброшено в любой момент, в том числе на каждом цикле GC, поэтому класть туда что-то ценное нельзя; внутри — per-P локальные очереди, чтобы избежать конкуренции. Mutex против RWMutex: первый пускает одного, второй — либо много читателей, либо одного писателя. RLock/RUnlock берут разделяемую блокировку (совместимы между собой), Lock/Unlock — исключительную (несовместима ни с чем). RWMutex write-preferring: ожидающий писатель блокирует новых читателей, зато RLock не рекурсивен и не должен вкладываться. Ни Mutex, ни RWMutex не реентерабельны, и оба нельзя копировать после первого использования (go vet это ловит).

Коротко. Обрывок исходника, вопрос не восстанавливается. Если имелось в виду блокирование горутины/операции — см. ответы про блокировки и sync.Mutex выше.

Коротко. Обрывок исходника, вопрос не восстанавливается. Скорее всего, это ссылка на mu.Lock() из соседнего вопроса про Lock/RLock — разбор см. выше и ниже в вопросе «Чем отличаются методы mu.Lock и mu.RLock?».

о базах данных и транзакциях; о шинах данных и паттернах работы с ними.

Заголовок раздела «о базах данных и транзакциях; о шинах данных и паттернах работы с ними.»

Коротко. Не вопрос, а перечень тем секции. Ожидаемый минимум: транзакции и ACID, уровни изоляции и аномалии, MVCC и блокировки в PostgreSQL — с одной стороны; брокеры (Kafka/RabbitMQ), гарантии доставки (at-most-once / at-least-once / effectively-once), идемпотентность, порядок сообщений и паттерны outbox/inbox/saga/CDC — с другой.

Глубже. Связующая идея, которую и проверяют такой формулировкой: транзакция БД заканчивается на границе БД, а шина живёт вне её, поэтому любая согласованность между ними достигается не «общей транзакцией», а паттернами. Полезно уметь сказать, чем Kafka отличается от RabbitMQ по модели (лог с офсетами и партициями против очереди с подтверждениями и роутингом), где берётся порядок (только внутри партиции по ключу) и почему exactly-once на практике — это at-least-once + дедупликация у потребителя.

Что такое ACID, транзакции, уровни изоляции, аномалии?

Заголовок раздела «Что такое ACID, транзакции, уровни изоляции, аномалии?»

Коротко. См. выше: транзакция — атомарная единица работы; ACID — четыре её гарантии; уровни изоляции — шкала строгости буквы I; аномалии — конкретные наблюдаемые эффекты конкурентности, которые эти уровни запрещают.

Глубже. Полный список аномалий, который стоит держать в голове: dirty read, dirty write (перезапись незакоммиченных данных — запрещена на всех уровнях), non-repeatable read, phantom read, lost update, read skew (прочитали связанные строки в разных состояниях), write skew, а также аномалия только для чтения (read-only serialization anomaly) — редкий случай, когда даже читающая транзакция видит несогласованную картину при snapshot isolation. Первые четыре описаны в стандарте, остальные добавлены в статье «A Critique of ANSI SQL Isolation Levels» (Berenson et al., 1995), которую полезно упомянуть.

Какие существуют уровни изоляции транзакций?

Заголовок раздела «Какие существуют уровни изоляции транзакций?»

Коротко. Read Uncommitted, Read Committed, Repeatable Read, Serializable. Подробности и матрицу аномалий см. выше.

Коротко. Блокировка — механизм, который на время не даёт другим транзакциям/потокам выполнить конфликтующую операцию над ресурсом. У блокировки есть объект (строка, страница, таблица, произвольный ключ), режим (разделяемый/исключительный и более тонкие градации) и время жизни — в PostgreSQL все транзакционные локи держатся до конца транзакции.

Глубже. В PostgreSQL полезно различать три семейства. Табличные локи — восемь режимов от ACCESS SHARE (берёт SELECT) до ACCESS EXCLUSIVE (берут DROP, TRUNCATE, большинство ALTER TABLE), с матрицей совместимости; именно они виноваты в «миграция положила прод». Строковые локи — FOR UPDATE, FOR NO KEY UPDATE, FOR SHARE, FOR KEY SHARE, реализованные не в общей памяти, а прямо в кортеже (xmax + инфомаска), поэтому число залоченных строк не ограничено. Advisory-локи — произвольные bigint-ключи, сессионные или транзакционные (pg_advisory_lock / pg_advisory_xact_lock и их try-версии), их семантику определяет приложение. Плюс внутренние: LWLock и spinlock в разделяемой памяти, предикатные SIReadLock у Serializable. Смотреть текущие локи — pg_locks в join с pg_stat_activity, а конкретно ожидание — через pg_blocking_pids(pid).

Какие уровни изоляции транзакций существуют в PostgreSQL? Объясните разницу между ними. Какие проблемы конкурентности решает каждый уровень?

Заголовок раздела «Какие уровни изоляции транзакций существуют в PostgreSQL? Объясните разницу между ними. Какие проблемы конкурентности решает каждый уровень?»

Коротко. Синтаксически поддерживаются все четыре, фактически работают три. Read Committed (дефолт): каждый оператор видит свежий снимок, грязных чтений нет, неповторяющиеся и фантомные возможны. Repeatable Read: один снимок на всю транзакцию — нет ни неповторяющихся, ни фантомных чтений, но возможен write skew, а конфликтующая запись падает с 40001. Serializable: то же плюс SSI, который отслеживает опасные структуры rw-зависимостей и откатывает транзакции так, что итог всегда эквивалентен последовательному выполнению.

Глубже. Разница в реализации: Read Committed вызывает GetSnapshotData() перед каждым оператором, Repeatable Read — один раз за транзакцию. Serializable дополнительно ведёт учёт предикатных чтений (SIReadLock, видны в pg_locks) и ищет паттерн «опасная структура» — транзакцию с входящей и исходящей rw-зависимостью; при обнаружении одна из участниц получает could not serialize access due to read/write dependencies among transactions. Практические заметки: SSI требует памяти под предикатные локи (max_pred_locks_per_transaction), может огрублять их до уровня страницы/таблицы и тогда давать ложные откаты; на Serializable полезен SET TRANSACTION READ ONLY DEFERRABLE для длинных отчётов — такая транзакция подождёт безопасного снимка, зато не будет откатываться и не заставит откатываться других. Ретраи обязательны в обоих верхних уровнях, и повторять надо всю транзакцию с самого начала, потому что её снимок уже невалиден.

Что такое транзакции? Что означает аббревиатура ACID? Какие уровни изоляции транзакций существуют и какие проблемы они решают?

Заголовок раздела «Что такое транзакции? Что означает аббревиатура ACID? Какие уровни изоляции транзакций существуют и какие проблемы они решают?»

Коротко. Сводный вопрос: транзакция — атомарная единица работы с БД; ACID = Atomicity, Consistency, Isolation, Durability; уровней четыре (Read Uncommitted / Read Committed / Repeatable Read / Serializable), и они последовательно запрещают грязное чтение, неповторяющееся чтение, фантомы и остальные аномалии сериализации.

Глубже. Такой вопрос — приглашение к структурированному монологу минуты на три. Разумный план: определение и границы транзакции → четыре буквы с одним предложением на каждую → аномалии как мотивация уровней → матрица уровней → особенности PostgreSQL (дефолт Read Committed, нет реального Read Uncommitted, RR = snapshot isolation, 40001 и ретраи) → практический вывод: большинство задач решается дефолтом плюс точечным FOR UPDATE или атомарным UPDATE, а Serializable — редкий и осознанный выбор.

Что такое оптимистичные и пессимистичные блокировки? В каких сценариях лучше применять оптимистичные блокировки?

Заголовок раздела «Что такое оптимистичные и пессимистичные блокировки? В каких сценариях лучше применять оптимистичные блокировки?»

Коротко. Пессимистичная блокировка — заранее захватить ресурс (SELECT ... FOR UPDATE) и держать до конца транзакции, исходя из того, что конфликт вероятен. Оптимистичная — не блокировать ничего, а при записи проверить, что данные не изменились с момента чтения (WHERE version = :old), и при неудаче повторить. Оптимистичная выгодна, когда конфликты редки, между чтением и записью проходит заметное время (например, пользователь редактирует форму) и операцию можно безопасно повторить.

Глубже. Пессимистичная схема даёт предсказуемую латентность при высокой конкуренции: транзакции выстраиваются в очередь, никто не делает лишнюю работу дважды. Её минусы — риск дедлоков, чувствительность к длительности транзакции и невозможность держать лок через пользовательский «think time». Оптимистичная не держит ресурсов, отлично масштабируется на редких конфликтах, естественно ложится на HTTP (ETag/If-Match) и позволяет показать пользователю осмысленную ошибку «данные изменились». Её минусы — при частых конфликтах доля откатов и повторов растёт нелинейно, и требуется идемпотентность повторяемой операции. Отдельно стоит отметить, что Repeatable Read/Serializable в PostgreSQL — это по сути встроенная оптимистичная схема на уровне СУБД: конфликт обнаруживается на записи/коммите и превращается в 40001.

-- оптимистично
UPDATE documents SET body = :body, version = version + 1
WHERE id = :id AND version = :expected_version;
-- если 0 строк — конфликт, перечитать и повторить
-- пессимистично
BEGIN;
SELECT body, version FROM documents WHERE id = :id FOR UPDATE;
UPDATE documents SET body = :body WHERE id = :id;
COMMIT;

Расскажи про D в ACID и как базы обеспечивают durability?

Заголовок раздела «Расскажи про D в ACID и как базы обеспечивают durability?»

Коротко. Durability означает, что после успешного ответа на COMMIT изменения переживут падение процесса, ОС и внезапное отключение питания. Обеспечивается журналом упреждающей записи (WAL): запись об изменении сначала попадает в журнал и сбрасывается на диск через fsync, и только потом считается закоммиченной; сами страницы данных пишутся лениво, при контрольной точке. После сбоя СУБД восстанавливается, проигрывая WAL с последнего checkpoint.

Глубже. Детали PostgreSQL: правило WAL — журнальная запись должна попасть на устройство раньше, чем соответствующая страница данных; COMMIT ждёт fsync (или fdatasync, зависит от wal_sync_method) до LSN своей commit-записи. Есть group commit: commit_delay/commit_siblings позволяют объединить fsync нескольких коммитов. Долговечность настраиваема: synchronous_commit = off возвращает ответ, не дожидаясь fsync — теряется до wal_writer_delay×3 последних коммитов при крахе хоста (но БД остаётся консистентной, это важное отличие от fsync = off, который ломает всё). full_page_writes защищает от torn pages, записывая полный образ страницы при первом изменении после checkpoint. За пределами одного узла durability расширяется синхронной репликацией: synchronous_commit = remote_apply/remote_write/on и synchronous_standby_names заставляют коммит дожидаться реплики. В InnoDB аналог — redo log плюс innodb_flush_log_at_trx_commit = 1 и, отдельно, binlog с sync_binlog = 1. Общая мысль для ответа: durability — это не «данные сразу в таблице», а «изменение гарантированно есть в журнале, из которого его можно восстановить».

Какие уровни изоляции ты знаешь? Какая по дефолту в посгрес?

Заголовок раздела «Какие уровни изоляции ты знаешь? Какая по дефолту в посгрес?»

Коротко. Четыре уровня: Read Uncommitted, Read Committed, Repeatable Read, Serializable. В PostgreSQL по умолчанию Read Committed. Проверяется через SHOW default_transaction_isolation;.

Глубже. Полезно добавить сравнение дефолтов: PostgreSQL, Oracle и SQL Server — Read Committed, MySQL/InnoDB — Repeatable Read. Именно поэтому код, перенесённый с MySQL на PostgreSQL, иногда начинает вести себя иначе внутри длинных транзакций: там, где в MySQL был стабильный снимок, в PostgreSQL каждый оператор видит новые данные.

Какие ты знаешь уровни изоляции транзакций?

Заголовок раздела «Какие ты знаешь уровни изоляции транзакций?»

Коротко. См. выше — Read Uncommitted, Read Committed, Repeatable Read, Serializable, с матрицей допустимых аномалий и поправками на реализацию в PostgreSQL.

Коротко. В PostgreSQL уровень изоляции устроен как правило получения снимка плюс правило разрешения конфликтов. Read Committed берёт новый снимок перед каждым оператором; Repeatable Read — один снимок на транзакцию; Serializable — тот же снимок плюс отслеживание rw-зависимостей (SSI) и откат участников опасных циклов. Видимость версии строки вычисляется из её xmin/xmax и снимка.

Глубже. Снимок — структура (xmin, xmax, xip[]): всё, что зафиксировано транзакциями с номером меньше xmin и не входит в список активных xip, видно; всё, что создано транзакциями из xip или с номером ≥ xmax, невидимо. Дальше правила простые: версия видна, если её xmin виден и закоммичен, а xmax либо отсутствует, либо не виден/не закоммичен. Конфликты записи разрешаются на уровне строки: писатель, наткнувшийся на строку с активным xmax, ждёт завершения транзакции-владельца (это ожидание видно как Lock | transactionid в pg_locks), после чего либо перечитывает новую версию (Read Committed, EvalPlanQual), либо получает 40001 (RR/Serializable). В стандарте SQL те же уровни описаны иначе — через двухфазную блокировку и длительность удержания S/X-локов; на этом и построены реализации без MVCC. Знание обеих моделей полезно: вопрос «как устроены» часто подразумевает именно «через блокировки или через версии?».

Коротко. У sync.RWMutex: Lock берёт исключительную блокировку на запись — в этот момент внутри критической секции не может быть ни одного читателя и ни одного другого писателя; RLock берёт разделяемую блокировку на чтение — таких одновременно может быть много, но ни одного писателя. Освобождаются соответственно Unlock и RUnlock.

Глубже. Три детали, на которых валятся. Первая: RWMutex write-preferring — как только горутина вызвала Lock и ждёт, последующие RLock не проскакивают вперёд, а встают в очередь; это защищает писателя от голодания, но делает вложенный RLock внутри уже удерживаемого RLock потенциальным дедлоком. Вторая: RLock не даёт права на запись даже к «своим» полям — обновление под RLock это гонка данных, которую поймает -race. Третья: у sync.Mutex есть только Lock/Unlock, и с Go 1.9 в нём есть режим голодания (starvation mode): если горутина ждёт лок дольше 1 мс, мьютекс переключается в честную FIFO-передачу владения, жертвуя пропускной способностью ради отсутствия голодания. Разблокировать мьютекс может любая горутина, не обязательно захватившая (в отличие от, например, pthread-мьютексов), а разблокировка незалоченного — паника fatal error: sync: unlock of unlocked mutex.

На уровне Serializable две транзакции апдейтят одну строку, что произойдет?

Заголовок раздела «На уровне Serializable две транзакции апдейтят одну строку, что произойдет?»

Коротко. Первая транзакция берёт row lock и продолжает; вторая на своём UPDATE блокируется до её завершения. Если первая закоммитилась, вторая не сможет применить изменение к устаревшей версии и упадёт с ERROR: could not serialize access due to concurrent update (SQLSTATE 40001) — её нужно откатить и повторить целиком. Если первая откатилась, вторая спокойно продолжит.

Глубже. Важно: такое поведение начинается уже с Repeatable Read — это свойство snapshot isolation, а не только Serializable. Специфика Serializable в другом: он дополнительно ловит конфликты, где строки вообще не пересекаются по записи (write skew), выдавая could not serialize access due to read/write dependencies among transactions. Оба случая имеют один и тот же SQLSTATE 40001, и правильная реакция приложения одинаковая — retry с ограничением попыток и экспоненциальной задержкой с джиттером, обязательно всей транзакции с начала, потому что промежуточные результаты уже невалидны. И ретраить имеет смысл только идемпотентную по внешним эффектам операцию: если внутри транзакции успел уйти HTTP-вызов, повтор его продублирует — ещё один довод не делать сетевых вызовов внутри транзакции.

for attempt := 0; attempt < 5; attempt++ {
err := runTx(ctx, db)
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) && (pgErr.Code == "40001" || pgErr.Code == "40P01") {
time.Sleep(time.Duration(1<<attempt)*10*time.Millisecond + jitter())
continue
}
return err
}
return errors.New("tx: too many serialization failures")

Коротко. Набор из четырёх гарантий транзакции: Atomicity (всё или ничего), Consistency (соблюдение ограничений и инвариантов), Isolation (изолированность от конкурентных транзакций), Durability (сохранность после коммита). См. развёрнутый разбор выше.

Может ли одна транзакция повлиять на другую?

Заголовок раздела «Может ли одна транзакция повлиять на другую?»

Коротко. Да, и тремя способами: через видимость данных (после коммита её изменения увидят другие — на Read Committed сразу же), через блокировки (её row/table locks заставят других ждать, вплоть до дедлока), и через откаты сериализации (на RR/Serializable её коммит может вызвать 40001 у соседа). Плюс косвенно: длинная транзакция держит горизонт видимости и мешает VACUUM всей базе.

Глубже. Чего одна транзакция сделать не может — показать другой свои незакоммиченные данные (в PostgreSQL грязного чтения нет ни на каком уровне) и «отменить» чужой коммит. Отдельно стоит упомянуть неявные каналы влияния: расход общих ресурсов (соединения в пуле, work_mem, временные файлы), нагрев/вытеснение shared buffers и, если транзакция пишет много, ускоренная ротация WAL и чекпойнты, которые ощутит вся система.

Коротко. См. выше — Atomicity, Consistency, Isolation, Durability. Отличие от предыдущих формулировок вопроса нулевое; отвечать одинаково, добавляя по одному предложению механики на каждую букву (WAL и pg_xact для A и D, констрейнты для C, MVCC и локи для I).

Что будет, если попытаться взять Lock, не отпустив RUnlock?

Заголовок раздела «Что будет, если попытаться взять Lock, не отпустив RUnlock?»

Коротко. Если это делает та же горутина, которая уже держит RLock, — гарантированный дедлок: Lock ждёт освобождения всех читателей, а читатель ждёт возврата из Lock. Если разные горутины — Lock просто заблокируется до тех пор, пока последний читатель не вызовет RUnlock; если тот его никогда не вызовет, писатель зависнет навсегда, а вместе с ним и все последующие RLock (из-за write-preferring политики).

Глубже. В худшем случае Go-рантайм печатает fatal error: all goroutines are asleep - deadlock! — но только если заблокированы вообще все горутины; в реальном сервере с HTTP-listener’ом этого не произойдёт, и симптом будет другим: растущее число горутин, зависшие запросы и таймауты. Диагностика — runtime/pprof профиль goroutine (/debug/pprof/goroutine?debug=2) с явно видимыми стеками на sync.(*RWMutex).Lock, или GODEBUG не поможет, а вот -race детектор дедлоки не ловит вовсе (он про гонки данных, не про взаимные блокировки). Практические правила: не вкладывать RLock в RLock, не апгрейдить RLock до Lock (в Go это не поддерживается — надо отпустить и перезахватить, перепроверив состояние), всегда defer mu.RUnlock() сразу после захвата, не удерживать лок через вызовы внешнего кода.

Есть две горутины, каждая из которых выполняет свою транзакцию. Каким образом можно обернуть это в одну глобальную транзакцию?

Заголовок раздела «Есть две горутины, каждая из которых выполняет свою транзакцию. Каким образом можно обернуть это в одну глобальную транзакцию?»

Коротко. Никак, если это две разные транзакции на двух соединениях: транзакция в PostgreSQL привязана к соединению, и объединить два соединения в одну транзакцию средствами обычного SQL нельзя. Реальные варианты: (1) выполнять обе части в одной транзакции, передав горутинам один *sql.Tx и сериализовав работу — параллельности всё равно не будет, транзакция живёт на одном соединении; (2) двухфазный коммит (PREPARE TRANSACTION / COMMIT PREPARED) с внешним координатором; (3) отказаться от глобальной транзакции и построить сагу с компенсациями плюс outbox.

Глубже. По пункту (1): *sql.Tx держит ровно одно соединение, и database/sql не даёт выполнять на нём операции параллельно — они всё равно будут сериализованы, а работа с Tx после Commit/Rollback вернёт sql.ErrTxDone. То есть «параллельные горутины в одной транзакции» — это иллюзия ускорения: сервер обрабатывает операторы одного бэкенда строго по одному. По пункту (2): 2PC в PostgreSQL включается max_prepared_transactions > 0, и главная опасность — «зависшая» prepared-транзакция, которая вечно держит локи и горизонт вакуума, если координатор умер и никто не сделал COMMIT PREPARED/ROLLBACK PREPARED; поэтому её применяют только с надёжным журналом координатора и мониторингом pg_prepared_xacts. По пункту (3): в микросервисной архитектуре это дефолтный ответ — локальная транзакция в каждом сервисе, события через outbox, компенсации при неуспехе, идемпотентность на каждом шаге.

Что вы знаете о транзакциях в базах данных?

Заголовок раздела «Что вы знаете о транзакциях в базах данных?»

Коротко. Открытый вопрос — отвечать структурой: определение и границы (BEGIN/COMMIT/ROLLBACK, autocommit), ACID, уровни изоляции и аномалии, механика (MVCC, WAL, блокировки), эксплуатация (длительность транзакций, idle in transaction, таймауты, ретраи на 40001/40P01) и границы применимости (транзакция не покрывает брокер и внешние API — отсюда outbox и saga).

Глубже. Такой вопрос — возможность самому задать структуру беседы. Хорошая тактика: за минуту пройти по всем пяти блокам верхнеуровнево и закончить фразой «могу углубиться в любой из пунктов» — почти всегда интервьюер выберет один, и дальше разговор пойдёт по вашей карте, а не по его списку.

Вы работаете с деньгами, и нужно, чтобы со счета списалось определенное количество денег, и чтобы никакая другая транзакция не смотрела на данные, которые вы меняете. Что произойдет, если одновременно придет 1000 запросов на изменение одного и того же счета? Как можно решить эту проблему?

Заголовок раздела «Вы работаете с деньгами, и нужно, чтобы со счета списалось определенное количество денег, и чтобы никакая другая транзакция не смотрела на данные, которые вы меняете. Что произойдет, если одновременно придет 1000 запросов на изменение одного и того же счета? Как можно решить эту проблему?»

Коротко. Если каждый запрос делает SELECT ... FOR UPDATE по одной и той же строке, все 1000 выстроятся в очередь на row lock: они не потеряют данные, но выполнятся строго последовательно, латентность последнего будет суммой всех предыдущих, а при таймаутах и удержании соединений из пула это выльется в деградацию всего сервиса. Решения: убрать read-modify-write (один атомарный UPDATE ... SET balance = balance - :amt WHERE id = :id AND balance >= :amt с проверкой RowsAffected), сериализовать поток по счёту (очередь/партиция по account_id, один воркер на ключ), перейти на append-only ledger вместо обновления одной строки, шардировать баланс на N подстрок при экстремальной нагрузке.

Глубже. Разберём по шагам. Первое: FOR UPDATE не «прячет» данные от читателей — обычные SELECT благодаря MVCC продолжат видеть старую версию; если требование именно «никто не читает промежуточное состояние», то в PostgreSQL оно выполняется автоматически, потому что незакоммиченное не видно никому. Второе: очередь из 1000 ожидающих опасна не только латентностью — это 1000 занятых соединений/бэкендов, поэтому обязателен lock_timeout и ограничение параллелизма (пул, семафор, PgBouncer). Третье: атомарный UPDATE сокращает критическую секцию до одного оператора (сотни микросекунд), и та же тысяча запросов проходит за доли секунды — этого достаточно почти всегда. Четвёртое: если и этого мало (горячий счёт мерчанта), применяют либо буферизацию/батчинг списаний, либо шардированный баланс (account_shards(account_id, shard_no, delta), инкремент случайного шарда, баланс = sum), либо перевод счёта в модель журнала проводок с периодической свёрткой. Пятое: при любом варианте нужен ключ идемпотентности на операцию, чтобы ретрай клиента не списал деньги дважды — это часть корректности, а не опция.

-- безопасное списание без явных локов и без чтения в приложение
UPDATE accounts
SET balance = balance - :amt
WHERE id = :id AND balance >= :amt;
-- 0 строк => недостаточно средств

Коротко. См. выше «Что такое транзакция?»: логически неделимая последовательность операций с гарантиями ACID, ограниченная BEGIN и COMMIT/ROLLBACK.

В чем разница между уровнями изоляции Read Uncommitted и Read Committed?

Заголовок раздела «В чем разница между уровнями изоляции Read Uncommitted и Read Committed?»

Коротко. По стандарту Read Uncommitted допускает грязное чтение — транзакция может увидеть данные, которые другая транзакция изменила, но ещё не закоммитила (и, возможно, откатит). Read Committed это запрещает: видны только зафиксированные изменения. В PostgreSQL разницы нет вообще — Read Uncommitted принимается синтаксически, но работает как Read Committed.

Глубже. Причина такого поведения PostgreSQL — MVCC: видимость определяется статусом транзакции-создателя версии, и «показать незакоммиченное» просто не предусмотрено механизмом. В MySQL/InnoDB Read Uncommitted работает по-настоящему: чтения не консультируются с undo log и видят текущее состояние строк, включая незафиксированные изменения. Практическая ценность Read Uncommitted близка к нулю: единственный сценарий, где его иногда вспоминают, — грубые оценочные запросы по огромным таблицам в СУБД, где низкий уровень изоляции снимает блокировки чтения (например, WITH (NOLOCK) в SQL Server); в MVCC-базах эта мотивация отсутствует, потому что читатели и так не блокируются.

  • Называют четыре уровня изоляции по стандарту и не знают, что в PostgreSQL Read Uncommitted эквивалентен Read Committed, а Repeatable Read сильнее стандартного и запрещает фантомы.
  • Считают, что высокий уровень изоляции «сам всё починит», и не упоминают ошибку 40001 и необходимость ретраить транзакцию целиком — в реальности без ретраев Serializable просто превращает аномалии в 500-е.
  • Путают неповторяющееся чтение с фантомным: первое — изменилось значение уже прочитанной строки, второе — изменился состав множества строк по условию.
  • Уверены, что SELECT ... FOR UPDATE мешает другим читать строку. В PostgreSQL обычные SELECT не блокируются никогда; блокируются только конкурирующие писатели и явные блокирующие чтения.
  • Делают read-modify-write через приложение (SELECT баланс → посчитали в Go → UPDATE) и называют это транзакцией: на Read Committed это классический lost update.
  • Защищают данные от гонки между несколькими подами через sync.Mutex в процессе — лок локален, между репликами он не работает.
  • Забывают, что все локи в PostgreSQL держатся до конца транзакции, и делают внутри транзакции HTTP-вызовы, ожидания или тяжёлые расчёты, порождая idle in transaction и bloat.
  • Не знают про SKIP LOCKED и делают очередь заданий на FOR UPDATE, из-за чего воркеры стоят в общей очереди и параллелизм не растёт.
  • Объясняют durability как «данные сразу записались в таблицу» вместо «commit-запись гарантированно во WAL после fsync, страницы дописываются при чекпойнте».
  • Путают C из ACID (соблюдение ограничений внутри одной БД) с C из CAP (согласованность реплик в распределённой системе).
  • Утверждают, что дедлоки в PostgreSQL приводят к зависанию: на деле есть детектор с deadlock_timeout (1 с по умолчанию), и одна из транзакций получает 40P01.
  • Считают sync.RWMutex всегда быстрее Mutex и не знают про write-preferring политику, нерекурсивность RLock и невозможность апгрейда RLock до Lock.

Список исходных вопросов с привязкой к компаниям: ../questions/transactions.md