大神写的RedisManger各种调用,绕来绕去4,5个类的调用,写的确实牛逼,不过一不留神就不知道绕到哪里去了,不好去排查问题,这里把这个组件的逻辑给梳理下。
我们以评论的这组Redis为例来说明:
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)形成单例
/**
* 根据配置加载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];
}
形成单例并指定路由
/**
* 功能:执行操作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
根据传入的配置创建单例
/**
* 获取单例
* @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将配置中的数组以
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)的所有数组
因此在调用lookupConfig的时候只用去查找比key的crc32小的$this->_nodeKeys的值并从$this->_nodes中读取对应的host1_port对应的配置数组,其实也就是包含了write跟Read的一组值,得到配置了就很简单了,直接根据操作redis方法的是在读的方法列表中还是写的方法列表中得到读还是写的配置,然后就行了。
归纳起来其实核心方法是defaultRouter.php的getFinalConf方法,这个方法就是根据执行的Redis方法及key确定Redis连接的机器,他调用hashConf方法只是为了去格式化配置,生成一致性哈希环并返回key对应的这组Redis机器