Implementing distributed lock with Go + Redis
Why do we need distributed locks
- Placing orders
Lock the uid to prevent duplicate orders.
- Stock deduction
Lock inventory to prevent overselling.
- Balance deduction
Lock the account to prevent concurrent operations. Distributed locks are often needed to ensure change resource consistency when sharing the same resource in distributed systems.
The features that distributed locks need to have
- Exclusivity
The basic feature of a lock, and can only be held by the first holder.
- Anti-Deadlock
Once a deadlock occurs on a critical resource in a high concurrency scenario, it is very difficult to troubleshoot and can usually be avoided by setting a timeout to automatically release the lock when it expires.
- Re-entrance
The lock holder supports reentrancy to prevent the lock from being released by timeout when the lock holder reenters again.
- High performance and high availability
Lock is usually a key operation, if unavailable, the only thing that a service can do is returning a failure. In high concurrency scenarios, high performance and high availability are the basic requirements.
Key points on implementing redis locks
- set command
SET key value [EX seconds] [PX milliseconds] [NX|XX]
EX
second: Sets the expiration time of the key to given seconds. SET key value EX second has the same effect as SETEX key second value.PX
millisecond : Sets the key expiration time to given milliseconds. SET key value PX millisecond has the same effect as PSETEX key millisecond value.NX
: Set the key only when the key does not exist. SET key value NX has the same effect as SETNX key value.XX
: Sets the key only if the key already exists. 2.- redis lua script
The redis lua script encapsulates a series of command operations into a pipline to achieve atomicity of the overall operation.
go-zero’s RedisLock source code explained
core/stores/redis/redislock.go
1.
- locking process
-- KEYS[1]: lock key
-- ARGV[1]: lock value, random string
-- ARGV[2]: expiration time
-- determine if the value held by the lock key is equal to the value passed in
-- If equal, it means that the lock is acquired again and the acquisition time is updated to prevent expiration on reentry
-- this means it is a "reentrant lock"
if redis.call("GET", KEYS[1]) == ARGV[1] then
-- set
redis.call("SET", KEYS[1], ARGV[1], "PX", ARGV[2])
return "OK"else
-- If the lock key.value is not equal to the incoming value, it is the first time to get the lock.
-- SET key value NX PX timeout : Set the value of the key only when the key does not exist
-- Set success will automatically return "OK", set failure returns "NULL Bulk Reply"
-- Why add "NX" here, because we need to prevent the lock from being overwritten by others
return redis.call("SET", KEYS[1], ARGV[1], "NX", "PX", ARGV[2])
end

- Unlocking process
-- Release the lock
-- cannot release someone else's lock
if redis.call("GET", KEYS[1]) == ARGV[1] then
-- return "1" for successful execution
return redis.call("DEL", KEYS[1])
else
return 0
end

- Source code explained
package redisimport (
"math/rand"
"strconv"
"sync/atomic"
"time" red "github.com/go-redis/redis"
"github.com/tal-tech/go-zero/core/logx"
)const (
letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
lockCommand = `if redis.call("GET", KEYS[1]) == ARGV[1] then
redis.call("SET", KEYS[1], ARGV[1], "PX", ARGV[2])
return "OK"
else
return redis.call("SET", KEYS[1], ARGV[1], "NX", "PX", ARGV[2])
end`
delCommand = `if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end`
randomLen = 16
// default timeout to prevent deadlocks
tolerance = 500 // milliseconds
millisPerSecond = 1000
)// A RedisLock is a redis lock.
type RedisLock struct {
// redis client
store *Redis
// Timeout time
seconds uint32
// lock key
key string
// Lock the value to prevent the lock from being accessed by others
id string
id string }func init() {
rand.Seed(time.Now().UnixNano())
}// NewRedisLock returns a RedisLock.
func NewRedisLock(store *Redis, key string) *RedisLock {
return &RedisLock{
store: store,
key: key,
// When getting a lock, the value of the lock is generated by a random string
// Actually go-zero provides a more efficient way to generate random strings
// See core/stringx/random.go: Randn
id: randomStr(randomLen),
}
}// Acquire acquires the lock.
func (rl *RedisLock) Acquire() (bool, error) {
// Acquire the expiration time
seconds := atomic.LoadUint32(&rl.seconds)
// Default lock expiration time is 500ms to prevent deadlocks
resp, err := rl.store.Eval(lockCommand, []string{rl.key}, []string{
rl.id, strconv.Itoa(int(seconds)*millisPerSecond + tolerance),
})
if err == red.Nil {
return false, nil
} else if err ! = nil {
logx.Errorf("Error on acquiring lock for %s, %s", rl.key, err.Error())
return false, err
} else if resp == nil {
return false, nil
} reply, ok := resp.(string)
if ok && reply == "OK" {
return true, nil
} logx.Errorf("Unknown reply when acquiring lock for %s: %v", rl.key, resp)
return false, nil
}// Release releases the lock.
func (rl *RedisLock) Release() (bool, error) {
resp, err := rl.store.Eval(delCommand, []string{rl.key}, []string{rl.id})
if err ! = nil {
return false, err
} reply, ok := resp.(int64)
if !ok {
return false, nil
} return reply == 1, nil
}// SetExpire sets the expire.
// Note that it needs to be called before Acquire()
// otherwise the default is 500ms auto-release
func (rl *RedisLock) SetExpire(seconds int) {
atomic.StoreUint32(&rl.seconds, uint32(seconds))
}func randomStr(n int) string {
b := make([]byte, n)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return string(b)
}
Other options for distributed locks
Project address
https://github.com/zeromicro/go-zero
Welcome to use go-zero
and star support us!
Join FAUN: Website 💻|Podcast 🎙️|Twitter 🐦|Facebook 👥|Instagram 📷|Facebook Group 🗣️|Linkedin Group 💬| Slack 📱|Cloud Native News 📰|More.
If this post was helpful, please click the clap 👏 button below a few times to show your support for the author 👇