由会话重定向看到的对象销毁问题

By 深空, 2009年10月12日 15:58

  今天和同事又讨论到DB类中析构函数是否要关闭数据库连接的问题,粗略看起来貌似在DB对象销毁之前关闭数据库连接再正常不过:

class DB {
    public $conn;

    public function __construct() {
        $this->conn = mysql_connect('localhost', 'root', '');
        mysql_select_db('test', $this->conn);
    }

    public function query($sql) {
        return mysql_query($sql, $this->conn);
    }

    public function __destruct() {
        echo "destruct:close<br />";
        mysql_close($this->conn);
    }
}

  执行:

<?php
//class DB ..
$db = new DB;
?>

  正常打印 destruct:close
  再来看看下面的会话重定向到DB:

<?php

class DB {
    public $conn;

    public function __construct() {
        $this->conn = mysql_connect('localhost', 'root', '');
        mysql_select_db('test', $this->conn);
    }

    public function query($sql) {
        return mysql_query($sql, $this->conn);
    }

    public function __destruct() {
        echo "destruct:close<br />";
        mysql_close($this->conn);
    }
}

class SessionHandler {
    public static $db;

    function open() {
    }

    function close() {
    }

    function read() {
    }

    function write($id, $data) {
        echo "session:write<br />";
        $sql = "REPLACE INTO session VALUES('$id', '$data')";
        self::$db->query($sql);
    }

    function destroy() {
    }

    function gc() {
    }
}

SessionHandler::$db = new DB;
session_set_save_handler(array('SessionHandler', 'open'), array('SessionHandler', 'close'), array('SessionHandler', 'read'), array('SessionHandler', 'write'), array('SessionHandler', 'destroy'), array('SessionHandler', 'gc'));

session_start();
$_SESSION['user'] = '深空';

?>

  这次析构正常,只不过会话写入出错了,析构在会话写入之前被执行了:

destruct:close
session:write

Warning: mysql_query(): 2 is not a valid MySQL-Link resource on line 12

  厄,见鬼了吧,PHP竟然在脚本执行结束后先执行析构函数,然后才处理会话。

  我再稍微修改一下 SessionHandler:

<?php

class DB {
    public $conn;

    public function __construct() {
        $this->conn = mysql_connect('localhost', 'root', '');
        mysql_select_db('test', $this->conn);
    }

    public function query($sql) {
        return mysql_query($sql, $this->conn);
    }

    public function __destruct() {
        echo "destruct:close<br />";
        mysql_close($this->conn);
    }
}

class SessionHandler {
    function open() {
    }

    function close() {
    }

    function read() {
    }

    function write($id, $data) {
        global $db;
        echo "session:write<br />";
        $sql = "REPLACE INTO session VALUES('$id', '$data')";
        $db->query($sql);
    }

    function destroy() {
    }

    function gc() {
    }
}

$db = new DB();
session_set_save_handler(array('SessionHandler', 'open'), array('SessionHandler', 'close'), array('SessionHandler', 'read'), array('SessionHandler', 'write'), array('SessionHandler', 'destroy'), array('SessionHandler', 'gc'));

session_start();
$_SESSION['user'] = '深空';

?>

  输出如下:

destruct:close
session:write

Fatal error: Call to a member function query() on a non-object on line 35

  这次报错让我感到意外,根据提示可以认定35行的$db变量不是个对象,后来想想也确实应该是这样,因为__destruct方法已经执行了,那么其实对象在write之前已经被销毁了,所以找不到$db变量已经是空的,不足为奇。我又测试了一些字符串和数组变量,发现write执行的时候,这些变量还没有被销毁,我又做了如下修改:

<?php

class DB {
    public $conn;

    public function __construct() {
        $this->conn = mysql_connect('localhost', 'root', '');
        mysql_select_db('test', $this->conn);
    }

    public function query($sql) {
        return mysql_query($sql, $this->conn);
    }

    public function __destruct() {
        echo "destruct:close<br />";
        mysql_close($this->conn);
    }
}

class SessionHandler {
    function open() {
    }

    function close() {
    }

    function read() {
    }

    function write($id, $data) {
        global $db;
        echo "session:write<br />";
        $sql = "REPLACE INTO session VALUES('$id', '$data')";
        $db['session']->query($sql);
    }

    function destroy() {
    }

    function gc() {
    }
}

$db['session'] = new DB();
session_set_save_handler(array('SessionHandler', 'open'), array('SessionHandler', 'close'), array('SessionHandler', 'read'), array('SessionHandler', 'write'), array('SessionHandler', 'destroy'), array('SessionHandler', 'gc'));

session_start();
$_SESSION['user'] = '深空';

?>

  输出:

destruct:close
session:write

Warning: mysql_query(): 2 is not a valid MySQL-Link resource on line 12

  呵呵,这次$db没有被销毁,因为它是个数组变量,后来和同事分析觉得可能是这些变量和对象的存储方式不一样,销毁的顺序也不一样导致的。不过__destruct仍然是在write前执行,基本上可以判断会话是在很后面才执行的。要解决这个问题,只能去掉DB的__destruct方法。

  我没有去深究PHP内部对变量的处理和回收方式,只是在重定向会话处理方法的时候这个问题才显露出来,是个比较有趣的问题,发出来和大家分享我的发现主要是想让大家在碰到这个问题的时候不会太浪费时间在调试上。

  希望明真相的高手不吝指点。

VN:F [1.9.3_1094]
Rating: 7.1/10 (13 votes cast)
VN:F [1.9.3_1094]
Rating: +1 (from 7 votes)
由会话重定向看到的对象销毁问题, 7.1 out of 10 based on 13 ratings

15 Responses to “由会话重定向看到的对象销毁问题”

  1. Mr.h 说:

    想想open 和 close 两个函数用来干嘛
    session的资源应该由session自己打开和关闭

    VA:F [1.9.3_1094]
    Rating: 10.0/10 (1 vote cast)
    VA:F [1.9.3_1094]
    Rating: +1 (from 1 vote)
    • 深空 说:

      有道理,不过问题在于open在session_start时候执行,write的时候你还是获取不到open创建的东西的

      VN:F [1.9.3_1094]
      Rating: 0.0/10 (0 votes cast)
      VN:F [1.9.3_1094]
      Rating: 0 (from 0 votes)
  2. netyum 说:

    你这验证码直垃圾!!
    “要解决这个问题,只能去掉DB的__destruct方法。”
    话的说太满了吧!
    完全可以在query方法时重新连接,内部作判断就可了! 只要db对象存在!!
    更好的解决方法是 $olddb = new DB; $db = &$olddb;

    VA:F [1.9.3_1094]
    Rating: 0.0/10 (0 votes cast)
    VA:F [1.9.3_1094]
    Rating: 0 (from 0 votes)
    • 深空 说:

      可能你误会我的意思了,解决数据库问题的方法有很多,我说的是使用一个全局的数据库对象的时候产生的问题,当然你想在write里临时new一个DB对象是不会有任何问题的,但是采用单例的DB这样做不是最好的,这里反映的另外一个问题是在你创建一个通用DB类的时候需要去考虑__destruct方法close数据库连接可能导致别人的代码产生不可预期的效果。应该避免。
      PS:我这个验证码有什么问题呀。

      VN:F [1.9.3_1094]
      Rating: 0.0/10 (0 votes cast)
      VN:F [1.9.3_1094]
      Rating: 0 (from 0 votes)
      • Janpoem 说:

        楼主,你的验证码一旦输入出错的时候,会提交到一个提示出错的页面,结果再后退的时候,之前输入的内容白打了。而你的验证码明明输入正确了,却会提示出错了。

        VA:F [1.9.3_1094]
        Rating: 0.0/10 (0 votes cast)
        VA:F [1.9.3_1094]
        Rating: 0 (from 0 votes)
  3. kimjxie 说:

    在php执行过程中,session是由ZE管理,存在于内存当中
    之后由php_session_flush按handler所设置的写入存储设备
    这一部分的执行是定义在session的PHP_RSHUTDOWN_FUNCTION中的
    也就是说,当请求完成ZE开始清理内存的时候才会将session写入存储设备

    VA:F [1.9.3_1094]
    Rating: 10.0/10 (2 votes cast)
    VA:F [1.9.3_1094]
    Rating: 0 (from 0 votes)
  4. Torr 说:

    好文章,转载到 deving.cn,希望可以和更多的人分享

    VA:F [1.9.3_1094]
    Rating: 0.0/10 (0 votes cast)
    VA:F [1.9.3_1094]
    Rating: 0 (from 0 votes)
  5. DNF1100 说:

    上面的朋友说的对,老兄给你这博客留言要输入好几次验证码的。

    VA:F [1.9.3_1094]
    Rating: 0.0/10 (0 votes cast)
    VA:F [1.9.3_1094]
    Rating: 0 (from 0 votes)
  6. wps2000 说:

    session_write_close()

    程序的最后手工调用一下

    VA:F [1.9.3_1094]
    Rating: 10.0/10 (1 vote cast)
    VA:F [1.9.3_1094]
    Rating: 0 (from 0 votes)
  7. Anders 说:

    使用Apc了吧,看看这篇文章:

    http://www.laruence.com/2009/12/05/1172.html

    VA:F [1.9.3_1094]
    Rating: 10.0/10 (1 vote cast)
    VA:F [1.9.3_1094]
    Rating: 0 (from 0 votes)
  8. Janpoem 说:

    哦,天啊,你的验证码真的不太好用。

    我已经回复你在phpchina的帖子,但由于我无法在专家区发帖,请到我的博客:http://www.cnblogs.com/janpoem/archive/2010/02/06/1664786.html。

    VA:F [1.9.3_1094]
    Rating: 0.0/10 (0 votes cast)
    VA:F [1.9.3_1094]
    Rating: 0 (from 0 votes)
    • 深空 说:

      这里主要说的不是说怎么解决,而是说这样使用会带来这些问题,要解决这些问题很容易,但是根源是什么呢?你还是没有说明。只要避免我文中所说的使用方法就可以了。

      VN:F [1.9.3_1094]
      Rating: 0.0/10 (0 votes cast)
      VN:F [1.9.3_1094]
      Rating: 0 (from 0 votes)
      • Janpoem 说:

        OK,我个人的理解是,根源在于PHP的Class的析构函数的执行时间,但这个本身是没有可存在质疑的地方,他的析构执行从没犹豫过。

        只是在使用session handle以后,session close会更晚于Class的析构后执行,你可以做个简单的实验,在close里面echo一些内容出来,他永远是最后一段输出的。

        所以,我觉得问题不在于尝试理解PHP变量的释放和Class析构,而在于理解PHP的Session Handle。

        VA:F [1.9.3_1094]
        Rating: 0.0/10 (0 votes cast)
        VA:F [1.9.3_1094]
        Rating: +1 (from 1 vote)
  9. Janpoem 说:

    另外楼上的没有一个是正解,鉴定完毕。

    VA:F [1.9.3_1094]
    Rating: 0.0/10 (0 votes cast)
    VA:F [1.9.3_1094]
    Rating: 0 (from 0 votes)
  10. Arlen 说:

    噢,,My god 很少用SESSION。。。

    VA:F [1.9.3_1094]
    Rating: 6.0/10 (1 vote cast)
    VA:F [1.9.3_1094]
    Rating: +1 (from 1 vote)

Leave a Reply

京ICP备05002071号 ©2003-2010 深空