上一篇文章介绍了用Swoole HTTP服务器替代NginX + PHP-FPM来运行Lumen项目的方法,提高运行效率。不过,在处理用户请求过程中还是会存在很多IO阻塞情况,比如MySQL数据查询。有没有可能在Lumen中使用异步MySQL客户端,以此避免IO阻塞呢?
异步MySQL客户端
Swoole1.8.6增加了内置异步MySQL客户端的支持,无需依赖其他第三方库,使用方法也非常简单,(具体参考Swoole官方文档异步MySQL客户端):
1 |
|
这是典型回调处理的异步编程风格,而Lumen本身是同步编程风格,在编程层面,两者不能融合。比如,有这么一个Controller
:
1 |
|
若是改写成:
1 |
|
这样返回的响应永远是null
,因为response()->json($a)
会在$db->query()
之前被执行。
一开始我也觉得Lumen项目里永远没办法使用异步MySQL客户端了,直到看了这篇文章:Cooperative multitasking using coroutines (in PHP!)。当然,我看的是中文版: 在PHP中使用协程实现多任务调度,文中提到了PHP5.5加入的一个新功能:yield。
yield
yield
是个动词,意思是“生成”,PHP中yield生出的东西叫Generator
,译作“生成器”。
yield可以做什么呢?yield可以将当前执行的上下文作为当前函数的结果返回(yield必须在函数中使用)。
有了yield,又能怎样呢?
首先,我们声明一个类,叫SlwoQuery
:
1 |
|
结合上一篇文章《用Swoole HTTP服务器运行Lumen项目的实现方法》,我们修改一下Service
类的onRequest
方法:
1 |
|
代码是长了些,因为没做分拆和封装,主要关注$db->connect()
和$caller->makeResponse()
的出现位置。整块代码的意思是:
- 如果Lumen处理用户请求的返回结果是一个生成器,那么就从这个生成器的函数套层里寻找(SlowQuery);
- 如果当前生成器的当前值也是一个生成器,那么就往更深一层里寻找;
- 如果当前生成器的当前值是一个
SlowQuery
对象,那么将SlowQuery
对象的sql
属性,交给异步MySQL客户swoole_mysql
查询数据; - 将查询数据传入当前生成器,获得当前层函数的返回结果;
- 将函数返回结果传入上一层生成器,获取上一层函数的返回结果,重复直到没有更高层生成器;
- 才将最顶层层的函数返回结果作为响应输出给用户。
虽然整个过程很绕,但是,在Lumen的Controller层面却是十分的直接了然:1
2
3
4
5
6
7
8
9
10
11
12
13
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use BL\SwooleHttp\Database\SlowQuery;
class TestController extends Controller
{
public function test()
{
$a = yield new SlowQuery('select * from users;');
response()->json($a);
}
}
就这样,实现了在同步编程风格的Lumen项目中使用上了Swoole的异步MySQL客户端。实现的思路,就像后置中间件,保持控制器代码的整洁,脏活累活放在中间件里执行。
效率提升
假如用户请求执行一个数据库慢查询语句(可能耗时1秒,如select sleep(1);
),单个PHP进程使用同步MySQL客户端处理1个这样的用户请求,至少需要1秒,处理10个,则至少需要10秒;而使用异步MySQL客户端,处理1个,可能需要1秒多,处理10个,可能也只是需要1秒。异步执行带来的效率提升,是不言而喻的。
不过,使用异步MySQL客户端是会消耗系统资源的,不能大量使用;而且非慢查询的查询语句,根本不需要使用异步MySQL客户端来执行,比如select * from users.id = 1;
,id有主键索引,查询时间不会太长。
最后
yield可以将整个程序的各个代码块的执行秩序交由程序员自行调度,确实可以实现很多意向不到的效果。像以上这种“后置执行”的思路,不仅仅是可以使用异步MySQL客户端,还可以使用异步Redis客户端,异步HTTP客户端,还有更多各种各样的应用。
下一篇文章介绍如何自定义后置异步协程。