前文提要
承接前文《一次線上Mysql數據庫崩潰事故的記錄》,在文章中講到了一次線上數據庫崩潰的事件記錄,建議兩篇文章結合在一起看,不至於摸不着頭腦。
由於時間原因,其中只講了當時的一些經過以及我當時的一些心理活動,至於原因和后續處理步驟並沒有在文章中很清晰的寫出來,以致於很多朋友說看得不清不楚的,這里向他們道個歉,主要是上周真的沒有足夠的時間將兩篇文章同時准備好,不然也不會草草結尾了,而且上篇文章中主觀因素占了較大的比重,因為回憶起這件事的時候確實有很多想法,因此顯得有些個人化、日記化了。
這篇文章就不再講述事件經過了,主要是把事件的原因和后續的處理步驟整理好。
憶往昔 1
有張圖,是后來老大發給我的,能夠看出當時的數據庫情況:
這是數據庫宕機后的實例信息,基本癱瘓了,至於事務鎖相關的截圖,當時沒有保存,因此就無法放在文章中了,有朋友給我留言問當時為什么不直接kill掉鎖住的進程,我回答是因為我不懂這個知識點,我找了一下那幾天的日記確實有這方面的記錄,是當時老大發我的幾條命令:
show processlist;
//找到鎖進程
kill id ;
應該也是做了這些操作的,但是看到上面那張圖也應該知道為什么這些都不管用了,事務鎖確實存在,導致了部分表無法正常操作,但是主要原因還是因為數據庫資源被消耗光了,即使kill了相關的進程也無法解決。
入庫功能介紹
已經定位到入庫是發生這次事故的主要原因,那么為什么頻繁的操作會導致這件事的發生呢?
首先來介紹一下當時的功能設計改動及涉及到的SQL語句。
表結構設計及功能設計
入庫功能中涉及到的表和實體:
- 倉庫信息(tb_storehouse)
- 貨架信息(tb_shelf)
- 格子信息(tb_shelf_grid)
- 商品信息(tb_product)
- 商品位置信息(tb_grid_product)
- 入庫信息(tb_store_in_record)
釋義:
- 由於有多個倉庫,因此倉庫也獨立做了一張表;
- 一個倉庫中有多個貨架,tb_storehouse與tb_shelf是一對多的關系;
- 一個貨架中有多個格子(貨架規格不同,有的是8個有的是4個),tb_shelf與tb_shelf_grid也是一對多的關系;
- 商品信息,以商品碼作為主鍵,還有其他屬性,但是與入庫信息無關就沒有羅列出來;
- 商品的位置信息就是一件商品是在哪個格子上,表結構的設計就是四個字段:id,商品碼,格子id,number(數量),存儲了兩個屬性的主鍵id和數量值;
- 入庫記錄信息,就是哪件商品在哪個時間點由哪個入庫員在進行入庫,涉及到的字段有:product_id,grid_id,operator_id,create_time,還有其他字段但是與入庫操作無關聯就不列舉出來了。
入庫功能 V1.0
在最初的版本中,入庫功能的設計較為簡單和清晰,入庫操作只做一件事,就是入庫,頁面邏輯也比較簡單:進入后台-->倉庫管理-->貨架管理-->格子管理-->點擊入庫按鈕-->入庫,進入想要入庫的格子頁面點擊入庫,這時頁面會彈出一個彈框,上面有一個input框,然后入庫員用掃描槍掃描商品碼即可完成入庫,入庫過程中input框為不可選中狀態,成功后方可進行下一次入庫,這種方式入庫也挺快的,入庫員找到想要入庫的格子然后一直掃碼就可以了。
在最初的版本中需要執行的SQL語句有:
- 根據商品碼查詢商品,為空則報錯並提醒需要完善商品SKU;
- 查詢格子信息,為空則報錯;
- 查詢位置信息,如果已存在,則執行數量number加一,不存在則執行新增操作;
- 添加入庫記錄信息,完成入庫,返回。
共計4條SQL即可完成此一次入庫操作,第一個版本是開發自己設計的,功能很明確,不會有其他操作和查詢。
入庫功能 V2.0
這個版本就是引起數據庫崩潰的版本,改動原因很簡單,新的"產品經理"覺得入庫頁面太丑,沒有美感,因此要求重做,新的功能如下:進入后台-->倉庫管理-->貨架管理-->格子管理-->點擊入庫按鈕-->入庫-->刷新頁面-->點擊入庫按鈕-->入庫,新的頁面改動如下,原來的格子列表頁只會顯示格子的信息,但是新頁面不同了,要顯示格子上有什么商品,商品數量是多少,以及在此倉庫中共有多少此商品,分頁列表顯示,新的功能中要求在入庫后執行頁面刷新操作,讓入庫員可以看到變化。
看到這里,你可能覺得不妥或者不合理的地方,暫時先保留一下意見,我們來看一下新的改動執行了哪些SQL語句:
- 根據商品碼查詢商品,為空則報錯並提醒需要完善商品SKU;
- 查詢格子信息,為空則報錯;
- 查詢位置信息,如果已存在,則執行數量number加一,不存在則執行新增操作;
- 添加入庫記錄信息;
- 查找位置信息列表("產品經理"要求一頁20條數據);
- 根據20條記錄中的商品碼查找對應的真實庫存,完成入庫,返回。
因為查詢真實庫存需要另外執行SQL語句,因此新的功能一次入庫操作需執行的sql共計25條,除了第一個版本中的幾條SQL外,這次功能改動加的SQL語句都是復雜SQL。
答疑
說明:由於牽涉到公司的一些業務,因此入庫操作在文章中被簡化了,實際的入庫操作比文章中描述的過程要復雜一些,當時的功能改動比這個更為糟糕,本來的入庫操作是執行大概6條SQL,但是在功能修改后,一次入庫要執行60多條SQL。
- Q:為什么要這么設計?
- A:"產品經理"覺得好看。
- Q:倉管需要這種設計嗎?
- A:"產品經理"覺得倉管是傻子,不用管他們的想法。
- Q:為什么要商品所有的真實庫存數據?
- A:"產品經理"覺得需要全局統籌規划。
- Q:頁面好看了但是功能麻煩了,為什么還要這么做?
- A:"產品經理"覺得倉管事情多一點無所謂。
- Q:開發人員針對每個問題反駁了嗎?
- A:"產品經理"說要做,反駁沒用。
這種設計其實一眼就知道多此一舉,后來跟倉管私聊,他們也氣得罵娘,而且在收到流程圖就明確問了關於所有真實庫存的問題,但是沒辦法,不做不行啊,原本一次入庫可能也就一秒鍾不到的時間,新功能一出來,有時可能需要3-4秒鍾設置更長時間才行,而且還導致了這次事故。
崩潰原因
通過前文的描述,大致也能夠知道是什么原因導致了數據庫的崩潰,我們公司有一位女黑客!哈哈哈,這個是開玩笑的。
入庫操作由原來的6條SQL執行語句增長為60條SQL執行語句,入庫時長也隨之增長,而且這60條SQL語句中關於查詢真實庫存的SQL也比較復雜,用到了多表聯查及函數操作,性能也比較差,而當天的入庫操作也比較密集,因此數據庫承受了比原來要重n倍的負荷!資源被逐步耗盡也就不奇怪了。
崩潰原因總結如下:
- 一個業務功能執行了太多的SQL語句,此功能在短時間內又會被多次調用。
- SQL語句中有復雜語句,比如用到了一些函數,比如多表聯查,大數量的SQL語句加上復雜SQL語句無疑是雪上加霜。
- 數據庫連接池的選擇和設置問題,導致出現了大量的數據庫連接。
- service層的代碼不規范,select語句也加了事務,增加了一些不必要事務的開啟和關閉,增加了myslq數據庫的開銷。
- 部分表沒有加索引,或者說索引不完整,導致了慢SQL的出現。
原因列舉了這么多,事務出了問題、索引不規范導致查詢出了問題、慢SQL的出現、數據庫連接爆表,一環扣一環,一個問題牽連着一個問題出現,但是這些其實都不是主要的問題所在,第一環並不是這些,最主要的原因還是功能設計的極不合理,導致短時間內執行了巨量的SQL語句,進而將所有的不足之處都暴露出來,最終將問題引爆,一般情況下,慢SQL和復雜SQL語句並不會拖垮數據庫,即使沒有索引,也只是查詢返回時間會多一些,不可能導致整個應用崩潰掉。
其實問題是多方面的,不僅僅是因為這次功能改動,雖然這次改動是導致問題的主因,但是代碼不規范,表結構優化不到位,慢SQL沒有處理,這些問題還是存在的,即使這次由於倉管流量的增加沒有導致數據庫崩潰,說不定下一次商城流量增加或者其他頁面流量增加也會打垮數據庫,因此,功能修改也是全方位的動刀,而不僅僅是回退版本就行了。
后續處理
讓老板換女朋友是開玩笑的。
后續處理的步驟比較多,總結如下:
- 入庫功能修改,保留頁面設計,功能做改動;
- 數據庫連接池更改;
- 表結構優化;
- 清理慢SQL;
- 業務代碼規范,減少事務開銷;
- Mysql參數修改,印象比較深的是
wait_timeout
參數; - 整合緩存功能。
雖然事故發生讓人很無奈很沮喪,但是看到處理結果再去想想,如果沒有類似這種事故的發生,也不會想着去優化代碼,去優化數據庫,去整合緩存等等一系列的操作,這些不僅讓系統更加健壯,更重要的,是經驗!因此不要害怕出現問題,經驗就是在磕磕碰碰中增加的。
幾年的工作經歷,也讓我漸漸明白了技術的成長離不開一個又一個的錯誤,失敗中雖然有心酸和不甘,但是也不可否認它也帶來了成長,不管是心態的強大,還是效率的提升,經驗也是在一次次事故的產生和解決中積累。未來依然如此,還是會遇到一個又一個的難處。
憶往昔 2
在這次事件中,我也第一次接觸到Mysql宕機,數據庫竟然也能被請求到崩潰,以往遇到的是tomcat服務器被請求擊垮或者服務器流量被打滿,因此關於這件事的記憶比較深刻,可能細節記不太清晰,但是對我的影響還是很大的。這次事件后也是我第一次在項目中用到緩存,這也是為什么在寫緩存整合文章前先寫了這兩篇文章。當然,一開始的整合代碼是老大寫的,后面又學了很久,才一點點的入門,不僅僅是入庫操作,其他的功能中整合緩存對於系統來說也是極有幫助的,在這里,緩存就是負責減輕數據庫的壓力,轉移一部分請求使得其壓力不直接落在數據庫上。
打個不恰當的比方,一個功能執行6條SQL會運行的很好,而執行60條SQL時,一旦操作比較密集就有可能會崩潰,而緩存就可以避免這一點,盡量的分擔掉數據庫的壓力,不用每次請求都去訪問數據庫,就像這次事件中的60條SQL一樣,如果后面的54條SQL語句返回的結果都放入緩存中,也就不會出現這個崩潰的事件了。
因為如今距離事故發生已經有大概兩年的時間,所以可能無法回憶的特別精確,只能根據當時寫的幾篇日記來還原一下事件的經過,不過也僅僅是個大概,畢竟事件發生的時間點離現在有點遠了。其實呢,過程的准確性倒不是特別重要,從這次事件記錄里也能看出當時處理事情的不成熟和稚嫩,沒想到用緩存去處理,因為壓根沒有這方面的概念,這件事情以現在的視角去看待的話肯定可以很快的定位到問題並且快速的處理掉,但是對於當時的我來說,還是很麻煩的,在技術角度來說甚至可以說是一件不可能的事情,一開始聽到鎖表的時候,整個人都蒙了,這是啥,數據庫鎖是什么?表鎖了該怎么辦?
結語
關於線上Mysql數據庫崩潰事故記錄的文章到這里就結束啦!如果有問題或者有一些好的創意,歡迎給我留言,也感謝向我指出項目中存在問題的朋友。
首發於我的個人博客,新的項目演示地址:perfect-ssm,登錄賬號:admin,密碼:123456
如果你想繼續了解該項目可以查看整個系列文章Spring+SpringMVC+MyBatis+easyUI整合系列文章,也可以到我的GitHub倉庫或者開源中國代碼倉庫中查看源碼及項目文檔。