1. 什么是分布式锁
在一个分布式系统中,也会涉及到多个节点同时访问一个公共资源的情况,此时就需要通过锁来做互斥控制,避免出现类似于"线程安全"的问题
java中的 synchronized 这样的锁只能在当前的进程中生效,在分布式这种多个进程多个主机的场景下就无能为力了,此时就需要使用到分布式锁
2. 分布式锁的基础实现
思路很简单,本质上是通过一个键值对来标识锁的状态

所谓的分布式锁也是一个/一组单独的服务器程序,给其他的服务器提供"加锁"这样的业务
想要执行业务代码的时候就需要先访问Redis,在Redis上设置一个键值对
如果这个操作设置成功,就视为当前没有被锁,可以执行,执行完后将键值对删除
如果设置键值对时已经存在就说明"加锁失败"",需要等待或放弃
3. 引入过期时间
当服务器加锁之后开始执行业务,如果服务器1意外宕机了,就会导致解锁操作不能执行,就可能引起其他服务器始终无法获取到锁的情况
为了解决这个问题,可以在设置key的同时引入过期时间,即这个锁最多持有多少就应该被释放
只能使用 set ex nx,如果分开多个操作,由于Redis的多个指令之间不存在关联,并且即使使用了事务也不能保证这两操作一定都成功
4. 引入校验id
对于Redis中写入的加锁键值对,其他节点也是可以删除的
为了解决上述问题,可以引入一个校验id
在删除key的时候,先校验当前删除key的服务器是否是当初加锁的服务器,如果是才可以删除
String key = [要加锁的资源 id];
String serverId = [服务器的编号];
// 加锁, 设置过期时间为 10s
redis.set(key, serverId, "NX", "EX", "10s");
// 执⾏各种业务逻辑, ⽐如修改数据库数据.
doSomeThing();
// 解锁, 删除 key. 但是删除前要检验下 serverId 是否匹配
if (redis.get(key) == serverId) {
redis.del(key);
}
但是很明显,解锁逻辑是两步操作,并非是原子性
5. 引入lua
为了使解锁操作原子性,可以使用Redis的Lua脚本功能
if redis.call('get',KEYS[1]) == ARGV[1] then
return redis.call('del',KEYS[1])
else
return 0
end;
上述代码可以编写成一个.lua后缀的文件,有redis-cli或者jedis等客户端加载,并且发送给Redis服务器,由Redis服务器来执行这段逻辑
一个 lua 脚本会被Redis服务器一原子的方式来执行
6. 引入看 watch dog
上述方案仍然存在一个重要问题,当我们设置了key过期时间之后,仍然存在一定的可能性,当任务还没有完成,key就过期了,这就导致了提前失效
所谓的watch dog 本质上是加锁服务器上的一个单独的线程,通过这个线程来对锁过期时间进行"续约"
7. 引入 Redlock算法
实践中 Redis 一般是以集群的方式部署的,那么就可能出现以下问题:
服务器1向master节点进⾏加锁操作.这个写⼊key的过程刚刚完成,master挂了;slave节 点升级成了新的master节点.但是由于刚才写⼊的这个key尚未来得及同步给slave呢,此时 就相当于服务器1的加锁操作形同虚设了,服务器2仍然可以进⾏加锁(即给新的master写 ⼊key.因为新的master不包含刚才的key).
为了解决这个问题,Redis作者提出了 Redlock算法
我们引入一组Redis节点,其中每一组Redis都包含一个主节点和若干从节点,并且组合组之间存储的数据都是一致的,相互之间是备份关系(并非数据集合的一部分)
加锁的时候,按照一定的顺序,写多个emaster节点,在写锁的时候需要设定操作的"超时时间"

如果给某个节点加锁失败,就立即尝试下一个节点
当加锁成功的接地那数超过总数一般,才视为加锁成功
同理,释放锁的时候,也需要把所有接地那都进行解锁操作
简而言之,Redlck算法的核心就是,加锁操作不能只写给一个Redis节点,而是写给多个,分布式系统中,任何一个节点都是不可靠的,最终的加锁成功结论是"少数服从多数"
由于一个分布式系统不至于大部分节点都同时发生故障,因此这样的可靠性要比但隔节点来书靠谱
8. 其他功能
上述的锁只是一个简单的互斥锁,但是实际上一些特定场景中,还有一些其他特殊的锁
- 可重入锁
- 公平锁
- 读写锁
- .......
基于Redis的分布式锁,也可以实现上述特性