在我们的一款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框架的一个较老的版本。 -
-
-
-
- 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);
- }
-
-
- 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];
- }
-
-
- 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;
- }
- return $this->resultArray;
- }
-
-
-
-
-
- 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()方法的所有代码,结果,发现量并不小。后来,我们决定两种方案同时实施,防止第二种出现漏掉的情况。 |