必须用innodb引擎、pdo异常模式和三步事务(begin/try/commit+rollback)确保扣款、下单、库存更新等操作原子性,配合隔离级别控制与资源清理。

要在PHP中安全执行扣款、下单、库存更新等多步数据库操作,必须确保所有步骤要么全部成功、要么全部失败,否则会导致订单创建了但库存没扣减、或库存扣了但订单没生成这类数据错乱问题。
开启事务前的必要准备
先确认你的MySQL表使用的是InnoDB引擎——MyISAM不支持事务,执行SHOW CREATE TABLE your_table_name查看ENGINE字段,不是InnoDB就立刻改:【ALTER TABLE your_table_name ENGINE=InnoDB】。
用PDO连接时,必须显式启用异常模式,否则beginTransaction()失败也不会报错,事务形同虚设。
连接代码里加上这两行:$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);和$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);——后者能确保SQL语法错误真实抛出,不被模拟层吞掉。
标准事务流程:三步闭环
第一步:调用$pdo->beginTransaction()显式关闭自动提交,后续所有SQL都在事务上下文中执行。
第二步:在try块内顺序执行业务SQL,例如先UPDATE库存再INSERT订单;每条语句都必须放在try里,漏掉任意一条,出错就无法回滚。
第三步:执行$pdo->commit()提交。只要没走到这一步,所有变更都还锁在内存里,对外不可见。
catch块中必须调用$pdo->rollback()——这一步不能省,否则事务残留会卡住连接,高并发下极易引发死锁。
捕获特定异常类型
方法一:用通用PDOException $e捕获所有数据库异常,适合调试阶段快速定位问题。
方法二:针对唯一约束冲突(如重复插入邮箱),单独捕获SQLSTATE 23000错误码:
在catch块里加判断if ($e->getCode() === '23000') { echo '该邮箱已被注册'; }。
方法三:区分连接失败与SQL执行失败——连接异常在new PDO时抛出,SQL异常在exec/execute时抛出,两者堆栈位置不同,日志里要分开记录。
高并发下的隔离级别控制
默认隔离级别是REPEATABLE READ,能防止脏读和不可重复读,但幻读仍可能发生。若业务要求强一致性(如秒杀场景),需手动提升:$pdo->exec("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE");。
注意:【SERIALIZABLE会锁整张表,务必只在关键事务开头设置,且事务体越短越好】,否则大量请求排队等待,响应时间飙升。
执行完关键操作后,可立即切回默认级别:$pdo->exec("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ");,避免影响其他普通查询。
事务结束后的资源清理
事务提交或回滚后,PDOStatement对象仍持有结果集引用,不释放会持续占用内存。
显式置空所有stmt变量:$stmt1 = null; $stmt2 = null;,再把$pdo也置为null——这样PHP才能真正释放底层MySQL连接。
不要依赖脚本结束时的自动回收,尤其在长生命周期的常驻进程(如Swoole Worker)中,未释放的连接会迅速耗尽MySQL最大连接数。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











