详解分布式锁

news2025/1/11 20:46:44

知识点: 

单体锁存在的问题:

  • 单体锁,即单体应用中的锁,通过加单体锁(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;
    }
}

测试结果:

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

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

相关文章

vue2结合element-ui实现TreeSelect 树选择功能

需求背景 在日常开发中&#xff0c;我们会遇见很多不同的业务需求。如果让你用element-ui实现一个 tree-select 组件&#xff0c;你会怎么做&#xff1f; 这个组件在 element-plus 中是有这个组件存在的&#xff0c;但是在 element-ui 中是没有的。 可能你会直接使用 elemen…

nginx配置文件和配置命令详解案例

一.nginx.conf配置结构 1.1配置结构图 1.2 nginx中配置nginx.conf配置内容 #user nobody; user root; # 表示worker进程是有root用户去执行的 worker_processes 2; events {# 默认使用epolluse epoll;# 每个worker链接最大数据worker_connections 1024; } http {include …

Java入门基础学习笔记14——数据类型转换

类型转换&#xff1a; 1、存在某种类型的变量赋值给另一种类型的变量&#xff1b; 2、存在不同类型的数据一起运算。 自动类型转换&#xff1a; 类型范围小的变量&#xff0c;可以直接赋值给类型范围大的变量。 byte类型赋值给int类型&#xff0c;就是自动类型转换。 pack…

【MySQL基本查询(下)】

文章目录 一、update案例 二、Delete案例注意&#xff1a;delete 全表数据的行为慎用&#xff01;truncate 三、插入查询结果案例 四、了解一些函数1.count函数2.sum函数3. avg函数4.max函数5. min函数 五、group by子句的使用案例having和where 一、update 该关键字的功能就是…

基于SpringBoot的全国风景区WebGIS按省展示实践

目录 前言 一、全国风景区信息介绍 1、全国范围内数据分布 2、全国风景区分布 3、PostGIS空间关联查询 二、后台查询的设计与实现 1、Model和Mapper层 2、业务层和控制层设计 三、WebGIS可视化 1、省份范围可视化 2、省级风景区可视化展示 3、成果展示 总结 前…

C++ 抽象与封装

一 抽象 抽象实例&#xff1a;时钟 数据抽象&#xff1a; 具有表面当前时间的时、分、秒 行为抽象&#xff1a; 具有设置时间和显示时间两个最基本的功能。 抽象实例&#xff1a;人 数据抽象&#xff1a;姓名、年龄、性别等。 行为抽象&#xff1a; 生物属性&#xff1a;吃…

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

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

Java类加载器介绍

在Java中&#xff0c;类加载器是一种动态加载类的机制&#xff0c;它负责在运行时查找、加载和链接类文件。当Java应用程序需要创建某个类的对象时&#xff0c;类加载器会在运行时查找该类对应的.class文件&#xff0c;并将其加载到Java虚拟机中。Java类加载器通常分为三层&…

【MQTT】mosquitto 的 “下载、交叉编译、使用” 详细教程,手把手搭建一个MQTT Broker

&#x1f601;博客主页&#x1f601;&#xff1a;&#x1f680;https://blog.csdn.net/wkd_007&#x1f680; &#x1f911;博客内容&#x1f911;&#xff1a;&#x1f36d;嵌入式开发、Linux、C语言、C、数据结构、音视频&#x1f36d; ⏰发布时间⏰&#xff1a;2024-05-11 0…

系统架构设计师 - 计算机组成与体系结构(1)

计算机组成与体系结构 计算机组成与体系结构计算机结构 ★CPU 组成结构运算器组成控制器组成 计算机体系结构冯诺依曼结构哈弗结构 嵌入式芯片&#xff08;了解&#xff09; 存储系统 ★★★★概述Cache主存编址磁盘管理磁盘基本结构与存取过程磁盘优化分布存储磁盘管理 大家好…

C++:多态-重写和重载

重写&#xff08;Override&#xff09;和重载&#xff08;Overload&#xff09;是面向对象编程中常用的两个概念&#xff0c;它们虽然都涉及到方法的定义&#xff0c;但是在实现和使用上有着不同的特点。 重写&#xff08;Override&#xff09;&#xff1a; 重写是指在子类中重…

部署xwiki服务需要配置 hibernate.cfg.xml如何配置?

1. 定位 hibernate.cfg.xml 文件 首先&#xff0c;确保您可以在 Tomcat 的 XWiki 部署目录中找到 hibernate.cfg.xml 文件&#xff1a; cd /opt/tomcat/latest/webapps/xwiki/WEB-INF ls -l hibernate.cfg.xml如果文件存在&#xff0c;您可以继续编辑它。如果不存在&#xff…

Go语言系统学习笔记(一):基础篇

1. 写在前面 公司的新业务开发需要用到go语言&#xff0c;虽然之前没接触过这门语言&#xff0c;但在大模型的帮助下&#xff0c;边看项目边写代码也能进行go的项目开发&#xff0c;不过&#xff0c;写了一段时间代码之后&#xff0c;总感觉对go语言本身&#xff0c;我的知识体…

【SRC实战】合成类小游戏外挂漏洞

挖个洞先 https://mp.weixin.qq.com/s/ZnaRn222xJU0MQxWoRaiJg “以下漏洞均为实验靶场&#xff0c;如有雷同&#xff0c;纯属巧合” 合成类小游戏三个特点&#xff1a; 1、一关比一关难&#xff0c;可以参考“羊了个羊” 2、无限关卡无限奖励&#xff0c;可以参考“消灭星星…

BBS客户端服务器的编写

根据网络编程中的内容&#xff0c;我们本篇文章将讲解一个bbs通信的项目&#xff0c;首先让我们了解一下什么是bbs. 一、bbs介绍 BBS&#xff0c;即Bulletin Board System的缩写&#xff0c;中文译为“电子公告板系统”或“网络论坛”。它是一个在网络上进行信息交流和讨论的…

【科研】常用的实验结果评价指标(1) —— R2(R-square)是什么?

常用的实验结果评价指标&#xff08;1&#xff09; —— R2(R-square)&#xff0c;可能为负数吗&#xff1f;&#xff01; 提示&#xff1a;先说概念&#xff0c;后续再陆续上代码 文章目录 常用的实验结果评价指标&#xff08;1&#xff09; —— R2(R-square)&#xff0c;可能…

基于WPF的DynamicDataDisplay曲线显示

一、DynamicDataDisplay下载和引用 1.新建项目,下载DynamicDataDisplay引用: 如下图: 二、前端开发: <Border Grid.Row="0" Grid.Column="2" BorderBrush="Purple" BorderThickness="1" Margin="2"><Grid>…

【智能算法应用】基于麻雀搜索算法-支持向量回归预测(SSA-SVR)

目录 1.算法原理2.数学模型3.结果展示4.调试记录5.参考文献6.代码获取 1.算法原理 【智能算法】麻雀搜索算法&#xff08;SSA&#xff09;原理及实现 2.数学模型 支持向量机(SVM)是针对二分类问题&#xff0c;支持向量回归(SVR)基于SVM应用与回归问题。SVR回归与SVM分类的区…

【JVM】了解JVM规范中的虚拟机结构

目录 JVM规范的主要内容 1&#xff09;字节码指令集(相当于中央处理器CPU) JVM指令分类 2&#xff09;Class文件的格式 3&#xff09;数据类型和值 4&#xff09;运行时数据区 5&#xff09;栈帧 6&#xff09;特殊方法 7&#xff09;类库 JVM规范的主要内容 1&#…

Paddle 实现DCGAN

传统GAN 传统的GAN可以看我的这篇文章&#xff1a;Paddle 基于ANN&#xff08;全连接神经网络&#xff09;的GAN&#xff08;生成对抗网络&#xff09;实现-CSDN博客 DCGAN DCGAN是适用于图像生成的GAN&#xff0c;它的特点是&#xff1a; 只采用卷积层和转置卷积层&#x…