(一).什么是分布式锁
在分布式系统中,会涉及到多个节点访问同一个公共资源的情况,此时就需要通过 "锁" 来做互斥控制,避免出现"线程安全"的问题。
但是Java中的synchronized这样的锁都是只能在当前进程中生效 ,但是在分布式的这样的多个进程多个主机的场景下就无能为力了。
事实上,本质上就是使用一个公共的服务器,来记录加锁状态。
(二).分布式锁的基础实现

就拿上图的"买票"系统来说吧。
所谓的"分布式锁",也就是一个单独的服务器程序,给其他的服务器提供"加锁"这样的服务。免票服务器,在进行买票的时候,需要进行先加锁(往redis上设置一个特殊的 key - value )完成上述的买票操作,买完之后,再将这个 key - value删除掉。
其他的服务器也像买票的时候,也去redis上尝试设置 key - value ,如果发现 key - value 已经存在,则"加锁失败"。
我们可以使用redis提供的 setnx 命令来进行加锁操作,当不存在的时候就进行设置,如果存在,则直接失败。
(三).引入过期时间
现在有一种情况,就是,某个服务器,现在执行了setnx命令成功了,此时就说明加锁成功。但是,后面,因为种种原因,程序崩溃了,导致这个锁没有进行释放。没有释放,就意味着没有进行解锁,此时,其他客户端,想要买票的时候,就无法进行购买,这不是我们期待的结果。
为了解决这个问题,我们可以引入**"过期时间"**。给key设置过期时间,一旦时间到了,即使没有释放锁,那么key也会自动被删除。
我们可以通过 set ex nx 命令实现。
但是不能使用 setnx 命令 和 expire 命令。因为redis上的多个命令,是无法保证原子性,此时就可能出现,两个命令,一个成功,一个失败。所以,一个命令,更大稳妥。
(四).引入校验id
对于Redis中写入的加锁键值对,其他的节点也是可以进行删除的,因为这个"锁",本质上就是redis的普通键值对。
例如,服务器1执行了加锁操作,然后服务器2执行了解锁操作。
为了解决上述问题,就需要引入一点校验机制。
①.给每一台服务器进行编号,每个服务器都有自己的身份标识。
②.在进行加锁的时候,设置 key - value 。 key对应着要针对那个资源进行加锁,value就可以存储刚才的服务器的编号,标识出当前这个锁是哪个服务器加上的。
后续,在进行解锁 的时候,先查询这个锁对应的服务器编号,然后判定一下这个编号是否就是当前执行解锁的服务器编号,如果是,才能真正执行del,如果不是,就失效。
(五).引入lua脚本
在进行解锁的时候,第一步先要查询判定,第二步再进行del 。这两个操作,并不是原子的。此时,就有可能同一个服务器,两个线程都在执行上述的解锁操作。

当服务器1的线程A执行了DEL操作的之后,锁就被释放掉了。那么服务器2的线程C执行加锁操作是可以执行成功的。但是,服务器1的线程B紧接着又执行了DEL操作,此时就会把刚刚服务器2的加锁操作给解锁了。
上述问题,本质上就是get和del操作不是原子的产生的问题。
为了解决上述问题,我们可以采用**"lua脚本"**。
我们可以使用lua编写一段逻辑,把这个脚本上传到redis服务器上,然后就可以让客户端来控制redis执行上述的脚本。
redis执行lua脚本的过程,是原子的,相当于执行一条命令,事实上,lua脚本中可以写多个命令。
Lua
if redis.call('get',KEYS[1]) == ARGV[1] then
return redis.call('del',KEYS[1])
else
return 0
end;
(六).引入watch dog(看门狗)
上面的方案还是有一个问题的。
在加锁的时候,我们需要给key设置过期时间。那么这个过期时间到底应该设置多少合适?
如果设置的短,那么就有可能在业务逻辑还没执行完,就释放锁了;如果设置的长,则会导致"锁释放不及时"的问题。
此时,更好的方式应该是"动态续约"。服务器这边,需要引入一个专门的线程,负责"续约",这个"专门的线程"称为"看门狗"。
例如,初始情况下,设置一个过期时间为1000ms,当剩下300ms的时候,如果当前任务还没有执行完,就把过期时间再续上1000ms,等时间快到了,任务还没执行完,就再进行续约。如果服务器中途崩溃了,自然就没有人负责续约了,此时这个锁就会在较短的时间内被自动释放。
(七).引入Redlock算法
上面引入的一些方案,都是针对于"服务器"进行设置的。那么使用redis作为分布式锁,redis本身是否会挂掉?
答案是可能的。
在前面介绍了 主从复制,哨兵,集群。都是用来保证redis"高可用"的方案。
redis的主节点和从节点进行数据同步的时候,是存在延时的。可能主节点收到了set请求后,还没来得及同步给从节点,主节点就挂了。即使从节点升级成了主节点,但是刚才的加锁对应的数据也是不存在的。
至此,redis作者提供了一个方案,就是使用**"Redlock算法"**

此处的加锁,就按照一定的顺序,针对这些组redis都进行加锁操作。如果说某个节点挂了,可能是redis挂了,那么继续给下一个节点加锁即可。如果写入key的成功的节点数超过总数的一半,就视为加锁成功。在进行解锁的时候,也就会把上述的节点都设置一遍解锁。