PHP mysql_stmt::fetch()导致PHP致命错误内存耗尽


PHP mysql_stmt::fetch() gives PHP Fatal error memory exhausted

CentOS 6.4 PHP 5.3.3 MySQL 5.1.69 x86_64

mysql_stmt::fetch()

当使用准备好的语句执行fetch时,PHP会产生错误:PHP致命错误:允许的内存大小134217728字节已用完(试图分配4294967296字节)。

当用于创建临时表的SELECT语句中包含的变量未设置时,无论在调用存储过程之前是否在环境中设置了该变量,都会发生这种情况。变量必须在存储过程中设置。当SELECT语句用于将临时表中的数据返回给PHP,而PHP使用mysql_stmt::fetch()访问数据时,PHP会生成上述致命错误。

MySQL代码:

DELIMITER $$
CREATE PROCEDURE test_sp()
BEGIN
    # uncomment below line, and PHP call to mysqli_stmt::fetch() works
    # SET @status = 1;
    # remove tmp table
    DROP TABLE IF EXISTS tmp_table;
    # CREATE TEMPORARY TABLE
    CREATE TEMPORARY TABLE tmp_table
        SELECT @status AS status;
    SELECT * FROM tmp_table;
END $$
DELIMITER ;

PHP代码:

// obtain MySQL login info
require_once(MYSQLOBJ);
// initialize status
$status = "";

$db = new mysqli(
    DB_HOST,
    DB_USER,
    DB_PASSWORD,
    DB_NAME
    );

$query = "CALL test_sp";
$stmt = $db->prepare($query);
$stmt->execute();
$stmt->bind_result( $status );
$stmt->store_result();
$stmt->fetch(); // PHP FATAL ERROR OCCURS HERE
$stmt->free_result();
$db->close();
print "<p>status = $status</p>'n";

您会发现,只有当@statusNULL或字符串时,才会发生

问题有两个:

  1. 与本地变量不同,MySQL用户变量支持一组非常有限的数据类型:

    用户变量可以从一组有限的数据类型中分配一个值:整数、十进制、浮点、二进制或非二进制字符串,或NULL值。

    文档中未提及实际使用的数据类型分别为BIGINTDECIMAL(65,30)DOUBLELONGBLOBLONGTEXTLONGBLOB。关于最后一个,手册至少解释了:

    如果引用的变量尚未初始化,则其值为NULL,类型为字符串。

    前三种数据类型(即整数、十进制和浮点值)的存储分别需要8、30和8个字节。其他数据类型(即字符串和NULL值)需要(最多)4 GB的存储空间。

  2. 由于您使用的是v.4.0之前的PHP版本,因此默认的MySQL驱动程序是libmysql,在数据绑定时,服务器只能使用该驱动程序获取列类型的元数据—因此MySQLi试图分配足够的内存来保存每个可能的值(即使最终不需要完整的缓冲区);因此,NULL和字符串值用户变量的最大可能大小为4GiB,导致PHP超过其默认内存限制(自PHP v.2.0以来为128MiB)

您的选择包括:

  • 重写表定义中的列数据类型:

    DROP TEMPORARY TABLE IF EXISTS tmp_table;
    CREATE TEMPORARY TABLE tmp_table (
      status VARCHAR(2)
    ) SELECT @status AS status;
    
  • 显式地将用户变量强制转换为更特定的数据类型:

    DROP TEMPORARY TABLE IF EXISTS tmp_table;
    CREATE TEMPORARY TABLE tmp_table
      SELECT CAST(@status AS CHAR(2)) AS status;
    
  • 使用局部变量,用显式数据类型声明:

    DECLARE status VARCHAR(2) DEFAULT @status;
    DROP TEMPORARY TABLE IF EXISTS tmp_table;
    CREATE TEMPORARY TABLE tmp_table
      SELECT status;
    
  • 通过在mysqli_stmt::bind_result()之前调用mysqli_stmt::store_result()来解决这个问题,这会导致结果集存储在libmysql中(超出PHP的内存限制),然后PHP在获取记录时只分配保存记录所需的实际内存:

    $stmt->execute();
    $stmt->store_result();
    $stmt->bind_result( $status );
    $stmt->fetch();
    
  • 提高PHP的内存限制,以适应4GiB缓冲区的分配(尽管应该意识到这样做对硬件资源的影响)—例如,完全消除内存限制(尽管要注意这样做可能产生的负面副作用,例如真正的内存泄漏):

    ini_set('memory_limit', '-1');
    
  • 重新编译PHP,配置为使用本机mysqlnd驱动程序(自v.3.0起随PHP一起提供,但在PHP v.4.0之前未配置为默认驱动程序),而不是libmysql:

    ./configure --with-mysqli=mysqlnd
    
  • 升级到PHP v.4.0或更高版本,以便在默认情况下使用mysqlnd。