Redis如何实现分布式锁?

news2024/11/20 1:37:53

📢📢📢📣📣📣

哈喽!大家好,我是【一心同学】,一位上进心十足的【Java领域博主】!😜😜😜

✨【一心同学】的写作风格:喜欢用【通俗易懂】的文笔去讲解每一个知识点,而不喜欢用【高大上】的官方陈述。

✨【一心同学】博客的领域是【面向后端技术】的学习,未来会持续更新更多的【后端技术】以及【学习心得】。

✨如果有对【后端技术】感兴趣的【小可爱】,欢迎关注一心同学】💞💞💞

❤️❤️❤️感谢各位大可爱小可爱!❤️❤️❤️ 


目录

一、什么是分布式锁?

二、分布式锁实现方案

🌴 常见的分布式锁方案

🌴 推演实现分布式锁

🌵 问题:发生死锁

🌵 解决——死锁

🌵 解决——锁被别人释放了

🌵 解决——锁的过期时间

三、redisson实现分布式锁

🌴 依赖

🌴 配置

🌴 业务类使用


一、什么是分布式锁?

分布式锁可以理解为:控制分布式系统有序的去对共享资源进行操作,通过互斥来保持一致性。

例如共享的资源就是一个房子,里面有各种书,分布式系统就是要进屋看书的人, 分布式锁就是保证这个房子只有一个门并且一次只有一个人可以进,而且门只有一把钥匙。 然后许多人要去看书,进行排队,第一个人拿着钥匙把门打开进屋看书并且把门锁上, 然后第二个人没有钥匙,那就等着,等第一个出来,然后你在拿着钥匙进去,然后就是以此类推。

单机环境中,应用是在同一进程下的,只需要保证单进程多线程环境中的线程安全性,通过 JAVA 提供的 volatile、ReentrantLock、synchronized 以及 concurrent 并发包下一些线程安全的类等就可以做到。而在多机部署环境中,不同机器不同进程,就需要在多进程下保证线程的安全性了。因此,分布式锁应运而生。

 

二、分布式锁实现方案

🌴 常见的分布式锁方案

分类方案实现原理优点缺点
基于数据库基于mysql 表唯一索引1.表增加唯一索引
2.加锁:执行insert语句,若报错,则表明加锁失败
3.解锁:执行delete语句
完全利用DB现有能力,实现简单1.锁无超时自动失效机制,有死锁风险
2.不支持锁重入,不支持阻塞等待
3.操作数据库开销大,性能不高
基于MongoDB findAndModify原子操作1.加锁:执行findAndModify原子命令查找document,若不存在则新增
2.解锁:删除document
实现也很容易,较基于MySQL唯一索引的方案,性能要好很多1.大部分公司数据库用MySQL,可能缺乏相应的MongoDB运维、开发人员
2.锁无超时自动失效机制
基于分布式协调系统基于ZooKeeper1.加锁:在/lock目录下创建临时有序节点,判断创建的节点序号是否最小。若是,则表示获取到锁;否,则则watch /lock目录下序号比自身小的前一个节点
2.解锁:删除节点
1.由zk保障系统高可用
2.Curator框架已原生支持系列分布式锁命令,使用简单
需单独维护一套zk集群,维保成本高
基于缓存基于redis命令1. 加锁:执行setnx,若成功再执行expire添加过期时间
2. 解锁:执行delete命令
实现简单,相比数据库和分布式系统的实现,该方案最轻,性能最好1.setnx和expire分2步执行,非原子操作;若setnx执行成功,但expire执行失败,就可能出现死锁
2.delete命令存在误删除非当前线程持有的锁的可能
3.不支持阻塞等待、不可重入
基于redis Lua脚本能力1. 加锁:执行SET lock_name random_value EX seconds NX 命令

2. 解锁:执行Lua脚本,释放锁时验证random_value 
-- ARGV[1]为random_value,  KEYS[1]为lock_name

if redis.call("get", KEYS[1]) == ARGV[1] then

    return redis.call("del",KEYS[1])

else

    return 0

end

同上;实现逻辑上也更严谨,除了单点问题,生产环境采用用这种方案,问题也不大。不支持锁重入,不支持阻塞等待

在以上的方案中,redis+lua基本可应付工作中分布式锁的需求,但是还有一种解决方案——redisson分布式锁,相比以上方案,redisson保持了简单易用、支持锁重入、支持阻塞等待、Lua脚本原子操作。

🌴 推演实现分布式锁

分布式锁实现条件:

1、互斥性。在任意时刻,只有一个客户端能持有锁。

2、不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。

3、解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了,即不能误解锁。

4、具有容错性。只要大多数Redis节点正常运行,客户端就能够获取和释放锁。

想要实现分布式锁,必须要求Redis有互斥的能力。可以使用SETNX命令,其含义是SET IF NOT EXIST,即如果key不存在,才会设置它的值,否则什么也不做。两个客户端进程可以执行这个命令,达到互斥,就可以实现一个分布式锁。

 

我们可以将整体流程写成以下伪代码:

// 加锁
SETNX lock_key 1
// 业务逻辑
DO SOMETHING
// 释放锁
DEL lock_key

🌵 问题:发生死锁

不难发现上面的代码在某些场景下是会发生死锁的,如:

1、程序处理业务逻辑异常,没及时释放锁。

2、进程挂了,没机会释放锁。

🌵 解决——死锁

为了解决以上死锁问题,最容易想到的方案是在申请锁时,在Redis中实现时,给锁设置一个过期时间,假设操作共享资源的时间不会超过10s,那么加锁时,给这个key设置10s过期即可。

如何加锁?

加锁、设置过期时间是2条命令,有可能只执行了第一条,第二条却执行失败,例如:

  • SETNX执行成功,执行EXPIRE时由于网络问题,执行失败
  • SETNX执行成功,Redis异常宕机,EXPIRE没有机会执行
  • SETNX执行成功,客户端异常崩溃,EXPIRE没有机会执行

之这两条命令如果不能保证是原子操作,就有潜在的风险导致过期时间设置失败,依旧有可能发生死锁问题。幸好在Redis 2.6.12之后,Redis扩展了SET命令的参数,可以在SET的同时指定EXPIRE时间,这条操作是原子的,例如以下命令是设置锁的过期时间为10秒。

SET lock_key 1 EX 10 NX

新问题:

但是,过期时间也带来了新的问题,如下:

 

  • 客户端1加锁成功,开始操作共享资源。
  • 客户端1操作共享资源耗时太久,超过了锁的过期时间,锁失效(锁被自动释放)。
  • 客户端2加锁成功,开始操作共享资源。
  • 客户端1操作共享资源完成,在finally块中手动释放锁,但此时它释放的是客户端2的锁。

我们把上面归为两个主要问题:

1、锁过期

2、释放了别人的锁

第1个问题是评估操作共享资源的时间不准确导致的,客户端在拿到锁之后,在操作共享资源时,遇到的场景是很复杂的,既然是预估的时间,也只能是大致的计算,不可能覆盖所有导致耗时变长的场景。

第2个问题是释放了别人的锁,原因在于释放锁的操作是无脑操作,并没有检查这把锁的归属,这样解锁不严谨。

🌵 解决——锁被别人释放了

客户端在加锁时,设置一个只有自己知道的唯一标识进去,如可以是自己的线程ID,如果是redis实现,就是SET key unique_value EX 10 NX。之后在释放锁时,要先判断这把锁是否归自己持有,只有是自己的才能释放它。

//释放锁 比较unique_value是否相等,避免误释放
if redis.get("key") == unique_value then
    return redis.del("key")

但是释放锁使用的是GET + DEL两条命令,这时又会遇到原子性问题了。这时候我们就需要Lua脚本帮我们将其转为原子命令,因为Redis处理每个请求是单线程执行的,在执行一个Lua脚本时其它请求必须等待,直到这个Lua脚本处理完成,这样一来GET+DEL之间就不会有其他命令执行了。

以下是使用Lua脚本(unlock.script)实现的释放锁操作的伪代码,其中,KEYS[1]表示lock_key,ARGV[1]是当前客户端的唯一标识,这两个值都是我们在执行 Lua脚本时作为参数传入的。

//Lua脚本语言,释放锁 比较unique_value是否相等,避免误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

最后在redis客户端执行以下命令即可:

redis-cli  --eval  unlock.script lock_key , unique_value 

到此,我们的流程都是非常严谨的,目前流程如下:

1、加锁时要设置过期时间SET lock_key unique_value EX expire_time NX

2、操作共享资源

3、释放锁:Lua脚本,先GET判断锁是否归属自己,再DEL释放锁

现在只剩一个问题了,确定锁的过期时间。

🌵 解决——锁的过期时间

方案:加锁时,先设置一个预估的过期时间,然后开启一个守护线程,定时去检测这个锁的失效时间,如果锁快要过期了,操作共享资源还未完成,那么就自动对锁进行续期,重新设置过期时间。

这种方案是比较好的,而且有一个库把这些工作都封装好了,它就是RedissonRedisson是一个Java语言实现的Redis SDK客户端,在使用分布式锁时,它就采用了自动续期的方案来避免锁过期,这个守护线程我们一般叫它看门狗线程。客户端一旦加锁成功,就会启动一个watch dog看门狗线程,它是一个后台线程,会每隔一段时间(这段时间的长度与设置的锁的过期时间有关)检查一下,如果检查时客户端还持有锁key(也就是说还在操作共享资源),那么就会延长锁key的生存时间。而且使用redisson来实现分布式锁,由于其加锁,解锁都封装好了,思路也是我们一路推导过来的思路,所以使用起来非常友好。

三、redisson实现分布式锁

我们先来看看不使用redisson的话加解锁过程是有多复杂:

加锁:


    //加锁之后返回锁的持有者(锁的value使用唯一时间戳标志每个客户端,保证只有锁的持有者才可以释放锁)
    public static String lock(Jedis jedis, String key,Long waitEnd,String requestId) {
        try {
            // 1秒内数次加锁如果失败,则不断请求重新获取锁,超过1秒还没能加锁,就加锁失败(为了每个线程拥有公平的机会获取锁)
            while (System.currentTimeMillis() < waitEnd) {// 1秒类不断尝试加锁(加锁之后返回锁的持有者)
                String result = jedis.set(key, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, EXPIRE_TIME);
                
                if (LOCK_SUCCESS.equals(result)) {
                    return requestId;
                }
            }
        } catch (Exception ex) {
            log.error("lock error", ex);
        }
        return null;
}

解锁:

   
 
    /**
     * 释放分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public static boolean unLock(Jedis jedis, String lockKey, String requestId) {
 
        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(requestId));
 
        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }

是不是过程十分繁琐,现在开始我们的redisson来实现:

🌴 依赖

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.16.8</version>
        </dependency>

🌴 配置

package com.yixin.config;

import org.redisson.Redisson;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.io.IOError;

@Configuration
public class MyRedisConfig {

    @Bean
    public Redisson redisson() throws IOError{
        //1、创建配置
        Config config = new Config();
        //2、根据Config 创建出RedissonClient示例
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        return (Redisson) Redisson.create(config);
    }

}

🌴 业务类使用

package com.yixin.service;

import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class AccountService {

    @Autowired
    private Redisson redisson;

    public void submit(){
        String lockKey = "product_order";
        RLock redissionLock=redisson.getLock(lockKey);
        redissionLock.lock(); // 相当于 setIfAbsent(lockKey,clientId,30,TimeUnit.SECONDS);
        //业务操作。。。
        redissionLock.unlock(); //底层是已经封装lua脚本了
    }

}

这样就完成好了,看着是不是特别友好。

如果这篇【文章】有帮助到你,希望可以给【一心同学】点个👍,创作不易,相比官方的陈述,我更喜欢用【通俗易懂】的文笔去讲解每一个知识点,如果有对【后端技术】感兴趣的小可爱,也欢迎关注❤️❤️❤️ 【一心同学】❤️❤️❤️,我将会给你带来巨大的【收获与惊喜】💕💕!

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

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

相关文章

OpenMMLab学习笔记(一)

OpenMMLab学习笔记&#xff08;一&#xff09; day01 计算机视觉与OpenMMLab开源算法体系 1. 基本知识 计算机视觉的基础任务&#xff1a;分类、分类和定位、物体检测、分割&#xff08;语义分割、实例分割&#xff09;&#xff0c;其中实例分割统一种类索引不同 注意语义分…

indexedDB存储

一、概述 随着浏览器的功能不断增强&#xff0c;越来越多的网站开始考虑&#xff0c;将大量数据储存在客户端&#xff0c;这样可以减少从服务器获取数据&#xff0c;直接从本地获取数据。 现有的浏览器数据储存方案&#xff0c;都不适合储存大量数据&#xff1a;Cookie 的大小…

rocketmq 笔记

cap理论 Consistency&#xff08;一致性&#xff09;Availability&#xff08;可用性&#xff09;Partition tolerance&#xff08;分区容忍性&#xff09; ①一致性&#xff1a;对于客户端的每次读操作&#xff0c;要么读到的是最新的数据&#xff0c;要么读取失败。换句话说…

ElasticSearch -- Prometheus+Grafana监控

向导介绍部署Prometheus配置Grafana下载仪表盘导入仪表盘报警核心指标集群健康和节点可用性主机级别的系统和网络指标JVM内存和垃圾回收搜索和索引性能资源饱和度注册自动重启介绍 Prometheus官方提供了ES的exporter&#xff1a;EsExporter Github地址&#xff1a;elasticsea…

【Vue】Vue不同版本的基本使用

一、Vue的版本 1. Vue1.x几乎被淘汰&#xff0c;不再建议学习与使用。2. Vue2.x<scriptsrc"" target"_blank">https://cdn.jsdelivr.net/npm/vue2.6.14"></script>3. Vue3.x<scriptsrc"" target"_blank">ht…

【IO异常】HTTP请求报错Error writing to server

报错信息如下&#xff1a; [2023-01-04 13:36:02.185]-ERROR-[biz:aplus-task-oms1060189862335877121][sys:aplus-cms-tran1060189866052390912][com.phfund.aplus.cms.tran.module.counter.service.impl.OcrServiceImpl-102][调用远程服务发送文件异常:] cn.hutool.http.Http…

一、HTML5

文章目录一、HTML5二、html5的基本结构三、基本标签四、HTML常用标记4.1 文本标题(h1-h6)4.2 段落文本p4.4 空格4.5 换行 br4.6 水平线4.7 加粗标记4.8 倾斜4.9 扩展4.10 列表4.10.1 ul 无序列表4.10.2 ol 有序列表4.10.3 dl 自定义列表五、块级标签、行级标签一、HTML5 H5是H…

洛谷 P1886 滑动窗口 /【模板】单调队列

滑动窗口 /【模板】单调队列 题目描述 有一个长为 nnn 的序列 aaa&#xff0c;以及一个大小为 kkk 的窗口。现在这个从左边开始向右滑动&#xff0c;每次滑动一个单位&#xff0c;求出每次滑动后窗口中的最大值和最小值。 例如&#xff1a; The array is [1,3,−1,−3,5,3,…

辨别三星内存条的真假

目录前言一、三星内存为什么水深&#xff1f;二、通过物理手段辨别1.包装2.日期3.是否透光4.颜色5.电阻颜色6.颗粒丝印&#xff08;重点&#xff09;7.其他标签或字迹结语前言 本文截止2023.2.2&#xff0c;针对笔记本内存条 省流&#xff1a;直接JD自营 最近随手在tb买了条三星…

快速掌握任意 Android 应用的抓包

抓包是流量分析的基础&#xff0c;也是安全研究重要的一环。抓包软件有很多种&#xff0c;如 Burpsuite、mitmproxy 以及 Fiddle&#xff0c;抓包方式常见的有设置系统代理、AP 热点抓包、透明代理等。不同方式有不同优缺点&#xff0c;也有不同的应用场景。相信很多安全研究者…

Google Analytics | 学习笔记

一.简介 1.什么是GA&#xff1f; 是谷歌开发的一款分析网页流量的工具&#xff0c;可以帮助网站解决数据分析与统计的问题&#xff0c;并且将这些数据可视化展现报告出来&#xff0c;帮助网站商家更好的分析受众&#xff0c;流量等&#xff0c;从而更好地进行运营网站营销等 …

UV统计的学习

12.1 、UV统计-HyperLogLog 首先我们搞懂两个概念&#xff1a; UV&#xff1a;全称Unique Visitor&#xff0c;也叫独立访客量&#xff0c;是指通过互联网访问、浏览这个网页的自然人。1天内同一个用户多次访问该网站&#xff0c;只记录1次。 PV&#xff1a;全称Page View&am…

大龄考生上岸浙大MPA的“曲线救国”策略

先来介绍一下我个人的背景把&#xff0c;我是通过参加浙大提前批面试获得A资格&#xff0c;笔试接近两百分上岸MPA项目的。说起来我的个人优势真的不多&#xff0c;只是一个普通一本&#xff0c;不是什么211/985的名校&#xff0c;所以就对浙大有一种向往&#xff0c;使得自己的…

【数据结构】1.4 算法和算法分析

文章目录1. 算法的定义及特性算法的特性算法设计的要求2. 算法的时间复杂度分析算法时间复杂度的基本方法算法时间复杂度分析例题算法时间复杂度的计算3. 算法的空间复杂度1. 算法的定义及特性 算法的定义 对特定问题求解方法和步骤的一种描述&#xff0c;它是指令的有限序列…

C++:String类应用

string对象的构造 //string() string(const string &s) string(const char *s) string(first,last) void TestString01() {string s1;string s2("hello");string s3(s2);const char* p "hello";string s4(p, p 2);cin >> s1;cout << …

微信小程序 Springboot旅游景点酒店预订管理系统 java

功能模块划分 &#xff08;1&#xff09;用户信息管理模块 用户管理模块主要是对用户信息进行管理。包括&#xff1a; 用户的注册&#xff1a;实现用户信息的注册&#xff0c;用户注册是的信息校验&#xff0c;用户信息的保存。 用户的登录&#xff1a;检验用户是否为该网站的合…

如何打造优秀高绩效的团队?Google亚里士多德计划揭开谜底!

研究人员通过亚里士多德计划开展研究&#xff0c;通过对大量团队效率进行定性&#xff08;360主观评估&#xff09;与定量&#xff08;产出成果度量&#xff09;结合的统计评估&#xff0c;筛选出左右着团队动态的五个关键支柱&#xff1a;心理安全、可靠性、结构和清晰度、意义…

Nginx与Upstream之间产生大量TIME_WAIT连接的解决办法

1. 现象 Nginx反向代理了一个Java服务&#xff0c;QPS大概是200&#xff0c;问题发生时的Nginx配置&#xff1a; location / {proxy_pass http://192.168.3.4:18600; }在上游Java服务器上可以观察到大量&#xff08;约2000个&#xff09;的TIME_WAIT状态的网络连接 从Nginx…

一位十年测试老前辈的修炼之路,希望能帮你点清现实

对于刚进入软件测试工作岗位的新人&#xff0c;如何快速、健康的在职业道路上成长&#xff0c;作者谈了几点自己看法&#xff1a; 1、兴趣是最好的老师 对于软件测试工作&#xff0c;通常是比较枯燥的&#xff0c;如果没有兴趣很难做到持久。 我最近参与了一个软件测试项目&a…

Plecs电力电子仿真专业教程-第一季 第三节 Plecs界面介绍

Plecs电力电子仿真专业教程-第一季 第三节 Plecs界面介绍 Plecs仿真软件主要包含两个部分&#xff1a;元件库窗口和电路图编辑窗口。元件库窗口主要用于选择仿真所用到的电子元器件&#xff0c;通过拖拽的方式可以将所需要的元器件放置在主电路图窗口中。 主界面窗口如下&…