大家好

大家好 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

大家好 赞了文章 · 2020-08-16

【2020Java最新学习路线】写了很久,这是一份最适合普通大众、科班、非科班的路线

本文 GitHubhttps://github.com/JavaFamily 已收录,有一线大厂面试完整考点、资料以及我的系列文章。

前言

这期我想写很久了,但是因为时间的原因一直拖到了现在,我以为一两天就写完了,结果从构思到整理资料,再到写出来用了差不多一周的时间吧。

你们也知道丙丙一直都是创作鬼才来的,所以我肯定不会一本正经的写,我想了好几个切入点,最后决定用一个完整的电商系统作为切入点,带着大家看看,我们需要学些啥,我甚至还收集配套视频和资料,暖男石锤啊,这期是呕心沥血之作,不要白嫖了。

正文

在写这个文章之前,我花了点时间,自己臆想了一个电商系统,基本上算是麻雀虽小五脏俱全,我今天就用它开刀,一步步剖析,我会讲一下我们可能会接触的技术栈可能不全,但是够用,最后给个学习路线。

Tip:请多欣赏一会,每个点看一下,看看什么地方是你接触过的,什么技术栈是你不太熟悉的,我觉得还算是比较全的,有什么建议也可以留言给我。

不知道大家都看了一下没,现在我们就要庖丁解牛了,我从上到下依次分析。

前端

你可能会会好奇,你不是讲后端学习路线嘛,为啥还有前端的部分,我只能告诉你,傻瓜,肤浅。

我们可不能闭门造车,谁告诉你后端就不学点前端了?

前端现在很多也了解后端的技术栈的,你想我们去一个网站,最先接触的,最先看到的是啥?

没错就是前端,在大学你要是找不到专门的前端同学,去做系统肯定也要自己顶一下前端的,那我觉得最基本的技术栈得熟悉和了解吧,丙丙现在也是偶尔会开发一下我们的管理系统主要是VUEReact

在这里我列举了我目前觉得比较简单和我们后端可以了解的技术栈,都是比较基础的。

作为一名后端了解部分前端知识还是很有必要的,在以后开发的时候,公司有前端那能帮助你前后端联调更顺畅,如果没前端你自己也能顶一下简单的页面。

HTMLCSSJSAjax我觉得是必须掌握的点,看着简单其实深究或者去操作的话还是有很多东西的,其他作为扩展有兴趣可以了解,反正入门简单,只是精通很难很难。

在这一层不光有这些还有Http协议和Servlet,requestresponsecookiesession这些也会伴随你整个技术生涯,理解他们对后面的你肯定有不少好处。

Tip:我这里最后删除了JSP相关的技术,我个人觉得没必要学了,很多公司除了老项目之外,新项目都不会使用那些技术了。

前端在我看来比后端难,技术迭代比较快,知识好像也没特定的体系,所以面试大厂的前端很多朋友都说难,不是技术多难,而是知识多且复杂,找不到一个完整的体系,相比之下后端明朗很多,我后面就开始讲后端了。

网关层:

互联网发展到现在,涌现了很多互联网公司,技术更新迭代了很多个版本,从早期的单机时代,到现在超大规模的互联网时代,几亿人参与的春运,几千亿成交规模的双十一,无数互联网前辈的造就了现在互联网的辉煌。

微服务分布式负载均衡等我们经常提到的这些名词都是这些技术在场景背后支撑。

单机顶不住,我们就多找点服务器,但是怎么将流量均匀的打到这些服务器上呢?

负载均衡,LVS

我们机器都是IP访问的,那怎么通过我们申请的域名去请求到服务器呢?

DNS

大家刷的抖音,B站,快手等等视频服务商,是怎么保证同时为全国的用户提供快速的体验?

CDN

我们这么多系统和服务,还有这么多中间件的调度怎么去管理调度等等?

zk

这么多的服务器,怎么对外统一访问呢,就可能需要知道反向代理的服务器。

Nginx

这一层做了反向负载、服务路由、服务治理、流量管理、安全隔离、服务容错等等都做了,大家公司的内外网隔离也是这一层做的。

我之前还接触过一些比较有意思的项目,所有对外的接口都是加密的,几十个服务会经过网关解密,找到真的路由再去请求。

这一层的知识点其实也不少,你往后面学会发现分布式事务,分布式锁,还有很多中间件都离不开zk这一层,我们继续往下看。

服务层:

这一层有点东西了,算是整个框架的核心,如果你跟我帅丙一样以后都是从事后端开发的话,我们基本上整个技术生涯,大部分时间都在跟这一层的技术栈打交道了,各种琳琅满目的中间件,计算机基础知识,Linux操作,算法数据结构,架构框架,研发工具等等。

我想在看这个文章的各位,计算机基础肯定都是学过的吧,如果大学的时候没好好学,我觉得还是有必要再看看的。

为什么我们网页能保证安全可靠的传输,你可能会了解到HTTP,TCP协议,什么三次握手,四次挥手。

还有进程、线程、协程,什么内存屏障,指令乱序,分支预测,CPU亲和性等等,在之后的编程生涯,如果你能掌握这些东西,会让你在遇到很多问题的时候瞬间get到点,而不是像个无头苍蝇一样乱撞(然而丙丙还做得不够)。

了解这些计算机知识后,你就需要接触编程语言了,大学的C语言基础会让你学什么语言入门都会快点,我选择了面向对象的JAVA,但是也不知道为啥现在还没对象。

JAVA的基础也一样重要,面向对象(包括类、对象、方法、继承、封装、抽象、 多态、消息解析等),常见API,数据结构,集合框架设计模式(包括创建型、结构型、行为型),多线程和并发I/O流,Stream,网络编程你都需要了解。

代码会写了,你就要开始学习一些能帮助你把系统变得更加规范的框架,SSM可以会让你的开发更加便捷,结构层次更加分明。

写代码的时候你会发现你大学用的Eclipse在公司看不到了,你跟大家一样去用了IDEA,第一天这是什么玩意,一周后,真香,但是这玩意收费有点贵,那免费的VSCode真的就是不错的选择了。

代码写的时候你会接触代码的仓库管理工具mavenGradle,提交代码的时候会去写项目版本管理工具Git

代码提交之后,发布之后你会发现很多东西需要自己去服务器亲自排查,那Linux的知识点就可以在里面灵活运用了,查看进程,查看文件,各种Vim操作等等。

系统的优化很多地方没优化的空间了,你可能会尝试从算法,或者优化数据结构去优化,你看到了HashMap的源码,想去了解红黑树,然后在算法网上看到了二叉树搜索树和各种常见的算法问题,刷多了,你也能总结出精华所在,什么贪心,分治,动态规划等。

这么多个服务,你发现HTTP请求已经开始有点不满足你的需求了,你想开发更便捷,像访问本地服务一样访问远程服务,所以我们去了解了Dubbo,Spring cloud

了解Dubbo的过程中,你发现了RPC的精华所在,所以你去接触到了高性能的NIO框架,Netty

代码写好了,服务也能通信了,但是你发现你的代码链路好长,都耦合在一起了,所以你接触了消息队列,这种异步的处理方式,真香。

他还可以帮你在突发流量的时候用队列做缓冲,但是你发现分布式的情况,事务就不好管理了,你就了解到了分布式事务,什么两段式,三段式,TCC,XA,阿里云的全局事务服务GTS等等。

分布式事务的时候你会想去了解RocketMQ,因为他自带了分布式事务的解决方案,大数据的场景你又看到了Kafka

我上面提到过zk,像DubboKafka等中间件都是用它做注册中心的,所以很多技术栈最后都组成了一个知识体系,你先了解了体系中的每一员,你才能把它们联系起来。

服务的交互都从进程内通信变成了远程通信,所以性能必然会受到一些影响。

此外由于很多不确定性的因素,例如网络拥塞、Server 端服务器宕机、挖掘机铲断机房光纤等等,需要许多额外的功能和措施才能保证微服务流畅稳定的工作。

Spring Cloud 中就有 Hystrix 熔断器、Ribbon客户端负载均衡器、Eureka注册中心等等都是用来解决这些问题的微服务组件。

你感觉学习得差不多了,你发现各大论坛博客出现了一些前沿技术,比如容器化,你可能就会去了解容器化的知识,像Docker,Kubernetes(K8s)等。

微服务之所以能够快速发展,很重要的一个原因就是:容器化技术的发展和容器管理系统的成熟。

这一层的东西呢其实远远不止这些的,我不过多赘述,写多了像个劝退师一样,但是大家也不用慌,大部分的技术都是慢慢接触了,工作中慢慢去了解,去深入的。

好啦我们继续沿着图往下看,那再往下是啥呢?

数据层:

数据库可能是整个系统中最值钱的部分了,在我码文字的前一天,刚好发生了微盟程序员删库跑路的操作,删库跑路其实是我们在网上最常用的笑话,没想到还是照进了现实。

这里也提一点点吧,36小时的故障,其实在互联网公司应该是个笑话了吧,权限控制没做好类似rm -rf 、fdisk、drop等等这样的高危命令是可以实时拦截掉的,备份,全量备份,增量备份,延迟备份,异地容灾全部都考虑一下应该也不至于这样,一家上市公司还是有点点不应该。

数据库基本的事务隔离级别索引,SQL,主被同步,读写分离等都可能是你学的时候要了解到的。

上面我们提到了安全,不要把鸡蛋放一个篮子的道理大家应该都知道,那分库的意义就很明显了,然后你会发现时间久了表的数据大了,就会想到去接触分表,什么TDDLSharding-JDBCDRDS这些插件都会接触到。

你发现流量大的时候,或者热点数据打到数据库还是有点顶不住,压力太大了,那非关系型数据库就进场了,Redis当然是首选,但是MongoDB、memcache也有各自的应用场景。

Redis使用后,真香,真快,但是你会开始担心最开始提到的安全问题,这玩意快是因为在内存中操作,那断点了数据丢了怎么办?你就开始阅读官方文档,了解RDB,AOF这些持久化机制,线上用的时候还会遇到缓存雪崩击穿、穿透等等问题。

单机不满足你就用了,他的集群模式,用了集群可能也担心集群的健康状态,所以就得去了解哨兵,他的主从同步,时间久了Key多了,就得了解内存淘汰机制……

他的大容量存储有问题,你可能需要去了解Pika….

其实远远没完,每个的点我都点到为止,但是其实要深究每个点都要学很久,我们接着往下看。

实时/离线/大数据

等你把几种关系型非关系型数据库的知识点,整理清楚后,你会发现数据还是大啊,而且数据的场景越来越多多样化了,那大数据的各种中间件你就得了解了。

你会发现很多场景,不需要实时的数据,比如你查你的支付宝去年的,上个月的账单,这些都是不会变化的数据,没必要实时,那你可能会接触像ODPS这样的中间件去做数据的离线分析。

然后你可能会接触Hadoop系列相关的东西,比如于Hadoop(HDFS)的一个数据仓库工具Hive,是建立在 Hadoop 文件系统之上的分布式面向列的数据库HBase

写多的场景,适合做一些简单查询,用他们又有点大材小用,那Cassandra就再合适不过了。

离线的数据分析没办法满足一些实时的常见,类似风控,那Flink你也得略知一二,他的窗口思想还是很有意思。

数据接触完了,计算引擎Spark你是不是也不能放过……

搜索引擎:

传统关系型数据库和NoSQL非关系型数据都没办法解决一些问题,比如我们在百度,淘宝搜索东西的时候,往往都是几个关键字在一起一起搜索东西的,在数据库除非把几次的结果做交集,不然很难去实现。

那全文检索引擎就诞生了,解决了搜索的问题,你得思考怎么把数据库的东西实时同步到ES中去,那你可能会思考到logstash去定时跑脚本同步,又或者去接触伪装成一台MySQL从服务的Canal,他会去订阅MySQL主服务的binlog,然后自己解析了去操作Es中的数据。

这些都搞定了,那可视化的后台查询又怎么解决呢?Kibana,他他是一个可视化的平台,甚至对Es集群的健康管理都做了可视化,很多公司的日志查询系统都是用它做的。

学习路线

看了这么久你是不是发现,帅丙只是一直在介绍每个层级的技术栈,并没说到具体的一个路线,那是因为我想让大家先有个认知或者说是扫盲吧,我一样用脑图的方式汇总一下吧,如果图片被平台二压了,可以去公众号回复【路线】。

资料/学习网站

Tip:本来这一栏有很多我准备的资料的,但是都是外链,或者不合适的分享方式,博客的运营小姐姐提醒了我,所以大家去公众号回复【路线】好了。

絮叨

如果你想去一家不错的公司,但是目前的硬实力又不到,我觉得还是有必要去努力一下的,技术能力的高低能决定你走多远,平台的高低,能决定你的高度。

如果你通过努力成功进入到了心仪的公司,一定不要懈怠放松,职场成长和新技术学习一样,不进则退。

丙丙发现在工作中发现我身边的人真的就是实力越强的越努力,最高级的自律,享受孤独(周末的歪哥)。

总结

我提到的技术栈你想全部了解,我觉得初步了解可能几个月就够了,这里的了解仅限于你知道它,知道他是干嘛的,知道怎么去使用它,并不是说深入了解他的底层原理,了解他的常见问题,熟悉问题的解决方案等等。

你想做到后者,基本上只能靠时间上的日积月累,或者不断的去尝试积累经验,也没什么速成的东西,欲速则不达大家也是知道的。

技术这条路,说实话很枯燥,很辛苦,但是待遇也会高于其他一些基础岗位。

所实话我大学学这个就是为了兴趣,我从小对电子,对计算机都比较热爱,但是现在打磨得,现在就是为了钱吧,是不是很现实?若家境殷实,谁愿颠沛流离。

但是至少丙丙因为做软件,改变了家庭的窘境,自己日子也向小康一步步迈过去。

说做程序员改变了我和我家人的一生可能夸张了,但是我总有一种下班辈子会因为我选择走这条路而改变的错觉。

我是敖丙,一个在互联网苟且偷生的工具人。

创作不易,本期硬核,不想被白嫖,各位的「三连」就是丙丙创作的最大动力,我们下次见!

本文 GitHubhttps://github.com/JavaFamily 已经收录,有大厂面试完整考点,欢迎Star。
查看原文

赞 97 收藏 61 评论 6

大家好 关注了用户 · 2020-08-16

敖丙 @aobing

关注 5798

大家好 收藏了文章 · 2020-08-16

嗯,手搓一个TinyPng压缩图片的WebpackPlugin也SoEasy啦

前言

曾经发表过一篇性能优化的文章《前端性能优化指南》,笔者总结了一些在项目开发过程中使用过的性能优化经验。说句真话,性能优化可能在面试过程中会有用,实际在项目开发过程中可能没几个同学会注意这些性能优化的细节。

若经常关注性能优化的话题,可能会发现无论怎样对代码做最好的优化也不及对一张图片做一次压缩好。所以压缩图片成了性能优化里最常见的操作,不管是手动压缩图片还是自动压缩图片,在项目开发过程中必须得有。

自动压缩图片通常在webpack构建项目时接入一些第三方Loader&Plugin来处理。打开Github,搜素webpack image等关键字,Star最多还是image-webpack-loaderimagemin-webpack-plugin这两个Loader&Plugin。很多同学可能都会选择它们,方便快捷,简单易用,无脑接入。

可是,这两个Loader&Plugin存在一些特别问题,它们都是基于imagemin开发的。imagemin的某些依赖托管在国外服务器,在npm i xxx安装它们时会默认走GitHub Releases的托管地址,若不是规范上网,你们是不可能安装得上的,即使规范上网也不一定安装得上。所以笔者又刨根到底发表了一篇关于NPM镜像处理的文章《聊聊NPM镜像那些险象环生的坑》,专门解决这些因为网络环境而导致安装失败的问题。除了这个安装问题,imagemin还存在另一个大问题,就是压缩质感损失得比较严重,图片体积越大越明显,压缩出来的图片总有几张是失真的,而且总体压缩率不是很高。这样在交付项目时有可能被细心的QA小姐姐抓个正着,怎么和设计图对比起来不清晰啊!

工具

图片压缩工具

此时可能有些同学已转战到手动压缩图片了。比较好用的图片压缩工具无非就是以下几个,若有更好用的工具麻烦在评论里补充喔!同时笔者也整理出它们的区别,供各位同学参考。

工具集合

工具开源收费API免费体验
QuickPicture✖️✔️✖️可压缩类型较多,压缩质感较好,有体积限制,有数量限制
ShrinkMe✖️✖️✖️可压缩类型较多,压缩质感一般,无数量限制,有体积限制
Squoosh✔️✖️✔️可压缩类型较少,压缩质感一般,无数量限制,有体积限制
TinyJpg✖️✔️✔️可压缩类型较少,压缩质感很好,有数量限制,有体积限制
TinyPng✖️✔️✔️可压缩类型较少,压缩质感很好,有数量限制,有体积限制
Zhitu✖️✖️✖️可压缩类型一般,压缩质感一般,有数量限制,有体积限制

从上述表格对比可看出,免费体验都会存在体积限制,这个可理解,即使收费也一样,毕竟每个人都上传单张10多M的图片,哪个服务器能受得了。再来就是数量限制,一次只能上传20张,好像有个规律,压缩质感好就限制数量,否则就不限制数量,当然收费后就没有限制了。再来就是可压缩类型,图片类型一般是jpgpnggifsvgwebpgif压缩后一般都会失真,svg通常用在矢量图标上很少用在场景图片上,webp由于兼容性问题很少被使用,故能压缩jpgpng就足够了。当然压缩质感是最优考虑,综上所述,大部分同学都会选择TinyJpgTinyPng,其实它俩就是兄弟,出自同一厂商。

在笔者公众号的微信讨论群里发起了一个简单的投票,最终还是TinyJpgTinyPng胜出。

工具投票

TinyJpg/TinyPng存在问题
  • 上传下载全靠手动
  • 只能压缩jpgpng
  • 每次只能压缩20
  • 每张体积最大不能超过5M
  • 可视化处理信息不是特别齐全
TinyJpg/TinyPng压缩原理

TinyJpg/TinyPng使用智能有损压缩技术将图片体积降低,选择性地减少图片中相似颜色,只需很少字节就能保存数据。对视觉影响几乎不可见,但是在文件体积上就有很大的差别。而使用到智能有损压缩技术被称为量化

TinyJpg/TinyPng在压缩png文件时效果更显著。扫描图片中相似颜色并将其合并,通过减少颜色数量将24位png文件转换成体积更小的8位png文件,丢弃所有不必要的元数据。

大部分png文件都有50%~70%的压缩率,即使视力再好也很难区分出来。使用优化过的图片可减少带宽流量和加载时间,整个网站使用到的图片经TinyJpg/TinyPng压缩一遍,其成效是再多的代码优化也无法追赶得上的。

熊猫

TinyJpg/TinyPng开发API

查阅相关资料,发现TinyJpg/TinyPng暂时还未开源其压缩算法,不过提供了适合开发者使用的API。有兴趣的同学可到其开发API文档瞧瞧。

Node方面,TinyJpg/TinyPng官方提供了tinify作为压缩图片的核心JS库,使用很简单,看文档吧。可是换成开发API还是逃不过收费,你是想包月呢还是免费呢,想免费的话就继续往下看,土豪随意!

图片压缩工具

实现

笔者也是经常使用TinyJpg/TinyPng的程序猿,收费,那是不可能的😂。寻找突破口,解决问题,是作为一位程序猿最基本的素养。我们需明确什么问题,需解决什么问题

分析

从上述得知,只需对TinyJpg/TinyPng原有功能改造成以下功能。

  • 上传下载全自动
  • 可压缩jpgpng
  • 没有数量限制
  • 存在体积限制,最大体积不能超过5M
  • 压缩成功与否输出详细信息
自动处理

对于前端开发者来说,这种无脑的上传下载操作必须得自动化,省事省心省力。但是这个操作得结合webpack来处理,到底是开发成Loader还是Plugin,后面再分析。不过细心的同学看标题就知道用什么方式处理了。

压缩类型

gif压缩后一般都会失真,svg通常用在矢量图标上很少用在场景图片上,webp由于兼容性问题很少被使用,故能压缩jpgpng就足够了。在过滤图片时,使用path模块判断文件类型是否为jpgpng,是则继续处理,否则不处理。

数量限制

数量限制当然是不能存在的,万一项目里超过20张图片,那不是得分批处理,这个不能有。对于这种无需登录状态就能处理一些用户文件的网站,通常都会通过IP来限制用户的操作次数。有些同学可能会说,刷新页面不就行了吗,每次压缩20张图片,再刷新再压缩,万一有500张图片呢,你就刷新25次吗,这样很好玩是吧!

由于大多数Web架构很少会将应用服务器直接对外提供服务,一般都会设置一层Nginx作为代理和负载均衡,有的甚至可能有多层代理。鉴于大多数Web架构都是使用Nginx作为反向代理,用户请求不是直接请求应用服务器的,而是通过Nginx设置的统一接入层将用户请求转发到服务器的,所以可通过设置HTTP请求头字段X-Forwarded-For来伪造IP。

X-Forwarded-For指用来识别通过代理负载均衡的方式连接到Web服务器的客户端最原始的IP地址的HTTP请求头字段。当然,这个IP也不是一成不变的,每次请求都需随机更换IP,骗过应用服务器。若应用服务器增加了伪造IP识别,那可能就无法继续使用随机IP了。

体积限制

体积限制这个能理解,也没必要搞一张那么大的图片,多浪费带宽流量和加载时间啊。在上传图片时,使用fs模块判断文件体积是否超过5M,是则不上传,否则继续上传。当然,交给TinyJpg/TinyPng接口判断也行。

输出信息

压缩成功与否得让别人知道,输出原始大小、压缩大小、压缩率和错误提示等,让别人清楚这些处理信息。

编码

通过上述抽丝剥茧的分析,那么就开始着手编码了。

随机生成HTTP请求头

既然可通过X-Forwarded-For来伪造IP,那么得有一个随机生成HTTP请求头字段的函数,每次请求接口时都随机生成相关的请求头字段。打开tinyjpg.comtinypng.com上传一张图片,通过Chrome DevTools分析Network发现其请求接口是web/shrink。另外每次请求也不要集中在单一的hostname上,随机派发到tinyjpg.comtinypng.com上会更好。通过封装RandomHeader函数随机生成请求头信息,后续使用https模块RandomHeader()生成的配置作为入参进行请求。

trample是笔者开发的一个Web/Node通用函数工具库,包含常规的工具函数,助你少写更多通用代码。详情请查看文档,顺便给一个Star以作鼓励。

工具接口

const { RandomNum } = require("trample/node");

const TINYIMG_URL = [
    "tinyjpg.com",
    "tinypng.com"
];

function RandomHeader() {
    const ip = new Array(4).fill(0).map(() => parseInt(Math.random() * 255)).join(".");
    const index = RandomNum(0, 1);
    return {
        headers: {
            "Cache-Control": "no-cache",
            "Content-Type": "application/x-www-form-urlencoded",
            "Postman-Token": Date.now(),
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36",
            "X-Forwarded-For": ip
        },
        hostname: TINYIMG_URL[index],
        method: "POST",
        path: "/web/shrink",
        rejectUnauthorized: false
    };
}
上传图片与下载图片

使用Promise封装上传图片下载图片的函数,方便后续使用Async/Await同步化异步代码。以下函数的具体断点调试就不说了,有兴趣的同学自行调试函数的入参和出参哈!

const Https = require("https");
const Url = require("url");

function UploadImg(file) {
    const opts = RandomHeader();
    return new Promise((resolve, reject) => {
        const req = Https.request(opts, res => res.on("data", data => {
            const obj = JSON.parse(data.toString());
            obj.error ? reject(obj.message) : resolve(obj);
        }));
        req.write(file, "binary");
        req.on("error", e => reject(e));
        req.end();
    });
}

function DownloadImg(url) {
    const opts = new Url.URL(url);
    return new Promise((resolve, reject) => {
        const req = Https.request(opts, res => {
            let file = "";
            res.setEncoding("binary");
            res.on("data", chunk => file += chunk);
            res.on("end", () => resolve(file));
        });
        req.on("error", e => reject(e));
        req.end();
    });
}
压缩图片

通过上传图片函数获取压缩后的图片信息,再依据图片信息通过下载图片函数生成本地文件。

const Fs = require("fs");
const Path = require("path");
const Chalk = require("chalk");
const Figures = require("figures");
const { ByteSize, RoundNum } = require("trample/node");

async function CompressImg(path) {
    try {
        const file = Fs.readFileSync(path, "binary");
        const obj = await UploadImg(file);
        const data = await DownloadImg(obj.output.url);
        const oldSize = Chalk.redBright(ByteSize(obj.input.size));
        const newSize = Chalk.greenBright(ByteSize(obj.output.size));
        const ratio = Chalk.blueBright(RoundNum(1 - obj.output.ratio, 2, true));
        const dpath = Path.join("img", Path.basename(path));
        const msg = `${Figures.tick} Compressed [${Chalk.yellowBright(path)}] completed: Old Size ${oldSize}, New Size ${newSize}, Optimization Ratio ${ratio}`;
        Fs.writeFileSync(dpath, data, "binary");
        return Promise.resolve(msg);
    } catch (err) {
        const msg = `${Figures.cross} Compressed [${Chalk.yellowBright(path)}] failed: ${Chalk.redBright(err)}`;
        return Promise.resolve(msg);
    }
}
压缩目标图片

完成上述步骤对应的函数后,就能自由压缩图片了,以下使用一张图片作为演示。

const Ora = require("ora");

(async() => {
    const spinner = Ora("Image is compressing......").start();
    const res = await CompressImg("src/pig.png");
    spinner.stop();
    console.log(res);
})();

你看,压缩完后笨猪都变帅猪了,能电眼的猪都是好猪。源码请查看compress-img

电眼猪

若压缩指定文件夹里符合条件的所有图片,可通过fs模块获取图片并使用map()将单个图片路径映射为CompressImg(path),再通过Promise.all()操作即可。在这里就不贴代码了,当作思考题,自行完成。

将上述压缩图片的功能封装成Loader还是Plugin呢?接下来会一步一步分析。

Loader&Plugin

webpack是一个前端资源打包工具,它根据模块依赖关系进行静态分析,然后将这些模块按照指定规则生成对应的静态资源。

网上一大堆webpack教程,笔者就不再花大篇幅啰嗦了,相信各位同学都是一位标准的Webpack配置工程师。以下简单回顾一次webpack的组成、构建机制和构建流程,相信也能从这些知识点中定位出LoaderPluginWebpack构建流程中是处于一个什么样的角色地位。

本文所说的webpack都是基于webpack v4
组成
  • Entry:入口
  • Output:输出
  • Loader:转换器
  • Plugin:扩展器
  • Mode:模式
  • Module:模块
  • Target:目标
构建机制
  • 通过Babel转换代码并生成单个文件依赖
  • 从入口文件开始递归分析并生成依赖图谱
  • 将各个引用模块打包成一个立即执行函数
  • 将最终bundle文件写入bundle.js
构建流程
  • 初始

    • 初始参数:合并命令行和配置文件的参数
  • 编译

    • 执行编译:依据参数初始Compiler对象,加载所有Plugin,执行run()
    • 确定入口:依据配置文件找出所有入口文件
    • 编译模块:依据入口文件找出所有依赖模块关系,调用所有Loader进行转换
    • 生成图谱:得到每个模块转换后的内容及其之间的依赖关系
  • 输出

    • 输出资源:依据依赖关系将模块组装成块再组装成包(module → chunk → bundle)
    • 生成文件:依据配置文件将确认输出的内容写入文件系统
Loader

Loader用于转换模块源码,笔者将其翻译为转换器Loader可将所有类型文件转换为webpack能够处理的有效模块,然后利用webpack的打包能力对它们进行二次处理。

Loader具有以下特点:

  • 单一职责原则(只完成一种转换)
  • 转换接收内容
  • 返回转换结果
  • 支持链式调用

Loader将所有类型文件转换为应用程序的依赖图谱可直接引用的模块,所以Loader可用于编译一些文件,例如pug → htmlsass → cssless → csses5 → es6ts → js等。

处理一个文件可使用多个LoaderLoader的执行顺序和配置顺序是相反的,即末尾Loader最先执行,开头Loader最后执行。最先执行的Loader接收源文件内容作为参数,其它Loader接收前一个执行的Loader的返回值作为参数,最后执行的Loader会返回该文件的转换结果。一句话概括:富土康流水线厂工

Loader开发思路总结如下:

  • 通过module.exports导出一个函数
  • 函数第一默认参数为source(源文件内容)
  • 在函数体中处理资源(可引入第三方模块扩展功能)
  • 通过return返回最终转换结果(字符串形式)
编写Loader时要遵循单一职责原则,每个Loader只做一种转换工作
Plugin

Plugin用于扩展执行范围更广的任务,笔者将其翻译为扩展器Plugin的范围很广,在Webpack构建流程里从开始到结束都能找到时机作为插入点,只要你想不到没有你做不到。所以笔者认为Plugin的功能比Loader更加强大。

Plugin具有以下特点:

  • 监听webpack运行生命周期中广播的事件
  • 在合适时机通过webpack提供的API改变输出结果
  • webpack的Tapable事件流机制保证Plugin的有序性

webpack运行生命周期中会广播出许多事件,Plugin可监听这些事件并在合适时机通过webpack提供的API改变输出结果。在webpack启动后,在读取配置过程中执行new MyPlugin(opts)初始化自定义Plugin获取其实例,在初始化Compiler对象后,通过compiler.hooks.event.tap(PLUGIN_NAME, callback)监听webpack广播事件,当捕抓到指定事件后,会通过Compilation对象操作相关业务逻辑。一句话概括:自己看着办

Plugin开发思路总结如下:

  • 通过module.exports导出一个函数或类
  • 函数原型或类上绑定apply()访问Compiler对象
  • apply()中指定一个绑定到webpack自身的事件钩子
  • 在事件钩子中通过webpack提供的API处理资源(可引入第三方模块扩展功能)
  • 通过webpack提供的方法返回该资源
传给每个Plugin的Compiler和Compilation都是同一个引用,若修改它们身上的属性会影响后面的Plugin,所以需谨慎操作
Loader/Plugin区别
  • 本质

    • Loader本质是一个函数,转换接收内容,返回转换结果
    • Plugin本质是一个类,监听webpack运行生命周期中广播的事件,在合适时机通过webpack提供的API改变输出结果
  • 配置

    • Loadermodule.rule中配置,类型是数组,每一项对应一个模块解析规则
    • Pluginplugin中配置,类型是数组,每一项对应一个扩展器实例,参数通过构造函数传入

封装

分析

从上述可知LoaderPlugin在角色定位和执行机制上有很多不一样,到底如何选择呢?各有各好,当然还是需分析后进行选择。

Loaderwebpack中扮演着转换器的角色,用于转换模块源码,简单理解就是将文件转换成另外形式的文件,而本文主题是压缩图片jpg压缩后还是jpgpng压缩后还是png,在文件类型上来说还是没有变化。Loader的转换过程是附属在整个Webpack构建流程中的,意味着打包时间包含了压缩图片的时间成本,对于追求webpack性能优化来说实属有点违背原则。而Plugin恰好是监听webpack运行生命周期中广播的事件,在合适时机通过webpack提供的API改变输出结果,所以可在整个Webpack构建流程完成后(全部打包文件输出完成后)插入压缩图片的操作。换句话说,打包时间不再包含压缩图片的时间成本,打包完成后该干嘛就干嘛,还能干嘛,压缩图片啊。

所以依据需求情况,Plugin作为首选。

编码

依据上述Plugin开发思路,那么就开始着手编码了。

笔者把这个压缩图片的Plugin命名为tinyimg-webpack-plugintinyimg意味着TinyJpgTinyPng合体。

新建项目,目录结构如下。

tinyimg-webpack-plugin
├─ src
│  ├─ index.js
│  ├─ schema.json
├─ util
│  ├─ getting.js
│  ├─ setting.js
├─ .gitignore
├─ .npmignore
├─ license
├─ package.json
├─ readme.md

主要文件如下。

  • src

    • index.js:入口函数
    • schema.json:参数校验
  • util

    • getting.js:常量集合
    • setting.js:函数集合

安装项目所需模块,和上述compress-img的依赖一致,额外安装schema-utils用于校验Plugin参数是否符合规定。

npm i chalk figures ora schema-utils trample
封装常量集合和函数集合

将上述compress-imgTINYIMG_URLRandomHeader()封装到工具集合中,其中常量集合增加IMG_REGEXPPLUGIN_NAME两个常量。

// getting.js
const IMG_REGEXP = /\.(jpe?g|png)$/;

const PLUGIN_NAME = "tinyimg-webpack-plugin";

const TINYIMG_URL = [
    "tinyjpg.com",
    "tinypng.com"
];

module.exports = {
    IMG_REGEXP,
    PLUGIN_NAME,
    TINYIMG_URL
};
// setting.js
const { RandomNum } = require("trample/node");

const { TINYIMG_URL } = require("./getting");

function RandomHeader() {
    const ip = new Array(4).fill(0).map(() => parseInt(Math.random() * 255)).join(".");
    const index = RandomNum(0, 1);
    return {
        headers: {
            "Cache-Control": "no-cache",
            "Content-Type": "application/x-www-form-urlencoded",
            "Postman-Token": Date.now(),
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36",
            "X-Forwarded-For": ip
        },
        hostname: TINYIMG_URL[index],
        method: "POST",
        path: "/web/shrink",
        rejectUnauthorized: false
    };
}

module.exports = {
    RandomHeader
};
通过module.exports导出一个函数或类
// index.js
module.exports = class TinyimgWebpackPlugin {};
函数原型或类上绑定apply()访问Compiler对象
// index.js
module.exports = class TinyimgWebpackPlugin {
    apply(compiler) {
        // Do Something
    }
};
apply()中指定一个绑定到webpack自身的事件钩子

从上述分析中可知,在全部打包文件输出完成后插入压缩图片的操作,所以应该选择该时机对应的事件钩子。从Webpack Compiler Hooks API文档中可发现,emit正是这个Plugin所需的事件钩子。emit生成资源到输出目录前执行,此刻可获取所有图片文件的数据和输出路径。

为了方便在特定条件下启用功能打印日志,所以设置相关配置。

  • enabled:是否启用功能
  • logged:是否打印日志

apply()中处理相关业务逻辑,可能使用到Plugin的入参,那么就得对参数进行校验。定义一个PluginSchema,通过schema-utils来校验Plugin的入参。

// schema.json
{
    "type": "object",
    "properties": {
        "enabled": {
            "description": "start plugin",
            "type": "boolean"
        },
        "logged": {
            "description": "print log",
            "type": "boolean"
        }
    },
    "additionalProperties": false
}
// index.js
const SchemaUtils = require("schema-utils");

const { PLUGIN_NAME } = require("../util/getting");
const Schema = require("./schema");

module.exports = class TinyimgWebpackPlugin {
    constructor(opts) {
        this.opts = opts;
    }
    apply(compiler) {
        const { enabled } = this.opts;
        SchemaUtils(Schema, this.opts, { name: PLUGIN_NAME });
        enabled && compiler.hooks.emit.tap(PLUGIN_NAME, compilation => {
            // Do Something
        });
    }
};
整合compress-imgPlugin

在整合过程中会有一些小修改,各位同学可对比看看哪些细节发生了变化。

// index.js
const Fs = require("fs");
const Https = require("https");
const Url = require("url");
const Chalk = require("chalk");
const Figures = require("figures");
const { ByteSize, RoundNum } = require("trample/node");

const { RandomHeader } = require("../util/setting");

module.exports = class TinyimgWebpackPlugin {
    constructor(opts) { ... }
    apply(compiler) { ... }
    async compressImg(assets, path) {
        try {
            const file = assets[path].source();
            const obj = await this.uploadImg(file);
            const data = await this.downloadImg(obj.output.url);
            const oldSize = Chalk.redBright(ByteSize(obj.input.size));
            const newSize = Chalk.greenBright(ByteSize(obj.output.size));
            const ratio = Chalk.blueBright(RoundNum(1 - obj.output.ratio, 2, true));
            const dpath = assets[path].existsAt;
            const msg = `${Figures.tick} Compressed [${Chalk.yellowBright(path)}] completed: Old Size ${oldSize}, New Size ${newSize}, Optimization Ratio ${ratio}`;
            Fs.writeFileSync(dpath, data, "binary");
            return Promise.resolve(msg);
        } catch (err) {
            const msg = `${Figures.cross} Compressed [${Chalk.yellowBright(path)}] failed: ${Chalk.redBright(err)}`;
            return Promise.resolve(msg);
        }
    }
    downloadImg(url) {
        const opts = new Url.URL(url);
        return new Promise((resolve, reject) => {
            const req = Https.request(opts, res => {
                let file = "";
                res.setEncoding("binary");
                res.on("data", chunk => file += chunk);
                res.on("end", () => resolve(file));
            });
            req.on("error", e => reject(e));
            req.end();
        });
    }
    uploadImg(file) {
        const opts = RandomHeader();
        return new Promise((resolve, reject) => {
            const req = Https.request(opts, res => res.on("data", data => {
                const obj = JSON.parse(data.toString());
                obj.error ? reject(obj.message) : resolve(obj);
            }));
            req.write(file, "binary");
            req.on("error", e => reject(e));
            req.end();
        });
    }
};
在事件钩子中通过webpack提供的API处理资源

通过compilation.assets获取全部打包文件的对象,筛选出jpgpng,使用map()将单个图片数据映射为this.compressImg(file),再通过Promise.all()操作即可。

整个业务逻辑结合了PromiseAsync/Await两个ES6常用特性,它俩组合起来玩异步编程极其有趣,关于它俩更多细节可查看笔者这篇4000点赞量14万阅读量的文章《1.5万字概括ES6全部特性》

// index.js
const Ora = require("ora");
const SchemaUtils = require("schema-utils");

const { IMG_REGEXP, PLUGIN_NAME } = require("../util/getting");
const Schema = require("./schema");

module.exports = class TinyimgWebpackPlugin {
    constructor(opts) { ... }
    apply(compiler) {
        const { enabled, logged } = this.opts;
        SchemaUtils(Schema, this.opts, { name: PLUGIN_NAME });
        enabled && compiler.hooks.emit.tap(PLUGIN_NAME, compilation => {
            const imgs = Object.keys(compilation.assets).filter(v => IMG_REGEXP.test(v));
            if (!imgs.length) return Promise.resolve();
            const promises = imgs.map(v => this.compressImg(compilation.assets, v));
            const spinner = Ora("Image is compressing......").start();
            return Promise.all(promises).then(res => {
                spinner.stop();
                logged && res.forEach(v => console.log(v));
            });
        });
    }
    async compressImg(assets, path) { ... }
    downloadImg(url) { ... }
    uploadImg(file) { ... }
};
通过webpack提供的方法返回该资源

由于压缩图片的操作是在整个Webpack构建流程完成后,所以没有什么可返回了,故不作处理。

控制webpack依赖版本

由于tinyimg-webpack-plugin基于webpack v4,所以需在package.json中添加peerDependencies,用来告知安装该Plugin的模块必须存在peerDependencies里的依赖。

{
    "peerDependencies": {
        "webpack": ">= 4.0.0",
        "webpack-cli": ">= 3.0.0"
    }
}
总结

按照上述总结的开发思路一步一步来完成编码,其实是挺简单的。若需开发一些跟自己项目相关的Plugin,还是需多多熟悉Webpack Compiler Hooks API文档,相信各位同学都能手戳一个完美的Plugin出来。

tinyimg-webpack-plugin源码请戳这里查看,Star一个如何,嘻嘻。

电眼猪

测试

整个Plugin开发完成,接下来需走一遍测试流程,看能不能把这个压缩图片的扩展器跑通。相信各位同学都是一位标准的Webpack配置工程师,可自行编写测试Demo验证你们的Plugin

在根目录下创建test文件夹,并按照以下目录结构加入文件。

tinyimg-webpack-plugin
├─ test
│  ├─ src
│  │  ├─ img
│  │  │  ├─ favicon.ico
│  │  │  ├─ gz.jpg
│  │  │  ├─ pig-1.jpg
│  │  │  ├─ pig-2.jpg
│  │  │  ├─ pig-3.jpg
│  │  ├─ index.html
│  │  ├─ index.js
│  │  ├─ index.scss
│  │  ├─ reset.css
│  └─ webpack.config.js

安装测试Demo所需的webpack相关配置模块。

npm i -D @babel/core @babel/preset-env babel-loader clean-webpack-plugin css-loader file-loader html-webpack-plugin mini-css-extract-plugin node-sass sass sass-loader style-loader url-loader webpack webpack-cli webpackbar

安装完成后,着手完善webpack.config.js代码,代码量有点多,直接贴链接好了,请戳这里

最后在package.json中的scripts插入以下npm scripts,然后执行npm run test调试测试Demo。

{
    "scripts": {
        "test": "webpack --config test/webpack.config.js"
    }
}

发布

发布到NPM仓库上非常简单,仅需几行命令。若还没注册,赶紧去NPM上注册一个账号。若当前镜像为淘宝镜像,需执行npm config set registry https://registry.npmjs.org/切换回源镜像。

接下来一波操作就可完成发布了。

  • 进入目录:cd my-plugin
  • 登录账号:npm login
  • 校验状态:npm whoami
  • 发布模块:npm publish
  • 退出账号:npm logout

若不想牢记这么多命令,可用笔者开发的pkg-master一键发布,若存在某些错误会立马中断发布并提示错误信息,是一个非常好用的集成创建和发布的NPM模块管理工具。详情请查看文档,顺便给一个Star以作鼓励。

安装

npm i -g pkg-master

使用
命令缩写功能描述
pkg-master createpkg-master c创建模块生成模块的基础文件
pkg-master publishpkg-master p发布模块检测NPM的运行环境账号状态,通过则自动发布模块

发布模块

接入

安装

npm i tinyimg-webpack-plugin

使用
配置功能格式描述
enabled是否启用功能true/false建议只在生产环境下开启
logged是否打印日志true/false打印处理信息

webpack.config.jswebpack配置插入以下代码。

在CommonJS中使用
const TinyimgPlugin = require("tinyimg-webpack-plugin");

module.exports = {
    plugins: [
        new TinyimgPlugin({
            enabled: process.env.NODE_ENV === "production",
            logged: true
        })
    ]
};
在ESM中使用

必须在babel加持下的Node环境中使用

import TinyimgPlugin from "tinyimg-webpack-plugin";

export default {
    plugins: [
        new TinyimgPlugin({
            enabled: process.env.NODE_ENV === "production",
            logged: true
        })
    ]
};
推荐一个零配置开箱即用的React/Vue应用自动化构建脚手架

bruce-cli是一个React/Vue应用自动化构建脚手架,其零配置开箱即用的优点非常适合入门级、初中级、快速开发项目的前端同学使用,还可通过创建brucerc.js文件来覆盖其默认配置,只需专注业务代码的编写无需关注构建代码的编写,让项目结构更简洁。使用时记得查看文档哟,喜欢的话给个Star。

当然,笔者已将tinyimg-webpack-plugin集成到bruce-cli中,零配置开箱即用走起。

总结

总体来说开发一个Webpack Plugin不难,只需好好分析需求,了解webpack运行生命周期中广播的事件,编写自定义Plugin在合适时机通过webpack提供的API改变输出结果。

若觉得tinyimg-webpack-plugin对你有帮助,可在Issue提出你的宝贵建议,笔者会认真阅读并整合你的建议。喜欢tinyimg-webpack-plugin的请给一个Star,或Fork本项目到自己的Github上,根据自身需求定制功能。

查看原文

大家好 收藏了文章 · 2020-04-28

值得收藏!面试中有哪些经典的数据库问题?


作者:程序员之言  |  今日头条号

一、为什么用自增列作为主键

1、如果我们定义了主键(PRIMARY KEY),那么InnoDB会选择主键作为聚集索引、如果没有显式定义主键,则InnoDB会选择第一个不包含有NULL值的唯一索引作为主键索引、如果也没有这样的唯一索引,则InnoDB会选择内置6字节长的ROWID作为隐含的聚集索引(ROWID随着行记录的写入而主键递增,这个ROWID不像ORACLE的ROWID那样可引用,是隐含的)。

2、数据记录本身被存于主索引(一颗B+Tree)的叶子节点上。这就要求同一个叶子节点内(大小为一个内存页或磁盘页)的各条数据记录按主键顺序存放,因此每当有一条新的记录插入时,MySQL会根据其主键将其插入适当的节点和位置,如果页面达到装载因子(InnoDB默认为15/16),则开辟一个新的页(节点)

3、如果表使用自增主键,那么每次插入新的记录,记录就会顺序添加到当前索引节点的后续位置,当一页写满,就会自动开辟一个新的页

4、如果使用非自增主键(如果身份证号或学号等),由于每次插入主键的值近似于随机,因此每次新纪录都要被插到现有索引页得中间某个位置,此时MySQL不得不为了将新记录插到合适位置而移动数据,甚至目标页面可能已经被回写到磁盘上而从缓存中清掉,此时又要从磁盘上读回来,这增加了很多开销,同时频繁的移动、分页操作造成了大量的碎片,得到了不够紧凑的索引结构,后续不得不通过OPTIMIZE TABLE来重建表并优化填充页面。

二、为什么使用数据索引能提高效率

1、数据索引的存储是有序的

2、在有序的情况下,通过索引查询一个数据是无需遍历索引记录的

3、极端情况下,数据索引的查询效率为二分法查询效率,趋近于 log2(N)

三、B+树索引和哈希索引的区别

B+树是一个平衡的多叉树,从根节点到每个叶子节点的高度差值不超过1,而且同层级的节点间有指针相互链接,是有序的

哈希索引就是采用一定的哈希算法,把键值换算成新的哈希值,检索时不需要类似B+树那样从根节点到叶子节点逐级查找,只需一次哈希算法即可,是无序的

四、哈希索引的优势:

1、等值查询。哈希索引具有绝对优势(前提是:没有大量重复键值,如果大量重复键值时,哈希索引的效率很低,因为存在所谓的哈希碰撞问题。)

五、哈希索引不适用的场景:

1、不支持范围查询

2、不支持索引完成排序

3、不支持联合索引的最左前缀匹配规则

通常,B+树索引结构适用于绝大多数场景,像下面这种场景用哈希索引才更有优势:

在HEAP表中,如果存储的数据重复度很低(也就是说基数很大),对该列数据以等值查询为主,没有范围查询、没有排序的时候,特别适合采用哈希索引,例如这种SQL:

select id,name from table where name='李明'; — 仅等值查询

而常用的InnoDB引擎中默认使用的是B+树索引,它会实时监控表上索引的使用情况,如果认为建立哈希索引可以提高查询效率,则自动在内存中的“自适应哈希索引缓冲区”建立哈希索引(在InnoDB中默认开启自适应哈希索引),通过观察搜索模式,MySQL会利用index key的前缀建立哈希索引,如果一个表几乎大部分都在缓冲池中,那么建立一个哈希索引能够加快等值查询。

注意:在某些工作负载下,通过哈希索引查找带来的性能提升远大于额外的监控索引搜索情况和保持这个哈希表结构所带来的开销。但某些时候,在负载高的情况下,自适应哈希索引中添加的read/write锁也会带来竞争,比如高并发的join操作。like操作和%的通配符操作也不适用于自适应哈希索引,可能要关闭自适应哈希索引。

六、B树和B+树的区别

1、B树,每个节点都存储key和data,所有节点组成这棵树,并且叶子节点指针为nul,叶子结点不包含任何关键字信息。

2、B+树,所有的叶子结点中包含了全部关键字的信息,及指向含有这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大的顺序链接,所有的非终端结点可以看成是索引部分,结点中仅含有其子树根结点中最大(或最小)关键字。 (而B 树的非终节点也包含需要查找的有效信息)

七、为什么说B+比B树更适合实际应用中操作系统的文件索引和数据库索引?

1、B+的磁盘读写代价更低B+的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B树更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说IO读写次数也就降低了。

2、B+-tree的查询效率更加稳定由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。

八、MySQL联合索引

1、联合索引是两个或更多个列上的索引。对于联合索引:Mysql从左到右的使用索引中的字段,一个查询可以只使用索引中的一部份,但只能是最左侧部分。例如索引是key index (a,b,c). 可以支持a 、 a,b 、 a,b,c 3种组合进行查找,但不支持 b,c进行查找 .当最左侧字段是常量引用时,索引就十分有效。

2、利用索引中的附加列,您可以缩小搜索的范围,但使用一个具有两列的索引 不同于使用两个单独的索引。复合索引的结构与电话簿类似,人名由姓和名构成,电话簿首先按姓氏对进行排序,然后按名字对有相同姓氏的人进行排序。如果您知 道姓,电话簿将非常有用;如果您知道姓和名,电话簿则更为有用,但如果您只知道名不姓,电话簿将没有用处。

九、什么情况下应不建或少建索引

1、表记录太少

2、经常插入、删除、修改的表

3、数据重复且分布平均的表字段,假如一个表有10万行记录,有一个字段A只有T和F两种值,且每个值的分布概率大约为50%,那么对这种表A字段建索引一般不会提高数据库的查询速度。

4、经常和主字段一块查询但主字段索引值比较多的表字段

十、什么是表分区?

表分区,是指根据一定规则,将数据库中的一张表分解成多个更小的,容易管理的部分。从逻辑上看,只有一张表,但是底层却是由多个物理分区组成。

十一、表分区与分表的区别

分表:指的是通过一定规则,将一张表分解成多张不同的表。比如将用户订单记录根据时间成多个表。

分表与分区的区别在于:分区从逻辑上来讲只有一张表,而分表则是将一张表分解成多张表。

十二、表分区有什么好处?

1、分区表的数据可以分布在不同的物理设备上,从而高效地利用多个硬件设备。 2. 和单个磁盘或者文件系统相比,可以存储更多数据

2、优化查询。在where语句中包含分区条件时,可以只扫描一个或多个分区表来提高查询效率;涉及sum和count语句时,也可以在多个分区上并行处理,最后汇总结果。

3、分区表更容易维护。例如:想批量删除大量数据可以清除整个分区。

4、可以使用分区表来避免某些特殊的瓶颈,例如InnoDB的单个索引的互斥访问,ext3问价你系统的inode锁竞争等。

十三、分区表的限制因素

1、一个表最多只能有1024个分区

2、MySQL5.1中,分区表达式必须是整数,或者返回整数的表达式。在MySQL5.5中提供了非整数表达式分区的支持。

3、如果分区字段中有主键或者唯一索引的列,那么多有主键列和唯一索引列都必须包含进来。即:分区字段要么不包含主键或者索引列,要么包含全部主键和索引列。

4、分区表中无法使用外键约束

5、MySQL的分区适用于一个表的所有数据和索引,不能只对表数据分区而不对索引分区,也不能只对索引分区而不对表分区,也不能只对表的一部分数据分区。

十四、如何判断当前MySQL是否支持分区?

命令:show variables like '%partition%' 运行结果:

mysql> show variables like '%partition%';
+-------------------+-------+
| Variable_name     | Value |
+-------------------+-------+
| have_partitioning | YES   |
+-------------------+-------+
1 row in set (0.00 sec)

have_partintioning 的值为YES,表示支持分区。

十五、MySQL支持的分区类型有哪些?

1、RANGE分区: 这种模式允许将数据划分不同范围。例如可以将一个表通过年份划分成若干个分区

2、LIST分区: 这种模式允许系统通过预定义的列表的值来对数据进行分割。按照List中的值分区,与RANGE的区别是,range分区的区间范围值是连续的。

3、HASH分区 :这中模式允许通过对表的一个或多个列的Hash Key进行计算,最后通过这个Hash码不同数值对应的数据区域进行分区。例如可以建立一个对表主键进行分区的表。

4、KEY分区 :上面Hash模式的一种延伸,这里的Hash Key是MySQL系统产生的。

十六、四种隔离级别

1、Serializable (串行化):可避免脏读、不可重复读、幻读的发生。

2、Repeatable read (可重复读):可避免脏读、不可重复读的发生。

3、Read committed (读已提交):可避免脏读的发生。

4、Read uncommitted (读未提交):最低级别,任何情况都无法保证。

十七、关于MVVC

MySQL InnoDB存储引擎,实现的是基于多版本的并发控制协议——MVCC (Multi-Version Concurrency Control) (注:与MVCC相对的,是基于锁的并发控制,Lock-Based Concurrency Control)。MVCC最大的好处:读不加锁,读写不冲突。在读多写少的OLTP应用中,读写不冲突是非常重要的,极大的增加了系统的并发性能,现阶段几乎所有的RDBMS,都支持了MVCC。

1、LBCC:Lock-Based Concurrency Control,基于锁的并发控制。

2、MVCC:Multi-Version Concurrency Control,基于多版本的并发控制协议。纯粹基于锁的并发机制并发量低,MVCC是在基于锁的并发控制上的改进,主要是在读操作上提高了并发量。

十八、在MVCC并发控制中,读操作可以分成两类:

1、快照读 (snapshot read):读取的是记录的可见版本 (有可能是历史版本),不用加锁(共享读锁s锁也不加,所以不会阻塞其他事务的写)。

2、当前读 (current read):读取的是记录的最新版本,并且,当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录。

十九、行级锁定的优点:

1、当在许多线程中访问不同的行时只存在少量锁定冲突。

2、回滚时只有少量的更改

3、可以长时间锁定单一的行。

二十、行级锁定的缺点:

1、比页级或表级锁定占用更多的内存。

2、当在表的大部分中使用时,比页级或表级锁定速度慢,因为你必须获取更多的锁。

3、如果你在大部分数据上经常进行GROUP BY操作或者必须经常扫描整个表,比其它锁定明显慢很多。

4、用高级别锁定,通过支持不同的类型锁定,你也可以很容易地调节应用程序,因为其锁成本小于行级锁定。

二十一、MySQL优化

1、开启查询缓存,优化查询

2、explain你的select查询,这可以帮你分析你的查询语句或是表结构的性能瓶颈。EXPLAIN 的查询结果还会告诉你你的索引主键被如何利用的,你的数据表是如何被搜索和排序的

3、当只要一行数据时使用limit 1,MySQL数据库引擎会在找到一条数据后停止搜索,而不是继续往后查少下一条符合记录的数据

4、为搜索字段建索引

5、使用 ENUM 而不是 VARCHAR,如果你有一个字段,比如“性别”,“国家”,“民族”,“状态”或“部门”,你知道这些字段的取值是有限而且固定的,那么,你应该使用 ENUM 而不是VARCHAR。

6、Prepared StatementsPrepared Statements很像存储过程,是一种运行在后台的SQL语句集合,我们可以从使用 prepared statements 获得很多好处,无论是性能问题还是安全问题。Prepared Statements 可以检查一些你绑定好的变量,这样可以保护你的程序不会受到“SQL注入式”攻击

7、垂直分表

8、选择正确的存储引擎

二十二、key和index的区别

1、key 是数据库的物理结构,它包含两层意义和作用,一是约束(偏重于约束和规范数据库的结构完整性),二是索引(辅助查询用的)。包括primary key, unique key, foreign key 等

2、index是数据库的物理结构,它只是辅助查询的,它创建时会在另外的表空间(mysql中的innodb表空间)以一个类似目录的结构存储。索引要分类的话,分为前缀索引、全文本索引等;

二十三、Mysql 中 MyISAM 和 InnoDB 的区别有哪些?

区别:

1、InnoDB支持事务,MyISAM不支持,对于InnoDB每一条SQL语言都默认封装成事务,自动提交,这样会影响速度,所以最好把多条SQL语言放在begin和commit之间,组成一个事务;

2、InnoDB支持外键,而MyISAM不支持。对一个包含外键的InnoDB表转为MYISAM会失败;

3、InnoDB是聚集索引,数据文件是和索引绑在一起的,必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大。而MyISAM是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的。

4、InnoDB不保存表的具体行数,执行select count(*) from table时需要全表扫描。而MyISAM用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快;

5、Innodb不支持全文索引,而MyISAM支持全文索引,查询效率上MyISAM要高;

如何选择:

1、是否要支持事务,如果要请选择innodb,如果不需要可以考虑MyISAM;

2、如果表中绝大多数都只是读查询,可以考虑MyISAM,如果既有读写也挺频繁,请使用InnoDB。

3、系统奔溃后,MyISAM恢复起来更困难,能否接受;

4、MySQL5.5版本开始Innodb已经成为Mysql的默认引擎(之前是MyISAM),说明其优势是有目共睹的,如果你不知道用什么,那就用InnoDB,至少不会差。

二十四、数据库表创建注意事项

1、字段名及字段配制合理性

  • 剔除关系不密切的字段;
  • 字段命名要有规则及相对应的含义(不要一部分英文,一部分拼音,还有类似a.b.c这样不明含义的字段);
  • 字段命名尽量不要使用缩写(大多数缩写都不能明确字段含义);
  • 字段不要大小写混用(想要具有可读性,多个英文单词可使用下划线形式连接);
  • 字段名不要使用保留字或者关键字;
  • 保持字段名和类型的一致性;
  • 慎重选择数字类型;
  • 给文本字段留足余量;

2、系统特殊字段处理及建成后建议

  • 添加删除标记(例如操作人、删除时间);
  • 建立版本机制;

3、表结构合理性配置

  • 多型字段的处理,就是表中是否存在字段能够分解成更小独立的几部分(例如:人可以分为男人和女人);
  • 多值字段的处理,可以将表分为三张表,这样使得检索和排序更加有调理,且保证数据的完整性!

4、其它建议

  • 对于大数据字段,独立表进行存储,以便影响性能(例如:简介字段);
  • 使用varchar类型代替char,因为varchar会动态分配长度,char指定长度是固定的;
  • 给表创建主键,对于没有主键的表,在查询和索引定义上有一定的影响;
  • 避免表字段运行为null,建议设置默认值(例如:int类型设置默认值为0)在索引查询上,效率立显;
  • 建立索引,最好建立在唯一和非空的字段上,建立太多的索引对后期插入、更新都存在一定的影响(考虑实际情况来创建);
查看原文

大家好 关注了用户 · 2020-04-28

民工哥 @jishuroad

民工哥,10多年职场老司机的经验分享,坚持自学一路从技术小白成长为互联网企业信息技术部门的负责人。

我的新书:《Linux系统运维指南》

微信公众号:民工哥技术之路

民工哥:知乎专栏

欢迎关注,我们一同交流,相互学习,共同成长!!

关注 3020

大家好 关注了用户 · 2020-04-28

CrazyCodes @crazycodes

https://github.com/CrazyCodes... 我的博客
_
| |__ __ _
| '_ | | | |/ _` |
| |_) | |_| | (_| |
|_.__/ \__,_|\__, |

         |___/   感谢生命可以让我成为一名程序员

                         CrazyCodes To Author

关注 4684

大家好 关注了专栏 · 2020-04-28

Grace development

记录分享开发、学习中的点点滴滴

关注 4683

大家好 赞了文章 · 2020-04-28

给自己点时间再记记这200条Git命令

photo-1519076651956-95f25edf6780.jpeg
我平时使用 Git 的时候,很多的 Git 命令我都不是很常用,工作中一般我们会配合一些可视化工具,或者编辑器自带的一些插件去维护 Git 仓库,但是我们也要记得一些常用 Git 命令来应变一些特殊的场景,下面是我收录整理的常用和不常用的一些 Git 命令,希望能帮助到大家更好的掌握 Git 的使用,如果文章和笔记能带您一丝帮助或者启发,请不要吝啬你的赞和收藏,你的肯定是我前进的最大动力😁

新建

创建一个新的 git 版本库。这个版本库的配置、存储等信息会被保存到.git 文件夹中

# 初始化当前项目
$ git init

# 新建一个目录,将其初始化为Git代码库
$ git init [project-name]

# 在指定目录创建一个空的 Git 仓库。运行这个命令会创建一个名为 directory,只包含 .git 子目录的空目录。

$ git init --bare <directory>

# 下载一个项目和它的整个代码历史
# 这个命令就是将一个版本库拷贝到另一个目录中,同时也将分支都拷贝到新的版本库中。这样就可以在新的版本库中提交到远程分支
$ git clone [url]

配置

更改设置。可以是版本库的设置,也可以是系统的或全局的

# 显示当前的Git配置
$ git config --list

# 编辑Git配置文件
$ git config -e [--global]

# 输出、设置基本的全局变量
$ git config --global user.email
$ git config --global user.name

$ git config --global user.email "MyEmail@gmail.com"
$ git config --global user.name "My Name"

# 定义当前用户所有提交使用的作者邮箱。
$ git config --global alias.<alias-name> <git-command>

# 为Git命令创建一个快捷方式(别名)。
$ git config --system core.editor <editor>

帮助

git 内置了对命令非常详细的解释,可以供我们快速查阅

# 查找可用命令
$ git help

# 查找所有可用命令
$ git help -a

# 在文档当中查找特定的命令
# git help <命令>
$ git help add
$ git help commit
$ git help init

状态

显示索引文件(也就是当前工作空间)和当前的头指针指向的提交的不同

# 显示分支,未跟踪文件,更改和其他不同
$ git status

# 查看其他的git status的用法
$ git help status

信息

获取某些文件,某些分支,某次提交等 git 信息

# 显示commit历史,以及每次commit发生变更的文件
$ git log --stat

# 搜索提交历史,根据关键词
$ git log -S [keyword]

# 显示某个commit之后的所有变动,每个commit占据一行
$ git log [tag] HEAD --pretty=format:%s

# 显示某个commit之后的所有变动,其"提交说明"必须符合搜索条件
$ git log [tag] HEAD --grep feature

# 显示某个文件的版本历史,包括文件改名
$ git log --follow [file]
$ git whatchanged [file]

# 显示指定文件相关的每一次diff
$ git log -p [file]

# 显示过去5次提交
$ git log -5 --pretty --oneline

# 显示所有提交过的用户,按提交次数排序
$ git shortlog -sn

# 显示指定文件是什么人在什么时间修改过
$ git blame [file]

# 显示暂存区和工作区的差异
$ git diff

# 显示暂存区和上一个commit的差异
$ git diff --cached [file]

# 显示工作区与当前分支最新commit之间的差异
$ git diff HEAD

# 显示两次提交之间的差异
$ git diff [first-branch]...[second-branch]

# 显示今天你写了多少行代码
$ git diff --shortstat "@{0 day ago}"

# 比较暂存区和版本库差异
$ git diff --staged

# 比较暂存区和版本库差异
$ git diff --cached

# 仅仅比较统计信息
$ git diff --stat

# 显示某次提交的元数据和内容变化
$ git show [commit]

# 显示某次提交发生变化的文件
$ git show --name-only [commit]

# 显示某次提交时,某个文件的内容
$ git show [commit]:[filename]

# 显示当前分支的最近几次提交
$ git reflog

# 查看远程分支
$ git br -r

# 创建新的分支
$ git br <new_branch>

# 查看各个分支最后提交信息
$ git br -v

# 查看已经被合并到当前分支的分支
$ git br --merged

# 查看尚未被合并到当前分支的分支
$ git br --no-merged

添加

添加文件到当前工作空间中。如果你不使用 git add 将文件添加进去,那么这些文件也不会添加到之后的提交之中

# 添加一个文件
$ git add test.js

# 添加一个子目录中的文件
$ git add /path/to/file/test.js

# 支持正则表达式
$ git add ./*.js

# 添加指定文件到暂存区
$ git add [file1] [file2] ...

# 添加指定目录到暂存区,包括子目录
$ git add [dir]

# 添加当前目录的所有文件到暂存区
$ git add .

# 添加每个变化前,都会要求确认
# 对于同一个文件的多处变化,可以实现分次提交
$ git add -p

删除

rm 和上面的 add 命令相反,从工作空间中去掉某个文件

# 移除 HelloWorld.js
$ git rm HelloWorld.js

# 移除子目录中的文件
$ git rm /pather/to/the/file/HelloWorld.js

# 删除工作区文件,并且将这次删除放入暂存区
$ git rm [file1] [file2] ...

# 停止追踪指定文件,但该文件会保留在工作区
$ git rm --cached [file]

分支

管理分支,可以通过下列命令对分支进行增删改查切换等

# 查看所有的分支和远程分支
$ git branch -a

# 创建一个新的分支
$ git branch [branch-name]

# 重命名分支
# git branch -m <旧名称> <新名称>
$ git branch -m [branch-name] [new-branch-name]

# 编辑分支的介绍
$ git branch [branch-name] --edit-description

# 列出所有本地分支
$ git branch

# 列出所有远程分支
$ git branch -r

# 新建一个分支,但依然停留在当前分支
$ git branch [branch-name]

# 新建一个分支,并切换到该分支
$ git checkout -b [branch]

# 新建一个分支,指向指定commit
$ git branch [branch] [commit]

# 新建一个分支,与指定的远程分支建立追踪关系
$ git branch --track [branch] [remote-branch]

# 切换到指定分支,并更新工作区
$ git checkout [branch-name]

# 切换到上一个分支
$ git checkout -

# 建立追踪关系,在现有分支与指定的远程分支之间
$ git branch --set-upstream [branch] [remote-branch]

# 合并指定分支到当前分支
$ git merge [branch]

# 选择一个commit,合并进当前分支
$ git cherry-pick [commit]

# 删除分支
$ git branch -d [branch-name]

# 删除远程分支
$ git push origin --delete [branch-name]
$ git branch -dr [remote/branch]

# 切换到某个分支
$ git co <branch>

# 创建新的分支,并且切换过去
$ git co -b <new_branch>

# 基于branch创建新的new_branch
$ git co -b <new_branch> <branch>

# 把某次历史提交记录checkout出来,但无分支信息,切换到其他分支会自动删除
$ git co $id

# 把某次历史提交记录checkout出来,创建成一个分支
$ git co $id -b <new_branch>

# 删除某个分支
$ git br -d <branch>

# 强制删除某个分支 (未被合并的分支被删除的时候需要强制)
$ git br -D <branch>

检出

将当前工作空间更新到索引所标识的或者某一特定的工作空间

# 检出一个版本库,默认将更新到master分支
$ git checkout
# 检出到一个特定的分支
$ git checkout branchName
# 新建一个分支,并且切换过去,相当于"git branch <名字>; git checkout <名字>"
$ git checkout -b newBranch

远程同步

远程同步的远端分支

# 下载远程仓库的所有变动
$ git fetch [remote]

# 显示所有远程仓库
$ git remote -v

# 显示某个远程仓库的信息
$ git remote show [remote]

# 增加一个新的远程仓库,并命名
$ git remote add [shortname] [url]

# 查看远程服务器地址和仓库名称
$ git remote -v

# 添加远程仓库地址
$ git remote add origin git@ github:xxx/xxx.git

# 设置远程仓库地址(用于修改远程仓库地址)
$ git remote set-url origin git@ github.com:xxx/xxx.git

# 删除远程仓库
$ git remote rm <repository>

# 上传本地指定分支到远程仓库
# 把本地的分支更新到远端origin的master分支上
# git push <远端> <分支>
# git push 相当于 git push origin master
$ git push [remote] [branch]

# 强行推送当前分支到远程仓库,即使有冲突
$ git push [remote] --force

# 推送所有分支到远程仓库
$ git push [remote] --all

撤销

# 恢复暂存区的指定文件到工作区
$ git checkout [file]

# 恢复某个commit的指定文件到暂存区和工作区
$ git checkout [commit] [file]

# 恢复暂存区的所有文件到工作区
$ git checkout .

# 重置暂存区的指定文件,与上一次commit保持一致,但工作区不变
$ git reset [file]

# 重置暂存区与工作区,与上一次commit保持一致
$ git reset --hard

# 重置当前分支的指针为指定commit,同时重置暂存区,但工作区不变
$ git reset [commit]

# 重置当前分支的HEAD为指定commit,同时重置暂存区和工作区,与指定commit一致
$ git reset --hard [commit]

# 重置当前HEAD为指定commit,但保持暂存区和工作区不变
$ git reset --keep [commit]

# 新建一个commit,用来撤销指定commit
# 后者的所有变化都将被前者抵消,并且应用到当前分支
$ git revert [commit]

# 恢复最后一次提交的状态
$ git revert HEAD

# 暂时将未提交的变化移除,稍后再移入
$ git stash
$ git stash pop

# 列所有stash
$ git stash list

# 恢复暂存的内容
$ git stash apply

# 删除暂存区
$ git stash drop

commit

将当前索引的更改保存为一个新的提交,这个提交包括用户做出的更改与信息

# 提交暂存区到仓库区附带提交信息
$ git commit -m [message]

# 提交暂存区的指定文件到仓库区
$ git commit [file1] [file2] ... -m [message]

# 提交工作区自上次commit之后的变化,直接到仓库区
$ git commit -a

# 提交时显示所有diff信息
$ git commit -v

# 使用一次新的commit,替代上一次提交
# 如果代码没有任何新变化,则用来改写上一次commit的提交信息
$ git commit --amend -m [message]

# 重做上一次commit,并包括指定文件的新变化
$ git commit --amend [file1] [file2] ...

diff

显示当前工作空间和提交的不同

# 显示工作目录和索引的不同
$ git diff

# 显示索引和最近一次提交的不同
$ git diff --cached

# 显示工作目录和最近一次提交的不同
$ git diff HEAD

grep

可以在版本库中快速查找

可选配置:

# 感谢Travis Jeffery提供的以下用法:
# 在搜索结果中显示行号
$ git config --global grep.lineNumber true

# 是搜索结果可读性更好
$ git config --global alias.g "grep --break --heading --line-number"
# 在所有的java中查找variableName
$ git grep 'variableName' -- '*.java'

# 搜索包含 "arrayListName" 和, "add" 或 "remove" 的所有行
$ git grep -e 'arrayListName' --and \( -e add -e remove \)

log

显示这个版本库的所有提交

# 显示所有提交
$ git log

# 显示某几条提交信息
$ git log -n 10

# 仅显示合并提交
$ git log --merges

# 查看该文件每次提交记录
$ git log <file>

# 查看每次详细修改内容的diff
$ git log -p <file>

# 查看最近两次详细修改内容的diff
$ git log -p -2

#查看提交统计信息
$ git log --stat

merge

合并就是将外部的提交合并到自己的分支中

# 将其他分支合并到当前分支
$ git merge branchName

# 在合并时创建一个新的合并后的提交
# 不要 Fast-Foward 合并,这样可以生成 merge 提交
$ git merge --no-ff branchName

mv

重命名或移动一个文件

# 重命名
$ git mv test.js test2.js

# 移动
$ git mv test.js ./new/path/test.js

# 改名文件,并且将这个改名放入暂存区
$ git mv [file-original] [file-renamed]

# 强制重命名或移动
# 这个文件已经存在,将要覆盖掉
$ git mv -f myFile existingFile

tag

# 列出所有tag
$ git tag

# 新建一个tag在当前commit
$ git tag [tag]

# 新建一个tag在指定commit
$ git tag [tag] [commit]

# 删除本地tag
$ git tag -d [tag]

# 删除远程tag
$ git push origin :refs/tags/[tagName]

# 查看tag信息
$ git show [tag]

# 提交指定tag
$ git push [remote] [tag]

# 提交所有tag
$ git push [remote] --tags

# 新建一个分支,指向某个tag
$ git checkout -b [branch] [tag]

pull

从远端版本库合并到当前分支

# 从远端origin的master分支更新版本库
# git pull <远端> <分支>
$ git pull origin master

# 抓取远程仓库所有分支更新并合并到本地,不要快进合并
$ git pull --no-ff

ci

$ git ci <file>
$ git ci .
# 将git add, git rm和git ci等操作都合并在一起做
$ git ci -a
$ git ci -am "some comments"
# 修改最后一次提交记录
$ git ci --amend

rebase (谨慎使用)

将一个分支上所有的提交历史都应用到另一个分支上
_不要在一个已经公开的远端分支上使用 rebase_.

# 将experimentBranch应用到master上面
# git rebase <basebranch> <topicbranch>
$ git rebase master experimentBranch

reset (谨慎使用)

将当前的头指针复位到一个特定的状态。这样可以使你撤销 merge、pull、commits、add 等
这是个很强大的命令,但是在使用时一定要清楚其所产生的后果

# 使 staging 区域恢复到上次提交时的状态,不改变现在的工作目录
$ git reset

# 使 staging 区域恢复到上次提交时的状态,覆盖现在的工作目录
$ git reset --hard

# 将当前分支恢复到某次提交,不改变现在的工作目录
# 在工作目录中所有的改变仍然存在
$ git reset dha78as

# 将当前分支恢复到某次提交,覆盖现在的工作目录
# 并且删除所有未提交的改变和指定提交之后的所有提交
$ git reset --hard dha78as

其他

# 生成一个可供发布的压缩包
$ git archive

# 打补丁
$ git apply ../sync.patch

# 测试补丁能否成功
$ git apply --check ../sync.patch

# 查看Git的版本
$ git --version

参考文档

查看原文

赞 110 收藏 87 评论 9

大家好 关注了用户 · 2020-04-24

无欲则刚 @wuyuzegang_5e5d113565a60

微信公众号HR工作室,欢迎关注

关注 5

大家好 收藏了文章 · 2020-04-23

视频面试超高频在线编程题,搞懂这些足以应对大部分公司

引言

现在大厂面试几乎都会问到算法,回答不上来会让你在面试官前大打折扣。前端怎么进阶算法喃?

本周是瓶子君前端进阶算法的第三周🎉🎉🎉,这里,会带你 从 0 到 1 构建完整的前端数据结构与算法体系。

本周已经不单是简单的链表操作(一般链表的问题可以考虑使用快慢指针),开始涉及五大常用算法策略、二叉树、Trie树、队列等,这里仅作为入门,后面会详细介绍,发散思维,你会发现面试中的算法、开发中的算法真的很 easy。

往期精彩系列

以及题目:

本节是第三周的总结与回顾,下面开始进入正题吧!👇👇👇

一、图解字节&leetcode14:最长公共前缀

1. 题目

编写一个函数来查找字符串数组中的最长公共前缀。

如果不存在公共前缀,返回空字符串 ""

示例 1:

输入: ["flower","flow","flight"]
输出: "fl"

示例 2:

输入: ["dog","racecar","car"]
输出: ""
解释: 输入不存在公共前缀。

2. 答案

解法一:逐个比较

解题思路: 从前往后一次比较字符串,获取公共前缀

画图帮助理解一下:



代码实现:

var longestCommonPrefix = function(strs) {
    if (strs === null || strs.length === 0) return "";
    let prevs = strs[0]
    for(let i = 1; i < strs.length; i++) {
        let j = 0
        for(; j < prevs.length && j < strs[i].length; j++) {
            if(prevs.charAt(j) !== strs[i].charAt(j)) break
        }
        prevs = prevs.substring(0, j)
        if(prevs === "") return ""
    }
    return prevs
};

时间复杂度:O(s),s 是所有字符串中字符数量的总和

空间复杂度:O(1)

解法二:仅需最大、最小字符串的最长公共前缀

解题思路: 获取数组中的最大值及最小值字符串,最小字符串与最大字符串的最长公共前缀也为其他字符串的公共前缀,即为字符串数组的最长公共前缀

例如 abcabcdabac ,最小 ab 与最大 ac 的最长公共前缀一定也是 abcabcd 的公共前缀

画图帮助理解一下:

代码实现:

var longestCommonPrefix = function(strs) {
    if (strs === null || strs.length === 0) return "";
    if(strs.length === 1) return strs[0]
    let min = 0, max = 0
    for(let i = 1; i < strs.length; i++) {
        if(strs[min] > strs[i]) min = i
        if(strs[max] < strs[i]) max = i
    }
    for(let j = 0; j < strs[min].length; j++) {
        if(strs[min].charAt(j) !== strs[max].charAt(j)) {
            return strs[min].substring(0, j)
        }
    }
    return strs[min]
};

时间复杂度:O(n+m),n是数组的长度, m 是字符串数组中最短字符的长度

空间复杂度:O(1)

解法三:分治策略 归并思想

分治,顾名思义,就是分而治之,将一个复杂的问题,分成两个或多个相似的子问题,在把子问题分成更小的子问题,直到更小的子问题可以简单求解,求解子问题,则原问题的解则为子问题解的合并。

这道题就是一个典型的分治策略问题:

  • 问题:求多个字符串的最长公共前缀
  • 分解成多个相似的子问题:求两个字符串的最长公共前缀
  • 子问题可以简单求解:两个字符串的最长公共前缀求解很简单
  • 原问题的解为子问题解的合并:多个字符串的最长公共前缀为两两字符串的最长公共前缀的最长公共前缀,我们可以归并比较两最长公共前缀字符串的最长公共前缀,知道最后归并比较成一个,则为字符串数组的最长公共前缀:LCP(S1, S2, ..., Sn) = LCP(LCP(S1, Sk), LCP(Sk+1, Sn))

画图帮助理解一下:

abcabcdabac 为例:

代码实现:

var longestCommonPrefix = function(strs) {
    if (strs === null || strs.length === 0) return "";
    return lCPrefixRec(strs)
};

// 若分裂后的两个数组长度不为 1,则继续分裂
// 直到分裂后的数组长度都为 1,
// 然后比较获取最长公共前缀
function lCPrefixRec(arr) {
  let length = arr.length
  if(length === 1) {
    return arr[0]
  }
  let mid = Math.floor(length / 2),
      left = arr.slice(0, mid),
      right = arr.slice(mid, length)
  return lCPrefixTwo(lCPrefixRec(left), lCPrefixRec(right))
}

// 求 str1 与 str2 的最长公共前缀
function lCPrefixTwo(str1, str2) {
    let j = 0
    for(; j < str1.length && j < str2.length; j++) {
        if(str1.charAt(j) !== str2.charAt(j)) {
            break
        }
    }
    return str1.substring(0, j)
}

时间复杂度:O(s),s 是所有字符串中字符数量的总和

空间复杂度:O(m*logn),n是数组的长度,m为字符串数组中最长字符的长度

解法四:Trie 树(字典树)

Trie 树,也称为字典树或前缀树,顾名思义,它是用来处理字符串匹配问题的数据结构,以及用来解决集合中查找固定前缀字符串的数据结构。

解题思路: 构建一个 Trie 树,字符串数组的最长公共序列就为从根节点开始遍历树,直到:

  • 遍历节点存在超过一个子节点的节点
  • 或遍历节点为一个字符串的结束字符

为止,走过的字符为字符串数组的最长公共前缀

画图帮助理解一下:

构建一个 Trie 树,以 abcabcdabac 为例:

代码实现:

var longestCommonPrefix = function(strs) {
    if (strs === null || strs.length === 0) return "";
    // 初始化 Trie 树
    let trie = new Trie()
    // 构建 Trie 树
    for(let i = 0; i < strs.length; i++) {
        if(!trie.insert(strs[i])) return ""
    }
    // 返回最长公共前缀
    return trie.searchLongestPrefix()
};
// Trie 树
var Trie = function() {
    this.root = new TrieNode()
};
var TrieNode = function() {
    // next 放入当前节点的子节点
    this.next = {};
    // 当前是否是结束节点
    this.isEnd = false;
};
Trie.prototype.insert = function(word) {
    if (!word) return false
    let node = this.root
    for (let i = 0; i < word.length; i++) {
        if (!node.next[word[i]]) {
            node.next[word[i]] = new TrieNode()
        }
        node = node.next[word[i]]
    }
    node.isEnd = true
    return true
};
Trie.prototype.searchLongestPrefix = function() {
    let node = this.root
    let prevs = ''
    while(node.next) {
        let keys = Object.keys(node.next)
        if(keys.length !== 1) break
        if(node.next[keys[0]].isEnd) {
            prevs += keys[0]
            break
        }
        prevs += keys[0]
        node = node.next[keys[0]]
    }
    return prevs
}

时间复杂度:O(s+m),s 是所有字符串中字符数量的总和,m为字符串数组中最长字符的长度,构建 Trie 树需要 O(s) ,最长公共前缀查询操作的复杂度为 O(m)

空间复杂度:O(s),用于构建 Trie 树
leetcode

3. 更多解法请看 图解字节&leetcode14:最长公共前缀

二、图解字节&leetcode151:翻转字符串里的单词

1. 题目

给定一个字符串,逐个翻转字符串中的每个单词。

示例 1:

输入: "the sky is blue"
输出: "blue is sky the"

示例 2:

输入: "  hello world!  "
输出: "world! hello"
解释: 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。

示例 3:

输入: "a good   example"
输出: "example good a"
解释: 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。

说明:

  • 无空格字符构成一个单词。
  • 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。
  • 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。

2. 答案

解法一:正则 + JS API
var reverseWords = function(s) {
    return s.trim().replace(/\s+/g, ' ').split(' ').reverse().join(' ')
};
解法二:双端队列(不使用 API)

双端队列,故名思义就是两端都可以进队的队列

解题思路:

  • 首先去除字符串左右空格
  • 逐个读取字符串中的每个单词,依次放入双端队列的对头
  • 再将队列转换成字符串输出(已空格为分隔符)

画图理解:

代码实现:

var reverseWords = function(s) {
    let left = 0
    let right = s.length - 1
    let queue = []
    let word = ''
    while (s.charAt(left) === ' ') left ++
    while (s.charAt(right) === ' ') right --
    while (left <= right) {
        let char = s.charAt(left)
        if (char === ' ' && word) {
            queue.unshift(word)
            word = ''
        } else if (char !== ' '){
            word += char
        }
        left++
    }
    queue.unshift(word)
    return queue.join(' ')
};

leetcode

3. 更多解法请看 图解字节&leetcode151:翻转字符串里的单词

三、图解字节&leetcode160:编写一个程序,找到两个单链表相交的起始节点

1. 题目

编写一个程序,找到两个单链表相交的起始节点。

如下面的两个链表:

在节点 c1 开始相交。

 

示例 1:

输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Reference of the node with value = 8
输入解释:相交节点的值为 8 (注意,如果两个列表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。

示例 2:

输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Reference of the node with value = 2
输入解释:相交节点的值为 2 (注意,如果两个列表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [0,9,1,2,4],链表 B 为 [3,2,4]。在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。

示例 3:

输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
输入解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
解释:这两个链表不相交,因此返回 null。

注意:

  • 如果两个链表没有交点,返回 null.
  • 在返回结果后,两个链表仍须保持原有的结构。
  • 可假定整个链表结构中没有循环。
  • 程序尽量满足 O(n) 时间复杂度,且仅用 O(1) 内存。

2. 答案

解法一:标记法(简单但空间复杂度为O(n),不符合,仅做参考)

解题思路: 两次遍历,先遍历一个链表,给链表中的每个节点都增加一个标志位,然后遍历另外一个链表,遍历到第一个已被标志过的节点为两链表相交的起始节点。

若遍历完都没有发现已被标志过的节点,则两链表不相交,返回 null

var getIntersectionNode = function(headA, headB) {
    while(headA) {
        headA.flag = true
        headA = headA.next
    }
    while(headB) {
        if (headB.flag) return headB
        headB = headB.next
    }
    return null
};

时间复杂度:O(n)

空间复杂度:O(n)

解法二:双指针法

解题思路: 如果 A、B 两链表相交,则 A 、B 自相交点往后的链表是一致的。

我们可以尝试消除 A、B 链表的长度差,同步遍历上图中的方框里的节点,判断是否有相同节点,若有相同则是两链表相交,返回第一个相同节点 即可。否则返回 null ,两链表不相交。

解题步骤:

  • 同步遍历 A、B 链表 pApB ,直到遍历完其中一个链表(短链表),如上图,设A为长链表
  • 那么此时 A、B 两遍表的长度差就为 pA 到链尾的长度,此时可以把 pB 指向长链表的表头 headA ,继续同步遍历,直到遍历完长链表
  • 此时,headApB 的长度就为两链表的长度差,pB 到链表的长度与 headB 到链尾的长度一致
  • 此时,可将 pA 指向 headB ,然后同步遍历 pBpA ,直到有相交节点,返回相交节点,否则返回 null

画图帮助理解:

var getIntersectionNode = function(headA, headB) {
    // 清除高度差
    let pA = headA, pB = headB
    while(pA || pB) {
        if(pA === pB) return pA
        pA = pA === null ? headB : pA.next
        pB = pB === null ? headA : pB.next
    }
    return null
};

时间复杂度:O(n)

空间复杂度:O(1)

leetcode

3. 更多解法请看 图解字节&leetcode160:编写一个程序,找到两个单链表相交的起始节点

四、leetcode19:删除链表倒数第 n 个结点

1. 题目

给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。

示例:

给定一个链表: 1->2->3->4->5, 和 n = 2.
当删除了倒数第二个节点后,链表变为 1->2->3->5.

说明:

给定的 n 保证是有效的。

进阶:

你能尝试使用一趟扫描实现吗?

2. 解法:快慢指针

解题思路: 需要删除链表中的倒数第 n 个节点,我们需要知道的就是倒数第 n+1 个节点,然后删除删除倒数第 n+1 节点的后继节点即可

步骤:

使用 2 个指针:

  • fast 快指针提前走 n+1
  • slow 指针指向当前距离 fast 倒数第 n 个节点, 初始为 head

然后, fastslow 同步向前走,直到 fast.nextnull

此时,fast 为最后一个节点,slow 就是倒数第 n+1 个节点,此时问题就变更为删除链表中的 slow 的后继节点

但存在一个问题,当链表长度为 n 时,fast 是前进不到 n+1 个节点位置的,所以此时有两种解决思路:

  • 创建一个头节点 preHead ,设置 preHead.next = head ,这样就可以解决以上问题,删除倒数第 n 个节点后,返回的 preHead.next 即可
  • 另外一种是,fast 快指针提前走 n 步后,判断 fast.next 是否为 null ,即 fast 是否是最后一个节点,如果是,则 head 为倒数第 n 个节点,此时问题可以简化为删除头节点;如果不是, fast = fast.nextfast 再前进一步,slow 为倒数第 n+1 个节点,也解决了以上问题。
解决方案一:添加 preHead 节点
var removeNthFromEnd = function(head, n) {
    let preHead = new ListNode(0)
    preHead.next = head
    let fast = preHead, slow = preHead
    // 快先走 n+1 步
    while(n--) {
        fast = fast.next
    }
    // fast、slow 一起前进
    while(fast && fast.next) {
        fast = fast.next
        slow = slow.next
    }
    slow.next = slow.next.next
    return preHead.next
};
解决方案二:单独处理倒数第 n 节点
var removeNthFromEnd = function(head, n) {
    let fast = head, slow = head
    // 快先走 n 步
    while(--n) {
        fast = fast.next
    }
    if(!fast.next) return head.next
    fast = fast.next
    // fast、slow 一起前进
    while(fast && fast.next) {
        fast = fast.next
        slow = slow.next
    }
    slow.next = slow.next.next
    return head
};

时间复杂度:O(n)

空间复杂度:O(1)

leetcode

3. 更多解法请看 leetcode19:删除链表倒数第 n 个结点

五、leetcode876:求链表的中间结点

1. 题目

给定一个带有头结点 head 的非空单链表,返回链表的中间结点。

如果有两个中间结点,则返回第二个中间结点。 

示例 1:

输入:[1,2,3,4,5]
输出:此列表中的结点 3 (序列化形式:[3,4,5])
返回的结点值为 3 。 (测评系统对该结点序列化表述是 [3,4,5])。

注意,我们返回了一个 ListNode 类型的对象 ans,这样:
ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.

示例 2:

输入:[1,2,3,4,5,6]
输出:此列表中的结点 4 (序列化形式:[4,5,6])

由于该列表有两个中间结点,值分别为 3 和 4,我们返回第二个结点。

提示:

给定链表的结点数介于 1 和 100 之间。

2. 解法:快慢指针

解题思路: 快指针一次走两步,慢指针一次走一步,当快指针走到终点时,慢指针刚好走到中间

var middleNode = function(head) {
    let fast = head, slow = head
    while(fast && fast.next) {
        slow = slow.next
        fast = fast.next.next
    }
    return slow
};

时间复杂度:O(n)

空间复杂度:O(1)

leetcode

3. 更多解法请看 leetcode876:求链表的中间结点

六、图解leetcode206:反转链表

1. 题目

示例:

输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL

进阶:
你可以迭代或递归地反转链表。你能否用两种方法解决这道题?

2. 答案

解法一:迭代法

解题思路: 将单链表中的每个节点的后继指针指向它的前驱节点即可

画图实现: 画图帮助理解一下

确定边界条件: 当链表为 null 或链表中仅有一个节点时,不需要反转

代码实现:

var reverseList = function(head) {
    if(!head || !head.next) return head
    var prev = null, curr = head
    while(curr) {
        // 用于临时存储 curr 后继节点
        var next = curr.next
        // 反转 curr 的后继指针
        curr.next = prev
        // 变更prev、curr 
        // 待反转节点指向下一个节点 
        prev = curr
        curr = next
    }
    head = prev
    return head
};

时间复杂度:O(n)

空间复杂度:O(1)

解法二:尾递归法

解题思路: 从头节点开始,递归反转它的每一个节点,直到 null ,思路和解法一类似

代码实现:

var reverseList = function(head) {
    if(!head || !head.next) return head
    head = reverse(null, head)
    return head
};

var reverse = function(prev, curr) {
    if(!curr) return prev
    var next = curr.next
    curr.next = prev
    return reverse(curr, next)
};

时间复杂度:O(n)

空间复杂度:O(n)

解法三:递归法

解题思路: 不断递归反转当前节点 head 的后继节点 next

画图实现: 画图帮助理解一下

代码实现:

var reverseList = function(head) {
    if(!head || !head.next) return head
    var next = head.next
    // 递归反转
    var reverseHead = reverseList(next)
    // 变更指针
    next.next = head
    head.next = null
    return reverseHead
};

时间复杂度:O(n)

空间复杂度:O(n)

leetcode

3. 更多解法请看 leetcode206:反转链表

七、前端算法集训营第一期免费加入啦

欢迎关注「前端瓶子君」,回复「算法」自动加入,从0到1构建完整的数据结构与算法体系!

在这里,瓶子君不仅介绍算法,还将算法与前端各个领域进行结合,包括浏览器、HTTP、V8、React、Vue源码等。

在这里,你可以每天学习一道大厂算法题(阿里、腾讯、百度、字节等等)或 leetcode,瓶子君都会在第二天解答哟!

⬆️ 扫码关注公众号「前端瓶子君」,回复「算法」即可自动加入 👍👍👍

查看原文

认证与成就

  • 获得 1 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2018-05-05
个人主页被 251 人浏览