InnoDB блокировка строк - как реализовать

13

Я сейчас осматриваюсь, читаю сайт MySQL и до сих пор не понимаю, как это работает.

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

схема

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>

Нужно ли что-то делать, чтобы снять блокировку, и она работает, как я делал выше?

Wizzard
источник

Ответы:

26

То, что вы хотите, это SELECT ... FOR UPDATE из контекста транзакции. SELECT FOR UPDATE устанавливает эксклюзивную блокировку выбранных строк, как если бы вы выполняли UPDATE. Он также неявно работает на уровне изоляции READ COMMITTED независимо от того, какой уровень изоляции установлен явно. Просто помните, что SELECT ... FOR UPDATE очень вреден для параллелизма и должен использоваться только тогда, когда это абсолютно необходимо. Он также имеет тенденцию к умножению в кодовой базе по мере того, как люди копируют и вставляют.

Вот пример сеанса из базы данных Sakila, который демонстрирует некоторые поведения запросов FOR UPDATE.

Во-первых, просто для ясности установите уровень изоляции транзакции на REPEATABLE READ. Обычно это не нужно, так как это уровень изоляции по умолчанию для 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, потому что мы были в REPEATABLE READ, Линда по-прежнему ЛИНДА УИЛЬЯМС:

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)

Но теперь нам нужен эксклюзивный доступ к этой строке, поэтому мы вызываем FOR UPDATE для этой строки. Обратите внимание, что теперь мы получаем самую последнюю версию строки, которая была обновлена ​​в 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)

Давайте проверим блокировку, установленную в session1. Обратите внимание, что 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;

Если часть «делать что-то другое» не нужна, и вам не нужно хранить информацию о строке вокруг, тогда SELECT FOR UPDATE является ненужным и расточительным, и вместо этого вы можете просто запустить обновление:

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

Надеюсь, в этом есть смысл.

Аарон Браун
источник
3
Благодарю. Кажется, это не решает мою проблему, когда два потока входят с "SELECT id FROM itemsWHERE status= 'pending' LIMIT 1 FOR UPDATE;" и они оба увидят один и тот же ряд, тогда один заблокирует другой. Я надеялся, что каким-то образом он сможет обойти заблокированный ряд и перейти к следующему пункту, который ожидал ..
Wizzard
1
Природа баз данных заключается в том, что они возвращают согласованные данные. Если вы выполните этот запрос дважды, прежде чем значение будет обновлено, вы получите тот же результат обратно. Нет никакого «получить мне первое значение, которое соответствует этому запросу, если строка не заблокирована» расширения SQL, о котором я знаю. Это звучит подозрительно, как будто вы реализуете очередь поверх реляционной базы данных. Это тот случай?
Аарон Браун
Аарон; да, это то, что я пытаюсь сделать. Я смотрел на использование чего-то вроде шестеренки - но это был перебор. Вы имеете в виду что-то еще?
Wizzard
Я думаю, что вы должны прочитать это: engineyard.com/blog/2011/… - для очередей сообщений их много, в зависимости от выбранного вами языка клиента. ActiveMQ, Resque (Ruby + Redis), ZeroMQ, RabbitMQ и т. Д.
Аарон Браун
Как сделать так, чтобы сеанс 2 блокировался на чтение до тех пор, пока не будет зафиксировано обновление в сеансе 1?
CMCDragonkai
2

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

Разные клиенты могут читать одни и те же строки одновременно.

Разные клиенты могут изменять разные строки одновременно.

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

По сути, вам не нужно указывать явную блокировку. InnoDB обрабатывает ее самостоятельно, хотя в некоторых ситуациях вам может потребоваться предоставить подробные сведения о блокировке, касающиеся явной блокировки, приведенной ниже:

В следующем списке описаны доступные типы блокировок и их эффекты:

ЧИТАТЬ

Блокирует стол для чтения. Блокировка 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_affered_rows (), если возвращаемое значение равно 1, вы успешно заблокировали его.

Затем вы можете либо обновить последнюю блокировку до 0 после того, как вы сделали то, что вам нужно, или быть ленивым и подождать 5 минут, когда следующая попытка блокировки все равно будет успешной.

РЕДАКТИРОВАТЬ: Вам может потребоваться немного работы, чтобы проверить, как это работает, как ожидается, в летнее время, так как часы вернутся на час назад, возможно, делая проверку недействительной. Вы должны убедиться, что метки времени Unix были в UTC - какими они могут быть в любом случае.

Стив Чайлдс
источник
-1

В качестве альтернативы вы можете фрагментировать поля записи, чтобы разрешить параллельную запись и обойти блокировку строки (стиль фрагментированных пар json). Таким образом, если одно поле записи сложного чтения было целым / действительным, вы могли бы иметь фрагмент 1-8 этого поля (в действительности 8 записей / строк записи). Затем суммируйте фрагменты циклически после каждой записи в отдельном поиске для чтения. Это позволяет до 8 одновременно работающих пользователей.

Поскольку вы работаете только с каждым фрагментом, создающим частичное итоговое значение, нет столкновений и истинно параллельных обновлений (т.е. вы записываете блокировку каждого фрагмента, а не всей объединенной записи чтения). Это работает только на числовые поля, очевидно. То, что опирается на математическую модификацию для сохранения результата.

Таким образом, несколько фрагментов записи на одно поле чтения на одну запись чтения. Эти числовые фрагменты также пригодны для ECC, шифрования и передачи / хранения на уровне блоков. Чем больше фрагментов записи, тем выше скорость параллельного / параллельного доступа к записи для насыщенных данных.

MMORPG сильно страдает от этой проблемы, когда большое количество игроков начинают бить друг друга умениями Area of ​​Effect. Этим нескольким игрокам необходимо одновременно писать / обновлять каждого другого игрока, параллельно, создавая шторм блокировки строк записи для записей объединенного игрока.

Мик Сондерс
источник