搶票系統,一直是後端工程師在面試時的一大難題,而搶票系統又與我們的生活息息相關(例如:周杰倫演唱會搶票、雙 11 搶購),因此這篇文章就會淺談一下搶票系統的設計和遇到的挑戰,帶大家簡易入門搶票系統的實作。
補充:搶票系統真的是一個很難的題目,我其實也不到真的很懂,所以這篇文章真的只是淺淺談一下,和大家分享目前我所學習過的內容,大家如果有什麼想法也歡迎留言,一起學習成長💪
在我們實作高流量的搶票系統之前,萬事都得從平地開始做起,所以我們就先從最基本的功能開始設計,並且也在設計的過程中,討論搶票系統可能會出現的潛在問題。
一般來說,在搶票時,不管今天是搶周杰倫的票、搶五月天、或是搶 iPhone…等等,都是要執行 「創建一筆訂單,並且將商品庫存 -1」 的操作,所以如果用程式來實作的話,可以寫成下面這個樣子(此處使用 Spring Boot 程式當作範例):
所以到這裡,我們就完成最基本的下訂單的雛形了。不過這個實作其實是有問題的,在一次只有一個人來下訂單時,這段程式可以正常運作,但是只要「多人同時一起來下訂單」,這段程式就會出現「商品超賣」的情形。
在上面的實作中,雖然程式看起來很正確沒錯,但是在「多人同時下單」時,也就是「多個 Thread 同時去執行這段程式」時,就會出現商品超賣的問題。
大家可以想像一下,假設現在資料庫中還有 1 個商品庫存,然後此時有三個人同時執行到了第 11 行的 getProductAmount()
程式的話,那麼這三個人都會覺得:「現在資料庫中還有 1 個商品庫存,太棒了!這最後一個就是我的了!!」,所以這三個人都會繼續往下執行後面的程式,去創建一筆訂單,並且將商品的庫存 -1。
但是這樣子的行為,就會導致資料庫中的商品庫存被減了 3 次(也就是被賣了 3 次),但是我們實際上只剩下 1 個商品庫存啊!!所以這時候就會導致商品超賣的問題出現,也就是我們賣超過自己擁有的庫存,導致有些消費者付了錢、但是拿不到真正的商品。
要知道,商品超賣這件事在公關危機上算是非常嚴重的事件,所以為了避免這個問題,我們就必須修改這段程式,也就是 「為他加上一個同步鎖,確保一次只會有一個人執行這段程式」,因此我們就可以將這段程式改寫成下面這樣:
(Java 中的同步鎖是用 synchronized
來實作,不熟悉 Java 語法的人可以不用管細節沒關係,只要知道這裡是加一個同步鎖就好)
也因為我們改寫了上面這段程式,在第 11 行的地方加上了一個 synchronized
的同步鎖,因此這個同步鎖一次只會放一個人進去執行第 14~19 行的程式,就可以避免商品超賣的問題了!
所以到這裡,我們才真的算是完成了「下訂單」的基本功能實作,也就是先確保我們的程式運行是正確的,至少不會產生商品超賣的問題。
而一般在系統設計的流程中,當我們實作完「基本功能」之後,就可以來探討「怎麼承受住高流量」這類的問題了。
在實作「能承受高流量的搶票系統」之前,我們先跳出來補充一下系統設計的相關概念。
系統設計通常分兩塊,一塊是 Functional Requirement(功能性需求)、另一塊是 Non-functional Requirement(非功能性需求)。
所謂的 Functional Requirement(功能性需求),就是「你是否能夠正確的解決問題」,譬如說你是否能夠實作出一個不會商品超賣的訂單系統、你是否能夠實作出一個 url 的短連結跳轉…等等,所以所謂的 Functional Requirement,就是你先把這個功能真的實作出來這樣。
而至於 Non-functional Requirement(非功能性需求),則是「你能夠多厲害的解決問題」,譬如說你這個訂單系統,是否能夠承受 1000 人的流量?是否能夠承受 1 萬人的流量?是否能夠承受 100 萬人的流量?或是你這個訂單系統,他的可用性是多少?有沒有辦法確保 99.9999% 的時間不會當機?…等等。
因此所謂的 Non-functional Requirement,就是在考驗你的系統到底有多強、執行效率有多高、可用性有多好…等。
所以在進行系統設計時,可以總結成一句話:「先求有,再求好」。
首先是 「求有」,也就是一定要先確保 Functional Requirement 能正常運作,這種最最最基本的功能一定要先完成,至少要先確保商品不要超賣,討論後面的高流量才有意義。
等到最基本的 Functional Requirement 實作完畢之後,再來就是 「求好」,也就是處理 Non-functional Requirement 的問題,例如高流量、高性能、高可用,這類的三高問題(沒錯系統設計跟血糖一樣,也有三高問題🥹)。
當然一般在系統設計中,面試官更關注的是 Non-functional Requirement 的部分,也就是你的系統能夠架設到多厲害這樣,所以 Non-functional Requirement 也可以說是在準備系統設計時,最需要花大量時間準備的地方(因為牽涉範圍又深又廣),不過在實作上,還是會建議大家「先求有,再求好」,沒有一步到位的系統,Don’t Over Design,Facebook 也不是第一天就長成那麼龐大的架構,都是慢慢跟著需求一起變化而來的。
在了解系統設計的概念之後,如果我們回到最一開始的問題的話,現在我們已經把「創建訂單」的功能實作出來了,所以現在我們可以開始探討,要如何提升這個系統,讓他可以應付更高的流量。
現在在這個架構中,因為我們是使用 synchronized
這個同步鎖,強制讓同時湧進來的許多人被擋在 synchronized
上,一次只允許一個人執行第 14 ~ 19 行的程式,因此雖然可以確保商品不會超賣,但是卻讓使用者會被同步鎖卡太久,導致使用者體驗不好。
一個較常見的解法,就是改用 Redis 來取代同步鎖,即是將商品庫存的數據改成暫存到 Redis 中,然後 Redis 中可以使用 lua 腳本確保原子性(參考 Bilibili 影片介紹 、 阿里雲介紹 )。
因此這個優化的概念,就是用 Redis 來取代 Java 內建的同步鎖,進而達到更高的效能。但是具體的實作細節,抱歉我也不是很熟悉、也沒有真的實作過,所以這樣子的作法到底能夠支撐多少流量呢?我可能也沒辦法給大家一個很好的量化數字🥹。
我目前所學習到的知識,大概就是停在 Redis 這裡而已,後面的部分我也還在探索中,所以目前還沒辦法給大家答案🥹,如果大家有興趣的話,可以在 Bilibili 上面搜尋關鍵字「秒殺系統」,大陸那邊有很多類似的影片介紹可以參考,如果想找英文資源的話,也可以搜尋「How to design a Ticketmaster」,也會有滿多相關的討論可以看一下。
也希望將來的某一天,我可以重寫這篇主題,重新深入探討搶票系統,到時候如果有學到更多新的知識,也想再重新整理出來分享給大家!
以台灣來說,比較知名的搶票系統為「拓元售票」,如果大家對拓元售票的系統架構感興趣的話,也可以參考 AWS 於 2020 年提供的公開資料( tixCraft 案例研究 ),拓元就用到了大量的 AWS 功能,來處理高流量的搶票問題。
不過因為這篇案例研究中牽涉到更多複雜的 AWS 架構機制,因此建議大家在了解簡單的搶票系統實作、以及熟悉 AWS 功能的前提下,再去閱讀這篇文章會比較易懂。
這篇文章我們有淺淺的談了一下搶票系統,不得不說這部分我真的還有很多地方可以學習和加強,目前只是先把我所知道的部分、以及網路上的公開資料,一起整理到這篇文章裡面,希望將來的某一天我可以重新回頭來寫這篇文章,把搶票系統給吃得透透的!!
如果你對後端技術有興趣的話,也歡迎免費訂閱 《古古的後端筆記》電子報 ,每週二為你送上一篇後端技術分享,那我們就下一篇文章見啦!