为了防止分布式系统中的多个进程之间相互干扰,咱们须要一种分布式协调技术来对这些进程进行调度。而这个分布式协调技术的核心就是来实现这个分布式锁。node
分布式锁有哪些实现redis
add
命令。此命令是原子性操做,只有在 key
不存在的状况下,才能 add
成功,也就意味着线程获得了锁。setnx
命令。此命令一样是原子性操做,只有在 key
不存在的状况下,才能 set
成功。setnx(lock_sale_商品ID, 1)
当一个线程实行 setnx
返回 1
,说明 key
本来不存在,该线程成功的到了锁;当一个线程执行setnx
返回 0
,说明 key
已经存在,该线程抢锁失败。算法
当获得锁的线程执行完任务,须要释放锁,以便其余线程能够进入。释放锁的最简单方式是执行 del
指令,伪代码以下:apache
del(lock_sale_商品ID)
释放锁后,其余线程就能够继续执行 setnx
命令来得到锁。服务器
若是一个获得锁的线程在执行任务的过程当中挂掉,来不及显式地释放锁,这块资源将永远被锁住(死锁),别的线程再也别想进来。因此,setnx
的 key
必须设置一个超时时间,以保证即便没有被显式释放,这把锁也要在必定时间后自动释放。setnx
不支持超市参数,因此须要额外的指令,伪代码以下:网络
expire(lock_sale_商品ID, 30)
if(setnx(lock_sale_商品ID, 1) == 1){ expire(lock_sale_商品ID, 30) try { do something } finally { del(lock_sale_商品ID) } }
setnx
和 expire
的非原子性setnx
不支持传入超时时间,但可使用 set
指令,并增长可选参数:数据结构
set(lock_sale_商品ID, 1, 30, NX)
del
致使误删能够在 del
释放锁以前作一个判断,验证当前的锁是否是本身加的锁。具体实现:能够在加锁的时候把当前的线程ID当作 value
,并在删除以前验证 key
对应的 value
是否是本身线程的ID。架构
加锁:框架
String threadId = Thread.currentThread().getId() set(key, threadId, 30, NX)
解锁: 致使新问题:判断和释放锁是两个独立操做,不是原子性的。异步
if(threadId.equals(redisClient.get(key))){ del(key) }
对于分布式锁,应减小死锁的可能,并保证操做的原子性
Zookeeper 是一种分布式协调服务,用于管理大型主机。在分布式环境中协调和管理服务是一个复杂的过程。Zookeeper 经过极其简单的架构和 API 解决了这个问题。
Zookeeper 的数据模型很像数据结构中的树,也很像文件系统的目录。
树是由节点组成的,Zookeeper 的数据存储也是一样基于节点,这种节点佳做Znode。对于不一样树的节点,Znode 的引用方式是路径引用,相似于文件路径:/动物/猫
/汽车/宝马
注意:Zookeeper 是为读多写少的场景所设计。Znode并非用来存储大规模业务数据,而是用于存储少许的状态和配置信息,每一个节点的数据最大不能超过 1MB。
create
delete
exists
getData
setData
getChildren
其中,exests
, getData
, getChildren
属于读操做。Zookeeper 客户端在请求读操做的时候,能够选择是否设置 Watch。
咱们能够把 Watch 理解成是注册在特定 Znode 上的触发器。当这个 Znode 发生改变,也就是调用了 create
, delete
,setData
等方法的时候,将会出发 Znode 上注册的对应事件,请求 Watch 的客户端会接受到异步通知。
具体交互过程:
getData
方法,watch
参数是 true
。服务端接收到请求,返回节点数据,而且在对应的哈希表里插入被 Watch 的 Znode 路径,以及 Watcher 列表。Zookeeper 身为分布式系统协调服务,为了防止单机挂掉的状况,Zookeeper 维护了一个集群。
Zookeeper Service 集群是一主多从结构。
在更新数据时,首先更新到主节点(这里的节点指的是服务器,不是 Znode),在同步到从节点。
在读取数据时,直接读取任意从节点。
为了保证主从节点的数据一致性,Zookeeper 采用 ZAB协议,这种协议很是相似于一致性算法那 Paxos 和 Raft。
Zookeeper Atomic Broadcast,有效解决了 Zookeeper 集群崩溃恢复,以及主从同步数据的问题。
最大 ZXID 也就是节点本地的最新事务编号,包含 epoch 和计数两部分。epoch是纪元的意思,至关于Raft算法选主时候的 term。
加入 Zookeeper 当前主节点挂掉了,集群会进行奔溃恢复。ZAB 的奔溃恢复分红三个阶段:
Leader Election
发现阶段,用于在从节点中发现最新的 ZXID和事务日志(为了防止某些意外的状况,好比因网络缘由在上一阶段产生了多个Leader的状况)。
同步阶段,把 Leader 刚才收集获得的最新历史事务日志,同步给集群中全部的 Follower。只有当半数 Follower 同步成功,这个准 Leader 才能成为正式的 Leader。
自此,故障恢复正式完成。
ZAB 的数据写入涉及到 Broadcast 阶段,简单来讲,就是 Zookeeper 常规状况下更新数据的时候,由 Leader 广播到全部的 Follower。其过程以下:
ZAB 协议既不是强一致性,也不是若一致性,而是处于二者之间的单调一致性(顺序一致性)。它依靠事务 ID 和版本号,保证了数据的更新和读取是有序的。
利用 Zookeeper 的临时顺序节点,能够轻松实现分布式锁。
利用 Znode 和 Watcher,能够实现分布式服务的注册和发现。
Redis 的分布式解决方案 Codis,就利用了 Zookeeper 来存放数据路由表和 codis-proxy 节点的元信息。同时 codis-config 发起的命令都会经过 Zookeeper 同步到各个存活的 codis-proxy。
此外, Kafka、HBase、Hadoop,也都依靠 Zookeeper 同步节点信息,实现高可用。
Zookeeper 的数据结构就像一棵树,这棵树由节点组成,这种节点叫作 Znode。
Znode 分为四种类型:
默认的节点类型。建立节点的客户端与 Zookeeper 断开链接后,该节点依旧存在。
所谓顺序节点,就是在建立节点时,Zookeeper 根据建立的时间顺序给该节点名称进行编号。
与持久节点相反,当建立节点的客户端与 Zookeeper 断开链接后,临时节点会被删除。
临时顺序节点结合 临时节点和顺序节点的特色:在建立节点时,Zookeeper 根据建立的时间顺序给该节点名称进行编号;当建立节点的客户端与 Zookeeper 断开链接后,临时节点会被删除。
释放锁的分为两种状况:
当任务完成时,Client1 会显示调用删除节点 Lock1 的指令。
得到锁的 Client1 在执行任务过程当中,若是崩溃,则会断开与 Zookeeper 服务端的连接。根据临时节点的特性,相关联的节点 Lock1 会随之自动删除。
这时候Client2 会再次查询 ParentLock 下面的全部节点,确认本身建立的 Lock2 是否是目前最小的节点。若是是最小,则 Client2 瓜熟蒂落地得到了锁。
分布式锁 | 优势 | 缺点 |
---|---|---|
Zookeeper | 1. 有封装好的框架,容易实现。 2. 有等待锁的队列,大大提高抢锁效率。 |
添加和删除节点性能低。 |
Redis | Set 和 Del 指令的性能较高。 | 1. 实现复杂,须要考虑超时、原子性、误删等情形。 2. 没有等待锁的队列,只能在客户端自旋来等锁,效率低下。 |