Redis 锁

77 阅读3分钟

分布式锁

  • 互斥性:锁的目的是获取资源的使用权,所以只让一个竞争者持有锁。
  • 安全性:避免死锁情况的发生。当一个竞争者持有锁期间,有可能因为意外的崩溃导致锁未能主动释放。所以持有锁也能够正常释放,并保证后续竞争者能获取到锁。
  • 对称性:同一个锁,枷锁和解锁必须是同一个拥有者。不能把其他竞争者的锁释放,这个又称为锁的可重入性。
  • 可靠性:需要有一定程度的异常处理能力、容灾能力。

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表示增加一个过期时间

注意:

  1. 这里会出现一个问题,服务器A对key进行加锁并设置了过期时间,但是A在处理业务逻辑的时候A持有的锁到期,但是业务逻辑还在处理
  2. 这个时候B服务对key进行加锁并且成功(因为A持有的锁已经过期自动释放)
  3. 这个时候A处理完业务逻辑准备释放锁
  4. 这时候问题就来了,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主节点

  1. 向5台redis节点申请加锁。
  2. 只要超过一半以上同意加锁也就是3台返回成功,那么就是获取到了锁。如果超过一半都失败,需要向每个redis发送锁命令。
  3. 由于向5个Redis发送请求,会有一定的耗时,所以锁剩余持有时间需要减去请求时间。这个可以作为判断依据,如果剩余时间为0,那么也是获取锁失败。
  4. 使用完成后,向所有的redis发送解锁请求。
  5. 这个就是redis的RedLock

RedLock算法设置了加锁的超时时间,为了避免因为某个Reids实例发生故障而一直等待。