用 Go + Redis 实现分布式锁(Implementing distributed locks with go + redis)

  为什么需要分布式锁

  1. 用户下单

  锁住 uid,防止重复下单。

  2. 库存扣减

  锁住库存,防止超卖。

  3.余额扣减

  锁住账户,防止并发操作。 分布式系统中共享同一个资源时往往需要分布式锁来保证变更资源一致性。

  分布式锁需要具备特性

  1.排他性

  锁的基本特性,并且只能被第一个持有者持有。

  2.防死锁

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

  3.可重入

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

  4.高性能高可用

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

  实现 Redis 锁应先掌握哪些知识点

  1.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 :只在键已经存在时,才对键进行设置操作。

  2.Redis.lua 脚本

  使用 redis lua 脚本能将一系列命令操作封装成 pipline 实现整体操作的原子性。

  go-zero 分布式锁 RedisLock 源码分析

  core/stores/redis/redislock.go

  1.加锁流程

  – KEYS[1]: 锁key

  – ARGV[1]: 锁value,随机字符串

  – 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

  2.解锁流程

  – 释放锁

  – 不可以释放别人的锁

  if redis.call(“GET”, KEYS[1]) == ARGV[1] then

  – 执行成功返回“1”

  return redis.call(“DEL”, KEYS[1])

  else

  return 0

  end

  3.源码解析

  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”

  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`

  randomLen = 16

  // 默认超时时间,防止死锁

  tolerance = 500 // milliseconds

  millisPerSecond = 1000

  )

  // A RedisLock is a redis lock.

  type RedisLock struct {

  // redis客户端

  store *Redis

  // 超时时间

  seconds uint32

  // 锁key

  key string

  // 锁value,防止锁被别人获取到

  id string

  }

  func init() {

  rand.Seed(time.Now().UnixNano())

  }

  // NewRedisLock returns a RedisLock.

  func NewRedisLock(store *Redis, key string) *RedisLock {

  return &RedisLock{

  store: store,

  key: key,

  // 获取锁时,锁的值通过随机字符串生成

  // 实际上go-zero提供更加高效的随机字符串生成方式

  // 见core/stringx/random.go:Randn

  id: 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.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

  }

  logx.Errorf(“Unknown reply when acquiring lock for %s: %v”, rl.key, resp)

  return false, nil

  }

  // Release releases the lock.

  // 释放锁

  func (rl *RedisLock) Release() (bool, error) {

  resp, err := rl.store.Eval(delCommand, []string{rl.key}, []string{rl.id})

  if err != nil {

  return false, err

  }

  reply, ok := resp.(int64)

  if !ok {

  return false, nil

  }

  return reply == 1, nil

  }

  // SetExpire sets the expire.

  // 需要注意的是需要在Acquire()之前调用

  // 不然默认为500ms自动释放

  func (rl *RedisLock) SetExpire(seconds int) {

  atomic.StoreUint32(&rl.seconds, uint32(seconds))

  }

  func randomStr(n int) string {

  b := make([]byte, n)

  for i := range b {

  b[i] = letters[rand.Intn(len(letters))]

  }

  return string(b)

  }

————————

Why do I need distributed locks

1. User orders

Lock uid to prevent repeated orders.

2. Inventory deduction

Lock inventory to prevent oversold.

3. Balance deduction

Lock the account to prevent concurrent operations. When sharing the same resource in a distributed system, distributed locks are often needed to ensure the consistency of change resources.

Distributed locks require features

1. exclusiveness

The basic characteristics of a lock and can only be held by the first holder.

2. Anti deadlock

In high concurrency scenarios, it is very difficult to troubleshoot the deadlock of critical resources, which can usually be avoided by setting the timeout to automatically release the lock when it expires.

3. Reentrant

The lock holder supports re-entry to prevent the lock from being released over time when the lock holder re-enters.

4. High performance and availability

Lock is the key front node of code operation. Once it is unavailable, the business will report a failure directly. In high concurrency scenarios, high performance and high availability are the basic requirements.

What knowledge points should I master before implementing redis lock

1. Set command

  > SET key value [EX seconds] [PX milliseconds] [NX|XX]

Ex second: set the expiration time of the key to second seconds. The effect of set key value Ex second is equivalent to that of set key second value.

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

Nx: set the key only when the key does not exist. Set key value NX effect is equivalent to setnx key value.

XX: set the key only when it already exists.

2. Redis. Lua script

The redis Lua script can encapsulate a series of command operations into pipline to realize the atomicity of the overall operation.

Go zero distributed lock redislock source code analysis

  core/stores/redis/redislock.go

1. Locking process

— keys [1]: lock key

— argv [1]: lock value, random string

— argv [2]: expiration time

— judge whether the value held by the lock key is equal to the passed in value

— if equal, it means that the lock is acquired again and the acquisition time is updated to prevent expiration on reentry

— the description here is “reentrant lock”

  if redis.call(“GET”, KEYS[1]) == ARGV[1] then

— settings

  redis.call(“SET”, KEYS[1], ARGV[1], “PX”, ARGV[2])

return “OK”

else

— lock key If value is not equal to the value passed in, it indicates that the lock is obtained for the first time

— set key value NX PX timeout: set the value of the key only when the key does not exist

— if the setting is successful, it will automatically return “OK”, and if the setting fails, it will return “null bulk reply”

— why add “NX” here, because it is necessary to prevent covering other people’s locks

  return redis.call(“SET”, KEYS[1], ARGV[1], “NX”, “PX”, ARGV[2])

end

2. Unlocking process

— release lock

— do not release other people’s locks

  if redis.call(“GET”, KEYS[1]) == ARGV[1] then

— successful execution returns “1”

  return redis.call(“DEL”, KEYS[1])

else

return 0

end

3. Source code analysis

  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”

  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`

  randomLen = 16

/ / default timeout to prevent deadlock

  tolerance = 500 // milliseconds

  millisPerSecond = 1000

)

  // A RedisLock is a redis lock.

  type RedisLock struct {

/ / redis client

  store *Redis

/ / timeout

  seconds uint32

/ / lock key

  key string

/ / lock value to prevent the lock from being acquired by others

  id string

}

func init() {

  rand.Seed(time.Now().UnixNano())

}

  // NewRedisLock returns a RedisLock.

  func NewRedisLock(store *Redis, key string) *RedisLock {

  return &RedisLock{

  store: store,

key: key,

/ / when obtaining a lock, the value of the lock is generated through a random string

/ / in fact, go zero provides a more efficient way to generate random strings

  // 见core/stringx/random.go:Randn

  id: randomStr(randomLen),

}

}

  // Acquire acquires the lock.

/ / lock

  func (rl *RedisLock) Acquire() (bool, error) {

/ / get expiration time

  seconds := atomic.LoadUint32(&rl.seconds)

/ / the default lock expiration time is 500ms to prevent deadlock

  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

}

  logx.Errorf(“Unknown reply when acquiring lock for %s: %v”, rl.key, resp)

  return false, nil

}

  // Release releases the lock.

/ / release the lock

  func (rl *RedisLock) Release() (bool, error) {

  resp, err := rl.store.Eval(delCommand, []string{rl.key}, []string{rl.id})

if err != nil {

  return false, err

}

  reply, ok := resp.(int64)

if ! ok {

  return false, nil

}

  return reply == 1, nil

}

  // SetExpire sets the expire.

/ / we need to note that we need to call before Acquire ().

/ / otherwise, it defaults to 500ms automatic release

  func (rl *RedisLock) SetExpire(seconds int) {

  atomic.StoreUint32(&rl.seconds, uint32(seconds))

}

  func randomStr(n int) string {

b := make([]byte, n)

  for i := range b {

  b[i] = letters[rand.Intn(len(letters))]

}

  return string(b)

}