Flexihash.php 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. <?php
  2. namespace Qii\Library;
  3. /**
  4. * Flexihash - A simple consistent hashing implementation for PHP.
  5. *
  6. * The MIT License
  7. *
  8. * Copyright (c) 2008 Paul Annesley
  9. *
  10. * Permission is hereby granted, free of charge, to any person obtaining a copy
  11. * of this software and associated documentation files (the "Software"), to deal
  12. * in the Software without restriction, including without limitation the rights
  13. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  14. * copies of the Software, and to permit persons to whom the Software is
  15. * furnished to do so, subject to the following conditions:
  16. *
  17. * The above copyright notice and this permission notice shall be included in
  18. * all copies or substantial portions of the Software.
  19. *
  20. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  21. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  22. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  23. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  24. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  25. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  26. * THE SOFTWARE.
  27. *
  28. * @author Paul Annesley
  29. * @link http://paul.annesley.cc/
  30. * @copyright Paul Annesley, 2008
  31. * @comment by MyZ (http://blog.csdn.net/mayongzhan)
  32. */
  33. /**
  34. * A simple consistent hashing implementation with pluggable hash algorithms.
  35. *
  36. * @author Paul Annesley
  37. * @package Flexihash
  38. * @licence http://www.opensource.org/licenses/mit-license.php
  39. *
  40. * 使用方法:
  41. *
  42. * $hash = new \Qii\Library\Flexihash(null, 64);
  43. * $hash->addTarget('Node1');
  44. * $hash->addTarget('Node2');
  45. * $hash->addTarget('Node3');
  46. * $hash->addTarget('Node4');
  47. * print_r($hash->getAllTargets());
  48. * echo PHP_EOL;
  49. * print_r($hash->lookup('www.baidu.com'));
  50. *
  51. * echo PHP_EOL;
  52. * $hash->removeTarget('Node1');
  53. * $hash->removeTarget('Node4');
  54. * print_r($hash->lookup('www.baidu.com'));
  55. */
  56. class Flexihash
  57. {
  58. /**
  59. * The number of positions to hash each target to.
  60. *
  61. * @var int
  62. * @comment 虚拟节点数,解决节点分布不均的问题
  63. */
  64. private $_replicas = 64;
  65. /**
  66. * The hash algorithm, encapsulated in a Flexihash_Hasher implementation.
  67. * @var object Flexihash_Hasher
  68. * @comment 使用的hash方法 : md5,crc32
  69. */
  70. private $_hasher;
  71. /**
  72. * Internal counter for current number of targets.
  73. * @var int
  74. * @comment 节点记数器
  75. */
  76. private $_targetCount = 0;
  77. /**
  78. * Internal map of positions (hash outputs) to targets
  79. * @var array { position => target, ... }
  80. * @comment 位置对应节点,用于lookup中根据位置确定要访问的节点
  81. */
  82. private $_positionToTarget = array();
  83. /**
  84. * Internal map of targets to lists of positions that target is hashed to.
  85. * @var array { target => [ position, position, ... ], ... }
  86. * @comment 节点对应位置,用于删除节点
  87. */
  88. private $_targetToPositions = array();
  89. /**
  90. * Whether the internal map of positions to targets is already sorted.
  91. * @var boolean
  92. * @comment 是否已排序
  93. */
  94. private $_positionToTargetSorted = false;
  95. /**
  96. * Constructor
  97. * @param object $hasher Flexihash_Hasher
  98. * @param int $replicas Amount of positions to hash each target to.
  99. * @comment 构造函数,确定要使用的hash方法和虚拟节点数,虚拟节点数越多,分布越均匀,但程序的分布式运算越慢
  100. */
  101. public function __construct(Flexihash_Hasher $hasher = null, $replicas = null)
  102. {
  103. $this->_hasher = $hasher ? $hasher : new Flexihash_Crc32Hasher();
  104. if (!empty($replicas)) $this->_replicas = $replicas;
  105. }
  106. /**
  107. * Add a target.
  108. * @param string $target
  109. * @chainable
  110. * @comment 添加节点,根据虚拟节点数,将节点分布到多个虚拟位置上
  111. */
  112. public function addTarget($target)
  113. {
  114. if (isset($this->_targetToPositions[$target])) {
  115. throw new \Flexihash_Exception("Target '$target' already exists.");
  116. }
  117. $this->_targetToPositions[$target] = array();
  118. // hash the target into multiple positions
  119. for ($i = 0; $i < $this->_replicas; $i++) {
  120. $position = $this->_hasher->hash($target . $i);
  121. $this->_positionToTarget[$position] = $target; // lookup
  122. $this->_targetToPositions[$target] [] = $position; // target removal
  123. }
  124. $this->_positionToTargetSorted = false;
  125. $this->_targetCount++;
  126. return $this;
  127. }
  128. /**
  129. * Add a list of targets.
  130. * @param array $targets
  131. * @chainable
  132. */
  133. public function addTargets($targets)
  134. {
  135. foreach ($targets as $target) {
  136. $this->addTarget($target);
  137. }
  138. return $this;
  139. }
  140. /**
  141. * Remove a target.
  142. * @param string $target
  143. * @chainable
  144. */
  145. public function removeTarget($target)
  146. {
  147. if (!isset($this->_targetToPositions[$target])) {
  148. throw new \Flexihash_Exception("Target '$target' does not exist.");
  149. }
  150. foreach ($this->_targetToPositions[$target] as $position) {
  151. unset($this->_positionToTarget[$position]);
  152. }
  153. unset($this->_targetToPositions[$target]);
  154. $this->_targetCount--;
  155. return $this;
  156. }
  157. /**
  158. * A list of all potential targets
  159. * @return array
  160. */
  161. public function getAllTargets()
  162. {
  163. return array_keys($this->_targetToPositions);
  164. }
  165. /**
  166. * Looks up the target for the given resource.
  167. * @param string $resource
  168. * @return string
  169. */
  170. public function lookup($resource)
  171. {
  172. $targets = $this->lookupList($resource, 1);
  173. if (empty($targets)) throw new \Flexihash_Exception('No targets exist');
  174. return $targets[0];
  175. }
  176. /**
  177. * Get a list of targets for the resource, in order of precedence.
  178. * Up to $requestedCount targets are returned, less if there are fewer in total.
  179. *
  180. * @param string $resource
  181. * @param int $requestedCount The length of the list to return
  182. * @return array List of targets
  183. * @comment 查找当前的资源对应的节点,
  184. * 节点为空则返回空,节点只有一个则返回该节点,
  185. * 对当前资源进行hash,对所有的位置进行排序,在有序的位置列上寻找当前资源的位置
  186. * 当全部没有找到的时候,将资源的位置确定为有序位置的第一个(形成一个环)
  187. * 返回所找到的节点
  188. */
  189. public function lookupList($resource, $requestedCount)
  190. {
  191. if (!$requestedCount)
  192. throw new \Flexihash_Exception('Invalid count requested');
  193. // handle no targets
  194. if (empty($this->_positionToTarget))
  195. return array();
  196. // optimize single target
  197. if ($this->_targetCount == 1)
  198. return array_unique(array_values($this->_positionToTarget));
  199. // hash resource to a position
  200. $resourcePosition = $this->_hasher->hash($resource);
  201. $results = array();
  202. $collect = false;
  203. $this->_sortPositionTargets();
  204. // search values above the resourcePosition
  205. foreach ($this->_positionToTarget as $key => $value) {
  206. // start collecting targets after passing resource position
  207. if (!$collect && $key > $resourcePosition) {
  208. $collect = true;
  209. }
  210. // only collect the first instance of any target
  211. if ($collect && !in_array($value, $results)) {
  212. $results [] = $value;
  213. }
  214. // return when enough results, or list exhausted
  215. if (count($results) == $requestedCount || count($results) == $this->_targetCount) {
  216. return $results;
  217. }
  218. }
  219. // loop to start - search values below the resourcePosition
  220. foreach ($this->_positionToTarget as $key => $value) {
  221. if (!in_array($value, $results)) {
  222. $results [] = $value;
  223. }
  224. // return when enough results, or list exhausted
  225. if (count($results) == $requestedCount || count($results) == $this->_targetCount) {
  226. return $results;
  227. }
  228. }
  229. // return results after iterating through both "parts"
  230. return $results;
  231. }
  232. public function __toString()
  233. {
  234. return sprintf(
  235. '%s{targets:[%s]}',
  236. get_class($this),
  237. implode(',', $this->getAllTargets())
  238. );
  239. }
  240. // ----------------------------------------
  241. // private methods
  242. /**
  243. * Sorts the internal mapping (positions to targets) by position
  244. */
  245. private function _sortPositionTargets()
  246. {
  247. // sort by key (position) if not already
  248. if (!$this->_positionToTargetSorted) {
  249. ksort($this->_positionToTarget, SORT_REGULAR);
  250. $this->_positionToTargetSorted = true;
  251. }
  252. }
  253. }
  254. /**
  255. * Hashes given values into a sortable fixed size address space.
  256. *
  257. * @author Paul Annesley
  258. * @package Flexihash
  259. * @licence http://www.opensource.org/licenses/mit-license.php
  260. */
  261. interface Flexihash_Hasher
  262. {
  263. /**
  264. * Hashes the given string into a 32bit address space.
  265. *
  266. * Note that the output may be more than 32bits of raw data, for example
  267. * hexidecimal characters representing a 32bit value.
  268. *
  269. * The data must have 0xFFFFFFFF possible values, and be sortable by
  270. * PHP sort functions using SORT_REGULAR.
  271. *
  272. * @param string
  273. * @return mixed A sortable format with 0xFFFFFFFF possible values
  274. */
  275. public function hash($string);
  276. }
  277. /**
  278. * Uses CRC32 to hash a value into a signed 32bit int address space.
  279. * Under 32bit PHP this (safely) overflows into negatives ints.
  280. *
  281. * @author Paul Annesley
  282. * @package Flexihash
  283. * @licence http://www.opensource.org/licenses/mit-license.php
  284. */
  285. class Flexihash_Crc32Hasher
  286. implements Flexihash_Hasher
  287. {
  288. /* (non-phpdoc)
  289. * @see Flexihash_Hasher::hash()
  290. */
  291. public function hash($string)
  292. {
  293. return crc32($string);
  294. }
  295. }
  296. /**
  297. * Uses CRC32 to hash a value into a 32bit binary string data address space.
  298. *
  299. * @author Paul Annesley
  300. * @package Flexihash
  301. * @licence http://www.opensource.org/licenses/mit-license.php
  302. */
  303. class Flexihash_Md5Hasher
  304. implements Flexihash_Hasher
  305. {
  306. /* (non-phpdoc)
  307. * @see Flexihash_Hasher::hash()
  308. */
  309. public function hash($string)
  310. {
  311. return substr(md5($string), 0, 8); // 8 hexits = 32bit
  312. // 4 bytes of binary md5 data could also be used, but
  313. // performance seems to be the same.
  314. }
  315. }
  316. /**
  317. * An exception thrown by Flexihash.
  318. *
  319. * @author Paul Annesley
  320. * @package Flexihash
  321. * @licence http://www.opensource.org/licenses/mit-license.php
  322. */
  323. class Flexihash_Exception extends Exception
  324. {
  325. }