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;

    @BeforeEach
    void setUp() {
        jedis = new Jedis("127.0.0.1", 6379);
//        jedis.auth("123456");
    }

    @Test
    public void testTryGetDistributedLock() {
        boolean result = RedisCommandLock.tryGetDistributedLock(jedis, "test:lock", UUID.randomUUID().toString(), 300000);
        System.out.println(result);
    }

    @Test
    public 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接口

加锁解锁


    @Test
    public 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();
        }
    }

锁重入


    @Test
    public 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
分段锁

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

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

相关文章

GPT-1

GPT 系列是 OpenAI 的一系列预训练模型&#xff0c;GPT 的全称是 Generative Pre-Trained Transformer&#xff0c;顾名思义&#xff0c;GPT 的目标是通过 Transformer&#xff0c;使用预训练技术得到通用的语言模型。目前已经公布论文的有 GPT-1、GPT-2、GPT-3。 最近非常火的…

腾讯云ubuntu新建用户后,命令行只显示$

这是因为&#xff0c;新建用户命令行解释器默认是sh&#xff0c;需要手动切换为bash&#xff0c;bash可以认为是sh的加强版本。 所以我们只需要将&#xff0c;shell切换为bash就好了。 切换到root 修改配置文件 vim/etc/bash 将sh修改为bash

AcWing 3194:最大的矩形 ← 笛卡尔树

【题目来源】https://www.acwing.com/problem/content/3197/【题目描述】 在横轴上放了 n 个相邻的矩形&#xff0c;每个矩形的宽度是 1&#xff0c;而第 i&#xff08;1≤i≤n&#xff09;个矩形的高度是 hi。 这 n 个矩形构成了一个直方图。 例如&#xff0c;下图中六个矩形的…

类和对象【四】运算符重载

文章目录 运算符重载的概念运算符重载&#xff08;函数&#xff09;返回值类型&#xff1a;任意类型函数名&#xff1a;operator已有操作符 运算符重载&#xff08;函数&#xff09;的特点和注意点3个比较特殊的运算符重载赋值运算符&#xff08;&#xff09;重载返回值类型和返…

Linux CentOS7部署ASP.NET Core应用程序,并配置Nginx反向代理服务器和Supervisor守护服务

前言&#xff1a; 本篇文章主要讲解的是如何在Linux CentOS7操作系统搭建.NET Core运行环境并发布ASP.NET Core应用程序&#xff0c;以及配置Nginx反向代理服务器。因为公司的项目一直都是托管在Window服务器IIS上&#xff0c;对于Linux服务器上托管.NET Core项目十分好奇。因为…

简单学生信息管理系统

简单&#xff0c;单表&#xff1b; https://download.csdn.net/download/bcbobo21cn/89251742

【QT学习】12.UDP协议,广播,组播

一。Udp详细解释 UDP&#xff08;User Datagram Protocol&#xff09;是一种无连接的传输层协议&#xff0c;它提供了一种简单的、不可靠的数据传输服务。与TCP相比&#xff0c;UDP不提供可靠性、流量控制、拥塞控制和错误恢复等功能&#xff0c;但由于其简单性和低开销&#x…

Java | Leetcode Java题解之第64题最小路径和

题目&#xff1a; 题解&#xff1a; class Solution {public int minPathSum(int[][] grid) {if (grid null || grid.length 0 || grid[0].length 0) {return 0;}int rows grid.length, columns grid[0].length;int[][] dp new int[rows][columns];dp[0][0] grid[0][0]…

《罪与罚》读后感

陀思妥耶夫斯基和列夫托尔斯泰是公认的俄国文学黄金时代的两座高峰&#xff0c;分别代表着俄国文学的“深度”和“广度”。列夫托尔斯泰的鸿篇巨著《复活》《安娜卡列尼娜》等等都已经拜读过&#xff0c;但陀思妥耶夫斯基的作品却一本也没有看过&#xff0c;实在是有点遗憾。这…

LabVIEW换智能仿真三相电能表研制

LabVIEW换智能仿真三相电能表研制 在当前电力工业飞速发展的背景下&#xff0c;确保电能计量的准确性与公正性变得尤为重要。本文提出了一种基于LabVIEW和单片机技术&#xff0c;具有灵活状态切换功能的智能仿真三相电能表&#xff0c;旨在通过技术创新提高电能计量人员的培训…

Flutter笔记:谈Material状态属性-为什么FlatButton等旧版按钮就废弃了

Flutter笔记 谈Material状态属性-为什么FlatButton等旧版按钮就废弃了 - 文章信息 - Author: 李俊才 (jcLee95) Visit me at CSDN: https://jclee95.blog.csdn.netMy WebSite&#xff1a;http://thispage.tech/Email: 291148484163.com. Shenzhen ChinaAddress of this artic…

MySQL-逻辑架构

1、MySQL服务器处理客户端请求 MySQL是典型的C/S架构&#xff0c;服务端程序使用 mysqld。实现效果&#xff1a;客户端进程像服务端发送&#xff08;SQL语句&#xff09;&#xff0c;服务器进程处理后再像客户端进程发送 处理结果。 2、connectors 指不同语言中与SQL的交互…

Vue3+ts(day05:ref、props、生命周期、hook)

学习源码可以看我的个人前端学习笔记 (github.com):qdxzw/frontlearningNotes 觉得有帮助的同学&#xff0c;可以点心心支持一下哈&#xff08;笔记是根据b站上学习的尚硅谷的前端视频【张天禹老师】&#xff0c;记录一下学习笔记&#xff0c;用于自己复盘&#xff0c;有需要学…

OpenMLDB v0.9.0 发布:SQL 能力大升级覆盖特征上线全流程

发布日期 25 April 2024 Release note https://github.com/4paradigm/OpenMLDB/releases/tag/v0.9.0 亮点特性 增加最新版 SQLAlchemy 2 的支持&#xff0c;无缝集成 Pandas 和 Numpy 等常用 Python 框架。支持更多数据后端&#xff0c;融合 TiDB 的分布式文件存储能力以及…

【Redis】Redis安装、配置、卸载使用可视化工具连接Redis

文章目录 1.前置条件2.安装Redis2.1下载Redis安装包并解压2.2在redis目录下执行make命令2.3修改Redis配置文件2.4启动Redis服务2.5连接redis服务 3.Redis卸载4.使用可视化工具连接Redis 1.前置条件 Linux操作系统需要要是64位.如果不清楚自己Linux上是多少位的,可以使用以下命…

详解 Go 程序的启动流程,你知道 g0,m0 是什么吗?

自古应用程序均从 Hello World 开始&#xff0c;你我所写的 Go 语言亦然&#xff1a; import "fmt"func main() {fmt.Println("hello world.") }这段程序的输出结果为 hello world.&#xff0c;就是这么的简单又直接。但这时候又不禁思考了起来&#xff0…

vue3 ——笔记 (表单输入,监听器)

表单输入 在Vue 3中&#xff0c;v-model指令的用法稍有不同于Vue 2。在Vue 3中&#xff0c;v-model指令实际上是一个语法糖&#xff0c;它会自动将value属性和input事件绑定到组件或元素上&#xff0c;以实现双向数据绑定。 在自定义组件中使用v-model时&#xff0c;需要在组…

SQL注入漏洞--报错/union/布尔盲注/时间盲注

之前介绍了数据库的基本操作&#xff0c;今天这篇文章就来实操SQL注入。 阅读本文前可以先看一下基本操作&#xff0c;有助于更好理解本文。。。 https://blog.csdn.net/weixin_60885144/article/details/138356410?spm1001.2014.3001.5502 what SQL---结构化查询语言---S…

Codeforces Round 943 (Div. 3) (A-G1) C++题解

目录 比赛链接 : A. Maximize? B. Prefiquence C. Assembly via Remainders D. Permutation Game E. Cells Arrangement F. Equal XOR Segments G1. Division LCP (easy version) G2. Division LCP (hard version) 比赛链接 : Dashboard - Codeforces Round 943 (…

用vim或gvim编辑程序

vim其实不难使用&#xff0c;学习一下就好了。简单功能很快学会。它有三种模式&#xff1a;命令模式&#xff0c;编辑模式&#xff0c;视模式。打开时在命令模式。在命令模式下按 i 进入编辑模式&#xff0c;在编辑模式下按<Esc>键退出编辑模式。在命令模式按 :wq 保存文…