1. 程式人生 > >如何應對併發-關於資料索引

如何應對併發-關於資料索引

從整體架構來說分很多部分,比如常見的,儲存層的i/o優化,網路層負載均衡,通訊層的連線池等等,不過我這裡不講這些。不講這些的原因第一呢,是這些我基本都不太會;第二呢,是在實踐過程中發現,特別是創業公司,中小企業,一般最容易出問題,也是最難處理的,往往是資料庫方面的問題。
非技術人員往往會認為,負載高了,請求多了,加伺服器加硬體不就完了? 如果是隻是應用程式處理,常見的負載均衡方案很成熟,加加硬體的確可以快速,有效的分擔負載,提高支撐能力;但是且慢,通常到了資料庫這裡,如果你前期設計不合理,或者對這類問題考慮不全,那麼,加硬體,很遺憾,是沒用的。
我們常說運維中要關注所謂的單點隱患,什麼是單點隱患呢?就是這個點一旦崩潰,無法實現自動的災難容錯響應,從而導致全盤崩潰。一般比如說web伺服器,負載均衡輪詢,一臺出問題了,系統會自動將負載轉移到其他伺服器,那麼資料庫可以不可以呢?其實不是不可以,但是就比較需要做好設計,否則很可能直接就死在這個環節上。而發展趨勢不錯的創業公司死在資料庫的併發能力上的案例,可以說,比比皆是。

今天的第一課,我們先要對資料索引和查詢效率有個基本認識,連基本優化都做不好去講什麼架構是沒意義的。
第一個問題,為什麼一條查詢語句,使用了資料索引會提高效率?
以及,通過一條SQL語句,能否估算出其執行開銷和最佳索引選擇?
熟悉資料結構同學大概知道,一般資料庫的索引大概是btree,b+tree,類似這樣的結構,那麼現在非關係型資料庫特別流行,也就是所謂的key-value資料庫,最求極端效率,通常是 hash結構的資料索引。但其實這些,我認為對於我這樣笨的人來說,通常,只需要理解最基本的概念就行,最基本的是什麼呢?就是資料索引提供了一種有序,在有序的情況下,進行檢索,二分法效率最高,n條記錄中定位查詢開銷是 log2(N),(hash索引效率更高,但不提供關係型查詢,應用場景比較受侷限)。 那麼所謂的btree結構也好,或其他的類似結構也好,把握一個原則,接近二分法的查詢效率,因為如果做一個完全有序的佇列,那麼插入,刪除,修改需要做的操作開銷太大了,大家可以思考一下,所以才會有人設計樹形結構,兼顧查詢和更新操作。理解這一點對理解整個資料查詢效率和索引結構,幫助極大。
簡單複習就是,查詢效率的關鍵是有序,二分,反過來理解就是,無需遍歷所有資料,即可實現快速的定位。
這裡就引出了一個特別經典的題目,ip地址反查。
應用場景非常常見,你上一個什麼旅遊訂票的網站,社群,或者上百度,該網站都希望立即知道你的地理位置,從而基於你的位置定向投放內容,比如當地酒店,或者當地的本地廣告。網站一般是獲取使用者的ip地址,然後在ip->地區的對應表裡去查詢比對,通常,ip - 地區的對應表,有大約十萬到數十萬條記錄(看地區粒度),格式是 ipstart, ipend, area 這樣的資料結構。如果用純粹的SQL查詢是

select * from iparea where $ip between ipstart and ipend;

在早期mysql及大部分資料庫是不支援between and 中使用索引的,據說最新版本已經提供了支援,但是最近幾年沒有從事技術,沒有測試,不知道效率如何,那麼在早期,如果資料查詢,這樣一條SQL,無法使用索引,就要遍歷所有結果,這個開銷是不能忍受的,(雖然不用1秒就可以執行出結果,但是開銷依然比較大,一秒鐘可以處理的查詢最多幾十次,而我們的要求是,一秒鐘幾千次!)
那麼這個問題的特點是什麼呢,ip地址區間表並不是經常變化的,比較固定,那麼在這種情況下,其實不用資料庫都可以,一個完全排好序的佇列放在記憶體裡,程式用二分法來查詢,每秒種處理幾千個非常輕鬆(這程式不用教了吧),當然,其實還有更極端效率的處理途徑,這裡不展開,有興趣的同學自己思考。

第二個問題,從一個常見SQL如何確定索引的構成
以下所有案例均以mysql 為例,原因是,這個我熟悉。
非mysql可能部分語法不同,但邏輯和思路相同。
發現有一個簡單問題很多人會答錯,一個SQL可以用到幾個索引?很多人會說是多個,其實是一個,目前一些第三方的資料引擎似乎開始支援一條SQL使用多索引了,比如我前幾天看淘寶公開的那個開源資料結構的文件,從官方部落格的描述中似乎有這樣的提法,但是我最近確實很懶惰也脫離技術,所以沒有去測試和仔細研究,這個留給有興趣的同學吧,我還是回頭說通常,我們用mysql或其他常見資料庫的,一個查詢只能用到一個索引;但是這裡要強調的是,一個索引可以用到多個欄位,也就是所謂的複合索引。
那麼,按照剛才提到的,基於有序這個概念,如何理解索引的使用和效率呢?特簡單,你就把索引當作是一個有序數列放在腦子裡,然後思考這個SQL,這個條件子句和排序子句,能否在這個索引的連續範圍內精確命中結果,也就是所謂索引命中率高,這個查詢就效率高,如果無法在索引這個有序數列連續範圍內精確命中,查詢效率就不高。
那有人說了,索引並不是真的有序數列啊,我說的是一種模擬的思考方式,這樣思考效率最高,當然,必須案例說話。
比如一個社群,我希望使用者進來,就能看到本地的使用者,當然,是最新線上的,否則都是死使用者就無法交流了。

SQL select * from user where area='$area' order by lastlogin desc limit 30;

(這個 limit 特別重要)
稍微懂一點索引的同學都應該知道,正確的索引是area+lastlogin 複合索引,那麼,我們把這個思考方式推演一下。
如果只把area當作索引會怎樣,資料庫會把符合這個area的所有結果拿出來,然後按照lastlogin排好序給你,這樣就要遍歷所有符合這個area的使用者記錄;
如果只把lastlogin作為索引會如何,我們想象,lastlogin是一個有序的數列,資料庫會從最後一條開始往前挨條遍歷,每條都去比對area是不是符合查詢條件,直到數出30條,遍歷結束,請注意,不是全部遍歷,在這裡,如果area 是個熱門城市,比如上海,北京,可能遍歷200次左右就出結果了,效率很快,但如果是個冷門城市,可能要遍歷幾千條几萬條結果,甚至全部資料表遍歷都湊不出符合條件的30條。這樣效率就要命了。 所以用lastlogin為索引,效率存在風險。
那麼兩個我都建立索引呢?這個mysql只會選擇一個索引,我記得不同資料庫版本的選擇策略都不同(實戰中遇到過測試伺服器用的索引很正確,線上伺服器使用了錯誤索引,因為資料庫版本不同),所以我給不出肯定的答案,但是有一點,兩個索引沒有意義,都不是最優解。
那麼如果把lastlogin+area建立索引呢?你們設想一下,兩個欄位拼在一起,作為有序數列,然後資料庫去查詢的時候,lastlogin+area,這時候area是沒用的字尾,在排序中根本體現不出他存在的意義,和單獨lastlogin索引是完全一樣的。
而area+lastlogin呢,把兩個欄位拼接然後排好序後,看這條SQL在這個數列中查詢的體現,所命中的完全是連續的30條,也就是資料庫只遍歷30條索引記錄即完成搜尋,效率最好。
這段有點囉嗦,如果不理解,建議多讀幾遍,理解這個思路,對理解索引的效率幫助特別大,我剛工作的時候寫SQL也是瞎寫,對索引一知半解全靠蒙,有了這個概念後豁然開朗,從此對索引效率的認識精進了一大截,我看網上各種索引優化的教程,各種規律總結,其實你把這個認識達到了,那些規律基本上不用記,都淺顯的如1+1一樣。
理解如上思路,就能一併理解如下策略
A+B索引可以替代A索引,而不能替代B索引。
where key like ‘keyword%’ 可以用到key 索引
where key like ‘%keyword%’ 不能用到key索引
我很笨,所以我的理解方式都是基於中學生知識基礎的思路,如果您有更好的理解思路,也可以忽略本文。
第三個問題,如何評估SQL的執行開銷
剛才提到一個重要的概念,就是索引中遍歷的記錄越少,效率越高,遍歷的記錄越多,效率越差。 在慢查詢日誌或者explain分析中,一個重要的指標是 affected rows,(好像也有別的叫法,不查證了,大家應該能知道我說的是什麼),這個就是索引遍歷的記錄說,我以前硬翻譯叫做影響結果集,我後來看其他人寫的資料庫文件叫索引掃描行數,概念是一樣的。
那麼,要強調一點,一條查詢語句,其執行開銷,在大多數情況下,與影響結果集,也就是索引掃描行數,呈線性相關,舉兩個常見經典資料優化的問題案例。
經典案例1,大翻頁問題
論壇社群常見,翻頁越靠後效率越低,很多論壇本身訪客到沒事,訪客不太會翻幾百頁幾千頁,但是被搜尋引擎蜘蛛抓取的時候,因為連續抓取大翻頁,導致資料庫崩潰,這案例太多了,很多站長為此鬱悶莫名,不知所措。
案例SQL如下
按最新更新的板塊第一頁帖子

select * from post where boardid=$id order by lastupd desc limit 0,30;

按最新更新的板塊第100頁帖子

select * from post where boardid=$id order by lastupd desc limit 3000,30;

這兩個SQL 看上去只有limit有區別,索引都是boardid+lastupd (不要搞錯順序,理解一下)
但第一條SQL索引只掃描30行;第二條SQL索引掃描了3030行,其效率是第一條SQL的1/100.
搜尋引擎的蜘蛛抓取 大翻頁就是 這樣把論壇搞死的。
經典案例2,積分排行問題
比如很多小遊戲提交成績,告訴你排名全球多少名,有印象吧。
這個問題我依稀記得雲風大神吐槽過,好像曾經陌陌有一款遊戲在這裡有非常嚴重的效能問題,被他狠狠BS了一把。
案例SQL如下

select count(*) from gamescore where gameid=$gameid and score>'$score' ;

索引怎麼建?
gameid+score複合索引,順序不能錯,為什麼,按照上面說的思路,自己思考一下。
那麼這個效率怎麼評估?
看結果,如果你遊戲成績特別好,前幾名,前幾十名,你的結果就是索引掃描行數,(如果索引都設計錯了那就不要提了)。
如果你的遊戲成績很爛,幾萬名,幾十萬名,那麼索引掃描了幾萬條,幾十萬條,就效率非常低了,如果有一批人同時在提交成績,又都是這種幾萬名,幾十萬名的使用者,資料庫非崩潰不可,你再多伺服器也白搭。
所以,常見的解決方法是,積分排行只針對最靠前的使用者提供,後面只給估算或區間了。
當然,這裡有個終極方案,用redis的有序陣列結構,一勞永逸的解決這個問題,redis四種資料結構,各有所長,有興趣的可以深入研究一下,今天這裡不展開。
第四個常見問題,MYSQL 分析和優化的方法
剛才我說了索引掃描行數,或者說影響結果集,對查詢效率的影響極大,那麼有人說了,怎麼證明呢?
給大家一個日常SQL分析和自我測試的方法。
首先,你一條SQL如果執行很慢,你用explain 解析一下,看看是否影響結果集很大,這是其一。
其二,對這條很慢的SQL做一個狀態拆解,在mysql中是這樣操作的。
set profiling=1;
執行問題SQL;
show profile for query 1;
通常,如果這個問題SQL確實是索引出了問題,也就是影響結果集,或者說索引掃描行數較多,那麼他的執行狀態最多的消耗就在 sending data這個狀態上,這個狀態不要被名字騙了,其實負載是在i/o,硬碟掃描上。
你測試的時候就可以看,影響結果集的數字,和sending data上狀態的開銷,是不是線性相關,對一個複雜的資料表結構,匯入上百萬條記錄,然後用不同索引方式和不同SQL查詢,利用 explain 和set profiling 這些操作反覆分析SQL的影響結果集和開銷構成。結合我今天說的思考方式,就可以更好理解了。