coding code
change world

近期宝塔Linux版本安装的PHP7.3缺少ZIP

卢哥阅读(152)

面板版本:BT-Panel 6.9.32 Beta

系统版本:CentOS 7.6 1810

问题现象:近期安装的 PHP-7.3 缺少 zip 扩展

原因分析:这个 Bug 实际上算是个历史遗留问题。2019年1月18日,有用户发现,PHP 更新到 7.3.1 就会报错:PHP模块重复加载。于是发帖报告的一个 Bug,帖子链接如下:
https://www.bt.cn/bbs/thread-21723-1-1.html
次日,河妖在论坛发帖,公布了问题原因和解决办法:
https://www.bt.cn/bbs/thread-21750-1-1.html

因早期安装的Php7.3是独立编译zip扩展,后续安装及升级是整合编译的,导致升级后会重复加载zip模块

因此,其后的PHP就不再单独编译ZIP扩展了。到了现在,半年多过去了,PHP 更新到了 7.3.8,ZIP 模块又不再整合了(不整合是对的,保持独立编译是最稳妥的方式),可是,却没有单独编译和安装ZIP扩展。

那么,解决办法也很简单,ZIP扩展本来就包含在软件包里,添加回来就可以了:

cd /www/server/php/73/src/ext/zip/
/www/server/php/73/bin/phpize
./configure --with-php-config=/www/server/php/73/bin/php-config
make && make install


然后,把以前从PHP配置文件里删去的行加回来:

echo "extension = zip.so" >> /www/server/php/73/etc/php.ini


最后重载一下 PHP 服务即可:

service php-fpm-73 reload

提示需要重新安装libzip库,应该是zip需要依赖libzip库

系统本身是有安装`libzip`库的(通过`yum info libzip`得知),但是这里还是提示,应该是版本问题,yum安装的是0.10.1版本,从官方获取最新的libzip版本

安装之前我们先删除旧版本

yum remove -y libzip
wget https://nih.at/libzip/libzip-1.2.0.tar.gz
tar -zxvf libzip-1.2.0.tar.gz
cd libzip-1.2.0
./configure
make && make install

安装好libzip 1.2之后,我们再装zip扩展

继续下面的步骤:

cd /www/server/php/73/src/ext/zip/
 /www/server/php/73/bin/phpize ./configure --with-php-config=/www/server/php/73/bin/php-config make

上面的编译有错误:提示如下,找不到文件。

 /usr/local/include/zip.h:59:21: fatal error: zipconf.h: No such file or directory

添加软连接,即可解决

ln -s /usr/local/lib/libzip/include/zipconf.h /usr/local/include/zipconf.h

接下来执行编译安装:

 make install

结果出现提示如下:

Installing shared extensions:www/server/php/73/lib/php/extensions/no-debug-non-zts-20180731/

表示,我们已经成功安装zip扩展。最后一步就是写php.ini配置文件了。

我们修改php.ini文件在文件末尾添加: extension = /www/server/php/73/lib/php/extensions/no-debug-non-zts-20180731/zip.so

15年程序员经验分享:40个改变你编程技能的小技巧

卢哥阅读(113)

40个改变编程技能的小技巧

1、将大块代码分解成小函数

2、今日事今日毕,如果没毕,就留到明天。

如果下班之前还没有解决的问题,那么你需要做的,就是关闭电脑,把它留到明天。
中途不要再想着问题了!

3、YAGNI原则

「You aren’t gonna need it!」
你自以为有用的功能,实际上是用不到的。除了要求的核心功能,其他功能一概不要部署。
这一原则的核心思想是,尽可能快、尽可能简单的将软件运行起来。

4、不必全知全能,但基础一定要扎实

比如学习一些基础知识,SOLID原则,如何写干净的代码等等。

5、KISS原则

「Keep it simple,stupid.」or「Keep it stupid simple.」,一种程序设计原则。
大多数系统往往「最简单」,运行效率最高,但实际操作起来并不简单。

6、别想太多

7、被问题/Bug卡住时,walk away!

不过还是要记得回来。

当你走在去上班、去厕所、去散步的时候,也许就能想到解决方法。
尤其是在与客户、同事生气时,甚至关乎你工作去留的时候,效率会更高。

8、学会写测试代码TDD

TDD是一个软件开发过程,它依赖于重复一个很短的开发周期:写一个测试,运行所有的测试,看看新的测试是否失败,写一些代码,运行测试,重构代码,重复。

9、先分解问题再开始写代码

不要不知道怎么做就开始写代码。

10、代码不要死记硬背

要理解逻辑。

11、学好用好Stack Overflow

如果你复制粘贴一个Stack Overflow解决方案,请一定要确保已经理解了它。

12、不要「光学不练」

如果你想学点什么,就去练习,光学是不够的。

13、与小伙伴互相审查代码

研究别人的代码,让别人时常研究你的代码。
互帮互助,共同进步。

14、Don’t Reinvent The Wheel

「不要重新发明轮子。」
充分利用已有的经验和成果,避免不必要的投入和浪费。

15、你的代码是最好的文档

16、懂得如何搜索

对于这一点,你需要有经验以及读很多书,才知道要找什么东西。

17、写代码时要之后维护考虑

你的代码将来需要你自己或者别人来维护。
所以,写代码的时候要考虑到读者,而不是想成为最聪明的人,让它读起来就像在读一个故事。

18、复制粘贴

用谷歌、百度解决错误的最好方式就是「复制粘贴」。

19、不要放弃

到最后,不管用什么方式,问题肯定会解决。

20、休息、休息再休息

解决问题的最好方法是有一个安稳的心态。

21、学习软件设计模式

设计模式是软件设计中常见问题的解决方案。每一种模式就像一个蓝图,你可以自定义来解决代码中常见的设计问题。(不要重复发明轮子。)

22、使用集成工具

尽可能实现自动化。

23、Do code katas.

「Code kata」是编程中的一种练习,可以帮助程序员通过练习和重复来提高他们的技能。

24、依赖注入是一个要求

编程到一个接口,而不是implementation。
所谓依赖注入,就是组件之间的依赖关系由容器在应用系统运行期来决定,也就是由容器动态地将某种依赖关系的目标对象实例注入到应用系统中的各个关联的组件之中。

25、重构-测试-重构

重构是一种对现有代码进行重组的技术,在不改变其外部行为的前提下,改变和改进其内部结构。

26、及时寻求帮助

不要浪费时间。

27、Practice makes perfect.

业精于勤。

28、不必太在意评论

虽然有时评论可以帮到你,但不要太过在意。他们可能已经过时了。

29、了解你的开发环境

了解你的开发环境,并invest一个足够强大的环境,如,IntelliJ。

30、复用组件

31、考虑相关限制

在开发网络应用时,要考虑到移动优先以及相关的功率和带宽限制。

32、不要过早优化或重构

更重要的是尽快拥有一个最低限度可行的产品。

33、不要投机取巧

千万不要为了节省几分钟的时间而选择效率低下的捷径方式。
「Every time you code, give your best!」

34、遵循规定的标准

35、用户不是技术人员

当你开发你的UI时,需要考虑到这一点。

36、坚持使用Github或bitbucket

可以进行小规模、频繁的git提交。

37、记录所有关键部分

记录系统日志比调试代码更好。

38、风格保持一致

如果你使用一种风格,请总是使用相同的风格。
如果你和更多的人一起工作,对所有的团队都使用相同的风格。

39、Don’t stop learning

但比起学习新语言或框架,更要注重学习软件开发的基础知识。

40、patience and love

最后,对你正在做的事情保有足够的耐心和热爱。

2分钟实现一个极简的PHP签名类

卢哥阅读(113)

今天由于某一部分数据比较敏感,所以传输过程需加密,解密后需认证数据是否被篡改,而自己又不大喜欢为了一个小功能就composer require一个大包,所以花2分钟撸了一个,暂时先顶着用用,项目进度太紧凑,无太多时间写出比较优雅的代码

<?php

class Signature
{
	public static function makeSign(string $secret_key, string $data, string $alg = 'MD5')
	{
		if ('MD5' === strtoupper($alg)) {
			return strtoupper(md5($data . '&' . $secret_key));
		}
		
		if ('SHA256' === strtoupper($alg)) {
			return strtoupper(hash_hmac('sha256', $data, $secret_key));
		}
		return null;
	}
	
	
	public static function checkSign(string $secret_key, string $data, string $sign, string $alg = 'MD5')
	{
		$real_sign = self::makeSign($secret_key, $data, $alg);
		return $sign === $real_sign;
	}
}


echo Signature::makeSign('key001', 'helloworld', 'sha256');
// 输出结果:47FB290B88DF9C45A659B0879C097F6B4512ED789E008396BE9229549EE5470E
var_dump(Signature::checkSign('key001', 'helloworld', '47FB290B88DF9C45A659B0879C097F6B4512ED789E008396BE9229549EE5470E', 'sha256'));
// 输出结果 bool(true)

装饰器模式

卢哥阅读(108)

装饰器模式,又名包装(Wrapper)模式,该模式向一个已有的对象添加新的功能,而不改变其结构。

通常给对象添加功能有3中方法:

  1. 直接修改类,添加相应的功能
  2. 派生对应的子类扩展新功能
  3. 使用对象组合的方式

直接修改类代码是一种侵入式修改,很容易对类功能造成损害。

使用继承方式扩展功能,则在整个子类中添加功能,即使有的对象不需要,new出来也会有这些新功能。

装饰器模式是典型的基于对象组合的方式,可以很灵活的给对象添加所需要的功能,

它能动态的为一个对象增加功能,而且还能动态撤销。

继承不能做到这一点,继承的功能是静态的,不能动态增删。

1 问题

假设我们有一个邮件内容模板类,如下,一般情况下我们都用这个模板发送邮件:

class emailBody
{
    public function body()
    {
        echo "公司准备为您加薪50%。\n";
    }
}

现在元旦准备来了,我们想在邮件模板上加一些元旦快乐的祝福语。

这时,我们可以直接修改emailBody类,但是这样实在不好。

所以,选择用子类继承方式来实现:

class newYearEmail extends emailBody
{
    public function body()
    {
        echo '元旦快乐!!!';
        parent::body();
    }
}

$email = new newYearEmail();
$email->body();

再过一个多月,春节来了,我们又可以按照元旦的做法,添加一个springFestivalEmail类。

可是,如果想在不修改原来类的基础上,同时祝福元旦快乐和春节快乐呢?

这时我们不禁会想,这种情况继承方式是否好用。

2 解决

从上面的问题中,我们看到,我们可以用子类的方式为类扩展一个功能,

但是如果要同时增加多个功能时,会变得冗长而复杂,这是装饰器模式要解决的问题。

首先,创建一个接口,用以规范邮件类:

/**
 * 邮件内容接口,规范实现类
 */
interface EmailBody
{
    public function body();
}

然后是正常的邮件内容类,我们用装饰器的目的就是,在某些情况下不改变其代码,也能得到不同的结果。

/**
 * 正常邮件内容类
 */
class MainEmail implements EmailBody
{
    public function body()
    {
        echo "公司准备为您加薪50%。\n";
    }
}

然后是主装饰器类,这个类用属性保存MainEmail类的对象,然后根据需要改变它的行为。

/**
 * 邮件内容装饰器类
 *
 */
abstract class EmailBodyDecorator implements EmailBody
{
    // 保存MainEmail类对象
    protected $emailBody;

    // 实例化这个类或者子类时,必须传入一个被修饰的对象
    public function __construct(EmailBody $emailBody)
    {
        $this->emailBody = $emailBody;
    }

    // 用抽象方法声明EmailBody规定的方法,
    // 在子类中用来改变MainEmail对象的行为
    abstract public function body();
}

然后我们定义两个装饰器的子类,在这两个子类里面我们改变原MainEmail的行为:

class NewYearEmail extends EmailBodyDecorator
{
    public function body()
    {
        echo '元旦快乐!';
        $this->emailBody->body();
    }
}

class SpringFestivalEmail extends EmailBodyDecorator
{
    public function body()
    {
        echo '春节快乐!';
        $this->emailBody->body();
    }
}

客户端使用:

/**
 * 【正常】发送邮件
 * 输出: 公司准备为您加薪50%。
 */
$email = new MainEmail();
$email->body();

/**
 * 发送有【元旦】祝福的邮件
 * 输出: 元旦快乐!公司准备为您加薪50%。
 */
$emailNewYear = new NewYearEmail($email);
$emailNewYear->body();

/**
 * 发送有【春节】祝福的邮件
 * 输出: 春节快乐!公司准备为您加薪50%。
 */
$emailSpring = new SpringFestivalEmail($email);
$emailSpring->body();

/**
 * 发送同时有【元旦】和【春节】祝福的邮件
 * 输出: 春节快乐!元旦快乐!公司准备为您加薪50%。
 */
$emailTwo = new SpringFestivalEmail($emailNewYear);
$emailTwo->body();

组合模式

卢哥阅读(106)

组合模式将对象组合成树形结构,以表示‘部分-整体’的层次结构。

在组合模式,客户端访问独立对象和组合对象(或称对象集合)一样

独立对象是一个有特定功能的对象,它不引用其他任何其他对象。

组合对象则是一个提供相似功能对象集合,主要用来管理独立对象,并为客户端提供和独立对象一样的访问方式。

接下来,我们就以目录和文件来举例,利用它们的一个相同的功能“查看大小”来举例。

1 问题

在文件系统中,我们有文本文件、图片文件、视频文件等类型的文件。

它们格式不同,但都有容量大小。

现在,我们创建文本和图片文件两个类,且各自有固定的大小。

(实际当然不是如此,这里举例所以简化处理。)

接着,我们要获取文件和目录的大小。

首先是文件:

abstract class File
{
    abstract function getSize();
}

class TextFile extends File
{
    public function getSize()
    {
        return 2;
    }
}

class ImageFile extends File
{
    public function getSize()
    {
        return 100;
    }
}

这样,在创建文本或图片对象后,就可以通过getSize()方法获取到它们的大小。

然后,我们创建一个目录类,它可以把文件组合起来

class Dir
{
    private $files = [];

    // 传入参数必须为File文件对象
    public function addFile(File $file)
    {
        $this->files[] = $file;
    }

    public function getSize()
    {
        $size = 0;
        foreach ($this->files as $file) {
            $size += $file->getSize();
        }

        return $size;
    }
}

然后,我们就可以计算目录的大小,它等于目录下所有文件大小之和。

例如,这个目录加入一个文本文件,一个图片文件,那么这个目录大小就是:102

当然,如果问题一直这样简单的话,那么这个模型还是非常令人满意的。

但是,如果有一些新的需求加入会怎样?

比如,要在目录中再加一层目录,那么Dir类就需要变成:

class NewDir
{
    private $files = [];
    private $dirs = [];

    public function addFile(File $file)
    {
        $this->files[] = $file;
    }

    public function addDir(NewDir $newDir) {
        $this->dirs = $newDir;
    }

    public function getSize()
    {
        $size = 0;
        foreach ($this->files as $file) {
            $size += $file->getSize();
        }

        foreach ($this->dirs as $dir) {
            $size += $dir->getSize();
        }

        return $size;
    }
}

是不是比之前又复杂了些?

这还不算,我们还需要修改原来的类,可能无意间又影响原来的功能 。

另外,如果我们现在要计算多级子目录的大小、或者从目录中删除目录,是不是还需要修改原有类?

显然,这个模型无法实现这些复杂的功能,我们需要一个更加灵活的模型。

2 组合模式

组合模式的解决方法是,用抽象类规范统一的对外接口。

然后,让文件类和目录类实现这个接口,并在目录类中递归计算文件的大小

同时,目录类比文件类两个方法:add()remove(),用以管理文件对象。

这样,目录类就能用同样的方式获取自身的大小。

并且,还能灵活从目录总增删子目录和文件。

2.1 接口

接口用于规范独立对象和组合对象,保证能够对外提供一致性的使用方法。

这里以getName()getSize()方法为例:

/**
 * 规范独立对象和组合对象必须实现的方法,保证它们提供给客户端统一的
 * 访问方式
 */
abstract class Filesystem
{
    protected $name;

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

    public abstract function getName();
    public abstract function getSize();
}

其中,__construct构建函数用于传入文件或目录名称,并非必须。

这个接口中规范的方法要根据需求来定义,并且同时要考虑独立对象拥有的功能

如果独立对象之间有差异的功能,不适合聚合在一起,则不能放在组合类中。

2.2 目录类

目录类是对象集合,通过add()remove()方法管理文件对象和其他目录对象

目录类也需要实现抽象类中的方法,以提供给客户端一致性的使用方式。

/**
 * 目录类
 */
class Dir extends Filesystem
{
    private $filesystems = [];

    // 组合对象必须实现添加方法。因为传入参数规定为Filesystem类型,
    // 所以目录和文件都能添加
    public function add(Filesystem $filesystem)
    {
        $key = array_search($filesystem, $this->filesystems);
        if ($key === false) {
            $this->filesystems[] = $filesystem;
        }
    }

    // 组合对象必须实现移除方法
    public function remove(Filesystem $filesystem)
    {
        $key = array_search($filesystem, $this->filesystems);
        if ($key !== false) {
            unset($this->filesystems[$key]);
        }
    }

    public function getName()
    {
        return '目录:' . $this->name;
    }

    public function getSize()
    {
        $size = 0;
        foreach ($this->filesystems as $filesystem) {
            $size += $filesystem->getSize();
        }

        return $size;
    }
}

2.3 文件类

文件类实现具体的功能,但是没有add()remove()方法。

/**
 * 独立对象:文本文件类
 */
class TextFile extends Filesystem
{
    public function getName()
    {
        return '文本文件:' . $this->name;
    }

    public function getSize()
    {
        return 10;
    }
}

/**
 * 独立对象2:图片文件类
 */
class ImageFile extends Filesystem
{
    public function getName()
    {
        return '图片:' . $this->name;
    }

    public function getSize()
    {
        return 100;
    }
}

/**
 * 独立对象:视频文件类
 */
class VideoFile extends Filesystem
{
    public function getName()
    {
        return '视频:'. $this->name;
    }

    public function getSize()
    {
        return 200;
    }
}

组合模式中,组合对象必须在合适的地方提供独立对象的管理方法,如:add()remove()等。

组合模式分为安全模式透明模式,这是根据接口中是否包含管理对象的方法来区分的。

上面的例子我们举例的是安全模式,在接口中没有声明add()remove()方法管理方法。

这样有一个缺点:组合对象和独立对象不具有相同的接口,客户端调用需要做相应的判断,带来了不便。

另外一种是透明模式,在接口中就声明add()remove()方法。

这样所有的实现类都具备了add()remove(),好处就是组合对象和独立对象具有一致的接口。

但问题也很明显,因为独立对象不具备add()remove()方法的功能,所以实现他是没有意义的。

不管那种模式,都根据实际需要来配置。

2.4 客户端

然后,我们就可以在客户端中使用这个程序。

例如,我们要构建这样一个文件目录结构:

home
├─text1.txt
├─bg1.png
├─film1.mp4
├─source
│  ├─text2.txt

代码就是:

// 创建home目录,并加入三个文件
$dir = new Dir('home');
$dir->add(new TextFile('text1.txt'));
$dir->add(new ImageFile('bg1.png'));
$dir->add(new VideoFile('film1.mp4'));

// 在home下创建子目录source
$subDir = new Dir('source');
$dir->add($subDir);

// 创建一个text2.txt,并放到子目录source中
$text2 = new TextFile('text2.txt');
$subDir->add($text2);

// 打印信息
echo $text2->getName(), '-->', $text2->getSize();
echo '<br />';
echo $subDir->getName(), ' --> ',$subDir->getSize();
echo '<br />';
echo $dir->getName(), ' --> ', $dir->getSize();

可以看到,文件对象(独立对象)获取名称用getName()方法,目录对象(组合对象)用的也是getName()方法。

同样,获取大小用的也都是getSize()方法。

输出的结果为:

文本文件:text2.txt-->10
目录:source --> 10
目录:home --> 320

3 特点

在组合模式中,组合对象和独立对象必须实现一个接口

其中,组合对象必须包含添加和删除节点对象

组合模式通过和装饰模式有着类似的结构图,但是组合模式旨在构造类,而装饰模式重在不生成子类即可给对象添加职责。

并且,装饰模式重在修饰,而组合模式重在表示

组合模式的UML图:

适配器模式

卢哥阅读(105)

适配器模式,即根据客户端需要,将某个类的接口转换成特定样式的接口,以解决类之间的兼容问题。

如果我们的代码依赖一些外部的API,或者依赖一些可能会经常更改的类,那么应该考虑用适配器模式。

下面我们以集成支付宝支付功能为例。

1 问题

假设支付宝支付类的功能如下:

/**
 * 支付宝支付类
 */
class Alipay
{
    public function sendPayment()
    {
        echo '使用支付宝支付。';
    }
}

// 客户端代码
$alipay = new Alipay();
$alipay->sendPayment();

我们直接实例化Alipay类完成支付功能,这样的客户端代码可能很多。

一段时间后,如果支付宝的Alipay类升级,方法名由sendPayment()变成goPayment()会怎样?

所有用了sendPayment()的客户端代码都要改变。

如果Alipay类频繁升级,或者客户端在很多地方使用,这会是极大的工作量。

2 解决

现在我们用适配器模式来解决。

我们在客户端和Alipay类之间加一个中间类,也就是适配器类,转换原始的Alipay为客户端需要的形式。

为让客户端能调用到统一的类方法,我们先定义一个适配器接口:

/**
 * 适配器接口,所有的支付适配器都需实现这个接口。
 * 不管第三方支付实现方式如何,对于客户端来说,都
 * 用pay()方法完成支付
 */
interface PayAdapter
{
    public function pay();
}

因为Alipay类我们无法控制,而且它有可能经常更新,所以我们不对它做任何修改。

我们新建一个AlipayAdapter适配器类,在pay()中转换Alipay的支付功能,如下:

/**
 * 支付宝适配器
 */
class AlipayAdapter implements PayAdapter
{
    public function pay()
    {
        // 实例化Alipay类,并用Alipay的方法实现支付
        $alipay = new Alipay();
        $alipay->sendPayment();
    }
}

客户端使用方式:

// 客户端代码
$alipay = new AlipayAdapter();
// 用pay()方法实现支付
$alipay->pay();

这样,当Alipay的支付方法改变,只需要修改AlipayAdapter类就可以了。

3 适配新类

有了适配器后,扩展也变得更容易了。

继续以上的例子,在支付宝的基础上,我们再增加微信支付,它与支付宝的支付方式不同,必须通过扫码才能支付。

这种情况也应该使用适配器,而不是直接使用微信的支付功能。

代码如下:

/**
 * 微信支付类
 */
class WechatPay
{
    public function scan()
    {
        echo '扫描二维码后,';
    }

    public function doPay()
    {
        echo '使用微信支付';
    }
}

/**
 * 微信支付适配器
 */
class WechatPayAdapter implements PayAdapter
{
    public function pay()
    {
        // 实例化WechatPay类,并用WechatPay的方法实现支付。
        // 注意,微信支付的方式和支付宝的支付方式不一样,但是
        // 适配之后,他们都能用pay()来实现支付功能。
        $wechatPay = new WechatPay();
        $wechatPay->scan();
        $wechatPay->doPay();
    }
}

客户端使用:

// 客户端代码
$wechat = new WechatPayAdapter();
// 也是用pay()方法实现支付
$wechat->pay();

这就是适配器的扩展特性。

我们创建了一个用于处理第三方类(支付宝、微信支付)的方法,

如果它们的API有变化,我们仅需修改客户端依赖的适配器类就可以,不用修改、暴露第三方类本身。

4 UML图

以上适配器模式的代码对应UML如下:

适配器模式UML

注意:适配器模式中,适配器类的名称和创建方式一定是不会频繁改动的。

对于客户端来说,引用适配器类的方式应该是统一而不变的,这才算是正确使用适配器。

5 总结

大的应用都会不断地加入新库和新API。

为避免它们的变更引发问题,应该用适配器模式包装起来,提供应用统一的引用方式。

它会让我们的代码更具结构化,便于管理和扩展。

[创建型模式]单例模式

卢哥阅读(84)

单例模式,正如其名,允许我们创建一个而且只能创建一个对象的类。

这在整个系统的协同工作中非常有用,特别明确了只需一个类对象的时候。

那么,为什么要实现这么奇怪的类,只实例化一次?

在很多场景下会用到,如:配置类Session类Database类Cache类File类等等。

这些只需要实例化一次,就可以在应用全局中使用。

本文我们以数据库类为例。

1 问题

如果没有使用单例模式,会有什么样的问题?

如下是一个简单的数据库连接类,它没有使用单例模式。

class Database
{
    public $db = null;
    public function __construct($config = array())
    {
        $dsn = sprintf('mysql:host=%s;dbname=%s', $config['db_host'], $config['db_name']);
        $this->db = new PDO($dsn, $config['db_user'], $config['db_pass']);
    }
}

然后创建3个对象:

$config = array(
    'db_name' => 'test',
    'db_host' => 'localhost',
    'db_user' => 'root',
    'db_pass' => 'root'
);

$db1 = new Database($config);
var_dump($db1);
$db2 = new Database($config);
var_dump($db2);
$db3 = new Database($config);
var_dump($db3);

这种情况下,每当我们创建一个这个类的实例,就会新增一个到数据库的连接。

开发者每在一个地方实例化一次这个类,就会在那里多一个数据库连接。

不知不觉中,开发者就犯了个错误,给数据库和服务器性能带来巨大的影响。

上面的代码输入如下:

object(Database)[1]
  public 'db' => object(PDO)[2]
object(Database)[3]
  public 'db' => object(PDO)[4]
object(Database)[5]
  public 'db' => object(PDO)[6]

每个对象都分配一个新的资源ID,都是新的引用,它们占用3个的内存空间。

如果有100个对象创建,就会占用内存中100块不同的空间,而其余99块并非是必须的。

2 解决

开发者怎样使用基础框架,如何数据库连接,这很难控制。

如果在代码评审阶段再找出问题,又会浪费大量的人力物力。

要解决这样的问题,我们可以控制住基类,在源头上限制这个类,使其无法生成多个对象,如果已经生成过,直接返回

于是,我们的目标就是,控制数据库类,使其生成一次而且只能生成一次对象。

如下就是单例模式连接数据库代码:

class Database
{
    // 声明$instance为私有静态类型,用于保存当前类实例化后的对象
    private static $instance = null;
    // 数据库连接句柄
    private $db = null;

    // 构造方法声明为私有方法,禁止外部程序使用new实例化,只能在内部new
    private function __construct($config = array())
    {
        $dsn = sprintf('mysql:host=%s;dbname=%s', $config['db_host'], $config['db_name']);
        $this->db = new PDO($dsn, $config['db_user'], $config['db_pass']);
    }

    // 这是获取当前类对象的唯一方式
    public static function getInstance($config = array())
    {
        // 检查对象是否已经存在,不存在则实例化后保存到$instance属性
        if(self::$instance == null) {
            self::$instance = new self($config);
        }
        return self::$instance;
    }

    // 获取数据库句柄方法
    public function db()
    {
        return $this->db;
    }

    // 声明成私有方法,禁止克隆对象
    private function __clone(){}
    // 声明成私有方法,禁止重建对象
    private function __wakeup(){}
}

再通过getInstance()方法使用类对象,

$config = array(
    'db_name' => 'test',
    'db_host' => 'localhost',
    'db_user' => 'root',
    'db_pass' => 'root'
);

$db1 = Database::getInstance($config);
var_dump($db1);
$db2 = Database::getInstance($config);
var_dump($db2);
$db3 = Database::getInstance($config);
var_dump($db3);

输出信息如下:

object(Database)[1]
  private 'db' => object(PDO)[2]
object(Database)[1]
  private 'db' => object(PDO)[2]
object(Database)[1]
  private 'db' => object(PDO)[2]

对比两个输出可以看出,单例模式中,不同对象获得的资源ID是一样的。

也就是说,虽然我们用getInstance()获取Database类对象3次,其实引用的是一个内存空间,PDO也只连接了数据库一次。

以上的例子是数据库连接类,要使用数据库,在应用这样获得连接句柄:

$db = database::getInstance($config)->db();

如果是其他类,则按需要修改数据库相关的代码,单例实现部分保留。

3 特点

单例模式的特点是4私1公一个私有静态属性,构造方法私有,克隆方法私有,重建方法私有,一个公共静态方法。

其他方法根据需要增加。

最基础的单例模式代码如下:

class Singleton
{
    private static $instance = null;

    public static function getInstance()
    {
        if(self::$instance == null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    private function __construct(){}
    private function __clone(){}
    private function __wakeup(){}
}

$instance用以保存类的实例化,getInstance()方法提供给外部本类的实例化对象:

对应的UML图如下,

PHP单例模式

单例模式在应用请求的整个生命周期中都有效,这点类似全局变量,会降低程序的可测试性。

大部分情况下,也可以用依赖注入来代替单例模式,避免在应用中引入不必要的耦合。

所以,对于仅需生成一个对象的类,首先考虑用依赖注入方式,其次考虑用单例模式来实现。

[创建型模式]简单工厂模式

卢哥阅读(86)

工厂模式:顾名思义就是可用生产出物品的工厂这么一个模式,对应到计算机也很好理解,就是一个工厂类,这个工厂类可用生成其他各种各样的类,而这个工厂类就提供这样一种功能

好处:所有的模式都是为了能让代码重用性更高,重构起来更方便而存在,而简单工厂模式省去了在内部有大量的new class的操作

<?php

interface Vehicle
{
    public function drive();
}

class Car implements Vehicle()
{
    public function drive()
    {
        echo '我是一辆小汽车,载人少但是速度很快';
    }
}

class Bus implements Vehicle()
{
    public function drive()
    {
        echo '我是一辆公交车,载人多但是速度慢';
    }
}

class AirPlane implements Vehicle()
{
    public function drive()
    {
        echo '我是一辆飞机,我飞的很快,而且载人更多';
    }
}

class VehicleFactory
{
    public static function build($class_name = null)
    {
        $class_name = ucfirst($class_name);
        if ($class_name !== null && class_exists($class_name)) {
            return new $class_name();
        }
        return null;
    }
}

// 工厂类用了一个静态方法来创建其他类,在客户端中就可以这样使用:

VehicleFactory::build('car')->drive();
VehicleFactory::build('bus')->drive();
VehicleFactory::build('airplane')->drive();

[转载]彻底搞懂 MySQL 事务的隔离级别

卢哥阅读(93)

事前准备数据

mysql> create table city(
    -> id int(10) auto_increment,
    -> name varchar(30),
    -> primary key (id)
    -> )engine=innodb charset=utf8mb4;

insert into city(name) values('武汉市');

mysql> select * from city;
+----+-----------+
| id | name |
+----+-----------+
| 1 | 武汉市 |
+----+-----------+

事务并发可能出现的情况

脏读(Dirty Read)

一个事务读到了另一个未提交事务修改过的数据

从根上理解MySQL事务的隔离级别

会话B开启一个事务,把id=1的name为武汉市修改成温州市,此时另外一个会话A也开启一个事务,读取id=1的name,此时的查询结果为温州市,会话B的事务最后回滚了刚才修改的记录,这样会话A读到的数据是不存在的,这个现象就是脏读。(脏读只在读未提交隔离级别才会出现)

不可重复读(Non-Repeatable Read)

一个事务只能读到另一个已经提交的事务修改过的数据,并且其他事务每对该数据进行一次修改并提交后,该事务都能查询得到最新值。(不可重复读在读未提交和读已提交隔离级别都可能会出现)

从根上理解 MySQL 事务的隔离级别

会话A开启一个事务,查询id=1的结果,此时查询的结果name为武汉市。接着会话B把id=1的name修改为温州市(隐式事务,因为此时的autocommit为1,每条SQL语句执行完自动提交),此时会话A的事务再一次查询id=1的结果,读取的结果name为温州市。会话B再此修改id=1的name为杭州市,会话A的事务再次查询id=1,结果name的值为杭州市,这种现象就是不可重复读。

幻读(Phantom)

一个事务先根据某些条件查询出一些记录,之后另一个事务又向表中插入了符合这些条件的记录,原先的事务再次按照该条件查询时,能把另一个事务插入的记录也读出来。(幻读在读未提交、读已提交、可重复读隔离级别都可能会出现)

从根上理解 MySQL 事务的隔离级别

会话A开启一个事务,查询id>0的记录,此时会查到name=武汉市的记录。接着会话B插入一条name=温州市的数据(隐式事务,因为此时的autocommit为1,每条SQL语句执行完自动提交),这时会话A的事务再以刚才的查询条件(id>0)再一次查询,此时会出现两条记录(name为武汉市和温州市的记录),这种现象就是幻读。

事务的隔离级别

MySQL的事务隔离级别一共有四个,分别是读未提交、读已提交、可重复读以及可串行化。

MySQL的隔离级别的作用就是让事务之间互相隔离,互不影响,这样可以保证事务的一致性。

隔离级别比较:可串行化>可重复读>读已提交>读未提交

隔离级别对性能的影响比较:可串行化>可重复读>读已提交>读未提交

由此看出,隔离级别越高,所需要消耗的MySQL性能越大(如事务并发严重性),为了平衡二者,一般建议设置的隔离级别为可重复读,MySQL默认的隔离级别也是可重复读。

读未提交(READ UNCOMMITTED)

彻底搞懂 MySQL 事务的隔离级别

在读未提交隔离级别下,事务A可以读取到事务B修改过但未提交的数据。

可能发生脏读、不可重复读和幻读问题,一般很少使用此隔离级别。

读已提交(READ COMMITTED)

彻底搞懂 MySQL 事务的隔离级别

在读已提交隔离级别下,事务B只能在事务A修改过并且已提交后才能读取到事务B修改的数据。

读已提交隔离级别解决了脏读的问题,但可能发生不可重复读和幻读问题,一般很少使用此隔离级别。

可重复读(REPEATABLE READ)

彻底搞懂 MySQL 事务的隔离级别

在可重复读隔离级别下,事务B只能在事务A修改过数据并提交后,自己也提交事务后,才能读取到事务B修改的数据。

可重复读隔离级别解决了脏读和不可重复读的问题,但可能发生幻读问题。

提问:为什么上了写锁(写操作),别的事务还可以读操作?

因为InnoDB有MVCC机制(多版本并发控制),可以使用快照读,而不会被阻塞。

可串行化(SERIALIZABLE)

彻底搞懂 MySQL 事务的隔离级别
彻底搞懂 MySQL 事务的隔离级别
彻底搞懂 MySQL 事务的隔离级别
彻底搞懂 MySQL 事务的隔离级别

各种问题(脏读、不可重复读、幻读)都不会发生,通过加锁实现(读锁和写锁)。

彻底搞懂 MySQL 事务的隔离级别
彻底搞懂 MySQL 事务的隔离级别

隔离级别的实现原理

使用MySQL的默认隔离级别(可重复读)来进行说明。

每条记录在更新的时候都会同时记录一条回滚操作(回滚操作日志undo log)。同一条记录在系统中可以存在多个版本,这就是数据库的多版本并发控制(MVCC)。即通过回滚(rollback操作),可以回到前一个状态的值。

假设一个值从 1 被按顺序改成了 2、3、4,在回滚日志里面就会有类似下面的记录。

彻底搞懂 MySQL 事务的隔离级别

当前值是 4,但是在查询这条记录的时候,不同时刻启动的事务会有不同的 read-view。如图中看到的,在视图 A、B、C 里面,这一个记录的值分别是 1、2、4,同一条记录在系统中可以存在多个版本,就是数据库的多版本并发控制(MVCC)。对于 read-view A,要得到 1,就必须将当前值依次执行图中所有的回滚操作得到。

同时你会发现,即使现在有另外一个事务正在将 4 改成 5,这个事务跟 read-view A、B、C 对应的事务是不会冲突的。

提问:回滚操作日志(undo log)什么时候删除?

MySQL会判断当没有事务需要用到这些回滚日志的时候,回滚日志会被删除。

提问:什么时候不需要了?

当系统里么有比这个回滚日志更早的read-view的时候。

查看当前会话隔离级别

方式1

命令:SHOW VARIABLES LIKE 'transaction_isolation';

mysql> show variables like 'transaction_isolation';
+-----------------------+--------------+
| Variable_name  | Value |
+-----------------------+--------------+
| transaction_isolation | SERIALIZABLE |
+-----------------------+--------------+

方式2

命令:SELECT @@transaction_isolation;

mysql> select @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| SERIALIZABLE            |
+-------------------------+

设置隔离级别

方式1:通过set命令

SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL level;
其中level有4种值:
level: {
     REPEATABLE READ
   | READ COMMITTED
   | READ UNCOMMITTED
   | SERIALIZABLE
}
关键词:GLOBAL
SET GLOBAL TRANSACTION ISOLATION LEVEL level;
* 只对执行完该语句之后产生的会话起作用
* 当前已经存在的会话无效
关键词:SESSION
SET SESSION TRANSACTION ISOLATION LEVEL level;
* 对当前会话的所有后续的事务有效
* 该语句可以在已经开启的事务中间执行,但不会影响当前正在执行的事务
* 如果在事务之间执行,则对后续的事务有效。
无关键词
SET TRANSACTION ISOLATION LEVEL level;
* 只对当前会话中下一个即将开启的事务有效
* 下一个事务执行完后,后续事务将恢复到之前的隔离级别
* 该语句不能在已经开启的事务中间执行,会报错的

方式2:通过服务启动项命令

可以修改启动参数transaction-isolation的值

比方说我们在启动服务器时指定了–transaction-isolation=READ UNCOMMITTED,那么事务的默认隔离级别就从原来的REPEATABLE READ变成了READ UNCOMMITTED。

大学主修的是java和c++,为什么写了PHP

卢哥阅读(74)

问:你会java吗?

答:不会

问:你为什么不写Java啊,Java程序员多厉害

答:PHP是世界上最好的语言

至于我为什么写了php,说起来还有一段渊源,我是大学就开始写php了,

那时候大学并没有开设php相关的课程,主要还是为了赚生活费,那时候帮老师或者老师的朋友做网站,基本上一个网站能赚1500-3000块,要知道,10几年前,我一个月生活费,我妈才给我600块啊,当然这也不是我走上PHP这条路的主要原因。大学期间,其实我除了完成了学校的java课程,我还参加了北大青鸟的java培训课程,写java代码应该说问题也不大,那时候花了我整整8000大洋,后来还没去写java,直到现在我还是没跟我妈说过这事,不然她会心疼她的8000大洋扔水里了。

大三开始,我就开始去外面兼职写PHP赚钱,每周去2个全天,然后加上周末,基本上每周3-4天在公司写PHP代码,平时在学校上课,由于学校的计算机考试内容早就已经掌握了,所以计算机的考试,每次都还能考到全班名列前茅,在大学期间,还拿了连续3年的网页设计比赛一等奖,第四年基本不在学校了,不然就拿满了,哈哈。应该说第一份工作决定了我现在是个PHP码农。

后来工作中,也用过比较短时间的sprintboot和go,曾经半年迷上了go语言,我的上司(美国微软回来的清华高材生)强烈建议我去写go,写了go,证明了那句话,再差的程序员都写不出差的go代码,这是真实确切的。我写的第一个go项目,到现在去看代码,还是那么的优雅。

好了,今天周五,写完这篇,下班了。最近加班猛,上周又去医院配了很多调理身体的中药,工作不能停,药也不能停。