# redis lock 既然是锁,首先想到的一个作用就是:**防重复点击,在一个时间点只有一个请求产生效果**。 而既然是 `redis`,就得具有排他性,同时也具有锁的一些共性: - 高性能 - 不能出现死锁 - 不能出现节点down掉后加锁失败 `go-zero` 中利用 redis `set key nx` 可以保证key不存在时写入成功,`px` 可以让key超时后自动删除「最坏情况也就是超时自动删除key,从而也不会出现死锁」 ## example ```go redisLockKey := fmt.Sprintf("%v%v", redisTpl, headId) // 1. New redislock redisLock := redis.NewRedisLock(redisConn, redisLockKey) // 2. 可选操作,设置 redislock 过期时间 redisLock.SetExpire(redisLockExpireSeconds) if ok, err := redisLock.Acquire(); !ok || err != nil { return nil, errors.New("当前有其他用户正在进行操作,请稍后重试") } defer func() { recover() // 3. 释放锁 redisLock.Release() }() ``` 和你在使用 `sync.Mutex` 的方式时一致的。加锁解锁,执行你的业务操作。 ## 获取锁 ```go 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` func (rl *RedisLock) Acquire() (bool, error) { seconds := atomic.LoadUint32(&rl.seconds) // execute luascript 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 } else { logx.Errorf("Unknown reply when acquiring lock for %s: %v", rl.key, resp) return false, nil } } ``` 先介绍几个 `redis` 的命令选项,以下是为 `set` 命令增加的选项: - `ex seconds` :设置key过期时间,单位s - `px milliseconds` :设置key过期时间,单位毫秒 - `nx`:key不存在时,设置key的值 - `xx`:key存在时,才会去设置key的值 其中 `lua script` 涉及的入参: | **args** | **示例** | **含义** | | --- | --- | --- | | KEYS[1] | key$20201026 | redis key | | ARGV[1] | lmnopqrstuvwxyzABCD | 唯一标识:随机字符串 | | ARGV[2] | 30000 | 设置锁的过期时间 | 然后来说说代码特性: 1. `Lua` 脚本保证原子性「当然,把多个操作在 Redis 中实现成一个操作,也就是单命令操作」 1. 使用了 `set key value px milliseconds nx` 1. `value` 具有唯一性 1. 加锁时首先判断 `key` 的 `value` 是否和之前设置的一致,一致则修改过期时间 ## 释放锁 ```go delCommand = `if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end` func (rl *RedisLock) Release() (bool, error) { resp, err := rl.store.Eval(delCommand, []string{rl.key}, []string{rl.id}) if err != nil { return false, err } if reply, ok := resp.(int64); !ok { return false, nil } else { return reply == 1, nil } } ``` 释放锁的时候只需要关注一点: **不能释放别人的锁,不能释放别人的锁,不能释放别人的锁** 所以需要先 `get(key) == value「key」`,为 true 才会去 `delete`