文章討論內容
秒殺類的問題一直都是web領域比較熱點的問題,一個超高併發的網站須要考慮從產品、前端優化、站點部署及後端服務等等全部環節進行考慮。mysql所能抗住的寫壓力是必定的,高併發的web站點,你須要在數據持久化以前控制好壓力,而不是把全部的請求都落到數據服務這一層。今天我不在這篇文章裏討論秒殺總體設計的問題(我也沒這個資格),咱們討論的是如何在流速已經獲得控制的狀況下,如何利用mysql更安全、高效的解決這個問題。php
從網上能夠看到各類各樣的實現方案,如今針對這些方案及其優缺點和理解誤區進行討論。html
常見寫法安全性及效率分析
假設咱們的商品表的schema是下面這樣的:前端
CREATE TABLE `goods` ( `id` int(10) NOT NULL AUTO_INCREMENT COMMENT '自增id', `name` varchar(256) NOT NULL DEFAULT '' COMMENT '商品名稱', `available` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '庫存剩餘量', `stock` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '總庫存量', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='商品表'
設置爲字段無符號解決 mysql
num = select available from goods where id = xx ; if(num > 0){ affectRows = udpate goods set available = available - 1 where id = xx ; if(affectRows == 1){ return ok ; }else{ return fatal ; } }
這種作法你們的想法是咱們將庫存字段設置成無符號類型,這樣當庫存字段在sql執行時候被置爲負數的時候mysql就會報錯,那麼affectRow就會是0或者能夠捕獲到這個異常,從而實現併發下的數據安全。web
解法釋義
實際上這段代碼是危險的,由於在不一樣版本的mysql和配置下,這段代碼的表現徹底不一樣。具體的狀況會出現3種不一樣的結果:sql
- 1.代碼正常運行,執行update的時候報錯
- 2.代碼最終執行結果出現了 -1
- 3.最終update操做以後,available變成了一個很大的數目
爲何會出現這三種狀況呢?數據庫
我想在學習開始學習計算機的時候都講過計算機的加減法計算方法。後端
思考一下,無符號2 減去 無符號3 在計算機中的運算是什麼樣的? 2 - 3 = 2 + (-3) 假設咱們的計算機是4位的,2的補碼錶示:0010,-3的補碼錶示爲1011 那麼加和的結果是 0010 1011 + ------ 1111 = 1111解釋爲有符號數是多少呢? -1 1111解釋爲無符號數是多少呢? 15
因此呢? 安全
若是mysql不作任何處理的話,你的無符號數減法的結果不會報錯,最終你算出來的庫存仍是一個很是大的值(可怕)。
可是幸運的是mysql 後來的版本幫你作了這件事情(具體哪一個版本我也不清楚),因此若是是mysql作了無符號檢測的話,若是減出的結果是負值,會報錯,這是大多數人期待的結果。
-1這種狀況是須要你設置一下sqlmode的,這也是會出現的狀況。併發
解法總結
- 這個辦法不少人用的時候沒問題,那隻能說明多是機緣巧合,可是對於業務代碼而言,不能靠碰運氣,須要消除不肯定性、縮小遷移成本。
- 若是你想採用這種辦法,辛苦你把大家msyql相應的版本及配置搞清楚,肯定無符號在你所在的版本會出現什麼結果。
select for update
解法釋義
讀取時候就開始加排他鎖也是網上常見的辦法之一,具體實現以下:
begin tran ;
num = select avaliable from goods where id = xxx for update;
if (num >= 0){
affectNum = udpate goods set available = available - 1 where id = xx ;
commit ;
return affectNum ;
}else{
rollback ;
}
該解法在用戶讀取的時候對相應的數據加排他鎖,保證本身在更新的時候該行的數據不會被別的進程更改.全部寫請求及排他鎖加鎖都會被阻塞。
想一想這樣的狀況,A進程執行過程當中,出現死機的狀況致使commit/rollback請求沒有被髮送到mysqlserver,那麼全部請求都會鎖等待。
解法總結
- 低流量能夠採用這種辦法來保證數據的安全性
- 性能低下,平均須要發送4次mysql請求,同時會形成全部同類請求鎖等待。
常見問題
- select for udpate 須要在顯式的指定在事務代碼塊執行,否則不會起做用。不少網友都理所固然的人爲select for update直接就能夠加排他鎖
- 排他鎖的釋放是在rollback/commit 動做完成纔會釋放,不是在update操做以後。mysql innodb執行兩段鎖協議,加鎖階段只加鎖,解鎖階段只解鎖。
採用事務,先查後寫再查,確保沒問題
解法釋義
這時候的available設置爲有符號類型,解決方案一的問題
begin tran ; num = select available from goods where id = xx ; if(num > 0){ //實際須要關心這裏的返回值,這裏不考慮 udpate goods set available = available - 1 where id = xx ; num_afterupdate = select available from goods where id = xx ; if(num_afterupdate < 0 ){ rollback ; }else{ commit ; } }
這種解法區分於第一種的辦法在於,加了事務、available類型更改、採用了更新後確認的形式,嘗試解決問題。
咱們都知道數據庫的事務隔離級別有4種:
RU,RC,RR,Serializable。
咱們常見的innodb中RR模式是能夠保證可重複讀,意思是在同一個事務內部,屢次讀取的結果是一致的。那麼最後一次的讀取對於RR隔離級別其實是無效的。
RC模式下,這個代碼是可用的,每次請求能夠確保本身的進程不會超發。
解法總結
- RR、RC模式下結果不一致.RR下不可保證安全、RC能夠。
- 性能不高,一次業務請求到mysql的轉化爲 1 : 5。
- 這種解法就像老奶奶鎖門,老是不放心本身到底鎖了沒有,走了幾步再回來看看,實際上有些時候是徒勞。
update語句增長available查詢條件
解法釋義
udpate goods set available = available - 1 where id = xx and available - 1 >= 0 ;
你們有的另外一個誤區是單條語句不是事務,實際上單條sql也是一個事務。
問題的關鍵就集中在怎麼證實這句的安全性的。
咱們都知道update操做對於id爲主鍵索引的狀況下,是會對數據加行鎖。
其實update操做在mysql內部也是一個先查後改的過程,這個過程若是是原子的,那麼能夠保證update語句是串行的,那咱們就來看一下update語句在mysql內部的執行過程。
那麼對於上面這個語句,同樣遵循兩段鎖協議。
update執行的過程,會去查詢知足條件的行並加鎖,這個加鎖是innodb作的,那麼就能夠保證別的事務必須等到該事務執行完了以後才能得到鎖,此時拿到最新數據。
解法總結
- 語句安全、效率最優(個人認知裏)
採用設置庫存而不是扣減庫存
這幾天我把相似的文章幾乎翻了一遍,惟一看到批評個人上一條作法的是個人那個作法是不具有冪等性的。
- 所謂冪等性就是,同一個用戶對同一鏈接的訪問不會產生反作用。好比上一條的方案,若是記錄用戶的操做和扣減庫存不是原子操做的話,就有可能出現的問題是,庫存扣減成功了,可是用戶記錄失敗了,那麼用戶重複請求,就會出現屢次減庫存的問題。
那麼他們的解法是這樣的,採用設置而不是扣減,代碼以下:
num_old = select available from goods where id = xx and available >= 1 ;
num_new = num_old - 1 ;
update goods set num=num_new where id=xx and num=num_old ;
這段代碼也是安全的,採用的是樂觀所的理念來完成的操做。
總結
- 上面的作法,最後兩個是相對安全的,可是你的庫存字段仍是要設置爲無符號,關因而否冪等,要看結合請求看,不是單個扣減塊代碼。
- 較真是一個學習的過程,只有較真才能把這些概念搞清楚。若是你須要徹底弄懂這些內容,可能你須要對mysql鎖、事務、mvcc這些概念都作一下預習。
- 感謝工做過程當中小夥伴們的努力,讓咱們把問題追查的更清楚。
引用
- 何登成的技術博客mysql udpate流程學習
- 冪等性作法來源使用設置庫存代替庫存扣減
- mysql 無符號問題
固然這個過程當中我也是看了一些比較經典的mysql書籍,也推薦你看一下:
1.《高性能MySQL(第3版)》 https://u.jd.com/DyajuZ
2.《MySQL技術內幕:InnoDB存儲引擎(第2版)》 https://u.jd.com/BsVLNm
有什麼問題均可以掃碼一塊兒交流,這篇文章很早以前在本身搭建的一個博客地址上寫的,由於coidng.me 的域名再也不使用了,因此沒法維護。