抢单超卖? 并发问题解决思路

news2024/10/21 15:46:47

1. 问题介绍

在用户抢单或者商品售卖的过程中,正常情况下是一人一件,但是当网络流量剧增时多个用户同时抢到一个商品应该如何分配?假设这样一个场景A商品库存是100个,但是秒杀的过程中,一共卖出去500个A商品。对于卖家来说,这就是超售。造成的经济损失应该由谁来承担?所以这类问题必需从根源解决!!
这就是该问题的简单描述,有了解过的小伙伴肯定知道应该用并发技术来解决,但是对应这种高并发的场景要怎么操作呢?下面来介绍几种常见的解决方法

2. 技术

2.1 设置事务的隔离级别×

那就是设置事物的隔离级别为Serializable。这个事物隔离级别非常严格,因为多个事物并发执行,对同一条记录修改,就会出现超售现象。所以干脆,咱们就禁止事物的并发执行吧。Serializable就是让数据库,串行执行事物,一个事物执行完,才能执行下一个事物,这种办法确实解决了超售的问题。
但是磁盘的IO速度比内存和CPU慢多了。所以说,串行化执行事务会让事务堆积指数递增,这在用户角度来说中是绝对无法忍受的。这种办法在技术上最稳妥,但是业务上不可行!

2.2 乐观锁

我们在数据表上面添加一个乐观锁字段,数据类型是整数的,用来记录数据更新的版本号,这个跟SVN机制很像。乐观锁是一种逻辑锁,他是通过版本号来判定有没有更新冲突出现
比如说,现在A商品的乐观锁版本号是0,现在有事务1来抢购商品了。事务1记录下版本号是0,等到执行修改库存的时候,就把乐观锁的版本号设置成1。但是事务1在执行的过程中,还没来得及执行UPDATE语句修改库存。这个时候事务2进来了,他执行的很快,直接把库存修改成99,然后把版本号变成了1。这时候,事务1开始执行UPDATE语句,但是发现乐观锁的版本号变成了1,这说明,肯定有人抢在事务1之前,更改了库存,所以事务1就不能更新,否则就会出现超售现象。
在这里插入图片描述

但是由于其中大量的回滚操作,使得乐观锁不适用于冲突频率高时读写操作,而且乐观锁具有写倾斜和无法解决幻读问题

“幻读”问题:乐观锁无法解决“幻读”问题,即一个事务在读取某些数据后,另一个事务插入了一些新数据,导致第一个事务在后续的操作中看到了它之前没有读取到的数据。
写倾斜:在乐观锁机制下,可能会出现多个事务同时读取同一条记录,然后基于读取的数据进行修改,最后在提交时发生冲突,只有一个事务能够成功,其他事务需要重试,这种现象称为写倾斜。

2.3 分布式锁

在这里插入图片描述

在微服务架构中最常见的就是分布式事务锁,像是之前学习过synchronized 及lock锁,但是在微服务环境中就不可以使用(本地锁只在JVM中生效)了,那么怎么办呢?我可以使用分布式锁,分布式的实现方式多种多样,常见的分布式说可以基于以下集中方式实现:

2.3.1 基于 Redis 做分布式锁

基于 REDIS 的 SETNX()、EXPIRE() 方法做分布式锁

  1. setnx(lockkey, 1) 如果返回 0,则说明占位失败;如果返回 1,则说明占位成功
  2. expire() 命令对 lockkey 设置超时时间,为的是避免死锁问题。
  3. 执行完业务代码后,可以通过 delete 命令删除 key
//对应redis中SETNX()操作,只是redisTemplate对其做了一层封装暴露出来setIfAbsent方法
//同时设置过期时间,防止异常导致死锁无法释放
Boolean ifAbsent = redisTemplate.opsForValue().setIfAbsent("lock", "lock", 10, TimeUnit.SECONES);

//可以获取到锁才进来操作
if(ifAbsent){
	//并发业务操作。。。
	
	//最后删除分布式锁,其他线程可以重新获得
	redisTemplate.delete("lock");
}

但是这种情况会导致误删锁的问题 ,导致其他线程面临无锁的状态并发漏洞!!
解决方法在删之前判断是否是自己的锁,即添加一个UUID或者是任意一个唯一标识来确定该锁是自己的锁,删之前做判断就可以了,但是还是没有解决原子性(在判断通过进入删除前锁刚好过期,且锁刚好被另一个线程获取到),最终版本通过lua脚本解决,具体代码如下:

/**
 * 采用SpringDataRedis实现分布式锁
 * 原理:执行业务方法前先尝试获取锁(setnx存入key val),如果获取锁成功再执行业务代码,业务执行完毕后将锁释放(del key)
 */
@Override
public void testLock() {

    //锁值设置为uuid
    String uuid = UUID.randomUUID().toString();
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid, 10, TimeUnit.SECONDS);

    if(flag){
        //获取锁成功,执行业务代码。。。

        //将锁释放 判断uuid,redis执行lua脚本保证原子,lua脚本执行会作为一个整体执行
        //执行脚本参数 参数1:脚本对象封装lua脚本,参数二:lua脚本中需要key参数(KEYS[i])  参数三:lua脚本中需要参数值 ARGV[i]
        // 先创建脚本对象 DefaultRedisScript泛型脚本语言返回值类型 Long 0:失败 1:成功
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        // 设置脚本文本
        String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                "then\n" +
                "    return redis.call(\"del\",KEYS[1])\n" +
                "else\n" +
                "    return 0\n" +
                "end";
        redisScript.setScriptText(script);
        // 设置响应类型
        redisScript.setResultType(Long.class);
        stringRedisTemplate.execute(redisScript, Arrays.asList("lock"), uuid);
    }else{
        try {
            //睡眠
            Thread.sleep(100);
            //自旋重试
            this.testLock();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

2.3.2 基于 REDISSON 做分布式锁

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service) Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上

官方文档地址:https://github.com/Redisson/Redisson/wiki

Github 地址:https://github.com/Redisson/Redisson

redisson 是 redis 官方的分布式锁组件。上面的代码没有接耦合,锁的代码和业务代码混在一起了需要解耦合!!

2.3.2.1 引入依赖
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
</dependency>
2.3.2.2 配置RedissonClient
import lombok.Data;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.config.SingleServerConfig;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;

/**
 * redisson配置信息
 */
@Data
@Configuration
@ConfigurationProperties("spring.data.redis") //读取nacos配置文件
public class RedissonConfig {

    private String host;

    private String password;

    private String port;

    private int timeout = 3000;
    private static String ADDRESS_PREFIX = "redis://";

    /**
     * 自动装配
     *
     */
    @Bean
    RedissonClient redissonSingle() {
        Config config = new Config();

        if(!StringUtils.hasText(host)){
            throw new RuntimeException("host is  empty");
        }
        SingleServerConfig serverConfig = config.useSingleServer()
                .setAddress(ADDRESS_PREFIX + this.host + ":" + port)
                .setTimeout(this.timeout);
        if(StringUtils.hasText(this.password)) {
            serverConfig.setPassword(this.password);
        }
        return Redisson.create(config);
    }
}

注意:这里读取了一个名为RedisProperties的属性,因为我们引入了SpringDataRedis,Spring已经自动加载了RedisProperties,并且读取了配置文件中的Redis信息。

2.3.2.3 修改实现类(其中定义三种锁的加锁操作)
@Autowired
private RedissonClient redissonClient;

/**
 * 使用Redison实现分布式锁
 * 开发步骤:
 * 1.使用RedissonClient客户端对象 创建锁对象
 * 2.调用获取锁方法
 * 3.执行业务逻辑
 * 4.将锁释放
 *
 */
public void testLock() {

    //1 创建锁对象
    RLock lock = redissonClient.getLock("lock1");

    //2 尝试加锁 三种选其一
    //2.1 lock() 阻塞等待一直到获取锁,默认锁有效期30s
    lock.lock();
    //2.2 lock() 阻塞等待一直到获取锁,默认锁有效期10s
    lock.lock(10,TimeUnit.SECONDS);
    //2.3 tryLock() 等待30s获取锁,超过时间自动放弃,且默认锁有效期10s
    lock.tryLock(30,10,TimeUnit.SECONDS);

    //3 业务代码。。。

    //4 将锁释放
    lock.unlock();

}

基于Redis的Redisson分布式可重入锁RLock Java对象实现了java.util.concurrent.locks.Lock接口。

大家都知道,如果负责储存这个分布式锁的Redisson节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。

另外Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了。

看门狗原理:

只要线程一加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下,如果线程一还持有锁,那么就会不断的延长锁key的生存时间。因此,Redisson就是使用Redisson解决了锁过期释放,业务没执行完问题。

1、如果我们指定了锁的超时时间,就发送给Redis执行脚本,进行占锁,默认超时就是我们制定的时间,不会自动续期;
2、如果我们未指定锁的超时时间,就使用 lockWatchdogTimeout = 30 * 1000 【看门狗默认时间】

2.3.3 基于 ZooKeeper 做分布式锁

基于临时顺序节点实现

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

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

相关文章

AXI GPIO按键控制——ZYNQ学习笔记4

一、AXI GPIO接口简介 是什么&#xff1f;是PL部分的一个IP软核&#xff0c;实现通用输入输出接口的功能&#xff0c;并通过AXI协议实现与处理系统通信&#xff0c;方便控制与拓展GPIO接口。 AXI GPIO IP 核为 AXI 接口提供了一个通用的输入/输出接口。 与 PS 端的 GPIO 不同&…

【YOLO系列】YOLO11原理和深入解析——待完善

文章目录 前言一、主要新增特性二、主要改进2.1 C3K2网络结构2.2 C2PSA网络结构2.3 Head部分 三、对比与性能优势四、X-AnyLabeling4.1 目标检测&#xff1a;4.2 实例分割&#xff1a;4.3 图像分类&#xff1a;4.4 姿态估计&#xff1a;4.5 旋转目标检测&#xff1a; 五、总结 …

Vue+Vant实现7天日历展示,并在切换日期时实时变换

效果图&#xff1a; 主要使用 moment.js 插件完成 HTML部分 <div class"day-content"><div class"day-content-t"><div>{{ monthVal }}</div><div click"onCalendar()">更多>></div></div><…

HTTP vs WebSocket

本文将对比介绍HTTP 和 WebSocket &#xff01; 相关文章&#xff1a; 1.HTTP 详解 2.WebSocket 详解 一、HTTP&#xff1a;请求/响应的主流协议 HTTP&#xff08;超文本传输协议&#xff09;是用于发送和接收网页数据的标准协议。它最早于1991年由Tim Berners-Lee提出来&…

【C++】二叉搜索树的概念与实现

目录 二叉搜索树 概念 key类型 概念 代码实现 key_value类型 概念 代码实现 二叉搜索树 概念 ⼆叉搜索树⼜称⼆叉排序树&#xff0c;它或者是⼀棵空树&#xff0c;或者是具有以下性质的⼆叉树: 左子树的值默认小于根节点&#xff0c;右子树的值默认大于根节点 。 ⼆…

具备技术三:通用类型any实现

一、背景 一个连接必须拥有请求接收与解析的上下文。 上下文的结构不能固定&#xff0c;因为服务器支持的协议很多&#xff0c;不同协议有不同的上下文结构&#xff0c;所以必须拥有一个容器保存不同的类型结构数据。 二、设计思路 目标&#xff1a;一个容器保存各种不同数…

opencv学习:CascadeClassifier和detectMultiScale算法进行人脸识别

CascadeClassifier CascadeClassifier 是 OpenCV 提供的一个用于对象检测的类&#xff0c;它基于Haar特征和AdaBoost算法。它能够识别图像中的特定对象&#xff0c;比如人脸、眼睛、微笑等。CascadeClassifier 需要一个预训练的XML分类器文件&#xff0c;该文件包含了用于检测…

SHA1算法学习

SHA-1&#xff08;安全哈希算法1&#xff09;是一种加密哈希函数&#xff0c;它接受一个输入并生成一个160位&#xff08;20字节&#xff09;的哈希值&#xff0c;通常表示为一个40位的十六进制数。 SHA1的特点 输入与输出&#xff1a;SHA-1可以接受几乎任意大小的输入&#…

21世纪20年代最伟大的情侣:泰勒斯威夫特和特拉维斯凯尔西每张照片都在秀恩爱

在时代的长河中&#xff0c;每一代都毫无例外地拥有属于自己的 it couple&#xff08;当红情侣&#xff09;&#xff0c;他们成为了那个特定时期大众瞩目的焦点和津津乐道的话题。 千禧年间&#xff0c;确实涌现出了诸多令人瞩目的情侣组合。就像汤姆克鲁斯和凯蒂霍尔姆斯&…

【H2O2|全栈】更多关于HTML(2)HTML5新增内容

目录 HTML5新特性 前言 准备工作 语义化标签 概念 新内容 案例 多媒体标签 音频标签audio 视频标签 video 新增部分input表单属性 预告和回顾 后话 HTML5新特性 前言 本系列博客是对入门专栏的HTML知识的补充&#xff0c;并伴随一些补充案例。 这一期主要介绍H…

从源码上剖析AQS的方方面面(超详细版)

AQS在 ReentrantLock 的使用方式&#xff08;非公平锁&#xff09; 我们之前学习过 ReentrantLock 非公平锁与公平锁的区别在于&#xff0c;非公平锁不会强行按照任务等待队列去等待任务&#xff0c;而是在获取锁的时候先去尝试使用 CAS 改变一下 State&#xff0c;如果改变成…

架构设计笔记-18-安全架构设计理论与实践

知识要点 常见的安全威胁&#xff1a; 信息泄露&#xff1a;信息被泄露或透露给某个非授权的实体。破坏信息的完整性&#xff1a;数据被非授权地进行增删、修改或破坏而受到损失。拒绝服务&#xff1a;对信息或其他资源的合法访问被无条件地阻止。攻击者向服务器发送大量垃圾…

多选框的单选操作 Element ui

文章目录 样式预览Q&#xff1a;为什么要这么做&#xff1f;实现原理探索路程 样式预览 Q&#xff1a;为什么要这么做&#xff1f; 单选框的样式不够好看单选框因为框架等原因&#xff0c;无法取消选择 实现原理 判断多选框绑定的 value&#xff0c;如果长度为2&#xff0c;那…

实缴新玩法:公司注册资金与知识产权的完美结合

在当今商业环境中&#xff0c;公司注册资金的实缴方式不断创新和发展。其中&#xff0c;将公司注册资金与知识产权相结合&#xff0c;成为了一种引人注目的新玩法。 以往&#xff0c;公司注册资金的实缴往往依赖于货币资金的注入。然而&#xff0c;随着知识经济的崛起&#xf…

中文学术期刊(普刊)-全学科

文章目录 一、征稿简介二、重要信息三、服务简述四、投稿须知五、联系咨询 一、征稿简介 二、重要信息 期刊官网&#xff1a;https://ais.cn/u/3eEJNv 三、服务简述 中国知网是最负盛名的中文数据图书馆&#xff0c;收录来自自然科学、社会科学的优质学术期刊&#xff1b;维…

Redis哨兵TILT模式问题解决方案

Redis sentinel的TILT影响范围 Redis版本影响范围&#xff1a;5、6、7版本 部署方式为k8s部署&#xff0c;都会受到影响&#xff0c;裸金属部署没有问题 当redis哨兵集群进入TILT模式后&#xff0c;业务无法正常连接到redis集群&#xff0c;无法正常使用redis集群。 TILT 模式&…

你用过最好用的AI工具有哪些?探寻用户心中的最爱与最佳

随着人工智能技术的飞速发展&#xff0c;AI 工具如雨后春笋般涌现&#xff0c;广泛应用于各个领域。在 10 月 8 日至 10 月 27 日这段时间里&#xff0c;我们深入探讨了人们在使用 AI 工具时的偏好和体验&#xff0c;旨在揭示那些最受用户喜爱以及被认为最好用的 AI 工具&#…

构造函数

引入&#xff1a;构造函数的由来 对于以下Date类&#xff1a; class Date { public:void Init(int year, int month, int day){year year;_month month;_day day;}void Print(){cout << _year << "-" << _month << "-" <&…

STL源码剖析:STL算法

STL 算法总览 质变算法 mutating algorithms—会改变操作对象之值 所有的 STL算法都作用在由迭代器(first,last)所标示出来的区间上。所谓“质变算法”,是指运算过程中会更改区间内(迭代器所指)的元素内容。诸如拷贝(copy)、互换(swap)、替换(replace)、填写(fill)、删除(remov…

吐槽kotlin之垃圾设计

本文重点在于吐槽垃圾设计&#xff0c;基本直只说缺点。 一.没有static关键字 static其实不是很面向对象&#xff0c;但是是很有必要和方便的。 kotlin为了实现java的static功能&#xff0c;必须使用伴生类&#xff0c;一般情况下没啥问题&#xff0c;但是反编译之后的class多…