Mastering Redis Distributed Locks: From SETNX+Lua to Redisson with Go Examples

This article examines the evolution of Redis distributed lock implementations—from the basic SETNX command to atomic SET with Lua scripts and the feature‑rich Redisson framework—detailing core requirements, common pitfalls, Go code samples, and production‑grade recommendations.

Architecture & Thinking
Architecture & Thinking
Architecture & Thinking
Mastering Redis Distributed Locks: From SETNX+Lua to Redisson with Go Examples

1 Distributed Lock Core Requirements

A qualified distributed lock must satisfy four essential properties:

Mutual exclusion : only one client can hold the lock at any moment.

Safety : only the lock owner can release it, preventing accidental deletion.

Dead‑lock avoidance : the lock should auto‑expire if the client crashes.

Fault tolerance : the lock must continue working when Redis nodes fail.

2 Correct Implementation of SETNX+Lua

2.1 Basic version: SETNX + EXPIRE (deprecated)

# non‑atomic operation, serious issue
SETNX lock:order1
EXPIRE lock:order 30

Problem analysis : the two commands are not atomic; if the client crashes after SETNX, EXPIRE never runs, leaving the lock forever and causing a deadlock.

2.2 Improved version: atomic SET command

Since Redis 2.6.12, the SET command supports NX and EX/PX options, enabling an atomic lock acquisition.

# single‑command atomic operation
SET lock:order client_001 NX PX 30000

Parameter explanation: lock:order: the lock key identifying the business resource. client_001: lock value, a unique client identifier (UUID + thread ID is recommended). NX: set only if the key does not exist (Not eXists). PX 30000: set expiration to 30 000 ms (30 s).

2.3 Ultimate version: SET + Lua script for unlocking

While lock acquisition can use the atomic SET command, unlocking still requires a Lua script to guarantee atomicity. A common mistake is:

// non‑atomic unlock, race window
val := redisClient.Get(ctx, lockKey).Val()
if val == myToken {
    redisClient.Del(ctx, lockKey) // may delete another client's lock
}

Problem description : GET and DEL are separate operations, creating a concurrency window where another client could acquire the lock before DEL executes.

Thread A GETs the token and finds it matches.

The lock expires just then.

Thread B acquires a new lock (value = tokenB).

Thread A executes DEL and unintentionally deletes B's lock.

Correct unlock Lua script :

-- KEYS[1] = lockKey
-- ARGV[1] = token
if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
else
    return 0
end

3 Common Pitfalls Deep Dive

3.1 Expiration and accidental deletion

Scenario : Service A obtains the lock and stalls; the lock expires, Service B acquires it; when Service A finally finishes it deletes the lock, inadvertently removing B's lock.

Solution : Use a unique token (e.g., UUID) as the lock value and verify it with a Lua script during unlock.

3.2 Business execution exceeds lock TTL

Problem : TTL is set to 10 s but the business operation needs 15 s, causing the lock to expire and another client to acquire it, leading to concurrent execution.

Solutions :

Set TTL based on the maximum expected business time plus a safety margin (e.g., 2×).

Implement a watchdog that periodically extends the TTL while the holder is alive.

3.3 Non‑reentrancy

SETNX cannot provide a re‑entrant lock because a second acquisition by the same thread returns 0 (key already exists).

Solution : Store lock information in a Redis hash, using a field for the owner identifier and a value for the re‑entry count.

3.4 Master‑slave switch causing lock loss

Scenario : Client A locks the master node; the master crashes before replication; the slave becomes the new master; Client B locks the new master; both A and B now hold the same logical lock, breaking mutual exclusion.

Solution : Use the RedLock algorithm or wait for cluster data synchronization before acquiring the lock.

4 Redisson Implementation Deep Dive

4.1 Data structure design

Redisson stores lock information in a Redis hash:

Key: lock:{resource} Field: UUID:ThreadID (client ID + thread ID)

Value: re‑entry count

This design supports re‑entrancy, owner identification, and lock renewal.

4.2 Lock acquisition Lua script

-- KEYS[1] = lockKey, ARGV[1] = ttl(ms), ARGV[2] = threadId
if redis.call('exists', KEYS[1]) == 0 then
    redis.call('hset', KEYS[1], ARGV[2], 1)
    redis.call('pexpire', KEYS[1], ARGV[1])
    return nil
end
if redis.call('hexists', KEYS[1], ARGV[2]) == 1 then
    redis.call('hincrby', KEYS[1], ARGV[2], 1)
    redis.call('pexpire', KEYS[1], ARGV[1])
    return nil
end
return redis.call('pttl', KEYS[1])

Script logic:

If the lock does not exist, create a hash with a re‑entry count of 1.

If the lock exists and the current thread already holds it, increment the count and refresh the TTL.

If the lock is held by another thread, return the remaining TTL.

4.3 WatchDog mechanism

When lock() is called without an explicit TTL, Redisson assigns a default 30 s lease and starts a background task that runs every TTL/3 (≈10 s). If the lock holder is still alive, the task renews the TTL via Lua; if the process crashes, the task stops and the lock expires automatically, preventing deadlocks.

4.4 Pub/Sub waiting queue

Redisson leverages Redis Pub/Sub: a client that fails to acquire the lock subscribes to a release channel; the lock holder publishes a message on unlock, waking waiting clients without busy‑polling.

5 Comparison: SETNX+Lua vs Redisson

Implementation complexity : manual handling vs ready‑made library.

Re‑entrancy : not supported vs native support.

Lock renewal : custom watchdog vs built‑in mechanism.

Waiting mechanism : polling or custom Pub/Sub vs built‑in queue.

Fair lock : unavailable vs supported.

Timeout handling : application‑level vs automatic strategies.

Master‑slave consistency : developer responsibility vs RedLock algorithm.

Performance : lightweight fastest vs richer feature set with slight overhead.

Suitable scenarios : simple high‑performance cases vs production environments needing full feature set.

6 Golang Implementation Examples

6.1 Basic SETNX+Lua

package main

import (
    "context"
    "fmt"
    "github.com/go-redis/redis/v8"
    "github.com/google/uuid"
    "time"
)

type RedisDistributedLock struct {
    client  *redis.Client
    lockKey string
    token   string
    timeout time.Duration
}

func NewRedisDistributedLock(client *redis.Client, lockKey string, timeout time.Duration) *RedisDistributedLock {
    return &RedisDistributedLock{client: client, lockKey: lockKey, token: uuid.New().String(), timeout: timeout}
}

// Acquire lock using atomic SETNX+PX
func (l *RedisDistributedLock) Lock(ctx context.Context) (bool, error) {
    result, err := l.client.SetNX(ctx, l.lockKey, l.token, l.timeout).Result()
    if err != nil {
        return false, err
    }
    return result, nil
}

// Unlock Lua script (atomic)
var unlockScript = redis.NewScript(`
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end`)

// Release lock
func (l *RedisDistributedLock) Unlock(ctx context.Context) (bool, error) {
    result, err := unlockScript.Run(ctx, l.client, []string{l.lockKey}, l.token).Int()
    if err != nil {
        return false, err
    }
    return result == 1, nil
}

// Lock with retry logic
func (l *RedisDistributedLock) LockWithRetry(ctx context.Context, maxRetries int, retryInterval time.Duration) (bool, error) {
    for i := 0; i < maxRetries; i++ {
        locked, err := l.Lock(ctx)
        if err != nil {
            return false, err
        }
        if locked {
            return true, nil
        }
        time.Sleep(retryInterval)
    }
    return false, nil
}

6.2 Enhanced version with re‑entrancy and watchdog

package main

import (
    "context"
    "fmt"
    "github.com/go-redis/redis/v8"
    "sync"
    "time"
)

type EnhancedRedisLock struct {
    client      *redis.Client
    lockKey     string
    clientID    string
    timeout     time.Duration
    renewTicker *time.Ticker
    mu          sync.Mutex
    reentrant   int
}

// Lua scripts (lockScript, unlockScriptEnhanced, renewScript) implement hash‑based lock acquisition,
// re‑entrant count handling, atomic unlock, and TTL renewal. They are omitted here for brevity.

func (l *EnhancedRedisLock) Lock(ctx context.Context) (bool, error) {
    l.mu.Lock()
    defer l.mu.Unlock()

    if l.reentrant > 0 {
        l.reentrant++
        return true, nil
    }

    result, err := lockScript.Run(ctx, l.client, []string{l.lockKey}, l.clientID, l.timeout.Milliseconds()).Int()
    if err != nil {
        return false, err
    }
    if result == 1 {
        l.reentrant = 1
        l.startWatchDog(ctx)
        return true, nil
    }
    return false, nil
}

func (l *EnhancedRedisLock) Unlock(ctx context.Context) (bool, error) {
    l.mu.Lock()
    defer l.mu.Unlock()

    if l.reentrant == 0 {
        return false, fmt.Errorf("lock not held by current client")
    }
    l.reentrant--
    if l.reentrant > 0 {
        return true, nil
    }
    if l.renewTicker != nil {
        l.renewTicker.Stop()
        l.renewTicker = nil
    }
    result, err := unlockScriptEnhanced.Run(ctx, l.client, []string{l.lockKey}, l.clientID, l.timeout.Milliseconds()).Int()
    if err != nil {
        return false, err
    }
    return result > 0, nil
}

// Watchdog renews the lock every timeout/3.
func (l *EnhancedRedisLock) startWatchDog(ctx context.Context) {
    l.renewTicker = time.NewTicker(l.timeout / 3)
    go func() {
        for range l.renewTicker.C {
            l.renewLock(ctx)
        }
    }()
}

func (l *EnhancedRedisLock) renewLock(ctx context.Context) {
    l.mu.Lock()
    defer l.mu.Unlock()
    if l.reentrant == 0 {
        return
    }
    _, err := renewScript.Run(ctx, l.client, []string{l.lockKey}, l.clientID, l.timeout.Milliseconds()).Int()
    if err != nil {
        fmt.Printf("lock renewal failed: %v
", err)
    }
}

6.3 Usage example

func main() {
    rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379", Password: "", DB: 0})
    ctx := context.Background()
    lock := NewEnhancedRedisLock(rdb, "order:lock:1001", 30*time.Second)

    locked, err := lock.Lock(ctx)
    if err != nil {
        fmt.Printf("lock error: %v
", err)
        return
    }
    if !locked {
        fmt.Println("lock already held by another client")
        return
    }
    fmt.Println("lock acquired, processing...")

    // Simulate business work
    time.Sleep(10 * time.Second)

    // Re‑enter the lock
    if lock.Lock(ctx) {
        fmt.Println("re‑enter successful")
        lock.Unlock(ctx)
    }

    // Final unlock
    if ok, err := lock.Unlock(ctx); err != nil {
        fmt.Printf("unlock error: %v
", err)
    } else if ok {
        fmt.Println("lock released")
    } else {
        fmt.Println("lock already released or owned by another client")
    }
}

7 Production‑grade Recommendations

7.1 Choosing a strategy

Simple scenarios : use the lightweight SETNX+Lua approach for maximum performance.

Production environments : adopt Redisson or a comparable mature framework to avoid reinventing complex features.

Golang projects : consider libraries such as github.com/go-redsync/redsync or adapt the examples above.

7.2 Key configurations

Lock timeout – set to 2‑3 × the estimated maximum business execution time.

Retry strategy – implement exponential back‑off to prevent thundering‑herd effects.

Monitoring – track lock wait time, acquisition failure rate, and other critical metrics.

Fallback – design a degradation path (local lock or pass‑through) when the distributed lock cannot be obtained.

7.3 Performance tuning

Connection pooling to reduce connection overhead.

Command pipelining to minimize network round‑trips.

Local caching for hot locks to lower Redis traffic.

Fine‑grained lock granularity to reduce contention.

RedisGoDistributed LockLuaredissonsetnx
Architecture & Thinking
Written by

Architecture & Thinking

🍭 Frontline tech director and chief architect at top-tier companies 🥝 Years of deep experience in internet, e‑commerce, social, and finance sectors 🌾 Committed to publishing high‑quality articles covering core technologies of leading internet firms, application architecture, and AI breakthroughs.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.