如何确保PHP+MySQL事务提交后,全部事务成功交由MySQL托管?
PHP+MySQL事务提交的可靠性保障
问题背景
我开发PHP应用时,使用mysqli_begin_transaction启动InnoDB事务返回true,后续一系列insert操作均检查错误码确认成功。但提交前执行非MySQL延迟操作期间,事务可能因死锁等原因被隐式回滚,而PHP未调用MySQL操作时无法感知。此时执行mysqli_commit会提交空事务,MySQL不报错,导致无法察觉事务已回滚。现需:
- 确保事务按预期提交的代码示例
- 确定性复现隐式回滚的方法
版本环境:MySQL 5.7.38-log、PHP 7.4
一、确保事务按预期提交的代码示例
核心思路:启用mysqli错误报告捕获驱动层异常,提交前主动检查事务状态,提交后可追加数据校验(可选但更严谨)。
<?php // 启用mysqli错误报告,将MySQL错误转为PHP异常 mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); try { $conn = new mysqli('localhost', 'username', 'password', 'dbname'); $conn->set_charset('utf8mb4'); // 启动事务 $conn->begin_transaction(); // 执行批量插入操作(示例) $stmt = $conn->prepare("INSERT INTO test_table (col1) VALUES (?)"); $stmt->bind_param('s', $value); $values = ['val1', 'val2', 'val3']; foreach ($values as $value) { $stmt->execute(); // 校验插入生效 if ($stmt->affected_rows === 0) { throw new RuntimeException("插入数据失败,无行受影响"); } } $stmt->close(); // 模拟非MySQL延迟操作(如调用外部API、文件处理) sleep(5); // 提交前检查事务状态:@@in_transaction返回1表示事务仍在进行 $result = $conn->query("SELECT @@in_transaction AS in_transaction"); $row = $result->fetch_assoc(); $result->free(); if ((int)$row['in_transaction'] !== 1) { throw new RuntimeException("事务已被隐式回滚,无法提交"); } // 提交事务 $conn->commit(); // 可选:最终数据校验,确保提交后数据持久化 $checkResult = $conn->query("SELECT COUNT(*) AS count FROM test_table WHERE col1 IN ('val1','val2','val3')"); $checkRow = $checkResult->fetch_assoc(); $checkResult->free(); if ((int)$checkRow['count'] !== 3) { throw new RuntimeException("事务提交后数据未完全写入"); } echo "事务提交成功,数据已持久化"; } catch (mysqli_sql_exception $e) { // 捕获MySQL驱动异常(含死锁、隐式回滚等) if (isset($conn) && $conn->in_transaction) { $conn->rollback(); } echo "MySQL错误:" . $e->getMessage(); } catch (RuntimeException $e) { // 捕获自定义业务异常 if (isset($conn) && $conn->in_transaction) { $conn->rollback(); } echo "业务错误:" . $e->getMessage(); } finally { if (isset($conn)) { $conn->close(); } } ?>
关键要点:
- 启用
MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT,让MySQL错误自动抛出异常,避免遗漏隐式回滚的通知 - 提交前通过
SELECT @@in_transaction确认事务状态,0则说明事务已被回滚 - 可选的最终数据校验,应对极端场景下提交成功但数据未持久化的情况
二、确定性复现隐式回滚的方法
通过两个并发连接触发死锁,MySQL会自动回滚其中一个事务:
步骤1:创建测试表
CREATE TABLE test_deadlock ( id INT PRIMARY KEY AUTO_INCREMENT, balance INT NOT NULL ) ENGINE=InnoDB; INSERT INTO test_deadlock (balance) VALUES (100);
步骤2:事务1脚本(先运行)
<?php mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); $conn1 = new mysqli('localhost', 'username', 'password', 'dbname'); $conn1->set_charset('utf8mb4'); $conn1->begin_transaction(); // 持有id=1的行锁 $conn1->query("UPDATE test_deadlock SET balance = balance - 50 WHERE id = 1"); // 等待事务2触发死锁 sleep(10); try { // 尝试获取id=2的锁(事务2已持有) $conn1->query("UPDATE test_deadlock SET balance = balance - 50 WHERE id = 2"); $conn1->commit(); echo "事务1提交成功"; } catch (mysqli_sql_exception $e) { echo "事务1错误:" . $e->getMessage(); // 死锁后事务已被自动回滚,in_transaction返回false var_dump($conn1->in_transaction); } $conn1->close(); ?>
步骤3:事务2脚本(事务1运行后立即启动)
<?php mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); $conn2 = new mysqli('localhost', 'username', 'password', 'dbname'); $conn2->set_charset('utf8mb4'); $conn2->begin_transaction(); // 插入并持有id=2的行锁 $conn2->query("INSERT INTO test_deadlock (balance) VALUES (100)"); $conn2->query("UPDATE test_deadlock SET balance = balance - 50 WHERE id = 2"); try { // 尝试获取id=1的锁(事务1已持有) $conn2->query("UPDATE test_deadlock SET balance = balance - 50 WHERE id = 1"); $conn2->commit(); echo "事务2提交成功"; } catch (mysqli_sql_exception $e) { echo "事务2错误:" . $e->getMessage(); } $conn2->close(); ?>
复现流程
- 先运行事务1脚本,在10秒延迟期间启动事务2脚本
- 两个事务形成循环锁,MySQL自动回滚其中一个(通常是后发起锁请求的事务)
- 被回滚的脚本会输出错误信息,且
$conn->in_transaction为false,此时执行commit将提交空事务
内容的提问来源于stack exchange,提问作者user21266319
相关产品推荐
相关产品推荐

