第1章 多线程下单
一.实现思路分析
- 在审视秒杀中,操作一般都是比较复杂的,而且并发量特别高,比如,检查当前账号操作是否已经秒杀过该商品,检查该账号是否存在存在刷单行为,记录用户操作日志等。
下订单这里,我们一般采用多线程下单,但多线程中我们又需要保证用户抢单的公平性,也就是先抢先下单。我们可以这样实现,用户进入秒杀抢单,如果用户复合抢单资格,只需要记录用户抢单数据,存入队列,多线程从队列中进行消费即可,存入队列采用左压,多线程下单采用右取的方式。
二.Spring实现多线程
- 我们过去实现多线程的方式通常是继承Thread类或者实现Runnable 接口,这种方式实现起来比较麻烦。spring封装了Java的多线程的实现,你只需要关注于并发事物的流程以及一些并发负载量等特性。spring通过任务执行器TaskExecutor来实现多线程与并发编程。通常使用ThreadPoolTaskExecutor来实现一个基于线程池的TaskExecutor.
- 开启线程池
首先你要实现AsyncConfigurer 这个接口,目的是开启一个线程池 ,这个步骤我们可以基于spring的配置文件实现,修改qingcheng_service_seckill的applicationContext-timer.xml文件,代码如下: - 异步执行声明
然后注入一个类,实现你的业务,并在你的Bean的方法中使用@Async注解来声明其是一个异步任务 ,例如,我们创建一个类com.qingcheng.task.MultiThreadingCreateOrder,在类里写一个方法createOrder,加上注解@Async,代码如下: - 异步调用
在每次创建订单的时候,我们调用上面异步方法,测试是否异步执行。
修改秒杀抢单SeckillOrderServiceImpl代码,注入MultiThreadingCreateOrder,并调用createOrder方法,代码如下:
三.多线程抢单
- 用户每次下单的时候,我们都让他们先进行排队,然后采用多线程的方式创建订单,排队我们可以采用Redis的队列实现,多线程下单我们可以采用Spring的异步实现。
- 多线程下单
将之前下单的代码全部挪到多线程的方法中,com.qingcheng.service.impl.SeckillOrderServiceImpl类的方法值负责调用即可,代码如下:
多线程下单代码如下图: - 排队下单
- 排队信息封装
用户每次下单的时候,我们可以创建一个队列进行排队,然后采用多线程的方式创建订单,排队我们可以采用Redis的队列实现。 排队信息中需要有用户抢单的商品信息,主要包含商品ID,商品抢购时间段,用户登录名。我们可以设计个javabean,如下: - 排队实现
我们可以将秒杀抢单信息存入到Redis中,这里采用List方式存储,List本身是一个队列,用户点击抢购的时候,就将用户抢购信息存入到Redis中,代码如下:
多线程每次从队列中获取数据,分别获取用户名和订单商品编号以及商品秒杀时间段,进行下单操作,代码如下:
- 排队信息封装
- 下单状态查询
按照上面的流程,虽然可以实现用户下单异步操作,但是并不能确定下单是否成功,所以我们需要做一个页面判断,每过1秒钟查询一次下单状态,多线程下单的时候,需要修改抢单状态,支付的时候,清理抢单状态。- 下单更新抢单状态
用户每次点击抢购的时候,如果排队成功,则将用户抢购状态存储到Redis中,多线程抢单的时候,如果抢单成功,
则更新抢单状态。
修改com.qingcheng.service.impl.SeckillOrderServiceImpl的add方法,记录状态,代码如下:
多线程抢单更新状态,修改com.qingcheng.task.MultiThreadingCreateOrder的createOrder方法,代码如下: - 后台查询抢单状态
后台提供抢单状态查询方法,修改com.qingcheng.service.seckill.SeckillOrderService,添加如下查询方法:
修改com.qingcheng.service.impl.SeckillOrderServiceImpl,添加如下实现方法:
修改com.qingcheng.controller.SeckillOrderController,添加如下查询方法: - 前端循环查询
在js中添加一个循环查询方法,每秒钟查询一次,累计查询120秒,代码如下:
抢单后,立即执行上面每秒查询1次的方法,代码如下:
- 下单更新抢单状态
第2章 防止秒杀重复排队
用户每次抢单的时候,一旦排队,我们设置一个自增值,让该值的初始值为1,每次进入抢单的时候,对它进行递增,如果值>1,则表明已经排队,不允许重复排队,如果重复排队,则对外抛出异常,并抛出异常信息100表示已经正在排队。
一.后台排队记录
- 修改com.qingcheng.service.impl.SeckillOrderServiceImpl的add方法,新增递增值判断是否排队中,代码如下:
二.页面识别重复排队
- 修改页面下单js方法add,添加重复排队识别代码,代码如下:
第3章 并发超卖问题解决
一.思路分析
- 解决超卖问题,可以利用Redis队列实现,给每件商品创建一个独立的商品个数队列,例如:A商品有2个,A商品的ID为1001,则可以创建一个队列,key=SeckillGoodsCountList_1001,往该队列中塞2次该商品ID。
- 每次给用户下单的时候,先从队列中取数据,如果能取到数据,则表明有库存,如果取不到,则表明没有库存,这样就可以防止超卖问题产生了。
- 在我们队Redis进行操作的时候,很多时候,都是先将数据查询出来,在内存中修改,然后存入到Redis,在并发场景,会出现数据错乱问题,为了控制数量准确,我们单独将商品数量整一个自增键,自增键是线程安全的,所以不担心并发场景的问题。
二.商品个数队列创建
- 每次将商品压入Redis缓存的时候,另外多创建一个商品的队列。
修改com.qingcheng.timer.SeckillGoodsPushTask,添加一个pushIds方法,用于将指定商品ID放入到指定的数字中,代码如下: - 修改SeckillGoodsPushTask的loadGoodsPushRedis方法,添加队列操作,代码如下:
三.超卖控制
- 修改多线程下单方法,分别修改数量控制,以及售罄后用户抢单排队信息的清理,修改代码如下图:
- 用户抢单的时候,也做一个剩余库存数量判断,修改com.qingcheng.service.impl.SeckillOrderServiceImpl的add方法,代码如下:
- 页面加上对应判断
第4章 订单支付
完成秒杀下订单后,进入支付页面,此时前端会每3秒中向后台发送一次请求用于判断当前用户订单是否完成支付,如果完成了支付,则需要清理掉排队信息,并且需要修改订单状态信息。
一.修改订单状态
- 创建一个SeckillOrderMapper接口,作为Dao层,代码如下:
- 修改com.qingcheng.service.seckill.SeckillOrderService接口,添加如下方法:
- 修改com.qingcheng.service.impl.SeckillOrderServiceImpl添加updatePayStatus方法,代码如下:
二.创建支付二维码
- 下单成功后,会跳转到支付选择页面,在支付选择页面要显示订单编号和订单金额,所以我们需要在下单的时候,将订单金额以及订单编号信息存储到用户查询对象中。
选择微信支付后,会跳转到微信支付页面,微信支付页面会根据用户名查看用户秒杀订单,并根据用户秒杀订单的ID创建预支付信息并获取二维码信息,展示给用户看,此时页面每3秒查询一次支付状态,如果支付成功,需要修改订单状态信息。 - 回显订单号、金额
下单后,进入支付选择页面,需要显示订单号和订单金额,所以需要在用户下单后将该数据传入到pay.html页面,所以查询订单状态的时候,需要将订单号和金额封装到查询的信息中,修改查询订单装的方法加入他们即可。
修改com.qingcheng.controller.SeckillOrderController的queryStatus方法,代码如下: - 用户订单查询
编写一个方法用于根据用户名查询用户订单信息。
修改com.qingcheng.service.seckill.SeckillOrderService接口,添加如下方法
修改com.qingcheng.service.impl.SeckillOrderServiceImpl,添加根据用户名查询订单信息的方法,代码如下: - 创建二维码
在qingcheng_web_seckill工程中添加com.qingcheng.controller.PayController,并创建获取二维码信息的方法,代码如下: - 创建二维码页面对接
将支付工程中的pay.html,weixinpay.html,paysuccess.html,payfail.html拷贝到qingcheng_web_seckill工程根目录。
修改seckill-item.html页面的queryStatus方法,一旦支付成功,跳转到支付选择页面,代码如下:
三.支付状态查询
- 用户支付后,从前端循环查询状态,如果支付成功了,则修改订单状态,并清理用户排队信息。
在PayController.java中,添加后台查询状态的方法,代码如下:
第5章 超时订单库存回滚
用户每次下单后,不一定会立即支付,甚至有可能不支付,那么此时我们需要删除用户下的订单,并回滚库存。这里我们可以采用MQ的延时消息实现,每次用户下单的时候,如果订单创建成功,则立即发送一个延时消息到MQ中,等待消息被消费的时候,先检查对应订单是否下单支付成功,如果支付成功,会在MySQL中生成一个订单,如果MySQL中没有支付,则Redis中还有该订单信息的存在,需要删除该订单信息以及用户排队信息,并恢复库存。
一.RabbitMQ延时队列讲解
- RabbitMQ并没有直接实现延时队列,但是可以利用RabbitMQ两个属性实现延时队列特性:
- x-message-ttl:消息过期时间(Time To Live,TTL),超过过期时间之后即变为死信(Dead-letter),不会再被消费者消费。
设置TTL有两种方式:
(1)创建队列时指定x-message-ttl,此时整个队列具有统一过期时间;
(2)发送消息为每个消息设置expiration,此时消息之间过期时间不同。
注意:如果两者都设置,过期时间取两者最小。 - x-dead-letter-exchange:过期消息路由转发,当消息达到过期时间由该exchange按照配置的x-dead-letterrouting-key转发到指定队列,最后被消费者消费。
- x-message-ttl:消息过期时间(Time To Live,TTL),超过过期时间之后即变为死信(Dead-letter),不会再被消费者消费。
二.关闭微信支付订单
- 取消订单库存回滚的时候,需要注意这么个场景,用户有可能正在扫码支付,所以我们需要先关闭微信支付,然后再取消本地订单回滚库存。
- 关闭微信订单API
微信支付API参考地址:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_3
接口链接:https://api.mch.weixin.qq.com/pay/closeorder
请求参数:
返回结果:
这里我们只关心成功结果,失败结果人工处理。 - 关闭微信支付实现
修改com.qingcheng.service.pay.WeixinPayService,添加关闭支付方法,代码如下:
修改com.qingcheng.service.impl.WeixinPayServiceImpl,添加关闭支付实现,代码如下:
三.下单延时消息发送
- 集成RabbitMQ
修改qingcheng_service_seckill工程,在该工程中添加applicationContext-rabbitmq-producer.xml,用于配置消息发送对象 - 下单延时消息发送
在下单的时候,实现消息发送,这里采用延时消息队列,代码如下:
订单创建完成后记得调用上面发送延时消息的方法,代码如下: - 延时消息消费
创建一个com.qingcheng.consumer.OrderMessageListener类用于消费延时消息,并在该方法中实现数据回滚等操作,代码如下: - 库存回滚
创建一个方法,实现库存回滚,并在消息消费后调用该方法,完整代码如下:
由于消息消费 类中使用到了Dubbo的服务提供对象,所以需要在applicationContext-service.xml中新增一个dubbo的包扫描,代码如下: