DEV Community

Arun Chaitanya
Arun Chaitanya

Posted on

Next-Key LOCK in MySQL

InnoDB engine in MySQL has a myriad of locking options often leads to confusion. This article tries to explore next-key lock to better understand the concept.

Example table

CREATE TABLE `gaplock_test` (
    `id` BIGINT(20) NOT NULL AUTO_INCREMENT,
    `emp_div` INT(11) NOT NULL DEFAULT '0',
    `emp_id` BIGINT(20) NOT NULL DEFAULT '0',
    `start_date` DATETIME(3) NOT NULL,
    `end_date` DATETIME(3) DEFAULT NULL,
    `div_id` BIGINT(20) NOT NULL DEFAULT '0',
    `code` VARCHAR(50) COLLATE UTF8_UNICODE_CI DEFAULT NULL,
    PRIMARY KEY (`id` , `start_date`),
    UNIQUE KEY `uidx_gaplock_01` (`emp_id` , `start_date` , `div_id` , `emp_div`),
    KEY `idx_gaplock_01` (`div_id` , `code` , `start_date`)
)  ENGINE=INNODB AUTO_INCREMENT=0 DEFAULT CHARSET=UTF8 COLLATE = UTF8_UNICODE_CI ROW_FORMAT=DYNAMIC;
Enter fullscreen mode Exit fullscreen mode

InnoDB has a concept of Record lock defined as

A record lock is a lock on an index record. Record locks always lock index records, even if a table is defined with no indexes.

uidx_gaplock_01 is a unique index on the table. Hence this is the target for record locking.

Initial data

ID EMP_DIV EMP_ID START_DATE END_DATE DIV_ID CODE
1 0 100 1909/12/31 15:00:00 2382/12/31 0:00:00 2 testA
2 0 200 1909/12/31 15:00:00 2382/12/31 0:00:00 2 testB
3 0 300 1909/12/31 15:00:00 2382/12/31 0:00:00 2 testC
4 0 250 1909/12/31 15:00:00 2382/12/31 0:00:00 2 testD

So ideally when we operate on a certain row, only the index record corresponding to that row should be locked.

But InnoDB has concept of next-key lock.

A next-key lock is a combination of a record lock on the index record and a gap lock on the gap before the index record.

So if we operate on EMP_ID 100, we should expect to see record 200 locked sometimes.

According to the manual, one such case is duplicate-key checking.

Gap locking is used only for foreign-key constraint checking and duplicate-key checking.

So if we delete and insert the same record, we will trigger gap locking.

Experiment with Transactions

Consider a transaction TR1 that operates on record 100.

-- TR1 
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
DELETE FROM testdb.gaplock_test WHERE div_id = 2 AND emp_id = 100 AND emp_div = 0;
INSERT INTO testdb.gaplock_test (id, emp_div, emp_id, start_date, end_date, div_id, code) VALUES (1, 0, 100, '1909-12-31 15:00:00', '2382-12-31 00:00:00', 2, 'testA');

Enter fullscreen mode Exit fullscreen mode

Consider another transaction TR2 that operates on record 200.

-- TR2
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
DELETE FROM testdb.gaplock_test WHERE div_id = 2 AND emp_id = 200 AND emp_div = 0;
Enter fullscreen mode Exit fullscreen mode

In this case, Transaction 2 waits for Transaction 1 to complete.

Results

-- INNODB_TRX
select * from  information_schema.INNODB_TRX;
Enter fullscreen mode Exit fullscreen mode
trx_id trx_state trx_started trx_query
2418 LOCK WAIT 2021-01-25 06:13:03 'DELETE FROM testdb.gaplock_test WHERE div_id = 2 AND emp_id = 200 AND emp_div = 0'
2402 RUNNING 2021-01-25 06:02:21

Let's check locked contents

-- INNODB_LOCKS 
select * from  information_schema.INNODB_LOCKS;
Enter fullscreen mode Exit fullscreen mode
lock_id lock_trx_id lock_mode lock_type lock_table lock_index lock_data
2418:27:4:3 2418 X RECORD testdb.gaplock_test uidx_gaplock_01 200, 0x983F7EF0000000, 2, 0
2402:27:4:3 2402 S RECORD testdb.gaplock_test uidx_gaplock_01 200, 0x983F7EF0000000, 2, 0

Conclusion

As we can see, even though transaction only operates on record 100, record 200 is locked too.

Code

All the code is here

use testdb;
drop table gaplock_test;
CREATE TABLE `gaplock_test` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`emp_div` INT(11) NOT NULL DEFAULT '0',
`emp_id` BIGINT(20) NOT NULL DEFAULT '0',
`start_date` DATETIME(3) NOT NULL,
`end_date` DATETIME(3) DEFAULT NULL,
`div_id` BIGINT(20) NOT NULL DEFAULT '0',
`code` VARCHAR(50) COLLATE UTF8_UNICODE_CI DEFAULT NULL,
PRIMARY KEY (`id` , `start_date`),
UNIQUE KEY `uidx_gaplock_01` (`emp_id` , `start_date` , `div_id` , `emp_div`),
KEY `idx_gaplock_01` (`div_id` , `code` , `start_date`)
) ENGINE=INNODB AUTO_INCREMENT=0 DEFAULT CHARSET=UTF8 COLLATE = UTF8_UNICODE_CI ROW_FORMAT=DYNAMIC;
INSERT INTO gaplock_test (id, emp_div, emp_id, start_date, end_date, div_id, code) VALUES (1, 0, 100, '1909-12-31 15:00:00', '2382-12-31 00:00:00', 2, 'testA');
INSERT INTO gaplock_test (id, emp_div, emp_id, start_date, end_date, div_id, code) VALUES (2, 0, 200, '1909-12-31 15:00:00', '2382-12-31 00:00:00', 2, 'testB');
INSERT INTO gaplock_test (id, emp_div, emp_id, start_date, end_date, div_id, code) VALUES (3, 0, 300, '1909-12-31 15:00:00', '2382-12-31 00:00:00', 2, 'testC');
INSERT INTO gaplock_test (id, emp_div, emp_id, start_date, end_date, div_id, code) VALUES (4, 0, 250, '1909-12-31 15:00:00', '2382-12-31 00:00:00', 2, 'testD');
view raw create.sql hosted with ❤ by GitHub
-- INNODB_TRX
select * from information_schema.INNODB_TRX;
-- INNODB_LOCKS
select * from information_schema.INNODB_LOCKS;
-- TABLE
select * from testdb.gaplock_test;
view raw debug.sql hosted with ❤ by GitHub
version: '3.3'
services:
db:
image: mysql:5.7
volumes:
- db_data:/var/lib/mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: testdb
MYSQL_USER: test_user
MYSQL_PASSWORD: test_password
ports:
- 44444:3306
volumes:
db_data: {}
-- TR1
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
DELETE FROM testdb.gaplock_test WHERE div_id = 2 AND emp_id = 100 AND emp_div = 0;
INSERT INTO testdb.gaplock_test (id, emp_div, emp_id, start_date, end_date, div_id, code) VALUES (1, 0, 100, '1909-12-31 15:00:00', '2382-12-31 00:00:00', 2, 'testA');
-- TR2
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
DELETE FROM testdb.gaplock_test WHERE div_id = 2 AND emp_id = 200 AND emp_div = 0;
view raw transaction.sql hosted with ❤ by GitHub

Sentry image

Hands-on debugging session: instrument, monitor, and fix

Join Lazar for a hands-on session where you’ll build it, break it, debug it, and fix it. You’ll set up Sentry, track errors, use Session Replay and Tracing, and leverage some good ol’ AI to find and fix issues fast.

RSVP here →

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay