详解分布式锁

目录

基于MYSQL实现

通过 insert 实现

通过 select 实现

基于Redis实现

基于set实现

redlock

基于zookeeper实现


在分布式系统中对于共享资源我们需要确保在任何时刻只有一个节点或进程能够访问,也就需要加锁,而我们的本地锁无法满足这个需求,本地锁是在单个节点或进程内部使用的锁机制,它只能保证在该节点或进程内的线程安全。锁本身就是共享资源需要对所有节点或线可见,我们的本地锁实际上是私有资源。

分布式锁的要求

  • 全局唯一性:分布式锁需要在整个分布式系统中保证唯一性,即任何时刻只能有一个节点或进程能够获取到锁。这样才能确保共享资源在分布式环境下的安全访问。
  • 高可用性:分布式锁服务应该具备高可用性,即使部分节点出现故障,也不能影响锁的正常获取和释放,以保证分布式系统的正常运行。

我们在这里介绍基于三种中间件实现分布式锁的思路:MYSQL,Redis,ZooKeeper。 


基于MYSQL实现

在 MySQL 中实现分布式锁由两种方法:通过 insert 实现,通过 select 实现。 

通过 insert 实现

这种方法的原理是 MYSQL 的唯一索引性质。当插入相同唯一键的数据时会失败。通过插入一条具有唯一键的数据来尝试获取锁,如果插入成功则表示获取锁成功,插入失败则表示锁已被其他线程或进程持有。

首先需要建立锁表:

CREATE TABLE distributed_lock (id INT AUTO_INCREMENT PRIMARY KEY,lock_name VARCHAR(255) NOT NULL,UNIQUE KEY unique_lock_name (lock_name)
);

锁中有两个字段,一个是自增主键 id字段,一个是唯一索引 lock_name。

获取锁:

-- 尝试插入一条记录来获取锁
INSERT INTO distributed_lock (lock_name) VALUES ('your_lock_name');

如果插入成功,则表示获取锁成功;如果插入失败(因为唯一索引冲突),则表示锁已被其他线程或进程持有。

释放锁:

-- 通过删除记录来释放锁
DELETE FROM distributed_lock WHERE lock_name = 'your_lock_name';

这种方法实现起来十分简单,但他实现的是非公平锁,锁的分配不依据申请的顺序,而是随机的。对于公平锁,我们可以通过 select 实现。

通过 select 实现

SELECT ... FOR UPDATE 是当前读,为了避免幻读现象MYSQL会为当前行加锁(独占锁),任何对该行的任何写操作与当前读都会被阻塞,直到当前事务提交或回调。

创建锁表:

CREATE TABLE distributed_lock (id INT AUTO_INCREMENT PRIMARY KEY,lock_name VARCHAR(255) NOT NULL,UNIQUE KEY unique_lock_name (lock_name)
);

获取锁:

BEGIN;SELECT * FROM distributed_lock WHERE lock_name = "your_lock_name" FOR UPDATE;

开启事务,并执行当前读即加上独占锁。 

释放锁:

COMMIT;

提交事务,独占锁释放。 

这种方法实现了公平锁,但是也有许多缺点,比如说被阻塞的连接会一直占用,产生连接爆满的问题。

实际上这两种方法我们实现的都是不可重入锁,大家可以思考一下如何实现可重入锁。

这两种方法都没有实现过期功能,如果有结点在获取锁后没有释放锁就挂掉了,锁会一直存在,会出现死锁问题。我们需要为锁设置过期时间,在到期后,自动删除锁。这点可以通过MYSQL定时任务完成。

基于MYSQL实现分布式锁有许多缺点:

  • 并发性能有限:MySQL 本身在处理高并发的锁操作时,性能会受到一定限制。当大量请求同时竞争分布式锁时,MySQL 可能会成为性能瓶颈,导致响应时间变长,吞吐量下降。
  • 锁的粒度较粗:基于 MySQL 实现分布式锁,通常是通过对某一行数据或某张表进行锁操作来实现。这种方式的锁粒度相对较粗,会导致一些不必要的锁竞争。比如,如果多个业务操作只是涉及到表中的不同行,但由于锁是针对整个表或某一行进行的,就可能出现本可以并发执行的操作却因为锁的原因而串行执行,降低了系统的并发处理能力。
  • 单点故障问题:如果 MySQL 数据库出现故障,如服务器宕机、网络故障等,那么基于该数据库实现的分布式锁将无法正常工作,可能会导致整个分布式系统中依赖该锁的业务出现混乱,影响系统的稳定性和可靠性。
  • 数据一致性风险:在 MySQL 的主从复制架构中,存在数据同步延迟的问题。如果在主库上获取锁后,还未来得及将锁的状态同步到从库,此时从库可能会认为锁未被占用,从而导致在从库上也能获取到相同的锁,出现数据不一致的情况。 

基于Redis实现

与MYSQL相比,Redis是纯内存操作因此更快,能承受更高的并发。

基于set实现

Redis 实现分布式锁主要基于其原子操作特性。核心思路是在 Redis 中设置一个特定的键值对,当多个客户端尝试设置这个键时,只有一个客户端能够成功,成功设置的客户端就获得了锁,其他客户端则需要等待。当客户端完成对共享资源的操作后,需要释放锁,即删除这个键。

获取锁:

SET lock_key unique_value NX EX 10
  • lock_key:锁的键名,多个客户端通过这个键名来竞争锁。
  • unique_value:客户端生成的唯一值,用于在释放锁时进行验证,防止误释放其他客户端的锁。
  • NX:表示只有当键不存在时才进行设置操作。
  • EX 10:表示设置键的过期时间为 10 秒,防止因客户端异常崩溃而导致锁无法释放。

 释放锁:

if redis.call("GET", KEYS[1]) == ARGV[1] thenreturn redis.call("DEL", KEYS[1])
elsereturn 0
end
  • KEYS[1]:锁的键名。
  • ARGV[1]:客户端生成的唯一值。

释放锁时,需要先验证锁的唯一值是否匹配,然后再删除锁。可以使用 Lua 脚本来保证操作的原子性。

这种方法非常简单,同时也设置了过期时间避免了死锁,但是在集群模式下就会出现问题:

现在有线程A,B与redis哨兵集群如下图:

 线程A申请锁。

主结点执行命令成功后立即返回,线程 A 获取锁成功。在主从结点数据同步之前,主节点宕机。集群重新选取主结点。 

 分布式 CAP 原理是指在一个分布式系统中,一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)这三个基本需求不能同时被满足,最多只能同时满足其中的两个。Redis 是AP架构简单来说就是,主节点执行完命令后会立即返回再做主从结点的数据同步。

线程B申请同一把锁,此时线程A还未释放锁,但是新选举的主节点并没有锁的数据,因此线程B也申请成功。

这时就会出现两个线程持有同一把锁的情况,而为了解决单点故障问题又不得不使用集群。怎么解决这个问题呢?redis的作者设计了一种算法 redlock。

redlock

redlock 实现思路是多节点加锁,在redis的分布式环境中有 n 个独立的节点。节点之间必须是完全独立的,不存在集群同时节点也必须是独立节点。

获取锁的过程如下:

  • 记录当前时间:客户端记录当前时间(以毫秒为单位)。
  • 依次尝试获取锁:客户端依次向 N 个独立的 Redis 节点发送获取锁的请求(使用 SET 命令),每个请求都有一个超时时间,以防止客户端长时间阻塞在某个故障节点上。
  • 计算获取锁的总时间:客户端在获取到所有节点的响应后,计算从开始请求到获取到所有响应所花费的总时间。只有当总时间小于锁的有效时间,并且在大多数(超过半数,即 N/2 + 1)节点上都成功获取到锁时,才认为最终获取锁成功。
  • 设置锁的有效时间:如果获取锁成功,客户端需要重新计算锁的有效时间,即原来的锁有效时间减去获取锁所花费的总时间。

释放锁:

  • 当客户端完成任务后,需要释放锁。释放锁的过程很简单,客户端只需向所有的 Redis 节点发送释放锁的请求,无论该节点上是否成功获取到锁。

这种方法通过在多个独立的 Redis 节点上获取锁,提高了分布式锁的可用性和容错性,即使部分节点发生故障,仍然可以保证锁的正常使用。保证了高可用。

但由于需要在多个节点上依次尝试获取锁,会增加一定的网络延迟和性能开销。同时算法依赖于各个 Redis 节点的时钟同步,如果节点之间的时钟存在较大偏差,可能会导致锁的安全性受到影响。

为什么节点自身必须独立,不能有主从?

假设这里有四个节点 A B C  D 其中 C  D 为 主从集群。

服务端有两个线程,线程一与线程二同时申请锁,线程一在 节点 B C 加锁成功,线程二在节点 A 加锁成功。线程一获取锁成功。

这时节点C宕机,但是主从未同步,节点D成为主节点。此时线程二向节点D发送请求成功。这时 线程二也获得了一半以上节点。所以此时就会出现两个线程同时持有锁的情况。


基于zookeeper实现

zookeeper的集群是CP架构,在接收请求后,会先保证节点的数据一致性。在超过半数节点同步后再返回。所以对于zk集群不需要考虑主节点故障问题。

zookeeper 实现分布式锁的核心原理基于其临时顺序节点和监听机制。

获取锁:

  • 创建临时顺序节点:当客户端请求锁时,在 ZooKeeper 的特定节点下创建一个临时顺序节点。
  • 判断锁的归属:客户端检查自己创建的节点是否是当前所有子节点中序号最小的。如果是,则表示该客户端获得了锁;否则,它需要等待。
  • 监听:若客户端没有获得锁,它会监听比自己序号小一位的节点的删除事件。当该节点被删除时,客户端会收到通知,再次检查自己是否成为序号最小的节点,若是则获得锁。

释放锁

  • 当客户端完成操作后,删除自己创建的临时顺序节点,释放锁。

 ZooKeeper 的临时节点有一个特性:当创建该临时节点的会话失效(如客户端与 Zk 服务器断开连接)时,该临时节点会被自动删除。因此当客户端异常断开连接时,zk会自动释放锁。

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

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

相关文章

五. Redis 配置内容(详细配置说明)

五. Redis 配置内容(详细配置说明) 文章目录 五. Redis 配置内容(详细配置说明)1. Units 单位配置2. INCLUDES (包含)配置3. NETWORK (网络)配置3.1 bind(配置访问内容)3.2 protected-mode (保护模式)3.3 port(端口)配置3.4 timeout(客户端超时时间)配置3.5 tcp-keepalive()配置…

物业管理系统源码提升社区智能化管理效率与用户体验

内容概要 物业管理系统源码是一种针对社区管理需求而设计的软件解决方案,通过先进的智能化技术,使物业管理变得更加高效和人性化。随着城市化进程的加快,社区的管理复杂性不断增加,而这一系统的推出恰好为物业公司提供了极大的便…

springboot集成钉钉,发送钉钉日报

目录 1.说明 2.示例 3.总结 1.说明 学习地图 - 钉钉开放平台 在钉钉开放文档中可以查看有关日志相关的api,主要用到以下几个api: ①获取模板详情 ②获取用户发送日志的概要信息 ③获取日志接收人员列表 ④创建日志 发送日志时需要根据模板规定日志…

python算法和数据结构刷题[1]:数组、矩阵、字符串

一画图二伪代码三写代码 LeetCode必刷100题:一份来自面试官的算法地图(题解持续更新中)-CSDN博客 算法通关手册(LeetCode) | 算法通关手册(LeetCode) (itcharge.cn) 面试经典 150 题 - 学习计…

院校联合以项目驱动联合培养医工计算机AI人才路径探析

一、引言 1.1 研究背景与意义 在科技飞速发展的当下,医疗人工智能作为一个极具潜力的新兴领域,正深刻地改变着传统医疗模式。从疾病的早期诊断、个性化治疗方案的制定,到药物研发的加速,人工智能技术的应用极大地提升了医疗服务…

【Numpy核心编程攻略:Python数据处理、分析详解与科学计算】2.18 对象数组:在NumPy中存储Python对象

2.18 对象数组:在NumPy中存储Python对象 目录 #mermaid-svg-shERrGOBuM2rBzeB {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-shERrGOBuM2rBzeB .error-icon{fill:#552222;}#mermaid-svg-shERrGOBuM2rB…

响应式编程与协程

响应式编程与协程的比较 响应式编程的弊端虚拟线程Java线程内核线程的局限性传统线程池的demo虚拟线程的demo 响应式编程的弊端 前面用了几篇文章介绍了响应式编程,它更多的使用少量线程实现线程间解耦和异步的作用,如线程的Reactor模型,主要…

python学opencv|读取图像(五十三)原理探索:使用cv.matchTemplate()函数实现最佳图像匹配

【1】引言 前序学习进程中,已经探索了使用cv.matchTemplate()函数实现最佳图像匹配的技巧,并且成功对两个目标进行了匹配。 相关文章链接为:python学opencv|读取图像(五十二)使用cv.matchTemplate()函数实现最佳图像…

javaEE-8.JVM(八股文系列)

目录 一.简介 二.JVM中的内存划分 JVM的内存划分图: 堆区:​编辑 栈区:​编辑 程序计数器:​编辑 元数据区:​编辑 经典笔试题: 三,JVM的类加载机制 1.加载: 2.验证: 3.准备: 4.解析: 5.初始化: 双亲委派模型 概念: JVM的类加…

【01】共识机制

BTF共识 拜占庭将军问题 拜占庭将军问题是一个共识问题 起源 Leslie Lamport在论文《The Byzantine Generals Problem》提出拜占庭将军问题。 核心描述 军中可能有叛徒,却要保证进攻一致,由此引申到计算领域,发展成了一种容错理论。随着…

AI大模型开发原理篇-1:语言模型雏形之N-Gram模型

N-Gram模型概念 N-Gram模型是一种基于统计的语言模型,用于预测文本中某个词语的出现概率。它通过分析一个词语序列中前面N-1个词的出现频率来预测下一个词的出现。具体来说,N-Gram模型通过将文本切分为长度为N的词序列来进行建模。 注意:这…

【汽车电子软件架构】AutoSAR从放弃到入门专栏导读

本文是汽车电子软件架构:AutoSAR从放弃到入门专栏的导读篇。文章延续专栏文章的一贯作风,从概念与定义入手,希望读者能对AutoSAR架构有一个整体的认识,然后对专栏涉及的文章进行分类与链接。本文首先从AutoSAR汽车软件架构的概念&…

python-UnitTest框架笔记

UnitTest框架的基本使用方法 UnitTest框架介绍 框架:framework,为了解决一类事情的功能集合 UnitTest框架:是python自带的单元测试框架 自带的,可以直接使用,不需要格外安装 测试人员用来做自动化测试,作…

【数据结构】_链表经典算法OJ:复杂链表的复制

目录 1. 题目链接及描述 2. 解题思路 3. 程序 1. 题目链接及描述 题目链接:138. 随机链表的复制 - 力扣(LeetCode) 题目描述: 给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,…

Linux——进程间通信之SystemV共享内存

前言 SystemV通信一般包括三种:共享内存、消息队列和信号量。共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到 内核,换句话说是进程不再通过执行进入内核的系统调用来…

Linux网络 | 网络层IP报文解析、认识网段划分与IP地址

前言:本节内容为网络层。 主要讲解IP协议报文字段以及分离有效载荷。 另外, 本节也会带领友友认识一下IP地址的划分。 那么现在废话不多说, 开始我们的学习吧!! ps:本节正式进入网络层喽, 友友们…

SQLGlot:用SQLGlot解析SQL

几十年来,结构化查询语言(SQL)一直是与数据库交互的实际语言。在一段时间内,不同的数据库在支持通用SQL语法的同时演变出了不同的SQL风格,也就是方言。这可能是SQL被广泛采用和流行的原因之一。 SQL解析是解构SQL查询…

Windows程序设计10:文件指针及目录的创建与删除

文章目录 前言一、文件指针是什么?二、设置文件指针的位置:随机读写,SetFilePointer函数1.函数说明2.函数实例 三、 目录的创建CreateDirectory四、目录的删除RemoveDirectory总结 前言 Windows程序设计10:文件指针及目录的创建与…

线程互斥同步

前言: 简单回顾一下上文所学,上文我们最重要核心的工作就是介绍了我们线程自己的LWP和tid究竟是个什么,总结一句话,就是tid是用户视角下所认为的概念,因为在Linux系统中,从来没有线程这一说法,…

DRM系列七:Drm之CREATE_DUMB

本系列文章基于linux 5.15 DRM驱动的显存由GEM(Graphics execution management)管理。 一、创建流程 创建buf时,user层提供需要buf的width,height以及bpp(bite per pixel),然后调用drmIoctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &…