秒杀中的常见问题的解决

1)解决超卖的问题

1)Redis预减库存,有一个下单请求过来时预减库存,若减完后的redis库存小于0说明已经卖完,此时直接返回客户端已经卖完。后续使用内存标记,减少Redis访问。若预减库存成功,则异步下单,请求入队列,返回客户端排队中。

2)数据库层面防止超卖:Redis预减库存只是抢到了这个机会,真正是否购买成功还是要等到所有数据库操作的真正成功,即消息队列的消费端是否消费成功。

数据库层面,秒杀的订单表设置唯一索引,防止重复下单。

数据库层面,减库存的时候同时判断此时库存是否大于0。

由于是订单模块和库存模块,涉及到分布式事务的问题,使用seata框架。

2)如何保证Redis的库存与数据库库存的一致性

在我们的项目中,Redis的库存并不是真正的库存,而是用于阻挡多余的下单请求,用于保证有多少秒杀商品库存就放多少个请求到消息队列,大大减少数据库访问。

真正的下单和减库存操作还是操作数据库的。

所以我们不需要保证Redis缓存与数据库的一致性。

3)如何保证MQ不丢失消息

消息丢失的三种情况:

  • **生产者丢失消息:**生产者发送消息到MQ时因为网络问题丢失消息。
  • **MQ丢失消息:**没来得及持久化,就挂掉后消息丢失
  • **消费端丢失消息:**刚从MQ获取消息,没处理完消费者就挂掉了

1 生产者丢失消息的解决方案:

1) rabbitmq提供事务支持,在生产者发送之前开启事务,然后发送消息,如果消息没有成功被rabbitmq接收到,那么生产者会受到异常报错,这时就可以回滚事物,然后尝试重新发送;如果收到了消息,那么就可以提交事物。但是这种方案会阻塞生产者,吞吐量下降。

2)可以将channel开启confirm机制。在生产者哪里设置开启了confirm模式之后,每次写的消息都会分配一个唯一的id,然后写入了rabbitmq之中;**如果rabbitmq没能处理这个消息,会回调你一个nack接口,告诉你这个消息失败了,你可以进行重试。**如果成功发送到mq,也会回调一个ack的接口方法,告诉你成功发送消息。

这里使用方案二!这样吞吐量更高。

2 MQ自己丢失数据的解决方案

设置持久化!持久化有两个步骤:

1)创建queue时设置持久化,但是这时候持久化的是queue的元数据,不会持久化queue里面的数据。

2)发送消息的时候将消息的deliveryMode设置为2,表示将消息持久化。

而且可以将持久化与生产者的confirm机制结合只有持久化成功后才回调ack方法。超时未持久化成功或持久化失败也会回调nack。

3 消费者丢失数据的解决方案:

消费者端丢失数据都是因为开启了rabbitmq的autoACK功能,即消费者获取了数据之后就自动告诉MQ已经消费。

**解决方案:**关闭rabbitmq的autoAck,在确保消息被消费成功之后才发送ACK。消息没有成功消费的话rabbitmq会重发消息,这样能保证消息不会再消费者端丢失。

redis预减库存超卖了 秒杀redis预减库存问题_秒杀系统设计

4)MQ如何保证不重复消费

这里不保证不重复消费,因为保证了消息不丢失就有可能读取重复的消息。这里保证接口的幂等性即可。

在保证幂等性的基础上,因为写入MQ中的数据都有一个唯一编号,当MQ消费成功后立即往redis中写入该编号。在消费端,读取MQ数据后先判断是否已经消费过。

5)如何保证分布式事务

在项目中,MQ消费端需要保证下订单和减库存同时成功或失败,这就涉及到事务的问题。

由于是分布式架构,每一个模块对应自己的数据库,跨库之间的事务就需要分布式事务解决方案。

当然,还有一种简单的方案,秒杀模块单独建立自己的订单表、自己的秒杀商品库存表,即在秒杀下单业务中没有调用其他模块的接口,此时也就简单了,没有分布式事务的存在,采用本地事务解决问题。

在这里,暂且考虑分布式事务,学技术为主。尝试使用SpringCloudAlibaba提供的Seata组件去完成分布式事务,只需要加上@GlobalTransaction注解

通过TC、TM、RM三个组件完成:全局事务管理者、事务发起方、事务的参与方。

Seata事务的执行流程(默认是使用二阶段提交):

  • TM开启分布式事务(TM向TC注册全局事务记录)
  • 按业务场景,编排数据库、服务等事务内资源(RM向TC汇报资源准备状态)
  • TM结束分布式事务,事务一阶段结束(TM通知TC提交/回滚事务)
  • TC汇总事务信息,决定事务是提交还是回滚
  • TC通知所有的RM提交或回滚资源,事务的二阶段结束。

在MQ消费端:

@RabbitListener(queues = MQConfig.SECKILL_QUEUE)
    public void receiveSkInfo(Channel channel, MSMessage message, @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws IOException {
        String courseId = message.getCourseId();
        String id = message.getId();
        String memberId = message.getMemberId();

        //这里不需要判断库存,而是执行SQL的时候检查是否超卖
        // 获取商品的库存
//        QueryWrapper<MsCourse> wrapper = new QueryWrapper<>();
//        wrapper.eq("course_id", courseId);
//
//        MsCourse msCourse = msCourseService.getOne(wrapper);
//        Integer stockCount = msCourse.getCount();
//        if (stockCount <= 0) {
//            //此时已经卖完了
//            channel.basicAck(tag, false);  //手动确认
//            return;
//        }

        // 判断是否已经秒杀到了(保证幂等性)
        QueryWrapper<MsOrder> wrapper = new QueryWrapper<>();
        wrapper.eq("course_id", courseId);
        wrapper.eq("member_id", memberId);
        MsOrder one = msOrderService.getOne(wrapper);
        if (one != null) {
            channel.basicAck(tag, false);  //手动确认
            return;
        }

        // 1.减库存 2.写入订单 3.写入秒杀订单
        try {
            boolean flag = msOrderService.createOrders(id, courseId, memberId);
            channel.basicAck(tag, false);  //手动确认
        }catch (Exception e){
            e.printStackTrace();
            channel.basicNack(tag, false, false); //
        }
    }

减库存、写订单的全局事务处理

@Override
@GlobalTransaction(rollbackFor=Exception.class)  //分布式事务
public boolean createOrders(String orderId, String courseId, String memberId) throws Exception {
    //减库存
    int res = courseService.reduceCount(courseId);//这条SQL语句就会检查超卖的情况
    if(res <= 0) throw new RuntimeException("减库存失败");
    //写订单
    MsOrder msOrder = new MsOrder();
    msOrder.setId(orderId);
    msOrder.setCourseId(courseId);
    msOrder.setMemberId(memberId);
    flag = orderService.save(msOrder);
    if(!flag) throw new RuntimeException("写入订单失败");
    return true;
}
-- 在减库存时同时查询库存数量是否大于0,返回结果如果为0,说明更新失败,库存此时不是大于0
update ms_course set count = count -1 where course_id=#{courseId} and count>0;

请求下订单时的逻辑

//用于标记商品是否卖完。HashMap虽然不是线程安全,但是不影响,因为只会写入true。
    private volatile Map<String, Boolean> localMap = new HashMap<String, Boolean>();

    @PostMapping("createOrder/{courseId}")
    @ApiOperation("下订单")
    public R createOrder(@PathVariable("courseId") String courseId, HttpServletRequest request) {
        // 1 判断用户是否登录
//        String memberId = JwtUtils.getMemberIdByJwtToken(request);
            // 随机生成用户Id,用于压测
         String memberId = UUID.randomUUID().toString().substring(0,18);
        if(StringUtils.isEmpty(memberId)){
            return R.ok().code(20001).message("请先登录");
        }

        //2 生成订单
        // 2.1基于内存判断商品是否已经秒杀完毕,减少redis访问
        Boolean flag = localMap.get(courseId);
        if (flag != null && flag) {
            return R.error().message("已卖完");
        }
        // 2.2 判断是否重复秒杀(单机redis不用分布式锁,而且后面会使用redis的setnx往redis中写入订单信息,不会导致重复下单)
        String orderKey = "order::"+memberId+"::"+courseId;
        Boolean hasOrder = redisTemplate.hasKey(orderKey);
        if(hasOrder){
            return R.error().message("重复秒杀");
        }

        // 2.3 在redis中预减库存
        //预减库存
        String stock = "msCourse::" + courseId + "::count";
        Long count = redisTemplate.opsForValue().decrement(stock);
        if (count >= 0) {
            if(count == 0) localMap.put(courseId, true);
            //生成订单信息,加入MQ去持久化
            // 使用redis的setnx保证不会重复下单,虽然2.2中判断了是否重复下单,但这里在多线程下任然可能重复
            if(redisTemplate.opsForValue().setIfAbsent(orderKey, "", 60, TimeUnit.SECONDS)){
                //生成订单。
                String orderId = UUID.randomUUID().toString().replace("-", "").substring(0, 18);
                MSMessage msMessage = new MSMessage(orderId, courseId, memberId);
                mqProvider.sendMessage(msMessage);

                return R.ok().message("校验订单,请稍后");
            }else {
                redisTemplate.opsForValue().increment("msCourse::" + courseId + "::count");
                return R.error().message("重复秒杀");
            }
        }else {
            //小于0说明库存为空还去减,此时要将库存加回去
            redisTemplate.opsForValue().increment("msCourse::" + courseId + "::count");
            return R.error().message("已卖完");
        }
    }
}