分布式锁
- 互斥性:锁的目的是获取资源的使用权,所以只让一个竞争者持有锁。
- 安全性:避免死锁情况的发生。当一个竞争者持有锁期间,有可能因为意外的崩溃导致锁未能主动释放。所以持有锁也能够正常释放,并保证后续竞争者能获取到锁。
- 对称性:同一个锁,枷锁和解锁必须是同一个拥有者。不能把其他竞争者的锁释放,这个又称为锁的可重入性。
- 可靠性:需要有一定程度的异常处理能力、容灾能力。
redis分布式锁的实现
- 版本一
# 加锁
setnx key value
# 解锁
delete key
- 如果当前key不存在,则会将key设置为value,并返回1
- 如果当前key存在,不会对业务有影响,返回0
- 返回1获取到锁,加锁成功
注意:这里存在一个问题,如果我们在上锁的期间,如果程序崩溃的情况下就会出现死锁的情况,没有能主动释放锁
- 版本二
set key value nx ex seconds
delete key
- 添加一个过期时间,这里使用set来对key进行加锁
- nx表示具备setnx的原子性
- ex表示增加一个过期时间
注意:
- 这里会出现一个问题,服务器A对key进行加锁并设置了过期时间,但是A在处理业务逻辑的时候A持有的锁到期,但是业务逻辑还在处理
- 这个时候B服务对key进行加锁并且成功(因为A持有的锁已经过期自动释放)
- 这个时候A处理完业务逻辑准备释放锁
- 这时候问题就来了,A释放了B的锁,出现很大问题。
- 版本三
# 为代码
set lock_key hyggebest nx ex 10000
get lock value --> hyggebest
if hyggebest == (set lock_key){
delete lock
return true
}
return false
-
这里就是在做一层验证,保证自己的加的锁自己释放。
-
这里需要引入lua脚本,通过lua脚本可以以原子性的方式执行,从而保证了锁释放的原子性
-
最终版本
# 加锁
set lock_key lock_value nx ex 10000
# 解锁
if redis.call("get",KEYS[1]) == ARGV[1] the
return redis.call("DEL",KEYS[1])
else
return 0
go简化版的reids解锁实例
func AcquireLock(client *redis.Client, key, value string, ttl time.Duration) bool{
return client.SetNX(context.Background(), key, value, ttl).Val()
}
func ReleaseLock(client *redis.Client,key, value string) bool {
script := `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
`
result := client.Eval(context.Background(), script, []string{key}, value)
return result.Val() == int64(1)
}
-
redis可靠性
主从容灾
为redis配置从节点,搭建哨兵模式,主节点挂了,自动切换主从节点 -
一致性
多机部署
如果对一致性要求高,可以常尝试使用多机部署。思路:多个机子,通常为基数,达到一半以上同意加锁才算加锁成功。
操作:配置5台redis主节点
- 向5台redis节点申请加锁。
- 只要超过一半以上同意加锁也就是3台返回成功,那么就是获取到了锁。如果超过一半都失败,需要向每个redis发送锁命令。
- 由于向5个Redis发送请求,会有一定的耗时,所以锁剩余持有时间需要减去请求时间。这个可以作为判断依据,如果剩余时间为0,那么也是获取锁失败。
- 使用完成后,向所有的redis发送解锁请求。
- 这个就是redis的RedLock
RedLock算法设置了加锁的超时时间,为了避免因为某个Reids实例发生故障而一直等待。