美文网首页
实现分布式锁

实现分布式锁

作者: 有斧头的IceBear | 来源:发表于2022-03-07 10:10 被阅读0次

    实现分布式锁

    分布式锁的目的是保证在分布式部署的应用集群中,多个服务在请求同一个方法或者同一个业务操作的情况下,对应业务逻辑只能被一台机器上的一个线程执行,避免出现并发问题。

    实现分布式锁目前有三种流行方案,即基于数据库、Redis、ZooKeeper 的方案

    「方案一:」

    使用节点中的存储数据区域,ZK中节点存储数据的大小不能超过1M,但是只是存放一个标识是足够的,线程获得锁时,先检查该标识是否是无锁标识,若是可修改为占用标识,使用完再恢复为无锁标识

    「方案二:」

    使用子节点,每当有线程来请求锁的时候,便在锁的节点下创建一个子节点,子节点类型必须维护一个顺序,对子节点的自增序号进行排序,默认总是最小的子节点对应的线程获得锁,释放锁时删除对应子节点便可

    图片

    「死锁风险:」

    两种方案其实都是可行的,但是使用锁的时候一定要去规避死锁

    • 方案一看上去是没问题的,用的时候设置标识,用完清除标识,但是要是持有锁的线程发生了意外,释放锁的代码无法执行,锁就无法释放,其他线程就会一直等待锁,相关同步代码便无法执行

    • 方案二也存在这个问题,但方案二可以利用ZK的临时顺序节点来解决这个问题,只要线程发生了异常导致程序中断,就会丢失与ZK的连接,ZK检测到该链接断开,就会自动删除该链接创建的临时节点,这样就可以达到即使占用锁的线程程序发生意外,也能保证锁正常释放的目的

    「避免羊群效应」

    把锁请求者按照后缀数字进行排队,后缀数字小的锁请求者先获取锁。

    如果所有的锁请求者都 watch 锁持有者,当代表锁请求者的 znode 被删除以后,所有的锁请求者都会通知到,但是只有一个锁请求者能拿到锁。这就是羊群效应。

    为了避免羊群效应,每个锁请求者 watch 它前面的锁请求者。

    每次锁被释放,只会有一个锁请求者 会被通知到。

    这样做还让锁的分配具有公平性,锁定的分配遵循先到先得的原则。

    图片

    「用 ZooKeeper 实现分布式锁的算法流程,根节点为 /lock:」

    • 客户端连接 ZooKeeper,并在/lock下创建临时有序子节点,第一个客户端对应的子节点为/lock/lock01/00000001,第二个为 /lock/lock01/00000002

    • 其他客户端获取/lock01下的子节点列表,判断自己创建的子节点是否为当前列表中序号最小的子节点;

    • 如果是则认为获得锁,执行业务代码,否则通过 watch 事件监听/lock01的子节点变更消息,获得变更通知后重复此步骤直至获得锁;

    • 完成业务流程后,删除对应的子节点,释放分布式锁;

    在实际开发中,可以应用 Apache Curator 来快速实现分布式锁,Curator 是 Netflix 公司开源的一个 ZooKeeper 客户端,对 ZooKeeper 原生 API 做了抽象和封装。

    实现分布式ID

    我们可以通过 ZooKeeper 自身的客户端和服务器运行模式,来实现一个分布式网络环境下的 ID 请求和分发过程。

    每个需要 ID 编码的业务服务器可以看作是 ZooKeeper 的客户端。ID 编码生成器可以作为 ZooKeeper 的服务端。

    客户端通过发送请求到 ZooKeeper 服务器,来获取编码信息,服务端接收到请求后,发送 ID 编码给客户端。

    「实现原理:」

    可以利用 ZooKeeper 数据模型中的顺序节点作为 ID 编码。

    • 客户端通过调用 create 函数创建顺序节点。服务器成功创建节点后,会响应客户端请求,把创建好的节点信息发送给客户端。

    • 客户端用数据节点名称作为 ID 编码,进行之后的本地业务操作。

    利用 ZooKeeper 中的顺序节点特性,很容易使我们创建的 ID 编码具有有序的特性。并且我们也可以通过客户端传递节点的名称,根据不同的业务编码区分不同的业务系统,从而使编码的扩展能力更强。

    虽然使用 ZooKeeper 的实现方式有这么多优点,但也会有一些潜在的问题。

    其中最主要的是,在定义编码的规则上还是强烈依赖于程序员自身的能力和对业务的深入理解。

    很容易出现因为考虑不周,造成设置的规则在运行一段时间后,无法满足业务要求或者安全性不够等问题。

    实现负载均衡

    「常见负载均衡算法」

    轮询法

    轮询法是最为简单的负载均衡算法,当接收到来自网络中的客户端请求后,负载均衡服务器会按顺序逐个分配给后端服务。

    比如集群中有 3 台服务器,分别是 server1、server2、server3,轮询法会按照 sever1、server2、server3 这个顺序依次分发会话请求给每个服务器。当第一次轮询结束后,会重新开始下一轮的循环。

    随机法

    随机算法是指负载均衡服务器在接收到来自客户端的请求后,会根据一定的随机算法选中后台集群中的一台服务器来处理这次会话请求。

    不过,当集群中备选机器变的越来越多时,通过统计学我们可以知道每台机器被抽中的概率基本相等,因此随机算法的实际效果越来越趋近轮询算法。

    原地址哈希法

    原地址哈希算法的核心思想是根据客户端的 IP 地址进行哈希计算,用计算结果进行取模后,根据最终结果选择服务器地址列表中的一台机器,处理该条会话请求。

    采用这种算法后,当同一 IP 的客户端再次访问服务端后,负载均衡服务器最终选举的还是上次处理该台机器会话请求的服务器,也就是每次都会分配同一台服务器给客户端。

    加权轮询法

    加权轮询的方式与轮询算法的方式很相似,唯一的不同在于选择机器的时候,不只是单纯按照顺序的方式选择,还根据机器的配置和性能高低有所侧重,配置性能好的机器往往首先分配。

    加权随机法

    加权随机法和我们上面提到的随机算法一样,在采用随机算法选举服务器的时候,会考虑系统性能作为权值条件。

    最小连接数法

    最小连接数算法是指,根据后台处理客户端的连接会话条数,计算应该把新会话分配给哪一台服务器。

    一般认为,连接数越少的机器,在网络带宽和计算性能上都有很大优势,会作为最优先分配的对象。

    「利用 ZooKeeper 实现 负载均衡 算法」

    这里我们通过采用最小连接数算法,来确定究竟如何均衡地分配网络会话请求给后台客户端。

    如下图所示,建立的 ZooKeeper 数据模型中 Severs 节点可以作为存储服务器列表的父节点。

    在它下面创建 servers_host1、servers_host2、servers_host3等临时节点来存储集群中的服务器运行状态信息。

    图片

    整个实现的过程如下图所示。

    图片
    • 首先,在接收到客户端的请求后,通过 getData 方法获取服务端 Severs 节点下的服务器列表,其中每个节点信息都存储有当前服务器的连接数。

    • 通过判断选择最少的连接数作为当前会话的处理服务器,并通过 setData 方法将该节点连接数加 1。

    • 最后,当客户端执行完毕,再调用 setData 方法将该节点信息减 1。

    • 我们定义当服务器接收到会话请求后。在 ZooKeeper 服务端增加连接数的 addBlance 方法。

    • 我们通过 readData 方法获取服务器最新的连接数,之后将该连接数加 1,再通过 writeData 方法将新的连接数信息写入到服务端对应节点信息中。

    • 当服务器处理完该会话请求后,需要更新服务端相关节点的连接数。

    • 具体的操作与 addBlance 方法基本一样,只是对获取的连接信息进行减一操作。

    「这里注意:」

    我们日常用到的负载均衡器主要是选择后台处理的服务器,并给其分发请求。

    而通过 ZooKeeper 实现的服务器,只提供了服务器的筛选工作。

    在请求分发的过程中,还是通过负载算法计算出要访问的服务器,之后客户端自己连接该服务器,完成请求操作。

    相关文章

      网友评论

          本文标题:实现分布式锁

          本文链接:https://www.haomeiwen.com/subject/xrhirrtx.html