Redis7-分布式锁

news2024/11/15 19:40:48

目录

基本原理

分布式锁的实现

基于Redis的分布式锁

Redis分布式锁误删

分布式锁的原子性问题

基于Redis的分布式锁优化

Redission概述

Redisson入门

Redisson可重入锁原理

​编辑

Reddisson锁重试和WatchDog机制

Redisson分布式锁原理

Redission的MultiLock原理

分布式锁总结


基本原理

分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁

分布式锁需要满足的条件:

  • 可见性:多个线程都能看到相同的结果,注意:这个地方说的可见性并不是并发编程中指的内存可见性,只是说多个进程之间都能感知到变化
  • 互斥:互斥是分布式锁的最基本的条件,使得程序串行执行
  • 高可用:程序不易崩溃,时时刻刻都保证较高的可用性
  • 高性能:由于加锁本身就让性能降低,对于分布式锁本身需要有较高的加锁性能和释放锁性能
  • 安全性

分布式锁的实现

分布式锁的核心是实现多线程之间互斥,常见的三种实现方式:

基于Redis的分布式锁

实现分布式锁时需要实现的两个基本方法:

1.获取锁

  • 互斥:确保只能有一个线程获取锁

  • 非阻塞:尝试一次,成功返回true,失败返回false

2.释放锁

  • 手动释放

  • 超时释放:获取锁时添加一个超时时间

流程: 

代码实现:

需求:定义一个类,实现下面接口,利用Redis实现分布式锁功能

public class SimpleRedisLock implements ILock{
  private static final String KEY_PREFIX="lock:";

  @Override
  public boolean tryLock(long timeoutSec) {
      //获取线程标示
      String threadId = Thread.currentThread().getId();
      //获取锁
      Boolean success = stringRedisTemplate.opsForValue()
            .setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
      return Boolean.TRUE.equals(success);
  }

  @Override
  public void unlock() {
      //通过del删除锁
      stringRedisTemplate.delete(KEY_PREFIX + name);
  }
}
    @Override
    public Result seckillVoucher(Long voucherId) {
        // 1.查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        // 2.判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            // 尚未开始
            return Result.fail("秒杀尚未开始!");
        }
        // 3.判断秒杀是否已经结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            // 尚未开始
            return Result.fail("秒杀已经结束!");
        }
        // 4.判断库存是否充足
        if (voucher.getStock() < 1) {
            // 库存不足
            return Result.fail("库存不足!");
        }
        Long userId = UserHolder.getUser().getId();
        //创建锁对象(新增代码)
        SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
        //获取锁对象
        boolean isLock = lock.tryLock(1200);
		//加锁失败
        if (!isLock) {
            return Result.fail("不允许重复下单");
        }
        try {
            //获取代理对象(事务)
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
            //释放锁
            lock.unlock();
        }
    }

Redis分布式锁误删

持有锁的线程在锁的内部出现了阻塞,导致它的锁自动释放,这时线程2来尝试获得锁,就拿到了这把锁,然后线程2在持有锁执行过程中,线程1反应过来,继续执行,而线程1执行过程中,走到了删除锁逻辑,此时就会把本应该属于线程2的锁进行删除,这就是误删别人锁的情况说明

解决方案:在每个线程释放锁的时候,去判断一下当前这把锁是否属于自己,如果不属于自己,则不进行锁的删除

需求:

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

1.在获取锁时存入线程标示(可以用UUID表示)

2.在释放锁时先获取锁中的线程标示,判断是否与当前线程标示一致

  • 如果一致则释放锁

  • 如果不一致则不释放锁

代码实现:

获取锁

private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
@Override
public boolean tryLock(long timeoutSec) {
   // 获取线程标示
   String threadId = ID_PREFIX + Thread.currentThread().getId();
   // 获取锁
   Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
   return Boolean.TRUE.equals(success);
}

释放锁

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

分布式锁的原子性问题

线程1现在持有锁之后,在执行业务逻辑过程中,正准备删除锁,而且已经走到了条件判断的过程中,比如它已经拿到了当前这把锁,确实是属于自己的,正准备删除锁,但是此时它的锁到期了,那么此时线程2进来,但是线程1它会接着往后执行,当它卡顿结束后,它直接就会执行删除锁那行代码,相当于条件判断并没有起到作用,这就是删锁时的原子性问题

Lua脚本解决多条命令原子性问题

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

Redis提供的调用函数:

redis.call('命令名称', 'key', '其它参数', ...)

例:执行set name jack

# 执行 set name jack
redis.call('set', 'name', 'jack')

例:先执行set name Rose,再执行get name

# 先执行 set name jack
redis.call('set', 'name', 'Rose')
# 再执行 get name
local name = redis.call('get', 'name')
# 返回
return name

写好脚本以后,需要用Redis命令来调用脚本,调用脚本的常见命令如下:

例:执行 redis.call('set', 'name', 'jack') 这个脚本

如果脚本中的key、value不想写死,可以作为参数传递。key类型参数会放入KEYS数组,其它参数会放入ARGV数组,在脚本中可以从KEYS和ARGV数组获取这些参数:  

Lua脚本实现释放锁:

-- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标示
-- 获取锁中的标示,判断是否与当前线程标示一致
if (redis.call('GET', KEYS[1]) == ARGV[1]) then
  -- 一致,则删除锁
  return redis.call('DEL', KEYS[1])
end
-- 不一致,则直接返回
return 0

利用Java代码调用Lua脚本改造分布式锁

private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    static {
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }

public void unlock() {
    // 调用lua脚本
    stringRedisTemplate.execute(
            UNLOCK_SCRIPT,
            Collections.singletonList(KEY_PREFIX + name),
            ID_PREFIX + Thread.currentThread().getId());
}

总结

基于Redis的分布式锁实现思路:

  • 利用set nx ex获取锁,并设置过期时间,保存线程标示

  • 释放锁时先判断线程标示是否与自己一致,一致则删除锁

特性:

  • 利用set nx满足互斥性

  • 利用set ex保证故障时锁依然能释放,避免死锁,提高安全性

  • 利用Redis集群保证高可用和高并发特性

基于Redis的分布式锁优化

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

Redission概述

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid),它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现

Redission提供了分布式锁的多种多样的功能:

官网:https://redisson.org

Redisson入门

1.引入依赖:

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

2.配置Redisson客户端:

@Configuration
public class RedissonConfig {

    @Bean
    public RedissonClient redissonClient(){
        // 配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        // 创建RedissonClient对象
        return Redisson.create(config);
    }
}

3.使用Redission的分布式锁:

@Resource
private RedissionClient redissonClient;

@Test
void testRedisson() throws Exception{
    //获取锁(可重入),指定锁的名称
    RLock lock = redissonClient.getLock("anyLock");
    //尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
    boolean isLock = lock.tryLock(1,10,TimeUnit.SECONDS);
    //判断获取锁成功
    if(isLock){
        try{
            System.out.println("执行业务");          
        }finally{
            //释放锁
            lock.unlock();
        }
        
    }
    
}

4.在 VoucherOrderServiceImpl注入RedissonClient:

@Resource
private RedissonClient redissonClient;

@Override
public Result seckillVoucher(Long voucherId) {
        // 1.查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        // 2.判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            // 尚未开始
            return Result.fail("秒杀尚未开始!");
        }
        // 3.判断秒杀是否已经结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            // 尚未开始
            return Result.fail("秒杀已经结束!");
        }
        // 4.判断库存是否充足
        if (voucher.getStock() < 1) {
            // 库存不足
            return Result.fail("库存不足!");
        }
        Long userId = UserHolder.getUser().getId();
        //创建锁对象 使用分布式锁
        RLock lock = redissonClient.getLock("lock:order:" + userId);
        //获取锁对象
        boolean isLock = lock.tryLock();
       
		//加锁失败
        if (!isLock) {
            return Result.fail("不允许重复下单");
        }
        try {
            //获取代理对象(事务)
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
            //释放锁
            lock.unlock();
        }
 }

Redisson可重入锁原理

Reddisson锁重试和WatchDog机制

Redisson分布式锁原理

可重入:利用hash结构记录线程id和重入次数

可重试:利用信号量和Pubsub功能实现等待、唤醒,获取锁失败的重试机制

超时续约:利用whtchDog,每隔一段时间(releaseTime / 3),重置超时时间

Redission的MultiLock原理

为了提高redis的可用性,会搭建集群或者主从,以主从为例

此时去写命令,写在主机上, 主机会将数据同步给从机,但是假设主机还没有来得及把数据写入到从机时,此时主机宕机,哨兵会发现主机宕机,并且选举一个slave变成master,而此时新的master中实际上并没有锁信息,锁信息已经丢掉了

为了解决这个问题,Redission提出来了MultiLock锁,使用这把锁就不再使用主从,每个节点的地位都是一样的, 这把锁加锁的逻辑需要写入到每一个主丛节点上,只有所有的服务器都写入成功,此时才是加锁成功,假设现在某个节点挂了,那么它去获得锁的时候,只要有一个节点拿不到,都不能算是加锁成功,就保证了加锁的可靠性

MultiLock加锁原理:

当设置了多个锁时,Redission会将多个锁添加到一个集合中,然后用while循环不停去尝试拿锁,但是会有一个总共的加锁时间,这个时间是用需要加锁的个数 * 1500ms ,假设有3个锁,那么时间就是4500ms,假设在这4500ms内,所有的锁都加锁成功, 那么此时才算是加锁成功,如果在4500ms有线程加锁失败,则会再次去进行重试

分布式锁总结

1.不可重入Redis分布式锁

  • 原理:利用setnx的互斥性;利用ex避免死锁;释放锁时判断线程标示
  • 缺陷:不可重入、无法重试、锁超时失效

2.可重入的Redis分布式锁

  • 原理:利用hash结构,记录线程标示和重入次数;利用watchDog延续锁时间;利用信号量控制锁重试等待
  • 缺陷:Redis宕机引起锁失效问题

3.Redisson的MultiLock

  • 原理:多个独立的Redis节点,必须在所有节点都获取重入锁,才算获取锁成功
  • 缺陷:运维成本高,实现复杂

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

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

相关文章

《江南:在爱开始的地方等你》定档9月15日,愿有情人终成眷属

由康锐原创小说《月落姑苏》改编&#xff0c;并由康锐导演、编剧&#xff0c;赖雨濛、刘冬沁领衔主演、朱丹妮、王沛为、金巧巧、阎青妤、李菁菁、苇青等共同主演的电影《江南&#xff1a;在爱开始的地方等你》官宣定档中秋。 影片讲述了陆骁&#xff08;刘冬沁饰&#xff09;…

Excel VBA批量获取文件夹内文件名及重命名文件教程

在本文中&#xff0c;我们将介绍如何使用Excel VBA宏来批量获取文件夹内的文件名&#xff0c;并将其输出到Excel单元格区域。此外&#xff0c;我们还将展示如何根据Excel中的列表批量重命名这些文件。 一、批量获取文件夹内文件名 首先&#xff0c;我们需要编写一个VBA宏来列出…

【《Kafka 入门指南:从零基础到精通》】

前言&#xff1a; &#x1f49e;&#x1f49e;大家好&#xff0c;我是书生♡&#xff0c;本阶段和大家一起分享和探索KAFKA&#xff0c;本篇文章主要讲述了&#xff1a;消息队列的基础知识&#xff0c;KAFKA消息队列等等。欢迎大家一起探索讨论&#xff01;&#xff01;&#x…

用7EPhone云手机进行TikTok的矩阵运营

“根据市局机构Statista发布的报告显示&#xff0c;截至2024年4月&#xff0c;TikTok全球下载量超过49.2亿次&#xff0c;月度活跃用户数超过15.82亿。TikTok的流量受欢迎程度可想而知&#xff0c;也一跃成为了全球第五大最受欢迎的社交APP。” 人群密集的地方社区也是适合推广…

《pygame游戏开发实战指南》第八节 Sprite类和Group类

Sprite(精灵)是游戏中一个非常重要的概念。在游戏开发中&#xff0c;‌Sprite指的是一个可以移动、‌旋转或变换的二维图像&#xff0c;‌它负责管理游戏中的图像元素&#xff0c;‌使得开发者可以轻松地在游戏中创建各种动态效果和角色。‌pygame.sprite模块提供了Sprite类和G…

分类预测|基于鲸鱼优化-卷积-长短期记忆网络-注意力数据分类预测Matlab程序 WOA-CNN-LSTM-Attention

分类预测|基于鲸鱼优化-卷积-长短期记忆网络-注意力数据分类预测Matlab程序 WOA-CNN-LSTM-Attention 文章目录 前言分类预测|基于鲸鱼优化-卷积-长短期记忆网络-注意力数据分类预测Matlab程序 WOA-CNN-LSTM-Attention 一、WOA-CNN-LSTM-Attention模型1. 鲸鱼优化算法&#xff0…

产品经理的具体职责有哪些?

产品经理作为产品团队的核心角色&#xff0c;负责从概念到市场发布的整个过程&#xff0c;确保产品能够满足用户需求&#xff0c;实现商业目标。其工作职责广泛且深入&#xff0c;涵盖了产品规划、设计、开发、运营、推广等多个方面。以下是详细的工作职责描述以及产品经理的核…

一文彻底搞懂Transformer - 注意力机制

Transformer 一、注意力机制 Seq2Seq 注意力机制目标 Attention模块的主要作用是确定在给定上下文中哪些嵌入向量与当前任务最相关&#xff0c;并据此更新或调整这些嵌入向量的表示。 Transformer注意力机制 注意力机制案例 注意力机制计算公式 生成Q、K、V向量&#xff1a;对…

智能升降晾衣架:NRK3301语音识别模块ic让家务变得更轻松

对于经常做家务的人来说&#xff0c;洗衣服和晾衣服是一件非常耗费体力和时间的任务。传统的晾衣架安装在了一个固定的高度&#xff0c;挂衣服和取衣服需要通过撑衣杆来晾取衣物&#xff0c;即便是电动升降的晾衣架&#xff0c;也需要人手动去操作&#xff0c;增加了工作量。然…

Vue 项目中导入文件时如何默认找寻该文件夹下的 index.vue 文件

文章目录 需求分析 需求 如下图&#xff0c;在Vue 项目中导入 frequencyChange 文件夹时如何默认找寻该文件夹下的 index.vue 文件 分析 确保项目结构和命名约定 首先&#xff0c;确保你的 Vue 单文件组件按照约定命名&#xff0c;例如&#xff1a; components/Example/inde…

Python酷库之旅-第三方库Pandas(080)

目录 一、用法精讲 331、pandas.Series.str.repeat方法 331-1、语法 331-2、参数 331-3、功能 331-4、返回值 331-5、说明 331-6、用法 331-6-1、数据准备 331-6-2、代码示例 331-6-3、结果输出 332、pandas.Series.str.replace方法 332-1、语法 332-2、参数 33…

【QT常用技术讲解】QTableView添加QCheckBox、QPushButton

前言 QT展示列表信息的时候通常用到列表&#xff08;比如用户信息、机构信息、设备信息等菜单&#xff09;&#xff0c;当需要对某列进行修改、删除操作时&#xff0c;就需要加入按钮&#xff08;QPushButton&#xff09;&#xff0c;当需要对多列进行右键菜单操作时&#xff0…

DjangoRF-15-分布式celery应用

前面我们同步实现了测试任务的执行&#xff0c;但是它有一个致命的问题。 实际项目测试任务耗时会非常长&#xff0c;而django框架的请求是有超时的&#xff0c;哪怕没有超时&#xff0c;这么做显然不妥。所以需要使 用异步任务的方式来执行测试任务。 发送一个执行任务的请求&…

沐风老师3DMAX纹理工具箱TexTools使用方法详解

DMAX纹理工具箱TexTools是一组工具,可帮助任何纹理艺术家完成UV和纹理相关任务。主要理念是将典型步骤简化为简单的上下文相关单击。 大多数功能仅在3dMax中处于editUVW模式时才起作用(展开UVW修改器,然后单击编辑按钮)。 【版本要求】 3dMax9及更高版本 【安装方法】 将…

EmbeddedBuilder_v1.4.1.23782 - 在工程中添加自己的C实现文件

文章目录 EmbeddedBuilder_v1.4.1.23782 - 在工程中添加自己的C实现文件概述笔记添加自己的文件夹在文件夹中建立新文件在文件夹中载入已经存在的文件修改工程编译时的包含路径和库路径添加包含路径添加实现路径 在main.c或其他实现中添加自己的头文件引用和自己的函数调用保存…

Seaborn库

目录 主要功能和特点 使用方法 实例应用 Seaborn库的最新版本有哪些新功能和改进&#xff1f; 如何在Seaborn中实现复杂的数据预处理步骤&#xff0c;例如数据清洗和转换&#xff1f; Seaborn与其他数据可视化库&#xff08;如Matplotlib、Plotly&#xff09;相比有哪些优…

【图像去雾系列】使用暗通道先验去雾算法对图像进行去雾处理

目录 一 暗通道先验去雾算法 1 雾形成机理-大气散射模型 2 暗通道先验的整体思想 二 实践 一 暗通道先验去雾算法 论文名称:Single Image Haze Removal Using Dark Channel Prior 论文地址:Single Image Haze Removal Using Dark Channel Prior | IEEE Journals & …

合合信息的OCR技术在智能文档处理方面有哪些具体的应用案例?

智能文档处理(IDP)是利用人工智能技术,自动从复杂的非结构化和半结构化文档中抽取关键数据,并将其转换成结构化数据的技术。能够自动识别、提取并结构化处理文档中的关键信息。这种技术通常基于自然语言处理&#xff08;NLP&#xff09;和计算机视觉等先进技术&#xff0c;可以…

【连续4届EI检索,SPIE 出版】第五届信号处理与计算机科学国际学术会议(SPCS 2024,8月23-25)

第五届信号处理与计算机科学国际学术会议&#xff08;SPCS 2024) 将于2024年8月23-25日在中国哈尔滨举行。会议主要围绕信号处理与计算机科学等研究领域展开讨论。 会议旨在为从事信号处理与计算机科学研究的专家学者、工程技术人员、技术研发人员提供一个共享科研成果和前沿技…

如何使用Wireshake解密Wi-Fi QoS Data报文?

1. 使用Wireshake解密Wi-Fi数据报文 通常当Wi-Fi发生某些问题时&#xff0c;我们都会抓取Wi-Fi sniffer log&#xff0c;用以协助分析问题&#xff0c;但是如果Wi-Fi使用了加密&#xff0c;则我们无法从sniffer log中获取到IP数据的层级&#xff0c;因为在Wi-Fi报文中&#xf…