一次开发踩坑笔记
1. 起因.
在公司的一个项目中, 使用 PHP + Swoole开发一个常驻内存服务端, 对 \Swoole\Database\PDOPool 进行了封装. 编写了一个查询语句构造器. 因为要对 查询结果进行类型转换, 我的解决思路是在系统启动对时候, 通过遍历数据库连接, 对数据库中的表结构进行扫描, 并将其作为配置 缓存在 config/tables_gen.php 文件中. 一切看上去很美好, 知道我将工作进程根据CPU核心数配置成多个时候, 问题出现了.
2. 问题现象
\sys\Db 类通过 table 方法 构造出一个 SqlBuilder 然后 使用 Sqlbuilder 构造Sql语句, 工作进程的查询结果出现了错乱,本该上一个查询的结果出现在下一个查询返回中, 我的第一反应是 连接池出了问题. 事实上也确实是连接池出了问题. 由于我使用 类静态属性来缓存每一个连接池. 构造代码如下:
1 | class Db{ |
3. 发现问题
经过分析 最终发现问题出在 构造表结构的部分. 因为构造标结构的时候使用了Db 类, 实际上这段代码在 master 进程中执行, 因此后续创建Worker进程的时候, 为每个Worker 进程都创建了一份拷贝, 而实际上 这时候 Db::pool 属性指向的都是同一个连接池池. 在后续使用连接池的时候, 出现了多个进程引用到同一个数据库连接的情况, 既然确定了问题所在, 解决问题的思路也就容易了. 解决思路有2个.
- 创建一个不缓存的连接池(需要改造或者重新实现一个Db类) 代价有点高.
- 在全局部分避免使用\sys\Db 比如使用 \Swoole\Coroutine\MySQL 替代 (相对简单)
最终, 我选择了方案2来解决问题, 系统工作正常.
4. 框架执行流程如下.
4.1 Master 进程启动过程
- Master 进程启动
- Master 进程根据数据库配置扫描生成字段类型缓存.
- Master 进程根据配置 启动一定数量的 Worker 进程.
4.2 Worker 进程启动过程.
- Worker 进程启动.
- Worker 进程根据传递进来的 server_id worker_id 标记好自身(缓存到 static 变量中)
- Worker 进程加载属于自己的商家信息.
- Worker 启动 Http/WebSocket 服务器 开始监听端口工作.