优惠卷秒杀(并发问题)

Redis实战篇 | Kyle's Blog (cyborg2077.github.io)

目录

一、Redis实现全局唯一id

二、添加优惠卷

 三、实现秒杀下单

 四、解决超卖问题(库存为负)

乐观锁解决超卖问题(CAS法)

五、实现一人一单

​编辑 悲观锁解决一人一单问题

六、集群环境下的并发问题(引出分布式锁)


一、Redis实现全局唯一id

  • 在各类购物App中,都会遇到商家发放的优惠券
  • 当用户抢购商品时,生成的订单会保存到tb_voucher_order表中,而订单表如果使用数据库自增ID就会存在一些问题
    1. id规律性太明显
    2. 受单表数据量的限制
  • 如果我们的订单id有太明显的规律,那么对于用户或者竞争对手,就很容易猜测出我们的一些敏感信息,例如商城一天之内能卖出多少单,这明显不合适
  • 随着我们商城的规模越来越大,MySQL的单表容量不宜超过500W,数据量过大之后,我们就要进行拆库拆表,拆分表了之后,他们从逻辑上讲,是同一张表,所以他们的id不能重复,于是乎我们就要保证id的唯一性
  • 那么这就引出我们的全局ID生成器
  • 为了增加ID的安全性,我们可以不直接使用Redis自增的数值,而是拼接一些其他信息
  • ID组成部分
    • 符号位:1bit,永远为0
    • 时间戳:31bit,以秒为单位,可以使用69年(2^31秒约等于69年)
    • 序列号:32bit,秒内的计数器,支持每秒传输2^32个不同ID

全局唯一id: 

@Component
public class RedisIdWorker {/*** 开始时间戳*/private static final long BEGIN_TIMESTAMP = 1640995200L;/*** 序列号的位数*/private static final long COUNT_BITS = 32L;@Autowiredprivate StringRedisTemplate stringRedisTemplate;// 根据传进的参数区分不同的业务,生成唯一idpublic long nextId(String keyPrefix){// 1. 生成时间戳//获取当前时间 转换为秒, 当前时间减起始时间为时间戳LocalDateTime now = LocalDateTime.now();long nowSecond = now.toEpochSecond(ZoneOffset.UTC);long nowTimeStamp = nowSecond - BEGIN_TIMESTAMP;// 2. 生成序列号//获取当前日期,精确到天, 设置序列号自增长String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);// 3. 拼接并返回return nowTimeStamp << COUNT_BITS | count;}
}

二、添加优惠卷

由于这里并没有后台管理页面,所以我们只能用POSTMAN模拟发送请求来新增秒杀券,请求路径http://localhost:8081/voucher/seckill, 请求方式POST

新增普通券,也就只是将普通券的信息保存到表中

/*** 新增普通券* @param voucher 优惠券信息* @return 优惠券id*/
@PostMapping
public Result addVoucher(@RequestBody Voucher voucher) {voucherService.save(voucher);return Result.ok(voucher.getId());
}

新增秒杀券主要看addSeckillVoucher中的业务逻辑 

/*** 新增秒杀券* @param voucher 优惠券信息,包含秒杀信息* @return 优惠券id*/
@PostMapping("seckill")
public Result addSeckillVoucher(@RequestBody Voucher voucher) {voucherService.addSeckillVoucher(voucher);return Result.ok(voucher.getId());
}

秒杀券可以看做是一种特殊的普通券,将普通券信息保存到普通券表中,同时将秒杀券的数据保存到秒杀券表中,通过券的ID进行关联

@Override
@Transactional
public void addSeckillVoucher(Voucher voucher) {// 保存优惠券save(voucher);// 保存秒杀信息SeckillVoucher seckillVoucher = new SeckillVoucher();// 关联普通券idseckillVoucher.setVoucherId(voucher.getId());// 设置库存seckillVoucher.setStock(voucher.getStock());// 设置开始时间seckillVoucher.setBeginTime(voucher.getBeginTime());// 设置结束时间seckillVoucher.setEndTime(voucher.getEndTime());// 保存信息到秒杀券表中seckillVoucherService.save(seckillVoucher);
}

 三、实现秒杀下单

实现类 

@Service
@Transactional
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Resourceprivate ISeckillVoucherService seckillVoucherService;@Resourceprivate RedisIdWorker redisIdWorker;@Overridepublic Result seckillVoucher(Long voucherId) {// 1.查询优惠券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);// 2.判断秒杀是否开始if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {return Result.fail("秒杀尚未开始");}// 3.判断秒杀是否结束if (voucher.getEndTime().isBefore(LocalDateTime.now())) {return Result.fail("秒杀已经结束");}// 4.判断库存是否充足// 库存不足if (voucher.getStock() < 1) {return Result.fail("库存不足");}// 5.库存充足 扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).update();if (!success) {return Result.fail("库存不足");}// 6.创建订单VoucherOrder voucherOrder = new VoucherOrder();// 6.1.订单id(全局唯一id)long orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);// 6.2.用户id (从拦截器中获取)Long userId = UserHolder.getUser().getId();voucherOrder.setUserId(userId);// 6.3.代金券idvoucherOrder.setVoucherId(voucherId);// 6.4.保存订单save(voucherOrder);// 7.返回订单idreturn Result.ok(orderId);}
}

接口

public interface IVoucherOrderService extends IService<VoucherOrder> {Result seckillVoucher(Long voucherId);
}

Controller 

@RestController
@RequestMapping("/voucher-order")
public class VoucherOrderController {@Autowiredprivate IVoucherOrderService voucherOrderService;@PostMapping("seckill/{id}")public Result seckillVoucher(@PathVariable("id") Long voucherId) {return voucherOrderService.seckillVoucher(voucherId);}
}

 四、解决超卖问题(库存为负)

实现秒杀下单的代码其实是有问题的,当遇到高并发场景时,会出现超卖现象,我们可以用Jmeter开200个线程来模拟抢优惠券的场景。

测试完毕之后,查看数据库中的订单表,我们明明只设置了100张优惠券,却有166条数据,去优惠券表查看,库存为-66,超卖了66张 

  • 那么如何解决这个问题呢?先来看看我们的代码中是怎么写的
//4. 判断库存是否充足
if (seckillVoucher.getStock() < 1) {return Result.fail("优惠券已被抢光了哦,下次记得手速快点");
}
//5. 扣减库存
boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).update();
if (!success) {return Result.fail("库存不足");
}

  • 假设现在只剩下一张优惠券,线程1过来查询库存,判断库存数大于1,但还没来得及去扣减库存,此时库线程2也过来查询库存,发现库存数也大于1,那么这两个线程都会进行扣减库存操作,最终相当于是多个线程都进行了扣减库存,那么此时就会出现超卖问题

  • 超卖问题是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁:而对于加锁,我们通常有两种解决方案

 

乐观锁解决超卖问题(CAS法)

以上逻辑的核心含义是:只要我扣减库存时的库存和之前我查询到的库存是一样的,就意味着没有人在中间修改过库存,那么此时就是安全的,但是以上这种方式通过测试发现会有很多失败的情况,失败的原因在于:在使用乐观锁过程中假设100个线程同时都拿到了100的库存,然后大家一起去进行扣减,但是100个人中只有1个人能扣减成功,其他的人在处理时,他们在扣减时,库存已经被修改过了,所以此时其他线程都会失败。因为我们还需要判断是否有剩余优惠券,即只要数据库中的库存大于0,都能顺利完成扣减库存操作 

修改“实现秒杀下单”的代码为:

@Service
@Transactional
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Resourceprivate ISeckillVoucherService seckillVoucherService;@Resourceprivate RedisIdWorker redisIdWorker;@Overridepublic Result seckillVoucher(Long voucherId) {// 1.查询优惠券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);// 2.判断秒杀是否开始if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {return Result.fail("秒杀尚未开始");}// 3.判断秒杀是否结束if (voucher.getEndTime().isBefore(LocalDateTime.now())) {return Result.fail("秒杀已经结束");}// 4.判断库存是否充足// 库存不足if (voucher.getStock() < 1) {return Result.fail("库存不足");}// 5.库存充足 扣减库存// 乐观锁解决超卖问题boolean success = seckillVoucherService.update().setSql("stock = stock - 1")// set stock = stock - 1.eq("voucher_id", voucherId).gt("stock", 0)// where stock > 0 库存大于0就扣减库存.update();if (!success) {return Result.fail("库存不足");}// 6.创建订单VoucherOrder voucherOrder = new VoucherOrder();// 6.1.订单id(全局唯一id)long orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);// 6.2.用户id (从拦截器中获取)Long userId = UserHolder.getUser().getId();voucherOrder.setUserId(userId);// 6.3.代金券idvoucherOrder.setVoucherId(voucherId);// 6.4.保存订单save(voucherOrder);// 7.返回订单idreturn Result.ok(orderId);}
}

五、实现一人一单

  • 需求:修改秒杀业务,要求同一个优惠券,一个用户只能抢一张
  • 具体操作逻辑如下:我们在判断库存是否充足之后,根据我们保存的订单数据,判断用户订单是否已存在
    • 如果已存在,则不能下单,返回错误信息
    • 如果不存在,则继续下单,获取优惠券

 悲观锁解决一人一单问题

 实现类

@Service
@Transactional
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Resourceprivate ISeckillVoucherService seckillVoucherService;@Resourceprivate RedisIdWorker redisIdWorker;@Overridepublic Result seckillVoucher(Long voucherId) {LambdaQueryWrapper<SeckillVoucher> queryWrapper = new LambdaQueryWrapper<>();//1. 查询优惠券queryWrapper.eq(SeckillVoucher::getVoucherId, voucherId);SeckillVoucher seckillVoucher = seckillVoucherService.getOne(queryWrapper);//2. 判断秒杀时间是否开始if (LocalDateTime.now().isBefore(seckillVoucher.getBeginTime())) {return Result.fail("秒杀还未开始,请耐心等待");}//3. 判断秒杀时间是否结束if (LocalDateTime.now().isAfter(seckillVoucher.getEndTime())) {return Result.fail("秒杀已经结束!");}//4. 判断库存是否充足if (seckillVoucher.getStock() < 1) {return Result.fail("优惠券已被抢光了哦,下次记得手速快点");}Long userId = UserHolder.getUser().getId();synchronized (userId.toString().intern()) {// 获取当前代理对象 确保下面的事务生效IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);}}@Transactionalpublic Result createVoucherOrder(Long voucherId) {// 一人一单逻辑Long userId = UserHolder.getUser().getId();synchronized (userId.toString().intern()) {int count = query().eq("voucher_id", voucherId).eq("user_id", userId).count();if (count > 0) {return Result.fail("你已经抢过优惠券了哦");}//5. 扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).gt("stock", 0).update();if (!success) {return Result.fail("库存不足");}//6. 创建订单VoucherOrder voucherOrder = new VoucherOrder();//6.1 设置订单idlong orderId = redisIdWorker.nextId("order");//6.2 设置用户idLong id = UserHolder.getUser().getId();//6.3 设置代金券idvoucherOrder.setVoucherId(voucherId);voucherOrder.setId(orderId);voucherOrder.setUserId(id);//7. 将订单数据保存到表中save(voucherOrder);//8. 返回订单idreturn Result.ok(orderId);}//执行到这里,锁已经被释放了,但是可能当前事务还未提交,如果此时有线程进来,不能确保事务不出问题}
}

启动类添加

@EnableAspectJAutoProxy(exposeProxy = true)

 依赖

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

接口

public interface IVoucherOrderService extends IService<VoucherOrder> {Result seckillVoucher(Long voucherId);Result createVoucherOrder(Long voucherId);
}

六、集群环境下的并发问题(引出分布式锁)

  • 通过加锁可以解决在单机情况下的一人一单安全问题,但是在集群模式下就不行了

    1. 我们将服务启动两份,端口分别为8081和8082
    2. 然后修改nginx的config目录下的nginx.conf文件,配置反向代理和负载均衡(默认轮询就行)
  • 具体操作,我们使用POSTMAN发送两次请求,header携带同一用户的token,尝试用同一账号抢两张优惠券,发现是可行的。

  • 失败原因分析:由于我们部署了多个Tomcat,每个Tomcat都有一个属于自己的jvm,那么假设在服务器A的Tomcat内部,有两个线程,即线程1和线程2,这两个线程使用的是同一份代码,那么他们的锁对象是同一个,是可以实现互斥的。但是如果在Tomcat的内部,又有两个线程,但是他们的锁对象虽然写的和服务器A一样,但是锁对象却不是同一个,所以线程3和线程4可以实现互斥,但是却无法和线程1和线程2互斥

  • 这就是集群环境下,syn锁失效的原因,在这种情况下,我们需要使用分布式锁来解决这个问题,让锁不存在于每个jvm的内部,而是让所有jvm公用外部的一把锁(Redis)

1. 添加Tomcat 形成集群

 改端口

 

2. 修改nginx的conf目录下的nginx.conf文件,配置反向代理和负载均衡


worker_processes  1;events {worker_connections  1024;
}http {include       mime.types;default_type  application/json;sendfile        on;keepalive_timeout  65;server {listen       8080;server_name  localhost;# 指定前端项目所在的位置location / {root   html/hmdp;index  index.html index.htm;}error_page   500 502 503 504  /50x.html;location = /50x.html {root   html;}//反向代理location /api {  default_type  application/json;#internal;  keepalive_timeout   30s;  keepalive_requests  1000;  #支持keep-alive  proxy_http_version 1.1;  rewrite /api(/.*) $1 break;  proxy_pass_request_headers on;#more_clear_input_headers Accept-Encoding;  proxy_next_upstream error timeout;  #proxy_pass http://127.0.0.1:8081;proxy_pass http://backend;}}
//负载均衡(轮询)upstream backend {server 127.0.0.1:8081 max_fails=5 fail_timeout=10s weight=1;server 127.0.0.1:8082 max_fails=5 fail_timeout=10s weight=1;}  
}

 启动nginx输入该命令重新配置nginx

nginx.exe -s reload

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

QT绘画仪表盘

代码一步一步讲&#xff0c;就不写用啥之类的了&#xff0c;暗部走来&#xff0c;自己找使用的类以及使用方法 1、创建工程 2、重载paintEvent #include <QMainWindow> #include <QPainter> #include <QPaintEvent> protected:virtual void paintEvent(QP…

QT自定义标题栏窗口其一:实现拖动及可拉伸效果

1、效果 2、核心代码 #include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(paren

前端新手小白的Vue3入坑指南

昨天有同学说想暑假在家学一学Vue3&#xff0c;问我有没有什么好的文档&#xff0c;我给他找了一些&#xff0c;然后顺带着&#xff0c;自己也写一篇吧&#xff0c;希望可以给新手小白们一些指引&#xff0c;Vue3欢迎你。 目录 1 项目安装 1.1 初始化项目 1.2 安装初始化依…

redis哨兵模式下业务代码连接实现

目录 一&#xff1a;背景 二&#xff1a;实现过程 三&#xff1a;总结 一&#xff1a;背景 在哨兵模式下&#xff0c;真实的redis服务地址由一个固定ip转变为可以变化的ip,这样我们业务代码在连接redis的时候&#xff0c;就需要判断哪个主redis服务地址&#xff0c;哪个是从…

火绒安全删除explorer.exe文件造成windows系统异常的问题

问题 过程是这样的&#xff0c;电脑在使用过程中突然就变成了黑色的&#xff0c;任务栏、桌面等都消失了&#xff0c;只有部分程序的窗口。具体如下&#xff1a; 因为&#xff0c;在变化的时候&#xff0c;我有瞟到一眼有个火绒的气泡消息&#xff0c;就感觉是火绒错误的删除…

适用于Mac的免费外置硬盘数据恢复软件

“我有一个 1 TB 的外置硬盘&#xff0c;它被意外格式化了。我尝试从中恢复丢失的数据。我把它连接到我的Mac&#xff0c;但里面什么也没找到。我正在寻找适用于Mac的免费外置硬盘数据恢复软件&#xff0c;例如奇客数据恢复Mac版或其他Mac数据恢复免费软件来扫描它并恢复数据。…

如何有效地进行机台数据管理,让数据发挥更大的价值?

机台数据管理是一个涉及收集、存储、分析和保护与机台&#xff08;可能是机械设备、生产线设备、医疗设备等&#xff09;相关数据的过程。通常包括以下几个方面&#xff1a; 1.数据收集&#xff1a;使用传感器和数据采集系统来收集机台的性能数据&#xff0c;如温度、压力、速…

Python轻松设置Excel单元格数字显示格式

Excel作为强大的数据处理与分析工具&#xff0c;不仅能够存储大量数据&#xff0c;还支持复杂的数据处理与可视化功能。而如何恰当地展示Excel表格中的数据是Excel文件制作的关键之一。这便涉及到Excel单元格数字格式的设置。数字格式不仅关乎数据的美学呈现&#xff0c;如货币…

【Sa-Token|4】Sa-Token微服务项目应用

若微服务数量多&#xff0c;如果每个服务都改动&#xff0c;工作量大&#xff0c;则可以只在网关和用户中心进行改动&#xff0c;也是可以实现单点登录的。 这种方式可以通过在网关服务中生成和验证 Sa-Token&#xff0c;并将其与现有的 Token关联存储在 Redis 中。用户中心提供…

arsetryhtehrwgefwadasdadasd

驱动项目 https://github.com/wqreytuk/article/blob/main/agsdygasidgasuidgoGx64_ASM_Kernel_Mode-main.7z 48b91400000080f7ffff48b8bd427ae5d594bfd6488b0948f7e148b8cdcccccccccccccc48c1ea1748f7e24c8bea49c1ed02 直接在windbg中把执行内存修改为上面这一串字节序列&am…

Java开发环境配置

一、JDK 下载JDK&#xff1a;Java Downloads | Oracle 配置环境变量&#xff1a;09、Java入门&#xff1a;Path、JAVA_HOME环境变量配置_哔哩哔哩_bilibili 二、IDEA 下载IDEA&#xff1a; Download IntelliJ IDEA – The Leading Java and Kotlin IDE (jetbrains.com) 建…

【Sa-Token|3】Sa-Token集成到现有微服务详细介绍

一、系统架构调整 用户中心&#xff1a;保持现有的用户登录、注册接口不变。多个项目&#xff1a;前后端分离&#xff0c;保持现有逻辑不变。网关服务&#xff1a;新增或配置网关服务&#xff0c;处理所有请求并进行 Token 校验和转发。统一 Token 管理&#xff1a;通过 Sa-Tok…

Kafka多维度调优

优化金字塔 应用程序层面 框架层面&#xff08;Broker层面&#xff09; JVM层面 操作系统层面 应用程序层面&#xff1a;应当优化业务代码合理使用kafka&#xff0c;合理规划主题&#xff0c;合理规划分区&#xff0c;合理设计数据结构&#xff1b; 框架层面&#xff1a;在不…

俄罗斯塔斯社TASS 媒体投放报道:海外媒体发稿扭转战局

大舍传媒 -作为一家颇具影响力的媒体机构&#xff0c;一直致力于传播客观、真实的新闻信息。最近&#xff0c;大舍传媒注意到了塔斯社TASS的报道&#xff0c;了解到海外媒体发稿对于扭转国内局势有着重要的影响。本文将就此话题进行分析探讨。 塔斯社TASS&#xff1a;俄语区最…

transformer之位置编码

由于 Transformer 模型中自注意力模块具有置换不变性,因此仅使用注意力机制无法捕捉序列中的顺序关系,从而退化为“词袋模型”。为了解决这一问题,需要引入位置编码(Position Embedding, PE)对于序列信息进行精确建模,从而将绝对或相对位置信息整合到模型中。 什么是位置…

Excel 组内多列明细拼成一行

某表格有 1 个分组列和 2 个明细列。 ABC1ObjectNameInfo212AGggtz44456312AGggtr99987412AKkkio66543512ABbvgf66643612AVvvhg888765712AFffgt8786FGggtf23232596FXxxde44321610P23Cccvb554328711P23Vvvbj565656412P23Sswec898976413P23Llloiu343432 现在要把组内的多列明细…

在ubuntu中恢复误删除的文件

1、安装 TestDisk 在 Ubuntu 上&#xff0c;可以使用以下命令安装 TestDisk&#xff1a; sudo apt-get install testdisk2、查询你删除的文件所在那个分区 #查询分区 df -h #我这里是/dev/sda2 #也可以使用下面命令查看具体哪个分区 lsblk3、查询该分区是什么系统类型 sudo …

数据库系统概论(个人笔记)(第四部分)

数据库系统概论&#xff08;个人笔记&#xff09; 文章目录 数据库系统概论&#xff08;个人笔记&#xff09;4、中间的SQL4.1 连接表达式4.2 视图4.3 事务4.4 完整性约束4.5 SQL数据类型和模式4.6 SQL中的索引定义4.7 授权 4、中间的SQL 4.1 连接表达式 Join Expressions Join…

计算机网络:网络层 - 路由选择协议

计算机网络&#xff1a;网络层 - 路由选择协议 路由器的结构路由选择协议概述自治系统 AS内部网关协议路由信息协议 RIP距离向量算法RIP报文格式收敛问题 开放最短路径优先 OSPF基本工作原理自治系统分区 外部网关协议BGP-4 路由器的结构 如图所示&#xff0c;路由器被分为路由…

PostgreSQL的学习心得和知识总结(一百四十五)|深入理解PostgreSQL数据库之ShowTransactionState的使用及父子事务有限状态机

目录结构 注&#xff1a;提前言明 本文借鉴了以下博主、书籍或网站的内容&#xff0c;其列表如下&#xff1a; 1、参考书籍&#xff1a;《PostgreSQL数据库内核分析》 2、参考书籍&#xff1a;《数据库事务处理的艺术&#xff1a;事务管理与并发控制》 3、PostgreSQL数据库仓库…