redis lock

[!TIP] This document is machine-translated by Google. If you find grammatical and semantic errors, and the document description is not clear, please PR

Since it is a lock, the first function that comes to mind is: Anti-repeated clicks, only one request has an effect at a time.

Since it is redis, it must be exclusive and also have some common features of locks:

  • High performance
  • No deadlock
  • No lock failure after the node is down

In go-zero, redis set key nx can be used to ensure that the write is successful when the key does not exist. px can automatically delete the key after the timeout. “The worst case is that the key is automatically deleted after the timeout, so that there will be no death. lock”

example

  1. redisLockKey := fmt.Sprintf("%v%v", redisTpl, headId)
  2. // 1. New redislock
  3. redisLock := redis.NewRedisLock(redisConn, redisLockKey)
  4. // 2. Optional operation, set the redislock expiration time
  5. redisLock.SetExpire(redisLockExpireSeconds)
  6. if ok, err := redisLock.Acquire(); !ok || err != nil {
  7. return nil, errors.New("another user is currently operating, please try again later")
  8. }
  9. defer func() {
  10. recover()
  11. redisLock.Release()
  12. }()

It is the same as when you use sync.Mutex. Lock and unlock, perform your business operations.

Acquire the lock

  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. }

First introduce several redis command options, the following are the added options for the set command:

  • ex seconds : Set the key expiration time, in s
  • px milliseconds : set the key expiration time in milliseconds
  • nx : When the key does not exist, set the value of the key
  • xx : When the key exists, the value of the key will be set

The input parameters involved in lua script:

args example description
KEYS[1] key$20201026 redis key
ARGV[1] lmnopqrstuvwxyzABCD Unique ID: random string
ARGV[2] 30000 Set the expiration time of the lock

Then talk about the code features:

  1. The Lua script guarantees atomicity “Of course, multiple operations are implemented as one operation in Redis, that is, a single command operation”
  2. Use set key value px milliseconds nx
  3. value is unique
  4. When locking, first determine whether the value of the key is consistent with the previous setting, and modify the expiration time if it is consistent

Release lock

  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. }

You only need to pay attention to one point when releasing the lock:

Can’t release other people’s locks, can’t release other people’s locks, can’t release other people’s locks

Therefore, you need to first get(key) == value「key」, and then go to delete if it is true