微服务商城系统(十六)秒杀核心

代码链接:
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 发送

1557396863944

  • 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?在这里插入图片描述

五、延时队列实现订单关闭回滚库存

秒杀流程回顾:
1558832314454
如上图,步骤分析如下:
(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 方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值