"孤独被染上童话底色~"
我们谈到"锁"这个概念,你一定会想到这一定是涉及到了线程安全的问题。当一个进程内的不同线程,需要访问同一资源(共享资源)时,如果进行不加锁,就会出现线程安全的问题。在分布式系统中,每个进程都是独立运行于一台独立的机器中的,当它们对共享资源进行访问时,如果不进行限制,也会出现类似的安全问题。但,之前的,仅仅运用于进程内的锁,不会再起任何作用,其次,分布式系统中多个进程间的执行顺序也具有不确定性。
------前言
什么是分布式锁
在分布式系统中,涉及到的多个节点访问共享资源的问题时,也需要"锁" 来做互斥控制,避免类似"线程"安全的问题。但在分布式系统中使用的不再是普通的锁机制,诸如:java 的 synchronized 或者 C++ 的 std::mutex等。这样的锁都是只能在当前进程中⽣效, 在分布式的这种多个进程多个主机的场景下就⽆能为⼒了.
此时就需要使⽤到分布式锁:
本质上就是使⽤⼀个公共的服务器, 来记录 加锁状态。
这个公共的服务器可以是Redis,可以是Mysql、ZooKeeper等组件,还可以 是我们⾃⼰写的⼀个服务。
分布式锁的实现
本标题同Redis相关,所以咱们使用的公共服务器基于Redis实现。本质上的思路十分简单,就是利用Redis内部的键值对,来识别锁的信息。
(1) 加\解锁
当我们引入分布式锁之后:
设置键值对的逻辑是,不存在就设置,存在就设置失败。
这同Redis中的一个命令十分契合:setnx
使用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) 原子性
即:在服务器1中的线程A执行DEL后,线程B执行DEL前,加入一个新的服务器2执行加锁操作,此时因为线程A已经把锁释放了,所以新服务器2加锁是成功的。但紧接着,线程B又执行DEL操作,把服务器2的加锁给释放了。
引入lua脚本
归根到底,出现这个问题的原因就在于GET、DEL不是原子性的,中间被"插队"了。解决这个问题的方法很多:比如说Redis自身提供的事务。但在实际中往往会采用使用lua脚本,这更好的方案。
lua是一个编程语言,作为Redis中内嵌的语言。
使用lua编写的脚本传递到Redis服务器上,可以通过客户端来控制Redis执行该脚本。Redis执行lua脚本的过程,是原子的。就相当于把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 节点都包含⼀个主节点和若⼲从节点. 并且组和组之间存 储的数据都是⼀致的, 相互之间是 "备份" 关系。
如果给某个节点加锁失败, 就⽴即再尝试下⼀个节点。当加锁成功的节点数超过总节点数的⼀半, 才视为加锁成功。
所以,当一个Master节点挂了,并且没有将新到的数据同步给从节点时,也不会影响加锁的正确性了。
同理, 释放锁的时候, 也需要把所有节点都进⾏解锁操作。这种"少数服从多数"的策略操作的思想,也同哨兵机制选取leader,集群选取新的主节点相似。
实际开发中, 我们也并不会真的⾃⼰实现⼀个分布式锁. 已经有很多现成的库帮我们封装好了, 我们直接 使⽤即可:⽐如 Java 中的 Redisson, C++ 中的 redis-plus-plus. 当然, 有些⼤⼚也会有⾃⼰版本的分布式锁的实现。
当然以上仅仅是例举了分布式锁的一些实现特性,实际中分布式锁需要考虑的问题复杂、繁多。此处我们不做过多讨论了。
小结:
① 什么时分布式锁?
② 分布式锁的一些特性:如何加\解锁? 设置过期时间、校验机制、原子性(lua脚本)、看门狗、Redlock算法
本篇到此结束,感谢你的阅读。
祝你好运,向阳而生~