webman 事务回滚失效问题记录
简单介绍下webman
webman是一款基于workerman开发的高性能HTTP服务框架。webman用于替代传统的php-fpm架构,提供超高性能可扩展的HTTP服务。你可以用webman开发网站,也可以开发HTTP接口或者微服务。
除此之外,webman还支持自定义进程,可以做workerman能做的任何事情,例如websocket服务、物联网、游戏、TCP服务、UDP服务、unix socket服务等等。
是否推荐大家使用
首先,小编不会推荐看到的朋友使用,不是因为它不好,而是因为有更多的选择.
事务回滚失效问题排查
一段很普通的代码逻辑:更新订单退款状态字段+新增退款申请记录.然后在一个事务里就行原子操作.
return Db::transaction(function () use ($order, $explain) { $order->refund_status = OrderEnum::ORDER_REFUND_STATUS_ING; $order->save(); $orderRefundModel = new OrderRefund(); $orderRefundModel->refund_id = 'R' . date('YmdHis') . random_int(10000, 99999); $orderRefundModel->order_id = $order->order_id; $orderRefundModel->status = OrderEnum::ORDER_REFUND_APPLY_STATUS_ING; $orderRefundModel->refund_price = $order->paid_amount; $orderRefundModel->refund_num = 1; $orderRefundModel->refund_phone = $order->user_mobile; $orderRefundModel->refund_explain = $explain; $orderRefundModel->save(); return $orderRefundModel->id; });
这样的代码在我们的laravel项目上是一点问题没有,但是在webman框架使用却出现了事务回滚失败的问题.
问题的原因
其实这个问题还是因为小编没有认真阅读官方文档导致的,而上面的代码导致了一个什么错误让回滚失效的呢?
当事务中操作模型时,特别需要注意模型是否设置了连接。如果模型设置了连接开启事务的时候要指定连接,否则事务无效(think-orm类似)。例如
<?phpnamespace app\model;use support\Model;class User extends Model{ // 这里给模型指定了连接 protected $connection = 'mysql'; protected $table = 'users'; protected $primaryKey = 'id';}
当模型指定了连接时,开启事务、提交事务、回滚事务必须指定连接
Db::connection('mysql')->beginTransaction();try { // 业务处理 $user = new User; $user->name = 'webman'; $user->save(); Db::connection('mysql')->commit();} catch (\Throwable $exception) { Db::connection('mysql')->rollBack();}
排错之路
通过代码是无法发现问题的,小编通过打开mysql 查询日志general_log来观察执行的sql
打开查询日志配置
[mysqld]general_log = ON# 这个日志文档路径必须要有权限写入,否则general_log打开失败general_log_file = /path/to/your/logfile.logSHOW VARIABLES LIKE 'general_log%';
将 general_log 设置为 ON表示启用查询日志,general_log_file 指定了日志文件的路径,重启 MySQL 服务器:在修改了配置文件后,需要重启 MySQL 服务器以使更改生效。查看日志文件:启用查询日志后,MySQL 会将所有执行过的 SQL 语句记录到指定的日志文件中。您可以查看该文件以获取所有 SQL 语句以及它们对应的连接信息。
查询日志
从上面的日志我们可以看到,事务和业务sql对应的线程ID是不一样的,那么这就是事务回滚两个业务sql不启用的原因了
为啥是不同的线程ID呢
线程ID不一样,肯定是两边分别创建了各自的连接,那我们就开始排查.通过查找了,我发现了webman admin 插件所有的model 都继承了Base类,而Base类指定了连接名,这下就破案了,也对应了官方的那句话:当模型指定了连接时,开启事务、提交事务、回滚事务必须指定连接
因此我们正确的代码:
Db::connection('plugin.admin.mysql')->transaction(...)
再看看连接的源码,默认的连接指向我们配置文件`database.php`的default属性值
DB manager 源码:
/** * Get a database connection instance. * * @param string|null $name * @return \Illuminate\Database\Connection */ public function connection($name = null) { [$database, $type] = $this->parseConnectionName($name); $name = $name ?: $database; // If we haven't created this connection, we'll create it based on the config // provided in the application. Once we've created the connections we will // set the "fetch mode" for PDO which determines the query return types. if (! isset($this->connections[$name])) { $this->connections[$name] = $this->configure( $this->makeConnection($database), $type ); } return $this->connections[$name]; } /** * Parse the connection into an array of the name and read / write type. * * @param string $name * @return array */ protected function parseConnectionName($name) { $name = $name ?: $this->getDefaultConnection(); return Str::endsWith($name, ['::read', '::write']) ? explode('::', $name, 2) : [$name, null]; } /** * Make the database connection instance. * * @param string $name * @return \Illuminate\Database\Connection */ protected function makeConnection($name) { $config = $this->configuration($name); // First we will check by the connection name to see if an extension has been // registered specifically for that connection. If it has we will call the // Closure and pass it the config allowing it to resolve the connection. if (isset($this->extensions[$name])) { return call_user_func($this->extensions[$name], $config, $name); } // Next we will check to see if an extension has been registered for a driver // and will call the Closure if so, which allows us to have a more generic // resolver for the drivers themselves which applies to all connections. if (isset($this->extensions[$driver = $config['driver']])) { return call_user_func($this->extensions[$driver], $config, $name); } return $this->factory->make($config, $name); }