Feron's BLOG

工作中的PHP-工作中遇到的问题点滴累计

  • SESSION 的原理

    HTTP 协议是无状态协议,session 是在服务器端保持用户会话数据的一种方法。
    实现过程如下
  1. 将客户端和服务器端建立一对一的联系,每个客户端都有一个唯一标识,这样服务器才能识别出来。建议唯一标识的方法有两种:使用cookie 或者通过 GET 方式指定。默认配置的 PHP 使用 session 的时会建立一个名叫 PHPSESSIDcookie(可以通过 php.ini 修改 session.name 值指定),如果客户端禁用 cookie ,你也可以指定通过 GET 方式把 session id 传到服务器(修改 php.inisession.use_trans_sid 等参数)。
  2. 客户端将 session id 传递到服务器,服务器根据 session id 找到对应的文件,读取的时候对文件内容进行反序列化就得到 session 数据,保存的时候先序列化再写入文件。
  • PHP 类自动加载原理和实现

PHP中类的实例化过程中,系统所做的工作大致是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 模拟系统实例化过程 */
function instance($class)
{
// 如果类存在则返回其实例
if (class_exists($class, false)) {
return new $class();
}
// 查看 autoload 函数是否被用户定义
if (function_exists('__autoload')) {
__autoload($class); // 最后一次引入的机会
}
// 再次检查类是否存在
if (class_exists($class, false)) {
return new $class();
} else { // 系统:我实在没辙了
throw new Exception('Class Not Found');
}
}

PHP 5.1.0 以后 spl_autoload_register — 注册给定的函数作为 __autoload 的实现
函数原型

1
bool spl_autoload_register ([ callable $autoload_function [, bool $throw = true [, bool $prepend = false ]]] )

如果需要多条 autoload 函数,spl_autoload_register() 满足了此类需求。 它实际上创建了 autoload 函数的队列,按定义时的顺序逐个执行。相比之下, __autoload() 只可以定义一次。
当调用未定义类时,系统就会按顺序调用注册到 spl_autoload_register() 函数的所有函数

现在,我们来创建一个 Linux 类,它使用 os 作为它的命名空间(建议文件名与类名保持一致):

1
2
3
4
5
6
7
8
9
namespace os; // 命名空间
class Linux // 类名
{
function __construct()
{
echo '<h1>' . __CLASS__ . '</h1>';
}
}

接着,在同一个目录下新建一个 PHP 文件,使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
spl_autoload_register 以函数回调的方式实现自动加载:
spl_autoload_register(function ($class) { // class = os\Linux
/* 限定类名路径映射 */
$class_map = array(
// 限定类名 => 文件路径
'os\\Linux' => './Linux.php',
);
/* 根据类名确定文件名 */
$file = $class_map[$class];
/* 引入相关文件 */
if (file_exists($file)) {
include $file;
}
});
new \os\Linux();

这里我们使用了一个数组去保存类名与文件路径的关系,这样当类名传入时,自动加载器就知道该引入哪个文件去加载这个类了。

PSR-4 规范中必须要有一个顶级命名空间,它的意义在于表示某一个特殊的目录(文件基目录)。子命名空间代表的是类文件相对于文件基目录的这一段路径(相对路径),类名则与文件名保持一致(注意大小写的区别)。

举个例子:在全限定类名 \app\view\news\Index 中,如果 app 代表 C:\Baidu,那么这个类的路径则是 C:\Baidu\view\news\Index.php

我们就以解析 \app\view\news\Index 为例,编写一个简单的 Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$class = 'app\view\news\Index';
/* 顶级命名空间路径映射 */
$vendor_map = array(
'app' => 'C:\Baidu',
);
/* 解析类名为文件路径 */
$vendor = substr($class, 0, strpos($class, '\\')); // 取出顶级命名空间[app]
$vendor_dir = $vendor_map[$vendor]; // 文件基目录[C:\Baidu]
$rel_path = dirname(substr($class, strlen($vendor))); // 相对路径[/view/news]
$file_name = basename($class) . '.php'; // 文件名[Index.php]
/* 输出文件所在路径 */
echo $vendor_dir . $rel_path . DIRECTORY_SEPARATOR . $file_name;

通过这个 Demo 可以看出限定类名转换为路径的过程。那么现在就让我们用规范的面向对象方式去实现自动加载器吧。

首先我们创建一个文件 Index.php,它处于 \app\mvc\view\home 目录中:

1
2
3
4
5
6
7
8
9
namespace app\mvc\view\home;
class Index
{
function __construct()
{
echo '<h1> Welcome To Home </h1>';
}
}

接着我们在创建一个加载类(不需要命名空间),它处于 \ 目录中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Loader
{
/* 路径映射 */
public static $vendorMap = array(
'app' => __DIR__ . DIRECTORY_SEPARATOR . 'app',
);
/**
* 自动加载器
*/
public static function autoload($class)
{
$file = self::findFile($class);
if (file_exists($file)) {
self::includeFile($file);
}
}
/**
* 解析文件路径
*/
private static function findFile($class)
{
$vendor = substr($class, 0, strpos($class, '\\')); // 顶级命名空间
$vendorDir = self::$vendorMap[$vendor]; // 文件基目录
$filePath = substr($class, strlen($vendor)) . '.php'; // 文件相对路径
return strtr($vendorDir . $filePath, '\\', DIRECTORY_SEPARATOR); // 文件标准路径
}
/**
* 引入文件
*/
private static function includeFile($file)
{
if (is_file($file)) {
include $file;
}
}
}

最后,将 Loader 类中的 autoload 注册到 spl_autoload_register 函数中:

1
2
3
4
5
6
7
8
include 'Loader.php'; // 引入加载器
spl_autoload_register('Loader::autoload'); // 注册自动加载
new \app\mvc\view\home\Index(); // 实例化未引用的类
/**
* 输出: <h1> Welcome To Home </h1>
*/

  • PHP trait 详解

    PHP 5.4.0 起,PHP 实现了一种代码复用的方法,称为 trait

Trait 是为类似 PHP 的单继承语言而准备的一种代码复用机制。Trait 为了减少单继承语言的限制,使开发人员能够自由地在不同层次结构内独立的类中复用 methodTraitClass 组合的语义定义了一种减少复杂性的方式,避免传统多继承和 Mixin 类相关典型问题。

TraitClass 相似,但仅仅旨在用细粒度和一致的方式来组合功能。 无法通过 trait 自身来实例化。它为传统继承增加了水平特性的组合;也就是说,应用的几个 Class 之间不需要继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
trait ezcReflectionReturnInfo {
function getReturnType() { /*1*/ }
function getReturnDescription() { /*2*/ }
}
class ezcReflectionMethod extends ReflectionMethod {
use ezcReflectionReturnInfo;
/* ... */
}
class ezcReflectionFunction extends ReflectionFunction {
use ezcReflectionReturnInfo;
/* ... */
}

优先级
从基类继承的成员会被 trait 插入的成员所覆盖。优先顺序是来自当前类的成员覆盖了 trait 的方法,而 trait 则覆盖了被继承的方法。

Example #2 优先顺序示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
class Base {
public function sayHello() {
echo 'Hello ';
}
}
trait SayWorld {
public function sayHello() {
parent::sayHello();
echo 'World!';
}
}
class MyHelloWorld extends Base {
use SayWorld;
}
$o = new MyHelloWorld();
$o->sayHello();

  • 如何排查PHP程序性能瓶颈

  • 性能优化的核心问题
    哪里有性能问题。
  • 最佳线程数
    很多资料上有讲 N + 1,或者 N - 1,这两种情况在保证线程持续运行的情况,也许可以适用;但是在时间的应用场景中,一个业务的处理,线程不可能一直处理运行状态,比如当调用外部 WS ,或者 DB 操作时,都有可能存在 IO 阻塞,在这段时间线程是没有运行的,因此如果只是按照核数确定线程数,不太合理。
  • 请求响应时间(线程执行总时间
    一个请求开始到结束的总时间,也就是一个线程执行的总时间,包括线程在 CPU 上运行的时间+线程阻塞等待的时间。
  • 资源占用时间
    首先要搞明白,何为资源?很多人第一个想到的肯定是 CPU,性能优化的核心问题是:哪里有性能问题?那些资源有性能问题。并发情况下,资源的抢占是影响性能的主要原因。在某些业务场景,CPU是主要的瓶颈资源,业务执行,占用了大量的 CPU 运算时间,并发请求主要在等待 CPU 的调度执行,因此,此时,CPU 为瓶颈资源,资源占用时间=业务在 CPU 上运行的时间。还有的场景,在等待稀有的共享资源,比如数据库连接,或者数据库锁。比如锁为昂贵的稀缺资源,锁具有互斥排他性,一个线程占据锁,其他线程必须阻塞等待,此时并发性能受到严重的影响。因此,此时,锁为瓶颈资源,资源占用时间=业务在锁独占时间。 还有的场景,调用外围时,外围的响应时间较长,这种情况也造成 QPS 较低,外围响应时间较长,如果没有很好的超时机制,会带来比较严重的后果。单纯的增加线程池数量,或者等待队列的长度,可以提高应用的吞吐量,但是可能有很大的负载压力。
  • 资源并行数
    同一个时间点,可以并发执行的请求数。比如 CPU 场景下,并行数= CPU 核数;锁独占的情况下,如果共享资源一个,那么并行数=1。

    最佳线程数 = 线程执行总时间 / 资源占用时间 * 资源并行数

  • 缓存穿透问题

  • 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
  • 不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
  • 做二级缓存,A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期

  • 缓存穿透:查询一个必然不存在的数据。比如文章表,查询一个不存在的id,每次都会访问DB,如果有人恶意破坏,很可能直接对DB造成影响。解决办法:对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃。