烟熏妆

烟熏妆 查看完整档案

西安编辑  |  填写毕业院校大麦  |  php 编辑 www.lostinyou.cn 编辑
编辑

Lost in you
www.lostinyou.cn

个人动态

烟熏妆 赞了文章 · 7月2日

【SWOOLE系列】浅谈SWOOLE协程篇

阅读本文需要以下知识点

  • 了解进程、线程相关基础
  • 熟练php的hello world输出
  • 会swoole单词拼写

协程的介绍

协程是什么?

A coroutine is a function that can suspend its execution (yield) until the given given YieldInstruction finishes.

简单的说协程是寄宿在线程下程序员实现的一种跟更轻量的并发的协作轻量线程

随着程序员人群的增大,大佬也不断的爆发式增长,当然就开始有人觉得线程不好用了,那怎么办呢?当然是基于线程的理念上再去实现一套更加轻量、更好骗star的一套轻量线程(事实上协程不能完全被认为线程,因为一个线程可以有多个协程)

协程和线程的区别

本质

线程 内核态
协程 用户态

调度方式

线程的调度方式为系统调度,常用的调度策略有分时调度抢占调度。说白就是线程的调度完全不受自己控制

协程的调度方式为协作式调度 不受内核控制由自由策略调度切换

等等

协作式调度?

上述说了协程是用户态的,所以所谓的协作式调度直接可以理解为是程序员写的调度方式,也就是我想怎么调度就怎么调度,而不用通过系统内核被调度。

深。。。。浅入理解swoole的协程

既然打算浅入理解的swoole的协程,我们必须要知道swoole的协程模型。
swoole的协程是基于单线程。可以理解为协程的切换是串行的,再同一个时间点只运行一个协程.

说到这里,肯定就有人问了。go呢,go的协程的是基于多线程。当然各有各的好处,具体可以自行使用搜索引擎了解

我们可以直接copy & paste 下面代码,再本地的环境进行的 demo run

<?php

$func = function ($index, $isCorotunine = true) {
    $isCorotunine && \Swoole\Coroutine::sleep(2);
    echo "index:" . $index . PHP_EOL;
    echo "is corotunine:" . intval($isCorotunine) . PHP_EOL;
};

$func(1, false);
go($func, 2, true);
go($func, 3, true);
go($func, 4, true);
go($func, 5, true);
go($func, 6, true);
$func(7, false);    

会得到以下结果

index:1
is corotunine:0
index:7
is corotunine:0
index:2
is corotunine:1
index:6
is corotunine:1
index:5
is corotunine:1
index:4
is corotunine:1
index:3
is corotunine:1

肯定有人会想,哇塞,尽然2秒都执行完了,一点都不堵塞啊!!

好了,事实上关于2秒执行完的事情可以回过头再去看下协程的概念。
我们可以关注的是执行顺序,1和7是非协程的执行能立马返回结果符合预期。
关于协程的调度顺序
为什么是26543不是65432或者23456有序的返回呢

为了找到我们的答案,我们只能通过源码进行知晓一些东西

分析源码

image

图来自https://segmentfault.com/a/11...

如果没有较强的基础还有啃烂的apue的前提下(当然我也没有!T_T)
我们需要关心的是以下两个
yield 切换协程
resume 恢复协程

协程的创建

<?php
go (function(){
echo "swoole 太棒了";
});

调用的swoole封装给PHPgo函数为创建一个协程

我们根据拓展源码中的

大部分的PHP扩展函数以及扩展方法的参数声明放在swoole_*.ccswoole.cc里面。
PHP_FALIAS(go, swoole_coroutine_create, arginfo_swoole_coroutine_create)

可以知道 go->swoole_coroutine_create

在swoole_coroutine.cc文件里找到

PHP_FUNCTION(swoole_coroutine_create)
{
    ....
    // 划重点 要考
    long cid = PHPCoroutine::create(&fci_cache, fci.param_count, fci.params);
    ....
}

long PHPCoroutine::create(zend_fcall_info_cache *fci_cache, uint32_t argc, zval *argv)
{
    if (sw_unlikely(Coroutine::count() >= config.max_num))
    {
        php_swoole_fatal_error(E_WARNING, "exceed max number of coroutine %zu", (uintmax_t) Coroutine::count());
        return SW_CORO_ERR_LIMIT;
    }

    if (sw_unlikely(!active))
    {
        // 划重点 要考
        activate();
    }

    // 保存回调函数
    php_coro_args php_coro_args;
    //函数信息
    php_coro_args.fci_cache = fci_cache;
    //参数
    php_coro_args.argv = argv;
    php_coro_args.argc = argc;
    // 划重点 要考
    save_task(get_task());

    // 划重点 要考
    return Coroutine::create(main_func, (void*) &php_coro_args);
}
// 保存栈 
void PHPCoroutine::save_task(php_coro_task *task)
{
    save_vm_stack(task);
    save_og(task);
}
// 初始化reactor的事件
inline void PHPCoroutine::activate()
{
    if (sw_unlikely(active))
    {
        return;
    }

    /* init reactor and register event wait */
    php_swoole_check_reactor();

    /* replace interrupt function */
    orig_interrupt_function = zend_interrupt_function;
    zend_interrupt_function = coro_interrupt_function;
    
    /* replace the error function to save execute_data */
    orig_error_function = zend_error_cb;
    zend_error_cb = error;

    if (config.hook_flags)
    {
        enable_hook(config.hook_flags);
    }

    if (SWOOLE_G(enable_preemptive_scheduler) || config.enable_preemptive_scheduler)
    {
        /* create a thread to interrupt the coroutine that takes up too much time */
        interrupt_thread_start();
    }

    if (!coro_global_active)
    {
        if (zend_hash_str_find_ptr(&module_registry, ZEND_STRL("xdebug")))
        {
            php_swoole_fatal_error(E_WARNING, "Using Xdebug in coroutines is extremely dangerous, please notice that it may lead to coredump!");
        }

        /* replace functions that can not work correctly in coroutine */
        inject_function();

        coro_global_active = true;
    }
    /**
     * deactivate when reactor free.
     */
    swReactor_add_destroy_callback(SwooleG.main_reactor, deactivate, nullptr);
    active = true;
}

根据Coroutine::create继续往下跳转

    static inline long create(coroutine_func_t fn, void* args = nullptr)
    {
        return (new Coroutine(fn, args))->run();
    }

在创建完协程后立马执行
我们观察下构造方法

    Coroutine(coroutine_func_t fn, void *private_data) :
            ctx(stack_size, fn, private_data)
    {
        cid = ++last_cid;
        coroutines[cid] = this;
        if (sw_unlikely(count() > peak_num))
        {
            peak_num = count();
        }
    }

上述代码我可以发现还有一个Context的类 这个构造函数我们可以猜到做了3件事情

  1. 分配对应协程id (每个协程都有自己的id)
  2. 保存上下文
  3. 更新当前的协程的数量
swoole使用的协程库为 boost.context 可自行搜索
主要暴露的函数接口为jump_fcontextmake_fcontext
具体的作用保存当前执行状态的上下文暂停当前的执行状态够跳转到其他位置继续执行

创建完协程立马执行

inline long run()
    {
        long cid = this->cid;
        origin = current;
        current = this;
        // 依赖boost.context 切栈
        ctx.swap_in();
        // 判断是否执行结束
        check_end();
        return cid;
    }

判断是否结束

inline void check_end()
    {
        if (ctx.is_end())
        {
            close();
        }
        else if (sw_unlikely(on_bailout))
        {
            SW_ASSERT(current == nullptr);
            on_bailout();
            // expect that never here
            exit(1);
        }
    }

根据ctx.is_end()的函数找到

    inline bool is_end()
    {
        return end_;
    }
bool Context::swap_in()
{
    jump_fcontext(&swap_ctx_, ctx_, (intptr_t) this, true);
    return true;
}

我们可以总结下swoole在创建协程的时候主要做了哪些事情

  1. 检测环境
  2. 解析参数
  3. 保存上下文
  4. 切换C栈
  5. 执行协程

协程的yield

上述的demo我们使用\Swoole\Coroutine::sleep(2)
根据上述说函数申明的我们在swoole_corotunine_system.cc发现对应函数为swoole_coroutine_systemsleep

PHP_METHOD(swoole_coroutine_system, sleep)
{
    double seconds;

    ZEND_PARSE_PARAMETERS_START(1, 1)
        Z_PARAM_DOUBLE(seconds)
    ZEND_PARSE_PARAMETERS_END_EX(RETURN_FALSE);

    if (UNEXPECTED(seconds < SW_TIMER_MIN_SEC))
    {
        php_swoole_fatal_error(E_WARNING, "Timer must be greater than or equal to " ZEND_TOSTR(SW_TIMER_MIN_SEC));
        RETURN_FALSE;
    }
    System::sleep(seconds);
    RETURN_TRUE;
}

调用了sleep函数之后对当前的协程做了三件事
1.增加了timer定时器
2.注册回掉函数再延迟之后resume协程
3.通过yield让出调度

int System::sleep(double sec)
{
// 获取当前的协程
    Coroutine* co = Coroutine::get_current_safe();
   //swTimer_add 注册定时器 sleep_timeout回调的函数
   if (swTimer_add(&SwooleG.timer, (long) (sec * 1000), 0, co, sleep_timeout) == NULL)
    {
        return -1;
    }
    // 让出当前cpu
    co->yield();
    return 0;
}

// 回调函数
static void sleep_timeout(swTimer *timer, swTimer_node *tnode)
{
   // 恢复调度
    ((Coroutine *) tnode->data)->resume();
}
swTimer_node* swTimer_add(swTimer *timer, long _msec, int interval, void *data, swTimerCallback callback)
{
    ....
    // 保存当前上下文和对应过期时间
    tnode->data = data;
    tnode->type = SW_TIMER_TYPE_KERNEL;
    tnode->exec_msec = now_msec + _msec;
    tnode->interval = interval ? _msec : 0;
    tnode->removed = 0;
    tnode->callback = callback;
    tnode->round = timer->round;
    tnode->dtor = NULL;

    // _next_msec保存最快过期的事件
    if (timer->_next_msec < 0 || timer->_next_msec > _msec)
    {
        timer->set(timer, _msec);
        timer->_next_msec = _msec;
    }

    tnode->id = timer->_next_id++;
    if (sw_unlikely(tnode->id < 0))
    {
        tnode->id = 1;
        timer->_next_id = 2;
    }

    tnode->heap_node = swHeap_push(timer->heap, tnode->exec_msec, tnode);
    ....
    timer->num++;
    return tnode;
}

协程的切换

我们

void Coroutine::resume()
{
    SW_ASSERT(current != this);
    if (sw_unlikely(on_bailout))
    {
        return;
    }
    state = SW_CORO_RUNNING;
    if (sw_likely(on_resume))
    {
        on_resume(task);
    }
    // 将当前的协程保存为origin -> 理解程previous
    origin = current;
    // 需要执行的协程 变成 current
    current = this;
    // 入栈执行
    ctx.swap_in();
    check_end();
}

到这里时候 关于协程调用顺序的答案已经出来了

在创建协程的时候(new Coroutine(fn, args))->run();sleep触发yield都在不断变更的Corotuninecurrentorigin 再执切换的时候和php代码创建协程的时间发生穿插,而不是我们想象中的队列有序执行
比如当创建协程只有2个的时候

<?php

$func = function ($index, $isCorotunine = true) {
    $isCorotunine && \Swoole\Coroutine::sleep(2);
    echo "index:" . $index . PHP_EOL;
    echo "is corotunine:" . intval($isCorotunine) . PHP_EOL;
};

$func(1, false);

go($func, 2, true);
go($func, 3, true);

返回输出 因为连续创建协程的执行时间小没有被打乱

php swoole_go_demo1.php
index:1
is corotunine:0
index:2
is corotunine:1
index:3
is corotunine:1

当连续创建的时候200个协程的时候
返回就变得打乱的index 符合预计猜想

index:1,index:2,index:4,index:8,index:16,index:32,index:64,index:128,index:129,index:65,index:130,index:131,index:33,index:66,index:132,index:133,index:67,index:134,index:135,index:17,index:34,index:68,index:136,index:137,index:69,index:138,index:139,index:35,index:70,index:140,index:141,index:71,index:142,index:143,index:9,index:18,index:36,index:72,index:144,index:145,index:73,index:146,index:147,index:158,index:157,index:156,index:155,index:154,index:153,index:152,index:151,index:37,index:74,index:148,index:149,index:75,index:150,index:19,index:38,index:76,index:77,index:39,index:78,index:79,index:5,index:10,index:20,index:40,index:80,index:81,index:41,index:82,index:83,index:21,index:127,index:126,index:125,index:124,index:123,index:122,index:121,index:120,index:119,index:118,index:117,index:116,index:115,index:114,index:113,index:112,index:111,index:110,index:109,index:108,index:107,index:106,index:105,index:104,index:103,index:102,index:101,index:100,index:99,index:98,index:97,index:96,index:95,index:94,index:93,index:92,index:91,index:90,index:89,index:88,index:87,index:42,index:84,index:85,index:43,index:86,index:11,index:22,index:44,index:45,index:23,index:46,index:47,index:3,index:6,index:12,index:24,index:48,index:49,index:25,index:50,index:51,index:13,index:26,index:63,index:62,index:61,index:60,index:59,index:58,index:57,index:56,index:55,index:52,index:53,index:27,index:54,index:7,index:14,index:28,index:29,index:15,index:30,index:31,index:200,index:199,index:192,index:185,index:175,index:168,index:161,index:163,index:172,index:179,index:187,index:194,index:174,index:160,index:173,index:176,index:198,index:195,index:180,index:167,index:169,index:184,index:197,index:193,index:177,index:162,index:171,index:186,index:182,index:164,index:191,index:183,index:166,index:196,index:178,index:170,index:189,index:188,index:165,index:181,index:190,index:159

最后彩蛋

我们使用GO的协程的来实现上述的demo

package main

import (
    "fmt"
    "time"
)

var count int = 0

func main() {
    output(false, 1)

    go output(true, 2)
    go output(true, 3)
    go output(true, 4)
    go output(true, 5)
    go output(true, 6)

    output(false, 7)

    time.Sleep(time.Second)
}

func output(isCorotunine bool, index int) {
    time.Sleep(time.Second)
    count = count + 1
    fmt.Println(count, isCorotunine, index)
}

猜猜返回结果是如何的 可以根据go的协程基于多线程的方式再去研究下
image.png

写给最后,文章纯属自己根据代码和资料理解,如果有错误麻烦提出来,倍感万分,如果因为一些错误的观点被误导我只能说

查看原文

赞 25 收藏 14 评论 3

烟熏妆 赞了文章 · 6月22日

🚀 Hyperf 2.0 发布!想象的开端!

前言

Hyperf 从 2019 年 6 月 20 日发布 1.0 版本至今,获得了非常多的关注和用户,短短的一年期间,Hyperf 飞速发展和持续迭代,同时也拥有了非常惊人的数据。

  • Github 2700 stars / Gitee 328 stars
  • 113 名 contributors
  • 1100+ Pull Requests
  • 共发布 47 个版本
  • 92 个代码仓库
  • 1438 个单元测试用例,4412 个断言条件

这些数据是整个开源社区共同努力的结果,感谢所有支持 Hyperf 的大神的厚爱,期待未来更多的支持与合作。

Hyperf 2.0

在持续迭代的过程中,我们也产生了一些新的思路,我们对这些思路进行了验证、迭代、再验证、再迭代,并最终将这些思路的实现沉淀到了 Hyperf 中来,并于今日,以 2.0 版本正式发布了 🎉🎉🎉

感谢 Hyperf 团队成员日以继夜的努力,使这些设想成为了可能。

主要功能迭代

AOP 和注解功能底层重构

在 1.1 版本下,尽管提供了非常强大的 AOP 和注解功能,但也仍有一些限制和不足如下:

  • AOP 只能切入由 hyperf/di 组件管理的对象,无法切入其它方式创建的对象,如 new
  • 通过 DI 获取的类实际上是由 AOP 生成的一个原始类的子类,在子类上完成了对方法的修改,以完成 AOP 的功能实现,而子类的类名与原始类是不一致的,也就导致了 get_class(), __CLASS__ 之类的方法或常量获取的数据可能会不对;
  • 同上,异常堆栈信息会充满了代理类的链路,不容易看清楚调用链路;
  • 同上,由于是通过继承实现的代理类,故一个 final 类是无法被切入的;
  • 同上,对一个父类进行 AOP 切入后,这个类的子类并不会被切入;
  • 通过 new 实例化的一个对象 @Inject@Value 注解无法生效;
  • 您无法在一个类的构造函数内使用通过 @Inject@Value 注解获取的值;
  • 在 trait 内通过 @Inject@Value 注解标注的属性无法正常运作;
  • 在 PHP 8 下无法通过类成员属性的强类型声明替代 @var 声明来指定 @Inject 时的类声明;
  • 使用注解时必须声明对应注解的命名空间;
  • 定义 Aspect 类时无法在注解上一并定义要切入的目标;

以上列举了一些 1.1 下 AOP 的限制和不足,而在 2.0 版本下,我们对底层的逻辑进行了重构,上面这些问题全部都被解决掉了,其中最具想象空间的是通过新的 AOP 功能,你可以对几乎所有的类和注解进行动态的切入了,无论是通过 new 实例化出来的对象还是通过 DI 创建出来的对象,无论是切入了这个类的父类还是更深的继承层次,无论是 final 类还是一个普通类。

简而言之,在新的机制下,Hyperf 会在启动时扫描所有的扫描域,并扫描代码得到所有类的 AST 抽象语法树,并从中解析所有与 AOP 相关的元数据,根据这些元数据来对要被代理的类进行 AST 节点信息的修改,并注入 AOP 相关的逻辑,最终通过 PHP 的 Autoload 机制,在实例化一个类并进行自动加载时,ClassLoader 返回经过修改后的类文件。

那么用正向的角度来描述这个功能的变更如下:

  • AOP 可以作用于 new 关键词创建的对象;
  • AOP 可以作用于 Final 类;
  • 您可以在构造函数中使用 @Inject@Value 注解标记的属性值;
  • 代理类的类名和继承关系与原类一致;
  • 对父类进行 AOP 切入,子类同样生效;
  • AOP 代理类缓存和注解缓存可以自动识别是否需要重新生成;
  • 通过 new 关键词创建的对象,@Inject@Value 注解标记的属性值可以生效;
  • 可在 trait 中使用 @Inject@Value 注解,并作用于 use 的类;
  • PHP 8 下使用 @Inject 注解时可通过强类型声明替代 @var 注解声明;
  • 提供了注解全局引入机制,以达到在使用注解时允许不引入对应的命名空间;
  • 在定义 Aspect 时可直接在 @Aspect 注解上定义要切入的目标类和注解;
  • Aspect 增加了 priority 优先级属性,可定义多个 Aspect 类的优先级;
  • 使用依赖懒加载功能时无需再注册 Hyperf\Di\Listener\LazyLoaderBootApplicationListener 监听器;
  • 新增 annotations.scan.class_map 配置,通过该配置可以直接将任意类替换为你指定的类;

支持 Coroutine Server 协程服务

在 Swoole 4.4 版本时新增了 Coroutine Server,通过该功能可以通过协程的形式来运行 Server,也就意味着可以在一个进程下同时运行多个不同协议的 Server 来提供服务,这样的做法更加的协程,且单进程的模型对 Docker 和 Kubernetes 更加友好,通过调整 Pod 的数量即可对应到真实的进程数;
在 Hyperf 2.0 版本,我们也对 Coroutine Server 进行了支持,您可通过在 config/autoload/server.php 配置文件中添加一个 type => Hyperf\Server\CoroutineServer::class 配置即可切换到 Coroutine Server 的运行模式去。同时一些原本要使用自定义进程来实现功能的场景,如配置中心的配置拉取、服务监控的数据提供、消息队列消费者的消费等,我们的提供了对应的协程模式的运行模式,最终只需要启动一个进程即可完成所有之前需要多个进程才能完成的事情。

增加 ResponseEmitter 机制

在 1.1 版本下,我们只能在 HTTP Server 中返回由 hyperf/http-message 组件或 hyperf/http-server 组件提供的 Response 对象,但其它同样遵循了 PSR-7 标准的 Response 却无法正常响应,比如 Guzzle 客户端请求后获得的 Response 对象,在 1.1 下需要转换为 Hyperf 的 Response 对象才能正确响应客户端请求。而在 2.0 版本下,通过 ResponseEmitter 机制,您可以直接返回任意符合 PSR-7 标准的 Response 对象,以获得更强的兼容性。

增加 Reactive-X 组件

hyperf/reactive-x 组件提供了 Swoole/Hyperf 环境下的 ReactiveX 集成。关于 ReactiveX,微软给的定义是,Rx 是一个函数库,让开发者可以利用可观察序列和 LINQ 风格查询操作符来编写异步和基于事件的程序,使用 Rx,开发者可以用 Observables 表示异步数据流,用 LINQ 操作符查询异步数据流, 用 Schedulers 参数化异步数据流的并发处理,Rx 可以这样定义:Rx = Observables + LINQ + Schedulers。而 Reactivex.io 给的定义是,Rx 是一个使用可观察数据流进行异步编程的编程接口,ReactiveX 结合了观察者模式、迭代器模式和函数式编程的精华。

通过该组件,您可以在 Hyperf 中实现响应式编程的范式,为您的应用提供更多的可能性。

统一 HTTP 异常

在 1.1 版本下,HTTP Server 在处理如 路由未找到(404)请求方法不允许(405) 等 HTTP 异常时,是在 Dispatcher 中提供对应的方法,并直接响应 Response 结果,如果需要自定义对应的响应结果,则需要通过 DI 来重写 Dispatcher 类的对应方法。而在 2.0 版本下,我们对异常的处理方式进行了统一,统一抛出 Hyperf\HttpMessage\Exception\HttpException 异常类的子类,并统一由默认提供的 Hyperf\HttpServer\Exception\Handler\HttpExceptionHandler 来处理响应的结果,这样一来用户便可以非常便捷的通过 ExceptionHandler 来对异常响应进行统一的处理了。

升级到 2.0 版本

从现在的 1.1 版本升级到 2.0 版本,也是一件非常轻松的事情,我们提供了一份详尽的 2.0 升级指南 来指引您完成对应的升级动作,具体可查阅该升级指南;

更多

以上只是笔者本人最为期待的功能迭代,只是冰山一角,2.0 版本还包含了大量的细节更新以及新功能,具体可以查阅 版本更新记录 获得更多的细节信息。

总的来说,2.0 是一个充满了想象空间的版本,它提供了远超原来的可能性,我们可以在 Hyperf 上、在 Swoole 上、在 PHP 上,去想、去做更多原来不曾深思过的事情。

查看原文

赞 20 收藏 4 评论 1

烟熏妆 赞了文章 · 6月3日

思否开源项目推介丨Hyperf:基于 Swoole 4.4+ 实现的 PHP 协程框架

思否开源项目推介

开源项目名称:Hyperf
开源项目负责人:@huangzhhui
开源项目简介:基于 Swoole 4.4+ 实现的 PHP 协程框架
开源项目类型:个人开源项目
项目创建时间:2018 年创建,2019 年 6 月发布 1.0 版本
GitHub 数据:2.6K Star,470 Fork
GitHub 地址:https://github.com/hyperf/hyperf

项目介绍

Hyperf 是基于 Swoole 4.4+ 实现的高性能、高灵活性的 PHP 协程框架,内置协程服务器及大量常用的组件,性能较传统基于 PHP-FPM 的框架有质的提升,提供超高性能的同时,也保持着极其灵活的可扩展性,标准组件均基于 PSR 标准 实现,基于强大的依赖注入设计,保证了绝大部分组件或类都是可替换与 可复用的。

框架组件库除了常见的协程版的 MySQL 客户端、Redis 客户端,还为您准备了协程版的 Eloquent ORM、WebSocket 服务端及客户端、JSON RPC 服务端及客户端、GRPC 服务端及客户端、Zipkin/Jaeger (OpenTracing) 客户端、Guzzle HTTP 客户端、Elasticsearch 客户端、Consul 客户端、ETCD 客户端、AMQP 组件、Apollo 配置中心、阿里云 ACM 应用配置管理、ETCD 配置中心、基于令牌桶算法的限流器、通用连接池、熔断器、Swagger 文档生成、Swoole Tracker、视图引擎、Snowflake 全局 ID 生成器 等组件,省去了自己实现对应协程版本的麻烦。

Hyperf 还提供了 基于 PSR-11 的依赖注入容器、注解、AOP 面向切面编程、基于 PSR-15 的中间件、自定义进程、基于 PSR-14 的事件管理器、Redis/RabbitMQ 消息队列、自动模型缓存、基于 PSR-16 的缓存、Crontab 秒级定时任务、国际化、Validation 表单验证器 等非常便捷的功能,满足丰富的技术场景和业务场景,开箱即用。

项目负责人自荐

Hyperspeed + Flexibility = Hyperf,从名字上我们就将 超高速 和 灵活性 作为 Hyperf 的基因。

  • 对于超高速,我们基于 Swoole 协程并在框架设计上进行大量的优化以确保超高性能的输出。
  • 对于灵活性,我们基于 Hyperf 强大的依赖注入组件,组件均基于 PSR 标准 的契约和由 Hyperf 定义的契约实现,达到框架内的绝大部分的组件或类都是可替换的。

基于以上的特点,Hyperf 将存在丰富的可能性,如实现 Web 服务,网关服务,分布式中间件,微服务架构,游戏服务器,物联网(IOT)等。

思否推荐语

基于 PHP 语言开发的框架处于一个百家争鸣的时代,但仍旧未能看到一个优雅的设计与超高性能的共存的完美框架,亦没有看到一个真正为 PHP 微服务铺路的框架。

很高兴看到 Hyperf 及其团队成员正在向着这个方向努力,推荐广大社区开发者可以关注该项目的发展与成长,或者直接参与到项目的开源建设当中去。


思否开源项目推介

该项目已入选「SFOSSP - 思否开源项目支持计划」,我们希望借助社区的资源对开源项目进行相关的宣传推广,并作为一个长期项目助力开源事业的发展,与广大开发者共建开源新生态。

有意向的开源项目负责人或团队成员,可通过邮箱提供相应的信息(开源项目地址、项目介绍、团队介绍、联系方式等),以便提升交流的效率。

联系邮箱:pr@segmentfault.com

segmentfault 思否

查看原文

赞 7 收藏 0 评论 1

烟熏妆 赞了文章 · 6月2日

不会看 Explain执行计划,劝你简历别写熟悉 SQL优化

个人博客地址:http://www.chengxy-nds.top

昨天中午在食堂,和部门的技术大牛们坐在一桌吃饭,作为一个卑微技术渣仔默默的吃着饭,听大佬们高谈阔论,研究各种高端技术,我TM也想说话可实在插不上嘴。

聊着聊着突然说到他上午面试了一个工作6年的程序员,表情挺复杂,他说:我看他简历写着熟悉SQL语句调优,就问了下 Explain 执行计划怎么看?结果这老哥一问三不知,工作6年这么基础的东西都不了解!

感受到了大佬的王之鄙视,回到工位我就开始默默写这个,哎~ 我TM也不太懂 Explain ,老哥你这是针对我啊!哭唧唧~
在这里插入图片描述

Explain有什么用

ExplainSQL语句一起使用时,MySQL 会显示来自优化器关于SQL执行的信息。也就是说,MySQL解释了它将如何处理该语句,包括如何连接表以及什么顺序连接表等。

  • 表的加载顺序
  • sql 的查询类型
  • 可能用到哪些索引,哪些索引又被实际使用
  • 表与表之间的引用关系
  • 一个表中有多少行被优化器查询

.....

Explain有哪些信息

Explain 执行计划包含字段信息如下:分别是 idselect_typetablepartitionstypepossible_keyskeykey_lenrefrowsfilteredExtra 12个字段。

下边我们会结合具体的SQL示例,详细的解读每个字段以及每个字段中不同参数的含义,以下所有示例数据库版本为 MySQL.5.7.17

mysql> select version() from dual;
+------------+
| version()  |
+------------+
| 5.7.17-log |
+------------+

我们创建三张表 onetwothree,表之间的关系 one.two_id = two.two_id AND two.three_id = three.three_id

Explain执行计划详解

一、id

id: :表示查询中执行select子句或者操作表的顺序,id的值越大,代表优先级越高,越先执行id大致会出现 3种情况:

1、id相同

看到三条记录的id都相同,可以理解成这三个表为一组,具有同样的优先级,执行顺序由上而下,具体顺序由优化器决定。

mysql> EXPLAIN SELECT * FROM one o,two t, three r WHERE o.two_id = t.two_id AND t.three_id = r.three_id;
+----+-------------+-------+------------+--------+---------------+---------+---------+----------------------+------+----------+----------------------------------------------------+
| id | select_type | table | partitions | type   | possible_keys | key     | key_len | ref                  | rows | filtered | Extra                                              |
+----+-------------+-------+------------+--------+---------------+---------+---------+----------------------+------+----------+----------------------------------------------------+
|  1 | SIMPLE      | o     | NULL       | ALL    | NULL          | NULL    | NULL    | NULL                 |    2 |      100 | NULL                                               |
|  1 | SIMPLE      | t     | NULL       | ALL    | PRIMARY       | NULL    | NULL    | NULL                 |    2 |       50 | Using where; Using join buffer (Block Nested Loop) |
|  1 | SIMPLE      | r     | NULL       | eq_ref | PRIMARY       | PRIMARY | 4       | xin-slave.t.three_id |    1 |      100 | NULL                                               |
+----+-------------+-------+------------+--------+---------------+---------+---------+----------------------+------+----------+----------------------------------------------------+
2、id不同

如果我们的 SQL 中存在子查询,那么 id的序号会递增,id值越大优先级越高,越先被执行 。当三个表依次嵌套,发现最里层的子查询 id最大,最先执行。

mysql> EXPLAIN select * from one o where o.two_id = (select t.two_id from two t where t.three_id = (select r.three_id  from three r where r.three_name='我是第三表2'));
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | PRIMARY     | o     | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    2 |       50 | Using where |
|  2 | SUBQUERY    | t     | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    2 |       50 | Using where |
|  3 | SUBQUERY    | r     | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    2 |       50 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+

##### 3、以上两种同时存在

将上边的 SQL 稍微修改一下,增加一个子查询,发现 id的以上两种同时存在。相同id划分为一组,这样就有三个组,同组的从上往下顺序执行,不同组 id值越大,优先级越高,越先执行。

mysql>  EXPLAIN select * from one o where o.two_id = (select t.two_id from two t where t.three_id = (select r.three_id  from three r where r.three_name='我是第三表2')) AND o.one_id in(select one_id from one where o.one_name="我是第一表2");
+----+-------------+-------+------------+--------+---------------+---------+---------+--------------------+------+----------+-------------+
| id | select_type | table | partitions | type   | possible_keys | key     | key_len | ref                | rows | filtered | Extra       |
+----+-------------+-------+------------+--------+---------------+---------+---------+--------------------+------+----------+-------------+
|  1 | PRIMARY     | o     | NULL       | ALL    | PRIMARY       | NULL    | NULL    | NULL               |    2 |       50 | Using where |
|  1 | PRIMARY     | one   | NULL       | eq_ref | PRIMARY       | PRIMARY | 4       | xin-slave.o.one_id |    1 |      100 | Using index |
|  2 | SUBQUERY    | t     | NULL       | ALL    | NULL          | NULL    | NULL    | NULL               |    2 |       50 | Using where |
|  3 | SUBQUERY    | r     | NULL       | ALL    | NULL          | NULL    | NULL    | NULL               |    2 |       50 | Using where |
+----+-------------+-------+------------+--------+---------------+---------+---------+--------------------+------+----------+-------------+

二、select_type

select_type:表示 select 查询的类型,主要是用于区分各种复杂的查询,例如:普通查询联合查询子查询等。

1、SIMPLE

SIMPLE:表示最简单的 select 查询语句,也就是在查询中不包含子查询或者 union交并差集等操作。

2、PRIMARY

PRIMARY:当查询语句中包含任何复杂的子部分,最外层查询则被标记为PRIMARY

3、SUBQUERY

SUBQUERY:当 selectwhere 列表中包含了子查询,该子查询被标记为:SUBQUERY

4、DERIVED

DERIVED:表示包含在from子句中的子查询的select,在我们的 from 列表中包含的子查询会被标记为derived

5、UNION

UNION:如果union后边又出现的select 语句,则会被标记为union;若 union 包含在 from 子句的子查询中,外层 select 将被标记为 derived

6、UNION RESULT

UNION RESULT:代表从union的临时表中读取数据,而table列的<union1,4>表示用第一个和第四个select的结果进行union操作。

mysql> EXPLAIN select t.two_name, ( select one.one_id from one) o from (select two_id,two_name from two where two_name ='') t  union (select r.three_name,r.three_id from three r);

+------+--------------+------------+------------+-------+---------------+---------+---------+------+------+----------+-----------------+
| id   | select_type  | table      | partitions | type  | possible_keys | key     | key_len | ref  | rows | filtered | Extra           |
+------+--------------+------------+------------+-------+---------------+---------+---------+------+------+----------+-----------------+
|    1 | PRIMARY      | two        | NULL       | ALL   | NULL          | NULL    | NULL    | NULL |    2 |       50 | Using where     |
|    2 | SUBQUERY     | one        | NULL       | index | NULL          | PRIMARY | 4       | NULL |    2 |      100 | Using index     |
|    4 | UNION        | r          | NULL       | ALL   | NULL          | NULL    | NULL    | NULL |    2 |      100 | NULL            |
| NULL | UNION RESULT | <union1,4> | NULL       | ALL   | NULL          | NULL    | NULL    | NULL | NULL | NULL     | Using temporary |
+------+--------------+------------+------------+-------+---------------+---------+---------+------+------+----------+-----------------+

三、table

查询的表名,并不一定是真实存在的表,有别名显示别名,也可能为临时表,例如上边的DERIVED<union1,4>等。

四、partitions

查询时匹配到的分区信息,对于非分区表值为NULL,当查询的是分区表时,partitions显示分区表命中的分区情况。

+----+-------------+----------------+---------------------------------+-------+---------------+---------+---------+------+------+----------+-------------+
| id | select_type | table          | partitions                      | type  | possible_keys | key     | key_len | ref  | rows | filtered | Extra       |
+----+-------------+----------------+---------------------------------+-------+---------------+---------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | one            | p201801,p201802,p201803,p300012 | index | NULL          | PRIMARY | 9       | NULL |    3 |      100 | Using index |
+----+-------------+----------------+---------------------------------+-------+---------------+---------+---------+------+------+----------+-------------+

五、type

type:查询使用了何种类型,它在 SQL优化中是一个非常重要的指标,以下性能从好到坏依次是:system > const > eq_ref > ref > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL

1、system

system: 当表仅有一行记录时(系统表),数据量很少,往往不需要进行磁盘IO,速度非常快。

2、const

const:表示查询时命中 primary key 主键或者 unique 唯一索引,或者被连接的部分是一个常量(const)值。这类扫描效率极高,返回数据量少,速度非常快。

mysql> EXPLAIN SELECT * from three where three_id=1;
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type  | possible_keys | key     | key_len | ref   | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | three | NULL       | const | PRIMARY       | PRIMARY | 4       | const |    1 |      100 | NULL  |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
3、eq_ref

eq_ref:查询时命中主键primary key 或者 unique key索引, type 就是 eq_ref

mysql> EXPLAIN select o.one_name from one o ,two t where o.one_id = t.two_id ; 
+----+-------------+-------+------------+--------+---------------+----------+---------+--------------------+------+----------+-------------+
| id | select_type | table | partitions | type   | possible_keys | key      | key_len | ref                | rows | filtered | Extra       |
+----+-------------+-------+------------+--------+---------------+----------+---------+--------------------+------+----------+-------------+
|  1 | SIMPLE      | o     | NULL       | index  | PRIMARY       | idx_name | 768     | NULL               |    2 |      100 | Using index |
|  1 | SIMPLE      | t     | NULL       | eq_ref | PRIMARY       | PRIMARY  | 4       | xin-slave.o.one_id |    1 |      100 | Using index |
+----+-------------+-------+------------+--------+---------------+----------+---------+--------------------+------+----------+-------------+
4、ref

ref:区别于eq_refref表示使用非唯一性索引,会找到很多个符合条件的行。

mysql> select o.one_id from one o where o.one_name = "xin" ; 
+--------+
| one_id |
+--------+
|      1 |
|      3 |
+--------+
mysql> EXPLAIN select o.one_id from one o where o.one_name = "xin" ; 
+----+-------------+-------+------------+------+---------------+----------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key      | key_len | ref   | rows | filtered | Extra       |
+----+-------------+-------+------------+------+---------------+----------+---------+-------+------+----------+-------------+
|  1 | SIMPLE      | o     | NULL       | ref  | idx_name      | idx_name | 768     | const |    1 |      100 | Using index |
+----+-------------+-------+------------+------+---------------+----------+---------+-------+------+----------+-------------+
5、ref_or_null

ref_or_null:这种连接类型类似于 ref,区别在于 MySQL会额外搜索包含NULL值的行。

mysql> EXPLAIN select o.one_id from one o where o.one_name = "xin" OR o.one_name IS NULL; 
+----+-------------+-------+------------+-------------+---------------+----------+---------+-------+------+----------+--------------------------+
| id | select_type | table | partitions | type        | possible_keys | key      | key_len | ref   | rows | filtered | Extra                    |
+----+-------------+-------+------------+-------------+---------------+----------+---------+-------+------+----------+--------------------------+
|  1 | SIMPLE      | o     | NULL       | ref_or_null | idx_name      | idx_name | 768     | const |    3 |      100 | Using where; Using index |
+----+-------------+-------+------------+-------------+---------------+----------+---------+-------+------+----------+--------------------------+
6、index_merge

index_merge:使用了索引合并优化方法,查询使用了两个以上的索引。

下边示例中同时使用到主键one_id 和 字段one_nameidx_name 索引 。

mysql> EXPLAIN select * from one o where o.one_id >1 and o.one_name ='xin'; 
+----+-------------+-------+------------+-------------+------------------+------------------+---------+------+------+----------+------------------------------------------------+
| id | select_type | table | partitions | type        | possible_keys    | key              | key_len | ref  | rows | filtered | Extra                                          |
+----+-------------+-------+------------+-------------+------------------+------------------+---------+------+------+----------+------------------------------------------------+
|  1 | SIMPLE      | o     | NULL       | index_merge | PRIMARY,idx_name | idx_name,PRIMARY | 772,4   | NULL |    1 |      100 | Using intersect(idx_name,PRIMARY); Using where |
+----+-------------+-------+------------+-------------+------------------+------------------+---------+------+------+----------+------------------------------------------------+
7、unique_subquery

unique_subquery:替换下面的 IN子查询,子查询返回不重复的集合。

value IN (SELECT primary_key FROM single_table WHERE some_expr)
8、index_subquery

index_subquery:区别于unique_subquery,用于非唯一索引,可以返回重复值。

value IN (SELECT key_column FROM single_table WHERE some_expr)
9、range

range:使用索引选择行,仅检索给定范围内的行。简单点说就是针对一个有索引的字段,给定范围检索数据。在where语句中使用 bettween...and <><=in 等条件查询 type 都是 range

举个栗子:three表中three_id为唯一主键,user_id普通字段未建索引。

mysql> EXPLAIN SELECT * from three where three_id BETWEEN 2 AND 3;
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type  | possible_keys | key     | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | three | NULL       | range | PRIMARY       | PRIMARY | 4       | NULL |    1 |      100 | Using where |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+

从结果中看到只有对设置了索引的字段,做范围检索 type 才是 range

mysql> EXPLAIN SELECT * from three where user_id BETWEEN 2 AND 3;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | three | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    3 |    33.33 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
10、index

indexIndexALL 其实都是读全表,区别在于index是遍历索引树读取,而ALL是从硬盘中读取。

下边示例:three_id 为主键,不带 where 条件全表查询 ,type结果为index

mysql> EXPLAIN SELECT three_id from three ;
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type  | possible_keys | key     | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | three | NULL       | index | NULL          | PRIMARY | 4       | NULL |    1 |      100 | Using index |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
11、ALL

ALL:将遍历全表以找到匹配的行,性能最差。

mysql> EXPLAIN SELECT * from two ;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
|  1 | SIMPLE      | two   | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    2 |      100 | NULL  |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+

六、possible_keys

possible_keys:表示在MySQL中通过哪些索引,能让我们在表中找到想要的记录,一旦查询涉及到的某个字段上存在索引,则索引将被列出,但这个索引并不定一会是最终查询数据时所被用到的索引。具体请参考上边的例子。

七、key

key:区别于possible_keys,key是查询中实际使用到的索引,若没有使用索引,显示为NULL。具体请参考上边的例子。

typeindex_merge 时,可能会显示多个索引。

八、key_len

key_len:表示查询用到的索引长度(字节数),原则上长度越短越好 。

  • 单列索引,那么需要将整个索引长度算进去;
  • 多列索引,不是所有列都能用到,需要计算查询中实际用到的列。
注意:key_len只计算where条件中用到的索引长度,而排序和分组即便是用到了索引,也不会计算到key_len中。

九、ref

ref:常见的有:constfuncnull,字段名。

  • 当使用常量等值查询,显示const
  • 当关联查询时,会显示相应关联表的关联字段
  • 如果查询条件使用了表达式函数,或者条件列发生内部隐式转换,可能显示为func
  • 其他情况null

十、rows

rows:以表的统计信息和索引使用情况,估算要找到我们所需的记录,需要读取的行数。

这是评估SQL 性能的一个比较重要的数据,mysql需要扫描的行数,很直观的显示 SQL 性能的好坏,一般情况下 rows 值越小越好。

mysql> EXPLAIN SELECT * from three;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
|  1 | SIMPLE      | three | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    3 |      100 | NULL  |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+

十一、filtered

filtered 这个是一个百分比的值,表里符合条件的记录数的百分比。简单点说,这个字段表示存储引擎返回的数据在经过过滤后,剩下满足条件的记录数量的比例。

MySQL.5.7版本以前想要显示filtered需要使用explain extended命令。MySQL.5.7后,默认explain直接显示partitionsfiltered的信息。

十二、Extra

Extra :不适合在其他列中显示的信息,Explain 中的很多额外的信息会在 Extra 字段显示。

1、Using index

Using index:我们在相应的 select 操作中使用了覆盖索引,通俗一点讲就是查询的列被索引覆盖,使用到覆盖索引查询速度会非常快,SQl优化中理想的状态。

什么又是覆盖索引?

一条 SQL只需要通过索引就可以返回,我们所需要查询的数据(一个或几个字段),而不必通过二级索引,查到主键之后再通过主键查询整行数据(select * )。

one_id表为主键

mysql> EXPLAIN SELECT one_id from one ;
+----+-------------+-------+------------+-------+---------------+------------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type  | possible_keys | key        | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-------+------------+-------+---------------+------------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | one   | NULL       | index | NULL          | idx_two_id | 5       | NULL |    3 |      100 | Using index |
+----+-------------+-------+------------+-------+---------------+------------+---------+------+------+----------+-------------+

注意:想要使用到覆盖索引,我们在 select 时只取出需要的字段,不可select *,而且该字段建了索引。

mysql> EXPLAIN SELECT * from one ;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
|  1 | SIMPLE      | one   | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    3 |      100 | NULL  |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
2、Using where

Using where:查询时未找到可用的索引,进而通过where条件过滤获取所需数据,但要注意的是并不是所有带where语句的查询都会显示Using where

下边示例create_time 并未用到索引,typeALL,即MySQL通过全表扫描后再按where条件筛选数据。

mysql> EXPLAIN SELECT one_name from one where create_time ='2020-05-18';
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | one   | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    3 |    33.33 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
3、Using temporary

Using temporary:表示查询后结果需要使用临时表来存储,一般在排序或者分组查询时用到。

mysql> EXPLAIN SELECT one_name from one where one_id in (1,2) group by one_name;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | one   | NULL       | range| NULL          | NULL | NULL    | NULL |    3 |    33.33 | Using where; Using temporary; Using filesort |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
4、Using filesort

Using filesort:表示无法利用索引完成的排序操作,也就是ORDER BY的字段没有索引,通常这样的SQL都是需要优化的。

mysql> EXPLAIN SELECT one_id from one  ORDER BY create_time;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra          |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------+
|  1 | SIMPLE      | one   | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    3 |      100 | Using filesort |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------+

如果ORDER BY字段有索引就会用到覆盖索引,相比执行速度快很多。

mysql> EXPLAIN SELECT one_id from one  ORDER BY one_id;
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type  | possible_keys | key     | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | one   | NULL       | index | NULL          | PRIMARY | 4       | NULL |    3 |      100 | Using index |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
5、Using join buffer

Using join buffer:在我们联表查询的时候,如果表的连接条件没有用到索引,需要有一个连接缓冲区来存储中间结果。

先看一下有索引的情况:连接条件 one_nametwo_name 都用到索引。

mysql> EXPLAIN SELECT one_name from one o,two t where o.one_name = t.two_name;
+----+-------------+-------+------------+-------+---------------+----------+---------+----------------------+------+----------+--------------------------+
| id | select_type | table | partitions | type  | possible_keys | key      | key_len | ref                  | rows | filtered | Extra                    |
+----+-------------+-------+------------+-------+---------------+----------+---------+----------------------+------+----------+--------------------------+
|  1 | SIMPLE      | o     | NULL       | index | idx_name      | idx_name | 768     | NULL                 |    3 |      100 | Using where; Using index |
|  1 | SIMPLE      | t     | NULL       | ref   | idx_name      | idx_name | 768     | xin-slave.o.one_name |    1 |      100 | Using index              |
+----+-------------+-------+------------+-------+---------------+----------+---------+----------------------+------+----------+--------------------------+

接下来删掉 连接条件 one_nametwo_name 的字段索引。发现Extra 列变成 Using join buffertype均为全表扫描,这也是SQL优化中需要注意的地方。

mysql> EXPLAIN SELECT one_name from one o,two t where o.one_name = t.two_name;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra                                              |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
|  1 | SIMPLE      | t     | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    2 |      100 | NULL                                               |
|  1 | SIMPLE      | o     | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    3 |    33.33 | Using where; Using join buffer (Block Nested Loop) |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
6、Impossible where

Impossible where:表示在我们用不太正确的where语句,导致没有符合条件的行。

mysql> EXPLAIN SELECT one_name from one WHERE 1=2;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+------------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra            |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+------------------+
|  1 | SIMPLE      | NULL  | NULL       | NULL | NULL          | NULL | NULL    | NULL | NULL | NULL     | Impossible WHERE |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+------------------+
7、No tables used

No tables used:我们的查询语句中没有FROM子句,或者有 FROM DUAL子句。

mysql> EXPLAIN select now();
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra          |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------+
|  1 | SIMPLE      | NULL  | NULL       | NULL | NULL          | NULL | NULL    | NULL | NULL | NULL     | No tables used |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------+

Extra列的信息非常非常多,这里就不再一一列举了,详见 MySQL官方文档 :https://dev.mysql.com/doc/ref...
在这里插入图片描述

总结

上边只是简单介绍了下 Explain 执行计划各个列的含义,了解它不仅仅是要应付面试,在实际开发中也经常会用到。比如对慢SQL进行分析,如果连执行计划结果都不会看,那还谈什么SQL优化呢?


整理了几百本各类技术电子书和视频课程,送给小伙伴们。同名公号内自行领取。和一些小伙伴们建了一个技术交流群,一起探讨技术、分享技术资料,旨在共同学习进步,如果感兴趣就加入我们吧!

查看原文

赞 46 收藏 33 评论 2

烟熏妆 赞了文章 · 5月11日

有技术就能自建云盘,PDF预览,文件下载。速度与激情掌握在自己手里!

作者:小傅哥
博客:https://bugstack.cn

沉淀、分享、成长,让自己和他人都能有所收获!😄

一、前言

本篇文章只介绍如何自建云盘,不剐蹭任何云服务。

在技术学习的路上经常与同好交流心得,时而分享一些技术的PDF书籍。但也经常发现即使是一些可以开源的书籍,分享出来的链接也常常遇到链接失效问题。

尤其是最近希望把看过的一些不错的技术书籍和伙伴的推荐的一起汇总,并附上简单的书评和推荐指数。分享给新人在学习的过程中多有一些干货的内容。同时在一些需要付费的书籍上,也会引导购买纸质书籍。给创作者一份回报,也让自己可以更加方便的阅读。

但在做这件事的时候,经常遇到分享的链接过一会就失效,同时预览效果也不是很好,不能随开随读。所以就萌生了,构建自己的云盘。

小傅哥,那个不是简单的男人,一直比较能折腾!从折腾中不断的触及到新知识领域!


在折腾之前,我是有一些积累的,比如我有一个终身免费的海外云虚拟机,可用部署PHP、ASP项目。所以经常会在上面做一些实验,确定可用后在考虑付费去购买部署备案。不得已不谨慎,否则就是成本!

在这个过程中,我先是考虑PHP有CMS内容管理系统,测试后并不能满足我的需求。接下来就反复更换关键词搜索到了;可道云 - kodcloud.com。这是一款基于PHP开发的私有云存储&协同办公服务,同时可以在本地下载软件管理自建的云盘服务。好,这些我就不多介绍了,可以自行从官网查看。下面先上一张图,让你感受下;

  • 初次部署使用后,被这个页面震住了,竟然如此强大。
  • 有一个完整的桌面系统,支持企业级使用,可以建立组织关系。甚至你可以任意调整桌面背景,功能很好。
  • 可以分享文件成链接,支持设置隐私级别,与其他云盘功能一致。且对浏览PDF效果很理想,可以在线阅读。
  • 另外,有免费版!免费版就是你不要通过代码去改一些显示内容,否则会提示升级到付费版本。

好! 那么接下来,就教你如何去部署这样一款自己的云服务。

二、系统环境

  1. PHP云虚拟机,也可以部署到本地。php 5.3及以上另外使用 php7.3,开启缓存效果更佳。
  2. mysql 5.7,或者sqlite。如果有Redis服务,还可以支持缓存。
  3. IDEA,Java开发同学比较喜欢。主要用在FTP功能,非常方便管理。
  4. Github代码:https://github.com/kalcaddle/KodExplorer
  5. 官网下载:https://kodcloud.com/download/

三、工程部署

本文中使用了到云虚拟机的方式进行部署,支持PHP的虚拟机一般会自带数据库服务。只需要把代码上传进去后,打开域名即会提示安装,按照步骤执行即可。

使用IDEA打开下载后的 PHP 云盘工程,如果你安装了PHP版本IDEA开发工具,也可以。

1. IDEA 配置FTP和上传代码

在IDEA中有一个非常牛的功能,就是可以配置;FTP、FTPS、SFTP。这样就可以在开发的过程,直接将代码上传到虚拟机云服务中。

1.1 配置路径

Tools -> Deployment -> Configuration - 按照路径找到后填写ftp链接信息。

1.2 上传文件

配置好路径后,就可以打开FTP服务。在工程中点击上传文件即可。

2. 云盘安装和配置

安装过程非常简单,只要打开我们的云虚拟机配置的域名,进行访问即可。他会提示你按照步骤进行按照,你只需要准备好PHP版本、数据库用户名密码即可。

2.1 服务安装

环境检测

数据库设置

安装完成

2.2 初始设置

按照执行步骤安装完成后,现在就可以使用了。整体的页面功能也非常简单易操作。如果你有一些其他需要也可以在桌面进行设置。

名称信息

资源上传

扩展功能

到这,我们的自己的云盘就已经安装好了,那么接下来就可以进行使用了。😺开心!

四、云盘使用「分享书籍」

如果分享书籍有任何涉及网络不可传播,随时删除!

1. 上传PDF书籍

  • 上传的过程非常简单,还可以批量上传。具体速度取决于你的云服务。

2. 设置外链

分享外链可以设置的功能非常多;

  • 是否设置提取码
  • 标题修改
  • 可见用户权限
  • 到期时间
  • 下载次数和禁止下载

3. 分享链接(阅读原文直达)

分享一波提升技术格调的书籍19本:https://github.com/fuzhengwei/CodeGuide/wiki/值得一看的好书

CodeGuide 程序员编码指南,一波提升编程技能格调的19本书籍

五、总结

  • 技术总是能让生活越来越美好,永远不要局限在自己的小窝里去点评一件你尚未了解清楚的事情。
  • 个人建造云盘在号主、学校、社团、小企业中都有一定的用武之地,自行体会建设。可能你不知道的事情总在创造价值。
  • 如果你说这是PHP的,不要在意语言!所有的技术都是为了产品服务于业务,用于承载多样性用户行为数据的。

六、彩蛋

CodeGuide | 程序员编码指南 Go!
<br/>本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总,旨在为大家提供一个清晰详细的学习教程,侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助,请给予支持(关注、点赞、分享)!

CodeGuide | 程序员编码指南

查看原文

赞 5 收藏 4 评论 0

烟熏妆 赞了文章 · 5月8日

在 PHP 中管道(Pipeline) 能帮我们做什么?

前言

看到这个标题,你或许会疑惑:“什么是管道?”;“它有什么用?”;”它有什么优势?“;“它能解决什么”‘下面,就以我的视角来给你解开它。

什么是管道?

管道,在 PHP 开发中其实我们很少有听到有人说管道这个词,在 Linux 操作中听到的比较多 “管道操作符 |”,其实这里我们要探讨的管道和这个也是一个概念,就如名字一样,“管道”,当数据从管道的一头进入后,经过内部的处理,最终再从另一头出来。
在经过管道的途中,我们可以对我们传入的数据或者处理结果进行相应的调整以达到我们的目的。

它有什么用?

经过上面的介绍,你可能对管道有了一定的认识,但是仅限于理论层面的,相当抽象,甚至于还无法接受这种思想。如果我现在告诉你,如果你使用过 Laravel,其实你早就已经用过它了,你可能会惊讶。是的,在大多是情况下,你确实已经使用过它了,在 Laravel 中最常见的功能 「中间件」,就是基于管道来实现的。

中间件

通过使用中间件,可以在对传入的 Request $request 进行操作,甚至修改它的值,我们也可以调用 $next($request) ,来获取结果后对处理的 Response 添加 Header ,我们也可以在接到一些特殊请求后直接处理返回,比如处理跨域,大多数时候就是在这里处理的。如果你有兴趣,还可以往下看看

它能解决什么?

是的,很多时候我们学习一个新事物的时候,这个事物能给我们带来什么,通常是我们最关心的。
试着想一下,你现在有一个电商程序,在最初的时候,你只需要顾客提交商品创建订单、支付,这一切看起来似乎很简单。

// OrderService.php
class OrderService {
    // 创建订单
    public function create(){
        // some code
        return new Order();
    }
    public function pay(Order $order){
        // some code
        $payInterface = new AliPay($order);
        return $payInterface->response();
    }
}

现在我功能基本有了,但是新的需求下来了,商城新加了一个会员卡,一级会员打 9.5 折,二级会员 9 折,三级会员 8.5 折,现在我们的代码可能成了这样。

class OrderService {
    public function pay(Order $order){
        $vipLevel = (int) Auth::user()->vipLevel();
        $vipMappings = [
            0 => 1,
            1 => 0.95,
            2 => 0.9,
            3 => 0.85,
        ];
        // 是的 这为了保险起见,当取值出现问题时,默认为 1 ,即为不打折。
        $order->amount = bcmul($order->amount,$vipMappings[$vipLevel] ?? 1);
        $payInterface = new AliPay($order);
        return $payInterface->response();
    }
}

这看起来还好,只是折扣计算使得你原来的代码不再那么“好看”了,如果接下来我告诉你,我们加入了优惠券系统?部分商品需要打折,你会怎么样?一起来看看。

// ...
// ...
// ...
class OrderService {
    public function pay(Order $order){
        bcscale(2);
        $amount = '0';
        foreach($order->products as $product){
            // 这里的 discount 我们就当他是跟下面 VIP 一样的小数那样。
            // 这里似乎还应该考虑限时折,这里就不展开写了。
            $amount = bcadd($amount,bcmul($product->price,$product->discount));
        }
        $order->amount = $amount;
        $vipLevel = (int) Auth::user()->vipLevel();
        $vipMappings = [
            0 => 1,
            1 => 0.95,
            2 => 0.9,
            3 => 0.85,
        ];
        // 是的 这为了保险起见,当取值出现问题时,默认为 1 ,即为不打折。
        $order->amount = bcmul($order->amount,$vipMappings[$vipLevel] ?? 1);
        $payInterface = new AliPay($order);
        return $payInterface->response();
    }
}

看起来似乎也还好,但是如果我们还有更多这样类似的需求呢?你会发现有点儿不妙,我们代码方向变了,而且更乱了,更加的不好控制,可能过一段时间,我都已经“不敢承认”,这曾经是自己写的代码了,看起来我们需要解决这个问题,现在,是时候让文章的主角“管道”出场了。

Laravel 中的管道

在 Laravel 中,Laravel 已经帮助我们实现了一个管道 \Illuminate\Pipeline\Pipeline ,我们只需要根据需要,把数据放入管道,然后使用我们自己的处理器去处理数据,最后在管道的另外一头将数据输出,数据从进入管道后就不再去关心里面会发生什么,就像水一样,它只需要关心从管道另一头输出数据,首先我们要开始简化一下 pay 方法,让他看起来尽量保持简洁。

class OrderService {
    public function pay(Order $order){
        try{
        $order = pipeline('order_service::pay',$order);
        }catch (Exception $e){
            // log..
            // 这里我们可以记录日志,也可以不记录,直接不处理这个异常,然后由调用者来处理这个异常,
            // 因为这里在关东中可能出现了一些情况需要停止向下传播
            throw $e;
        }
        $payInterface = new AliPay($order);
        return $payInterface->response();
    }
}

现在引入了一个 pipeline 方法但是现在并不存在这个方法,需要去创建它,用来做管道的触发埋点。

现在我们创建了一个管道函数,用来帮助我们简化调用,以便在其他地方进行复用,这个方法接收2个参数,一个是埋点,一个是要传递的数据

// functions.php
if(!function_exists('pipeline')){
    function pipeline($pipe,$data){
        $pipeline = resolve(Pipeline::class);
        $handlers = config('pipeline.'.$pipe);
        return $pipeline->through($handlers)->send($data)->thenReturn();
    }
}

现在在配置文件中定义一下这个触发点关联的处理器,就像注册事件那样。

// config/pipeline.php
return [
    'order_service::pay'=>[
        // 产品折扣
        ProductDiscount::class,
        // 会员卡折扣
        UserVipDiscount::class,
        // 使用优惠券
        CouponUsing::class,
    ],
];

接着我们来实现这些不同的处理器

class ProductDiscount {
    public function handle($order,$next){
        bcscale(2);
        $amount = '0';
        foreach($order->products as $product) {
            // 这里的 discount 我们就当他是跟下面 VIP 一样的小数那样。
            // 这里似乎还应该考虑限时折,这里就不展开写了。
            $amount = bcadd($amount,bcmul($product->price,$product->discount));
        }
        $order->amount = $amount;
        return $next($order);
    }
}
class UserVipDiscount {
    public function handle($order,$next) {
        $vipLevel = (int) Auth::user()->vipLevel();
                $vipMappings = [
                    0 => 1,
                    1 => 0.95,
                    2 => 0.9,
                    3 => 0.85,
                ];
        // 是的 这为了保险起见,当取值出现问题时,默认为 1 ,即为不打折。
        $order->amount = bcmul($order->amount,$vipMappings[$vipLevel] ?? 1);
        return $next($order);
    }
}

现在就对代码实现了一个解耦,同时保持了 pay 方法的洁净,使得其更加单一,但是,这并不完美,为什么?

试想一下,管道中任意一个处理器返回数据错了,按照预期,他应该总是接收一个 Order 对象,并且返回也应该是一个 Order 对象,一直这样重复下去,到最终管道的结果也应该是一个 Order 对象,让我们可以给 AliPay 对象作为构造方法传递进去调用,但是在这里,我们并没有去限制它,你也可以自己来实现这一部分。

它有什么优势?

管道和事件

通过上面笼统的介绍,和简单的应用,你可能会觉得,这样看起来,管道不是和事件很相似了么?

都是注册、然后触发、最后获取结果、异常处理。看起来是很相似,尤其是前面的步骤,注册、触发、异常处理,但是获取结果,比如在 Laravel 中,如果一个事件上注册了多个事件
处理器,那在触发事件后,我们可以在触发函数接收到每个事件处理器的返回值组成的数组,是的,如果上面的栗子我们用事件去做,那么就会返回三个 Order,这三个可能并没有关系,因为当商品打折后使用会员卡时,我们使用会员卡前的价格就应该是打折后的了,而管道就不一样,管道始终都是使用的源对象 ,也就是说在我们后面的步骤中,实际上使用的还是我们触发事件时所传递的那个 Order 对象,但是 Pipeline 会顺序、准确的处理 Order 对象,并在最终返回一个对象给我们。除此之外,还有一个不同点,多数情况下管道的应用总是同步的,而事件则是可以选择使用异步来进行处理,进一步提升我们业务处理效率。

所以说,根据不同的场景来选择适合业务的方案,不但可以优化我们的代码,还可以提升性能。

他有点儿像 Hook

如果你看到过一些有关 PHP 中和 插件功能实现的文章或者 ThinkPHP 3.2 的代码,你可能会觉得,它有点儿像 Hook,
但是如果你梳理完这两者的实现原理,你会发现,其实 「Hooks」 更像「事件」。

更酷的方案 「AOP(面向切面编程)」

通过上面的应用,我们知道管道可以帮我们解决这种简单的问题,但是现在还有一种能加 cool 的方案,使用 AOP 中的前置通知或者环绕通知,我们也可以做到不侵入原有业务的情况下实现这个功能,如果你感兴趣可以去看看 Swoft 中关于 AOP) 章节的介绍。

但是,AOP 也是完全能够替代管道,只是在这个例子中可以完全替代,希望你能明白这一点,管道可与在代码进行中的任意一个阶段埋点,这一点 AOP 是没法做到的。

结束

看完了之后,你是否很好奇,管道是怎么实现的 ?通过这个链接,你可以了解到在 Laravel 中管道的源码,从而来帮助你更加深刻的去理解它。

参考资料

查看原文

赞 16 收藏 9 评论 0

烟熏妆 赞了文章 · 4月29日

Vagrant+PHPStorm+Google+XDebug断点调试

1.登陆vagrant修改xdebug.ini配置

A.  登陆vagrant:vagrant ssh

B.  修改配置:sudo vim /etc/php/7.0/fpm/conf.d/20-xdebug.ini
      配置内容:
     zend_extension=xdebug.so
     xdebug.remote_enable = 1
     xdebug.remote_connect_back = 1
     xdebug.remote_port = 9001
     xdebug.max_nesting_level = 512
     xdebug.remote_host="192.168.10.10"
     xdebug.idekey = "PHPSTORM"
     xdebug.default_enable = 1
     xdebug.remote_enable = 1
     xdebug.remote_autostart = 1
     xdebug.remote_handler="dbgp"

  C.重启php: sudo service php7.0-fpm restart

2.在PHPStorm->Preferences->Languages & Frameworks->PHP中箭头指向配置

clipboard.png

选择+,选择from vagrant

clipboard.png

配置如下:

clipboard.png

在PHP->Debug->DBGp Proxy设置如下

clipboard.png

在PHP->Debug设置端口

clipboard.png

在PHP->Servers中设置映射路径

clipboard.png

3.在google中下载Xdebug helper,并配置

clipboard.png

备注:如果不能断点代表Xdebug和php版本不对应,可以将其他php版本的20-xdebug.ini都设置

查看原文

赞 2 收藏 4 评论 0

烟熏妆 赞了文章 · 3月30日

这 10 个片段,有助于你理解 ES 中的 Promise

作者:Jay Chow
译者:前端小智
来源:jamesknelson

点赞再看,养成习惯

本文 GitHubhttps://github.com/qq44924588... 上已经收录,更多往期高赞文章的分类,也整理了很多我的文档,和教程资料。欢迎Star和完善,大家面试可以参照考点复习,希望我们一起有点东西。

在开发中,了解 JavaScript 和 Promise 基础,有助于提高我们的编码技能,今天,我们一起来看看下面的 10 片段,相信看完这 10 个片段有助于我们对 Promise 的理解。

片段1:

const prom = new Promise((res, rej) => {
  console.log('first');
  res();
  console.log('second');
});
prom.then(() => {
  console.log('third');
});
console.log('fourth');

// first
// second
// fourth
// third

Promise同步执行,promise.then异步执行。

片段2:

const prom = new Promise((res, rej) => {
  setTimeout(() => {
    res('success');
  }, 1000);
});
const prom2 = prom.then(() => {
  throw new Error('error');
});

console.log('prom', prom);
console.log('prom2', prom2);

setTimeout(() => {
  console.log('prom', prom);
  console.log('prom2', prom2);
}, 2000);

// prom 
// Promise {<pending>}
// __proto__: Promise
// [[PromiseStatus]]: "resolved"
// [[PromiseValue]]: "success"

// 2 秒后还会在打一遍上面的两个

promise 有三种不同的状态:

  • pending
  • fulfilled
  • rejected

一旦状态更新,pending->fulfilledpending->rejected,就可以再次更改它。 prom1prom2不同,并且两者都返回新的Promise状态。

片段3:

const prom = new Promise((res, rej) => {
  res('1');
  rej('error');
  res('2');
});

prom
  .then(res => {
    console.log('then: ', res);
  })
  .catch(err => {
    console.log('catch: ', err);
  });

// then: 1

即使reject后有一个resolve调用,也只能执行一次resolvereject ,剩下的不会执行。

片段 4:

Promise.resolve(1)
  .then(res => {
    console.log(res);
    return 2;
  })
  .catch(err => {
    return 3;
  })
  .then(res => {
    console.log(res);
  });

// 1
// 2

Promises 可以链接调用,当提到链接调用 时,我们通常会考虑要返回 this,但Promises不用。 每次 promise 调用.then.catch时,默认都会返回一个新的 promise,从而实现链接调用。

片段 5:

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log('first')
    resolve('second')
  }, 1000)
})

const start = Date.now()
promise.then((res) => {
  console.log(res, Date.now() - start, "third")
})
promise.then((res) => {
  console.log(res, Date.now() - start, "fourth")
})

// first
// second 1054 third
// second 1054 fourth

promise 的 .then.catch可以被多次调用,但是此处Promise构造函数仅执行一次。 换句话说,一旦promise的内部状态发生变化并获得了一个值,则随后对.then.catch的每次调用都将直接获取该值。

片段 6:

const promise = Promise.resolve()
  .then(() => {
    return promise
  })
promise.catch(promise )

// [TypeError: Chaining cycle detected for promise #<Promise>]
// Uncaught SyntaxError: Identifier 'promise' has already been declared
//    at <anonymous>:1:1
// (anonymous) @ VM218:1

.then.catch返回的值不能是promise本身,否则将导致无限循环。


大家都说简历没项目写,我就帮大家找了一个项目,还附赠【搭建教程】

我和阿里云合作服务器,折扣价比较便宜:89/年,223/3年,比学生9.9每月还便宜,买了搭建个项目,熟悉技术栈比较香(老用户用家人账号买就好了,我用我妈的)推荐买三年的划算点,点击本条就可以查看


片段 7:

Promise.resolve()
  .then(() => {
    return new Error('error');
  })
  .then(res => {
    console.log('then: ', res);
  })
  .catch(err => {
    console.log('catch: ', err);
  });

// then: Error: error!
// at Promise.resolve.then (...)
// at ...

.then.catch中返回错误对象不会引发错误,因此后续的.catch不会捕获该错误对象,需要更改为以下对象之一:

return Promise.reject(new Error('error')) throw new Error('error')

因为返回任何非promise 值都将包装到一个Promise对象中,也就是说,返回new Error('error')等同于返回Promise.resolve(new Error('error'))

片段 8:

Promise.resolve(1)
  .then(2)
  .then(Promise.resolve(3))
  .then(console.log)

  // 1

.then.catch的参数应为函数,而传递非函数将导致值的结果被忽略,例如.then(2).then(Promise.resolve(3)

片段 9:

Promise.resolve()
  .then(
    function success(res) {
      throw new Error('Error after success');
    },
    function fail1(e) {
      console.error('fail1: ', e);
    }
  )
  .catch(function fail2(e) {
    console.error('fail2: ', e);
  });

//   fail2:  Error: Error after success
//     at success (<anonymous>:4:13)

.then可以接受两个参数,第一个是处理成功的函数,第二个是处理错误的函数。 .catch是编写.then的第二个参数的便捷方法,但是在使用中要注意一点:.then第二个错误处理函数无法捕获第一个成功函数和后续函数抛出的错误。 .catch捕获先前的错误。 当然,如果要重写,下面的代码可以起作用:

Promise.resolve()
  .then(function success1 (res) {
    throw new Error('success1 error')
  }, function fail1 (e) {
    console.error('fail1: ', e)
  })
  .then(function success2 (res) {
  }, function fail2 (e) {
    console.error('fail2: ', e)
  })

片段 10:

process.nextTick(() => {
  console.log('1')
})
Promise.resolve()
  .then(() => {
    console.log('2')
  })
setImmediate(() => {
  console.log('3')
})
console.log('4');

// Print 4
// Print 1
// Print 2
// Print 3

process.nextTickpromise.then都属于微任务,而setImmediate属于宏任务,它在事件循环的检查阶段执行。 在事件循环的每个阶段(宏任务)之间执行微任务,并且事件循环的开始执行一次。


代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

原文:http://jamesknelson.com/grokk...


交流

干货系列文章汇总如下,觉得不错点个Star,欢迎 加群 互相学习。

https://github.com/qq44924588...

我是小智,公众号「大迁世界」作者,对前端技术保持学习爱好者。我会经常分享自己所学所看的干货,在进阶的路上,共勉!

关注公众号,后台回复福利,即可看到福利,你懂的。

clipboard.png

查看原文

赞 26 收藏 15 评论 0

烟熏妆 赞了文章 · 2019-12-06

《领域驱动设计之PHP实现》 全书翻译 - 架构风格

架构风格

  1. 《领域驱动设计之PHP实现》全书翻译 - DDD入门
  2. 《领域驱动设计之PHP实现》全书翻译 - 架构风格
  3. 《领域驱动设计之PHP实现》全书翻译 - 值对象
  4. 《领域驱动设计之PHP实现》全书翻译 - 实体
  5. 《领域驱动设计之PHP实现》全书翻译 - 服务
  6. 《领域驱动设计之PHP实现》全书翻译 - 领域事件
  7. 《领域驱动设计之PHP实现》全书翻译 - 模块
  8. 《领域驱动设计之PHP实现》全书翻译 - 聚合
  9. 《领域驱动设计之PHP实现》全书翻译 - 工厂
  10. 《领域驱动设计之PHP实现》全书翻译 - 仓储
  11. 《领域驱动设计之PHP实现》全书翻译 - 应用服务
  12. 《领域驱动设计之PHP实现》全书翻译 - 集成上下文
  13. 《用PHP实现六边形架构》

对于构建复杂应用,一个关键点就是得有一个适合应用需求的架构设计。领域驱动设计的一个优势就是不必绑定到任何特定的架构风格之上。相反的,我们可以根据每个核心域内的限界上下文自由选择最佳的架构,限界上下文同时为每个特定领域问题提供了丰富多彩的架构选择。

例如,一个订单系统可以使用事件源(Event Sourcing)来追踪所有不同订单的操作;一个产品目录服务可以使用 CQRS 来暴露产品细节给不同客户端;一个内容管理系统可以使用一般的六边形架构来暴露如博客(blogs),静态页等服务。

从传统守旧派的 PHP 代码到更复杂先进的架构,本章将跟随这些历史来对 PHP 圈子内每个相关的架构风格做一些介绍。请注意尽管已经有许多其它存在的架构风格,例如数据网络架构(Data Fabric)或者面向服务架构(SOA),但我们发现从 PHP 的视角介绍它们还是有一些复杂的。

美好的旧时光

在 PHP4 发布之前 ,PHP 还没有拥抱面向对象模式。那时候,写应用的普遍方法就是用面向过程和全局状态。像关注点分离(SoC)和模型-视图-控制器(MVC)的概念是与当时的 PHP 社区相抵触的。

下面的例子就是用传统方式写的一个由许多混合了 HTML 代码前端控制器构成的应用。在那个时代,基础设施层,表现层,UI,及领域层代码都交织在一起:

<?php
include __DIR__ . '/bootstrap.php';
$link = mysql_connect('localhost', 'a_username', '4_p4ssw0rd');
if (!$link) {
    die('Could not connect: ' . mysql_error());
}
mysql_set_charset('utf8', $link);
mysql_select_db('my_database', $link);
$errormsg = null;
if (isset($_POST['submit'] && isValid($_POST['post'])) {
    $post = getFrom($_POST['post']);
    mysql_query('START TRANSACTION', $link);
    $sql = sprintf(
        "INSERT INTO posts (title, content) VALUES ('%s','%s')",
        mysql_real_escape_string($post['title']),
        mysql_real_escape_string($post['content']
        ));
    $result = mysql_query($sql, $link);
    if ($result) {
        mysql_query('COMMIT', $link);
    } else {
        mysql_query('ROLLBACK', $link);
        $errormsg = 'Post could not be created! :(';
    }
}
$result = mysql_query('SELECT id, title, content FROM posts', $link);
?>
    <html>
    <head></head>
    <body>
    <?php if (null !== $errormsg) : ?>
        <div class="alert error"><?php echo $errormsg; ?></div>
    <?php else: ?>
        <div class="alert success">
            Bravo! Post was created successfully!
        </div>
    <?php endif; ?>
    <table>
        <thead>
        <tr>
            <th>ID</th>
            <th>TITLE</th>
            <th>ACTIONS</th>
        </tr>
        </thead>
        <tbody>
        <?php while ($post = mysql_fetch_assoc($result)) : ?>
            <tr>
                <td><?php echo $post['id']; ?></td>
                <td><?php echo $post['title']; ?></td>
                <td><?php editPostUrl($post['id']); ?></td>
            </tr>
        <?php endwhile; ?>
        </tbody>
    </table>
    </body>
    </html>
<?php mysql_close($link); ?>

这种风格的代码就是我们常说的大泥球,在第一章我们也提及过。下面的代码就做一些改进,然而仅仅是通过封装 header 和 footer 到单独的文件内,就可以避免重复及有利于重用:

<?php
include __DIR__ . '/bootstrap.php';
$link = mysql_connect('localhost', 'a_username', '4_p4ssw0rd');
if (!$link) {
    die('Could not connect: ' . mysql_error());
}
mysql_set_charset('utf8', $link);
mysql_select_db('my_database', $link);
$errormsg = null;
if (isset($_POST['submit'] && isValid($_POST['post'])) {
    $post = getFrom($_POST['post']);
    mysql_query('START TRANSACTION', $link);
    $sql = sprintf(
        "INSERT INTO posts(title, content) VALUES('%s','%s')",
        mysql_real_escape_string($post['title']),
        mysql_real_escape_string($post['content'])
    );
    $result = mysql_query($sql, $link);
    if ($result) {
        mysql_query('COMMIT', $link);
    } else {
        mysql_query('ROLLBACK', $link);
        $errormsg = 'Post could not be created! :(';
    }
}
$result = mysql_query('SELECT id, title, content FROM posts', $link);
?>
<?php include __DIR__ . '/header.php'; ?>
<?php if (null !== $errormsg) : ?>
    <div class="alert error"><?php echo $errormsg; ?></div>
<?php else: ?>
    <div class="alert success">
        Bravo! Post was created successfully!
    </div>
<?php endif; ?>
    <table>
        <thead>
        <tr>
            <th>ID</th>
            <th>TITLE</th>
            <th>ACTIONS</th>
        </tr>
        </thead>
        <tbody>
        <?php while ($post = mysql_fetch_assoc($result)): ?>
            <tr>
                <td><?php echo $post['id']; ?></td>
                <td><?php echo $post['title']; ?></td>
                <td><?php editPostUrl($post['id']); ?></td>
            </tr>
        <?php endwhile; ?>
        </tbody>
    </table>
<?php include __DIR__ . '/footer.php'; ?>

现今,尽管这种方式令人沮丧,但仍有大量应用使用这种方式编写代码。这种风格的架构主要坏处是没有做到真正的关注点分离 - 维护和开发这样一个应用的持续成本与其它已知和已验证的架构相比急剧增长。

分层架构

从代码的可维护性和可重用性角度来看,使代码更容易维护的最好方式就是拆分的思想,即为每个不同的关注点分层。在我们之前的例子中,非常容易形成不同层次:一个是封装数据访问和操作,另一个是处理基础设施的关注点,最后一个即是封装前两者的编排。分层架构的一个基本原则就是-每一层都必须与其下一层紧密相连,如下图所示:

分层架构

分层架构真正寻求的是对应用的不同组件进行分离。例如,在前面的例子当中,一个博客帖子的表示必须完全地独立于实体概念的博客帖子。一个博客帖子实体可以与一个或多个表示相关联。这就是通常所说的关注点分离。

另一种寻求相同目的的架构模式就是模型-视图-控制器模式。它最初被认为和广泛用于创建桌面 GUI 应用。现在主要应用于 web 应用。这得益于像 Symfony, Zend FrameworkCodeIgniter 这些的流行框架。

模型-视图-控制器(MVC)

模型-视图-控制器模式将应用划分为三个主要层次,要点描述如下:

  1. 模型层:提取和集中所有领域模型的行为。这一层独立管理表现层的所有数据,逻辑及业务规则。所有说模型层是每个 MVC 应用程序的心脏和灵魂。
  2. 控制层:即其他两层之间的抽象编排,主要是触发模型的行为来更新其状态,以及刷新与模型关联的表现层。除此之外,控制层还能发送消息给视图层来改变特定的领域表现形式。
  3. 视图层:暴露模型层的不同表现形式,同时提供改变模型状态的一些触发动作。

MVC

分层架构的示例

模型层

继续之前的例子,我们注意到不同的关注点需要被分离。为了达到这一点,所有层次都必须从我们这些原始的混乱代码中识别出来。在这个过程中,我们需要特别注意与模型层有关的代码,即应用的核心代码:

class Post
{
    private $title;
    private $content;

    public static function writeNewFrom($title, $content)
    {
        return new static($title, $content);
    }

    private function __construct($title, $content)
    {
        $this->setTitle($title);
        $this->setContent($content);
    }

    private function setTitle($title)
    {
        if (empty($title)) {
            throw new RuntimeException('Title cannot be empty');
        }
        $this->title = $title;
    }

    private function setContent($content)
    {
        if (empty($content)) {
            throw new RuntimeException('Content cannot be empty');
        }
        $this->content = $content;
    }
}

class PostRepository
{
    private $db;

    public function __construct()
    {
        $this->db = new PDO(
            'mysql:host=localhost;dbname=my_database',
            'a_username',
            '4_p4ssw0rd',
            [
                PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4',
            ]
        );
    }

    public function add(Post $post)
    {
        $this->db->beginTransaction();
        try {
            $stm = $this->db->prepare(
                'INSERT INTO posts (title, content) VALUES (?, ?)'
            );
            $stm->execute([
                $post->title(),
                $post->content(),
            ]);
            $this->db->commit();
        } catch (Exception $e) {
            $this->db->rollback();
            throw new UnableToCreatePostException($e);
        }
    }
}

模型层现在用一个 Post 类和一个 PostRepository 类定义。Post 类表示一个博客帖子,PostRepository 类表示可用博客帖子的整个集合。除此之外,另一层 - 用来协调和编排这些领域行为 - 也是模型层内需要的。现在进入应用层:

class PostService
{
    public function createPost($title, $content)
    {
        $post = Post::writeNewFrom($title, $content);
        (new PostRepository())->add($post);
        return $post;
    }
}

PostService 类即我们所说的应用服务,它的目的是编排和组织领域行为。换句话说,应用服务是领域模型的直接客户端,是那些使业务发生的服务。没有其他类型的对象可以直接与模型层内部直接对话。

视图层

视图层可以从模型层和/或者控制层接收数据,也能向其发送数据。它的主要目的是向用户UI层呈现模型,同时在模型每次更新后刷新UI的呈现形式。一般来说,视图层接收的对象 - 通常是一个数据传输对象(DTO)而不是模型层实例 - 从而收集被成功呈现的所有必需信息。对于 PHP,这已经有几种模板引擎可以帮助从模型本身和从控制层分离模型的表示。其中最流行的一个叫 Twig。让我们看看使用 Gwig 的视图层是怎样的。

为什么是数据传输对象(DTO)而不是模型实例?

这是一个古老且有活力的话题。为什么要创建一个 DTO 而不是把模型实例直接交给视图层?简短来说,还是关注点分离。让视图层方便直接使用模型实例将导致视图层与模型层间的紧耦合。事实上,模型层中的一个改变将可能破坏所有使用改变后的模型的所有视图。

{% extends "base.html.twig" %}
{% block content %}
{% if errormsg is defined %}
<div class="alert error">{{ errormsg }}</div>
{% else %}
<div class="alert success">
    Bravo! Post was created successfully!
</div>
{% endif %}
<table>
    <thead>
    <tr>
        <th>ID</th>
        <th>TITLE</th>
        <th>ACTIONS</th>
    </tr>
    </thead>
    <tbody>
    {% for post in posts %}
    <tr>
        <td>{{ post.id }}</td>
        <td>{{ post.title }}</td>
        <td><a href="{{ editPostUrl(post.id) }}">Edit Post</a></td>
    </tr>
    {% endfor %}
    </tbody>
</table>
{% endblock %}

大多数时候,当模型触发一个状态改变,同时也会通知相关视图 UI 已经刷新了。在一个典型的 web 场景中,由于客户端-服务器这一约束,模型和它的表示之间的同步可能会有一点棘手。在这些情况下,通常要用一些 JavaScript 定义的交互方式来维护这些同步。由于这个原因,近年来 JavaScript MVC 框架开始变得广泛流行,正如下面这些框架:

  • AngularJS
  • Ember.js
  • Marionette.js
  • React

控制层

控制层主要负责组织和编排视图和模型。它接收来自视图层的消息和为了执行期望的动作而触发模型行为。此外,为了呈现模型的表示,它也发送消息给视图。被执行的动作也需要感谢应用层,即负责编排,组织和封装领域行为的这一层。
就一个 PHP 的 web 应用来说,控制层包括一组类,为了达到它们的目的,叫做 "HTTP" 。换句话说,它们接收一个 HTTP 请求,同时返回一个 HTTP 响应:

class PostsController
{
    public function updateAction(Request $request)
    {
        if (
            $request->request->has('submit') &&
            Validator::validate($request->request->post)
        ) {
            $postService = new PostService();
            try {
                $postService->createPost(
                    $request->request->get('title'),
                    $request->request->get('content')
                );
                $this->addFlash(
                    'notice',
                    'Post has been created successfully!'
                );
            } catch (Exception $e) {
                $this->addFlash(
                    'error',
                    'Unable to create the post!'
                );
            }
        }
        return $this->render('posts/update-result.html.twig');
    }
}

依赖倒置:六边形架构

依照分层架构的基本思想,当实现包含有关基础设施层的领域接口时,是存在风险的。
以 MVC 为例,先前例子中的 PostRepository 类应该放在领域模型当中。然而,把基础设施细节放在领域之中是违背关注点分离这一原则的.这是有问题的;它很难避免违背分层架构的基本思想,如果模型层有技术实现,这将会导致一种很难测试的代码类型出现。

依赖倒置原则(DIP)

我们可以怎样改进呢?由于领域模型层依赖基础设施的具体实现,依赖倒置原则(DIP),可以通过应将基础设施层重新放在其它三层之上来应用。

依赖倒置原则

高层次模型不应该依赖于低层次模型。它们都应该依赖于抽象。
抽象不应该依赖于细节,细节应该依赖于抽象。
-- Robert C.Martin

通过使用依赖倒置原则,架构模式改变了,基础设施层 - 可以称为低层次模块 - 现在依赖于 UI,应用层和模型层这些高层次模块。于是依赖被倒置了。

但什么是六边形架构呢?它是怎样适合这里面的所有问题呢?六边形架构(即端口与适配器)是 Alistair Cockburn 在他的书《六边形架构》中定义的。它将应用描述成一个六边形,每条边被表示为一个端口和多个适配器。端口是一个可插拔适配器的连接器件,适配器将外部输入转换为应用内部可理解的数据。就依赖倒置(DIP)来说,端口是高层次模块,适配器是低层次模块。此外,如果应用需要发送消息给外部,它可以用一个带适配器的端口来发送和转换可以被外部可理解的数据。正因为如此,六边形架构提出了应用里对称性的概念,这也是为什么架构模式发生变化的主要原因。它经常被表示为六边形,因为讨论顶层或者底层不再有任何意义。相反,六边形架构主要是外与内部间的对话。

如果你想要了解更多细节,Youtube 上有 Matthias Noback 关于六边形架构的非常好的视频

应用六边形架构

我们继续博客应用的例子,首先我们需要的概念就是端口,即外部世界与应用程序对话的渠道。在这个例子中,我们使用一个 HTTP 端口及相应的适配器,外部通过端口发送消息给应用程序。博客例子使用数据库存储整个博客帖子集合,所以为了让应用程序从数据库中检索博客帖子数据,端口就是必须的:

interface PostRepository
{
    public function byId(PostId $id);

    public function add(Post $post);
}

该接口暴露有关博客帖子的端口,应用程序通过它检索信息。它也被放置在领域层。现在,则需要这个端口的适配器。该适配器负责定义用特定技术检索博客帖子的方法:

class PDOPostRepository implements PostRepository
{
    private $db;

    public function __construct(PDO $db)
    {
        $this->db = $db;
    }

    public function byId(PostId $id)
    {
        $stm = $this->db->prepare(
            'SELECT * FROM posts WHERE id = ?'
        );
        $stm->execute([$id->id()]);
        return recreateFrom($stm->fetch());
    }

    public function add(Post $post)
    {
        $stm = $this->db->prepare(
            'INSERT INTO posts (title, content) VALUES (?, ?)'
        );
        $stm->execute([
            $post->title(),
            $post->content(),
        ]);
    }
}

只要我们定义了端口及其适配器,最后就是重构 PostService 从而可以它们。这可以通过依赖注入(Dependency Injection)轻松实现:

class PostService
{
    private $postRepository;

    public function __construct(PostRepositor $postRepository)
    {
        $this->postRepository = $postRepository;
    }

    public function createPost($title, $content)
    {
        $post = Post::writeNewFrom($title, $content);
        $this->postRepository->add($post);
        return $post;
    }
}

这仅仅是六边形架构的一个简单例子,它是一个灵活的,类似分层,有利于关注点分离的架构。由于内部应用通过端口与外部通信,这也同时提升了对称性。从现在开始,这将作为基本架构来构建和解释 CQRS 及事件源模式。

想了解更多关于这种架构的例子,你可以去查看附录中的 《Hexagonal Architecture with PHP》。对于一个更详细的例子,你可以跳到第 11 章 - 应用程序,此章介绍了一些高级主题,像事务性和其它交叉问题。

命令查询职责分离(CQRS)

六边形架构是一个很好的基础性架构,但它有一些限制。例如,复杂 UI 需要在不同的表单上显示聚合信息(第八章,聚合),或者它们可以从多个聚合获取数据。在这种场景下,我们可以在仓储里使用许多查找方法(可能和应用程序里存在的 UI 视图一样多)。或者,也许我们可以直接将这种复杂性转移到应用服务,使用复杂结构来从多个聚合里积累数据,这里有一个例子:

interface PostRepository
{
    public function save(Post $post);

    public function byId(PostId $id);

    public function all();

    public function byCategory(CategoryId $categoryId);

    public function byTag(TagId $tagId);

    public function withComments(PostId $id);

    public function groupedByMonth();
// ...
}

当这些技术被滥用时,对 UI 视图层的构建将变得非常痛苦。我们应该权衡是该用应用服务返回领域实例还是某些 DTO 。后一种选择里,我们避免了领域模型与基础设施代码( web 控制器,CLI 控制器等等)间的紧耦合。

幸运的是,我们有另一种方法。如果需求有许多且独立的视图,我们可以将它们从领域模型中排除,把它们视为一种纯粹的基础设施问题。这种方法即基于一个设计原则,命令查询分离(CQS)。这个原则由 Bertrand Meyer 提出,然后,相应地,成长为一个全新的架构模式,叫作命令查询职责分离(CQRS),CQRS 由 Greg Young 定义。

命令查询分离

提出一个问题不应该改变对应的答案 - Bertrand Meyer

这种设计原则提出每个方法应该要么是执行动作的命令,要么是返回数据给调用者的查询,而不是两者都是 - 维基百科

CQRS谋求一种更为激进的关注点分离,即将模型分为两部分:

  • 写模型: 同时也称为命令模型,它执行写入和负责真实的领域行为。
  • 读模型: 它在应用内负责读取,并将这部分视为领域模型之外的内容。

每次只要触发一个命令给写模型,它就会执行渴求数据的存储写入。除此之外,它还会触发读模型的更新,保证在读模型上显示最后一次的更改。

这种严格的分离导致了另一个问题,最终一致性。读模型的一致性现在受写模型执行的命令的影响。换句话说,读模型是最终一致性的。也就是说,每次当写模型执行一个命令,它就会负责挂起一个进程,依照写模型上最后一次更改,来更新读模型。所以这里存在一个时间窗口,UI可能会向用户展示旧的信息。在 web 场景中,这种情况经常发生,因为我们受当前技术因素限制。

考虑一个 web 应用的缓存系统,每次用新信息数更新数据库时,缓存层的数据有可能是陈旧的,所以每当模型有更新时,也应该同时更新缓存系统。所以 缓存系统是最终一致性的。

这些处理过程,在 CQRS 术语中被称为写模型投影,或者就称作投影。即投影一个写模型到读模型上。这个过程可以是同步或者异步,取决于你的需要,同时它可以用另一种很有用的战术设计模式 - 领域事件(本书后面的章节会讲到)来实现。写模型投影的基本过程就是收集所有发布的领域事件,然后用事件中的信息来更新读模型。

写模型

写模型是领域行为的真实持有者,继续我们的例子,仓储接口将被简化如下:

interface PostRepository
{
    public function save(Post $post);
    public function byId(PostId $id);
}

现在 PostRepository 已经从所有读关注点中分离出来,除了一个:byId 方法,负责通过 ID 来加载聚合以便我们对其进行操作。那么只要这一步完成,所有的查询方法都将从 Post 模型中剥离出来,只留下命令方法。这意味着我们可以有效地摆脱所有getter方法和任何其它暴露 Post 聚合信息的方法。取而代之的是,通过订阅聚合模型来发布领域事件,以触发写模型投影:

class AggregateRoot
{
    private $recordedEvents = [];

    protected function recordApplyAndPublishThat(
        DomainEvent $domainEvent
    )
    {
        $this->recordThat($domainEvent);
        $this->applyThat($domainEvent);
        $this->publishThat($domainEvent);
    }

    protected function recordThat(DomainEvent $domainEvent)
    {
        $this->recordedEvents[] = $domainEvent;
    }

    protected function applyThat(DomainEvent $domainEvent)
    {
        $modifier = 'apply' . get_class($domainEvent);
        $this->$modifier($domainEvent);
    }

    protected function publishThat(DomainEvent $domainEvent)
    {
        DomainEventPublisher::getInstance()->publish($domainEvent);
    }

    public function recordedEvents()
    {
        return $this->recordedEvents;
    }

    public function clearEvents()
    {
        $this->recordedEvents = [];
    }
}

class Post extends AggregateRoot
{
    private $id;
    private $title;
    private $content;
    private $published = false;
    private $categories;

    private function __construct(PostId $id)
    {
        $this->id = $id;
        $this->categories = new Collection();
    }

    public static function writeNewFrom($title, $content)
    {
        $postId = PostId::create();
        $post = new static($postId);
        $post->recordApplyAndPublishThat(
            new PostWasCreated($postId, $title, $content)
        );
    }

    public function publish()
    {
        $this->recordApplyAndPublishThat(
            new PostWasPublished($this->id)
        );
    }

    public function categorizeIn(CategoryId $categoryId)
    {
        $this->recordApplyAndPublishThat(
            new PostWasCategorized($this->id, $categoryId)
        );
    }

    public function changeContentFor($newContent)
    {
        $this->recordApplyAndPublishThat(
            new PostContentWasChanged($this->id, $newContent)
        );
    }

    public function changeTitleFor($newTitle)
    {
        $this->recordApplyAndPublishThat(
            new PostTitleWasChanged($this->id, $newTitle)
        );
    }
}

所有触发状态改变的动作都通过领域事件来实现。对于每一个已发布的领域事件,都有一个对应的 apply 方法负责状态的改变:

class Post extends AggregateRoot
{
// ...
    protected function applyPostWasCreated(
        PostWasCreated $event
    )
    {
        $this->id = $event->id();
        $this->title = $event->title();
        $this->content = $event->content();
    }

    protected function applyPostWasPublished(
        PostWasPublished $event
    )
    {
        $this->published = true;
    }

    protected function applyPostWasCategorized(
        PostWasCategorized $event
    )
    {
        $this->categories->add($event->categoryId());
    }

    protected function applyPostContentWasChanged(
        PostContentWasChanged $event
    )
    {
        $this->content = $event->content();
    }

    protected function applyPostTitleWasChanged(
        PostTitleWasChanged $event
    )
    {
        $this->title = $event->title();
    }
}

读模型

读模型,同时也称为查询模型,是一个纯粹的从领域中提取的非规范化的数据模型。事实上,使用 CQRS,所有的读取侧都被视为基础设施关注的表述过程。一般来说,当使用 CQRS 时,读模型与 UI 所需有关,与组合视图的 UI 复杂性有关。在一个关系型数据库中定义读模型的情况下,最简单的方法就是建立数据表与 UI 视图一对一的关系。这些数据表和 UI 视图将用写模型投影更新,由写一侧发布的领域事件来触发:

-- Definition of a UI view of a single post with its comments
CREATE TABLE single_post_with_comments (
id INTEGER NOT NULL,
post_id INTEGER NOT NULL,
post_title VARCHAR(100) NOT NULL,
post_content TEXT NOT NULL,
post_created_at DATETIME NOT NULL,
comment_content TEXT NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Set up some data
INSERT INTO single_post_with_comments VALUES
(1, 1, "Layered" , "Some content", NOW(), "A comment"),
(2, 1, "Layered" , "Some content", NOW(), "The comment"),
(3, 2, "Hexagonal" , "Some content", NOW(), "No comment"),
(4, 2, "Hexagonal", "Some content", NOW(), "All comments"),
(5, 3, "CQRS", "Some content", NOW(), "This comment"),
(6, 3, "CQRS", "Some content", NOW(), "That comment");
-- Query it
SELECT * FROM single_post_with_comments WHERE post_id = 1;

这种架构风格的一个重要特征就是,读模型应该完全是一次性的,因为应用的真实状态是由写模型来处理。这意味着读模型在需要时,可以用写模型投影来移除和重建。

这里我们可以看到一个博客应用里的一些可能存在的视图的例子:

SELECT * FROM posts_grouped_by_month_and_year ORDER BY month DESC,year ASC;
SELECT * FROM posts_by_tags WHERE tag = "ddd";
SELECT * FROM posts_by_author WHERE author_id = 1;

需要特别指出的是,CQRS 并不约束读模型的定义和实现要用关系型数据库,它取决于被构建的应用实际所需。它可以是关系型数据库,面向文档的数据库,键-值型存储,或任意适合应用所需的存储引擎。在博客帖子应用里,我们使用 Elasticsearch - 一个面向文档的数据库 - 来实现一个读模型:

class PostsController
{
    public function listAction()
    {
        $client = new ElasticsearchClientBuilder::create()->build();
        $response = $client->search([
            'index' => 'blog-engine',
            'type' => 'posts',
            'body' => [
                'sort' => [
                    'created_at' => ['order' => 'desc']
                ]
            ]
        ]);
        return [
            'posts' => $response
        ];
    }
}

读模型被彻底地简化为针对 Elasticsearch 的单个查询索引。
这表明读模型并不真正需要一个对象关系映射器,因为这是多余的。然而,写模型可能会得益于对象关系映射的使用,因为这允许你根据应用程序所需要来组织和构建读模型。

用读模型同步写模型

接下来便是棘手的部分。如何用写模型同步读模型?我们之前已经说过,通过使用写模型事务中捕获的领域事件来完成它。对于捕获的每种类型的领域事件,将执行一个特定的投影。因此,将设置领域事件和投影间的一个一对一的关系。

让我们看看配置投影的一个例子,以便我们得到一个更好的方法。首先,我们需要定义一个投影接口:

interface Projection
{
    public function listensTo();
    public function project($event);
}

所以为 PostWasCreated 事件定义一个 Elasticsearch 投影如下述一般简单:

namespace Infrastructure\Projection\Elasticsearch;

use Elasticsearch\Client;
use PostWasCreated;

class PostWasCreatedProjection implements Projection
{
    private $client;

    public function __construct(Client $client)
    {
        $this->client = $client;
    }

    public function listensTo()
    {
        return PostWasCreated::class;
    }

    public function project($event)
    {
        $this->client->index([
            'index' => 'posts',
            'type' => 'post',
            'id' => $event->getPostId(),
            'body' => [
                'content' => $event->getPostContent(),
// ...
            ]
        ]);
    }
}

Projector 的实现就是一种特殊的领域事件监听器。它与默认的领域事件监听器的主要区别在于 Projector 触发了一组领域事件而不是仅仅一个:

namespace Infrastructure\Projection;
class Projector
{
    private $projections = [];

    public function register(array $projections)
    {
        foreach ($projections as $projection) {
            $this->projections[$projection->eventType()] = $projection;
        }
    }

    public function project(array $events)
    {
        foreach ($events as $event) {
            if (isset($this->projections[get_class($event)])) {
                $this->projections[get_class($event)]
                    ->project($event);
            }
        }
    }
}

下面的代码展示了 projector 和事件间的流向:

$client = new ElasticsearchClientBuilder::create()->build();
$projector = new Projector();
$projector->register([
new Infrastructure\Projection\Elasticsearch\
PostWasCreatedProjection($client),
new Infrastructure\Projection\Elasticsearch\
PostWasPublishedProjection($client),
new Infrastructure\Projection\Elasticsearch\
PostWasCategorizedProjection($client),
new Infrastructure\Projection\Elasticsearch\
PostContentWasChangedProjection($client),
new Infrastructure\Projection\Elasticsearch\
PostTitleWasChangedProjection($client),
]);
$events = [
new PostWasCreated(/* ... */),
new PostWasPublished(/* ... */),
new PostWasCategorized(/* ... */),
new PostContentWasChanged(/* ... */),
new PostTitleWasChanged(/* ... */),
];
$projector->project($event);

这里的代码是一种同步技术,但如果需要的话也可以是异步的。你也通过在视图层放置一些警告通知来让客户知道这些不同步的数据。

对于接下来的例子,我们将结合使用 amqplib PHP 扩展和 ReactPHP:

// Connect to an AMQP broker
$cnn = new AMQPConnection();
$cnn->connect();
// Create a channel
$ch = new AMQPChannel($cnn);
// Declare a new exchange
$ex = new AMQPExchange($ch);
$ex->setName('events');
$ex->declare();

// Create an event loop
$loop = ReactEventLoopFactory::create();
// Create a producer that will send any waiting messages every half a
second
$producer = new Gos\Component\React\AMQPProducer($ex, $loop, 0.5);
$serializer = JMS\Serializer\SerializerBuilder::create()->build();
$projector = new AsyncProjector($producer, $serializer);
$events = [
    new PostWasCreated(/* ... */),
    new PostWasPublished(/* ... */),
    new PostWasCategorized(/* ... */),
    new PostContentWasChanged(/* ... */),
    new PostTitleWasChanged(/* ... */),
];
$projector->project($event);

为了能让它工作,我们需要一个异步的 projector。这有一个原生的实现如下:

namespace Infrastructure\Projection;

use Gos\Component\React\AMQPProducer;
use JMS\Serializer\Serializer;

class AsyncProjector
{
    private $producer;
    private $serializer;

    public function __construct(
        Producer $producer,
        Serializer $serializer
    )
    {
        $this->producer = $producer;
        $this->serializer = $serializer;
    }

    public function project(array $events)
    {
        foreach ($events as $event) {
            $this->producer->publish(
                $this->serializer->serialize(
                    $event, 'json'
                )
            );
        }
    }
}

在 RabbitMQ 交换机上的事件消费者如下:

// Connect to an AMQP broker
$cnn = new AMQPConnection();
$cnn->connect();
// Create a channel
$ch = new AMQPChannel($cnn);
// Create a new queue
$queue = new AMQPQueue($ch);
$queue->setName('events');
$queue->declare();
// Create an event loop
$loop = React\EventLoop\Factory::create();
$serializer = JMS\Serializer\SerializerBuilder::create()->build();
$client = new Elasticsearch\ClientBuilder::create()->build();
$projector = new Projector();
$projector->register([
    new Infrastructure\Projection\Elasticsearch\
    PostWasCreatedProjection($client),
    new Infrastructure\Projection\Elasticsearch\
    PostWasPublishedProjection($client),
    new Infrastructure\Projection\Elasticsearch\
    PostWasCategorizedProjection($client),
    new Infrastructure\Projection\Elasticsearch\
    PostContentWasChangedProjection($client),
    new Infrastructure\Projection\Elasticsearch\
    PostTitleWasChangedProjection($client),
]);
// Create a consumer
$consumer = new Gos\Component\ReactAMQP\Consumer($queue, $loop, 0.5, 10);
// Check for messages every half a second and consume up to 10 at a time.
$consumer->on(
    'consume',
    function ($envelope, $queue) use ($projector, $serializer) {
        $event = $serializer->unserialize($envelope->getBody(), 'json');
        $projector->project($event);
    }
);
$loop->run();

从现在开始,只需让所有所需的仓储使用 projector 实例,然后让它们调用投影过程就可以了:

class DoctrinePostRepository implements PostRepository
{
    private $em;
    private $projector;

    public function __construct(EntityManager $em, Projector $projector)
    {
        $this->em = $em;
        $this->projector = $projector;
    }

    public function save(Post $post)
    {
        $this->em->transactional(
            function (EntityManager $em) use ($post) {
                $em->persist($post);
                foreach ($post->recordedEvents() as $event) {
                    $em->persist($event);
                }
            }
        );
        $this->projector->project($post->recordedEvents());
    }

    public function byId(PostId $id)
    {
        return $this->em->find($id);
    }
}

Post 实例和记录事件在同一个事务中触发和持久化。这就确保没有事件丢失,只要事务成功了,我们就会把它们投影到读模型中。因此,在写模型和读模型之间不存在不一致的情况。

用 ORM 还是不用 ORM

一个非常普遍的问题就是当实现 CQRS 时,是否真正需要一个对象关系映射(ORM)。我们真的认为,写模型使用 ORM 是极好的,同时有使用工具的所有优点,这将帮助我们节省大量的工作,只要我们使用了关系型数据库。但我们不应该忘了我们仍然需要在关系型数据库中持久化和检索写模型状态。

事件源

CQRS 是一个非常强大和灵活的架构。在收集和保存领域事件(在聚合操作期间发生)这方面,它有一个额外的好处,就是给你领域中发生的事件一个高度的细节。因为领域事件描述了过去发生的事情,它对于领域的意义,使它成为战术模式的一个关键点。

小心记录太多事件

越来越多的事件是一种坏味道。在领域中记录事件也许是一种成瘾,这也最有可能被企业激励。作为一条经验法则,记住要保持简单。

通过使用 CQRS,我们可以在领域层记录所有发生的相关性事件。领域的状态可以通过重现之前记录的领域事件来呈现。我们只需要一个工具,用一致的方法来存储所有这些事件。所以我们需要储存事件。

事件源背后的基本原理是用一个线性的事件集来表现聚合的状态。

用 CQRS,我们基本上可以实现如下:Post 实体用领域事件输出他的状态,但它的持久化,可以将对象映射至数据表。
事件源则更进一步。按照之前的做法,如果我们使用数据表存储所有博客帖子的状态,那么另外一个表存储所有博客帖子评论的状态,依次类推。而使用事件源我们则只需要一张表:一个数据库中附加的单独的一张表,来存储所有领域模型中的所有聚合发布的所有的领域事件。是的,你得看清了,是单独的一张表。
按照这种模型思路,像对象关系映射的工具就不再需要了。唯一需要的工具就是一个简单的数据抽象层,通过它来附加事件:

interface EventSourcedAggregateRoot
{
    public static function reconstitute(EventStream $events);
}

class Post extends AggregateRoot implements EventSourcedAggregateRoot
{
    public static function reconstitute(EventStream $history)
    {
        $post = new static($history->getAggregateId());
        foreach ($events as $event) {
            $post->applyThat($event);
        }
        return $post;
    }
}

现在 Post 聚合有一个方法,当给定一组事件集(或者说事件流)时,可以一步步重现状态直到当前状态,这些都在保存之前。下一步将构建一个 PostRepository 适配器端口从 Post 聚合中获取所有已发布的事件,并将它们添加到数据存储区,所有的事件都存储在这里。这就是我们所说的事件存储:

class EventStorePostRepository implements PostRepository
{
    private $eventStore;
    private $projector;

    public function __construct($eventStore, $projector)
    {
        $this->eventStore = $eventStore;
        $this->projector = $projector;
    }

    public function save(Post $post)
    {
        $events = $post->recordedEvents();
        $this->eventStore->append(new EventStream(
                $post->id(),
                $events)
        );
        $post->clearEvents();
        $this->projector->project($events);
    }
}

这就是为什么 PostRepository 的实现看起来像我们使用一个事件存储来保存所有 Post 聚合发布的事件。现在我们需要一个方法,通过历史事件来重新存储一个聚合。Post 聚合实现的 reconsititute 方法,它通过事件触发来重建博客帖子状态,此刻派上用场:

class EventStorePostRepository implements PostRepository
{
    public function byId(PostId $id)
    {
        return Post::reconstitute(
            $this->eventStore->getEventsFor($id)
        );
    }
}

事件存储就像是负责关于保存和存储事件流的驮马。它的公共 API 由两个简单方法组成:它们是 appendgetEventsFrom. 前者追加一个事件流到事件存储,后者加载所有事件流来重建聚合。
我们可以通过一个键-值实现来存储所有事件:

class EventStore
{
    private $redis;
    private $serializer;

    public function __construct($redis, $serializer)
    {
        $this->redis = $redis;
        $this->serializer = $serializer;
    }

    public function append(EventStream $eventstream)
    {
        foreach ($eventstream as $event) {
            $data = $this->serializer->serialize(
                $event, 'json'
            );
            $date = (new DateTimeImmutable())->format('YmdHis');
            $this->redis->rpush(
                'events:' . $event->getAggregateId(),
                $this->serializer->serialize([
                    'type' => get_class($event),
                    'created_on' => $date,
                    'data' => $data
                ], 'json')
            );
        }
    }

    public function getEventsFor($id)
    {
        $serializedEvents = $this->redis->lrange('events:' . $id, 0, -1);
        $eventStream = [];
        foreach ($serializedEvents as $serializedEvent) {
            $eventData = $this->serializerdeserialize(
                $serializedEvent,
                'array',
                'json'
            );
            $eventStream[] = $this->serializer->deserialize(
                $eventData['data'],
                $eventData['type'],
                'json'
            );
        }
        return new EventStream($id, $eventStream);
    }
}

这里的事件存储的实现是基于 Redis,一个广泛使用的键-值存储器。追加在列表里的事件使用一个 event 前缀:除此之外,在持久化这些事件之前,我们提取一些像类名或者创建时间之类的元数据,这些在之后会派上用场。

显然,就性能而言,聚合总是通过重现它的历史事件来达到最终状态是非常奢侈的。尤其是当事件流有成百上千个事件。克服这种局面最好的办法就是从聚合中拍摄一个快照,只重现快照拍摄后发生的事件。快照就是聚合状态在给定时刻的一个简单的序列化版本。它可以基于聚合的事件流的事件序号,或者基于时间。第一种方法,每 N 次事件触发时就要拍摄一次快照(例如每20,50,或者200次)。第二种方法,每 N 秒就要拍摄一次。

在下面的例子中,我们使用第一种方法。在事件的元数据中,我们添加一个附加字段,版本(version),即从我们开始重现聚合历史状态之处:

class SnapshotRepository
{
    public function byId($id)
    {
        $key = 'snapshots:' . $id;
        $metadata = $this->serializer->unserialize(
            $this->redis->get($key)
        );
        if (null === $metadata) {
            return;
        }
        return new Snapshot(
            $metadata['version'],
            $this->serializer->unserialize(
                $metadata['snapshot']['data'],
                $metadata['snapshot']['type'],
                'json'
            )
        );
    }

    public function save($id, Snapshot $snapshot)
    {
        $key = 'snapshots:' . $id;
        $aggregate = $snapshot->aggregate();
        $snapshot = [
            'version' => $snapshot->version(),
            'snapshot' => [
                'type' => get_class($aggregate),
                'data' => $this->serializer->serialize(
                    $aggregate, 'json'
                )
            ]
        ];
        $this->redis->set($key, $snapshot);
    }
}

现在我们需要重构 EventStore 类,来让它使用 SnapshotRepository 在可接受的次数内加载聚合:

class EventStorePostRepository implements PostRepository
{
    public function byId(PostId $id)
    {
        $snapshot = $this->snapshotRepository->byId($id);
        if (null === $snapshot) {
            return Post::reconstitute(
                $this->eventStore->getEventsFrom($id)
            );
        }
        $post = $snapshot->aggregate();
        $post->replay(
            $this->eventStore->fromVersion($id, $snapshot->version())
        );
        return $post;
    }
}

我们只需要定期拍摄聚合快照。我们可以同步或者异步地通过监视事件存储进程来实现。下面的代码例子简单地演示了聚合快照的实现:

class EventStorePostRepository implements PostRepository
{
    public function save(Post $post)
    {
        $id = $post->id();
        $events = $post->recordedEvents();
        $post->clearEvents();
        $this->eventStore->append(new EventStream($id, $events));
        $countOfEvents = $this->eventStore->countEventsFor($id);
        $version = $countOfEvents / 100;
        if (!$this->snapshotRepository->has($post->id(), $version)) {
            $this->snapshotRepository->save(
                $id,
                new Snapshot(
                    $post, $version
                )
            );
        }
        $this->projector->project($events);
    }
}
是否需要 ORM?
从这种架构风格的用例中明显可知,仅仅使用 ORM 来持久/读取 使用未免太过度了。就算我们使用关系型数据库来存储它们,我们也仅仅只是从事件存储中持久/读取事件而已。

小结

在这一章,因为有大量可选的架构风格,你可能会感到一点困惑。为了做出明显的选择,你不得不在它们中考虑和权衡。不过一件事是明确的:大泥球是不可取的,因为代码很快就会变质。分层架构是一个更好的选择,但它也带来一些缺点,例如层与层之间的紧耦合。可以说,最合适的选择就是六边形架构,因为它可以作为一个基础的架构来使用,它能促进高层次的解耦并且带来内外应用间的对称性,这就是为什么我们在大多数场景下推荐使用它。

我们还可以看到 CQRS 和事件源这些相对灵活的架构,可以帮助你应对严重的复杂性。CQRS 和事件源都有它们的场景,但不要让它的魅力因素分散你判断它们本身提供的价值。由于它们都存在一些开销,你应该有技术原因来证明你必须得使用它。这些架构风格确实有用,在大量的 CQRS 仓储查找方法中,和事件源事件触发量上,你可以很快受到这些风格的启发。如果查找方法的数量开始增长,仓储层开始变得难以维护,那么是时候开始考虑使用 CQRS 来分离读写关注了。之后,如果每个聚合操作的事件量趋向于增长,业务也对更细粒度的信息感兴趣,那么一个选项就该考虑,转向事件源是否能够获得回报。

摘自 Brian Foote 和 Joseph Yoder 的一篇论文:

大泥球就是杂乱无章的,散乱泥泞的,牵连交织的意大利式面条代码。

查看原文

赞 55 收藏 29 评论 3

认证与成就

  • 获得 35 次点赞
  • 获得 31 枚徽章 获得 0 枚金徽章, 获得 6 枚银徽章, 获得 25 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-11-29
个人主页被 2.6k 人浏览