分布式锁的多种实现方式

news2024/9/23 5:22:05

1、不使用分布式锁

synchronized (this){
      int stock = Integer.parseInt(Objects.requireNonNull(stringRedisTemplate.opsForValue().get("stock")));
      if (stock > 0) {
          int realStock = stock - 1;
          // 更新库存
          stringRedisTemplate.opsForValue().set("stock", realStock + "");
          // 此处执行业务代码
          System.out.println("扣减成功,剩余库存:" + realStock);
      } else {
          System.out.println("扣减失败,库存不足");
      }
      // 扣减库存操作执行完,删除这个键
      stringRedisTemplate.delete(goodsId);
      return new ResponseResult<>("执行成功", 200);
}

缺陷:
集群环境下并不能解决商品超卖BUG。真正的工作中,线上是一个集群环境(分布式环境),nginx会将请求分发到不同的后端服务上,但是因为synchronized锁是JVM进程级别的锁,也就是说是一个单机锁,并不能跨服务控制线程并发。

2、入门版分布式锁

在这里插入图片描述

@Autowired
    private StringRedisTemplate stringRedisTemplate;

    @GetMapping("/deduct_stock/{goodsId}")
    @ApiOperation("秒杀减库存场景")
    public ResponseResult<?> deductStock(@PathVariable(name = "商品id") String goodsId) {
        // 将商品id作为键,存到redis。每一个线程执行减库存方法时,如果存成功,则执行扣减库存的代码,存失败说明前面有线程正在执行,需等待。
        // setIfAbsent(key,value);如果key不存在,则创建这个key,否则什么也不做
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(goodsId, "分布式锁");

        if (Boolean.TRUE.equals(flag)) {
            int stock = Integer.parseInt(Objects.requireNonNull(stringRedisTemplate.opsForValue().get("stock")));
            if (stock > 0) {
                int realStock = stock - 1;
                // 更新库存
                stringRedisTemplate.opsForValue().set("stock", realStock + "");
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
            // 扣减库存操作执行完,删除这个键
            stringRedisTemplate.delete(goodsId);
            return new ResponseResult<>("执行成功", 200);
        } else {
            return new ResponseResult<>("当前商品抢购繁忙,请稍后再试", 210);
        }
    }

缺陷:

  1. 在删除锁之前,如果业务代码有异常,则锁无法删除,死锁!
  2. 如果请求执行到一半宕机了,锁无法删除,死锁!

2.1、优化

  1. 业务代码通过try-catch-finally代码块包裹一下,删除锁的操作放在finally里。
  2. 给锁设个过期时间。
@Autowired
    private StringRedisTemplate stringRedisTemplate;

    @GetMapping("/deduct_stock/{goodsId}")
    @ApiOperation("秒杀减库存场景")
    public ResponseResult<?> deductStock(@PathVariable(name = "商品id") String goodsId) {
        // 将商品id作为键,存到redis。每一个线程执行减库存方法时,如果存成功,则执行扣减库存的代码,存失败说明前面有线程正在执行,需等待。
        // setIfAbsent(key,value);如果key不存在,则创建这个key,否则什么也不做
        // 设置锁过期时间为10s
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(goodsId, "分布式锁", 10, TimeUnit.SECONDS);
        if (Boolean.FALSE.equals(flag)) {
            return new ResponseResult<>("当前商品抢购繁忙,请稍后再试", 210);
        }
        try {
            int stock = Integer.parseInt(Objects.requireNonNull(stringRedisTemplate.opsForValue().get("stock")));
            if (stock > 0) {
                int realStock = stock - 1;
                // 更新库存
                stringRedisTemplate.opsForValue().set("stock", realStock + "");
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        } catch (Exception e) {

        } finally {
            // 扣减库存操作不管成功与否,都删除这个键
            stringRedisTemplate.delete(goodsId);
        }
        return new ResponseResult<>("执行成功", 200);
    }

但还是有缺陷:
上述操作虽然避免了死锁问题,但不能解决误删锁的问题。因为业务代码的执行时间是不可控的,假设给锁设置过期时间为10s,而业务代码执行完需要15s,就会导致第一个请求还没执行完,锁就已经删掉了。这时第二个请求会创建锁,恰巧执行到一半时第一个请求执行完,删了第二个请求加的锁。这种极端情况下,有锁和没锁一样,很容易造成库存的脏读,导致超卖BUG。

2.2、再优化

  1. 每一次请求都生成一个uuid作为锁的值,删除锁时先判断这个锁是否属于当前线程,如果是则删除这个锁
    @GetMapping("/deduct_stock/{goodsId}")
    @ApiOperation("秒杀减库存场景")
    public ResponseResult<?> deductStock(@PathVariable String goodsId) {
        String clientId = IdUtil.randomUUID();
        // 将商品id作为键,存到redis。每一个线程执行减库存方法时,如果存成功,则执行扣减库存的代码,存失败说明前面有线程正在执行,需等待。
        // setIfAbsent(key,value);如果key不存在,则创建这个key,否则什么也不做
        // 设置锁过期时间为10s
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(goodsId, clientId, 10, TimeUnit.SECONDS);
        if (Boolean.FALSE.equals(flag)) {
            return new ResponseResult<>("当前商品抢购繁忙,请稍后再试", 210);
        }
        try {
            int stock = Integer.parseInt(Objects.requireNonNull(stringRedisTemplate.opsForValue().get("stock")));
            if (stock > 0) {
                int realStock = stock - 1;
                // 更新库存
                stringRedisTemplate.opsForValue().set("stock", realStock + "");
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        } catch (Exception e) {

        } finally {
            // 扣减库存操作不管成功与否,只要这个锁属于当前线程,都要删除这个锁
            String lockValue = stringRedisTemplate.opsForValue().get(goodsId);
            if (clientId.equalsIgnoreCase(lockValue)) {
                stringRedisTemplate.delete(goodsId);
                log.info("删除锁。。。");
            }
        }
        return new ResponseResult<>("执行成功", 200);
    }

还有BUG:
在这里插入图片描述
解决方案:
锁续命方案,每一次请求开一个分线程执行定时任务,定时查询锁有没有过期,如果没过期则延长过期时间到10s。

3、Redisson实现分布式锁

@GetMapping("/deduct_stock_redisson/{goodsId}")
    @ApiOperation("redisson实现分布式锁")
    public ResponseResult<?> deductStockByRedisson(@PathVariable String goodsId) {
        // 获取锁对象
        RLock redissonLock = redisson.getLock(goodsId);
        // 加锁
        redissonLock.lock();
        try {
            int stock = Integer.parseInt(Objects.requireNonNull(stringRedisTemplate.opsForValue().get("stock")));
            if (stock > 0) {
                int realStock = stock - 1;
                // 更新库存
                stringRedisTemplate.opsForValue().set("stock", realStock + "");
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        } catch (Exception e) {

        } finally {
            // 释放锁
            redissonLock.unlock();
        }
        return new ResponseResult<>("执行成功", 200);
    }

原理:
在这里插入图片描述
分布式锁是串行化操作,与并发编程的并行执行相违背,但可以采取一些方法优化性能。

  1. 锁的范围粒度越小越好。可以不在锁代码块里的尽量挪出去。
  2. 线程安全的明发map。分段锁

Redisson实现分布式锁并非绝对安全:
因为redis的主从复制功能,线程1需要先向master节点上加锁,master节点加锁成功后会将加锁命令同步到各个slave节点。假设master节点向slave节点同步的时候突然挂了,这时候slave节点并没有加锁成功,那么线程2就会在slave节点上加锁,依旧会出现超卖情况。

4、redLock

在这里插入图片描述

实现原理:
RedLock的实现原理就是使用Redis实现分布式锁,通过搭建多个独立的没有主从关系的redis,每次加锁都要往所有redis上加锁,超过一半(也有说所有)节点加锁成功才算加锁成功。同时每一个独立redis都不要进行主从复制,以免出现主节点宕机造成锁丢失。锁记得加上过期时间避免死锁。redis的持久化也必须要选择always,即没执行一次命令,进行一次持久化。

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

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

相关文章

vi编辑器的三种模式及其对应模式下常用指令

vi是Linux系统的第一个全屏幕交互式编辑工具&#xff0c;在嵌入式的 学习中是一个不可或缺的强大的文本编辑工具。 一、三种模式 命令模式 如何进入命令模式&#xff1a;按esc键 复制&#xff1a;yy nyy(n&#xff1a;行数) 删除(剪切): dd ndd 粘贴&#xff1a;p 撤销&…

【Java】java | 将可运行jar打包成exe可执行文件

一、说明 1、javafx桌面程序&#xff0c;但又不想安装jre环境 2、需要将可执行jar打包成exe 3、使用工具exe4j 二、操作步骤 1、下载exe4j https://exe4j.apponic.com/ 2、安装 说明1&#xff1a; 在d盘建个exe4j的文件夹 说明2&#xff1a; 建个output文件jar&#xff0c;存放…

计算机组成原理——计算机系统的组成

一台完整的计算机包括硬件和软件两部分&#xff0c;另外还有一部分固话的软件成为固件(Frimware)&#xff0c;固件兼具软件和硬件的特性&#xff0c;常见的如个人计算机中的BIOS&#xff0c;BIOS&#xff08;Basic Input/Output System&#xff09;是个人计算机上的一个基本输入…

React 路由

React 的路由跳转需要引用第三方的 React Router npm i react-router-dom5.2.0 React Router 分为 BrowserRouter 和 HashRouter 如果我们的应用有服务器响应 web 的请求&#xff0c;建议使用<BrowserRouter>组件; 如果使用静态文件服务器&#xff0c;建议使用<Hash…

[golang gin框架] 29.Gin 商城项目-用户登录,注册操作

一.用户登录,注册界面展示说明 先看登录,注册界面以及相关流程,再根据流程写代码,一般网站的登录,注册功能都会在一个页面进行操作,还有的是在几个页面进行操作,这里讲解在几个页面进行注册的操作,步骤如下: 登录: 1.点击 登录按钮,进入登录界面 2.在登录界面输入手机号,密码,图…

Linux内核中与“文件系统”相关的数据结构

文件系统相关的数据结构 4.1 file结构体 文件结构体代表一个打开的文件&#xff0c;系统中的每个打开的文件在内核空间都有一个关联的struct file。它由内核在打开文件时创建&#xff0c;并传递给在文件上进行操作的任何函数。在文件的所有实例都关闭后&#xff0c;内核释放这…

【Flink】DataStream API使用之源算子(Source)

源算子 创建环境之后&#xff0c;就可以构建数据的业务处理逻辑了&#xff0c;Flink可以从各种来源获取数据&#xff0c;然后构建DataStream进项转换。一般将数据的输入来源称为数据源&#xff08;data source&#xff09;&#xff0c;而读取数据的算子就叫做源算子&#xff08…

【vue3】06-vue的组件化开发-脚手架创建项目

文章目录 Vue的组件化组件化开发注册组件的方式vue全局组件vue局部组件 Vue的开发模式Vue CLI脚手架安装Vue CLI使用Vue CLI Vue的组件化 Vue是一款前端框架&#xff0c;在这个框架中&#xff0c;组件化开发是非常重要的。Vue的组件化就是将一个页面划分为多个独立的、可复用的…

LeetCode5. 最长回文子串

写在前面&#xff1a; 题目链接&#xff1a;LeetCode5. 最长回文子串 编程语言&#xff1a;C 题目难度&#xff1a;中等 一、题目描述 给你一个字符串 s&#xff0c;找到 s 中最长的回文子串。 如果字符串的反序与原始字符串相同&#xff0c;则该字符串称为回文字符串。 示例…

算力256TOPS,典型功耗35W,存算一体芯片杀入智能驾驶

作者 | 张祥威 编辑 | 德新 国产智驾芯片有了新玩家 “最高物理算力256 TOPS&#xff0c;典型功耗35W&#xff0c;基于12nm制程工艺。” 5月10日&#xff0c;后摩智能发布首款基于存算一体架构的智驾芯片——鸿途™H30&#xff0c;并公布上述关键指标。 算力、数据和算法&am…

单例模式的饿汉和懒汉写法(基于C++)

目录 单例模式例程饿汉懒汉 对比函数调用线程安全总结 单例模式 单例模式确保一个类只有一个实例&#xff0c;并提供全局访问点。这样可以避免在系统中出现多个相同的对象&#xff0c;从而提高系统的性能和可维护性。 单例模式的实现包括饿汉和懒汉&#xff0c;下面介绍C中这两…

操作系统基础知识之处理器性能方程指标(包含阿达姆定律、CPI、Clock cycle time等)

计算机设计人员通过持续时间或速率来指代时钟周期的时间。程序的 CPU 时间可以用两种方式表示&#xff1a; CPU 时间程序的 CPU 时钟周期 / 时钟频率 除了执行程序所需的时钟周期数外&#xff0c;我们还可以计算执行的指令数。 如果我们知道时钟周期数和指令数&#xff0c;就…

金融学第二版笔记第一章1.1

第1部分 金融和金融体系 第一章金融学 1.1 一、 对金融学进行界定 1.金融 金融是货币流通、信用活动及与之相关的经济行为的总称。 简言之&#xff0c;就是货币资金的融通。一般是指以银行、证券市场等为中心的货币流通和信用调节活动&#xff0c;包括货币的发行和流通、存…

转置卷积(一) 搞懂转置卷积的计算

搞懂转置卷积的计算 0、参考文档1、转置卷积是什么&#xff1f;1.1 定义1.2 需要注意 2、转置卷积的计算2.1 从最简单的开始2.2 考虑stride2.3 考虑padding2.4 考虑dilation 3 转置卷积的加速 文章首发于https://zhaodongyu-ak47.github.io/Transposed_Convolution/ 最近做了一…

数据结构入门-二叉树

树的概念及结构 树的概念 树的一种非线性的数据结构&#xff0c;它是由n&#xff08;n>0&#xff09;个有限节点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一颗倒挂的树&#xff0c;也就是说它树根朝上&#xff0c;而叶子朝下。 有一个特殊的节点&#xff…

Web安全行业:零基础学习网络安全需要掌握哪些知识?(附系统路线+工具笔记)

前言 “没有网络安全就没有国家安全”。当前&#xff0c;网络安全已被提升到国家战略的高度&#xff0c;成为影响国家安全、社会稳定至关重要的因素之一。 一、网络安全行业特点 行业发展空间大&#xff0c;岗位非常多 网络安全行业产业以来&#xff0c;随即新增加了几十个…

单元测试 - 集成H2 Dao测测试

SpringBoot 2.7、Mybatis plus、H2 1. pom引入h2 <dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><version>2.1.214</version> </dependency> 2. 配置h2数据源 & mapper路径 spring:datas…

地狱级的字节跳动面试,6年测开的我被按在地上摩擦.....

前几天我朋友跟我吐苦水&#xff0c;这波面试又把他打击到了&#xff0c;做了快6年软件测试员。。。为了进大厂&#xff0c;也花了很多时间和精力在面试准备上&#xff0c;也刷了很多题。但题刷多了之后有点怀疑人生&#xff0c;不知道刷的这些题在之后的工作中能不能用到&…

( 位运算 ) 260. 只出现一次的数字 III ——【Leetcode每日一题】

❓260. 只出现一次的数字 III 难度&#xff1a;中等 给你一个整数数组 nums&#xff0c;其中恰好有两个元素只出现一次&#xff0c;其余所有元素均出现两次。 找出只出现一次的那两个元素。你可以按 任意顺序 返回答案。 你必须设计并实现线性时间复杂度的算法且仅使用常量额…

精炼计算机网络——数据链路层(一)

文章目录 前言3.1 数据链路和帧3.1.1 数据链路和帧3.1.2 三个基本问题 总结 前言 上篇文章&#xff0c;我们一同学完了物理层的全部内容&#xff0c;在本篇文章中&#xff0c;我们初步学习数据链路层&#xff0c;理解数据链路和帧的相应概念&#xff0c;知晓封装成帧&#xff…