Реализация распределенных блокировок с помощью Go + Redis

Микросервисы Go

Зачем нужны распределенные блокировки

  1. Пользователь размещает заказ

Заблокируйте uid, чтобы предотвратить повторные заказы.

  1. Инвентаризационный вычет

Заблокируйте запасы, чтобы предотвратить перепроданность.

  1. Вычет баланса

Заблокируйте учетную запись, чтобы предотвратить одновременные операции. При совместном использовании одного и того же ресурса в распределенной системе часто требуются распределенные блокировки для обеспечения согласованности изменяющихся ресурсов.

Распределенные блокировки должны иметь характеристики

  1. исключительность

Основное свойство замка, которым может владеть только первый владелец.

  1. Антитупик

В сценариях с высокой степенью параллелизма, когда на критических ресурсах возникает взаимоблокировка, устранить неполадку очень сложно.Обычно этого можно избежать, установив время ожидания для автоматического снятия блокировки.

  1. возвращающийся

Держатель блокировки поддерживает повторный вход, предотвращая снятие блокировки по тайм-ауту, когда держатель блокировки снова входит в систему.

  1. Высокая производительность и высокая доступность

Замок является ключевым предварительным узлом для запуска кода. Если он недоступен, бизнес напрямую сообщит об ошибке. В сценариях с высокой степенью параллелизма основными требованиями являются высокая производительность и высокая доступность.

Какие знания следует освоить перед внедрением блокировки Redis

  1. установить команду

SET key value [EX seconds] [PX milliseconds] [NX|XX]

  • EX second : Установите время истечения срока действия ключа в секундах. Значение ключа SET EX в секундах имеет тот же эффект, что и второе значение ключа SETEX.
  • PX миллисекунда : Установите время истечения срока действия ключа в миллисекунды миллисекунды. Значение ключа SET PX в миллисекундах имеет тот же эффект, что и значение ключа PSETEX в миллисекундах.
  • NX : Установите ключ, только если ключ не существует. Значение ключа SET NX имеет тот же эффект, что и значение ключа SETNX.
  • XX : Устанавливайте ключ только в том случае, если он уже существует.
  1. Скрипт Redis.lua

Использование скрипта redis lua может инкапсулировать ряд командных операций в конвейер для достижения атомарности всей операции.

go-zero анализ исходного кода распределенной блокировки RedisLock

core/stores/redis/redislock.go

  1. Процесс блокировки
-- 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. Разблокировать процесс
-- 释放锁
-- 不可以释放别人的锁
if redis.call("GET", KEYS[1]) == ARGV[1] then
    -- 执行成功返回“1”
    return redis.call("DEL", KEYS[1])
else
    return 0
end

  1. Анализ исходного кода
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)
}

Какие еще реализации существуют для распределенных блокировок?

  1. etcd
  2. redis redlock

адрес проекта

GitHub.com/ноль микро/…

Добро пожаловатьgo-zeroиstarПоддерживать нас!

Группа обмена WeChat

обрати внимание на"Практика микросервисов』Общедоступный номер и нажмитегруппа обменаПолучите QR-код группы сообщества.