Skip to content

递归

02-12月-18

栈有一个很重要的应用:在程序设计语言中实现了递归。我们先来看一个经典的递归例子:斐波那契数列。

斐波那契数列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, … 特点:前面相邻两项之和,构成了后一项

斐波那契数列的实现

  1. 一对兔子在出生两个月后,就有繁殖能力,一对兔子每个月能生出一对小兔子来。假设所有的兔子都不死亡,那么一年以后可以繁殖多少对兔子呢?

    #include <stdio.h>
    
    // 斐波那契的递归函数 
    int Fbi(int i)
    {
        if (i < 2)
            return i == 0 ? 0 : 1;
        return Fbi(i-1) + Fbi(i-2);     
    }
    
    int main()
    {
        int i;
        for (i=1; i<=12; i++)
            printf("%d\n", Fbi(i));
        return 0;
    }
    
  2. 假设有 n 个台阶,每次你可以跨 1 个台阶或者 2 个台阶,请问走这 n 个台阶总共有多少种走法?

    #include <stdio.h>
    
    // 递归实现方式
    int f(int n)
    {
        if (n == 1) 
            return 1;
        if (n == 2)
            return 2;
        return f(n-1) + f(n-2);
    }
    
    // 迭代循环的非递归实现方式
    int f(int n)
    {   
        if (n == 1)
            return 1;
        if (n == 2) 
            return 2;
    
        int ret = 0;
        int pre = 2;
        int prepre = 1;
        int i;      
        for (i=3; i<=n; ++i) {
            ret = pre + prepre;
            prepre = pre;
            pre = ret;
        }
        return ret;
    }
    
    int main()
    {
        printf("%d\n", f(6)); // 6 个台阶总共有 13 种走法
        return 0;
    }
    

    下面分别为问题 1 和 2 的数学函数定义:

斐波那契数列

递归是一种应用非常广泛的算法(或者编程技巧),很多数据结构和算法的编码实现都要用到递归,比如图的深度优先搜索(DFS),二叉树的前中后序遍历递归实现。

递归代码简洁高效,但是也有很多弊端。比如堆栈溢出、重复计算、函数调用耗时多、空间复杂度高等。

区块链入门

17-11月-18

区块链的概念

从技术的角度来讲,区块链就是一种特殊的分布式数据库。

区块链的分类

  • 公有链:比特币、以太坊、EOS。
  • 私有链:公司内部搭建的开发节点、测试节点等。
  • 联盟链:Fabric、R3 联盟、EEA、阳光链。

区块链的应用场景

  • 资产:数字资产发行(ICO)、支付(跨境支付)、交易、结算。
  • 记账:股权交易、供应链金融、商业积分。
  • 不可篡改:溯源(跨境电商商品溯源)、众筹、医疗证明、存在性证明。
  • 点对点:共享经济、物联网。
  • 隐私:匿名交易。

比特币的原理

1. 账本如何验证

哈希函数:Hash(原始信息) = 摘要信息,简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。

哈希函数的特点:

  • 同样的原始信息用同一个哈希函数总能得到相同的摘要信息。
  • 原始信息任何微小的变化都会哈希出面目全非的摘要信息。
  • 从摘要信息无法逆向推算出原始信息。

区块结构如下:

区块结构

第 1 块区块(区块 0)叫做创世区块。除第 1 块区块外,其他区块的 Hash 值 = Hash(上一个区块的 Hash 值 + 当前账本信息)。

2. 账户所有权问题

账户是用一个地址来表示的,转账的过程是把 btc 从一个地址转到另外一个地址。

{
    "付款地址":"2A39CBa2390FDe",
    "收款地址":"AAC9CBa239aFcc",
    "金额":"0.2btc"
}

每个地址都会对应一个私钥,私钥可以通过两次 Hash 运算得到地址,但地址不能反推出私钥。Hash(Hash(fun(私钥))) = 地址

非对称加密技术的应用:

  • 签名:sign(“交易摘要”, “私钥”) = 签名信息
  • 验证:verify(“签名信息”, “付款方地址”) = 交易摘要

3. 为什么记账(挖矿)

记账实际是将交易记录进行 Hash 打包的过程,会消耗计算机资源,那节点为什么要参与记账?

因为完成记账的节点可以获得系统一定数量的 btc 奖励(12.5 个)。

规则:

  • 一段时间内(10 分钟)只能有一人可以记账成功。
  • 通过解决密码学难题(即工作量证明)竞争获得唯一记账权。
  • 其他节点复制记账结果。

工作量证明:Hash(上一个区块的 Hash 值, 交易记录集, 随机数) = Hash 值(必须满足若干个 0 开头)

第一个完成工作量证明的节点有优先的记账权,唯一的记账权就可以打包区块。

4. 共识机制

两个节点同时完成工作量证明:每个节点只认可累计工作量最大的链。

网络精选题

09-11月-18

Q:某用户的计算机通过以太网连入互联网,该用户在浏览器的地址栏中输入了某网站的地址,并按下回车键,随后看到了该网站的主页。请依据 TCP/IP 参考模型列出该通信过程所涉及的网络协议,并写出各个协议的作用。

  1. 应用层

    • HTTP:客户端浏览器和 Web 服务器之间的应用层通信协议。
    • DNS:域名解析服务,提供将主机名解析成 IP 地址服务。
  2. 传输层

    • TCP:用于在不可靠的因特网上提供可靠地、端对端的字节流通信的协议。
  3. 网络层

    • IP:提供了不可靠的、无连接的数据报传输机制。
    • ICMP:提供差错报告和请求应答服务。
    • ARP:提供将 IP 地址解析成以太网 48bit 地址。
  4. 网络接口层

    • IEEE802.3:是一种局域网标准协议,使用 CSMA/CD 为以太网提供介质访问控制方法。

依赖注入

12-10月-18

出自维基百科 Wikipedia 中的 依赖注入

依赖注入是一种允许我们从硬编码的依赖中解耦出来,从而在运行时或者编译时能够修改的软件设计模式。

我们可以用一个简单的例子来说明依赖注入的概念。

下面的代码中有一个 Database 的类,它需要一个适配器来与数据库交互。我们在构造函数里实例化了适配器,从而产生了耦合。这会使测试变得很困难,而且 Database 类和适配器耦合的很紧密。

<?php
namespace Database;

class Database
{
    protected $adapter;

    public function __construct()
    {
        $this->adapter = new MySqlAdapter;
    }
}

class MysqlAdapter {}

这段代码可以用依赖注入重构,从而解耦。

<?php
namespace Database;

class Database
{
    protected $adapter;

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

class MysqlAdapter {}

现在,我们以参数的形式向 Database 类传递其依赖的对象,而不是让它自己产生所依赖的对象。我们甚至可以创建一个方法(method),让这个方法可以接受依赖对象作为参数并在内部设置依赖关系,或者,如果 $adapter 属性本身是 public 的,我们可以直接给它赋值。

控制反转

你可能见过 “控制反转”(Inversion of Control) 或者 “依赖反转准则”(Dependency Inversion Principle)这种说法。顾名思义,一个系统通过组织控制和对象的完全分离来实现 “控制反转”。对于依赖注入,这就意味着通过在系统的其他地方控制和实例化依赖对象,从而实现了解耦。

一些 PHP 框架很早以前就已经实现控制反转了,但是问题是,应该反转哪部分以及到什么程度?比如,MVC 框架通常会提供超类或者基本的控制器类以便其他控制器可以通过继承来获得相应的依赖。这就是控制反转的例子,但是这种方法是直接移除了依赖而不是减轻了依赖。

依赖注入允许我们通过按需注入的方式更加优雅地解决这个问题,完全不需要任何耦合。

依赖反转准则

依赖反转准则是面向对象设计准则 S.O.L.I.D 中的 “D”,倡导 “依赖于抽象而不是具体”。简单来说就是依赖应该是接口/约定或者抽象类,而不是具体的实现。我们能很容易重构前面的例子,使之遵循这个准则。

<?php
namespace Database;

class Database
{
    protected $adapter;

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

interface AdapterInterface {}

class MysqlAdapter implements AdapterInterface {}

现在 Database 类依赖于接口,相比依赖于具体实现有更多的优势。

假设你工作的团队中,一位同事负责设计适配器。在第一个例子中,我们需要等待适配器设计完之后才能单元测试。现在由于依赖是一个接口/约定,我们能轻松地模拟接口测试,因为我们知道同事会基于约定实现那个适配器

这种方法的一个更大的好处是代码扩展性变得更高。如果一年之后我们决定要迁移到一种不同的数据库,我们只需要写一个实现相应接口的适配器并且注入进去,由于适配器遵循接口的约定,我们不需要额外的重构。

容器

你应该明白的第一件事是依赖注入容器和依赖注入不是相同的概念。容器是帮助我们更方便地实现依赖注入的工具,但是他们通常被误用来实现反模式设计 Service Location 。把一个依赖注入容器作为 Service Locator 注入进类中隐式地建立了对于容器的依赖,而不是真正需要替换的依赖,而且还会让你的代码更不透明,最终变得更难测试。

大多数现代的框架都有自己的依赖注入容器,允许你通过配置将依赖绑定在一起。这实际上意味着你能写出和框架层同样干净、解耦的应用层代码。

注:本文非原创,原文出自 PHP 编程之道之依赖注入

一个面向对象留言本的实例

04-10月-18

用面向对象的思想完成一个简单的留言本模型,这个模型不涉及实际的数据库操作以及界面显示,只是一个 demo,用来演示面向对象的一些思维。

在面向过程的思维里,要设计一个留言本,一切都将以留言本为核心,抓到什么是什么,按流程走下来,即按用户填写信息→留言→展示的流程进行。

在面向对象的世界,会想尽办法把肉眼能看见的以及看不见的,但实际存在的物或者流程抽象出来。既然是留言本,那么就存在留言内容这个实体,这个留言实体应该包括留言者的姓名、E-mail、留言内容等要素,如下所示(留言实体类 message.php)。

<?php
class message 
{
    public $name;    // 留言者姓名
    public $email;   // 留言者联系方式
    public $content; // 留言内容    

    public function __set($name, $value)
    {
        $this->$name = $value;
    }

    public function __get($name)
    {
        if (! isset($this->$name)) {
            $this->$name = null;
        }
    }       
}

然后,需要一个留言本模型,这个留言本模型包括留言本的基本属性和基本操作,如下所示(留言本模型 gbookModel.php)。

<?php
/**
 * 留言本模型,负责管理留言本
 * @author asus
 *
 */
class gbookModel
{
    private $bookPath; // 留言本文件
    private $data;     // 留言数据

    public function setBookPath($bookpath)
    {
        $this->bookPath = $bookpath;
    }

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

    public function open(){}

    public function close(){}

    public function read()
    {
        return file_get_contents($this->bookPath);
    }

    // 写入留言
    public function write($data)
    {
        $this->data = self::safe($data)->name . '&' . self::safe($data)->email . '\r\nsaid:\r\n' . self::safe($data)->content;
        return file_put_contents($this->bookPath, $this->data, FILE_APPEND); // 第三个参数代表,如果文件 filename 已经存在,追加数据而不是覆盖
    }

    // 模拟数据的安全处理,先拆包再打包
    public static function safe($data)
    {
        $reflect = new ReflectionObject($data);
        $props = $reflect->getProperties();
        $messagebox = new stdClass();
        foreach ($props as $prop) {
            $ivar = $prop->getName();
            $messagebox->$ivar = trim($prop->getValue($data));
        }
        return $messagebox;
    }

    public function delete()
    {
        file_put_contents($this->bookPath, '');
    }
}

实际留言的过程可能会更复杂,可能还包括一系列准备工作以及 Log 处理,所以应定义一个类负责数据的逻辑处理,如下所示(留言本业务逻辑处理 leaveModel.php)。

<?php
class leaveModel
{
    public function write(gbookModel $gb, $data)
    {
        $book = $gb->getBookPath();
        $gb->write($data);
        // 记录日志
    }
}

最后,通过一个控制器,负责对各种操作的封装,这个控制器是直接面向用户的,所以包括留言本查看、删除、留言等功能。可以形象理解为这个控制器就是留言本所提供的直接面向使用者的功能,封装了操作细节,只需要调用控制器的相应方法即可,如下所示(前端控制部分代码)。

<?php
class authorControl
{
    // 在留言本上留言
    public function message(leaveModel $l, gbookModel $g, message $data)
    {
        $l->write($g, $data);
    }

    // 查看留言本内容
    public function view(gbookModel $g)
    {
        return $g->read();
    }

    public function delete(gbookModel $g)
    {
        $g->delete();
    }
}

测试代码如下:

<?php   
$message = new message();
$message->name = 'phper';
$message->email = 'phper@php.net';
$message->content = 'a crazy phper love php so much.';
// 新建一个留言相关的控制器
$gb = new authorControl();
// 拿出笔
$pen = new leaveModel();
// 翻出笔记本
$book = new gbookModel();
$book->setBookPath("./a.txt");
$gb->message($pen, $book, $message);
echo $gb->view($book);

Crontab 定时任务命令

03-10月-18

检查 crontab 工具是否安装:crontab -l

检查 crond 服务是否启动:service crond status

命令基本形式:分 小时 日 月 星期 命令

// 每晚的 21:30 重启 apache

30 21 * * * service httpd restart

// 每月 1、10、22 日的 4:45 重启 apache

45 4 1,10,22 * * service httpd restart

// 每月 1 到 10 日的 4:45 重启 apache

45 4 1-10 * * service httpd restart

// 每隔两分钟重启 apache 

*/2 * * * * service httpd restart

// 每隔两小时重启 apache 

0 */2 * * * service httpd restart

// 晚上 11 点到早上 7 点之间,每隔一小时重启 apache

0 23-7/1 * * * service httpd restart

// 每天 18:00 至 23:00 之间每隔 30 分钟重启 apache

0,30 18-23 * * * service httpd restart

0-59/30 18-23 * * * service httpd restart

// crontab 的日志
cd /var/log
ls -l cron*
tail -f cron    // cron 代表当天执行的定时任务日志

Git 常用命令

30-9月-18

Git 和其它版本控制系统(包括 Subversion 和近似工具)的主要差别在于 Git 对待数据的方法。 概念上来区分,其它大部分系统以文件变更列表的方式存储信息。 这类系统(CVS、Subversion、Perforce、Bazaar 等等)将它们保存的信息看作是一组基本文件和每个文件随时间逐步累积的差异。

Git 更像是一个小型的文件系统,提供了许多以此为基础构建的超强工具,而不只是一个简单的 VCS。

// 列出所有配置
git config --list
// 设置用户名称与邮件地址
git config --global user.name "John Doe"
git config --global user.email johndoe@example.com

// 新建一个分支并同时切换到那个分支
git checkout -b branch_name
// 删除本地分支
git branch -d branch_name
// 删除远程分支
git push origin --delete branch_name

// 删除本地标签
git tag -d tagname
// 删除远程标签
git push origin :refs/tags/tagname

// 取消暂存
git reset HEAD 123.php
// 撤销工作目录中所有未提交文件的修改内容
git reset --hard HEAD 
// 从暂存区域移除,但仍然希望保留在当前工作目录中
git rm --cached 123.php

// 查看提交历史
git log -p -2   // 选项 -p,用来显示每次提交的内容差异。 -2 显示最近两次提交
// 行日志搜索
git log -L :db_check_user_pw:bbscommon.fun.php

// 尝试重新提交
git commit --amend

// 查看哪些分支已经合并到当前分支
git branch --merged
// 查看尚未合并到当前分支的分支
git branch --no-merged

// 储藏修改
git stash
git stash save "文件上传" 
// 查看储藏的东西
git stash list
// 将最近的储藏工作重新应用
git stash apply

// 清理工作目录(移除没有忽略的未跟踪文件)
git clean -n    // 使用 -n 选项来运行命令,这意味着 “做一次演习然后告诉你将要移除什么”

// 查看尚未暂存的文件更新了哪些部分
git diff
// 查看已经暂存起来的变化
git diff --cached
// 查看系统支持哪些 Git Diff 插件
git difftool --tool-help 

// 查看远程仓库 origin
git remote show origin
// 使用远程分支覆盖本地工作目录
git reset --hard origin/dev

// 示例操作 
victor.zhu@VICTOR-ZHU MINGW64 /e/shejiben (master)
$ git fetch --tag   
$ git merge victor-opt
$ git tag | tail -2
$ git tag -a shejiben-6.5.2.0-201805311128-victor -m "小程序落地页"   
$ git push origin master --tag

PHP 内核中的 HashTable 分析

04-7月-18

HashTable 是 PHP 的灵魂,因为在 Zend 引擎中大量地使用了 HashTable,如变量表、常量表、函数表等,这些都是使用 HashTable 保存的,另外 PHP 的数组也是使用 HashTable 实现的。

PHP 内核 HashTable 的数据结构

PHP 中的 HashTable 实现代码保存在 Zend/zend_hash.hZend/zend_hash.c 这两个文件中,首先看 HashTable 的数据结构定义:

// Zend/zend_hash.h
#include <stdio.h>

typedef struct bucket {
    ulong h;                    // 保存经过 hash 函数处理之后的 hash 值
    uint nKeyLength;            // 保存索引(key)的长度
    void *pData;                // 指向要保存的内存块地址
    void *pDataPtr;             // 保存指针数据
    struct bucket *pListNext;   // 指向双向链表的下一个元素
    struct bucket *pListLast;   // 指向双向链表的上一个元素
    struct bucket *pNext;       // 指向具有同一个 hash 值的下一个元素
    struct bucket *pLast;       // 指向具有同一个 hash 值的上一个元素
    char arKey[1];              // 保存索引(key),而且必须为最后一个成员

} Bucket;

typedef struct _hashtable {
    uint nTableSize;            // 记录 Bucket 数组的大小
    uint nTableMask;            
    uint nNumOfElements;        // 记录 HashTable 中保存元素的个数
    ulong nNextFreeElement;     // 下一个可用 Bucket 位置
    Bucket *pInternalPointer;   // 遍历 HashTable 元素
    Bucket *pListHead;          // 双向链表表头
    Bucket *pListTail;          // 双向链表表尾
    Bucket **arBuckets;         // Bucket 数组
    dtor_func_t pDestructor;
    zend_bool persistent;
    unsigned char nApplyCount;
    zend_bool bApplyProtection;
} HashTable;

在上面的数据结构定义中看到,Bucket 和 HashTable 这两个结构体构成一个完整的 HashTable

Bucket 结构体分析

PHP 的 HashTable 同时维护一个双向链表,用于有序遍历 HashTable 里的元素

在这些成员变量中,pData 和 pDataPtr 比较容易混淆。pData 指向的是想要保存的内存块地址,一般是通过 malloc 之类的系统调用分配出来。但是有时候只想保存一个指针,如果这样也去调用 malloc 分配内存的话就会造成很多细小的内存块,从而导致产生内存碎片。这种情况 PHP 内核是不能容忍的,所以出现了 pDataPtr 指针。pDataPtr 的作用就是当想要保存的数据是一个指针类型的数据时,就直接保存到 pDataPtr 成员变量中,而不用调用 malloc 分配内存,从而防止内存碎片的产生,然后 pData 直接指向 pDataPtr。当要保存的数据不是指针时,pDataPtr 被设置成 NULL。

HashTable 结构体分析

一个 Bucket 只能保存一个数据,而 HashTable 的目的就是通过索引(key)把每个元素分散到一个唯一的位置(当没有冲突时),这是怎么做到的呢?答案是 通过 hash 算法把索引处理成一个 int 的数,然后定位到一个 Bucket 数组的其中一个元素中,如图所示。

HashTable 原理

字符串索引经过 hash 函数处理后,返回一个值为 1 的 int 索引,然后从 Bucket 数组中取得索引为 1 的元素(第二个元素),这就是 HashTable 的原理和实现方法。

PHP 内核通过 HashTable 结构管理 Bucket 数组。arBuckets 中保存的就是上图所示的 Bucket 数组,而 nTableSize 记录 arBuckets 数组的大小。

// PHP 内核是怎样通过一个字符串索引定位到 Bucket 数组(arBuckets)中的一个元素的,代码如下:
h = hash(key);
pos = h % nTableSize;
bucket = arBuckets[pos]

int hash(char *key) {
    int h = 0;
    char *p = key;
    while (*p) {
        h += *p;
        p++;
    }
    return h;
}

// 上面的 hash 算法只展示了 hash 函数是怎么把一个字符串处理成一个整数,实际上 PHP 内核的 hash 算法会更复杂。

值得注意的是 nNumOfElements 与 nTableSize 的区别。

Bucket 存储数据示意图

pListHead 和 pListTail 分别是双向链表的表头和表尾指针。传统的 HashTable 是不会同时维护双向链表的。下图展示了这个双向链表。

HashTable 中的双向链表

MySQL 索引知识点

17-6月-18
  1. 存储引擎负责 MySQL 中数据的存储和提取。

  2. 索引是存储引擎用于快速找到记录的一种数据结构。

  3. 索引可以包含一个或多个列的值。如果索引包含多个列,那么列的顺序也十分重要,因为 MySQL 只能高效地使用索引的最左前缀列。

  4. 有时候需要索引很长的字符列,这会让索引变得大且慢。对于 BLOBTEXT 或者很长的 VARCHAR 类型的列,必须使用前缀索引,因为 MySQL 不允许索引这些列的完整长度。

    mysql> ALTER TABLE sakila.city_demo ADD INDEX (city(7));
    
  5. 索引合并策略 type: index_merge 有时候是一种优化的结果,但实际上更多时候说明了表上的索引建得很糟糕:

    • 当出现服务器对多个索引做相交操作时 Extra: Using intersect(film_id,actor_id)(通常有多个 AND 条件),通常意味着需要一个包含所有相关列的多列索引,而不是多个独立的单列索引。

    • 当服务器需要对多个索引做联合操作时 Extra: Using union(film_id,actor_id)(通常有多个 OR 条件),通常需要耗费大量 CPU 和内存资源在算法的缓存、排序和合并操作上。

  6. 如果一个索引包含所有需要查询的字段的值,我们就称之为 覆盖索引 Extra: Using index

  7. MySQL 可以使用同一个索引既满足排序,又用于查找行。因此,如果可能,设计索引时应该尽可能地同时满足这两种任务。如果 EXPLAIN 出来的 type: index,则说明 MySQL 使用了索引扫描来做排序。

    • 只有当索引的列顺序和 ORDER BY 子句的顺序完全一致,并且所有列的排序方向都一样时,MySQL 才能够使用索引来对结果做排序。

    • ORDER BY 子句和查找型查询的限制是一样的:需要满足索引的最左前缀的要求;否则,MySQL 都需要执行排序操作,而无法利用索引排序。有一种情况下 ORDER BY 子句可以不满足索引的最左前缀的要求,就是前导列为常量的时候。

      # 例如,KEY rental_date(rental_date,inventory_id,customer_id)
      
      # 可以使用 rental_date 索引做排序的查询示例:
      ... WHERE rental_date = '2005-05-25' ORDER BY inventory_id, customer_id;
      ... WHERE rental_date = '2005-05-25' ORDER BY inventory_id DESC;
      ... WHERE rental_date > '2005-05-25' ORDER BY rental_date, inventory_id; // ORDER BY 使用的两列就是索引的最左前缀
      
      # 不可以使用 rental_date 索引做排序的查询示例:
      ... WHERE rental_date = '2005-05-25' ORDER BY inventory_id DESC, customer_id ASC; // 使用了两种不同的排序方向
      ... WHERE rental_date = '2005-05-25' ORDER BY inventory_id, staff_id; // 引用了一个不在索引中的列
      ... WHERE rental_date = '2005-05-25' ORDER BY customer_id; // 无法组合成索引的最左前缀
      ... WHERE rental_date > '2005-05-25' ORDER BY inventory_id, customer_id; // 索引列的第一列上是范围条件,所以 MySQL 无法使用索引的其余列       
      
  8. 理想情况下扫描的行数和返回的行数应该是相同的。

  9. EXPLAIN 语句中的 type 列反应了访问类型。访问类型有很多种,从全表扫描到索引扫描、范围扫描、唯一索引查询、常数引用等。

  10. 查询优化、索引优化、库表结构优化需要齐头并进,一个不落。

MySQL 查询执行过程

03-6月-18

当希望 MySQL 能够以更高的性能运行查询时,最好的办法就是弄清楚 MySQL 是如何优化和执行查询的。

当向 MySQL 发送一个请求的时候,MySQL 到底做了些什么:

  1. 客户端发送一条查询给服务器。

  2. 服务器先检查查询缓存,如果命中了缓存,则立刻返回存储在缓存中的结果。否则进入下一阶段。

  3. 服务器端进行 SQL 解析、预处理,再由优化器生成对应的执行计划。

    • 首先,MySQL 通过关键字将 SQL 语句进行解析,并生成一颗对应的“解析树”。MySQL 解析器将使用 MySQL 语法规则验证和解析查询。例如,它将验证是否使用错误的关键字,或者使用关键字的顺序是否正确等,再或者它还会验证引号是否能前后正确匹配。
    • 预处理器则根据一些 MySQL 规则进一步检查解析树是否合法,例如,这里将检查数据表和数据列是否存在,还会解析名字和别名,看看它们是否有歧义。下一步预处理器会验证权限。
    • 现在语法树被认为是合法的了,并且由优化器将其转化成执行计划。一条查询可以有很多种执行计划,最后都返回相同的结果。优化器的作用就是找到这其中最好的执行计划。
  4. MySQL 根据优化器生成的执行计划,调用存储引擎的 API 来执行查询。

  5. 将结果返回给客户端。如果查询可以被缓存,那么 MySQL 在这个阶段也会将结果存放到查询缓存中。

扩展:

1. 分解关联查询的好处?

  • 对 MySQL 的查询缓存来说,如果关联中的某个表发生了变化,那么就无法使用查询缓存了,而拆分后,如果某个表很少改变,那么基于该表的查询就可以重复利用查询缓存结果了。
  • 将查询分解后,执行单个查询可以减少锁的竞争。

2. 优化 LIMIT 分页。

  • 在偏移量非常大的时候(翻页到非常靠后的页面),例如可能是 LIMIT 10000,20 这样的查询,这时 MySQL 需要查询 10020 条记录然后只返回最后 20 条,前面 10000 条记录都将被抛弃。
  • LIMIT 和 OFFSET 的问题,其实是 OFFSET 的问题,它会导致 MySQL 扫描大量不需要的行然后再抛弃掉。如果可以记录上次取数据的位置,那么下次就可以直接从该书签记录的位置开始扫描,这样就可以避免使用 OFFSET。

    mysql> SELECT * FROM sakila.rental ORDER BY rental_id DESC LIMIT 20;
    
    // 假设上面的查询返回的是主键为 16049 到 16030 的记录,那么下一页查询就可以从 16030 这个点开始:        
    mysql> SELECT * FROM sakila.rental WHERE rental_id < 16030 ORDER BY rental_id DESC LIMIT 20;
    
    * 该技术的好处是无论翻页到多么后面,其性能都会很好。