Java开发大厂面试第20讲:什么是分布式锁?Redi 怎样实现的分布式锁?

news2024/11/23 17:56:27

“锁”是我们实际工作和面试中无法避开的话题之一,正确使用锁可以保证高并发环境下程序的正确执行,也就是说只有使用锁才能保证多人同时访问时程序不会出现问题。

我们本课时的面试题是,什么是分布式锁?如何实现分布式锁?

分布式锁是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调不同系统或同一系统的不同主机之间的动作。如果多个系统或主机共享了一个或一组资源,那么在访问这些资源时,就需要使用分布式锁来防止彼此干扰,保证数据的一致性。

分布式锁的实现方式有多种,以下是三种常见的实现方式:

  1. 基于数据库实现分布式锁:
  • 使用悲观锁,通过select ... where ... for update语句实现排他锁。但需要注意的是,where语句中的字段必须走索引,否则可能会导致锁表问题。
  • 在获取锁的时候设置一个获取的超时时间,若超过这个时间则放弃获取锁。
  1. 基于缓存(如Redis)实现分布式锁:
  • Redis提供了许多原生命令,如setnxexpire等,可以实现分布式锁。但这种方式需要确保Redis的高可用性,否则可能会出现锁丢失的情况。
  • 还可以使用Redis的set命令结合NXPX选项来实现分布式锁,这样可以防止缓存突然失效或过期,避免高并发直接访问数据库。
  1. 基于ZooKeeper实现分布式锁:
  • ZooKeeper是一个分布式协调服务,它提供了许多分布式相关的功能,包括分布式锁。
  • 使用ZooKeeper实现分布式锁可以通过创建一个临时顺序节点来实现,然后监听前一个节点的删除事件。当自己的前一个节点被删除时,就可以获取到锁。

从性能角度来看,基于缓存的方式通常性能最好,其次是基于ZooKeeper的方式,最后是基于数据库的方式。但具体选择哪种方式还需要根据具体的业务场景和需求来决定。

典型回答

第 06 课时讲了单机锁的一些知识,包括悲观锁、乐观锁、可重入锁、共享锁和独占锁等内容,但它们都属于单机锁也就是程序级别的锁,如果在分布式环境下使用就会出现锁不生效的问题,因此我们需要使用分布式锁来解决这个问题。

分布式锁是控制分布式系统之间同步访问共享资源的一种方式。是为了解决分布式系统中,不同的系统或是同一个系统的不同主机共享同一个资源的问题,它通常会采用互斥来保证程序的一致性,这就是分布式锁的用途以及执行原理。

分布式锁示意图,如下图所示:

image.png

分布式锁的常见实现方式有四种:

  • 基于 MySQL 的悲观锁来实现分布式锁,这种方式使用的最少,因为这种实现方式的性能不好,且容易造成死锁;
  • 基于 Memcached 实现分布式锁,可使用 add 方法来实现,如果添加成功了则表示分布式锁创建成功;
  • 基于 Redis 实现分布式锁,这也是本课时要介绍的重点,可以使用 setnx 方法来实现;
  • 基于 ZooKeeper 实现分布式锁,利用 ZooKeeper 顺序临时节点来实现。

由于 MySQL 的执行效率问题和死锁问题,所以这种实现方式会被我们先排除掉,而 Memcached 和 Redis 的实现方式比较类似,但因为 Redis 技术比较普及,所以会优先使用 Redis 来实现分布式锁,而 ZooKeeper 确实可以很好的实现分布式锁。但此技术在中小型公司的普及率不高,尤其是非 Java 技术栈的公司使用的较少,如果只是为了实现分布式锁而重新搭建一套 ZooKeeper 集群,显然实现成本和维护成本太高,所以综合以上因素,我们本文会采用 Redis 来实现分布式锁。

之所以可以使用以上四种方式来实现分布式锁,是因为以上四种方式都属于程序调用的“外部系统”,而分布式的程序是需要共享“外部系统”的,这就是分布式锁得以实现的基本前提

考点分析

分布式锁的问题看似简单,但却有很多细节需要注意,比如,需要考虑分布式锁的超时问题,如果不设置超时时间的话,可能会导致死锁的产生,所以在对待这个“锁”的问题上,一定不能马虎。和此知识点相关的面试还有以下这些:

  • 单机锁有哪些?它为什么不能在分布式环境下使用?
  • Redis 是如何实现分布式锁的?可能会遇到什么问题?
  • 分布式锁超时的话会有什么问题?如何解决?

知识扩展

单机锁

程序中使用的锁叫单机锁,我们日常中所说的“锁”都泛指单机锁,其分类有很多,大体可分为以下几类:

  • 悲观锁,是数据对外界的修改采取保守策略,它认为线程很容易把数据修改掉,因此在整个数据被修改的过程中都会采取锁定状态,直到一个线程使用完,其他线程才可以继续使用,典型应用是 synchronized;
  • 乐观锁,和悲观锁的概念恰好相反,乐观锁认为一般情况下数据在修改时不会出现冲突,所以在数据访问之前不会加锁,只是在数据提交更改时,才会对数据进行检测,典型应用是 ReadWriteLock 读写锁;
  • 可重入锁,也叫递归锁,指的是同一个线程在外面的函数获取了锁之后,那么内层的函数也可以继续获得此锁,在 Java 语言中 ReentrantLock 和 synchronized 都是可重入锁;
  • 独占锁和共享锁,只能被单线程持有的锁叫做独占锁,可以被多线程持有的锁叫共享锁,独占锁指的是在任何时候最多只能有一个线程持有该锁,比如 ReentrantLock 就是独占锁;而 ReadWriteLock 读写锁允许同一时间内有多个线程进行读操作,它就属于共享锁。

单机锁之所以不能应用在分布式系统中是因为,在分布式系统中,每次请求可能会被分配在不同的服务器上,而单机锁是在单台服务器上生效的。如果是多台服务器就会导致请求分发到不同的服务器,从而导致锁代码不能生效,因此会造成很多异常的问题,那么单机锁就不能应用在分布式系统中了。

使用 Redis 实现分布式锁

使用 Redis 实现分布式锁主要需要使用 setnx 方法,也就是 set if not exists(不存在则创建),具体的实现代码如下:

127.0.0.1:6379> setnx lock true
(integer) 1 #创建锁成功
#逻辑业务处理...
127.0.0.1:6379> del lock
(integer) 1 #释放锁

当执行 setnx 命令之后返回值为 1 的话,则表示创建锁成功,否则就是失败。释放锁使用 del 删除即可,当其他程序 setnx 失败时,则表示此锁正在使用中,这样就可以实现简单的分布式锁了。

但是以上代码有一个问题,就是没有设置锁的超时时间,因此如果出现异常情况,会导致锁未被释放,而其他线程又在排队等待此锁就会导致程序不可用。

有人可能会想到使用 expire 来设置键值的过期时间来解决这个问题,例如以下代码:

127.0.0.1:6379> setnx lock true
(integer) 1 #创建锁成功
127.0.0.1:6379> expire lock 30 #设置锁的(过期)超时时间为 30s
(integer) 1 
#逻辑业务处理...
127.0.0.1:6379> del lock
(integer) 1 #释放锁

但这样执行仍然会有问题,因为 setnx lock true 和 expire lock 30 命令是非原子的,也就是一个执行完另一个才能执行。但如果在 setnx 命令执行完之后,发生了异常情况,那么就会导致 expire 命令不会执行,因此依然没有解决死锁的问题。

这个问题在 Redis 2.6.12 之前一直没有得到有效的处理,当时的解决方案是在客户端进行原子合并操作,于是就诞生了很多客户端类库来解决此原子问题,不过这样就增加了使用的成本。因为你不但要添加 Redis 的客户端,还要为了解决锁的超时问题,需额外的增加新的类库,这样就增加了使用成本,但这个问题在 Redis 2.6.12 版本中得到了有效的处理。

在 Redis 2.6.12 中我们可以使用一条 set 命令来执行键值存储,并且可以判断键是否存在以及设置超时时间了,如下代码所示:

127.0.0.1:6379> set lock true ex 30 nx
OK #创建锁成功

其中,ex 是用来设置超时时间的,而 nx 是 not exists 的意思,用来判断键是否存在。如果返回的结果为“OK”则表示创建锁成功,否则表示此锁有人在使用。

锁超时

从上面的内容可以看出,使用 set 命令之后好像一切问题都解决了,但在这里我要告诉你,其实并没有。例如,我们给锁设置了超时时间为 10s,但程序的执行需要使用 15s,那么在第 10s 时此锁因为超时就会被释放,这时候线程二在执行 set 命令时正常获取到了锁,于是在很短的时间内 2s 之后删除了此锁,这就造成了锁被误删的情况,如下图所示:

image (1).png

锁被误删的解决方案是在使用 set 命令创建锁时,给 value 值设置一个归属标识。例如,在 value 中插入一个 UUID,每次在删除之前先要判断 UUID 是不是属于当前的线程,如果属于再删除,这样就避免了锁被误删的问题。

注意:在锁的归属判断和删除的过程中,不能先判断锁再删除锁,如下代码所示:

if(uuid.equals(uuid)){ // 判断是否是自己的锁
	del(luck); // 删除锁
}

应该把判断和删除放到一个原子单元中去执行,因此需要借助 Lua 脚本来执行,在 Redis 中执行 Lua 脚本可以保证这批命令的原子性,它的实现代码如下:

/**
 * 释放分布式锁
 * @param jedis Redis客户端
 * @param lockKey 锁的 key
 * @param flagId 锁归属标识
 * @return 是否释放成功
 */
public static boolean unLock(Jedis jedis, String lockKey, String flagId) {
    String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(flagId));
    if ("1L".equals(result)) { // 判断执行结果
        return true;
    }
    return false;
}

其中,Collections.singletonList() 方法是将 String 转成 List,因为 jedis.eval() 最后两个参数要求必须是 List 类型。

锁超时可以通过两种方案来解决:

  • 把执行耗时的方法从锁中剔除,减少锁中代码的执行时间,保证锁在超时之前,代码一定可以执行完;
  • 把锁的超时时间设置的长一些,正常情况下我们在使用完锁之后,会调用删除的方法手动删除锁,因此可以把超时时间设置的稍微长一些。

最后

今天我们分享了分布式锁的四种实现方式,即 MySQL、Memcached、Redis 和 ZooKeeper,因为 Redis 的普及率比较高,因此对于很多公司来说使用 Redis 实现分布式锁是最优的选择。我们还讲了使用 Redis 实现分布式锁的具体步骤以及实现代码,还讲了在实现过程中可能会遇到的一些问题以及解决方案。


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

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

相关文章

秋招突击——算法打卡——5/24——无重复字符的最长字串

题目描述 实现代码 // 无重复字符的最长子串 int lengthOfLongestSubstring(string s) {int l 0,r 0;int res 0;unordered_map<char,int> temp;while(l < s.size()){temp[s.at(l)] l;for (r l 1; r < s.size() ; r) {if(temp.count(s.at(r))) break;else te…

Python UDP编程简单实例

TCP是建立可靠的连接&#xff0c;并且通信双方都可以以流的形式发送数据。 相对于TCP&#xff0c;UDP则是面向无连接的协议&#xff0c;不需要建立连接&#xff0c;只需要知道对方IP地址和端口号&#xff0c;就可以直接发送数据包。但是只管发送不保证到达。 虽然UDP传输数据…

详解 UML 中的关系概念

关联&#xff08;Association&#xff09; 表示两个类之间的一种语义性联系。例如: 学生与班级之间的关联关系。 有向关联&#xff08;Directed Association&#xff09; 关联关系有方向性,表示一个类能访问另一个类,但不一定反过来。例如: 教师能查看学生的成绩,但学生不能查…

三能一体运营体系助力政企支撑水平提升

生产力的发展是现代社会孜孜不倦的追求&#xff0c;由此产生了我们熟悉的“机械化、电子化、信息化”乃至现今正在发生的“智能化”四次工业革命。这些是由技术的突破性发展带来的&#xff0c;但我们也注意到生产力发展的另一个助力&#xff0c;即生产效率的提升&#xff0c;19…

PE文件(六)新增节-添加代码作业

一.手动新增节添加代码 1.当预备条件都满足&#xff0c;节表结尾没有相关数据时&#xff1a; 现在我们将ipmsg.exe用winhex打开&#xff0c;在节的最后新增一个节用于存放我们要增加的数据 注意&#xff1a;飞鸽的文件对齐和内存对齐是一致的 先判断节表末尾到第一个节之间…

系统架构师-考试-基础题-错题集锦1

系统架构师-考试-基础题-错题集锦 1.当一台服务器出现故障时将业务迁移到另外一台物理服务器上&#xff0c;保障了业务的连续性。 2.面向对象&#xff1a; 实体类&#xff0c;边界类&#xff0c;控制类 3.RUP&#xff1a;UP&#xff0c;统一过程&#xff0c;以架构为中心&am…

养鸡游戏牧场游戏已对接广告联盟功能介绍

养鸡游戏牧场游戏在对接广告联盟后&#xff0c;主要实现了以下几个功能&#xff1a; 广告展示与收益&#xff1a; 游戏内会嵌入广告联盟的广告&#xff0c;这些广告可能会以横幅、插屏、视频等多种形式出现在游戏的各个界面&#xff0c;如主界面、场景切换、任务完成等时机。广…

【深度学习02】注意力机制

1.自注意力机制 自注意力机制&#xff08;Self-Attention Mechanism&#xff09;是深度学习中的一种方法&#xff0c;广泛应用于自然语言处理和其他领域。为了更好地理解它&#xff0c;可以用一个简单的类比来解释。 类比&#xff1a;学生在课堂上做笔记 假设你是一个学生&a…

合约开发的基本结构剖析及前置知识梳理

前置知识点 上下文变量初步 合约函数的背后是transaction&#xff0c;上下文变量访问的是transaction中的信息两个上下文变量&#xff1a;tx和msg ERC20 规范代码实现Metamask测试 ganache-cli的安装 安装 npm install -g ganache-cli启动 ganache-cli如果出现以下这种…

ZooKeeper系列之ZAB协议

概述 ZooKeeper Atomic Broadcast&#xff0c;ZooKeeper原子消息广播协议。ZAB协议是为分布式协调服务ZK专门设计的一种支持崩溃恢复的原子广播协议。ZK主要依赖ZAB协议来实现分布式数据的最终一致性&#xff0c;基于该协议&#xff0c;ZK实现一种主备模式的系统架构来保持集群…

【EI会议】2024年雷达、电子与通信工程国际会议(ICREC 2024)

2024年雷达、电子与通信工程国际会议 2024 International Conference on Radar, Electronics and Communication Engineering 【1】会议简介 2024年雷达、电子与通信工程国际会议即将在深圳隆重召开。深圳&#xff0c;这座充满活力的现代化都市&#xff0c;以其卓越的科技创新…

后端之路第二站(正片)——SprintBoot之:设置请求接口

这一篇讲怎么简单结合模拟云接口&#xff0c;尝试简单的后端接接口、接受并传数据 一、下载Apifox接口文档软件 目前的企业都是采用前后端分离开发的&#xff0c;在开发阶段前后端需要统一发送请求的接口&#xff0c;前端也需要在等待后端把数据存到数据库之前&#xff0c;自己…

微信H5跳小程序 wx-open-launch-weapp ios显示且正常跳转,安卓不显示不报错解决方案

前提&#xff1a;在一切都正常(无报错&#xff0c;没有写法错误等)的情况下&#xff0c;出现这个问题: 去你的h5项目&#xff0c;用浏览器打开&#xff0c;在network随便找一个静态文件&#xff0c;在response响应标头中找找&#xff0c;是否有Content-Security-Policy这个头&…

vue2流星雨(可调角度)

新建StarBackground.vue组件 打开组件注释部分可以随机颜色 <template><div class"rain"><divv-for"(item,index) in rainNumber":key"index"class"rain-item"ref"rain-item":style"transform:rotate(…

【MySQL进阶之路 | 基础篇】触发器

1. 为什么要使用触发器 我们可能会遇到如下场景.我们有两个相互关联的表&#xff0c;如商品信息表与库存信息表.当我们向商品信息表添加一条记录时&#xff0c;为了保证数据完整性&#xff0c;也必须向库存信息表添加一条数据.我们就必须把这两个关联的操作写在程序里&#xf…

【APKtool】APKtool实现某瓣APP重签名

APP name 重打包 重打包完成 开始签名 apktool签名 使用 APKtool 或其他工具生成的签名文件与原始签名文件的区别主要在于它们使用的密钥和证书可能不同。当你使用 APKtool 对 APK 文件进行反编译、修改后再重新打包时&#xff0c;你通常需要使用一个新的密钥和证书对修改后…

机器人非线性控制方法——线性化与解耦

机器人非线性控制方法是针对具有非线性特性的机器人系统所设计的一系列控制策略。其中&#xff0c;精确线性化控制和反演控制是两种重要的方法。 1. 非线性反馈控制 该控制律采用非线性反馈控制的方法&#xff0c;将控制输入 u 分解为两个部分&#xff1a; α(x): 这是一个与…

计算机毕业设计 | springboot养老院管理系统 老人社区管理(附源码)

1&#xff0c;绪论 1.1 背景调研 养老院是集医疗、护理、康复、膳食、社工等服务服务于一体的综合行养老院&#xff0c;经过我们前期的调查&#xff0c;院方大部分工作采用手工操作方式,会带来工作效率过低&#xff0c;运营成本过大的问题。 院方可用合理的较少投入取得更好…

HTML5 + CSS3模拟庆余年中“五竹”的镭射眼动画特效

庆余年2已经火热开播了&#xff0c;据说反响强烈啊&#xff0c;不知道这一部里面&#xff0c;五竹的镭射眼会不会表现出来&#xff0c;我还挺想看看他的镭射眼的&#xff0c;我看到底有没有杀死剧中的庆帝。 回想第一部&#xff0c;我都快记不清那是几年前开播的了&#xff0c;…

Ubuntu 安装 LibreOffice

1. 删除预安装的LibreOffice Ubuntu 和其他的 Linux 发行版带有预安装的 LibreOffice。这可能不是最新的&#xff0c;这是因为发行版有特定的发行周期。在进行新安装之前&#xff0c;你可以通过以下命令删除 Ubuntu 及其衍生发行版中的的旧版本。 sudo apt remove –purge li…