Блокування рядків InnoDB - як реалізувати


13

Я зараз озирався, читав сайт mysql і досі не можу точно зрозуміти, як він працює.

Я хочу вибрати та записати результат блокування для запису, написати зміну та відпустити замок. аудіокомісія увімкнена.

схема

id (int)
name (varchar50)
status (enum 'pending', 'working', 'complete')
created (datetime)
updated (datetime) 

Виберіть предмет зі статусом очікування та оновіть його до робочого. Скористайтеся ексклюзивним записом, щоб переконатися, що один і той же предмет не був зібраний двічі.

так;

"SELECT id FROM `items` WHERE `status`='pending' LIMIT 1 FOR WRITE"

отримати ідентифікатор від результату

"UPDATE `items` SET `status`='working', `updated`=NOW() WHERE `id`=<selected id>

Чи потрібно мені щось робити, щоб звільнити замок, і чи працює він так, як я робив вище?

Відповіді:


26

Що Ви хочете, це ВИБІРИТЕ ... ДЛЯ ОНОВЛЕННЯ в межах транзакції. SELECT FOR UPDATE ставить ексклюзивний замок на вибрані рядки так само, як якщо б ви виконували UPDATE. Він також неявно працює у рівні ізоляції ПРОЧИТАНО ЗАВАНТАЖЕНО незалежно від того, на який явно встановлений рівень ізоляції. Просто майте на увазі, що SELECT ... FOR UPDATE дуже поганий для одночасності, і його слід застосовувати лише при крайній необхідності. Він також має тенденцію до розмноження в кодовій базі під час вирізання та вставки людей.

Ось приклад сеансу з бази даних Sakila, який демонструє деяку поведінку ЗАПИТАННЯ запитів.

По-перше, просто, щоб ми були кристально чистими, встановіть рівень ізоляції транзакцій на ПОТРІБНЕ ЧИТАННЯ. Це, як правило, непотрібно, оскільки це стандартний рівень ізоляції для InnoDB:

session1> SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
session1> BEGIN;
session1> SELECT first_name, last_name FROM customer WHERE customer_id = 3;
+------------+-----------+
| first_name | last_name |
+------------+-----------+
| LINDA      | WILLIAMS  |
+------------+-----------+
1 row in set (0.00 sec)    

В іншому сеансі оновіть цей рядок. Лінда вийшла заміж і змінила ім'я:

session2> UPDATE customer SET last_name = 'BROWN' WHERE customer_id = 3;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

Повернувшись до сесії1, оскільки ми були у ПОВТОРЕННІ ЧИТАННЯ, Лінда все ще LINDA WILLIAMS:

session1> SELECT first_name, last_name FROM customer WHERE customer_id = 3;
+------------+-----------+
| first_name | last_name |
+------------+-----------+
| LINDA      | WILLIAMS  |
+------------+-----------+
1 row in set (0.00 sec)

Але зараз ми хочемо ексклюзивний доступ до цього рядка, тому ми закликаємо ДЛЯ ОНОВЛЕННЯ в рядку. Зауважте, що тепер ми отримуємо останню версію рядка назад, яка була оновлена ​​в session2 за межами цієї транзакції. Це не повторно читати, це ЧИТАЙТЕ ЗВ'ЯЗАНО

session1> SELECT first_name, last_name FROM customer WHERE customer_id = 3 FOR UPDATE;
+------------+-----------+
| first_name | last_name |
+------------+-----------+
| LINDA      | BROWN     |
+------------+-----------+
1 row in set (0.00 sec)

Випробуємо блокування, встановлене в сесії1. Зауважте, що session2 не може оновити рядок.

session2> UPDATE customer SET last_name = 'SMITH' WHERE customer_id = 3;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

Але ми все одно можемо вибрати з нього

session2> SELECT c.customer_id, c.first_name, c.last_name, a.address_id, a.address FROM customer c JOIN address a USING (address_id) WHERE c.customer_id = 3;
+-------------+------------+-----------+------------+-------------------+
| customer_id | first_name | last_name | address_id | address           |
+-------------+------------+-----------+------------+-------------------+
|           3 | LINDA      | BROWN     |          7 | 692 Joliet Street |
+-------------+------------+-----------+------------+-------------------+
1 row in set (0.00 sec)

І ми все одно можемо оновити дочірню таблицю із відносинами із зовнішнім ключем

session2> UPDATE address SET address = '5 Main Street' WHERE address_id = 7;
Query OK, 1 row affected (0.05 sec)
Rows matched: 1  Changed: 1  Warnings: 0

session1> COMMIT;

Ще один побічний ефект полягає в тому, що ви значно збільшуєте ймовірність спричинити тупик.

У вашому конкретному випадку ви, мабуть, хочете:

BEGIN;
SELECT id FROM `items` WHERE `status`='pending' LIMIT 1 FOR UPDATE;
-- do some other stuff
UPDATE `items` SET `status`='working', `updated`=NOW() WHERE `id`=<selected id>;
COMMIT;

Якщо фрагмент "зробити якісь інші речі" непотрібний, і вам фактично не потрібно зберігати інформацію про рядок навколо, то ВИБІР ДЛЯ ОНОВЛЕННЯ є непотрібним і марним, і ви можете замість цього просто запустити оновлення:

UPDATE `items` SET `status`='working', `updated`=NOW() WHERE `status`='pending' LIMIT 1;

Сподіваюся, це має певний сенс.


3
Спасибі. Схоже, це не вирішує мою проблему, коли два потоки надходять із "SELECT id FERE itemsWHERE status=" в очікуванні "LIMIT 1 FOR UPDATE;" і вони бачать один і той же ряд, тоді один заблокує другий. Я сподівався якось вдасться обійти заблокований рядок і перейти до наступного пункту, який очікує ..
Wizzard

1
Характер баз даних полягає в тому, що вони повертають послідовні дані. Якщо виконати цей запит двічі до оновлення значення, ви отримаєте той самий результат. Немає «отримайте мені перше значення, яке відповідає цьому запиту, якщо рядок не заблоковано» розширення SQL, про яке я знаю. Це звучить підозріло, ніби ви реалізуєте чергу над реляційною базою даних. Це так?
Аарон Браун

Аарон; так, це я намагаюся зробити. Я дивився на те, щоб використовувати щось на кшталт екіпажа - але це був бюст. Ви маєте щось на увазі?
Чарівник

Я думаю, ви повинні прочитати це: engineyard.com/blog/2011/… - для черг на повідомлення, їх там багато, залежно від мови вибору вашого клієнта. ActiveMQ, Resque (Ruby + Redis), ZeroMQ, RabbitMQ тощо
Аарон Браун

Як зробити так, щоб сеанс 2 блокував читання до моменту оновлення в сесії 1?
CMCDragonkai

2

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

Різні клієнти можуть читати однакові рядки одночасно.

Різні клієнти можуть змінювати різні рядки одночасно.

Різні клієнти не можуть змінювати один і той же рядок одночасно. Якщо одна транзакція змінює рядок, інші транзакції не можуть змінювати той самий рядок, поки перша транзакція не завершиться. Інші транзакції також не можуть читати модифікований рядок, якщо тільки вони не використовують РЕАЛІЗАЦІЙНИЙ РЕЗУЛЬТАТ рівень. Тобто вони побачать оригінальний немодифікований рядок.

В основному, вам не потрібно вказувати явне блокування InnoDB обробляє iteslf, хоча в деяких ситуаціях вам, можливо, доведеться дати чіткі деталі блокування щодо явного блокування, наведені нижче:

У наступному списку описані доступні типи блокування та їх ефекти:

ЧИТАЙТЕ

Замикає таблицю для читання. Блокування READ блокує таблицю для запитів читання, таких як SELECT, які отримують дані з таблиці. Це не дозволяє операції запису, такі як INSERT, DELETE або UPDATE, які змінюють таблицю, навіть клієнтом, який тримає замок. Коли таблиця заблокована для читання, інші клієнти можуть читати зі столу одночасно, але жоден клієнт не може писати до неї. Клієнт, який хоче записатись у таблицю із зачитуванням, повинен зачекати, поки всі клієнти, які зараз читають її, закінчать і вийдуть свої блокування.

ЗАПИСЬ

Замикає таблицю для написання. Замок WRITE - це ексклюзивний замок. Її можна придбати лише тоді, коли таблиця не використовується. Після придбання лише клієнт, який тримає блокування запису, може читати або записувати до таблиці. Інші клієнти не можуть ні читати, ні писати. Жоден інший клієнт не може заблокувати стіл ні для читання, ні для написання.

ЧИТАЙТЕ МІСЦЕ

Блокує таблицю для читання, але дозволяє одночасно вставляти. Паралельна вставка - виняток із принципу "читачі блокують письменників". Це стосується лише таблиць MyISAM. Якщо у таблиці MyISAM немає середини отворів у результаті видалених або оновлених записів, вставки завжди мають місце в кінці таблиці. У такому випадку клієнт, який читає з таблиці, може заблокувати його блокуванням READ LOCAL, щоб дозволити іншим клієнтам вставлятись у таблицю, тоді як клієнт, який тримає блокування читання, читає з неї. Якщо у таблиці MyISAM є отвори, ви можете їх видалити, використовуючи OPTIMIZE TABLE для дефрагментації таблиці.


дякую за відповідь. Оскільки у мене є ця таблиця і 100 клієнтів, які перевіряють, чи не очікують позиції, у мене виникало багато зіткнень - 2-3 клієнта отримують той самий відкладений ряд. Блокування столу - це сповільнення.
Wizzard

0

Іншою альтернативою було б додати стовпчик, який зберігав час останнього успішного блокування, а потім все, що хотілося заблокувати рядок, потрібно було б почекати, поки він не буде очищений або пройде 5 хвилин (або що завгодно).

Щось на зразок...

Schema

id (int)
name (varchar50)
status (enum 'pending', 'working', 'complete')
created (datetime)
updated (datetime)
lastlock (int)

lastlock - це int, оскільки він зберігає часову позначку unix як її простіше (а може, і швидше) порівняти.

// Вибачте з семантики, я не перевіряв, чи вони бігають гостро, але вони повинні бути досить близькими, якщо вони цього не роблять.

UPDATE items 
  SET lastlock = UNIX_TIMESTAMP() 
WHERE 
  lastlock = 0
  OR (UNIX_TIMESTAMP() - lastlock) > 360;

Потім перевірте, скільки рядків було оновлено, оскільки рядки не можуть бути оновлені двома процесами одночасно, якщо ви оновили рядок, ви отримали блокування. Припустимо, що ви використовуєте PHP, ви б використовували mysql_affected_rows (), якщо повернення від цього було 1, ви успішно заблокували його.

Тоді ви можете або оновити останнє замовлення до 0 після того, як ви зробите те, що вам потрібно зробити, або лінуватися і чекати 5 хвилин, коли наступна спроба блокування все-таки буде успішною.

РЕДАКТУВАННЯ: Можливо, вам доведеться трохи попрацювати, щоб перевірити його роботу так, як очікувалося, коли зміниться літній час, оскільки годинник повернеться на годину назад, можливо, чек недійсний. Вам потрібно буде переконатися, що часові позначки unix були в UTC - які вони все одно можуть бути.


-1

Крім того, ви можете фрагментувати поля запису, щоб дозволити паралельне записування та обхід блокування рядків (фрагментарний стиль json пар). Отже, якщо одне поле складеної записи читання було цілим / реальним, ви могли б мати фрагмент 1-8 цього поля (фактично 8 записів / рядків запису). Потім підсумовуйте фрагменти круглобільно після кожного запису в окремий пошук читання. Це дозволяє паралельно виконувати до 8 одночасних користувачів.

Оскільки ви працюєте лише з кожним фрагментом, створюючи часткову сукупність, немає зіткнення та справжніх паралельних оновлень (тобто ви записуєте блокування кожного фрагмента, а не цілої уніфікованої записи читання). Це очевидно працює лише на числових полях. Щось, що спирається на математичну модифікацію для збереження результату.

Таким чином, кілька фрагментів запису на об'єднане поле читання на об'єднану запис читання. Ці числові фрагменти також піддаються ECC, шифруванню та блоку передачі / зберігання рівня. Чим більше фрагментів запису є, тим більша паралельна / паралельна швидкості запису на насичені дані.

MMORPG сильно страждає від цього питання, коли велика кількість гравців починають вражати один одного навичками Area of ​​Effect. Ці кілька гравців повинні записувати / оновлювати кожного іншого гравця точно в той самий час, паралельно, створюючи шторм блокування рядків для запису на об'єднаних записах гравців.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.