大神写的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机器