上篇文章介紹了 ETL 場景下的高性能最終一致性解決方案,此次的問題也是一個常見的問題。html
最近發現不少人被相似秒殺這樣的設計困擾,其實這類問題能夠很方便地解決,先來講說這類問題的關鍵點是什麼:sql
總結一下,仍是那幾個詞:高性能強一致性!數據庫
下文的全部解決方案是在 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 | | +-----------+------------------+------+-----+---------+----------------+
表結構很簡單,其實就是一個user
和deal
的關聯表。誰買了多少就插入數據唄。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 (?, ?, ?)
你們別笑,這樣的設計你必定作過,剛畢業的時候誰沒設計過這樣的系統啊?並且大部分系統對性能和一致性的要求並無那麼高,因此以上的設計方案還真是廣泛存在的。
那就說說在什麼狀況下會出問題吧:
那就讓咱們一步步來解決裏面存在的問題吧。
先來解決最簡單的問題,保證單用戶不會重複購買。
其實只要利用數據庫特性便可,讓咱們來加一個索引:
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
的狀況,除非你實現的不對。
…… 無圖無真相,徹底混亂了
只看文字不清晰,仍是來張完整的流程圖吧!
毫無破綻啊!不是說要犧牲一致性嗎?爲何沒看到?由於上面的流程圖尚未考慮數據庫故障或者網絡故障,最後仍是來一張最完整的流程圖吧:
仔細看一下整張流程圖,最終就這幾種狀況:
前三種是正常的,只有「損失庫存」是有問題的。其實,「損失庫存」這種狀況其實很難出現,只有在網絡故障或者數據庫的狀況下才可能偶爾。
那你的業務能夠容忍它嗎?最終仍是具體問題具體分析了。
最後仍是提醒一句,千萬不要過分優化,第一個使用事務的方案其實已經夠好了!
除非你的業務特殊,全中國幾十萬人幾百萬人會同時來買,那纔有必要犧牲一下一致性提高性能。
對了,若是是像雙十一或者小米這樣子的搶購,上面的方案也是不夠的…
本做品由 Dozer 創做,採用 知識共享署名-非商業性使用 4.0 國際許可協議 進行許可。