Cluster.php 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. <?php
  2. /**
  3. * Credis, a Redis interface for the modest
  4. *
  5. * @author Justin Poliey <jdp34@njit.edu>
  6. * @copyright 2009 Justin Poliey <jdp34@njit.edu>
  7. * @license http://www.opensource.org/licenses/mit-license.php The MIT License
  8. * @package Credis
  9. */
  10. /**
  11. * A generalized Credis_Client interface for a cluster of Redis servers
  12. *
  13. * @deprecated
  14. */
  15. class Credis_Cluster
  16. {
  17. /**
  18. * Collection of Credis_Client objects attached to Redis servers
  19. * @var Credis_Client[]
  20. */
  21. protected $clients;
  22. /**
  23. * If a server is set as master, all write commands go to that one
  24. * @var Credis_Client
  25. */
  26. protected $masterClient;
  27. /**
  28. * Aliases of Credis_Client objects attached to Redis servers, used to route commands to specific servers
  29. * @see Credis_Cluster::to
  30. * @var array
  31. */
  32. protected $aliases;
  33. /**
  34. * Hash ring of Redis server nodes
  35. * @var array
  36. */
  37. protected $ring;
  38. /**
  39. * Individual nodes of pointers to Redis servers on the hash ring
  40. * @var array
  41. */
  42. protected $nodes;
  43. /**
  44. * The commands that are not subject to hashing
  45. * @var array
  46. * @access protected
  47. */
  48. protected $dont_hash;
  49. /**
  50. * Currently working cluster-wide database number.
  51. * @var int
  52. */
  53. protected $selectedDb = 0;
  54. /**
  55. * Creates an interface to a cluster of Redis servers
  56. * Each server should be in the format:
  57. * array(
  58. * 'host' => hostname,
  59. * 'port' => port,
  60. * 'db' => db,
  61. * 'password' => password,
  62. * 'timeout' => timeout,
  63. * 'alias' => alias,
  64. * 'persistent' => persistence_identifier,
  65. * 'master' => master
  66. * 'write_only'=> true/false
  67. * )
  68. *
  69. * @param array $servers The Redis servers in the cluster.
  70. * @param int $replicas
  71. * @param bool $standAlone
  72. * @throws CredisException
  73. */
  74. public function __construct($servers, $replicas = 128, $standAlone = false)
  75. {
  76. $this->clients = array();
  77. $this->masterClient = null;
  78. $this->aliases = array();
  79. $this->ring = array();
  80. $this->replicas = (int)$replicas;
  81. $client = null;
  82. foreach ($servers as $server)
  83. {
  84. if(is_array($server)){
  85. $client = new Credis_Client(
  86. $server['host'],
  87. $server['port'],
  88. isset($server['timeout']) ? $server['timeout'] : 2.5,
  89. isset($server['persistent']) ? $server['persistent'] : '',
  90. isset($server['db']) ? $server['db'] : 0,
  91. isset($server['password']) ? $server['password'] : null
  92. );
  93. if (isset($server['alias'])) {
  94. $this->aliases[$server['alias']] = $client;
  95. }
  96. if(isset($server['master']) && $server['master'] === true){
  97. $this->masterClient = $client;
  98. if(isset($server['write_only']) && $server['write_only'] === true){
  99. continue;
  100. }
  101. }
  102. } elseif($server instanceof Credis_Client){
  103. $client = $server;
  104. } else {
  105. throw new CredisException('Server should either be an array or an instance of Credis_Client');
  106. }
  107. if($standAlone) {
  108. $client->forceStandalone();
  109. }
  110. $this->clients[] = $client;
  111. for ($replica = 0; $replica <= $this->replicas; $replica++) {
  112. $md5num = hexdec(substr(md5($client->getHost().':'.$client->getPort().'-'.$replica),0,7));
  113. $this->ring[$md5num] = count($this->clients)-1;
  114. }
  115. }
  116. ksort($this->ring, SORT_NUMERIC);
  117. $this->nodes = array_keys($this->ring);
  118. $this->dont_hash = array_flip(array(
  119. 'RANDOMKEY', 'DBSIZE', 'PIPELINE', 'EXEC',
  120. 'SELECT', 'MOVE', 'FLUSHDB', 'FLUSHALL',
  121. 'SAVE', 'BGSAVE', 'LASTSAVE', 'SHUTDOWN',
  122. 'INFO', 'MONITOR', 'SLAVEOF'
  123. ));
  124. if($this->masterClient !== null && count($this->clients()) == 0){
  125. $this->clients[] = $this->masterClient;
  126. for ($replica = 0; $replica <= $this->replicas; $replica++) {
  127. $md5num = hexdec(substr(md5($this->masterClient->getHost().':'.$this->masterClient->getHost().'-'.$replica),0,7));
  128. $this->ring[$md5num] = count($this->clients)-1;
  129. }
  130. $this->nodes = array_keys($this->ring);
  131. }
  132. }
  133. /**
  134. * @param Credis_Client $masterClient
  135. * @param bool $writeOnly
  136. * @return Credis_Cluster
  137. */
  138. public function setMasterClient(Credis_Client $masterClient, $writeOnly=false)
  139. {
  140. if(!$masterClient instanceof Credis_Client){
  141. throw new CredisException('Master client should be an instance of Credis_Client');
  142. }
  143. $this->masterClient = $masterClient;
  144. if (!isset($this->aliases['master'])) {
  145. $this->aliases['master'] = $masterClient;
  146. }
  147. if(!$writeOnly){
  148. $this->clients[] = $this->masterClient;
  149. for ($replica = 0; $replica <= $this->replicas; $replica++) {
  150. $md5num = hexdec(substr(md5($this->masterClient->getHost().':'.$this->masterClient->getHost().'-'.$replica),0,7));
  151. $this->ring[$md5num] = count($this->clients)-1;
  152. }
  153. $this->nodes = array_keys($this->ring);
  154. }
  155. return $this;
  156. }
  157. /**
  158. * Get a client by index or alias.
  159. *
  160. * @param string|int $alias
  161. * @throws CredisException
  162. * @return Credis_Client
  163. */
  164. public function client($alias)
  165. {
  166. if (is_int($alias) && isset($this->clients[$alias])) {
  167. return $this->clients[$alias];
  168. }
  169. else if (isset($this->aliases[$alias])) {
  170. return $this->aliases[$alias];
  171. }
  172. throw new CredisException("Client $alias does not exist.");
  173. }
  174. /**
  175. * Get an array of all clients
  176. *
  177. * @return array|Credis_Client[]
  178. */
  179. public function clients()
  180. {
  181. return $this->clients;
  182. }
  183. /**
  184. * Execute a command on all clients
  185. *
  186. * @return array
  187. */
  188. public function all()
  189. {
  190. $args = func_get_args();
  191. $name = array_shift($args);
  192. $results = array();
  193. foreach($this->clients as $client) {
  194. $results[] = call_user_func_array([$client, $name], $args);
  195. }
  196. return $results;
  197. }
  198. /**
  199. * Get the client that the key would hash to.
  200. *
  201. * @param string $key
  202. * @return \Credis_Client
  203. */
  204. public function byHash($key)
  205. {
  206. return $this->clients[$this->hash($key)];
  207. }
  208. /**
  209. * @param int $index
  210. * @return void
  211. */
  212. public function select($index)
  213. {
  214. $this->selectedDb = (int) $index;
  215. }
  216. /**
  217. * Execute a Redis command on the cluster with automatic consistent hashing and read/write splitting
  218. *
  219. * @param string $name
  220. * @param array $args
  221. * @return mixed
  222. */
  223. public function __call($name, $args)
  224. {
  225. if($this->masterClient !== null && !$this->isReadOnlyCommand($name)){
  226. $client = $this->masterClient;
  227. }elseif (count($this->clients()) == 1 || isset($this->dont_hash[strtoupper($name)]) || !isset($args[0])) {
  228. $client = $this->clients[0];
  229. }
  230. else {
  231. $client = $this->byHash($args[0]);
  232. }
  233. // Ensure that current client is working on the same database as expected.
  234. if ($client->getSelectedDb() != $this->selectedDb) {
  235. $client->select($this->selectedDb);
  236. }
  237. return call_user_func_array([$client, $name], $args);
  238. }
  239. /**
  240. * Get client index for a key by searching ring with binary search
  241. *
  242. * @param string $key The key to hash
  243. * @return int The index of the client object associated with the hash of the key
  244. */
  245. public function hash($key)
  246. {
  247. $needle = hexdec(substr(md5($key),0,7));
  248. $server = $min = 0;
  249. $max = count($this->nodes) - 1;
  250. while ($max >= $min) {
  251. $position = (int) (($min + $max) / 2);
  252. $server = $this->nodes[$position];
  253. if ($needle < $server) {
  254. $max = $position - 1;
  255. }
  256. else if ($needle > $server) {
  257. $min = $position + 1;
  258. }
  259. else {
  260. break;
  261. }
  262. }
  263. return $this->ring[$server];
  264. }
  265. public function isReadOnlyCommand($command)
  266. {
  267. static $readOnlyCommands = array(
  268. 'DBSIZE' => true,
  269. 'INFO' => true,
  270. 'MONITOR' => true,
  271. 'EXISTS' => true,
  272. 'TYPE' => true,
  273. 'KEYS' => true,
  274. 'SCAN' => true,
  275. 'RANDOMKEY' => true,
  276. 'TTL' => true,
  277. 'GET' => true,
  278. 'MGET' => true,
  279. 'SUBSTR' => true,
  280. 'STRLEN' => true,
  281. 'GETRANGE' => true,
  282. 'GETBIT' => true,
  283. 'LLEN' => true,
  284. 'LRANGE' => true,
  285. 'LINDEX' => true,
  286. 'SCARD' => true,
  287. 'SISMEMBER' => true,
  288. 'SINTER' => true,
  289. 'SUNION' => true,
  290. 'SDIFF' => true,
  291. 'SMEMBERS' => true,
  292. 'SSCAN' => true,
  293. 'SRANDMEMBER' => true,
  294. 'ZRANGE' => true,
  295. 'ZREVRANGE' => true,
  296. 'ZRANGEBYSCORE' => true,
  297. 'ZREVRANGEBYSCORE' => true,
  298. 'ZCARD' => true,
  299. 'ZSCORE' => true,
  300. 'ZCOUNT' => true,
  301. 'ZRANK' => true,
  302. 'ZREVRANK' => true,
  303. 'ZSCAN' => true,
  304. 'HGET' => true,
  305. 'HMGET' => true,
  306. 'HEXISTS' => true,
  307. 'HLEN' => true,
  308. 'HKEYS' => true,
  309. 'HVALS' => true,
  310. 'HGETALL' => true,
  311. 'HSCAN' => true,
  312. 'PING' => true,
  313. 'AUTH' => true,
  314. 'SELECT' => true,
  315. 'ECHO' => true,
  316. 'QUIT' => true,
  317. 'OBJECT' => true,
  318. 'BITCOUNT' => true,
  319. 'TIME' => true,
  320. 'SORT' => true,
  321. );
  322. return array_key_exists(strtoupper($command), $readOnlyCommands);
  323. }
  324. }