散列表 分布式散列表(DHT)哈希表

★预备知识


  (如果你自认为是一个熟练的程序员,请直接略过“预备知识”这个章节,看下一章节)
 

◇什么是“散列/哈希(hash)”?


  (注:在本文中,凡是提及“散列”或“哈希”或“hash”,均表示相同含义)
  关于 hash 的概念,俺曾经写过一篇相关的扫盲教程《扫盲文件完整性校验——关于散列值和数字签名》,不了解此概念的同学,可以先看看。
  老实说,如果你还没有搞明白 hash 的概念,就不要浪费时间看本文的后续部分啦。
 

◇什么是“散列表/哈希表(hash table)”?


  “散列表/哈希表”是用来存储“键值对”的一种容器。“键值对”洋文称之为“key/value pairs”,简称“K/V”。有了“散列表”,你可以很方便快速地通过 key 来获得 value。
  举个例子:
  手机通讯簿可以通俗理解成一个“散列表”。里面的每一条记录都包含“姓名”和“电话号码”。“姓名”相当于“键值对”中的 key,电话号码相当于 value。你可以通过姓名方便地查找出电话号码。
 

◇如何实现散列表?


  (考虑到本文的完整性,【简单介绍】一下散列表的实现)
  在散列表这种数据结构中,会包含 N 个 bucket(桶)。对于某个具体的散列表,N(桶的数量)通常是【固定不变】的。于是可以对每个桶进行编号,从 0 到 N-1。
  “桶”是用来存储“键值对”的,你可以把它通俗理解成一个动态数组,里面可以存放【多个】“键值对”。
  下面这张图是从维基百科上剽窃来的。它展示了散列表的【查找】原理。当使用某个 key 进行查找,会先用某个散列函数计算这个 key 的散列值。得到散列值通常是一个整数,然后用散列值对 N(桶数)进行“取模”运算(除法求余数),就可以算出对应的桶编号。
  (注:取模运算是最常用的做法,但不是唯一的做法)
 


(使用散列表存储电话簿的示意图,剽窃自维基百科)

◇什么是“散列表”的【碰撞/冲突】(Collision)?


  在俺那篇扫盲教程中,已经介绍了“散列碰撞”(也称为“散列冲突”)的概念。
  当两个不同的 key 进行哈希计算却得到【相同的散列值】,就是所谓的【散列函数碰撞】。一旦出现这种情况,这两个 key 对应的两个键值对就会被存储在【同一个】桶(bucket)里面。
  另一种情况是:虽然计算出来的散列值【不同】,但经过“取模运算”之后却得到【相同】的桶编号。这时候也会出现:两个键值对存储在一个桶里面。
 


(出现“散列碰撞”的示意图,剽窃自维基百科)


  如果某个哈希表在存储数据时【完全没有碰撞】,那么每个桶里面都只有 0个 或 1个 键值对。查找起来就非常快。
  反之,如果某个哈希表在存储数据时出现【严重碰撞】,就会导致某些桶里面存储了一大坨的键值对。将来查找 key 的时候,如果定位到的是这种“大桶”,就需要在这个桶里面逐一比对 key 是否相同——查找效率就会变得很差。
 

◇“散列表”有哪些优点?


  主要优点是:(当数据量很大时)散列表可以提供快速且稳定的查找速度。
  当然,这里有个前提就是:散列函数要【足够好】——
1、计算出的散列值要足够离散(从而使得不同的键值对可以比较【均匀】地分配到各个桶里面)
2、要尽可能降低碰撞(碰撞会降低性能)
  另一个前提是:桶的数量也有一定的讲究——
1、桶数要足够大。否则的话,【必定会】导致某些桶里面的键值对太多(这点很明显,没想明白的同学,可参见“抽屉原理”)
2、(如果用常见的“取模”映射到桶)桶的总数最好是【质数/素数】(这个不解释,爱思考的同学自己想一下)


★分布式散列表(DHT)概述

◇啥是 DHT?


  “分布式散列表”也称为“分布式哈希表”,洋文是“distributed hash table”,简称 DHT。
  “分布式散列表”在概念上类似与传统的“散列表”,差异在于——
“传统的散列表”主要是用于单机上的某个软件中;
“分布式散列表”主要是用于分布式系统(此时,分布式系统的节点可以通俗理解为散列表中的 bucket)

  “分布式散列表”主要是用来存储大量的(甚至是海量的)数据。在实际使用场景中,直接对所存储的“每一个业务数据”计算散列值,然后用散列值作为 key,业务数据本身是 value。
 


(分布式散列表的示意图,此图剽窃自维基百科)


  (为了偷懒,本文以下部分均使用 DHT 来表示“分布式散列表”)
 

◇为啥会出现 DHT?


  在 P2P 文件共享的发展史上,出现过3种不同的技术路线(三代)。

  第1代
  采用【中央服务器】的模式——每个节点都需要先连接到中央服务器,然后才能查找到自己想要的文件在哪里。
  这种技术的最大缺点是——中央服务器成为整个 P2P 网络的【单点故障】。
  (关于“单点故障”这个概念,可以看另一篇介绍:《聊聊“单点故障”——关于“德国空难”和“李光耀”的随想》)
  这类 p2p 的典型代表是 Napster

  第2代
  采用【广播】的模式——要找文件的时候,每个节点都向自己相连的【所有节点】进行询问;被询问的节点如果不知道这个文件在哪里,就再次进行“广播”......如此往复,直至找到所需文件。
  这种技术的最大缺点是——会引发“广播风暴”并严重占用网络带宽,也会严重消耗节点的系统资源。即使在协议层面通过设置 TTL(time to live),限制查询过程只递归 N 轮,依然【无法】彻底解决此弊端。
  因为这种手法太吓人,获得“Query Flooding”的绰号。下面放一张示意图。
 


(示意图:第2代 P2P 的 Query Flooding)


  这类 p2p 的典型代表是 Gnutella 的早期版本。

  第3代
  这一代采用的技术就是今天要聊的 DHT。
  通过 DHT 这个玩意儿,不但避免了第一代技术的【单点故障】,也避免了第二代技术的【广播风暴】。
 

◇DHT 有哪些应用场景?


  DHT 最早用于 P2P 文件共享和文件下载(比如:BT、电驴、电骡),之后也被广泛用于某些分布式系统中,比如:

分布式文件系统
分布式缓存
暗网(比如: I2PFreenet
无中心的聊天工具/IM(比如: TOX
无中心的微博客/microblogging(比如: Twister
无中心的社交网络/SNS

  正是因为【无中心】的分布式系统普遍使用 DHT,所以本文开头称之为:分布式系统的【基础设施】。

https://program-think.blogspot.com/2017/09/Introduction-DHT-Kademlia-Chord.html

公众哈:微程序学堂

猜你喜欢

转载自blog.csdn.net/u013288190/article/details/125200943