当前位置: 首页 > news >正文

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
分段锁

相关文章:

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

文章目录 Redis分布式锁原理1.使用set的命令时&#xff0c;同时设置过期时间2.使用lua脚本&#xff0c;将加锁的命令放在lua脚本中原子性的执行 Jedis分布式锁实现pom.xmlRedisCommandLock.javaRedisCommandLockTest.java 锁过期问题1乐观锁方式&#xff0c;增加版本号(增加版本…...

Jackson工具,java对象和json字符串之间的互相转换

一、maven依赖引入jackson <dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId><version>2.12.5</version></dependency>jackson-databind依赖见下&#xff1a; <depend…...

【设计模式】之装饰器模式

系列文章目录 【设计模式】之模板方法模式 【设计模式】之责任链模式 【设计模式】之策略模式 【设计模式】之工厂模式&#xff08;三种&#xff09; 前言 今天给大家介绍23种设计模式中的装饰器模式。&#x1f308; 一、什么是装饰器模式 装饰器模式&#xff08;Decora…...

leetcode_46.全排列

46. 全排列 题目描述&#xff1a;给定一个不含重复数字的数组 nums &#xff0c;返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。 示例 1&#xff1a; 输入&#xff1a;nums [1,2,3] 输出&#xff1a;[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]示例 2&#…...

【牛客】[HNOI2003]激光炸弹

原题链接&#xff1a;登录—专业IT笔试面试备考平台_牛客网 目录 1. 题目描述 2. 思路分析 3. 代码实现 1. 题目描述 2. 思路分析 二维前缀和板题。 注意从&#xff08;1,1&#xff09;开始存即可&#xff0c;所以每次输入x,y之后&#xff0c;要x,y。 因为m的范围最大为…...

Docker与Harbor:构建企业级私有Docker镜像仓库

目录 引言 一、本地私有仓库 &#xff08;一&#xff09;基本概述 &#xff08;二&#xff09;搭建本地私有仓库 1.下载registry镜像 2.启动容器 3.上传本地镜像 4.客户端下载镜像 二、Harbor简介 &#xff08;一&#xff09;什么是 Harbor &#xff08;二&#xff…...

推荐几个傻瓜式短视频去水印在线网站

在数字化时代&#xff0c;短视频已成为信息传播的重要方式之一。随着TikTok、Instagram Reels、抖音等平台的流行&#xff0c;短视频的制作和分享成为了日常生活的一部分。然而&#xff0c;在分享或编辑这些短视频时&#xff0c;去除水印成为了一项不可或缺的需求。水印是视频原…...

大模型LLM之SFT微调总结

一. SFT微调是什么 在大模型的加持下现有的语义理解系统的效果有一个质的飞跃&#xff1b;相对于之前的有监督的Pre-Train模型&#xff1b;大模型在某些特定的任务中碾压式的超过传统nlp效果&#xff1b;由于常见的大模型参数量巨大&#xff1b;在实际工作中很难直接对大模型训…...

【RocketMQ问题总结-2】

RocketMQ 消息持久化 Broker通过底层的Netty服务器获取到一条消息后&#xff0c;会把这条消息的内容写入到一个CommitLog文件里去&#xff08;一个Broker进程就只有一个CommitLog文件&#xff0c;也就是说这个Broker上所有Topic的消息都会写入这个文件&#xff09;。 同时&…...

掌握Android Fragment开发之魂:Fragment的深度解析(上)

Fragment是Android开发中用于构建动态和灵活界面的基石。它不仅提升了应用的模块化程度&#xff0c;还增强了用户界面的动态性和交互性&#xff0c;允许开发者将应用界面划分为多个独立、可重用的部分&#xff0c;每个部分都可以独立于其他部分进行操作。本文将从以下几个方面深…...

深度解读DreamFusion:一站式AI解决方案

DreamFusion是一款备受瞩目的人工智能解决方案&#xff0c;它整合了多种AI技术&#xff0c;为用户提供了一站式的解决方案。本文将全面解读DreamFusion&#xff0c;探讨其特点、功能和应用场景&#xff0c;助您深入了解这一创新工具。 1. 特点概述 DreamFusion具备以下显著特…...

JVM-02

字节码文件是一种特殊的文件格式&#xff0c;它包含了将源代码转换为机器可执行代码所需的指令集。字节码文件通常是由编译器将源代码编译为字节码的中间表示形式。 在Java中&#xff0c;字节码文件的扩展名为.class&#xff0c;它存储了编译后的Java代码。这些字节码文件可以在…...

【一起深度学习——NIN】

NIN神经网络 原理图&#xff1a;代码实现&#xff1a;输出结果&#xff1a; 原理图&#xff1a; 代码实现&#xff1a; import torch from torch import nn from d2l import torch as d2ldef nin_block(in_channels, out_channels, kernel_size, strides, padding):return nn.…...

数字工厂管理系统如何助力企业数据采集与分析

随着科技的不断进步&#xff0c;数字化已成为企业发展的重要趋势。在制造业领域&#xff0c;数字工厂管理系统的应用日益广泛&#xff0c;它不仅提升了生产效率&#xff0c;更在数据采集与分析方面发挥着举足轻重的作用。本文旨在探讨数字工厂管理系统如何助力企业数据采集与分…...

uniap之微信公众号支付

近来用uniapp开发H5的时候&#xff0c;需要接入支付&#xff0c;原来都是基于后端框架来做的&#xff0c;所以可谓是一路坑中过&#xff0c;今天整理下大致流程分享给大家。 先封装util.js&#xff0c;便于后面调用 const isWechat function(){return String(navigator.userA…...

Django知识点总结

因为最近在搞一个Python项目&#xff0c;使用的Django框架。所以快速学习了一下这个web框架。并做一些总结。 Django官网的介绍&#xff1a;Django is a high-level Python web framework that encourages rapid development and clean, pragmatic design. Built by experience…...

算法(C++

题目&#xff1a;螺旋矩阵&#xff08;59. 螺旋矩阵 II - 力扣&#xff08;LeetCode&#xff09;&#xff09; 给你一个正整数 n &#xff0c;生成一个包含 1 到 n2 所有元素&#xff0c;且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix 。 示例 1&#xff1a; 输入&am…...

Python专题:六、循环语句(1)

补充知识 代码的注释 #描述性文字 阅读代码的人更好的理解代码 while循环语句 x<100条件控制语句&#xff0c;Totalx,Total自增加x&#xff0c;x1&#xff0c;x自增加1&#xff0c;x<100此条件满足时&#xff0c;执行while循环&#xff0c;当x101时&#xff0c;x101条…...

力扣2105---给植物浇水II(Java、模拟、双指针)

题目描述&#xff1a; Alice 和 Bob 打算给花园里的 n 株植物浇水。植物排成一行&#xff0c;从左到右进行标记&#xff0c;编号从 0 到 n - 1 。其中&#xff0c;第 i 株植物的位置是 x i 。 每一株植物都需要浇特定量的水。Alice 和 Bob 每人有一个水罐&#xff0c;最初是…...

Windows设置Redis为开机自启动

前言 Redis作为当前最常用的当前缓存技术&#xff0c;基本上Web应用中都有使用。所以&#xff0c;每次我们在本地启动项目前&#xff0c;都必须将Redis服务端启动。但是&#xff0c;每次都要去启动Redis就很麻烦&#xff0c;有没有办法做到开机自动启动Redis呢&#xff1f;这当…...

后进先出(LIFO)详解

LIFO 是 Last In, First Out 的缩写&#xff0c;中文译为后进先出。这是一种数据结构的工作原则&#xff0c;类似于一摞盘子或一叠书本&#xff1a; 最后放进去的元素最先出来 -想象往筒状容器里放盘子&#xff1a; &#xff08;1&#xff09;你放进的最后一个盘子&#xff08…...

Python爬虫实战:研究MechanicalSoup库相关技术

一、MechanicalSoup 库概述 1.1 库简介 MechanicalSoup 是一个 Python 库,专为自动化交互网站而设计。它结合了 requests 的 HTTP 请求能力和 BeautifulSoup 的 HTML 解析能力,提供了直观的 API,让我们可以像人类用户一样浏览网页、填写表单和提交请求。 1.2 主要功能特点…...

OpenLayers 可视化之热力图

注&#xff1a;当前使用的是 ol 5.3.0 版本&#xff0c;天地图使用的key请到天地图官网申请&#xff0c;并替换为自己的key 热力图&#xff08;Heatmap&#xff09;又叫热点图&#xff0c;是一种通过特殊高亮显示事物密度分布、变化趋势的数据可视化技术。采用颜色的深浅来显示…...

Prompt Tuning、P-Tuning、Prefix Tuning的区别

一、Prompt Tuning、P-Tuning、Prefix Tuning的区别 1. Prompt Tuning(提示调优) 核心思想:固定预训练模型参数,仅学习额外的连续提示向量(通常是嵌入层的一部分)。实现方式:在输入文本前添加可训练的连续向量(软提示),模型只更新这些提示参数。优势:参数量少(仅提…...

Leetcode 3576. Transform Array to All Equal Elements

Leetcode 3576. Transform Array to All Equal Elements 1. 解题思路2. 代码实现 题目链接&#xff1a;3576. Transform Array to All Equal Elements 1. 解题思路 这一题思路上就是分别考察一下是否能将其转化为全1或者全-1数组即可。 至于每一种情况是否可以达到&#xf…...

【git】把本地更改提交远程新分支feature_g

创建并切换新分支 git checkout -b feature_g 添加并提交更改 git add . git commit -m “实现图片上传功能” 推送到远程 git push -u origin feature_g...

数据库分批入库

今天在工作中&#xff0c;遇到一个问题&#xff0c;就是分批查询的时候&#xff0c;由于批次过大导致出现了一些问题&#xff0c;一下是问题描述和解决方案&#xff1a; 示例&#xff1a; // 假设已有数据列表 dataList 和 PreparedStatement pstmt int batchSize 1000; // …...

自然语言处理——Transformer

自然语言处理——Transformer 自注意力机制多头注意力机制Transformer 虽然循环神经网络可以对具有序列特性的数据非常有效&#xff0c;它能挖掘数据中的时序信息以及语义信息&#xff0c;但是它有一个很大的缺陷——很难并行化。 我们可以考虑用CNN来替代RNN&#xff0c;但是…...

三分算法与DeepSeek辅助证明是单峰函数

前置 单峰函数有唯一的最大值&#xff0c;最大值左侧的数值严格单调递增&#xff0c;最大值右侧的数值严格单调递减。 单谷函数有唯一的最小值&#xff0c;最小值左侧的数值严格单调递减&#xff0c;最小值右侧的数值严格单调递增。 三分的本质 三分和二分一样都是通过不断缩…...

LRU 缓存机制详解与实现(Java版) + 力扣解决

&#x1f4cc; LRU 缓存机制详解与实现&#xff08;Java版&#xff09; 一、&#x1f4d6; 问题背景 在日常开发中&#xff0c;我们经常会使用 缓存&#xff08;Cache&#xff09; 来提升性能。但由于内存有限&#xff0c;缓存不可能无限增长&#xff0c;于是需要策略决定&am…...