秒杀业务: 简单梳理

news/2024/9/25 21:26:18/

回顾概述

(1)在一人一单优惠券秒杀中,先是解决了因线程安全导致的超卖问题,为了满足一人一单的业务需求需要给查询订单表查询库存表上锁,但是synchronized只能满足单机环境下线程安全,在分布式环境对多个JVM需要使用分布式锁,于是手写分布式锁,但是在极端情况下,例如业务超时,会出现“误删”问题,手写锁无法解决,于是引入redisson,借助看门狗机制可以有效解决这个问题。
(2)在整个过程中,涉及到了多次查询和写入数据库操作,通过Jmeter测试性能,最长响应时间达到了秒级,用户体验很差,想到借助异步秒杀(用到了Stream流),并且将两次查询数据库的操作放到redis中以Lua脚本方式(主要是为了保证原子性),将“处理订单”的操作放到消息队列当中,让其他线程接收并且处理。
我们只需要执行一个判断的逻辑,如果库存足够并且满足“一人一单”,就给用户响应下单成功的结果。

业务流程

当用户发起请求时,会请求到nginx,nginx会访问tomcat,而tomcat中的程序,会进行串行操作,分成如下几个步骤:
1、查询优惠卷
2、判断秒杀库存是否足够
3、查询订单
4、校验是否是一人一单
5、扣减库存
6、创建订单
在这里插入图片描述

方案思考

我们将耗时比较短的逻辑判断放入到redis中,比如是否库存足够,比如是否一人一单,这样的操作,只要这种逻辑可以完成,就意味着我们是一定可以下单完成的,我们只需要进行快速的逻辑判断,根本就不用等下单逻辑走完,我们直接给用户返回成功, 再在后台开一个线程,后台线程慢慢的去执行queue里边的消息,这样程序不就超级快了吗?而且也不用担心线程池消耗殆尽的问题,因为这里我们的程序中并没有手动使用任何线程池,当然这里边有两个难点。

如何快速判断一人一单

当用户下单之后,判断库存是否充足只需要导redis中去根据key找对应的value是否大于0即可,如果不充足,则直接结束,如果充足,继续在redis中判断用户是否可以下单,如果set集合中没有这条数据,说明他可以下单,如果set集合中没有这条记录,则将userId和优惠卷存入到redis中,并且返回0,整个过程需要保证是原子性的,我们可以使用lua来操作。

如何知道下单是否成功

我们在redis操作完之后,我们会将一些信息返回给前端,同时也会把这些信息丢到异步queue中去,后续操作中,可以通过这个id来查询我们tomcat中的下单逻辑是否完成了。

在这里插入图片描述

阻塞队列

内存限制/数据安全都无法得到保证

//异步处理线程池
private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();//在类初始化之后执行,因为当这个类初始化好了之后,随时都是有可能要执行的
@PostConstruct
private void init() {SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
}
// 用于线程池处理的任务
// 当初始化完毕后,就会去从对列中去拿信息private class VoucherOrderHandler implements Runnable{@Overridepublic void run() {while (true){try {// 1.获取队列中的订单信息VoucherOrder voucherOrder = orderTasks.take();// 2.创建订单handleVoucherOrder(voucherOrder);} catch (Exception e) {log.error("处理订单异常", e);}}}private void handleVoucherOrder(VoucherOrder voucherOrder) {//1.获取用户Long userId = voucherOrder.getUserId();// 2.创建锁对象RLock redisLock = redissonClient.getLock("lock:order:" + userId);// 3.尝试获取锁boolean isLock = redisLock.lock();// 4.判断是否获得锁成功if (!isLock) {// 获取锁失败,直接返回失败或者重试log.error("不允许重复下单!");return;}try {//注意:由于是spring的事务是放在threadLocal中,此时的是多线程,事务会失效proxy.createVoucherOrder(voucherOrder);} finally {// 释放锁redisLock.unlock();}}//aprivate BlockingQueue<VoucherOrder> orderTasks =new  ArrayBlockingQueue<>(1024 * 1024);@Overridepublic Result seckillVoucher(Long voucherId) {Long userId = UserHolder.getUser().getId();long orderId = redisIdWorker.nextId("order");// 1.执行lua脚本Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,Collections.emptyList(),voucherId.toString(), userId.toString(), String.valueOf(orderId));int r = result.intValue();// 2.判断结果是否为0if (r != 0) {// 2.1.不为0 ,代表没有购买资格return Result.fail(r == 1 ? "库存不足" : "不能重复下单");}VoucherOrder voucherOrder = new VoucherOrder();// 2.3.订单idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);// 2.4.用户idvoucherOrder.setUserId(userId);// 2.5.代金券idvoucherOrder.setVoucherId(voucherId);// 2.6.放入阻塞队列orderTasks.add(voucherOrder);//3.获取代理对象proxy = (IVoucherOrderService)AopContext.currentProxy();//4.返回订单idreturn Result.ok(orderId);}@Transactionalpublic  void createVoucherOrder(VoucherOrder voucherOrder) {Long userId = voucherOrder.getUserId();// 5.1.查询订单int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();// 5.2.判断是否存在if (count > 0) {// 用户已经购买过了log.error("用户已经购买过了");return ;}// 6.扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1") // set stock = stock - 1.eq("voucher_id", voucherOrder.getVoucherId()).gt("stock", 0) // where id = ? and stock > 0.update();if (!success) {// 扣减失败log.error("库存不足");return ;}save(voucherOrder);}

Stream流

思路

  • 创建一个Stream类型的消息队列,名为stream.orders
  • 修改之前的秒杀下单Lua脚本,在认定有抢购资格后,直接向stream.orders中添加消息,内容包含voucherId、userId、orderId
  • 项目启动时,开启一个线程任务,尝试获取stream.orders中的消息,完成下单

实现

lua">-- 1.参数列表
-- 1.1.优惠券id
local voucherId = ARGV[1]
-- 1.2.用户id
local userId = ARGV[2]
-- 1.3.订单id
local orderId = ARGV[3]-- 2.数据key
-- 2.1.库存key
local stockKey = 'seckill:stock:' .. voucherId
-- 2.2.订单key
local orderKey = 'seckill:order:' .. voucherId-- 3.脚本业务
-- 3.1.判断库存是否充足 get stockKey
if(tonumber(redis.call('get', stockKey)) <= 0) then-- 3.2.库存不足,返回1return 1
end
-- 3.2.判断用户是否下单 SISMEMBER orderKey userId
if(redis.call('sismember', orderKey, userId) == 1) then-- 3.3.存在,说明是重复下单,返回2return 2
end
-- 3.4.扣库存 incrby stockKey -1
redis.call('incrby', stockKey, -1)
-- 3.5.下单(保存用户)sadd orderKey userId
redis.call('sadd', orderKey, userId)
-- 3.6.发送消息到队列中, XADD stream.orders * k1 v1 k2 v2 ...
redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId)
return 0
    public Result seckillVoucher(Long voucherId){Long userId = UserHolder.getUser().getId();//生成订单编号long orderId = redisIdWorker.nextId("order");// 执行lua脚本// 会在lua脚本当中将订单放到stream流中Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,Collections.emptyList(),voucherId.toString(),userId.toString(),String.valueOf(orderId));int success = result.intValue();if (success != 0) return Result.fail(success == -1 ? "库存不足" : "不能重复下单");//将订单保存至阻塞队列//3.返回订单idreturn Result.ok(orderId);}
    //异步秒杀业务更改,执行lua脚本之后异步下单private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();@PostConstructprivate void init(){SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());}static {SECKILL_SCRIPT = new DefaultRedisScript<>();SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));SECKILL_SCRIPT.setResultType(Long.class);}//子线程private class VoucherOrderHandler implements Runnable{@Overridepublic void run() {while (true){try {// 1.获取消息队列中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 >List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(Consumer.from("g1", "c1"),StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),StreamOffset.create("stream.orders", ReadOffset.lastConsumed()));//2.判断订单是否为空if (list == null || list.isEmpty()) continue;//3.解析数据MapRecord<String,Object,Object> record = list.get(0);//使用value来填充voucherOrder的属性Map<Object, Object> value = record.getValue();VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);//3.创建订单createVoucherOrder2(voucherOrder);//4.确认消息xackstringRedisTemplate.opsForStream().acknowledge("s1","g1",record.getId());}catch (Exception e){e.printStackTrace();handlePendingList();}}}private void handlePendingList() {while (true){try {// 1.获取消息队列中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 >List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(Consumer.from("g1", "c1"),StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),StreamOffset.create("stream.orders", ReadOffset.lastConsumed()));//2.判断订单是否为空if (list == null || list.isEmpty()) continue;//3.解析数据//因为只读取一条记录所以list.get(0)MapRecord<String,Object,Object> record = list.get(0);//使用value来填充voucherOrder的属性Map<Object, Object> value = record.getValue();VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);//3.创建订单createVoucherOrder2(voucherOrder);//4.确认消息xackstringRedisTemplate.opsForStream().acknowledge("s1","g1",record.getId());}catch (Exception e){log.error("处理订单异常{}",e.getMessage());
//                    handlePendingList();}}}/*** 改造成为异步下单* 创建订单* @param voucherOrder 订单信息*/private void createVoucherOrder2(VoucherOrder voucherOrder) {//添加逻辑:该用户是否已经已经下过单了//防止黄牛恶意刷单 但是会存在和超卖相同的问题 同一个用户但是超卖了三张票Long userId = voucherOrder.getUserId();RLock lock = redissonClient.getLock("lock:order:" + userId);boolean tryLock = lock.tryLock();if (!tryLock) {log.error("不允许重复下单");return;}try {Long voucherId = voucherOrder.getVoucherId();//3.查询订单Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();//4.校验一人一单if (count > 0) {log.error("无法重复下单");return;}//5.库存扣减 超减问题并发量3000的情况下 超卖10张boolean success = seckillVoucherService.update().setSql("stock= stock -1").eq("voucher_id", voucherId).gt("stock", 0).update();if (!success) {log.error("库存不足,下单失败");return;}//6.添加订单voucherOrderService.save(voucherOrder);}catch (Exception e){log.error("下单失败{}",e.getMessage());}finally {lock.unlock();}}}

http://www.ppmy.cn/news/1499861.html

相关文章

cesium获取模型的数据包含b3dm和cmpt

getreadyPromise()方法在模型加载完成后调用 url为模型地址 // tileset模型 function tilesetM(url) {tileset viewer.scene.primitives.add(new Cesium.Cesium3DTileset({// url: ../../public/asd/tileset.json,url: url,// type: "3dtiles",maximumScreenSpace…

实变函数精解【7】

文章目录 点集基础理论点集稠密一、度量空间中的稠密性二、偏序集中的稠密性三、具体例子四、稠密性的重要性五、结论 参考文献 点集 基础 设 E 1 , E 2 是 R 中的非空点集&#xff0c;且 E 2 ′ ≠ ∅ , 试证明 E ˉ 1 E 2 ′ ⊂ ( E 1 E 2 ) ′ 设E_1,E_2是R中的非空点集&…

VSCode | 修改编辑器注释的颜色

1 打开VsCode的设置进入settings.json 2 添加如下代码 "editor.tokenColorCustomizations": {"comments": "#17e917"},3 保存即可生效

Radon(拉当) 变换:超详细讲解(附MATLAB,Python 代码)

Radon 变换 Radon 变换是数学上用于函数或图像的一种积分变换&#xff0c;广泛应用于图像处理领域&#xff0c;尤其是在计算机断层成像 (CT) 中。本文档将详细介绍 Radon 变换的数学含义及其在图像处理中的应用。 数学定义 Radon 变换的数学定义是将二维函数 f ( x , y ) f…

爬虫基础之HTTP基本原理

引言 在Web开发中&#xff0c;爬虫&#xff08;Web Crawler&#xff09;扮演着重要的角色&#xff0c;它们能够自动浏览万维网并抓取信息。这些程序通过遵循HTTP&#xff08;超文本传输协议&#xff09;协议与服务器进行通信&#xff0c;从而获取网页内容。了解HTTP基本原理对…

Windows下编译安装Kratos

Kratos是一款开源跨平台的多物理场有限元框架。本文记录在Windows下编译Kratos的流程。 Ref. from Kratos KRATOS Multiphysics ("Kratos") is a framework for building parallel, multi-disciplinary simulation software, aiming at modularity, extensibility, a…

以flask为后端的博客项目——星云小窝

以flask为后端的博客项目——星云小窝 文章目录 以flask为后端的博客项目——星云小窝前言一、星云小窝项目——项目介绍&#xff08;一&#xff09;二、星云小窝项目——项目启动&#xff08;二&#xff09;三、星云小窝项目——项目结构&#xff08;三&#xff09;四、谈论一…

Android小技巧:利用动态代理自动切换线程(续)

本文是针对上文Android小技巧&#xff1a;利用动态代理自动切换线程的一个补充&#xff0c;补充一种简单的实现方式。 上文中我们提到利用动态代理将对某个对象的方法调用自动切换到对应线程中去&#xff0c;只是探讨了可行性和局限&#xff0c;但如果每个功能都手动创建代理就…