Немного про Deadlock

Это совсем краткий пост о причинах возникновения Deadlock

В более менее нагруженных проектах, использующих транзакции InnoDB, в любой момент может возникнуть ошибка вида

«Deadlock found when trying to get lock; try restarting transaction»

Главное не паниковать при виде этих страшных слов, сейчас мы разберемся почему это происходит.


Немного о типах блокировок

В оффициальной документации Mysql про типы блокировок написано совсем немного, а именно:

Есть 2 типа блокировок — Shared (S) и Exclusive (X). Первый тип позволяет только читать данные прикрытые этой блокировкой, второй — читать, писать, удалять и (о чем скромно умолчали) — получить блокировку уровня S

Так же сказано что если Транзакция№1 владеет блокировкой типа S на строке r, то другая Транзакция№2 может захватить эту блокировку. Чтобы получить блокировку типа X на этой строке, второй транзакции придется тихо подождать в сторонке.

Если же Транзакция№1 владеет блокировкой типа X на строке r, то Транзакция№2 не может ни захватить эту же блокировку, ни получить новую уровня S. Она опять тихо идет и ждет пока Транзакция№1 освободит требуемую строку.

Здесь есть один важный момент, который необходимо усвоить: блокировки S и X — это 2 разные блокировки. Это не значит что блокировка S, это какое-то подмножество блокировки X. Это две разных сущности.

Вернемся к дедлокам. На некоторых форумах я встречал вопросы «Как получит deadlock в Mysql». На самом деле очень просто.

Все необходимы ингредиенты у нас имеются в наличии: две транзакции, блокировки типа S и X и строка, на которую получают блокировки.

Краткий рецепт приготовления deadlock на одной строке
1) Транзакция№1 получает блокировку S и продолжает работу
2) Транзакция№2 пытается получить блокировку типа X и… начинает ждать когда Транзакция№1 освободит блокировку S
3) Транзакция№1 пытается получить блокировку типа X и… начинает ждать когда Транзакция№2 получит блокировку типа X и освободит её

Блюдо подано

Тут есть один скользкий момент. Казалось бы что мешает Транзакции№1 получить блокировку X если она уже имеет блокировку S на этой же строке. А мешает то о чем мы говорили
1) Во-первых X и S это две разных блокировки
2) Во-вторых блокировка типа S не дает право на получение блокировки типа X. Никаких привилегий — в очередь!

Код для ситуации выше

Transaction #1
BEGIN;
SELECT * FROM `testlock` WHERE id=1 LOCK IN SHARE MODE; /* GET S LOCK */
SELECT SLEEP(5);
SELECT * FROM `testlock` WHERE id=1 FOR UPDATE; /* TRY TO GET X LOCK */
COMMIT;

Transaction #2
BEGIN;
SELECT * FROM `testlock` WHERE id=1 FOR UPDATE; /* TRY TO GET X LOCK - DEADLOCK AND ROLLBACK HERE */
COMMIT;


Как же с этим бороться? Офф. сайт Mysql советует комититься почаще, а так же перепроверять код ошибки и перепроводить откатившуюся транзакцию. Мне кажется есть вариант получше — сразу получать блокировку типа X. Тогда на третьем шаге нашего рецепта Транзакция№1 смогла бы получить свою законную блокировку и спокойно завершиться

Напоследок скажу что определить причину deadlock поможет команда SHOW ENGINE INNODB STATUS, которая показывает какие блокировки кто держит и какие ожидает
Метки:
Поделиться публикацией
Похожие публикации
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 49
  • +14
    > Офф. сайт Mysql советует комититься почаще

    Этот совет выглядит как «составляйте имена переменных подлиннее, чтобы наверняка не создать ещё одну переменную с таким же именем».

    Транзакции должны быть такими, какими должны быть, не длиннее, не короче. Длина транзакции должна трактоваться бизнес-задачей и здравым смыслом. Слепое следование совету «короче-лучше» ни к чему хорошему не приведёт.
    • 0
      Меня это тоже мягко говоря удивило )
      • 0
        Нормальный совет, старая как мир ошибка ожидания пользовательского ввода внутри транзакции или чтение файлика из сети. Может быть совет из разряда не переходите дорогу в неправильных местах, но это не значит что такие советы не нужны
        • 0
          И какая проблема этим советом, собственно, решается?

          старая как мир ошибка ожидания пользовательского ввода внутри транзакции или чтение файлика из сети.


          Если не все операции ещё завершены, и для продолжения работы с данными нужен дополнительный ввод (файл из сети, пользовательский ввод) то что вы предлагаете делать?

          Коммитить? Неконсистентные данные?
          • 0
            Сначала запрашиваем или получаем все данные необходимые для транзакции, затем вставляем и коммитим. Не получив всех требуемых данных даже не начинаем транзакцию.
            • 0
              У нас может не быть достаточно информации, чтобы запросить их до начала работы. Данные могут поступать во время, или зависеть вообще от текущего состояния базы (которое до начала транзакции невозможно предугадать в принципе).

              Пример навскидку: обработка какой-то очереди файлов, очередь в таблице. Обработчиков больше одного.
              • 0
                Это скорее всего проблема архитектуры приложения. За много лет не было такого случая. когда между begin tran и commit было что-то в конец неопределенное, которое может занять секунду, а может час. Но хорошо помню случай когда один программист у нас при открытии модального окна начинал транзакцию, инсертил что-то, по «Ок» коммитил, по «Cancel» роллбачил. О последствиях нетрудно догадаться. Полконторы висело в блокировке когда менеджер открыл этот диалог и ушел курить. Я думаю тут речь именно об этом. А случаев разных кривых конечно много можно придумать.
                • 0
                  Предложите ровное решение.

                  Пускай у нас есть mysql с путями до файлов, и файлы в фс.

                  Нужно обрабатывать эти файлы. Обработчиков больше одного. После обработки запись из таблицы удаляется.
                  • 0
                    Уверен что решение есть :) Надо смотреть систему в целом. Как вариант — не держать пути к файлам в mysql :)
                    Я говорю — суррогатных примеров можно много придумать, но как раз совет про то что такие случаи надо решать. Как — это и есть работа разработчика в каждом конкретном случае. Не пойму о чем спор? Совет правильный, не всегда ему можно следовать к сожалению, если было бы иначе и было бы универсальное решение, то и дедлоков не было бы совсем.
            • 0
              Например. Пользователи популярных Java-фреймворков начинают смотреть в настройки по умолчанию наконец и узнают почему SELECT, запущенный 3 часа назад, заблокировал сотню апдейтов.
        • +1
          Этот совет почти как ПДД: написан кровью на основании практики работы с приложениями, страдающими от deadlock-ов, случившихся в результате необдуманного использования autocommit=0 (а это значение по умолчанию во многих популярных framework-ах) и транзакций, запущенных часами. Классический пример: autocommit=0, connection pool, делаем SELECT и забываем закрыть транзакцию.
          • +3
            Совет «не забываем закрывать транзакции» смысл имеет.

            Совет «делайте транзакции короче» — нет. Никто в здравом уме не будет делать транзакции длиннее чем нужно. И наоборот — короче чем есть транзакцию сделать нельзя, потому что бизнес требует, чтобы определённые операции выполнялись в единой транзакции. Так что делать её короче — значит не выполнять бизнес-требования.

            PS: я комментирую исходя из того, что программисты руководствуются здравым смыслом и понимают, что делают.

            PPS: ещё аналогия — «делайте транзакции короче» это то же самое что и «выбирайте меньше записей» (в случае, например, если мы реализуем экспорт и нам физически нужно выбрать все записи).
            • 0
              Совет «делайте транзакции короче» — нет. Никто в здравом уме не будет делать транзакции длиннее чем нужно. И наоборот — короче чем есть транзакцию сделать нельзя, потому что бизнес требует, чтобы определённые операции выполнялись в единой транзакции.


              Ещё один часто встречающийся пример: залить таблицу из бэкапа на боевом, скажем, слэйве. 100,000 записей. Имеет смысл разбить на несколько транзакций?

              > PS: я комментирую исходя из того, что программисты руководствуются здравым смыслом и понимают, что делают.

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

              > PPS: ещё аналогия — «делайте транзакции короче» это то же самое что и «выбирайте меньше записей» (в случае, например, если мы реализуем экспорт и нам физически нужно выбрать все записи).

              А что? Нормальная аналогия. Часто эффективнее выбирать данные по частям, чем сразу все. К сожалению, MySQL не поддерживает server-side cursors =(
              • 0
                > А что? Нормальная аналогия. Часто эффективнее выбирать данные по частям, чем сразу все. К сожалению, MySQL не поддерживает server-side cursors =(

                Я в моей аналогии выражал другое, скорее не то, что не нужно выбирать много за раз, а что вообще не нужно выбирать много совсем. Никогда.

                > Ещё один часто встречающийся пример: залить таблицу из бэкапа на боевом, скажем, слэйве. 100,000 записей. Имеет смысл разбить на несколько транзакций?

                Зависит от задачи. Если с таблицей больше никто не работает — то 1 транзакция лучше. Но я с mysql давно уже не работаю плотно. В оракле 1 транзакция была бы лучше и правильнее.
                • 0
                  > Я в моей аналогии выражал другое, скорее не то, что не нужно выбирать много за раз, а что вообще не нужно выбирать много совсем. Никогда.

                  =)

                  > Зависит от задачи. Если с таблицей больше никто не работает — то 1 транзакция лучше. Но я с mysql давно уже не работаю плотно. В оракле 1 транзакция была бы лучше и правильнее.

                  В MySQL это не всегда так.
                  • 0
                    zerkms, мы знаем чтоты грамотный разработчик, и для тебя совет «делайте транзакции короче» вполне очевиден, одняко для большинства разработчиков — это грабли…
      • 0
        Жаль в MySQL нет автономных транзакций. Можно было бы получить dead lock из одной сессии :)
        • 0
          Я не понимаю, если MySQL может задетектить сложившуюся патовую ситуацию, то почему он не может разрулить её способом, который для всех очевиден? Какие в этом способе подводные камни и будут ли они больше, чем существующий dead lock?
          • 0
            В данном случает он разруливает её единственно верным способо — откатом последней транзакции, которая учавствует в образовании deadlock. Но мне кажется тут какая-то архитектурная проблема, ведь логично же в третьем шаге дать возможность первой транзакции получить блокировку, после чего, дать доступ второй транзакции. Но происходит так, как происходит
            • +1
              Вообще то, я как раз говорил про логичный способ, называя его «способ, который для всех очевиден»: на третьем шаге дать первой транзакции доступ. Зачем выдавать ошибку там где ситуацию можно безошибочно разрулить?
              • 0
                «очевидный способ» нарушает ACID-принцип

                Если процесс получил блокировку на ресурс, как-то воспользовался этим ресурсом, то потом лишив его этой блокировки, даже под самыми благими намерениями, мы всю изоляцию транзакций нахрен порушим

                Если подумать об варианте, чтобы сервак сам предпринимал, кроме отката повторную попытку выполнения транзакции, но здесь тоже ерунда может получиться- внутри транзакции могут быть ресурсы, которые не по ACID-принципу управляются — переменные, обращения к внешним ресурсам (типа файлов) итп- тоже полный ад получится.

                • 0
                  На третьем шаге не надо никого лишать блокировки.
                  2) Транзакция№2 пытается получить блокировку типа X и… начинает ждать когда Транзакция№1 освободит блокировку S
                  3) Транзакция№1 пытается получить блокировку типа X и… начинает ждать когда Транзакция№2 получит блокировку типа X и освободит её

                  Т2 ещё не получила блокировку, по этому, можно легко дать блокировку типа Х транзакции№1.
                  • 0
                    Видимо там какая-то очередь. И на втором шаге в очередь попал запрос от Транзакции№2
                    • 0
                      хм… пожалуй соглашусь — логично было дать Т1 поднять уровень блокировки до X

                      повторил на MS SQL -думал он поумнее- тоже дедлок получил
                      чего то мы не учитываем в наших рассуждениях
                      • 0
                        Я же говорю — логично предположить что там очередь запросов на получение блокировки, которая не учитывает «автора» этой блокировки. Ну это единственное пока логичное объяснение такого поведения
          • +2
            Мне кажется есть вариант получше — сразу получать блокировку типа X.


            Да не получше он. S-локи — они не для искуственных примеров, когда мы пытаемся сначала получить shared, а потом, в следующем запросе, эксклюзивный на ту же таблицу в той же транзакции. А для того, чтобы дать параллельным транзакциям читать те же строки.

            Так можно договориться до того, что лучше использовать табличные блокировки. LOCK TABLE xxx WRITE, что мелочиться? Пофиг, что всю таблицу использовать нельзя, зато никаких дедлоков.
            • 0
              >> они не для искуственных примеров
              Да не скажите. У меня вот на рабочем сервере откуда-то (пока не разобрался) берутся S-локи
              • 0
                Это шутка?

                S-локи — это локи чтения. Например,
                insert into t2 select * from t1; 
                

                выставляет такие локи.
                • 0
                  На t1, естественно.
                  • +1
                    S-локи — это «shared», т.е. «разделяемые», это не обязательно «чтение», все может зависеть от уровня изоляции транзакции. При уровне изоляции Serializable чтение точно также выставит X (эксклюзивную) блокировку. А при ReadUncommitted — запись выставит разделяемую. В «версионниках» запись тоже «разделяемая».
                    • 0
                      Ну да, правильно. Я для дефолтной REPEATABLE READ пример написала.
                      • 0
                        А при ReadUncommitted

                        Уважаемый, вы точно не спутали название уровня изоляции? Может вы имели в виду REPEATABLE READ? При ReadUncommitted происходят грязные чтения (чтения незакоммиченных данных из других транзакций), нужно совсем не переживать за консистентность данных, если используется этот уровень изоляции.
                        • 0
                          при repeatable read невозможно обновление данных, которые были прочитаны в другой транзакции.
                        • 0
                          При уровне изоляции Serializable чтение точно также выставит X (эксклюзивную) блокировку

                          А в каком случае чтение выставит SHARED блокировку (кроме ручного указания LOCK IN SHARE MODE), при REPEATABLE READ? И если да, то для того, чтобы записи не блокировались при чтении, мне нужен уровень изоляции READ COMMITTED?
                          • 0
                            Начиная с Repeatable read и далее read committed (snapshot), read uncommitted. При serializable упрощенно последовательное выполнение (но дедлоки все равно возможны).
                      • 0
                        Там было все немного сложнее. S-локи так же появляются при инсерте в таблицу, в которой есть foreign key check. Т.е. вставляем в table1 строку например с user_id = 2 и получаем автоматически S-lock в таблице User на строке с primary key = 2
                        • 0
                          Сложнее, но, в общем, то же самое: таблицу User же вы не изменяете. Хотя угадать сложно, это правда. Пользователь Fragster хороший комментарий по теме написал.

                          PS: надеюсь, про InnoDB lock monitor и таблицы InnoDB в Information Schema вы знаете =)
                  • +4
                    > Мне кажется есть вариант получше — сразу получать блокировку типа X.

                    Это не избавит от дедлоков. Классический вариант:

                    Транзакция 1: блокирует строку 1 и продолжает работу
                    Транзакция 2: блокирует строку 2, пытается заблокировать строку 1 и виснет
                    Транзакция 1: пытается заблокировать строку 2
                    • +2
                      к дэдлоку приволят 2 вещи
                      • разныйпорядок захвата ресурсов (транзакция Т1 захватила рескурс Р1, транзакция Т2 захватила ресурс Т2, транзакция хочет дальше ресутс Т2, а Т2 — Т1)
                      • эскалация блокировок (который и описан с статье)

                      И если на первый программист может как-то влиять (хотя тут при больших проектах без фреймворков уже обойтись сложно), то на второй — не всегда. Например MS SQL при большой нагрузке иногда производит эскалацию со страничной блокировки до табличной, что на больших нагрузках печалит :(
                      • 0
                        +1
                        особенно про порядок захвата ресурсов

                        а автор тему не раскрыл совсем, особенно умиляют советы про «коммитится почаще»
                        • 0
                          Вот цитата с сайта mysql
                          «Commit your transactions often. Small transactions are less prone to collision.»
                          • 0
                            ну рекомендация — абсолютно точная и настолько же бесполезная, как в анекдоте — «где мы? на воздушном шаре, сэр», с таким же успехом можно было порекомендовать не писать код приводящий к дедлокам.

                            один из комментаторов выше точно подметил, что размер транзакции определяется обычно бизнес-логикой.
                            На этапе борьбы с дедлоками изменение размера транзакции, как правило, не является доступной опцией

                            другими словами, если у меня, по бизнес-требованиям, есть возможность сделать транзакцию на только из 9 выражений, я никогда не буду делать транзакцию на 10 выражений
                            • 0
                              Так я и не говорю что это хорошая рекомендация, я абсолютно согласен что это бредовый совет
                        • 0
                          кстати в 2008R2 появился волшебный хинт позволяющий запрещать эскалацию
                        • +3
                          Я решаю эту проблему повторением транзакции по которому вернулось исключение дидлока, в исключении даже описание приходит: 1213 Deadlock found when trying to get lock; try restarting transaction
                          На второй проход всегда проходит.
                          • 0
                            А в каких случаях блокировки могут быть лучше, например, optimistic locking (с версиями)?
                            • 0
                              optimistic locking вас ставит перед фактом уже случившегося и они немного для других целей — для длительных бизнес-процессов скорее
                            • 0
                              не коротко о дедлоках на РСДН статья есть хорошая

                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.