一次开发踩坑笔记

1. 起因.

在公司的一个项目中, 使用 PHP + Swoole开发一个常驻内存服务端, 对 \Swoole\Database\PDOPool 进行了封装. 编写了一个查询语句构造器. 因为要对 查询结果进行类型转换, 我的解决思路是在系统启动对时候, 通过遍历数据库连接, 对数据库中的表结构进行扫描, 并将其作为配置 缓存在 config/tables_gen.php 文件中. 一切看上去很美好, 知道我将工作进程根据CPU核心数配置成多个时候, 问题出现了.

2. 问题现象

\sys\Db 类通过 table 方法 构造出一个 SqlBuilder 然后 使用 Sqlbuilder 构造Sql语句, 工作进程的查询结果出现了错乱,本该上一个查询的结果出现在下一个查询返回中, 我的第一反应是 连接池出了问题. 事实上也确实是连接池出了问题. 由于我使用 类静态属性来缓存每一个连接池. 构造代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Db{
protected static $pool = [];
protected $name;
public function __construct(string $connection = 'db.default') {
$this->name = $connection; # 标记连接名字

$conf = Config::get($this->name); # 获取数据库连接的配置

# 这里是表前缀, 后续返回查询结果的时候, 需要读取字段映射
$this->_tableGenPfx = "tables_gen.{$conf['dbname']}.";

f(!empty(self::$pool[$this->name])) {
$this->conn = static::$pool[$this->name]->get();
}

static::$pool[$this->name] = $pool = new \Swoole\Database\PDOPool((new \Swoole\Database\PDOConfig)
->withHost($conf['dbhost'])
->withPort($conf['dbport'])
// ->withUnixSocket('/tmp/mysql.sock')
->withDbName($conf['dbname'])
->withCharset($conf['charset'])
->withUsername($conf['dbuser'])
->withPassword($conf['dbpass'])
->withOptions([
\PDO::ATTR_DEFAULT_FETCH_MODE =>\PDO::FETCH_ASSOC, //返回 k->v 数组
\PDO::ATTR_ERRMODE=>\PDO::ERRMODE_EXCEPTION, //异常模式
\PDO::ATTR_PERSISTENT=>true, //定义为持久连接
])
,16 # 初始创建16个数据库连接
);

$this->conn = static::$pool[$this->name]->get();
}
# ....
}

3. 发现问题

经过分析 最终发现问题出在 构造表结构的部分. 因为构造标结构的时候使用了Db 类, 实际上这段代码在 master 进程中执行, 因此后续创建Worker进程的时候, 为每个Worker 进程都创建了一份拷贝, 而实际上 这时候 Db::pool 属性指向的都是同一个连接池池. 在后续使用连接池的时候, 出现了多个进程引用到同一个数据库连接的情况, 既然确定了问题所在, 解决问题的思路也就容易了. 解决思路有2个.

  1. 创建一个不缓存的连接池(需要改造或者重新实现一个Db类) 代价有点高.
  2. 在全局部分避免使用\sys\Db 比如使用 \Swoole\Coroutine\MySQL 替代 (相对简单)

最终, 我选择了方案2来解决问题, 系统工作正常.

4. 框架执行流程如下.

4.1 Master 进程启动过程

  1. Master 进程启动
  2. Master 进程根据数据库配置扫描生成字段类型缓存.
  3. Master 进程根据配置 启动一定数量的 Worker 进程.

4.2 Worker 进程启动过程.

  1. Worker 进程启动.
  2. Worker 进程根据传递进来的 server_id worker_id 标记好自身(缓存到 static 变量中)
  3. Worker 进程加载属于自己的商家信息.
  4. Worker 启动 Http/WebSocket 服务器 开始监听端口工作.
作者

bywayboy

发布于

2021-12-04

更新于

2021-12-04

许可协议