初尝秒杀架构

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

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

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

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

1 准备阶段

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

图画的比较丑(实在不太会画架构图),从图中看出架构比较简单,比最简单的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这个测试工具,下图是线程组的配置:

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

下图是测试的结果:

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

4 小结

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容