今天本來是想做一個URL排重的東西,當然url排重常用的是md5方式,為了做測試,我做造了100萬的URL在數據庫之中,然後,想把這些URL從數據庫,加載到內存之中,可是問題來了.
問題如下:
1 首先數據庫肯定要分頁,不可能把100萬的數據,一次全部讀到內存之中,因此一定要分頁,google了一次,常用的分頁方式:如下的例子
select id,url from urlmd5 where siteid=0 limit 500 ,代表取siteid為0的前500行記錄.
如果要取第500,1000記錄的sql為:
select id,url select id,url from urlmd5 where siteid=0 limit 500,500,前面的500代表偏移量,後面的代表取的長度,不是limit500,1000,開始我就犯了這個錯誤,呵呵!
2 分頁之後,需要將這些URL的md5值,加載到內存之中,100萬的md5值加入內存之中,又是一個問題,我用的java,如果使用常用32的字符串,那麼,Java的一個字符是2個字節,也就是一個md5的字符串至少需要64個字節,那麼100萬字符串的內存為,64*100000/1024/1024=61M的內存,這個有點大,我的設計是千萬級URL,所以,拋棄了這種方式,使用了字節數組,md5的另一種表達方式為16個字節的數組,可以由16個字節生成32的md5字符串,這樣一來,就節省了1/4的內存,也就是100萬的URL需要15M內存.1000萬需要150M內存,1億的URL也只需1500M內存,估計就是2G內存,足夠了,這個結果是比較滿意的.
因此,我打算采用堆的方式來存儲與查找上面的16個字節的md5值,一次從數據庫采用分頁方式記讀取500條URL到內存,可是發現一個問題,剛開始的時候,還很快,只有5ms左右,越到10萬左右就要400ms,並且越來越慢,我一次只能取500條,還不到10萬就快要1秒了,啥時才能讀完?因此,覺得這個查詢有些問題.
在google上google了半天,有人說分區表,這個我不太滿意,才100萬就要分區,太小了,還有人說,這就是MySQL的性能瓶頸,discuz在100萬的數據之後,查詢後面的數據也會非常慢,因此,為了防止這種情況,他們一般只提供前面的分頁,後面的就不提供了.
(這種也許是這樣,但是說了等了沒有說,不解決我的實際的問題)
功夫不負有心人,1,2小時之後,終於google到一篇有用的文章,以下為鏈接:
http://forums.MySQL.com/read.PHP?24,112440,112461#msg-112461
有一個兄弟與我是同樣的問題,百萬級的數據,在查找後面的數據之後,比如:
select id,url select id,url from urlmd5 where siteid=0 limit 88500,500
查到後面要8秒左右,這個速度令人抓狂,下面摘取回復的一段:
If you use LIMIT and OFFSET like this, the way it works is that MySQL processes the query as if you'd said (in your case) LIMIT 2501000 then just throws away (i.e. doesn't return to you) the first 2500000 rows. It's really not an efficIEnt way to do things at all.
If you have a condition that you can specify in a WHERE clause that will use an index to identify the required rows then you'll see really huge speedups. (So yeah using an auto increment rownumber col and WHERE would work very nicely)
上面的意思是說,我們使用offset的時候,也就是上面的limit 88550,500,首先數據庫會遍歷所有的數據,然後,拋棄前面的88550行數據,只返回剩下的500行數據,所以,offset越大,拋棄的就越多,速度自然就越慢.
他提供的解決的方案是:在where條件之中,加一個限制條件,比如id大於多少,通過這個條件,就可以不用拋棄前面的數據,然後,直接返回後面的數據.(great idea!)
因此,針對我的這種情況,
select url from urlmd5 where siteid=0,siteid=0是一個限制條件,一定要加上,我每次只需要url,我需要加上這個大於的條件?
於是我這樣寫:
select id,url from urlmd5 where siteid=0 and id>500 limit 500.
我的想法是,每一次我可以找到500條記錄之中最大的id號,把這個id號保存起來,下一次分頁查詢的時候,將這個id號帶過去,比如,對於上面的這個例子:
我會得到500行記錄,然後,我取出每一行的id,取得最大的id號,比如1000,然後,在下一次再取值的時候,再把這個id號放在條件之中.也就是如下:
select id,url from urlmd5 where siteid=0 and id>1000 limit 500.
這樣,我可實際上面的需求了,這樣不就沒有偏移量了,速度就應該快了.
可是,速度並沒有快,還是差不多.只是提高一點點,只是沒有越到後面越大的這種情況,但是仍然很慢,為什麼呢?
我知道是索引的問題,我的索引是(siteid,id)沒有問題,我先排按siteid,再按id號.這樣應該沒有什麼問題啊,用MySQL的查詢計劃:
explain解釋,發現sql是用到索引了,但是仍然慢,為什麼呢?難道要加上url也要加上:
反復的試驗,over and over and over again。
終於,明白了,索引的順序錯了,應該首先是id,然後再是siteid.
原因是:
索引的基本是b樹的結構,而建索引最基本的一條准則就是要把類別多的列,放在第1位,然後基次,因為把類別多的列放在第1位,那麼整個的bt的第一個分層,就會很大,從而,整個b樹的高度,就會降低,而樹的高度決定了查詢的速度.
因此,最後的索引是(id,siteid)
每一次查詢的時間是,從開始到最後都是8ms到10ms之間,100萬的URL從讀取到最後加入到堆之中,建堆的時間為:31.948秒.
perfect !
以下為最後的一段展示:
2010-04-25 15:59:40,695 [com.winkee.barspider.core.MD5Wrapper.loadMD5(MD5Wrapper.Java:103)] INFO [com.winkee.barspider.core.MD5Wrapper] - <data size=500>
2010-04-25 15:59:40,695 [com.winkee.barspider.core.MD5Wrapper.loadMD5(MD5Wrapper.Java:106)] INFO [com.winkee.barspider.core.MD5Wrapper] - <http://www.test.com/urlindex=999500>
2010-04-25 15:59:40,697 [com.winkee.barspider.core.MD5Wrapper.loadMD5(MD5Wrapper.Java:120)] INFO [com.winkee.barspider.core.MD5Wrapper] - <query time=7 insert time=3>
2010-04-25 15:59:40,698 [com.winkee.barspider.core.MD5Wrapper.loadMD5(MD5Wrapper.Java:122)] INFO [com.winkee.barspider.core.MD5Wrapper] - <sql=select id,url from urlmd5 where id> after size=1000000>
2010-04-25 15:59:40,698 [com.winkee.barspider.core.MD5Wrapper.loadMD5(MD5Wrapper.Java:136)] DEBUG [com.winkee.barspider.core.MD5Wrapper] - <siteid=0 load 1000000 url load time=31948>
可以看見query time即使到了最後是也仍然是7ms.