Implementing distributed locks with Go + Redis

Posted by Solarpitch on Wed, 15 Dec 2021 04:29:34 +0100

Why do I need distributed locks

  1. User orders

Lock uid to prevent repeated orders.

  1. Inventory deduction

Lock inventory to prevent oversold.

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

  1. exclusiveness

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

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

  1. Reentrant

The lock holder supports re-entry to prevent the lock from being released over time when the lock holder re-enters.

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

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

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

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

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

  1. etcd
  2. redis redlock

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.

Topics: Go Microservices Distributed lock distributed system go-zero