Build a Redis Distributed Lock in Go from Scratch

This article walks through the problem of implementing a reliable Redis distributed lock in Go, explains the pitfalls of naive SetNx usage, introduces timeout handling and GetSet replacement, provides step‑by‑step Go code, and demonstrates its correctness with a multithreaded test.

Architect
Architect
Architect
Build a Redis Distributed Lock in Go from Scratch

When using Java, many developers rely on Redisson for distributed locks, but the author argues that relying on ready‑made components can make us lazy; writing our own lock deepens understanding.

01 Problem Introduction

Interviewers often ask how to implement a distributed lock with Redis. The basic idea is to use SETNX to acquire a lock, but two critical issues arise:

If the machine holding the lock crashes, the lock may never be released.

If the lock expires, two threads (A and B) might acquire it simultaneously.

These are the two problems the custom Redis lock must solve.

02 Theory

Using SETNX alone can acquire a lock, and the lock is released after the business logic finishes. However, if the process dies before calling DEL, the lock stays forever, so we must store an expiration timestamp and compare it with the current time to decide whether the lock is stale.

When the lock is stale, we reset the expiration and try to acquire the lock again.

In a concurrent scenario, both A and B may call SETNX at the same time, causing both to think they have the lock. To avoid this, we replace SETNX with GETSET: GETSET sets a new value and returns the old one atomically. The author illustrates the process with a concrete example:

Both threads read the current timeout T1 = 100.

Thread A calls GETSET with new timeout Ta = 200, receives old value T2 = 100, sees T1 == T2, and acquires the lock.

Thread B then calls GETSET with Tb = 201, receives old value T2 = 200, which does not equal its original T1, so it fails to acquire the lock.

The tiny time difference between A and B is negligible in real concurrent environments, so the slight change in timeout does not cause issues.

03 Code Implementation

The following Go code implements the lock acquisition, release, and a sample business process. Comments explain each step.

func GetDistributeLock(key string, expireTime int64) bool {
    currentTime := time.Now().Unix()
    expires := currentTime + expireTime
    redisAlias := "jointly"
    // 1. Try to set lock with expiration as value
    redisRet, err := redis.SetNx(redisAlias, key, expires)
    if err == nil && utils.MustInt64(1) == redisRet {
        // lock acquired
        return true
    }
    // 2. If lock holder crashed, check expiration
    currentLockTime, err := redis.GetKey(redisAlias, key)
    if err != nil {
        return false
    }
    if utils.MustInt64(currentLockTime) >= currentTime {
        // lock not expired
        return false
    }
    // 3. Update expiration atomically
    oldLockTime, err := redis.GetSet(redisAlias, key, expires)
    if err != nil {
        return false
    }
    if utils.MustString(oldLockTime) == currentLockTime {
        return true
    }
    return false
}

func DelDistributeLock(key string) bool {
    redisAlias := "jointly"
    redisRet := redis.Del(redisAlias, key)
    if redisRet != nil {
        return false
    }
    return true
}

func DoProcess(processId int) {
    fmt.Printf("启动第%d个线程
", processId)
    redisKey := "redis_lock_key"
    for {
        isGetLock := GetDistributeLock(redisKey, 10)
        if isGetLock {
            fmt.Printf("Get Redis Key Success, id:%d
", processId)
            time.Sleep(time.Second * 3)
            DelDistributeLock(redisKey)
        } else {
            // avoid high Redis load
            time.Sleep(time.Second * 1)
        }
    }
}

func main() {
    // init resources (omitted)
    for i := 0; i <= 9; i++ {
        go DoProcess(i)
    }
    // keep main alive
    time.Sleep(time.Second * 100)
}

The program launches ten goroutines, each repeatedly trying to acquire the lock, performing a dummy workload, and releasing the lock. The output shows that at any moment only one goroutine succeeds, confirming the lock’s exclusivity.

启动第0个线程
启动第6个线程
... (output truncated) ...
Get Redis Key Success, id:3

04 Postscript

The author wrote this code years ago when no open‑source Go distributed lock existed. It has run in production for two years without issues, except for a migration bug where old machines kept holding stale locks.

Overall, the article provides a thorough, step‑by‑step guide to building a Redis‑based distributed lock in Go, covering problem definition, theoretical reasoning, concrete code, and real‑world verification.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Backend DevelopmentconcurrencyRedisGoDistributed Lock
Architect
Written by

Architect

Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.

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.