内容简介:分布式环境下,锁定全局唯一公共资源 表现为:第一步是上锁的资源目标,是锁定全局唯一公共资源,只有是全局唯一的资源才存在多个线程或服务竞争的情况,互斥性表现为一个资源的隔离级别串行化,如果对照单机事务ACID的隔离型来说,互斥性的事务隔离级别是SERLALIZABLE,属于最高的隔离级别。(事务隔离级别:DEFAULT,READ_UNCOMMITTED,READ_COMMITED,REPEATABLE_READ,SERLALIZABLE)
分布式环境下,锁定全局唯一公共资源 表现为:
- 请求串行化
- 互斥性
第一步是上锁的资源目标,是锁定全局唯一公共资源,只有是全局唯一的资源才存在多个线程或服务竞争的情况,互斥性表现为一个资源的隔离级别串行化,如果对照单机事务ACID的隔离型来说,互斥性的事务隔离级别是SERLALIZABLE,属于最高的隔离级别。
(事务隔离级别:DEFAULT,READ_UNCOMMITTED,READ_COMMITED,REPEATABLE_READ,SERLALIZABLE)
分布式锁目的
- 解决业务层幂等性
- 解决MQ消费端多次接受同一消息
- 确保串行 | 隔离级别
- 多台机器同时执行定时任务
寻找唯一资源进行上锁
例子:
1. 防止用户重复下单 共享资源进行上锁的对象 : 【用户id】 2. 订单生成后发送MQ给消费者进行积分的添加 寻找上锁的对象 :【订单id】 3. 用户已经创建订单,准备对订单进行支付,同时商家在对这个订单进行改价 寻找上锁对象 : 【订单id】 复制代码
基于 redis 分布式锁
redis单线程串行处理天然就是解决串行化问题,用来解决分布式锁是再适合不过。
实现方式:
setnx key value Expire_time
获取到锁 返回 1 , 获取失败 返回 0
存在问题:
锁时间不可控
redis 只能在setnx指定一个锁的超时时间,假设初始设定锁的时间是10秒钟,但是业务获取到锁跑了20秒钟,在10秒钟之后,如果又有一个业务可以获取到相同的一把锁,这个时候可能就存在两个相同的业务都获取得到锁的问题,并且两个业务处在并行阶段。也就是第一个获取锁的业务无法对自身的锁进行续租。
单点连接超时问题
redis 的client与server端并没有维持心跳的机制,如果在连接出现问题,client会得到一个超时的回馈。
主从问题
redis的集群实际上在CAP模式中是处在与AP的模型,保证可用性。在主从复制中“主”有数据,但可能“从”还没有数据,这个时候,一旦主挂掉或者网络抖动等各种原因,可能会切换到“从”节点,这个时候有可能会导致两个业务线程同时的获取到两把锁。
- 业务线程-1 向主节点请求锁
- 业务线程-1 获取锁
- 业务线程-1 获取到锁并开始执行业务
- 这个时候redis刚生成的锁在主从之间还未进行同步
- redis这时候主节点挂掉了
- redis的从节点升级为主节点
- 业务线程-2 想新的主节点请求锁
- 业务线程-2 获取到新的主节点返回的锁
- 业务线程-2 获取到锁开始执行业务
- 这个时候 业务线程-1 和 业务线程-2 同时在执行任务
redlock
上述的问题其实并不是redis的缺陷,只是redis采用了AP模型,它本身无法确保我们对一致性的要求。redis官方推荐redlock算法来保证,问题是redlock至少需要三个redis主从实例来实现,维护成本比较高,相当于redlock使用三个redis集群实现了自己的另一套一致性算法,比较繁琐,在业界也使用得比较少。
能不能使用redis作为分布式锁
能不能使用redis作为分布式锁,这个本身就不是redis的问题,还是取决于业务场景,我们先要自己确认我们的场景是适合 AP 还是 CP , 如果在社交发帖等场景下,我们并没有非常强的事务一致性问题,redis提供给我们高性能的AP模型是非常适合的,但如果是交易类型,对数据一致性非常敏感的场景,我们可能要寻在一种更加适合的 CP 模型
redis可能作为高可用的分布式锁并不合适,我们需要确立高可用分布式锁的设计目标
高可用分布式锁设计目标
- 强一致性,是CP模型
- 服务高可用,不存在单点问题
- 锁能够续租和自动释放
- 业务接入简单
三种分布式锁方案对比
- | redis | zookeeper | etcd |
---|---|---|---|
一致性算法 | 无 | zab | raft |
CAP | AP | CP | CP |
高可用 | 主从 | N+1 | N+1 |
实现 | setnx | create临时有序节点 | restful |
基于zookeeper分布式锁
刚刚也分析过,redis其实无法确保数据的一致性,先来看zookeeper是否合适作为我们需要的分布式锁,首先zk的模式是CP模型,也就是说,当zk锁提供给我们进行访问的时候,在zk集群中能确保这把锁在zk的每一个节点都存在。
(这个实际上是zk的leader通过二阶段提交写请求来保证的,这个也是zk的集群规模大了的一个瓶颈点)
zk 锁实现的原理
说zk的锁问题之前先看看zookeeper中几个特性,这几个特性构建了zk的一把分布式锁 特性:
-
有序节点
当在一个父目录下如 /lock 下创建 有序节点,节点会按照严格的先后顺序创建出自节点 lock000001,lock000002,lock0000003,以此类推,有序节点能严格保证各个自节点按照 排序 命名生成。
-
临时节点
客户端建立了一个临时节点,在客户端的会话结束或会话超时,zookepper会自动删除该解ID那。
-
事件监听
在读取数据时,我们可以对节点设置监听,当节点的数据发生变化(1 节点创建 2 节点删除 3 节点数据变成 4 自节点变成)时,zookeeper会通知客户端。
结合这几个特点,来看下zk是怎么组合分布式锁。
- 业务线程-1 业务线程-2 分别向zk的/lock目录下,申请创建有序的临时节点
- 业务线程-1 抢到/lock0001 的文件,也就是在整个目录下最小序的节点,也就是线程-1获取到了锁
- 业务线程-2 只能抢到/lock0002的文件,并不是最小序的节点,线程2未能获取锁
- 业务线程-1 与 lock0001 建立了连接,并维持了心跳,维持的心跳也就是这把锁的租期
- 当业务线程-1 完成了业务,将释放掉与zk的连接,也就是释放了这把锁
zk分布式锁的代码实现
zk官方提供的客户端并不支持分布式锁的直接实现,我们需要自己写代码去利用zk的这几个特性去进行实现。
zk分布式锁客户端假死的问题
客户端创建了临时有序节点并建立了事件监听,就可以让业务线程与zk维持心跳,这个心跳也就是这把锁的租期。当客户端的业务线程完成了执行就把节点进行删除,也就释放了这把锁,不过中间也可能存在问题
-
客户端挂掉
因为注册的是临时节点,客户端挂掉,zk会进行感知,也就会把这个临时节点删除,锁也就随着释放
-
业务线程假死
业务线程并没有消息,而是一个假死状态,(例如死循环,死锁,超长gc),这个时候锁会被一直霸占不能释放,这个问题需要从两个方面进行解决。
第一个是本身业务代码的问题,为何会出现死循环,死锁等问题。
第二个是对锁的异常监控问题,这个其实也是微服务治理的一个方面。
zk分布式锁 的GC 问题
刚刚说了zk锁的维持是靠zk和客户端的心跳进行维持,如果客户端出现了长时间的GC会出现什么状况
- 业务线程-1 获取到锁,但未开始执行业务
- 业务线程-2 发生长时间的GC
- 业务线程-1 和 zk 的心跳发生断链
- lock0001 的临时节点因为心跳断链而被删除
- 业务线程-2 获取到锁
- 业务线程-2 开始执行业务
- 业务线程-1 GC完毕,开始执行业务
- 业务线程-1 和 业务线程-2 同时执行业务
基于 etcd 分布式锁
etcd分布式锁的实现原理
etcd实现分布式锁比zk要简单很多,就是使用key value的方式进行写入,在集群中,如果存在key的话就不能写入,也就意味着不能获取到锁,如果集群中,可以写入key,就意味着获取得到锁。
etc到使用了raft保证了集群的一致性,也就是在外界看来,只要etcd集群中某一台机器存在了锁,所有的机器也就存在了锁,这个跟zk一样属于强一致性,并且数据是可以进行持久化,默认数据一更新就持久化。
锁的租期续约问题
etcd 并不存在一个心跳的机制,所以跟redis一样获取锁的时候就要对其进行expire的指定,这个时候就存在一个锁的租期问题。
租期问题有几种思路可以去解决,这里讨论其中一种:
在获取到锁的业务线程,可以开启一个子线程去维护和轮训这把锁的有效时间,并定时的对这把锁进行续租
假设业务线程获取到一把锁,锁的expire时间为10s,业务线程会开启一个子线程通过轮训的方式每2秒钟去把这把锁进行续租,每次都将锁的expire还原到10s,当业务线程执行完业务时,会把这把锁进行删除,事件完毕。
这种思路一样会存在问题:
- 客户端挂掉,业务线程和续租子线程都会挂掉,锁最终会释放
- 业务线程假死,这个跟zk的假死情况一样,也是属于业务代码应该解决的问题
- 客户端超长GC问题,长GC导致续租子进程没有进行及时续租,锁被超时释放。(GC的问题可能是个极端问题,一般GC超过几秒就可能去查看问题了)
总结
首先得了解清楚我们使用分布式锁的场景,为何使用分布式锁,用它来帮我们解决什么问题,先聊场景后聊分布式锁的技术选型。
无论是redis,zk,etcd其实在各个场景下或多或少都存在一些问题,例如redis的AP模型会限制很多使用场景,但它却拥有了几者中最高的性能,zookeeper的分布式锁要比redis可靠很多,但他繁琐的实现机制导致了它的性能不如redis,而且zk会随着集群的扩大而性能更加下降。etcd 看似是一种折中的方案,不过像锁的租期续约都要自己去实现。
简单来说,先了解业务场景,后进行技术选型。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 用匠心精神,打造高可用分布式系统
- 用匠心精神,打造高可用分布式系统
- 分布式系统关注点:初识高可用
- 实力分享,聚焦分布式高可用消息队列
- 超全面分布式缓存高可用方案:哨兵机制
- 高可用分布式存储 etcd 的实现原理
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。