「秒殺」問題的數據庫和SQL設計【轉載】

問題的來源

上篇文章介紹了 ETL 場景下的高性能最終一致性解決方案,此次的問題也是一個常見的問題。html

最近發現不少人被相似秒殺這樣的設計困擾,其實這類問題能夠很方便地解決,先來講說這類問題的關鍵點是什麼:sql

  1. 必定要高性能,否則還能叫秒殺嗎?
  2. 要強一致性,庫存只有100個,不能賣出去101個吧?可是庫存10000實際只賣了9999是否容許呢?
  3. 既然這裏說了是秒殺,那每每還會針對每一個用戶有購買數量的限制。

總結一下,仍是那幾個詞:高性能強一致性!數據庫

下文的全部解決方案是在 Mysql InnoDB 下作的。由於用到了不少數據庫特性。其餘的數據庫或其餘的數據庫引擎會有不一樣的表現,請注意。網絡

 

徹底不考慮一致性的方案

表結構

+-----------+------------------+------+-----+---------+----------------+
| Field     | Type             | Null | Key | Default | Extra          |
+-----------+------------------+------+-----+---------+----------------+
| id | int(11) unsigned | NO | PRI | NULL | auto_increment | | user_id | int(11) | NO | | NULL | | | deal_id | int(11) | NO | | NULL | | | buy_count | int(11) | NO | | NULL | | +-----------+------------------+------+-----+---------+----------------+ 

 

方案

表結構很簡單,其實就是一個userdeal的關聯表。誰買了多少就插入數據唄。post

首先,還要檢查一下傳過來的buy_count是否超過單人購買限制。性能

接下來,每次插入前執行如下如下操做檢查一下是否超賣便可:優化

select sum(buy_count) from UserDeal where deal_id = ?網站

最後還要檢查一下這個用戶是否購買過:spa

select count(*) from UserDeal where user_id = ? and deal_id = ?設計

全都沒問題了就插入數據:

insert into UserDeal (user_id, deal_id, buy_count) values (?, ?, ?)

 

存在的問題

你們別笑,這樣的設計你必定作過,剛畢業的時候誰沒設計過這樣的系統啊?並且大部分系統對性能和一致性的要求並無那麼高,因此以上的設計方案還真是廣泛存在的。

那就說說在什麼狀況下會出問題吧:

  1. 若是庫存只剩一個,兩個用戶同時點購買,兩我的檢查所有成功,最後,就超賣了。
  2. 若是一個用戶同時發起兩次請求,檢測部分一樣可能會同時經過,最後,數據就異常了。

那就讓咱們一步步來解決裏面存在的問題吧。

 

保證單用戶不會重複購買

先來解決最簡單的問題,保證單用戶不會重複購買。

其實只要利用數據庫特性便可,讓咱們來加一個索引:

alter table UserDeal add unique user_id_deal_id(user_id, deal_id)

加上惟一索引後,不只查詢性能提升了,插入的時候若是重複還會自動報錯。

固然別忘了在業務代碼中 catch 一下這個異常,並在頁面上給用戶友好的提醒。

 

解決超賣問題

方案

爲了解決這個問題,第一個想到的就是把這幾回操做在事務中操做。不然不管怎麼改,也都不是原子性的了。

可是加完事務後就完了?

上面的select語句沒有使用for update關鍵字,因此就算加入了事務也不會影響其餘人讀寫。

因此咱們只要改一下select語句便可:

select sum(buy_count) from UserDeal where deal_id = ? for update

 

優化

剛改完後發現,問題解決了!so easy!步步高點讀機,哪裏不會點哪裏,so easy!

可是不對啊!爲何兩個用戶操做不一樣的deal也會相互影響呢?

原來咱們的select語句中的查詢條件是where deal_id = ?,你覺得只會鎖全部知足條件的數據對吧?

但實際上,若是你查詢的條件不在索引中,那麼 InnoDB 會啓用表鎖!

那就加一個索引唄:

alter table UserDeal add index ix_deal_id(deal_id)

 

提升性能了

好了,到目前爲止,不管用戶怎沒點,不管多少我的買同一單,都不會出現一致性的問題的。

並且事務都是行鎖,若是你的業務場景不是秒殺,操做是分散在各個單子上的。並且你的壓力不大,那麼優化到這就夠了。

可是,若是你真的會有幾萬人、幾十萬人同時秒殺一個單子怎麼辦?

不少交易類網站都會有這樣的活動。

 

咱們如今思考一下,上面的優化好像已是極致了,不只知足了一致性,並且性能方面也作了足夠的考量,無從下手啊!

這時候,只能犧牲一些東西了。

 

魚與熊掌不可兼得

優化的思路

性能和一致性經常同時出現,卻又相互排斥。剛纔咱們爲了解決一致性問題帶入了性能問題。如今咱們又要爲了性能而犧牲一致性了。

這裏想提升性能的話,就要去掉事務了。那麼一旦去掉事務,一致性就沒辦法保證了,但有些一致性的問題並非那麼地嚴重。

因此,這裏最關鍵的就是要想清楚,你的業務場景對什麼不能容忍,對什麼能夠容忍。不一樣業務場景最後的方案必定是不一樣的。

 

秒殺能夠容忍什麼

本文標題說的是秒殺,由於這個業務場景很常見,那麼咱們就來講說秒殺。

秒殺最怕的是超賣,但卻能夠接受少賣。什麼是少賣?我有一萬份,賣了9999份,但數據庫裏卻說已經買完了。

這個嚴重嗎?只要咱們能把這個錯誤的量控制在必定比例之內而且能夠後續修復,那這在秒殺中就不是一個問題了。

 

爲了性能犧牲一致性的設計方案

去掉了事務會發生什麼

在上述的方案中,若是去掉了事務,單用戶重複購買是不會有問題的,由於這個是經過惟一索引來實現的。

因此這邊咱們主要是去解決超賣問題。

既然去掉了事務,那麼for update鎖行就無效了,咱們能夠另闢蹊徑,來解決這個問題。

 

修改表結構

剛纔一直沒有提Deal表,其實它就是存了一下基本信息,包括最大售賣量。

以前咱們是經過對關聯表進行sum(buy_count)操做來獲得已經賣掉的數量的,而後進行判斷後再進行插入數據。

如今沒了事務,這樣的操做就不是原子性的了。

因此讓咱們來修改一下Deal表,把已經售賣的量也存放在Deal表中,而後巧妙地把操做轉換成一行update語句。

+-----------+------------------+------+-----+---------+----------------+
| Field     | Type             | Null | Key | Default | Extra          |
+-----------+------------------+------+-----+---------+----------------+
| id | int(11) unsigned | NO | PRI | NULL | auto_increment | | buy_max | int(11) | NO | | NULL | | | buy_count | int(11) | NO | | NULL | | +-----------+------------------+------+-----+---------+----------------+ 

 

修改執行過程

若是你繼續先把數據查出來到內存中而後再操做,那就不是原子性的了,一定會出問題。

這時候,神奇的update語句來了:

update Deal set buy_count = buy_count + 1 where id = ? and buy_count + 1 <= buy_max

若是一單的buy_max是1000,若是有2000個用戶同時操做會發生什麼?

雖然沒有事務,可是update語句自然會有行鎖,前1000個用戶都會執行成功,返回生效行數1。而剩下的1000人不會報錯,可是生效行數爲0。

因此程序中只要判斷update語句的生效行數就知道是否搶購成功了。

 

尚未結束

問題解決了?好像也沒犧牲一致性啊,用戶根本不會超賣啊?

可是,購買的時候有兩個關鍵信息,「剩餘多少」和「誰買了」,剛纔的執行過程只處理了第一個信息,它根本沒存「誰買了」這個信息。

而這兩個信息其實也是原子性的,可是爲了性能,咱們不得不犧牲一下了。

剛纔說到若是update的生效行數是1,就表明購買成功。因此,若是一個用戶購買成功了,那麼就再去UserDeal表中插入一下數據。

可若是一個用戶重複購買了,那麼這裏也會出錯,因此若是這裏出錯的話還須要去操做一下Deal表把剛纔購買的還回去:

update Deal set buy_count = buy_count - 1 where id = ? and buy_count - 1 >= 0

這邊理論上不會出現buy_count - 1 < 0的狀況,除非你實現的不對。

…… 無圖無真相,徹底混亂了

只看文字不清晰,仍是來張完整的流程圖吧!

flow1

毫無破綻啊!不是說要犧牲一致性嗎?爲何沒看到?由於上面的流程圖尚未考慮數據庫故障或者網絡故障,最後仍是來一張最完整的流程圖吧:

flow2

仔細看一下整張流程圖,最終就這幾種狀況:

  1. 執行成功
  2. 無庫存
  3. 回滾成功
  4. 損失庫存

前三種是正常的,只有「損失庫存」是有問題的。其實,「損失庫存」這種狀況其實很難出現,只有在網絡故障或者數據庫的狀況下才可能偶爾。

那你的業務能夠容忍它嗎?最終仍是具體問題具體分析了。

 

不要過分優化

最後仍是提醒一句,千萬不要過分優化,第一個使用事務的方案其實已經夠好了!

除非你的業務特殊,全中國幾十萬人幾百萬人會同時來買,那纔有必要犧牲一下一致性提高性能。

對了,若是是像雙十一或者小米這樣子的搶購,上面的方案也是不夠的…

本做品由 Dozer 創做,採用 知識共享署名-非商業性使用 4.0 國際許可協議 進行許可。

相關文章
相關標籤/搜索