Перейти до основного вмісту

Конкурентний доступ

Управління конкурентним доступом

Блокування

Є два базові типи блокувань:

  • shared lock - спільне блокування, блокування на читання - дає змогу іншим транзакціям читати рядок і ставити на нього таке саме спільне блокування, але не дає змоги змінювати рядок або ставити виняткове блокування.
  • exclusive lock - виняткове блокування, блокування на запис - забороняє іншим транзакціям блокувати рядок, а також може блокувати рядок як на запис, так і на читання залежно від поточного рівня ізоляції.

Також блокування можна поділити за детальністю:

  • Табличне блокування - блокує всю таблицю. Коли клієнт хоче виконати запис у таблицю (вставку, видалення, оновлення тощо), він захоплює блокування на запис для всієї таблиці. Таке блокування запобігає всім іншим операціям читання і запису. У той момент, коли ніхто не здійснює запис, будь-який клієнт може отримати блокування на читання, і воно не конфліктуватиме з іншими аналогічними блокуваннями.
  • Порядкове блокування - Блокування рядків реалізуються підсистемами зберігання даних, а не сервером (погляньте на ілюстрацію логічної архітектури). Сервер нічого не знає про блокування, реалізовані підсистемою зберігання даних.

Явні та неявні блокування

У підсистемі InnoDB використовується двофазний протокол блокування. Вона може встановлювати блокування в будь-який момент часу протягом усієї транзакції, але не знімає їх до виконання команди COMMIT або ROLLBACK. Усі блокування знімаються одночасно. Описані раніше механізми блокування є неявними. InnoDB обробляє блокування автоматично відповідно до рівня ізоляції. Однак InnoDB підтримує і явне блокування, яке в стандарті SQL взагалі не згадується:

SELECT... LOCK IN SHARE MODE - блокує зчитувані рядки на запис. Інші сесії можуть читати, але чекають закінчення транзакції для зміни порушених рядків. Якщо ж у момент такого SELECT'а рядок уже змінено іншою транзакцією, але ще не зафіксовано, то запит чекає закінчення транзакції і потім читає свіжі дані. Ця конструкція потрібна, як правило, для того, щоб отримати найсвіжіші дані (незалежно від часу життя транзакції) і заразом переконатися в тому, що їх ніхто не змінить.

SELECT... FOR UPDATE - блокує зчитувані рядки на читання. Точно таке ж блокування ставить звичайний UPDATE, коли зчитує дані для оновлення.

MySQL також підтримує команди LOCK TABLES і UNLOCK TABLES, які реалізуються сервером, а не підсистемою зберігання. У них є своє застосування, але вони не є заміною транзакціям. Якщо вам потрібні транзакції, використовуйте транзакційну підсистему зберігання.

Існує ще 2 типи блокувань, назвемо їх блокуваннями "про наміри". Не можна просто так узяти і заблокувати запис в InnoDB. Блокування intention shared і intention exclusive є блокуваннями на рівні таблиці і блокують тільки створення інших блокувань і операції на всій таблиці типу LOCK TABLE. Накладення такого блокування транзакцією лише повідомляє про намір цієї транзакції отримати відповідне спільне або виключне блокування рядка.

InnoDB накладає блокування не на самі рядки з даними, а на записи індексів. Вони бувають таких типів:

Record Lock

Блокування індексного запису - таке блокування відбувається, якщо умова запиту обмежує тільки один запис унікального індексу (unique index); наприклад, якщо в таблиці t поле c1 є унікальним індексом та існує запис, для якого с1 = 10, то під час виконання блокуючого читання SELECT * FROM t WHERE c1 = 10 FOR UPDATE InnoDB встановить блокування на цей індекс і не допустить, щоб інша транзакція вставила, оновила чи видалила рядок із полем с1 = 10. Якщо виконати той самий запит, але запису з полем с1 = 10 (а відповідно, і запису індексу) не буде існувати, то це вже буде блокування інтервалу

Gap Lock

Блокування інтервалу - відбувається, коли блокується інтервал між індексними записами, інтервал до першого індексного запису або інтервал після останнього індексного запису; припустимо, що в таблиці є два рядки, для яких с1 = 10 і c1 = 20, тобто індекс містить значення 10 і 20. інтервалами будуть такі відрізки: (мінус нескінченність, 10), (10, 20), (20, плюс нескінченність); якщо ми виконаємо запит на блокуюче читання неіснуючого поки запису SELECT * FROM t WHERE c1 = 15 FOR UPDATE, то буде блоковано інтервал від 10 до 20, але не включно, тобто оновити граничні записи можна буде в інтервалі від 10 до 20, тобто в інтервалі від 10 до 20, але не включно. тобто оновити граничні записи можна, можна навіть їх видалити, а ось вставлення нового рядка в цей інтервал буде блоковано; ще один цікавий приклад: якщо виконати попередній запит на блокуюче читання рядка, але таблиця t буде порожньою, то заблокується інтервал розміром у весь індексний простір, тобто вся таблиця

Next Key Lock

Блокування наступного ключа - комбінація блокувань індексних записів і блокувань інтервалів; візьмемо попередній приклад, але виконаємо інший запит: SELECT * FROM t WHERE c1 > 15 у цьому разі, окрім індексу зі значенням 20, заблокуються також інтервали (10, 20) та (20, плюс нескінченність). При цьому рядок з індексом 10 можна змінювати, тому що він не блокується; у загальному випадку блокованих індексних інтервалів та індексних записів, які блокуються, може бути набагато більше, все залежить від умов блокуючого запиту.

У момент першого запиту в транзакції створюється снепшот даних БД (т.зв. read view), на який не впливають зміни в паралельних транзакціях, але впливають зміни в поточній. Читання з такого снепшота називають неблокуючим узгодженим читанням. Не блокувальним, тому що для створення такого снепшота не потрібне навішування блокувань, узгодженим, тому що жодні катаклізми в зовнішньому світі (крім DROP TABLE і ALTER TABLE) не вплинуть на затишний світ снепшота. InnoDB можна попросити зробити снепшот і до першого запиту в транзакції, для цього потрібно згадати про це під час старту транзакції - START TRANSACTION WITH CONSISTENT SNAPSHOT.

DeadLock

Deadlock(взаємоблокування) - ситуація, де транзакції не здатні продовжити роботу тому, що заблокували ресурси, потрібні одна одній. Відбувається, коли дві або більше транзакції запитують блокування одних і тих самих ресурсів, унаслідок чого утворюється циклічна залежність. Вони також виникають у разі, якщо +транзакції намагаються заблокувати ресурси в різному порядку.

Для розв'язання цієї проблеми в системах баз даних реалізовано різні форми виявлення взаємоблокувань і тайм-аутів. InnoDB, виявляють циклічні залежності і негайно повертають помилку. Насправді це дуже добре, інакше взаємоблокування проявлялися б як дуже повільні запити. Інші системи в подібних ситуаціях відкочують транзакцію після закінчення тайм-ауту, що не дуже добре. У разі виявлення дедлоку, InnoDB автоматично відкочує транзакцію, з найменшим числом ефектед (вставлених, видалених або змінених) рядків.

InnoDB використовує автоматичне блокування рівня рядка. Ви можете створити взаємоблокування навіть у разі транзакцій, які всього лише додають або видаляють одиничний рядок. Це відбувається через те, що насправді ці операції не є "атомарними": вони автоматично встановлюють блокування на індексні записи рядків, що додаються/видаляються (або на кілька записів).

Взаємоблокування не можна дозволити без відкату однієї з транзакцій, часткового або повного. Існування взаємоблокувань у транзакційних системах - непорушний факт, з огляду на який ваш застосунок і потрібно проектувати. У разі виникнення такої ситуації багато застосунків можуть просто спробувати виконати транзакцію з самого початку.

Ви можете уникнути взаємоблокувань або зменшити їхню кількість, дотримуючись таких прийомів:

  • Використовуйте SHOW INNODB STATUS в MySQL починаючи з 3.23.52 і 4.0.3 для визначення причини останнього взаємоблокування. Це допоможе вам налаштувати ваш додаток, щоб уникнути взаємоблокувань.
  • Завжди готуйте перезапуск транзакції, якщо стався відкат через взаємоблокування. Взаємоблокування не є небезпечним: всього лише спробуйте ще раз.
  • Частіше фіксуйте свої транзакції. Маленькі транзакції менше схильні до суперечностей.
  • Якщо ви використовуєте читання з блокуванням SELECT ... FOR UPDATE або ... LOCK IN SHARE MODE, спробуйте використовувати нижчий рівень ізоляції READ COMMITTED.
  • Проводьте операції з вашими таблицями і рядками у фіксованому порядку. Тоді транзакції формуватимуть чергу і не відбуватиметься взаємоблокування.
  • Додайте хороші індекси на ваші таблиці. Тоді ваші запити скануватимуть менше індексних записів і, відповідно, встановлюватимуть менше блокувань. Використовуйте EXPLAIN SELECT для того, щоб дізнатися, чи вибирає MySQL відповідний індекс для ваших запитів.
  • Використовуйте менше блокувань: якщо ви можете допустити, щоб SELECT повертав дані зі старого знімка, не додавайте до виразу FOR UPDATE або LOCK IN SHARE MODE. Використовуйте рівень ізоляції READ COMMITTED, який найбільше підходить для цієї ситуації, оскільки кожне узгоджене читання всередині однієї і тієї самої транзакції читає свій власний свіжий знімок.
  • Якщо нічого не допомогло, серіалізуйте свої транзакції з блокуванням рівня таблиць: LOCK TABLES t1 WRITE, t2 READ, .... ; [тут можете розважатися з таблицями t1 і t2]; UNLOCK TABLES. Блокування на рівні таблиць вибудовує ваші транзакції в чергу, і дає змогу уникнути взаємоблокування. Зауважте, що LOCK TABLES неявно починає транзакцію на кшталт BEGIN, і UNLOCK TABLES неявно завершує її в COMMIT.
  • Інше рішення для серіалізації транзакцій - це створення допоміжного "семафора" таблиці, де є всього лише один рядок. Кожна транзакція оновлює цей рядок перед доступом до іншої таблиці. У цьому випадку всі транзакції виконуються у вигляді черги. Зазначимо, що в такий самий спосіб зараз працює й алгоритм визначення взаємоблокувань в InnoDB, оскільки блокування серіалізації - це блокування рівня рядка. У разі блокування на рівні таблиці в MySQL ми використовуємо метод таймауту для вирішення взаємоблокування.

Динамічне взаємоблокування (livelock) означає таку ситуацію: система не "застряє" (як у звичайному взаємному блокуванні), а займається даремною роботою, її стан постійно змінюється - але, проте, вона "зациклилася", не виконує жодної корисної роботи.

Життєвий приклад такої ситуації: двоє зустрічаються віч-на-віч. Кожен із них намагається посторонитися, але вони не розходяться, а кілька секунд зсуваються в один і той самий бік.

Livelock - це програми, які активно виконують паралельні операції, але ці операції ніяк не впливають на просуваннястану програми вперед.

Ситуація, в якій два або більше процесів безперервно змінюють свої стани у відповідь на зміни в інших процесах без будь-якої корисної роботи. Це схоже на deadlock, але різниця в тому, що процеси стають "ввічливими" і дозволяють іншим робити свою роботу.

Виконання алгоритмів пошуку видалення взаємних блокувань може призвести до livelock - взаємне блокування утворюється, скидається, знову утворюється, знову скидається і так далі.

Livelock - це підмножина ширшого набору проблем, які називаються Starvation.

Starvation - це будь-яка ситуація, коли паралельний процес не може отримати всі ресурси, необхідні для виконання його роботи.

Multiversion Concurrency Control (MVCC)

Велика частина транзакційних підсистем зберігання в MySQL, наприклад InnoDB, використовують не просто механізм блокування рядків, а блокування рядків у поєднанні з методикою підвищення ступеня конкурентності під назвою MVCC (multiversion concurrency control - багатоверсійне управління конкурентним доступом). MVCC дає змогу в багатьох випадках взагалі відмовитися від блокування і здатна значно знизити накладні витрати. Залежно від способу реалізації вона може допускати читання без блокувань, а блокувати лише необхідні рядки під час операцій запису.

Принцип роботи MVCC полягає у збереженні миттєвого знімка даних, якими вони були в деякий момент часу. Це означає, що незалежно від своєї тривалості транзакції можуть бачити узгоджене подання даних. Це також означає, що різні транзакції можуть бачити різні дані в одних і тих самих таблицях в один і той самий час! Якщо ви ніколи не стикалися з цим раніше, то напевно будете здивовані.

InnoDB реалізує MVCC шляхом збереження з кожним рядком двох додаткових прихованих значень, у яких записано, коли рядок було створено та коли термін його зберігання закінчився (або його було видалено). Замість запису реальних значень моменту часу, коли відбулися зазначені події, рядок зберігає системний номер версії для цього моменту. Це число збільшується на одиницю на початку кожної транзакції. Нова транзакція на момент її початку зберігає свій власний запис поточної версії системи. Будь-який запит має порівнювати номери версій кожного рядка з версією транзакції. Подивімось, як ця методика застосовується до конкретних операцій, коли транзакція має рівень ізоляції REPEATABLE READ:

SELECT - Підсистема InnoDB повинна перевірити кожен рядок, щоб переконатися, що він відповідає двом критеріям:

  • InnoDB повинна знайти версію рядка, яка принаймні така ж стара, як версія транзакції (тобто її номер версії має бути меншим або дорівнювати номеру версії транзакції). Це гарантує, що або рядок існував до початку транзакції, або транзакція створила або змінила цей рядок.
  • Версія видалення рядка повинна бути не визначена або її значення більше, ніж версія транзакції. Це гарантує, що рядок не було видалено до початку транзакції. Рядки, які проходять обидві перевірки, можуть бути повернуті як результат запиту.
    • INSERT - InnoDB записує поточний системний номер версії разом із новим рядком.
    • DELETE - InnoDB записує поточний системний номер версії як ідентифікатор видалення рядка.
    • UPDATE - InnoDB створює нову копію рядка, використовуючи системний номер версії як версію нового рядка. Вона також записує системний номер версії як версію видалення старого рядка.

Результатом зберігання всіх цих додаткових записів є те, що більшість запитів на читання ніколи не ставить блокування. Вони просто зчитують дані настільки швидко, наскільки можна, забезпечуючи вибірку тільки тих рядків, які задовольняють заданому критерію. Недоліком такого підходу є те, що підсистема зберігання повинна записувати для кожного рядка додаткові дані, виконувати зайву роботу під час перевірки рядків і виконувати деякі додаткові службові операції.

Методика MVCC працює тільки на рівнях ізоляції REPEATABLE READ і READ COMMITTED. Рівень READ UNCOMMITTED несумісний з MVCC, оскільки запити не зчитують версію рядка, що відповідає їхній версії транзакції. Вони читають найостаннішу версію, незважаючи ні на що. Рівень SERIALIZABLE несумісний з MVCC, оскільки операції читання блокують кожен рядок, що повертається.