数据库连接共用问题

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

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

如何避免这种情况

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

产生场景

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

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

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

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

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

if(pcntl_fork()){
    //A
    $con = new mysqli();//创建一个数据库链接
    $con->exec($sql1);
}else{
    //B
    $con = new mysqli();//创建一个数据库链接
    $con->exec($sql2);
}

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

类比EasySwoole

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

$conf = Config::getInstance()->getConf('MYSQL');
Di::getInstance()->set(SysConst::MYSQL,\MysqliDb::class,array(
   'host' => $conf['HOST'],
   'username' => $conf['USER'],
   'password' => $conf['PASSWORD'],
   'db' => $conf['DB_NAME'],
   'port' => $conf['PORT'],
));

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

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

错误用法:

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

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

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

frameInitialize(){
    $db = new Mysql();
    $db->exec($sql);
    $db->close();
}

results matching ""

    No results matching ""