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 to second seconds. SET key value EX second has the same effect as SETEX key second value.
  • PX millisecond : Set the key's expiration time to millisecond 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 : 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)
}

Likes(0)

Comment list count 0 Comments

No Comments

WeChat Self-Service

WeChat Consult

TaoBao

support@elephdev.com

发表
评论
Go
Top