Ответ
Блокировки и 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
для запросов к БД, чтобы прервать операцию, которая ждет блокировку слишком долго. - Оптимизируйте запросы и индексы: Медленные запросы дольше держат блокировки. Наличие правильных индексов кардинально ускоряет операции и сокращает время блокировки.