redis設計思想

pfyc2581 8年前發布 | 14K 次閱讀 Redis NoSQL數據庫

來自: http://blog.csdn.net//jiao_fuyou/article/details/32705753


不同于nginx的精雕細琢,redis代碼的風格趨向于簡潔實用。簡潔啟事,下面所述不再列舉任何源碼,不拼湊任何外來資料。去除末枝,下面直入redis主題,盡可能簡潔地描述redis的設計思想。
整體模型:單進程單線程事件驅動模式。
Redis在主處理流程中,采用了單進程接受各種client請求并返回結果,整體處理流程采用事件驅動的方式進行。通過其IO復用的方式監聽aeEventLoop事件,在事件處理的過程中,這種模式對處理函數的要求:進行一次事件處理需要盡可能地快。因此會有以下幾種處理方法:
1.對于非長時間處理事件,直接處理。如set命令;
2.對于需要長時間處理的事件,分步處理,直至完成。如rehash過程,在單次事件處理過程中,每次只移動一個桶。
3.對于阻塞函數,如從磁盤讀取vm數據等,采用異步的模式。從磁盤load數據,是一個較為耗時的操作,如處理函數中直接讀取數據,將會阻塞整體的單線程處理其他的client請求。
多線程開發的復雜性是我們所清楚的。采用單進程單線程的模式,可以避免很多復雜性,另外事務性操作,無須額外加鎖,大大降低了開發難度。
IO復用
RedisIO復用層,可以看作是一個tiny libevent,其實現只有四個文件,ae.cae_select.c
ae_kqueue.cae_epoll.c。一目了然,ae是事件監聽總的interfaceselectkquequeepoll是三種可選的IO復用方式。小而夠用,簡潔高效,充分體現了redis的設計理念。Redis DefaultselectC10K max
事件監聽
在單線程事件驅動模型中,往往會遇到兩種任務,定時處理任務及事件觸發任務,于是會有如下問題:事件觸發任務在無任務時,會處于阻塞狀態;定時任務要求定時處理,于是往往會有如下兩種處理方式:
1. 如果定時任務時間間隔為t,一般設IO復用層的超時時間為t/10,這樣可以保證定時任務得到及時處理,在調用所有cron任務時,會做間隔時間判斷。
2. 計算當前事件與最近的定時任務開始時間的間隔,設置IO復用超時時間為該事件。這樣定時任務也能得到及時處理。
注意:以上定時任務的處理時間,都為估約時間,非精確時間。前一種方式每輪計算量少,當容易引起空轉,后一種計算量大,但減少了空轉次數。Redis采用的是后一種方式。
這里也許有人會問:為什么不采用sigactionsetitimer等設置精確時鐘,以后再也不用為cron事件做額外處理。原因如下:
定時時鐘,往往會讓進程陷入內核態,內核軟中斷往往會打破用戶態一個函數的完整性,因此會破壞相關的狀態轉移;另外加入軟中斷的程序,將可能為程序帶來不可避免地復雜性。
以曾經做過一個項目的一段挫折經歷為例:
一個電話多路呼轉的項目,采用的是單線程異步事件驅動模型,每次定時任務有個讓空閑通道“掛機”的任務。先前采用預制的驅動時鐘,時鐘信號為實時信號(信號宏值取32-64之間的一個數),信號在sigqueue中排隊,這樣如果任務阻塞返回后,會瞬間拋出大量時鐘事件,造成空閑通道多次掛機失敗。后改用setitimer設置系統時鐘,軟中斷打斷了用戶態的一次mutex_lockmutex_unlock,并且掛機時使用了該鎖,造成整個進程時有時無的死鎖現象。
數據結構
Intset,該set不同于hash set數據結構,該數據結構其實是一個有序數組,對于:
插入操作:二分查找該有序數組——>找到位置——>有則返回,無則插入該位置,memmove后續元素。
查找操作:二分查找。
相對于以hash table為基礎的set而言,該模型簡單且更省空間。時間復雜度,在作者的presentation中提及在size 20-50k的數據量下,基本與hashset無異。這里可以認為,hash set遍歷鏈表的時間,與intset二分查找的時間基本差不多。但其實另一方面,作者沒有提及的,intset的插入速度,實際降低了,因為需要memmove的數據量,是O(n)的;但較hash set更有優勢的是,該array set,是有序的。
Ziplist,該數據結構是針對small size數據設計的數組list,好比常見的索引數組,不同于linked list,其保存在一個數組中,且用value-header保存鏈接項及其本身的偏移、長度,encoding等,用于遍歷list。在需求上,encoding保證多種不同的數據,可以保存在同一個list中,如int16int32str等。
zsethash dict + skip list。常見的有序setrb tree為基礎。而在整個redis中,我們很難發現任何復雜數據結構tree的蹤影。Redishash dict保證散列查找的效率,以跳表結構保證有序及范圍查找需求的快速滿足,范圍查找拋棄了rb treeb+-* tree,也體現了其不想復雜的簡潔美學。
......
整個redis中,沒有任何專門的內存池結構。所有內存分配,不像stlniginx,內存分配都有其內存池機構分配,redis沒有!Redis全是size+malloczmalloc分配,其常見的小對象,在intsetziplist等數據結構地以數組方式申請,并不斷用realloc的方式resize,間接地體現了內存池的思想,但卻沒有實現一個內存池管理模塊,作者簡單暴力的美學,體現得淋漓盡致。
持久化模塊
持久化是redis這種內存數據庫的拓展功能,如memcachedbmemcached一樣。其借鑒卻大大削減了OSvm處理機制(復雜程度較memecached遠不在數量級),以夠用的理念拿為己用,在某些地方按照需求定制。其思想為一個大的磁盤文件,作為vm文件,bitmap保存所有block的使用情況,redisObjectvmPointer為同型異構體,后者指明valuevm文件中的位置等信息,且對大的value進行lzf壓縮解壓以減少磁盤IO所帶來的性能開銷;而Key作為索引永遠保存在內存中。
常見的LRU實現有如下幾種:
memcached,有專門的lru list數據結構,保存著數據的LRU信息(存入的時間戳,鏈表),每次使用會改變這個鏈表,最終淘汰置換該list末尾的數據。
Linux內核,分為activeinactive兩個list,根據相應的計算經驗規則,將頁放入相應的list,并對inactive中的page進行再激活或淘汰置換。
redis沒有相應的lru數據結構,其lru機制為:整個redis有一個時鐘,每隔一段時間更新一次,每一個數據都會保存它們最近使用時當前的時鐘,隨機選出dict中幾個數據,根據其value的大小和最后更新的系統時鐘值,確定其swap的能力,超過某個閾值,則將其更換出去。
Vm的思想,對于redis這種崇尚簡潔實效為美的設計來說,多少還是有些復雜,因此作者一直在考慮另外一種實現方式去替代vm的持久化機制,可見
主輔同步模塊:redis的弱項,目前只能同到輔,而輔不能到主,有待繼續開發,對此模塊感興趣的可以參考mysql的實現。
結語:redis崇尚簡潔、實效、暴力的美學,讓redis成為kv-db中效率的代表,而所有dbsource code中,恐怕很難找到比redis更簡單易讀的代碼了。這很多方面,值得我們設計開發借鑒。
 本文由用戶 pfyc2581 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!