初尝秒杀架构

秒杀这个东西虽然快被玩“烂”了,但如果仅仅是浏览网上的文章的话,并不能真正理解那些文章中说到的各种方案。例如都说要消息队列来削峰,那该如何做?就算知道如何做,那真正上手写的时候,情况真的那么简单么?所以,计算机这个玩意,尤其是软件工程,实践是非常非常非常重要的,理论背的再熟也不如上手尝试开发来的实在。

其实很久之前我就想做这个了,但一直没有太好的环境,因为之前做的那个项目有点“大”了,还用了各种组件(ES、Kafka,Redis等),单单启动就能让我电脑的内存占用达到90%,即使做了也没法测试。就在昨天,我重新开了一个小项目,仅仅写了一些简单的业务逻辑,对于“浅尝”秒杀这个场景来说足够了。

下面我会把我在实现的过程中所思考的和遇到的坑分享给大家。

下面的内容都假设大家心中已经了秒杀架构的理论知识,如果对秒杀架构的理论还不是太了解,建议先到网上搜索相关资料学习(这样的资料网上非常多)。

1 准备阶段

在做之前,得必须想明白整体架构,不求完美,只求至少合理,毕竟一个好的架构是迭代出来的而不是一开始就设计出来的。下面是项目的初步架构图:

i3F3B4.png

图画的比较丑(实在不太会画架构图),从图中看出架构比较简单,比最简单的MVC架构仅仅多了Redis和消息队列层而已。我大致描述一下整个流程:

  1. 前端发送HTTP请求
  2. 前端负载均衡器接受请求,将根据某种规则将请求转发到对应的机器上
  3. 服务器收到一个请求,开始着手处理业务。
  4. 首先先到Redis中查看Redis是否有库存的缓存,如果有,就取出来判断库存是否充足,否则就需要到数据库去查询,查询完毕后将其放入缓存中。
  5. 如果缓存中的数据表示库存充足,就发送一条消息到消息队列里,并返回下单成功的消息给前端,如果库存不足,就直接返回下单失败给前端,不再发送消息到消息队列。
  6. 此时消息接受者会收到消息,消息接受者会根据消息来生成订单,并存入数据库,完成本次下单。

2 开始编写业务逻辑

有了基本架构之后,写业务逻辑应该是一件非常简单的事了,为了简单,我仅仅写了三个实体类,User、Order、Product。分别代表用户,订单和商品,而且也仅仅包含了几个必要的字段。然后就是数据访问接口了,每个实体类对应一个接口,我项目中使用的是JPA这个框架,搭建起来非常简单。

还要编写对应的Controller,下面我只贴出OrderController的代码,其他的Controller都非常简单,玩过Spring的朋友应该都能快速解决:

@RestController
@RequestMapping("/orders")
public class OrderController {

    @Autowired
    private IOrderService orderService;

    private static final String CURRENT_USER = "CURRENT_USER";

    @PostMapping
    public ServerResponse createOrder(Long productId, HttpSession session) {
        if (session.getAttribute(CURRENT_USER) == null) {
            return ServerResponse.createByErrorMessage("请先登录");
        }
        User user = (User) session.getAttribute(CURRENT_USER);
        return orderService.createOrder(productId, user.getId());
    }
}

然后就是对应的业务处理orderService了:

@Service
public class OrderService implements IOrderService {

    @Autowired
    private OrderRepository orderRepository;

    @Autowired
    private ProductRepository productRepository;

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    @Transactional
    public ServerResponse createOrder(Long productId, Long userId) {
        //校验库存
        if (!checkStock(productId)) {
            return ServerResponse.createBySuccessMessage("同学,来晚了,东西都被其他人抢走了....");
        }
        //发送异步消息
        sendToQueue(productId, userId);
        
        //不用等待消息处理完毕,就可以直接返回下单成功了。
        return ServerResponse.createBySuccessMessage("下单成功!");
    }
    
    //发送消息的具体逻辑
    private void sendToQueue(Long productId, Long userId) {
        OrderInfo orderInfo = new OrderInfo();
        orderInfo.setProductId(productId);
        orderInfo.setUserId(userId);
        rabbitTemplate.convertAndSend(
                RabbitMQConfig.DEFAULT_DIRECT_EXCHANGE,
                RabbitMQConfig.ORDER_ROUTE_KEY,
                orderInfo);
    }

    //消息接受者
    @RabbitListener(queues = RabbitMQConfig.ORDER_QUEUE)
    private void orderReceiver(OrderInfo orderInfo) {
        addOrder(orderInfo.getProductId(), orderInfo.getUserId());
    }

    //校验库存的业务逻辑
    private boolean checkStock(Long productId) {
        //先尝试去缓存中取库存
        Long stock = (Long) redisTemplate.opsForHash().get("SK_ORDER", productId);
        //如果缓存中不存在该行缓存
        if (stock == null) {
            //就到数据库中取
            Product product = productRepository.findStockById(productId);
            //如果数据库中的库存小于等于0了,就直接返回false,表示库存不足
            if (product == null || product.getStock() <= 0)
                return false;
            //否则,将库存信息存入缓存
            redisTemplate.opsForHash().put("SK_ORDER", productId, product.getStock());
        } else if (stock <= 0){
            //如果存在缓存,就直接判断,如果小于等于0,就表明库存不足,返回false即可
            return false;
        }
        //走到这表示库存充足,返回true即可
        return true;
    }

    @Transactional
    public void addOrder(Long productId, Long userId) {
        //获取Product对象
        Product product = productRepository.findById(productId).orElse(null);
        if (product == null)
            return;
        //生成新的订单
        Order order = new Order();
        order.setUserId(userId);
        order.setStatus(OrderStatus.NO_PAY.getCode());
        order.setOrderNo(UUID.randomUUID().toString());
        //将库存减1
        product.setStock(product.getStock() - 1);
        //写回数据库
        productRepository.saveAndFlush(product);
        //新生成的订单存入数据库
        orderRepository.save(order);
        //还要记得更新缓存的值
        redisTemplate.opsForHash().put("SK_ORDER", productId, product.getStock());
    }
}

这个是核心的处理方法,基本上就是按照上面描述的流程编写的,注释写的也的比较清楚了,直接看注释吧,不再赘述。

因为写的太着急了,没认真好好写,一些变量的命名是有问题的,建议各位如果要自己尝试的话,最好认真一些,这样以后还能看懂自己的代码,哈哈。

3 测试一下

写完了代码之后肯定要测试一下(对自己的代码负责)。我使用的是JMetter这个测试工具,下图是线程组的配置:

i3kVKO.png

在单机上搞那么激进的配置,在使用消息队列之前,我想都不敢想,那时候开个300个线程,就各种连接失败了,错误率高达80%以上。这个配置是我先从小的200开始慢慢增加的,各位最好不要一开始就搞这样(弄不好就死机了),慢慢增加,让压力慢慢上去。

下图是测试的结果:

i3kuad.png

主要看看吞吐量,order这里是220/s,对于单机来说已经不算低了。

4 小结

秒杀这个场景虽然已经被玩“烂”了,但还是非常值得学习的。还是开头的那句话,不要只看理论而不上手实践,上手实践才能加深对理论的理解,而且实践之后的成就感也是不实践所没有的。本文描述的仅仅是总多秒杀架构方案的其中一种,其实还有很多种方案,例如用Redis而不是消息队列来做,或者采用服务熔断,服务降级结合消息队列来做.....,以后有机会再写吧。

你可能感兴趣的:(初尝秒杀架构)