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

goCenter · · 802 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

#### 为什么需要分布式锁 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 ,实现整体操作的原子性。 **加锁的整个流程,详细原理说明看注释** ```go -- 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 ```   **加锁流程图** ![图片](https://cdn.learnku.com/uploads/images/202201/21/92746/o2hFUBfJpQ.webp!large) **解锁流程** ```go -- 释放锁 -- 不可以释放别人的锁 if redis.call("GET", KEYS[1]) == ARGV[1] then -- 执行成功返回“1” return redis.call("DEL", KEYS[1]) else return 0 end ```   **解锁的流程图** ![图片](https://cdn.learnku.com/uploads/images/202201/21/92746/rW0ANStfhr.webp!large) **源码解析** ```go 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)) } ```   这个详细源码根据自己的业务需要,可以利用.

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

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

802 次点击  ∙  2 赞  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传