Maksim - 实战
type: channel
link: https://www.maksim.website/category/%E5%AE%9E%E6%88%98/
items: PHP进化史：PHP8 Jit 来了
items: PHP进化史：PHP7 鸟哥 YYDS
items: 【Swoole源码研究】深入理解Swoole协程实现
items: 漫谈Swoole协程与异步IO
items: 为高性能优化 PHP-FPM
items: Curl 请求引发一场血案：利用 Redis 实现熔断器
items: PHP 利用 Redis Sorted Set的延时队列
items: PHP 利用 Redis List 数据结构实现消息队列
items: 变种的数据中心：实现数据的组合返回
items: 工厂模式的故事：程序员、书和狗

PHP进化史：PHP8 Jit 来了
type: item
date: 2023-03-22T13:21:00+00:00
description: 其实在 PHPng 的项目中就已经开始实现 jit 了，不过由于 PHP 的底层实现问题，使用 Jit 带来的收益并不大，于是就放弃了。

PHP进化史：PHP7 鸟哥 YYDS
type: item
date: 2023-03-22T13:20:00+00:00
description: PHP7 最受关注的一点就是性能上面的提升。还没开始写，打算用鸟哥的博客作为大纲，然后再写一些变更的细节。大纲：https://www.laruence.com/?s=PHP7

【Swoole源码研究】深入理解Swoole协程实现
type: item
date: 2020-10-23T15:49:00+00:00
description: 这篇文章转载自好未来学而思技术团队，分享老师：李乐,本文基于Swoole-4.3.2和PHP-7.1.0版本Swoole协程简介Swoole4为PHP语言提供了强大的CSP协程编程模式，用户可以通过go函数创建一个协程，以达到并发执行的效果，如下面代码所示：//Co::sleep() 是 Swoole 提供的 API，并不会阻塞当前进程，只会阻塞协程触发协程切换 go(function() { Co::sleep(1); echo "a"; }); go(function() { Co::sleep(2); echo "b"; }); echo "c"; //输出结果:cab //程序总执行时间 2 秒其实在Swoole4之前就实现了多协程编程模式，在协程创建、切换以及结束的时候，相应的操作php栈即可（创建、切换以及回收php栈）。此时的协程实现无法完美的支持php语法，其根本原因在于没有保存c栈信息。（vm内部或者某些扩展提供的API是通过c函数实现的，调用这些函数时如果发生协程切换，c栈该如何处理？）Swoole4新增了c栈的管理，在协程创建、切换以及结束的同时会伴随着c栈的创建、切换以及回收。Swoole4协程实现方案如下图所示：其中：API层是提供给用户使用的协程相关函数，比如go()函数用于创建协程；Co::yield()使得当前协程让出CPU；Co::resume()可恢复某个协程执行；Swoole4协程需要同时管理c栈与php栈，Coroutine用于管理c栈，PHPCoroutine用于管理php栈；其中Coroutine()，yield()，resume()实现了c栈的创建以及换入换出；create_func()，on_yield()，on_resume()实现了php栈的创建以及换入换出；Swoole4在管理c栈时，用到了boost.context库，make_fcontext()和jump_fcontext()函数均使用汇编语言编写，实现了c栈上下文的创建以及切换；Swoole4对boost.context进行了简单封装，即Context层，Context()，SwapIn()以及SwapOut()对应c栈的创建以及换入换出。深入理解C栈函数是对代码的封装，对外暴露的只是一组指定的参数和一个可选的返回值；假设函数P调用函数Q，Q执行后返回函数P，实现该函数调用需要考虑以下三点：指令跳转：进入函数Q的时候，程序计数器必须被设置为Q的代码的起始地址；在返回时，程序计数器需要设置为P中调用Q后面那条指令的地址；数据传递：P能够向Q提供一个或多个参数，Q能够向P返回一个值；内存分配与释放：Q开始执行时，可能需要为局部变量分配内存空间，而在返回前，又需要释放这些内存空间；大多数语言的函数调用都采用了栈结构实现，函数的调用与返回即对应的是一系列的入栈与出栈操作，我们通常称之为函数栈帧（stack frame）。示意图如下：上面提到的程序计数器即寄存器%rip，另外还有两个寄存器需要重点关注：%rbp指向栈帧底部，%rsp指向栈帧顶部。下面将通过具体的代码事例，为读者讲解函数栈帧。c代码与汇编代码如下：分析汇编代码：main函数与add函数入口，首先将寄存器%rbp压入栈中用于保存其值，其次移动%rbp指向当前栈顶部（此时%rbp，%rsp都指向栈顶，开始新的函数栈帧）；main函数"subq $16, %rsp"，是为main函数栈帧预留16个字节的内存空间；调用add函数时，第一个参数和第二个参数分别保存在寄存器%edi和%esi，返回值保存在寄存器%eax；call指令用于函数调用，实现了两个功能：寄存器%rip压入栈中，跳转到新的代码位置；ret指令用于函数返回，弹出栈顶内容到寄存器%rip，依次实现代码跳转；leave指令等同于两条指令：movq %rsp,%rbp和popq %rbp，用于释放main函数栈帧，恢复前一个函数栈帧；注意add函数栈帧，并没有为其预留空间，寄存器%rsp和%rbp都指向栈帧底部；根本原因是add函数没有调用其他函数。该程序的栈结构示意图如下：问题：观察上面的汇编代码，输入参数分别使用的是寄存器%edi和%esi，返回值使用的是寄存器%eax，输入输出参数不应该保存在栈上吗？寄存器比内存访问要快的多，现代处理器寄存器数目也比较多，因此倾向于将参数优先保存在寄存器。比如%rdi, %rsi, %rdx, %rcx, %r8d, %r9d 六个寄存器用于存储函数调用时的前6个参数，那么当输入参数数目超过6个时，如何处理？这些输入参数只能存储在栈上了。（%rdi等表示64位寄存器，%edi等表示32位寄存器）Swoole C栈管理通过学习c栈基本知识，我们知道最主要有三个寄存器：%r…

漫谈Swoole协程与异步IO
type: item
date: 2020-10-23T14:10:00+00:00
description: 本文转载于学而思网校技术团队，2020.10月我加入了好未来励步事业部，我们事业部开始大面积应用 swoole，所以对 swoole 进行了深入学习。本文作者：陈曹奇昊初次接触Swoole的PHP开发者多少都会有点雾里看花的感觉，看不清本质。一部分PHP开发者并不清楚Swoole是什么，只是觉得很牛掰就想用了，这种行为无异于写作文的时候总想堆砌一些华丽的辞藻或是引经据典来提升文章逼格，却背离了文章的主题，本末倒置，每一种技术的诞生都有它的原因，异步或是协程不是万能的银弹，你需要它的时候再去用它，而不是想用它而用它，毕竟编程世界的惯性是巨大的，这天下还是同步阻塞的天下。还有一部分开发者是对Swoole有了一些自己的见解，但对错参半，写出来的程序能跑，甚至也能上生产，但不是最优的，其中大部分问题都源于开发者无法将惯有的思维方式灵活转变。协程首先协程的最简定义是用户态线程，它不由操作系统而是由用户创建，跑在单个线程（核心）上，比进程或是线程都更加轻量化，通常创建它只有内存消耗：假如你的配置允许你开几千个进程或线程，那么开几万个几十万个协程也是很轻松的事情，只要内存足够大，你可以几乎无止境地创建新的协程。在Swoole下，协程的切换实现是依靠双栈切换，即C栈和PHP栈同时切换，由于有栈协程的上下文总是足够的小，且在用户态便能完成切换，它的切换速度也总是远快于进程、线程，一般只需要纳秒级的CPU时间，对于实际运行的逻辑代码来说这点开销总是可以忽略不计（尤其是在一个重IO的程序中，通过调用分析可以发现协程切换所占的CPU时间非常之低）。对于Swoole这样的有栈协程，你完全可以简单地将其看做是一个栈切换器，你可以在运行的子程序中随意切换到另一个子程序，底层会保存好被切走的协程的执行位置，回来时可以从原先的位置继续往下运行。Swoole多进程模型下的进程、线程、协程关系图但这篇文章我们要谈的并不只是单单「协程」这一个概念，还隐含了关于异步网络IO一系列的东西，光有协程是什么也做不了的，因为Swoole的协程永远运行在一个线程中，想用它做并行计算是不可能的，运行速度只会因为创建开销而更慢，没有异步网络IO支持，你只能在不同协程间切来切去玩。实际上PHP早就实现了协程，yield关键字就是允许你从一个函数中让出执行权，需要的时候能重新回到让出的位置继续往下执行，但它没有流行起来也有多种原因，一个是它的传染性，每一层调用都需要加关键字，另一个就是PHP没有高效可靠的异步IO支持，让其食之无味。异步注：本文中提到的异步IO并非全为严格定义上的异步IO，更多的是日常化的表达简单了解了协程，再让我们来理解一下什么是异步IO吧。严格来说，在Unix下我们常说的异步并不是真异步，而是同步非阻塞，但是其效果和异步非常相近，所以我们日常中还是以异步相称。同步非阻塞和真异步区别在于：真异步是你提交读写请求后直接检查读写是否已完成即可，所以在Win下这样的技术被叫做「完成端口」，而同步非阻塞仅是操作不会长时间地陷入内核，但你需要在检查到可读或可写后，调用API同步地去拷贝数据，这会不可避免地陷入内核态，但read/write通常并不会阻塞太多的时间，从宏观上整个程序仍可以看作是全异步的。 阻塞非阻塞同步read, write(read, write) + poll/ select / epoll / kqueue异步-aio_read, aio_write, IOCP(windows)在实际使用中，「伪异步」的Reactor模型并不比Windows下IOCP的Proactor逊色，并且我更喜欢Reactor的可控性，当然为了追求极致的性能和解决网络和文件异步IO统一的问题，未来Linux的io_uring可能会成为新的趋势。Reactor运行流程简图我们可以通过上面的图片简单理解Reactor模型的运行流程，所谓的「异步」不过是多路复用带来的观感效果，你的程序不会阻塞在一个IO上，而是在无事可干的时候再阻塞在一堆IO上，即IO操作不在你需要CPU的时候阻塞你，你就不会感受到IO阻塞的存在。结合现实情景来说，以前你要买饭（IO操作），你得下楼去买，还得排队等饭店大厨做完才能取回家吃（IO阻塞），到了下一餐，你又得重复之前的操作，很是麻烦，而且越是繁忙的时候等的时间越长（慢速IO），你觉得一天到晚净排队了，极大地浪费了你写代码的时间（CPU时间）。现在有了外卖，你直接下单（异步请求）就可以继续专心写代码（非阻塞），你还可以一次定三份饭（多路IO），饭到了骑手打电话让你下楼取（事件触发），前后只花了不到几分钟（同步读写，如果是Proactor连取餐都省了，直接给你送上楼），周六晚上的九点，你终于合上电脑，觉得充实极了，因为你几乎一整周都在写代码（CPU利用率高）。协程+…

为高性能优化 PHP-FPM
type: item
date: 2020-04-20T05:00:00+00:00
description: PHP 是无处不在的，可以说是互联网 Web 应用上使用最广泛的语言。然而，它的高性能并不为人所知，尤其是在涉及到高并发系统时。这就是为什么对于这样特殊的用例，正在被 Node (是的，我知道，它不是一种语言)、Go 和 Elixir 等语言接管。也就是说，您可以做很多事情来改进服务器上的 PHP 性能。本文主要关注 php-fpm 方面的内容，如果您使用 Nginx，这是在服务器上的默认配置。如果你知道 php-fpm 是什么，请直接跳到优化部分。什么是 php-fpm?许多开发人员对 DevOps 方面的知识不太感兴趣，即使是那些对此感兴趣的开发人员，也极少有人知道它的底层原理。有趣的是，当浏览器发送一个请求到运行 PHP 的服务器上时，PHP 也不是最先进行处理请求的服务；而是，HTTP 服务器，Apache 和 Nginx 是其中最主要的两个。「web 服务器」决定如何与 PHP 进行通信，然后传递请求的类型，数据和头部信息到 PHP 进程。上图是 PHP 项目的请求 - 响应生命周期（图片来源: ProinerTech）在现代 PHP 应用中，「find file」部分即为 index.php 文件，它是在服务器配置文件中配置的用于处理所有请求的代理。如今，Web 服务器究竟如何连接 PHP 正在进化，如果我们要深入研究所有细节，这篇文章的长度将激增。但粗略来说，在 Apache 作为 Web 服务器首选的时间段，PHP 是作为包含在服务器内部的模块。所以每当一个请求被接收，服务器将开启一个新的进程，它将自动包含 PHP 和执行请求。这个方法被称作 mod_php，“PHP 作为一个模块” 的缩写。这种方法有其局限性，而 Nginx 和 php-fpm 克服了它。在 php-fpm 中，管理 PHP 的责任在于服务器内部的 PHP 程序。换言之，Web 服务器 (Nginx, 在本例中), 不在乎 PHP 在哪和怎样运行的，只要它知道如何发送和接收数据即可。如果需要，在这种情况下，您可以将 PHP 视为另一台服务器，它管理传入请求的某些子 PHP 进程（因此，我们将请求送到服务器，该请求由服务器接收并传递到服务器 — — 太疯狂了！:-P)。如果你用过 Nginx, 你会看到这些代码：location ~ \.php$ { try_files $uri =404; fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_pass unix:/run/php/php7.2-fpm.sock; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; }对于这一行：fastcgi_pass unix:/run/php/php7.2-fpm.sock;，它告诉 Nginx 通过 php7.2-fpm.sock 的 socket 与 php 进程通信。因此，对于每个传入的请求，Nginx 都通过这个文件写入数据，在接收到输出后，将其发送回浏览器。我必须再次强调，对于如何运行这不是最完整或者最准确的，但对于大多数 DevOps 任务是完全准确的。除此之外，让我们回顾一下到目前为止所学到的东西：PHP 不会直接接收浏览器发送的请求。像 Nginx 这种 Web 服务器首先会拦截它。Web 服务器知道如何连接到 PHP 进程，并将所有请求数据（粘贴所有内容）传递到 PHP 上。PHP 完成其职责后，会将响应发送回 Web 服务器，然后将其发送回客户端（在大多数情况下为浏览器）。流程图如下：PHP 和 Nginx 如何协同工作？ (图片来源：数据狗)到目前为止都不错，那么关键问题来了：PHP-FPM 到底是什么呢？PHP 中的 FPM 代表 「快速进程管理器」, 花式解释就是说，在服务器上运行的 PHP 并不是单个进程，而是由这个 FPM 进程管理器派生、控制和终止的一些 PHP 进程。web 服务器将请求传递给的就是这个进程管理器。PHP-FPM 本身就是一个完整的兔子洞，所以如果您愿意，可以随意探索，但是对于我们的目的，这些解释就足够啦。 ?为什么要优化 php-fpm?一般在正常运行的情况下，为什么要考虑优化呢？ 为什么不将事物保持原样。具有讽刺意味的是，一般我为大多数用例提供建议的话。 如果您的设置运行良好，并且没有特殊用例，请使用默认设置。 但是，如果您希望扩展一台机器之外的能力，那么从一台机器中挤出最大的处理能力是必不可少的，因为它可以将您服务器的花费减少一半（甚至更多！）。要说明的另一件事情是，Nginx …

Curl 请求引发一场血案：利用 Redis 实现熔断器
type: item
date: 2019-12-30T12:30:00+00:00
description: 在前段时间，我负责的推送中台出现了一个非常严重的 Bug，差点造成线上服务宕机。我简单来描述一下当时的场景。我们使用个推来来推送我们的 APP 通知，以及用户画像，用户分析等等。每一次在 app 启动都会访问一个接口来判断用户信息，然后绑定标签，这个动作会触发 curl 函数访问个推的 API。结果悲剧了，个推北京机房宕机。我们当时的主要技术栈是 PHP，我们线上 curl 请求默认超时时间是 60 秒，这就导致我们的数据库链接近乎被打满。由于 PHP-FPM 在脚本执行完成后才会释放进程，这也包括其中的资源，curl 请求的默认时间是 60 秒，这样也就意味着在 60 秒内，该进程也会被一直阻塞，直到超时为止，而 PDO 的生命周期是跟着 FPM 走的，除非调用手动 close() 这样才会关闭。MySQL 连接断开也是有默认超时时间的，如果在超时时间范围内，客户端没有主动关闭，该连接会一直存在，但是 MySQL 的连接数是有限的，如果连接被打满那么新的连接将无法建，整个服务 GG 。如果这个时候再有流量进来，那么就只有宕机一条死路了，于是立即找到运维修改默认配置，然后重启 php-fpm 释放 MYSQL 连接。处理方式以及断路器经过这一次的线上事故，总结出了一点，对于任何第三方提供的接口，都不要完全信赖！，都要去考虑如果对方出现问题，我这里应该如何操作！经过复盘我们当时存在以下问题：环境配置不透明，开发基本不关注线上配置。没有考虑第三方失败该如何进行处理。架构设计不完善，此类功能完全也可以异步化。第一点是由于工作经验不足或者缺乏沟通导致的，很好解决，当这个事情解决之后，团队对线上的配置情况和运维一起进行了一次梳理。第二点，我们选择了熔断器，首先说一下什么是熔断器。第三点，对业务进行了梳理，将强依赖接口改成了消息队列进行异步处理。图 1.1 熔断器如果你看过自己家中的电闸开关的话就会看到上图这个东西， 当电流正常情况下熔断器是关闭状态电流就可以正常通过，如果电流负载过大为了保护家用电器就会开启，然后关闭电源保证电器安全。在我们的代码中熔断器其实就是一个设计模式，我们可以通过很简单的代码就可以实现熔断器。图 1.2 无熔断器流程图图 1.2 中是在没有熔断器我们在开发这类业务的时候，可能会对第三方服务进行 try catch 来判断异常，如果有异常在反复几次调用，多次尝试失败后，可能就直接返回报错，提供有损的服务，像上面我提到的那场事故中这个问题就会被放大，因为 60 秒后才会触发超时，也就意味着，如果重试三次那么该接口的耗时就会增加到 3 分钟，正常情况下是不会有业务接口能够接收三分钟这样耗时的。当我们加上熔断器后，那么流程就会变成图1.3 那样。图 1.3引入断路器后，不再是 app server 直接访问第三方服务，而是将访问委托给 fuse 熔断器来进行，断路器判断当前的开闭状态来判断是否要发起请求。如果是处于关闭状态，也就是第三方服务可以正常访问，那就直接访问第三方服务，如果熔断器处于开启状态，那么正面目前第三方服务不可用，则直接调用备用函数，这也是微服务中一个重要的概念——服务降级。利用 Redis Sorted Set 实现简单的熔断器由于我们使用的 PHP-FPM，这也就导致我们的 PHP 代码是无法常驻内存的，这个时候，就需要使用 Redis 来进行记数，在数据结构方面我们选择使用有序集合。keyscoremembercircuit10pushMessage2Usercircuit1pushMessage2Listscore 用来存放失败的次数，member 用来存放失败的函数。搭建骨架我们先来按照上一小节的内容先将熔断器的挂架代码打起来。<?php class CircuitBreaker { private $zSetKey = 'circuit'; public function invoke(object $class, string $method, array $params, callable $fallback) { try { return $class->$method(...$params); } catch (Throwable $exception) { return $fallback(); //函数降级 } } }CircuitBreaker 就是我们的熔断器类。其中设计了一个 invoke 方法，这个方法就是上面讲到的代理请求，我们来通过 invoke 方法来让熔断器代理我们的请求。$class: 我们首先要将类实例化后传入到 invoke中，这样的好处是进行依赖翻转，让熔断器可以通用。$method: 是该类需要执行的方法。$params: 是执行该方法时候需要传入的参数。…

PHP 利用 Redis Sorted Set的延时队列
type: item
date: 2019-08-22T11:30:00+00:00
description: 延迟队列是我们在业务中经常遇到的场景，例如订单过期，定时发布，定时推送等等。在 Zset里面，每一个成员都有一个所谓的分数：score，把当前时间作为分数，因为 Zset 是有序的，时间越小的排名越靠前。所以使用Zset作为延时队列就充分利用了score。 我们还是用订单例子来距离，如果订单超过 24 小时未付款，那么久取消订单，这是在电商业务中最常见的处理模式。那么在 redis 中的数据个是就如图 1.1。图 1.1而程序执行流程如下图用户下单后将订单信息插入到数据库中将数据插入到 redis 中，结构如图 1.1启动死循环，定时从 zset 中获取接近当前时间戳的值，然后逻辑判断是否更新数据。接下来我们回顾一下 zset相关的命令:ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]注意排序是按照 score 从小到大排序的，这也是我们想要的，其中 min 和 max 可以是 -inf 和 +inf。-inf 分数的最低值+inf 分数的最高值ZRANGEBYSCORE order:delay_queue -inf 5000 WINTHSCORES这代表你不知道最低值是多少，取值范围是 <= 5000ZRANGEBYSCORE order:delay_queue -inf current_timestamp WINTHSCORES limit 0 10代表取 10 条最小等于当前时间的所有内容。我们还是使用命令行脚本来模拟入队操作。<?php $orderId = $argv[1] ?? ''; if (empty($orderId)) { exit("请填写要压入队列的数据"); } $redis = new Redis(); $redis->connect('127.0.0.1', 6379); $redis->auth('123456'); $redis->select(0); $queueName = 'order:delay_queue'; $redis->zAdd($queueName, time(), $orderId);执行代码，向队列中插入四条数据。# 执行入队操作 ➜ php zset_client.php O20190101 ➜ php zset_client.php O20190102 ➜ php zset_client.php O20190103 ➜ php zset_client.php O20190104 # redis 查看数据 127.0.0.1:6379> zrange order:delay_queue 0 -1 withscores 1) "O20190101" 2) "1667213048" 3) "O20190102" 4) "1667213049" 5) "O20190103" 6) "1667213050" 7) "O20190104" 8) "1667213051"接下来我们来实现消费端的代码：<?php $redis = new Redis(); $redis->connect('127.0.0.1', 6379); $redis->auth('123456'); $redis->select(0); $queueName = 'order:delay_queue'; while (true) { $orderIds = $redis->zRangeByScore($queueName, "-inf", time(), ['limit' => [0, 10]]); $ids = implode(',', $orderIds); if (!empty($ids)) { //模拟执行 SQL 语句，4 代表已经取消 $sql = "update `orders` set `status` = 4 where in ({$ids})"; echo "$sql" . PHP_EOL; //执行其他业务逻辑，例如推送通知等操作 } $redis->zRem($queueName, ...$orderIds); usleep(500 * 1000); }这里需要注意我们在更新数据库的时候不要一条一条的更新，同时这些代码只是用来演示基本原理，还有好多需要处理的内容。处理程序的退出，监听信号尽量使用异步方法处理业务逻辑，可以尝试使用 swoole，workman 带有多进程管理的框架或者已经封装好的类库。

PHP 利用 Redis List 数据结构实现消息队列
type: item
date: 2019-08-20T11:30:00+00:00
description: Redis 本身是用来做缓存的，但是其中有一些特性是我们可以用来完成消息队列的功能，比如如果能够容忍数据的丢失，并且持久化方面要求不是很高的场景下完成任务的分布式处理。队列的基本实现Redis列表是简单的字符串列表，按照插入顺序排序。你可以添加一个元素到列表的头部（左边）或者尾部（右边），而我们取出的时候可以从头部或者尾部来获取数据。我们可以先头lpush 插入数据，然后再rpop来取数据，来模拟队列的先进先出。$ redis-cli -h 127.0.0.1 127.0.0.1:6379> auth 123456 OK 127.0.0.1:6379> select 0 OK 127.0.0.1:6379> lpush order:queue SN20190801 (integer) 1 127.0.0.1:6379> lpush order:queue SN20190802 (integer) 2 127.0.0.1:6379> lpush order:queue SN20190803 (integer) 3 127.0.0.1:6379> rpop order:queue "SN20190801" 127.0.0.1:6379> rpop order:queue "SN20190802" 127.0.0.1:6379> rpop order:queue "SN20190803" 127.0.0.1:6379> rpop order:queue (nil)如果想要批量处理，我们可以使用 LRANGE 命令：127.0.0.1:6379> lpush order:queue SN20190801 (integer) 1 127.0.0.1:6379> lpush order:queue SN20190802 (integer) 2 127.0.0.1:6379> lpush order:queue SN20190803 (integer) 3 127.0.0.1:6379> lrange order:queue 0 10 1) "SN20190803" 2) "SN20190802" 3) "SN20190801"基于这三个命令，我们就可以实现一个异步的消息队列了，我们首先来模拟消息队列的生产，最终我们的消费模型就如图 1.1。图 1.1 消费模型<?php // 接收参数 $orderId = $argv[1] ?? ''; if (empty($orderId)) { exit("请填写要压入队列的数据"); } // 新建 redis 连接 $redis = new Redis(); $redis->connect('127.0.0.1', 6379); $redis->auth('123456'); $redis->select(1); try { echo $redis->lPush('order:queue', $orderId) . PHP_EOL; } catch (Exception $exception) { //捕获异常，出现异常情况应该进行处理重传或者报警等等 echo $exception->getMessage() . PHP_EOL; } // 释放资源 $redis->close();这段代码非常简单，通过 $argv 来获取参数，也就是 order_id，通知消费端，该 id 有数据变更，要进行一些异步操作。处理端我们使用 brPop 来阻塞获进行消费就可以了。<?php $redis = new Redis(); $redis->connect('127.0.0.1', 6379); $redis->auth('123456'); $redis->select(1); while (true) { //阻塞获取,10 秒后没有获取到证明超时 $result = $redis->brPop("order:queue", 10); //$result 包含两个返回值 //$result[0] redis key 的名称 order:queue //$result[1] redis value 也就是我们压入队列的数据 if ($result && $result[0]) { //业务处理逻辑 echo "接收到订单号为 [" . $result[1] . "] 的订单，开始执行业务逻辑"; usleep(500 * 1000); // 休眠500 毫秒，让出 CPU 时间片 } else { continue; } }首先是使用 while 死循环来模拟一直在消费， brPop 设置一个十秒的阻塞，如果阻塞过了十秒就放弃获取，执行下一次逻辑，在处理完成后，我们要 usleep 500 毫秒，让出…

变种的数据中心：实现数据的组合返回
type: item
date: 2017-12-08T11:30:00+00:00
description: 网上有很多数据模式的文章，这里不会按照已经成型的书和文章来进行编写，这样没有任何意义，在应用设计模式的时候一定要根据自己业务和使用的语言来进行编写。在工厂模式中，我们基于商城的案例编写了一个书、狗和酒的工厂案例，在线我们对需求进行一次迭代，老板决定每个商品的 getList 方法都要额外加上狗的信息（狗本身除外）。当我们接到这个需求的时候，首先第一个想法肯定是这样实现：include 'autoload.php'; //$object = new Books(); $book = ProductFactory::getProduct('book'); $bookList = $book->getList(); $dog = ProductFactory::getProduct('dog'); $dogList = $dog->getList();接下来，我们利用设计模式来设计一下，不过首先我们需要解决一个问题，目前的代码虽然都有 getList 但是却并不是一个硬性的规范，如果现在商城增加品类了，例如增加了一个 Phone 交给了一个新的开发人员去开发，获取列表的方法它给写成了 getPhones 那么我们对应的 ProductFactory 在实现上可能就会存在很大的麻烦。对于约束，我们可以先定义个接口：<?php interface IProduct { public function getList(); } 这样无论以后我们新增什么品类都使用这个方法去进行创建,我们目前已有的三个类都去 implements ，接下来我们对工厂模式进行一些改进。<?php class ProductFactory { public static function getProduct($type) { switch ($type) { case "book" : $obj = new Books(); break; case "dog" : $obj = new Dogs(); break; case "wine" : $obj = new Wines(); break; default: $obj = null; } if (is_subclass_of($obj, "IProduct")) { return $obj; } return null; } }我们在最后增加了 is_subclass_of 判断，该函数可以判断当前的变量是否是某一个类、抽象类、接口的子类，这样可以让我们的工厂更加健全，避免返回的对象不满足 IProduct 的约束，可以减少运行时的报错，因为你无法保证其他人修改工厂方法的时候放进来的类到底满不满足我们的业务要求。如果要实现我们现在的需求，需要用到注册树模式，我们在这里叫它数据中你想你模式，我们把数据全部都放到数据中心中进行统一的管理，取数据的时候也从数据中心取，坚决不合类本身私自交往。<?php class ProductDataCenter { public static $objectList = []; public static function set($k, $v) { self::$objectList[$k] = $v; } public static function get($key) { return self::$objectList[$key] ?? []; } }ProductDataCenter 的职责非常简单，就是存储我们的产品列表,对外提供两个方法：set 存值接下来我们继续改造工厂方法，工厂不再继续返回值，而是向 ProductDataCenter 中增加数据。<?php class ProductFactory { public static function getProduct($type) { switch ($type) { case "book" : $obj = new Books(); break; case "dog" : $obj = new Dogs(); break; case "wine" : $obj = new Wines(); break; default: $obj = null; } if (is_subclass_of($obj, "IProduct")) { // 把创建的对象放到数据中心 ProductDataCenter::set($type, $obj); } } }这样一来，我们在外面调用的时候就变成了这个样子：<?php include 'autoload.php'; //$object = new Books(); ProductFactory::getProduct('book'); $data = ProductD…

工厂模式的故事：程序员、书和狗
type: item
date: 2017-12-05T05:20:00+00:00
description: 在正式开始之前，我们先来编写一个自动加载功能，这样我们就不用老是 include 文件了。<?php define('ROOT_PATH', str_replace('\\', '/', realpath(dirname(__FILE__) . '/')) . "/"); function autoload($className) { $classPath = ROOT_PATH . 'src' . DIRECTORY_SEPARATOR . $className . '.class.php'; include $classPath; } spl_autoload_register('autoload');首先我们声明一个 ROOT_PATH，用来帮助我们定位当前目录autoload 函数，用来自动文件的函数，其中定义了加载规则，我们统一将代码放到 src 目录下，每个类文件的后缀为 .class.php通过 spl_autoload_register 来做自动加载注册，当 php 运行后如果发现当前没有要用得类就会触发 autoload 函数进行文件加载。我们的项目目录最终结构如下：├── autoload.php ├── index.php └── src └── xxx.class.php好了我们回到正题。很多人觉着设计模式在实际代码中离自己很远，希望看完这个系列的文章后，能够利用设计模式改善自己的代码。最开始我们来学最简单的工厂模式，网上有很多关于工厂模式的文章，在这里，我们结合 PHP7 来进一步的演化和演进，我们写设计模式不能按照网上的通用代码，一定要结合语言和项目才能写好，很多同学在网上看到了 Java 的工厂模式，就和 Java 写的一模一样，那为什么不直接使用 Java 而使用 PHP 呢？我们来看一个非典型的电商系统，刚起步的时候往往只有 1-2 个商品，例如只有图书，此时对的协作模式很简单，开发人员可能只有一个，编写的代码可能就是下面这个样子。$object = new Books();在 Book 中包含：图书信息的维护图书点击量图书架构图书的订单如果这种规模确实这么做就可以了，随着项目越来越大，我们的库中会增加的一些其他商品除了图书还增加了酒喝狗，开发人员也越来越多。# index.php # 程序员 A 负责 Book 的维护迭代 $obj = new Books(); # 程序员 B 负责 Dog 的维护迭代 $obj = new Dogs(); # 程序员 C 负责 Wine 的维护迭代 $obj = new Wines();我们大哥比方，假设每个类都有一个获取商品列表的方法：getList();Books 的代码实现:<?php class Books { public function getList() { return [ ['product_id' => 101, "product_name" => "java 从入门到精通"], ['product_id' => 102, "product_name" => "PHP 从入门到精通"], ]; } }Dogs 的代码实现<?php class Dogs { public function getList() { return [ ['product_id' => 201, "product_name" => "泰迪"], ['product_id' => 202, "product_name" => "金毛"], ]; } }Wines的代码实现<?php class Wines { public function getList() { return [ ['product_id' => 301, "product_name" => "红酒"], ['product_id' => 302, "product_name" => "白酒"], ]; } }正常业务代码中，getList 中的应该从数据库中取，这里为了演示方便直接返回数组。如果我们做这个开发，这三个商品都应该在库中标记为商品，不过类型不同，很可能呈现的展现形式和业务逻辑是不一样的，所以需要区分成不同的商品。这么写有没有什么问题？如果我们把书和狗看成两个独立的子频道，开发起来是没有任何问题的。但是假设现在有一天图书的货源出问题了，老板临时决定范式访问书的数，统一返回狗的数据。这个时候程序员可能这么做：<?php include 'autoload.php'; //$object = new Books(); $obj = new Dogs(); $list = $obj->getList();注释掉原有的代码，用新的实体类替换老的实体类，如果项目做大了，这个时候要进行这样的…