HashRing.php 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. <?php
  2. /*
  3. * This file is part of the Predis package.
  4. *
  5. * (c) Daniele Alessandri <suppakilla@gmail.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Predis\Distribution;
  11. /**
  12. * This class implements an hashring-based distributor that uses the same
  13. * algorithm of memcache to distribute keys in a cluster using client-side
  14. * sharding.
  15. *
  16. * @author Daniele Alessandri <suppakilla@gmail.com>
  17. * @author Lorenzo Castelli <lcastelli@gmail.com>
  18. */
  19. class HashRing implements IDistributionStrategy
  20. {
  21. const DEFAULT_REPLICAS = 128;
  22. const DEFAULT_WEIGHT = 100;
  23. private $nodes;
  24. private $ring;
  25. private $ringKeys;
  26. private $ringKeysCount;
  27. private $replicas;
  28. /**
  29. * @param int $replicas Number of replicas in the ring.
  30. */
  31. public function __construct($replicas = self::DEFAULT_REPLICAS)
  32. {
  33. $this->replicas = $replicas;
  34. $this->nodes = array();
  35. }
  36. /**
  37. * Adds a node to the ring with an optional weight.
  38. *
  39. * @param mixed $node Node object.
  40. * @param int $weight Weight for the node.
  41. */
  42. public function add($node, $weight = null)
  43. {
  44. // In case of collisions in the hashes of the nodes, the node added
  45. // last wins, thus the order in which nodes are added is significant.
  46. $this->nodes[] = array('object' => $node, 'weight' => (int) $weight ?: $this::DEFAULT_WEIGHT);
  47. $this->reset();
  48. }
  49. /**
  50. * {@inheritdoc}
  51. */
  52. public function remove($node)
  53. {
  54. // A node is removed by resetting the ring so that it's recreated from
  55. // scratch, in order to reassign possible hashes with collisions to the
  56. // right node according to the order in which they were added in the
  57. // first place.
  58. for ($i = 0; $i < count($this->nodes); ++$i) {
  59. if ($this->nodes[$i]['object'] === $node) {
  60. array_splice($this->nodes, $i, 1);
  61. $this->reset();
  62. break;
  63. }
  64. }
  65. }
  66. /**
  67. * Resets the distributor.
  68. */
  69. private function reset()
  70. {
  71. unset(
  72. $this->ring,
  73. $this->ringKeys,
  74. $this->ringKeysCount
  75. );
  76. }
  77. /**
  78. * Returns the initialization status of the distributor.
  79. *
  80. * @return Boolean
  81. */
  82. private function isInitialized()
  83. {
  84. return isset($this->ringKeys);
  85. }
  86. /**
  87. * Calculates the total weight of all the nodes in the distributor.
  88. *
  89. * @return int
  90. */
  91. private function computeTotalWeight()
  92. {
  93. $totalWeight = 0;
  94. foreach ($this->nodes as $node) {
  95. $totalWeight += $node['weight'];
  96. }
  97. return $totalWeight;
  98. }
  99. /**
  100. * Initializes the distributor.
  101. */
  102. private function initialize()
  103. {
  104. if ($this->isInitialized()) {
  105. return;
  106. }
  107. if (count($this->nodes) === 0) {
  108. throw new EmptyRingException('Cannot initialize empty hashring');
  109. }
  110. $this->ring = array();
  111. $totalWeight = $this->computeTotalWeight();
  112. $nodesCount = count($this->nodes);
  113. foreach ($this->nodes as $node) {
  114. $weightRatio = $node['weight'] / $totalWeight;
  115. $this->addNodeToRing($this->ring, $node, $nodesCount, $this->replicas, $weightRatio);
  116. }
  117. ksort($this->ring, SORT_NUMERIC);
  118. $this->ringKeys = array_keys($this->ring);
  119. $this->ringKeysCount = count($this->ringKeys);
  120. }
  121. /**
  122. * Implements the logic needed to add a node to the hashring.
  123. *
  124. * @param array $ring Source hashring.
  125. * @param mixed $node Node object to be added.
  126. * @param int $totalNodes Total number of nodes.
  127. * @param int $replicas Number of replicas in the ring.
  128. * @param float $weightRatio Weight ratio for the node.
  129. */
  130. protected function addNodeToRing(&$ring, $node, $totalNodes, $replicas, $weightRatio)
  131. {
  132. $nodeObject = $node['object'];
  133. $nodeHash = $this->getNodeHash($nodeObject);
  134. $replicas = (int) round($weightRatio * $totalNodes * $replicas);
  135. for ($i = 0; $i < $replicas; $i++) {
  136. $key = crc32("$nodeHash:$i");
  137. $ring[$key] = $nodeObject;
  138. }
  139. }
  140. /**
  141. * {@inheritdoc}
  142. */
  143. protected function getNodeHash($nodeObject)
  144. {
  145. return (string) $nodeObject;
  146. }
  147. /**
  148. * Calculates the hash for the specified value.
  149. *
  150. * @param string $value Input value.
  151. * @return int
  152. */
  153. public function generateKey($value)
  154. {
  155. return crc32($value);
  156. }
  157. /**
  158. * {@inheritdoc}
  159. */
  160. public function get($key)
  161. {
  162. return $this->ring[$this->getNodeKey($key)];
  163. }
  164. /**
  165. * Calculates the corrisponding key of a node distributed in the hashring.
  166. *
  167. * @param int $key Computed hash of a key.
  168. * @return int
  169. */
  170. private function getNodeKey($key)
  171. {
  172. $this->initialize();
  173. $ringKeys = $this->ringKeys;
  174. $upper = $this->ringKeysCount - 1;
  175. $lower = 0;
  176. while ($lower <= $upper) {
  177. $index = ($lower + $upper) >> 1;
  178. $item = $ringKeys[$index];
  179. if ($item > $key) {
  180. $upper = $index - 1;
  181. }
  182. else if ($item < $key) {
  183. $lower = $index + 1;
  184. }
  185. else {
  186. return $item;
  187. }
  188. }
  189. return $ringKeys[$this->wrapAroundStrategy($upper, $lower, $this->ringKeysCount)];
  190. }
  191. /**
  192. * Implements a strategy to deal with wrap-around errors during binary searches.
  193. *
  194. * @param int $upper
  195. * @param int $lower
  196. * @param int $ringKeysCount
  197. * @return int
  198. */
  199. protected function wrapAroundStrategy($upper, $lower, $ringKeysCount)
  200. {
  201. // Binary search for the last item in ringkeys with a value less or
  202. // equal to the key. If no such item exists, return the last item.
  203. return $upper >= 0 ? $upper : $ringKeysCount - 1;
  204. }
  205. }