当前位置: 首页 > news >正文

Redis---------实现商品秒杀业务,包括唯一ID,超卖问题,分布式锁

 订单ID必须是唯一

59e1602b09834e0aadaa9602bbc8a20f.png

 唯一ID构成:

e0304f55e1f348838355b3126378a22d.png

代码生成唯一ID:


import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;//基于redis自增长的生成策略
@Component
public class RedisUUID {//起始时间时间秒数private static final long BEGIN_TIMESTAMP=1640995200L;//使用Redis自增策略private StringRedisTemplate stringRedisTemplate;public RedisUUID(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}//参数是业务的类型public long nextid(String keyType){//1,生成时间戳LocalDateTime now = LocalDateTime.now();long nowsecend = now.toEpochSecond(ZoneOffset.UTC);//当前时间的秒数减去起始时间的秒数得到时间戳long nowtime_stamp = nowsecend - BEGIN_TIMESTAMP;//2,生成序列号String nowdate = now.format(DateTimeFormatter.ofPattern("yyyy:mm:dd"));//使得每天都会生成新的一轮IDlong count = stringRedisTemplate.opsForValue().increment("icr:" + keyType + ":" + nowdate);//3,拼接返回return nowtime_stamp << 32 | count;}
}

827128d84da44b4793c030401783ed74.png

 

商品下单操作

业务逻辑:

7700ca73332d48708595361f1ae99dc3.jpg

 思路:主要是要了解以及掌握整个业务的流程:①先看商品是不是在秒杀的时间范围内②然后还要去看库存中是否还有该商品③如果有的话就扣减库存④然后就会生成订单,订单ID为唯一ID⑤把订单写入数据库中,再返回数据给前端

代码实现:

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Autowiredprivate ISeckillVoucherService iSeckillVoucherService;@Autowiredprivate RedisUUID redisUUID;@Override@Transactionalpublic Result seckillVoucher(Long voucherId) {//1,查询商品的信息SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);//2,看是否在秒杀时间范围内if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {return Result.fail("尚未开始!");}if (voucher.getEndTime().isBefore(LocalDateTime.now())) {return Result.fail("已经结束啦!");}//3,再看库存是否还有if (voucher.getStock()<1) {return Result.fail("库存不足!");}//4,如果有就减扣库存boolean sucess = iSeckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).update();if (!sucess) {return Result.fail("库存不足!");}//5,然后就创建订单信息VoucherOrder voucherOrder = new VoucherOrder();//5.1,订单id----id生成器long order = redisUUID.nextid("order");voucherOrder.setVoucherId(order);//5.2,用户idLong id = UserHolder.getUser().getId();voucherOrder.setUserId(id);//5.3,商品idvoucherOrder.setVoucherId(voucherId);//6,保存进数据库save(voucherOrder);//7,返回数据return Result.ok(order);}
}

 

库存超卖问题

先看看什么是库存超卖问题:

正常情况:

ec6c0f2b42804805a77a24f6e75ce430.jpg

 但是涉及到高并发的时候一定会出问题:

b46184b741a443fdbfa8f540833db7cf.jpg

所以我们要想办法去解决这个问题,锁!!!

ee298fbbb6684e16bc4411dcc7c90c40.jpg

 悲观锁认为一定发生并发问题,所以每一次操作都会加锁,是线程串行进行,不会出现并发问题,但是这样的话就导致性能降低,所以我们使用乐观锁,乐观锁是先让你操作,等你要修改数据库的时候再判断与你查到的数据是否是一样,如果是一样的才可以修改,否则不可以减库存。

乐观锁的两种实现判断法:

第一种:版本号法,就是通过查询两次版本号来判断是否被修改过库存

ac6f979be91d4cba9c36fff6f96e61a8.jpg

第二种:CAS法,是在版本号法上做的改进方法,既然要判断两次版本是否相同,为啥不判断库存量是否相同呢,所以CSA法就是去判断前后两次查询到的库存量是否一样,如果一样就可以改

c859dfee73f44fd9844699936966ab0c.jpg

用乐观锁CAS法来解决超卖问题:

//4,如果有就减扣库存boolean sucess = iSeckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).eq("stock",voucher.getStock()).update();if (!sucess) {return Result.fail("库存不足!");}

但是这样任然还不能解决超卖问题,因为如果两个线程同时来查到100,线程1做完修改还剩99,线程2查到不是100就会不执行修改,这样也会有问题,所以又要进行改进策略

 //4,如果有就减扣库存boolean sucess = iSeckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).gt("stock",0).update();if (!sucess) {return Result.fail("库存不足!");}

一人一单问题

 使用悲观锁处理单体服务下的多线程问题:

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Autowiredprivate ISeckillVoucherService iSeckillVoucherService;@Autowiredprivate RedisUUID redisUUID;@Overridepublic Result seckillVoucher(Long voucherId) {//1,查询商品的信息SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);//2,看是否在秒杀时间范围内if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {return Result.fail("尚未开始!");}if (voucher.getEndTime().isBefore(LocalDateTime.now())) {return Result.fail("已经结束啦!");}//3,再看库存是否还有if (voucher.getStock()<1) {return Result.fail("库存不足!");}//实现单体服务下的一人一单的多线程安全问题Long id = UserHolder.getUser().getId();//先获取锁,再提交事务,保证线程安全synchronized (id.toString().intern()){//获得Spring的代理对象(事务)IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);}}@Transactionalpublic Result createVoucherOrder(Long voucherId) {//一人一单问题Long id = UserHolder.getUser().getId();Integer count = query().eq("user_id", id).eq("voucher_id", voucherId).count();if(count > 0){return Result.fail("你已经购买过!");}//4,如果有就减扣库存boolean sucess = iSeckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).gt("stock",0).update();if (!sucess) {return Result.fail("库存不足!");}//5,然后就创建订单信息VoucherOrder voucherOrder = new VoucherOrder();//5.1,订单id----id生成器long order = redisUUID.nextid("order");voucherOrder.setVoucherId(order);//5.2,用户idvoucherOrder.setUserId(id);//5.3,商品idvoucherOrder.setVoucherId(voucherId);//6,保存进数据库save(voucherOrder);//7,返回数据return Result.ok(order);}
}

 添加依赖:

        <dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId></dependency>

 在启动类上添加:

@EnableAspectJAutoProxy(exposeProxy = true)

分布式集群模式下的多线程问题:

当我们是处理分布式集群模式下,两个JVM不是共用一把锁,导致每个JVM都有自己的锁导致我们之前的锁锁不住,每个JVM都有一个线程会获得锁。

 

 分布式锁:满足分布式系统或者集群模式下多进程可见并且互斥的锁

 

 

 基于Redis实现分布式锁:

创建锁对象:
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;public class SimpleRedisLock implements ILock{private String name;private StringRedisTemplate stringRedisTemplate;private static final  String KEY_PREFXY="lock:";public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name = name;this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean trylock(long timeoutSec) {//获取线程ID作为标识long ThreadId = Thread.currentThread().getId();//获取锁Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFXY + name, ThreadId + "", timeoutSec, TimeUnit.MINUTES);//避免空指针return Boolean.TRUE.equals(success);}@Overridepublic void unlock() {stringRedisTemplate.delete(KEY_PREFXY + name);}
}
代码实现Redis分布式锁的应用:

①先创建锁的对象,然后先是去获取锁②没有获取到锁就直接返回错误③获取到锁就可以进行对数据库的操作④操作完之后进行释放锁

Long id = UserHolder.getUser().getId();//创建锁对象SimpleRedisLock simpleRedisLock = new SimpleRedisLock("order" + id, stringRedisTemplate);//获取锁boolean trylock = simpleRedisLock.trylock(1200);//判断是否获得锁成功if (!trylock) {//获取锁失败return Result.fail("不允许重复下单!");}//获得Spring的代理对象(事务)try {IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);} finally {//释放锁simpleRedisLock.unlock();}

 但是就上面的处理还不够严谨,因为如果一个线程发生阻塞的话,其他线程可能会获得锁并且释放锁,导致锁误删问题,

解决锁误删问题:
import cn.hutool.core.lang.UUID;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;public class SimpleRedisLock implements ILock{private String name;private StringRedisTemplate stringRedisTemplate;private static final  String KEY_PREFXY="lock:";//得到一个唯一锁的标识private static final  String ID_PREFXY= UUID.randomUUID(true)+"-";public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name = name;this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean trylock(long timeoutSec) {//获取线程标识String ThreadId = ID_PREFXY+Thread.currentThread().getId();//获取锁Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFXY + name, ThreadId, timeoutSec, TimeUnit.MINUTES);//避免空指针return Boolean.TRUE.equals(success);}@Overridepublic void unlock() {//获取线程标识String ThreadId = ID_PREFXY+Thread.currentThread().getId();//判断要来修改的进程跟锁的标识是否一致String s = stringRedisTemplate.opsForValue().get(KEY_PREFXY + name);if(ThreadId.equals(s)){//释放锁stringRedisTemplate.delete(KEY_PREFXY + name);}}
}

相关文章:

Redis---------实现商品秒杀业务,包括唯一ID,超卖问题,分布式锁

订单ID必须是唯一 唯一ID构成&#xff1a; 代码生成唯一ID&#xff1a; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import java.time.LocalDateTime; import java.time.ZoneOffset; import java.tim…...

C++之QT文本处理QDir、QFileDialog、QStringList、QFile

一、相应的头文件 #include <QFileDialog> #include <QDir> #include <QStringList> 二、简介 1.QFileDialog 实际效果如下&#xff1a;比如需要选择打开的文件夹或者文件名&#xff0c;通过调用资源管理器的方式进行可视化操作。 代码示例为&#xff1a…...

24.5.8数据结构|单向循环链表

一、理解原理&#xff1a; 初始状态&#xff1a; 1、对比前两种的不同之处 1&#xff09;保存到栈空间&#xff08;局部变量&#xff09;。静态初始化。 2&#xff09; 二、代码实现 1、initLinkLoop函数 疑问&#xff1a; 1、地址怎么处理&#xff1f; 注意&#xff1…...

2024年,抖音小店开通需要多少钱?一篇详解!

大家好&#xff0c;我是电商糖果 2024年了&#xff0c;想在抖音开店卖货的朋友越来越多。 主要原因还是看到&#xff0c;这几年在抖音上赚到钱的人越来越多。 于是大家在今年比较关心的问题&#xff0c;就是抖音小店开通需要多少钱&#xff1f; 糖果做抖音小店四年了&#…...

2023年全国职业院校技能大赛(高职组)“云计算应用”赛项赛卷1(私有云)

#需要资源&#xff08;软件包及镜像&#xff09;或有问题的&#xff0c;可私聊博主&#xff01;&#xff01;&#xff01; #需要资源&#xff08;软件包及镜像&#xff09;或有问题的&#xff0c;可私聊博主&#xff01;&#xff01;&#xff01; #需要资源&#xff08;软件包…...

Python数据可视化------地图

基础地图使用 # 地图基本演示 # 导包 from pyecharts.charts import Map from pyecharts.options import TitleOpts, VisualMapOpts# 准备地图对象 cmap Map() # 准备数据&#xff08;列表&#xff09; data [("北京市", 99), ("上海市", 199), ("…...

Rust中的并发性:Sync 和 Send Traits

在并发的世界中&#xff0c;最常见的并发安全问题就是数据竞争&#xff0c;也就是两个线程同时对一个变量进行读写操作。但当你在 Safe Rust 中写出有数据竞争的代码时&#xff0c;编译器会直接拒绝编译。那么它是靠什么魔法做到的呢&#xff1f; 这就不得不谈 Send 和 Sync 这…...

|Python新手小白中级教程|第二十七章:面向对象编程(示例操作)(3)使用turtle库与类结合

文章目录 前言一、项目&#xff1a;使用类Circle画出圆形&#xff08;不调用turtle库&#xff09;1.基础指令class2.使用turtle画出大圆与小圆3.使用其他功能画一只眼睛 二、使用turtle库画正方形总结 前言 hello&#xff0c;我是BoBo仔&#xff0c;welcome来看我的文章 这节课…...

Android OpenMAX(五)高通OMX Core实现

上一节了解了OMX Core提供的内容,这一节我们看看高通OMX Core是如何实现的。本节代码参考自: omx_core_cmp.cpp registry_table_android.c qc_omx_core.h 1、OMX_Init/OMX_Deinit OMX_API OMX_ERRORTYPE OMX_APIENTRY OMX_Init() {DEBUG_PRINT(...

XXE漏洞

一、概述 1、XXE&#xff1a;XML外部实体注入攻击 2、XML&#xff1a;可扩展标记语言。 (1)没有固定标签&#xff0c;所有标签都可以自定义&#xff0c;但有限制规则。 (2)用于数据对的传输与存储&#xff0c;常被用于充当配置文件 推荐教程&#xff1a;XML 教程 (3)后缀…...

[华为OD]C卷 BFS 亲子游戏 200

题目&#xff1a; 宝宝和妈妈参加亲子游戏&#xff0c;在一个二维矩阵&#xff08;N*N&#xff09;的格子地图上&#xff0c;宝宝和妈妈抽签决定各自 的位置&#xff0c;地图上每个格子有不同的Q糖果数量&#xff0c;部分格子有障碍物。 游戏规则Q是妈妈必须在最短的时间&a…...

大模型微调实战之强化学习 贝尔曼方程及价值函数(五)

大模型微调实战之强化学习 贝尔曼方程及价值函数&#xff08;五&#xff09; 现在&#xff0c; 看一下状态-动作值函数的示意图&#xff1a; 这个图表示假设首先采取一些行动(a)。因此&#xff0c;由于动作&#xff08;a&#xff09;&#xff0c;代理可能会被环境转换到这些状…...

初探MFC程序混合使用QT

一、背景 随着操作系统国产化替代的趋势越发明显&#xff0c;软件支持国际化、跨平台&#xff0c;已然是必须做的一件事情。原有的软件UI层用的是MFC&#xff0c;将其换成QT&#xff0c;想必是一种较好的方案。对于大型软件&#xff0c;特别是已发布&#xff0c;但还处于不断迭…...

【LeetCode题库】1068. 产品销售分析 I —— MySQL 性能提升,using()关键字

文章目录 原题题解解题笔记 —— JOIN USING()关键字对性能的提升 我是一名立志把细节都说清楚的博主&#xff0c;欢迎【关注】&#x1f389; ~ 原创不易&#xff0c; 如果有帮助 &#xff0c;记得【点赞】【收藏】 哦~ ❥(^_-)~ 如有错误、疑惑&#xff0c;欢迎【评论】指正…...

leetcode 1 ~ 100

文章目录 1. 两数之和&#xff08;用哈希表减少查找的时间复杂度&#xff09;2. 两数相加&#xff08;高精度加法&#xff09;3.无重复字符的最长子串&#xff1a;&#xff08;模板&#xff1a;经典的滑动窗口算法&#xff09;5. 最长回文子串&#xff08;枚举&#xff09;6. Z…...

分享6个免费下载电子书的网站

着急看书的宝子们看这里&#xff01; 收藏了一堆电子书网站终于能派上用场了~ 01/Z-Library https://zh.zlibrary-be.se/ 世界上最大的电子图书馆&#xff0c;拥有超千万的书籍和文章资源&#xff0c;99%的书籍资料都能在这里找到。 我给的这个网址现在还能正常打开使用&…...

typescript的入门到吐槽:看了typescript,发现前端真的卷,

typescript TypeScript 是一种由微软开发的自由和开源的编程语言。它是 JavaScript 的一个超集&#xff0c;而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程。 TypeScript 与 JavaScript 的区别 其实就是对JavaScript的封装&#xff0c;把一个弱类型语言封…...

抖店商品详情API接口,商品上架(主图,价格,sku等属性,)item_get-获得抖店商品详情

抖店商品详情API接口&#xff0c;商品上架&#xff08;主图&#xff0c;价格&#xff0c;sku等属性&#xff0c;&#xff09;item_get-获得抖店商品详情 {"code": 0,"msg": "调用成功","time": "1715166889","data&quo…...

STM32使用ADC单/多通道检测数据

文章目录 1. STM32单片机ADC功能详解 2. AD单通道 2.1 初始化 2.2 ADC.c 2.3 ADC.h 2.4 main.c 3. AD多通道 3.1 ADC.c 3.2 ADC.h 3.3 main.c 3.4 完整工程文件 1. STM32单片机ADC功能详解 STM32单片机ADC功能详解 2. AD单通道 这个代码实现通过ADC功能采集三脚电…...

Unity 性能优化之动态批处理(四)

提示&#xff1a;仅供参考&#xff0c;有误之处&#xff0c;麻烦大佬指出&#xff0c;不胜感激&#xff01; 文章目录 前言一、动态合批是什么&#xff1f;二、使用动态批处理1.打开动态合批2.满足条件 三、检查动态合批是否成功五、动态合批弊端总结 前言 动态批处理是常用优…...

UE5 学习系列(二)用户操作界面及介绍

这篇博客是 UE5 学习系列博客的第二篇&#xff0c;在第一篇的基础上展开这篇内容。博客参考的 B 站视频资料和第一篇的链接如下&#xff1a; 【Note】&#xff1a;如果你已经完成安装等操作&#xff0c;可以只执行第一篇博客中 2. 新建一个空白游戏项目 章节操作&#xff0c;重…...

使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式

一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明&#xff1a;假设每台服务器已…...

<6>-MySQL表的增删查改

目录 一&#xff0c;create&#xff08;创建表&#xff09; 二&#xff0c;retrieve&#xff08;查询表&#xff09; 1&#xff0c;select列 2&#xff0c;where条件 三&#xff0c;update&#xff08;更新表&#xff09; 四&#xff0c;delete&#xff08;删除表&#xf…...

微信小程序云开发平台MySQL的连接方式

注&#xff1a;微信小程序云开发平台指的是腾讯云开发 先给结论&#xff1a;微信小程序云开发平台的MySQL&#xff0c;无法通过获取数据库连接信息的方式进行连接&#xff0c;连接只能通过云开发的SDK连接&#xff0c;具体要参考官方文档&#xff1a; 为什么&#xff1f; 因为…...

Linux --进程控制

本文从以下五个方面来初步认识进程控制&#xff1a; 目录 进程创建 进程终止 进程等待 进程替换 模拟实现一个微型shell 进程创建 在Linux系统中我们可以在一个进程使用系统调用fork()来创建子进程&#xff0c;创建出来的进程就是子进程&#xff0c;原来的进程为父进程。…...

Mysql8 忘记密码重置,以及问题解决

1.使用免密登录 找到配置MySQL文件&#xff0c;我的文件路径是/etc/mysql/my.cnf&#xff0c;有的人的是/etc/mysql/mysql.cnf 在里最后加入 skip-grant-tables重启MySQL服务 service mysql restartShutting down MySQL… SUCCESS! Starting MySQL… SUCCESS! 重启成功 2.登…...

[ACTF2020 新生赛]Include 1(php://filter伪协议)

题目 做法 启动靶机&#xff0c;点进去 点进去 查看URL&#xff0c;有 ?fileflag.php说明存在文件包含&#xff0c;原理是php://filter 协议 当它与包含函数结合时&#xff0c;php://filter流会被当作php文件执行。 用php://filter加编码&#xff0c;能让PHP把文件内容…...

wpf在image控件上快速显示内存图像

wpf在image控件上快速显示内存图像https://www.cnblogs.com/haodafeng/p/10431387.html 如果你在寻找能够快速在image控件刷新大图像&#xff08;比如分辨率3000*3000的图像&#xff09;的办法&#xff0c;尤其是想把内存中的裸数据&#xff08;只有图像的数据&#xff0c;不包…...

实战三:开发网页端界面完成黑白视频转为彩色视频

​一、需求描述 设计一个简单的视频上色应用&#xff0c;用户可以通过网页界面上传黑白视频&#xff0c;系统会自动将其转换为彩色视频。整个过程对用户来说非常简单直观&#xff0c;不需要了解技术细节。 效果图 ​二、实现思路 总体思路&#xff1a; 用户通过Gradio界面上…...

ui框架-文件列表展示

ui框架-文件列表展示 介绍 UI框架的文件列表展示组件&#xff0c;可以展示文件夹&#xff0c;支持列表展示和图标展示模式。组件提供了丰富的功能和可配置选项&#xff0c;适用于文件管理、文件上传等场景。 功能特性 支持列表模式和网格模式的切换展示支持文件和文件夹的层…...