hyperf事务安全取决于变量绑定、协程连接归属与上下文穿透:拼接sql不防注入,手动begintransaction易致连接泄漏,读写分离需显式指定写库,异步事务须避免跨协程调用。

Hyperf 事务本身不防 SQL 注入,也不自动隔离协程间数据——你写的事务代码是否安全,取决于怎么写、在哪写、参数怎么传。
Db::transaction() 里拼 SQL 就等于没事务
很多人以为套了 DB::transaction() 就万事大吉,结果在闭包里直接字符串拼接:
-
DB::transaction(function () use ($uid) { DB::update("UPDATE user SET status = '{$status}' WHERE id = {$uid}"); });—— 这段代码既不防注入,又可能因连接复用导致“MySQL server has gone away” - 事务只保证原子性,不提供输入校验或预处理能力;底层仍走 PDO,但拼接绕过了预处理机制
- 协程环境下,连接对象可能被多个请求复用,拼接后的 SQL 会污染后续查询(尤其当
$status含单引号或注释符时)
正确做法:所有变量必须走绑定,哪怕在事务闭包里:
DB::transaction(function () use ($uid, $status) {
DB::update("UPDATE user SET status = ? WHERE id = ?", [$status, $uid]);
});
手动 beginTransaction() + commit() 是协程陷阱
不用 DB::transaction() 而自己调 DB::beginTransaction(),极易引发连接泄漏和跨协程错乱:
- 协程 A 调用
beginTransaction()获取连接,中途让出控制权;协程 B 又从池里拿到同一连接并执行commit(),A 的事务就失效了 - 未捕获异常时,
rollback()不会被触发,连接卡在“已开启事务”状态,后续请求可能阻塞在wait_timeout上 - 连接池配置中
pool.max_idle_time默认 60 秒,但长时间挂起的事务连接不会被主动回收
必须用闭包式事务,它会在退出时强制清理上下文:
// ✅ 安全:自动 rollback / commit,连接归还池
DB::transaction(function () {
User::create([...]);
Order::create([...]);
});
// ❌ 危险:手动流程无法保证协程安全
DB::beginTransaction();
try {
User::create([...]);
Order::create([...]);
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
读写分离场景下,事务默认走读库?
Hyperf 默认开启读写分离时,DB::transaction() 仍可能路由到从库——这不是 bug,而是配置逻辑:
- 如果
config/autoload/database.php中未显式指定写库连接名(如'write_connection' => 'default'),事务可能复用当前上下文中的读连接 - 刚插入数据立刻在事务内查不到,大概率是查了从库,且主从延迟未同步完成
- 模型操作(如
User::create())默认走写库,但原始 SQL 查询(DB::select())不自动继承写库策略
解决方案:强制指定连接或使用 useWritePdo():
// 方式1:显式指定连接
DB::connection('default')->transaction(function () {
...
});
// 方式2:模型查询强制走写库
User::on('default')->where('id', 1)->useWritePdo()->first();
协程上下文丢失导致事务回滚失败
在异步任务(如 Coroutine::create() 或事件监听器)中启动事务,容易因上下文隔离而失效:
-
Context::set()是协程局部的,父协程设的事务状态,子协程拿不到 - 用
go(function () { DB::transaction(...); })启动的协程,其事务连接不会与主请求共享,rollback()可能作用于错误连接 - Hyperf 的
DB::transaction()依赖当前协程上下文存储连接,跨协程调用等于新开一个连接池实例
真正需要异步写库时,别在子协程里开事务,改用消息队列或同步回调:
// ❌ 错误:子协程事务无法被主流程感知
go(function () {
DB::transaction(fn () => Log::create([...]));
});
// ✅ 正确:由主协程控制,子协程只做非事务操作
DB::transaction(function () {
Order::create([...]);
// 日志另起轻量级操作,不参与事务
go(fn () => Log::create([...]));
});
事务安全的关键不在“有没有 wrap”,而在“变量是否绑定”“连接是否归属当前协程”“上下文是否穿透”。很多问题不是框架缺陷,是把传统 PHP 的写法直接搬进协程环境里造成的。











