PHP与MySQL事务怎么处理

wen PHP项目 4

PHP与MySQL事务处理终极指南:从基础到高并发下的数据一致性实战


📚 目录导读

  1. 什么是事务?为什么你的数据需要它?
  2. MySQL事务的四大特性(ACID)与底层原理
  3. PHP中开启事务的三种核心方式(MySQLi / PDO / 传统mysql扩展)
  4. 事务的提交、回滚与保存点(Savepoint)实战代码
  5. 高并发下的陷阱:死锁、锁等待与隔离级别选择
  6. 常见业务场景代码示例:银行转账、订单库存扣减
  7. 故障排查:如何通过日志与状态变量定位事务问题
  8. 常见问答(FAQ)与性能优化建议

什么是事务?为什么你的数据需要它?

在开发电商、金融或任何涉及多表更新的系统时,一个操作往往由多个SQL步骤组成,用户下单需要同时插入订单表、扣减库存、更新用户余额,如果其中某一步失败,会导致数据不一致——钱扣了但订单没生成,或者库存扣了但订单取消。

PHP与MySQL事务怎么处理

事务(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。
  • 使用批量UPDATEINSERT ... ON DUPLICATE KEY UPDATE合并语句。
  • 对于长事务,考虑拆分为多个短事务,并利用保存点实现部分回滚。

最后总结:PHP与MySQL的事务处理核心在于“显式控制”和“异常捕捉”,没有银弹,必须结合业务判断锁粒度与隔离级别,建议生产环境开启slow_query_loginnodb_print_all_deadlocks(MySQL 5.7+),以便事后审计与优化。数据一致性永远是第一优先级,性能优化需在数据安全的前提下进行。

抱歉,评论功能暂时关闭!