分布式锁详解

news2024/9/21 17:46:00

文章目录

  • 分布式锁是什么
  • 基于 Redis 实现分布式锁
  • 如何基于Redis实现一个简单的分布式锁
  • 为什么要给锁设置一个过期时间?
  • 如何实现锁的优雅续期?
  • 如何实现可重入锁?
  • Redis 如何解决集群情况下分布式锁的可靠性?

分布式锁是什么

java单机下,我们使用ReetrantLock类,synchronized 关键字这类JDK自带的本地锁来控制一个JVM进程中多个线程对共享资源的访问。

分布式系统下,不同的服务/客户端通常运行在独立的 JVM 进程上。如果多个 JVM 进程共享同一份资源的话,使用本地锁就没办法实现资源的互斥访问了。于是,分布式锁 就诞生了。

一个最基本的分布式锁需要满足:

  • 互斥: 任意一个时刻,锁只能被一个线程持有
  • 高可用: 即使客户端的释放锁的代码逻辑出现问题,锁最终一定还是会被释放,不会影响其他线程对共享资源的访问。
  • 可重入: 一个节点获取了锁后,还可以再次获取锁

通常情况下,我们一般会选择基于 Redis 或者 ZooKeeper 实现分布式锁,Redis 用的要更多一点,我这里也以 Redis 为例介绍分布式锁的实现。

基于 Redis 实现分布式锁

如何基于Redis实现一个简单的分布式锁

核心是实现互斥,再Redis中,SETNX可以帮助我们实现互斥。SETNX 即 SET if Not eXists (对应 Java 中的 setIfAbsent 方法),如果 key 不存在的话,才会设置 key 的值。如果 key 已经存在, SETNX 啥也不做。

SETNX lockKey uniqueValue
(integer) 1
SETNX lockKey uniqueValue
(integer) 0

释放锁的话,直接通过 DEL 命令删除对应的 key 即可。

DEL lockKey
(integer) 1

为了误删到其他的锁,这里我们建议使用 Lua 脚本通过 key 对应的 value(唯一值)来判断。

选用 Lua 脚本是为了保证解锁操作的原子性。因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,从而保证了锁释放操作的原子性。

if redis.call("get",KEYS[1])==ARGV[1] then
	return redis.call("del",KEYS[1])
else
	return 0
end

在这里插入图片描述
这是一种最简易的 Redis 分布式锁实现,实现方式比较简单,性能也很高效。不过,这种方式实现分布式锁存在一些问题。就比如应用程序遇到一些问题比如释放锁的逻辑突然挂掉,可能会导致锁无法被释放,进而造成共享资源无法再被其他线程/进程访问。

为什么要给锁设置一个过期时间?

为了避免锁无法被释放,我们可以想到的一个解决办法就是: 给这个 key(也就是锁) 设置一个过期时间

127.0.0.1:6379> SET lockKey uniqueValue EX 3 NX
OK
  • lockKey :加锁的锁名;
  • uniqueValue :能够唯一标示锁的随机字符串;
  • NX :只有当 lockKey 对应的 key 值不存在的时候才能 SET 成功;
  • EX :过期时间设置(秒为单位)EX 3 标示这个锁有一个 3 秒的自动过期时间。与 EX 对应的是 PX(毫秒为单位),这两个都是过期时间设置。

一定要保证设置指定 key 的值和过期时间是一个原子操作!!! 不然的话,依然可能会出现锁无法被释放的问题。

这样确实可以解决问题,不过,这种解决办法同样存在漏洞:如果操作共享资源的时间大于过期时间,就会出现锁提前过期的问题,进而导致分布式锁直接失效。如果锁的超时时间设置过长,又会影响到性能。

你或许在想: 如果操作共享资源的操作还未完成,锁过期时间能够自己续期就好了!

如何实现锁的优雅续期?

对于 Java 开发的小伙伴来说,已经有了现成的解决方案:Redisson

Redisson 是一个开源的 Java 语言 Redis 客户端,提供了很多开箱即用的功能,不仅仅包括多种分布式锁的实现。并且,Redisson 还支持 Redis 单机、Redis Sentinel 、Redis Cluster 等多种部署架构。

Redisson 中的分布式锁自带自动续期机制,使用起来非常简单,原理也比较简单,其提供了一个专门用来监控和续期锁的 Watch Dog( 看门狗),如果操作共享资源的线程还未执行完成的话,Watch Dog 会不断地延长锁的过期时间,进而保证锁不会因为超时而被释放。

在这里插入图片描述
看门狗名字的由来于 getLockWatchdogTimeout() 方法,这个方法返回的是看门狗给锁续期的过期时间,默认为 30 秒(redisson-3.17.6open in new window)。

//默认 30秒,支持修改
private long lockWatchdogTimeout = 30 * 1000;

public Config setLockWatchdogTimeout(long lockWatchdogTimeout) {
    this.lockWatchdogTimeout = lockWatchdogTimeout;
    return this;
}
public long getLockWatchdogTimeout() {
  	return lockWatchdogTimeout;
}

renewExpiration() 方法包含了看门狗的主要逻辑:

private void renewExpiration() {
         //......
        Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
            @Override
            public void run(Timeout timeout) throws Exception {
                //......
                // 异步续期,基于 Lua 脚本
                CompletionStage<Boolean> future = renewExpirationAsync(threadId);
                future.whenComplete((res, e) -> {
                    if (e != null) {
                        // 无法续期
                        log.error("Can't update lock " + getRawName() + " expiration", e);
                        EXPIRATION_RENEWAL_MAP.remove(getEntryName());
                        return;
                    }

                    if (res) {
                        // 递归调用实现续期
                        renewExpiration();
                    } else {
                        // 取消续期
                        cancelExpirationRenewal(null);
                    }
                });
            }
         // 延迟 internalLockLeaseTime/3(默认 10s,也就是 30/3) 再调用
        }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);

        ee.setTimeout(task);
    }

默认情况下,每过 10 秒,看门狗就会执行续期操作,将锁的超时时间设置为 30 秒。看门狗续期前也会先判断是否需要执行续期操作,需要才会执行续期,否则取消续期操作。

Watch Dog 通过调用 renewExpirationAsync() 方法实现锁的异步续期:

protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {
    return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            // 判断是否为持锁线程,如果是就执行续期操作,就锁的过期时间设置为 30s(默认)
            "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                    "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                    "return 1; " +
                    "end; " +
                    "return 0;",
            Collections.singletonList(getRawName()),
            internalLockLeaseTime, getLockName(threadId));
}

可以看出, renewExpirationAsync 方法其实是调用 Lua 脚本实现的续期,这样做主要是为了保证续期操作的原子性。

我这里以 Redisson 的分布式可重入锁 RLock 为例来说明如何使用 Redisson 实现分布式锁:

// 1.获取指定的分布式锁对象
RLock lock = redisson.getLock("lock");
// 2.拿锁且不设置锁超时时间,具备 Watch Dog 自动续期机制
lock.lock();
// 3.执行业务
...
// 4.释放锁
lock.unlock();

只有未指定锁超时时间,才会使用到 Watch Dog 自动续期机制。

// 手动给锁设置过期时间,不具备 Watch Dog 自动续期机制
lock.lock(10, TimeUnit.SECONDS);

如果使用 Redis 来实现分布式锁的话,还是比较推荐直接基于 Redisson 来做的

如何实现可重入锁?

所谓可重入锁指的是在一个线程中可以多次获取同一把锁,比如一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法即可重入 ,而无需重新获得锁。像 Java 中的 synchronizedReentrantLock 都属于可重入锁。

不可重入的分布式锁基本可以满足绝大部分业务场景了,一些特殊的场景可能会需要使用可重入的分布式锁。

可重入分布式锁的实现核心思路是线程在获取锁的时候判断是否为自己的锁,如果是的话,就不用再重新获取了。为此,我们可以为每个锁关联一个可重入计数器和一个占有它的线程。当可重入计数器大于 0 时,则锁被占有,需要判断占有该锁的线程和请求获取锁的线程是否为同一个。

实际项目中,我们不需要自己手动实现,推荐使用我们上面提到的 Redisson ,其内置了多种类型的锁比如可重入锁(Reentrant Lock)、自旋锁(Spin Lock)、公平锁(Fair Lock)、多重锁(MultiLock)、 红锁(RedLock)、 读写锁(ReadWriteLock)。

Redis 如何解决集群情况下分布式锁的可靠性?

为了避免单点故障,生产环境下的 Redis 服务通常是集群化部署的。

Redis 集群下,上面介绍到的分布式锁的实现会存在一些问题。由于 Redis 集群数据同步到各个节点时是异步的,如果在 Redis 主节点获取到锁后,在没有同步到其他节点时,Redis 主节点宕机了,此时新的 Redis 主节点依然可以获取锁,所以多个应用服务就可以同时获取到锁。

在这里插入图片描述
针对这个问题,Redis 之父 antirez 设计了 Redlock 算法 来解决。

Redlock 算法的思想是让客户端向 Redis 集群中的多个独立的 Redis 实例依次请求申请加锁,如果客户端能够和半数以上的实例成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败。

Redlock 实现比较复杂,性能比较差,发生时钟变迁的情况下还存在安全性隐患 实际项目中不建议使用 Redlock 算法,成本和收益不成正比。

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

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

相关文章

从客户端的角度来看移动端IM即时通讯的消息可靠性和送达机制

如何确保IM 不丢消息是个相对复杂的话题&#xff0c;从客户端发送数据到服务器&#xff0c;再从服务器抵达目标客户端&#xff0c;最终在 UI 成功展示&#xff0c;其间涉及的环节很多&#xff0c;这里只取其中一环「接收端如何确保消息不丢失」来探讨&#xff0c;粗略聊下我接触…

crmeb商城部署(踩坑及解决方法)

源码地址&#xff1a; https://gitee.com/ZhongBangKeJi/CRMEB 原版是PHP版&#xff0c;我也不懂PHP&#xff0c;但看到功能很全&#xff0c;而且有docker-compose的脚本可以部署&#xff0c;并且教程也很完善&#xff0c;就拿来部署试下。 所以也适用于和我一样&#xff0c;…

【NodeJs】NodeJs专题之理解企业BFF框架原理

BFF—服务于前端的后端 一、什么是BFF框架 BFF框架指的是一种逻辑分成,而非一种新技术即 Backend For Frontend&#xff08;服务于前端的后端&#xff09;&#xff0c;也就是服务器设计 API 时会考虑前端的使用&#xff0c;并在服务端直接进行业务逻辑的处理&#xff0c;又称…

每日记录自己的Android项目(一)

Jetpack之Navigation今天新创建一个项目&#xff0c;选的是这个。首先映入眼帘的是一个这样的界面。由ViewBinding绑定好XML布局和根布局和标题栏。这个应该是Navigationg里面的内容还有个字段private AppBarConfiguration appBarConfiguration;Overrideprotected void onCreat…

C# 序列化时“检测到循环引用”错误的彻底解决方案

目录 一&#xff0c;问题表现 二、没有技术含量的解决方案 三、本人彻底的解决方案 简要说明 贴代码 思路解析 思路 一&#xff0c;问题表现 示例代码如下&#xff1a; [Serializable] public class NodeTest {public NodeTest (){new List<NodeTest> ();}p…

SpringBoot之@ConfigurationProperties、@EnableConfigurationProperties

ConfigurationProperties 这个注解不仅可以为yml某个类注入还可以为第三方bean绑定属性 为yml某个类注入 只要将对应的yml类对象声明实体pojo并交给spring容器管理&#xff0c;再在类上使用ConfigurationProperties绑定对应的类名即可 涉及到两个知识点&#xff0c;这个类对…

C语言中的数据储存规则

写在开头 关于复习的相关内容其实从一开始就列出了大纲&#xff0c;但是迟迟没有开始复习&#xff0c;一方面是因为学校学业却是繁忙&#xff0c;另一方面还是内心对旧知识掌握不熟练需要再学一遍的畏惧和懒惰&#xff0c;但如今&#xff0c;复习必须开始了。今天我从C语言的最…

Linux/MacOS 生成双击可执行文件

双击可执行文件包含两种&#xff1a;终端shell脚本 Unix可执行文件 1.终端shell脚本 随意新建一个文件&#xff08;可使用command键N&#xff0c;前提是有已打开的文件&#xff09;&#xff0c;输入shell格式的测试代码&#xff0c;比如&#xff1a; #! /bin/sh echo “h…

双喜临门|炼石荣获2023年中国网络和数据安全高峰论坛双奖项

2023年2月23日-24日&#xff0c;工业和信息化部、四川省人民政府联合主办以“新征程 新思路 高质量发展”为主题的“2023年中国网络和数据安全产业高峰论坛”在成都隆重召开。工信安全中心第一届网络安全高成长性企业“勇攀之星”正式揭晓&#xff0c;炼石以高成长性、高创新性…

【vue】iframe相关问题

一、刷新iframe页面iframe的地址没有改变的话&#xff0c;每打开一次iframe页面&#xff0c;都不会主动更新页面的。以下有几种方法&#xff0c;都可以实现&#xff0c;每打开一次页面&#xff0c;就刷新一下给iframe添加key<template><div id"Iframe">&…

一名IC验证工程师的成长路径是怎么样的?来听听工程师的见解

IC验证这个岗位对于非科班的学生是比较友好的&#xff0c;因为验证需要具备的技能UVM&#xff0c;SV&#xff0c;C等&#xff0c;非科班和科班的差距不会拉开太大。因其岗位需求量巨大而格外受到了大家的青睐&#xff0c;甚至成为不少学生的转行首选。 验证对于IC的重要性 IC…

汽车 12V 和 24V 电池输入保护推荐

简介汽车电池电源线路在运行系统时容易出现瞬变。所需的典型保护包括过压、过载、反极性和跨接启动。在汽车 的生命周期中&#xff0c;交流发电机可能会被更换为非OEM 部件。售后市场上的交流发电机可能具有不同的负载突降&#xff08;LOAD DUMP&#xff09;保护或没有负载突降…

论文阅读-Attention Bottlenecks for Multimodal Fusion(多模态特征融合)

一、论文信息 论文题目&#xff1a;Attention Bottlenecks for Multimodal Fusion paperwithcode&#xff1a;https://paperswithcode.com/paper/attention-bottlenecks-for-multimodal-fusion Github&#xff1a;https://github.com/google-research/scenic/tree/main/scen…

产品故事:语雀两度生死局

语雀是一款文档和知识库产品&#xff0c;2016 年从一个技术团队支付宝体验技术部生长出来&#xff0c;2021 年蚂蚁成立了智能协同事业部&#xff0c;重点产品即为语雀&#xff0c;以独立 BU 运作&#xff0c;算是完成了“成人礼”。我们和玉伯聊了聊语雀的成长故事。 极客时间&…

C++基础知识【3】控制语句

目录 前言 一、条件语句 1.1、if 语句 1.2、if-else 语句 1.3、switch 语句 二、循环语句 2.1、while 循环 2.2、do-while 循环 2.3、for 循环 三、跳转语句 3.1、break语句 3.2、continue语句 3.3、goto语句 四、一些新特性 4.1、if 语句和 switch 语句…

【数据结构与算法】图遍历算法 ( 深度优先搜索代码示例 )

文章目录一、深度优先搜索算法二、完整代码示例完整代码示例执行结果一、深度优先搜索算法 深度优先搜索算法步骤 : 将 深度优先搜索 算法步骤 转为代码 ; ① 访问初始结点 : 访问 初始结点 v , 并将该 初始结点 v 标记为 " 已访问 " ; 设置一个 访问标记 数组 , 数…

《C++ Primer》 第九章 顺序容器

《C Primer》 第九章 顺序容器 9.1 顺序容器概述 容器&#xff1a;特定类型对象的集合 顺序容器类型 vector 可变大小数组&#xff0c;支持快速随机访问&#xff0c;在尾部之外的位置插入或删除元素可能很慢deque 双端队列。支持快速随机访问。在头尾位置插入/删除速度很快…

【2022-09-14】米哈游秋招笔试三道编程题

第一题&#xff1a;最短子串 题目描述 米小游拿到了一个字符串&#xff0c;她想截取一个连续子串&#xff0c;使得该子串中包含至少k个连续的“mihoyo”。 你可以帮米小游求出最短的子串长度&#xff0c;以及对应的子串位置吗&#xff1f; 输入描述 第一行输入两个正整数n…

产品父子流程技术方案设计

产品父子流程技术方案设计 一、整体设计 根据业务需求分析&#xff0c;产品涉及法人代表及实控人风控决策流程调用&#xff0c;旨在降低风险&#xff0c;提高行内线上贷款业务风险决策的能力。 二、业务流程 1.业务流程图 2.交易流程 在授信交易切面入口处对法人代表及实控…

Spark性能优化三 checkpoint

&#xff08;一&#xff09;checkpoint介绍 checkpoint&#xff0c;是Spark提供的一个比较高级的功能。有时候&#xff0c;我们的Spark任务&#xff0c;比较复杂&#xff0c;从初始化RDD开始&#xff0c;到最后整个任务完成&#xff0c;有比较多的步骤&#xff0c;比如超过10个…