单机环境下一人一单

优惠券秒杀

添加优惠卷

店铺发布优惠券又分为平价券和特价券, 平价券可以任意购买而特价券需要秒杀抢购(限制数量和时间)

tb_voucher(平价券): 优惠券的基本信息

在这里插入图片描述

tb_seckill_voucher(秒杀券): 有voucher_id字段表示具有优惠卷的基本信息,此外还有库存,开始抢购时间,结束抢购时间等特殊字段

在这里插入图片描述

VoucherController提供了一个接口方法用来添加普通优惠券

/*** 新增普通券* @param voucher 优惠券信息* @return 优惠券id*/
@PostMapping
public Result addVoucher(@RequestBody Voucher voucher) {// 直接将普通券的信息保存到普通券表中voucherService.save(voucher);return Result.ok(voucher.getId());
}

VoucherController提供了一个接口方法用来添加秒杀券,在VoucherService中的addSeckillVoucher方法实现添加秒杀券的业务逻辑

/*** 新增秒杀券* @param voucher 优惠券信息,包含秒杀券信息(库存,生效时间,失效时间)* @return 优惠券id*/
@PostMapping("seckill")
public Result addSeckillVoucher(@RequestBody Voucher voucher) {voucherService.addSeckillVoucher(voucher);return Result.ok(voucher.getId());
}
// 新增秒杀券就是在数据库中新增普通卷和秒杀券的信息
@Override
@Transactional// 因为是操作两张表所以需要添加事务
public void addSeckillVoucher(Voucher voucher) {// 将秒杀券的基本信息保存到普通券表中,如果没有指定Id会自动生成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);
}

添加秒杀券: 由于没有后台管理页面,使用Postman模拟发送POST请求http://localhost:8081/voucher/seckill来新增秒杀券(截止日期要超过当前日期否则不显示)

{"shopId":1,"title":"100元代金券","subTitle":"周一至周五可用","rules":"全场通用\\n无需预约\\n可无限叠加",// 数据库中金额的单位是分"payValue":8000,"actualValue":10000,"type":1,"stock":100,"beginTime":"2022-01-01T00:00:00","endTime":"2023-10-31T23:59:59"
}

实现秒杀下单

当我们点击抢购时会触发右侧的请求,我们只需要在VoucherOrderController编写对应的Controller处理请求即可

在这里插入图片描述

当用户开始进行下单我们应当提交优惠券Id去查询优惠卷信息,然后判断是否满足秒杀条件即秒杀是否开始和库存是否充足

在这里插入图片描述

@RestController
@RequestMapping("/voucher-order")
public class VoucherOrderController {@Autowiredprivate IVoucherOrderService voucherOrderService;@PostMapping("/seckill/{id}")public Result seckillVoucher(@PathVariable("id") Long voucherId) {return voucherOrderService.seckillVoucher(voucherId);}
}public interface IVoucherOrderService extends IService<VoucherOrder> {Result seckillVoucher(Long voucherId);
}@Slf4j
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Resourceprivate ISeckillVoucherService seckillVoucherService;@Autowiredprivate RedisIdWorker redisIdWorker;@Override@Transactional// 操作两张表应该加上事务public Result seckillVoucher(Long voucherId) {//1.查询优惠券的基本信息//SeckillVoucher seckillVouche = seckillVoucherService.getById(voucherId)LambdaQueryWrapper<SeckillVoucher> queryWrapper = new LambdaQueryWrapper<>();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("优惠券已被抢光了哦,下次记得手速快点");}//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 设置生成的订单idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);//6.2 设置用户idLong id = UserHolder.getUser().getId();voucherOrder.setUserId(id);//6.3 设置代金券idvoucherOrder.setVoucherId(voucherId);//7. 将订单数据保存到订单表中save(voucherOrder);//8. 返回订单idreturn Result.ok(orderId);}

库存超卖问题

当遇到高并发场景时会出现超卖现象,我们可以用Jmeter开200个线程来模拟抢100张优惠券的场景,结果优惠券库存为负数表示出现了超卖现象

  • 添加请求的信息头管理器携带我们登录的token(可能会过期),然后发起POST请求http://localhost:8081/voucher-order/seckill/voucher_id

在这里插入图片描述

在这里插入图片描述

乐观锁的两种实现方式

超卖问题是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁(悲观锁或乐观锁)

  • 悲观锁: 悲观锁比较适合插入数据,简单粗暴但是性能一般
  • 乐观锁: 比较适合更新数据, 性能好但是成功率低(多个线程同时执行时只有一个可以执行成功),还需要访问数据库造成数据库压力过大

在这里插入图片描述

版本号法: 给数据库表增加一个版本号version字段,每次操作表中的数据时会查询版本号,修改数据时再次验证版本号有没有变化,没有变化才可以更新数据

在这里插入图片描述

CAS(Compare-And-Switch): 首先查询要修改字段的值,在修改数据时再次验证字段值有没有发生变化(或满足某种条件),没有变化(或满足条件)才会更新字段的值

在这里插入图片描述

乐观锁解决超卖问题

使用stock来充当版本号,VoucherOrderServiceImpl在扣减库存时比较查询到的优惠券库存和实际数据库中优惠券库存是否相同

  • 假设100个线程同时都拿到了100的库存, 但是100个人中只有1个人能扣减成功(其他的人在扣减时库存发现库存已经被修改过了,所以不再执行扣减操作)
boolean success = seckillVoucherService.update().setSql("stock= stock -1") //set stock = stock -1.eq("voucher_id", voucherId).eq("stock",voucher.getStock()).update(); //where id = ? and stock = ?

使用stock>0 充当判断条件, 在扣减库存时只要判断是否有剩余优惠券,即只要数据库中的库存大于0都能顺利完成扣减库存操作

boolean success = seckillVoucherService.update().setSql("stock= stock -1").eq("voucher_id", voucherId).gt("stock",0).update(); where voucher_id = ? and stock > 0

单机环境下一人一单(悲观锁)

需求:修改秒杀业务要求同一个优惠券一个用户只能下一单

  • 如果时间和库存都充足,还需要根据优惠卷id和用户id查询是否已经下过这个订单,如果下过这个订单则不能再下单

在这里插入图片描述

在VoucherOrderServiceImpl中库存和时间都充足时即将扣减库存之前再增加一人一单逻辑

一个用户开了多个线程抢优惠券,在判断库存充足之后和执行一人一单逻辑之前间如果进来了多个线程,此时它们都在数据库中查询不到订单然后都会执行扣减操作

// 4. 判断库存是否充足
if (seckillVoucher.getStock() < 1) {return Result.fail("优惠券已被抢光了哦,下次记得手速快点");
}// 5.根据用户id查询用户对应的订单是否存在
Long userId = UserHolder.getUser().getId();
int count = query().eq("voucher_id", voucherId).eq("user_id", userId).count();
if (count > 0) {// 用户已经下过单return Result.fail("您已经抢过优惠券了哦");
}
// 6.使用stock>0充当判断条件,在扣减库存时只要判断是否有剩余优惠券,即只要数据库中的库存大于0都能顺利完成扣减库存操作
boolean success = seckillVoucherService.update().setSql("stock= stock -1").eq("voucher_id", voucherId).gt("stock",0).update(); //where voucher_id = ? and stock > 0
if (!success) {return Result.fail("库存不足");
}

把一人一单逻辑之后生成订单记录的代码都提取到一个createVoucherOrder方法中(ctrl + alt + m)然后加悲观锁synchronized(悲观锁适合插入数据)

  • 不管哪一个线程运行到这个方法时都要检查有没有其它线程正在用这个方法(或者该类的其他同步方法),有的话要等正在使用synchronized方法的线程结束
  • 把锁加在createVoucherOrder方法上锁的范围太大(粒度太粗)会导致每个线程进来都会锁住,锁的对象是this所有用户都公用一把锁串行执行会导致效率很低
@Transactional
public synchronized Result createVoucherOrder(Long voucherId) {// 5.根据用户id查询用户对应的订单是否存在Long userId = UserHolder.getUser().getId();int count = query().eq("voucher_id", voucherId).eq("user_id", userId).count();// 判断用户是否下过单if (count > 0) {return Result.fail("您已经抢过优惠券了哦");}// 6.使用stock>0充当判断条件,在扣减库存时只要判断是否有剩余优惠券,即只要数据库中的库存大于0都能顺利完成扣减库存操作boolean success = seckillVoucherService.update().setSql("stock= stock -1").eq("voucher_id", voucherId).gt("stock",0).update(); //where voucher_id = ? and stock > 0if (!success) {return Result.fail("库存不足");}//7. 创建订单VoucherOrder voucherOrder = new VoucherOrder();//7.1 设置订单idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);//7.2 设置用户idLong id = UserHolder.getUser().getId();voucherOrder.setUserId(id);//7.3 设置代金券idvoucherOrder.setVoucherId(voucherId);//8. 将订单数据保存到表中save(voucherOrder);//9. 返回订单idreturn Result.ok(orderId);
}

要完成一人一单的业务应该把这个锁只加在单个用户上(用户标识可以用userId), 如果我们直接使用userId.toString()每次锁住的都不是同一个String对象

方法名功能
String intern()从常量池中拿数据,如果字符串常量池中已经包含了一个等于这个String对象的字符串(由equals方法确定)将返回池中的字符串
如果没有则将此String对象添加到池中并返回对此String对象的引用
public static String toString(long i) {if (i == Long.MIN_VALUE)return "-9223372036854775808";int size = (i < 0) ? stringSize(-i) + 1 : stringSize(i);char[] buf = new char[size];getChars(i, size, buf);return new String(buf, true);
}
@Transactional
public Result createVoucherOrder(Long voucherId) {Long userId = UserHolder.getUser().getId();// toString的源码是new String所以userId.toString()拿到的也不是同一个String对象即不是同一个用户/不是同一把锁synchronized (userId.toString().intern()) {// 5.根据用户id查询用户对应的订单是否存在int count = query().eq("voucher_id", voucherId).eq("user_id", userId).count();// 判断用户是否下过单if (count > 0) {return Result.fail("您已经抢过优惠券了哦");}// 6.使用stock>0充当判断条件,在扣减库存时只要判断是否有剩余优惠券,即只要数据库中的库存大于0都能顺利完成扣减库存操作boolean success = seckillVoucherService.update().setSql("stock= stock -1").eq("voucher_id", voucherId).gt("stock",0).update(); //where voucher_id = ? and stock > 0if (!success) {return Result.fail("库存不足");}//7. 创建订单VoucherOrder voucherOrder = new VoucherOrder();//7.1 设置订单idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);//7.2 设置用户idLong id = UserHolder.getUser().getId();voucherOrder.setUserId(id);//7.3 设置代金券idvoucherOrder.setVoucherId(voucherId);//8. 将订单数据保存到表中save(voucherOrder);//9. 返回订单idreturn Result.ok(orderId);}//执行到这里锁已经被释放了但是可能当前事务还未提交,如果此时有线程进来不能确保事务不出问题
}

createVoucherOrder方法被Spring的事务控制,如果你在方法内部加锁可能会导致当前方法事务还没有提交但是锁已经释放了,此时新增的订单还没有写入数据库

  • 在seckillVoucher方法中将createVoucherOrder方法整体包裹起来, 保证事务提交之后才会释放锁确保数据库中有订单存在,同时控制锁的粒度
@Override
public 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("优惠券已被抢光了哦,下次记得手速快点");}// 获取用户idLong userId = UserHolder.getUser().getId();synchronized (userId.toString().intern()) {return createVoucherOrder(voucherId);}
}
@Transactional
public Result createVoucherOrder(Long voucherId) {// toString的源码是new String所以userId.toString()拿到的也不是同一个String对象即不是同一个用户/不是同一把锁synchronized (userId.toString().intern()) {// 5.根据用户id查询用户对应的订单是否存在int count = query().eq("voucher_id", voucherId).eq("user_id", userId).count();// 判断用户是否下过单if (count > 0) {return Result.fail("您已经抢过优惠券了哦");}// 6.使用stock>0充当判断条件,在扣减库存时只要判断是否有剩余优惠券,即只要数据库中的库存大于0都能顺利完成扣减库存操作boolean success = seckillVoucherService.update().setSql("stock= stock -1").eq("voucher_id", voucherId).gt("stock",0).update(); //where voucher_id = ? and stock > 0if (!success) {return Result.fail("库存不足");}//7. 创建订单VoucherOrder voucherOrder = new VoucherOrder();//7.1 设置订单idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);//7.2 设置用户idLong id = UserHolder.getUser().getId();voucherOrder.setUserId(id);//7.3 设置代金券idvoucherOrder.setVoucherId(voucherId);//8. 将订单数据保存到表中save(voucherOrder);//9. 返回订单idreturn Result.ok(orderId);}//执行到这里锁已经被释放了但是可能当前事务还未提交,如果此时有线程进来不能确保事务不出问题
}

由于seckillVoucher方法没有加事务注解,所以调用createVoucherOrder方法是this.的方式调用的,this此时是VoucherOrderServiceImpl没有事务功能

  • 事务想要生效需要利用VoucherOrderServiceImpl的代理对象,所以我们需要获得原始的事务对象来操作事务

  • 使用AopContext.currentProxy()获取当前对象的代理对象(具有事务功能),然后再用代理对象调用方法底层需要使用aspectjweaver依赖

  • 获取事务的代理对象需要在IVoucherOrderService中创建createVoucherOrder方法

  • 在启动类上加上@EnableAspectJAutoProxy(exposeProxy = true)注解

public interface IVoucherOrderService extends IService<VoucherOrder> {Result seckillVoucher(Long voucherId);Result createVoucherOrder(Long voucherId);
}
Long userId = UserHolder.getUser().getId();
synchronized (userId.toString().intern()) {IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);
}
<dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId>
</dependency>
@MapperScan("com.hmdp.mapper")
@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true)
public class HmDianPingApplication {public static void main(String[] args) {SpringApplication.run(HmDianPingApplication.class, args);}
}

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

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

相关文章

Java使用Microsoft Entra微软 SSO 认证接入

1. Microsoft Entra Microsoft Entra ID 是基于云的标识和访问管理服务&#xff0c;可帮助员工访问外部资源。 示例资源包括 Microsoft 365、Azure 门户以及成千上万的其他 SaaS 应用程序。 Microsoft Entra ID 还可帮助他们访问你的企业 Intranet 上的应用等内部资源&#x…

每天五分钟计算机视觉:GoogLeNet的核心模型结构——Inception

本文重点 当构建卷积神经网络的时候,我们需要判断我们的过滤器的大小,这往往也作为一个超参数需要我们进行选择。过滤器的大小究竟是 11,33 还是 55,或者要不要添加池化层,这些都需要我们进行选择。而本文介绍的Inception网络的作用就是代替你来决定,把它变成参数的一部…

利用Pytorch预训练模型进行图像分类

Use Pre-trained models for Image Classification. # This post is rectified on the base of https://learnopencv.com/pytorch-for-beginners-image-classification-using-pre-trained-models/# And we have re-orginaized the code script.预训练模型(Pre-trained models)…

uniapp交互反馈api的使用示例

官方文档链接&#xff1a;uni.showToast(OBJECT) | uni-app官网 1.uni.showToast({}) 显示消息提示框。 常用属性&#xff1a; title:页面提示的内容 image&#xff1a;改变提示框默认的icon图标 duration&#xff1a;提示框在页面显示多少秒才让它消失 添加了image属性后。 注…

前端体系:前端应用

目录 前端体系基础 html&#xff08;超文本标记语言&#xff09; css&#xff08;层叠样式单&#xff09; javascript&#xff08;&#xff09; 一、前端体系概述 二、前端框架 React Vue Angular 三、前端库和工具 lodash Redux Webpack 四、模块化和组件化 ES…

Java中的链表

文章目录 前言一、链表的概念及结构二、单向不带头非循坏链表的实现2.1打印链表2.2求链表的长度2.3头插法2.4尾插法2.5任意位置插入2.6查找是否包含某个元素的节点2.7删除第一次出现这个元素的节点2.8删除包含这个元素的所以节点2.9清空链表单向链表的测试 三、双向不带头非循坏…

RNN介绍及Pytorch源码解析

介绍一下RNN模型的结构以及源码&#xff0c;用作自己复习的材料。 RNN模型所对应的源码在&#xff1a;\PyTorch\Lib\site-packages\torch\nn\modules\RNN.py文件中。 RNN的模型图如下&#xff1a; 源码注释中写道&#xff0c;RNN的数学公式&#xff1a; 表示在时刻的隐藏状态…

可替代LM5145,5.5V-100V Vin同步降压控制器_SCT82A30

SCT82A30是一款100V电压模式控制同步降压控制器&#xff0c;具有线路前馈。40ns受控高压侧MOSFET的最小导通时间支持高转换比&#xff0c;实现从48V输入到低压轨的直接降压转换&#xff0c;降低了系统复杂性和解决方案成本。如果需要&#xff0c;在低至6V的输入电压下降期间&am…

C语言之文件操作(下)

C语言之文件操作&#xff08;下&#xff09; 文章目录 C语言之文件操作&#xff08;下&#xff09;1. 文件的顺序读写1.1 文件的顺序读写函数1.1.1 字符输入/输出函数&#xff08;fgetc/fputc&#xff09;1.1.2 ⽂本⾏输⼊/输出函数&#xff08;fgets/fputs&#xff09;1.1.3 格…

Spring Boot之自定义starter

&#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 接下来看看由辉辉所写的关于Spring Boot的相关操作吧 目录 &#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 一. starter是什么 二.为什么要使…

大模型应用_PrivateGPT

https://github.com/imartinez/privateGPT 1 功能 整体功能&#xff0c;想解决什么问题 搭建完整的 RAG 系统&#xff0c;与 FastGPT相比&#xff0c;界面比较简单。但是底层支持比较丰富&#xff0c;可用于知识库的完全本地部署&#xff0c;包含大模型和向量库。适用于保密级…

SWPU NSS新生赛

&#x1f60b;大家好&#xff0c;我是YAy_17&#xff0c;是一枚爱好网安的小白&#xff0c;正在自学ing。 本人水平有限&#xff0c;欢迎各位大佬指点&#xff0c;一起学习&#x1f497;&#xff0c;一起进步⭐️。 ⭐️此后如竟没有炬火&#xff0c;我便是唯一的光。⭐️ 最近…

万界星空科技AI低代码云MES系统

在企业生产管理过程中&#xff0c;从市场、生产现场到产品交付&#xff0c;生产制造行业都面临着诸多挑战&#xff0c;比如&#xff1a; 订单排产难度大&#xff1a;订单混乱&#xff0c;常漏排产、错排产&#xff1b;产能不明晰&#xff0c;无法承诺交期&#xff0c;常丢单&a…

流程控制之条件判断

目录 流程控制之条件判断 2.1.if语句语法 2.1.1单分支结构 2.1.2双分支结构 2.1.3多分支结构 2.2.案例 例一: 例2: 例3: 例4: 例5: 例6: 例7: 例8: 例9: 2.3.case多条件判断 2.3.1.格式 2.3.2.执行过程 例10: 流程控制之条件判断 2.1.if语句语法 2.1.1单分…

ArcGIS for Android开发引入arcgis100.15.2

最后再点击同步即可&#xff01;&#xff01;&#xff01;

oracle aq java jms使用(数据类型为XMLTYPE)

记录一次冷门技术oracle aq的使用 版本 oracle 11g 创建用户 -- 创建用户 create user testaq identified by 123456; grant connect, resource to testaq;-- 创建aq所需要的权限 grant execute on dbms_aq to testaq; grant execute on dbms_aqadm to testaq; begindbms_a…

基于Spring Boot、Mybatis、Redis和Layui的企业电子招投标系统源码实现与立项流程

招投标管理系统是一款适用于招标代理、政府采购、企业采购和工程交易等领域的企业级应用平台。该平台以项目为主线&#xff0c;从项目立项到项目归档&#xff0c;实现了全流程的高效沟通和协作。通过该平台&#xff0c;用户可以实时共享项目数据信息&#xff0c;实现规范化管理…

【数据结构入门精讲 | 第一篇】打开数据结构之门

数据结构与算法是计算机科学中的核心概念&#xff0c;也与现实生活如算法岗息息相关。鉴于全网数据结构文章良莠不齐且集成度不高&#xff0c;故开设本专栏&#xff0c;为初学者提供指引。 目录 基本概念数据结构为何面世算法基本数据类型抽象数据类型使用抽象数据类型的好处 数…

微信小程序:模态框(弹窗)的实现

效果 wxml <!--新增&#xff08;点击按钮&#xff09;--> <image classimg src"{{add}}" bindtapadd_mode></image> <!-- 弹窗 --> <view class"modal" wx:if"{{showModal}}"><view class"modal-conten…

消息队列(MQ)

对于 MQ 来说&#xff0c;不管是 RocketMQ、Kafka 还是其他消息队列&#xff0c;它们的本质都是&#xff1a;一发一存一消费。下面我们以这个本质作为根&#xff0c;一起由浅入深地聊聊 MQ。 01 从 MQ 的本质说起 将 MQ 掰开了揉碎了来看&#xff0c;都是「一发一存一消费」&…