如何基于Redis实现分布式锁?

分布式锁介绍

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

下面是我对本地锁画的一张示意图。

图片

本地锁

从图中可以看出,这些线程访问共享资源是互斥的,同一时刻只有一个线程可以获取到本地锁访问共享资源。

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

举个例子:系统的订单服务一共部署了 3 份,都对外提供服务。用户下订单之前需要检查库存,为了防止超卖,这里需要加锁以实现对检查库存操作的同步访问。由于订单服务位于不同的 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 脚本时,可以以原子性的方式执行,从而保证了锁释放操作的原子性。

// 释放锁时,先比较锁对应的 value 值是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

图片

Redis 实现简易分布式锁

这是一种最简易的 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[1]** 。其他语言的解决方案,可以在 Redis 官方文档中找到,地址:https://redis.io/topics/distlock 。

图片

Distributed locks with Redis

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

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

图片

Redisson 看门狗自动续期

看门狗名字的由来于 getLockWatchdogTimeout() 方法,这个方法返回的是看门狗给锁续期的过期时间,默认为 30 秒(redisson-3.17.6[2])。

//默认 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 中的 synchronized 和 ReentrantLock 都属于可重入锁。

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

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

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

图片

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

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

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

图片

针对这个问题,Redis 之父 antirez 设计了 Redlock 算法[3] 来解决。

图片

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

即使部分 Redis 节点出现问题,只要保证 Redis 集群中有半数以上的 Redis 节点可用,分布式锁服务就是正常的。

Redlock 是直接操作 Redis 节点的,并不是通过 Redis 集群操作的,这样才可以避免 Redis 集群主从切换导致的锁丢失问题。

Redlock 实现比较复杂,性能比较差,发生时钟变迁的情况下还存在安全性隐患。《数据密集型应用系统设计》一书的作者 Martin Kleppmann 曾经专门发文(How to do distributed locking - Martin Kleppmann - 2016[4])怼过 Redlock,他认为这是一个很差的分布式锁实现。感兴趣的朋友可以看看Redis 锁从面试连环炮聊到神仙打架这篇文章,有详细介绍到 antirez 和 Martin Kleppmann 关于 Redlock 的激烈辩论。

实际项目中不建议使用 Redlock 算法,成本和收益不成正比。

如果不是非要实现绝对可靠的分布式锁的话,其实单机版 Redis 就完全够了,实现简单,性能也非常高。如果你必须要实现一个绝对可靠的分布式锁的话,可以基于 ZooKeeper 来做,只是性能会差一些。

站在巨人的肩膀上:支付宝一面:如何基于Redis实现分布式锁?

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

NetSuite Non-Inventory Item 公司内外采购总账影响

上篇文章提到&#xff0c;Non-Inventory Item的科目维护会根据各个企业的实际情况而有所不同&#xff0c;通常情况下都涉及外部交易&#xff0c;即对外采购与销售&#xff1b;另外也涉及到公司内部的相关交易&#xff0c;本篇以采购为例&#xff0c;来看看公司内外采购交易所对…

【分布式系列】分布式锁timeout了怎么办?

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

Apache IoTDB vs InfluxDB 开源版,架构性能全面对比!

分布式、端边云同步、读写查询性能&#xff0c;Apache IoTDB 与 InfluxDB 开源版的详尽对照&#xff01; 在物联网&#xff08;IoT&#xff09;领域&#xff0c;数据的采集、存储和分析是确保系统高效运行和决策准确的重要环节。随着物联网设备数量的增加和数据量的爆炸式增长&…

Mysql安装 /lib64/libc.so.6: version `GLIBC_2.28‘ not found 缺少 glibc-2.28.tar.gz

问题&#xff1a;安装Mysql出现 Error mysql/bin/mysqld: /lib64/libm.so.6: version GLIBC_2.27 not found (required by mysql/bin/mysqld) mysql/bin/mysqld: /lib64/libc.so.6: version GLIBC_2.28 not found (required by mysql/bin/mysqld) mysql/bin/mysqld: /lib64/lib…

【仿真建模-anylogic】INetwork相关接口说明

Author&#xff1a;赵志乾 Date&#xff1a;2024-06-22 Declaration&#xff1a;All Right Reserved&#xff01;&#xff01;&#xff01; 1. 类图 2. 说明 INetwork为辊道网络、路线网路的顶层接口&#xff0c;其组成元素有节点和路径两种&#xff0c;对应的接口为INode、IP…

Android-Android Studio-FAQ

1 需求 2 接口 3 Android Studio xml布局代码补全功能失效问题 最终解决方案就是尝试修改compileSdk 为不同SDK版本来解决问题&#xff0c;将原本34修改为32测试会发现xml代码补全功能有效了&#xff01; 参考资料 Android Studio xml布局代码补全功能失效问题_android studi…

10.华为路由器使用ospf动态路由连通两个部门网络

目的&#xff1a;实验ospf动态路由协议连通A与B部门 AR1配置 [Huawei]int g0/0/0 [Huawei-GigabitEthernet0/0/0]ip add 1.1.1.1 24 [Huawei]vlan batch 10 [Huawei]int Vlanif 10 [Huawei]int e0/0/0 [Huawei-Ethernet0/0/0]port link-type access [Huawei-Ethernet0/0/0]por…

CubeFS - 新一代云原生存储系统

CubeFS 是一种新一代云原生存储系统,支持 S3、HDFS 和 POSIX 等访问协议,支持多副本与纠删码两种存储引擎,为用户提供多租户、 多 AZ 部署以及跨区域复制等多种特性。 官方文档 CubeFS 作为一个云原生的分布式存储平台,提供了多种访问协议,因此其应用场景也非常广泛,下面…

Apple - Text Attribute Programming Topics

本文翻译整理自&#xff1a;Text Attribute Programming Topics&#xff08;更新日期&#xff1a;2004-02-16 https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/TextAttributes/TextAttributes.html#//apple_ref/doc/uid/10000088i 文章目录 一、文…

音视频开发—FFmpeg 打开摄像头进行RTMP推流

实验平台&#xff1a;Ubuntu20.04 摄像头&#xff1a;普通USB摄像头&#xff0c;输出格式为YUV422 1.配置RTMP服务器推流平台 使用Nginx 配置1935端口即可&#xff0c;贴上教程地址 ubuntu20.04搭建Nginxrtmp服务器) 2.配置FFmpeg开发环境 过程较为简单&#xff0c;这里不…

【龙晰 离线安装openssl-devel】openssl-devel rpm 离线安装 需要下载哪些安rpm 包

进入龙晰镜像源地址下载 http://mirrors.openanolis.cn/anolis/8/BaseOS/x86_64/os/Packages/(base) [rootAI lib64]# yum install openssl-devel Last metadata expiration check: 14:03:32 ago on Fri 21 Jun 2024 07:26:56 AM CST. Dependencies resolved. Package …

不服 GPT-4o ,就是干?

OpenAI 最大的对手 Anthropic &#xff0c;深夜发布了他们迄今为止最智能的模型&#xff1a;Claude 3.5 Sonnet。 这俩家公司的竞争愈演愈烈&#xff0c;Anthropic 声称新 Claude AI 超越了 GPT-4o。 通过 Anthropic 发布的测试报告来看&#xff0c;新模型在推理、知识储备、代…

怎么处理整合了shiro的应用的RPC接口鉴权问题

这篇文章分享一下&#xff1a;当一个服务提供者整合了shiro安全框架来实现权限访问控制时&#xff0c;服务消费者通过feign请求服务提供者的接口时的鉴权不通过问题。 问题描述 博主有一个项目pms&#xff08;权限管理系统&#xff09;&#xff0c;使用了shiro框架来实现鉴权功…

什么是局域网IP?

局域网IP&#xff08;Local Area Network IP&#xff09;指的是在局域网内使用的IP地址。局域网是指在某个地理区域内&#xff0c;由一组相互连接的计算机组成的小型网络&#xff0c;常见于家庭、学校、办公室等场所。局域网IP可以用来实现内网穿透&#xff0c;即在复杂的网络环…

http发展史(http0.9、http1.0、http1.1、http/2、http/3)详解

文章目录 HTTP/0.9HTTP/1.0HTTP/1.1队头阻塞&#xff08;Head-of-Line Blocking&#xff09;1. TCP 层的队头阻塞2. HTTP/1.1 的队头阻塞 HTTP/2HTTP/3 HTTP/0.9 发布时间&#xff1a;1991年 特点&#xff1a; 只支持 GET 方法没有 HTTP 头部响应中只有 HTML 内容&#xff0…

2.XSS-存储型

储存型XSS 或持久型 XSS 交互的数据会被存在在数据库里面,永久性存储,具有很强的稳定性。 在留言板里面进行测试一下是否有做过滤 "<>?&66666点击提交 查看元素代码&#xff0c;已经提交完成&#xff0c;并且没有做任何的过滤措施 接下来写一个javascrip…

2024年十大数据集成工具和软件应用场景解析

详细阐述了十大数据集成工具及其优缺点&#xff1a; 数据集成的挑战 在当今的商业环境中&#xff0c;企业常常面临着数据分散、系统孤立的难题。传统的数据集成方式&#xff0c;就像一张复杂的蜘蛛网&#xff0c;难以理顺&#xff0c;令人头痛不已。 数据孤岛: 各个业务系统独立…

使用Jetpack Compose为Android App创建自定义页面指示器

使用Jetpack Compose为Android App创建自定义页面指示器 在现代移动应用中&#xff0c;页面指示器在提供视觉导航提示方面发挥着重要作用&#xff0c;帮助用户理解其在应用内容中的当前位置。页面指示器特别适用于顺序展示内容的场景&#xff0c;如图片轮播、图像库、幻灯片放…

在自托管基础设施上使用 GitOps 部署 MinIO

基于MinIO Weaviate Python GitOps探索的见解&#xff0c;本文探讨了如何增强软件部署流程的自动化。 通过将 GitHub Actions 与 Docker Swarm 集成而产生的协同作用&#xff0c;以自托管基础架构的稳健性为基础&#xff0c;标志着 CI/CD 实践的关键进步。这种方法不仅利用了软…

Mybatis缓存测试

实体类 Student Data Table(name "student") public class StudentEntity implements Serializable {private static final long serialVersionUID 1L;IdColumn(name "id")private Long id;Column(name "name")private String name;Column…