zhangrxiang

zhangrxiang 查看完整档案

无锡编辑无锡科技职业技术学院  |  软件与服务外包专业 编辑上海君众有限公司  |  Linux C开发工程师 编辑 github.com/zhangrxiang 编辑
编辑

难能可贵的品质是坚持

个人动态

zhangrxiang 关注了标签 · 2020-07-18

rust

一门赋予每个人构建可靠且高效软件能力的语言。

关注 473

zhangrxiang 赞了回答 · 2019-05-17

解决php 需要对变量中的指定字符串进行替换

<?php
$tpl['from'] = 'http://m.def.com/';
$tpl['ads'] = '你好';
$url = 'http://m.abc.com/?aa=123&bb=dddd-[from]-[ads]';

$url = preg_replace_callback('/\[(.*?)\]/', function ($matches) use ($tpl) {
  return $tpl[$matches[1]];
}, $url);

关注 3 回答 4

zhangrxiang 赞了文章 · 2018-11-22

菜鸟的 GIS 基本概念学习

因为一些缘故,工作中了解了一下 GIS。本文算是菜鸟的学习笔记吧,如有错误,衷心希望专业的 GIS 同学指正~

本文章采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。

原文最早发布于:https://cloud.tencent.com/developer/article/1151521,同为本人的专栏。


Reference


什么是 GIS

GIS 就是 Geographic Information System,地理信息系统。这算是一门地理学和计算机科学的交叉学科。在大学里面这可以是一个专门的专业,一般开在地理系下面。

维基百科上的定义是:

“_GIS是一个设计用来捕获、存储、操作(manipulate)、分析、管理和展示空间或图像数据的系统。_”

GIS能够将我们日常相关的信息以空间信息的形式,在地图上展示,有了这些内容之后,我们就可以做很多事情。

概念上看起来很复杂,但是简单而言,广义上,如果一个系统包含地图、与地图相关的数据、能够展示这些数据,而且能够使用这些数据,协助我们处理某些问题,那么这就算是一个GIS系统。

日常生活中最常见的可以归结为GIS系统的如下:

  • 导航系统(纯粹的电子地图不算):导航系统包含地图、地图上面的各个定位点信息,并且我们用来完成一个目标:规划一条最优路线,从一个地方到达另一个地方
  • 类似于美团之类的消费推荐系统:系统包含了地图,地图上包含了各个商家的地理位置、评价、菜谱等信息。我们使用这个系统获得商家的信息和推荐,协助我们决定如何消费。

上面是 to C 的应用场景。To B 业务在日常生活中我们则比较少直接接触到。Esri(ArcGIS 的开发商)给的例子如下:

  • 政府部门利用在线地图对台风、洪水或传染病等灾害进行预警和应急指挥
  • 公共事业服务部门通过在线和移动地图迅速找到管道爆裂的位置,并准确的指挥现场工作人员该关闭哪个阀门和在什么位置进行挖掘维修
  • 商业公司利用在线地图和空间上的商业分析,找到最好的分店位置,分析客户的消费习惯,并能有针对性的发送促销信息

GIS 中的相关概念

GIS 应用 = 基础底图 + 操作图层 + 任务

  • 基础底图:可以简单地理解为就是地图,并且要求是一个不会频繁改变的地图。
  • 操作图层:用户使用的主要图层,承载主要的待编辑、展示和分析的信息数据。
  • 任务:如导航、地址编码等计算量比较大的分析任务。

这里以 ArcGIS 的两个入门教程为例。两个入门教程分别是 Venice Acqua Alta 和 Egmont National Park。

基础底图

比如下面的威尼斯地图里,系统设置的基础底图,是 ArcGIS 网站上的一张世界投影地图。

把界面缩到最小比例尺就可以看出来:

而另一个教程 Egmont 国家公园,使用的则是区域地图:

一个工程中可以有多个基础底图。上述两个地图中均包含 2D 和 3D 的地图和相应的图层。

操作图层

我们回到 Venice Acqua Alta 的主界面。我们看左边的 “绘制顺序” 信息,包含了以下三个可以下拉的内容,分别可以对应三个操作图层

  1. Landmarks:地标——在工程中这里包含了威尼斯几个热门旅行点的信息,可以点开看到描述信息。系统中以原点表示
  2. Canals:运河——在系统中,以红色线段表示
  3. Structures:建筑——在系统中,以青色多边形表示。

下图中,把其中的一个地标信息点开了:

任务

这里以 Venice Acqua Alta 为例,其任务就是使用其 3D 地图图层中的水位数据图层,进行威尼斯涝季城市水位分析。笔者在这里没有具体深入研究,因为不同的任务,其操作方式多种多样,这里只作为记录。


GIS 数据和协议

由于 ArcGIS 在行业内的龙头地位,其指定的一系列数据格式和交换协议,成为 GIS 业界的标准协议。目前制定 GIS 协议的标准化组织为开放地理空间协会(OGC),其制定的规范称为 OpenGIS(Open Geodata Interoperation Specification)。

相关的接口和协议非常多,以后再一一补齐。目前大部分 demo 系统,导入的数据格式为 shp 文件,往往是由甲方直接提供、或者是网上的公开 GIS 服务提供商可提供下载的,不需要我们自行转换数据格式。ArcGIS 可以直接识别并导入,入库后也使用标准接口协议开放服务,比如 WMS(网络地图服务)和 WFS(网络要素服务)。


ArcGIS 软件

ArcGIS Pro

ArcGIS Pro 是单机版的 ArcGIS 系统,这就纯粹用于需要在本地进行数据分析的应用场景。相比起 Web GIS,主要是少了 web 功能,便于建立 GIS 系统,适合企业内部进行快速的数据分析,但是不适合网络分享或者多地展示和部署。

学习 GIS 基本概念,可以从 ArcGIS Pro 开始,这个软件提供了 21 天的免费试用期。

ArcGIS for Server

ArcGIS for Server 是一个 Web GIS。

Web GIS 顾名思义,就是运行在 web 页面的 GIS。当然只是终端的数据呈现在 web 层面。对于 ArcGIS,就是 ArcGIS for Server

Server 的部署和我们普通的服务部署非常像,大块可以分为 web server、GIS server、GIS database server 三大块。


免费开源 GIS 软件

开源 GIS 软件,从大类来分,包含服务器版和桌面版(即桌面版)。显然我们最关注的是服务器版的 GIS 软件 / 组件。

服务器版的部署架构和 ArcGIS 基本类似,也是分 web、GIS Server、database 三大模块,并且可以分开部署。

开源 GIS Server

主流开源 GIS Server 可以参考 OSGeo-Live 的 “网络服务” 项:

  1. MapServer 免费版
  2. 明尼苏达大学开发
  3. 纯 C,效率高
  4. 跨系统支持:Windows、Linux、macOS
  5. 支持绝大部分 GIS
  6. 没有内置 ajax 支持
  7. GeoServer
  8. 基于 JDK,效率较低
  9. 跨系统支持:Windows、Linux、macOS
  10. Orracle 发布地理数据时经常使用,原因未知
  11. 也有帖子提到 GeoServer 的功能最全,不过并没有专门看到相关的资料说明

开源 GIS Database Server

GIS 数据库相对而言没有 server 那样五花八门,主要是围绕着数据应用来划分成几类:

  1. PostgreSQL:读作 “post-gress-Q-L”,是一个自由的 “对象 - 关系型” DB 服务器。
  2. PostGIS:这是为 PostgreSQL 设计的扩展,用于支持存储、查询和修改空间关系的能力。为 GIS 向量和关系数据量身定做。
  3. PostGIS Raster:正在开发中,为 PostGIS 支持栅格数据——貌似已经开发完成了,但是网上资料还不多。
  4. SpatiaLite:具有空间数据功能的 SQLite 数据库系统。优势:比较接近 SQL;SpatiaLite-GUI 提供有好的 GUI;但是劣势是:资料少
  5. Rasdaman:多维栅格数据库。目前是开源项目中支持栅格数据的直接方案
  6. pgRouting:主要用于路径规划

开源 Web 组件

这里主要指的是一些开源的 Javascript API,使用这些 API 来实现地图的呈现。由于 Javascript 不是笔者擅长的内容,因此了解不多。这里只列出其中 GeoServer 自带的一个 JavaScript 库:OpenLayer,GeoServer 的管理页面中,针对每个图层,都会给出 OpenLayer 的例子,非常便于快速搭建。

桌面 GIS

桌面 GIS 从我们的应用来看并非毫无用处,主要可以用来预览、修改 GIS 数据。桌面 GIS 软件很多,这里列出几个推荐的:

QGISGRASSuDig
界面友好,接近 ArcGIS美国军方开发基于 eclipse,慢,耗内存
对 WMS 支持好界面差,需要很多手动命令操作简单,支持多种数据库
无缝集成 PostGIS分析功能弱
分析功能强,照搬 GRASS 的分析功能

开源 GIS 部署建议

国内做 toG(to 政府) 的 Web GIS 项目的时候,经常是使用 ArcGIS + Oracle + Web server 的模式来制作,成本很高(ArcGIS 需要一笔授权费)。之所以这么做,结合一些帖子的说法,整理了一些的 “中国特色” 的原因:

  1. 项目交期短,要求尽快出成果
  2. 与政府打交道的不少公司,研发能力和项目管控能力较弱(相对主流 IT 公司而言),使用 ArcGIS 不用太多研发投入,很快就可以出效果
  3. 政府网站很少被黑(违法成本高、信息价值低)、流量也低,不用花太多精力去维护和容灾,因此只需要一份 ArcGIS 授权
  4. 政府项目资金一般不会太缺,足以购买和外包 ArcGIS 解决方案

当然还有一些比较通用的原因啦:

  1. ArcGIS 是商业 GIS 的领头人,用得多很正常
  2. Esri 是业界冠楚,GIS 研发能力强,既提供 PaaS 服务也提供 SaaS 服务,售前售后团队一流
  3. 不论国内外,Esri 都早早进入大学,为大学科研提供大量免费的软件和技术服务支持。这样一来,大学生走出校园后,基本上只会用 ArcGIS,并且还用得非常熟——不得不说这是非常高明的商业做法

实际上,由于不少 GIS 项目功能是比较简单的,因此使用如上的开源 GIS 完全可以完成相应的内容。比较典型的搭配是:

  • GIS Server:GeoServer / MapServer
  • DB Server:PostgreSQL (PostGIS)
  • Web Server:Tomcat
  • 反向代理:nginx,特别是对于 GeoServer 和 Tomcat 部署在同一台机器的情况而言,使用 nginx 进行反向代理是非常舒服的

国产 GIS

最近中美贸易战风风火火,而 Esri 作为一家美国公司,个人猜测可能会受到 toB 和 toG 客户一定程度上的排斥。除了采用开源 GIS 之外,据我了解,还有不少 GIS 开发公司采用国产的 GIS 产品。这方面我还没了解,等以后有机会了再学习学习吧~


本文章采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。

原文最早发布于:https://cloud.tencent.com/developer/article/1151521,同为本人的专栏。

查看原文

赞 17 收藏 15 评论 0

zhangrxiang 赞了文章 · 2018-11-18

MySql之sql执行过程

图片描述

概述

MySQL是一个关系型数据库管理系统,由瑞典MySQL AB 公司开发,目前属于 Oracle 旗下产品。MySQL 是最流行的关系型数据库管理系统之一,在 WEB 应用方面,MySQL是最好的 RDBMS (Relational Database Management System,关系数据库管理系统) 应用软件。

执行过程

编写Sql语句基本是程序开发中的日常,执行Sql在MySql中都经历了哪些过程呢?例如:select * from user where id=1

1.连接器

负责与客户端建立连接、获取权限、位置和管理连接。
  1. 连接命令:

     mysql -h$ip -P$port -u$user -p
  2. 环节:

      1. TCP握手       
      2. 认证你的身份
      3. 获取权限
    

2.查询缓存

当连接建立完成后就开始执行 select 语句,执行逻辑就会来到第二步:查询缓存

MySql 在执行查询时会先对查询缓存进行查询,是否之前执行过此查询,之前执行过的语句会以key-value形式,被直接缓存在内存当中key为语句,value是查询结果,查询后2种结果:

 1. 查询缓存命中:直接返回结果(效率很高)
 2. 查询缓存未命中: 继续直行后面的阶段,执行完成后,执行结果会被存入查询缓存中
  1. 大多数情况下我会建议你不要使用查询缓存,为什么呢?因为查询缓存往往利大于弊

    查询缓存的失效非常频繁,只要有对一个表的更新,整个表上的查询缓存都会被清空,对于更新频繁的表使用查询缓存命中率就会很低,对于更新比较少的静态表则很适用。
  2. 按需使用:

    将MySQL参数 query_cache_type 设置为 DEMAND 这样SQL语句都不会使用缓存,对于需要使用查询缓存的查询语句可以用SQL_CACHE显示指定。(MySQL8.0 已经将查询缓存模块移除)
    mysql> select SQL_CACHE * from user where id=1

3.分析器

如果没有命中查询缓存,就要开始真正执行语句了。

首先 Mysql 需要 知道你要执行什么,所以要对sql语句做解析

  • 词法分析

     你输入的是由多个字符串和空格组成的一条 SQL 语句,MySQL 需要识别出里面的字符串分别是什么,代表什么。
     MySQL 从你输入的"select"这个关键字识别出来,这是一个查询语句。它也要把字符串“T”识别成“表名 T”,
     把字符串“ID”识别成“列 ID”
     
  • 语法分析

     根据词法分析的结果,语法分析器会根据语法规则,判断你输入的这SQL语句是否满足 MySQL 语法。
     如果你的语句不对,就会收到“You have an erroin your SQL syntax”的错误提醒,比如面这个语句 select 少打了开头的字母“s”。

    mysql> elect * from t where ID=1;
    ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use naer 'elect * from t where ID=1' at line 1

4.优化器

经过分析器之后,MySQL就知道你需要做什么,在执行之前还要经过优化器的处理。

优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的链接顺序,例如这个语句执行两个表的join:

 mysql> select * from t1 join t2 using(ID) where t1.c=10 and t2.d=20 
  • 既可以先从t1里面取出c=10的记录的Id值,再根据Id值关联到表t2,在判断t2 里面d的值是否等于20
  • 也可以先从t2里面取出d=2的记录值Id值,再根据Id值关联到表t1,在判断t1 里面d的值是否等于10

两种方法逻辑和结果相同,但是执行效率会有不同,优化器就是要决定使用哪一种方案。

5.执行器

到这里Mysql已经知道你想要做什么了,优化器也帮你优化了知道该怎么做了,那么就开始执行语句吧。
  • 执行语句需要:

    • 判断你是否有查询权限有就继续执行没有就返回权限错误
    • 执行器根据表的引擎定义去掉用引擎接口(例:InnoDB)

      • 无索引:

        • 调用innoDB引擎接口取出这个表的第一行,判断ID是否等于1,如果不是则跳过,如果是则将这行存在结果集中;
        • 调用引擎接口取 ‘下一行’ 执行相同判断逻辑,直到取到表的最后一行
        • 执行器将所有满足条件的行 组成的记录作为结果集返回给客户端
      • 有索引

        • 与无索引逻辑差不多,第一次调用的是“取满足条件的第一行”这个接口,之后循环的是“满足条件的下一行”这个几口,接口在引擎中已经定义好。

致谢

在这里非常感谢·林晓斌·老师在极客时间发布的文章,让自己对数据库有了一个好的理解。感谢各位观众的耐心观看,如有错误请指正,谢谢!
查看原文

赞 22 收藏 16 评论 0

zhangrxiang 提出了问题 · 2018-10-22

laravel查询构造器如何编写下面SQL语句

select * from i_program_layout
where not (start_at > '2018-10-25 14:05:35' or end_at < '2018-10-22 14:05:36') and b_zone_id=2

关注 3 回答 2

zhangrxiang 提出了问题 · 2018-10-22

laravel查询构造器如何编写下面SQL语句

select * from i_program_layout
where not (start_at > '2018-10-25 14:05:35' or end_at < '2018-10-22 14:05:36') and b_zone_id=2

关注 3 回答 2

zhangrxiang 发布了文章 · 2018-04-13

freeSWITCH之多平台测试通信

开始测试使用

强烈建议在统一的局域网下进行配置,通信

本机IP:192.168.1.155

架构

freeSWITCH搭建在以Windows平台作为通信服务器。fs_cli为服务器上测试客户端。
X-lite客户端程序搭建在WindowsMac平台。
zoiper配置客户端搭建在Android平台,测试不同平台上的通信。

Windows

freeSWITCH

  • 以管理员权限运行freeSWITCH服务端C:\Program Files\FreeSWITCH\FreeSwitchConsole.exe
images
  • 运行客户端软件 C:\Program Files\FreeSWITCH\fs_cli.exe
images

X-lite

运行X-lite

Windows

images

Mac

images

配置

IP和端口

通过输入以下命令可以知道 FreeSWITCH 监听在哪个IP地址上,记住这个 IP 地址(:5060以前的部分),下面要用到:

$ netstat -an | grep 5060
  TCP    192.168.1.155:5060     0.0.0.0:0              LISTENING
  UDP    192.168.1.155:5060     *:*

Windows X-lite配置

FreeSWITCH 默认配置了 1000 ~ 101920 个用户,你可以随便选择一个用户进行配置:
X-Lite 上点右键,选 Sip Account Settings...,点Add添加一个账号,填入以下参数(Zoiper 可参照配置):

Display Name: 1000
User name: 1000
Password: 1234
Authorization user name: 1000
Domain: 你的IP地址,就是刚才你记住的那个

images

其它都使用默认设置,点 OK 就可以了。然后点 Close 关闭 Sip Account 设置窗口。这时 X-Lite 将自动向 FreeSWITCH 注册。注册成功后会显示"Ready. Your username is 1000",另外,左侧的"拨打电话"(Dial)按钮会变成绿色的。

images


Mac X-lite配置

同理配置Mac上的X-lite
images

images


Android zoiper配置

images
images

images
images


号码说明

9999 | 保持音乐
9996 | echo,回音测试 
9992 | info,在控制台上显示呼叫参数
9888 | FreeSWITCH电话会议,每周召开
5900 | 呼叫挂起
5901 | 接听挂起的呼叫
5000 | 示例IVR
4000 | 听取语音信箱
33xx | 电话会议,48K(其中xx可为00-99,下同)
32xx | 电话会议,32K
31xx | 电话会议,16K
30xx | 电话会议,8K
2000-2002 | 呼叫组
1000-1019 | 默认分机号

通信

  • Windows X-lite 为 1000
  • Mac X-lite 为 1001
  • Android zoiper 为 1002

1000 --> 1001

拨号

images
images

接通

images
images

挂断

退出接通界面,返回正常界面

1000 --> 1002

拨号

images
images

接通

images
images

挂断

退出接通界面,返回正常界面

1001 --> 1002

注意

freeswitch默认密码修改

修改FreeSWITCH\conf\var.xml为如下

    <X-PRE-PROCESS cmd="set" data="default_password=your password"/>

freeswitch在多网卡服务器下如何指定IP地址

FreeSWITCH\conf\sip_profiles\internal.xml 修改

    <!-- ip address to use for rtp, DO NOT USE HOSTNAMES ONLY IP ADDRESSES -->
    <param name="rtp-ip" value="$${local_ip_v4}"/>
    <!-- ip address to bind to, DO NOT USE HOSTNAMES ONLY IP ADDRESSES -->
    <param name="sip-ip" value="$${local_ip_v4}"/>

修改为

    <!-- ip address to use for rtp, DO NOT USE HOSTNAMES ONLY IP ADDRESSES -->
    <param name="rtp-ip" value="your ip"/>
    <!-- ip address to bind to, DO NOT USE HOSTNAMES ONLY IP ADDRESSES -->
    <param name="sip-ip" value="your ip"/>

FreeSWITCH\conf\autoload_configs\sofia.conf.xml修改

修改为如下,该属性设置的目的是防止FS在检测到IP地址发生改变后,自动重启sofia模块。

    <param name="auto-restart" value="false"/>

重启FreeSWITCH,开始测试。

查看原文

赞 1 收藏 0 评论 0

zhangrxiang 发布了文章 · 2018-04-13

freeSWITCH之安装

freeSWITCH 安装

官网教程 https://freeswitch.org/conflu...

Windows

download page

install

  • 下载对应版本
  • 默认安装目录 C:\Program Files\FreeSWITCH

run

  • 以管理员权限运行freeswitch命令 FreeSwitchConsole.exe
images
  • 客户端 fs_cli.exe
images
显示安装成功

Mac

install

  • brew install freeswitch
images

run

  • 服务端 brew services start freeswitch
  • 客户端 fs_cli

images

Linux

debin-jessie install

官网教程https://freeswitch.org/conflu...

aliyun source
deb http://mirrors.aliyun.com/debian/ jessie main non-free contrib
deb http://mirrors.aliyun.com/debian/ jessie-proposed-updates main non-free contrib
deb-src http://mirrors.aliyun.com/debian/ jessie main non-free contrib
deb-src http://mirrors.aliyun.com/debian/ jessie-proposed-updates main non-free contrib

wget -O - https://files.freeswitch.org/repo/deb/debian/freeswitch_archive_g0.pub | apt-key add -
echo "deb http://files.freeswitch.org/repo/deb/freeswitch-1.6/ jessie main" > /etc/apt/sources.list.d/freeswitch.list

apt-get update && apt-get install -y freeswitch-meta-all

run

  • 服务端 sudo systemctl start freeswitch.service
images
  • 客户端 fs_cli
images

X-lite客户端安装

Before you download X-Lite for Windows PC or Mac, please note that in order to use X-Lite to make audio calls to softphone/mobile/landline numbers and make video calls/send Instant Messages to softphones, a VoIP subscription with a local service provider or Internet Service Provider is required. Please contact your local service provider to subscribe.

默认安装即可

zoiper 安装(移动端)

开发文档

查看原文

赞 2 收藏 1 评论 0

zhangrxiang 发布了文章 · 2018-03-26

PHP面向对象

面向对象

面向对象基础

面向对象

什么是类?

具有相同属性(特征)和方法(行为)的一系列个体的集合,类是一个抽象的概念。

什么是对象?

从类中,拿到的具有具体属性值的个体,称为对象。对象是一个具体的个体。
eg:人类;张三

类和对象的关系?

  • 类是对象的抽象化!对象是类的具体化!
  • 类仅仅表明这类对象有哪些属性,但是不能有具体的值,所以类是抽象的。
  • 对象是将类的所有属性赋值后,产生具体的个体,所有对象是具体的。

类的声明与实例化

如何声明一个类:

class 类名{
    访问修饰符 $属性[=默认值];
    [访问修饰符] function 方法(){}
}
class SimpleClass {
    // property declaration
    public $var = 'a default value';

    // method declaration
    public function displayVar() {
        echo $this->var;
    }
}

声明一个类的注意事项:

  • 类名只能有字母数字下划线组成,开头不能是数字,必须符合大驼峰法则;
  • 类名必须使用class修饰,类名后面一定不能有();
  • 属性必须要带访问修饰符,方法可以不带访问修饰符。

实例化对象及对象属性方法的调用:

$对象名 = new 类名(); //()可以不带

类外部调用属性和方法:

$对象名 -> $属性名; //使用->调用属性时,属性名不能带$符号

类内部调用属性和方法:

$this -> $属性名;

构造函数

什么是构造函数?

构造函数是类中的一个特殊函数,当我们使用new关键字实例化对象时,相当于调用了类的构造函数。

构造函数有什么作用?

实例化对象时,自动调用,用于给对象的属性赋初值!

构造函数的写法:

  • 构造函数名,必须与类同名(废弃)
[public] function Person($name){
    $this -> name = $name;
}
  • 使用魔术方法__construct
[public] function __construct($name){
    $this -> name = $name;
}

构造函数注意事项:

  • 第一种写法,构造函数名必须与类同名!!!!
  • 如果一个类没有手写构造函数,则系统默认会有一个空参构造,因此可以使用new Person();
  • 如果我们写了带参数的构造函数,则将不会再有空参构造,也就是不能直接使用new Person();
  • Person后面的()中的参数列表,必须符合构造函数的要求!!!!
  • 如果两种构造函数同时存在,将使用__construct

析构函数:__destruct():

  • 析构函数在对象被销毁释放之前自动调用;
  • 析构函数不能带有任何的参数;
  • 析构函数常用于对象使用完以后,释放资源,关闭资源等。

魔术方法:

PHP中,给我们提供一系列用__开头的函数,这些函数无需自己手动调用,
会在合适的时机自动调用,这类函数称为魔术称为魔术函数。
eg:function __construct(){} 在类new一个对象时自动调用
function __destruct(){} 在对象被销毁时自动调用,我们要求,除了魔术方法之外,自定义的函数与方法不能使用__开头。最后,一般对于功能比较复杂的类,我们会单独的写到一个类文件中。类文件的命名,同一小写,使用"类名小写.class.php"的方式命名。在其他文件中使用这个类时,可以使用include导入这个".class.php"文件。

封装和继承

什么是封装?

通过访问修饰符,将类中不需要外部访问的属性和方法进行私有化处理,以实现访问控制。

注意:是实现访问控制,而不是拒绝访问。也就是说,我们私有化属性后,需要提供对应的方法,让用户通过我们提供的方法处理属性。

封装的作用?

  • 使用者只关心类能够提供的功能,不关心功能实现的细节!(封装方法)
  • 对用户的数据进行控制,防止设置不合法数据,控制返回给用户的数据(属性封装+set/get方法)

实现封装操作?

方法的封装

对于一些只在类内部使用的方法,而不像对外部提供使用,那么,这样的方法我们可以使用private进行私有化处理。

private function formatName(){} //这个方法仅仅能在类内部使用$this调用
function showName(){
    $this->formatName();
}

属性的封装+set/get方法

为了控制属性的设置以及读取,可以将属性进行私有化处理,并要求用户通过我们提供的set/get方法进行设置

private $age;
//set方法
function setAge($age){
    $this->age=$age;
} 
//get方法
function getAge(){
     return $this->age;
}
$对象->getAge();
$对象->setAge(12);

属性的封装+魔术方法

__get( -> __set(, ->= }
$对象->age; //访问对象私有属性时,自动调用__get()魔术方法,并且将访问的属性名传给__get()方法;
$对象->age=12; //设置对象私有属性时,自动调用__set()魔术方法,并且将设置的属性名以及属性值传给__set()方法;
注意:在魔术方法中,可以使用分支结构,判断$key的不同,进行不同操作。

关于封装的魔术方法:

  • __set($key,$value):给类私有属性赋值时自动调用,调用时给方法传递两个参数:需要设置的属性名,属性值。
  • __get($key,$value):读取类私有属性时自动调用,调用时给方法传递一个参数,需要读取的属性名;
  • __isset($key):外部使用isset()函数检测私有属性时,自动调用。
类外部使用isset();检测私有属性,默认是检测不到的。false
所以,我们可以使用__isset();函数,在自动调用时,返回内部检测结果。
  • function __isset($key){return isset($this -> $key);}
当外部使用isset($对象名->私有属性);检测时,将自动调用上述__isset()返回的结果!
  • __unset($key):外部使用unset()函数删除私有属性时,自动调用;

function __unset($key){unset($this -> $key);}

当外部使用unset($对象名->私有属性);删除属性时,自动将属性名传给__unset(),并交由这个魔术方法处理。

继承的基础知识:

如何实现继承?

给子类使用extends关键字,让子类继承父类;

class Student extends Person{}

现继承的注意事项?

  • 子类只能继承父类的非私有属性。
  • 子类继承父类后,相当于将父类的属性和方法copy到子类,可以直接使用$this调用。
  • PHP只能单继承,不支持一个类继承多个类。但是一个类进行多层继承。
class Person{}
class Adult extends Person{}
class Student extends Adult{} 
//Student 类就同时具有了Adult类和Person类的属性和方法

方法覆盖(方法重写)

  • 子类继承父类
  • 子类重写父类已有方法
符合上述两个条件,称为方法覆盖。覆盖之后,子类调用方法,将调用子类自己的方法。
同样,除了方法覆盖,子类也可以具有与父类同名的属性,进行属性覆盖。

如果,子类重写了父类方法,如何在子类中调用父类同名方法?

partent::方法名();
所以,当子类继承父类时,需在子类的构造中的第一步,首先调用父类构造进行复制。

function __construct($name,$sex,$school){
    partent::__construct($name,$sex);
    $this -> school = $school;
}

PHP关键字

final

  • final修饰类,此类为最终类,不能被继承!
  • final修饰方法,此方法为最终方法,不能被重写!
  • final不能修饰属性。

static

  • 可以修饰属性和方法,分别称为静态属性和静态方法,也叫类属性,类方法;
  • 静态属性,静态方法,只能使用类名直接调用。
使用"类名::$静态属性" , "类名::静态方法()"
Person::$sex; Person::say();
  • 静态属性和方法,在类装载时就会声明,先于对象产生。
  • 静态方法中,不能调用非静态属性或方法;
非静态方法,可以调用静态属性和方法。 (因为静态属性和方法在类装载时已经产生,而非静态的属性方法,此时还没有实例化诞生)
  • 在类中,可以使用self关键字,代指本类名。
class Person{
    static $sex = "nan";
    function say(){
        echo self::$sex;
    }
}
  • 静态属性是共享的,也就是new出很多对象,也是共用一个属性。

const关键字:

在类中声明常量,不能是define()函数!必须使用const关键字。与define()声明相似,const关键字声明常量不能带$,必须全部大写!
常量一旦声明,不能改变。调用时与static一样,使用类名调用Person::常量

instanceof操作符:

检测一个对象,是否是某一个类的实例。(包括爹辈,爷爷辈,太爷爷辈……)

$zhangsan instanceof Person;

【小总结】几种特殊操作符:

  • . 只能连接字符串; "".""
  • => 声明数组时,关联键与值["key"=>"value"]
  • -> 对象($this new出的对象)调用成员属性,成员方法;
  • :: 使用parent关键字,调用父类中的同名方法:parent::say();,使用类名(和self)调用类中的静态属性,静态方法,以及常量。

魔术方法小总结

  • __construct():构造函数,new一个对象时,自动调用。
  • __destruct():析构函数,当一个对象被销毁前,自动调用。
  • __get():访问类中私有属性时,自动调用。传递读取的属性名,返回$this->属性名
  • __set():给类的私有属性赋值时,自动调用。传递需要设置的属性名和属性值;
  • __isset():使用isset()检测对象私有属性时,自动调用。传递检测的属性名,返回isset($this -> 属性名);
  • __unset():使用unset()删除对象私有属性时,自动调用。传递删除的属性名,方法中执行unset($this -> 属性名);
  • __toString():使用echo打印对象时,自动调用。返回想要在打印对象时,显示的内容;返回必须是字符串;
  • __call():调用一个类中未定义或未公开的方法时,自动调用。传递被调用的函数名,和参数列表数组;
  • __clone():当使用clone关键字,克隆一个对象时,自动调用。作用是为新克隆的对象进行初始化赋值;
  • __sleep():对象序列化时,自动调用。返回一个数组,数组中的值就是可以序列化的属性;
  • __wakeup():对象反序列化时,自动调用。为反序列化新产生的对象,进行初始化赋值;
  • __autoload():需要在类外部声明函数。当实例化一个未声明的类时,自动调用。传递实例化的类名,可以使用类名自动加载对应的类文件。

抽象类和抽象方法

  • 什么是抽象方法?

没有方法体{}的方法,必须使用abstract关键字修饰。这样的方法,我们称为抽象方法。

abstract function say(); //抽象方法

什么是抽象类?

使用abstract关键字修饰的类就是抽象类。

abstract class Person{}

抽象类的注意事项:

  • 抽象类可以包含非抽象方法;
  • 包含抽象方法的类必须是抽象类,抽象类并不一定必须包含抽象方法;
  • 抽象类,不能实例化。(抽象类中可能包含抽象方法,抽象方法没有方法体,实例化调用没有意义)

我们使用抽象类的目的,就是限制实例化!!!

  • 子类继承抽象类,那么子类必须重写父类的所有抽象方法,除非,子类也是抽象类。

使用抽象类的作用?

  • 限制实例化。(抽象类是一个不完整的类,里面的抽象方法没有方法体,所以不能实例化)
  • 抽象类为子类的继承提供一种规范,子类继承一个抽象类,则必须包含并且实现抽象类中已定的抽象方法。

接口与多态

接口

什么是接口?

接口是一种规范,提供了一组实现接口的类所必须实现的方法组合。
接口使用interface关键字声明;

interface Inter{}
  • 接口中的所有方法,必须都是抽象方法。
  • 接口中的抽象方法不需要也不能使用abstract修饰。
  • 接口中不能声明变量,不能有属性,只能使用常量!!!

接口可以继承接口,使用extends关键字!

接口使用extends继承接口,可以实现多继承。

interface int1 extends Inter,Inter2{}

类可以实现接口,使用implements关键字!

类使用implements实现接口,可同时实现多个接口,多个接口间逗号分隔;

abstract class Person implements Inter,Inter2{}

一个类实现一个或多个接口,那么这个类,必须实现所有接口中的所有抽象方法!
除非,这个类是抽象类。

接口&&抽象类区别

  • 声明方式上,接口使用interface关键字,抽象类使用abstract class
  • 实现/继承方式上,一个类使用extends继承抽象类,使用implements实现接口。
  • 抽象类只能单继承,接口可以多实现。(接口extends接口)、多实现(类implements接口)
  • 抽象类中可以有非抽象方法,接口中只能有抽象方法,不能有费抽象方法。抽象类中的抽象方法必须使用abstract关键字修饰,接口中抽象方法不能带修饰词。
  • 抽象类是个类,可以有属性、变量;接口中只能有常量。

多态

多态

一个类,被多个子类继承。

如果,这个类的某个方法,在多个子类中,表现出不同的功能,我们称这种行为为多态。

实现多态的必要途径:
  • 子类继承父类;
  • 子类重写父类方法;
  • 父类引用指向子类对象
查看原文

赞 2 收藏 10 评论 0

zhangrxiang 发布了文章 · 2018-03-25

MySQL Optimization 优化原理

MySQL Optimization 优化原理

MySQL逻辑架构

如果能在头脑中构建一幅MySQL各组件之间如何协同工作的架构图,有助于深入理解MySQL服务器。下图展示了MySQL的逻辑架构图。

images

  • MySQL逻辑架构,来自:高性能MySQL

MySQL逻辑架构整体分为三层,最上层为客户端层,并非MySQL所独有,诸如:连接处理、授权认证、安全等功能均在这一层处理。

MySQL大多数核心服务均在中间这一层,包括查询解析、分析、优化、缓存、内置函数(比如:时间、数学、加密等函数)。所有的跨存储引擎的功能也在这一层实现:存储过程、触发器、视图等。

最下层为存储引擎,其负责MySQL中的数据存储和提取。和Linux下的文件系统类似,每种存储引擎都有其优势和劣势。中间的服务层通过API与存储引擎通信,这些API接口屏蔽了不同存储引擎间的差异。

MySQL查询过程

我们总是希望MySQL能够获得更高的查询性能,最好的办法是弄清楚MySQL是如何优化和执行查询的。一旦理解了这一点,就会发现:很多的查询优化工作实际上就是遵循一些原则让MySQL的优化器能够按照预想的合理方式运行而已。

images

客户端/服务端通信协议

MySQL客户端/服务端通信协议是“半双工”的:在任一时刻,要么是服务器向客户端发送数据,要么是客户端向服务器发送数据,这两个动作不能同时发生。一旦一端开始发送消息,另一端要接收完整个消息才能响应它,所以我们无法也无须将一个消息切成小块独立发送,也没有办法进行流量控制。

客户端用一个单独的数据包将查询请求发送给服务器,所以当查询语句很长的时候,需要设置max_allowed_packet参数。但是需要注意的是,如果查询实在是太大,服务端会拒绝接收更多数据并抛出异常。

与之相反的是,服务器响应给用户的数据通常会很多,由多个数据包组成。但是当服务器响应客户端请求时,客户端必须完整的接收整个返回结果,而不能简单的只取前面几条结果,然后让服务器停止发送。因而在实际开发中,尽量保持查询简单且只返回必需的数据,减小通信间数据包的大小和数量是一个非常好的习惯,这也是查询中尽量避免使用SELECT *以及加上LIMIT限制的原因之一。

查询缓存

在解析一个查询语句前,如果查询缓存是打开的,那么MySQL会检查这个查询语句是否命中查询缓存中的数据。如果当前查询恰好命中查询缓存,在检查一次用户权限后直接返回缓存中的结果。这种情况下,查询不会被解析,也不会生成执行计划,更不会执行。

MySQL将缓存存放在一个引用表(不要理解成table,可以认为是类似于HashMap的数据结构),通过一个哈希值索引,这个哈希值通过查询本身、当前要查询的数据库、客户端协议版本号等一些可能影响结果的信息计算得来。所以两个查询在任何字符上的不同(例如:空格、注释),都会导致缓存不会命中。

如果查询中包含任何用户自定义函数、存储函数、用户变量、临时表、mysql库中的系统表,其查询结果
都不会被缓存。比如函数NOW()或者CURRENT_DATE()会因为不同的查询时间,返回不同的查询结果,再比如包含CURRENT_USER或者CONNECION_ID()的查询语句会因为不同的用户而返回不同的结果,将这样的查询结果缓存起来没有任何的意义。

既然是缓存,就会失效,那查询缓存何时失效呢?MySQL的查询缓存系统会跟踪查询中涉及的每个表,如果这些表(数据或结构)发生变化,那么和这张表相关的所有缓存数据都将失效。正因为如此,在任何的写操作时,MySQL必须将对应表的所有缓存都设置为失效。如果查询缓存非常大或者碎片很多,这个操作就可能带来很大的系统消耗,甚至导致系统僵死一会儿。而且查询缓存对系统的额外消耗也不仅仅在写操作,读操作也不例外:

  • 任何的查询语句在开始之前都必须经过检查,即使这条SQL语句永远不会命中缓存
  • 如果查询结果可以被缓存,那么执行完成后,会将结果存入缓存,也会带来额外的系统消耗

基于此,我们要知道并不是什么情况下查询缓存都会提高系统性能,缓存和失效都会带来额外消耗,只有当缓存带来的资源节约大于其本身消耗的资源时,才会给系统带来性能提升。但要如何评估打开缓存是否能够带来性能提升是一件非常困难的事情,也不在本文讨论的范畴内。如果系统确实存在一些性能问题,可以尝试打开查询缓存,并在数据库设计上做一些优化,比如:

  • 用多个小表代替一个大表,注意不要过度设计
  • 批量插入代替循环单条插入
  • 合理控制缓存空间大小,一般来说其大小设置为几十兆比较合适
  • 可以通过SQL_CACHESQL_NO_CACHE来控制某个查询语句是否需要进行缓存

最后的忠告是不要轻易打开查询缓存,特别是写密集型应用。如果你实在是忍不住,可以将query_cache_type设置为DEMAND,这时只有加入SQL_CACHE的查询才会走缓存,其他查询则不会,这样可以非常自由地控制哪些查询需要被缓存。

当然查询缓存系统本身是非常复杂的,这里讨论的也只是很小的一部分,其他更深入的话题,比如:缓存是如何使用内存的?如何控制内存的碎片化?事务对查询缓存有何影响等等,读者可以自行阅读相关资料,这里权当抛砖引玉吧。

语法解析和预处理

MySQL通过关键字将SQL语句进行解析,并生成一颗对应的解析树。这个过程解析器主要通过语法规则来验证和解析。比如SQL中是否使用了错误的关键字或者关键字的顺序是否正确等等。预处理则会根据MySQL规则进一步检查解析树是否合法。比如检查要查询的数据表和数据列是否存在等等。

查询优化

经过前面的步骤生成的语法树被认为是合法的了,并且由优化器将其转化成查询计划。多数情况下,一条查询可以有很多种执行方式,最后都返回相应的结果。优化器的作用就是找到这其中最好的执行计划。

MySQL使用基于成本的优化器,它尝试预测一个查询使用某种执行计划时的成本,并选择其中成本最小的一个。在MySQL可以通过查询当前会话的last_query_cost的值来得到其计算当前查询的成本。

mysql> select * from t_message limit 10;
...省略结果集

mysql> show status like 'last_query_cost';
+-----------------+-------------+
| Variable_name   | Value       |
+-----------------+-------------+
| Last_query_cost | 6391.799000 |
+-----------------+-------------+

示例中的结果表示优化器认为大概需要做6391个数据页的随机查找才能完成上面的查询。这个结果是根据一些列的统计信息计算得来的,这些统计信息包括:每张表或者索引的页面个数、索引的基数、索引和数据行的长度、索引的分布情况等等。

有非常多的原因会导致MySQL选择错误的执行计划,比如统计信息不准确、不会考虑不受其控制的操作成本(用户自定义函数、存储过程)、MySQL认为的最优跟我们想的不一样(我们希望执行时间尽可能短,但MySQL值选择它认为成本小的,但成本小并不意味着执行时间短)等等。

MySQL的查询优化器是一个非常复杂的部件,它使用了非常多的优化策略来生成一个最优的执行计划:

  • 重新定义表的关联顺序(多张表关联查询时,并不一定按照SQL中指定的顺序进行,但有一些技巧可以指定关联顺序)
  • 优化MIN()MAX()函数(找某列的最小值,如果该列有索引,只需要查找B+Tree索引最左端,反之则可以找到最大值,具体原理见下文)
  • 提前终止查询(比如:使用Limit时,查找到满足数量的结果集后会立即终止查询)
  • 优化排序(在老版本MySQL会使用两次传输排序,即先读取行指针和需要排序的字段在内存中对其排序,然后再根据排序结果去读取数据行,而新版本采用的是单次传输排序,也就是一次读取所有的数据行,然后根据给定的列排序。对于I/O密集型应用,效率会高很多)

随着MySQL的不断发展,优化器使用的优化策略也在不断的进化,这里仅仅介绍几个非常常用且容易理解的优化策略,其他的优化策略,大家自行查阅吧。

查询执行引擎

在完成解析和优化阶段以后,MySQL会生成对应的执行计划,查询执行引擎根据执行计划给出的指令逐步执行得出结果。整个执行过程的大部分操作均是通过调用存储引擎实现的接口来完成,这些接口被称为handler API。查询过程中的每一张表由一个handler实例表示。实际上,MySQL在查询优化阶段就为每一张表创建了一个handler实例,优化器可以根据这些实例的接口来获取表的相关信息,包括表的所有列名、索引统计信息等。存储引擎接口提供了非常丰富的功能,但其底层仅有几十个接口,这些接口像搭积木一样完成了一次查询的大部分操作。

返回结果给客户端

查询执行的最后一个阶段就是将结果返回给客户端。即使查询不到数据,MySQL仍然会返回这个查询的相关信息,比如该查询影响到的行数以及执行时间等等。

如果查询缓存被打开且这个查询可以被缓存,MySQL也会将结果存放到缓存中。

结果集返回客户端是一个增量且逐步返回的过程。有可能MySQL在生成第一条结果时,就开始向客户端逐步返回结果集了。这样服务端就无须存储太多结果而消耗过多内存,也可以让客户端第一时间获得返回结果。需要注意的是,结果集中的每一行都会以一个满足①中所描述的通信协议的数据包发送,再通过TCP协议进行传输,在传输过程中,可能对MySQL的数据包进行缓存然后批量发送。

回头总结一下MySQL整个查询执行过程,总的来说分为6个步骤:

  • 客户端向MySQL服务器发送一条查询请求
  • 服务器首先检查查询缓存,如果命中缓存,则立刻返回存储在缓存中的结果。否则进入下一阶段
  • 服务器进行SQL解析、预处理、再由优化器生成对应的执行计划
  • MySQL根据执行计划,调用存储引擎的API来执行查询
  • 将结果返回给客户端,同时缓存查询结果

性能优化建议

看了这么多,你可能会期待给出一些优化手段,是的,下面会从3个不同方面给出一些优化建议。但请等等,还有一句忠告要先送给你:不要听信你看到的关于优化的“绝对真理”,包括本文所讨论的内容,而应该是在实际的业务场景下通过测试来验证你关于执行计划以及响应时间的假设。

Scheme设计与数据类型优化

选择数据类型只要遵循小而简单的原则就好,越小的数据类型通常会更快,占用更少的磁盘、内存,处理时需要的CPU周期也更少。越简单的数据类型在计算时需要更少的CPU周期,比如,整型就比字符操作代价低,因而会使用整型来存储ip地址,使用DATETIME来存储时间,而不是使用字符串。

这里总结几个可能容易理解错误的技巧:

  • 通常来说把可为NULL的列改为NOT NULL不会对性能提升有多少帮助,只是如果计划在列上创建索引,就应该将该列设置为NOT NULL
  • 对整数类型指定宽度,比如INT(11),没有任何卵用。INT使用32位(4个字节)存储空间,那么它的表示范围已经确定,所以INT(1)和INT(20)对于存储和计算是相同的。
  • UNSIGNED表示不允许负值,大致可以使正数的上限提高一倍。比如TINYINT存储范围是-128 ~ 127,而UNSIGNED TINYINT存储的范围却是0 - 255。
  • 通常来讲,没有太大的必要使用DECIMAL数据类型。即使是在需要存储财务数据时,仍然可以使用BIGINT。比如需要精确到万分之一,那么可以将数据乘以一百万然后使用BIGINT存储。这样可以避免浮点数计算不准确和DECIMAL精确计算代价高的问题。
  • TIMESTAMP使用4个字节存储空间,DATETIME使用8个字节存储空间。因而,TIMESTAMP只能表示1970 - 2038年,比DATETIME表示的范围小得多,而且TIMESTAMP的值因时区不同而不同。
  • 大多数情况下没有使用枚举类型的必要,其中一个缺点是枚举的字符串列表是固定的,添加和删除字符串(枚举选项)必须使用ALTER TABLE(如果只是在列表末尾追加元素,不需要重建表)。
  • schema的列不要太多。原因是存储引擎的API工作时需要在服务器层和存储引擎层之间通过行缓冲格式拷贝数据,然后在服务器层将缓冲内容解码成各个列,这个转换过程的代价是非常高的。如果列太多而实际使用的列又很少的话,有可能会导致CPU占用过高。
  • 大表ALTER TABLE非常耗时,MySQL执行大部分修改表结果操作的方法是用新的结构创建一个张空表,从旧表中查出所有的数据插入新表,然后再删除旧表。尤其当内存不足而表又很大,而且还有很大索引的情况下,耗时更久。当然有一些奇技淫巧可以解决这个问题,有兴趣可自行查阅。

创建高性能索引

索引是提高MySQL查询性能的一个重要途径,但过多的索引可能会导致过高的磁盘使用率以及过高的内存占用,从而影响应用程序的整体性能。应当尽量避免事后才想起添加索引,因为事后可能需要监控大量的SQL才能定位到问题所在,而且添加索引的时间肯定是远大于初始添加索引所需要的时间,可见索引的添加也是非常有技术含量的。

接下来将向你展示一系列创建高性能索引的策略,以及每条策略其背后的工作原理。但在此之前,先了解与索引相关的一些算法和数据结构,将有助于更好的理解后文的内容。

索引相关的数据结构和算法

通常我们所说的索引是指B-Tree索引,它是目前关系型数据库中查找数据最为常用和有效的索引,大多数存储引擎都支持这种索引。使用B-Tree这个术语,是因为MySQL在CREATE TABLE或其它语句中使用了这个关键字,但实际上不同的存储引擎可能使用不同的数据结构,比如InnoDB就是使用的B+Tree

B+Tree中的B是指balance,意为平衡。需要注意的是,B+树索引并不能找到一个给定键值的具体行,它找到的只是被查找数据行所在的页,接着数据库会把页读入到内存,再在内存中进行查找,最后得到要查找的数据。

在介绍B+Tree前,先了解一下二叉查找树,它是一种经典的数据结构,其左子树的值总是小于根的值,右子树的值总是大于根的值,如下图①。如果要在这课树中查找值为5的记录,其大致流程:先找到根,其值为6,大于5,所以查找左子树,找到3,而5大于3,接着找3的右子树,总共找了3次。同样的方法,如果查找值为8的记录,也需要查找3次。所以二叉查找树的平均查找次数为(3 + 3 + 3 + 2 + 2 + 1) / 6 = 2.3次,而顺序查找的话,查找值为2的记录,仅需要1次,但查找值为8的记录则需要6次,所以顺序查找的平均查找次数为:(1 + 2 + 3 + 4 + 5 + 6) / 6 = 3.3次,因此大多数情况下二叉查找树的平均查找速度比顺序查找要快。

images

由于二叉查找树可以任意构造,同样的值,可以构造出如图②的二叉查找树,显然这棵二叉树的查询效率和顺序查找差不多。若想二叉查找数的查询性能最高,需要这棵二叉查找树是平衡的,也即平衡二叉树AVL树)。

平衡二叉树首先需要符合二叉查找树的定义,其次必须满足任何节点的两个子树的高度差不能大于1。显然图②不满足平衡二叉树的定义,而图①是一课平衡二叉树。平衡二叉树的查找性能是比较高的(性能最好的是最优二叉树),查询性能越好,维护的成本就越大。比如图①的平衡二叉树,当用户需要插入一个新的值9的节点时,就需要做出如下变动。

images

通过一次左旋操作就将插入后的树重新变为平衡二叉树是最简单的情况了,实际应用场景中可能需要旋转多次。至此我们可以考虑一个问题,平衡二叉树的查找效率还不错,实现也非常简单,相应的维护成本还能接受,为什么MySQL索引不直接使用平衡二叉树?

随着数据库中数据的增加,索引本身大小随之增加,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。这样的话,索引查找过程中就要产生磁盘I/O消耗,相对于内存存取,I/O存取的消耗要高几个数量级。可以想象一下一棵几百万节点的二叉树的深度是多少?如果将这么大深度的一颗二叉树放磁盘上,每读取一个节点,需要一次磁盘的I/O读取,整个查找的耗时显然是不能够接受的。那么如何减少查找过程中的I/O存取次数?

一种行之有效的解决方法是减少树的深度,将二叉树变为m叉树(多路搜索树),而B+Tree就是一种多路搜索树。理解B+Tree时,只需要理解其最重要的两个特征即可:第一,所有的关键字(可以理解为数据)都存储在叶子节点Leaf Page),非叶子节点(Index Page)并不存储真正的数据,所有记录节点都是按键值大小顺序存放在同一层叶子节点上。其次,所有的叶子节点由指针连接。如下图为高度为2的简化了的B+Tree

images

怎么理解这两个特征?MySQL将每个节点的大小设置为一个页的整数倍(原因下文会介绍),也就是在节点空间大小一定的情况下,每个节点可以存储更多的内结点,这样每个结点能索引的范围更大更精确。所有的叶子节点使用指针链接的好处是可以进行区间访问,比如上图中,如果查找大于20而小于30的记录,只需要找到节点20,就可以遍历指针依次找到25、30。如果没有链接指针的话,就无法进行区间查找。这也是MySQL使用B+Tree作为索引存储结构的重要原因。

MySQL为何将节点大小设置为页的整数倍,这就需要理解磁盘的存储原理。磁盘本身存取就比主存慢很多,在加上机械运动损耗(特别是普通的机械硬盘),磁盘的存取速度往往是主存的几百万分之一,为了尽量减少磁盘I/O,磁盘往往不是严格按需读取,而是每次都会预读,即使只需要一个字节,磁盘也会从这个位置开始,顺序向后读取一定长度的数据放入内存,预读的长度一般为页的整数倍。

页是计算机管理存储器的逻辑块,硬件及OS往往将主存和磁盘存储区分割为连续的大小相等的块,每个存储块称为一页(许多OS中,页的大小通常为4K)。主存和磁盘以页为单位交换数据。当程序要读取的数据不在主存中时,会触发一个缺页异常,此时系统会向磁盘发出读盘信号,磁盘会找到数据的起始位置并向后连续读取一页或几页载入内存中,然后一起返回,程序继续运行。

MySQL巧妙利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次I/O就可以完全载入。为了达到这个目的,每次新建节点时,直接申请一个页的空间,这样就保证一个节点物理上也存储在一个页里,加之计算机存储分配都是按页对齐的,就实现了读取一个节点只需一次I/O。假设B+Tree的高度为h,一次检索最多需要h-1次I/O(根节点常驻内存),复杂度O(h) = O(logmN)。实际应用场景中,M通常较大,常常超过100,因此树的高度一般都比较小,通常不超过3。

最后简单了解下B+Tree节点的操作,在整体上对索引的维护有一个大概的了解,虽然索引可以大大提高查询效率,但维护索引仍要花费很大的代价,因此合理的创建索引也就尤为重要。

仍以上面的树为例,我们假设每个节点只能存储4个内节点。首先要插入第一个节点28,如下图所示。

images

  • leaf page和index page都没有满

接着插入下一个节点70,在Index Page中查询后得知应该插入到50 - 70之间的叶子节点,但叶子节点已满,这时候就需要进行也分裂的操作,当前的叶子节点起点为50,所以根据中间值来拆分叶子节点,如下图所示。

images

  • Leaf Page拆分

最后插入一个节点95,这时候Index Page和Leaf Page都满了,就需要做两次拆分,如下图所示。

images

  • Leaf Page与Index Page拆分

拆分后最终形成了这样一颗树。

images

  • 最终树

B+Tree为了保持平衡,对于新插入的值需要做大量的拆分页操作,而页的拆分需要I/O操作,为了尽可能的减少页的拆分操作,B+Tree也提供了类似于平衡二叉树的旋转功能。当Leaf Page已满但其左右兄弟节点没有满的情况下,B+Tree并不急于去做拆分操作,而是将记录移到当前所在页的兄弟节点上。通常情况下,左兄弟会被先检查用来做旋转操作。就比如上面第二个示例,当插入70的时候,并不会去做页拆分,而是左旋操作。

images

  • 左旋操作

通过旋转操作可以最大限度的减少页分裂,从而减少索引维护过程中的磁盘的I/O操作,也提高索引维护效率。需要注意的是,删除节点跟插入节点类似,仍然需要旋转和拆分操作,这里就不再说明。

高性能策略

通过上文,相信你对B+Tree的数据结构已经有了大致的了解,但MySQL中索引是如何组织数据的存储呢?以一个简单的示例来说明,假如有如下数据表:

CREATE TABLE People(
    last_name varchar(50) not null,
    first_name varchar(50) not null,
    dob date not null,
    gender enum(`m`,`f`) not null,
    key(last_name,first_name,dob)
);

对于表中每一行数据,索引中包含了last_namefirst_namedob列的值,下图展示了索引是如何组织数据存储的。

images

  • 索引如何组织数据存储,来自:高性能MySQL

MySQL不会使用索引的情况:非独立的列

“独立的列”是指索引列不能是表达式的一部分,也不能是函数的参数。比如:

select * from table_name where id + 1 = 5

我们很容易看出其等价于 id = 4,但是MySQL无法自动解析这个表达式,使用函数是同样的道理。

前缀索引

如果列很长,通常可以索引开始的部分字符,这样可以有效节约索引空间,从而提高索引效率。

多列索引和索引顺序

在多数情况下,在多个列上建立独立的索引并不能提高查询性能。理由非常简单,MySQL不知道选择哪个索引的查询效率更好,所以在老版本,比如MySQL5.0之前就会随便选择一个列的索引,而新的版本会采用合并索引的策略。举个简单的例子,在一张电影演员表中,在actor_idfilm_id两个列上都建立了独立的索引,然后有如下查询:

select film_id,actor_id from film_actor where actor_id = 1 or film_id = 1
-- 老版本的MySQL会随机选择一个索引,但新版本做如下的优化:

select film_id,actor_id from film_actor where actor_id = 1  
union all 
select film_id,actor_id from film_actor where film_id = 1 and actor_id <> 1
  • 当出现多个索引做相交操作时(多个AND条件),通常来说一个包含所有相关列的索引要优于多个独立索引。
  • 当出现多个索引做联合操作时(多个OR条件),对结果集的合并、排序等操作需要耗费大量的CPU和内存资源,特别是当其中的某些索引的选择性不高,需要返回合并大量数据时,查询成本更高。所以这种情况下还不如走全表扫描。

因此explain时如果发现有索引合并(Extra字段出现Using union),应该好好检查一下查询和表结构是不是已经是最优的,如果查询和表都没有问题,那只能说明索引建的非常糟糕,应当慎重考虑索引是否合适,有可能一个包含所有相关列的多列索引更适合。

前面我们提到过索引如何组织数据存储的,从图中可以看到多列索引时,索引的顺序对于查询是至关重要的,很明显应该把选择性更高的字段放到索引的前面,这样通过第一个字段就可以过滤掉大多数不符合条件的数据。

索引选择性是指不重复的索引值和数据表的总记录数的比值,选择性越高查询效率越高,因为选择性越高的索引可以让MySQL在查询时过滤掉更多的行。唯一索引的选择性是1,这是最好的索引选择性,性能也是最好的。

理解索引选择性的概念后,就不难确定哪个字段的选择性较高了,查一下就知道了,比如:

SELECT * FROM payment where staff_id = 2 and customer_id = 584

是应该创建(staff_id,customer_id)的索引还是应该颠倒一下顺序?执行下面的查询,哪个字段的选择性更接近1就把哪个字段索引前面就好。

select count(distinct staff_id)/count(*) as staff_id_selectivity,
       count(distinct customer_id)/count(*) as customer_id_selectivity,
       count(*) from payment

多数情况下使用这个原则没有任何问题,但仍然注意你的数据中是否存在一些特殊情况。举个简单的例子,比如要查询某个用户组下有过交易的用户信息:

select user_id from trade where user_group_id = 1 and trade_amount > 0

MySQL为这个查询选择了索引(user_group_id,trade_amount),如果不考虑特殊情况,这看起来没有任何问题,但实际情况是这张表的大多数数据都是从老系统中迁移过来的,由于新老系统的数据不兼容,所以就给老系统迁移过来的数据赋予了一个默认的用户组。这种情况下,通过索引扫描的行数跟全表扫描基本没什么区别,索引也就起不到任何作用。

推广开来说,经验法则和推论在多数情况下是有用的,可以指导我们开发和设计,但实际情况往往会更复杂,实际业务场景下的某些特殊情况可能会摧毁你的整个设计。

避免多个范围条件

实际开发中,我们会经常使用多个范围条件,比如想查询某个时间段内登录过的用户:

select user.* from user where login_time > '2017-04-01' and age between 18 and 30;

这个查询有一个问题:它有两个范围条件,login_time列和age列,MySQL可以使用login_time列的索引或者age列的索引,但无法同时使用它们。

覆盖索引

如果一个索引包含或者说覆盖所有需要查询的字段的值,那么就没有必要再回表查询,这就称为覆盖索引。覆盖索引是非常有用的工具,可以极大的提高性能,因为查询只需要扫描索引会带来许多好处:

  • 索引条目远小于数据行大小,如果只读取索引,极大减少数据访问量
  • 索引是有按照列值顺序存储的,对于I/O密集型的范围查询要比随机从磁盘读取每一行数据的IO要少的多

使用索引扫描来排序

MySQL有两种方式可以生产有序的结果集,其一是对结果集进行排序的操作,其二是按照索引顺序扫描得出的结果自然是有序的。如果explain的结果中type列的值为index表示使用了索引扫描来做排序。

扫描索引本身很快,因为只需要从一条索引记录移动到相邻的下一条记录。但如果索引本身不能覆盖所有需要查询的列,那么就不得不每扫描一条索引记录就回表查询一次对应的行。这个读取操作基本上是随机I/O,因此按照索引顺序读取数据的速度通常要比顺序地全表扫描要慢。

在设计索引时,如果一个索引既能够满足排序,又满足查询,是最好的。

只有当索引的列顺序和ORDER BY子句的顺序完全一致,并且所有列的排序方向也一样时,才能够使用索引来对结果做排序。如果查询需要关联多张表,则只有ORDER BY子句引用的字段全部为第一张表时,才能使用索引做排序。ORDER BY子句和查询的限制是一样的,都要满足最左前缀的要求(有一种情况例外,就是最左的列被指定为常数,下面是一个简单的示例),其他情况下都需要执行排序操作,而无法利用索引排序。

-- 最左列为常数,索引:(date,staff_id,customer_id)
select  staff_id,customer_id from demo where date = '2015-06-01' order by staff_id,customer_id

冗余和重复索引

冗余索引是指在相同的列上按照相同的顺序创建的相同类型的索引,应当尽量避免这种索引,发现后立即删除。比如有一个索引(A,B),再创建索引(A)就是冗余索引。冗余索引经常发生在为表添加新索引时,比如有人新建了索引(A,B),但这个索引不是扩展已有的索引(A)。

大多数情况下都应该尽量扩展已有的索引而不是创建新索引。但有极少情况下出现性能方面的考虑需要冗余索引,比如扩展已有索引而导致其变得过大,从而影响到其他使用该索引的查询。

删除长期未使用的索引

定期删除一些长时间未使用过的索引是一个非常好的习惯。

关于索引这个话题打算就此打住,最后要说一句,索引并不总是最好的工具,只有当索引帮助提高查询速度带来的好处大于其带来的额外工作时,索引才是有效的。对于非常小的表,简单的全表扫描更高效。对于中到大型的表,索引就非常有效。对于超大型的表,建立和维护索引的代价随之增长,这时候其他技术也许更有效,比如分区表。最后的最后,explain后再提测是一种美德。

特定类型查询优化

优化COUNT()查询

COUNT()可能是被大家误解最多的函数了,它有两种不同的作用,其一是统计某个列值的数量,其二是统计行数。统计列值时,要求列值是非空的,它不会统计NULL。如果确认括号中的表达式不可能为空时,实际上就是在统计行数。最简单的就是当使用COUNT(*)时,并不是我们所想象的那样扩展成所有的列,实际上,它会忽略所有的列而直接统计行数。

我们最常见的误解也就在这儿,在括号内指定了一列却希望统计结果是行数,而且还常常误以为前者的性能会更好。但实际并非这样,如果要统计行数,直接使用COUNT(*),意义清晰,且性能更好。

有时候某些业务场景并不需要完全精确的COUNT值,可以用近似值来代替,EXPLAIN出来的行数就是一个不错的近似值,而且执行EXPLAIN并不需要真正地去执行查询,所以成本非常低。通常来说,执行COUNT()都需要扫描大量的行才能获取到精确的数据,因此很难优化,MySQL层面还能做得也就只有覆盖索引了。如果不还能解决问题,只有从架构层面解决了,比如添加汇总表,或者使用redis这样的外部缓存系统。

优化关联查询

在大数据场景下,表与表之间通过一个冗余字段来关联,要比直接使用JOIN有更好的性能。如果确实需要使用关联查询的情况下,需要特别注意的是:

  • 确保ONUSING字句中的列上有索引。在创建索引的时候就要考虑到关联的顺序。当表A和表B用列c关联的时候,如果优化器关联的顺序是A、B,那么就不需要在A表的对应列上创建索引。没有用到的索引会带来额外的负担,一般来说,除非有其他理由,只需要在关联顺序中的第二张表的相应列上创建索引(具体原因下文分析)。
  • 确保任何的GROUP BYORDER BY中的表达式只涉及到一个表中的列,这样MySQL才有可能使用索引来优化。

要理解优化关联查询的第一个技巧,就需要理解MySQL是如何执行关联查询的。当前MySQL关联执行的策略非常简单,它对任何的关联都执行嵌套循环关联操作,即先在一个表中循环取出单条数据,然后在嵌套循环到下一个表中寻找匹配的行,依次下去,直到找到所有表中匹配的行为为止。然后根据各个表匹配的行,返回查询中需要的各个列。

太抽象了?以上面的示例来说明,比如有这样的一个查询:

SELECT A.xx,B.yy 
FROM A INNER JOIN B USING(c)
WHERE A.xx IN (5,6)

假设MySQL按照查询中的关联顺序A、B来进行关联操作,那么可以用下面的伪代码表示MySQL如何完成这个查询:

outer_iterator = SELECT A.xx,A.c FROM A WHERE A.xx IN (5,6);
outer_row = outer_iterator.next;
while(outer_row) {
    inner_iterator = SELECT B.yy FROM B WHERE B.c = outer_row.c;
    inner_row = inner_iterator.next;
    while(inner_row) {
        output[inner_row.yy,outer_row.xx];
        inner_row = inner_iterator.next;
    }
    outer_row = outer_iterator.next;
}

可以看到,最外层的查询是根据A.xx列来查询的,A.c上如果有索引的话,整个关联查询也不会使用。再看内层的查询,很明显B.c上如果有索引的话,能够加速查询,因此只需要在关联顺序中的第二张表的相应列上创建索引即可。

优化LIMIT分页

当需要分页操作时,通常会使用LIMIT加上偏移量的办法实现,同时加上合适的ORDER BY字句。如果有对应的索引,通常效率会不错,否则,MySQL需要做大量的文件排序操作。

一个常见的问题是当偏移量非常大的时候,比如:LIMIT 10000 20这样的查询,MySQL需要查询10020条记录然后只返回20条记录,前面的10000条都将被抛弃,这样的代价非常高。

优化这种查询一个最简单的办法就是尽可能的使用覆盖索引扫描,而不是查询所有的列。然后根据需要做一次关联查询再返回所有的列。对于偏移量很大时,这样做的效率会提升非常大。考虑下面的查询:

SELECT film_id,description FROM film ORDER BY title LIMIT 50,5;

如果这张表非常大,那么这个查询最好改成下面的样子:

SELECT film.film_id,film.description
FROM film INNER JOIN (
    SELECT film_id FROM film ORDER BY title LIMIT 50,5
) AS tmp USING(film_id);

这里的延迟关联将大大提升查询效率,让MySQL扫描尽可能少的页面,获取需要访问的记录后在根据关联列回原表查询所需要的列。

有时候如果可以使用书签记录上次取数据的位置,那么下次就可以直接从该书签记录的位置开始扫描,这样就可以避免使用OFFSET,比如下面的查询:

SELECT id FROM t LIMIT 10000, 10;
-- 改为:
SELECT id FROM t WHERE id > 10000 LIMIT 10;

其他优化的办法还包括使用预先计算的汇总表,或者关联到一个冗余表,冗余表中只包含主键列和需要做排序的列。

优化UNION

MySQL处理UNION的策略是先创建临时表,然后再把各个查询结果插入到临时表中,最后再来做查询。因此很多优化策略在UNION查询中都没有办法很好的时候。经常需要手动将WHERELIMITORDER BY等字句“下推”到各个子查询中,以便优化器可以充分利用这些条件先优化。

除非确实需要服务器去重,否则就一定要使用UNION ALL,如果没有ALL关键字,MySQL会给临时表加上DISTINCT选项,这会导致整个临时表的数据做唯一性检查,这样做的代价非常高。当然即使使用ALL关键字,MySQL总是将结果放入临时表,然后再读出,再返回给客户端。虽然很多时候没有这个必要,比如有时候可以直接把每个子查询的结果返回给客户端。

结语

理解查询是如何执行以及时间都消耗在哪些地方,再加上一些优化过程的知识,可以帮助大家更好的理解MySQL,理解常见优化技巧背后的原理。希望本文中的原理、示例能够帮助大家更好的将理论和实践联系起来,更多的将理论知识运用到实践中。

其他也没啥说的了,给大家留两个思考题吧,可以在脑袋里想想答案,这也是大家经常挂在嘴边的,但很少有人会思考为什么?

有非常多的程序员在分享时都会抛出这样一个观点:尽可能不要使用存储过程,存储过程非常不容易维护,也会增加使用成本,应该把业务逻辑放到客户端。既然客户端都能干这些事,那为什么还要存储过程?

JOIN本身也挺方便的,直接查询就好了,为什么还需要视图呢?

See

All rights reserved

查看原文

赞 15 收藏 45 评论 0

认证与成就

  • 获得 101 次点赞
  • 获得 8 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 7 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

注册于 2016-06-06
个人主页被 2.1k 人浏览