最近读了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
的快速操做时不用再解压缩改指针内容,而其余元素的压缩预计能够节省一半的空间。
在语言的数组设计中,咱们会发现几个通性: