【秒殺系統】從零開始打造簡易秒殺系統(一):防止超賣

【秒殺系統】從零開始打造簡易秒殺系統(一):防止超賣

前言

大家好,好久不發文章了。(快一個月了- -)最近有很多學習的新知識想和大家分享,但無奈最近項目蠻忙的,很多文章寫了一半擱置在了筆記里,待以后慢慢補充發布。

本文主要是通過實際代碼講解,幫助你一步步搭建一個簡易的秒殺系統。從而快速的了解秒殺系統的主要難點,并且迅速上手實際項目。

我對秒殺系統文章的規劃:

  • 從零開始打造簡易秒殺系統:樂觀鎖防止超賣
  • 從零開始打造簡易秒殺系統:令牌桶限流
  • 從零開始打造簡易秒殺系統:Redis 緩存
  • 從零開始打造簡易秒殺系統:消息隊列異步處理訂單

歡迎關注我的公眾號:后端技術漫談(二維碼見底部)

秒殺系統

秒殺系統介紹

秒殺系統相信網上已經介紹了很多了,我也不想黏貼很多定義過來了。

廢話少說,秒殺系統主要應用在商品搶購的場景,比如:

  • 電商搶購限量商品
  • 賣周董演唱會的門票
  • 火車票搶座

秒殺系統抽象來說就是以下幾個步驟:

  • 用戶選定商品下單
  • 校驗庫存
  • 扣庫存
  • 創建用戶訂單
  • 用戶支付等后續步驟…

聽起來就是個用戶買商品的流程而已嘛,確實,所以我們為啥要說他是個專門的系統呢。。

為什么要做所謂的“系統”

如果你的項目流量非常小,完全不用擔心有并發的購買請求,那么做這樣一個系統意義不大。

但如果你的系統要像12306那樣,接受高并發訪問和下單的考驗,那么你就需要一套完整的流程保護措施,來保證你系統在用戶流量高峰期不會被搞掛了。(就像12306剛開始網絡售票那幾年一樣)

這些措施有什么呢:

  • 嚴格防止超賣:庫存100件你賣了120件,等著辭職吧
  • 防止黑產:防止不懷好意的人群通過各種技術手段把你本該下發給群眾的利益全收入了囊中。
  • 保證用戶體驗:高并發下,別網頁打不開了,支付不成功了,購物車進不去了,地址改不了了。這個問題非常之大,涉及到各種技術,也不是一下子就能講完的,甚至根本就沒法講完。

我們先從“防止超賣”開始吧

畢竟,你網頁可以卡住,最多是大家沒參與到活動,上網口吐芬芳,罵你一波。但是你要是賣多了,本該拿到商品的用戶可就不樂意了,輕則投訴你,重則找漏洞起訴賠償。讓你吃不了兜著走。

不能再說下去了,我這篇文章可是打著實戰文章的名頭,為什么我老是要講廢話啊啊啊啊啊啊。

上代碼。

說好的做“簡易”的秒殺系統,所以我們只用最簡單的SpringBoot項目

建立“簡易”的數據庫表結構

一開始我們先來張最最最簡易的結構表,參考了crossoverjie的秒殺系統文章。

等未來我們需要解決更多的系統問題,再擴展表結構。

一張庫存表stock,一張訂單表stock_order

-- ----------------------------
-- Table structure for stock
-- ----------------------------
DROP TABLE IF EXISTS `stock`;
CREATE TABLE `stock` (`id` int(11) unsigned NOT NULL AUTO_INCREMENT,`name` varchar(50) NOT NULL DEFAULT '' COMMENT '名稱',`count` int(11) NOT NULL COMMENT '庫存',`sale` int(11) NOT NULL COMMENT '已售',`version` int(11) NOT NULL COMMENT '樂觀鎖,版本號',PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;-- ----------------------------
-- Table structure for stock_order
-- ----------------------------
DROP TABLE IF EXISTS `stock_order`;
CREATE TABLE `stock_order` (`id` int(11) unsigned NOT NULL AUTO_INCREMENT,`sid` int(11) NOT NULL COMMENT '庫存ID',`name` varchar(30) NOT NULL DEFAULT '' COMMENT '商品名稱',`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '創建時間',PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
通過HTTP接口發起一次購買請求

代碼中我們采用最傳統的Spring MVC+Mybaits的結構

結構如下圖:

Controller層代碼

提供一個HTTP接口: 參數為商品的Id

@RequestMapping("/createWrongOrder/{sid}")
@ResponseBody
public String createWrongOrder(@PathVariable int sid) {LOGGER.info("購買物品編號sid=[{}]", sid);int id = 0;try {id = orderService.createWrongOrder(sid);LOGGER.info("創建訂單id: [{}]", id);} catch (Exception e) {LOGGER.error("Exception", e);}return String.valueOf(id);
}
Service層代碼
@Override
public int createWrongOrder(int sid) throws Exception {//校驗庫存Stock stock = checkStock(sid);//扣庫存saleStock(stock);//創建訂單int id = createOrder(stock);return id;
}private Stock checkStock(int sid) {Stock stock = stockService.getStockById(sid);if (stock.getSale().equals(stock.getCount())) {throw new RuntimeException("庫存不足");}return stock;
}private int saleStock(Stock stock) {stock.setSale(stock.getSale() + 1);return stockService.updateStockById(stock);
}private int createOrder(Stock stock) {StockOrder order = new StockOrder();order.setSid(stock.getId());order.setName(stock.getName());int id = orderMapper.insertSelective(order);return id;
}
發起并發購買請求

我們通過JMeter(https://jmeter.apache.org/) 這個并發請求工具來模擬大量用戶同時請求購買接口的場景。

注意:POSTMAN并不支持并發請求,其請求是順序的,而JMeter是多線程請求。希望以后PostMan能夠支持吧,畢竟JMeter還在倔強的用Java UI框架。畢竟是親兒子呢。

如何通過JMeter進行壓力測試,請參考下文,講的非常入門但詳細,包教包會:

https://www.cnblogs.com/stulzq/p/8971531.html

我們在表里添加一個Iphone,庫存100。(請忽略訂單表里的數據,開始前我清空了)

在JMeter里啟動1000個線程,無延遲同時訪問接口。模擬1000個人,搶購100個產品的場景。點擊啟動:

你猜會賣出多少個呢,先想一想。。。

答案是:

賣出了14個,庫存減少了14個,但是每個請求Spring都處理了,創建了1000個訂單。

我這里該夸Spring強大的并發處理能力,還是該罵MySQL已經是個成熟的數據庫,卻都不會自己鎖庫存?

避免超賣問題:更新商品庫存的版本號

為了解決上面的超賣問題,我們當然可以在Service層給更新表添加一個事務,這樣每個線程更新請求的時候都會先去鎖表的這一行(悲觀鎖),更新完庫存后再釋放鎖。可這樣就太慢了,1000個線程可等不及。

我們需要樂觀鎖。

一個最簡單的辦法就是,給每個商品庫存一個版本號version字段

我們修改代碼:

Controller層
/*** 樂觀鎖更新庫存* @param sid* @return*/
@RequestMapping("/createOptimisticOrder/{sid}")
@ResponseBody
public String createOptimisticOrder(@PathVariable int sid) {int id;try {id = orderService.createOptimisticOrder(sid);LOGGER.info("購買成功,剩余庫存為: [{}]", id);} catch (Exception e) {LOGGER.error("購買失敗:[{}]", e.getMessage());return "購買失敗,庫存不足";}return String.format("購買成功,剩余庫存為:%d", id);
}
Service層
@Override
public int createOptimisticOrder(int sid) throws Exception {//校驗庫存Stock stock = checkStock(sid);//樂觀鎖更新庫存saleStockOptimistic(stock);//創建訂單int id = createOrder(stock);return stock.getCount() - (stock.getSale()+1);
}private void saleStockOptimistic(Stock stock) {LOGGER.info("查詢數據庫,嘗試更新庫存");int count = stockService.updateStockByOptimistic(stock);if (count == 0){throw new RuntimeException("并發更新庫存失敗,version不匹配") ;}
}

?

Mapper
<update id="updateByOptimistic" parameterType="cn.monitor4all.miaoshadao.dao.Stock">update stock<set>sale = sale + 1,version = version + 1,</set>WHERE id = #{id,jdbcType=INTEGER}AND version = #{version,jdbcType=INTEGER}</update>

我們在實際減庫存的SQL操作中,首先判斷version是否是我們查詢庫存時候的version,如果是,扣減庫存,成功搶購。如果發現version變了,則不更新數據庫,返回搶購失敗。

發起并發購買請求

這次,我們能成功嗎?

再次打開JMeter,把庫存恢復為100,清空訂單表,發起1000次請求。

這次的結果是:

賣出去了39個,version更新為了39,同時創建了39個訂單。我們沒有超賣,可喜可賀。

由于并發訪問的原因,很多線程更新庫存失敗了,所以在我們這種設計下,1000個人真要是同時發起購買,只有39個幸運兒能夠買到東西,但是我們防止了超賣。

手速快未必好,還得看運氣呀!

OK,今天先到這里,之后我們繼續一步步完善這個簡易的秒殺系統,它總有從樹苗變成大樹的那一天!

源碼

我會隨著文章的更新,一直同步更新項目代碼,歡迎關注:

https://github.com/qqxx6661/miaosha

參考

  • https://cloud.tencent.com/developer/article/1488059
  • https://juejin.im/post/5dd09f5af265da0be72aacbd
  • https://crossoverjie.top/%2F2018%2F05%2F07%2Fssm%2FSSM18-seconds-kill%2F

關注我

我是一名后端開發工程師。

主要關注后端開發,數據安全,物聯網,邊緣計算方向,歡迎交流。

各大平臺都可以找到我
  • 微信公眾號:后端技術漫談
  • Github:@qqxx6661
  • CSDN:@Rude3knife
  • 知乎:@后端技術漫談
  • 簡書:@蠻三刀把刀
  • 掘金:@蠻三刀把刀
原創博客主要內容
  • 后端開發技術
  • Java面試知識點
  • 設計模式/數據結構
  • LeetCode/劍指offer 算法題解析
  • SpringBoot/SpringCloud入門實戰系列
  • 數據分析/數據爬蟲
  • 逸聞趣事/好書分享/個人生活
個人公眾號:后端技術漫談

公眾號:后端技術漫談.jpg

如果文章對你有幫助,不妨收藏,轉發,在看起來~

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/18313.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/18313.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/18313.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

redis筆記1

1-nosql&#xff08;非關系型數據庫&#xff09; 定位緩存&#xff0c;提高數據讀寫速度&#xff0c;減輕對數據儲存與訪問壓力&#xff0c;不建議存敏感數據&#xff08;重要數據&#xff09;。 2-特征 &#xff08;1&#xff09;鍵值&#xff08;key-value&#xff09;型 &a…

【面試】Oracle JDK和Open JDK什么關系?

目錄 1. 起源與發展2. 代碼與許可3. 功能與組件4. 使用場景5. 版本更新與支持 1. 起源與發展 1.Oracle JDK是由Oracle公司基于Open JDK源代碼開發的商業版本。2.Open JDK是java語言的一個開源實現。 2. 代碼與許可 1.Oracle JDK包含了閉源組件&#xff0c;并根據二進制代碼許…

深入Java:JSON解析與操作的藝術

哈嘍&#xff0c;大家好&#xff0c;我是木頭左&#xff01; 一、初識JSON&#xff1a;數據格式的優雅舞者 在現代Web開發中&#xff0c;JSON&#xff08;JavaScript Object Notation&#xff09;以其輕量級和易于閱讀的特點成為了數據交換的首選格式。它基于JavaScript的一個…

用最通俗的話理解什么是協程

參考&#xff1a; 用最通俗的話理解什么是協程-CSDN博客

FreeRTOS_信號量_學習筆記

信號量的特性 消息隊列用于傳輸多個數據&#xff0c;但是有時候我們只需要傳遞狀態&#xff0c;這個狀態值需要用一個數值表示。套用隊列筆記中的流水線例子&#xff0c;可以理解為流水線上工件的數量。 信號&#xff1a;起通知作用 量&#xff1a;還可以用來表示資源的數量 當…

打印機手動雙面打印技巧

一、WORD和PDF &#xff08;1&#xff09;首先選擇要打印的頁面范圍&#xff0c;然后選擇僅奇數頁打印 &#xff08;2&#xff09;將打印完的紙張翻過來&#xff0c;白紙朝上&#xff0c;紙張的頭部先放入打印機 &#xff08;3&#xff09;選擇要打印的頁面范圍&#xff0c;然…

oracle.jdbc.OracleDatabaseException: ORA-00911: 無效字符

先吐槽一句&#xff0c;oracle 真坑啊&#xff01; 一個很正常的sql 語句一直報 ORA-00911: 無效字符 &#xff0c;拿到數據庫去執行一點問題沒有&#xff0c;一運行代碼就報錯&#xff0c;然后一個字符一個字符的對比&#xff0c;竟然是因為sql 結尾的一個 ";" 導致…

TP6開發文檔概述

TP6&#xff08;ThinkPHP 6&#xff09;是一個使用PHP語言開發的快速、兼容且簡單的面向對象開發框架。以下是一個簡化的TP6開發文檔概述&#xff0c;涵蓋了核心功能和一些常用方法&#xff1a; 一、環境準備 PHP安裝&#xff1a;確保已經安裝了與TP6兼容的PHP版本。 Composer…

PHPIPAM在建立數據庫階段報錯

如題&#xff0c;參考大佬的操作IP地址管理系統phpipam部署-CSDN博客搭建PHPIPAM&#xff0c;不過我沒有選擇1.4&#xff0c;而是直接搭建了1.6版本&#xff0c;一切順利&#xff0c;到了最后建立數據庫階段&#xff0c;輸入數據庫賬號和密碼后提示Cannot install sql SCHEMA f…

大模型部署_書生浦語大模型 _作業2

本節課可以讓同學們實踐 4 個主要內容&#xff0c;分別是&#xff1a; 1、部署 InternLM2-Chat-1.8B 模型進行智能對話 1.1安裝依賴庫&#xff1a; pip install huggingface-hub0.17.3 pip install transformers4.34 pip install psutil5.9.8 pip install accelerate0.24.1…

Elasticsearch之文本分析

文本分析基本概念 官網&#xff1a;Text analysis | Elasticsearch Guide [7.17] | Elastic 官網稱為文本分析&#xff0c;這是對文本進行一直分析處理的方式&#xff0c;基本處理邏輯是為按照預先制定的分詞規則&#xff0c;把原本的文檔進行分割成多個小顆粒度的詞項&#x…

Python pands使用引擎實現excel條件格式

截至我的知識更新日期&#xff08;2023年&#xff09;&#xff0c;Pandas 庫本身并不直接支持Excel條件格式。Pandas 是一個強大的Python數據分析庫&#xff0c;它主要用于數據分析和操作&#xff0c;而不是用于創建或編輯Excel文件的格式。 然而&#xff0c;你可以使用 openp…

如何給出好的“文言一心”指令?

一、文言一心是什么&#xff1f; 在現代技術背景下&#xff0c;“文言一心”還是百度公司創建的一款大語言模型。這款模型基于飛槳深度學習平臺和文心知識增強大模型&#xff0c;并擁有強大的中文語料庫&#xff0c;可以理解和生成富含文化內涵和哲理的文本內容。其核心技術架構…

社區醫院|基于SprinBoot+vue的社區醫院管理服務系統(源碼+數據庫+文檔)

社區醫院管理服務系統 目錄 基于SprinBootvue的社區醫院管理服務系統 一、前言 二、系統設計 三、系統功能設計 1系統功能模塊 2管理員功能模塊 3用戶功能模塊 4醫生功能模塊 四、數據庫設計 五、核心代碼 六、論文參考 七、最新計算機畢設選題推薦 八、源碼獲取…

看花眼,眼花繚亂的主食凍干到底應該怎么選?靠譜的主食凍干分享

隨著科學養貓知識的普及&#xff0c;主食凍干喂養越來越受到養貓人的青睞。主食凍干不僅符合貓咪的飲食天性&#xff0c;還能提供均衡的營養&#xff0c;有助于維護貓咪的口腔和消化系統健康。許多貓主人認識到了主食凍干喂養的諸多益處&#xff0c;計劃嘗試這種喂養方式&#…

學英語材料:單口喜劇、講故事、短劇喜劇以及廣播劇和播客節目

學習英語節目 有名的單口喜劇、講故事、短劇喜劇以及廣播劇和播客節目&#xff1a; 單口喜劇&#xff08;Stand-up Comedy&#xff09; 描述&#xff1a;這是最接近相聲的形式&#xff0c;表演者獨自一人站在舞臺上&#xff0c;用幽默的方式講述個人經歷、觀察到的社會現象或…

C++面向對象程序設計 - 標準輸出流

在C中&#xff0c;標準輸出流通常指的是與標準輸出設備&#xff08;通常是終端或控制臺&#xff09;相關聯的流對象。這個流對象在C標準庫中被定義為std::cout、std::err、std::clog&#xff0c;它們是std::ostream類的一個實例。 一、cout&#xff0c;cerr和clog流 ostream類…

echarts(6大基礎圖表)的使用

目錄 一、vue2掛載 二、柱狀圖 2.1、基礎柱狀圖介紹 2.2、標記&#xff1a;最大值\最小值(markPoint)、平均值(markLine) 2.3、顯示&#xff1a;數值顯示(label)、柱子寬度(barWidth)、橫向柱狀圖 三、折線圖 3.1、標記&#xff1a;最大值\最小值(markPoint)、平均值(ma…

R可視化:組間點圖比較

散點組間比較 散點組間比較 介紹 ggplot2繪制散點組間比較加載R包 knitr::opts_chunk$set(message = FALSE, warning = FALSE)library(tidyverse) library(ggplot2) library(ggprism) library(ggbeeswarm) library(rstatix)rm(list = ls()) options(stringsAsFactors = F) o…

android11禁止進入屏保和自動休眠

應某些客戶要求&#xff0c;關閉了開機進入屏保&#xff0c;一段時間會休眠的問題。以下diff可供參考&#xff1a; diff --git a/overlay/frameworks/base/packages/SettingsProvider/res/values/defaults.xml b/overlay/frameworks/base/packages/SettingsProvider/res/value…