就這么簡單:秒殺應用的MySQL數(shù)據(jù)庫優(yōu)化
關于秒殺
隨著雙11活動的不斷發(fā)展,小米饑餓營銷模式的興起,“秒殺”已經(jīng)成為一個熱點詞匯。在一些活動中,熱銷商品會以驚人的速度售罄,比如最近筆者在搶購美圖M4手機,12點開賣,1分鐘之內就被售罄。
秒殺的實現(xiàn)
對于關注數(shù)據(jù)庫的筆者來說,更關心的是如何高效的實現(xiàn)秒殺應用。之前淘寶在2013年的數(shù)據(jù)庫大會上分享過他們的秒殺方案,修改MySQL數(shù)據(jù)庫源碼來實現(xiàn)高效的秒殺應用。但是,那篇分享過于高大上,沒有給出具體的實現(xiàn)過程。另外,從其他渠道打聽到的是這個方案并沒有在生產環(huán)境上線,不知道有沒有其他知道內幕的小伙伴,具體來說說淘寶的方案是否有上線。
當然,有多種方法來優(yōu)化秒殺應用,比如使用memcached的CAS功能,但是這些方法都不能實現(xiàn)事務的特性。對于深受Jim Gray事務處理教育長大的一代,筆者覺得任何事情都應該事務的,不支持事務只不過能取得暫時的勝利,整個世界的哲學應該就是事務,即要么全做,要么全不做,不要處于一個中間狀態(tài)。筆者的為人哲學就是,要么不去設定一個目標,否則這個目標一定會去實現(xiàn)。比如,筆者決定去讀博,那么一定會完成這個學業(yè)。
筆者感覺雖然淘寶沒有給出具體的實現(xiàn)方式,但是拋出了秒殺應用對于數(shù)據(jù)庫壓力的問題所在,即大并發(fā)量下更新同一行數(shù)據(jù)的壓力。例如并發(fā)執(zhí)行如下的SQL語句模擬秒殺場景:
- BEGIN;
- INSERT INTO stock_log VALUES
- SELECT count FROM stock WHERE id=1 AND count>0 FOR UPDATE;
- UPDATE stock SET count = count -1 WHERE id=1 AND count > 0;
- COMMIT;
在做秒殺時,最主要是對庫存表進行操作,在操作前可能需要插入一些其他操作,比如日志等,然后就是對庫存表進行更新。下圖顯示增大并發(fā)量的情況下,事務處理的性能:
顯而易見的是隨著并發(fā)量的增大,事務處理的性能越差。這和淘寶之前分享的數(shù)據(jù)基本一致。導致其中的原因就是秒殺是對同一件商品進行更新,需要對同一行記錄加鎖,因此秒殺操作雖然是并行的,但是在數(shù)據(jù)庫層面是串行的。
隨著并發(fā)的不斷增大,不斷發(fā)生事務的鎖等待與喚醒操作,導致性能的急劇下降。如果通過perf工具來觀察的話,應該可以觀察到類似如下的內容:
- #
- 59.06% mysqld mysqld [.] lock_deadlock_recursive
- 16.63% mysqld libc-2.13.so [.] 0x115171 3.09% mysqld mysqld [.] lock_rec_get_prev
- 2.96% mysqld mysqld [.] my_strnncollsp_utf8
- ......
可以發(fā)現(xiàn)鎖的死鎖檢測占據(jù)了大部分的CPU時間,究其原因,就是因為鎖等待。
innodb_thread_concurrency
有小伙伴或許會知道可以通過innodb_thread_concurrency參數(shù)來控制InnoDB存儲引擎層的并發(fā)量。的確,通過這個參數(shù)可以限制進入InnoDB引擎層的事務數(shù)量,對比測試的話,性能上的確會有一定的提升:
可以發(fā)現(xiàn),將innodb_thread_concurrency設置為16,性能的確會有一定的提升。并發(fā)線程數(shù)在128的時候,TPS從原有的4300提升為了7200,將近有65%的性能提升。但是在256線程之后,性能依舊堪憂。
導致上述的原因是雖然在InnoDB存儲引擎層做了“限流”,但是MySQL數(shù)據(jù)庫上層的線程依然需要等待喚醒。
#p#
線程池技術
業(yè)界提供了很多關于秒殺MySQL的解決方案,然而非常的定制化,并且需要應用修改相信的程序,比如通過在SQL語句中寫hint來進行排隊,而這種的排隊機制在我看來在低并發(fā)量下性能反而又會變差。因此,一個通用的解決方案是采用線程池技術。
線程池可以在MySQL上層限制住同時運行的MySQL的事務數(shù),這樣就解決了由秒殺而導致的資源競爭問題。例如,通過前面的測試,已經(jīng)得知并發(fā)16線程時,秒殺可以有***的性能,那么這時用戶將線程池的大小設置為16,這樣就能獲得用戶預期想要的性能:
可以發(fā)現(xiàn)即使在4096個并發(fā)線程下,秒殺依然可以有近10000的TPS。通過線程池技術,秒殺就是這么簡單,無需任何應用端的修改。
但是線程池這里有個參數(shù)thread_pool_oversubscribe,這個參數(shù)其實有點類似云計算中“超售”概念,即MySQL的線程池允許有額外的線程運行。該參數(shù)默認是3,之前thread_pool_size設置為16,那么總共允許16*(1+3)=64個線程同時運行。這個參數(shù)的默認值本身沒有問題,但是對于秒殺應用來說確是不需要的,因為之前已經(jīng)討論過,秒殺應用是串行的。所以將參數(shù)thread_pool_oversubscribe設置為1,秒殺應用還能有進一步的提升:
可以發(fā)現(xiàn)在大并發(fā)的線程下,性能還能有10%~30%的提升。
總結
其實秒殺應用的數(shù)據(jù)庫層優(yōu)化非常簡單,各個層面做好排隊即可,如:
- 應用層做好對于單個商品搶購的數(shù)量限制
- MySQL數(shù)據(jù)庫層使用線程池技術來保證大并發(fā)量下的性能
- 調整參數(shù)thread_pool_oversubscribe用來進一步提升性能
MySQL企業(yè)版提供了線程池插件,但是需要額外的費用。小伙伴們可以使用開源的MySQL版本InnoSQL,其免費提供了線程池,可以保證應用在大并發(fā)量下依舊保證應用的穩(wěn)定性,特別是對于秒殺類的應用。
點擊下方原文閱讀可以下載InnoSQL 5.5.30-v6/InnoSQL 5.6.19-v1版本,其中提供了免費的線程池,雙機高可用套件,TopSQL等插件,并行復制等功能。使用InnoSQL可以得到筆者完整的免費技術支持,還等什么呢?