小蚯蚓博客 - 系统设计
type: channel
link: https://www.xiaoqiuyinboke.cn/category/systme_design/
description: 归纳一些软件系统设计的经验和知识
items: 3种简单的限流算法解析与实现
items: PHP接口与抽象类型的差异性解析
items: 深入理解并掌握ThinkPHP6中间件
items: Thinkphp6的Facade门面详解 | Guide and Tutorial
items: 深度解析Thinkphp6的事件机制 | PHP开发教程
items: 深入理解设计模式 - 精通面向对象编程
items: 深入理解ThinkPHP6中的文件缓存
items: 深入理解ThinkPHP6的APP类核心逻辑 | PHP框架教程
items: ThinkPHP6框架底层原理探讨
items: 后台列表展示单子明细的专业解决方案

3种简单的限流算法解析与实现
type: item
date: 2022-10-11T14:03:00+08:00
description: 固定窗口限流算法 首维护一个计数器，将单位时间段当做一个窗口，计数器记录这个窗口接收请求的次数。 当次数少于限流阀值，就允许访问，并且计数器+1 当次数大于限流阀值，就拒绝访问。 当前的时间窗口过去之后，计数器清零。 假设单位时间是1秒，限流阀值为3。在单位时间1秒内，每来一个请求,计数器就加1，如果计数器累加的次数超过限流阀值3，后续的请求全部拒绝。等到1s结束后，计数器清0，重新开始计数。如下图： 伪代码如下： /** * 固定窗口时间算法 * @return */ boolean fixedWindowsTryAcquire() { long currentTime = System.currentTimeMillis(); //获取系统当前时间 if (currentTime - lastRequestTime > windowUnit) { //检查是否在时间窗口内 counter = 0; // 计数器清0 lastRequestTime = currentTime; //开启新的时间窗口 } if (counter < threshold) { // 小于阀值 counter++; //计数器加1 return true; } return false; } 但是，这种算法有一个很明显的临界问题：假设限流阀值为5个请求，单位时间窗口是1s,如果我们在单位时间内的前0.8-1s和1-1.2s，分别并发5个请求。虽然都没有超过阀值，但是如果算0.8-1.2s,则并发数高达10，已经超过单位时间1s不超过5阀值的定义啦。 滑动窗口限流算法 滑动窗口限流解决固定窗口临界值的问题。它将单位时间周期分为n个小周期，分别记录每个小周期内接口的访问次数，并且根据时间滑动删除过期的小周期。 一张图解释滑动窗口算法，如下： 假设单位时间还是1s，滑动窗口算法把它划分为5个小周期，也就是滑动窗口（单位时间）被划分为5个小格子。每格表示0.2s。每过0.2s，时间窗口就会往右滑动一格。然后呢，每个小周期，都有自己独立的计数器，如果请求是0.83s到达的，0.8~1.0s对应的计数器就会加1。 我们来看下滑动窗口是如何解决临界问题的？ 假设我们1s内的限流阀值还是5个请求，0.8~1.0s内（比如0.9s的时候）来了5个请求，落在黄色格子里。时间过了1.0s这个点之后，又来5个请求，落在紫色格子里。如果是固定窗口算法，是不会被限流的，但是滑动窗口的话，每过一个小周期，它会右移一个小格。过了1.0s这个点后，会右移一小格，当前的单位时间段是0.2~1.2s，这个区域的请求已经超过限定的5了，已触发限流啦，实际上，紫色格子的请求都被拒绝啦。 TIPS: 当滑动窗口的格子周期划分的越多，那么滑动窗口的滚动就越平滑，限流的统计就会越。 滑动窗口算法伪代码实现如下： /** * 单位时间划分的小周期（单位时间是1分钟，10s一个小格子窗口，一共6个格子） */ private int SUB_CYCLE = 10; /** * 每分钟限流请求数 */ private int thresholdPerMin = 100; /** * 计数器, k-为当前窗口的开始时间值秒，value为当前窗口的计数 */ private final TreeMap<Long, Integer> counters = new TreeMap<>(); /** * 滑动窗口时间算法实现 */ boolean slidingWindowsTryAcquire() { long currentWindowTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) / SUB_CYCLE * SUB_CYCLE; //获取当前时间在哪个小周期窗口 int currentWindowNum = countCurrentWindow(currentWindowTime); //当前窗口总请求数 //超过阀值限流 if (currentWindowNum >= thresholdPerMin) { return false; } //计数器+1 counters.get(currentWindowTime)++; return true; } /** * 统计当前窗口的请求数 */ private int countCurrentWindow(long currentWindowTime) { //计算窗口开始位置 long startTime = currentWindowTime - SUB_CYCLE* (60s/SUB_CYCLE-1); int count = ; //遍历存储的计数器 Iterator<Map.Ent…

PHP接口与抽象类型的差异性解析
type: item
date: 2022-10-10T18:01:00+08:00
description: 概述 抽象类和接口使用场景 抽象类是对一种事物的抽象，即对类抽象，而接口是对行为的抽象。 抽象类是对整个类整体进行抽象，包括属性、行为，但是接口却是对类局部（行为）进行抽象。 例子 举个简单的例子，飞机和鸟是不同类的事物，但是它们都有一个共性，就是都会飞。那么在设计的时候，可以将飞机设计为一个抽象类AirplaneAbstract，将鸟设计为一个抽象类BirdAbstract，但是不能将 飞行 这个特性也设计为类，因为它只是一个行为特性，并不是对一类事物的抽象描述。 此时可以将 飞行 设计为一个接口FlyInterFace，包含方法fly( )，然后Airplane和Bird分别根据自己的需要实现Fly这个接口。然后至于有不同种类的飞机，比如战斗机、民用飞机等直接继承Airplane即可，对于鸟也是类似的，不同种类的鸟直接继承Bird类即可。 从这里可以看出， 继承是一个 "是不是"的关系 ，而 接口 实现则是 "有没有"的关系 。如果一个类继承了某个抽象类，则子类必定是抽象类的种类，而接口实现则是有没有、具备不具备的关系，比如鸟是否能飞（或者是否具备飞行这个特点），能飞行则可以实现这个接口，不能飞行就不实现这个接口。 // 对飞机这事务进行抽象，包括属性、行为 // 只要是飞机都有这些东西 abstract class AirplaneAbstract { // 飞机有机长 protected $captain; // 飞机有检票，具体各种飞机的检票方式不一样 // 例如：客机需要候机，战斗机刷卡 abstract public function checkIn(); } // 对鸟这事务进行抽象，包括属性、行为 // 只要是鸟都有这些东西，具体怎么吃，要看具体的鸟 abstract class BirdAbstract { // 鸟有性别 protected $sex; // 鸟要吃 abstract public function eat(); } // 对飞的行为的抽象 interface FlyInterFace { // public function fly(); } // 客机 class Airliner extends AirplaneAbstract implements FlyInterFace { public function checkIn() { echo "客机候机检票"; } public function fly() { echo "客机喷气飞行"; } } class Eagle extends GlobalBirdAbstract implements GlobalFlyInterFace { public function eat() { echo "老鹰抓小鸡"; } public function fly() { echo '老鹰翱翔'; } } 注意 需要注意的是，修改抽象类，不一定要修改其之类，例如添加抽象类的方法； 但是修改接口，实现改接口的类都必须修改。

深入理解并掌握ThinkPHP6中间件
type: item
date: 2022-10-08T17:41:00+08:00
description: 概述 thinkphp6中，中间件是针对http组件来说的，在http组件入库方法runWithRequest里 // 加载全局中间件 $this->loadMiddleware(); // 执行全局中间件 return $this->app->middleware->pipeline()// pipeline方法里将中间件转为闭包 // 初始化 ->send($request) // 执行 ->then(function ($request) { // 最后执行路由分发 return $this->dispatchToRoute($request); }); // 加载基础目录下的中间件 protected function loadMiddleware(): void { if (is_file($this->app->getBasePath() . 'middleware.php')) { $this->app->middleware->import(include $this->app->getBasePath() . 'middleware.php'); } } 核心实现逻辑 通过中间件组件来实现，队列queue里有全局类型的中间件队列、route类型的队列、controller类型的队列 class Middleware { // 队列 protected $queue = []; /** *******添加中间件******** */ public function import(array $middlewares = [], string $type = 'global'): void { foreach ($middlewares as $middleware) { $this->add($middleware, $type); } } public function add($middleware, string $type = 'global'): void { $middleware = $this->buildMiddleware($middleware, $type); if (!empty($middleware)) // 队列里添加中间件 $this->queue[$type][] = $middleware; // 队列去重 $this->queue[$type] = array_unique($this->queue[$type], SORT_REGULAR); } } // 中间件可以是数组、闭包、字符串 protected function buildMiddleware($middleware, string $type): array { if (is_array($middleware)) { [$middleware, $params] = $middleware; } if ($middleware instanceof Closure) { // 闭包+参数 return [$middleware, $params ?? []]; } if (!is_string($middleware)) { throw new InvalidArgumentException('The middleware is invalid'); } //中间件别名检查 $alias = $this->app->config->get('middleware.alias', []); if (isset($alias[$middleware])) { $middleware = $alias[$middleware]; } if (is_array($middleware)) { $this->import($middleware, $type); return []; } // 类名+handle方法+参数，如果没有传入方法名称，默认是handle方法 return [[$middleware, 'handle'], $params ?? []]; } public function route($middleware): void { // route类型的队列 $this->add($middleware, 'route'); } public function controller($middleware): void { // controller类型的队列 $this->add($middleware, 'controller'); } /** *******管道执行中间件******** */ public function pipeline(string $type = 'glob…

Thinkphp6的Facade门面详解 | Guide and Tutorial
type: item
date: 2022-10-08T10:10:00+08:00
description: 概述 门面主要是为了方便调用类方法，可以像调用静态方法的方式调用非静态方法。例如 class Test { public function hello($name) { return 'hello,' . $name; } } // 这个类名不一定要和Test类一致，但通常为了便于管理，建议保持名称统一 class Test extends Facade { protected static function getFacadeClass() { return 'app\common\Test'; } } // 无需进行实例化 直接以静态方法方式调用hello \app\facade\Test::hello('thinkphp'); 核心实现逻辑 主要是利用魔术方法、容器技术实现 class Facade { // 用于配置是否单例模式 protected static $alwaysNewInstance; // 魔术方法 public static function __callStatic($method, $params) { // 调用对象的方法 return call_user_func_array([static::createFacade(), $method], $params); } // 可以为其它类增加门面功能 protected static function getFacadeClass() {} // protected static function createFacade(string $class = '', array $args = [], bool $newInstance = false) { // 默认只需要继承这个类的话，本类就有门面功能 $class = $class ?: static::class; // 可以为其它类设置门面功能 $facadeClass = static::getFacadeClass(); if ($facadeClass) { $class = $facadeClass; } // 是否单例模式 if (static::$alwaysNewInstance) { $newInstance = true; } // 容器单例 return Container::getInstance()->make($class, $args, $newInstance); } }

深度解析Thinkphp6的事件机制 | PHP开发教程
type: item
date: 2022-09-30T11:48:00+08:00
description: 概述 tp6的事件，是通过观察者模式设计的，事件的基本原理是，首先在系统初始化的时候注册一些监听事件，然后观察事件的触发，只要事件触发的时候就就能执行这些观察者。 事件机制再不修改框架代码的情况下，提升了框架的扩展性。 源码解析 别名功能 事件的别名，触发的时候可以直接触发这个别名，系统自定义的基本事件别名已经写死在事件类的属性里了。 class Event { // 事件的别名，触发的时候可以直接触发这个别名 protected $bind = [ 'AppInit' => event\AppInit::class, ]; // 批量注册别名 public function bind(array $events) { $this->bind = array_merge($this->bind, $events); return $this; } } 事件配置，因为事件相关的配置，最好放在一个配置文件里，所以通过key来分开 return [ 'bind' => [ 'UserLogin' => 'app\event\UserLogin', ], ]; 然后系统初始化加载配置的时候，会批量加载别名，配置文件如下 $this->event->bind($event['bind']); 绑定别名后，就可以通过别名触发事件 $this->event->trigger('AppInit'); 当然这个别名不设置也是可以的，只要触发的时候，直接触发事件类名称就好了，不要触发别名，下面触发事件的方式和上面别名触发的方式效果是一样的 $this->event->trigger(AppInit::class); 需要注意的是，这个别名最好是大写开头，因为事件自动订阅模式，截取on开头后，第一个字符是大写，详情见下面的事件订阅 事件的监听 事件的监听就是观察者，是事件的核心 class Event { // 监听类的类名 protected $listener = []; // 批量注册监听 public function listenEvents(array $events) { foreach ($events as $event => $listeners) { if (isset($this->bind[$event])) { $event = $this->bind[$event]; } $this->listener[$event] = array_merge($this->listener[$event] ?? [], $listeners); } return $this; } // 单个监听 public function listen(string $event, $listener) { if (isset($this->bind[$event])) { $event = $this->bind[$event]; } $this->listener[$event][] = $listener; return $this; } } 由上我门可以知道，监听基本的参数是事件的名称、监听类（到时候会执行监听类里的handle方法）或者闭包，例如批量监听的时候，是通过系统配置的方式 return [ 'listen' => [ 'UserLogin' => ['app\listener\UserLogin'], ], ]; 这个配置文件，会在系统初始化的是被批量注册 $this->event->listenEvents($event['listen']); 当然，我们还可以注册单个 // 类名称，触发的时候会执行UserLogin里的handle方法 Event::listen('UserLogin', UserLogin::class); // 闭包 Event::listen('UserLogin', function($user) { // }); // 对象的具体方法 $this->listen('UserLogin', [$userLoginObject, 'onUserLogin']); 订阅功能 订阅的概念跟关注差不多，类似一次性可以关注多个明星，因此我们也可以一次性订阅多个事件，例如用户相关的事件，可以放在用户订阅类里，这样好管理事件的监听 // 事件类 class Event { // 批量订阅，可以传入字符串或者对象 public function subscribe($subscriber) { // 如果不是数组的，转成数组，支持订阅单个 $subscribers = (array) $subscriber; foreach ($subscribers as $subscriber) { if (is_str…

深入理解设计模式 - 精通面向对象编程
type: item
date: 2022-09-27T23:02:00+08:00
description: 设计模式 介绍 面向对象编程里有 6 大原则和 24 种设计模式。 什么是设计模式 设计模式是一套被反复使用、容易被他人理解的、可靠的代码设计经验的总结。 设计模式的目的是为了更好的代码重用性，可读性，可靠性和可维护性。 设计模式的种类 23 种设计模式里，可以按照以下 3 种进行分类： 创建型：如何创建对象 结构型：如何实现类和对象的组合的 行为型：类或者对象怎样交互，以及怎样分配职责 有一个“简单工厂模式”不属于Gof23种设计模式，但是大部分设计模式的书籍里，都会对它进行专门的介绍，所以目前设计模式的种类可以分为： GoF的23种设计模式+简单工厂模式 = 24种设计模式 六大原则 单一职责原则 (Single Responsibility Principle) 指的是：类和方法的职能要单一。这样做好处非常多：可以降低复杂度，提高代码的重用性、可靠性、可读性、可维护性。所以这个原则很重要，一定要满足！ 尽量避免方法功能的重复，不然不知道情况的人，就不知道调用哪个方法了，容易造成困扰。 对于php代码来说，就是class和method职责要单一 对于go语言来说，就是结构体和method职责要单一 但是，也要不能拆分的过细，不然类就会太多，耦合度就高，要追求高内聚，低耦合 package main import "fmt" type WorkerClothes struct { } func (w WorkerClothes) Style() { fmt.Println("穿工作服") } type ShopClothes struct { } func (s ShopClothes) Style() { fmt.Println("穿工作服") } func main() { // 每个类的职责单一，即使两个方法打印的结果都是一样，也不会歧义 // 只需要从类的职责上就可以发现，一个是工作服，一个是逛街衣服 wc := WorkerClothes{} wc.Style() sc := ShopClothes{} sc.Style() } 里是替换原则（Liskov Substitution Principle) 指的是：将一个基类对象替换成它的子类对象，程序将不会有任何问题，主要是对类继承的一种约束，这样做好处是不会破坏原来的职能，提升可靠性： 不能随便继承不合适的、有多余方法或属性的类。（例子 1） 子类可以扩展父类的功能（比如增加方法），但不能改变父类原有的功能 (比如覆盖父类非抽象方法)。（例子 2） package main import "fmt" // 抽象层 type Car interface { Run() } type Driver interface { Drive(car Car) } // 实现层，依赖抽象 type Benz struct { } func (b Benz) Run() { fmt.Println("benz is running...") } type Bwm struct { } func (b Bwm) Run() { fmt.Println("bwm is running....") } type ZhangSang struct { } func (z ZhangSang) Drive(car Car) { car.Run() } type LiSi struct { } func (l LiSi) Drive(car Car) { car.Run() } // 业务层，也要依赖抽象 func main() { var benz Car // 依赖抽象 benz = new(Benz) var zs Driver // 依赖抽象 zs = new(ZhangSang) zs.Drive(benz) var bwm Car // 依赖抽象 bwm = new(Bwm) var ls Driver // 依赖抽象 ls = new(LiSi) ls.Drive(bwm) } 依赖倒置原则 (Dependence Inversion Principle) 指的是：依赖于抽象接口，不要依赖具体实现类，也就是针对接口编程。 如果保证业务逻辑层向下依赖抽象层，实现层向上依赖抽象层，这样我们只需要关心抽象层有什么方法即可！！，如果依赖具体类，我们还得去看具体类有哪些方法。 package main import "fmt" // 抽象层 type Car interface { Run() } type Driver interface { Drive(car Car) } // 实现层，依赖抽象 type Benz struct { } func (b Benz) Run() { …

深入理解ThinkPHP6中的文件缓存
type: item
date: 2022-09-19T10:42:00+08:00
description: 场景 我们经常会将excel导入模板文件保存在一个目录，然后提供一个下载按钮，去下载这个模板文件，但是默认情况下thinkphp会在文件响应的时候，会让浏览器缓存一段时间，导致模板文件修改没有立即得到生效 // 下载模板 public function downloadExcelTpl($fileName) { $file = ErpFacade::getContainerAssetsPath() . DIRECTORY_SEPARATOR . $fileName; return (new File($file))->header([ 'Access-Control-Expose-Headers' => 'filename', 'filename' => $fileName, ]); } File类的默认缓存时间如下所示： 解决方法 我们可以手动设置文件缓存的时间为0，修改为如下代码即可 // 下载模板 public function downloadExcelTpl($fileName) { $file = ErpFacade::getContainerAssetsPath() . DIRECTORY_SEPARATOR . $fileName; return (new File($file))->header([ 'Access-Control-Expose-Headers' => 'filename', 'filename' => $fileName, ])->expire(0);// 设置缓存时间为0 }

深入理解ThinkPHP6的APP类核心逻辑 | PHP框架教程
type: item
date: 2022-09-16T17:10:00+08:00
description: APP类 一般来说，在index入口文件在加载autoLoad文件后，紧接着就是实例化一个App类，需要注意的是，这个App是可以扩展的，我们可以自定义一个类来继承它 class Erp extends App { protected $bind = [ 'app' => Erp::class,// 通过继承，替换掉系统的默认的类 'cache' => Cache::class, // 等等...，有很多默认设置好的绑定，这里列出几个举例子 ] public function __construct(string $rootPath = '') { parent::__construct($rootPath); if (is_file($this->getBootstrapPath() . 'provider.php')) { $this->bind(include $this->getBootstrapPath() . 'provider.php'); } } } 然后创建一个boostrap目录，用于放一些启动文件，常见的有一下几个，不过可以自己增加其它的 boostrap/app.php，用于实例化自定义类，然后直接在入口文件index.php里引入这个文件$app = require_once __DIR__.'/../bootstrap/app.php'; $app = new \ship\foundation\Erp( realpath(__DIR__.'/../') ); return $app; 1. boostrap/provider.php，用于设置容器的绑定，修改request类为自定义的类，这个就是容器的强大扩展之处，可以不修改框架代码，实现自定义扩展 ```php return [ 'think\exception\Handle' => \ship\foundation\exceptions\HandleException::class, 'think\Request' => \ship\foundation\AppRequest::class, 'think\Validate' => \ship\foundation\validate\Validate::class, 'console' => \ship\parents\console\AppConsole::class, 'export' => \ship\foundation\Export::class, 'event' => \ship\foundation\events\Event::class, ]; boostrap/service.php，用于设置启动服务 return [ \ship\services\ShipService::class, ]; 然后实例化app类的时候，会执行app类里的构造方法 ```php class App extends Container { protected $bind = [ 'app' => App::class,// 别名=>类明的形式，调用时候方便，$this->别名 'cache' => Cache::class, // 等等...，有很多默认设置好的绑定，这里列出几个举例子 ] public function __construct(string $rootPath = '') { $this->thinkPath = dirname(__DIR__) . DIRECTORY_SEPARATOR; $this->rootPath = $rootPath ? rtrim($rootPath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR : $this->getDefaultRootPath(); $this->appPath = $this->rootPath . 'app' . DIRECTORY_SEPARATOR; $this->runtimePath = $this->rootPath . 'runtime' . DIRECTORY_SEPARATOR; if (is_file($this->appPath . 'provider.php')) { // 绑定自定义的类，或者覆盖系统默认的类，实现扩展 $this->bind(include $this->appPath . 'provider.php'); } static::setInstance($this); $this->instance('app', $this); $this->instance('think\Container', $this); } } 由此构造方法可知，a…

ThinkPHP6框架底层原理探讨
type: item
date: 2022-09-16T15:04:00+08:00
description: 首先，在文件组织上，框架的所有文件都是放在think开头的命名空间下 然后，在入口文件上，有两个入口： 一个是console入口，在项目的根目录下，用于执行脚本和定时任务 另一个是http入口，在项目的public目录下，用于提供http服务 由于上图可知，这两个文件的工作流思路也比较简单，只有三个步骤 第一、引入autoload文件，用于管理第三方扩展包，已经提供自动加载的功能 第二、实例化一个全局的App应用，用于管理应用，加载配置文件、提供容器的支持等 第三、因为有app类的支持，可以使用对应的组件来执行相关的功能，例如http组件用于提供http服务，console组件用于命令行的支持

后台列表展示单子明细的专业解决方案
type: item
date: 2022-09-16T10:33:00+08:00
description: 有时候为了方便阅读，会在列表也要展示明细，例如： 为了实现这个效果，其实有两种方式： 第一种是：数据是以单据的为主体分页的形式返回给前端，如果说操作按钮的主体是单据，那么用这种方式是最好不过的。 不过有个问题就是，如果单据的明细特别多，就会导致列表太长了，不好阅读，不过可以让前端实现合并展开的功能。同时如果明细数量多的话，一次性返回太多的结果，会影响列表的加载速度。 第二种是：数据以单据明细分页的方式返回给前端，单据明细以订单id进行排序，这样同一个订单的明细就会在一起，前端再把单据编号合并展示。如果列表的操作对象是单据明细的话，其实这个方式是不错的选择，这样就能确保每页的行数的固定的，阅读体验会比较好。 缺点： 可能同一个单子的明细，一个在第一页，另外一个在第二页。 同时如果操作按钮如果是针对单据的，那么就需要对操作按钮栏目也要做合并，如果明细一个在第一页，另外一个在第二页，那么同一个单子就会有两个操作按钮，逻辑上会比较不好点。 搜索明细的sn的时候，会得到一条明细结果，但是操作按钮是针对整个单子的话，点进去又是所有的明细，感觉上也有点怪怪的 综上所述，具体选择哪个需要自己根据实际情况权衡