分布式锁的实现有挺多细节要注意。
1. 要设置过期时间,避免释放锁的时候失败了,锁长期得不到释放导致的死锁问题
2. 要设置锁的拥有者
请求一拿到锁,开始执行业务,业务执行时长超过锁设置的过期时间时,锁过期了,假设这个时候请求二拿到锁,刚开始执行业务,请求一业务执行完成,开始释放锁。因为没有设置锁的拥有者,导致请求一释放了请求二的锁,就会出现问题。
具体代码:
package redislock
import (
"context"
"time"
"github.com/go-redis/redis/v8"
)
//Lock is a struct that handle config and context
type Lock struct {
Config *Config
context context.Context
}
//Config is a struct that maintains redis client
type Config struct {
Client *redis.Client
}
//New is a method that return a instance of Lock
func New(c *Config) *Lock {
return &Lock{
Config: c,
context: context.Background(),
}
}
//Get is a method that try to get distribution lock
func (l *Lock) Get(key string, ttl time.Duration, owner string) (bool, error) {
return l.Config.Client.SetNX(l.context, key, owner, ttl).Result()
}
//Release is a method that release lock
func (l *Lock) Release(key string, owner string) (bool, error) {
luaScript := `
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
`
res, err := l.Config.Client.Eval(l.context, luaScript, []string{key}, owner).Result()
if res.(int64) == 0 {
return false, err
}
return true, nil
}
但仅仅是这样是不够的,因为 Get 方法只试了一次,并没有实现锁的自旋,我们应该写一个 LoopGet 方法去循环尝试获取锁。
//LoopGet is a method that try to get distribution lock looply
func (l *Lock) LoopGet(key string, ttl time.Duration, owner string) (chan bool, error) {
c := make(chan bool, 1)
for {
if res, err := l.Get(key, ttl, owner); res {
if err != nil {
c <- false
return c, err
}
c <- res
break
}
}
go func() {
defer close(c)
for {
if len(c) == 0 {
break
}
time.Sleep(time.Millisecond * 800)
}
}()
return c, nil
}
不停地尝试获取锁,成功之后返回 channel,记得开一个协程回收 channel,当 channel 的缓冲数据被读取后,就回收该 channel,避免内存泄漏。