常用的分布式锁和redis和zk两种分布式锁的对比

常用的分布式锁

一、基于数据库实现分布式锁

1. 悲观锁

利用select … where … for update 排他锁

注意: 其他附加功能与实现一基本一致,这里需要注意的是”where name=lock “,name字段必须要走索引,否则会锁表。有些情况下,比如表不大,mysql优化器会不走这个索引,导致锁表问题。

2. 乐观锁

所谓乐观锁与前边最大区别在于基于CAS思想,是不具有互斥性,不会产生锁等待而消耗资源,操作过程中认为不存在并发冲突,只有update version失败后才能觉察到。我们的抢购、秒杀就是用了这种实现以防止超卖。
通过增加递增的版本号字段实现乐观锁

二、基于jdk的实现方式

思路:另启一个服务,利用jdk并发工具来控制唯一资源,如在服务中维护一个concurrentHashMap,其他服务对某个key请求锁时,通过该服务暴露的端口,以网络通信的方式发送消息,服务端解析这个消息,将concurrentHashMap中的key对应值设为true,分布式锁请求成功,可以采用基于netty通信调用,当然你想用java的bio、nio或者整合dubbo、spring cloud feign来实现通信也没问题
缺点:这种方式的分布式锁看似简单,但是要考虑可用性、可靠性、效率、扩展性的话,编码难度会比较高。

三、基于缓存(Redis等)实现分布式锁

1、官方叫做 RedLock 算法,是 redis 官方支持的分布式锁算法。

这个分布式锁有 3 个重要的考量点:

  • 1.互斥(只能有一个客户端获取锁)
  • 2.不能死锁
  • 3.容错(只要大部分 redis 节点创建了这把锁就可以)

2、下面是redis分布式锁的各种实现方式和缺点,按照时间的发展排序

  • 1、直接setnx
    直接利用setnx,执行完业务逻辑后调用del释放锁,简单粗暴
    缺点:如果setnx成功,还没来得及释放,服务挂了,那么这个key永远都不会被获取到
  • 2、setnx设置一个过期时间
    为了改正第一个方法的缺陷,我们用setnx获取锁,然后用expire对其设置一个过期时间,如果服务挂了,过期时间一到自动释放
    缺点:setnx和expire是两个方法,不能保证原子性,如果在setnx之后,还没来得及expire,服务挂了,还是会出现锁不释放的问题
  • 3、set nx px
    redis官方为了解决第二种方式存在的缺点,在2.8版本为set指令添加了扩展参数nx和ex,保证了setnx+expire的原子性,使用方法:
    set key value ex 5 nx
    缺点
    ①如果在过期时间内,事务还没有执行完,锁提前被自动释放,其他的线程还是可以拿到锁
    ②上面所说的那个缺点还会导致当前的线程释放其他线程占有的锁
  • 4、加一个事务id
    上面所说的第一个缺点,没有特别好的解决方法,只能把过期时间尽量设置的长一点,并且最好不要执行耗时任务
    第二个缺点,可以理解为当前线程有可能会释放其他线程的锁,那么问题就转换为保证线程只能释放当前线程持有的锁,即setnx的时候将value设为任务的唯一id,释放的时候先get key比较一下value是否与当前的id相同,是则释放,否则抛异常回滚,其实也是变相地解决了第一个问题
    缺点:get key和将value与id比较是两个步骤,不能保证原子性
  • 5、set nx px + 事务id + lua
    我们可以用lua来写一个getkey并比较的脚本,jedis/luttce/redisson对lua脚本都有很好的支持
    缺点:集群环境下,对master节点申请了分布式锁,由于redis的主从同步是异步进行的,master在内存中写入了nx之后直接返回,客户端获取锁成功,此时master节点挂了,并且数据还没来得及同步,另一个节点被升级为master,这样其他的线程依然可以获取锁
  • 6、redlock
    为了解决上面提到的redis集群中的分布式锁问题,redis的作者antirez的提出了red lock的概念,假设集群中所有的n个master节点完全独立,并且没有主从同步,此时对所有的节点都去setnx,并且设置一个请求过期时间re和锁的过期时间le,同时re必须小于le(可以理解,不然请求3秒才拿到锁,而锁的过期时间只有1秒也太蠢了),此时如果有n / 2 + 1个节点成功拿到锁,此次分布式锁就算申请成功
    缺点:可靠性还没有被广泛验证,并且严重依赖时间,好的分布式系统应该是异步的,并不能以时间为担保,程序暂停、系统延迟等都可能会导致时间错误(网上还有很多人都对这个方法提出了质疑,比如full gc发生的锁的正确性问题,但是antirez都一一作出了解答,感兴趣的同学可以参考一下这位同学的文章

四、基于zookeeper实现的分布式锁

1. 实现方式

ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。基于ZooKeeper实现分布式锁的步骤如下:

(1)创建一个目录mylock;
(2)线程A想获取锁就在mylock目录下创建临时顺序节点;
(3)获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;
(4)线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;
(5)线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。

这里推荐一个Apache的开源库Curator,它是一个ZooKeeper客户端,Curator提供的InterProcessMutex是分布式锁的实现,acquire方法用于获取锁,release方法用于释放锁。

优点:具备高可用、可重入、阻塞锁特性,可解决失效死锁问题。

缺点:因为需要频繁的创建和删除节点,性能上不如Redis方式。

2. 两种利用特性实现原理:

  • 1、利用临时节点特性
    zookeeper的临时节点有两个特性,一是节点名称不能重复,二是会随着客户端退出而销毁,因此直接将key作为节点名称,能够成功创建的客户端则获取成功,失败的客户端监听成功的节点的删除事件
    缺点:所有客户端监听同一个节点,但是同时只有一个节点的事件触发是有效的,造成资源的无效调度
  • 2、利用顺序临时节点特性
    zookeeper的顺序临时节点拥有临时节点的特性,同时,在一个父节点下创建创建的子临时顺序节点,会根据节点创建的先后顺序,用一个32位的数字作为后缀,我们可以用key创建一个根节点,然后每次申请锁的时候在其下创建顺序节点,接着获取根节点下所有的顺序节点并排序,获取顺序最小的节点,如果该节点的名称与当前添加的名称相同,则表示能够获取锁,否则监听根节点下面的处于当前节点之前的节点的删除事件,如果监听生效,则回到上一步重新判断顺序,直到获取锁。

总结

基于数据库分布式锁实现

优点:直接使用数据库,实现方式简单。
缺点

  1. db操作性能较差,并且有锁表的风险
  2. 非阻塞操作失败后,需要轮询,占用cpu资源;
  3. 长时间不commit或者长时间轮询,可能会占用较多连接资源

基于jdk的并发工具自己实现的锁

优点:不需要引入中间件,架构简单
缺点:编写一个可靠、高可用、高效率的分布式锁服务,难度较大

基于redis缓存

  1. redis set px nx + 唯一id + lua脚本
    优点:redis本身的读写性能很高,因此基于redis的分布式锁效率比较高
    缺点:依赖中间件,分布式环境下可能会有节点数据同步问题,可靠性有一定的影响,如果发生则需要人工介入

  2. 基于redis的redlock
    优点:可以解决redis集群的同步可用性问题
    缺点

  3. 依赖中间件,并没有被广泛验证,维护成本高,需要多个独立的master节点;需要同时对多个节点申请锁,降低了一些效率

  4. 锁删除失败 过期时间不好控制
  5. 非阻塞,操作失败后,需要轮询,占用cpu资源;

基于zookeeper的分布式锁

优点:不存在redis的超时、数据同步(zookeeper是同步完以后才返回)、主从切换(zookeeper主从切换的过程中服务是不可用的)的问题,可靠性很高

缺点:依赖中间件,保证了可靠性的同时牺牲了一部分效率(但是依然很高)。性能不如redis。

jdk的方式不太推荐。

  1. 从理解的难易程度角度(从低到高)数据库 > 缓存 > Zookeeper
  2. 从实现的复杂性角度(从低到高)Zookeeper >= 缓存 > 数据库
  3. 从性能角度(从高到低)缓存 > Zookeeper >= 数据库
  4. 从可靠性角度(从高到低)Zookeeper > 缓存 > 数据库

没有绝对完美的实现方式,具体要选择哪一种分布式锁,需要结合每一种锁的优缺点和业务特点而定。

作者:
森林木马

出处:
https://www.cnblogs.com/owenma/

Original: https://www.cnblogs.com/owenma/p/12355262.html
Author: 森林木马
Title: 常用的分布式锁和redis和zk两种分布式锁的对比

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/529434/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • cgroup-v1在android中的应用实现浅析

    本文档内容主要是分析android设备中cgroup v1实现了哪些控制器,他们有哪些子控制器以及如何配置这些控制器的。 我是使用红米Note4Plus的开发版本来调研分析的,手机…

    Linux 2023年6月7日
    0111
  • 从前端走向后端

    每次过年回老家聚会,遇到不熟悉的亲戚朋友,经常被问到职业是什么。一开始,我总是很认真的回答这个问题,结果常常引出一番尴尬的问答。 “你&…

    Linux 2023年6月6日
    0101
  • Golang 实现 Redis(6): 实现 pipeline 模式的 redis 客户端

    本文是使用 golang 实现 redis 系列的第六篇, 将介绍如何实现一个 Pipeline 模式的 Redis 客户端。 通常 TCP 客户端的通信模式都是阻塞式的: 客户端…

    Linux 2023年5月28日
    085
  • k8s vs k3s: 差异解析

    2. K3s的优势 小型 K3s 的最大优势是它的尺寸最小(小于 100 MB),这有助于它以最少的设置在小型硬件中启动 Kubernetes 集群。 快速部署 curl -sfL…

    Linux 2023年6月13日
    0114
  • dpdk21.11 添加igb_uio模块

    dpdk 21.11 添加 IGB_UIO模块 IGB_UIO模块 两种添加方式 零、 下载IGB_UIO模块 一、 直接添加到文件中 1.1 复制 dpdk-kmods/linu…

    Linux 2023年6月6日
    0104
  • Apache JMeter安装及使用

    Apache JMeter是Apache组织开发的基于Java的压力测试工具。用于对软件做压力测试,它最初被设计用于Web应用测试,但后来扩展到其他测试领域。 它可以用于测试静态和…

    Linux 2023年6月8日
    0117
  • JuiceFS V1.0 RC1 发布,大幅优化 dump/load 命令性能, 深度用户不容错过

    各位社区的伙伴, JuiceFS v1.0 RC1 今天正式发布了! 这个版本中,最值得关注的是对元数据迁移备份工具 dump/load 的优化。 这个优化需求来自于某个社区重度用…

    Linux 2023年6月14日
    082
  • 音频属性

    采样频率就是采用一段音频,做为样本,因为wav使用的是数码信号,它是用一堆数字来描述原来的模拟信号,所以它要对原来的模拟信号进行分析,我们知道所有的声音都有其波形,数码信号就是在原…

    Linux 2023年6月8日
    0111
  • PHP 通过两个日期计算年龄

    参数:$birth_year:出生年份;$birth_month:出生月份;$birth_day:出生日 function getAgeByBirth($birth_year,$b…

    Linux 2023年6月7日
    0105
  • redis 学习指南

    2、 redis.windows.conf各项配置参数介绍 默认情况下,redis不是在后台模式运行的,如果需要在后台进程运行,把该项的值更改为yes,默认为no daemoniz…

    Linux 2023年5月28日
    088
  • 字节 字符 位(比特)

    位(bit):binary digit,计算机储存的最小单位,n个比特可以确定2^n个情况,如11010100为8位。 字节(byte):一个字节储存8位无符号数,范围为0-225…

    Linux 2023年6月13日
    0104
  • 【深度学习】神经网络前向传播简单实现

    步骤 输入层的每个节点与隐藏层的每个节点做点对点计算,加权求和 + 激活函数 利用同样的方法,计算隐藏层到输出层 隐藏层对加权结合后的结果使用激活函数,本例使用Sigmoid 最终…

    Linux 2023年6月13日
    0102
  • ArchLinux安装-2022-01-12

    这篇教程,是我基于B站up住theCW的视频教程整理的,其中添加了一些我在安装n次之后的经验(虽然失败过几次,但我现在安装不会再出差错,所以请放心的看此教程) 当然,我认为theC…

    Linux 2023年6月13日
    0101
  • AWS修改RDS时区

    查看 RDS 当前时区 默认情况下,AWS 的 RDS 采用的是 UTC 时间。而我们地区一般位于东八区,因此我们本地的时间是 UTC+8。 连接到 RDS 上,查询当前实例的时区…

    Linux 2023年6月7日
    0102
  • 双系统设置默认启动系统

    在原有windows系统下,我们装完Ubuntu系统后,会出现Ubuntu的grub引导界面(倒计时后自动进入Ubuntu),如下图所示。 假设我们需要将倒计时后默认启动的系统改为…

    Linux 2023年6月14日
    0139
  • Linux运行Jar包方式

    1 运行Jar包 第一种方式 java -jar xxx.jar 最基本的方式,程序运行的信息会一直输出在控制台,ctrl+c中断或者关闭窗口时,程序中断执行。 第二种方式 jav…

    Linux 2023年6月7日
    0106
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球