Блокировки при работе с СУБД
Badoo (нынче Bumble, единственный серьезный конкурент Tinder на мировом рынке) когда-то был полноценной социальной сетью. Например, у меня в профайле были сотни фотографий. Лента обновлений была полна всяких событий, по разнообразию совсем не уступающих Фейсбуку, особенно в теперешние времена (кто с кем подружился, что поделал, кого полайкал - с кучей свистоплясок вокруг privacy настроек). И конечно, был мессенжер, и с мессенжером была куча проблем. Мы всё делали на базах, и одна из типичных проблем подобных проектов - битые денормализованные счетчики непрочитанных сообщений. Мы избегали частых операций count и старались все счётчики денормализовать. Бывало, где-то косячили, счетчики начинали биться, а отлаживать большую систему, в которой обновления могут прийти из разных кусков – непросто. Косячили не так чтобы много, и вроде работали достаточно стабильно, но время от времени счетчики всё равно бились. Это не только была наша проблема - хорошо помню минимум два крайне неприятных косяка с битыми счетчиками в Телеграме и мессенжере Фейсбука, на которые напарывался сам как юзер.
Есть несколько способов побороть такие проблемы, и самый простой и прямолинейный - оптимистичные блокировки в начале транзакции (select get_lock(), например). Первая транзакция получает блокировку, а вторая транзакция просто не стартует, тк вначале попытается получить блокировку на уже заблокированй ресурс.
А ещё есть метод оптимистичных блокировок через версионирование. Кажется, мы его использовали редко, хотя метод прикольный. Суть в следующем. Куда-то в базе добавляется версия, и обновление идёт не просто по ключу, а по ключу с дополнительным условием, которое гарантирует, что транзакция изменяет “свою” версию. После обновления можно проверить количество обновленных рядов, и если 0 - обработать эту ситуацию как ошибочную.
Однако, вдумчивый читатель мог насторожиться: у СУБД бывают разные уровни изоляции. И действительно, описанная выше логика не универсальна. Уровень READ UNCOMMITTED рассматривать несерьезно. Если у нас уровень изоляции READ COMMITTED, то оптимистичные блокировки работают, как описано выше: вторая транзакция, которая пытается обновить данные с указанием устаревшей версии, не сможет этого сделать, так как версия уже поменялась. В этом случае никаких ошибок нет, но количество обновленных строк равно 0, и вот эту ситуацию (0 affected rows) нужно поймать и обработать. Но READ COMMITTED default уже не у всех баз, у MySQL в отличие от PostgreSQL, уровень по умолчанию – REPEATABLE READ. А вот если у нас уровень изоляции REPEATABLE READ или даже SERIALIZED, то ситуация совсем другая. На этом уровне движок базы самостоятельно отслеживает конфликты, и просто не даст обновить данные, которые успела обновить другая транзакция. И скорее всего в таком случае вся транзакция откатится, нужно будет её повторять. Поэтому оптимистичные блокировки будут работать по-разному для уровней изоляции READ COMMITTED, REPEATABLE READ или SERIALIZED, и это нужно учитывать.
Пишу не проверяя, так что не является финансовой рекомендацией. Кстати, насчет финансов: именно в биллинге постоянно ловили проблемы с блокировками в MySQL, и там был смешной косяк. Старые версии движка блокировок get_lock() не поддерживали множественные блокировки, но это полбеды. Настоящая беда была в том, что предыдущие блокировки просто сбрасывались молча, что могло приводить к неприятным косякам с деньгами на проде. Мы тогда просили Костю Осипова исправить это поведение, причем я изначально просил Костю не просто пофиксить поведение, но и “пропушить” его в апстрим Оракла, так как самостоятельно поддерживать кастомные сборки базы ну совсем не хотелось. Дело было давно (см. скрин), но с тех пор вроде в MySQL get_clock() работает корректно.
Post #150
299
