redis-lock

redis lock

既然是锁,首先想到的一个作用就是:防重复点击,在一个时间点只有一个请求产生效果

而既然是 redis,就得具有排他性,同时也具有锁的一些共性:

  • 高性能
  • 不能出现死锁
  • 不能出现节点down掉后加锁失败

go-zero 中利用 redis set key nx 可以保证key不存在时写入成功,px 可以让key超时后自动删除「最坏情况也就是超时自动删除key,从而也不会出现死锁」

example

  1. redisLockKey := fmt.Sprintf("%v%v", redisTpl, headId)
  2. // 1. New redislock
  3. redisLock := redis.NewRedisLock(redisConn, redisLockKey)
  4. // 2. 可选操作,设置 redislock 过期时间
  5. redisLock.SetExpire(redisLockExpireSeconds)
  6. if ok, err := redisLock.Acquire(); !ok || err != nil {
  7. return nil, errors.New("当前有其他用户正在进行操作,请稍后重试")
  8. }
  9. defer func() {
  10. recover()
  11. // 3. 释放锁
  12. redisLock.Release()
  13. }()

和你在使用 sync.Mutex 的方式时一致的。加锁解锁,执行你的业务操作。

获取锁

  1. lockCommand = `if redis.call("GET", KEYS[1]) == ARGV[1] then
  2. redis.call("SET", KEYS[1], ARGV[1], "PX", ARGV[2])
  3. return "OK"
  4. else
  5. return redis.call("SET", KEYS[1], ARGV[1], "NX", "PX", ARGV[2])
  6. end`
  7. func (rl *RedisLock) Acquire() (bool, error) {
  8. seconds := atomic.LoadUint32(&rl.seconds)
  9. // execute luascript
  10. resp, err := rl.store.Eval(lockCommand, []string{rl.key}, []string{
  11. rl.id, strconv.Itoa(int(seconds)*millisPerSecond + tolerance)})
  12. if err == red.Nil {
  13. return false, nil
  14. } else if err != nil {
  15. logx.Errorf("Error on acquiring lock for %s, %s", rl.key, err.Error())
  16. return false, err
  17. } else if resp == nil {
  18. return false, nil
  19. }
  20. reply, ok := resp.(string)
  21. if ok && reply == "OK" {
  22. return true, nil
  23. } else {
  24. logx.Errorf("Unknown reply when acquiring lock for %s: %v", rl.key, resp)
  25. return false, nil
  26. }
  27. }

先介绍几个 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 中实现成一个操作,也就是单命令操作」
  2. 使用了 set key value px milliseconds nx
  3. value 具有唯一性
  4. 加锁时首先判断 keyvalue 是否和之前设置的一致,一致则修改过期时间

释放锁

  1. delCommand = `if redis.call("GET", KEYS[1]) == ARGV[1] then
  2. return redis.call("DEL", KEYS[1])
  3. else
  4. return 0
  5. end`
  6. func (rl *RedisLock) Release() (bool, error) {
  7. resp, err := rl.store.Eval(delCommand, []string{rl.key}, []string{rl.id})
  8. if err != nil {
  9. return false, err
  10. }
  11. if reply, ok := resp.(int64); !ok {
  12. return false, nil
  13. } else {
  14. return reply == 1, nil
  15. }
  16. }

释放锁的时候只需要关注一点:

不能释放别人的锁,不能释放别人的锁,不能释放别人的锁

所以需要先 get(key) == value「key」,为 true 才会去 delete