文章目录
1.什么是分布式锁
在一个分布式的系统中,也会涉及到多个节点访问同一个资源的情况。此时就需要通过 锁 来互斥控制,避免线程安全的情况发生。
C++ /Linux 中的mutex锁是只在当前进程中有效,在分布式多进程的环境中就不行了。
引入分布式锁:
分布式锁也是一个/一组单独的服务器,提供"加锁"服务。
在"买票"的例子中,当一个客户端进行买票时,在操作过程中就会加锁,往redis上设置一个特殊的key-value(内容是什么不重要),完成买票操作后,再删除key-value,当其他客户端也进行买票时,尝试设置key-value发现已经存在,--》加锁失败(放弃/阻塞)
setnx命令:不存在就设置,存在就出错
2.setnx
使用setnx完成加锁,del完成解锁。
但是如果程序崩溃了,没执行到解锁逻辑怎么办?
在C++中可以用到RAI I(智能指针)来释放锁。
Java中可以 try -catch -finally (finally就是无论是否异常都会执行) 来销毁锁。
但这种做法在分布式系统中没用。
当服务器掉电--》进程异常终止--》导致redis上设置的key无人删除--》导致其他服务器无法获取到锁。
这时候很容易联想到我们之前学的 "过期时间"
3.过期时间
给set的key设置过期时间,一旦时间到,key会自动被删除。
set ex nx完成设置
setnx + expire也能设置过期时间,但这里不能使用。
因为redis的多个命令直接不能保证原子性,如果两步操作完成加锁,可能会导致一个命令 成功 ,另一个命令失败。
所以还是一条命令实现比较稳妥。
4.校验id
所谓加锁,就是给redis设置一个key-value,
解锁,就是给redis上这个key-value删除。
所以可能出现服务器1执行了加锁,服务器2执行了解锁。
----》给系统带来严重的问题。
为了解决这个问题,就要引入校验机制。
步骤:
- 给服务器编号,让服务器有自己的身份标识
- 进行加锁的时候,key对应针对被操作的资源,value存储刚才服务器的编号 (出问题后能知道是哪个服务器的问题)
后续在解锁的时候,先查询是否是自己的编号,是才执行del,不是,就失败。
通过校验,可以有效避免"误解锁"
5.lua脚本
解锁的时候
- 查询判定
- del
这两步操作标识原子的,就可能出现问题。

比如在同一个服务器中,线程a先判定自己可以解锁,然后线程b也判定自己能解锁(前面的校验是针对不同的服务器的,在同一个服务器中,如果锁的粒度过大,可能就出现被别的线程解锁的情况),之后线程a解锁,然后如果此时,服务器2号的c,要使用锁,进行加锁,然后线程b把c的锁解了,这就出问题了。
所以说,查询判定和 del解锁 操作应该是原子的,才能避免问题。
解决措施:
- 使用redis的事务(原子性)
- 使用lua脚本
lua是编程语言,可以作为redis的内嵌语言,特别轻量。通过lua写的脚本逻辑,在redis上运行是原子的,相当于一条命令(尽管在脚本中写了很多命令)
6.看门狗
在加锁的时候,要给key加上过期时间。
如果是静态的过期时间:
太短的话--》业务逻辑还没执行完,就释放锁了
太长的话--》锁释放不及时。
所以就需要"动态续约 "
往往服务器也要有一个专门的线程去负责续约这件事。这个线程就叫 看门狗 (watch dog)
动态续约:初识情况下,设置一个过期时间(假设1s),在还剩300ms时(可以自己设置,这里是举例),如果当前任务还没执行完,就继续续约1s,等又到300ms时,再次续约,直到执行完。
如果服务器中途崩溃了,就没人负责续约了。锁也能在短时间内释放。
7.redlock算法
使用redis分布式锁,redis可能自身会挂掉。
进行加锁--》把key设置到主节点上。如果主节点挂了,哨兵自动把从节点升成主节点,确保刚刚的锁是可以使用的。
但是主节点和从节点间的数据同步,是存在延时的。如果主节点加锁后,还没同步给从节点就挂了,从节点晋升成为主节点,但是刚刚加锁的数据是没有的。
作为分布式系统,就要随时考虑某个节点挂了会不会影响大局。
解决措施:引入redlock算法(作者给出的另一个方案)
主要思路就是 冗余
之前都是主从结构,redlock算法把所有的节点都作为主节点。当客户端对资源进行加锁时,会按照一定的顺序,在所有的redis服务器上都进行加锁操作。
只要有一半以上的服务器加锁成功,那么客户端的加锁就成功了(其他客户端再次对该资源加锁,不可能有一半的服务器上还能加锁--》加锁不成功)
同样解锁也是一样,超过一半的key被del,就是解锁成功