swoole中pdo的begintransaction()不生效,因pdo非协程安全,事务状态无法跨协程保持;必须使用swoole\coroutine\mysql并确保同一协程内完成begin/commit/rollback,显式设置隔离级别,禁用sleep等阻塞操作。

为什么 Swoole 的 pdo->beginTransaction() 在协程里不生效
因为 PDO 默认不是协程安全的,底层 TCP 连接被协程调度器接管后,事务状态无法跨协程保持。你调用 beginTransaction() 后切走,另一个协程可能复用同一连接并执行 commit(),导致前一个事务被意外提交。
实操建议:
- 必须使用 Swoole 提供的协程 MySQL 客户端
Swoole\Coroutine\MySQL,它原生支持事务且状态绑定到当前协程 - 禁用 PDO 或 mysqli 的传统方式,哪怕加了
setConfig(['mysql' => ['strict' => true]])也无效 - 若用 Laravel + Swoole,确保启用了
laravel-swoole的协程数据库驱动,而非默认 PDO 封装
Swoole\Coroutine\MySQL::begin() 失败但没报错?检查这三点
常见现象是 begin() 返回 true,但后续 insert 报错后 rollback() 不起作用,查表发现数据已写入。
原因和排查点:
- 连接是否真的在同一个协程内?
go(function () { $db = new \Swoole\Coroutine\MySQL(); $db->connect(...); ... });每次new都是新连接,事务不共享 - 是否混用了
query()和execute()?query()不支持预处理,某些语句(如带变量的INSERT ... SELECT)会绕过事务控制 - MySQL 服务端 autocommit 是否关闭?需显式执行
$db->query('SET autocommit=0');,仅begin()不足以保证隔离
并发请求下事务隔离失效:不是 bug,是没设对 isolation
Swoole\Coroutine\MySQL 的 begin() 默认不指定隔离级别,实际走 MySQL 默认(通常是 REPEATABLE-READ),但协程高并发时容易出现幻读或脏读,尤其在秒杀类逻辑中。
Swoole 6.1.1 是一个专为 PHP 设计的高性能事件驱动并发网络引擎。作为稳定版,它修复了编译时对 zlib 依赖的缺失及 curl 模块的内存安全风险。该版本支持协程、多线程与多进程架构,内置 TCP/HTTP/WebSocket 服务器,能够显著提升 PHP 在微服务、实时通信等场景下的执行效率与并发能力。
正确做法:
- 显式传参:
$db->begin(\Swoole\Coroutine\MySQL::TRANSACTION_ISOLATION_READ_COMMITTED) - 避免在事务中调用外部 HTTP 请求或 sleep,协程挂起期间连接可能被回收或复用
- 注意 MySQL 版本兼容性:
READ_UNCOMMITTED在 5.7+ 支持,但 5.6 只认字符串'READ-COMMITTED',传错值会导致静默降级为默认级别
用 defer 做事务兜底,但 rollback() 被跳过?
习惯写 go(function () { $db->begin(); defer(function () use ($db) { $db->rollback(); }); ... $db->commit(); });,结果异常时 rollback() 没触发。
问题根源:
-
defer绑定的是协程生命周期,不是代码块作用域;如果commit()成功后协程未退出,defer不执行 - 更可靠的方式是用
try/catch+ 显式rollback(),并在finally中判断是否已提交 - 不要依赖
onClose或连接池close()回调做回滚——连接可能被复用,状态不可控
事务的边界必须由业务代码严格控制,协程的“自动”特性在这里反而是陷阱。最易忽略的,是以为 begin() 之后的所有操作天然属于该事务——其实只限于同一连接、同一协程、同一 MySQL 会话上下文。










