代码链接:
https://github.com/betterGa/ChangGou
文章目录
目标:
- 防止秒杀重复排队【一个人抢购商品,如果没有支付,不允许重复排队抢购】
- 并发超卖【一个商品卖给多个人:一商品多订单】问题解决
- 秒杀订单支付
- 超时支付订单库存回滚
一、防止秒杀重复排队
可以看到,使用 Redis 中的 incr,第一回会让值增为 1,之后每次递增1。
用户每次抢单的时候,一旦排队,我们设置一个自增值,让该值的初始值为 1,每次进入抢单的时候,对它进行递增,如果值 >1,则表明重复排队了,需要抛出异常。
修改 SeckillOrderServiceImpl 的 add 排队方法,新增递增值判断是否排队中,代码如下:
进行测试:
再进行一次抢单:
下单次数变成了 “2”:
再下单,再下单,UserQueueCount 里 username 对应的 value 还是会递增。(有必要递增吗???)
二、 并发超卖问题解决
超卖问题,这里是指多人抢购同一商品的时候,多人同时判断是否有库存,如果只剩一个,则都会判断有库存,此时会导致超卖现象产生,也就是一个商品下了多个订单的现象。
- 思路分析
解决超卖问题,可以利用 Redis 队列实现,给每件商品创建一个独立的商品个数队列,例如:A 商品有 2 个,A 商品的 ID 为 1001,则可以创建一个队列,key=SeckillGoodsCountList_1001,往该队列中塞 2 次该商品 ID。
每次给用户下单的时候,先从队列中取数据,如果能取到数据,则表明有库存,如果取不到,则表明没有库存,将排队信息删除,这样就可以防止超卖问题产生了。
在我们对 Redis 进行操作的时候,很多时候,都是先将数据查询出来,在内存中修改,然后存入到 Redis,在并发场景,会出现数据错乱问题,为了控制数量准确,我们单独将商品数量整一个自增键,自增键是线程安全的,所以不担心并发场景的问题。
- 代码实现
每次将商品压入 Redis 缓存的时候,另外多创建一个商品的队列,库存有多少,这个队列里就有多少个元素。如此实现线程安全。
修改 SeckillGoodsPushTask,添加一个 putAllIds 方法,用于将指定商品 ID 放入到指定的数字中,因为 Redis 设置为 String 类型,所以需要返回 String[ ] 类型的参数:
/***
* 将商品ID存入到数组中
* @param len:长度
* @param id :值
* @return
*/
public String[] putAllIds(int num,Long id){
String[] ids=new String[num];
for(int i=0;i<num;i++){
ids[i]=id.toString();
}
return ids;
}
修改 SeckillGoodsPushTask 的 loadGoodsPushRedis 方法,添加队列操作,代码如下:
/**
* 解决超卖问题
*/
// 将库存个商品id 存入 Redis
redisTemplate.boundListOps("SeckillGoodsCountList_" + seckillgood.getId())
.leftPushAll(putAllIds(seckillgood.getStockCount(), seckillgood.getId()));
运行结果:
可以看到,确实生成了 num 个 id。
//自增计数器
redisTemplate.boundHashOps(“SeckillGoodsCount”).increment(seckillGood.getId(),seckillGood.getStockCount());
生产了自然要消费,修改多线程下单方法,分别修改数量控制,以及售罄后用户抢单排队信息的清理,修改代码如下图:
/***
*当没有库存时,需要清理排队信息
*/
Object goods = redisTemplate.boundListOps("SeckillGoodsCountList_" + seckillStatus.getGoodsId())
.rightPop();
if (goods == null) {
clearUserQueue(username);
return;
}
其中 clearUserQueue 方法:
/**
* 清理用户排队抢单信息
*/
public void clearUserQueue(String username) {
// 排队标识
redisTemplate.boundHashOps("UserQueueCount").delete(username);
// 排队信息清理
redisTemplate.boundHashOps("UserQueueStatus").delete(username);
}
}
(但是现在有个问题:
// 库存递减
seckillGoods.setStockCount(seckillGoods.getStockCount() - 1);
递减的时候可能出现并发问题,导致库存不精准。
而且,当商品库存 stockCount 为 1 时,执行 seckillGoods.getStockCount() - 1
,库存值变为 0,然后就会更新到 MySQL 数据库中, 并且从 namespace 为 SeckillGoods_[time] 的缓存记录中删除,也就是说,该商品不再参与秒杀了 ,这是有问题的,因为调用 createOrder() 方法,会查询缓存中 namespace 为 SeckillGoods_[time] 的记录,这时是查不到的,会提示“已售罄!”,也就是说,剩下一个库存的时候 ,用户永远买不到。修改逻辑为:
三、 订单支付
之前生成订单后,支付微服务会向 MQ 中添加订单信息,然后订单微服务进行监听;现在生成秒杀订单后,需要与之前的普通订单加以区分,让秒杀订单与普通订单服务的队列名称不同,即可。
可以看到,普通订单用的队列名为 queue.order。
在微信支付结果通知的 API 中:
可以看到,“商家数据包” 是原样返回的。
创建二维码时,通过 “附加数据” 指定队列名。在 统一下单 API 中可以看到:
可以把队列名封装在 attach 中:
普通订单:
exchange:"exchange.order",
routingkey:"queue.order",
秒杀订单:
exchange:"exchange.seckillorder"
routingkey:"queue.seckillorder"
将 exchange 和 routingkey 放在 json 中,作为 attach 请求参数。
1、实现根据不同类型订单识别不同操作队列
在 WeiXinPayServiceImpl 中:
// 获取自定义数据
String exchange = parameterMap.get("exchange");
String routingkey = parameterMap.get("routingkey");
Map<String,String> attachMap=new HashMap<>();
// 如果是秒杀订单,需要传 username,后续作为秒杀订单表查询依据。
String username=parameterMap.get("username");
if(!StringUtils.isEmpty(username)){
attachMap.put("username",username);
}
attachMap.put("exchange",exchange);
attachMap.put("routingkey",routingkey);
String attach = JSON.toJSONString(attachMap);
paramMap.put("attach",attach);
对应的,在微信支付结果通知中,需要取出这个 attach:
String attach = resultMap.get("attach");
Map<String,String> attachMap=JSON.parseObject(attach,Map.class);
rabbitTemplate.convertAndSend(attachMap.get("exchange"),
attachMap.get("routingkey"),
JSON.toJSONString(resultMap));
在支付工程的 application.yml 中,添加 mq 的配置:
在 pay 工程用 @Configuration 修饰的 MQConfig 中创建秒杀队列:
/****
* 秒杀队列创建
* @return
*/
// 创建队列
@Bean
public Queue orderSeckillQueue() {
return new Queue(environment.getProperty("mq.pay.queue.seckillorder"));
}
// 创建交换机
@Bean
public Exchange orderSeckillExchange() {
// 持久化,不自动删除
return new DirectExchange(environment.getProperty("mq.pay.exchange.seckillorder"), true, false);
}
// 绑定
@Bean
public Binding seckillBinding(Queue orderSeckillQueue, Exchange orderSeckillExchange) {
return BindingBuilder.bind(orderSeckillQueue).to(orderSeckillExchange)
.with(environment.getProperty("mq.pay.routing.seckillkey"))
.noargs();
}
(一般都是在 MQ 中创建队列的,这里方便起见,在程序中创建。这样,一启动 pay 工程,当调用到 pay 工程里的方法时,就会运行这个配置类,创建秒杀队列。)
(而且注意到,使用 @Bean 注册 bean 时,方法参数名必须和 bean 的方法名保持一致,比如除了秒杀队列以外,之前还有普通队列的创建,普通队列创建的 public Queue orderQueue() 方法,和 秒杀队列的 public Queue orderSeckillQueue() 方法,返回类型都是 Queue,那么,seckillBinding 方法里传的参数,如果参数名写成 queue,它会先按照类型进行装配,普通队列和秒杀队列方法的返回类型都是 Queue,是要报错的。)
进行测试,启动 eureka、pay,需要自定义 exchange 和 queue 名:
生成支付二维码后,扫描并付款,看 RabbitMQ 中的队列:
这时启动 seckill 工程,会对秒杀队列进行监听:
运行结果:
至此,秒杀入队和监听队列的逻辑都没有问题。
2、支付回调逻辑更新
如果回调订单支付结果,支付成功的话,需要修改订单状态、清除用户排队信息;如果失败的话,需要修改订单状态、回滚库存。
先在 SeckillOrderService 接口里提供修改订单状态、清除用户排队的方法:
void updatePayStatus(String transactionId, String username, String endtime);
实现:
@Override
public void updatePayStatus(String transactionId, String username, String endtime){
/** 修改订单状态信息 */
// 获取订单
SeckillOrder seckillOrder = (SeckillOrder) redisTemplate.boundHashOps("SeckillOrder").get(username);
if (seckillOrder != null) {
// 设置状态为已支付
seckillOrder.setStatus("1");
seckillOrder.setTransactionId(transactionId);
// 支付完成时间,格式为yyyyMMddHHmmss
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
try {
Date payTime = simpleDateFormat.parse(endtime);
seckillOrder.setPayTime(payTime);
// 更新到数据库
seckillOrderMapper.insert(seckillOrder);
// 删除 Redis 中的订单记录
redisTemplate.boundHashOps("SeckillOrder").delete(username);
// 删除 Redis 排队信息
redisTemplate.boundHashOps("UserQueueCount").delete(username);
redisTemplate.boundHashOps("UserQueueStatus").delete(username);
} catch (ParseException e) {
e.printStackTrace();
}
}
}
在 SeckillOrderService 接口里提供 修改订单状态、回滚库存 的方法:
void deleteOrder(String username);
实现:
/**
* 删除订单
*
* @param username
*/
@Override
public void deleteOrder(String username) {
// 删除订单
redisTemplate.boundHashOps("SeckillOrder").delete(username);
// 查询用户排队信息
SeckillStatus userQueueStatus = (SeckillStatus) redisTemplate.boundHashOps("UserQueueStatus").get(username);
// 删除排队信息
redisTemplate.boundHashOps("UserQueueCount").delete(username);
redisTemplate.boundHashOps("UserQueueStatus").delete(username);
// 回滚库存
// Redis 递增
String namespace = "SeckillGoods_" + userQueueStatus.getTime();
SeckillGoods seckillGoods = (SeckillGoods) redisTemplate.boundHashOps(namespace)
.get(userQueueStatus.getGoodsId());
// 如果商品为空,到数据库查询
if (seckillGoods == null) {
seckillGoods = seckillGoodsMapper.selectByPrimaryKey(userQueueStatus.getGoodsId());
// 更新数据库内存
seckillGoods.setStockCount(seckillGoods.getStockCount() + 1);
seckillGoodsMapper.updateByPrimaryKey(seckillGoods);
} else {
seckillGoods.setStockCount(seckillGoods.getStockCount() + 1);
redisTemplate.boundHashOps(namespace).put(seckillGoods.getId(), seckillGoods);
redisTemplate.boundListOps("SeckillGoodsCountList" + seckillGoods.getId())
.leftPush(seckillGoods.getId());
}
}
数据库中表的属性主要是:
有了这两个逻辑,接下来,完善监听队列的逻辑:
@RabbitHandler
public void getMessage(String message) throws Exception {
/* System.out.println("Message:"+message);*/
// 将支付信息转成 Map
Map<String, String> resultMap = JSON.parseObject(message, Map.class);
String return_code = resultMap.get("return_code");
// 获取订单号
String outtradeno = resultMap.get("out_trade_no");
// 获取事务id
String transactionid = resultMap.get("transaction_id");
// 获取支付完成时间
String timeEnd = resultMap.get("time_end");
// 获取自定义数据
String attach = resultMap.get("attach");
Map<String, String> attachMap = JSON.parseObject(attach, Map.class);
String username = attachMap.get("username");
if ("SUCCESS".equals(return_code)) {
String result_code = resultMap.get("result_code");
if ("SUCCESS".equals(result_code)) {
// 秒杀订单支付成功后,需要修改订单状态,清理用户排队信息
seckillOrderService.updatePayStatu(transactionid, username, timeEnd);
} else {
// 支付失败,需要删除订单,回滚库存
seckillOrderService.deleteOrder(username);
}
}
}
}
测试:
此时有一个秒杀订单。
下单:
查询订单信息:
把 orderId 作为 outtradeno,可以知道将支付信息发送到哪个队列生成支付二维码:
其中,username 是用户名,可以根据用户名查询用户排队信息;outtradeno 是商户订单号,对于同一个商家而言,订单号是唯一的,也是下单必需的。
付款后,可以看到 MySQL 数据库中生成了订单记录:
而且队列信息被清除了,只剩下 时间段内秒杀商品队列,和防止超卖问题生成的商品数的商品队列:
可以看到,监听到支付信息后,根据支付信息判断,用户支付成功了,则修改订单信息,并将订单入库,删除用户排队信息。
四、Rabbit MQ 延时消息队列
1、延时队列介绍
延时队列 ,即 放置在该队列里面的消息是不需要立即消费的,而是等待一段时间之后取出消费。
那么,为什么需要延迟消费呢?我们来看以下的场景:
网上商城下订单后, 30 分钟后没有完成支付,取消订单(如:淘宝、去哪儿网) ;
系统创建了预约之后,需要在预约时间到达前一小时,提醒被预约的双方参会;
系统中的业务失败之后,需要重试。
这些场景都非常常见,我们可以思考,比如第二个需求,系统创建了预约之后,需要在预约时间到达前一小时提醒被预约的双方参会。那么一天之中肯定是会有很多个预约的,时间也是不一定的,假设现在有 1点、2点、3点 三个预约,如何让系统知道在当前时间等于 0点、1点、2点 给用户发送信息呢,是不是需要一个轮询,一直去查看所有的预约,比对当前的系统时间和预约提前一小时的时间是否相等呢?这样做非常浪费资源,而且轮询的时间间隔不好控制。如果我们使用延时消息队列呢,我们在创建时把需要通知的预约放入消息中间件中,并且设置该消息的过期时间,等过期时间到达时再取出消费即可。
- RabbitMQ 实现延时队列一般而言有两种形式:
-
- 第一种方式:利用两个特性: Time To Live(TTL)、Dead Letter Exchanges(DLX)[A 队列过期- --> 转发给 B 队列]
-
- 第二种方式:利用 RabbitMQ 中的插件 x-delay-message
2、 TTL、DLX 实现延时队列
-
TTL:Rabbit MQ 可以针对队列设置 x-expires(则队列中所有的消息都有相同的过期时间) 或者针对 Message 设置 x-message-ttl (对消息进行单独设置,每条消息 TTL 可以不同),来控制消息的生存时间,如果超时(两者同时设置以最先到期的时间为准),则消息变为 dead letter(死信)。
-
Dead Letter Exchanges(DLX):Rabbit MQ 的 Queue 可以配置 x-dead-letter-exchange 和 x-dead-letter-routing-key(可选)两个参数,如果队列内出现了 dead letter,则按照这两个参数 重新路由 转发到指定的队列。
-
- x-dead-letter-exchange:出现 dead letter 之后将 dead letter 重新发送到指定 exchange。
-
- x-dead-letter-routing-key:出现 dead letter 之后将 dead letter 重新按照指定的 routing-key 发送
- DLX 延时队列实现
(1)创建工程
创建 springboot_rabbitmq_delay 工程,并引入相关依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>changgou-parent</artifactId>
<groupId>com.changgou</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>springboot_rabbitmq_delay</artifactId>
<dependencies>
<!--starter-web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--加入ampq-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!--测试-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
</dependencies>
</project>
application.yml 配置:
spring:
application:
name: springboot-demo
rabbitmq:
host: 127.0.0.1
port: 5672
password: guest
username: guest
(2) 队列创建
创建 2 个队列,用于接收消息的叫延时队列 queue.message.delay,用于转发消息的队列叫 queue.message ,同时创建一个交换机,代码如下:
@Configuration
public class QueueConfig {
// 信息发送队列
public static final String QUEUE_MESSAGE = "queue.message";
// 交换机
public static final String DLX_EXCHANGE = "dix.exchange";
// 延迟队列
public static final String QUEUE_MESSAGE_DELAY = "queue.message.delay";
@Bean
public Queue messageQueue() {
return new Queue(QUEUE_MESSAGE, true);
}
@Bean
public Queue delayMessage() {
return QueueBuilder.durable(QUEUE_MESSAGE_DELAY)
// 消息超时进入死信队列,绑定死信交换机
.withArgument("x-dead-letter-exchange",
DLX_EXCHANGE)
// 绑定交换机
.withArgument("x-dead-letter-routing-key",
QUEUE_MESSAGE)
.build();
}
@Bean
public DirectExchange directExchange() {
return new DirectExchange(DLX_EXCHANGE);
}
// 交换机与队列绑定
@Bean
public Binding basicBinding(Queue messageQueue, DirectExchange directExchange) {
return BindingBuilder.bind(messageQueue)
.to(directExchange)
.with(QUEUE_MESSAGE);
}
}
(3) 消息监听
创建 MessageListener 用于监听消息:
@Component
@RabbitListener(queues = QueueConfig.QUEUE_MESSAGE)
public class MessageListener {
/**
* 监听队列
* @param msg
*/
@RabbitHandler
public void msg(@Payload Object msg){
SimpleDateFormat dateFormat=new SimpleDateFormat("yyyy-MM-dd-HH:mm:ss");
System.out.println("当前时间"+dateFormat.format(new Date()));
System.out.println("收到信息:"+msg);
}
}
(4)创建启动类
@SpringBootApplication
@EnableRabbit
public class SpringRabbitMQApplication {
public static void main(String[] args) {
SpringApplication.run(SpringRabbitMQApplication.class,args);
}
}
(5)测试
@SpringBootTest
@RunWith(SpringRunner.class)
public class RabbitMQTest {
@Autowired
private RabbitTemplate rabbitTemplate;
/***
* 发送消息
*/
@Test
public void sendMessage() throws InterruptedException, IOException {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("发送当前时间:"+dateFormat.format(new Date()));
Map<String,String> message = new HashMap<>();
message.put("name","szitheima");
rabbitTemplate.convertAndSend(QueueConfig.QUEUE_MESSAGE_DELAY, message, new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
message.getMessageProperties().setExpiration("10000");
return message;
}
});
System.in.read();
}
}
其中 message.getMessageProperties().setExpiration("10000")
,设置消息超时时间,超时后,会将消息转入到另外一个队列。
运行结果:
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210501164938727.png?
五、延时队列实现订单关闭回滚库存
秒杀流程回顾:
如上图,步骤分析如下:
(1)用户抢单,经过秒杀系统实现抢单,下单后会将向 MQ 发送一个延时队列消息,包含抢单信息,延时半小时后才能监听到
(2)秒杀系统同时启用延时消息监听,一旦监听到订单抢单信息,判断 Redis 缓存中是否存在订单信息,如果存在,则回滚
(3)秒杀系统还启动支付回调信息监听,如果支付完成,则将订单同步到 MySQL,如果没完成,清理排队信息,回滚库存
(4)每次秒杀下单后调用支付系统,创建二维码,如果用户支付成功了,微信系统会将支付信息发送给支付系统指定的回调地址,支付系统收到信息后,将信息发送给 MQ ,第 3 个步骤就可以监听到消息了。
延时队列实现订单关闭回滚库存,需要创建一个过期队列 Queue1、接收消息的队列 Queue2、中转交换机。
在 seckill 工程的 mq 包中新建 QueueConfi 配置类:
@Configuration
public class QueueConfig {
// queue1
@Bean
public Queue delaySeckillQueue() {
return QueueBuilder.durable("delaySeckillQueue")
// 当前队列消息一旦过期,进入到死信队列交换机
.withArgument("x-dead-letter-exchange",
"seckillExchange")
// 将死信队列的消息路由到指定队列
.withArgument("x-dead-letter-routing-key",
"seckillQueue")
.build();
}
// queue2
@Bean
public Queue seckillQueue() {
return new Queue("seckillQueue");
}
// 交换机
@Bean
public Exchange seckillExchange() {
return new DirectExchange("seckillExchange");
}
// 队列绑定交换机
public Binding seckillQueueBindingExchange(Queue seckillQueue,Exchange seckillExchange){
return BindingBuilder.bind(seckillQueue)
.to(seckillExchange)
.with("seckillQueue")
.noargs();
}
}
在多线程下单的 createOrder 方法中添加发送信息的逻辑:
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd HH:mm:ss");
System.out.println("下单信息发送时间:" + simpleDateFormat.format(new Date()));
// 发送消息给 queue1
rabbitTemplate.convertAndSend("delaySeckillQueue",
(Object) JSON.toJSONString(seckillStatus),
new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
message.getMessageProperties().setExpiration("10000");
return message;
}
});
提供 DelaySeckillMessageListener 监听类:
/**
* 延时订单监听
* 监听 queue2
*/
@Component
@RabbitListener(queues = "seckillQueue")
public class DelaySeckillMessageListener {
@Autowired
WeiXinPayFeign weiXinPayFeign;
@Autowired
SeckillOrderService seckillOrderService;
@Autowired
private RedisTemplate redisTemplate;
@RabbitHandler
public void getMessage(String message){
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd HH:mm:ss");
System.out.println("回滚时间:" + simpleDateFormat.format(new Date()));
// 获取队列中信息
SeckillStatus seckillStatus = JSON.parseObject(message, SeckillStatus.class);
Object userQueueStatus = redisTemplate.boundHashOps("UserQueueStatus").get(seckillStatus.getUsername());
// 如果没有排队信息,说明订单已经处理
// 否则说明尚未完成支付
if (userQueueStatus!=null) {
// 关闭微信支付订单
Result result=weiXinPayFeign.cancelOrder(seckillStatus.getOrderId().toString());
Map<String,String> resultMap = (Map<String, String>) result.getData();
if(resultMap!=null&&"SUCCESS".equals(resultMap.get("return_code"))
&&"SUCCESS".equals(resultMap.get("result_code"))){
// 删除订单
seckillOrderService.deleteOrder(seckillStatus.getUsername());
}
}
}
}
删除订单的 deleteOrder 方法中,包括 删除排队信息、回滚信息逻辑。
监听 Queue2 的逻辑:
(1)通过 SeckillStatus 的 username 检查 Redis 中是否有订单信息
(2)如果有订单信息,需要先关闭微信支付订单,防止中途用户支付。再调用删除订单方法,删除秒杀订单在 MQ 中的排队信息,并回滚库存
(3)如果关闭订单时,用户已支付,修改订单状态即可
(4)如果关闭订单时,发生了别的错误,记录日志,人工处理
六、总结
(1)利用 Redis 的单线程性质,解决并发中可能出现的问题。
(2)防止秒杀重复排队:
用户每次抢单的时候,在 Redis 中排队,namespace 为 userQueuecount,给 username 对应的 value 递增1,如果 value>1,说明重复排队了。
(3)超卖是指 一个商品下了多个订单的现象。使用 Redis 的 List 类型,例如:A 商品有 2 个,A 商品的 ID 为 1001,则可以创建一个队列,key=SeckillGoodsCountList_1001,往该队列中塞 2 次该商品 ID。每次给用户下单的时候,先从队列中取数据,如果能取到数据,则表明有库存;如果取不到,则表明没有库存,将 UserQueueCount、UserQueueStatus 排队信息删除。
(4)普通订单和秒杀订单是要区分开来的。
普通订单的交换机名为 exchange.order;秒杀订单的交换机名为 exchange.seckillorder。
普通订单用的队列名为 queue.order;秒杀订单的队列名为 queue.seckillorder。
在微信支付统一下单(生成支付二维码)API 的参数中,有个"attach",是附加数据,可以用作自定义参数。
在微信支付结果通知 API 的响应参数中,也有个 “attach”,会把商家数据原样返回。
通过这个 attach,在生成支付二维码时,指定 exchange、routingkey 和 username ,然后在微信支付结果通知中取出 attach。这样就能在微信支付结果通知时,向 exchange、routingkey 对应的队列发送支付结果。
SeckillMessageListener 类监听秒杀订单对应的队列。
如果结果是支付成功的话,调用 updatePayStatu 方法,根据 key 为 username ,获取到 namespace 为 SeckillOrder 的记录里,对应的 value,即订单对象 SeckillOrder,然后修改订单状态为"1" 已支付、设置 transactionid 、支付完成时间(支付结果里的 完成时间"end_time"),并把订单对象同步到数据库,然后清除用户排队信息 UserQueueCount、UserQueueStatus;
如果支付失败的话,调用 deleteOrder 方法,需要把 SeckillOrder 里 username 对应的记录删除,然后查询 UserQueueStatus 里 username 对应的记录,从而获得订单id、商品id,到 Redis 中查询 SeckillGoods_[time] 中的商品 id 对应的秒杀商品对象 seckillGoods,如果查询结果为空,说明此时商品不参与秒杀活动了,需要到数据库中进行查询,然后把商品库存数+1。如果查询到了秒杀商品,需要把 Redis 中SeckillGoods_[time]中商品的库存数 +1,并且对应的 SeckillGoodsCountList_[goodsid] 也要加一个记录,表示秒杀商品数量回滚。
(5)在多线程抢单的 createOrder 方法里,向 delaySeckillQueue 发送消息,消息内容为 seckillStatus 对象,过期时间为 10秒,10 秒后,这个队列会过期,把消息转发给 seckillQueue,使用 DelaySeckillMessageListener 方法监听 seckillQueue,查询 namespace 为 UserQueueStatus 中,username 对应的记录,如果为空,说明没有排队信息,订单已被处理;否则说明订单尚未完成支付,需要先调用微信支付 closeOrder 的 API,关闭微信支付订单,如果关闭成功的话,再删除订单,调用上面的 deleteOrder 方法。