Skip to main content

How to Use Advisory Locks in MySQL with GET_LOCK

· 5 min read
Jagdish Kumawat
Founder @ Dewiride

Two copies of your app are running, and only one of them should run the nightly report. There's no row to lock. GET_LOCK gives you a lock on a name you make up, held by one connection at a time, and it's the same tool EF Core uses to stop two migrations running at once.

Prerequisites​

Step 1: Take the Lock​

The first argument is any name you choose, and the second is how many seconds to wait for it:

mysql: terminal 1
SELECT GET_LOCK('nightly-report', 0);
Output
+-------------------------------+
| GET_LOCK('nightly-report', 0) |
+-------------------------------+
| 1 |
+-------------------------------+
1 row in set (0.001 sec)

1 means it's yours. A 0 for the wait means "don't wait at all".

Step 2: Try to Take It Again​

mysql: terminal 2
SELECT GET_LOCK('nightly-report', 2);
Output
+-------------------------------+
| GET_LOCK('nightly-report', 2) |
+-------------------------------+
| 0 |
+-------------------------------+
1 row in set (2.007 sec)

Terminal 2's GET_LOCK returning 0 after waiting 2.007 seconds, because terminal 1 holds the lock.

It waited two seconds, and returned 0. MySQL's locking functions page says: "Returns 1 if the lock was obtained successfully, 0 if the attempt timed out (for example, because another client has previously locked the name), or NULL if an error occurred".

So the job's logic is one line: if it's 1, run, and if it's 0, another copy is already at it, so stop.

Step 3: See Who Holds It​

mysql: terminal 2
SELECT IS_FREE_LOCK('nightly-report'), IS_USED_LOCK('nightly-report'), CONNECTION_ID();
Output
+--------------------------------+--------------------------------+-----------------+
| IS_FREE_LOCK('nightly-report') | IS_USED_LOCK('nightly-report') | CONNECTION_ID() |
+--------------------------------+--------------------------------+-----------------+
| 0 | 160 | 159 |
+--------------------------------+--------------------------------+-----------------+
1 row in set (0.002 sec)

IS_USED_LOCK returns the connection that holds it, here 160, which isn't this session, 159. From a root session, it's also in the metadata locks:

mysql: root session
SELECT OBJECT_TYPE, OBJECT_NAME, LOCK_TYPE, LOCK_DURATION, LOCK_STATUS
FROM performance_schema.metadata_locks
WHERE OBJECT_TYPE = 'USER LEVEL LOCK';
Output
+-----------------+----------------+-----------+---------------+-------------+
| OBJECT_TYPE | OBJECT_NAME | LOCK_TYPE | LOCK_DURATION | LOCK_STATUS |
+-----------------+----------------+-----------+---------------+-------------+
| USER LEVEL LOCK | nightly-report | EXCLUSIVE | EXPLICIT | GRANTED |
+-----------------+----------------+-----------+---------------+-------------+
1 row in set (0.002 sec)

Step 4: Release It​

mysql: terminal 1
SELECT RELEASE_LOCK('nightly-report');
Output
+--------------------------------+
| RELEASE_LOCK('nightly-report') |
+--------------------------------+
| 1 |
+--------------------------------+
1 row in set (0.001 sec)

1 means it was released, and the other session can have it now:

mysql: terminal 2
SELECT GET_LOCK('nightly-report', 2);
Output
+-------------------------------+
| GET_LOCK('nightly-report', 2) |
+-------------------------------+
| 1 |
+-------------------------------+
1 row in set (0.001 sec)

RELEASE_LOCK also tells you when something's off. A 0 means someone else holds it, and a NULL means nobody does:

Output (terminal 1 releasing a lock it no longer holds)
+--------------------------------+
| RELEASE_LOCK('nightly-report') |
+--------------------------------+
| 0 |
+--------------------------------+
1 row in set (0.002 sec)
Output (terminal 2 releasing the same lock twice: the second time)
+--------------------------------+
| RELEASE_LOCK('nightly-report') |
+--------------------------------+
| NULL |
+--------------------------------+
1 row in set (0.003 sec)

How It Differs from a Row Lock​

Row lockGET_LOCK
LocksA rowA name
Released byCOMMIT or ROLLBACKRELEASE_LOCK, or the session ending
Other code has to agree to use itNo, MySQL enforces itYes, it only works if everyone asks for it

The page is clear about the second row: "Locks obtained with GET_LOCK() are not released when transactions commit or roll back." And a session that ends gives up its locks: "A lock obtained with GET_LOCK() is released explicitly by executing RELEASE_LOCK() or implicitly when your session terminates (either normally or abnormally)."

Common Mistakes​

MistakeWhat happensDo this instead
Assuming COMMIT releases itIt doesn'tCall RELEASE_LOCK
Using a connection pool, with a lock on one connection and the release on anotherThe release returns 0 or NULL, and the lock stays with the first connectionKeep one connection open for the whole job
Using a short, common nameAnother program on the server can clash with itPrefix it with your app's name
Ignoring the return valueTwo copies of the job runCheck for 1

Troubleshooting​

SymptomLikely causeFix
GET_LOCK keeps returning 0Another connection still holds itIS_USED_LOCK gives you its ID
RELEASE_LOCK returns 0This connection doesn't hold itRelease from the connection that took it
RELEASE_LOCK returns NULLNobody holds itNothing to do

FAQ​

What does EF Core use it for?​

To keep two database update runs apart. It takes GET_LOCK('__EFMigrationsLock', -1) on this provider, where -1 means wait forever.

Conclusion​

GET_LOCK('name', seconds) returns 1 if the name is yours and 0 if someone else has it. It isn't tied to a transaction or a row, and it's released by RELEASE_LOCK or when the session ends, which makes it a simple way to let only one copy of a job run.

Additional Resources​

Stay Updated

Subscribe to our newsletter for the latest tutorials, tech insights, and developer news.

By subscribing, you agree to our privacy policy. Unsubscribe at any time.