「如何设计」高可用的分布式锁

栏目: 数据库 · 发布时间: 4年前

内容简介:分布式环境下,锁定全局唯一公共资源 表现为:第一步是上锁的资源目标,是锁定全局唯一公共资源,只有是全局唯一的资源才存在多个线程或服务竞争的情况,互斥性表现为一个资源的隔离级别串行化,如果对照单机事务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 向主节点请求锁
  2. 业务线程-1 获取锁
  3. 业务线程-1 获取到锁并开始执行业务
  4. 这个时候redis刚生成的锁在主从之间还未进行同步
  5. redis这时候主节点挂掉了
  6. redis的从节点升级为主节点
  7. 业务线程-2 想新的主节点请求锁
  8. 业务线程-2 获取到新的主节点返回的锁
  9. 业务线程-2 获取到锁开始执行业务
  10. 这个时候 业务线程-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. 业务线程-1 业务线程-2 分别向zk的/lock目录下,申请创建有序的临时节点
  2. 业务线程-1 抢到/lock0001 的文件,也就是在整个目录下最小序的节点,也就是线程-1获取到了锁
  3. 业务线程-2 只能抢到/lock0002的文件,并不是最小序的节点,线程2未能获取锁
  4. 业务线程-1 与 lock0001 建立了连接,并维持了心跳,维持的心跳也就是这把锁的租期
  5. 当业务线程-1 完成了业务,将释放掉与zk的连接,也就是释放了这把锁

zk分布式锁的代码实现

zk官方提供的客户端并不支持分布式锁的直接实现,我们需要自己写代码去利用zk的这几个特性去进行实现。

「如何设计」高可用的分布式锁

zk分布式锁客户端假死的问题

客户端创建了临时有序节点并建立了事件监听,就可以让业务线程与zk维持心跳,这个心跳也就是这把锁的租期。当客户端的业务线程完成了执行就把节点进行删除,也就释放了这把锁,不过中间也可能存在问题

  • 客户端挂掉

    因为注册的是临时节点,客户端挂掉,zk会进行感知,也就会把这个临时节点删除,锁也就随着释放

  • 业务线程假死

    业务线程并没有消息,而是一个假死状态,(例如死循环,死锁,超长gc),这个时候锁会被一直霸占不能释放,这个问题需要从两个方面进行解决。

    第一个是本身业务代码的问题,为何会出现死循环,死锁等问题。

    第二个是对锁的异常监控问题,这个其实也是微服务治理的一个方面。

zk分布式锁 的GC 问题

刚刚说了zk锁的维持是靠zk和客户端的心跳进行维持,如果客户端出现了长时间的GC会出现什么状况

「如何设计」高可用的分布式锁
  1. 业务线程-1 获取到锁,但未开始执行业务
  2. 业务线程-2 发生长时间的GC
  3. 业务线程-1 和 zk 的心跳发生断链
  4. lock0001 的临时节点因为心跳断链而被删除
  5. 业务线程-2 获取到锁
  6. 业务线程-2 开始执行业务
  7. 业务线程-1 GC完毕,开始执行业务
  8. 业务线程-1 和 业务线程-2 同时执行业务

基于 etcd 分布式锁

etcd分布式锁的实现原理

etcd实现分布式锁比zk要简单很多,就是使用key value的方式进行写入,在集群中,如果存在key的话就不能写入,也就意味着不能获取到锁,如果集群中,可以写入key,就意味着获取得到锁。

etc到使用了raft保证了集群的一致性,也就是在外界看来,只要etcd集群中某一台机器存在了锁,所有的机器也就存在了锁,这个跟zk一样属于强一致性,并且数据是可以进行持久化,默认数据一更新就持久化。

「如何设计」高可用的分布式锁

锁的租期续约问题

etcd 并不存在一个心跳的机制,所以跟redis一样获取锁的时候就要对其进行expire的指定,这个时候就存在一个锁的租期问题。

租期问题有几种思路可以去解决,这里讨论其中一种:

在获取到锁的业务线程,可以开启一个子线程去维护和轮训这把锁的有效时间,并定时的对这把锁进行续租

「如何设计」高可用的分布式锁

假设业务线程获取到一把锁,锁的expire时间为10s,业务线程会开启一个子线程通过轮训的方式每2秒钟去把这把锁进行续租,每次都将锁的expire还原到10s,当业务线程执行完业务时,会把这把锁进行删除,事件完毕。

这种思路一样会存在问题:

  1. 客户端挂掉,业务线程和续租子线程都会挂掉,锁最终会释放
  2. 业务线程假死,这个跟zk的假死情况一样,也是属于业务代码应该解决的问题
  3. 客户端超长GC问题,长GC导致续租子进程没有进行及时续租,锁被超时释放。(GC的问题可能是个极端问题,一般GC超过几秒就可能去查看问题了)

总结

首先得了解清楚我们使用分布式锁的场景,为何使用分布式锁,用它来帮我们解决什么问题,先聊场景后聊分布式锁的技术选型。

无论是redis,zk,etcd其实在各个场景下或多或少都存在一些问题,例如redis的AP模型会限制很多使用场景,但它却拥有了几者中最高的性能,zookeeper的分布式锁要比redis可靠很多,但他繁琐的实现机制导致了它的性能不如redis,而且zk会随着集群的扩大而性能更加下降。etcd 看似是一种折中的方案,不过像锁的租期续约都要自己去实现。

简单来说,先了解业务场景,后进行技术选型。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

精通Spring 4.x

精通Spring 4.x

陈雄华、林开雄、文建国 / 电子工业出版社 / 2017-1-1 / CNY 128.00

Spring 4.0是Spring在积蓄4年后,隆重推出的一个重大升级版本,进一步加强了Spring作为Java领域第一开源平台的翘楚地位。Spring 4.0引入了众多Java开发者翘首以盼的基于Groovy Bean的配置、HTML 5/WebSocket支持等新功能,全面支持Java 8.0,最低要求是Java 6.0。这些新功能实用性强、易用性高,可大幅降低Java应用,特别是Java W......一起来看看 《精通Spring 4.x》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

在线进制转换器
在线进制转换器

各进制数互转换器

SHA 加密
SHA 加密

SHA 加密工具