必须用beginTransaction():当一次业务操作涉及多张表写入且需“全成功或全失败”时,因MySQL默认autocommit=1,自动提交会破坏一致性,必须显式开启事务。

什么时候必须用 beginTransaction() 而不是靠自动提交
当一次业务操作涉及多张表写入,且必须“全成功或全失败”时,自动提交(autocommit)会直接破坏数据一致性。比如用户充值后更新余额、记录流水、发放积分,三步中任何一步出错,前面已执行的步骤不能留在数据库里。
常见错误现象:INSERT 成功但 UPDATE 报错后,余额没变、流水却多了一条;或者用 try...catch 包裹多个 query() 却没调用 beginTransaction(),结果事务根本没开启。
- MySQL 默认是 autocommit=1,
mysqli和PDO都不会自动开启事务,必须显式调用beginTransaction() -
PDO下还要确认PDO::ATTR_ERRMODE设为PDO::ERRMODE_EXCEPTION,否则execute()失败不会抛异常,catch捕不到 - MyISAM 引擎不支持事务,务必确认表是 InnoDB
commit() 和 rollback() 的调用时机怎么才安全
事务结束必须有且仅有一次明确的 commit() 或 rollback(),漏掉、重复调用、或在异常分支外提前返回,都会导致连接处于“事务中”状态,后续查询可能被意外包含进未提交的上下文,甚至锁表。
典型场景:函数内处理转账,中间调用了另一个也操作数据库的工具函数,但那个函数自己又开了事务——嵌套事务在 PHP 原生 PDO/MySQL 中并不真正支持,第二次 beginTransaction() 只是静默忽略,commit() 会提交最外层事务,容易误判成功。
立即学习“PHP免费学习笔记(深入)”;
- 所有
commit()/rollback()必须成对出现在同一个作用域,避免在 if 分支里只写commit()却漏掉 else 里的rollback() - 不要在事务块内做耗时操作(如 HTTP 请求、文件读写),超时或异常会导致连接长时间占用
- 使用
finally块不解决根本问题——PHP 不支持事务级 finally,必须在 catch 后显式 rollback
PDO 中预处理语句和事务一起用要注意什么
预处理语句本身不影响事务行为,但参数绑定错误或 SQL 语法错,会导致 execute() 失败却不触发 catch,如果错误模式没设对,事务就卡在“已开始未结束”状态。
例如:$stmt = $pdo->prepare("UPDATE accounts SET balance = ? WHERE id = ?"); 绑定字符串 ID 却传了整数,MySQL 可能隐式转换成功,但实际更新了错误行,而程序以为一切正常。
- 确保
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION)在创建连接后立即设置 - 每个
execute()后可加if (!$stmt->execute(...)) { throw new Exception(...); }做兜底(虽然冗余,但比静默失败强) - 事务中避免复用同一
PDOStatement对象执行不同 SQL,尤其跨表操作,易因内部状态混乱导致绑定失效
并发环境下事务隔离级别引发的典型问题
默认的 REPEATABLE READ 级别下,两个请求同时读取同一账户余额并各自加 100,最终结果可能是 +100 而非 +200——这不是 PHP 代码问题,是 MySQL 的幻读/不可重复读表现,需要靠应用层加锁或调整隔离级别来规避。
错误做法:用 SELECT ... FOR UPDATE 但没放在事务内,或放在 commit() 之后,锁立刻释放,起不到保护作用。
- 需要强一致性时,在
beginTransaction()后立即执行带FOR UPDATE的SELECT,再做计算和UPDATE -
READ COMMITTED可减少锁等待,但需确认业务能否接受“同一事务中两次 SELECT 结果不同” - 高并发扣减库存类场景,单纯依赖事务不够,得配合唯一索引+插入冲突判断,或 Redis 原子操作预占
begin/commit 就保证逻辑正确——它强制你把“哪些操作必须原子”“失败时如何回退到一致状态”“并发时谁先谁后”这些问题提前想清楚。很多 bug 是事务开启了,但业务逻辑本身没对齐一致性边界。



















