PHP与MySQL事务处理终极指南:从基础到高并发下的数据一致性实战
📚 目录导读
- 什么是事务?为什么你的数据需要它?
- MySQL事务的四大特性(ACID)与底层原理
- PHP中开启事务的三种核心方式(MySQLi / PDO / 传统mysql扩展)
- 事务的提交、回滚与保存点(Savepoint)实战代码
- 高并发下的陷阱:死锁、锁等待与隔离级别选择
- 常见业务场景代码示例:银行转账、订单库存扣减
- 故障排查:如何通过日志与状态变量定位事务问题
- 常见问答(FAQ)与性能优化建议
什么是事务?为什么你的数据需要它?
在开发电商、金融或任何涉及多表更新的系统时,一个操作往往由多个SQL步骤组成,用户下单需要同时插入订单表、扣减库存、更新用户余额,如果其中某一步失败,会导致数据不一致——钱扣了但订单没生成,或者库存扣了但订单取消。

事务(Transaction)正是为解决此问题而生,它是一组逻辑操作单元,要么全部成功,要么全部失败,没有事务保护的系统,在高并发下必然会出现脏读、幻读和不可重复读,最终导致业务逻辑崩溃。
MySQL事务的四大特性(ACID)与底层原理
- 原子性 (Atomicity):通过
undo log(回滚日志)实现,记录操作前的状态。 - 一致性 (Consistency):由应用层代码和数据库约束共同保证,确保数据满足业务规则。
- 隔离性 (Isolation):通过锁机制和
MVCC(多版本并发控制)实现,防止事务间相互干扰,默认隔离级别为REPEATABLE READ(可重复读)。 - 持久性 (Durability):通过
redo log(重做日志)实现,即使数据库崩溃也能恢复已提交的数据。
关键点:PHP脚本本身是无状态的,事务必须显式开启、提交或回滚,千万不能依赖PHP脚本结束后自动提交(在InnoDB引擎下,若未提交则自动回滚)。
PHP中开启事务的三种核心方式
① MySQLi(面向对象)
$mysqli = new mysqli("localhost", "user", "pass", "db");
$mysqli->begin_transaction();
try {
$mysqli->query("UPDATE accounts SET balance = balance - 100 WHERE id = 1");
$mysqli->query("UPDATE accounts SET balance = balance + 100 WHERE id = 2");
$mysqli->commit();
echo "转账成功";
} catch (Exception $e) {
$mysqli->rollback();
echo "失败:" . $e->getMessage();
}
② PDO(推荐,支持预处理)
$pdo = new PDO("mysql:host=localhost;dbname=test", "user", "pass");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->beginTransaction();
try {
$sql = "UPDATE stock SET num = num - ? WHERE goods_id = ?";
$stmt = $pdo->prepare($sql);
$stmt->execute([5, 1001]);
// 若需检查受影响行数,若为0则抛异常
if ($stmt->rowCount() == 0) {
throw new Exception("库存不足");
}
$pdo->commit();
} catch (Exception $e) {
$pdo->rollBack();
error_log($e->getMessage());
}
*③ 传统mysql_(已废弃,仅利旧项目)**
mysql_connect(...);
mysql_query("START TRANSACTION");
// 执行SQL
if ($all_ok) {
mysql_query("COMMIT");
} else {
mysql_query("ROLLBACK");
}
事务的提交、回滚与保存点实战
保存点允许在长事务中部分回滚,不必完全回滚到起点:
$pdo->beginTransaction();
$pdo->exec("UPDATE a SET ...");
$pdo->exec("SAVEPOINT SP1"); // 设置保存点
$pdo->exec("UPDATE b SET ...");
// 如果b表失败,只回滚到SP1
$pdo->exec("ROLLBACK TO SAVEPOINT SP1");
$pdo->commit();
重要:事务应尽量短小精悍,长时间持有锁会造成阻塞,拖垮数据库性能。
高并发下的陷阱:死锁、锁等待与隔离级别选择
-
死锁:两个事务互相持有对方需要的锁,MySQL会自动检测并回滚其中一个事务(
innodb_deadlock_detect)。// 死锁示例:事务A更新a表再更新b表;事务B更新b表再更新a表
解决方案:统一表访问顺序;重试机制捕获死锁异常(错误码1213)。
-
隔离级别:PHP中可通过执行SQL设置:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
高并发下,推荐从默认的
REPEATABLE READ降为READ COMMITTED以减少锁范围(但需评估业务是否能接受幻读风险)。 -
锁等待超时:
innodb_lock_wait_timeout默认50秒,在PHP中设置较短超时,配合重试更友好。
常见业务场景代码示例:银行转账与订单库存
场景A:银行转账(确保原子性)
$pdo->beginTransaction();
try {
// 1. 锁住转出账户行
$pdo->query("SELECT balance FROM accounts WHERE account_no='A' FOR UPDATE");
$pdo->query("UPDATE accounts SET balance=balance-1000 WHERE account_no='A'");
$pdo->query("UPDATE accounts SET balance=balance+1000 WHERE account_no='B'");
$pdo->commit();
} catch (Exception $e) {
$pdo->rollBack();
// 记录日志,发送告警
}
场景B:秒杀扣库存
// 为了防止超卖,使用条件更新 + 受影响行数判断
$sql = "UPDATE goods SET stock = stock - 1 WHERE id = :id AND stock > 0";
$stmt = $pdo->prepare($sql);
$stmt->execute([':id' => $goodsId]);
if ($stmt->rowCount() > 0) {
// 插入订单表...最后统一commit
} else {
// 无库存,回滚或直接抛出业务异常
}
故障排查:如何通过日志与状态变量定位问题
- 查看当前运行的事务:
SELECT * FROM information_schema.INNODB_TRX\G;
- 查看锁等待与死锁日志:
SHOW ENGINE INNODB STATUS\G;
重点关注
LATEST DETECTED DEADLOCK部分。 - PHP侧调试:
$pdo->setAttribute(PDO::ATTR_AUTOCOMMIT, 0); // 关闭自动提交 echo $pdo->errorInfo(); // 输出错误详情
常见问答(FAQ)与性能优化建议
问题1:PDO中beginTransaction()和exec("START TRANSACTION")有区别吗?
答:beginTransaction()会设置PDO的错误模式为异常,并自动关闭自动提交,更安全,不建议混用。
问题2:事务中能否执行DDL(CREATE TABLE)?
答:可以,但DDL隐式提交事务(MySQL默认),导致事务提前结束,尽量避免在事务中做DDL。
问题3:为什么我的事务回滚不生效?
答:检查表引擎是否为InnoDB(MyISAM不支持事务);检查是否使用了setAutoCommit(false)但未正确调用commit()。
性能优化建议:
- 尽量减少事务内网络延迟,避免PHP循环中逐条UPDATE。
- 使用
批量UPDATE或INSERT ... ON DUPLICATE KEY UPDATE合并语句。 - 对于长事务,考虑拆分为多个短事务,并利用
保存点实现部分回滚。
最后总结:PHP与MySQL的事务处理核心在于“显式控制”和“异常捕捉”,没有银弹,必须结合业务判断锁粒度与隔离级别,建议生产环境开启slow_query_log和innodb_print_all_deadlocks(MySQL 5.7+),以便事后审计与优化。数据一致性永远是第一优先级,性能优化需在数据安全的前提下进行。