Блокировки при работе с СУБД



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, то ситуация может быть совсем другой. На этом уровне движок базы самостоятельно отслеживает конфликтыи лочит данные более агрессивно, и теоретически может быть разное поведение. Например, такое: блокировка до коммита первой транзакции с последующим 0 affected rows. И тогда будет парадокс: я “вижу” старую версию, но не могу её обновить. А ещё транзакции могут быть сложнее, и например для SERIALIZED конфликт разрешится откатом. Поэтому сложные транзакции с оптимистичными блокировками могут работать по-разному для уровней изоляции READ COMMITTED, REPEATABLE READ или SERIALIZED, и это нужно учитывать. Чуть позже напишу пост, как это работает в MySQL и PostgreSQL подробнее.



Не является финансовой рекомендацией ;) Кстати, насчет финансов: именно в биллинге постоянно ловили проблемы с блокировками в MySQL, и там был смешной косяк. Старые версии движка блокировок get_lock() не поддерживали множественные блокировки, но это полбеды. Настоящая беда была в том, что предыдущие блокировки просто сбрасывались молча, что могло приводить к неприятным косякам с деньгами на проде. Мы тогда просили Костю Осипова исправить это поведение, причем я изначально просил Костю не просто пофиксить поведение, но и “пропушить” его в апстрим Оракла, так как самостоятельно поддерживать кастомные сборки базы ну совсем не хотелось. Дело было давно (см. скрин), но с тех пор вроде в MySQL get_clock() работает корректно.