Задача управления конкурентным доступом возникает в тот момент, когда нескольким запросам необходимо одновременно изменить данные. В рамках текущих моих блогов оговорим, что MySQL должна решать эту задачу на двух уровнях: сервера и подсистемы хранения данных. Управление конкурентным доступом — это обширная тема, которой посвящено множество теоретических исследований. Мы же просто представим обзор того, что MySQL делает с конкурентными запросами на чтение и запись, чтобы вы смогли получить общее представление об этой теме, а это, в свою очередь, позволит разобраться в данном материале.
В качестве примера будем использовать ящик электронной почты в системе UNIX. Классический файл формата mbox
очень прост. Все сообщения в почтовом ящике mbox расположены одно за другим, так что читать и анализировать почтовые сообщения очень просто. Это также существенно упрощает доставку почты: достаточно добавить новое сообщение в конец файла.
Но что происходит, когда два процесса пытаются одновременно поместить сообщения в почтовый ящик? Очевидно, что чередование строк этих сообщений приведет к повреждению файла. Чтобы предотвратить это, правильно работающие почтовые системы используют блокировку. Если клиент пытается отправить новое сообщение в тот момент, когда почтовый ящик заблокирован, ему придется подождать, пока он не сможет сам использовать блокировку, чтобы отправить сообщение.
Эта схема довольно хорошо работает, но не поддерживает конкурентный доступ. Поскольку в любой момент времени только один процесс может изменять содержимое почтового ящика, такой подход создает проблемы при работе с большими почтовыми ящиками.
Блокировки чтения/записи в MySQL
Чтение из почтового ящика не вызывает таких проблем. Ничего страшного, если несколько клиентов одновременно считывают информацию из одного и того же почтового ящика. Раз они не вносят изменений, ничего плохого случиться не должно. Но что произойдет, если кто-нибудь попытается удалить сообщение № 25 в тот момент, когда программы читают письма из почтового ящика? Возможны различные варианты развития ситуации, но программа чтения может получить почтовый ящик в поврежденном или неструктурированном виде. Поэтому для обеспечения безопасности даже чтение информации из почтового ящика требует определенных предосторожностей.
Если представить, что почтовый ящик — это таблица базы данных, а каждое почтовое сообщение — строка, легко увидеть, что и в этом контексте актуальна та же проблема. Во многих смыслах почтовый ящик является простой таблицей базы данных. Модификация строк в такой базе очень похожа на удаление или изменение содержимого сообщений в файле почтового ящика.
У классической задачи управления конкурентным доступом довольно простое решение. В системах, которые имеют дело с конкурентным доступом для чтения/записи, чаще всего реализуется система блокирования, содержащая два типа блокировок. Обычно их называют разделяемыми блокировками и монопольными блокировками, или блокировками чтения и блокировками записи.
Не вдаваясь в подробности технологии блокирования, данную концепцию можно описать следующим образом. Блокировки чтения ресурса являются разделяемыми или взаимно неблокирующими: множество клиентов могут производить считывание из ресурса в одно и то же время, не мешая друг другу. Блокировки записи, напротив, являются эксклюзивными. Другими словами, они исключают возможность установки блокировки чтения и других блокировок записи, поскольку единственной безопасной политикой является наличие только одного клиента, выполняющего запись в данный момент времени, и предотвращение во время этого всех операций чтения.
В базах данных блокировки происходят постоянно: MySQL запрещает одному клиенту считывать данные, когда другой клиент их изменяет. Управление блокировками осуществляется внутри СУБД в соответствии с принципами, которые достаточно прозрачны для клиентов.
Детальность блокировок
Одним из способов улучшения конкурентного доступа к разделяемому ресурсу является увеличение избирательности блокировок. Вместо того чтобы блокировать весь ресурс, можно заблокировать только ту его часть, которая содержит изменяемые данные. Еще лучше заблокировать лишь тот фрагмент данных, который будет изменен. Минимизация объема данных, которые вы блокируете в каждый момент времени, позволяет одновременно выполнять несколько изменений одного и того же ресурса, если эти операции не конфликтуют друг с другом.
Определенная проблема возникает из-за того, что блокировки потребляют ресурсы. Каждая операция блокирования — получение возможности блокировки, проверка того, можно ли применить блокировку, снятие блокировки и т. п. — влечет за собой издержки. Если система тратит слишком много времени на управление блокировками вместо того, чтобы расходовать его на сохранение и извлечение данных, то это может повлиять на производительность.
Стратегия блокирования является компромиссом между неизбежностью издержек на реализацию блокировок и безопасностью данных, причем подобный компромисс влияет на производительность. Большинство коммерческих серверов баз данных не предоставляют особого выбора: вы получаете возможность блокировки таблиц на уровне строки, при этом нередко в сочетании с множеством сложных способов обеспечить хорошую производительность при большом количестве блокировок.
MySQL также предоставляет выбор. Подсистемы хранения данных MySQL могут реализовывать собственные стратегии блокировки и уровни детализации блокировок. Управление блокировками является очень важным решением при проектировании подсистем хранения данных. Установка детализации на определенном уровне в ряде случаев может улучшить производительность, но сделать эту подсистему менее подходящей для других целей. Поскольку MySQL предлагает несколько подсистем хранения данных, нет необходимости принимать единственное решение на все случаи жизни. Рассмотрим две наиболее важные стратегии блокировок.
Табличные блокировки
Табличная блокировка является базовой стратегией блокировки в MySQL. Кроме того, у нее самые низкие издержки. Табличная блокировка аналогична рассмотренным ранее блокировкам почтового ящика — блокируется вся таблица. Когда клиент хочет сделать запись в таблицу (вставку, удаление, обновление и т. п.), он получает блокировку на запись для всей таблицы. Это предотвращает все остальные операции чтения и записи. Когда никто не выполняет запись, любой клиент может получить блокировку на чтение, которая не конфликтует с другими подобными блокировками.
У табличных блокировок есть вариации для обеспечения высокой производительности в различных ситуациях. Например, табличные блокировки READ LOCAL
разрешают выполнять некоторые типы параллельных операций записи. Кроме того, блокировки записи имеют более высокий приоритет, чем блокировки чтения. Поэтому запрос блокировки записи будет помещен в очередь перед уже находящимися там запросами блокировки чтения (блокировки записи могут отодвигать в очереди блокировки чтения, а блокировки чтения не могут отодвигать блокировки записи).
Подсистемы хранения также могут управлять собственными блокировками.
MySQL использует множество блокировок, эффективных на уровне таблиц, для различных целей. Например, для таких операторов, как ALTER TABLE
, сервер применяет табличную блокировку вне зависимости от подсистемы хранения данных.
Построчные блокировки
Построчные блокировки обеспечивают лучшее управление конкурентным доступом (и влекут максимальные издержки). Блокировка на уровне строк доступна, в частности, в подсистемах хранения InnoDB и XtraDB. Построчные блокировки реализуются подсистемами хранения данных, а не сервером (если нужно, еще раз взгляните на рис. 1. из поста об архитектуре MySQL). Сервер ничего не знает о блокировках, реализованных подсистемой хранения данных, и, как вы увидите далее, все подсистемы хранения данных реализуют блокировки по-своему.