PHP怎样处理事务_PHP数据库事务处理【事务】
PHP事务处理务必注意:MySQLi需先调用autocommit(false)函数关闭自动提交;PDO需同时设置ERRMODE_EXCEPTION并关闭自动提交。可使用SAVEPOINT模拟子事务实现部分回滚。高并发场景下应添加行锁或校验库存,避免长时间事务,并确保事务在单个请求内全部完成。
先说一个很多人踩过的坑——不管是 mysqli 还是 PDO,默认情况下都不开启事务行为。也就是说,你不显式关闭自动提交、不调用开始事务的方法,那所有 SQL 执行完就立刻落库,这时候你写 rollback() 是没有任何效果的。很多人折腾半天回滚没反应,问题就出在这里。
mysqli 的事务,关键在于先关掉自动提交
很多开发者会问:我明明调了 begin_transaction(),怎么回滚还是没生效?根本原因在于 mysqli 默认就是自动提交模式。begin_transaction() 其实只是“声明要开一个事务”,但如果 autocommit 仍然为 true,那每条 query() 执行后还是会立刻写入数据库。
正确的做法是:
- 连接成功后,第一时间调用
$mysqli->autocommit(false) begin_transaction()(PHP 7.0+ 引入)比手写START TRANSACTION更安全,可读性也好很多- 执行
commit()或rollback()之后,建议手动把autocommit恢复为true,否则会影响后面那些独立的查询 - 如果用到
multi_query(),必须配合next_result()逐条检查每条语句的执行状态,光看第一条返回值是不够的
一个典型的安全写法是这样的:
$mysqli = new mysqli($host, $user, $pass, $db);
$mysqli->autocommit(false);
try {
$mysqli->begin_transaction();
$mysqli->query("UPDATE accounts SET balance = balance - 100 WHERE id = 1");
$mysqli->query("UPDATE accounts SET balance = balance + 100 WHERE id = 2");
$mysqli->commit();
} catch (Exception $e) {
$mysqli->rollback();
throw $e;
} finally {
$mysqli->autocommit(true);
}
PDO 的事务失效,九成是因为错误模式没设对
PDO 默认的错误模式是 PDO::ERRMODE_SILENT,这意味着 SQL 执行报错时,它只返回 false,不会抛异常。后果就是 catch 块根本抓不到任何错误,rollback() 也就永远不会被执行。这是最常见的问题,没有之一。
解决方案很明确:
- 构造连接时,必须显式设置
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION - 同时建议设置
PDO::ATTR_AUTOCOMMIT => false,不要依赖beginTransaction()的隐式开关 - 要注意的是,
commit()和rollback()执行后会自动把autocommit重置为true。所以如果后面还有连续的事务,必须重复调用beginTransaction() - 还有一点容易被忽略:不要用多个
PDO实例来管理同一个事务,跨对象调用rollback()是无效的
一个标准配置示例:
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_AUTOCOMMIT => false,
]);
想要局部回滚?用 SA VEPOINT,别指望“嵌套事务”
MySQL 本身不支持真正的嵌套事务。如果你连续多次调用 beginTransaction(),后面的调用要么被忽略,要么直接报错。想要实现“子事务回滚不影响父事务”的效果,只能靠保存点来模拟。
- 在主事务中执行
$pdo->exec("SA VEPOINT sp1")来设置一个保存点 - 如果后续出错,用
$pdo->exec("ROLLBACK TO SA VEPOINT sp1")回滚到该点 - 保存点的名称必须唯一,而且不能跨事务边界使用——比如外层已经
commit了,内层再去ROLLBACK TO肯定会失败 RELEASE SA VEPOINT sp1可以显式释放保存点,但不是必需的。事务结束时,系统会自动清理所有保存点
必须提醒的是,保存点不是万能的。DDL 语句(比如 ALTER TABLE)会隐式提交当前事务,导致所有保存点失效。这一点在实际开发中很容易踩坑。
事务不是银弹:高并发场景下,光靠 commit/rollback 远远不够
拿库存扣减来举例:两个请求同时读到“剩余 10”,都判断可以扣减,结果就超卖了。事务本身并不解决“读-改-写”这个过程中的竞态问题,需要额外的保护措施。
- 用
SELECT ... FOR UPDATE加行锁——但要注意,这只对 InnoDB 有效,而且必须在事务内部使用 - 扣减前加库存校验,比如
WHERE qty >= ?,这样第二条UPDATE的影响行数就会是 0,你就可以据此判断发生了并发冲突 - 结合唯一索引或幂等令牌来防止重复提交——这一点在支付回调、入库单等场景下尤其重要
- 尽量避免长事务。锁持有时间越长,并发冲突的概率就越高。不要在事务内部做 HTTP 请求、文件操作这类耗时动作
最后还有一个容易被忽略的点:事务必须在单次 HTTP 请求的生命周期内完成闭环。PHP-FPM 的请求结束后,连接虽然可能被复用,但事务状态不会跨请求延续。千万别指望“前端分步提交、后端分步事务”这种方案,那基本是给自己挖坑。


































