算法复杂度汇总(图)!

Posted on

算法复杂度汇总(图)!

Know Thy Complexities!

Hi there! This webpage covers the space and time Big-O complexities of common algorithms used in Computer Science. When preparing for technical interviews in the past, I found myself spending hours crawling the internet putting together the best, average, and worst case complexities for search and sorting algorithms so that I wouldn't be stumped when asked about them. Over the last few years, I've interviewed at several Silicon Valley startups, and also some bigger companies, like Yahoo, eBay, LinkedIn, and Google, and each time that I prepared for an interview, I thought to msyelf "Why oh why hasn't someone created a nice Big-O cheat sheet?". So, to save all of you fine folks a ton of time, I went ahead and created one. Enjoy! Good

Fair

Poor

Searching

Algorithm Data Structure Time Complexity Space Complexity Average Worst Worst Depth First Search (DFS) Graph of |V| vertices and |E| edges -

O(|E| + |V|)

O(|V|) Breadth First Search (BFS) Graph of |V| vertices and |E| edges -

O(|E| + |V|)

O(|V|) Binary search Sorted array of n elements O(log(n))

O(log(n))

O(1) Linear (Brute Force) Array O(n)

O(n)

O(1) Shortest path by Dijkstra, using a Min-heap as priority queue Graph with |V| vertices and |E| edges O((|V| + |E|) log |V|)

O((|V| + |E|) log |V|)

O(|V|) Shortest path by Dijkstra, using an unsorted array as priority queue Graph with |V| vertices and |E| edges O(|V|^2)

O(|V|^2)

O(|V|) Shortest path by Bellman-Ford Graph with |V| vertices and |E| edges O(|V||E|)

O(|V||E|)

O(|V|)

Sorting

Algorithm Data Structure Time Complexity Worst Case Auxiliary Space Complexity Best Average Worst Worst Quicksort Array O(n log(n))

O(n log(n))

O(n^2)

O(log(n)) Mergesort Array O(n log(n))

O(n log(n))

O(n log(n))

O(n) Heapsort Array O(n log(n))

O(n log(n))

O(n log(n))

O(1) Bubble Sort Array O(n)

O(n^2)

O(n^2)

O(1) Insertion Sort Array O(n)

O(n^2)

O(n^2)

O(1) Select Sort Array O(n^2)

O(n^2)

O(n^2)

O(1) Bucket Sort Array O(n+k)

O(n+k)

O(n^2)

O(nk) Radix Sort Array O(nk)

O(nk)

O(nk)

O(n+k)

Data Structures

Data Structure Time Complexity Space Complexity Average Worst Worst Indexing Search Insertion Deletion Indexing Search Insertion Deletion Basic Array O(1)

O(n)

-

-

O(1)

O(n)

-

-

O(n) Dynamic Array O(1)

O(n)

O(n)

-

O(1)

O(n)

O(n)

-

O(n) Singly-Linked List O(n)

O(n)

O(1)

O(1)

O(n)

O(n)

O(1)

O(1)

O(n) Doubly-Linked List O(n)

O(n)

O(1)

O(1)

O(n)

O(n)

O(1)

O(1)

O(n) Skip List O(n)

O(log(n))

O(log(n))

O(log(n))

O(n)

O(n)

O(n)

O(n)

O(n log(n)) Hash Table -

O(1)

O(1)

O(1)

-

O(n)

O(n)

O(n)

O(n) Binary Search Tree -

O(log(n))

O(log(n))

O(log(n))

-

O(n)

O(n)

O(n)

O(n) B-Tree -

O(log(n))

O(log(n))

O(log(n))

-

O(log(n))

O(log(n))

O(log(n))

O(n) Red-Black Tree -

O(log(n))

O(log(n))

O(log(n))

-

O(log(n))

O(log(n))

O(log(n))

O(n) AVL Tree -

O(log(n))

O(log(n))

O(log(n))

-

O(log(n))

O(log(n))

O(log(n))

O(n)

Heaps

Heaps Time Complexity Heapify Find Max Extract Max Increase Key Insert Delete Merge Linked List (sorted) -

O(1)

O(1)

O(n)

O(n)

O(1)

O(m+n) Linked List (unsorted) -

O(n)

O(n)

O(1)

O(1)

O(1)

O(1) Binary Heap O(log(n))

O(1)

O(log(n))

O(log(n))

O(log(n))

O(log(n))

O(m+n) Binomial Heap -

O(log(n))

O(log(n))

O(log(n))

O(log(n))

O(log(n))

O(log(n)) Fibonacci Heap -

O(1)

O(log(n))/*

O(1)/*

O(1)

O(log(n))/*

O(1)

Graphs

Node / Edge Management Storage Add Vertex Add Edge Remove Vertex Remove Edge Query Adjacency list O(|V|+|E|)

O(1)

O(1)

O(|V| + |E|)

O(|E|)

O(|V|) Incidence list O(|V|+|E|)

O(1)

O(1)

O(|E|)

O(|E|)

O(|E|) Adjacency matrix O(|V|^2)

O(|V|^2)

O(1)

O(|V|^2)

O(1)

O(1) Incidence matrix O(|V| |E|)

O(|V| |E|)

O(|V| |E|)

O(|V| |E|)

O(|V| |E|)

O(|E|)

Notation for asymptotic growth

letter bound growth (theta) Θ upper and lower, tight[1] equal[2] (big-oh) O upper, tightness unknown less than or equal[3] (small-oh) o upper, not tight less than (big omega) Ω lower, tightness unknown greater than or equal (small omega) ω lower, not tight greater than

[1] Big O is the upper bound, while Omega is the lower bound. Theta requires both Big O and Omega, so that's why it's referred to as a tight bound (it must be both the upper and lower bound). For example, an algorithm taking Omega(n log n) takes at least n log n time but has no upper limit. An algorithm taking Theta(n log n) is far preferential since it takes AT LEAST n log n (Omega n log n) and NO MORE THAN n log n (Big O n log n).SO

[2] f(x)=Θ(g(n)) means f (the running time of the algorithm) grows exactly like g when n (input size) gets larger. In other words, the growth rate of f(x) is asymptotically proportional to g(n).

[3] Same thing. Here the growth rate is no faster than g(n). big-oh is the most useful because represents the worst-case behavior. In short, if algorithm is then its performance is algorithm performance o(n) < n O(n) ≤ n Θ(n) = n Ω(n) ≥ n ω(n) > n

Big-O Complexity Chart

Big O Complexity Graph

Contribute

Edit these tables!

Authors:

  1. Eric Rowell
  2. Quentin Pleple
  3. Nick Dizazzo
  4. Michael Abed
  5. Adam Forsyth
  6. Jay Engineer
  7. Josh Davis
  8. makosblade
  9. Alejandro Ramirez
  10. Joel Friedly
  11. Eric Lefevre-Ardant
  12. Thomas Dybdahl Ahle

一致性hash算法

Posted on

一致性hash算法 - consistent hashing

您还未登录!|登录|注册|帮助

sparkliang的专栏

#

一致性hash算法 - consistent hashing

分类: 算法艺术 2010-02-02 09:19 46510人阅读 评论(80) 收藏 举报 算法cacheobject服务器存储c

目录(?)[+]

  1. 一致性 hash 算法 consistent hashing

  2. 基本场景

  3. hash 算法和单调性
  4. consistent hashing 算法的原理

  5. 环形hash 空间

  6. 把对象映射到hash 空间
  7. 把cache 映射到hash 空间
  8. 把对象映射到cache
  9. 考察cache 的变动

  10. 虚拟节点

  11. 小结

    一致性 hash 算法( consistent hashing )

张亮

consistent hashing 算法早在 1997 年就在论文 Consistent hashing and random trees 中被提出,目前在 cache 系统中应用越来越广泛;

1 基本场景

比如你有 N 个 cache 服务器(后面简称 cache ),那么如何将一个对象 object 映射到 N 个 cache 上呢,你很可能会采用类似下面的通用方法计算 object 的 hash 值,然后均匀的映射到到 N 个 cache ;

hash(object)%N

一切都运行正常,再考虑如下的两种情况;

1 一个 cache 服务器 m down 掉了(在实际应用中必须要考虑这种情况),这样所有映射到 cache m 的对象都会失效,怎么办,需要把 cache m 从 cache 中移除,这时候 cache 是 N-1 台,映射公式变成了 hash(object)%(N-1) ;

2 由于访问加重,需要添加 cache ,这时候 cache 是 N+1 台,映射公式变成了 hash(object)%(N+1) ;

1 和 2 意味着什么?这意味着突然之间几乎所有的 cache 都失效了。对于服务器而言,这是一场灾难,洪水般的访问都会直接冲向后台服务器;

再来考虑第三个问题,由于硬件能力越来越强,你可能想让后面添加的节点多做点活,显然上面的 hash 算法也做不到。

有什么方法可以改变这个状况呢,这就是 consistent hashing...

2 hash 算法和单调性

Hash 算法的一个衡量指标是单调性( Monotonicity ),定义如下:

单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲加入到系统中。哈希的结果应能够保证原有已分配的内容可以被映射到新的缓冲中去,而不会被映射到旧的缓冲集合中的其他缓冲区。

容易看到,上面的简单 hash 算法 hash(object)%N 难以满足单调性要求。

3 consistent hashing 算法的原理

consistent hashing 是一种 hash 算法,简单的说,在移除 / 添加一个 cache 时,它能够尽可能小的改变已存在 key 映射关系,尽可能的满足单调性的要求。

下面就来按照 5 个步骤简单讲讲 consistent hashing 算法的基本原理。

3.1 环形hash 空间

考虑通常的 hash 算法都是将 value 映射到一个 32 为的 key 值,也即是 0~2^32-1 次方的数值空间;我们可以将这个空间想象成一个首( 0 )尾( 2^32-1 )相接的圆环,如下面图 1 所示的那样。

circle space

图 1 环形 hash 空间

3.2 把对象映射到hash 空间

接下来考虑 4 个对象 object1~object4 ,通过 hash 函数计算出的 hash 值 key 在环上的分布如图 2 所示。

hash(object1) = key1;

… …

hash(object4) = key4;

object

图 2 4 个对象的 key 值分布

3.3 把cache 映射到hash 空间

Consistent hashing 的基本思想就是将对象和 cache 都映射到同一个 hash 数值空间中,并且使用相同的 hash 算法。

假设当前有 A,B 和 C 共 3 台 cache ,那么其映射结果将如图 3 所示,他们在 hash 空间中,以对应的 hash 值排列。

hash(cache A) = key A;

… …

hash(cache C) = key C;

cache

图 3 cache 和对象的 key 值分布

说到这里,顺便提一下 cache 的 hash 计算,一般的方法可以使用 cache 机器的 IP 地址或者机器名作为 hash 输入。

3.4 把对象映射到cache

现在 cache 和对象都已经通过同一个 hash 算法映射到 hash 数值空间中了,接下来要考虑的就是如何将对象映射到 cache 上面了。

在这个环形空间中,如果沿着顺时针方向从对象的 key 值出发,直到遇见一个 cache ,那么就将该对象存储在这个 cache 上,因为对象和 cache 的 hash 值是固定的,因此这个 cache 必然是唯一和确定的。这样不就找到了对象和 cache 的映射方法了吗?!

依然继续上面的例子(参见图 3 ),那么根据上面的方法,对象 object1 将被存储到 cache A 上; object2 和 object3 对应到 cache C ; object4 对应到 cache B ;

3.5 考察cache 的变动

前面讲过,通过 hash 然后求余的方法带来的最大问题就在于不能满足单调性,当 cache 有所变动时, cache 会失效,进而对后台服务器造成巨大的冲击,现在就来分析分析 consistent hashing 算法。

3.5.1 移除 cache

考虑假设 cache B 挂掉了,根据上面讲到的映射方法,这时受影响的将仅是那些沿 cache B 逆时针遍历直到下一个 cache ( cache C )之间的对象,也即是本来映射到 cache B 上的那些对象。

因此这里仅需要变动对象 object4 ,将其重新映射到 cache C 上即可;参见图 4 。

remove

图 4 Cache B 被移除后的 cache 映射

3.5.2 添加 cache

再考虑添加一台新的 cache D 的情况,假设在这个环形 hash 空间中, cache D 被映射在对象 object2 和 object3 之间。这时受影响的将仅是那些沿 cache D 逆时针遍历直到下一个 cache ( cache B )之间的对象(它们是也本来映射到 cache C 上对象的一部分),将这些对象重新映射到 cache D 上即可。

因此这里仅需要变动对象 object2 ,将其重新映射到 cache D 上;参见图 5 。

add

图 5 添加 cache D 后的映射关系

4 虚拟节点

考量 Hash 算法的另一个指标是平衡性 (Balance) ,定义如下:

平衡性

平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。

hash 算法并不是保证绝对的平衡,如果 cache 较少的话,对象并不能被均匀的映射到 cache 上,比如在上面的例子中,仅部署 cache A 和 cache C 的情况下,在 4 个对象中, cache A 仅存储了 object1 ,而 cache C 则存储了 object2 、 object3 和 object4 ;分布是很不均衡的。

为了解决这种情况, consistent hashing 引入了“虚拟节点”的概念,它可以如下定义:

“虚拟节点”( virtual node )是实际节点在 hash 空间的复制品( replica ),一实际个节点对应了若干个“虚拟节点”,这个对应个数也成为“复制个数”,“虚拟节点”在 hash 空间中以 hash 值排列。

仍以仅部署 cache A 和 cache C 的情况为例,在图 4 中我们已经看到, cache 分布并不均匀。现在我们引入虚拟节点,并设置“复制个数”为 2 ,这就意味着一共会存在 4 个“虚拟节点”, cache A1, cache A2 代表了 cache A ; cache C1, cache C2 代表了 cache C ;假设一种比较理想的情况,参见图 6 。

virtual nodes

图 6 引入“虚拟节点”后的映射关系

此时,对象到“虚拟节点”的映射关系为:

objec1->cache A2 ; objec2->cache A1 ; objec3->cache C1 ; objec4->cache C2 ;

因此对象 object1 和 object2 都被映射到了 cache A 上,而 object3 和 object4 映射到了 cache C 上;平衡性有了很大提高。

引入“虚拟节点”后,映射关系就从 { 对象 -> 节点 } 转换到了 { 对象 -> 虚拟节点 } 。查询物体所在 cache 时的映射关系如图 7 所示。

map

图 7 查询对象所在 cache

“虚拟节点”的 hash 计算可以采用对应节点的 IP 地址加数字后缀的方式。例如假设 cache A 的 IP 地址为 202.168.14.241 。

引入“虚拟节点”前,计算 cache A 的 hash 值:

Hash(“202.168.14.241”);

引入“虚拟节点”后,计算“虚拟节”点 cache A1 和 cache A2 的 hash 值:

Hash(“202.168.14.241/#1”); // cache A1

Hash(“202.168.14.241/#2”); // cache A2

5 小结

Consistent hashing 的基本原理就是这些,具体的分布性等理论分析应该是很复杂的,不过一般也用不到。

http://weblogs.java.net/blog/2007/11/27/consistent-hashing 上面有一个 java 版本的例子,可以参考。

http://blog.csdn.net/mayongzhan/archive/2009/06/25/4298834.aspx 转载了一个 PHP 版的实现代码。

http://www.codeproject.com/KB/recipes/lib-conhash.aspx C语言版本

一些参考资料地址:

http://portal.acm.org/citation.cfm?id=258660

http://en.wikipedia.org/wiki/Consistent_hashing

http://www.spiteful.com/2008/03/17/programmers-toolbox-part-3-consistent-hashing/

http://weblogs.java.net/blog/2007/11/27/consistent-hashing

http://tech.idv2.com/2008/07/24/memcached-004/

http://blog.csdn.net/mayongzhan/archive/2009/06/25/4298834.aspx

分享到:

  1. 上一篇:Map Reduce – the Free Lunch is not over?
  2. 下一篇:Hadoop分布式文件系统:架构和设计要点 查看评论

70楼 Spark-zh 2013-07-09 20:27发表 [回复] [引用] [举报]感谢楼主的讲解,很明了。69楼 thomastianqq 2013-07-09 17:26发表 [回复] [引用] [举报]单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲加入到系统中。哈希的结果应能够保证原有已分配的内容可以被映射到新的缓冲中去,而不会被映射到旧的缓冲集合中的其他缓冲区。 Hi,LZ,文中关于单调性的阐述是错误的。正确的应该是:哈希的结果应能够保证原有已分配的内容可以被映射到原有的或者新的缓冲中去,而不会被映射到旧的缓冲集合中的其他缓冲区。68楼 lgfeng218 2013-06-26 11:59发表 [回复] [引用] [举报]通俗易懂67楼 midstr 2013-06-18 14:54发表 [回复] [引用] [举报]通俗易懂,顶一个哈66楼 zxh87 2013-06-03 15:18发表 [回复] [引用] [举报]很好,又看了一次!65楼 fanjiabing 2013-05-22 16:00发表 [回复] [引用] [举报]写的很好!64楼 mydreamongo 2013-05-20 16:27发表 [回复] [引用] [举报]讲的很好。。63楼 bingmo01 2013-05-17 00:31发表 [回复] [引用] [举报]很给力。基本上看懂62楼 perfectshark 2013-05-11 13:26发表 [回复] [引用] [举报]浅显易懂!61楼 pymqqq 2013-05-04 20:18发表 [回复] [引用] [举报]非常好,谢谢楼主!学习了!60楼 草原面朝大海 2013-04-30 22:59发表 [回复] [引用] [举报]假设我们已经在cache A中存储了一个object 1,然后不巧的是cacheA在之后被删除了,这时候我们需要访问object1,难道再一次进行hash,将object1存到下一个cache中吗? 如果上述成立,问题是我们如何将object1从cacheA中读取出来,因为在实际情况中,很有可能是cacheA是一下子坏掉了。Re: 总版主 2013-05-07 17:08发表 [回复] [引用] [举报]回复zhaowenchaofang:我觉得可以把OBJ存三份,分别再顺时针往后三个cache中,这样就算cache瞬间崩溃了,那么重新复制一份备份保证有三个备份即可,加CACHE也不是问题,剪切一部分obj到新增的cache上即可。59楼 sc274491910 2013-04-19 18:07发表 [回复] [引用] [举报]加入了虚拟节点后,出现加减节点的情况,这种情况怎么权衡呢?58楼 r4t5y6u7 2013-04-07 13:46发表 [回复] [引用] [举报]讲的太好了,清晰明了,浅显易懂57楼 轻舞飘扬 2013-03-30 17:09发表 [回复] [引用] [举报]非常好56楼 scncpb 2013-03-27 15:13发表 [回复] [引用] [举报]作者写的和老外写的一样通俗易懂55楼 mail_f5 2013-02-21 17:55发表 [回复] [引用] [举报]图文并茂,不错54楼 zhouxiaoqingelse 2013-02-20 18:18发表 [回复] [引用] [举报]非常清晰,感谢。53楼 sight_ 2013-01-22 11:08发表 [回复] [引用] [举报]楼主,有个问题请教一下,这个算法是如何保证后面添加的服务器能承担更多的任务,我应该如何控制"更多"这个量。例如我的服务器是分等级的,如何根据这个等级来控制hash到它上面的任务。Re: sparkliang 2013-01-22 13:57发表 [回复] [引用] [举报]回复sight:首先新加入的节点,将会把后继节点的部分数据分流给自己; 后面的新数据将会根据hash结果,无区别的分配到各节点上; 从我了解的来看,hash本身解决不了你说的分等级问题;hash环上的所有节点都是无差别对待的。不过对于引入虚拟节点的hash环来说,一个方法就是根据优先级来设置vnode的个数,这样也可以做到能力强的节点承担更多数据的结果,从统计意义上,实际上可能略有偏差。52楼 林大虫 2012-12-24 13:44发表 [回复] [引用] [举报]讲得很清晰啊51楼 dream1baby 2012-12-18 11:32发表 [回复] [引用] [举报]赞,学习了!50楼 blueskyltt 2012-12-03 18:30发表 [回复] [引用] [举报]楼主讲的很浅显易懂,赞49楼 syzcch 2012-11-12 09:11发表 [回复] [引用] [举报]讲的浅显易懂48楼 jixianwu2009 2012-11-05 10:03发表 [回复] [引用] [举报]清晰明了47楼 月亮床 2012-10-23 18:27发表 [回复] [引用] [举报]受益非浅46楼 ju136 2012-10-16 09:32发表 [回复] [引用] [举报]good45楼 yangqisheng 2012-10-07 13:42发表 [回复] [引用] [举报]楼主写的很好,一看就懂。我非常喜欢图文并茂的说明。谢谢楼主。44楼 lcw918110 2012-09-05 10:14发表 [回复] [引用] [举报]3.5.1中,这时受影响的将仅是那些沿 cache B 逆时针遍历直到下一个 cache ( cache C )之间的对象。其中的cache C如图所示应为 A。请楼主检查下。 谢谢楼主的博文。Re: sparkliang 2012-09-05 15:42发表 [回复] [引用] [举报]回复lcw918110:赞,看的很仔细;逆时针是在A上,顺时针落在C上。Re: lcw918110 2012-09-07 11:40发表 [回复] [引用] [举报]回复sparkliang:楼主,关于cache中使用的hash算法,楼主知道的还有哪些?学习学习。43楼 renjunyi6666 2012-08-23 11:38发表 [回复] [引用] [举报]受益!谢谢42楼 Myxiao7 2012-08-17 21:05发表 [回复] [引用] [举报]学习了41楼 skywhsq1987 2012-08-02 10:35发表 [回复] [引用] [举报]ding40楼 m_vptr 2012-08-02 09:25发表 [回复] [引用] [举报]好文章39楼 boYwell 2012-08-01 09:08发表 [回复] [引用] [举报]文笔很好,赞一个38楼 YcdoiT 2012-07-26 16:40发表 [回复] [引用] [举报]顶一个,我这种悟性的都能看懂了37楼 大城小爱 2012-07-06 19:02发表 [回复] [引用] [举报]讲得很清晰,不错不错36楼 brucest0078 2012-06-18 11:44发表 [回复] [引用] [举报]关于virtualNode 有一些问题,因为你的key在存储的时候可以这样,但是读取的时候只有原生的key去相关的内容,而不会把vNode的ip之类的带进去,所以这里只是理论呢还是已经有实践过?表示怀疑。Re: sparkliang 2012-06-18 13:52发表 [回复] [引用] [举报]回复BruceXX:读取的时候,关vnode什么事呢。读取者事先是不知道key存在那个节点上的。35楼 Honeybb 2012-06-12 09:44发表 [回复] [引用] [举报]最近在看这方面的文章,很迷茫。 楼主,分布式网络关于资源定位与一致性哈希算法和paxos算法的关系,请楼主明示啊!Re: sparkliang 2012-06-18 13:52发表 [回复] [引用] [举报]回复Honeybb:一致性哈希算法和paxos算法,没什么关联34楼 laigege 2012-06-02 09:48发表 [回复] [引用] [举报]楼主是好人33楼 dengjm_2011 2012-05-23 16:41发表 [回复] [引用] [举报]很不错的文章,简单明了!32楼 zcl198715 2012-04-29 21:34发表 [回复] [引用] [举报]分析的很好!赞一个!31楼 leonkyd 2012-04-21 10:28发表 [回复] [引用] [举报]非常好的技术文章,赞一个30楼 wumucheng 2012-04-06 20:04发表 [回复] [引用] [举报]讲得非常清晰,赞!29楼 lqbbduck 2012-03-30 00:51发表 [回复] [引用] [举报]分析得很好28楼 TCCaiWQ 2012-03-15 16:54发表 [回复] [引用] [举报]简洁明了27楼 RunZhi1989 2012-03-05 17:22发表 [回复] [引用] [举报]赞,我看英文维基百科一直没看懂,博主的东西真是简洁明了。赞赞赞!26楼 libobo5954451 2012-02-10 18:02发表 [回复] [引用] [举报]非常好25楼 humingchun 2012-01-02 19:44发表 [回复] [引用] [举报]偶竟然看明白了 说明写的很清楚 呵呵24楼 jmx_zhidai 2011-11-24 16:31发表 [回复] [引用] [举报]写的非常明白,赞一个!!!23楼 yfk 2011-11-16 15:27发表 [回复] [引用] [举报]很赞的文章 谢谢lZ22楼 南山道人 2011-11-16 12:47发表 [回复] [引用] [举报]顶,非常感谢!21楼 zhaopeinow 2011-10-20 17:52发表 [回复] [引用] [举报]顶20楼 lbqBraveheart 2011-10-12 15:03发表 [回复] [引用] [举报]顶19楼 向良玉 2011-10-11 13:59发表 [回复] [引用] [举报]好文章18楼 xgtogd 2011-09-25 23:26发表 [回复] [引用] [举报]很好17楼 fairywell 2011-09-12 21:54发表 [回复] [引用] [举报]好文,图文并茂,让我一下就看明白了 :) Mark并收藏~~16楼 parakpurple 2011-08-29 17:09发表 [回复] [引用] [举报]写的太棒了 深入浅出15楼 bokee 2011-08-06 16:12发表 [回复] [引用] [举报]很清晰啊,想问下你的图是用什么工具画的?Re: sparkliang 2011-08-30 09:19发表 [回复] [引用] [举报]回复bokee:visio啊14楼 dahai_888 2011-07-16 17:12发表 [回复] [引用] [举报]不得了。13楼 wayon_yang 2011-05-30 11:49发表 [回复] [引用] [举报][e01] 英语不好 之前没看懂 现在清晰多了12楼 laoyi19861011 2011-05-13 20:45发表 [回复] [引用] [举报]这么好的一篇文章居然没有推荐,csdn的编辑干嘛吃去了11楼 chemila 2011-03-31 17:45发表 [回复] [引用] [举报][e01]10楼 liomao 2011-03-29 09:13发表 [回复] [引用] [举报][e10]9楼 xunujNext 2011-02-17 22:28发表 [回复] [引用] [举报]不得不[e01]8楼 printfabcd 2010-12-17 14:51发表 [回复] [引用] [举报]请问虚拟节点,怎么保证均匀分布在,那个环上呢??O(∩∩)O~Re: sparkliang 2011-03-11 15:31发表 [回复] [引用] [举报]回复 printfabcd:这就要由hash算法来保证了,均匀分布是概率上的均匀,当虚拟节点足够时,就能保证大概均匀了。7楼 ccnuliu 2010-06-23 00:01发表 [回复] [引用] [举报]假如cache通过hash函数计算出的值 和 object通过hash函数计算出来的值是同一个hash值怎么办? 那object应该指向哪个cache?Re: sparkliang 2010-06-24 09:41发表 [回复] [引用] [举报]回复 ccnuliu:如果碰巧相同,就是指向具有相同hash值的cache。选择的原则是cache.hash >= object.hash;(当然在环的衔接处是例外)Re: a43350860 2011-03-05 18:27发表 [回复] [引用] [举报]回复 sparkliang:假设我给hash环开启一扇大门,所有的值进来都必须经过这一扇门,那么我只要在这一扇大门里加一个hash关卡,我不关心你传进来的值是什么类型,是什么形态,我都是统一做hash运算,然后再分配到相应的节点上面去。6楼 ccnuliu 2010-06-23 00:01发表 [回复] [引用] [举报]问你一个问题 加入cache通过hash函数计算出的值 和 object通过hash函数计算出来的值是同一个hash值怎么办? 那object应该指向哪个cache?5楼 chengqianl 2010-06-02 16:53发表 [回复] [引用] [举报]收益了4楼 specific_intel 2010-05-12 16:26发表 [回复] [引用] [举报][e01] 非常清晰,谢谢!3楼 yayaaishenghuo 2010-03-09 13:46发表 [回复] [引用] [举报]讲得太好了2楼 匿名用户 2010-02-22 10:06发表 [回复] [引用] [举报][e10]1楼 匿名用户 2010-02-22 10:05发表 [回复] [引用] [举报][e01] 您还没有登录,请[登录][注册]

/* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场 TOP

个人资料

sparkliang

  • 访问:383734次
  • 积分:3864分
  • 排名:第1399名

  • 原创:87篇

  • 转载:15篇
  • 译文:4篇
  • 评论:395条

文章搜索

文章分类

展开

阅读排行

推荐文章 最新评论

xystar2012: 代码有一个bug,在accpet后的sockfd,每次读或者写时,不能再调用epoll_ctl的EP...

zhkhhust: 楼主您好,我在ubuntu 下面测试时, 发现每次用 telnet 连接到 12345 端口,刚打了...

geniuslinchao: 例子程序少加了stirng.h和stdlib.h两个头文件

Spark-zh: 感谢楼主的讲解,很明了。

thomastianqq: 单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲加入到系统中。哈希的结果应能够...

lgfeng218: 通俗易懂

midstr: 通俗易懂,顶一个哈

xly931: @tiange0823:一般说的阻塞都是只IO操作本身是否阻塞(参见unix网络编程第六章);epo...

qiuzhenguang: 相当好的一篇教程,学习了!多谢楼主。

superyongzhe: 谢谢分享!!!

公司简介|招贤纳士|广告服务|银行汇款帐号|联系方式|版权声明|法律顾问|问题报告QQ客服 微博客服 论坛反馈 联系邮箱:webmaster@csdn.net 服务热线:400-600-2320京 ICP 证 070598 号北京创新乐知信息技术有限公司 版权所有世纪乐知(北京)网络技术有限公司 提供技术支持江苏乐知网络技术有限公司 提供商务支持Copyright © 1999-2012, CSDN.NET, All Rights Reserved GongshangLogo

Posted on

**

博客分类:

【栈】 是限定仅在表尾进行插入或删除操作的线性表 表尾称为栈顶,表头称为栈底 特点:后进先出

操作: 1.推入push 2.弹出pop

栈的数组实现

Java代码 收藏代码

  1. public class ArrayStack {
  2. private List list = new ArrayList();
  3. public boolean isEmpty(){
  4. return list.size()==0;
  5. }
  6. public void push(E element){
  7. list.add(element);
  8. }
  9. public void pop(){
  10. list.remove(list.size()-1);
  11. }
  12. public E getPop(){
  13. return list.get(list.size()-1);
  14. }
  15. public List getElements(){
  16. List result = new ArrayList();
  17. for(E e : list){
  18. result.add(((Element)e).getValue());
  19. }
  20. return result;
  21. }
  22. }

栈的链表实现

Java代码 收藏代码

  1. public class LinkedStack{
  2. private static class Node{
  3. E element;
  4. Node next;
  5. public Node(E element){
  6. this.element = element;
  7. }
  8. }
  9. private Node top = new Node(null);
  10. private int size = 0;
  11. public boolean isEmpty() {
  12. return size == 0;
  13. }
  14. public void push(E element){
  15. Node newNode = new Node(element);
  16. if(!isEmpty()){
  17. newNode.next = getPopNode();
  18. top.next = newNode;
  19. }else{
  20. top.next = newNode;
  21. }
  22. size++;
  23. }
  24. public void pop(){
  25. if(isEmpty()){
  26. throw new RuntimeException("The stack is empty");
  27. }
  28. Node firstNode = top.next;
  29. top.next = firstNode.next;
  30. firstNode.next = null;
  31. size--;
  32. }
  33. public E getPop(){
  34. return getPopNode().element;
  35. }
  36. private Node getPopNode(){
  37. if(isEmpty()){
  38. throw new RuntimeException("The stack is empty");
  39. }
  40. return top.next;
  41. }
  42. public List getElements(){
  43. if(isEmpty()){
  44. return null;
  45. }else{
  46. List elements = new ArrayList();
  47. Node node = (Node) top;
  48. while(node.next!=null){
  49. node = node.next;
  50. elements.add(((Element)node.element).getValue());
  51. }
  52. return elements;
  53. }
  54. }
  55. }

来源: [http://tangyanbo.iteye.com/blog/1472830](http://tangyanbo.iteye.com/blog/1472830)

NoSQL数据库的分布式算法

Posted on

NoSQL数据库的分布式算法

原文出处: highlyscalable.wordpress.com 译文出处: juliashine

系统的可扩展性是推动NoSQL运动发展的的主要理由,包含了分布式系统协调,故障转移,资源管理和许多其他特性。这么讲使得NoSQL听起来像是一个大筐,什么都能塞进去。尽管NoSQL运动并没有给分布式数据处理带来根本性的技术变革,但是依然引发了铺天盖地的关于各种协议和算法的研究以及实践。正是通过这些尝试逐渐总结出了一些行之有效的数据库构建方法。在这篇文章里,我将针对nosql数据库的分布式特点进行一些系统化的描述。

接下来我们将研究一些分布式策略,比如故障检测中的复制,这些策略用黑体字标出,被分为三段:

  • 数据一致性。NoSQL需要在分布式系统的一致性,容错性和性能,低延迟及高可用之间作出权衡,一般来说,数据一致性是一个必选项,所以这一节主要是关于数据复制数据恢复
  • 数据放置。一个数据库产品应该能够应对不同的数据分布,集群拓扑和硬件配置。在这一节我们将讨论如何分布以及调整数据分布才能够能够及时解决故障,提供持久化保证,高效查询和保证集群中的资源(如内存和硬盘空间)得到均衡使用。
  • 对等系统。像?leader election?这样的的技术已经被用于多个数据库产品以实现容错和数据强一致性。然而,即使是分散的的数据库(无中心)也要跟踪它们的全局状态,检测故障和拓扑变化。这一节将介绍几种使系统保持一致状态的技术。

数据一致性

众所周知,分布式系统经常会遇到网络隔离或是延迟的情况,在这种情况下隔离的部分是不可用的,因此要保持高可用性而不牺牲一致性是不可能的。这一事实通常被称作“CAP理论”。然而,一致性在分布式系统中是一个非常昂贵的东西,所以经常需要在这上面做一些让步,不只是针对可用性,还有多种权衡。为了研究这些权衡,我们注意到分布式系统的一致性问题是由数据隔离和复制引起的,所以我们将从研究复制的特点开始:

  • 可用性。在网络隔离的情况下剩余部分仍然可以应对读写请求。
  • 读写延迟。读写请求能够在短时间内处理。
  • 读写延展性。读写的压力可由多个节点均衡分担。
  • 容错性。对于读写请求的处理不依赖于任何一个特定节点。
  • 数据持久性。特定条件下的节点故障不会造成数据丢失。
  • 一致性。一致性比前面几个特性都要复杂得多,我们需要详细讨论一下几种不同的观点。 但是我们不会涉及过多的一致性理论和并发模型,因为这已经超出了本文的范畴,我只会使用一些简单特点构成的精简体系。

  • 读写一致性。从读写的观点来看,数据库的基本目标是使副本趋同的时间尽可能短(即更新传递到所有副本的时间),保证最终一致性。除了这个较弱的保证,还有一些更强的一致性特点:

  • 写后读一致性。在数据项X上写操作的效果总是能够被后续的X上的读操作看见。

  • 读后读一致性。在一次对数据项X的读操作之后,后续对X的读操作应该返回与第一次的返回值相同或是更加新的值。

  • 写一致性。分区的数据库经常会发生写冲突。数据库应当能处理这种冲突并保证多个写请求不会被不同的分区所处理。这方面数据库提供了几种不同的一致性模型:

  • 原子写。假如数据库提供了API,一次写操作只能是一个单独的原子性的赋值,避免写冲突的办法是找出每个数据的“最新版本”。这使得所有的节点都能够在更新结束时获得同一版本,而与更新的顺序无关,网络故障和延迟经常造成各节点更新顺序不一致。 数据版本可以用时间戳或是用户指定的值来表示。Cassandra用的就是这种方法。

  • 原子化的读-改-写。应用有时候需要进行 读-改-写 序列操作而非单独的原子写操作。假如有两个客户端读取了同一版本的数据,修改并且把修改后的数据写回,按照原子写模型,时间上比较靠后的那一次更新将会覆盖前一次。这种行为在某些情况下是不正确的(例如,两个客户端往同一个列表值中添加新值)。数据库提供了至少两种解决方法:

  • 冲突预防。 读-改-写 可以被认为是一种特殊情况下的事务,所以分布式锁或是 PAXOS [20, 21] 这样的一致协议都可以解决这种问题。这种技术支持原子读改写语义和任意隔离级别的事务。另一种方法是避免分布式的并发写操作,将对特定数据项的所有写操作路由到单个节点上(可以是全局主节点或者分区主节点)。为了避免冲突,数据库必须牺牲网络隔离情况下的可用性。这种方法常用于许多提供强一致性保证的系统(例如大多数关系数据库,HBase,MongoDB)。

  • 冲突检测。数据库跟踪并发更新的冲突,并选择回滚其中之一或是维持两个版本交由客户端解决。并发更新通常用向量时钟 [19] (这是一种乐观锁)来跟踪,或者维护一个完整的版本历史。这个方法用于 Riak, Voldemort, CouchDB.

现在让我们仔细看看常用的复制技术,并按照描述的特点给他们分一下类。第一幅图描绘了不同技术之间的逻辑关系和不同技术在系统的一致性、扩展性、可用性、延迟性之间的权衡坐标。 第二张图详细描绘了每个技术。

复本因子是4。读写协调者可以是一个外部客户端或是一个内部代理节点。

我们会依据一致性从弱到强把所有的技术过一遍:

  • (A, 反熵) 一致性最弱,基于策略如下。写操作的时候选择任意一个节点更新,在读的时候如果新数据还没有通过后台的反熵协议传递到读的那个节点,那么读到的仍然是旧数据。(下一节会详细介绍反熵协议)。这种方法的主要特点是:

  • 过高的传播延迟使它在数据同步方面不太好用,所以比较典型的用法是只作为辅助性的功能来检测和修复计划外的不一致。Cassandra就使用了反熵算法来在各节点之间传递数据库拓扑和其他一些元数据信息。

  • 一致性保证较弱:即使在没有发生故障的情况下,也会出现写冲突与读写不一致。
  • 在网络隔离下的高可用和健壮性。用异步的批处理替代了逐个更新,这使得性能表现优异。
  • 持久性保障较弱因为新的数据最初只有单个副本。
  • (B) 对上面模式的一个改进是在任意一个节点收到更新数据请求的同时异步的发送更新给所有可用节点。这也被认为是定向的反熵。

  • 与纯粹的反熵相比,这种做法只用一点小小的性能牺牲就极大地提高了一致性。然而,正式一致性和持久性保持不变。

  • 假如某些节点因为网络故障或是节点失效在当时是不可用的,更新最终也会通过反熵传播过程来传递到该节点。
  • (C) 在前一个模式中,使用提示移交技术 [8] 可以更好地处理某个节点的操作失败。对于失效节点的预期更新被记录在额外的代理节点上,并且标明一旦特点节点可用就要将更新传递给该节点。这样做提高了一致性,降低了复制收敛时间。
  • (D, 一次性读写)因为提示移交的责任节点也有可能在将更新传递出去之前就已经失效,在这种情况下就有必要通过所谓的读修复来保证一致性。每个读操作都会启动一个异步过程,向存储这条数据的所有节点请求一份数据摘要(像签名或者hash),如果发现各节点返回的摘要不一致则统一各节点上的数据版本。我们用一次性读写来命名组合了A、B、C、D的技术- 他们都没有提供严格的一致性保证,但是作为一个自备的方法已经可以用于实践了。
  • (E, 读若干写若干) 上面的策略是降低了复制收敛时间的启发式增强。为了保证更强的一致性,必须牺牲可用性来保证一定的读写重叠。 通常的做法是同时写入W个副本而不是一个,读的时候也要读R个副本。

  • 首先,可以配置写副本数W>1。

  • 其次,因为R+W>N,写入的节点和读取的节点之间必然会有重叠,所以读取的多个数据副本里至少会有一个是比较新的数据(上面的图中 W=2, R=3, N=4 )。这样在读写请求依序进行的时候(写执行完再读)能够保证一致性(对于单个用户的读写一致性),但是不能保障全局的读一致性。用下面图示里的例子来看,R=2,W=2,N=3,因为写操作对于两个副本的更新是非事务的,在更新没有完成的时候读就可能读到两个都是旧值或者一新一旧:

  • 对于某种读延迟的要求,设置R和W的不同值可以调整写延迟与持久性,反之亦然。
  • 如果W<=N/2,并发的多个写入会写到不同的若干节点(如,写操作A写前N/2个,B写后N/2个)。 设置 W>N/2 可以保证在符合回滚模型的原子读改写时及时检测到冲突。

  • 严格来讲,这种模式虽然可以容忍个别节点的失效, 但是对于网络隔离的容错性并不好。在实践中,常使用”近似数量通过“这样的方法,通过牺牲一致性来提高某些情景下的可用性。

  • (F, 读全部写若干)读一致性问题可以通过在读数据的时候访问所有副本(读数据或者检查摘要)来减轻。这确保了只要有至少一个节点上的数据更新新的数据就能被读取者看到。但是在网络隔离的情况下这种保证就不能起到作用了。
  • (G, 主从) 这种技术常被用来提供原子写或者 冲突检测持久级别的读改写。为了实现冲突预防级别,必须要用一种集中管理方式或者是锁。最简单的策略是用主从异步复制。对于特定数据项的写操作全部被路由到一个中心节点,并在上面顺序执行。这种情况下主节点会成为瓶颈,所以必须要将数据划分成一个个独立的片区(不同片有不同的master),这样才能提供扩展性。
  • (H, Transactional Read Quorum Write Quorum and Read One Write All)? 更新多个副本的方法可以通过使用事务控制技术来避免写冲突。 众所周知的方法是使用两阶段提交协议。但两阶段提交并不是完全可靠的,因为协调者失效可能会造成资源阻塞。 PAXOS提交协议 [20, 21] 是更可靠的选择,但会损失一点性能。 在这个基础上再向前一小步就是读一个副本写所有副本,这种方法把所有副本的更新放在一个事务中,它提供了强容错一致性但会损失掉一些性能和可用性。

上面分析中的一些权衡有必要再强调一下:

  • 一致性与可用性。?严密的权衡已经由CAP理论给出了。在网络隔离的情况下,数据库要么将数据集中,要么既要接受数据丢失的风险。
  • 一致性与扩展性。?看得出即使读写一致性保证降低了副本集的扩展性,只有在原子写模型中才可以以一种相对可扩展的方式处理写冲突。原子读改写模型通过给数据加上临时性的全局锁来避免冲突。这表明, 数据或操作之间的依赖,即使是很小范围内或很短时间的,也会损害扩展性。所以精心设计数据模型,将数据分片分开存放对于扩展性非常重要。
  • 一致性与延迟。?如上所述,当数据库需要提供强一致性或者持久性的时候应该偏向于读写所有副本技术。但是很明显一致性与请求延迟成反比,所以使用若干副本技术会是比较中允的办法。
  • 故障转移与一致性/扩展性/延迟。有趣的是容错性与一致性、扩展性、延迟的取舍冲突并不剧烈。通过合理的放弃一些性能与一致性,集群可以容忍多达 up to 的节点失效。这种折中在两阶段提交与 PAXOS 协议的区别里体现得很明显。这种折中的另一个例子是增加特定的一致性保障,比如使用严格会话进程的“读己所写”,但这又增加了故障转移的复杂性 [22]。

反熵协议, 谣言传播算法

让我们从以下场景开始:

有许多节点,每条数据会在其中的若干的节点上面存有副本。每个节点都可以单独处理更新请求,每个节点定期和其他节点同步状态,如此一段时间之后所有的副本都会趋向一致。同步过程是怎样进行的?同步何时开始?怎样选择同步的对象?怎么交换数据?我们假定两个节点总是用较新版本的数据覆盖旧的数据或者两个版本都保留以待应用层处理。

这个问题常见于数据一致性维护和集群状态同步(如集群成员信息传播)等场景。虽然引入一个监控数据库并制定同步计划的协调者可以解决这个问题,但是去中心化的数据库能够提供更好的容错性。去中心化的主要做法是利用精心设计的传染协议[7],这种协议相对简单,但是提供了很好的收敛时间,而且能够容忍任何节点的失效和网络隔离。尽管有许多类型的传染算法,我们只关注反熵协议,因为NoSQL数据库都在使用它。

反熵协议假定同步会按照一个固定进度表执行,每个节点定期随机或是按照某种规则选择另外一个节点交换数据,消除差异。有三种反风格的反熵协议:推,拉和混合。推协议的原理是简单选取一个随机节点然后把数据状态发送过去。在真实应用中将全部数据都推送出去显然是愚蠢的,所以节点一般按照下图所示的方式工作。

节点A作为同步发起者准备好一份数据摘要,里面包含了A上数据的指纹。节点B接收到摘要之后将摘要中的数据与本地数据进行比较,并将数据差异做成一份摘要返回给A。最后,A发送一个更新给B,B再更新数据。拉方式和混合方式的协议与此类似,就如上图所示的。

反熵协议提供了足够好的收敛时间和扩展性。下图展示了一个在100个节点的集群中传播一个更新的模拟结果。在每次迭代中,每个节点只与一个随机选取的对等节点发生联系。

可以看到,拉方式的收敛性比推方式更好,这可以从理论上得到证明[7]。而且推方式还存在一个“收敛尾巴”的问题。在多次迭代之后,尽管几乎遍历到了所有的节点,但还是有很少的一部分没受到影响。与单纯的推和拉方式相比, 混合方式的效率更高,所以实际应用中通常使用这种方式。反熵是可扩展的,因为平均转换时间以集群规模的对数函数形式增长。

尽管这些技术看起来很简单,仍然有许多研究关注于不同约束条件下反熵协议的性能表现。其中之一通过一种更有效的结构使用网络拓扑来取代随机选取 [10] 。在网络带宽有限的条件下调整传输率或使用先进的规则来选取要同步的数据 [9]。摘要计算也面临挑战,数据库会维护一份最近更新的日志以有助于摘要计算。

最终一致数据类型Eventually Consistent Data Types

在上一节我们假定两个节点总是合并他们的数据版本。但要解决更新冲突并不容易,让所有副本都最终达到一个语义上正确的值出乎意料的难。一个众所周知的例子是Amazon Dynamo数据库[8]中已经删除的条目可以重现。

我们假设一个例子来说明这个问题:数据库维护一个逻辑上的全局计数器,每个节点可以增加或者减少计数。虽然每个节点可以在本地维护一个自己的值,但这些本地计数却不能通过简单的加减来合并。假设这样一个例子:有三个节点A、B和C,每个节点执行了一次加操作。如果A从B获得一个值,并且加到本地副本上,然后C从B获得值,然后C再从A获得值,那么C最后的值是4,而这是错误的。解决这个问题的方法是用一个类似于向量时钟[19]的数据结构为每个节点维护一对计数器[1]: class Counter {

int[] plus int[] minus

int NODE_ID

increment() { plus[NODE_ID]++

}

decrement() { minus[NODE_ID]++

}

get() { return sum(plus) – sum(minus)

}

merge(Counter other) { for i in 1..MAX_ID {

     plus[i] = max(plus[i], other.plus[i])
     minus[i] = max(minus[i], other.minus[i])

  }

}

}

Cassandra用类似的方法计数[11]。利用基于状态的或是基于操作的复制理论也可以设计出更复杂的最终一致的数据结构。例如,[1]中就提及了一系列这样的数据结构,包括:

  • 计数器(加减操作)
  • 集合(添加和移除操作)
  • 图(增加边或顶点,移除边或顶点)
  • 列表(插入某位置或者移除某位置)

最终一致数据类型的功能通常是有限的,还会带来额外的性能开销。

数据放置

这部分主要关注控制在分布式数据库中放置数据的算法。这些算法负责把数据项映射到合适的物理节点上,在节点间迁移数据以及像内存这样的资源的全局调配。

均衡数据

我们还是从一个简单的协议开始,它可以提供集群节点间无缝的数据迁移。这常发生于像集群扩容(加入新节点),故障转移(一些节点宕机)或是均衡数据(数据在节点间的分布不均衡)这样的场景。如下图A中所描绘的场景 – 有三个节点,数据随便分布在三个节点上(假设数据都是key-value型)。

如果数据库不支持数据内部均衡,就要在每个节点上发布数据库实例,如上面图B所示。这需要手动进行集群扩展,停掉要迁移的数据库实例,把它转移到新节点上,再在新节点上启动,如图C所示。尽管数据库能够监控到每一条记录,包括MongoDB, Oracle Coherence, 和还在开发中的 Redis Cluster 在内的许多系统仍然使用的是自动均衡技术。也即,将数据分片并把每个数据分片作为迁移的最小单位,这是基于效率的考虑。很明显分片数会比节点数多,数据分片可以在各节点间平均分布。按照一种简单的协议即可实现无缝数据迁移,这个协议可以在迁移数据分片的时候重定向客户的数据迁出节点和迁入节点。下图描绘了一个Redis Cluster中实现的get(key)逻辑的状态机。

假定每个节点都知道集群拓扑,能够把任意key映射到相应的数据分片,把数据分片映射到节点。如果节点判断被请求的key属于本地分片,就会在本地查找(上图中上面的方框)。假如节点判断请求的key属于另一个节点X,他会发送一个永久重定向命令给客户端(上图中下方的方框)。永久重定向意味着客户端可以缓存分片和节点间的映射关系。如果分片迁移正在进行,迁出节点和迁入节点会标记相应的分片并且将分片的数据加锁逐条加锁然后开始移动。迁出节点首先会在本地查找key,如果没有找到,重定向客户端到迁入节点,假如key已经迁移完毕的话。这种重定向是一次性的,并且不能被缓存。迁入节点在本地处理重定向,但定期查询在迁移还没完成前被永久重定向。

动态环境中的数据分片和复制

我们关注的另一个问题是怎么把记录映射到物理节点。比较直接的方法是用一张表来记录每个范围的key与节点的映射关系,一个范围的key对应到一个节点,或者用key的hash值与节点数取模得到的值作为节点ID。但是hash取模的方法在集群发生更改的情况下就不是很好用,因为增加或者减少节点都会引起集群内的数据彻底重排。导致很难进行复制和故障恢复。

有许多方法在复制和故障恢复的角度进行了增强。最著名的就是一致性hash。网上已经有很多关于一致性hash的介绍了,所以在这里我只提供一个基本介绍,仅仅为了文章内容的完整性。下图描绘了一致性hash的基本原理:

一致性hash从根本上来讲是一个键值映射结构 – 它把键(通常是hash过的)映射到物理节点。键经过hash之后的取值空间是一个有序的定长二进制字符串,很显然每个在此范围内的键都会被映射到图A中A、B、C三个节点中的某一个。为了副本复制,将取值空间闭合成一个环,沿环顺时针前行直到所有副本都被映射到合适的节点上,如图B所示。换句话说,Y将被定位在节点B上,因为它在B的范围内,第一个副本应该放置在C,第二个副本放置在A,以此类推。

这种结构的好处体现在增加或减少一个节点的时候,因为它只会引起临接区域的数据重新均衡。如图C所示,节点D的加入只会对数据项X产生影响而对Y无影响。同样,移除节点B(或者B失效)只会影响Y和X的副本,而不会对X自身造成影响。但是,正如参考资料[8]中所提到的,这种做法在带来好处的同时也有弱点,那就是重新均衡的负担都由邻节点承受了,它们将移动大量的数据。通过将每个节点映射到多个范围而不是一个范围可以一定程度上减轻这个问题带来的不利影响,如图D所示。这是一个折中,它避免了重新均衡数据时负载过于集中,但是与基于模块的映射相比,保持了总均衡数量适当降低。

给大规模的集群维护一个完整连贯的hash环很不容易。对于相对小一点的数据库集群就不会有问题,研究如何在对等网络中将数据放置与网络路由结合起来很有意思。一个比较好的例子是Chord算法,它使环的完整性让步于单个节点的查找效率。Chord算法也使用了环映射键到节点的理念,在这方面和一致性hash很相似。不同的是,一个特定节点维护一个短列表,列表中的节点在环上的逻辑位置是指数增长的(如下图)。这使得可以使用二分搜索只需要几次网络跳跃就可以定位一个键。 ?

这张图画的是一个由16个节点组成的集群,描绘了节点A是如何查找放在节点D上的key的。 (A) 描绘了路由,(B) 描绘了环针对节点A、B、C的局部图像。在参考资料[15]中有更多关于分散式系统中的数据复制的内容。

按照多个属性的数据分片

当只需要通过主键来访问数据的时候,一致性hash的数据放置策略很有效,但是当需要按照多个属性来查询的时候事情就会复杂得多。一种简单的做法(MongoDB使用的)是用主键来分布数据而不考虑其他属性。这样做的结果是依据主键的查询可以被路由到接个合适的节点上,但是对其他查询的处理就要遍历集群的所有节点。查询效率的不均衡造成下面的问题:

有一个数据集,其中的每条数据都有若干属性和相应的值。是否有一种数据分布策略能够使得限定了任意多个属性的查询会被交予尽量少的几个节点执行?

HyperDex数据库提供了一种解决方案。基本思想是把每个属性视作多维空间中的一个轴,将空间中的区域映射到物理节点上。一次查询会被对应到一个由空间中多个相邻区域组成的超平面,所以只有这些区域与该查询有关。让我们看看参考资料[6]中的一个例子:

每一条数据都是一条用户信息,有三个属性First Name 、Last Name 和Phone Number。这些属性被视作一个三维空间,可行的数据分布策略是将每个象限映射到一个物理节点。像“First Name = John”这样的查询对应到一个贯穿4个象限的平面,也即只有4个节点会参与处理此次查询。有两个属性限制的查询对应于一条贯穿两个象限的直线,如上图所示,因此只有2个节点会参与处理。

这个方法的问题是空间象限会呈属性数的指数函数增长。结果就会是,只有几个属性限制的查询会投射到许多个空间区域,也即许多台服务器。将一个属性较多的数据项拆分成几个属性相对较少的子项,并将每个子项都映射到一个独立的子空间,而不是将整条数据映射到一个多维空间,这样可以一定程度上缓解这个问题:

这样能够提供更好的查询到节点的映射,但是增加了集群协调的复杂度,因为这种情况下一条数据会散布在多个独立的子空间,而每个子空间都对应各自的若干个物理节点,数据更新时就必须考虑事务问题。参考资料 [6]有这种技术的更多介绍和实现细节。

钝化副本

有的应用有很强的随机读取要求,这就需要把所有数据放在内存里。在这种情况下,将数据分片并把每个分片主从复制通常需要两倍以上的内存,因为每个数据都要在主节点和从节点上各有一份。为了在主节点失效的时候起到代替作用,从节点上的内存大小应该和主节点一样。如果系统能够容忍节点失效的时候出现短暂中断或性能下降,也可以不要分片。

下面的图描绘了4个节点上的16个分片,每个分片都有一份在内存里,副本存在硬盘上:

灰色箭头突出了节点2上的分片复制。其他节点上的分片也是同样复制的。红色箭头描绘了在节点2失效的情况下副本怎样加载进内存。集群内副本的均匀分布使得只需要预留很少的内存就可以存放节点失效情况下激活的副本。在上面的图里,集群只预留了1/3的内存就可以承受单个节点的失效。特别要指出的是副本的激活(从硬盘加载入内存)会花费一些时间,这会造成短时间的性能下降或者正在恢复中的那部分数据服务中断。

系统协调

在这部分我们将讨论与系统协调相关的两种技术。分布式协调是一个比较大的领域,数十年以来有很多人对此进行了深入的研究。这篇文章里只涉及两种已经投入实用的技术。关于分布式锁,consensus协议以及其他一些基础技术的内容可以在很多书或者网络资源中找到,也可以去看参考资料[17, 18, 21]。

故障检测

故障检测是任何一个拥有容错性的分布式系统的基本功能。实际上所有的故障检测协议都基于心跳通讯机制,原理很简单,被监控的组件定期发送心跳信息给监控进程(或者由监控进程轮询被监控组件),如果有一段时间没有收到心跳信息就被认为失效了。除此之外,真正的分布式系统还要有另外一些功能要求:

  • 自适应。故障检测应该能够应对暂时的网络故障和延迟,以及集群拓扑、负载和带宽的变化。但这有很大难度,因为没有办法去分辨一个长时间没有响应的进程到底是不是真的失效了,因此,故障检测需要权衡故障识别时间(花多长时间才能识别一个真正的故障,也即一个进程失去响应多久之后会被认为是失效)和虚假警报率之间的轻重。这个权衡因子应该能够动态自动调整。
  • 灵活性。乍看上去,故障检测只需要输出一个表明被监控进程是否处于工作状态的布尔值,但在实际应用中这是不够的。我们来看参考资料[12]中的一个类似MapReduce的例子。有一个由一个主节点和若干工作节点组成的分布式应用,主节点维护一个作业列表,并将列表中的作业分配给工作节点。主节点能够区分不同程度的失败。如果主节点怀疑某个工作节点挂了,他就不会再给这个节点分配作业。其次,随着时间推移,如果没有收到该节点的心跳信息,主节点就会把运行在这个节点上的作业重新分配给别的节点。最后,主节点确认这个节点已经失效,并释放所有相关资源。
  • 可扩展性和健壮性。失败检测作为一个系统功能应该能够随着系统的扩大而扩展。他应该是健壮和一致的,也即,即使在发生通讯故障的情况下,系统中的所有节点都应该有一个一致的看法(即所有节点都应该知道哪些节点是不可用的,那些节点是可用的,各节点对此的认知不能发生冲突,不能出现一部分节点知道某节点A不可用,而另一部分节点不知道的情况)

所谓的累计失效检测器[12]可以解决前两个问题,Cassandra[16]对它进行了一些修改并应用在产品中。其基本工作流程如下:

  • 对于每一个被监控资源,检测器记录心跳信息到达时间Ti。
  • 计算在统计预测范围内的到达时间的均值和方差。
  • 假定到达时间的分布已知(下图包括一个正态分布的公式),我们可以计算心跳延迟(当前时间t_now和上一次到达时间Tc之间的差值) 的概率,用这个概率来判断是否发生故障。如参考资料[12]中所建议的,可以使用对数函数来调整它以提高可用性。在这种情况下,输出1意味着判断错误(认为节点失效)的概率是10%,2意味着1%,以此类推。

根据重要程度不同来分层次组织监控区,各区域之间通过谣言传播协议或者中央容错库同步,这样可以满足扩展性的要求,又可以防止心跳信息在网络中泛滥[14]。如下图所示(6个故障检测器组成了两个区域,互相之间通过谣言传播协议或者像ZooKeeper这样的健壮性库来联系):

协调者竞选

协调者竞选是用于强一致性数据库的一个重要技术。首先,它可以组织主从结构的系统中主节点的故障恢复。其次,在网络隔离的情况下,它可以断开处于少数的那部分节点,以避免写冲突。

Bully 算法是一种相对简单的协调者竞选算法。MongoDB 用了这个算法来决定副本集中主要的那一个。Bully 算法的主要思想是集群的每个成员都可以声明它是协调者并通知其他节点。别的节点可以选择接受这个声称或是拒绝并进入协调者竞争。被其他所有节点接受的节点才能成为协调者。节点按照一些属性来判断谁应该胜出。这个属性可以是一个静态ID,也可以是更新的度量像最近一次事务ID(最新的节点会胜出)。

下图的例子展示了bully算法的执行过程。使用静态ID作为度量,ID值更大的节点会胜出:

  1. 最初集群有5个节点,节点5是一个公认的协调者。
  2. 假设节点5挂了,并且节点2和节点3同时发现了这一情况。两个节点开始竞选并发送竞选消息给ID更大的节点。
  3. 节点4淘汰了节点2和3,节点3淘汰了节点2。
  4. 这时候节点1察觉了节点5失效并向所有ID更大的节点发送了竞选信息。
  5. 节点2、3和4都淘汰了节点1。
  6. 节点4发送竞选信息给节点5。
  7. 节点5没有响应,所以节点4宣布自己当选并向其他节点通告了这一消息。

协调者竞选过程会统计参与的节点数目并确保集群中至少一半的节点参与了竞选。这确保了在网络隔离的情况下只有一部分节点能选出协调者(假设网络中网络会被分割成多块区域,之间互不联通,协调者竞选的结果必然会在节点数相对比较多的那个区域中选出协调者,当然前提是那个区域中的可用节点多于集群原有节点数的半数。如果集群被隔离成几个区块,而没有一个区块的节点数多于原有节点总数的一半,那就无法选举出协调者,当然这样的情况下也别指望集群能够继续提供服务了)。

参考资料

  1. M. Shapiro et al. A Comprehensive Study of Convergent and Commutative Replicated Data Types
  2. I. Stoica et al. Chord: A Scalable Peer-to-peer Lookup Service for Internet Applications
  3. R. J. Honicky, E.L.Miller. Replication Under Scalable Hashing: A Family of Algorithms for Scalable Decentralized Data Distribution
  4. G. Shah. Distributed Data Structures for Peer-to-Peer Systems
  5. A. Montresor, Gossip Protocols for Large-Scale Distributed Systems
  6. R. Escriva, B. Wong, E.G. Sirer. HyperDex: A Distributed, Searchable Key-Value Store
  7. A. Demers et al. Epidemic Algorithms for Replicated Database Maintenance
  8. G. DeCandia, et al. Dynamo: Amazon’s Highly Available Key-value Store
  9. R. van Resesse et al. Efficient Reconciliation and Flow Control for Anti-Entropy Protocols
  10. S. Ranganathan et al. Gossip-Style Failure Detection and Distributed Consensus for Scalable Heterogeneous Clusters
  11. http://www.slideshare.net/kakugawa/distributed-counters-in-cassandra-cassandra-summit-2010
  12. N. Hayashibara, X. Defago, R. Yared, T. Katayama. The Phi Accrual Failure Detector
  13. M.J. Fischer, N.A. Lynch, and M.S. Paterson. Impossibility of Distributed Consensus with One Faulty Process
  14. N. Hayashibara, A. Cherif, T. Katayama. Failure Detectors for Large-Scale Distributed Systems
  15. M. Leslie, J. Davies, and T. Huffman. A Comparison Of Replication Strategies for Reliable Decentralised Storage
  16. A. Lakshman, P.Malik. Cassandra – A Decentralized Structured Storage System
  17. N. A. Lynch. Distributed Algorithms
  18. G. Tel. Introduction to Distributed Algorithms
  19. http://basho.com/blog/technical/2010/04/05/why-vector-clocks-are-hard/
  20. L. Lamport. Paxos Made Simple
  21. J. Chase. Distributed Systems, Failures, and Consensus
  22. W. Vogels. Eventualy Consistent – Revisited
  23. J. C. Corbett et al. Spanner: Google’s Globally-Distributed Database

1 vote, average: 5.00 out of 51 vote, average: 5.00 out of 51 vote, average: 5.00 out of 51 vote, average: 5.00 out of 51 vote, average: 5.00 out of 5 (*1 个评分,平均: 5.00*) Loading ... Loading ...

时间复杂度

Posted on

时间复杂度

时间频度

一个算法执行所耗费的时间,从理论上是不能算出来的,必须上机运行测试才能知道。但我们不可能也没有必要对每个算法都上机测试,只需知道哪个算法花费的时间多,哪个算法花费的时间少就可以了。并且一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度。记为T(n)。 计算方法

  1. 一般情况下,算法的基本操作重复执行的次数是模块n的某一个函数f(n),因此,算法的时间复杂度记做:T(n)=O(f(n)) 分析:随着模块n的增大,算法执行的时间的增长率和f(n)的增长率成正比,所以f(n)越小,算法的时间复杂度越低,算法的效率越高。

  2. 在计算时间复杂度的时候,先找出算法的基本操作,然后根据相应的各语句确定它的执行次数,再找出T(n)的同数量级(它的同数量级有以下:1,Log2n ,n ,nLog2n ,n的平方,n的三次方,2的n次方,n!),找出后,f(n)=该数量级,若T(n)/f(n)求极限可得到一常数c,则时间复杂度T(n)=O(f(n)) 例:算法:

for(i=1;i<=n;++i) {

for(j=1;j<=n;++j) {

c[ i ][ j ]=0; //该步骤属于基本操作 执行次数:n的平方 次 for(k=1;k<=n;++k)

c[ i ][ j ]+=a[ i ][ k ]/*b[ k ][ j ]; //该步骤属于基本操作 执行次数:n的三次方 次 }

} 空间复杂度

一个程序的空间复杂度是指运行完一个程序所需内存的大小。利用程序的空间复杂度,可以对程序的运行所需要的内存多少有个预先估计。一个程序执行时除了需要存储空间和存储本身所使用的指令、常数、变量和输入数据外,还需要一些对数据进行操作的工作单元和存储一些为现实计算所需信息的辅助空间。程序执行时所需存储空间包括以下两部分。 (1)固定部分。这部分空间的大小与输入/输出的数据的个数多少、数值无关。主要包括指令空间(即代码空间)、数据空间(常量、简单变量)等所占的空间。这部分属于静态空间。

(2)可变空间,这部分空间的主要包括动态分配的空间,以及递归栈所需的空间等。这部分的空间大小与算法有关。 一个算法所需的存储空间用f(n)表示。

S(n)=O(f(n)) 其中n为问题的规模,S(n)表示空间复杂度。

则有 T(n)= n的平方+n的三次方,根据上面括号里的同数量级,我们可以确定 n的三次方 为T(n)的同数量级 则有f(n)= n的三次方,然后根据T(n)/f(n)求极限可得到常数c

则该算法的 时间复杂度:T(n)=O(n^3) 注:n^3即是n的3次方。 3.在pascal中比较容易理解,容易计算的方法是:看看有几重for循环,只有一重则时间复杂度为O(n),二重则为O(n^2),依此类推,如果有二分则为O(logn),二分例如快速幂、二分查找,如果一个for循环套一个二分,那么时间复杂度则为O(nlogn)。

分类 按数量级递增排列,常见的时间复杂度有:

常数阶O(1),对数阶O(log2n),线性阶O(n), 线性对数阶O(nlog2n),平方阶O(n^2),立方阶O(n^3),...,

k次方阶O(n^k), 指数阶O(2^n) 。随着问题规模n的不断增大,上述时间复杂度不断增大,算法的执行效率越低。 关于对其的理解

《数据结构(C语言版)》------严蔚敏 吴伟民编著 第15页有句话"整个算法的执行时间与基本操作重复执行的次数成正比。" 基本操作重复执行的次数是问题规模n的某个函数f(n),于是算法的时间量度可以记为:T(n) = O( f(n) )

如果按照这么推断,T(n)应该表示的是算法的时间量度,也就是算法执行的时间。 而该页对“语句频度”也有定义:指的是该语句重复执行的次数。

如果是基本操作所在语句重复执行的次数,那么就该是f(n)。 上边的n都表示的问题规模。