# 分布式锁实现

* [分布式锁实现原理](#分布式锁实现原理)
* [DB数据库乐观锁](#DB数据库乐观锁)
* [基于Redis的分布式锁](#基于Redis的分布式锁)
* [基于ZooKeeper的分布式锁](#基于ZooKeeper的分布式锁)

## 分布式锁实现

***

### 分布式锁实现原理

在同一个jvm进程中时，可以使用JUC提供的一些锁来解决多个线程竞争同一个共享资源时候的线程安全问题，但是当多个不同jvm进程中的线程共同竞争同一个共享资源时候，juc包的锁就无能无力了，这时候就需要分布式锁了。

常见的有使用zk的最小版本，redis的set函数，数据库锁来实现

首先，为了确保分布式锁可用，我们至少要确保锁的实现同时满足以下四个条件： 1、互斥性。在任意时刻，只有一个客户端能持有锁。 2、不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁，也能保证后续其他客户端能加锁。 3、具有容错性。只要大部分的Redis节点正常运行，客户端就可以加锁和解锁。 4、解铃还须系铃人。加锁和解锁必须是同一个客户端，客户端自己不能把别人加的锁给解了。

针对分布式锁的实现目前有多种方案：互斥、超时、可重入等 1、基于数据库实现分布式锁：获取锁插入一条记录，释放锁就删除记录 2、基于缓存（redis，memcached）实现分布式锁 3、基于Zookeeper实现分布式锁

分布式锁实现

1. DB
2. memcached(add)
3. Redis(setnx)
4. zookeeper(临时有序节点)

代码org.quickstart.javase.distributed.lock 使用数据库悲观锁实现不可重入的分布式锁 使用Redis单实例实现不可重入的分布式锁 使用zookeeper序列节点实现不可重入的分布式锁

分布式锁实现： <https://www.cnblogs.com/yuyutianxia/p/7149363.html> <http://blog.csdn.net/x\\_i\\_y\\_u\\_e/article/details/50864205> <http://www.importnew.com/27477.html?utm\\_source=tuicool\\&utm\\_medium=referral>

***

### DB数据库乐观锁

DB数据库： 使用select \* from lock where uid = 1 for update的拍他锁，设置不自动提交，先执行该SQL，然后执行业务，然后提交

数据库实现缺点：数据库单点问题 这把锁没有失效时间，一旦解锁操作失败，就会导致锁记录一直在数据库中，其他线程无法再获得到锁。 这把锁只能是非阻塞的，因为数据的insert操作，一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列，要想再次获得锁就要再次触发获得锁操作。 这把锁是非重入的，同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。

数据库是单点？搞两个数据库，数据之前双向同步。一旦挂掉快速切换到备库上。 没有失效时间？只要做一个定时任务，每隔一定时间把数据库中的超时数据清理一遍。 非阻塞的？搞一个while循环，直到insert成功再返回成功。 非重入的？在数据库表中加个字段，记录当前获得锁的机器的主机信息和线程信息，那么下次再获取锁的时候先查询数据库，如果当前机器的主机信息和线程信息在数据库可以查到的话，直接把锁分配给他就可以了。

***

memcached(add)

***

### 基于Redis的分布式锁

1. 利用setnx+expire命令 (错误的做法)\
   setnx和expire是分开的两步操作，不具有原子性，如果执行完第一条指令应用异常或者重启了，锁将无法过期。
2. 使用set+expire+事务 或者 使用setnx+Lua脚本（包含setnx和expire两条指令）\
   说道Redis分布式锁大部分人都会想到：setnx+lua，或者知道set key value px milliseconds nx。

这种实现方式有3大要点（也是面试概率非常高的地方）：

1. set命令要用set key value px milliseconds nx；
2. value要具有唯一性；
3. 释放锁时要验证value值，不能误解锁；

事实上这类琐最大的缺点就是它加锁时只作用在一个Redis节点上，即使Redis通过sentinel保证高可用，如果这个master节点由于某些原因发生了主从切换，那么就会出现锁丢失的情况：

1. 在Redis的master节点上拿到了锁；
2. 但是这个加锁的key还没有同步到slave节点；
3. master故障，发生故障转移，slave节点升级为master节点；
4. 导致锁丢失。
5. 使用 set key value \[EX seconds]\[PX milliseconds]\[NX|XX] 命令 (正确做法)

* EX seconds – 设置键key的过期时间，单位时秒
* PX milliseconds – 设置键key的过期时间，单位时毫秒
* NX – 只有键key不存在的时候才会设置key的值
* XX – 只有键key存在的时候才会设置key的值

1. Redlock算法与Redisson实现（Redisson实现了Redlock算法） the Redlock algorithm\
   在Redis的分布式环境中，Redis 的作者提供了RedLock 的算法来实现一个分布式锁。

[基于Redis的分布式锁实现](https://juejin.cn/post/6844903830442737671)\
[Redlock：Redis分布式锁最牛逼的实现](https://mp.weixin.qq.com/s?__biz=MzU5ODUwNzY1Nw==\&mid=2247484155\&idx=1\&sn=0c73f45f2f641ba0bf4399f57170ac9b\&scene=21#wechat_redirect)\
[使用Redis的分布式锁](https://redis.io/topics/distlock)\
[Redis分布式锁背后的原理](https://cloud.tencent.com/developer/article/1710618)

Redis： 使用SetNX，设置过期时间，过期时间太小会出现业务没有做完锁就释放了 还可以设置超过多少次没有获取就等待，随机生成一个等待时间，等时间到后在进行重试，升级成重量级锁

基于缓存： redis的setnx方法等。并且，这些缓存服务也都提供了对数据的过期自动删除的支持，可以直接设置超时时间来控制锁的释放。 使用缓存实现分布式锁的优点: 性能好，实现起来较为方便。 使用缓存实现分布式锁的缺点: 通过超时时间来控制锁的失效时间并不是十分的靠谱。

***

### 基于ZooKeeper的分布式锁

基于ZK的方式： 基于zookeeper临时有序节点可以实现的分布式锁。大致思想即为：每个客户端对某个方法加锁时，在zookeeper上的与该方法对应的指定节点的目录下，生成一个唯一的 瞬时有序节点。 判断是否获取锁的方式很简单，只需要判断有序节点中序号最小的一个。 当释放锁的时候，只需将这个瞬时节点删除即可。同时，其可以避免服务宕机导 致的锁无法释放，而产生的死锁问题。

锁无法释放？使用Zookeeper可以有效的解决锁无法释放的问题，因为在创建锁的时候，客户端会在ZK中创建一个临时节点，一旦客户端获取到锁之后突然挂掉（ Session连接断开），那么这个临时节点就会自动删除掉。其他客户端就可以再次获得锁。

非阻塞锁？使用Zookeeper可以实现阻塞的锁，客户端可以通过在ZK中创建顺序节点，并且在节点上绑定监听器，一旦节点有变化，Zookeeper会通知客户端，客户端可以检查自己创建的节点是不是当前所有节点中序号最小的，如果是，那么自己就获取到锁，便可以执行业务逻辑了。

不可重入？使用Zookeeper也可以有效的解决不可重入的问题，客户端在创建节点的时候，把当前客户端的主机信息和线程信息直接写入到节点中，下次想要获取锁的 时候和当前最小的节点中的数据比对一下就可以了。如果和自己的信息一样，那么自己直接获取到锁，如果不一样就再创建一个临时的顺序节点，参与排队。

单点问题？使用Zookeeper可以有效的解决单点问题，ZK是集群部署的，只要集群中有半数以上的机器存活，就可以对外提供服务。

使用zookeeper实现分布式锁 首先我们先来看看使用zk实现分布式锁的原理，在zk中是使用文件目录的格式存放节点内容，其中节点类型分为： 持久节点（PERSISTENT ）：节点创建后，一直存在，直到主动删除了该节点。 临时节点（EPHEMERAL）：生命周期和客户端会话绑定，一旦客户端会话失效，这个节点就会自动删除。 序列节点（SEQUENTIAL ）：多个线程创建同一个顺序节点时候，每个线程会得到一个带有编号的节点，节点编号是递增不重复的，如下图：

如上图，三个线程分别创建路径为/root/node的节点，可知在zk服务器端会在root下存在三个node节点，并且器编号唯一递增。 具体在节点创建过程中，可以混合使用，比如临时顺序节点（EPHEMERAL\_SEQUENTIAL），这里我们就使用临时顺序节点来实现分布式锁。

分布式锁实现：\
创建临时顺序节点,比如/root/node，假设返回结果为nodeId。 获取/root下所有孩子节点，用自己创建的nodeId的序号与所有子节点比较，看看自己是不是编号最小的。如果是最小的则就相当于获取到了锁，如果自己不是最小的，则从所有子节点里面获取比自己次小的一个节点，然后设置监听该节点的事件，然后挂起当前线程。 当最小编号的线程获取锁，处理完业务后删除自己对应的nodeId，删除后会激活比自己大一号的节点的线程从阻塞变为运行态，被激活的线程应该就是当前node序列号最小的了，然后就会获取到锁。

***
