Analyzing the Internal Implementation of Redis Distributed Locks in Go

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

  1. Generate a random token and append metadata for debugging purposes.
  2. Define retry strategy (e.g., exponential backoff, max retries, or fixed interval).
  3. Set a fail-fast timeout if specified; otherwise, rely on the retry logic.
  4. Execute the acquire.lua script in a loop until the lock is obtained or the strategy gives up.

Key Operations

  • Obtain: Uses msetnx for atomicity. If one key exists, it checks ownership beforee overriding.
  • Refresh: Calls extend.lua to reset the countdown timer.
  • Release: Calls free.lua to clean up keys only if the token matches.
  • TTL: Calls check_ttl.lua to 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.

Tags: Go Golang Redis Distributed Lock Lua Scripting

Posted on Tue, 25 Aug 2026 16:55:07 +0000 by binarymonkey