在最近的项目中,需要用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'];
}
}