Databases 14 min read

Understanding Next-Key Lock Deadlocks in MySQL: Analysis and Solutions

This article examines how common business operations using SELECT ... FOR UPDATE and INSERT can cause MySQL Next‑Key lock deadlocks, explains the underlying gap‑lock and row‑lock mechanisms, demonstrates practical experiments, and proposes strategies such as postponing locks or using primary‑key updates to avoid such deadlocks.

政采云技术
政采云技术
政采云技术
Understanding Next-Key Lock Deadlocks in MySQL: Analysis and Solutions

In the previous article we introduced the concept of Next‑Key Lock. This continuation explores a typical business scenario—"if exists then update, otherwise insert"—and shows how it can lead to deadlocks under the repeatable‑read isolation level.

Scenario reproduction

Two common solutions for ensuring uniqueness in concurrent environments are:

Using a distributed lock to synchronize the whole business flow.

Controlling concurrency at the database row‑lock level.

We focus on the second approach and analyze why it may cause deadlocks.

Pseudo‑code example

// Query configuration by businessCode
ConfigQueryDTO query = new ConfigQueryDTO();
query.setBusinessCode(businessCode);

// Acquire exclusive row lock
Config config = configDao.selectForUpdate(query);

// If configuration does not exist, insert; otherwise update
ConfigDO configData = new ConfigDO();
configData.setBusinessCode(businessCode);
if (config == null) {
    configData.setConfigValue(initData);
    configDao.insert(configData);
} else {
    configData.setConfigValue(config.getConfigValue + 1);
    configDao.update(configData);
}

The above pseudo‑code is translated into the following SQL statements:

-- When the row does not exist
BEGIN;
SELECT * FROM config WHERE business_code = businessCode FOR UPDATE;
INSERT INTO config (business_code, conf_value) VALUES (businessCode, initData);
COMMIT;

-- When the row exists
BEGIN;
SELECT * FROM config WHERE business_code = businessCode FOR UPDATE;
UPDATE config SET conf_value = confValue WHERE business_code = businessCode;
COMMIT;

Hands‑on experiment

We create a simple table with a unique index on business_code and simulate two concurrent sessions that both try to process business_code = 3 :

CREATE TABLE `config` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `business_code` int(11),
  `conf_value` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_business_code` (`business_code`)
) ENGINE=InnoDB;

INSERT INTO config VALUES (0,1,1),(2,5,1),(3,10,1);
-- Session A
BEGIN;
SELECT * FROM config WHERE business_code = 3 FOR UPDATE;
SELECT SLEEP(5) FROM config LIMIT 1;
INSERT INTO config (business_code, conf_value) VALUES (3,1);
COMMIT;

-- Session B
BEGIN;
SELECT * FROM config WHERE business_code = 3 FOR UPDATE;
INSERT INTO config (business_code, conf_value) VALUES (3,1);
SELECT SLEEP(10) FROM config LIMIT 1;
COMMIT;

We expected Session B to insert quickly while Session A would fail due to a primary‑key conflict, but the actual result was that Session B became blocked and Session A encountered a deadlock.

Analysis of the deadlock

MySQL’s lock unit is the Next‑Key Lock (row lock + gap lock). When SELECT ... FOR UPDATE finds no matching row, it acquires a gap lock on the interval surrounding the missing key. Both sessions acquire a gap lock on the interval (1, 5), which does not conflict with each other, allowing both SELECTs to succeed.

When Session B attempts the INSERT , it conflicts with Session A’s gap lock, causing it to wait. Conversely, Session A’s subsequent INSERT also conflicts with Session B’s gap lock, leading to a circular wait and a deadlock. InnoDB aborts Session A to break the cycle.

To avoid this, we can postpone the exclusive lock until after the existence check, or directly lock the row by primary key, eliminating the gap‑lock component.

Revised approach

// Query by businessCode
ConfigQueryDTO query = new ConfigQueryDTO();
query.setBusinessCode(businessCode);
Config config = configDao.select(query);

ConfigDO configData = new ConfigDO();
configData.setBusinessCode(businessCode);
if (config == null) {
    configData.setConfigValue(initData);
    configDao.insert(configData);
} else {
    // Acquire exclusive lock on the primary key only
    ConfigQueryDTO queryLock = new ConfigQueryDTO();
    queryLock.setId(config.getId);
    config = configDao.selectForUpdate(queryLock);
    configData.setConfigValue(config.getConfigValue + 1);
    configDao.update(configData);
}

Further experiments replace the unique index with a non‑unique index and show that even three SQL statements can produce a deadlock because the gap lock still spans a range that interferes with the other session’s operations.

Key take‑aways

Deadlocks often arise from UPDATE or DELETE statements that use non‑unique index conditions, which generate wide gap locks.

Prefer accessing rows by primary key when performing updates or deletes; this narrows the lock scope and prevents unnecessary gap locks.

Understanding the composition of Next‑Key Locks (gap lock + row lock) is essential for diagnosing and preventing lock‑related issues.

Even simple business logic can trigger deadlocks; thorough lock‑range analysis is required.

By applying these principles—postponing locks, using primary‑key based updates, and being aware of gap‑lock side effects—developers can significantly reduce the likelihood of MySQL deadlocks in high‑concurrency environments.

SQLDatabasedeadlockMySQLlockingnext-key lock
政采云技术
Written by

政采云技术

ZCY Technology Team (Zero), based in Hangzhou, is a growth-oriented team passionate about technology and craftsmanship. With around 500 members, we are building comprehensive engineering, project management, and talent development systems. We are committed to innovation and creating a cloud service ecosystem for government and enterprise procurement. We look forward to your joining us.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.