大神寫的RedisManger各類調用,繞來繞去4,5個類的調用,寫的確實牛逼,不過一不留神就不知道繞到哪裏去了,很差去排查問題,這裏把這個組件的邏輯給梳理下。php
咱們以評論的這組Redis爲例來講明:node
USER_COMMENT_CACHE => [ 'common' => [ 'type' => 'default', 'db' => [ [ 'write' => ['host' => 'X.X.X.X', 'port' => 28002, 'timeout' => 3, "prefix" => "wx_ucc_"], 'read' => ['host' => 'X.X.X.X', 'port' => 28002, 'timeout' => 3, "prefix" => "wx_ucc_"], ], [ 'write' => ['host' => 'X.X.X.X, 'port' => 28002, 'timeout' => 3, "prefix" => "wx_ucc_"], 'read' => ['host' => 'X.X.X.X', 'port' => 28002, 'timeout' => 3, "prefix" => "wx_ucc_"], ], [ 'write' => ['host' => 'X.X.X.X', 'port' => 28002, 'timeout' => 3, "prefix" => "wx_ucc_"], 'read' => ['host' => 'X.X.X.X', 'port' => 28002, 'timeout' => 3, "prefix" => "wx_ucc_"], ], [ 'write' => ['host' => 'X.X.X.X', 'port' => 28002, 'timeout' => 3, "prefix" => "wx_ucc_"], 'read' => ['host' => 'X.X.X.X', 'port' => 28002, 'timeout' => 3, "prefix" => "wx_ucc_"], ] ], ] ],
首先固然是業務調用方,經過\redisManager\redisManager::getInstance($redisConf)造成單例redis
/** * 根據配置加載redisManager到redis屬性 * @param null $redisType * @param string $channelId * * @return redisOperator * @throws \Exception */ public function getRedis($redisType = null, $channelId = 'common') { if (empty($redisType)) { $redisType = $this->redisType; } if (isset(\wyCupboard::$config['redis'][$redisType][$channelId])) { $redisConf = \wyCupboard::$config['redis'][$redisType][$channelId]; } elseif (isset(\wyCupboard::$config['redis'][$redisType]['common'])) { $redisConf = \wyCupboard::$config['redis'][$redisType]['common']; } else { throw new \Exception('unknow constant redis type:' . $redisType); } return \redisManager\redisManager::getInstance($redisConf); }
redisManager/redisManager.php算法
public static function getInstance($arrConfig = array()) { if(empty($arrConfig)){ throw new \Exception("redis config is empty"); } $objKey = md5(json_encode($arrConfig)); $type = $arrConfig['type']; $routerName = $type.'Router'; $className = "\\redisManager\\route\\$routerName\\$routerName"; if (empty(self::$selfObj[$objKey])) { self::$selfObj[$objKey] = new self($arrConfig); self::$selfObj[$objKey]->router=$className::getInstance($arrConfig); } return self::$selfObj[$objKey]; }
造成單例並指定路由json
/** * 功能:執行操做redis的方法,去加載redisOperator類 * @param $func * @param $params * @return \redisManager\redisOperator */ public function __call($func, $params) { $redisConf = $this->router->getFinalConf($func, $params); $redisOperator = redisOperator::getInstance($redisConf); //調用redis方法,若是調用的過程當中發現Redis異常,則認爲是redis鏈接丟失,嘗試從新鏈接 //由於service的調用方多是常駐進程,對於單例來講,沒法確認單例中的鏈接是否已經lost connection,因此須要從新發起鏈接嘗試 try { $return = $this->runFun($redisOperator, $func, $params); } catch (\Exception $e1) { //catch裏邊還有try catch,是由於上面的try是爲了取數據,異常以後,認爲鏈接丟失,這個時候從新嘗試鏈接,可是假如redis真的掛了,下面的重連仍是會報異常 //所以,須要再次捕獲這個異常,不能由於未捕獲異常,形成業務方500錯誤 try { $redisOperator->redisConnect($redisConf); //windows下的redis擴展,當遠程redis服務器主動關閉鏈接是會報一個notice,因此加上@來抑制錯誤 $return = @$this->runFun($redisOperator, $func, $params); } catch (\Exception $e2) { $logInfo = [ 'errorCode' => $e2->getCode(), 'errMsg' => $e2->getMessage(), 'config' => $redisConf, ]; $return = null; throw new \Exception("Redis server can not connect!"); } } return $return; }
redisManager/route/AbstractRouter.php
根據傳入的配置建立單例windows
/** * 獲取單例 * @param array $redisConf redis配置 * @return obj 返回router對象 * @throws \Exception */ public static function getInstance($redisConf) { if (!empty($redisConf)) { $md5Key = md5(json_encode($redisConf)); if (empty(self::$selfObj[$md5Key])) { self::$selfObj[$md5Key] = new static($redisConf); } return self::$selfObj[$md5Key]; } else { throw new \Exception('router Error: redis conf is empty'); } }
當redisManager調用某Redis方法時getFinalConf方法會先去調用selef::hashConf方法經過一致性哈希建立單例並分佈節點,傳入的是查詢的key名及配置的db數組數組
public static function hashConf($strKey, $rediscConf){ if(count($rediscConf)==1){//若是隻有一個元素的話,不用一致性哈希算法 return $rediscConf[0]; }else{ $hashDesObj = hashDes::instance($rediscConf); return $hashDesObj->lookupConfig($strKey); } }
//獲取最終配置 public function getFinalConf($fun, $params) { $enableFun = ['WYhSet', 'WYhGet', 'WYhExists', 'WYhDel']; if($this->useSubKeyOnce===true){ //這種狀況是用hash小key來作,只使用一次 if (in_array($fun, $enableFun)) { $hashConf = self::hashConf($params[1], $this->redisConf['db']); $this->setUseSubKeyOnceDefault(); } else { throw new \Exception("{$fun} not allow use SubKey"); } }elseif($this->useSubKeyStable ===true){ //這種狀況是用hash小key來作,一直使用,通常不推薦 if (in_array($fun, $enableFun)) { $hashConf = self::hashConf($params[1], $this->redisConf['db']); } else { throw new \Exception("{$fun} not allow use SubKey"); } }else { $hashConf = self::hashConf($params[0], $this->redisConf['db']); } if(empty($this->defaultConf)){ $this->defaultConf = require(__DIR__ . '/default.conf.php'); } $arrWriteFun = isset($this->redisConf['writeFun']) ? $this->redisConf['writeFun'] : $this->defaultConf['writeFun']; $arrReadFun = isset($this->redisConf['readFun']) ? $this->redisConf['readFun'] : $this->defaultConf['readFun']; if($this->onceType){//若是有設置一次的 if($this->onceType=='write'){ $finalConf = $hashConf['write']; }else{ $finalConf = $hashConf['read']; } $this->setDefaultOnce(); }elseif($this->stableType){//若是有設置持續的 if ($this->stableType == 'write') { $finalConf = $hashConf['write']; } else { $finalConf = $hashConf['read']; } }else{//都沒設置,走默認配置 if (in_array($fun, $arrWriteFun)) { $finalConf = $hashConf['write']; } elseif (in_array($fun, $arrReadFun)) { $finalConf = $hashConf['read']; } else { throw new \Exception("function {$fun} not defined in config"); } } return $finalConf; }
hashDes.php服務器
/** * 實例化方法 * * @param array $arrConfig * * @return mixed * @throws Exception */ public static function instance(array $arrConfig = []) { //config配置合法性判斷,配置必須爲二維數組 if ( !empty( $arrConfig )) { $res = static::_getConfigUniqueKey($arrConfig); $srtMKey = $res['key']; $arrConfig = $res['config']; //若是實例已存在,直接返回 if ( !isset( static::$instance[$srtMKey] )) { $instance = new static; $instance->config = $arrConfig; $instance->DistributeNode(); static::$instance[$srtMKey] = $instance; } return static::$instance[$srtMKey]; } else { throw new \Exception("config error", 1); } }
首先會調用自身的_getConfigUniqueKey將配置中的數組以ui
host1_port=>[ 'write' => ['host' => 'X.X.X.X', 'port' => 28002, 'timeout' => 3, "prefix" => "wx_ucc_"], 'read' => ['host' => 'X.X.X.X', 'port' => 28002, 'timeout' => 3, "prefix" => "wx_ucc_"], ] host2_port=>[ 'write' => ['host' => 'X.X.X.X', 'port' => 28002, 'timeout' => 3, "prefix" => "wx_ucc_"], 'read' => ['host' => 'X.X.X.X', 'port' => 28002, 'timeout' => 3, "prefix" => "wx_ucc_"], ]
並保存到this->config中
而後調用DistributeNode方法分佈節點,分佈節點其實是以每組的write數組中的值爲基礎
獲取host_port_i(默認狀況下i爲從0到127)的crc32的值做爲數組的key,值爲host_port,所以$this->_nodes保存的就是crc_32(host_port_i)=>host_port,$this->_nodeKeys保存的就是crc_32(host_port_i)的全部數組this
所以在調用lookupConfig的時候只用去查找比key的crc32小的$this->_nodeKeys的值並從$this->_nodes中讀取對應的host1_port對應的配置數組,其實也就是包含了write跟Read的一組值,獲得配置了就很簡單了,直接根據操做redis方法的是在讀的方法列表中仍是寫的方法列表中獲得讀仍是寫的配置,而後就好了。
概括起來其實核心方法是defaultRouter.php的getFinalConf方法,這個方法就是根據執行的Redis方法及key肯定Redis鏈接的機器,他調用hashConf方法只是爲了去格式化配置,生成一致性哈希環並返回key對應的這組Redis機器