0

Go语言&&Redis实现分布式锁,妥妥的!

 2 years ago
source link: https://studygolang.com/articles/35414
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Go语言&&Redis实现分布式锁,妥妥的!

goCenter · 8天之前 · 223 次点击 · 预计阅读时间 5 分钟 · 大约8小时之前 开始浏览    

为什么需要分布式锁

1 因为用户下单,需要锁住 uid,防止用户重复下单。

2 用在库存扣减上,锁住库存,可以防止库存超卖。

3 用在余额扣减场景,锁住账户,防止并发操作。

分布式系统中共享同一个资源时,就需要分布式锁来确保变更资源的一致性。这就是为什么要用到分布式锁的原因咯。

分布式锁需要具备特性

1 排他性
这个是锁的基本特性,并且只能被第一个持有者拥有。这个不用解释都明白

2 防死锁
高并发场景下临界资源一旦发生死锁,非常难以排查,通常我们可以通过设置超时,时间到期后就自动释放锁,来规避发生死锁。

3 可重入
锁持有者是支持可重入的,但是防止锁持有者再次重入时,锁被超时释放。

4 高性能,高可用
锁是代码运行的关键前置节点,一旦不可用,则业务直接就报故障。高并发场景下,高性能高可用是基本要求。所以说高并发,高性能,高可用是一并存在的。

实现 Redis 锁应先掌握的知识点

set 命令

SET key value [EX seconds] [PX milliseconds] [NX|XX]
  • EX second :设置键的过期时间为 second 秒。SET key value EX second 效果等同于 SETEX key second value

  • PX millisecond :设置键的过期时间为 millisecond 毫秒。SET key value PX millisecond ,效果等同于 PSETEX key millisecond value

  • NX :键不存在时,才对键进行设置操作。SET key value NX 等同于 SETNX key value

  • XX :键已经存在时,才对键进行设置操作。

Redis.lua 脚本
我们可以使用 redis lua 脚本,将一系列命令操作封装成 pipline ,实现整体操作的原子性。

加锁的整个流程,详细原理说明看注释

-- KEYS[1]: 锁key
-- ARGV[1]: 锁value,zh可以是随机字符串
-- 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

加锁流程图

图片

解锁流程

-- 释放锁
-- 不可以释放别人的锁
if redis.call("GET", KEYS[1]) == ARGV[1] then
    -- 执行成功返回“1”
    return redis.call("DEL", KEYS[1])
else
    return 0
end

解锁的流程图

图片

源码解析

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"
    randomLen = 16

    // 默认超时时间,用来防止死锁
    tolerance       = 300 // milliseconds
    millisPerSecond = 800

    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`

)

type redisLock struct {
    // redis客户端
    store *Redis
    // 超时时间
    seconds uint32
    // 锁key
    keys string
    // 锁value,防止锁被别人获取到
    value string
}

func init() {
    rand.Seed(time.Now().UnixNano())
}

// NewRedisLock returns a RedisLock.
func NewRedisLock(store *Redis, keys string) *RedisLock {
    return &RedisLock{
        store: store,
        keys:   keys,
        // 获取锁时,锁的值通过随机字符串生成
        // 实际上go-zero提供更加高效的随机字符串生成方式
        // 见core/stringx/random.go:Randn
        value:    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.keys}, []string{
        rl.value, strconv.Itoa(int(seconds)*millisPerSecond + tolerance),
    })
    if err == red.Nil {
        return false, nil
    } else if err != nil {
        logx.Errorf("Error on 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 lock for %s: %v", rl.keys, resp)
    return false, nil
}

// Release releases the lock.
// 释放锁
func (rl *RedisLock) Release() (bool, error) {
    resp, err := rl.store.Eval(delCommand, []string{rl.keys}, []string{rl.value})
    if err != nil {
        return false, err
    }

    reply, ok := resp.(int64)
    if !ok {
        return false, nil
    }

    return reply == 1, nil
}

func randomStr(n int) string {
    b := make([]byte, n)
    for i := range b {
        b[i] = letters[rand.Intn(len(letters))]
    }
    return string(b)
}

// SetExpire sets the expire.
// 需要注意的是需要在Acquire()之前调用
// 不然默认为300ms自动释放
func (rl *RedisLock) SetExpire(seconds int) {
    atomic.StoreUint32(&rl.seconds, uint32(seconds))
}

这个详细源码根据自己的业务需要,可以利用.


有疑问加站长微信联系(非本文作者))

280

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:701969077


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK