当php遇上redis

在最近的项目中,我们需要在php中访问redis,我们选择了使用phpredis库,下面是遇到的一些问题。

redis持久连接不靠谱。

可以说这是php的通病了,不管是mysql、memcache还是redis,指望由php本身(包含php扩展)来实现持久连接都是行不通的。

为什么这么说呢?
首先,所谓的持久连接的实现不外乎在进程(php-fpm)内建一个连接池,当php需要连接时,先以ip+port等信息为key在池中查找,找到则直接返回已有连接没有则新建连接。而当一个请求执行结束时,不关闭连接,而是把连接归还到池中。

这样当php需要用到多个redis实例时(分库),因为一个php-fpm进程会持有每个redis实例的一个连接,所以需要“php-fpm进程数“*“redis实例数"个redis连接,而对于每个redis服务器则有“php-fpm进程数“个客户端连接。

举个例子:一个web应用开了1000个php-fpm进程,有10个redis实例,那么保持的redis连接数就为1000*10也就是10000,每个redis实例有1000个客户端连接。如果前端或redis再扩容所需要的连接就会以乘积方式增加。一个redis实例有php-fpm进程数个连接的情况下表现如何呢,这就要好好测一测了,反正是每连接一线程的mysql是直接堵死了。

RedisArray不靠谱。

RedisArray实现了一致性hash分布式,但是它在初始化的时候就会连接上每个实例,这在web应用中简直是胡闹,它对一致性hash实现得比较完善,结点失效、动态添加结点时重新hash都有处理,在万不得已进行水平扩容时,可能会用得上。

需要自已关闭redis连接。

Redis的析构函数没有关闭redis连接,这会导致redis网络负载过高,要确保脚本结束时关闭连接,最好是能够封装一下Redis类再使用。

示例封装
/// 分布式Redis.
class RedisShard {
    /// 构造函数.
    public function __construct($shards) {
        $this->reinit($shards);
    }

    /// 析构函数.
    /// 脚本结束时,phpredis不会自动关闭redis连接,这里添加自动关闭连接支持.
    /// 可以通过手动unset本类对象快速释放资源.
    public function __destruct() {
        if(isset($this->shard)){
            $this->shard['redis']->close();
        }
    }

    /// 重新初始化.
    public function reinit($shards){
        $index = 0;
        $this->shards = array();
        foreach($shards as $shard){
            $this->shards[$index] = explode(':', $shard); //格式:host:port:db
            $this->shards[$index]['index'] = $index;
            ++$index;
        }        
    }

    /// 转发方法调用到真正的redis对象.
    public function __call($name, $arguments) {
        $result = call_user_func_array(array($this->redis($arguments[0]), $name), $arguments);
        if($result === false and in_array($name, array('set', 'setex', 'incr'))) {
            trigger_error("redis error: " . $this->shard[0] . ':' . $this->shard[1] . ':' .$this->shard[2] . " $name " . implode(' ', $arguments), E_USER_NOTICE);
        }
        return $result;
    }

    /// 获取1至max间的唯一序号name,达到max后会从1开始.
    /// redis的递增到最大值后会返回错误,本方法实现安全的递增。
    /// 失败返回false,最要确保已用redis()方法连到生成序号的某个redis对象.
    public function id($name, $max) {
        if(isset($this->shard)){
            $id = $this->shard['redis']->incr('_id_' . $name);
            if($id){
                $max = intval($max/count($this->shards));
                if($id % $max == 0){
                    while($this->shard['redis']->decrBy('_id_' . $name, $max) >= $max){
                    }
                    $id = $max;
                }
                else if($id > $max){
                    $id %= $max;
                }
                return ($id - 1)*count($this->shards) + ($this->shard['index'] + 1);
            }
        }
        return false;
    }

    /// 连接并返回key对应的redis对象.
    public function redis($key){
        //TODO: crc32在32位系统下会返回负数,因我们是部署在64位系统上,暂时忽略.
        assert(PHP_INT_SIZE === 8);
        $index = crc32($key) % count($this->shards);
        $shard = $this->shards[$index];
        if(isset($this->shard)){
            //尝试重用已有连接.
            if($this->shard[0] == $shard[0] and $this->shard[1] == $shard[1]){
                if($this->shard[2] != $shard[2]){
                    if(! $this->shard['redis']->select($shard[2])){
                        trigger_error('redis error: select ' . $shard[0] . ':' . $shard[1] . ':' .$shard[2], E_USER_ERROR);
                        return false;
                    }
                    $this->shard[2] = $shard[2];
                }
                return $this->shard['redis'];
            }
            $this->shard['redis']->close();
            unset($this->shard);
        }
        //新建连接.
        $shard['redis'] = new Redis();
        if(! $shard['redis']->connect($shard[0], $shard[1])){
            trigger_error('redis error: connect ' . $shard[0] . ':' . $shard[1], E_USER_ERROR);
            return false;
        }
        $db = intval($shard[2]);
        if($db != 0 and !$shard['redis']->select($db)){
            trigger_error('redis error: select ' . $shard[0] . ':' . $shard[1] . ':' .$shard[2], E_USER_ERROR);
            $shard['redis']->close();
            return false;
        }
        if(ENABLE_DEVELOP){
            trigger_error('redis connect success. ' . $shard[0] . ':' . $shard[1] . ':' . $shard[2], E_USER_NOTICE);
        }        
        $this->shard = $shard;
        return $this->shard['redis'];
    }
}