分布式锁的应用场景
在分布式业务中,若涉及到共享的资源,未防止多个进程同时访问、处理该资源,那么则需要一个分布式锁 来保证该共享资源在同一时刻只能被一个对象进行处理。
设计理论
- 为了实现锁互斥能力,则一定涉及到 Redis 的
SETNX
指令(SET if Not eXist) - 锁只能由创建锁的对象解开,所以需要记录锁是由哪个对象创建的,解锁时需要验证锁的所有权
- 为了防止死锁,第一 需要允许强制解除锁,第二 每个锁必须要有 超时时间
综上,便可以编写出一个锁的定义了
type lock struct {
context context.Context
name string // 锁名称
owner string // 锁标识
seconds int64 // 有效期
}
type LockInterface interface {
Lock() bool
Block(seconds int64) bool // 持续获取锁
Release() bool
ForceRelease()
}
这里的lock
是私有属性,主要考虑到防止使用者随意修改lock的属性,导致无法解锁的问题。
代码
生成一个锁
// GetLock 生成锁
func GetLock(name string, seconds int64) LockInterface {
return &lock{
context.Background(),
LOCK_PREFIX + name,
utils.RandString(16),
seconds,
}
}
尝试锁定
// Lock 获取锁
func (l *lock) Lock() bool {
// SetNX:SET if Not eXists
return Redis.SetNX(l.context, l.name, l.owner, time.Duration(l.seconds)*time.Second).Val()
}
若锁(key)不存在,则添加key-value,并返回 true,代表已获取到锁
若锁(key)已存在,则不进行任何操作,并返回 false,代表未获取到锁
尝试获取锁,若没有获取到,则持续尝试获取锁
// Block 阻塞1秒后,尝试重新获取锁,持续seconds秒。
func (l *lock) Block(seconds int64) bool {
starting := time.Now().Unix()
for {
if !l.Lock() {
time.Sleep(time.Duration(1) * time.Second)
if time.Now().Unix()-seconds >= starting {
return false
}
} else {
return true
}
}
}
释放锁
// 释放锁 Lua 脚本,防止任何对象都能解锁
// KEYS[1]: lock.name
// ARGV[1]: lock.owner
const releaseLockLuaScript = `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
`
// Release 释放锁
func (l *lock) Release() bool {
luaScript := redis.NewScript(releaseLockLuaScript)
result := luaScript.Run(
l.context, Redis,
[]string{l.name}, // []string{l.name} KEYS[1]
l.owner, // l.owner ARGV[1]
).Val().(int64)
return result != 0
}
在这里不用担心 LuaScript 会占用网络流量,go-redis 在向服务器发送请求时,发送的是 lua 脚本的sha1,
redis服务器收到lua哈希值时,会根据sha1先判断自己是否已经缓存了这个lua脚本,缓存了那就直接用,若没有缓存,go-redis才会向redis服务器发送完整的lua脚本。
强制释放锁
// ForceRelease 强制释放锁
func (l *lock) ForceRelease() {
Redis.Del(l.context, l.name).Val()
}
若上一个脚本发生了错误,导致 锁 无法被正常释放,那么则需要人手工去释放这个锁。
文章评论