Day110 Java项目 (SSM+Dubbo)商城(十九) 秒杀(二)

第1章 多线程下单

一.实现思路分析

  1. 在审视秒杀中,操作一般都是比较复杂的,而且并发量特别高,比如,检查当前账号操作是否已经秒杀过该商品,检查该账号是否存在存在刷单行为,记录用户操作日志等。
    下订单这里,我们一般采用多线程下单,但多线程中我们又需要保证用户抢单的公平性,也就是先抢先下单。我们可以这样实现,用户进入秒杀抢单,如果用户复合抢单资格,只需要记录用户抢单数据,存入队列,多线程从队列中进行消费即可,存入队列采用左压,多线程下单采用右取的方式。

二.Spring实现多线程

  1. 我们过去实现多线程的方式通常是继承Thread类或者实现Runnable 接口,这种方式实现起来比较麻烦。spring封装了Java的多线程的实现,你只需要关注于并发事物的流程以及一些并发负载量等特性。spring通过任务执行器TaskExecutor来实现多线程与并发编程。通常使用ThreadPoolTaskExecutor来实现一个基于线程池的TaskExecutor.
  2. 开启线程池
    首先你要实现AsyncConfigurer 这个接口,目的是开启一个线程池 ,这个步骤我们可以基于spring的配置文件实现,修改qingcheng_service_seckill的applicationContext-timer.xml文件,代码如下:
  3. 异步执行声明
    然后注入一个类,实现你的业务,并在你的Bean的方法中使用@Async注解来声明其是一个异步任务 ,例如,我们创建一个类com.qingcheng.task.MultiThreadingCreateOrder,在类里写一个方法createOrder,加上注解@Async,代码如下:
  4. 异步调用
    在每次创建订单的时候,我们调用上面异步方法,测试是否异步执行。
    修改秒杀抢单SeckillOrderServiceImpl代码,注入MultiThreadingCreateOrder,并调用createOrder方法,代码如下:

三.多线程抢单

  1. 用户每次下单的时候,我们都让他们先进行排队,然后采用多线程的方式创建订单,排队我们可以采用Redis的队列实现,多线程下单我们可以采用Spring的异步实现。
  2. 多线程下单
    将之前下单的代码全部挪到多线程的方法中,com.qingcheng.service.impl.SeckillOrderServiceImpl类的方法值负责调用即可,代码如下:

    多线程下单代码如下图:

  3. 排队下单
    1. 排队信息封装
      用户每次下单的时候,我们可以创建一个队列进行排队,然后采用多线程的方式创建订单,排队我们可以采用Redis的队列实现。 排队信息中需要有用户抢单的商品信息,主要包含商品ID,商品抢购时间段,用户登录名。我们可以设计个javabean,如下:
    2. 排队实现
      我们可以将秒杀抢单信息存入到Redis中,这里采用List方式存储,List本身是一个队列,用户点击抢购的时候,就将用户抢购信息存入到Redis中,代码如下:

      多线程每次从队列中获取数据,分别获取用户名和订单商品编号以及商品秒杀时间段,进行下单操作,代码如下:
  4. 下单状态查询
    按照上面的流程,虽然可以实现用户下单异步操作,但是并不能确定下单是否成功,所以我们需要做一个页面判断,每过1秒钟查询一次下单状态,多线程下单的时候,需要修改抢单状态,支付的时候,清理抢单状态。
    1. 下单更新抢单状态
      用户每次点击抢购的时候,如果排队成功,则将用户抢购状态存储到Redis中,多线程抢单的时候,如果抢单成功,
      则更新抢单状态。
      修改com.qingcheng.service.impl.SeckillOrderServiceImpl的add方法,记录状态,代码如下:

      多线程抢单更新状态,修改com.qingcheng.task.MultiThreadingCreateOrder的createOrder方法,代码如下:
    2. 后台查询抢单状态
      后台提供抢单状态查询方法,修改com.qingcheng.service.seckill.SeckillOrderService,添加如下查询方法:

      修改com.qingcheng.service.impl.SeckillOrderServiceImpl,添加如下实现方法:

      修改com.qingcheng.controller.SeckillOrderController,添加如下查询方法:
    3. 前端循环查询
      在js中添加一个循环查询方法,每秒钟查询一次,累计查询120秒,代码如下:


      抢单后,立即执行上面每秒查询1次的方法,代码如下:

第2章 防止秒杀重复排队

用户每次抢单的时候,一旦排队,我们设置一个自增值,让该值的初始值为1,每次进入抢单的时候,对它进行递增,如果值>1,则表明已经排队,不允许重复排队,如果重复排队,则对外抛出异常,并抛出异常信息100表示已经正在排队。

一.后台排队记录

  1. 修改com.qingcheng.service.impl.SeckillOrderServiceImpl的add方法,新增递增值判断是否排队中,代码如下:

二.页面识别重复排队

  1. 修改页面下单js方法add,添加重复排队识别代码,代码如下:

第3章 并发超卖问题解决

一.思路分析

  1. 解决超卖问题,可以利用Redis队列实现,给每件商品创建一个独立的商品个数队列,例如:A商品有2个,A商品的ID为1001,则可以创建一个队列,key=SeckillGoodsCountList_1001,往该队列中塞2次该商品ID。
  2. 每次给用户下单的时候,先从队列中取数据,如果能取到数据,则表明有库存,如果取不到,则表明没有库存,这样就可以防止超卖问题产生了。
  3. 在我们队Redis进行操作的时候,很多时候,都是先将数据查询出来,在内存中修改,然后存入到Redis,在并发场景,会出现数据错乱问题,为了控制数量准确,我们单独将商品数量整一个自增键,自增键是线程安全的,所以不担心并发场景的问题。

二.商品个数队列创建

  1. 每次将商品压入Redis缓存的时候,另外多创建一个商品的队列。
    修改com.qingcheng.timer.SeckillGoodsPushTask,添加一个pushIds方法,用于将指定商品ID放入到指定的数字中,代码如下:
  2. 修改SeckillGoodsPushTask的loadGoodsPushRedis方法,添加队列操作,代码如下:

三.超卖控制

  1. 修改多线程下单方法,分别修改数量控制,以及售罄后用户抢单排队信息的清理,修改代码如下图:


  2. 用户抢单的时候,也做一个剩余库存数量判断,修改com.qingcheng.service.impl.SeckillOrderServiceImpl的add方法,代码如下:
  3. 页面加上对应判断

第4章 订单支付

完成秒杀下订单后,进入支付页面,此时前端会每3秒中向后台发送一次请求用于判断当前用户订单是否完成支付,如果完成了支付,则需要清理掉排队信息,并且需要修改订单状态信息。

一.修改订单状态

  1. 创建一个SeckillOrderMapper接口,作为Dao层,代码如下:
  2. 修改com.qingcheng.service.seckill.SeckillOrderService接口,添加如下方法:
  3. 修改com.qingcheng.service.impl.SeckillOrderServiceImpl添加updatePayStatus方法,代码如下:

二.创建支付二维码

  1. 下单成功后,会跳转到支付选择页面,在支付选择页面要显示订单编号和订单金额,所以我们需要在下单的时候,将订单金额以及订单编号信息存储到用户查询对象中。
    选择微信支付后,会跳转到微信支付页面,微信支付页面会根据用户名查看用户秒杀订单,并根据用户秒杀订单的ID创建预支付信息并获取二维码信息,展示给用户看,此时页面每3秒查询一次支付状态,如果支付成功,需要修改订单状态信息。
  2. 回显订单号、金额
    下单后,进入支付选择页面,需要显示订单号和订单金额,所以需要在用户下单后将该数据传入到pay.html页面,所以查询订单状态的时候,需要将订单号和金额封装到查询的信息中,修改查询订单装的方法加入他们即可。
    修改com.qingcheng.controller.SeckillOrderController的queryStatus方法,代码如下:
  3. 用户订单查询
    编写一个方法用于根据用户名查询用户订单信息。
    修改com.qingcheng.service.seckill.SeckillOrderService接口,添加如下方法

    修改com.qingcheng.service.impl.SeckillOrderServiceImpl,添加根据用户名查询订单信息的方法,代码如下:
  4. 创建二维码
    在qingcheng_web_seckill工程中添加com.qingcheng.controller.PayController,并创建获取二维码信息的方法,代码如下:

  5.  创建二维码页面对接
    将支付工程中的pay.html,weixinpay.html,paysuccess.html,payfail.html拷贝到qingcheng_web_seckill工程根目录。
    修改seckill-item.html页面的queryStatus方法,一旦支付成功,跳转到支付选择页面,代码如下:

三.支付状态查询

  1. 用户支付后,从前端循环查询状态,如果支付成功了,则修改订单状态,并清理用户排队信息。
    在PayController.java中,添加后台查询状态的方法,代码如下:

第5章 超时订单库存回滚

       用户每次下单后,不一定会立即支付,甚至有可能不支付,那么此时我们需要删除用户下的订单,并回滚库存。这里我们可以采用MQ的延时消息实现,每次用户下单的时候,如果订单创建成功,则立即发送一个延时消息到MQ中,等待消息被消费的时候,先检查对应订单是否下单支付成功,如果支付成功,会在MySQL中生成一个订单,如果MySQL中没有支付,则Redis中还有该订单信息的存在,需要删除该订单信息以及用户排队信息,并恢复库存。

一.RabbitMQ延时队列讲解

  1. RabbitMQ并没有直接实现延时队列,但是可以利用RabbitMQ两个属性实现延时队列特性:
    1. x-message-ttl:消息过期时间(Time To Live,TTL),超过过期时间之后即变为死信(Dead-letter),不会再被消费者消费。
      设置TTL有两种方式:
      (1)创建队列时指定x-message-ttl,此时整个队列具有统一过期时间;
      (2)发送消息为每个消息设置expiration,此时消息之间过期时间不同。
      注意:如果两者都设置,过期时间取两者最小。
    2. x-dead-letter-exchange:过期消息路由转发,当消息达到过期时间由该exchange按照配置的x-dead-letterrouting-key转发到指定队列,最后被消费者消费。

二.关闭微信支付订单

  1. 取消订单库存回滚的时候,需要注意这么个场景,用户有可能正在扫码支付,所以我们需要先关闭微信支付,然后再取消本地订单回滚库存。
  2. 关闭微信订单API
    微信支付API参考地址:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_3
    接口链接:https://api.mch.weixin.qq.com/pay/closeorder
    请求参数:

    返回结果:

    这里我们只关心成功结果,失败结果人工处理。
  3. 关闭微信支付实现
    修改com.qingcheng.service.pay.WeixinPayService,添加关闭支付方法,代码如下:

    修改com.qingcheng.service.impl.WeixinPayServiceImpl,添加关闭支付实现,代码如下:

     

三.下单延时消息发送

  1.  集成RabbitMQ
    修改qingcheng_service_seckill工程,在该工程中添加applicationContext-rabbitmq-producer.xml,用于配置消息发送对象
  2. 下单延时消息发送
    在下单的时候,实现消息发送,这里采用延时消息队列,代码如下:


    订单创建完成后记得调用上面发送延时消息的方法,代码如下:
  3. 延时消息消费
    创建一个com.qingcheng.consumer.OrderMessageListener类用于消费延时消息,并在该方法中实现数据回滚等操作,代码如下:
  4. 库存回滚
    创建一个方法,实现库存回滚,并在消息消费后调用该方法,完整代码如下:


    由于消息消费 类中使用到了Dubbo的服务提供对象,所以需要在applicationContext-service.xml中新增一个dubbo的包扫描,代码如下:
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值