Why do I need distributed locks
- User orders
Lock uid to prevent repeated orders.
- Inventory deduction
Lock inventory to prevent oversold.
- Balance deduction
Lock the account to prevent concurrent operations.
When sharing the same resource in a distributed system, distributed locks are often needed to ensure the consistency of change resources.
Distributed locks require features
- exclusiveness
The basic characteristics of a lock and can only be held by the first holder.
- Anti deadlock
In high concurrency scenarios, it is very difficult to troubleshoot the deadlock of critical resources, which can usually be avoided by setting the timeout to automatically release the lock when it expires.
- Reentrant
The lock holder supports re-entry to prevent the lock from being released over time when the lock holder re-enters.
- High performance and availability
Lock is the key front node of code operation. 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 I master before implementing Redis lock
- set command
SET key value [EX seconds] [PX milliseconds] [NX|XX]
- EX second: set the expiration time of the key to second seconds. The effect of SET key value EX second is equivalent to that of set key second value.
- PX millisecond: set the expiration time of the key to millisecond. The SET key value PX millisecond effect is equivalent to PSETEX key millisecond value.
- Nx: set the key only when the key does not exist. SET key value NX effect is equivalent to SETNX key value.
- 20: Set the key only when it already exists.
- Redis.lua script
The redis lua script can encapsulate a series of command operations into pipline to realize the atomicity of the overall operation.
Go zero distributed lock RedisLock source code analysis
core/stores/redis/redislock.go
- Locking process
-- KEYS[1]: lock key -- ARGV[1]: lock value,Random string -- ARGV[2]: Expiration time -- Judgment lock key Held value Is it equal to the incoming value -- If equal, it means that the lock is acquired again and the acquisition time is updated to prevent expiration on 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 -- lock key.value Not equal to incoming value It indicates that the lock is acquired for the first time -- SET key value NX PX timeout : When key Set only when it does not exist key Value of -- If the setting is successful, it will be returned automatically“ OK",Setting failed return“ NULL Bulk Reply" -- Why add it here“ NX"Well, because we need to prevent covering other people's locks return redis.call("SET", KEYS[1], ARGV[1], "NX", "PX", ARGV[2]) end
- Unlocking process
-- Release lock -- You can't release someone else's lock if redis.call("GET", KEYS[1]) == ARGV[1] then -- Successful execution returns "1" return redis.call("DEL", KEYS[1]) else return 0 end
- 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 // Timeout seconds uint32 // Lock key key string // Lock value to prevent the lock from being obtained 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 a lock is acquired, the value of the lock is generated from 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() (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 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. // It should be noted that it is necessary to call before Acquire(). // Otherwise, it defaults to 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) }
What other implementation schemes are there for distributed locks
Project address
https://github.com/zeromicro/go-zero
Welcome to go zero and star support us!
Wechat communication group
Focus on the "micro service practice" official account and click on the exchange group to get the community community's two-dimensional code.