Привет, Хабр. Меня зовут Владислав Родин. В настоящее время я являюсь руководителем курса «Архитектор высоких нагрузок» в OTUS, а также преподаю на курсах, посвященных архитектуре ПО.

Эту статью я подготовил специально к старту нового набора на курс «Архитектор высоких нагрузок».




Введение


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


Блокировочники


Как я уже сказал, один из подходов основан на блокировках. Его также называют пессимистичным подходом. База данных может блокировать строчки с точки зрения выполнения операций чтения и записи. Все транзакции, работающие с какой-либо строчкой, выстраиваются в очередь. После выполнения транзакции блокировка снимается.

Виды блокировок


База данных в зависимости от выставленного уровня изоляции и операции, которая должна быть выполнена, может выставлять блокировки разных видов. К самым популярным видам блокировок относятся Shared и Exclusive lock. Для простоты можно считать, что Shared lock является блокировкой на запись. Она накладывается в случае выполнения какого-нибудь select'а, причем одна запись может быть вычитана несколькими транзакциями параллельно. Exclusive lock выставляется в том случае, если осуществляется update или delete некоторой строчки. При таком виде блокировки чтение этой строчки осуществить невозможно.

Механизм двухфазных блокировок


Для обеспечения изоляции может применяться механизм так называемых двухфазных блокировок. Двухфазные блокировки (2PL) часто путают с двухфазными коммитами (2PC), хотя они относятся к совершенно разным контекстам. Алгоритм состоит из двух фаз (что следует из его названия): установка блокировки и ее снятие.

Работает это так: когда транзакция приходит с тем, чтобы изменить какую либо запись, она устанавливает блокировку на запись и ждет, пока все блокировки на запись и на чтение, установленные ранее, будут сняты. После этого, транзакция вносит свои изменения и убирает установленную ею блокировку. Если же транзакция пришла с тем, чтобы прочитать запись, она устанавливает блокировку на чтение и ждет, пока все блокировки на запись, установленные ранее, будут сняты. После этого транзакция вычитывает нужные ей данные и снимает установленную блокировку.

Механизм двухфазных блокировок не позволяет избавиться разве что от phantom reads, потому что заблокировать то, чего еще в базе данных нет, невозможно. Для решения этой проблемы могут привлекаться предикативные блокировки.

Эскалация блокировок


Одной из самых неприятных ситуаций, связанных с блокировками, является эскалация блокировки. Иногда даже параллельные и непересекающиеся операции update могут висеть в ожидании друг друга. База данных может посчитать, что вместо того, чтобы заблокировать много строк, ей будет дешевле заблокировать страницу/индекс/таблицу/базу данных. Это и называется эскалация блокировки. Грань, по которой определяется степень эскалации блокировки, проходит по-разному в зависимости от базы данных. Это может быть определенный % изменений, превышение фиксированного количества изменяемых строк. Если число запросов велико, эскалация может приводить к тому, что транзакции могут начать отваливаться по таймауту.

Deadlock


Следующая неприятность, которая может подстерегать при использовании блокировок, это deadlock. Таблицы могут использоваться разными транзакциями в разном порядке и быть ими заблокированными. Внутренний процесс может обнаружить такую ситуацию и убить одну транзакцию. Выбор часто падает на маленькую транзакцию, которая изменила не очень много данных. Проблема здесь заключается в том, что большая транзакция могла быть вызвана фоновым процессом, а маленькая — клиентом, т. е. такой подход не учитывает бизнес — смысла транзакций. Как вариант, можно разбивать большую фоновую транзакцию на маленькие кусочки, либо регламентировать порядок работы с объектами. Также причиной deadlock'а может стать упомянутая выше эскалация блокировки.

Механизм отката


Механизм отката транзакции в блокировочниках заключается в том, что команды выполняются в обратном порядке. Этот процесс может занимать достаточно существенное количество времени, ведь если транзакция несколько часов что-то меняла, а вы делаете kill сессии, то будут возвращены удаленные записи, а на каждый update будет осуществлен откатывающий update, который в свою очередь будет ждать снятия блокировок, поставленных другими транзакциями и инициировать установление своей блокировки.

Заключение


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



Если вы дочитали до конца, приглашаю на свой бесплатный вебинар по теме «Индексы в MySQL: best practices и подводные камни».