分布式锁竟然这么简单?(荣耀典藏版)

大家好,我是小月夜枫,作为一个后台开发,不管是工作还是面试中,分布式一直是一个让人又爱又恨的话题。它如同一座神秘的迷宫,时而让你迷失方向,时而又为你揭示出令人惊叹的宝藏。

今天,让我们来聊聊分布式领域中那位不太引人注意却功不可没的角色,它就像是分布式系统的守卫,保护着资源不被随意访问——这就是分布式锁!

想象一下,如果没有分布式锁,多个分布式节点同时涌入一个共享资源的访问时,就像一群饥肠辘辘的狼崽子汇聚在一块肉前,谁都想咬一口,最后弄得肉丢了个精光,大家都吃不上。

 

 

而有了分布式锁,就像给这块肉上了道坚固的城墙,只有一只狼能够穿越,享受美味。

那它具体是怎么做的呢?这篇文章中,小❤将带大家一起了解分布式锁是如何解决分布式系统中的并发问题的。

一、什么是分布式锁?

在分布式系统中,分布式锁是一种机制,用于协调多个节点上的并发访问共享资源。

这个共享资源可以是数据库、文件、缓存或任何需要互斥访问的数据或资源。分布式锁确保了在任何给定时刻只有一个节点能够对资源进行操作,从而保持了数据的一致性和可靠性。

二、为什么要使用分布式锁?

1. 数据一致性

在分布式环境中,多个节点同时访问共享资源可能导致数据不一致的问题。分布式锁可以防止这种情况发生,确保数据的一致性。

2. 防止竞争条件

多个节点并发访问共享资源时可能出现竞争条件,这会导致不可预测的结果。分布式锁可以有效地防止竞争条件,确保操作按照预期顺序执行

3. 限制资源的访问

有些资源可能需要限制同时访问的数量,以避免过载或资源浪费。分布式锁可以帮助控制资源的访问

三、分布式锁要解决的问题

分布式锁的核心问题是如何在多个节点之间协调,以确保只有一个节点可以获得锁,而其他节点必须等待。

 

这涉及到以下关键问题:

1. 互斥性

只有一个节点能够获得锁,其他节点必须等待。这确保了资源的互斥访问。

2. 可重入性

指的是在同一线程内,外层函数获得锁之后,内层递归函数仍然可以获取到该锁。

说白了就是同一个线程再次进入同样代码时,可以再次拿到该锁。它的作用是:防止在同一线程中多次获取锁产生竞性条件而导致死锁发生

3. 超时释放

确保即使节点在业务过程中发生故障,锁也会被超时释放,既能防止不必要的线程等待和资源浪费,也能避免死锁。

四、分布式锁的实现方式

在分布式系统中,有多种方式可以实现分布式锁,就像是锁的品种不同,每种锁都有自己的特点。

  • 有基于数据库的锁,就像是厨师们用餐具把菜肴锁在柜子里,每个人都得排队去取。

  • 还有基于 ZooKeeper 的锁,它像是整个餐厅的门卫,只允许一个人进去,其他人只能在门口等。

  • 最后,还有基于缓存的锁,就像是一位服务员用号码牌帮你占座,先到先得。

1. 基于数据库的分布式锁

使用数据库表中的一行记录作为锁,通过事务来获取和释放锁。

例如,使用 MySQL 来实现事务锁。首先创建一张简单表,在某一个字段上创建唯一索引(保证多个请求新增字段时,只有一个请求可成功)。

CREATE TABLE `user` (  `id` bigint(20) NOT NULL AUTO_INCREMENT,  `uname` varchar(255) DEFAULT NULL,  PRIMARY KEY (`id`),  UNIQUE KEY `name` (`uname`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4

当需要获取分布式锁时,执行以下语句:

INSERT INTO `user` (uname) VALUES ('unique_key')

由于 name 字段上加了唯一索引,所以当多个请求提交 insert 语句时,只有一个请求可成功。

使用 MySQL 实现分布式锁的优点是可靠性高,但性能较差,而且这把锁是非重入的,同一个线程在没有释放锁之前无法获得该锁

2. 基于ZooKeeper的分布式锁

Zookeeper(简称 zk)是一个为分布式应用提供一致性服务的中间组件,其内部是一个分层的文件系统目录树结构。

zk 规定其某一个目录下只能有唯一的一个文件名,其分布式锁的实现方式如下:

  1. 创建一个锁目录(ZNode):首先,在 zk 中创建一个专门用于存储锁的目录,通常称为锁根节点。这个目录将包含所有获取锁的请求以及用于锁协调的节点。

  2. 获取锁:当一个节点想要获取锁时,它会在锁目录下创建一个临时顺序节点(Ephemeral Sequential Node)。zk 会为每个节点分配一个唯一的序列号,并根据序列号的大小来确定锁的获取顺序。

  3. 查看是否获得锁:节点在创建临时顺序节点后,需要检查自己的节点是否是锁目录中序列号最小的节点。如果是,表示节点获得了锁;如果不是,则节点需要监听比它序列号小的节点的删除事件。

  4. 监听锁释放:如果一个节点没有获得锁,它会设置一个监听器来监视比它序列号小的节点的删除事件。一旦前一个节点(序列号小的节点)释放了锁,zk 会通知等待的节点。

  5. 释放锁:当一个节点完成了对共享资源的操作后,它会删除自己创建的临时节点,这将触发 zk 通知等待的节点。

zk 分布式锁提供了良好的一致性和可用性,但部署和维护较为复杂,需要仔细处理各种边界情况,例如节点的创建、删除、网络分区等。

而且 zk 实现分布式锁的性能不太好,主要是获取和释放锁都需要在集群的 Leader 节点上执行,同步较慢。

3. 基于缓存的分布式锁

使用分布式缓存,如 Redis 或 Memcached,来存储锁信息,缓存方式性能较高,但需要处理分布式缓存的高可用性和一致性。

接下来,我们详细讨论一下在 Redis 中如何设计一个高可用的分布式锁以及可能会遇到的几个问题,包括:

  1. 死锁问题

  2. 锁提前释放

  3. 锁被其它线程误删

  4. 高可用问题

1)死锁问题

早期版本的 redis 没有 setnx 命令在写 key 时直接设置超时参数,需要用 expire 命令单独对锁设置过期时间,这可能会导致死锁问题。

比如,设置锁的过期时间执行失败了,导致后来的抢锁都会失败。

Lua脚本或SETNX

为了保证原子性,我们可以使用 Lua 脚本,保证SETNX + EXPIRE两条指令的原子性,我们还可以巧用RedisSET 指令扩展参数:SET key value[EX seconds][PX milliseconds][NX|XX],它也是原子性的。

SET key value [EX seconds] [PX milliseconds] [NX|XX]

  • NX:表示 key 不存在的时候,才能 set 成功,即保证只有第一个客户端请求才能获得锁,而其他客户端请求只能等待锁释放后,才能获取

  • EX seconds :设定 key 的过期时间,默认单位时间为秒

  • PX milliseconds: 设定 key 的过期时间,默认单位时间为毫秒

  • XX: 仅当 key 存在时设置值

在 Go 语言里面,关键代码如下所示:

func getLock() {    methodName := "getLock"    val, err := client.Do("set", methodName, "lock_value", "nx", "ex", 100) if err != nil {        zaplog.Errorf("%s set redis lock failed, %s", methodName, err)return}    if val == nil { zaplog.Errorf("%s get redis lock failed", methodName)        return }... // 执行临界区代码,访问公共资源client.Del(lock.key()).Err() // 删除key,释放锁
}

2)锁提前释放

上述方案解决了加锁过期的原子性问题,不会产生死锁,但还是可能存在锁提前释放的问题。

如图所示,假设我们设置锁的过期时间为 5 秒,而业务执行需要 10 秒。

在线程 1 执行业务的过程中,它的锁被过期释放了,这时线程 2 是可以拿到锁的,也开始访问公共资源。

很明显,这种情况下导致了公共资源没有被严格串行访问,破坏了分布式锁的互斥性

这时,有爱动脑瓜子的小伙伴可能认为,既然加锁时间太短,那我们把锁的过期时间设置得长一些不就可以了吗?

其实不然,首先我们没法提前准确知道一个业务执行的具体时间。其次,公共资源的访问时间大概率是动态变化的,时间设置得过长也不好。

Redisson框架

所以,我们不妨给加锁线程一个自动续期的功能,即每隔一段时间检查锁是否还存在,如果存在就延长锁的时间,防止锁过期提前释放

这个功能需要用到守护线程,当前已经有开源框架帮我们解决了,它就是——Redisson,它的实现原理如图所示:

 当线程 1 加锁成功后,就会启动一个 Watch dog 看门狗,它是一个后台线程,每隔 1 秒(可配置)检查业务是否还持有锁,以达到线程未主动释放锁,自动续期的效果。

 

3)锁被其它线程误删

除了锁提前释放,我们可能还会遇到锁被其它线程误删的问题。

如图所示,加锁线程 1 执行完业务后,去释放锁。但线程 1 自己的锁已经释放了,此时分布式锁是由线程 2 持有的,就会误删线程 2 的锁,但线程 2 的业务可能还没执行完毕,导致异常产生。

唯一 Value 值

要想解决锁被误删的问题,我们需要给每个线程的锁加一个唯一标识。

比如,在加锁时将 Value 设置为线程对应服务器的 IP。对应的 Go 语言关键代码如下:

const (  // HostIP,当前服务器的IP  HostIP = getLocalIP()
)func getLock() {    methodName := "getLock"    val, err := client.Do("set", methodName, HostIP, "nx", "ex", 100) if err != nil {        zaplog.Errorf("%s redis error, %s", methodName, err)return}    if val == nil { zaplog.Errorf("%s get redis lock error", methodName)        return }... // 执行临界区代码,访问公共资源if client.Get(methodName) == HostIP {// 判断为当前服务器线程加的锁,才可以删除client.Del(lock.key()).Err()}
}

这样,在删除锁的时候判断一下 Value 是否为当前实例的 IP,就可以避免误删除其它线程锁的问题了。

为了保证严格的原子性,可以用 Lua 脚本代替以上代码,如下所示:

if redis.call('get',KEYS[1]) == ARGV[1] thenreturn redis.call('del',KEYS[1])
elsereturn 0
end;
4)Redlock高可用锁

前面几种方案都是基于单机版考虑,而实际业务中 Redis 一般都是集群部署的,所以我们接下来讨论一下 Redis 分布式锁的高可用问题。

试想一下,如果线程 1 在 Redis 的 master 主节点上拿到了锁,但是还没同步到 slave 从节点。

这时,如果主节点发生故障,从节点升级为主节点,其它线程就可以重新获取这个锁,此时可能有多个线程拿到同一个锁。即,分布式锁的互斥性遭到了破坏。

为了解决这个问题,Redis 的作者提出了专门支持分布式锁的算法:Redis Distributed Lock,简称 Redlock,其核心思想类似于注册中心的选举机制。

Redis 集群内部署多个 master 主节点,它们相互独立,即每个主节点之间不存在数据同步。

且节点数为单数个,每次当客户端抢锁时,需要从这几个 master 节点去申请锁,当从一半以上的节点上获取成功时,锁才算获取成功。

优缺点和常用实现方式

以上是业界常用的三种分布式锁实现方式,它们各自的优缺点如下:

  • 基于数据库的分布式锁:可靠性高,但性能较差,不适合高并发场景。

  • 基于ZooKeeper的分布式锁:提供良好的一致性和可用性,适合复杂的分布式场景,但部署和维护复杂,且性能比不上缓存的方式。

  • 基于缓存的分布式锁:性能较高,适合大部分场景,但需要处理缓存的高可用性。

其中,业界常用的分布式锁实现方式通常是基于缓存的方式,如使用 Redis 实现分布式锁。这是因为 Redis 性能优秀,而且可以满足大多数应用场景的需求。

小结

尽管分布式世界曲折离奇,但有了分布式锁,我们就像是看电影的观众,可以有条不紊地入场,分布式系统里的资源就像胶片一样,等待着我们一张一张地观赏。

这就是分布式的魅力!它或许令人又爱又恨,但正是科技世界的多样复杂性,才让我们的技术之旅变得更加精彩。

好了,本文的技术部分就到这里啦。

最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下,您的支持是我坚持写作最大的动力。

求一键三连:点赞、转发、在看。

我从清晨走过,也拥抱夜晚的星辰,人生没有捷径,你我皆平凡,你好,陌生人,一起共勉。

 

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

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

相关文章

项目零散记录

Ts托管 仅本项目禁用本地vscode内置的ts服务 提交代码前的检查 husky(哈士奇)工具(是一个git hooks工具) 1、安装 pnpm dlx husky-init && pnpm install安装的时候,出现如下报错 解决方案,需要先执行git init初始化…

Android10.0 人脸解锁流程分析

人脸解锁概述 人脸解锁即用户通过注视设备的正面方便地解锁手机或平板。Android 10 为支持人脸解锁的设备在人脸认证期间添加了一个新的可以安全处理相机帧、保持隐私与安全的人脸认证栈的支持,也为安全合规地启用集成交易的应用(网上银行或其他服务&am…

Java Web基础详解

回顾 之前的两篇的文章已经大概的带我们了解了tomcat的一些基本的操作,比如从零搭建我们自己的调试环境以及官方文档构建的方式,接下来的话,我将带大家来了解一下tomcat的一些基础知识,这些基础知识将以问题的方式抛出&#xff0…

【SpringCloud笔记】(11)消息驱动之Stream

Stream 技术背景 底层不同模块可能使用不同的消息中间件,这就导致技术的切换,微服务的维护及开发变得麻烦起来 概述 官网: https://spring.io/projects/spring-cloud-stream#overview https://cloud.spring.io/spring-cloud-static/spring…

最小覆盖子串(LeetCode 76)

文章目录 1.问题描述2.难度等级3.热门指数4.解题思路参考文献 1.问题描述 给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。 注意: 对于 t 中重复字符&#xff…

git 常用基本命令, reset 回退撤销commit,解决gitignore无效,忽略记录或未记录远程仓库的文件,删除远程仓库文件

git 基本命令 reset 撤销commit https://blog.csdn.net/a704397849/article/details/135220091 idea 中 rest 撤销commit过程如下: Git -> Rest Head… 在To Commit中的HEAD后面加上^,点击Reset即可撤回最近一次的尚未push的commit Reset Type 有三…

Flink Has Become the De-facto Standard of Streaming Compute

摘要:本文整理自 Apache Flink 中文社区发起人、阿里巴巴开源大数据平台负责人王峰(莫问),在 Flink Forward Asia 2023 主会场的分享。Flink 从 2014 年诞生之后,已经发展了将近 10 年,尤其是最近这些年得到…

爬虫系列----Python解析Json网页并保存到本地csv

Python解析JSON 1 知识小课堂1.1 爬虫1.2 JSON1.3 Python1.4 前言技术1.4.1 range1.4.2 random1.4.3 time.sleep1.4.4 with open() as f: 2 解析过程2.1 简介2.2 打开调试工具2.3 分析网址2.3.1 网址的规律2.3.2 网址的参数 2.4 爬取第一页内容2.5 存入字典并获取2.6 循环主体数…

7-2 设计一元二次方程求解类(高教社,《Python编程基础及应用》习题9-4)——python

设计一个类Root来计算ax2bxc0的根。该类包括:a、b、c共3个属性表示方程的3个系数,getDiscriminant()方法返回b2-4ac, getRoot1()和getRoot2()返回方程的两个根。 其中,getRoot1()返回的根对应: getRoot2()返回的根对应&#xff1a…

百度沧海文件存储CFS推出新一代Namespace架构

随着移动互联网、物联网、AI 计算等技术和市场的迅速发展,数据规模指数级膨胀,对于分布式文件系统作为大规模数据场景的存储底座提出了更高的要求。已有分布式文件系统解决方案存在着短板,只能适应有限的场景: >> 新型分布式…

格密码:傅里叶矩阵

目录 一. 铺垫性介绍 1.1 傅里叶级数 1.2 傅里叶矩阵的来源 二. 格基与傅里叶矩阵 2.1 傅里叶矩阵详细解释 2.2 格基与傅里叶矩阵 写在前面:有关傅里叶变换的解释太多了,这篇博客主要总结傅里叶矩阵在格密码中的运用。对于有一定傅里叶变换基础的同…

IntelliJ IDEA [设置] 隐藏 .idea 等 .XXX 文件夹

文章目录 1. 问题描述2. 解决办法3. 最后效果4. 特殊处理(正常不需要此步骤)总结 我们使用 IntelliJ IDEA 导入项目的时候,经常会看到一些 .XXX 的文件夹(例如:.idea,.mvn,.gradle 等&#xff0…

支付宝、学习强国小程序input、textarea数据双向绑定

前言 和 vue 的绑定有些区别,需要注意。直接 value"{{inputValue}}" 是无法双向绑定的。 正确思路 文档说的比较详细,不过没有组合使用的案例,需要自行理解。这里正确的方法是先用 value 绑定数据,再使用 onInput 事件…

鸿蒙的基本项目_tabbar,首页,购物车,我的

以上效果,由四个ets文件实现,分别是容器页面。首页,购物车,我的。 页面里的数据,我是用json-server进行模拟的数据。 一、容器页面 使用组件Tabs和Tabcontent结合。 import Home from "./Home"; import …

短剧付费变现小程序源码系统:开通会员+在线充值+风口项目,变现利器+完整的代码包 附带部署安装教程

在当今数字化时代,短剧付费变现小程序源码系统已经成为了一个热门的风口项目。它以开通会员、在线充值、完整的代码包等特色功能,成为了一种有效的变现利器,受到了广泛的关注和应用。本文将详细介绍这个源码系统的背景和特色功能,…

实现阿里云oss云存储,简单几步

一、前言 虽然平常学习用的不多&#xff0c;但是用的时候再去找官方文档&#xff0c;也很繁琐&#xff0c;不如直接整理以下&#xff0c;方便粘贴复制&#xff0c;本文介绍两种图片上传方式①普通上传②服务端签名直传 1.普通上传 加载maven依赖 <dependency><grou…

centos 安装oracle 11.2.04 并配置数据库自启动操作记录,一次完成

环境&#xff1a; centos版本7.3&#xff0c;安装的有图形化界面 Oracle11.2.04&#xff0c;之所以选择这个版本是因为网上有人说11其他版本的在安装的过程中会出现这样或那样的问题&#xff0c;下载地址放到文章下面 步骤&#xff0c;按顺序&#xff1a; 1、创建安装Oracle…

万用表测接地电阻方法

万用表测接地电阻方法 用万用表在不同土质的土壤对接地电阻进行了实验&#xff0c;并将万用表所测数据和专用接地电阻测试仪所测数据进行了比较&#xff0c;两者十分接近。具体测量方法如下&#xff1a; 找两根8mm、1m长的圆钢&#xff0c;将其一端磨尖作为辅助测试棒&#x…

Mysql之视图

Mysql之视图 常见的数据库对象视图概述为什么使用视图视图的理解创建视图创建单表视图别名的运用 创建多表联合视图利用视图对数据进行格式化contact 函数以视图为基&#xff0c;再创建新的视图 查看视图更新视图的数据一般情况不可更新的视图 修改和删除视图修改视图删除视图注…

【C#】Visual Studio 2022 远程调试配置教程

在某些特殊的情况下&#xff0c;开发机和调试机可能不是同一台设备&#xff0c;此时就需要远程调试了。 开发机配置 首先需要确保两台机器在同一局域网下。 创建共享文件夹 随便找个地方新建一个文件夹&#xff0c;用来放编译结果。例如我这里是 D:\DebuggingWorkspace\。 …