003 redis分布式锁 jedis分布式锁 Redisson分布式锁 分段锁

文章目录

    • Redis分布式锁原理
      • 1.使用set的命令时,同时设置过期时间
      • 2.使用lua脚本,将加锁的命令放在lua脚本中原子性的执行
    • Jedis分布式锁实现
      • pom.xml
      • RedisCommandLock.java
      • RedisCommandLockTest.java
    • 锁过期问题
      • 1乐观锁方式,增加版本号(增加版本号需要调整业务逻辑,与之配合,所以会入侵代码)
      • 2watch do,自动延期(不会侵入业务代码,redisson就是采用这种方案)
    • Redisson分布式锁
      • 加锁解锁
      • 锁重入
      • 锁的存储结构
      • Redisson加锁原理
      • Redisson释放锁原理
      • watch dog自动延期
    • 分段锁

在同一个JVM内部,大家往往采用synchronized或者Lock的方式来解决多线程间的安全问题,但是在分布式架构下,在JVM之间,那么就需要一种更加高级的锁机制,来处理这种跨JVM进程之间的线程安全问题,解决方案就是:使用分布式锁。
分布式锁

Redis分布式锁原理

Redis分布式锁机制,主要借助setnx和expire两个命令完成
setnx:当key不存在,将key设置为value,存在不做任何操作,返回0
客户端如果宕机,锁谁也加不上,即死锁。当持有锁的客户端宕机时,它可能没有机会释放锁,导致其他客户端无法获取锁。
expire:设置key过期时间

原理解析:
1key不存在时创建,并设置value和过期时间,返回值为1;成功获取到锁
2如果key存在时直接返回0,抢锁失败
3持有锁的线程释放锁时,手动删除key;或者过期时间到,key自动删除,锁释放

加锁的问题
setnx成功
expire失败
如果没有手动释放,那么这个锁永远被占用,其他线程永远也抢不到锁
解决方案:

1.使用set的命令时,同时设置过期时间

命令:set lock ‘123’ EX 100 NX

SET lock_key unique_value NX PX 30000
NX 表示只有当 key 不存在时才设置它。
PX 30000 表示 key 的过期时间为 30,000 毫秒(即 30 秒)
如果正在使用较旧版本的 Redis,或者出于某种原因需要使用 SETNX,可以考虑结合 EXPIRE 命令来手动为 key 设置过期时间。但这种方法的一个缺点是,在 SETNX 和 EXPIRE 之间存在一个小的时间窗口,其中如果客户端宕机,可能会导致 key 没有设置过期时间。因此,使用 SET 命令的 NX 和 PX 选项是更安全和推荐的方法。

set 同时设置过期时间命令
set key value[EX seconds][PX milliseconds][NX|XX]EX seconds:设置失效时长,单位秒
PX milliseconds设置失效时长,单位毫秒
NX key不存在时设置value,成功返回OK,失败返回nil
XX key存在时设置value,成功返回OK,失败返回nil

2.使用lua脚本,将加锁的命令放在lua脚本中原子性的执行

EVAL:Lua脚本进行求值,命令如下:
EVAL script numkeys key [key ...] arg [arg ...]>eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
>1) "key1" 2)"key2" 3)"first" 4)"second"

1script:参数是一段Lua5.1脚本程序,它会被运行在Redis服务器上下文中
2numkeys:参数用于指定键名参数的个数
这个命令的含义如下:

EVAL 是执行 Lua 脚本的命令。
“return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}” 是要执行的 Lua 脚本。这个脚本很简单,它只是返回一个包含四个元素的表(在 Lua 中,表是唯一的复合数据类型,类似于其他语言中的数组或字典)。这四个元素分别是脚本接收到的前两个 key 和前两个 argv。
2 是传递给 Lua 脚本的 key 的数量。这告诉 Redis,接下来的两个参数(key1 和 key2)应该被视为 key。
key1 和 key2 是传递给 Lua 脚本的两个 key。在 Lua 脚本中,它们可以通过 KEYS[1] 和 KEYS[2] 来访问。
first 和 second 是传递给 Lua 脚本的两个参数值。在 Lua 脚本中,它们可以通过 ARGV[1] 和 ARGV[2] 来访问。
所以,当你执行这个命令时,Lua 脚本会返回一个表,包含这四个值:key1, key2, first, second。

在Lua脚本中,可以使用redis.call()函数来执行Redis命令

#这段脚本实现了将键stock的值设为no
>eval "return redis.call('set',KEYS[1],ARGV[1])" 1 stock no

Jedis分布式锁实现

加锁:就是调用SET key PX NX命令

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

key 加锁的key
value UUID.randomUUID().toString(),代表加锁的客户端请求标识
nxxx NX,表示SET IF NOT EXIST
expx PX,表示毫秒

pom.xml


<dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>2.9.0</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.16.4</version></dependency>

RedisCommandLock.java

package com.example.demo;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import redis.clients.jedis.Jedis;import java.util.Collections;@Slf4j
@Data
@AllArgsConstructor
public class RedisCommandLock {private RedisTemplate redisTemplate;private static final String LOCK_SUCCESS = "OK";private static final String SET_IF_NOT_EXIST = "NX";private static final String SET_WITH_EXPIRE_TIME = "PX";/*** 尝试获取分布式锁* @param jedis Redis客户端* @param lockKey 锁* @param requestId 请求标识* @param expireTime 超期时间* @return 是否获取成功*/public static   boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);if (LOCK_SUCCESS.equals(result)) {return true;}return false;}private static final Long RELEASE_SUCCESS = 1L;/*** 释放分布式锁* @param jedis Redis客户端* @param lockKey 锁* @param requestId 请求标识* @return 是否释放成功*/public static boolean releaseDistributedLock(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;}/*** 最常见的解锁代码就是直接使用 jedis.del() 方法删除锁,* 这种不先判断锁的拥有者而直接解锁的方式,* 会导致任何客户端都可以随时进行解锁,即使这把锁不是它的。* @param jedis* @param lockKey*/public static void wrongReleaseLock1(Jedis jedis, String lockKey) {jedis.del(lockKey);}/*** 这种解锁代码乍一看也是没问题,与正确姿势差不多,唯一区别的是分成两条命令去执行,代码如下:* 它首先检查锁的拥有者(通过 requestId.equals(jedis.get(lockKey))),然后如果条件满足,它会删除锁。但是,这两个操作(get 和 del)是分开的,不是原子的。这意味着,在检查锁的拥有者和删除锁之间,其他客户端可能已经更改了锁的状态。** 具体来说,以下是一个可能的问题场景:** 客户端A获取了锁,并在某个时间点尝试释放它。* 客户端A调用 jedis.get(lockKey) 来检查它是否仍然拥有锁。假设它仍然拥有锁。* 在客户端A执行 jedis.del(lockKey) 之前,另一个客户端B可能已经获取了该锁(因为客户端A还没有释放它)。* 客户端A继续执行 jedis.del(lockKey),此时它实际上删除了客户端B刚刚获取的锁。* @param jedis* @param lockKey* @param requestId*/public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) {// 判断加锁与解锁是不是同一个客户端if (requestId.equals(jedis.get(lockKey))) {// 若在此时,这把锁突然不是这个客户端的,则会误解锁jedis.del(lockKey);}}}

RedisCommandLockTest.java


package com.example.demo;import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import redis.clients.jedis.Jedis;import java.util.UUID;import static org.junit.jupiter.api.Assertions.*;@Slf4j
public class RedisCommandLockTest {private Jedis jedis;@BeforeEachvoid setUp() {jedis = new Jedis("127.0.0.1", 6379);
//        jedis.auth("123456");}@Testpublic void testTryGetDistributedLock() {boolean result = RedisCommandLock.tryGetDistributedLock(jedis, "test:lock", UUID.randomUUID().toString(), 300000);System.out.println(result);}@Testpublic void testReleaseDistributedLock() {boolean result = RedisCommandLock.releaseDistributedLock(jedis, "test:lock", "3c8feabb-befd-4552-805f-6a78bc7c43b4");System.out.println(result);}}

锁过期问题

锁过期

预估业务操作10秒,锁设置20秒,各种原因,比如STW问题,业务操作执行超过20秒,业务会在无锁状态下运行,就会发生数据紊乱
注:STW:Java中Stop-The-World机制简称STW,常发送于fullGC,这时Java应用程序的其他所有线程都被挂起(除了垃圾收集器之外)

1乐观锁方式,增加版本号(增加版本号需要调整业务逻辑,与之配合,所以会入侵代码)

乐观锁

2watch do,自动延期(不会侵入业务代码,redisson就是采用这种方案)

客户端1加锁的key默认生存时间才30秒,如果超过了30秒,客户端1还想一直持有这把锁
只要客户端1一旦加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下,如果客户端1还持有锁key,那么就会不断的延长锁key的生存时间

Redisson分布式锁

Redisson是基于Netty的Redis客户端。不但能操作原生的Redis数据结构,还为使用者提供了一系列具有分布式特性的常用工具类,实现了分布式锁。

Redis分布式锁和JUC的Lock方法相似。RLock接口继承了Lock接口

加锁解锁

@Testpublic void testLockDemo() {RLock disLock = client.getLock("DISLOCK");boolean isLock = false;try {disLock.lock(); //默认30s
//            isLock = disLock.tryLock(20000, 1500000, TimeUnit.MILLISECONDS);System.out.println(isLock);if (isLock) {//TODO if get lock success, do something;Thread.sleep(15000);}} catch (Exception e) {} finally {// 无论如何, 最后都要解锁disLock.unlock();}}

锁重入

@Testpublic void testLockDemo2() {RLock disLock = client.getLock("DISLOCK");boolean isLock = false;try {isLock = disLock.tryLock(2000, 1500000, TimeUnit.MILLISECONDS);isLock = disLock.tryLock(2000, 1500000, TimeUnit.MILLISECONDS);isLock = disLock.tryLock(2000, 1500000, TimeUnit.MILLISECONDS);} catch (Exception e) {} finally {// 无论如何, 最后都要解锁disLock.unlock();disLock.unlock();disLock.unlock();}}

锁的存储结构

锁的结构是Hash
key:锁的名字
字段: UUID+threadId
值:表示重入的次数

Redisson加锁原理

RedissonLock的tryLockInnerAsync是Redisson加锁的关键方法
加锁
1.判断有没有"DISLOCK"
2.如果没有,设置UUID:1=1
3.设置它的过期时间

锁重入
1.KEY和字段都存在,锁重入
2.执行命令incrby UUID:1 1
3.结果: DISLOCK: {UUID:1 2}

锁互斥
1.客户端2进入
2.判断有KEY,没有字段
3.返回过期时间
4.客户端2自旋等待

redisson源码 加锁
1.key不存在,加锁设置字段=1,过段时间
2.key、字段都存在,锁重入,字段值+1
3.key存在,字段不存在,抢锁失败
redisson源码 加锁

Redisson释放锁原理

RedissonLock的unlockInnerAsync是Redisson释放锁的关键方法
1.判断KEY是否存在
2.如果不存在,返回nil
3.如果存在,使用hincrby-1,减1
4.减完后,counter>0值仍大于0,则返回0
5.减完后,counter<=0,删除key
6.用publish广播锁释放消息
redisson源码 释放锁

redisson源码 释放锁
订阅channel源码
订阅channel源码

watch dog自动延期

watch dog:当加锁成功后,同时开启守护线程,默认有效期是30秒,每隔10秒就会给锁续期到30秒
watchDog只有在未显示指定加锁时间时才会生效
lockWatchdogTimeout:可以设置超时时间
watchDog

分段锁

思想来源map/reduce,ConcurrentHashMap
分段锁

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

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

相关文章

一、计算机基础(Java零基础一)

&#x1f33b;&#x1f33b;目录 一、&#x1f33b;&#x1f33b;剖析学习Java前的疑问&#x1f33b;&#x1f33b;1.1 零基础学习编程1.2 英语不好能学吗&#xff1f;1.3 理解慢能学好吗&#xff1f;1.4 现在学Java晚吗&#xff1f;1.5 Java 和 Python 还有 Go 的选择1.6 Java…

Elasticsearch 索引、类型、文档、分片与副本等核心概念介绍

&#x1f407;明明跟你说过&#xff1a;个人主页 &#x1f3c5;个人专栏&#xff1a;《洞察之眼&#xff1a;ELK监控与可视化》&#x1f3c5; &#x1f516;行路有良友&#xff0c;便是天堂&#x1f516; 目录 一、引言 1、Elasticsearch简介 2、分布式搜索引擎的工作原理…

【Linux】在Linux中执行命令ifconfig, 报错-bash:ifconfig: command not found解决方案

一、报错信息 ifconfig 报错-bash:ifconfig: command not found 同时&#xff0c;通过ip addr查看&#xff0c;也看不到IP信息 二、解决方案 找到ifcfg-ens0文件&#xff0c;此文件的目录在/etc/sysconfig/network-scripts目录下 命令&#xff1a;cd /etc/sysconfig/network…

英语学习笔记9——How are you today?

How are you today? 你好吗&#xff1f; 词汇 Vocabulary well adj. 好的 n. 井 fine adj. 美好的 两个方面&#xff1a;天气、身体。 搭配&#xff1a;a fine day 晴朗的一天    It’s a fine day today. 今天很晴朗。 good adj. 好的 口语偏多 搭配&#xff1a;Good jo…

【2022 深圳 ArchSummit 】大数据架构稳定性保障实践

文章目录 一、前言二、现状三、大数据架构的历史变迁&#xff08;一&#xff09;洪荒期&MR&#xff08;二&#xff09;远古期&MPP&#xff08;四&#xff09;近现代&Flink/Spark&#xff08;五&#xff09;现如今&实时数据湖架构 四、架构稳定的关键因素&#…

Bert 在 OCNLI 训练微调

目录 0 资料1 预训练权重2 wandb3 Bert-OCNLI3.1 目录结构3.2 导入的库3.3 数据集自然语言推断数据集路径读取数据集数据集样例展示数据集类别统计数据集类加载数据 3.4 Bert3.4 训练 4 训练微调结果3k10k50k 0 资料 【数据集微调】 阿里天池比赛 微调BERT的数据集&#xff0…

UE5(射线检测)学习笔记

这一篇会讲解射线检测点击事件、离开悬停、进入悬停事件的检测&#xff0c;以及关闭射线检测的事件&#xff0c;和射线检测蓝图的基础讲解。 创建一个简单的第三人称模板 创建一个射线检测的文件夹RadiationInspection&#xff0c;并且右键蓝图-场景组件-命名为BPC_Radiation…

路由模块封装

目录 一、问题引入 二、步骤 一、问题引入 随着项目内容的不断扩大&#xff0c;路由也会越来越多&#xff0c;把所有的路由配置都堆在main.js中就不太合适了&#xff0c;所以需要将路由模块抽离出来。其好处是&#xff1a;拆分模块&#xff0c;利于维护。 二、步骤 将路由相…

linux PXE高效批量网络装机

PXE批量部署的优点 规模化&#xff1a;同时装配多台服务器 自动化&#xff1a;安装系统、配置各种服务 远程实现&#xff1a;不需要光盘、U盘等安装介质 部署PXE远程安装服务 搭建PXE远程安装服务器 先做好初始化准备 1.安装并启用 TFTP 服务 yum -y install tftp-server …

从开发角度理解漏洞成因(02)

文章目录 文件上传类需求文件上传漏洞 文件下载类需求文件下载漏洞 扩展 留言板类&#xff08;XSS漏洞&#xff09;需求XSS漏洞 登录类需求cookie伪造漏洞万能密码登录 持续更新中… 文章中代码资源已上传资源&#xff0c;如需要打包好的请点击PHP开发漏洞环境&#xff08;SQL注…

缓存相关问题:雪崩、穿透、预热、更新、降级的深度解析

✨✨祝屏幕前的小伙伴们每天都有好运相伴左右✨✨ &#x1f388;&#x1f388;作者主页&#xff1a; 喔的嘛呀&#x1f388;&#x1f388; 目录 引言 1. 缓存雪崩 1.1 问题描述 1.2 解决方案 1.2.1 加锁防止并发重建缓存 2. 缓存穿透 2.1 问题描述 2.2 解决方案 2.2.1 …

基于TL431和CSA的恒压与负压输出

Hello uu们,51去那里玩了呀?该收心回来上班了,嘿嘿! 为什么会有这个命题,因为我的手头只有这些东西如何去实现呢?让我们一起来看电路图吧.电路图如下图1所示 图1:CSA恒压输出电路 图1中,R1给U2提供偏置,Q1给R1提供电流,当U1-VOUT输出大于2.5V时候,U2内部的三极管CE导通,使得…

Golang | Leetcode Golang题解之第73题矩阵置零

题目&#xff1a; 题解&#xff1a; func setZeroes(matrix [][]int) {n, m : len(matrix), len(matrix[0])col0 : falsefor _, r : range matrix {if r[0] 0 {col0 true}for j : 1; j < m; j {if r[j] 0 {r[0] 0matrix[0][j] 0}}}for i : n - 1; i > 0; i-- {for …

Python的Web框架Flask+Vue生成漂亮的词云图

生成效果图 输入待生成词云图的文本&#xff0c;点击生成词云即可&#xff0c;在词云图生成之后&#xff0c;可以点击下载图片保存词云图。 运行步骤 分别用前端和后端编译器&#xff0c;打开backend和frontend文件夹。前端运行 npm install &#xff0c;安装相应的包。后端…

【prometheus】Pushgateway安装和使用

目录 一、Pushgateway概述 1.1 Pushgateway简介 1.2 Pushgateway优点 1.3 pushgateway缺点 二、测试环境 三、安装测试 3.1 pushgateway安装 3.2 prometheus添加pushgateway 3.3 推送指定的数据格式到pushgateway 1.添加单条数据 2.添加复杂数据 3.SDk-prometheus-…

Python深度学习基于Tensorflow(8)自然语言处理基础

RNN 模型 与前后顺序有关的数据称为序列数据&#xff0c;对于序列数据&#xff0c;我们可以使用循环神经网络进行处理&#xff0c;循环神经网络RNN已经成功的运用于自然语言处理&#xff0c;语音识别&#xff0c;图像标注&#xff0c;机器翻译等众多时序问题&#xff1b;RNN模…

16地标准化企业申请!安徽省工业和信息化领域标准化示范企业申报条件

安徽省工业和信息化领域标准化示范企业申报条件有哪些&#xff1f;合肥市 、黄山市 、芜湖市、马鞍山、安庆市、淮南市、阜阳市、淮北市、铜陵市、亳州市、宣城市、蚌埠市、六安市 、滁州市 、池州市、宿州市企业申报安徽省工业和信息化领域标准化示范企业有不明白的可在下文了…

《TAM》论文笔记(上)

原文链接 [2005.06803] TAM: Temporal Adaptive Module for Video Recognition (arxiv.org) 原文代码 GitHub - liu-zhy/temporal-adaptive-module: TAM: Temporal Adaptive Module for Video Recognition 原文笔记 What&#xff1a; TAM: Temporal Adaptive Module for …

JAVA系列:IO流

JAVA IO流 IO流图解 一、什么是IO流 I/O流是Java中用于执行输入和输出操作的抽象。它们被设计成类似于流水&#xff0c;可以在程序和外部源&#xff08;如文件、网络套接字、键盘、显示器等&#xff09;之间传输数据。按处理数据单位分为&#xff1a; 1字符 2字节 、 1字节(…

阿里发布通义千问2.5:一文带你读懂通义千问!

大家好&#xff0c;我是木易&#xff0c;一个持续关注AI领域的互联网技术产品经理&#xff0c;国内Top2本科&#xff0c;美国Top10 CS研究生&#xff0c;MBA。我坚信AI是普通人变强的“外挂”&#xff0c;所以创建了“AI信息Gap”这个公众号&#xff0c;专注于分享AI全维度知识…