Why do we need distributed locks

  1. Users to place the order

Lock the UID to prevent repeat orders.

  1. Inventory deduction

Lock up inventory to prevent oversold.

  1. 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

  1. exclusive

The basic characteristics of a lock and can only be held by the first owner.

  1. 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.

  1. reentrant

The lock holder supports reentrant, preventing the lock from being released due to timeout when the lock holder re-enters.

  1. 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

  1. 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 value
  • NX : 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.
  1. 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

  1. 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

  1. 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

  1. 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

  1. etcd
  2. 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.