Why do you need distributed locks?
1 user placed an order
Lock the uid to prevent repeated orders.
2 Inventory deduction
Lock up inventory to prevent oversold.
3 Balance deduction
Lock the account to prevent concurrent operations.
When sharing the same resource in a distributed system, distributed locks are often required to ensure the consistency of changing resources.
Distributed locks need to have features
1 EXCLUSIVE
Basic property of a lock, and can only be held by the first holder.
2 Anti-deadlock
In high concurrency scenarios, once deadlock occurs on critical resources, it is very difficult to troubleshoot. Usually, it can be avoided by setting the timeout period to automatically release the lock.
3 Reentrant
The lock holder supports reentrancy, preventing the lock from being released by timeout when the lock holder re-entries again.
4 High performance and high availability
The lock is the key pre-node for the code to run. Once it is unavailable, the business will report a failure directly. In high concurrency scenarios, high performance and high availability are the basic requirements.
What knowledge points should be mastered before implementing Redis lock
set command
SET key value [EX seconds] [PX milliseconds] [NX|XX]
EX second
: Set the key's expiration time tosecond
seconds.SET key value EX second
has the same effect asSETEX key second value
.PX millisecond
: Set the key's expiration time tomillisecond
milliseconds.SET key value PX millisecond
has the same effect asPSETEX key millisecond value
.NX
: Set the key only when the key does not exist.SET key value NX
has the same effect asSETNX key value
.XX
: Set the key only if the key already exists.
Redis.lua script
Using redis lua script can encapsulate a series of command operations into pipline to achieve the atomicity of the overall operation.
Locking process
-- KEYS[1]: lock key
-- ARGV[1]: lock value, random string
-- ARGV[2]: Expiration time
-- Determine whether the value held by the lock key is equal to the incoming value
-- If it is equal, it means that the lock is acquired again and the acquisition time is updated to prevent expiration during reentry
-- The description here is "reentrant lock"
if redis.call("GET", KEYS[1]) == ARGV[1] then
-- set up
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 means that the lock is acquired for the first time
-- SET key value NX PX timeout : Only set the value of the key when the key does not exist
-- If the setting is successful, it will automatically return "OK", and if the setting fails, it will return "NULL Bulk Reply"
-- Why is "NX" added here, because it is necessary to prevent other people's locks from being overwritten
return redis.call("SET", KEYS[1], ARGV[1], "NX", "PX", ARGV[2])
end
As shown
Unlocking process
-- release lock
-- Can't release other people's locks
if redis.call("GET", KEYS[1]) == ARGV[1] then
-- Returns "1" if the execution is successful
return redis.call("DEL", KEYS[1])
else
return 0
end
As shown
Source code analysis
package redis
import (
"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 deadlock
tolerance = 500 // milliseconds
millisPerSecond = 1000
)
// A RedisLock is a redis lock.
type RedisLock struct {
// redis client
store *Redis
// overtime time
seconds uint32
// lock key
key string
// Lock the value to prevent the lock from being acquired by others
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 acquiring the lock, the value of the lock is generated from 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.
// lock
func (rl *RedisLock) Acquire() (bool, error) {
// get expiration time
seconds := atomic.LoadUint32(&rl.seconds)
// The default lock expiration time is 500ms to prevent deadlock
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.
// release 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 automatic 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)
}
Post comment 取消回复