Source Code
Repository: https://github.com/bsm/redislock
Core Logic Implementation
The library relies heavily on Lua scripts to ensure atomicity when interacting with Redis. Below is the rewritten logic of the scripts used.
Acquiring a Lock
The following script attempts to set a lock. If the lock is already held but by the same owner (determined by a token prefix), it allows overriding the lock to prevent deadlocks or stale locks.
-- acquire.lua: Params => [unique_val, prefix_len, duration]
-- Attempts to lock keys. Allows overriding if the token prefix matches.
local function updateTTL(duration)
for _, resource in ipairs(KEYS) do
redis.call("pexpire", resource, duration)
end
end
local function isOwner()
local prefix = tonumber(ARGV[2])
for _, resource in ipairs(KEYS) do
-- Check if the existing value starts with our token prefix
if redis.call("getrange", resource, 0, prefix - 1) ~= string.sub(ARGV[1], 1, prefix) then
return false
end
end
return true
end
local batchArgs = {}
for _, resource in ipairs(KEYS) do
table.insert(batchArgs, resource)
table.insert(batchArgs, ARGV[1])
end
-- Try to set all keys if none exist
if redis.call("msetnx", unpack(batchArgs)) ~= 1 then
if not isOwner() then
return false
end
-- Override existing keys if we own them
redis.call("mset", unpack(batchArgs))
end
updateTTL(ARGV[3])
return redis.status_reply("OK")
Extending Lock Duration
This script refreshes the Time-To-Live (TTL) for the lock if the caller still holds it.
-- extend.lua: Params => [unique_val, duration]
-- Refreshes TTL if the current value matches the input.
local currentValues = redis.call("mget", unpack(KEYS))
for i, _ in ipairs(KEYS) do
if currentValues[i] ~= ARGV[1] then
return false
end
end
for _, resource in ipairs(KEYS) do
redis.call("pexpire", resource, ARGV[2])
end
return redis.status_reply("OK")
Releasing a Lock
Deletes the keys only if the provided value matches the value stored in Redis to insure the caller is the rightful owner.
-- free.lua: Params => [unique_val]
-- Deletes keys if the value matches.
local currentValues = redis.call("mget", unpack(KEYS))
for i, _ in ipairs(KEYS) do
if currentValues[i] ~= ARGV[1] then
return false
end
end
redis.call("del", unpack(KEYS))
return redis.status_reply("OK")
Checking Remaining TTL
Returns the shortest TTL among the locked keys if the ownership is verified.
-- check_ttl.lua: Params => [unique_val]
-- Returns the minimum TTL of the keys if owned.
local currentValues = redis.call("mget", unpack(KEYS))
for i, _ in ipairs(KEYS) do
if currentValues[i] ~= ARGV[1] then
return false
end
end
local shortestTTL = -1
for _, resource in ipairs(KEYS) do
local ttl = redis.call("pttl", resource)
if ttl > 0 then
if shortestTTL == -1 or ttl < shortestTTL then
shortestTTL = ttl
end
end
end
return shortestTTL
Go Struct Definitions
The Go implemantation wraps the Redis client and defines structures for the Client and the Lock itself.
// Client handles the connection and script execution
type Client struct {
pool RedisPool
buffer []byte
mu sync.Mutex
}
// Lock represents a acquired distributed lock
type Lock struct {
*Client
resources []string
token string
prefixLen int
}
Note: The token is composed of a random identifier plus optional metadata. The prefixLen is used to identify the random part of the token to allow safe overrides without comparing the metadata.
Acquisition Workflow
- Generate a random token and append metadata for debugging purposes.
- Define retry strategy (e.g., exponential backoff, max retries, or fixed interval).
- Set a fail-fast timeout if specified; otherwise, rely on the retry logic.
- Execute the
acquire.luascript in a loop until the lock is obtained or the strategy gives up.
Key Operations
- Obtain: Uses
msetnxfor atomicity. If one key exists, it checks ownership beforee overriding. - Refresh: Calls
extend.luato reset the countdown timer. - Release: Calls
free.luato clean up keys only if the token matches. - TTL: Calls
check_ttl.luato see how much time is left.
Redis Command Note
The MSETNX command is atomic. It sets multiple keys only if none of them already exist. If any key exists, the operation fails. This is crucial for the "all or nothing" nature of the lock acquisition.