Redis分布式锁

Redis分布式锁_第1张图片

“孤独被染上童话底色~” 


        我们谈到“锁”这个概念,你一定会想到这一定是涉及到了线程安全的问题。当一个进程内的不同线程,需要访问同一资源(共享资源)时,如果进行不加锁,就会出现线程安全的问题。在分布式系统中,每个进程都是独立运行于一台独立的机器中的,当它们对共享资源进行访问时,如果不进行限制,也会出现类似的安全问题。但,之前的,仅仅运用于进程内的锁,不会再起任何作用,其次,分布式系统中多个进程间的执行顺序也具有不确定性。

——前言


什么是分布式锁

        在分布式系统中,涉及到的多个节点访问共享资源的问题时,也需要“锁” 来做互斥控制,避免类似“线程”安全的问题。但在分布式系统中使用的不再是普通的锁机制,诸如:java 的 synchronized 或者 C++ 的 std::mutex等。这样的锁都是只能在当前进程中⽣效, 在分布式的这种多个进程多个主机的场景下就⽆能为⼒了.

        此时就需要使⽤到分布式锁:
本质上就是使⽤⼀个公共的服务器, 来记录 加锁状态。

        这个公共的服务器可以是Redis,可以是Mysql、ZooKeeper等组件,还可以 是我们⾃⼰写的⼀个服务。

 

分布式锁的实现

        本标题同Redis相关,所以咱们使用的公共服务器基于Redis实现。本质上的思路十分简单,就是利用Redis内部的键值对,来识别锁的信息。

(1) 加\解锁 

Redis分布式锁_第2张图片

        当我们引入分布式锁之后:

Redis分布式锁_第3张图片        设置键值对的逻辑是,不存在就设置,存在就设置失败。

        这同Redis中的一个命令十分契合:setnx 

Redis分布式锁_第4张图片

        使用setnx确实可以得到一种“加锁”的效果,如果想要解锁,就使用Redis中的DEL命令。

        

 (2) 设置过期时间      

        如果某个服务器完成了setnx,实现了加锁的效果。但突然,该服务器断电,程序崩溃了!也就是说,现目前该服务器无法将这键值对删除,解除对资源的占用,那么其他服务器就无法得到锁。

        所以,正常情况下会给key设置过期时间,一旦时间到了,就会被自动删除掉。

        我们可以使用例如: set nx ex的命令完成设置。注意,这是一条命令!如果是使用 setnx expire的方式,就算Redis是原子的,它仅仅是保证该命令被执行,至于命令执行正确与否,它是压根不关心的。不同于,Mysql事务翻滚、回溯。

(3) 校验机制 

        所谓的加锁,就是在公共Redis器中设置键值对,所谓解锁就是在这个Redis上将这个键值对删除。

        那有没有一种可能,换句话说失误,服务器1设置的键值对,会被服务器2删除呢 ?答案是肯定的!因为压根没有任何身份标识,到底是谁设置的这个键值对。

        所以,为了解决这个问题,一般会引入校验机制。我们可以在键值对的Value设置编号,每一个服务器都有一个编号,设置键值对的时候就将自己的编号写入Value中。

        一旦Redis检测到DEL命令时,首先就需要判断发起该操作的服务器id,是否同键值对设置时,填入的Value一致。如果是,才会真正执行,不是就会失败。

(4) 原子性

Redis分布式锁_第5张图片

        即:在服务器1中的线程A执行DEL后,线程B执行DEL前,加入一个新的服务器2执行加锁操作,此时因为线程A已经把锁释放了,所以新服务器2加锁是成功的。但紧接着,线程B又执行DEL操作,把服务器2的加锁给释放了。

引入lua脚本

        归根到底,出现这个问题的原因就在于GET、DEL不是原子性的,中间被“插队”了。解决这个问题的方法很多:比如说Redis自身提供的事务。但在实际中往往会采用使用lua脚本,这更好的方案。

        lua是一个编程语言,作为Redis中内嵌的语言。

        使用lua编写的脚本传递到Redis服务器上,可以通过客户端来控制Redis执行该脚本。Redis执行lua脚本的过程,是原子的。就相当于把lua中的所有命令当成一条命令。 下面是一份伪代码:

# 获取KEYS[1]Value 
if redis.call('get',KEYS[1]) == ARGV[1] then
    return redis.call('del',KEYS[1])      # 执行删除
else
    return 0
end;

(5) 看门狗机制        

        在上述方案中,我们给Key设置了一个过期时间。当时间超过设定时间时,即便服务器自身不对该Key进行释放,redis也会对这个键值对进行删除。

        但有时也存在一定的可能,当前 任务可能还没有执行完,key 就先过期了。这就导致锁提前失效。所以到底设置多长的时间,作为过期时间是值得考虑的。所以,更好的方式是“动态续约”,初始情况是一个值,根据任务完成度,会在这个值的基础上延长过期时间。    

        即watch dog, 本质上是加锁的服务器上的⼀个单独的线程, 通过这个线程来对锁过期时间进⾏ "续约"。这是一个广义的概念,在很多场景,针对过期问题都会引入看门狗机制。
注意, 这个线程是业务服务器上的, 不是 Redis 服务器的.

(6) 引⼊ Redlock 算法

        实践中的 Redis ⼀般是以集群的⽅式部署的 (⾄少是主从的形式, ⽽不是单机). 那么就可能出现以下⽐较极端的⼤冤种情况:

        服务器1 向 master 节点进⾏加锁操作. 这个写⼊ key 的过程刚刚完成, master 挂了;

slave 节 点升级成了新的 master 节点. 但是由于刚才写⼊的这个 key 尚未来得及同步给 slave 呢, 此时 就相当于 服务器1 的加锁操作形同虚设了。

        为了解决这个问题, Redis 的作者提出了 Redlock 算法:

        我们引⼊⼀组 Redis 节点. 其中每⼀组 Redis 节点都包含⼀个主节点和若⼲从节点. 并且组和组之间存 储的数据都是⼀致的, 相互之间是 "备份" 关系。
Redis分布式锁_第6张图片

        如果给某个节点加锁失败, 就⽴即再尝试下⼀个节点。当加锁成功的节点数超过总节点数的⼀半, 才视为加锁成功。

        所以,当一个Master节点挂了,并且没有将新到的数据同步给从节点时,也不会影响加锁的正确性了。

         同理, 释放锁的时候, 也需要把所有节点都进⾏解锁操作。这种“少数服从多数”的策略操作的思想,也同哨兵机制选取leader,集群选取新的主节点相似。

        实际开发中, 我们也并不会真的⾃⼰实现⼀个分布式锁. 已经有很多现成的库帮我们封装好了, 我们直接 使⽤即可:⽐如 Java 中的 Redisson, C++ 中的 redis-plus-plus. 当然, 有些⼤⼚也会有⾃⼰版本的分布式锁的实现。

         当然以上仅仅是例举了分布式锁的一些实现特性,实际中分布式锁需要考虑的问题复杂、繁多。此处我们不做过多讨论了。


小结:

① 什么时分布式锁?

② 分布式锁的一些特性:如何加\解锁? 设置过期时间、校验机制、原子性(lua脚本)、看门狗、Redlock算法


本篇到此结束,感谢你的阅读。

祝你好运,向阳而生~

Redis分布式锁_第7张图片

你可能感兴趣的:(redis,redis,分布式,数据库)