設計Ticket Master (附解答)

專欄 | 九章算法
網址 | www.jiuzhang.com程序員

學員面經題web

一個相似ticket master的網站。說某個時間段開放某明星演唱會訂票,大概會同時有500K QPS 的訪問量,一共只有50K張票。訂票的過程是用戶打開訂票網頁(不用考慮認證等問題),填一個text box說要訂幾張票,而後click一個button就打開一個page,那個page會不停的spin直到系統可以預留那幾張票,若是預留成功,用戶會有幾分鐘時間填寫用戶信息已經完成支付,若是到期未支付,這些票就自動被系統收回了。每張票都是同樣的,沒有位置信息什麼的。
求教怎麼design這個系統,我一開始看到500K QPS就有點慌亂了。面試

九章講師解答 SOLUTIONSredis

訂票網站一直都是世界難題。12306應該比這個還恐怖。
不要被 500k QPS嚇到。500k也好,5k也好,500也好,分析的方法和思路都是同樣的。算法

這個題的關鍵首先是給出一個可行解(不管任何系統設計題的關鍵都是如此),一個核心要實現的功能是,票的預留與回收。在設計可行解的時候,能夠先將500k qps拋之腦後。假設如今只有10個用戶來買票。優化的事情,放到Evolve的那一步。數據庫

按照咱們的SNAKE分析法來:服務器

Scenario 設計些啥:微信

  • 用戶提交訂票請求
  • 客戶端等待訂票
  • 預留票,用戶完成支付
  • 票過時,回收票
  • 限制一場演唱會的票數。

Needs 設計得多牛?併發

  • 500k QPS,面試官已經給出
  • 響應時間——是在用戶點擊的一瞬間就要完成預約麼?不是,可讓用戶等個幾分鐘。也就是說,500K的請求,能夠在若干分鐘內完成就行了。所以所謂的 500k QPS,並非Average QPS,只是說峯值是 500k QPS。而你要作的事情並非在1秒以內完成500k的預約,而是把確認你收到了購票申請就行了。

Application 應用與服務分佈式

  • ReservationService —— 用戶提交一個預約請求,查詢本身的預約狀態
  • TicketService —— 系統幫一個預約完成預約,生成具體的票

Kilobyte 數據如何存儲與訪問

1.ReservationService ——用戶提交了一個訂票申請以後,把一條預約的數據寫到數據庫裏。因此須要一個Reservation的table。大概包含的columns有:

id(primary_key)
created_at(timestamp)
concert_id(foreign key)
user_id(foreign key)
tickets_count(int)
status(int)

簡單的說就是誰在什麼時刻預約了哪一個演唱會,預約了幾張,當前預約狀態是什麼(等待,成功,失敗)。

2. TicketService —— 系統從數據庫中按照順序選出預約,完成預約,預約成功的,生成對應的Ticket。表結構以下:

id (primary key)
created_at (timestamp)
user_id (fk)
concert_id (fk)
reservation_id (fk)
status (int) // 是否退票之類的

另外,咱們固然還須要一個Concert的table,主要記錄總共有多少票:

id (primary key)
title (string)
description (text)
start_at (timestamp)
tickets_amount (int)
remain_tickets_amount (int)
...

總結一下具體的一個Work Solution 的流程以下:

  1. 用戶提交一個預約,ReservationService 收到預約,存在數據庫裏,status=pending
  2. 用戶提交預約以後,跳轉到一個等待訂票結果的界面,該界面每隔5-10秒鐘像服務器發送一個請求查詢當前的預約狀態
  3. TicketService是一個單獨執行的程序,你能夠認爲是一個死循環,不斷檢查數據庫裏是否有pending狀態的票,取出一批票,好比1k張,而後順利處理,建立對應的Tickets,修改對應的Reservation的status。

Evolve

分析一下上述的每一個操做在500k qps的狀況下會發生什麼,以及該如何解決。

1. 用戶提交一個預約,ReservationService 收到預約,存在數據庫裏,status=pending

也就是說,在一秒鐘以內,咱們要同時處理500k的預約請求,首先web server一臺確定搞不定,須要增長到大概500臺,每臺web server一秒鐘同時處理1k的請求仍是能夠的。數據庫若是隻有一臺的話,也很難承受這樣大的請求。而且SQL和NoSQL這種數據庫處理這個問題也會很是吃力。能夠選用Redis這種既是內存級訪問速度,又能夠作持久化的key-value數據庫。而且Redis自帶一個隊列的功能,很是適合咱們訂票的這個模型。Redis的存取效率大概是每秒鐘幾十k,那麼也就是咱們要大概20臺Redis應該就能夠了。咱們能夠按照 user_id 做爲 shard key,分配到各個redis上。

2. 用戶提交預約以後,跳轉到一個等待訂票結果的界面,該界面每隔5-10秒鐘像服務器發送一個請求查詢當前的預約狀態

使用了redis的隊列以後,如何查詢一個預約信息是否在隊列裏呢?方法是reservation的基本信息除了放到隊列裏,還須要同時繼續存一份在redis裏。隊列裏能夠只放reservation_id。此時reservation_id能夠用user_id+concert_id+timestamp來表示。

3. TicketService是一個單獨執行的程序,你能夠認爲是一個死循環,不斷檢查數據庫裏是否有pending狀態的票,取出一批票,好比1k張,而後順利處理,建立對應的Tickets,修改對應的Reservation的status。

爲每一個Redis的數據庫後面添加一個TicketService的程序(在某臺機器上跑着),每一個TicketService負責一個Redis數據庫。該程序每次從Redis的隊列中讀出最多1k的數據,而後計算一下有須要多少張票,好比2k,而後訪問Concert的數據庫。問Concert要2k的票,若是還剩有那麼多,那麼就remain_tickets_amount - 2k,若是不夠的話,就返回還有多少張票,並把remain_tickets_acount 清零。這個過程要對數據庫進行加鎖,能夠用數據庫本身帶的鎖,也能夠用zookeeper之類的分佈式鎖。由於如今是1k爲一組進行處理,因此這個過程不會很慢,存Concert的數據庫也不須要不少,一臺就夠了。由於就算是500k的話,分紅500組,也就是500個queries峯值,數據庫處理起來綽綽有餘額。
假如獲得了2k張票的額度以後,就順序處理這1k個reservation,而後對每一個reservation生成對應的tickets,並在redis中標記reservation的狀態,這裏的話,tickets的table大概就會產生2k條的insert,因此tickets的數據庫須要大概可以承受 20 x 1k = 20k 的併發寫。這個的話,大概 20 臺 SQL數據庫也就搞定了。

從頭理一下

開放訂票,500k的請求從世界各地涌來
經過 Load Balancer 紛發給500臺 Web Server 。每臺Web Server大概一秒鐘處理1k的請求
Web Server 將1k的請求,按照 user_id 進行 shard,丟給對應的 redis 服務器裏的隊列,並把 Reservation 信息也丟給 Redis存儲。
此時,20臺 Redis,每臺 Redis 約收到 25k 的 排隊訂票記錄
每臺 Redis 背後對應一個 TicketService 的程序,不斷的查看 Redis 裏的隊列是否有訂票記錄,若是有的話,一次拿出1k個訂票記錄進行處理,問Concert 要額度,而後把1k的reservation對應的建立出2k左右的tickets出來(假如一個reservation有2張票平均)。假如這個部分的處理能力是1k/s的話,那麼這個過程完成須要25秒。也就是說,對於用戶來講,最慢大概25秒以後,就知道本身有沒有訂上票了,平均等待時間應該低於10秒,由於當concert的票賣完了的時候,就無需生成1-2k條新的tickets,那麼這個時候速度會快不少。
存儲tickets的數據庫須要多臺,由於須要處理的請求大概是20k的qps,因此大概20臺左右的Ticket數據庫。

超時的票回收

增長一個RecycleService。這個RecycleService 不斷訪問 Tickets 的數據庫,看看有沒有超時的票,若是超時了,那麼就回收,而且去Concert的數據庫裏把remain_tickets_acount 增長。

總結如何攻破 500k QPS的核心點

核心點就是,500k QPS 我只要作到收,不須要作處處理,那麼500臺web服務器+20臺Redis就能夠了。
處理的的時候,分紅1k一組進行處理,讓用戶多等個幾秒鐘,問題不大。用戶等10秒鐘的話,咱們須要的服務器數目就下降10-20倍,這是個tradeoff,須要好好權衡的。

一些可能的疑惑和能夠繼續進化的地方

問:500臺Web服務器不少,並且除了訂票的那幾秒種,大部分的時候都是閒置浪費的,怎麼辦?
答:用AWS的彈性計算服務,爲每場演唱會的火爆指數進行評估,而後預先開好機器,用完以後就能夠銷燬掉。

問:爲何不直接用Redis也來存儲全部的數據信息?
答:由於是針對通同一個Concert的預約,你們須要訪問同一條數據(remain_tickets_acount),shard是無論用的,Redis也承受不住500k QPS 對同一條數據進行讀寫,而且還要加鎖之類的保證一致性。因此這個對 remain_tickets_acount 的值進行修改,建立對應的 tickets 的過程,是不能在用戶請求的時候,實時完成的,須要延遲進行。

問:redis又用來作隊列,又用來作Reservation 表的存儲,是否有點亂?
答:是的,因此一個更好的辦法是,只把redis當作隊列來用 和 Reservation 信息的Cache來用。當一個Reservation 被處理的時候,再到SQL數據庫裏生成對應的持久化記錄。這樣的好處是,Redis 這種結構其實不是很擅長作持久化數據的存儲,咱們通常都仍是拿來當隊列和cache用得比較多。



歡迎關注個人微信公衆號:九章算法(ninechapter)。
精英程序員交流社區,按期發佈面試題、面試技巧、求職信息等

九章算法,IT教育領域的深耕者
相關文章
相關標籤/搜索