最近讀了Redis
的原理實現,感覺到程序語言的相通性,只要你掌握了語言的共性,舉一反三其餘語言的開發就變得很是簡單了。javascript
整體來講,各類程序語言底層的設計思想是很是相通的,首先針對須要解決的問題和場景選擇不一樣的數據結構和算法,根據運行環境設計不一樣的架構和特性,根據做者的喜愛選擇開發的風格,根據應用場景開發對外的接口,根據程序員的實踐維護社區和bug反饋區。java
不要將某種數據結構固化成你理解的某種語言的一種實現方式,它們都只是一種方便理解的概念,有許多種實現它的方式,甚至徹底不一樣。node
咱們下面看下數組這種數據結構的設計思路。mysql
當咱們想要設計一種數組的數據結構時,最容易想到的就是排成一隊的學生,每一個學生就是一個元素,咱們能夠對他們進行增刪查改。他們牢牢相連,就像一塊連續的存儲空間。git
當咱們能夠從頭至尾的看完全部學生信息(遍歷),也能夠從頭開始查找第4個學生(索引)。咱們能夠加入一個學生到任意位置(插入),也能夠將任意一位同窗移出隊列(刪除),但爲了保持緊密連續的隊列,咱們須要作一些額外的調整。程序員
這就是最經常使用的數據結構:數組。github
優點:算法
缺點:sql
在使用數組時,查詢元素的總數是常見的需求,遍歷元素獲取數組長度的方式很是低效,如mysql
普通的查詢總行數,select count(*) from table_name
,就會掃描全表。chrome
爲了支持總數快速查詢,咱們能夠看下javascript
的數組實現方式,它經過增長一個字段length
,在每次變動時更新這個數字,便可無需遍歷,直接讀取長度信息。
數組常常會進行遍歷,但也會使用下標獲取指定的元素,而典型的數組只能經過使用單獨的計數器來遍歷查找指定的元素,時間複雜度爲O(n)
,在元素不少時耗時好久。
這種方式下,咱們就可使用(目標元素地址 = 數組頭部地址 + 元素長度 * 元素下標)的方式訪問指定元素。
可是缺點也很明顯,應用場景比較狹窄,由於全部元素佔用空間都相同的狀況很是少,在大部分場景下各個元素使用的空間不盡相同,這樣就會致使空間的浪費。因此基本不會使用這種方式。
在這種存儲方式中,咱們先使用一個指定長度l
的連續數組做爲槽,這個長度就是hash
的模值,咱們用數組元素的索引i
對數組長度l
取模,獲得槽的索引,而後用鏈表的方式進行存儲,這樣就可以進行快速的下標訪問。
可是缺點也很明顯,就是若是中間的元素增長或刪除,後面的全部元素都須要從新hash和排列,所以也比較低效。
在原數組更新時,咱們能夠直接在原位置上進行重寫,而若是須要刪除元素2,咱們能夠直接申請一塊內存空間,將元素2以前和以後的連續內存空間直接拷貝到新空間中,就完成了數組的縮容。
擴容也是同樣的,新增了元素5,咱們一樣從新申請一塊內存空間,而後將元素5以前的拷貝到新空間,寫入元素5,再將元素5以後的連續內存空間進行批量拷貝。
// The JSArray describes JavaScript Arrays // Such an array can be in one of two modes: // - fast, backing storage is a FixedArray and length <= elements.length(); // Please note: push and pop can be used to grow and shrink the array. // - slow, backing storage is a HashTable with numbers as keys. class JSArray: public JSObject { public: // [length]: The length property. DECL\_ACCESSORS(length, Object)
首先看源碼實現,會發現JS中數組是基於對象的,根據數組狀態不一樣,元素屬性分爲固定長度的快數組,和hashTable
存儲的慢數組。
快數組和慢數組最大的區別就是存儲使用的數據結構不一樣,快數組採用連續空間的方式存儲,慢數組採用hashTable
的鏈表方式存儲。
// Constants for heuristics controlling conversion of fast elements // to slow elements. // Maximal gap that can be introduced by adding an element beyond // the current elements length. static const uint32\_t kMaxGap = 1024; // JSObjects prefer dictionary elements if the dictionary saves this much // memory compared to a fast elements backing store. static const uint32\_t kPreferFastElementsSizeFactor = 3;
查看快慢數組轉換源碼:
static inline bool ShouldConvertToSlowElements(JSObject object, uint32\_t capacity, uint32\_t index, uint32\_t\* new\_capacity) { STATIC\_ASSERT(JSObject::kMaxUncheckedOldFastElementsLength <= JSObject::kMaxUncheckedFastElementsLength); if (index < capacity) { \*new\_capacity = capacity; return false; } if (index - capacity >= JSObject::kMaxGap) return true; \*new\_capacity = JSObject::NewElementsCapacity(index + 1); DCHECK\_LT(index, \*new\_capacity); // TODO(ulan): Check if it works with young large objects. if (\*new\_capacity <= JSObject::kMaxUncheckedOldFastElementsLength || (\*new\_capacity <= JSObject::kMaxUncheckedFastElementsLength && ObjectInYoungGeneration(object))) { return false; } // If the fast-case backing storage takes up much more memory than a // dictionary backing storage would, the object should have slow elements. int used\_elements = object->GetFastElementsUsage(); uint32\_t size\_threshold = NumberDictionary::kPreferFastElementsSizeFactor \* NumberDictionary::ComputeCapacity(used\_elements) \* NumberDictionary::kEntrySize; return size\_threshold <= \*new\_capacity; }
快數組、慢數組二者轉化的臨界點有兩種:
if (index - capacity >= JSObject::kMaxGap) return true;
return size\_threshold <= \*new\_capacity;
其中kEntrySize根據數組存儲的內容不一樣,會在1|2|3
中選擇一個做爲係數,當爲數組索引時通常爲2。
根據代碼可知,也就是空洞元素大於1024個,或者新容量 > 3*舊容量*2
時,會將快數組轉化爲慢數組。
所謂的空洞就是未初始化的索引值,如
const a = [1,2]; a[1030] = 1;
此時就會產生1028個空洞產生,會直接使用滿數組來存儲,這樣可以節省大量的存儲空間。
總之,在JS V8
引擎中,數組使用快慢兩種方式設計,快數組提升操做效率,慢數組節省空間。
數組的經常使用push/pop
是經過直接在內存尾部追加或刪除,通常申請內存時會留有冗餘,空間不夠時再次申請。
// Number of element slots to pre-allocate for an empty array. static const int kPreallocatedArrayElements \= 4; };
從上面的代碼中能夠看到,初次申請就會分配4個元素槽位置。
static const uint32\_t kMinAddedElementsCapacity = 16; // Computes the new capacity when expanding the elements of a JSObject. static uint32\_t NewElementsCapacity(uint32\_t old\_capacity) { // (old\_capacity + 50%) + kMinAddedElementsCapacity return old\_capacity + (old\_capacity >> 1) + kMinAddedElementsCapacity; }
當空間不夠用時,就會申請新的空間,新空間容量=原空間+原空間/2+16
。
而後根據須要變更的位置分爲先後兩塊,直接按照連續內存空間的長度一次性拷貝到新內存地址上,效率是很高的。
Redis(Remote Dictionary Service, 遠程字典服務)
是使用最爲普遍的存儲中間件,因爲其超高的性能和豐富的客戶端支持,經常用於緩存服務,固然它也能夠用於持久化存儲服務。
Redis數組經常使用來存儲任務隊列,使用隊列或者棧的方式,進行任務分發和處理。
ziplist
壓縮列表Redis在數組元素較少時,使用ziplist
(壓縮列表)來存儲,它是一塊連續的內存空間,元素緊密存儲,沒有空隙。
// 壓縮列表結構體 struct ziplist<T> { int32 zlbytes; // 整個壓縮列表佔用字節數 int32 zltail_offset; // 最後一個元素的偏移量 int16 zllength; // 元素個數 T[] entries; // 元素內容列表 int8 zlend; // 結束標誌位,值爲0xFF } // 壓縮列表元素結構體 struct entry { int<var> prevlen; // 前一個entry的字節長度 int<var> encoding; // 元素類型編碼 optional byte[] content; // 元素內容 }
所以經過zltail_offset
咱們能夠快速定位到最後一個元素,經過prevlen
能夠支持雙向遍歷,經過zllength
屬性咱們能夠不用遍歷就能支持整個數組的元素個數。
因爲ziplist
採起緊湊存儲,所以沒有空間冗餘,致使每次插入新元素時,咱們都須要申請新的內存空間進行擴展,而後將原內存地址空間直接拷貝到新空間中。因爲Redis
是單線程,所以若是壓縮列表的容量過大,就會致使服務卡頓,所以不適合存儲過大空間的內容。當更新數據時,若是內容是減小的或者沒有超過已佔用的指定字節數閾值,就能夠原地更新。
quicklist
快速列表因爲ziplist
不適合大容量存儲,所以在數組元素較多時,咱們結合linkedlist
(鏈表)的方式設計了quicklist
。
struct quicklist { quicklistNode* head; // 頭部指針 quicklistNode* tail; // 尾部指針 long count; // 元素總數 int nodes; // ziplist節點個數 int compressDepth; // LZF壓縮算法深度 } struct quicklistNode { quicklistNode* prev; // 前節點指針 quicklistNode* next; // 後節點指針 ziplist* zl; // ziplist指針 int32 size; // ziplist字節總數 int16 count; // ziplist元素總數 int2 encoding; // 存儲形式:原生數組|LZF壓縮數組 }
通常每一個ziplist的空間上限爲8KB
,超過就會建立新的節點,這樣保證每一個節點在更新時不會操做過大的空間進行復制,同時在檢索時也大大提升了效率。每一個節點的空間限制能夠由list-max-ziplist-size
參數配置。
在該結構體中,爲了進一步壓縮空間佔用,可使用LZF算法進行壓縮,壓縮深度爲0|1|2
三種,0就是不壓縮,1就是首尾的前兩個元素不壓縮,其他都壓縮,2就是首尾的一個元素不壓縮,其他都壓縮。
首尾元素不壓縮是爲了保證push/pop
的快速操做時不用再解壓縮改指針內容,而其餘元素的壓縮預計能夠節省一半的空間。
在語言的數組設計中,咱們會發現幾個通性: