详解分布式锁

知识点: 

单体锁存在的问题:

  • 单体锁,即单体应用中的锁,通过加单体锁(synchronized或RentranLock)可以保证单个实例并发安全

  • 单体锁是JVM层面的锁,只能保证单个实例上的并发访问安全

  • 如果将单体应用部署到多个tomcat实例上,由负载均衡将请求分发到不同的实例

  • 每个tomocat实例都是一个JVM进程,多实例下会存在数据一致性问题。

分布式锁:

  • 分布式应用中所有线程都去获取同一把锁,但只有一个线程可以成功的获得锁,其他没有获得锁的线程必须全部等待,直到持有锁的线程释放锁。

  • 分布式锁是可以跨越多个tomcat实例,多个JVM进程的锁,所以分布式锁都是设计在第三方组件中的

  • 分布式锁都是通过第三方组件来实现的,目前主流的解决方案是使用Redis或Zookeeper来实现分布式锁

存在的问题:

出现用户超买,商家超卖的问题
具体案例:

添加相关依赖:

        <!--   redis     -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

配置application.yml

# 添加redis数据库
spring:
  redis:
    port: 6379
    database: 1
    host: 127.0.0.1

编写具体实例:

@RestController
@RequiredArgsConstructor
public class LockController {

    private final StringRedisTemplate redisTemplate;

    @SneakyThrows
    @GetMapping("/deductStock")
    public String deductStock() {
        System.out.println("用户正在下单……");

        /**
         * 单体锁
         */
        int total = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
        if (total > 0) {
            total = total - 1;
            Thread.sleep(3000);
            redisTemplate.opsForValue().set("stock", String.valueOf(total));
            System.out.println("下单成功!剩余库存为:" + total);
            return "下单成功!剩余库存为:" + total;
        }
        System.out.println("用户下单失败!");
        return "下单失败!剩余库存为:" + total;
    }
}

测试(点击要快):

我们模拟了系统休眠 ,多线程同时进入一个方法体中,此时,票100同时卖给了两个用户!

解决方案:
 

单体锁:

使用synchronized关键字:修改方法或代码块,用于实现同步控制。当一个线程进入synchronized修饰的方法或代码块时,其他线程需要等待该线程执行完毕后才能进入。

 其中,this关键字指的是,该类的具体实例,即LockController类的具体实例:

@RestController
@RequiredArgsConstructor
public class LockController {

    private final StringRedisTemplate redisTemplate;

    @SneakyThrows
    @GetMapping("/deductStock")
    public String deductStock() {
        System.out.println("用户正在下单……");

        /**
         * 单体锁
         */
        synchronized (this) {
            int total = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
            if (total > 0) {
                total = total - 1;
                Thread.sleep(3000);
                redisTemplate.opsForValue().set("stock", String.valueOf(total));
                System.out.println("下单成功!剩余库存为:" + total);
                return "下单成功!剩余库存为:" + total;
            }
            System.out.println("用户下单失败!");
            return "下单失败!剩余库存为:" + total;
        }
    }
}

测试结果:

虽然单体锁,解决了在同一个类中,多线程进入方法的问题,但是,当LockController并非单例,也会出现超卖现象: 

存在问题:当项目部署到集群服务器中,由反向代理服务器,负载均衡。会导致出现多个LockController实例。

解决方法(使用分布式锁):

分布式锁:

首先创建一个工具类,用于注入静态的组件:

@Component
public class ApplicationContextHolder implements ApplicationContextAware {
    private static ApplicationContext ac;
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.ac = applicationContext;
    }

    public static <T> T getBean(Class<T> clazz){
        return ac.getBean(clazz);
    }

    public static Object getBean(String name){
        return ac.getBean(name);
    }
}

定义一个工具类,用于获取锁,释放锁:

/**
 * 分布式锁工具类
 */
public class LockUtil {
    private static StringRedisTemplate redisTemplate = ApplicationContextHolder.getBean(StringRedisTemplate.class);

    //获取锁的超时时间(自旋重试时间)
    private static long waitTimeout = 10000L;

    //锁的过期时间,防止死锁
    private static long lockTimeout = 10L;

    /**
     * 获取分布式锁
     */
    public static boolean getLock(String lockName, String value) {
        //计算获取锁的超时时间
        long endTime = System.currentTimeMillis() + waitTimeout;
        //超时之前尝试获取锁
        while (System.currentTimeMillis() < endTime) {
            //判断是否能够获取锁,其实就是判断是否往redis中插入对应的key
            Boolean flag = redisTemplate.opsForValue().setIfAbsent(lockName, value, lockTimeout, TimeUnit.SECONDS);
            if (flag) {
                return true;
            }
        }
        return false;
    }

    /**
     * 释放分布式锁
     */
    public static void unlock(String lockName, String value) {
        if(value.equals(redisTemplate.opsForValue().get(lockName))){
            redisTemplate.delete(lockName);
        }
    }
}

使用分布式锁进行加锁:

@RestController
@RequiredArgsConstructor
public class LockController {

    private final StringRedisTemplate redisTemplate;

    @SneakyThrows
    @GetMapping("/deductStock")
    public String deductStock() {
        System.out.println(Thread.currentThread().getName() + "用户正在下单……");

        /**
         * 分布式锁
         */
        String lockName = "stock_lock";
        String value = UUID.randomUUID().toString();
        if (!LockUtil.getLock(lockName, value)) {
            return "获取锁失败……";
        }
        int total = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
        if (total > 0) {
            total = total - 1;
            Thread.sleep(3000);
            redisTemplate.opsForValue().set("stock", String.valueOf(total));
            System.out.println("下单成功!剩余库存为:" + total);
            LockUtil.unlock(lockName,value); //释放锁
            return "下单成功!剩余库存为:" + total;
        }
        System.out.println("用户下单失败!");
        LockUtil.unlock(lockName,value);
        return "下单失败!剩余库存为:" + total;
    }
}

测试结果:

 存在问题:当用户进入后,拿到锁后,执行后续代码,但是锁到期了,锁被释放出来。后续的用户,也是可以进入线程当中的。依旧会出现抄买现象。

解决方法(第三方库来实现分布式锁 ):判断当前用户是否完成后续操作,如果没有完成就自动续签(加时长),直到用户完成后续操作。

Redisson:

Redisson是一个基于Redis的Java驻留对象框架,它提供了一套易于使用的API,用于操作Redis的数据结构和执行分布式操作。

Redisson是Redis官网推荐实现分布式锁的一个第三方类库,用起来更简单。

执行流程:

  • 只要线程加锁成功(默认锁的超时时间为30s),Redisson就会启动一个用于监控锁的看门狗,它是一个守护线程,会每隔10秒检查一下,如果线程还持有锁,就会不断的延长锁的有效期(即每到20s就会自动续借成30s),也称为自动续期机制

  • 当业务执行完,释放锁后,会关闭守护线程。

  • 从而防止了线程业务还没执行完,而锁却过期的问题 。

首先引入相关依赖:

<!-- redisson -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.24.3</version>
</dependency>

编写代码,调用工具类:

@RestController
@RequiredArgsConstructor
public class LockController {

    private final StringRedisTemplate redisTemplate;
    private final RedissonClient redissonClient;

    @SneakyThrows
    @GetMapping("/deductStock")
    public String deductStock() {
        System.out.println(Thread.currentThread().getName() + "用户正在下单……");

        /**
         * 使用Redisson分布式锁
         */
        String lockName = "stock_lock";
        RLock rLock = redissonClient.getLock(lockName);
        rLock.lock(); //获取锁

        int total = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
        if (total > 0) {
            total = total - 1;
            Thread.sleep(3000);
            redisTemplate.opsForValue().set("stock", String.valueOf(total));
            System.out.println("下单成功!剩余库存为:" + total);
            rLock.unlock();
            return "下单成功!剩余库存为:" + total;
        }
        System.out.println("用户下单失败!");
        rLock.unlock();
        return "下单失败!剩余库存为:" + total;
    }
}

测试结果:

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

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

相关文章

消除试卷手写笔迹的软件免费的有哪些?这几款都不错

消除试卷手写笔迹的软件免费的有哪些&#xff1f;在数字化学习的浪潮中&#xff0c;学生们越来越频繁地利用电子设备来完成学习任务。然而&#xff0c;当纸质试卷需要被数字化并再次利用时&#xff0c;如何高效地消除手写笔迹便成为了一个有待解决的问题。那么&#xff0c;今天…

【Linux】简易进度条的实现

&#x1f389;博主首页&#xff1a; 有趣的中国人 &#x1f389;专栏首页&#xff1a; Linux &#x1f389;其它专栏&#xff1a; C初阶 | C进阶 | 初阶数据结构 小伙伴们大家好&#xff0c;本片文章将会讲解Linux中进度条的实现的相关内容。 如果看到最后您觉得这篇文章写得…

coherence protocal基础

一致性整体抽象 coherence protocol是通过保证如下的两点invariants&#xff0c;来实现一致性的&#xff1b; SWMR invariant, 对于任何一个地址&#xff0c;任何时刻&#xff0c;都只会有一个core写&#xff0c;可能会有多个core读&#xff1b;Data-value invariant&#xf…

每天五分钟深度学习:数学中的极值

本文重点 在数学领域中,极值是一个极其重要的概念,它不仅在纯数学理论研究中占据核心地位,而且在工程、物理、经济等实际应用领域也发挥着不可替代的作用。极值问题涉及函数的最大值和最小值,是微积分学中的一个基本问题。本文旨在详细介绍数学中的极值概念、性质、求解方…

Python查询PostgreSQL数据库

哈喽&#xff0c;大家好&#xff0c;我是木头左&#xff01; Python与PostgreSQL的连接 需要了解如何在Python中连接到PostgreSQL数据库。这通常涉及到使用一个库&#xff0c;如psycopg2&#xff0c;它是Python中用于PostgreSQL的最流行的适配器。安装psycopg2非常简单&#x…

202466读书笔记|《一天一首古诗词》——借问梅花何处落,风吹一夜满关山

202466读书笔记|《一天一首古诗词》——借问梅花何处落&#xff0c;风吹一夜满关山 上册下册 《一天一首古诗词》作者李锡琴&#xff0c;蛮早前加入书架的已购买书籍&#xff0c;这次刚好有点时间&#xff0c;利用起来读完。 赏析没有细看&#xff0c;只读了诗词部分&#xff0…

Cocos creator实现《战机长空》关卡本地存储功能

Cocos creator实现《战机长空》关卡本地存储功能 Cocos creator在开放小游戏过程中&#xff0c;经常会出现设置关卡&#xff0c;这里记录一下关卡数据本地存储功能。 一、关卡设置数据 假如我们有关卡数据如下&#xff0c; let settings [ { level: 1, // 第1关 score: 0,…

uniapp的app端推送功能,不使用unipush

1&#xff1a;推送功能使用htmlPlus实现&#xff1a;地址HTML5 API Reference (html5plus.org) 效果图&#xff1a; 代码实现&#xff1a; <template><view class"content"><view class"text-area"><button click"createMsg&q…

Spring框架学习笔记(一):Spring基本介绍(包含IOC容器底层结构)

1 官方资料 1.1 官网 https://spring.io/ 1.2 进入 Spring5 下拉 projects, 进入 Spring Framework 进入 Spring5 的 github 1.3 在maven项目中导入依赖 <dependencies><!--加入spring开发的基本包--><dependency><groupId>org.springframework<…

SpringBoot集成微信支付V3版本流程(商户平台篇)

一、前言 微信支付账号类型分为商户平台和合作伙伴平台&#xff0c;今天主要是梳理商品平台微信支付流程。 商品平台文档地址&#xff0c;(在接入前建议仔细阅读这份文档&#xff0c;会少走很多弯路!!!) 小程序下单 - 小程序支付 | 微信支付商户文档中心 二、接入流程 以下…

qml 和 c++类的数据交互

1、 新建一个需要交互的C++类 1)添加QObject头文件 2)添加自QObject的继承 3)添加Q_OBJECT宏 4)使用Q_PROPERTY,定义两个交互的属性,并设置读写的方法和变更属性的信号。 5)添加方法、槽函数和变量 2、在main.cpp中添加实例化对象的QML上下文 1)添加需要QML交互的…

多个.C文件被编译为一个可执行文件的详细过程

多个.C文件被编译为一个可执行文件的详细过程 文章目录 多个.C文件被编译为一个可执行文件的详细过程前言一、一个.C文件的编译过程二、多个.C文件的链接过程1.文件信息2.链接过程3.makefile 总结 前言 C语言经典的 “hello world ” 程序从编写、编译到运行&#xff0c;看到屏…

html实现网页插入音频

前言 欢迎来到我的博客 个人主页:北岭敲键盘的荒漠猫-CSDN博客 本文主要介绍html中 如何插入音乐和视频 视频插入 标签:<video></video> 兼容格式:mp4,因为别的浏览器都有不兼容的格式&#xff0c;唯一对mp4全都兼容。所以尽量使用mp4格式。 属性: 属性属性值…

249 基于matlab的MED、OMEDA、MOMEDA、MCKD信号处理方法

基于matlab的MED、OMEDA、MOMEDA、MCKD信号处理方法。最小熵反褶积(MED)&#xff0c;最优最小熵反卷积调整卷积 (OMEDA),多点最优最小熵解卷积调整&#xff08;Multipoint Optimal Minimum Entropy Deconvolution Adjusted&#xff0c;MOMEDA&#xff09;&#xff0c;最大相关峭…

【Shell脚本】Shell编程之循环语句

目录 一.循环语句 1.for语句的结构 1.1.格式 1.2.实操案例 案例1. 案例2. 案例3. 案例4. 2.while语句的结构 2.1.格式 2.2.实操案例 案例1. 案例2. 案例3. 案例4. 3.until循环命令 3.1.格式 3.2.实操案例 案例1. 二.补充 1.常用转义符 一.循环语句 1.for…

JAVA 双亲委派之一

JAVA 双亲委派之一 JVM类加载流程 java语言系统内置了众多类加载器&#xff0c;从一定程度上讲&#xff0c;只存在两种不同的类加载器&#xff1a;一种是启动类加载器&#xff0c;此类加载由C实现&#xff0c;是JVM的一部分&#xff1b;另一种就是所有其他的类加载器&#xf…

IF:23.2|从实验室到田间,微生物干预提高植物抗逆

期刊&#xff1a;Nature Food 影响因子&#xff1a;23.2 发表时间&#xff1a;2023年10月 本研究介绍了一种名为SynCom的微生物组合&#xff0c;该组合Rhodococcus erythropolis和Pseudomonas aeruginosa两种微生物组成。这两种微生物能够帮助水稻抵抗铝毒害和磷缺乏&…

springboot3项目练习详细步骤(第二部分:文章分类模块)

新增文章分类 接口文档 业务实现 参数校验 文章分类列表 接口文档 业务实现 获取文章分类详情 接口文档 业务实现 更新文章分类 接口文档 业务实现 分组校验 问题 概念 实现步骤 总结 删除文章分类 接口文档 业务实现 该模块大部分请求的路径相同&…

mac安装禅道

前提已安装&#xff1a;phpapacheMySQL mac安装 php7.1/apache-CSDN博客 安装MySQL 一、禅道下载 安装官方文档 源码包下载地址&#xff1a;禅道下载 - 禅道开源项目管理软件 。 1. 解压禅道源码包 2. 将解压后的文件复制到Apache访问目录下 &#xff08;默认路径为 /Libra…

【进程替换】多进程程序替换原理 | 进程程序替换函数 | execlexecv | execlpexecvp

目录 多进程程序替换 多进程程序替换原理 进程程序替换函数详解 execl&execv execlp&execvp execle&execvpe execve 多进程程序替换 我们想要进程替换的同时不影响旧的进程&#xff08;使用多进程版&#xff09;fork创建子进程&#xff0c;让子进程去替换执…