分布式锁的应用场景
当相同的业务被部署为多个实例时,单进程内的加锁逻辑就会失效,此时就需要一个可以跨实例共享的加锁机制,可以实现的方案有很多,redis作为一个大多项目都会引入并且支持原子操作的一个中间件显然会首先进入我们的视线中。
加锁
加锁比较简单使用setnx即可实现,当setnx失败时即为加锁失败(伪代码)
//key作为锁的名称,即用来表示其用于哪个业务中
func Lock(key) {
duration := time.Second * 1 //假设锁定1秒
ok := redis.SetNX(key,"",duration)
if !ok {
//加锁失败
}
}
当加锁失败时可能出现两种选择,一种是继续尝试加锁直到加锁成功获得所有权,另一种是放弃加锁结束操作,放弃就不用说了,重点在于如何继续尝试加锁,最初想到的就是做一个长循环,只要不成功就隔段时间再试一次,但是总感觉这样不停的去连接redis服务端不太科学,应该存在某种监听机制等待服务端主动告诉我可以加锁了。然而目前并没有发现redis下有更靠谱的方案,所以最终仍然只能按照主动轮询的方式。
//key作为锁的名称,即用来表示其用于哪个业务中
func Lock(key) string{
duration := time.Second * 1 //假设锁定1秒
uniqValue := uuid.NewString() //生成一个唯一值用于解锁
for { //可以增加一些超时机制避免死循环
ok := redis.SetNX(key,uniqValue,duration)
if ok {
return uniqValue
}
//加锁失败
time.Sleep(time.Millisecond * 50) //根据业务情况决定等待时长,太长太短都不太好
}
return "error"
}
上面的uniqValue是用于解锁的钥匙,防止误将其他进程的锁给解了,比如当前程序执行超时,此时当前进程的锁已经过期并且被其他进程锁定了。
解锁
解锁比较复杂的地方就在于原子操作上,这是难住很多人的地方,因为使用redis提供的所有command就没有能实现“使用key+value查询然后删除”这样的操作,虽然redis有get,有del,但仔细想想这两个并不能用在这里,当年mysql高并发下踩的坑还少吗,如果仅此而已那么redis显然并不能用于实现分布式锁,那么如何实现呢?答案是使用redis-lua,lua脚本会跟get、set这些方法一样不会被竞争,通过官方文档学习后编写一个简单的lua脚本
if redis.call('get',KEYS[1]) == ARGV[1] then
return redis.call('del',KEYS[1]) --正常情况下del会返回0或1
else
return 2 --没查到返回个2也可以返回个0,取决于你想要几种状态
end
那么这段脚本应该放在哪呢,放在redis的服务器上吗?其实只要通过eval命令把字符串传给redis服务端就可以了
func Unlock(key,uniqValue) bool {
luaScript := "if redis.call('get',KEYS[1]) == ARGV[1] then\n"
luaScript += " return redis.call('del',KEYS[1])\n"
luaScript += "else\n"
luaScript += " return 2\n"
luaScript += "end"
rs := redis.Eval(luaScript,{key},uniqValue) //具体怎么传过去要看项目所使用的封装方法
return rs==1 //结果为1时说明解锁成功
}
如此一个简单的redis锁就完成了。