Как стать автором
Обновить

Комментарии 59

Вы правда честно и искренне предлагаете блокировать запись в БД на время обращения ко внешнему API?

Скажите, пожалуйста, а что вы будете делать в сценарии «вы дернули внешний API, все успешно, но коммит в БД не прошел»?
в ситуации выше хотелось лишь понять, знает ли кандидат как пользоваться одной из фич innodb а именно построчным локом.

для звонка во внешний апи вариантов тут несколько:
-до звонка уменьшить баланс и закоммититься
-без построчного лока попытаться захватить запись способом вроде " update requests set status=%pid where status=0 and id=123" и продолжить
в ситуации выше хотелось лишь понять, знает ли кандидат как пользоваться одной из фич innodb а именно построчным локом.

Так у вас задача для этого не предназначена. Возможно, люди не знают про построчный лок — а возможно, считают, что внешний API в транзакцию включать нельзя.

без построчного лока попытаться захватить запись способом вроде " update requests set status=%pid where status=0 and id=123" и продолжить

А это — не построчный лок?
не на уровне бд. а смысл похож
коммит не пройдет из-за deadlock-a? если нужные строки залочить, то всё должно пройти.
Из-за чего угодно, начиная с падения сервера.
согласен, в такой ситуации море мест и вариантов где можно потерять данные.
Плакать и смеяться, пряники жевать, очевидно же :)
По мне так логичней сначала апдейтить баланс, а потом уже дёргать внешнее АПИ. Если внешнее АПИ зафейлится, никто не мешает нам заново проапдейтить, вернув баланс обратно.
По заголовку статьи рассчитывал не большее. «Транзакции в mysql», а тут только один маленький частный случай: select for update.

Я бы вообще с GET_LOCK/RELEASE_LOCK делал, и работало бы независимо от движка таблицы, и таким образом логику блокирования можно вынести в приложение.
Вы дополнили название статьи.
Хорошим тоном было бы написать небольшой upd. в конце статьи, если что-то было изменено (не считая опечаток), а то некоторые комментарии, как мой выше, становятся идиотскими.
исправил
Я думаю, что если внешний апи платный, то должно точно вестись логирование использования такого апи, а следовательно можно применить следующую схему:
таблицы
-user поле баланс
-queue со статусом (1-запланировано, деньги не сняты, 2 деньги сняты, 0 задача выполнена), и результатом запроса к внешнему апи

при нажатии кнопки

-INSERT INTO queue SET status = 1,… (autocommit)
-BEGIN; UPDATE queue SET status = 2 WHERE id = xx; UPDATE user SET balance = balance — yyy WHERE id = zzz;COMMIT;
-Запрос к внешнему API,
если все хорошо то
UPDATE queue SET status = 0, result = 'aaaa' WHERE id = xx;
если нет то
BEGIN; UPDATE queue SET status = 1 WHERE id = xx; UPDATE user SET balance = balance + yyy WHERE id = zzz;COMMIT;

При таком раскладе вы будете знать на каком этапе запрос. Если что то упадёт то:
1.Будет стоять в очереди невыполненная задача, баланс не тронут
2.Баланс списан, задача на выполнении.
3.Баланс списан, задача исполнена.

Если присутствует ситуация баланс списан, задача не исполнена в течении нужного времени, админ должен это увидеть.
Если в задаче появляется внешние api — то это задача про two phase commit

Применительно к django это можно реализовать через отдельную таблицу с операциями и их статусами. Ну а в конце, да, обновление баланса и пометку операции как пройденной можно обернуть в короткую транзакцию с блокировкой баланса.

Картинка не моя, но красивая

— достаем из базы баланс пользователя;
— если баланса достаточно, то дергаем API;
— если всё хорошо, то списываем с баланса сумму за услугу, делаем UPDATE, коммитим, иначе откатываемся;
— отвечаем пользователю

Если это обернуть в транзакцию, всё должно отработать правильно.
«Правильно» — это как? Представьте, что у вас баланса достаточно на одну операцию, а за время обращения к API пришел еще один запрос. Баланс не уменьшен? Не уменьшен, запустится вторая операция. На коммите все плохо? Плохо, откатываем транзакцию, но API-то уже был вызван дважды.
юзер жмет на кнопку «activate» — мы лочим баланс, холдим деньги и переведим запрос в статус TO_PROCESS

далее 2 таска:
-лок и смена статуса на что-то вроде PROCESSING + коммит + api call
-получение колбека либо опрос сервиса и перевод запроса в конечное состояние COMPLETE

но всё равноу нас должен быть либо способ узнать статус на удаленном сервисе либо получить обратный звонок от него. Иначе на любом этапе можно потерять данные, слепо дернуть api без обратной связи безопасно не получится
Это уже совсем другое решение, не имеющее ничего общего с «обернем в транзакцию».
сначала где-то увидел что я предлагаю дергать API в рамках лока, сейчас «обернем в транзакцию неуместно». Мессадж поста «немного» не тот. не?
Мессадж поста, как это часто бывает, оказался потерян за неудачным примером.
[Кличко моде он]
Не только лишь все понимают FOR UPDATE, мало кто может это делать.
[Кличко моде офф]
именно, до смешного. Львиная доля студентов у нас знает что такое классы в яп, а вот что с ними делать понимают от силы 5%.
Очень опасная команда. Я за такую (ну хорошо, за подобные) бил по руками и прочим частям тела до понимания. Почему? Потому что в любом развитом сайте это мгновенно приводит к огромному количеству локов, роняющих производительность в ноль.

Например.

Скажем, у нас есть веб-сайт, где у пользователей есть личный кабинет, в котором показывается баланс. Любой показ баланса вызывает лок. Поднимите руку те, кто считает, что лок обходится бескровно? Делать две функции показа баланса?

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

Есть сайты с развитым апи, к которым цепляются мобильные клиенты. Лазит себе юзер по сайту, а тот внезапно начинает тормозить из-за того, что мобильный клиент начал какую-то операцию, связанную с балансом?

Так что ТС выбрал очень плохой вариант и соискатели оказались просто опытней :)

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

Во-вторых, вы путаете сайт и standalone клиент. Если даже мы сделали select for update для чтения баланса, мы отдали страницу пользователю, запрос завершен, транзакция завершена, блокировка снята. Вне зависимости от того сколько окон у пользователя открыто.
Пока делается select for update, второй такой же select будет висеть, нет?
второй select for update будет висеть да, но это миллисекунды пока страница не уйдет пользователю.
По определению внешний сервис работает дольше нескольких миллисекунд. И если он первый успел заблочить, то запрос легко может провисеть и секунды.

Плюс такие запросы в случае многосерверной конфигурации типа «мастер-мастер» или «мастер-куча слейвов» очень сильно дают по производительности. Пока они локи засинхронизируют, а потом локи снимут…

В общем, надо искать другие задачи/примеры, когда реально без таких запросов не обойтись. Но если честно, так сразу в голову ничего не приходит.
Я про случай со страницой просто показывающей баланс говорю.
По определению внешний сервис работает дольше нескольких миллисекунд.

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

Плюс такие запросы в случае многосерверной конфигурации типа «мастер-мастер» или «мастер-куча слейвов» очень сильно дают по производительности. Пока они локи засинхронизируют, а потом локи снимут…

а такую репликацию вряд ли кто то будет делать, если только не псих

Я с чикагскими биржами не работал, я работаю с обычными, широко распространенными сервисами :) Скажем, вызов api от apple для проверки покупки легко может занять пару-тройку секунд (а может и сотней миллисекунд обойтись, да и то большая часть на сетевые издержки уходит).

И такие реплики делают гораздо чаще, чем Вам кажется. Например так часто делают для случаев, когда пишут в базу редко, зато читают очень и очень часто и помногу.
Сама база данных при этом работает очень короткое время. http сервер принял запрос — отдал его приложению. Приложение стартануло транзацию. Полочила необходимое, выполнило действие. commit. После это результат отдается http серверу. А сколько оно уже отправляется клиенту, какие там сетевые издержки базе данных все равно. Она идет дальше и выполняет запросы. При этом без лочек никто финансовые инструменты не делает, это я вам как разработчик биллинговых систем заявляю. Так что вам бы немного поразбираться в устройстве серверных приложений работающих с деньгами, а не делать голословные заявления.
Вообще-то изначально была речь про внешний апи, который вместе с фронтэндом обращается к одним и тем же данным. И тогда «полочила необходимое» легко может превратиться в паузу для отдачи страницы.

про «без лочек никто не делает» — бывает, если вы и есть никто. У меня есть совершенно противоположный опыт во вполне успешных проектах, которые гоняют миллионы транзакций без локов, хранимых процедур и прочего.
да как же можно сравнивать задачи по неблокирующей раздаче статики и stale кешей с процессингом? обычно юзер бд от фронтенда читает реплики процессинговых данных и уж точно не имеет прав на запись в таблицы (базы) процессинга. это разные объекты и по сущности и по ответсвенности.
Мы почему-то уперлись в процессинг. Думаю, стоит свернуть тред. Ибо по основным тезисам ни у кого нет возражений (ну или мне так кажется). А как оно применяется и где — тут можно долго меряться
я признаю провал этого поста, так как суть его совсем не в этом. не в том как процессинги строить и апи дергать) через неделю еще раз попробую)
По вашему посту видно, что работа с сервисами где учитывают деньги вас обошла стороной. Может, оно и к лучшему. Не поверите, но не просто так сделали построчную блокировку и транзакции, не только для того, чтобы были deadlock-и и второе окно тормозило. По дефолту на session file тоже ставится лок и вторая страница не открывается. Это так, на всякий случай.
Видимо про подводные камни при переезде на хранение сессий в бд тоже есть смысл написать
Может в oracle напишем, что второе окно тормозит?
Да легко, если деньги есть лишние :)
По-моему, блокировка не лучший способ решения проблемы. Тут скорее всего надо делать просто транзакцию, которую потом можно откатить в случае чего. Но в этом случае АПИ тоже должен поддерживать некий откат, т.к. при конфликтующих транзакциях коммит не пройдёт, надо сделать откат в АПИ, и далее повторить попытку. Поправьте плз, если это решение плохо.
Помимо предложенного метода в статье предложу более общий вариант — оптимистическая блокировка:

UPDATE users SET balance = ${newValue} WHERE balance=${expectedOldValue};

Данный запрос вернет 0, если ожидаемый баланс не совпадет с текущим — т.е. кто-то нас опередил и наши данные о балансе больше не актуальны.
вообще учет денег так нигде не делается (из бух учета по-моему). всегда на каждое изменение баланса — одна запись в бд. баланс — это сальдо.
Я лишь хотел напомнить про подход как таковой.

Если нужна оптимистическая блокировка в ситуации INSERT-а строки вместо UPDATE существующей, то можно использовать внешнюю таблицу для лока:

«Добавить новую строку баланса, проставив новую ревизию в глобальную таблицу ревизий, если ожидаемая старая ревизия такая-то»

Надеюсь идея понятна.
вот в статье я о том и пытался написать, что как бы там ни было, все пробуют придумать свой вариант блокировки (в комментах уже штук 5 наберется). я не против — варианты могут быть вполне рабочими, но есть же встроенные инструменты.
бывают неоткатываемые сервисы) совсем неоткатываемые.
С неоткатываемым сервисом видимо придётся реализовывать протокол двухфазного коммита, как предлагали выше.
На собеседованиях обычно спрашивают то, о чем сами узнали недавно :)
Или то, с чем недавно столкнулись и решили.
Вы правда честно и искренне считаете, что если на собеседовании python программиста спрашивают " что такое generator comprehension?" то это означает, что либо сами недавно узнали, либо недавно столкнулись и решили?)

также можно спросить «как сделать гугл» или «почему по индексу быстро ищется» или «чем плох питон».
или тут есть существенная разница?)
Нет, не означает. Но тем не менее, когда я на собеседовании перехожу от тривиальных вопросов к нетривиальным, я неизбежно спрашиваю что-то, с чем моя команда недавно сталкивалась (потому что это означает, что задача (а) достаточно сложная (б) решаемая (ц) сравнительно актуальная по технологиям).
НЛО прилетело и опубликовало эту надпись здесь
Я не жду решения, а смотрю на то, сталкивался ли человек с таким ранее (некоторые утверждают, что сталкивались, но в итоге способов решения предложить не могут), и на то, в какую сторону и как он размышляет.
НЛО прилетело и опубликовало эту надпись здесь
или же всё таки интересует опыт человека?
заметьте, что это не вопрос из разряда «как сделать чтоб в javascript plus(4)(5) вернула 9?»
Что интересует и что спрашивают — это две разные вещи.
лучше всегда в лоб — что интересно, то и спрашиваешь, а то начинают сопли жевать, то хихи то хаха, а по делу ноль по вдоль.
Бессмысленно спрашивать «расскажите о вашем опыте» — человек никогда не отвечает то, что мне нужно услышать. Поэтому все равно нужно задавать вопросы по тем областям, которые интересуют.
НЛО прилетело и опубликовало эту надпись здесь
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории