Введение
Когда мы разрабатываем интернет-приложения, легко столкнуться с проблемами взаимоблокировки в сценариях с высокой степенью параллелизма.По исключению взаимоблокировки, созданному jdbc, трудно увидеть конкретную причину взаимоблокировки.jdbc только что выдал исключение взаимоблокировки.lock, Однако причина взаимоблокировки не выдается, потому что сам mysql не выдает больше сообщений об ошибках при возникновении взаимоблокировки. Анализ блокировок MySQL/InnoDB также более сложен для разработки приложений.Из-за сложности блокировок многие статьи о блокировках базы данных не имеют фактической проверки, а носят скорее показной, чем спекулятивный характер, что несколько вводит в заблуждение. Ниже приведен процесс анализа случая взаимоблокировки, вызванного вставкой и обновлением одних и тех же данных индекса, в основном для описания решения проблемы для справки.
проблема найдена
Одноклассник X обнаружил следующее исключение взаимоблокировки в рабочей среде и хотел, чтобы я вместе исследовал его.После входа на сервер я увидел следующую информацию об исключении:
在分库:[ worker_10~~>jdbc:mysql://hostname:3306/dbname?user=user ],
执行SQL:[ UPDATE tablename SET col1 = col1 + 20, modified_date = NOW() WHERE order_id = 'xxx' ],
发生异常:Deadlock found when trying to get lock; try restarting transaction;
Устранение неполадок
Из журнала мы не можем увидеть конкретную причину взаимоблокировки.Я прочитал код стека исключений, который похож на проблему взаимоблокировки книги заказов, которую я делал раньше, потому что в последний раз, когда я проверял, я ссылаюсь Я собрал много информации, некоторые из которых также вводят в заблуждение, поэтому я воспроизведу причину этой взаимоблокировки, проанализирую причину взаимоблокировки с помощью реальных операций и проанализирую, как решить проблему взаимоблокировки в этом сценарии.
Мы можем проверить журнал взаимоблокировок, показав состояние innodb механизма, чтобы увидеть, можем ли мы увидеть, какой SQL выполняется.
2019-07-18 10:03:03 7f16ff826700
*** (1) TRANSACTION:
TRANSACTION 46497170213, ACTIVE 0 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1184, 2 row lock(s)
MySQL thread id 118471353, OS thread handle 0x7f1c2fe77700, query id 146140919609 10.240.24.25 dbname updating
UPDATE tablename SET col1 = col1 + 20, modified_date = NOW() WHERE order_id = 'xxx'
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 2632 page no 5065 n bits 440 index `uniq_table_name_col1` of table `tablename` trx id 46497170213 lock_mode X locks rec but not gap waiting
Record lock, heap no 374
*** (2) TRANSACTION:
TRANSACTION 46497170214, ACTIVE 0 sec starting index read, thread declared inside InnoDB 5000
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1184, 2 row lock(s)
MySQL thread id 118465375, OS thread handle 0x7f16ff826700, query id 146140919617 172.25.213.222 dbname updating
UPDATE tablename SET col1 = col1 + 30, modified_date = NOW() WHERE order_id = 'xxx'
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 2632 page no 5065 n bits 440 index `uniq_table_name_col1` of table `tablename` trx id 46497170214 lock mode S rec but not gap
Record lock, heap no 374
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 2632 page no 5065 n bits 440 index `uniq_table_name_col1` of table `tablename` trx id 46497170214 lock_mode X locks rec but not gap waiting
Record lock, heap no 374
*** WE ROLL BACK TRANSACTION (2)
Интерпретация журналов взаимоблокировок
Давайте сначала посмотрим на этот журнал взаимоблокировок, сосредоточив внимание на красном содержимом выше.Прочитав его, вы обнаружите, что журнал взаимоблокировок неполный? Блокировка S (режим блокировки S) сохраняется в TRANSACTION2, здесь мы не можем видеть, кто ее добавил. Это также одна из причин сложности анализа тупиковой ситуации.В настоящее время нам нужно вернуться к делу и понять логику всей транзакции. Хотя мы не получили всю информацию для разрешения взаимоблокировки, она все же дала нам полезную информацию, Мы определили, какие два SQL-запроса вызвали взаимоблокировку, и увидели S-блокировку в транзакции 2. Здесь мы можем посмотреть на программу, которая выполняет SQL, особенно на то, как добавляется S-блокировка. После расследования я обнаружил, что в программе есть строка кода, которая написана так:
try {
xxxService.insert(xx);
} catch(DuplicateKeyException duplicateKeyException) {
xxxService.updateXxByIdx(xx);
}
существующие знания
Взаимная блокировка снова вызвана проблемой вставки?
Давайте рассмотрим официально предоставленный MySQL случай вставки, вызывающий взаимоблокировку, а затем продолжим анализ проблемы взаимоблокировки, которая может быть вызвана этим местом. Подобные сценарии уже решались ранее, вы можете обратиться к этомувставить тупик.
If a duplicate-key error occurs, a shared lock on the duplicate index record is set. This use of a shared lock can result in deadlock should there be multiple sessions trying to insert the same row if another session already has an exclusive lock. Suppose that an InnoDB table t1 has the following structure:
Чтобы объяснить это предложение примерно, когда возникает ошибка дублирования ключа, к записи индекса добавляется S-блокировка. Когда один сеанс удерживает блокировку X, а затем несколько сеансов одновременно вставляют одну и ту же строку, это может привести к взаимоблокировке. Давайте рассмотрим этот случай:
-- table structure
CREATE TABLE t1 (i INT, PRIMARY KEY (i)) ENGINE = InnoDB;
-- Session 1:
START TRANSACTION;
INSERT INTO t1 VALUES(1);
-- Session 2:
START TRANSACTION;
INSERT INTO t1 VALUES(1);
-- Session 3:
START TRANSACTION;
INSERT INTO t1 VALUES(1);
-- Session 1:
ROLLBACK;
The first operation by session 1 acquires an exclusive lock for the row. The operations by sessions 2 and 3 both result in a duplicate-key error and they both request a shared lock for the row. When session 1 rolls back, it releases its exclusive lock on the row and the queued shared lock requests for sessions 2 and 3 are granted. At this point, sessions 2 and 3 deadlock: Neither can acquire an exclusive lock for the row because of the shared lock held by the other.
Объясните это предложение: сеанс 1 получает блокировку X, и когда ошибка дублирования ключа возникает в сеансах 2 и 3, он одновременно запрашивает блокировку S. Когда сеанс 1 выполняет откат, он освобождает блокировку X. В это время и сеанс 2, и сеанс 3 одновременно получают блокировку S и запрашивают блокировку X. Вызвал тупик. Давайте взглянем на эти отношения между владением и конкуренцией:
Это место является основой для понимания взаимоисключающих и сосуществующих отношений замков.Давайте посмотрим на эти отношения:
| S | X | |
|---|---|---|
| S | Не конфликтовать | конфликт |
| X | конфликт | конфликт |
Поскольку блокировки X и S являются взаимоисключающими, сеанс 2 требует блокировку X и должен ждать снятия блокировки S сеанса 3. Если сеанс 3 хочет получить блокировку X, сеанс 2 также должен освободить блокировку S. В это время, формируется ожидание цикла, вызывающее взаимоблокировку.
Вернемся к нашему делу
Очевидно, что наш случай вызван не простой вставкой, но подумайте, в этом месте существует та же проблема: если дубликат ключа сеанса 2 и сеанса 3 неверен, они одновременно запросят S-блокировку. Затем, когда вы выполняете данные обновления, обновление должно Добавьте блокировку X. Когда они запрашивают блокировку X, им необходимо снять блокировку S. В это время формируется ожидание цикла и возникает взаимоблокировка. Давайте смоделируем эту сцену. В этом месте мы предполагаем, что данные уже существуют. Я уменьшил чувствительность имени таблицы в приведенном выше выражении главным образом потому, что это онлайн-информация. Далее, Я создал тестовые данные непосредственно для имитации:
-- table structure
CREATE TABLE `tablename` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`col1` varchar(32)DEFAULT NULL COMMENT '订单号',
`out_money` bigint(20) DEFAULT '0' COMMENT '转出金额',
`in_money` bigint(20) DEFAULT '0' COMMENT '转入金额',
`created_date` datetime NOT NULL COMMENT '创建时间',
`modified_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间',
`mer_date` varchar(8) COLLATE utf8_bin DEFAULT NULL COMMENT '时间切分键',
`in_settle_money` bigint(20) DEFAULT '0' COMMENT '结算转入金额',
`out_settle_money` bigint(20) DEFAULT '0' COMMENT '结算转出金额',
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_order_account_3413` (`order_id`)
) ENGINE=InnoDB AUTO_INCREMENT=220016 DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='订单金额表';
-- transaction 1
begin;
insert into order_account_info_3413 values(200014,2000,0,0,now(),now(),20180101,0,0);
-- transaction 2
begin;
insert into order_account_info_3413 values(200014,2000,0,0,now(),now(),20180101,0,0);
-- transaction 1
update order_account_info_3413 set in_money= in_money + 1 where order_id = 2000;
-- transaction 2
update order_account_info_3413 set in_money= in_money - 1 where order_id = 2000;
-- transaction 2
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction~~~~
Это действительно единственная проблема?
Вернитесь и снова посмотрите на этот код:
if (xxxService.getXxByIdx(XX) != null) {
try {
xxxService.insert(xx);
} catch(DuplicateKeyException duplicateKeyException) {
xxxService.updateXxByIdx(xx);
}
} else {
xxxService.updateXxByIdx(xx);
}
Предположим, что состояние выполнения программы такое? Будет ли проблема? Нить 1 проходит через 1 и 2 до 3, а нить 2 проходит через 1 до 2.
Давайте смоделируем этот сценарий:
-- transaction 1
begin;
insert into order_account_info_3413 values(200014,2000,0,0,now(),now(),20180101,0,0);
-- transaction 2
begin;
update order_account_info_3413 set in_money= in_money + 1 where order_id = 2000;
-- transaction 1
update order_account_info_3413 set in_money= in_money - 1 where order_id = 2000;
-- transaction 2
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction~~~~
Зачем?
Головокружение, анализируйте этот процесс.
| time | session 1 | session2 |
|---|---|---|
| 1 | begin; | |
| 2 | вставить в order_account_info_3413 значения(200014,2000,0,0,now(),now(),20180101,0,0); Конфликты уникальных индексов, добавьте блокировку S к уникальному индексу | |
| 3 | begin; | |
| 4 | обновить order_account_info_3413, установить in_money= in_money + 1, где order_id = 2000; добавить блокировку X и дождаться блокировки S сеанса 1 | |
| 5 | обновить order_account_info_3413 установить in_money= in_money - 1 где order_id = 2000; это место должно выполнить проверку взаимоблокировки, подумать о взаимоблокировке, а затем откатить session2 | |
| 6 | ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction~~~~ |
Я не совсем понимаю это место.При проверке дедлока,почему он определяется как дедлок?Угадайте,потому что session2 сначала встал в очередь в ожидании X блокировки,но ему нужна S блокировка session1,а session1 нужна чтобы взять X-блокировку, но т.к. строка session2 входит в очередь ожидания X-блокировки, поэтому ей нужно дождаться, пока session2 получит X-блокировку и освободит S-блокировку. Кажется, это имеет смысл!
проверять
Если вы все еще не понимаете это место, вы можете отладить выполнение mysql.Ядро - это место для обнаружения взаимоблокировок.Как он думает, что существует взаимоблокировка и откатить сессию2?Мы можем скачать исходный код mysql, скомпилировать и отладить, ядро находит запись кода для обнаружения и обработки взаимоблокировок в Innodb: Мы обнаружили, что функцией обнаружения взаимоблокировок является функция lock_deadlock_occurs в storage/innobase/lock/lock0lock.c, которая вызывает функцию lock_deadlock_recursive для итеративной проверки взаимоблокировок:
static
ulint
lock_deadlock_recursive(
/*====================*/
trx_t* start, /*!< in: recursion starting point */
trx_t* trx, /*!< in: a transaction waiting for a lock */
lock_t* wait_lock, /*!< in: lock that is waiting to be granted */
ulint* cost, /*!< in/out: number of calculation steps thus
far: if this exceeds LOCK_MAX_N_STEPS_...
we return LOCK_EXCEED_MAX_DEPTH */
ulint depth); /*!< in: recursion depth: if this exceeds
LOCK_MAX_DEPTH_IN_DEADLOCK_CHECK, we
return LOCK_EXCEED_MAX_DEPTH */
lock_deadlock_recursive - это главная итерация функции. Запустите начальную транзакцию, должен быть определен замок WAIT_LOCK, стоимостью и расходной и совокупной глубиной глубины итерации.
Внутри есть очень важная логика обнаружения взаимоблокировок.Когда транзакция пытается получить (запросить) блокировку и ей нужно подождать, innodb начнет обнаружение взаимоблокировок.
if (lock_has_to_wait(wait_lock, lock)) {
ibool too_far
= depth > LOCK_MAX_DEPTH_IN_DEADLOCK_CHECK
|| *cost > LOCK_MAX_N_STEPS_IN_DEADLOCK_CHECK;
lock_trx = lock->trx;
if (lock_trx == start) {
/* We came back to the recursion starting
point: a deadlock detected; or we have
searched the waits-for graph too long */
}
}
Мы обнаружили, что когда функция рекурсивно повторяется во второй раз, значение lock_trx равно start, и возникает взаимоблокировка цикла. Хотели бы вы стать лучше? Давайте проанализируем, что же сделали эти две рекурсии? Этот вопрос я иМастер Чжан, который копает ямуПосле совместного обсуждения он записал идею отладки, которая является хорошей ссылкой.
Как с этим бороться?
- Первый вариант:
insert into order_account_info_3413 values(200014,2000,0,0,now(),now(),20180101,0,0) on duplicate key update in_money= in_money + 1;? - Второе решение: распределенная блокировка?
- Третье решение: когда происходит вставка, дубликат-ключ не выполняет обновление, а напрямую выдает исключение и снова ходит?
Первое решение,кофе латтеОдноклассник сказал, что есть еще проблема с дедлоком.Я не воспроизвел это исключение в этом месте под уровнем изоляции RC и RR. На данный момент я думаю, что этот метод доступен. Преимущество этого метода в том, что mysql предоставляет этот механизм защиты, и нет необходимости защищать его через программы. Недостаток в том, что эта ответственность передается mysql. ,ресурсы БД ценнее.И есть люди которые так пишут Обратная связь проблематична, особенно при наличии нескольких уникальных индексов. Поэтому при его использовании требуется определенное испытание давлением, либо требуется более систематическое изучение MySQL.
Второе решение, распределенная блокировка, является относительно простым решением. Эффективность не так уж плоха за счет замены параллелизма на сериализацию. Как правило, в интернет-приложениях это относительно распространенный метод. Недостатком является то, что он вводит новые компоненты, одного не хватает в этом месте Дело в том, что если это внешний интерфейс, то в этом месте также необходимо учитывать идемпотентность и доступность интерфейса, что выходит за рамки данной статьи.
Третья программа, в зависимости от стратегии повтора, проста в этом отношении, но если это внешний интерфейс, то еще придется рассмотреть вопрос доступности интерфейса.
В технологиях не существует абсолютно лучших практик. Лучшие практики основаны на бизнес-сценариях, где вы можете выбрать и рассмотреть лучшие решения на основе ваших бизнес-сценариев.
Суммировать
Вышеуказанные практики все выполняются на уровне изоляции транзакций RC, а интернет-решения в основном фрагментированы, так как мы должны научиться определять достоверность информации и проводить определенные попытки и эксперименты. Старайтесь систематически изучать официальные документы, статьи в Интернете используются как Пособия по пониманию. Затем медленно сформируйте свою собственную систему.
В этой статье также упоминаются:
официальное руководство по mysql