分布式锁Redis基础理论与落地实现与Redisson。

news2025/1/12 0:01:38

分布式锁Redis基础理论与落地实现

    • 基本概念
    • 基于Redis的分布式锁
      • 基本用法
      • 基于Redis实现分布式锁初级版本
      • 改进Redis的分布式锁
        • 问题
        • Redis的Lua脚本
        • 利用Lua脚本写释放锁业务流程
        • 再次改进Redis的分布式锁
      • 总结
    • Redisson
      • 基于setnx实现的分布式锁存在下面的问题
      • Redisson入门
        • Redisson可重入锁原理
        • Redisson锁重试原理和Watch Dog机制
    • 结尾

基本概念

什么是分布式锁
满足分布式系统或者集群模式下多进程可见并且互斥的锁。
请添加图片描述
不同分布式锁的实现方案
分别从互斥,高可用,高性能,安全性方面来分析。请添加图片描述

基于Redis的分布式锁

基本用法

获取锁:

  • 互斥:确保只能有一个县城获取锁。
  • 非阻塞:尝试一次,成果返回true,失败返回false。
    • 超时释放:获取锁时添加一个超时时间。

SETNX lock thread1 # 添加锁,利用setnx的互斥特性。
EXPIRE lock 10 # 添加锁过期时间,避免服务宕机引起的死锁。

释放锁:

  • 手动释放

DEL key # 释放锁,删除即可。

上面的方式有一个问题:如果在获取锁时,只来得及执行第一句后,就宕机了,过期时间没执行怎么办?

答:将这两个动作设置为原子动作,使用SET命令:SET lock thread1 EX 10 NX

流程图如下:
请添加图片描述

基于Redis实现分布式锁初级版本

先写一个接口:

public interface ILock {
    /**
     * 尝试获取锁
     * @param timeoutSec  锁持有的超时时间,过期后自动释放。
     * @return  true 代表获取锁成功,false代表获取锁失败。
     */
    boolean tryLock(long timeoutSec);

    void unlock();

}

再用redis实现一个锁:

import com.wang.Base.ILock;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.util.concurrent.TimeUnit;

public class SimpleRedisLock implements ILock {

    private static final String KEY_PRIFIX = "lock";
    private String name;
    private StringRedisTemplate stringRedisTemplate;

    public SimpleRedisLock(String name ,StringRedisTemplate stringRedisTemplate){
        this.name= name;
        this.stringRedisTemplate = stringRedisTemplate;
    }
    @Override
    public boolean tryLock(long timeoutSec) {
        //获取线程标示
        long threadId = Thread.currentThread().getId();
        //获取锁
        Boolean success = stringRedisTemplate.opsForValue().
                setIfAbsent(KEY_PRIFIX + name, threadId+"", timeoutSec, TimeUnit.SECONDS);
       // return success; 最好不要这样写,因为boolean如果为null,一拆箱就空指针了。
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
         stringRedisTemplate.delete(KEY_PRIFIX+name);
    }
}

上面代码存在一个问题:释放锁的时候并没有判断锁是不是自己的,那么在一些极端情况下,会出现问题,下面举个例子。
线程1因为业务阻塞的时间超过了锁持有的时间,在未完成业务的情况下释放了。
那么线程2此时获取到了锁,在线程2执行业务的期间线程1的业务完成,直接释放了锁。
那么此时线程3拿到了锁,开始了执行业务。
请添加图片描述
解决方法:
在释放锁前,判断一下锁标示是否是自己的。请添加图片描述

改进Redis的分布式锁

需求:
修改之前的分布式锁实现,满足:

  1. 在获取锁时存入线程标识(可以用UUID表示,用UUID确保不同的服务,线程id确保不同的线程,确保不同线程标示一定不一样。)
  2. 在释放锁时先获取锁中的线程标示,判断是否与当前线程标示一致:
  • 如果一致则释放锁。
  • 如果不一致则不释放锁。

优化如下代码所示:

import java.util.UUID;
import java.util.concurrent.TimeUnit;

public class SimpleRedisLock implements ILock {

    private static final String KEY_PRIFIX = "lock";
    private static  final String ID_PREFIX = UUID.randomUUID().toString()+"-";
    private String name;
    private StringRedisTemplate stringRedisTemplate;

    public SimpleRedisLock(String name ,StringRedisTemplate stringRedisTemplate){
        this.name= name;
        this.stringRedisTemplate = stringRedisTemplate;
    }
    @Override
    public boolean tryLock(long timeoutSec) {
        //获取线程标示
        String threadId = ID_PREFIX +Thread.currentThread().getId();
        //获取锁
        Boolean success = stringRedisTemplate.opsForValue().
                setIfAbsent(KEY_PRIFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
       // return success; 最好不要这样写,因为boolean如果为null,一拆箱就空指针了。
        return Boolean.TRUE.equals(success);
    }
    @Override
    public void unlock() {
        //获取线程标示
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        //获取锁中的标示
        String id = stringRedisTemplate.opsForValue().get(KEY_PRIFIX + name);
        //判断标示是否一致
        if(threadId.equals(id)){
            stringRedisTemplate.delete(KEY_PRIFIX+name);
        }
    }
}

问题

那么那么,上面的代码就是完美无缺的吗?

答:不是的,在更极端的条件下,依旧会出现问题,现在我们假设一个场景。
线程1正常执行了自己的业务完成后,获取锁标示并判断是否一致后,发生了阻塞(这个阻塞并不是代码层面,而是JVM层面,当垃圾回收执行到了Full GC的时,因为JVM发生阻塞),如果这个阻塞的时间超过了锁的过期时间,那么其他线程就可以趁虚而入了。
这时线程2获取到了锁,执行自己的业务。
那么线程1跳出阻塞,会直接把线程2的锁释放掉。
线程3就会趁虚而入。

如何避免上面的问题发生呢?
解决方法:
要把判断和释放并为原子性。
这里redis 的事务可以保证原子性,但是无法保证一致性。所以在这里先查询再判断是不行的,拿不到结果,是最后一次性执行的。没有办法把它们放在一个事务中。可以利用乐观锁解决,但是会很麻烦。
这里推荐使用Lua脚本去做。

Redis的Lua脚本

Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言。

  • Lua中使用Redis:
    Redis在Lua里面给我们提供了一个函数:redis.call(‘命令名称’,‘key’,‘其他参数’)。

  • Redis的Lua脚本:
    写好脚本以后,需要用Redis命令来调用脚本,调用脚本的常见命令如下:
    EVAL script numkeys key [key] arg [arg]

  1. 无参数
    例如我们要执行redis.call('set','name','jack')这个脚本,语法如下: EVAL “return redis.call(‘set’,‘name’,‘jacl’)” 0 `
    请添加图片描述
  2. 有参数
    如果脚本中的key,value不写死,可以作为参数传递。
    key类型参数会放入KEYS数组,其他参数会放入ARGV数组,在脚本中可以从KEYS和ARGV数组获取这些参数:
    EVAL "return redis.call('set',KEYS[1], ARGV[1] ) " 1 name wang
    请添加图片描述

利用Lua脚本写释放锁业务流程

  1. 获取锁中的线程标示。
  2. 判断是否与指定的标示(当前线程标示)一致。
  3. 如果一致则释放锁(删除)。
  4. 如果不一致则什么都不做。
-- 锁的key
local key = KEYS[1]
-- 当前线程标示
local threadId = ARGV[1]
-- 获取锁中的线程标示 get key
local id = redis.call('get',key)
-- 比较线程标示与锁中标示是否一致
if (id == threadId) then
    -- 释放锁 del key
    return redis.call('del',key)
end
return 0

简化写法:

local id = redis.call('get',KEYS[1])
-- 比较线程标示与锁中标示是否一致
if (id == ARGV[1]) then
    -- 释放锁 del key
    return redis.call('del',KEYS[1])
end
return 0

再次改进Redis的分布式锁

需求:基于Lua脚本实现分布式锁的释放锁逻辑
提示:RedisTemplate调用Lua脚本的API如下:
请添加图片描述
请添加图片描述
在idea中写lua文件:
请添加图片描述
对锁的实现进行改造:

    private static final String KEY_PRIFIX = "lock";
    private static  final String ID_PREFIX = UUID.randomUUID().toString()+"-";

    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
      //不要每次释放的时候读取文件,要提前读取好。
    static {
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }
    private String name;
    private StringRedisTemplate stringRedisTemplate;

    public SimpleRedisLock(String name ,StringRedisTemplate stringRedisTemplate){
        this.name= name;
        this.stringRedisTemplate = stringRedisTemplate;
    }
    @Override
    public boolean tryLock(long timeoutSec) {
        //获取线程标示
        String threadId = ID_PREFIX +Thread.currentThread().getId();
        //获取锁
        Boolean success = stringRedisTemplate.opsForValue().
                setIfAbsent(KEY_PRIFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
       // return success; 最好不要这样写,因为boolean如果为null,一拆箱就空指针了。
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {

//        //获取线程标示
//        String threadId = ID_PREFIX + Thread.currentThread().getId();
//        //获取锁中的标示
//        String id = stringRedisTemplate.opsForValue().get(KEY_PRIFIX + name);
//        //判断标示是否一致
//        if(threadId.equals(id)){
//            stringRedisTemplate.delete(KEY_PRIFIX+name);
//        }

        //调用Lua脚本
  
        stringRedisTemplate.execute(UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PRIFIX+name),
                ID_PREFIX+Thread.currentThread().getId()
                );
    }

总结

请添加图片描述
那么,目前为止,我们的分布式锁已经相对完善了,当然好学的你肯定不满足于当前的现状,那么就继续走下去吧。

Redisson

基于setnx实现的分布式锁存在下面的问题

  1. 不可重入
    同一个线程无法多次获取同一把锁。
  2. 不可重试
    获取锁只尝试一次就返回false,没有重试机制。
  3. 超时释放
    锁超时释放虽然可以避免思索,但如果业务执行耗时较长,也会导致锁释放,存在安全隐患。
  4. 主从一致性
    如果Redis提供了主从集群,主从同步存在延迟,当主宕机时,如果从并去同步主中的锁数据,则会出现锁实现。

Redisson入门

  1. 引入依赖
    <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.13.6</version>
        </dependency>
  1. 配置Redisson客户端:
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RedisConfig {
   @Bean
   public RedissonClient redissonClient() {
       //配置类
       Config config = new Config();
       //添加redis地址,这里添加了单点地址,也可以使用config.useClusterServers()添加集群地址
       config.useSingleServer().setAddress("redis://47.115.226.111:6379").setPassword("123321");
       //创建客户端
       return Redisson.create(config);
   }
}
   @Resource
    private RedissonClient redissonClient;
    
    public void testRedisson() throws InterruptedException {
        //获取锁(可重入),指定锁的名称。
        RLock lock = redissonClient.getLock("anyLock");
        //尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位。
        boolean isLock = lock.tryLock(1,10,TimeUnit.SECONDS);
        //判断释放获取成功
        if(isLock){
            try{
                System.out.println("执行业务");
            }finally {
                //释放锁
                lock.unlock();
            }
        }
    }

Redisson可重入锁原理

我们先举一个场景,代码如下:

@Slf4j
@SpringBootTest
class RedissonTest {

    @Resource
    private RedissonClient redissonClient;

    private RLock lock;

    @BeforeEach
    void setUp() {
        lock = redissonClient.getLock("order");
    }

    @Test
    void method1() throws InterruptedException {
        // 尝试获取锁
        boolean isLock = lock.tryLock(1L, TimeUnit.SECONDS);
        if (!isLock) {
            log.error("获取锁失败 .... 1");
            return;
        }
        try {
            log.info("获取锁成功 .... 1");
            method2();
            log.info("开始执行业务 ... 1");
        } finally {
            log.warn("准备释放锁 .... 1");
            lock.unlock();
        }
    }
    void method2() {
        // 尝试获取锁
        boolean isLock = lock.tryLock();
        if (!isLock) {
            log.error("获取锁失败 .... 2");
            return;
        }
        try {
            log.info("获取锁成功 .... 2");
            log.info("开始执行业务 ... 2");
        } finally {
            log.warn("准备释放锁 .... 2");
            lock.unlock();
        }
    }

如果我们用redis来做的话,不能实现可重入锁,原因如下:

  • 首先我们来看一下这个流程请添加图片描述
  • 数据结构请添加图片描述
    那么这样的数据结构不支持可重入锁的实现,因为我们没有字段去记录重入的次数,只能判断是否为自己的锁。

redisson的数据结构可以支持,它的数据结构如下:
请添加图片描述
它的流程如下:
请添加图片描述
redisson底层是通过Lua脚本去实现的,具体如下所示:

local key = KEY[1]; -- 锁的key
local threadId = ARGV[1]; -- 线程的唯一标识
local releaseTime = ARGV[2]; -- 锁的自动释放时间

-- 判断当前锁是否还是被自己持有
if (redis.call('HEXISTS',key,threadId)==0) then
  return nil; --如果已经不是自己,则直接返回
end;

-- 是自己的锁,则重入次数-1
local count = redis.call('HINCRBY’,key ,threadId,-1);
-- 判断是否重入次数是否已经为0
if (count > 0) then
 -- 大于0说明不能释放锁,重置有效期然后返回
 redis.call('EXPIRE',key,releaseTime);
 return nil;
else  -- 等于0说明可以释放锁,直接删除
  redis.call('DEL',key);
  return nil;
end;

Redisson锁重试原理和Watch Dog机制

结尾

准备面试八股没时间,有时间再补上吧。

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

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

相关文章

64位系统究竟牛逼在哪里?

想必大家都遇到过这样的问题&#xff1a;安装某个软件的时候&#xff0c;出现提示选择32位版本还是64位版本&#xff1f;我们也可以查看自己的电脑是32位还是64位系统。 Windows Linux 大家可能知道32位和64位和系统有关&#xff0c; 但其实 32 vs 64 可以有多重含义。 一般情…

JVM学习笔记(上)

1、总体路线 2、程序计数器 Program Counter Register 程序计数器&#xff08;寄存器&#xff09; 作用&#xff1a;是记录下一条 jvm 指令的执行地址行号。 特点&#xff1a; 是线程私有的不会存在内存溢出 解释器会解释指令为机器码交给 cpu 执行&#xff0c;程序计数器会…

GCC写个库给你玩,就这?

前言 什么是GCC GCC原名为 GNU C语言编译器 「GCC」(GNU Compiler Collection,GNU编译套件) 是由GNU开发的编程语言编译器。 正文 安装命令 sudo apt-get insatll gcc g注意安装版本要大于4.8.5因为4.8.5以后的版本才支持c11标准 查看版本 gcc -v gcc --version g -v g …

Vue.js 的数据双向绑定实现原理

Vue.js 的数据双向绑定实现原理 Vue.js 是一款流行的前端框架&#xff0c;它采用了数据双向绑定的方式&#xff0c;让前端开发人员更加方便地管理数据和视图。在本文中&#xff0c;我们将深入探讨 Vue.js 的数据双向绑定实现原理&#xff0c;以及相关的代码示例。 数据双向绑定…

1. TensorRT量化的定义及意义

前言 手写AI推出的全新TensorRT模型量化课程&#xff0c;链接&#xff1a;TensorRT下的模型量化。 课程大纲如下&#xff1a; 1. 量化的定义及意义 1.1 什么是量化&#xff1f; 定义 量化(Quantization)是指将高精度浮点数(如float32)表示为低精度整数(如int8)的过程&…

jmeter性能测试步骤实战教程

1. Jmeter是什么&#xff1f; 2. Jmeter安装 2.1 JDK安装 由于Jmeter是基于java开发&#xff0c;首先需要下载安装JDK &#xff08;目前JMeter只支持到Java 8&#xff0c;尚不支持 Java 9&#xff09; 1. 官网下载地址&#xff1a; http://www.oracle.com/technetwork/java/…

Map、Set和哈希表的应用练习(数据结构系列15)

目录 前言&#xff1a; 练习题&#xff1a; 结束语&#xff1a; 前言&#xff1a; 在上一节博客中小编给大家介绍了Map、Set和哈希表的一些简单的知识点&#xff0c;同时也给大家简单的演示了一下如何使用他们里面的一些基础方法&#xff0c;那么接下来让小编带着你们一起来…

当心!经济学家分析:未来三年内做好随时失业的准备

AI人工智能又来抢饭碗了&#xff0c;这次竟然通过了公认难考的会计行业考试&#xff01; 近期&#xff0c;OpenAI的大语言模型最新版GPT-4已经完成美国注册会计师&#xff08;简称CPA&#xff09;考试&#xff0c;四大主要会计考试所有科目的平均得分为85.1。 而在CPA考试中&…

落地页设计的营销心理学(三)

本文是「落地页设计的营销心理学」这个主题系列文章的收官篇&#xff0c;要给大家分享关于用户行动号召、提高用户参与度和整个营销落地页结构的设计。 回顾系列文章&#xff1a; 《落地页设计的营销心理学&#xff08;一&#xff09;》 《落地页设计的营销心理学&#xff08…

C++进阶 —— 线程库(C++11新特性)

十&#xff0c;线程库 thread类的简单介绍 在C11之前涉及多线程问题&#xff0c;都是和平台相关的&#xff0c;如windows和Linux下各有自己的接口&#xff0c;这使代码的可移植性较差&#xff1b;C11中最重要的特性就是对线程进行支持&#xff0c;使得C在并行编程时不需要依赖…

【社区图书馆】《写作脑科学》

文章目录 前言语言和思维写作技巧创造性思维总结 前言 杨滢著的《写作脑科学》是一本关于写作的科学读物&#xff0c;它深入探讨了人类大脑是如何进行创造性思维和表达的。这本书让我对写作有了全新的认识&#xff0c;也为我提供了一些实用的技巧和策略来提高自己的写作能力。…

整理 钢琴教材 约翰·汤普森现代钢琴教程(大汤)

邮箱不能及时回复,现放到网盘里了,文末按需自取 约翰-汤普森钢琴教程1 文件名:(大汤1)约翰汤普森现代钢琴教程 1 超清PDF 文件大小:9.9 MB 下载地址:https://download.csdn.net/download/qq_36040764/85051148 约翰-汤普森钢琴教程2 文件名:(大汤2)约翰汤普森现…

Python3中goto的用法

Python3代码指定跳转可以使用goto这个库&#xff1a; 安装&#xff1a; pip install goto-statement 一般安装的版本是1.2 需要做以下修改才能正常使用&#xff1a; python 使用goto&#xff0c;遇到的问题解决_奶嘴偷走初吻的博客-CSDN博客python goto 出现报错:Attribut…

Python difflib的使用

今天做了一个从list的内容取出一个与指定内容尽可能相似的内容,做完之后抽个几分钟记录下 difflib的作用 比对2个文件的差异. 使用的时候直接 import difflib 即可 get_close_matches 作用 匹配最大相似的内容返回结果 list1 ["abc", "acd", "…

NIO编程

目录 1、什么是NIO编程&#xff1f; 为什么说Java NIO是非阻塞的&#xff1f; 2、Java NIO 通道(Channel)详解 如何获取Channel对象&#xff1f; 3、Java NIO 缓冲区(Buffer)详解 &#xff08;1&#xff09;获取缓冲区对象 &#xff08;2&#xff09;将数据写入Buffer以…

没学过编程,本科学历,Java学到什么程度才能找工作?

好程序员之前写过多篇Java找工作方面的文章&#xff0c;今天说说零Java基础找工作的事情。首先请大家明确如下的要点。 1、在没有真实Java工作项目经验的前提下&#xff0c;靠自学&#xff0c;哪怕到培训班学&#xff0c;一定是无法真正掌握到能干Java项目的地步&#xff0c;原…

SpringData 基础篇

Spring Data 故事背景一&#xff1a;基础概念1.1 什么是SpringData1.2 为什么要用SpringData 二&#xff1a;JPA与Hibernate、MyBatis关系2.1 JPA与JDBC2.1.1 特点2.1.2 JPA规范提供2.1.3 JDBC的不足 2.2 Hibernate与JPA2.2.1 关系 2.3 mybatis 和Hibernate 三&#xff1a;Hibe…

裁剪与复原

目录 模型假设 模型建立 模型求解 通过建立匹配模型实现对破碎文件的拼接复原。 模型假设 模型建立 首先对每个图片按像素值进行二值化量化&#xff0c;可以得到19个1980*72的矩阵&#xff0c;再提取每个举证最左和最右的像素值采用绝对距离法建立像素匹配模型。 二值化是图…

大数据时代——生活、工作与思维的重大变革

最近读了维克托迈尔 – 舍恩伯格的《大数据时代》&#xff0c;觉得有不少收获&#xff0c;让我这个大数据的小白第一次理解了大数据。 作者是大数据的元老级先驱。 放一张帅照&#xff0c;膜拜下。 不过这本书我本人不推荐从头读一遍&#xff0c;因为书中的核心理念并不是特…

Django实现接口自动化平台(二)认证授权登录【持续更新中】

上一章&#xff1a; Django实现接口自动化平台&#xff08;一&#xff09;日志功能【持续更新中】_做测试的喵酱的博客-CSDN博客 下一章&#xff1a; Django实现接口自动化平台&#xff08;三&#xff09;实现注册功能【持续更新中】_做测试的喵酱的博客-CSDN博客 一、认证与…