говоря о цене, мы говорим о цене саппорта такого решения, а не о нагрузке на железо.
А что, если резервных счетов - два? или три? под каждый вариант напишете свой код, да? А вот на стороне SQL это всё делается опять же одним запросом, даже если этих счетов десяток.
я выберу 10 простых примитивных запросов, а не один сложный, и вот почему:
в простом запросе при рефакторинге LLM допустит ошибку с меньшей вероятностью, простор для "творчества" меньше ;)
у простого запроса меньше сайд-эффектов, я точно знаю что он делает (и мой Claude тоже)
делая сложный запрос я вынужден имплементировать механизм абстракции для того, чтобы работали все N (10) кейсов. эту логику надо знать и мейнтейнить. а ее могло бы не быть
возможно будут рейсы, решение @Akina не тестировал. но интересно, не знал, что в CTE возможны рейсы, думал что все блокируется на время такого запроса. надо будет изучить тему, спасибо
проверил ровно ваш сценарий на pg 16: pg_sleep(2) внутри транзакции, два перевода по 100 параллельно, старт main=70, reserve=500.
с FOR UPDATE: вторая транзакция встаёт на лок, ждёт первую, потом перечитывает свежие балансы. итог main=0, reserve=370 — оба списания на месте. по времени 4 секунды, то есть сериализовались, а не параллельно.
без FOR UPDATE — да, ровно как вы говорите: 2 секунды, итог 470, одно списание потерялось
pg_sleep ничего не ломает: лок не даёт второй прочитать устаревший баланс, сколько бы первая ни спала. об этом и статья
никто не предлагает в 2026 писать код руками. Но я бы никогда не гарантировал что ИИ всегда поставит for update там, где это необходимо. а цена ошибки слишком высокая - через такую дыру можно опустошить легко опустошить hot wallet проекта. и такое происходит на CEXах
сейчас все такие уязвимости у крупных проектов лечатся через bug bounty programs
cамо знание не такое сложное, а цена ошибки высокая. Ну и просто полезно понимать, что происходит под нагрузкой на сервисе, где балансы лежат в реляционной БД.
и как всегда бывает в CS - атаки усложняются вместе с эволюцией средств защиты:
спасибо за фиддл. согласен что считать в приложении в нашем примере необязательно. многие вещи можно посчитать и обновить на уровне SQL
но какой ценой?
протащить через CTE старые значения балансов
во внешнем запросе сравнить их с текущими
если хоть одна строка изменилась — уронить весь запрос ошибкой (через sql-хак)
приложение потом повторяет
(надеюсь что правильно понял идею)
ваш пример доказывает что расчет и перевод можно выразить в SQL, но по сути это теже оптимистичные блокировки, которые доступны через штатные механизмы БД, с ценой повторов.
у вас сложность не исчезла, а переехала в запрос. Просто сравните:
BEGIN;
-- 1. Залочили обе строки и прочитали свежие балансы
SELECT id, balance FROM accounts
WHERE id IN ('main', 'reserve')
FOR UPDATE;
-- main=70, reserve=500
-- 2. Посчитали в коде: с main снять 70, остаток 30 — с reserve
-- 3. Записали
UPDATE accounts SET balance = 0 WHERE id = 'main';
UPDATE accounts SET balance = 470 WHERE id = 'reserve';
COMMIT;
3 банальных стейтмента против двойного CTE с намеренной ошибкой-абортом
да, согласен, пример синтетический. главная мысль в том, что не все можно выразить внутри запроса. и еще есть системы, которые построены на базе ORM и не всегда рационально делать raw queries, только чтобы привести запрос к атомарному виду. и да, вычисления в рантайме приложения, существуют, и поэтому области применения FOR UPDATE всегда найдутся.
main/reserve это id-параметры, таблица одна; в шаге 4 опечатка, должно быть UPDATE accounts SET ... WHERE id = main/reserve. Поправил.
по CTE — тут не соглашусь в главном. да, multi-table UPDATE через data-modifying CTE выражается одним запросом, и раздачу 70/30 с защитой от минуса можно собрать на LEAST/GREATEST — считать в коде необязательно.
но «не нужны ни транзакции, ни FOR UPDATE» — неверно. Все под-запросы CTE работают на одном снапшоте, а в READ COMMITTED при конфликте Postgres перечитывает только саму обновляемую строку и её WHERE — не значения, подтянутые из другой строки. цитата из доки: [Because of the above rules, it is possible for an updating command to see an inconsistent snapshot: it can see the effects of concurrent updating commands on the same rows it is trying to update, but it does not see effects of those commands on other rows in the database](https://www.postgresql.org/docs/current/transaction-iso.html#XACT-READ-COMMITTED) .
сумма с reserve зависит от баланса main — другой строки. конкурентное изменение main между снапшотом и апдейтом приводит к неверному распределению: суммы посчитаны по старому балансу, а он уже изменился — спишется не та сумма. дока про Read Committed называет такой режим «unsuitable for commands that involve complex search conditions» (13.2.1 из доки выше), хендлится блокировкой строк — SELECT … FOR UPDATE — либо переходом на REPEATABLE READ + повтор.
если есть рабочий вариант без локов, который держит конкурентный тест на двух счетах — с интересом изучу, обновлю статью.
Путаница с термином «lost update» — в стандарте и в статье это разные вещи.
«No updates will be lost» в стандарте — про dirty write: незакоммиченную запись транзакции другая не перезатрёт. Эту гарантию дают все 4 уровня через write-локи до конца транзакции.
«Lost update» в статье — read-modify-write аномалия: две транзакции читают один баланс, обе пишут, обе коммитятся — формально ничего не «потеряно», но логически одно списание затёрто.
На практике Postgres так и ведёт себя: READ COMMITTED теряет такой апдейт, REPEATABLE READ ловит конфликт ошибкой 40001.
Как зайти на сайт со старыми кредами? Аккаунт с 2006 года, при попытке ввести пишут неправильный логин/пароль. Форма обратной связи не отвечает.(Писал два раза) Спасибо.
оу оу…
Я просто поделился с автором комментария своими ощущениями, срефлексировал по этому поводу… Типа знаете, как посмотрел российское кино, рассказал сюжет знакомому, а он говорит что это классическая голливудская комедия. На душе осадок остался…
Ваш подход такой очень понятен, очень широко распространен в бизнесе. Только сравнение с парикмахерской из вашего поста все же более уместно, чем с Apple, потому что последняя создавала новые ценности на основе старых, либо адаптировала то, что не нужно их хозяевам, для масс-маркета. И общественный резонанс, и в итоге кэш-флоу, у парикмахерской и Apple сопоставимый их заслугам.
Оправдание что хозяева отвечали, что им российский рынок не интересен, спроное. Это же их продукт, что хотят с ним то и делают. Какой-то странный расклад — не хотите быть партнерами, значит мы возьмем бесплатно.
Читал статью, думал, вот какие ребята молодцы, с такими инвестициями такой продукт запилили, наверное я своим заказчикам слишком высокие ценники заряжаю. И все как-то не верится что за 90к можно такой продукт запилить… А тут вон оно что. Фу, просто противно…
хм, у меня этих звездочек ненулевое количество, хотя я курсы из библиотеки не проходил…
также оно не равняется сумме всех баллов по всем курсам, набранных за все время. Загадочная цифра. Собственно, поэтому и задал вопрос
Каким образом преподаватель удаляет свои пометки? Замечено, что иногда он их стирает, а иногда удаляет часть, иногда все сразу. При этом видео не прерывается. Мне показалось, что должен быть специальный механизм, который позволяет это делать так ловко)
Вопросы не по теме поста:
Интересно, что конкретно означают недавно появившиеся звездочки в профиле?
В течение какого периода будет доступен контент по курсам (напр по курсу «Дискретные структуры») в том виде в котором он сейчас, со всем интерактивом?
PS Большое спасибо, очень интересный пост. Всегда были интересны технические детали процесса подготовки контента на Степике.
за что минусуете? мы же не о науке или творчестве. мы о промышленном программировании, о бизнесе. об эффективности, об иерархии. Этим начальником в мое случае, например, был инженер колоссального уровня, бывший разработчик JVM из SUN. Это тот случай, когда художник говорит своему подмастерью, что нужно делать.
говоря о цене, мы говорим о цене саппорта такого решения, а не о нагрузке на железо.
я выберу 10 простых примитивных запросов, а не один сложный, и вот почему:
в простом запросе при рефакторинге LLM допустит ошибку с меньшей вероятностью, простор для "творчества" меньше ;)
у простого запроса меньше сайд-эффектов, я точно знаю что он делает (и мой Claude тоже)
делая сложный запрос я вынужден имплементировать механизм абстракции для того, чтобы работали все N (10) кейсов. эту логику надо знать и мейнтейнить. а ее могло бы не быть
а, ясно, неправильно иерархию комментов понял)
возможно будут рейсы, решение @Akina не тестировал. но интересно, не знал, что в CTE возможны рейсы, думал что все блокируется на время такого запроса. надо будет изучить тему, спасибо
проверил ровно ваш сценарий на pg 16: pg_sleep(2) внутри транзакции, два перевода по 100 параллельно, старт main=70, reserve=500.
с FOR UPDATE: вторая транзакция встаёт на лок, ждёт первую, потом перечитывает свежие балансы. итог main=0, reserve=370 — оба списания на месте. по времени 4 секунды, то есть сериализовались, а не параллельно.
без FOR UPDATE — да, ровно как вы говорите: 2 секунды, итог 470, одно списание потерялось
pg_sleep ничего не ломает: лок не даёт второй прочитать устаревший баланс, сколько бы первая ни спала. об этом и статья
никто не предлагает в 2026 писать код руками. Но я бы никогда не гарантировал что ИИ всегда поставит for update там, где это необходимо. а цена ошибки слишком высокая - через такую дыру можно опустошить легко опустошить hot wallet проекта. и такое происходит на CEXах
ранее случались громкие истории типа Flexcoin и Poloniex
сейчас все такие уязвимости у крупных проектов лечатся через bug bounty programs
cамо знание не такое сложное, а цена ошибки высокая. Ну и просто полезно понимать, что происходит под нагрузкой на сервисе, где балансы лежат в реляционной БД.
и как всегда бывает в CS - атаки усложняются вместе с эволюцией средств защиты:
The single-packet attack: making remote race-conditions 'local'
спасибо за фиддл. согласен что считать в приложении в нашем примере необязательно. многие вещи можно посчитать и обновить на уровне SQL
но какой ценой?
протащить через CTE старые значения балансов
во внешнем запросе сравнить их с текущими
если хоть одна строка изменилась — уронить весь запрос ошибкой (через sql-хак)
приложение потом повторяет
(надеюсь что правильно понял идею)
ваш пример доказывает что расчет и перевод можно выразить в SQL, но по сути это теже оптимистичные блокировки, которые доступны через штатные механизмы БД, с ценой повторов.
у вас сложность не исчезла, а переехала в запрос. Просто сравните:
3 банальных стейтмента против двойного CTE с намеренной ошибкой-абортом
да, согласен, пример синтетический. главная мысль в том, что не все можно выразить внутри запроса. и еще есть системы, которые построены на базе ORM и не всегда рационально делать raw queries, только чтобы привести запрос к атомарному виду. и да, вычисления в рантайме приложения, существуют, и поэтому области применения FOR UPDATE всегда найдутся.
main/reserveэто id-параметры, таблица одна; в шаге 4 опечатка, должно бытьUPDATE accounts SET ... WHERE id = main/reserve. Поправил.по CTE — тут не соглашусь в главном. да, multi-table UPDATE через data-modifying CTE выражается одним запросом, и раздачу 70/30 с защитой от минуса можно собрать на
LEAST/GREATEST— считать в коде необязательно.но «не нужны ни транзакции, ни FOR UPDATE» — неверно. Все под-запросы CTE работают на одном снапшоте, а в READ COMMITTED при конфликте Postgres перечитывает только саму обновляемую строку и её WHERE — не значения, подтянутые из другой строки. цитата из доки: [Because of the above rules, it is possible for an updating command to see an inconsistent snapshot: it can see the effects of concurrent updating commands on the same rows it is trying to update, but it does not see effects of those commands on other rows in the database](https://www.postgresql.org/docs/current/transaction-iso.html#XACT-READ-COMMITTED) .
сумма с
reserveзависит от балансаmain— другой строки. конкурентное изменениеmainмежду снапшотом и апдейтом приводит к неверному распределению: суммы посчитаны по старому балансу, а он уже изменился — спишется не та сумма. дока про Read Committed называет такой режим «unsuitable for commands that involve complex search conditions» (13.2.1 из доки выше), хендлится блокировкой строк —SELECT … FOR UPDATE— либо переходом на REPEATABLE READ + повтор.если есть рабочий вариант без локов, который держит конкурентный тест на двух счетах — с интересом изучу, обновлю статью.
Путаница с термином «lost update» — в стандарте и в статье это разные вещи.
«No updates will be lost» в стандарте — про dirty write: незакоммиченную запись транзакции другая не перезатрёт. Эту гарантию дают все 4 уровня через write-локи до конца транзакции.
«Lost update» в статье — read-modify-write аномалия: две транзакции читают один баланс, обе пишут, обе коммитятся — формально ничего не «потеряно», но логически одно списание затёрто.
На практике Postgres так и ведёт себя: READ COMMITTED теряет такой апдейт, REPEATABLE READ ловит конфликт ошибкой 40001.
Я просто поделился с автором комментария своими ощущениями, срефлексировал по этому поводу… Типа знаете, как посмотрел российское кино, рассказал сюжет знакомому, а он говорит что это классическая голливудская комедия. На душе осадок остался…
Ваш подход такой очень понятен, очень широко распространен в бизнесе. Только сравнение с парикмахерской из вашего поста все же более уместно, чем с Apple, потому что последняя создавала новые ценности на основе старых, либо адаптировала то, что не нужно их хозяевам, для масс-маркета. И общественный резонанс, и в итоге кэш-флоу, у парикмахерской и Apple сопоставимый их заслугам.
Оправдание что хозяева отвечали, что им российский рынок не интересен, спроное. Это же их продукт, что хотят с ним то и делают. Какой-то странный расклад — не хотите быть партнерами, значит мы возьмем бесплатно.
также оно не равняется сумме всех баллов по всем курсам, набранных за все время. Загадочная цифра. Собственно, поэтому и задал вопрос
Вопросы не по теме поста:
Интересно, что конкретно означают недавно появившиеся звездочки в профиле?
В течение какого периода будет доступен контент по курсам (напр по курсу «Дискретные структуры») в том виде в котором он сейчас, со всем интерактивом?
PS Большое спасибо, очень интересный пост. Всегда были интересны технические детали процесса подготовки контента на Степике.
что я делаю не так?
переустанавливаю так:
apt-get purge mercurial-server
sudo rm -rf /var/lib/mercurial-server
sudo apt-get update
sudo apt-get install mercurial mercurial-server
команда hg работает, но
-bash: cd: /var/lib/mercurial-server/: No such file or directory