Topic 12.1
Basic Locks and Safe Unlock
In one line
Acquire with SET lock:resource <unique-token> NX PX 30000: one atomic command that only succeeds if nobody holds the lock and always sets an expiry. Release with a Lua script that deletes the key only if it still holds your token, never with a blind DEL.
Think of it like this
A meeting-room booking with a sign on the door that has your name and an end time. You can only put your sign up if the door is empty. When you leave you take down your sign, but only if it's still yours: if your booking already ran out and someone else put up theirs, you must not tear theirs down.
Key ideas
- 01
NXmakes acquisition exclusive;PX(milliseconds) makes sure a crashed holder can't keep the lock forever; the value must be a unique token per acquisition (a random UUID), so ownership can be checked. The oldSETNX+EXPIREpair isn't atomic: a crash between them leaves a lock without expiry. - 02
Safe unlock:
if GET(key) == token then DEL(key), done atomically in Lua. A blindDELcan remove someone else's lock: A's lock expired while A was slow, B acquired it, then A finishes and deletes B's lock, letting C in while B still works. - 03
Choosing the TTL: longer than the worst realistic critical section, but short enough that a crashed holder doesn't block others for too long. For long jobs, extend the lock periodically (a watchdog) with a Lua "extend if still mine" (
PEXPIREonly if the token matches). Redisson's lock does this automatically. - 04
Waiting: retry with jittered backoff and a total timeout, or subscribe to a release notification. Never spin in a tight loop.
- 05
Use cases: prevent duplicate cron runs across instances, single rebuild of a cache entry, serialize updates to an external resource. For correctness-critical exclusivity, you also need fencing (next topic).
Code & diagrams
127.0.0.1:6379> SET lock:report:daily 7f3a9c NX PX 30000
OK
127.0.0.1:6379> SET lock:report:daily b21e44 NX PX 30000
(nil) # someone else holds it
127.0.0.1:6379> EVAL "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end" 1 lock:report:daily 7f3a9c
(integer) 1-- release: KEYS[1] = lock key, ARGV[1] = token
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
end
return 0
-- extend (separate script): KEYS[1] = lock key, ARGV[1] = token, ARGV[2] = ttl ms
-- if redis.call('GET', KEYS[1]) == ARGV[1] then
-- return redis.call('PEXPIRE', KEYS[1], ARGV[2])
-- end
-- return 0public final class RedisLock {
private static final RedisScript<Long> RELEASE = RedisScript.of(
"if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end",
Long.class);
private final StringRedisTemplate redis;
public Optional<String> tryAcquire(String key, Duration ttl) {
String token = UUID.randomUUID().toString();
Boolean ok = redis.opsForValue().setIfAbsent(key, token, ttl); // SET NX PX
return Boolean.TRUE.equals(ok) ? Optional.of(token) : Optional.empty();
}
public boolean release(String key, String token) {
return Long.valueOf(1).equals(redis.execute(RELEASE, List.of(key), token));
}
}Interview problem
The problem
Only one instance should run the nightly report
Twelve instances run the same scheduler; the nightly report must run once. Design the lock, including TTL, release, a job that sometimes takes 40 minutes, and what happens if the holder crashes.
The interviewer follows up
Why must the lock value be unique per acquisition?
When it breaks
Lock acquired with SETNX then EXPIRE in two calls
What you see
A crash between them leaves a lock with no expiry; the job never runs again until someone deletes the key by hand.
Fix & prevent
Always use the single atomic SET key token NX PX ttl.
Lock stored in a cache Redis with allkeys-lru
What you see
Under memory pressure the lock is evicted and another worker acquires it while the first still runs.
Fix & prevent
Keep locks on a noeviction instance.
Explain it without notes
Why is a blind DEL unsafe for releasing a lock?
Practice
Implement acquire, extend and release, and write a test where the holder sleeps past the TTL and then tries to release.
Trade-offs
- ↔
Short TTLs recover quickly from crashes but risk expiring during slow work; watchdogs help but add complexity.
Done when you can
I acquire locks with one atomic
SET NX PXand a unique token.I release and extend only with token-checked Lua.