tumi

tumi 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑

只鸟归林

个人动态

tumi 发布了文章 · 10月13日

js json parse bigint 丢失精度的问题

js json parse bigint 丢失精度的问题

1.获取原始数据
2.使用正则替换bigint 为 string
3.JSON.parse()

const json = '{"smallNumber": 1, "bigNumber" : 9007199254740999}';
console.log(regexbig(json));
function regexbig(input:string) {
    // const re1 = input.replace(/:s*([0-9]{15,})s*(,?)/g, ': "$1" $2')
 //  console.log(re1); return JSON.parse(input);
}
查看原文

赞 0 收藏 0 评论 0

tumi 发布了文章 · 9月18日

yarn computed integrity doesn't match our records

yarn 安装包时出现 yarn computed integrity doesn't match our records 错误
content-disposition
error https://registry.npm.taobao.org/qs/download/qs-6.9.4.tgz: Integrity check failed for "qs" (computed integrity doesn't match our records, got "sha
512-+wYo+L6ZF6BMoEjtf8zB2esQsqdV6WsjRK/GP9WOgLPrq87PbNWgIxS76dS5uvl/QXtHGakZmwTznIfcPXcKlQ== sha1-pZ6FHBuhbAUT6hI4MN1jmgoVy2o=")

解决方法:

yarn cache clean
yarn --update-checksums
yarn
假如 以上还不能解决这个问题,尝试以下方法
npm install
查看原文

赞 1 收藏 1 评论 0

tumi 发布了文章 · 9月18日

yarn 安装包时出现错误

error An unexpected error occurred: "https://registry.npm.taobao.org/egg: tunnel ing socket could not be established, cause=getaddrinfo ENOTFOUND 9999

猜测原因: 本机有proxy工具
解决方法:

yarn config set proxy http://localhost:port
yarn config set https-proxy http://localhost:port

加速方法:

yarn config set registry https://registry.npm.taobao.org --global
// 查看配置
yarn config list
查看原文

赞 0 收藏 0 评论 0

tumi 赞了文章 · 7月22日

前端工程师成长之多读好书

1 引言

乱七八糟的书看了很多,有一本讲JavaScript的印象特别深开篇说的是"JavaScript是Java的脚本语言",但还是看完了,最后忘了书名。

下面列的这些都是看过后至少记得起书名的,也有部分是经常看的书,一起列出来,推荐给爱学习的同学。

2 前端技术

2.1 综合

  • 《现代前端技术解析》
  • 《Web前端开发最佳实践》
  • 《Web前端工程师修炼之道》
  • 《编写高质量代码-Web前端开发修炼之道》
  • 《响应式Web设计 HTML5和CSS3实战》 第二版
  • 《响应式设计、改造与优化》

2.2 基础

2.2.1 HTML && HTML5

  • 《HTML与CSS基础教程》第八版
  • 《HTML与XHTML权威指南》第六版
  • 《HTML5与CSS3实战指南》
  • 《HTML5和CSS3权威指南》
  • 《HTML5与CSS3设计模式》

2.2.2 CSS && CSS3

  • 《CSS世界》
  • 《CSS核心技术详解》
  • 《CSS权威指南》 第三版
  • 《CSS设计指南》第三版
  • 《精通CSS-高级Web标准解决方案》第二版
  • 《图解CSS3-核心技术与案例实战》

2.2.3 JavaScript && ES6+

  • 《看透JavaScript:原理、方法与实践》
  • 《实战ES2015:深入现代JavaScript 应用开发》
  • 《学习JavaScript数据结构与算法》 第二版
  • 《ES6标准入门》第三版
  • 《JavaScript忍者秘籍》第二版
  • 《JavaScript学习指南》第三版
  • 《You Don't Know JS》《你不知道的JS》
  • 《JavaScript权威指南》第六版
  • 《JavaScript高级程序设计》 第三版
  • 《JavaScript核心概念及实践》
  • 《JavaScript面向对象编程指南》第二版
  • 《JavaScript DOM编程艺术》第二版
  • 《JavaScript语言精粹》
  • 《动态函数式编程语言精髓与编程实践》

2.3 性能优化

  • 《Web性能权威指南》
  • 《高性能JavaScript》
  • 《JavaScript性能优化:度量、监控与可视化》
  • 《高性能网站建设指南》
  • 《高性能网站建设进阶指南》
  • 《大型网站性能监测、分析与优化》
  • 《网站性能监测与优化》
  • 《高效前端-Web高效编程与优化实践》
  • 《速度与激情-以网站性能提升用户体验》

2.4 安全

  • 《Web前端黑客技术揭秘》
  • 《白帽子讲Web安全》
  • 《黑客攻防技术宝典 Web实战篇》第二版
  • 《Web应用安全威胁与防治 基于OWASP Top 10与ESAPI》
  • 《Web之困-现代Web应用安全指南》
  • 《Web安全开发指南》
  • 《Web应用安全权威指南》
  • 《黑客攻防技术宝典 浏览器实战篇》
  • 《XSS跨站脚本攻击剖析与防御》

2.5 工程化 && 自动化

  • 《深入浅出Webpack》
  • 《深入PostCSS Web设计》
  • 《前端工程化体系设计与实践》
  • 《Web前端测试与集成- Jasmine/Selenium/Protractor/Jenkins的最佳实践》
  • 《Web前端自动化构建-Gulp、Bower和Yeoman开发指南》

2.6 协议

  • 《Web性能权威指南》
  • 《图解HTTP》
  • 《HTTP权威指南》
  • 《HTTPS权威指南》
  • 《图解TCP-IP》

2.7 浏览器

  • 《浏览器工作原理》 文章
  • 《Webkit技术内幕》

2.8 架构

  • 《JavaScript框架设计》第二版
  • 《前端架构设计》
  • 《JavaScript开发框架权威指南》
  • 《大型JavaScript应用实践最佳指南》
  • 《JavaScript框架高级编程》
  • 《JavaScript设计模式与开发实践》
  • 《JavaScript设计模式》
  • 《JavaScript模式》

3 学点其他的

3.1 所谓的全栈

  • Web开发者技能路线图
  • 教你成为全栈工程师
  • 《全栈增长工程师指南》 《全栈应用开发-精益实践》
  • 《Web全栈工程师的自我修养》
  • 《Web开发权威指南》
  • 《JavaScript快速全栈开发》
  • 《单页Web应用-JavaScript从前端到后端》
  • 《全栈开发之道-MongoDB+Express+AngularJS+Node.js》
  • 《全端Web开发-使用JavaScript和Java》

3.2 程序设计

  • 《代码大全》第二版
  • 《修改代码的艺术》
  • 《重构-改善既有代码的设计》
  • 《代码整洁之道》

3.3 计算机基础

  • 《深入理解计算机系统》第三版
  • 《计算机是怎样跑起来的》
  • 《程序是怎样跑起来的》
  • 《网络是怎样连接的》
查看原文

赞 329 收藏 296 评论 9

tumi 回答了问题 · 5月22日

解决Go语言关于 goroutines 泄漏的一个问题

备忘
放在之前:
worker 中的 sizes <- info.Size() 被阻塞 ,无法走到defer wg.Done()wg.Wait() 也被阻塞,那么无法走到

for size := range sizes {  
      total += size      
}

sizes没法被读取,导致死锁

放在之后:
因为没有close(sizes)

for size := range sizes {  
      total += size      
}

一直在阻塞,导致死锁

https://stackoverflow.com/que...

关注 5 回答 2

tumi 收藏了问题 · 5月22日

Go语言关于 goroutines 泄漏的一个问题

问题描述

在阅读《go语言圣经》这本数中,对 “并发的循环” 这个章节中,使用 WaitGoup 处理 goroutines 泄漏的描述未看懂,请各位指教。

书中描述说:

如果等待操作被放在了main goroutine中,在循环之前,这样的话就永远都不会结束了

我不明白这里为什么一定要把 wg.Wait() 放到一个goroutine中去执行呢?为什么放到 main goroutine 中,就永远不会结束了?

相关代码

// 请把代码文本粘贴到下方(请勿用图片代替代码)

func makeThumbnails6(filenames <-chan string) int64 {
    sizes := make(chan int64)
    var wg sync.WaitGroup // number of working goroutines
    for f := range filenames {
        wg.Add(1)
        // worker
        go func(f string) {
            defer wg.Done()
            thumb, err := thumbnail.ImageFile(f)
            if err != nil {
                log.Println(err)
                return
            }
            info, _ := os.Stat(thumb) // OK to ignore error
            sizes <- info.Size()
        }(f)
    }

    // closer
    go func() { // 为什么一定要在goroutine中进行执行?
        wg.Wait()
        close(sizes)
    }()

    var total int64
    for size := range sizes {
        total += size
    }
    return total
}

tumi 赞了文章 · 4月28日

NIO、BIO、AIO 与 PHP 实现

前言

最近看到NIO,AIO,Netty,Promise话题很热,我作为一个phper也想来凑凑热闹,凑着凑着发现周围怎么都是javaer,jser。那么PHP能做NIOAIO么?

什么BIO、NIO、AIO

BIO 同步阻塞I/O。

有小伙伴又要问了啥叫 同步,啥叫阻塞啊?

同步/异步 阻塞/非阻塞

同步: 两个同步任务相互依赖,并且一个任务必须以依赖于另一任务的某种方式执行。 比如在A->B事件模型中,你需要先完成 A 才能执行B。 再换句话说,同步调用种被调用者未处理完请求之前,调用不返回,调用者会一直等待结果的返回。

异步: 两个异步的任务完全独立的,一方的执行不需要等待另外一方的执行。再换句话说,异步调用种一调用就返回结果不需要等待结果返回,当结果返回的时候通过回调函数或者其他方式拿着结果再做相关事情,

阻塞: 阻塞就是发起一个请求,调用者一直等待请求结果返回,也就是当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续。

非阻塞: 非阻塞就是发起一个请求,调用者不用一直等着结果返回,可以先去干其他事情。

以上就是这四个词汇的解释,那么放到计算机IO上,比较接地气的解释

BIO (Blocking I/O)

那么我们拿快递揽件来举例,一个快递公司,有一部分工作是揽件,它的工作模式是只能一个一个的揽件,你要寄快递,必须排队,一个一个的来,这就是 同步 。好不容易轮到你了,你把快递一扔给他,他还让给你等着,快递工作人员说,我们这后面还有些信息要录入,快递要检查,必须等我们快递公司检查完毕后,你才能离开,这叫 阻塞

NIO (No-Blocking I/O)

同步非阻塞的I/O

继续啊,拿快递公司举例。这个快递公司发现有些用户在后面排队,排着排着,太久了就去隔壁快递公司了,怎么办呢?快递公司想了个办法,置办了一个发号器和一批收纳盒。来一个客户,就把快递放在一个收纳盒里,再给用户一个编号,此时再来一个用户,不论前面一个的快递是否检查完毕,还是给他一个收纳盒,发一个编号。不同客户之间不排队,一来就被受理了,这就是 非阻塞。 我们再来看看内部,快递呢还是一个个地录入信息,X光检查,这样就是 同步 运行的,等待快递人员检查完毕叫号,客户拿到回执才能离开快递点。

AIO (Asynchronous I/O)

异步非阻塞IO

也有Javaer叫他 NIO2,快递公司揽件又升级了,做了一个快递柜,客户又寄件需求,来了就放入快递柜,然后通过手机扫码关注这个柜子的动态,客户就可以离开了,此时服务被受理,并能马上离开。这就是 非阻塞 。等到快递人员来揽件时,会将柜子里面的寄件一并取走,快递点集中一起处理这些快递件,发现有问题的件,不是立即停下手中的活等待客户来出来,而是放一旁通知客户来,然后继续处理下一个快递,这就是 异步

异步 阻塞 IO

同步/异步 阻塞/非阻塞,这4个名词,两两组和,还有一个就是 异步/阻塞

那么我们还是先把例子举出来吧,还是这个快递点,来了一批客户来寄口罩到国外,由于有很大的可能会通不过检查,所以,快递点把大家都留了下来。等所有的 寄件 都检查完了在统一给大家发送回执单,这就是 阻塞 。快递人员检查寄件时,发现问题不是立马通知客户来处理,而已放到一边,继续处理下一个。 这就是 异步

伪异步 IO

这种模式,底层实现是多个 同步阻塞的BIO, 同时运行。

最后总结一下:

阻塞与非阻塞指的的是当不能进行读写(网卡满时的写/网卡空的时候的读)的时候, I/ O操作立即返回还是阻塞;同步异步指的是,当数据已经 ready 的时候,读写操作是同步读还是异步读,阶段不同而已。

区别

异步/同步在计算机区别

以上是一些举例,只是帮助大家理解记忆,接下来我们看看计算上的实现。

最初计算机提供的Web服务,采用的是 CGI 协议,就是纯正的 BIO 模式。一个cgi进程监听一个端口,处理完一个请求,才能接收下一个http请求。这就是同步

而客户的实际体验式是"异步"的,那是因为后来优化了,CGI 程序能够自我fork进程的达到同时响应多个http请求的效果。

注意,我们这里讨论的基础是 单进程 ,上的 异步/同步

阻塞/非阻塞在计算机区别

这里拿购物流程举例,用户的下单,需要做如下操作:

  • 商品可售否
  • 库存数量
  • 用户余额
  • 触发哪些优惠规则
  • 奖券有效性
  • ...

按照一般做法就是一步步验证,上一个检查完了,再进行下一个检查,这就是 阻塞 的方式。

那么非阻塞方式如何做呢,假设在微服务环境中,商品,库存,奖券,促销都是独立的系统,调用商品服务,发起商品可售检查请求;不等商品服务回复,继续调用库存服务,发起商品可售库存请求;紧接着依次发出...检查请求,这样5个检查项目的请求同时发起,最后,我等他们所有的请求都回复我,再来一起来校验是否所有的检查都通过了。就这种发起请求不等响应,就继续做下一件事的叫 非阻塞

转载著名来源sifou

PHP 能做什么

PHP 与 BIO 实现

PHP已经实现啦,这是最基本的好么。但平时测试时却感觉是不阻塞啊,好,我们来一起做个实验,将nginx和php-fpm的进程限制为1个试试。php-fpm就是 多进程的 BIO,现在我们强项改成单进程。

  • 调整Nginx配置

调整 /etc/nginx/nginx.conf 文件:

## 把nginx worker数量设置为1
worker_processes 1;

好了之后我们通过ps命令检查下
image.png

  • 调整PHP配置

调整 /etc/php/php-fpm/conf.d/www.conf 文件:

pm = static

pm.max_children = 1

pm.start_servers = 1

pm.min_spare_servers = 1

pm.max_spare_servers = 1

找到这几个配置都改为如上数值。

最后的结果如下

image.png

我在index.php代码里面加入第一行就加入了sleep。

<?php
sleep(5);

我们同时打开两个网页,一起访问试试
image.png

通过Firefox 抓包可以发现,其中一个耗时5s,另一个页面耗时9.3s,(0.7s误差是我手速慢了) 这就是 BIO。

image.png

好的,我们再做一个实验。把以上nginx,php-fpm配置中1改成2.然后我们打开三个网页,同时访问试试看。

image.png

结果是有两个网页耗时5s一个是9s,也就是说服务器同时处理了2个请求,第三个请求等待了4s才被处理。这就是 多线程-BIO,一个服务同时接待的客户数量取决与worker的数量

PHP 与 NIO 实现

我们写的大部分php-fpm代码以及第三方框架都是阻塞的。PHP也是支持非阻塞IO编程的。

这里其他博主也用PHP原生代码实现NIO编程: PHP回顾之socket编程

I/O 多路复用

在这段小Demo中,PHP 实现 NIO 核心两个函数就是 stream_set_blockingstream_select()
通过以上源码,发现原生的NIO实现还是比较繁琐,不易读的。同时,我就想问一句了,这个 NIO 就是为了实现一个 socket server 么,我们来看看Netty 官网。打开Netty首页,它是这样描述自己的

Netty is a NIO client server framework which enables quick and easy development of network applications such as protocol servers and clients. It greatly simplifies and streamlines network programming such as TCP and UDP socket server.

第一句话:Netty是一个 NIO 客户端 服务框架, 能快速轻松地开发协程客户端。第二句话:简化了网络编程,如创建TCP和UDP套接字服务。

好,重点是什么?第一句话就是重点——开发 协程客户端!回到我们业务上,刚刚举了一个例子,购物到下单,有很多个流程需要做检查,按照一般的BIO那么程序时序图如下:

nio购物流程检查

从上可以看到,三个检查依次分开执行。那么客户的等待时间是大于,库存检查时间加上,产品检查时间加上,促销检查时间 的。

假设, 库存,产品,促销是三个微服务,然后购物车服务用 NIO客户端,与这三个微服务交互,那么会是怎样的效果呢:

nio请求时序图

这里,我们发起检查请求时,是按照顺序发起的,但不等第一个服务返回检查结果就开始发起下一个检查请求。最后三个服务都返回后,综合结果,返回给用户。那么这三个检查的耗时,就等于一个服务(耗时最长的那个服务)的检查耗时。大大减少得了购物车服务响应时间。

我看到一些 NettyNodeJSSwoole 等教程 通篇都在讲如何实现一个WebSocket服务,TCP服务或者是Http服务。对,这是最基础的,但 NIO 框架核心优势在开发一个非阻塞客户端!这才是它的优势,这才是和 BIO 编程差异化所在.

NIO 客户端

看到以上两个时序图,还是给大家演示一下用PHP原生代码实现一个 PHP-BIOPHP Simple NIO Server

建议大家点击链接,把源码git clone https://gitee.com/xupaul/php-nio-server 到本地运行一下,再来看截图更容易理解。

image.png

这三个所依赖的服务响应耗时,我设置为:inventory: 4s, product: 2s, promo:6s

蓝色框和黄色框标注了两个请求,我们主要看参数 noBlocking: true/false 的不同, 第一个是非阻塞方式请求, 可以看到共耗时6s,第二个共耗时12s! (第三个为啥和第二个耗时不一样——6s这个留给大家去研究)。显而易见得非阻塞IO的优势。不过这代码结构就不那么友好了,看到代码 nio_server.php 中,有两种请求方式,阻塞代码流程还能看懂检查完成后就综合结果返回,而非阻塞方式中,发起三个检查后程序流程就开始进入到handleMessage,代码进入哪个分支,取决于 socket_read 的消息,不运行起程序来,没有文档,很难搞懂整个程序流程。

那么,有没有什么什么方便的php类库,让我们编码更友好一点呢,这里介绍下 ReactPHP

这里我用ReactPHP重新实现 nio_server, 代码在这里

这个回调代码写起来有点 NodeJS 的味道呢,当你的PHP没启用 libev 之类的拓展时,ReactPHP内部Loop依然用的 stream_select(), 可以看源码 ~/react/event-loop/src/StreamSelectLoop.php@290 .

执行效果如下:
image.png

能同时发起请求这个功能,那还得提一下 curl_multi, 它能同时发起多个curl请求,最后不断检查是否所有的curl请求已完成。这只是在发起多个Httpcurl请求阶段做到 非阻塞 运行。

还有个拓展pThreads,能够实现多线程,不过对PHP编译参数有限制,需要在线程安全的模式下运行。

pThreads 现在已不是PHP官方所推荐使用的拓展了,当然了这种就属于伪异步IO范畴了

PHP 与 AIO

PHP异步&非阻塞 编码。

此处, 非阻塞I/O 系统调用( nonblocking system call ) 和 异步I/O系统调用 (asychronous system call)的区别是:

  • 一个非阻塞I/O 系统调用 read() 操作立即返回的是任何可以立即拿到的数据, 可以是完整的结果, 也可以是不完整的结果, 还可以是一个空值。
  • 而异步I/O系统调用 read() 结果必须是完整的, 但是这个操作完成的通知可以延迟到将来的一个时间点。
<?php

/**
 * 消息处理
 */
function handleMessage() {
    global $changed, $clients, $cartCheck;
    foreach ($changed as $key => $client) {
        while (true) {
            // read socket data
            $msg = @fread($client, 1024);
//            $msg = 1;
            if ($msg) {
                // application process
            } else {
                if (feof($client)) {
                    // TODO check data eof
                }
                break;
            }

可以看到,在文件~/nio_server.php 中, 虽然设置了 stream_set_blocking false, 但是在209行的 fread() , 这是在一个循环里读,这是一个阻塞读取。这的系统函数的响应速度是受系统IO影响的。

而异步调用中,当有I/O事件时,系统会将数据复制到用户内存中,也就是准备好数据,再通知到用户程序。

那么原生PHP显然是不支持的,这里呢就要引入PHP拓展,就是 Event,或者 Ev 拓展。这篇博客主要讲 Event

Event 拓展是基于 libevent 库封装而来,而 Ev 拓展是基于 libev 库封装而来。 通过PHP接口,和C库的接口就能看到他们之间的联系,所以,如果通过PHP文档找不到相关资料可以去,看看C库的文档。而 Libevent 年久失修,不推荐大家使用。

这里放上用Event实现的Tcp Serverdemo

在用Event做这个demo中,我用到了EventBuffer ,读、写都和Buffer交互, Buffer数据是用户态数据,不会等待系统I/O或被阻塞,避免了程序耗时在I/O数据拷贝上。由此PHP 也能实现 AIO 程式,提高CPU利用率。

讲到这里,就会感觉这个PHP的AIO有些牵强了,我这找了其他博主的论点来帮助大家理解,这两张图展示了 用户程序,与内核采用 分阻塞异步 交互时的异同。

image.png

上面是非阻塞IO,下面是异步IO。中间的区别就是非阻塞IO的应用,需要不断的去访问内核获取数据(当然了,每一次访问都是有求必应,能取到数据),但不一定能取完; 而异步IO的特点就是,你告诉内核取数据,取完整了,我再一起发给应用程序。这就是Linux对异步IO的定义。

image.png

那么再看到我们的Demo,这是一个简单TCP server,一个TCP请求系统是能知道一个数据的包大小的,是否接收完毕,这是传输层要做的。而我们的应用层面,是接收到数据还要做合并,分包,以及数据转码。 这就和 AIO 数据结果必须是完整的,概率有些出入,(在系统层面显然是完整的) . 在应用层面呢,一次性收到的不一定是完整的数据,那么就还需要做额外代码来解决合包,分包,沾包。这就是AIO实现Tcp Server的需要问题。

为了解决以上问题,就需要自定义TCP通讯协议。相当于自己开发RPC框架了。

那我们来看看Http呢,在应用层面有明确公开的协议(协议有头无尾,标明了每次请求具体长度),并有丰富的实现。这就是一个非常适合采用AIO编程协议。而PHP的Event拓展,恰好有EventHttp实现。

话不多说,先上 Demo

<?php
...

/**
 * event http 请求回调函数
 * 
 * @param   \EventHttpRequest   $req    Http请求对象
 */
function _http_about($req) {
    echo __METHOD__, PHP_EOL;
    // print request URL
    echo "URI: ", $req->getUri(), PHP_EOL;
    // print request's headers
    echo "Input headers:"; var_dump($req->getInputHeaders());
    echo "\n >> Sending reply ...";
    /**
     * @var \EventBuffer    $buf
     */
    $buf = $req->getOutputBuffer();
    $buf->add("It's about Event http server");
    $req->sendReply(200, "OK", $buf);
    echo "OK\n";
}

这里是一个回调函数,入参数就是一个由 EventHttp 封装的http请求对象。这就满足了以上 调用时非阻塞,数据完全准备好后,再通知回调——异步I/O。好,借助Event,PHP就实现了AIO.

结语

关于性能提升,这就不做压测了,主要论证PHP实现NIOAIO 的可行性。也实际给大家展示了几个Demo, 简单展示了如何写异步,非阻塞程序。可以看到 异步编程 对大家的要求是比较高的,当需要发起 IO 操作,都要用非阻塞方式调用,不然就会阻塞整个进程,而纯粹的异步编程就是单进程,阻塞后该服务就不能响应新的请求。同时呢,我们常用PDO,mysqli,Redis这些不得不用的拓展,也只提供了阻塞读的接口。而当前PHP环境中,可以说“几乎所有”的第三方框架,都是阻塞编码,如果你的项目中使用了其他框架,那么你写的代码没问题,不保证依赖的第三方框架阻塞方式请求 I\O. 所以,一般 PHP 异步编程,都会采用多进程异步,让异步来提高每个请求的响应速度,如果进程阻塞,就让其他空闲的进程处理新进入的请求。

以上,希望大家通过文章能了解 异步/同步阻塞/非阻塞区别,以及对PHP异步非阻塞编程。

有问题欢迎提问~

参考

  1. PHP实现非阻塞
  2. PHP回顾之socket编程
  3. Cooperative multitasking using coroutines (in PHP!)
  4. IO - 同步,异步,阻塞,非阻塞
  5. 同步/异步,阻塞/非阻塞概念深度解析
  6. PHP之高性能I/O框架:Libevent
  7. 网络编程(三):从libevent到事件通知机制
查看原文

赞 57 收藏 34 评论 4

tumi 赞了文章 · 2019-12-31

MySQL 性能优化神器 Explain 使用分析

简介

MySQL 提供了一个 EXPLAIN 命令, 它可以对 SELECT 语句进行分析, 并输出 SELECT 执行的详细信息, 以供开发人员针对性优化.
EXPLAIN 命令用法十分简单, 在 SELECT 语句前加上 Explain 就可以了, 例如:

EXPLAIN SELECT * from user_info WHERE  id < 300;

准备

为了接下来方便演示 EXPLAIN 的使用, 首先我们需要建立两个测试用的表, 并添加相应的数据:

CREATE TABLE `user_info` (
  `id`   BIGINT(20)  NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(50) NOT NULL DEFAULT '',
  `age`  INT(11)              DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `name_index` (`name`)
)
  ENGINE = InnoDB
  DEFAULT CHARSET = utf8

INSERT INTO user_info (name, age) VALUES ('xys', 20);
INSERT INTO user_info (name, age) VALUES ('a', 21);
INSERT INTO user_info (name, age) VALUES ('b', 23);
INSERT INTO user_info (name, age) VALUES ('c', 50);
INSERT INTO user_info (name, age) VALUES ('d', 15);
INSERT INTO user_info (name, age) VALUES ('e', 20);
INSERT INTO user_info (name, age) VALUES ('f', 21);
INSERT INTO user_info (name, age) VALUES ('g', 23);
INSERT INTO user_info (name, age) VALUES ('h', 50);
INSERT INTO user_info (name, age) VALUES ('i', 15);
CREATE TABLE `order_info` (
  `id`           BIGINT(20)  NOT NULL AUTO_INCREMENT,
  `user_id`      BIGINT(20)           DEFAULT NULL,
  `product_name` VARCHAR(50) NOT NULL DEFAULT '',
  `productor`    VARCHAR(30)          DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `user_product_detail_index` (`user_id`, `product_name`, `productor`)
)
  ENGINE = InnoDB
  DEFAULT CHARSET = utf8

INSERT INTO order_info (user_id, product_name, productor) VALUES (1, 'p1', 'WHH');
INSERT INTO order_info (user_id, product_name, productor) VALUES (1, 'p2', 'WL');
INSERT INTO order_info (user_id, product_name, productor) VALUES (1, 'p1', 'DX');
INSERT INTO order_info (user_id, product_name, productor) VALUES (2, 'p1', 'WHH');
INSERT INTO order_info (user_id, product_name, productor) VALUES (2, 'p5', 'WL');
INSERT INTO order_info (user_id, product_name, productor) VALUES (3, 'p3', 'MA');
INSERT INTO order_info (user_id, product_name, productor) VALUES (4, 'p1', 'WHH');
INSERT INTO order_info (user_id, product_name, productor) VALUES (6, 'p1', 'WHH');
INSERT INTO order_info (user_id, product_name, productor) VALUES (9, 'p8', 'TE');

EXPLAIN 输出格式

EXPLAIN 命令的输出内容大致如下:

mysql> explain select * from user_info where id = 2\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: user_info
   partitions: NULL
         type: const
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 8
          ref: const
         rows: 1
     filtered: 100.00
        Extra: NULL
1 row in set, 1 warning (0.00 sec)

各列的含义如下:

  • id: SELECT 查询的标识符. 每个 SELECT 都会自动分配一个唯一的标识符.

  • select_type: SELECT 查询的类型.

  • table: 查询的是哪个表

  • partitions: 匹配的分区

  • type: join 类型

  • possible_keys: 此次查询中可能选用的索引

  • key: 此次查询中确切使用到的索引.

  • ref: 哪个字段或常数与 key 一起被使用

  • rows: 显示此查询一共扫描了多少行. 这个是一个估计值.

  • filtered: 表示此查询条件所过滤的数据的百分比

  • extra: 额外的信息

接下来我们来重点看一下比较重要的几个字段.

select_type

select_type 表示了查询的类型, 它的常用取值有:

  • SIMPLE, 表示此查询不包含 UNION 查询或子查询

  • PRIMARY, 表示此查询是最外层的查询

  • UNION, 表示此查询是 UNION 的第二或随后的查询

  • DEPENDENT UNION, UNION 中的第二个或后面的查询语句, 取决于外面的查询

  • UNION RESULT, UNION 的结果

  • SUBQUERY, 子查询中的第一个 SELECT

  • DEPENDENT SUBQUERY: 子查询中的第一个 SELECT, 取决于外面的查询. 即子查询依赖于外层查询的结果.

最常见的查询类别应该是 SIMPLE 了, 比如当我们的查询没有子查询, 也没有 UNION 查询时, 那么通常就是 SIMPLE 类型, 例如:

mysql> explain select * from user_info where id = 2\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: user_info
   partitions: NULL
         type: const
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 8
          ref: const
         rows: 1
     filtered: 100.00
        Extra: NULL
1 row in set, 1 warning (0.00 sec)

如果我们使用了 UNION 查询, 那么 EXPLAIN 输出 的结果类似如下:

mysql> EXPLAIN (SELECT * FROM user_info  WHERE id IN (1, 2, 3))
    -> UNION
    -> (SELECT * FROM user_info WHERE id IN (3, 4, 5));
+----+--------------+------------+------------+-------+---------------+---------+---------+------+------+----------+-----------------+
| id | select_type  | table      | partitions | type  | possible_keys | key     | key_len | ref  | rows | filtered | Extra           |
+----+--------------+------------+------------+-------+---------------+---------+---------+------+------+----------+-----------------+
|  1 | PRIMARY      | user_info  | NULL       | range | PRIMARY       | PRIMARY | 8       | NULL |    3 |   100.00 | Using where     |
|  2 | UNION        | user_info  | NULL       | range | PRIMARY       | PRIMARY | 8       | NULL |    3 |   100.00 | Using where     |
| NULL | UNION RESULT | <union1,2> | NULL       | ALL   | NULL          | NULL    | NULL    | NULL | NULL |     NULL | Using temporary |
+----+--------------+------------+------------+-------+---------------+---------+---------+------+------+----------+-----------------+
3 rows in set, 1 warning (0.00 sec)

table

表示查询涉及的表或衍生表

type

type 字段比较重要, 它提供了判断查询是否高效的重要依据依据. 通过 type 字段, 我们判断此次查询是 全表扫描 还是 索引扫描 等.

type 常用类型

type 常用的取值有:

  • system: 表中只有一条数据. 这个类型是特殊的 const 类型.

  • const: 针对主键或唯一索引的等值查询扫描, 最多只返回一行数据. const 查询速度非常快, 因为它仅仅读取一次即可.
    例如下面的这个查询, 它使用了主键索引, 因此 type 就是 const 类型的.

mysql> explain select * from user_info where id = 2\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: user_info
   partitions: NULL
         type: const
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 8
          ref: const
         rows: 1
     filtered: 100.00
        Extra: NULL
1 row in set, 1 warning (0.00 sec)
  • eq_ref: 此类型通常出现在多表的 join 查询, 表示对于前表的每一个结果, 都只能匹配到后表的一行结果. 并且查询的比较操作通常是 =, 查询效率较高. 例如:

mysql> EXPLAIN SELECT * FROM user_info, order_info WHERE user_info.id = order_info.user_id\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: order_info
   partitions: NULL
         type: index
possible_keys: user_product_detail_index
          key: user_product_detail_index
      key_len: 314
          ref: NULL
         rows: 9
     filtered: 100.00
        Extra: Using where; Using index
*************************** 2. row ***************************
           id: 1
  select_type: SIMPLE
        table: user_info
   partitions: NULL
         type: eq_ref
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 8
          ref: test.order_info.user_id
         rows: 1
     filtered: 100.00
        Extra: NULL
2 rows in set, 1 warning (0.00 sec)
  • ref: 此类型通常出现在多表的 join 查询, 针对于非唯一或非主键索引, 或者是使用了 最左前缀 规则索引的查询.
    例如下面这个例子中, 就使用到了 ref 类型的查询:

mysql> EXPLAIN SELECT * FROM user_info, order_info WHERE user_info.id = order_info.user_id AND order_info.user_id = 5\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: user_info
   partitions: NULL
         type: const
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 8
          ref: const
         rows: 1
     filtered: 100.00
        Extra: NULL
*************************** 2. row ***************************
           id: 1
  select_type: SIMPLE
        table: order_info
   partitions: NULL
         type: ref
possible_keys: user_product_detail_index
          key: user_product_detail_index
      key_len: 9
          ref: const
         rows: 1
     filtered: 100.00
        Extra: Using index
2 rows in set, 1 warning (0.01 sec)
  • range: 表示使用索引范围查询, 通过索引字段范围获取表中部分数据记录. 这个类型通常出现在 =, <>, >, >=, <, <=, IS NULL, <=>, BETWEEN, IN() 操作中.
    typerange 时, 那么 EXPLAIN 输出的 ref 字段为 NULL, 并且 key_len 字段是此次查询中使用到的索引的最长的那个.

例如下面的例子就是一个范围查询:

mysql> EXPLAIN SELECT *
    ->         FROM user_info
    ->         WHERE id BETWEEN 2 AND 8 \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: user_info
   partitions: NULL
         type: range
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 8
          ref: NULL
         rows: 7
     filtered: 100.00
        Extra: Using where
1 row in set, 1 warning (0.00 sec)
  • index: 表示全索引扫描(full index scan), 和 ALL 类型类似, 只不过 ALL 类型是全表扫描, 而 index 类型则仅仅扫描所有的索引, 而不扫描数据.
    index 类型通常出现在: 所要查询的数据直接在索引树中就可以获取到, 而不需要扫描数据. 当是这种情况时, Extra 字段 会显示 Using index.

例如:

mysql> EXPLAIN SELECT name FROM  user_info \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: user_info
   partitions: NULL
         type: index
possible_keys: NULL
          key: name_index
      key_len: 152
          ref: NULL
         rows: 10
     filtered: 100.00
        Extra: Using index
1 row in set, 1 warning (0.00 sec)

上面的例子中, 我们查询的 name 字段恰好是一个索引, 因此我们直接从索引中获取数据就可以满足查询的需求了, 而不需要查询表中的数据. 因此这样的情况下, type 的值是 index, 并且 Extra 的值是 Using index.

  • ALL: 表示全表扫描, 这个类型的查询是性能最差的查询之一. 通常来说, 我们的查询不应该出现 ALL 类型的查询, 因为这样的查询在数据量大的情况下, 对数据库的性能是巨大的灾难. 如一个查询是 ALL 类型查询, 那么一般来说可以对相应的字段添加索引来避免.
    下面是一个全表扫描的例子, 可以看到, 在全表扫描时, possible_keys 和 key 字段都是 NULL, 表示没有使用到索引, 并且 rows 十分巨大, 因此整个查询效率是十分低下的.

mysql> EXPLAIN SELECT age FROM  user_info WHERE age = 20 \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: user_info
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 10
     filtered: 10.00
        Extra: Using where
1 row in set, 1 warning (0.00 sec)

type 类型的性能比较

通常来说, 不同的 type 类型的性能关系如下:
ALL < index < range ~ index_merge < ref < eq_ref < const < system
ALL 类型因为是全表扫描, 因此在相同的查询条件下, 它是速度最慢的.
index 类型的查询虽然不是全表扫描, 但是它扫描了所有的索引, 因此比 ALL 类型的稍快.
后面的几种类型都是利用了索引来查询数据, 因此可以过滤部分或大部分数据, 因此查询效率就比较高了.

possible_keys

possible_keys 表示 MySQL 在查询时, 能够使用到的索引. 注意, 即使有些索引在 possible_keys 中出现, 但是并不表示此索引会真正地被 MySQL 使用到. MySQL 在查询时具体使用了哪些索引, 由 key 字段决定.

key

此字段是 MySQL 在当前查询时所真正使用到的索引.

key_len

表示查询优化器使用了索引的字节数. 这个字段可以评估组合索引是否完全被使用, 或只有最左部分字段被使用到.
key_len 的计算规则如下:

  • 字符串

    • char(n): n 字节长度

    • varchar(n): 如果是 utf8 编码, 则是 3 n + 2字节; 如果是 utf8mb4 编码, 则是 4 n + 2 字节.

  • 数值类型:

    • TINYINT: 1字节

    • SMALLINT: 2字节

    • MEDIUMINT: 3字节

    • INT: 4字节

    • BIGINT: 8字节

  • 时间类型

    • DATE: 3字节

    • TIMESTAMP: 4字节

    • DATETIME: 8字节

  • 字段属性: NULL 属性 占用一个字节. 如果一个字段是 NOT NULL 的, 则没有此属性.

我们来举两个简单的栗子:

mysql> EXPLAIN SELECT * FROM order_info WHERE user_id < 3 AND product_name = 'p1' AND productor = 'WHH' \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: order_info
   partitions: NULL
         type: range
possible_keys: user_product_detail_index
          key: user_product_detail_index
      key_len: 9
          ref: NULL
         rows: 5
     filtered: 11.11
        Extra: Using where; Using index
1 row in set, 1 warning (0.00 sec)

上面的例子是从表 order_info 中查询指定的内容, 而我们从此表的建表语句中可以知道, 表 order_info 有一个联合索引:

KEY `user_product_detail_index` (`user_id`, `product_name`, `productor`)

不过此查询语句 WHERE user_id < 3 AND product_name = 'p1' AND productor = 'WHH' 中, 因为先进行 user_id 的范围查询, 而根据 最左前缀匹配 原则, 当遇到范围查询时, 就停止索引的匹配, 因此实际上我们使用到的索引的字段只有 user_id, 因此在 EXPLAIN 中, 显示的 key_len 为 9. 因为 user_id 字段是 BIGINT, 占用 8 字节, 而 NULL 属性占用一个字节, 因此总共是 9 个字节. 若我们将user_id 字段改为 BIGINT(20) NOT NULL DEFAULT '0', 则 key_length 应该是8.

上面因为 最左前缀匹配 原则, 我们的查询仅仅使用到了联合索引的 user_id 字段, 因此效率不算高.

接下来我们来看一下下一个例子:

mysql> EXPLAIN SELECT * FROM order_info WHERE user_id = 1 AND product_name = 'p1' \G;
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: order_info
   partitions: NULL
         type: ref
possible_keys: user_product_detail_index
          key: user_product_detail_index
      key_len: 161
          ref: const,const
         rows: 2
     filtered: 100.00
        Extra: Using index
1 row in set, 1 warning (0.00 sec)

这次的查询中, 我们没有使用到范围查询, key_len 的值为 161. 为什么呢? 因为我们的查询条件 WHERE user_id = 1 AND product_name = 'p1' 中, 仅仅使用到了联合索引中的前两个字段, 因此 keyLen(user_id) + keyLen(product_name) = 9 + 50 * 3 + 2 = 161

rows

rows 也是一个重要的字段. MySQL 查询优化器根据统计信息, 估算 SQL 要查找到结果集需要扫描读取的数据行数.
这个值非常直观显示 SQL 的效率好坏, 原则上 rows 越少越好.

Extra

EXplain 中的很多额外的信息会在 Extra 字段显示, 常见的有以下几种内容:

  • Using filesort
    当 Extra 中有 Using filesort 时, 表示 MySQL 需额外的排序操作, 不能通过索引顺序达到排序效果. 一般有 Using filesort, 都建议优化去掉, 因为这样的查询 CPU 资源消耗大.

例如下面的例子:

mysql> EXPLAIN SELECT * FROM order_info ORDER BY product_name \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: order_info
   partitions: NULL
         type: index
possible_keys: NULL
          key: user_product_detail_index
      key_len: 253
          ref: NULL
         rows: 9
     filtered: 100.00
        Extra: Using index; Using filesort
1 row in set, 1 warning (0.00 sec)

我们的索引是

KEY `user_product_detail_index` (`user_id`, `product_name`, `productor`)

但是上面的查询中根据 product_name 来排序, 因此不能使用索引进行优化, 进而会产生 Using filesort.
如果我们将排序依据改为 ORDER BY user_id, product_name, 那么就不会出现 Using filesort 了. 例如:

mysql> EXPLAIN SELECT * FROM order_info ORDER BY user_id, product_name \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: order_info
   partitions: NULL
         type: index
possible_keys: NULL
          key: user_product_detail_index
      key_len: 253
          ref: NULL
         rows: 9
     filtered: 100.00
        Extra: Using index
1 row in set, 1 warning (0.00 sec)
  • Using index
    "覆盖索引扫描", 表示查询在索引树中就可查找所需数据, 不用扫描表数据文件, 往往说明性能不错

  • Using temporary
    查询有使用临时表, 一般出现于排序, 分组和多表 join 的情况, 查询效率不高, 建议优化.

查看原文

赞 261 收藏 282 评论 19

tumi 赞了回答 · 2019-12-07

解决go 从RGBA() 获取原始的值

fmt.Println(color1.(color.NRGBA).R)
fmt.Println(color1.(color.NRGBA).A)
fmt.Println(color1.(color.NRGBA).B)
fmt.Println(color1.(color.NRGBA).G)

NRGBA结构体

// NRGBA represents a non-alpha-premultiplied 32-bit color.
type NRGBA struct {
    R, G, B, A uint8
}

关注 2 回答 2

tumi 回答了问题 · 2019-12-02

解决go 从RGBA() 获取原始的值

fmt.Println(color1.(color.NRGBA).R)

关注 2 回答 2

认证与成就

  • 获得 14 次点赞
  • 获得 21 枚徽章 获得 0 枚金徽章, 获得 6 枚银徽章, 获得 15 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-05-27
个人主页被 410 人浏览