(三)库存超卖案例实战——使用redis分布式锁解决“超卖”问题

news2024/9/22 7:21:03

前言

在上一节内容中我们介绍了如何使用mysql数据库的传统锁(行锁、乐观锁、悲观锁)来解决并发访问导致的“超卖问题”。虽然mysql的传统锁能够很好的解决并发访问的问题,但是从性能上来讲,mysql的表现似乎并不那么优秀,而且会受制于单点故障。本节内容我们介绍一种性能更加优良的解决方案,使用内存数据库redis实现分布式锁从而控制并发访问导致的“超卖”问题。关于redis环境的搭建这里不做介绍,可查看作者往期博客内容。

正文

  • 在项目中添加redis的依赖和配置信息

- pom依赖配置

<!--        数据库连接池工具包-->
<dependency>
	<groupId>org.apache.commons</groupId>
	<artifactId>commons-pool2</artifactId>
</dependency>

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

- application.yml配置

spring:
  application:
    name: ht-atp-plat
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.110.88:3306/ht-atp?characterEncoding=utf-8&serverTimezone=GMT%2B8&useAffectedRows=true&nullCatalogMeansCurrent=true
    username: root
    password: root
  profiles:
    active: dev
  # redis配置
  redis:
    host: 192.168.110.88
    lettuce:
      pool:
        # 连接池最大连接数(使用负值表示没有限制) 默认为8
        max-active: 8
        # 连接池中的最小空闲连接 默认为 0
        min-idle: 1
        # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认为-1
        max-wait: 1000
        # 连接池中的最大空闲连接 默认为8
        max-idle: 8

- redis序列化配置

package com.ht.atp.plat.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

    /**
     * @param factory
     * @return
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        // 缓存序列化配置,避免存储乱码
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
                ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的value序列化方式采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }
}

  •  在redis中增加商品P0001的库存数量为10000

  • 使用redis不加锁的业务测试

- 业务测试代码

    /**
     * 使用redis不加锁
     */
    @Override
    public void checkAndReduceStock() {
        // 1. 查询库存数量
        String stockQuantity = redisTemplate.opsForValue().get("P0001").toString();

        // 2. 判断库存是否充足
        if (stockQuantity != null && stockQuantity.length() != 0) {
            Integer quantity = Integer.valueOf(stockQuantity);
            if (quantity > 0) {
                // 3.扣减库存
                redisTemplate.opsForValue().set("P0001", String.valueOf(--quantity));
            }
        }
    }

- 使用jmeter压测,查看测试结果:库存并没有减少为0,说明存在“超卖”问题

  • 使用redis的setnx指令加锁,开启三个相同服务,使用jmeter压测

- redis加锁测试代码

/**
     * 使用redis加锁
     * 
     */
    @Override
    public void checkAndReduceStock() {
        // 1.使用setnx加锁
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock-stock", "0000");
        // 2.重试:递归调用,如果获取不到锁
        if (!lock) {
            try {
                //暂停50ms
                Thread.sleep(50);
                this.checkAndReduceStock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            try {
                // 3. 查询库存数量
                String stockQuantity = (String) redisTemplate.opsForValue().get("P0001");
                // 4. 判断库存是否充足
                if (stockQuantity != null && stockQuantity.length() != 0) {
                    Integer quantity = Integer.valueOf(stockQuantity);
                    if (quantity > 0) {
                        // 5.扣减库存
                        redisTemplate.opsForValue().set("P0001", String.valueOf(--quantity));
                    }
                } else {
                    System.out.println("该库存不存在!");
                }
            } finally {
                // 5.解锁
                redisTemplate.delete("lock-stock");
            }
        }
    }

- 开启服务7000、7001、7002

 - jmeter压测结果:平均访问时间364ms,接口吞吐量为每秒249

- redis数据库库存结果为:0,并发“超卖”问题解决

  • 以上普通加锁方式存在死锁问题及死锁问题的解决方案

- 死锁产生的原因:在上述redis加锁的正常情况下,是可以解决并发访问的问题,但是也存在死锁的问题,例如7000的服务获取到锁之后,由于服务异常导致锁没有释放,那么7001和7002服务将永远不可能获取到锁。

- 解决方案:给锁设置过期时间,自动释放锁

①使用expire设置过期时间(缺乏原子性:如果在setnx和expire之间出现异常,锁也无法释放)

②使用setex指令设置过期时间:set key value ex 3 nx(保证原子性操作既达到setnx的效果,又设置了过期时间)

- 代码实现

public void checkAndReduceStock() {
        // 1.使用setex加锁,保证加锁的原子性,以及锁可以自动释放
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock-stock", "0000",3, TimeUnit.SECONDS);
        // 2.重试:递归调用,如果获取不到锁
        if (!lock) {
            try {
                //暂停50ms
                Thread.sleep(50);
                this.checkAndReduceStock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            try {
                // 3. 查询库存数量
                String stockQuantity = (String) redisTemplate.opsForValue().get("P0001");
                // 4. 判断库存是否充足
                if (stockQuantity != null && stockQuantity.length() != 0) {
                    Integer quantity = Integer.valueOf(stockQuantity);
                    if (quantity > 0) {
                        // 5.扣减库存
                        redisTemplate.opsForValue().set("P0001", String.valueOf(--quantity));
                    }
                } else {
                    System.out.println("该库存不存在!");
                }
            } finally {
                // 5.解锁
                redisTemplate.delete("lock-stock");
            }
        }
    }

- 测试结果:库存扣减为0,锁也释放

  •  防止误删,在以上普通加锁的方式下,存在锁被误删除的情况

- 锁误删除的原因:在上面的加锁场景中,会出现以下的情况,A请求方法获取到锁之后,在业务还没有执行完成,锁就被自动释放,这个时候B请求方法也会获取到锁,在B业务还未执行完成之前,A执行完成并执行手动删除锁操作,这个时候会把B业务的锁释放掉,导致B刚刚获取到锁就被释放,从而产生后续的并发访问问题。

- 模拟锁误删除产生的并发问题

- 库存扣减结果:没有扣减为0,产生并发问题

- 解决方案,每个请求使用全局唯一UUID为value值,删除锁之前,先判断value值是否相同,相同再删除锁

public void checkAndReduceStock() {
        // 1.使用setex加锁,保证加锁的原子性,以及锁可以自动释放
        String uuid = UUID.randomUUID().toString();
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock-stock", uuid, 1, TimeUnit.SECONDS);
        // 2.重试:递归调用,如果获取不到锁
        if (!lock) {
            try {
                //暂停50ms
                Thread.sleep(10);
                this.checkAndReduceStock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            try {
                // 3. 查询库存数量
                String stockQuantity = (String) redisTemplate.opsForValue().get("P0001");
                // 4. 判断库存是否充足
                if (stockQuantity != null && stockQuantity.length() != 0) {
                    Integer quantity = Integer.valueOf(stockQuantity);
                    if (quantity > 0) {
                        // 5.扣减库存
                        redisTemplate.opsForValue().set("P0001", String.valueOf(--quantity));
                    }
                } else {
                    System.out.println("该库存不存在!");
                }
            } finally {
                // 5.先判断是否是自己的锁,然后再解锁
                String redisUuid = (String) redisTemplate.opsForValue().get("lock-stock");
                if (StringUtils.equals(uuid, redisUuid)) {
                    redisTemplate.delete("lock-stock");
                }
            }
        }
    }

- 存在的问题:由于判断锁和解锁的操作不具有原子性,仍然会存在误删除的操作,如A请求在完成判断之后准备删除锁的时候,此时A的锁自动释放,B请求获取到锁,这个时候A请求会手动将B请求的锁删除掉,依然存在并发访问的问题。该概率很小。

  •  使用lua脚本解决锁手动释放删除的操作是原子性操作

- lua代码解决误删操作

public void checkAndReduceStock() {
        // 1.使用setex加锁,保证加锁的原子性,以及锁可以自动释放
        String uuid = UUID.randomUUID().toString();
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock-stock", uuid, 1, TimeUnit.SECONDS);
        // 2.重试:递归调用,如果获取不到锁
        if (!lock) {
            try {
                //暂停50ms
                Thread.sleep(10);
                this.checkAndReduceStock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            try {
                // 3. 查询库存数量
                String stockQuantity = (String) redisTemplate.opsForValue().get("P0001");
                // 4. 判断库存是否充足
                if (stockQuantity != null && stockQuantity.length() != 0) {
                    Integer quantity = Integer.valueOf(stockQuantity);
                    if (quantity > 0) {
                        // 5.扣减库存
                        redisTemplate.opsForValue().set("P0001", String.valueOf(--quantity));
                    }
                } else {
                    System.out.println("该库存不存在!");
                }
            } finally {
                // 5.先判断是否是自己的锁,然后再解锁
                String script = "if redis.call('get', KEYS[1]) == ARGV[1] " +
                        "then " +
                        "   return redis.call('del', KEYS[1]) " +
                        "else " +
                        "   return 0 " +
                        "end";
                redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList("lock-stock"), uuid);
            }
        }
    }

结语

关于使用redis分布式锁解决“超卖”问题的内容到这里就结束了,我们下期见。。。。。。

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

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

相关文章

ATA-8061射频功率放大器在心室导管式扩压电式测力传感器中的应用

心室导管式扩压电式测力传感器是一种广泛应用于心血管领域的关键设备&#xff0c;用于测量心脏内部的压力变化。本文将主要介绍关于压电式测力传感器的相关知识概念&#xff0c;以及功率放大器在心室导管式扩压电式测力传感器研究中的重要应用。我们通过对传感器原理和功率放大…

使用 JDAudioCrawler 将下载的音频存储到本地存储

前言 在当今数字化时代&#xff0c;音频数据的获取和处理变得越来越重要。本文将访问网易云音乐为案例&#xff0c;介绍如何使用JDAudioCrawler这个强大的工具&#xff0c;将音频数据存储下载到本地存储中。将详细介绍实现的流程和代码细节。 什么是 JDAudioCrawler DAudioC…

2023运营级别网赚网盘平台搭建(源码+教程)

023运营级别网赚网盘平台搭建&#xff08;源码教程&#xff09; 为什么要自己搭建网盘&#xff0c;现在许多大厂的网盘&#xff0c;文件都添加了许多限制&#xff0c;有好多文件会遭到和谐&#xff0c;而且大部分网盘也都会限速&#xff0c;不开通VIP是很难用的&#xff01;这…

【Docker】Docker Compose服务依赖与健康检查

docker compose环境变量 为增加安全性&#xff0c;在前面的python例子中增加redis的密码校验&#xff0c;密码从环境变量中获取&#xff1a; from flask import Flask from redis import StrictRedis import os import socketapp Flask(__name__) redis StrictRedis(hostos…

企业在数字化转型时如何通过SD-WAN保证业务连续性

企业数字化建设离不开高品质的网络&#xff0c;随着信息化系统和应用的部署&#xff0c;传统网络线路逐渐暴露出不同的问题&#xff0c;包括&#xff1a; 线路资源利用率低易造成浪费 关键业务流量被抢占&#xff0c;缺乏保障 网络不可视&#xff0c;无法实时查看线路情况 故…

利用已存在的conda环境

一、已存在的环境 二、在Pycharm中使用这些环境

Linux 基于sysfs的GPIO读写操作

https://bbs.huaweicloud.com/blogs/297252 前言 最近接触到Linux系统中的GPIO开发&#xff0c;这里做个小总结&#xff0c;也分享一下&#xff1b;本文会介绍GPIO的读写&#xff0c;介绍基本原理&#xff0c;以及不同读写方式的性能。 一、GPIO sysfs interface 基本原理 …

Android 备案公钥、签名 MD5获取方法

公钥和 MD5 值可以通过安卓开发工具、Keytool、Jadx-GUI 等多种工具获取&#xff0c;本文以 jadx-gui 为例。 1 windows 下载 jadx-gui 工具 下载 jadx-gui 工具 在这里选择一个下载 下载后 解压文件 双击运行程序&#xff0c;然后选择 release apk安装包 2 Mac 打开终端&a…

基于springboot实现企业客户信息反馈平台管理系统项目【项目源码+论文说明】

基于springboot实现企业客户信息反馈平台管理系统演示 摘要 网络的广泛应用给生活带来了十分的便利。所以把企业客户信息反馈管理与现在网络相结合&#xff0c;利用java技术建设企业客户信息反馈平台&#xff0c;实现企业客户信息反馈的信息化。则对于进一步提高企业客户信息反…

1024程序员节优惠来啦 | 芒果YOLO专栏《全年首次优惠全场8.8折活动来啦》,一年一次,优惠券数量有限,先到先得!

芒果YOLO改进专栏 订阅即可享受 优惠券直接减免&#xff0c;芒果专栏全场优惠&#xff01;&#xff01; 1024程序员 全年优惠活动 来啦&#xff01;&#xff01;&#xff01;&#xff01; 芒果专栏全场优惠8.8折&#xff01;&#xff01; 文章目录 活动时间&#x1f525;&…

全面解析优化企业Microsoft 365网络的加速方案

您的员工是否有因为Microsoft 365频繁掉线、卡顿、无法登录而向IT部门抱怨过&#xff1f; 很多时候企业会以为是自身网络带宽不足才导致访问失败&#xff0c;但是在采取增加带宽的方案后&#xff0c;办公文档协同打开仍旧很慢&#xff0c;文件分享依旧需要等待较长的时间&…

【漏洞复现】蓝凌EIS智慧协同平台任意文件上传

目录 蓝凌智慧协同平台介绍 漏洞搜索 漏洞点 漏洞复现 nuclei poc 验证 漏洞修复 蓝凌智慧协同平台介绍 蓝凌智慧协同平台是个自动化办公OA&#xff0c;具有多端同步、无缝协作,提供移动端&#xff08;蓝凌KK、阿里钉钉、微信企业号&#xff09;、桌面端、网页端多端应…

『第四章』一见倾心:初识小雨燕(上)

在本篇博文中,您将学到如下内容: 1. 基本数据类型2. 基本操作符3. 枚举和结构4. 类和 Actor5. 属性、方法与访问控制6. 聚集总结夜月一帘幽梦,春风十里柔情。 无声交谈情意深,一见心曲绕梁成。 1. 基本数据类型 无论是 macOS 还是 iOS 上的开发,Swift 基础类型和功能都内置于…

淘宝API接口(商品信息获取,订单管理,库存管理,数据分析和优化)

淘宝API接口可以用于许多业务场景&#xff0c;以下是一些常见的应用场景&#xff1a; 商品信息获取&#xff1a;通过淘宝API接口可以获取商品的详细信息&#xff0c;包括商品标题、价格、库存、销量、评价等数据。这些信息可以用于在自己的网站或应用程序中展示商品&#xff0…

17 结构型模式-享元模式

1 享元模式介绍 2 享元模式原理 3 享元模式实现 抽象享元类可以是一个接口也可以是一个抽象类,作为所有享元类的公共父类, 主要作用是提高系统的可扩展性. //* 抽象享元类 public abstract class Flyweight {public abstract void operation(String extrinsicState); }具体享…

在外包干了2年,我悟了...

前言 简单的说下&#xff0c;我大学的一个同学&#xff0c;毕业后我自己去了自研的公司&#xff0c;他去了外包&#xff0c;快两年了我薪资、技术各个方面都有了很大的提升&#xff0c;他在外包干的这两年人都要废了&#xff0c;技术没一点提升&#xff0c;学不到任何东西&…

(免费领源码)JavaWeb#Springboot#MYSQL跳蚤市场网络商城 99706-计算机毕业设计项目选题推荐

目 录 摘要 1 绪论 1.1 研究背景 1.2 研究现状 1.3 论文结构与章节安排 2 跳蚤市场网络商城系统分析 2.1 可行性分析 2.1.1 技术可行性分析 2.1.2 经济可行性分析 2.1.3 操作可行性分析 2.2 系统流程分析 2.2.1 数据流程 3.3.2 业务流程 2.3 系统功能分析 2.3.1…

霍尔电流传感器如何应用在数据中心电量监测的-安科瑞 蒋静

摘要&#xff1a;数据中心供电电源质量的好坏直接影响到IT设备的安全运行&#xff0c;因此对数据中心直流列头柜电源进出线实行监测非常重要&#xff0c;而通过霍尔电流传感器可以采集主进线电流、多路支路直流电流和漏电流。 关键词&#xff1a;数据中心&#xff1b;直流列头…

商场巨变!拓世法宝AI智能商业一体机引爆智慧购物新浪潮

在如今信息爆炸的时代&#xff0c;大型商场的规模与复杂程度也呈现出愈发庞大的趋势。它的背后不仅是商场规模的扩大&#xff0c;更是商业模式的转型升级。消费者对于购物体验和服务质量的要求也日益提高。传统商场单一提供商品销售的职能已无法满足消费者多元化的需求&#xf…

公网IP怎么设置?公网ip有哪些优点和缺点?

随着互联网的普及&#xff0c;越来越多的人开始关注网络安全和隐私保护。其中&#xff0c;公网IP的设置成为了一个备受关注的话题。本文将详细介绍公网IP的设置方法以及公网IP的优点和缺点。 一、公网IP设置方法 1. 路由器设置 在家庭或企业网络中&#xff0c;路由器通常是最重…