2
头图

Why do you need a distributed lock

  1. User orders

Lock the 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, a distributed lock is often needed to ensure the consistency of changed resources.

Distributed locks need to have characteristics

  1. exclusivity

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

  1. Anti-deadlock

Once a critical resource deadlock occurs in a high concurrency scenario, it is very difficult to troubleshoot, and it can usually be avoided by setting the timeout period to automatically release the lock.

  1. reentrant

The lock holder supports reentrancy to prevent the lock from being released over time when the lock holder reenters again.

  1. High performance and high availability

The lock is the key front node for 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 be mastered to realize 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. SET key value EX second has the same effect as SETEX key second value.
  • PX millisecond: Set the expiration time of the key to millisecond milliseconds. SET key value PX millisecond has the same effect as PSETEX key millisecond value.
  • NX : Only when the key does not exist, can the key be set. SET key value NX has the same effect as SETNX key value.
  • XX : Only when the key already exists, can the key be set.
  1. Redis.lua script

Using redis lua scripts can encapsulate a series of command operations into pipline to achieve the atomicity of the overall operation.

Go-zero distributed lock RedisLock source code analysis

core/stores/redis/redislock.go

  1. Locking process
-- KEYS[1]: 锁key
-- ARGV[1]: 锁value,随机字符串
-- ARGV[2]: 过期时间
-- 判断锁key持有的value是否等于传入的value
-- 如果相等说明是再次获取锁并更新获取时间,防止重入时过期
-- 这里说明是“可重入锁”
if redis.call("GET", KEYS[1]) == ARGV[1] then
    -- 设置
    redis.call("SET", KEYS[1], ARGV[1], "PX", ARGV[2])
    return "OK"

else
    -- 锁key.value不等于传入的value则说明是第一次获取锁
    -- SET key value NX PX timeout : 当key不存在时才设置key的值
    -- 设置成功会自动返回“OK”,设置失败返回“NULL Bulk Reply”
    -- 为什么这里要加“NX”呢,因为需要防止把别人的锁给覆盖了
    return redis.call("SET", KEYS[1], ARGV[1], "NX", "PX", ARGV[2])
end

  1. unlock process
-- 释放锁
-- 不可以释放别人的锁
if redis.call("GET", KEYS[1]) == ARGV[1] then
    -- 执行成功返回“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
    // 默认超时时间,防止死锁
    tolerance       = 500 // milliseconds
    millisPerSecond = 1000
)

// A RedisLock is a redis lock.
type RedisLock struct {
    // redis客户端
    store *Redis
    // 超时时间
    seconds uint32
    // 锁key
    key string
    // 锁value,防止锁被别人获取到
    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,
        // 获取锁时,锁的值通过随机字符串生成
        // 实际上go-zero提供更加高效的随机字符串生成方式
        // 见core/stringx/random.go:Randn
        id:    randomStr(randomLen),
    }
}

// Acquire acquires the lock.
// 加锁
func (rl *RedisLock) Acquire() (bool, error) {
    // 获取过期时间
    seconds := atomic.LoadUint32(&rl.seconds)
    // 默认锁过期时间为500ms,防止死锁
    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.
// 释放锁
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.
// 需要注意的是需要在Acquire()之前调用
// 不然默认为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)
}

What are the other implementation solutions 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 Exchange Group

Follow the " Practice " public account and click on the exchange group get the QR code of the community group.


kevinwan
931 声望3.5k 粉丝

go-zero作者