血通常的教訓,請慎用insert into select。同事應用以後,致使公司損失了近10w元,最終被公司開除。java
事情的原由
公司的交易量比較大,使用的數據庫是mysql,天天的增量差很少在百萬左右,公司並無分庫分表,因此想維持這個表的性能只能考慮作數據遷移。mysql
同事李某接到了這個任務,因而他想出了這兩個方案sql
- 先經過程序查詢出來,而後插入歷史表,再刪除原表
- 使用insert into select讓數據庫IO來完成全部操做
第一個方案使用的時候發現一次性所有加載,系統直接就OOM了,可是分批次作就過多io和時間長,因而選用了第二種方案,測試的時候沒有任何問題,開開心心上線,而後被開除。數據庫
到底發生了啥,咱們覆盤一下
- 先來看第一個方案,先看僞代碼
// 一、查詢對應須要遷移的數據 List<Object> list = selectData(); // 二、將數據插入歷史表 insertData(list); // 三、刪除原表數據 deleteByIds(ids);
咱們能夠從這段代碼中看到,OOM的緣由很簡單,咱們直接將數據所有加載內存,內存不爆纔怪。網絡
- 再來看看第二個方案,到底發生了啥
爲了維持表的性能,同時保留有效數據,通過商量定了一個量,保留10天的數據,差很少要在表裏面保留1kw的數據。因此同事就作了一個時間篩選的操做,直接insert into select ... dateTime < (Ten days ago),爽極了,直接就避免了要去分頁查詢數據,這樣就不存在OOM啦。還簡化了不少的代碼操做,減小了網絡問題。性能
爲了測試,還特地建了1kw的數據來模擬,測試環境固然是沒有問題啦,順利經過。考慮到這個表是一個支付流水錶,因而將這個任務作成定時任務,而且定在晚上8點執行。測試
晚上量也不是很大,天然是沒有什麼問題,可是次日公司財務上班,開始對帳,發現資金對不上,不少流水都沒有入庫。最終排查發現晚上8點以後,陸陸續續開始出現支付流水插入失敗的問題,不少數據所以丟失。最終定位到了是遷移任務引發的問題,剛開始還不明因此,白天沒有問題,而後想到晚上出現這樣的狀況多是晚上的任務出現了影響,最後停掉該任務的第二次上線,發現沒有了這樣的狀況。code
覆盤
- 問題在哪裏?
爲何停掉遷移的任務以後就行了呢?這個insert into select操做到底作了什麼?咱們來看看這個語句的explain。 咱們不難從圖中看出,這個查詢語句直接走了全表掃描。這個時候,咱們不難猜測到一點點問題。若是全表掃描,咱們這個表這麼大,是否是意味着遷移的時間會很長?倘若咱們這個遷移時間爲一個小時,那是否是意味着就解釋了咱們白天沒有出現這樣問題的緣由了。可是全表掃描是最根本的緣由嗎?blog
咱們不妨試試,一邊遷移,一邊作些的操做,還原現場。最終仍是會出現這樣的問題。這個時候,咱們能夠調整一下,大膽假設,若是不全表掃描,是否是就不會出現這樣的問題。當咱們將條件修改以後,果真發現沒有走了全表掃描了。最終再次還原現場,問題解決了 索引
得出結論:全表掃描致使了此次事故的發生。
這樣作就解決了發生的問題,可是作爲陸陸續續開始失敗這個就很差解釋了。
- 緣由
在默認的事務隔離級別下:insert into a select b的操做a表示直接鎖表,b表是逐條加鎖。這也就解釋了爲何出現陸續的失敗的緣由。在逐條加鎖的時候,流水錶因爲多數是複合記錄,因此最終部分在掃描的時候被鎖定,部分拿不到鎖,最終致使超時或者直接失敗,還有一些在這加鎖的過成功成功了。
- 爲何測試沒有問題?
在測試的時候充分的使用了正式環境的數據來測試,可是別忽視一個問題,那就是測試環境畢竟是測試環境,在測試的時候,數據量真實並不表明就是真實的業務場景。比方說,這個狀況裏面就少了一個遷移的時候,大量數據的插入這樣的狀況。最終致使線上bug
解決辦法
既然咱們避免全表掃描就能夠解決,咱們避免它就好了。想要避免全表掃描,對where後面的條件作索引,讓咱們的select查詢都走索引便可
insert into還能用嗎?
能夠
總結
使用insert into select的時候請慎重,必定要作好索引