BUG、BUG,似乎末学的学习历程都是因为BUG引起的:
在我们的一款WebGame的生产环境中,一次无意的strace抓包时,发现了php与mysql大量通讯的数据。这种情况,在游戏服务器刚启动时,是正常的,但如果是运行一段时间之后,出现大量SELECT的SQL查询,绝对是有问题的,而且,所操作的数据库并不是配置库,那意味着,我们程序员的程序出现了违规的操作。具体结果大约如下:
如上图所示,php持续接收读取进程内描述符为3的响应包数据,描述符为3的为php与mysql建立的TCP通讯链接,这点也可以从313行的SELECT语句来确认。(原始数据丢失了,我模仿了一条。所以是配置库的SQL语句)
这是什么程序,想实现什么逻辑?为何要取这么多数据?
跟着这里的SELECT的sql语句,我定位到了相应的程序段:
/*
** 业务逻辑的代码
*/
public function SItem($roleId,$baseId) {
//...
// ############写出下面这种代码的人都得死.##################
$this->dbrRole->select('*');
$this->dbrRole->from('role_items');
$this->dbrRole->where('role_id',$roleId);
$this->dbrRole->where('baseId',$baseId);
$result = $this->dbrRole->get()->row(); //看上去,这里好像正常,我们都以为框架会给我们只取一条。
//...
}
我们从代码上来看,好像明白程序员想根据对应的role_id到role_items表里取一条想符合的数据,所以,他调用了row方法,来取一条。看上去,这里好像正常,我们都以为框架会给我们只取一条。但实际上,框架是如何处理的呢?
我们来看下框架的对应row方法的实现过程。对了,我们是CodeIgniter框架的一个较老的版本。
/*
** 框架中,DB drive中,row相关方法的代码
**
*/
public function row($n = 0,$type = 'array'){
if(!is_numeric($n)){
if(! is_array($this->_rowData)){
$this->_rowData = $this->rowArray(0);
}
if(isset($this->_rowData[$n])){
return $this->_rowData[$n];
}
$n = 0;
}
return ($type == 'object') $this->rowObject($n) : $this->rowArray($n);
}
//继续跟进rowArray方法
public function rowArray($n = 0){
$result = $this->resultArray();
if(count($result) == 0){
return $result;
}
if($n != $this->_current && isset($result[$n])){
$this->_current = $n;
}
return $result[$this->_current];
}
//继续跟进resultArray方法 ###这个方法是重点###
public function resultArray(){
if(count($this->resultArray) > 0){
return $this->resultArray;
}
if(false === $this->resulter || 0 == $this->recordCount()){
return array();
}
$this->_dataSeek(0);
while($row = $this->_fetchAssoc()){
$this->resultArray[] = $row; //###########这个数组每次都增加_fetchAssoc()结果的内存大小数量#########################
}
return $this->resultArray;
}
//继续跟进_fetchAssoc方法
/*
** 对应driver的_fetchAssoc方法的代码
*/
protected function _fetchAssoc(){
return mysql_fetch_assoc($this->resulter);
}
我们可以看到CodeIgniter框架的resultArray方法使用mysql(我们的php调用mysql的api用的是mysql函数,有点绕,后面解释)的mysql_fetch_assoc函数对缓冲区的数据进行遍历转换。将所有缓冲区的数据全部复制给$this->resultArray属性,再判断row方法中所需要的key的结果是否存在,再与返回的。
也就是说,框架层并没有只从mysql server(潜意识上的mysql server)那边取一条给我们调用者,而是取了所有结果,再返回一条。(先别喷,后面解释) 当然,CI这种做法,也不是错。但我觉得有更好的改进方法。
这个问题,我们组的dietoad (征婚) 发现了这个问题,并给了修复方案。有些同学认为,这是程序员的错,程序员的SELECT语句没有加limit来限制条数。这我绝对赞同,而且,觉得写出这种代码的人都得死。
- 业务层:为这种业务需求的SQL语句加上limit限制
- 框架层:框架对于这种需求,自动控制,发现这种情况,直接返回1条
对于解决方案1,我写了一个正则,匹配select()方法被调用之后,row()方法被调用之前,中间没有使用limit()方法的所有代码,结果,发现量并不小。后来,我们决定两种方案同时实施,防止第二种出现漏掉的情况。
dietoad给出如下改进:
/*
** //改进为当_rowData不存在时,从_rowData的数量开始取,取小于$n条记录,避免 上面 resultArray方法中从缓冲区取所有数据,复制双倍数据,占用内存的情况
*/
public function row ($n = 0, $type = 'array')
{
if(isset($this->_rowData[$n]))
{
return $this->_rowData[$n];
}
if (! is_numeric($n))
{
return $this->rowObject($n);
}
$ln=count($this->_rowData);
//继续上次位置
while($ln++<=$n&&$r=$this->_fetchAssoc())
{
$this->_rowData[]=$r;
}
//需要几条就读几条
//防止记录集为空报warning
return isset($this->_rowData[$n]) $this->_rowData