mysql商品庫存扣減問題總結

文章討論內容

秒殺類的問題一直都是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執行過程

那麼對於上面這個語句,同樣遵循兩段鎖協議。
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書籍,也推薦你看一下:

1.《高性能MySQL(第3版)》       https://u.jd.com/DyajuZ

2.《MySQL技術內幕:InnoDB存儲引擎(第2版)》  https://u.jd.com/BsVLNm

 

 

有什麼問題均可以掃碼一塊兒交流,這篇文章很早以前在本身搭建的一個博客地址上寫的,由於coidng.me 的域名再也不使用了,因此沒法維護。

相關文章
相關標籤/搜索