初嘗秒殺架構

秒殺這個東西雖然快被玩“爛”了翘县,但如果僅僅是瀏覽網(wǎng)上的文章的話最域,并不能真正理解那些文章中說到的各種方案。例如都說要消息隊列來削峰炼蹦,那該如何做羡宙?就算知道如何做,那真正上手寫的時候掐隐,情況真的那么簡單么狗热?所以钞馁,計算機這個玩意,尤其是軟件工程匿刮,實踐是非常非常非常重要的僧凰,理論背的再熟也不如上手嘗試開發(fā)來的實在。

其實很久之前我就想做這個了熟丸,但一直沒有太好的環(huán)境训措,因為之前做的那個項目有點“大”了,還用了各種組件(ES光羞、Kafka绩鸣,Redis等),單單啟動就能讓我電腦的內(nèi)存占用達到90%纱兑,即使做了也沒法測試因妇。就在昨天卫键,我重新開了一個小項目陶舞,僅僅寫了一些簡單的業(yè)務邏輯司抱,對于“淺嘗”秒殺這個場景來說足夠了。

下面我會把我在實現(xiàn)的過程中所思考的和遇到的坑分享給大家铐炫。

下面的內(nèi)容都假設大家心中已經(jīng)了秒殺架構的理論知識垒手,如果對秒殺架構的理論還不是太了解,建議先到網(wǎng)上搜索相關資料學習(這樣的資料網(wǎng)上非常多)倒信。

1 準備階段

在做之前科贬,得必須想明白整體架構,不求完美堤结,只求至少合理唆迁,畢竟一個好的架構是迭代出來的而不是一開始就設計出來的鸭丛。下面是項目的初步架構圖:

圖畫的比較丑(實在不太會畫架構圖)竞穷,從圖中看出架構比較簡單,比最簡單的MVC架構僅僅多了Redis和消息隊列層而已鳞溉。我大致描述一下整個流程:

  1. 前端發(fā)送HTTP請求
  2. 前端負載均衡器接受請求瘾带,將根據(jù)某種規(guī)則將請求轉(zhuǎn)發(fā)到對應的機器上
  3. 服務器收到一個請求,開始著手處理業(yè)務熟菲。
  4. 首先先到Redis中查看Redis是否有庫存的緩存看政,如果有,就取出來判斷庫存是否充足抄罕,否則就需要到數(shù)據(jù)庫去查詢允蚣,查詢完畢后將其放入緩存中。
  5. 如果緩存中的數(shù)據(jù)表示庫存充足呆贿,就發(fā)送一條消息到消息隊列里嚷兔,并返回下單成功的消息給前端森渐,如果庫存不足,就直接返回下單失敗給前端冒晰,不再發(fā)送消息到消息隊列同衣。
  6. 此時消息接受者會收到消息,消息接受者會根據(jù)消息來生成訂單壶运,并存入數(shù)據(jù)庫耐齐,完成本次下單。

2 開始編寫業(yè)務邏輯

有了基本架構之后蒋情,寫業(yè)務邏輯應該是一件非常簡單的事了埠况,為了簡單,我僅僅寫了三個實體類棵癣,User询枚、Order、Product浙巫。分別代表用戶金蜀,訂單和商品,而且也僅僅包含了幾個必要的字段的畴。然后就是數(shù)據(jù)訪問接口了渊抄,每個實體類對應一個接口,我項目中使用的是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());
    }
}

然后就是對應的業(yè)務處理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("同學,來晚了缓呛,東西都被其他人搶走了....");
        }
        //發(fā)送異步消息
        sendToQueue(productId, userId);
        
        //不用等待消息處理完畢催享,就可以直接返回下單成功了。
        return ServerResponse.createBySuccessMessage("下單成功哟绊!");
    }
    
    //發(fā)送消息的具體邏輯
    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());
    }

    //校驗庫存的業(yè)務邏輯
    private boolean checkStock(Long productId) {
        //先嘗試去緩存中取庫存
        Long stock = (Long) redisTemplate.opsForHash().get("SK_ORDER", productId);
        //如果緩存中不存在該行緩存
        if (stock == null) {
            //就到數(shù)據(jù)庫中取
            Product product = productRepository.findStockById(productId);
            //如果數(shù)據(jù)庫中的庫存小于等于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);
        //寫回數(shù)據(jù)庫
        productRepository.saveAndFlush(product);
        //新生成的訂單存入數(shù)據(jù)庫
        orderRepository.save(order);
        //還要記得更新緩存的值
        redisTemplate.opsForHash().put("SK_ORDER", productId, product.getStock());
    }
}

這個是核心的處理方法裆操,基本上就是按照上面描述的流程編寫的怒详,注釋寫的也的比較清楚了鳄乏,直接看注釋吧,不再贅述棘利。

因為寫的太著急了橱野,沒認真好好寫,一些變量的命名是有問題的善玫,建議各位如果要自己嘗試的話水援,最好認真一些,這樣以后還能看懂自己的代碼茅郎,哈哈蜗元。

3 測試一下

寫完了代碼之后肯定要測試一下(對自己的代碼負責)。我使用的是JMetter這個測試工具系冗,下圖是線程組的配置:

在單機上搞那么激進的配置奕扣,在使用消息隊列之前,我想都不敢想掌敬,那時候開個300個線程惯豆,就各種連接失敗了,錯誤率高達80%以上奔害。這個配置是我先從小的200開始慢慢增加的楷兽,各位最好不要一開始就搞這樣(弄不好就死機了),慢慢增加华临,讓壓力慢慢上去芯杀。

下圖是測試的結(jié)果:

主要看看吞吐量,order這里是220/s雅潭,對于單機來說已經(jīng)不算低了揭厚。

4 小結(jié)

秒殺這個場景雖然已經(jīng)被玩“爛”了,但還是非常值得學習的扶供。還是開頭的那句話筛圆,不要只看理論而不上手實踐,上手實踐才能加深對理論的理解诚欠,而且實踐之后的成就感也是不實踐所沒有的顽染。本文描述的僅僅是總多秒殺架構方案的其中一種,其實還有很多種方案轰绵,例如用Redis而不是消息隊列來做,或者采用服務熔斷尼荆,服務降級結(jié)合消息隊列來做.....左腔,以后有機會再寫吧。

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末捅儒,一起剝皮案震驚了整個濱河市液样,隨后出現(xiàn)的幾起案子振亮,更是在濱河造成了極大的恐慌,老刑警劉巖鞭莽,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件坊秸,死亡現(xiàn)場離奇詭異,居然都是意外死亡澎怒,警方通過查閱死者的電腦和手機褒搔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來喷面,“玉大人星瘾,你說我怎么就攤上這事【灞玻” “怎么了琳状?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長盒齿。 經(jīng)常有香客問我念逞,道長,這世上最難降的妖魔是什么边翁? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任肮柜,我火速辦了婚禮,結(jié)果婚禮上倒彰,老公的妹妹穿的比我還像新娘审洞。我一直安慰自己,他們只是感情好待讳,可當我...
    茶點故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布芒澜。 她就那樣靜靜地躺著,像睡著了一般创淡。 火紅的嫁衣襯著肌膚如雪痴晦。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天琳彩,我揣著相機與錄音誊酌,去河邊找鬼。 笑死露乏,一個胖子當著我的面吹牛碧浊,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播瘟仿,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼箱锐,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了劳较?” 一聲冷哼從身側(cè)響起驹止,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤浩聋,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后臊恋,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體衣洁,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年抖仅,在試婚紗的時候發(fā)現(xiàn)自己被綠了坊夫。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,100評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡岸售,死狀恐怖践樱,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情凸丸,我是刑警寧澤拷邢,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站屎慢,受9級特大地震影響瞭稼,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜腻惠,卻給世界環(huán)境...
    茶點故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一环肘、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧集灌,春花似錦悔雹、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至唆阿,卻和暖如春益涧,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背驯鳖。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工闲询, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人浅辙。 一個月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓扭弧,卻偏偏與公主長得像,于是被迫代替她去往敵國和親摔握。 傳聞我的和親對象是個殘疾皇子寄狼,可洞房花燭夜當晚...
    茶點故事閱讀 42,834評論 2 345

推薦閱讀更多精彩內(nèi)容