美文网首页
初尝秒杀架构

初尝秒杀架构

作者: yeonon | 来源:发表于2018-12-09 17:00 被阅读25次

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

    其实很久之前我就想做这个了,但一直没有太好的环境,因为之前做的那个项目有点“大”了,还用了各种组件(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<Order> 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<String, Long> redisTemplate;
    
        @Override
        @Transactional
        public ServerResponse<Order> 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而不是消息队列来做,或者采用服务熔断,服务降级结合消息队列来做.....,以后有机会再写吧。

    相关文章

      网友评论

          本文标题:初尝秒杀架构

          本文链接:https://www.haomeiwen.com/subject/jjlkhqtx.html