Cluster.php 9.0 KB

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