数据库连接共用问题

必须每个进程单独创建Redis、MySQL、PDO连接,其他的存储客户端同样也是如此。原因是如果共用1个连接,那么返回的结果无法保证被哪个进程处理。持有连接的进程理论上都可以对这个连接进行读写,这样数据就发生错乱了。

极端情况下,若只能有一个连接,不得已共用的情况下,建议通过消息队列来解决问题,具体请参考示例代码中的Kafka应用。

如何避免这种情况

由于进程克隆原因,因此尽可能避免在Swoole启动前的位置创建数据库链接(回调事件是服务启动会才会被调用的,onStart事件除外)。

产生场景

简单的,我们把Swoole类比为pcntl_fork(),那么就有:

  1. /*
  2. 在pcntl_fork之前发生进程克隆的链接,在执行了pcntl_fork之后,会在A、B两区域共享。
  3. 就好比在Swoole Server启动前去创建链接。
  4. */
  5. $con = new mysqli();//创建一个数据库链接
  6. //pcntl_fork就好比执行了Server->start();会发生进程克隆。
  7. if(pcntl_fork()){
  8. //A
  9. $con->exec($sql1);
  10. }else{
  11. //B
  12. $con->exec($sql2);
  13. }

由于进程调度,导致不同进程分配到的CPU时间是零散,不连续的,因此会导致A、B两区域的代码块执行时间是错杂的,
最后执行顺序就是可能是:

  • A进程写了sql1前一半的数据进去
  • B进程写了sql2前一半的数据进去
  • A进程继续写了sql1后一半的数据进去
  • B进程继续写了sql2后一半的数据进去

因此,这样就会导致执行出问题。所以应该是:

  1. if(pcntl_fork()){
  2. //A
  3. $con = new mysqli();//创建一个数据库链接
  4. $con->exec($sql1);
  5. }else{
  6. //B
  7. $con = new mysqli();//创建一个数据库链接
  8. $con->exec($sql2);
  9. }

类比Swoole,A、B代码区域就好比Swoole的各个回调事件(onStart事件除外),执行这些回调事件的时候,已经发生了进程克隆。

类比EasySwoole

EasySwoole中,frameInitialize、frameInitialized、beforeWorkerStart、onStart均属于服务启动前事件。而IOC容器,也就是Di,是借助单例模式实现的,在服务启动后,会由于进程克隆原因,Di在服务启动前所
注入的数据会在各个进程中复制一份,并独立执行保存进程启动以后的状态数据。如何正确使用IOC来管理单例链接:

  1. $conf = Config::getInstance()->getConf('MYSQL');
  2. Di::getInstance()->set(SysConst::MYSQL,\MysqliDb::class,array(
  3. 'host' => $conf['HOST'],
  4. 'username' => $conf['USER'],
  5. 'password' => $conf['PASSWORD'],
  6. 'db' => $conf['DB_NAME'],
  7. 'port' => $conf['PORT'],
  8. ));

在frameInitialize、frameInitialized、beforeWorkerStart三个事件中,均可做IOC注入。

由于EasySwoole中的Di是懒惰加载模式,因此注入到Di中的数据,此刻实际上只有类名,和实例化参数。但是当执行了$di->get(‘MYSQL’)后,Di中MYSQL这个键值将会变为实例化后的MYSQL对象。
因此请避免在服务启动前的事件中调用$di->get(‘MYSQL’)。

错误用法:

  1. Di::getInstance()->set('MYSQL',new MysqlDb($conf));

以上设置到Di的内容就是一个实例化后的数据库对象,因此进程克隆后,仅会有一个连接。

注意,在beforeWorkerStart中去注册swoole的其他回调事件,并在事件回调内使用了$di->get(‘MYSQL’)不算违规调用。若在服务启动前事件需要用到数据库,请单独创建,并勿设置到Di中使用,例如:

  1. frameInitialize(){
  2. $db = new Mysql();
  3. $db->exec($sql);
  4. $db->close();
  5. }