写这篇东西的起因挺简单:有一次帮一个做仿抖音短视频项目的老同事排查线上问题,用户上传视频后列表接口偶发报错,他排查了整整一天,最后定位到是mysqli预处理语句里的参数绑定类型写错了——数字ID用了字符串类型,MySQL优化器选错索引,导致慢查询拖垮接口。这种问题说难不难,但如果你对mysqli的理解只停留在“能连上、能执行SQL”这个层面,遇到它就会很痛苦。
这篇文章想做的,就是把PHP的mysqli扩展从基础连接到高级实战完整梳理一遍。我会把我在真实项目里踩过的坑、调优过的参数、以及面试中反复被问到的细节都放进来,让你不光会用,还能知道为什么这么做。适合刚入行想系统学数据库操作的PHP新人,也适合写了两三年代码但一直在用框架封装好的DB类、没怎么直接接触过mysqli的开发者。
1. 面向过程还是面向对象:写法不同,底层同源
很多新手一开始学mysqli,最纠结的问题就是:到底用面向对象写法还是函数写法?网上教程两种都有,代码风格混着记容易乱。其实这两种方式没有本质区别,它们调用的都是同一个底层扩展,只是暴露出来的API风格不同。
1.1 两种写法的核心差异
mysqli扩展提供了一组方法,同时也提供了一组对应的过程式函数。比如创建连接,面向对象用 new mysqli(),函数式用 mysqli_connect();执行查询,面向对象用 $mysqli->query(),函数式用 mysqli_query($mysqli, $sql)。
从功能上讲,两种写法能做完全一样的事情。从性能上讲,差异微乎其微,不至于成为选型理由。真正影响选择的因素是代码可读性和团队习惯。我个人的建议是:新项目一律用面向对象写法。理由有三个:
- 方法调用链清晰,IDE自动补全友好。
- 和PDO的写法更接近,以后换数据库抽象层成本低。
- 代码更整洁,尤其在处理预处理和事务这种多步骤操作时,
$stmt->execute()这种链式语义比mysqli_stmt_execute($stmt)更直观。
下面这个表格是常用的API对照,方便你在看老项目代码时快速切换思维:
| 功能 | 面向对象 | 面向过程 |
|---|---|---|
| 创建连接 | new mysqli(...) |
mysqli_connect(...) |
| 设置字符集 | $mysqli->set_charset('utf8mb4') |
mysqli_set_charset($mysqli, 'utf8mb4') |
| 执行SQL | $mysqli->query($sql) |
mysqli_query($mysqli, $sql) |
| 预处理 | $mysqli->prepare($sql) |
mysqli_prepare($mysqli, $sql) |
| 获取错误码 | $mysqli->errno |
mysqli_errno($mysqli) |
| 获取错误信息 | $mysqli->error |
mysqli_error($mysqli) |
| 事务提交 | $mysqli->commit() |
mysqli_commit($mysqli) |
| 事务回滚 | $mysqli->rollback() |
mysqli_rollback($mysqli) |
注意一点,PHP 7.0以后老掉牙的 mysql_* 函数族已经被彻底移除了,现在还在用 mysql_query 的代码直接就是致命错误。如果你维护的老项目里有类似代码,迁移到mysqli时不要只把函数名替换了,要重新审视连接管理和错误处理这两块,因为它们的模型差别挺大。
1.2 底层驱动:mysqlnd和libmysqlclient决定了什么
php.ini里和MySQL相关的扩展配置,常见的有 mysqli、pdo_mysql,它们底层可以由两种驱动实现:mysqlnd(MySQL Native Driver)和 libmysqlclient(MySQL官方的C客户端库)。
mysqlnd是PHP官方维护的驱动,PHP 5.4之后默认内置,也是绝大多数发行版默认启用的方案。libmysqlclient是MySQL官方提供的客户端库,需要单独安装。对普通开发者来说,你不需要深入理解两者的实现差异,但有几个行为差异会影响你的代码:
- mysqlnd自带了
get_result()方法,可以直接拿到结果集对象,用起来非常方便。如果用libmysqlclient,get_result()不可用,只能用bind_result()逐列绑定变量。 - mysqlnd对内存的管理和PHP的zend内存管理器集成更好,长时间运行的脚本更不容易内存泄漏。
- 某些统计函数(如
mysqli_get_connection_stats())只有mysqlnd才有。
在PHP官网下载的Windows发行版里,默认启用的就是mysqlnd。Linux下如果用包管理器安装php-mysql,通常也是mysqlnd。你可以通过 php -i | grep mysqli.default_socket 查看当前环境用的哪个驱动,输出里如果有 Client API library version => mysqlnd 字样,说明就是mysqlnd。
我建议你在开发环境直接使用mysqlnd,可以少踩很多坑。特别是下面第三节要讲的结果集获取方式,mysqlnd支持 get_result(),写起来比 bind_result() 舒服太多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 连接这一步的坑,比你想的多:字符集、超时与持久连接
连接数据库听起来简单,但我在生产环境排查过不少跟连接阶段相关的诡异问题,比如页面偶尔报连接超时、中文乱码、以及数据库连接数被打满。这些问题往往都不是SQL写错了,而是连接阶段的配置和细节处理没到位。
2.1 一个标准的连接写法
首先给一个生产环境可用的连接样例:
php复制<?php
$config = [
'host' => '127.0.0.1',
'port' => 3306,
'username' => 'app_user',
'password' => 'your_password',
'database' => 'app_db',
'charset' => 'utf8mb4',
'timeout' => 5,
];
$mysqli = new mysqli();
$mysqli->options(MYSQLI_OPT_CONNECT_TIMEOUT, $config['timeout']);
$mysqli->real_connect(
$config['host'],
$config['username'],
$config['password'],
$config['database'],
$config['port']
);
if ($mysqli->connect_errno) {
throw new RuntimeException(
'数据库连接失败: ' . $mysqli->connect_error,
$mysqli->connect_errno
);
}
$mysqli->set_charset($config['charset']);
这里我用了 new mysqli() 不传参、再单独调 options() 和 real_connect() 的方式,而不是直接 new mysqli($host, $user, $pass, $db, $port)。这样做的目的只有一个:能在连接前设置超时时间。如果你直接用构造函数传参,一旦数据库不可达,默认可能要等几十秒甚至更久才报错,这在Web请求里是完全不可接受的。
关于 connect_errno 的判断,如果你用的PHP版本是8.1以上,情况有点变化:mysqli在8.1之后默认开启了 MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT,这意味着连接失败会直接抛出 mysqli_sql_exception 异常,根本走不到 connect_errno 那行代码。我的建议是统一开启异常模式并配合try-catch处理,代码更简洁,也避免漏判。
2.2 字符集乱码的真正成因
服务端数据库字符集、连接字符集、PHP文件本身编码、HTML页面声明的字符集,这四者如果不一致,就会出现经典的中文乱码问题。其中最容易忽略的是连接字符集。
MySQL的连接字符集是独立的,它不一定会跟着数据库表的字符集走。比如你的表是 utf8mb4,但连接字符集默认可能是 latin1,那么写入时会发生一次字符集转换,取出时又一次转换,来回折腾中文字符就容易变成问号。
正确做法就是在建立连接后立刻执行 `set_ch
