Distributed lock has a wide range of usage in our daily development, it can be used for exclusive access to shared resources.
The lock based on one single redis master is simple, we can use the atomic command to achieve this purpose, the way to go is as following,
set key random_value nx ex ttl
and after the process to shared resources is done, the lock can be removed by DEL command.
Until now, all is fine.
But what if the single master is down? then the application may become not robust as it was. To solve this problem and to promote our application's robustness, multiple masters are introduced into the design. So here comes the RedLock algorithm, which utilizes N masters to implement a distributed lock.
1. Odd number N indicates the number of masters, T indicates the connection timeout
when trying to connect to a master redis, TTL indicates the total validity time of the
lock.
2. A client trys to connect to a master with connection timeout T, if time T is up and no connection is established,
the client continues to connect to the next master.
Otherwise, it sets key to a random value using `set key random_value nx ex TTL` command
with the returned response indicating whether the key was set or not to
denote whether this single lock was acquired.
If the elapsed time is greater than TTL, the process of acquiring the lock is considered to be fail,
then the client must delete the already acquired locks.
The process continues until more than (N/2+1) locks are acquired through the way.
3. When (N/2+1) locks are acquired, then distributed lock based on multiple masters is successfully acquired.
4. After all work in the current request is done, client must remove all obtained locks to release the distributed lock.
The algothrim described above is called RedLock algorithm[1] which can be used in multiple masters scenario to promote our application's robustness.
The pseudo code may look like this,
type Server struct {
ip string
port int
passwd string
db int
}
func delKeyWithValue(rdb *redis.Client, key, value string) int {
lua := redis.NewScript(`if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end`)
return lua.Run(ctx, rdb, key, value...).Int()
}
func delAcquiredLocks(rdbs []*redis.Client, randomValue string) {
for _, rdb := range rdbs {
//use redis lua to delete key with specified randomValue to avoid delete other client's keys
delKeyWithValue(rdb, "redlock", randomValue)
}
}
func GetDistributedLock(servers []Server, ttl int) bool {
if len(servers) == 0 {
return false
}
N := len(servers)
T := 50 * time.Milliseconds
total := 0
now := time.Now()
randomValue := random.Randstr()
rdbs := []*redis.Client{}
for _, server := range servers {
rdb := redis.Connect(server.ip. server.port, server.passwd, server.db, T)
if rdb == nil {
continue
}
rdbs = append(rdbs, rdb)
ret, err := rdb.Do("set", "redlock", randomValue, "nx", "ex", ttl)
if err != nil || ret == 0 {
continue
}
if time.Now()-now > ttl {
break
}
total++
if total >= int(N/2)+1 {
return true
}
}
// if for some reason, the lock cannot be acquired, delete those single acquired locks
delAcquiredLocks(rdbs, randomValue)
return false
}
That is all, thank you for reading this article, if you have any questions or arguments about it, please write it down as comments below.
References
[1] redis.io/topics/dist…