Ответ
Блокировки и deadlock'и — это проблемы, возникающие на уровне системы управления базами данных (СУБД) для обеспечения целостности данных (ACID), а не в самом Go. Go-приложение лишь инициирует запросы, которые могут к ним приводить.
Как возникают блокировки?
Когда транзакция обращается к данным, СУБД устанавливает блокировку, чтобы другие транзакции не могли некорректно изменить эти данные. Основные типы блокировок:
- Shared Lock (S-lock, блокировка чтения): Несколько транзакций могут одновременно читать один и тот же ресурс, но ни одна не может его изменить, пока все S-блокировки не сняты.
- Exclusive Lock (X-lock, блокировка записи): Если транзакция захватывает X-блокировку на ресурсе (например, при
UPDATEилиDELETE), никакая другая транзакция не может получить ни S-, ни X-блокировку на этот ресурс до завершения первой транзакции.
Deadlock (взаимная блокировка)
Deadlock возникает, когда две или более транзакций ожидают друг друга для освобождения ресурсов, создавая замкнутый круг ожидания.
Пример на Go (database/sql):
// Транзакция 1: переводит 100 с аккаунта 1 на 2
go func() {
tx1, _ := db.Begin()
defer tx1.Rollback() // Откатить, если не будет Commit
// Шаг 1: Блокирует строку с id=1
tx1.Exec("UPDATE accounts SET balance = balance - 100 WHERE id = 1")
time.Sleep(100 * time.Millisecond) // Имитация работы
// Шаг 4: Пытается заблокировать строку с id=2, но она уже заблокирована tx2
tx1.Exec("UPDATE accounts SET balance = balance + 100 WHERE id = 2")
tx1.Commit()
}()
// Транзакция 2: переводит 50 с аккаунта 2 на 1
go func() {
tx2, _ := db.Begin()
defer tx2.Rollback()
// Шаг 2: Блокирует строку с id=2
tx2.Exec("UPDATE accounts SET balance = balance - 50 WHERE id = 2")
// Шаг 3: Пытается заблокировать строку с id=1, но она уже заблокирована tx1
tx2.Exec("UPDATE accounts SET balance = balance + 50 WHERE id = 1")
tx2.Commit()
}()
// Результат: Deadlock! СУБД принудительно откатит одну из транзакций.
Как избежать блокировок и deadlock'ов:
- Делайте транзакции короткими: Выполняйте внутри транзакции только необходимые операции с БД. Не делайте в них долгих вычислений или сетевых запросов.
- Соблюдайте единый порядок блокировки ресурсов: Все транзакции должны запрашивать блокировки в одном и том же порядке (например, всегда сначала обновлять таблицу
accounts, а потомusers, или по возрастаниюid). - Используйте правильный уровень изоляции: Более низкие уровни изоляции (например,
Read Committed) создают меньше блокировок, чем высокие (Serializable), но могут приводить к другим аномалиям. Выбирайте минимально необходимый уровень. - Используйте
SELECT ... FOR UPDATE: Если вам нужно прочитать данные, а затем обновить их, используйтеSELECT ... FOR UPDATE, чтобы сразу установить эксклюзивную блокировку и предотвратить изменение этих данных другими транзакциями. - Устанавливайте таймауты: Используйте
context.WithTimeoutдля запросов к БД, чтобы прервать операцию, которая ждет блокировку слишком долго. - Оптимизируйте запросы и индексы: Медленные запросы дольше держат блокировки. Наличие правильных индексов кардинально ускоряет операции и сокращает время блокировки.
Ответ 18+ 🔞
А, слушай, вот эта тема про блокировки в базах данных — это ж классика, блядь! Прям как в том анекдоте про двух упрямых козлов на мосту, только с транзакциями, сука.
Так вот, смотри. Эти все блокировки и deadlock'и — это не Go выдумал, чтобы тебе жизнь мёдом не казалась. Это всё СУБД, эта самая система управления, такая хитрая жопа, которая следит, чтобы данные не разъебались в хлам. Твоё Go-приложение — это просто инициатор, который кричит: «Эй, дай-ка мне это обновить!», а база уже решает, как это сделать, чтобы не было пиздеца.
Как это, блядь, работает?
Представь, что данные — это холодильник с пивом на общей хате. Есть два типа подходов:
- Блокировка чтения (Shared Lock). Ты подходишь, смотришь, сколько банок осталось. Твой кореш тоже может подойти и посчитать. Вы оба читаете. Но пока вы смотрите, ни один мудак не может взять и выгрести всё пиво, пока вы не отойдёте. Все видят одно и то же.
- Блокировка записи (Exclusive Lock). А вот если ты решил таки взять банку, то ты, сука, подходишь, хватаешь её и говоришь: «Всё, это моё, пока я не выпью и не поставлю пустую банку обратно (или не выкину её в урну, что равносильно
DELETE), никто не может ни посчитать оставшиеся, ни тем более взять ещё». Ты изменяешь ресурс. Пока ты не закончил, всем остальным — ждать.
А теперь deadlock, ёпта! Это когда два алкаша у холодильника.
Один (Транзакция 1) схватил последнюю банку «Балтики» и ждёт, пока ему подадут солёный огурчик (ресурс №2), чтобы правильно закусить. Второй (Транзакция 2) ухватил этот самый последний огурец и ждёт, когда освободится «Балтика» (ресурс №1), чтобы запить. И стоят они, смотрят друг на друга в пустых глазах. Каждый ждёт, пока другой отпустит то, что ему нужно. Замкнутый круг, пиздец! Система, видя эту ебанину, через какое-то время одного из них просто вырубает по таймауту (откатывает его транзакцию), и второй наконец может и выпить, и закусить. Но первый-то остался ни с чем!
Вот как это выглядит в коде, если накосячить:
// Алкаш №1: хочет перевести 100 рублей с одного счёта на другой
go func() {
tx1, _ := db.Begin()
defer tx1.Rollback() // На всякий, если всё пойдёт по пизде
// Шаг 1: Хватает за горло счёт id=1 ("Балтика")
tx1.Exec("UPDATE accounts SET balance = balance - 100 WHERE id = 1")
time.Sleep(100 * time.Millisecond) // Приценивается к закуске
// Шаг 4: Тянется за счётом id=2 ("Огурец"), но он уже в чужих руках! ТУПИК.
tx1.Exec("UPDATE accounts SET balance = balance + 100 WHERE id = 2")
tx1.Commit()
}()
// Алкаш №2: хочет перевести 50 рублей, но в обратном порядке!
go func() {
tx2, _ := db.Begin()
defer tx2.Rollback()
// Шаг 2: Хватает счёт id=2 ("Огурец")
tx2.Exec("UPDATE accounts SET balance = balance - 50 WHERE id = 2")
// Шаг 3: Тянется за счётом id=1 ("Балтика"), но он уже занят! И мы в жопе.
tx2.Exec("UPDATE accounts SET balance = balance + 50 WHERE id = 1")
tx2.Commit()
}()
// Результат: Deadlock! СУБД смотрит на это безобразие и одного прибивает.
Как не попадать в такие идиотские ситуации?
- Не затягивай транзакции. Быстро сделал дело — быстро отпустил блокировки. Не надо внутри транзакции, блядь, фотки в инсту грузить или с мамкой по телефону болтать. Вошёл-вышел.
- Договорись об порядке. Всегда блокируй ресурсы в одной и той же последовательности. Например, всегда сначала обновляй счёт с меньшим
id, потом с большим. Тогда второй алкаш просто подождёт, пока первый отпустит «Балтику», а не будет хватать «Огурец» первым. - Не жадничай с изоляцией. Уровень
Serializable— это как поставить сейф на холодильник и проверять отпечатки пальцев. Надёжно, но медленно и всех бесит. Часто хватает иRead Committed. - Говори чётко. Если ты прочитал данные, чтобы потом их обновить, сразу говори базе:
SELECT ... FOR UPDATE. Это как крикнуть: «Я щас посчитаю пиво и сразу одно возьму, не трогать!». - Ставь таймер. Используй
context.WithTimeout, чтобы твой запрос не висел вечно, если что-то пошло не так. «Жду ответа 5 секунд, иначе пошёл нахуй». - Оптимизируй запросы. Если твой запрос ползает как черепаха в сиропе, он и блокировки держит долго. Поставь индексы, ёбта! Это как прорубить отдельную дверцу к пиву, а не лазить через всё хозяйство.