Why do we need distributed locks
- Users to place the order
Lock the UID to prevent repeat orders.
- Inventory deduction
Lock up inventory to prevent oversold.
- Balance of deductions
Lock accounts to prevent concurrent operations. Distributed locks are often required to ensure the consistency of changed resources when sharing the same resource in a distributed system.
Distributed locks require features
- exclusive
The basic characteristics of a lock and can only be held by the first owner.
- Deadlock prevention
In high concurrency scenarios, it is difficult to detect deadlocks in critical resources. You can automatically release the locks when the timeout period expires to avoid deadlocks.
- reentrant
The lock holder supports reentrant, preventing the lock from being released due to timeout when the lock holder re-enters.
- High performance high availability
Locks are a critical pre-node for code execution, and once unavailable, the business is reported to fail. High performance and high availability are the basic requirements in high concurrency scenarios.
To implement Redis lock, you should first master some knowledge points
- The set command
SET key value [EX seconds] [PX milliseconds] [NX|XX]
EX
Second: Sets the expiration time of the key to second. SET key value EX second is the same as SETEX key second value.PX
Millisecond: Set the expiration time of the key to millisecond. SET key value PX millisecond = PSETEX key millisecond valueNX
: Sets the key only when the key does not exist. SET key value NX is the same as SETNX key value.XX
: Sets a key only when the key already exists.
- Redis. Lua scripts
The Redis Lua script can be used to encapsulate a series of command operations into pipline to achieve atomicity of the whole operation.
Go-zero distributed lock RedisLock source code analysis
core/stores/redis/redislock.go
- Locking process
- KEYS [1] : the lock key
-- ARGV[1]: lock value, random string
-- ARGV[2]: expiration time
-- Determines whether the value held by the lock key is equal to the value passed in
-- If it is equal, the lock is acquired again and the acquisition time is updated to prevent reentry expiration
-- 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
-- Lock key.value is not equal to the value passed in
-- SET key value NX PX timeout: SET the key value only when the key does not exist
- "OK" is automatically returned on success, and "NULL Bulk Reply" is returned on failure.
-- Why add "NX" here, because you need to prevent overwriting someone else's lock
return redis.call("SET", KEYS[1], ARGV[1]."NX"."PX", ARGV[2])
end
Copy the code
- The unlock process
- the lock is released
You may not release someone else's lock
if redis.call("GET", KEYS[1]) == ARGV[1] then
-- Returns "1" after successful execution
return redis.call("DEL", KEYS[1])
else
return 0
end
Copy the code
- The source code parsing
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 deadlocks
tolerance = 500 // milliseconds
millisPerSecond = 1000
)
// A RedisLock is a redis lock.
type RedisLock struct {
// The redis client
store *Redis
// The timeout period
seconds uint32
/ / lock key
key string
// Lock value to prevent others from obtaining the lock
id string
}
func init(a) {
rand.Seed(time.Now().UnixNano())
}
// NewRedisLock returns a RedisLock.
func NewRedisLock(store *Redis, key string) *RedisLock {
return &RedisLock{
store: store,
key: key,
// When a lock is acquired, the lock value is generated as a random string
// In fact, 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(a) (bool, error) {
// Get the expiration time
seconds := atomic.LoadUint32(&rl.seconds)
// the 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 iferr ! =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.
/ / releases the lock
func (rl *RedisLock) Release(a) (bool, error) {
resp, err := rl.store.Eval(delCommand, []string{rl.key}, []string{rl.id})
iferr ! =nil {
return false, err
}
reply, ok := resp.(int64)
if! ok {return false.nil
}
return reply == 1.nil
}
// SetExpire sets the expire.
// It is necessary to call before Acquire()
// otherwise, the default value is 500ms
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)
}
Copy the code
What are the other implementation schemes for distributed locks
- etcd
- redis redlock
The project address
Github.com/zeromicro/g…
Welcome to Go-Zero and star support us!
Wechat communication group
Pay attention to the public account of “micro-service Practice” and click on the exchange group to obtain the QR code of the community group.