mysql连接池就是把创建的mysql的连接,通过swoole保存下来,下个线程直接用而不用再次连接了。

        事情是这么个事情,但是这个事情下其实有个潜在的逻辑,那就是多个php线程的mysql线程是一个,那也就是说,这多个php线程在mysql端的上下文是一个,也就是说,PHP-A线程开启了事务,如果没有提交,PHP-B线程执行了回滚,PHP-A的sql就被回滚了。

        PHP-A没有提交的原因大概有2个,1个是忘了,这个可以用tp的Db::transaction(function(){});去写,就没有忘的问题了。第二个原因是各种意外错误,比如访问一个远程接口超时了,整个访问都断了,后面的代码自然都没运行。

        这个问题本来也无所谓,因为mysql的不允许事务嵌套的,在pdo下,连续开启2次事务,第二次会报错“There is already an active transaction”,发现问题自然就去解决了,问题出在用了框架。

        关于事务的控制,thinkphp都在文件vendor\topthink\think-orm\src\db\PDOConnection.php。(我是在关闭事务嵌套的前提先去说这个事儿,否则太复杂了不方便说明。关闭事务嵌套是改这个方法“supportSavepoint”)

        这个文件用$this->transTimes来记录是否开启过事务,如果这个变量大于1,那就不执行开始事务的代码,只累加,commit和rollback也是同理,如果发现$this->transTimes不是1,那么就只累减,只有1了才去执行代码。这样就保证了只有最外层的事务有效,里面写的再多其实都没用。

        如果PHP-A没有结束事务,PHP-B没有写事务,但是有一些update和insert,PHP-C是正常的事务开始,事务提交。因为PHP-C的事务开始代码,tp根据$this->transTimes已经大于1来判断,实际是没运行任何代码,PHP-C的事务提交代码,同理也是没运行任何代码。一个mysql线程可能会用几个小时(我通过rds的日志看的,是真实情况),这样各种代码不断的累积,指不定那句代码就写了个事务回滚,这样之前所有的sql就都回滚了(真实经历,当出现这个问题是完全是懵了)。

        到这里其实是有个问题的,$this->transTimes是个变量,怎么可能2次PHP线程,这个东西会累加。我一开始也觉得不可能,这个道理如果能成立,代码岂不是都乱套了。但是实际测试确实如此。所以猜测tp不是单纯的把mysql的link存起来,而是把自己写的PDO类存起来,PDO类里包含了mysql连接和这个transTimes,所以就出现了累加。

        到这里还有个问题,如果多个PHP线程是公用一个mysql线程,如果PHP-A线程假如运行10秒,每秒插入一条数据,PHP-B在第五秒开始运行,写了句回滚,岂不是就把PHP-A的影响了。这个问题看起来是这么回事儿,但是实际应该不是,否则进程池不是扯淡了吗,实际测试了一下,发现是互不影响的,通过RDS查看2次的mysql线程号,也不是一个。猜测是swoole内部做了判断,并发的PHP进程,不共用一个mysql池。

        原理应该都说完了,优化改动就比较简单了,先在PDOConnection.php增加2个方法,可以获取transTimes和设置transTimes,找个中间件,在代码最开始检测下,如果大于1,就提交下事务。

if(Db::transTimesGet()!=0){
    Db::transTimesSet(1);
    Db::commit();
}

 在开启事务的代码那里,多加个判断,防止transTimes记录错误,造成事务二次开启

public function startTrans(): void { try { $this->initConnect(true); ++$this->transTimes; if (1 == $this->transTimes) { try { $this->linkID->beginTransaction(); }catch (\Exception $e){ //如果事务产生嵌套,开启事务会报错,捕捉到错误后,先提交commit if($e->getMessage()=='There is already an active transaction'){ $this->commit(); $this->linkID->beginTransaction(); } } } elseif ($this->transTimes > 1 && $this->supportSavepoint()) { $this->linkID->exec( $this->parseSavepoint('trans' . $this->transTimes) ); } $this->reConnectTimes = 0; } catch (\Throwable | \Exception $e) { if ($this->transTimes === 1 && $this->reConnectTimes < 4 && $this->isBreak($e)) { --$this->transTimes; ++$this->reConnectTimes; $this->close()->startTrans(); } else { if ($this->isBreak($e)) { // 尝试对事务计数进行重置 $this->transTimes = 0; } throw $e; } } }

        我说的这些,都是在关闭事务嵌套的前提下,个人觉得事务嵌套的应用场景几乎没有,所以干脆不要了。

protected function supportSavepoint(): bool {   //原来是true,我改成了false return false; }

        最后说点感慨,这个问题简直太操蛋了,最开始呈现出来的现象,就是数据无缘无故的没了,没有任何规律,出现于任何表,单纯看那个控制器,没有任何bug。通过rds的日志,看几万条sql,才发现是这个现象。各种教程光说公用进程池,进程池怎么好,就没有看见一个人提过事务的问题,我自己总结出来的这些也不知道对不对,只是我从现实情况来看,确实是这样。