面试指北
方便自己进行查看 ,如有冒犯请私信
一、面试准备篇
如何准备Java面试
前言
大家身边一定有很多编程比你厉害但是找的工作并没有你好的朋友!技术面试不同于编程,编程厉害不代表技术面试就一定能过。
现在你去面个试,不简单准备一下子,那简直就是往枪口上撞。我们大部分都只是普通人,没有发过顶级周刊或者获得过顶级大赛奖项。在这样一个技术面试氛围下,我们需要花费很多精力来准备面试,来提高自己的技术能力。“面试造火箭,工作拧螺丝钉” 就是目前的一个常态,预计未来很久也还是会这样。
准备面试不等于耍小聪明或者死记硬背面试题。 一定不要对面试抱有侥幸心理。打铁还需自身硬! 千万不要觉得自己看几篇面经,看几篇面试题解析就能通过面试了。一定要静下心来深入学习!
这篇我会从宏观面出发简单聊聊如何准备 Java 面试。
尽早以求职为导向来学习
我是比较建议还在学校的同学尽可能早一点以求职为导向来学习的。
这样更有针对性,并且可以大概率减少自己处在迷茫的时间,很大程度上还可以让自己少走很多弯路。
但是!不要把“以求职为导向学习”理解为“我就不用学课堂上那些计算机基础课程了”!
我在之前的很多次分享中都强调过:一定要用心学习计算机基础知识!操作系统、计算机组成原理、计算机网络真的不是没有实际用处的学科!!!
你会发现大厂面试你会用到,以后工作之后你也会用到。我分别列举 2 个例子吧!
面试中 :像字节、腾讯这些大厂的技术面试以及几乎所有公司的笔试都会考操作系统相关的问题。
工作中 :在实际使用缓存的时候,你会发现在操作系统中可以找到很多缓存思想的影子。比如 CPU Cache 缓存的是内存数据用于解决 CPU 处理速度和内存不匹配的问题,内存缓存的是硬盘数据用于解决硬盘访问速度过慢的问题。再比如操作系统在页表方案基础之上引入了快表来加速虚拟地址到物理地址的转换。我们可以把快表理解为一种特殊的高速缓冲存储器(Cache)。
如何求职为导向学习呢? 简答来说就是:根据招聘要求整理一份目标岗位的技能清单,然后按照技能清单去学习和提升。
你首先搞清楚自己要找什么工作
然后根据招聘岗位的要求梳理一份技能清单
根据技能清单写好最终的简历
最后再按照建立的要求去学习和提升。
这其实也是 以终为始 思想的运用。
何为以终为始? 简单来说,以终为始就是我们可以站在结果来考虑问题,从结果出发,根据结果来确定自己要做的事情。
你会发现,其实几乎任何领域都可以用到 以终为始 的思想。
了解投递简历的黄金时间
面试之前,你肯定是先要搞清楚春招和秋招的具体时间的。
正所谓金三银四,金九银十,错过了这个时间,很多公司都没有 HC 了。
秋招一般 7 月份就开始了,大概一直持续到 9 月底。
春招一般 3 月份就开始了,大概一直持续到 4 月底。
很多公司(尤其大厂)到了 9 月中旬(秋招)/3 月中旬(春招),很可能就会没有 HC 了。面试的话一般都是至少是 3 轮起步,一些大厂比如阿里、字节可能会有 5 轮面试。面试失败话的不要紧,某一面表现差的话也不要紧,调整好心态。又不是单一选择对吧?你能投这么多企业呢! 调整心态。 今年面试的话,因为疫情原因,有些公司还是可能会还是集中在线上进行面试。然后,还是因为疫情的影响,可能会比往年更难找工作(对大厂影响较小)。
知道如何获取招聘信息
目标企业的官网/公众号 :最及时最权威的获取秋招信息的途径。
牛客网 : 每年秋招/春招,都会有大批量的公司会到牛客网发布招聘信息,并且还会有大量的公司员工来到这里发内推的帖子。
超级简历
超级简历目前整合了各大企业的校园招聘入口,地址:https://www.wondercv.com/jobs/。
如果你是校招的话,点击“校招网申”就可以直接跳转到各大企业的校园招聘入口的整合页面了。
4.认识的朋友
如果你有认识的朋友在目标企业工作的话,你也可以找他们了解秋招信息,并且可以让他们帮你内推。
5.宣讲会现场
Guide 当时也参加了几场宣讲会。不过,我是在荆州上学,那边没什么比较好的学校,一般没有公司去开宣讲会。所以,我当时是直接跑到武汉来了,参加了武汉理工大学以及华中科技大学的几场宣讲会。总体感觉还是很不错的!
6.其他
校园就业信息网、学校论坛、班级 or 年级 QQ 群、各大招聘网站比如拉勾……
多花点时间完善简历
一定一定一定要重视简历啊!朋友们!至少要花 2~3 天时间来专门完善自己的简历。
最近看了很多份简历,满意的很少,我简单拿出一份来说分析一下(欢迎在评论区补充)。
1.个人介绍没太多实用的信息。
技术博客、Github 以及在校获奖经历的话,能写就尽量写在这里。 你可以参考下面 👇 的模板进行修改:
2.项目经历过于简单,完全没有质量可言
每一个项目经历真的就一两句话可以描述了么?还是自己不想写?还是说不是自己做的,不敢多写。
如果有项目的话,技术面试第一步,面试官一般都是让你自己介绍一下你的项目。你可以从下面几个方向来考虑:
对项目整体设计的一个感受(面试官可能会让你画系统的架构图)
在这个项目中你负责了什么、做了什么、担任了什么角色
从这个项目中你学会了那些东西,使用到了那些技术,学会了那些新技术的使用
你是如何协调项目组成员协同开发的或者在遇到某一个棘手的问题的时候你是如何解决的又或者说你在这个项目用了什么技术实现了什么功能比如:优化了数据库的设计减少了冗余字段、用 redis 做缓存提高了访问速度、使用消息队列削峰和降流、进行了服务拆分并集成了 dubbo 和 nacos 等等。
3.计算机二级这个证书对于计算机专业完全不用写了,没有含金量的。
4.技能介绍问题太大。
技术名词最好规范大小写比较好,比如 java->Java ,spring boot -> Spring Boot 。这个虽然有些面试官不会介意,但是很多面试官都会在意这个细节的。
技能介绍太杂,没有亮点。不需要全才,某个领域做得好就行了!
对 Java 后台开发的部分技能比如 Spring Boot 的熟悉度仅仅为了解,无法满足企业的要求。
相关阅读:程序员简历到底该怎么写?有哪些注意的点?
提前准备技术面试和手撕算法
面试之前一定要提前准备一下常见的面试题:
自己面试中可能涉及哪些知识点、那些知识点是重点。
面试中哪些问题会被经常问到、面试中自己改如何回答。(强烈不推荐死记硬背,第一:通过背这种方式你能记住多少?能记住多久?第二:背题的方式的学习很难坚持下去!)
这块内容只会介绍面试大概会涉及到哪方面的知识点,具体这些知识点涵盖哪些问题,后面的文章有介绍到。
Java :
Java 基础
Java 集合
Java 并发
JVM
计算机基础 :
算法
数据结构
计算机网络
操作系统
数据库 :
MySQL
Redis
常用框架 :
Spring
SpringBoot
MyBatis
Netty
Zookeeper
Dubbo
分布式 :
CAP 理论 和 BASE 理论、Paxos 算法和 Raft 算法
RPC
分布式事务
分布式 ID
高并发 :
- 消息队列
- 读写分离&分库分表
- 负载均衡
高可用 :
- 限流
- 降级
- 熔断
不同类型的公司对于技能的要求侧重点是不同的比如腾讯、字节可能更重视计算机基础比如网络、操作系统这方面的内容。阿里、美团这种可能更重视你的项目经历、实战能力。
关于如何准备算法面试请看《Java 面试指北》的「面试准备篇」中对应的文章。
提前准备自我介绍
自我介绍一般是你和面试官的第一次面对面正式交流,换位思考一下,假如你是面试官的话,你想听到被你面试的人如何介绍自己呢?一定不是客套地说说自己喜欢编程、平时花了很多时间来学习、自己的兴趣爱好是打球吧?
我觉得一个好的自我介绍应该包含这几点要素:
用简单的话说清楚自己主要的技术栈于擅长的领域;
把重点放在自己在行的地方以及自己的优势之处;
重点突出自己的能力比如自己的定位的 bug 的能力特别厉害;
从社招和校招两个角度来举例子吧!我下面的两个例子仅供参考,自我介绍并不需要死记硬背,记住要说的要点,面试的时候根据公司的情况临场发挥也是没问题的。另外,网上一般建议的是准备好两份自我介绍:一份对 hr 说的,主要讲能突出自己的经历,会的编程技术一语带过;另一份对技术面试官说的,主要讲自己会的技术细节和项目经验。
社招:
面试官,您好!我叫独秀儿。我目前有 1 年半的工作经验,熟练使用 Spring、MyBatis 等框架、了解 Java 底层原理比如 JVM 调优并且有着丰富的分布式开发经验。离开上一家公司是因为我想在技术上得到更多的锻炼。在上一个公司我参与了一个分布式电子交易系统的开发,负责搭建了整个项目的基础架构并且通过分库分表解决了原始数据库以及一些相关表过于庞大的问题,目前这个网站最高支持 10 万人同时访问。工作之余,我利用自己的业余时间写了一个简单的 RPC 框架,这个框架用到了 Netty 进行网络通信, 目前我已经将这个项目开源,在 Github 上收获了 2k 的 Star! 说到业余爱好的话,我比较喜欢通过博客整理分享自己所学知识,现在已经是多个博客平台的认证作者。 生活中我是一个比较积极乐观的人,一般会通过运动打球的方式来放松。我一直都非常想加入贵公司,我觉得贵公司的文化和技术氛围我都非常喜欢,期待能与你共事!
校招:
面试官,您好!我叫秀儿。大学时间我主要利用课外时间学习了 Java 以及 Spring、MyBatis 等框架 。在校期间参与过一个考试系统的开发,这个系统的主要用了 Spring、MyBatis 和 shiro 这三种框架。我在其中主要担任后端开发,主要负责了权限管理功能模块的搭建。另外,我在大学的时候参加过一次软件编程大赛,我和我的团队做的在线订餐系统成功获得了第二名的成绩。我还利用自己的业余时间写了一个简单的 RPC 框架,这个框架用到了 Netty 进行网络通信, 目前我已经将这个项目开源,在 Github 上收获了 2k 的 Star! 说到业余爱好的话,我比较喜欢通过博客整理分享自己所学知识,现在已经是多个博客平台的认证作者。 生活中我是一个比较积极乐观的人,一般会通过运动打球的方式来放松。我一直都非常想加入贵公司,我觉得贵公司的文化和技术氛围我都非常喜欢,期待能与你共事!
减少抱怨
就像现在的技术面试一样,大家都说内卷了,抱怨现在的面试真特么难。然而,单纯抱怨有用么?你对其他求职者说:“大家都不要刷 Leetcode 了啊!都不要再准备高并发、高可用的面试题了啊!现在都这么卷了!”
会有人听你的么?你不准备面试,但是其他人会准备面试啊!那你是不是傻啊?还是真的厉害到不需要准备面试呢?
因此,准备 Java 面试的第一步,我们一定要尽量减少抱怨。抱怨的声音多了之后,会十分影响自己,会让自己变得十分焦虑。
面试之后及时复盘
如果失败,不要灰心;如果通过,切勿狂喜。面试和工作实际上是两回事,可能很多面试未通过的人,工作能力比你强的多,反之亦然。
面试就像是一场全新的征程,失败和胜利都是平常之事。所以,劝各位不要因为面试失败而灰心、丧失斗志。也不要因为面试通过而沾沾自喜,等待你的将是更美好的未来,继续加油!
总结
这篇文章内容有点多,如果这篇文章只能让你记住 4 句话,那请记住下面这 4 句:
- 一定要提前准备面试!技术面试不同于编程,编程厉害不代表技术面试就一定能过。
- 一定不要对面试抱有侥幸心理。打铁还需自身硬!千万不要觉得自己看几篇面经,看几篇面试题解析就能通过面试了。一定要静下心来深入学习!
- 建议大学生尽可能早一点以求职为导向来学习的。这样更有针对性,并且可以大概率减少自己处在迷茫的时间,很大程度上还可以让自己少走很多弯路。 但是,不要把“以求职为导向学习”理解为“我就不用学课堂上那些计算机基础课程了”!
- 手撕算法是当下技术面试的标配,尽早准备!
面试常见词汇扫盲
春招和秋招
春招的时候一般会同时进行 准应届生暑期实习生招聘 和 应届生校园招聘 (准应届生指的是来年毕业的在校大学生)。
不过, 这个时候应届生校园招聘的岗位相对已经比较少了,基本是对秋招的补招,秋招的时候才是应届生校园招聘的关键时期。
春招期间,集中进行的实习生招聘一般是暑期实习生招聘。
秋招一般 7 月份就开始了,大概一直持续到 9 月底。春招一般 3 月份就开始了,大概一直持续到 4 月底。很多公司(尤其大厂)到了 9 月中旬(秋招)/3 月中旬(春招),很可能就会没有 HC 了。
暑期实习和日常实习
暑期实习通常是在春招的时候开始大规模招聘,面试难度大于日常的实习招聘,性价比也比日常实习要高。
暑期实习的招聘对象是准应届生(来年毕业的在校大学生),这一部分实习生其本质是校招后备军。通常都会有转正名额提供给暑期实习生,通过转正考核可以拿到正式校招 offer。
一般来说,暑期实习会在 6-7 月也就是暑期那会入职。
日常实习通常全年都会进行,一般为部门的散招,一不会给转正名额。日常实习生的招聘对象通常是大一、大二、研一、研二的同学。
一般来说,拿到日常实习 offer 后,立刻就会入职。
提前批
为什么很多公司有提前批?
很明显啊!提前批就是各个公司提前抢夺一波优秀毕业生。
你没必要担心这个提前批的含金量如何,觉得自己能力足够的话,一定要把握这次机会!提前批还是会有很多 sp 甚至 ssp offer 的!
为什么推荐提前批呢?
因为,提前批的结果并不影响你的秋招,也就是说你可以多一次机会。这样的话,即使你失败了,也没关系,好好分析一下自己的短板,努力准备秋招就完事了!并且许多公司的提前批是直接面试,免笔试的。
但是!我这里建议,投提前批的时候,不要一次把你最想去的公司全投了。比如你最想去腾讯、百度、阿里。那么你提前批可以投百度,再投两个小一些的公司,然后根据几次的面试反馈继续提升自己,再陆续去投自己最想去的公司。虽然很多公司都说面试挂了不影响正式批再战,但是你面试的时候会有评价记录的,这个面试记录 hr 是可以看到的,以后的面试官面试也会看到。如果面试官给你的评价记录比较中性还好,但如果面试官给你一个很差的面试评价。那么正式批的时候 hr 筛简历就不会通过你了。我去年面试快手提前批没过,不知道那位面试官给我写的是什么评价,简历再投别的部门就通不过了。但是面字节虽然第一次面试没通过,我后续还是被很多部门捞。
如果提前批有那种部门组织的预面试,就是不会被录入公司系统的面试,这种机会你要果断投简历。这种面试机会很难得,公司不会有你的面试记录,面试没过也不会影响你后续投别的部门,还获得了一次难得的面试机会。一定不要因为觉得自己没准备好而放弃这种面试,大厂的每一次面试都是特别好的学习机会。其实许多人最初几次面试都是不能通过的,经历过几次失败,然后总结面试中的问题,你就离大厂 offer 越来越近了。
偷偷告诉你:这些大厂可能会组织那种不留面试记录的部门预面试,阿里、百度、京东、字节跳动 ~ 大家可以去找在这些公司工作的学长学姐了解,也可以去牛客上了解。
内推
每年的秋招开始以后大家可能会看到大量的内推宣传。但是不同形式的内推差别其实是很大的。如果只是从网上随便找一个内推码,内推人都不认识就把简历投了,这种内推是没用的。有用的内推是,内推者可以直接把你的简历交到筛选简历的部门 HR 手里,这样 HR 能快速看到你的简历,并且给你安排面试。
HC(Headcount)
俗称人头,稍微专业点讲就是这家公司打算招的人数。公司会录用很多实习生,也有“广撒 offer”的说法,把人留住,但实际最后只会录用其中的一部分,不会录取所有。最后真正录取的实习生,即可转正。而不被录取的一部分,可能是不在 HC 之内,由于工作能力、工作需要等等。 以往都是先定了 HC 再发 offer,但最近新闻上也有很多企业是先发了 offer,但后来再以 HC 已招够为由来拒收实习生的。所以同学们在找实习,申请校招的时候要格外注意这一点。
面试记录
大家进行互联网公司组织的面试,都会留下自己的面试记录。面试记录上会有面试官的面试评语。这个面试记录,是以后面试你的面试官还有 HR 都能看到的。
预面试
部门收到你的简历后,先不录入公司系统,由 HR 筛选。如果通过简历筛选。部门直接发起预面试,面试通过后,录入系统直接走下面的流程。面试不过,不影响你投这个公司的其它部门,因为公司没有你的面试记录。找预面试的途径是找自己在这个公司的师兄师姐,或者在牛客网上找部门直招的帖子。预面试在部分公司是不合规的。
主管面
主管面指的是部门的技术主管对你进行面试,走到这一关可以证明大家的技术已经问题不大了。主管面基本上都会采用半问技术,半聊理想的形式对你进行面试。有时候也会问你在校的一些活动经历,甚至会问你毕业论文在做什么。主管面除了考察技术外,一个重要的考察点是考察你是否和团队契合。
HR 面
HR 面指的就是人力资源对你进行面试。HR 通常第一个问题就是你是哪人,这个问题其实是想看你是不是来公司面试解闷子的。如果你面的是一家北京的公司,而且你是河北人、河南人、山西人等北京周边的城市,你说了你是哪人以后你就不用多说了。但是如果你家是西北那边的,上学又是在东北那嘎达上的,又恰巧你面的是一个广州深圳的公司,你最好说清楚你为啥想去那边工作。另外,HR 会问一些在校经历,通过交流来判断你的性格是否符合团队。对了,还有一个 HR 常问问题,你拿到了哪些 offer?这个问题你就要甩出一些比较硬的 offer 了,因为优质人才谁都想抢。但是你甩出的 offer 要和现在面试的公司是在一个量级上的。不要你面试的是一个小公司,你跟人家说你已经拿到了字节的工牌,你觉得人家相信不相信给了你 offer 你会来?
八股文
各种面试题题目,主要是一些概念性的知识,比如 jvm 的运行时数据区的构成、 mysql 的索引之类的,这些问题的回答一般有固定套路。现在的面试主要就是八股文+算法。我在之后的文章也在总结面试八股文的重点,预计一周内能发出来。面试八股文背的熟是面试成功的必要不充分条件。现在背八股文也是一个潮流,但是我其实不太喜欢这个潮流。大家在平时学习时还是要打好基础,我把平时看到的比较好的计算机基础资料收集在我的公众号里,大家关注 CS 指南 ,回复计算机基础就能领取。
手撕算法
手撕算法简单来说就是完成面试官给你布置的算法题(有些公司提供思路即可)。国内现在的校招面试开始越来越重视算法了,尤其是像字节跳动、腾讯这类大公司。绝大部分公司的校招笔试是有算法题的,如果 AC 率比较低的话,基本就挂掉了。
常规面试
现在互联网大厂的常规面试大多都采用这种形式,前半小时自我介绍、问项目、背面试八股文,后半小时一道代码题。
程序员简历到底该怎么写?
前言
一份好的简历可以在整个申请面试以及面试过程中起到非常重要的作用。
为什么说简历很重要呢? 我们可以从下面几点来说:
1.简历就像是我们的一个门面一样,它在很大程度上决定了是否能够获得面试机会。
- 假如你是网申,你的简历必然会经过 HR 的筛选,一张简历 HR 可能也就花费 10 秒钟看一下,然后 HR 就会决定你这一关是 Fail 还是 Pass。
- 假如你是内推,如果你的简历没有什么优势的话,就算是内推你的人再用心,也无能为力。
另外,就算你通过了第一轮的筛选获得面试机会,后面的面试中,面试官也会根据你的简历来判断你究竟是否值得他花费很多时间去面试。
2.简历上的内容很大程度上决定了面试官提问的侧重点。
- 一般情况下你的简历上注明你会的东西才会被问到(Java、数据结构、网络、算法这些基础是每个人必问的),比如写了你熟练使用 Redis,那面试官就很大概率会问你 redis 的一些问题。再比如你写了你在项目中使用了消息队列,那面试官大概率问很多消息队列相关的问题。
- 技能熟练度在很大程度上也决定了面试官提问的深度。
在不夸大自己能力的情况下,写出一份好的简历也是一项很棒的能力。
简历模板
简历的样式真的非常非常重要!!!如果你的简历样式丑到没朋友的话,面试官真的没有看下去的欲望。一天处理上百份的简历的痛苦,你不懂!
我这里的话,推荐大家使用 Markdown 语法写简历,然后再将 Markdown 格式转换为 PDF 格式后进行简历投递。如果你对 Markdown 语法不太了解的话,可以花半个小时简单看一下 Markdown 语法说明:http://www.markdown.cn/。
下面是我收集的一些还不错的简历模板:
- 木及简历(推荐 👍) : https://resume.mdedit.online 。
- typora+markdown+css 自定义简历模板(推荐 👍) :https://github.com/Snailclimb/typora-markdown-resume
- 极简简历 : https://www.polebrief.com/index
●Markdown 简历排版工具:https://resume.mdnice.com/ - 超级简历 : https://www.wondercv.com/
上面这些简历模板大多是只有 1 页内容,很难展现足够的信息量。如果你不是顶级大牛(比如 ACM 大赛获奖)的话,我建议还是尽可能多写一点可以突出你自己能力的内容(2~3 页皆可,记得精炼语言,不要过多废话)。
再总结几点 简历排版的注意事项:
- 尽量简洁,不要太花里胡哨。
- 一些技术名词不要弄错了大小写比如 MySQL 不要写成 mysql,Java 不要写成 java。
- 中文和数字英文之间加上空格的话看起来会舒服一点。
简历内容
个人信息
最基本的 :姓名(身份证上的那个)、年龄、电话、籍贯、联系方式、邮箱地址
潜在加分项 : Github 地址、博客地址(如果技术博客和 Github 上没有什么内容的话,就不要写了)
示例:
求职意向
你想要应聘什么岗位,希望在什么城市。另外,你也可以将求职意向放到个人信息这块写。
示例:
教育经历
教育经历也不可或缺。通过教育经历的介绍,你要确保能让面试官就可以知道你的学历、专业、毕业学校以及毕业的日期。
示例:
北京理工大学 硕士,软件工程 2019.09 - 2022.01 湖南大学 学士,应用化学 2015.09 ~ 2019.06
专业技能
先问一下你自己会什么,然后看看你意向的公司需要什么。一般 HR 可能并不太懂技术,所以他在筛选简历的时候可能就盯着你专业技能的关键词来看。对于公司有要求而你不会的技能,你可以花几天时间学习一下,然后在简历上可以写上自己了解这个技能。
下面这个专业技能介绍,你可以根据自己的实际情况参考一下。
我这里再单独放一个我看过的某位同学的技能介绍,我们来找找问题。
上图中的技能介绍存在的问题:
技术名词最好规范大小写比较好,比如 java->Java ,spring boot -> Spring Boot 。这个虽然有些面试官不会介意,但是很多面试官都会在意这个细节的。
技能介绍太杂,没有亮点。不需要全才,某个领域做得好就行了!
对 Java 后台开发的部分技能比如 Spring Boot 的熟悉度仅仅为了解,无法满足企业的要求。
实习经历/工作经历
工作经历针对社招,实际经历针对校招。
工作经历建议采用时间倒序的方式来介绍,实习经历建议将最有价值的放在最前面。
示例:
XXX 公司 (201X 年 X 月 ~ 201X 年 X 月 )
●职位:Java 后端开发工程师
●工作内容:主要负责 XXX
项目经历
简历上有一两个项目经历很正常,但是真正能把项目经历很好的展示给面试官的非常少。
很多求职者的项目经历介绍都会面临过于啰嗦、过于简单、没突出亮点等问题。
项目经历应该突出自己做了什么,简单概括项目基本情况。项目经历取得的成果尽量要量化一下,多挖掘一些亮点比如自己是如何解决项目中存在也一个痛点的 。除了解决痛点,还能如何挖掘亮点呢? 从你项目涉及到的技术上来挖掘,想想这些技术能为项目带来哪些改进。
技术优化取得的成果尽量要量化一下:
- 我使用 xxx 技术解决了 xxx 问题,系统 qps 从 xxx 提高到了 xxx。
- 我使用 xxx 技术了优化了 xxx 接口,系统 qps 从 xxx 提高到了 xxx。
另外,如果你觉得你的项目技术比较落后的话,可以自己私下进行改进。重要的是让项目比较有亮点,通过什么方式就无所谓了。
项目经历介绍模板:
个人工作内容描述最好可以体现自己的综合素质,比如你是如何协调项目组成员协同开发的或者在遇到某一个棘手的问题的时候你是如何解决的又或者说你在这个项目优化了某个模块的性能。示例:项目的 MySQL 数据库中的某张表的数据量达到千万级别,查询速度非常缓慢,数据库压力非常大,我使用 Sharding-JDBC 进行了分库分表,单表的数据量都在 300w 以下。
荣誉奖项(可选)
如果你有含金量比较高的竞赛(比如 ACM、阿里的天池大赛)的获奖经历的话,荣誉奖项这块内容一定要写一下!并且,你还可以将荣誉奖项这块内容适当往前放,放在一个更加显眼的位置。
校园经历(可选)
如果有比较亮眼的校园经历的话就简单写一下,没有就不写!
个人评价
个人评价就是对自己的解读,一定要用简洁的语言突出自己的特点和优 势,避免废话! 像勤奋、吃苦这些比较虚的东西就不要扯了,面试官看着这种个人评价就烦。
列举 3 个实际的例子:
学习能力较强,大三参加国家软件设计大赛的时候快速上手 Python 写了一个可配置化的爬虫系统。
具有团队协作精神,大三参加国家软件设计大赛的时候协调项目组内 5 名开发同学,并对编码遇到困难的同学提供帮助,最终顺利在 1 个月的时间完成项目的核心功能。
项目经验丰富,在校期间主导过多个企业级项目的开发。
STAR 法则和 FAB 法则
STAR 法则(Situation Task Action Result)
相信大家一定听说过 STAR 法则。对于面试,你可以将这个法则用在自己的简历以及和面试官沟通交流的过程中。
STAR 法则由下面 4 个单词组成(STAR 法则的名字就是由它们的首字母组成):
Situation: 情景。 事情是在什么情况下发生的?
Task:: 任务。你的任务是什么?
Action: 行动。你做了什么?
Result: 结果。最终的结果怎样?
FAB 法则(Feature Advantage Benefit)
除了 STAR 法则,你还需要了解在销售行业经常用到的一个叫做 FAB 的法则。
FAB 法则由下面 3 个单词组成(FAB 法则的名字就是由它们的首字母组成):
- Feature: 你的特征/优势是什么?
- Advantage: 比别人好在哪些地方;
- Benefit: 如果雇佣你,招聘方会得到什么好处。
简单来说,FAB 法则主要是让你的面试官知道你的优势和你能为公司带来的价值。
注意事项和建议
- 一定要使用 PDF 格式投递,不要使用 Word 或者其他格式投递。这是最基本的!
- 尽量避免主观表述,少一点语义模糊的形容词。表述要简洁明了,简历结构要清晰。
- 精简表述,突出亮点。校招简历建议不要超过 2 页,社招简历建议不要超过 3 页。如果内容过多的话,不需要非把内容压缩到一页,保持排版干净整洁就可以了。
- 不会的东西就不要写在简历上了。注意简历真实性,适当润色没有问题。
- 技术博客、Github 以及获奖经历等可以直接证明自己能力的东西,能写就尽量写在这里。不过,如果技术博客和 Github 上没有什么内容的话,就不要写了。
- 技术名词最好规范大小写比较好,比如 java->Java ,spring boot -> Spring Boot 。这个虽然有些面试官不会介意,但是很多面试官都会在意这个细节的。
- 工作经历建议采用时间倒序的方式来介绍,实习经历建议将最有价值的放在最前面。
- 将自己的项目经历完美的展示出来非常重要,重点是突出自己做了什么(挖掘亮点),而不是介绍项目是做什么的。
- 项目经历建议以时间倒序排序,另外项目经历不在于多(精选 2~3 即可),而在于有亮点。
- 个人评价就是对自己的解读,一定要用简洁的语言突出自己的特点和优势,避免废话! 像勤奋、吃苦这些比较虚的东西就不要扯了,面试官看着这种个人评价就烦。
- 准备面试的过程中应该将你写在简历上的东西作为重点,尤其是项目经历上和技能介绍上的。
- 面试和工作是两回事,聪明的人会把面试官往自己擅长的领域领,其他人则被面试官牵着鼻子走。虽说面试和工作是两回事,但是你要想要获得自己满意的 offer ,你自身的实力必须要强。
为什么要学习源码?源码这块面试会怎么问呢?如何阅读源码?
为什么要学习源码?
学习优秀的代码实践
如果我们沉浸在自己的世界,整天 CRUD,实际是很难提高自己的编码能力,重复编码提升的不过是熟练度罢了!
如果我们想要写出质量更高、扩展性更好的代码,我们要做的事情非常简单:看一些技术大佬是怎么写的,模仿就完事了。
这个模仿不同于照葫芦画瓢,我们需要搞懂优秀设计背后的原理。
那怎么检验自己是否掌握了呢?很简单,看自己能不能在后续的编码中实践就好了。但是,切记不要为了用“好的编码实践”而用,一切要结合业务实际需要。
一些不错的开源项目,都是一些技术大佬们几个月甚至是几年的成果。只要肯花时间看,我们一定能从源码中学到很多东西。
我们需要重点关注源码中的这些点:
如何抽象接口的?
如何运用设计模式的?
- 如何实践 SOLID 软件设计原则的?
- 有哪些优秀的编码实践?
- ……
借鉴
如果我们想要设计一个类似的框架或者轮子的话,参考已有的优秀框架不失为一个好手段。俗话说的好:“他山之石可以攻玉”。
我们平时接触到的很多开源项目都是例子,比如阿里开源的消息队列 RocketMQ 就借鉴了 Kafka 。
面试需要
据我观察,大部分真正愿意去看源码的朋友都是为了面试。这些朋友会找到对应框架比较重要的部分来学习源码,拿 Spring Boot 来说的话,就是 Spring Boot 启动流程、自动配置原理…。
确实,短时间内突击源码,我们一定要重点关注那些重要的地方。
但是,这种为了面试而突击源码的方式,往往很难真正学到源码的精髓,能收货的东西也会很有限。
项目需求
很多时候,我们阅读源码是因为项目需要。
比如说我们的项目在前期引入了某个开源框架,但是到项目中期的时候,我们发现这个开源框架并不能很好地满足我们的需求,甚至说还有一些小 bug 。与这个开源框架相关的负责人员交涉之后,我们的反馈并没有得到相应。这个时候, 我们就需要自己去实现某些功能以及修复某些 bug。想要做这些事情的前提是:我们当前对这个开源框架某一块的源码比较熟悉了。
源码面试这块会怎么问?
首先,你需要明确一点的是:随便一个框架的源码都 10w+行了,都看一遍是不可能的。你需要挑选比较重要的地方看。
拿 Spring/Spring Boot 源码举例:你一定要去看 IOC 和 AOP 具体的实现,要知道一个 Spring Bean 是如何一步一步被创建出来的。一定要搞清 Spring Boot 是如何实现自动配置的。
源码面试这个不会太细节。如果你知道的话一定是加分项,不知道的话不一定就会被 pass。不过你写简历的时候尽量写清楚点,写清楚自己看过哪部分的源码。
平时学习过程中,有时间的话可以多看看源码,对于提升自己的能力非常有帮助!
如果你不知道阅读什么源码的话,可以先从 JDK 的几个常用集合看起。另外,我比较推荐看 Dubbo 的,因为感觉会稍微相对容易一点,模块划分清晰,注释也比较详细。搞清楚了 Dubbo 基本的原理之后,看起来就没那么吃力了。
有哪些值得阅读的优秀源码?
下面有部分内容是摘自朋友写的一篇文章:《如何提升代码质量 - Thoughtworks 洞见》
JDK
为什么要看 JDK 源码?
- JDK 源码是其它所有源码的基础,看懂了 JDK 源码再看其它的源码会达到事半功倍的效果。
- JDK 源码中包含大量的数据结构知识,是学习数据结构很好的资料,比如,链表、队列、散列表、红黑树、跳表、桶、堆、双端队列等。
- JDK 源码中包含大量的设计模式,是学习设计模式很好的资料,比如,适配器模式、模板方法模式、装饰器模式、迭代器模式、代理模式、工厂模式、命令模式、状态模式等。
- JDK 源码中包含大量 Java 的高阶知识,比如弱引用、Unsafe、CAS、锁原理、伪共享等,不看源码是很难学会这些知识的。
JDK 源码阅读顺序 :
- java.lang 包下的基本包装类(Integer、Long、Double、Float 等),还有字符串相关类(String、StringBuffer、StringBuilder 等)、常用类(Object、Exception、Thread、ThreadLocal等)。
- java.lang.ref 包下的引用类(WeakReference、SoftReference 等)
- java.lang.annotation 包下的注解的相关类
- java.lang.reflect 包下的反射的相关类
- java.util 包下为一些工具类,主要由各种容器和集合类(Map、Set、List 等)
- java.util.concurrent 为并发包,主要是原子类、锁以及并发工具类
- java.io 和 java.nio 可以结合着看
- java.time 主要包含时间相关的类,可以学习下 Java 8 新增的几个
- java.net 包下为网络通信相关的类,可以阅读下 Socket 和 HTTPClient 相关代码
源码量那么大,不要妄想一口气都看完。最好符合你当前的目的,比如你想搞懂多线程,你就主要看 JUC,想搞懂 IO 就多去看 NIO,想看常量池就去看 ClassFileParser。看模块的时候,要注意接口大于一切,或者说函数大于一切。先不要妄想搞懂所有细节,先找几个比较关键的函数,搞懂函数的作用(比如应该仔细分析一下函数名称和参数名称)然后再往下进行。
在看 Java 类库的时候要多注意类是不是 abstract 的,是不是用的模板方法,多关注函数前的修饰词,这一般说明这个函数是给谁用的。多注意这些细节而不是傻傻过一遍逻辑,能从里面学到不少关于设计的东西。还可以注意什么地方是为了之前的设计而委曲求全的做法,毕竟一个这么多年的类库,肯定不是什么地方都是完美的。
JDK 源码一定要看 Java 并发相关的源码, Doug Lea 的并发源码比较漂亮,一行行都是精华,非常值得阅读学习。
Spring
Spring 是一个开源的设计层面框架,它解决的是业务逻辑层和其他各层的松耦合问题,因此它将面向接口的编程思想贯穿整个系统应用。包括在此基础上衍生的 Spring MVC、 Spring Boot 、Spring Cloud 等,在现在企业中的应用越来越广泛。无论是设计思想,代码规范,还是设计模式,接口设计,类加载,都是非常优秀的源码。
个人学习心得如下:先去看视频,大概熟悉一下 Spring 的使用情况,然后再去学习源码,此处可以阅读《Spring 源码深度解析》,除了看书之外,记得打开 IDEA 查看对应的源码,如果能调试看看具体调用逻辑那就更好了。
Google Guava
Google Guava 是 Google 公司内部 Java 开发工具库的开源版本。Google 内部的很多 Java 项目都在使用它。它提供了一些 JDK 没有提供的功能,以及对 JDK 已有功能的增强功能。其中就包括:集合(Collections)、缓存(Caching)、原生类型支持(Primitives Support)、并发库(Concurrency Libraries)、通用注解(Common Annotation)、字符串处理(Strings Processing)、数学计算(Math)、I/O、事件总线(EventBus)等等。
Netty
Netty 是一个优秀的开源网络编程框架,我们平常经常接触的 Dubbo、RocketMQ、Elasticsearch、gRPC 等等都用到了 Netty。
Netty 中使用了大量的设计模式以及优秀的设计原则。
Dubbo
Dubbo 是一款优秀的国产 RPC 框架,其 SPI 自适应扩展、负载均衡实现、集群实现、服务调用过程等部分的源码都非常值得阅读和学习。
保姆级别的源码阅读教学
如何高效阅读项目源码
⚠️ 注意:
阅读源码之前,一定要先熟悉项目。你连 Dubbo 怎么使用、RPC 是个啥都不知道,就直接去看 Dubbo 源码的话,不是纯属扯淡么?
阅读源码之前,一定要对项目源码使用的技术有一个最基本的认识。
从了解并使用项目开始
开始看源码之前,自己花一些时间阅读以下官方的文档、使用教程。如果官方文档是英文的话,也可以找一些国人写的博客看看。不知道项目是用法和用途的就去看项目源代码的行为,无疑是在黑夜中穿针。
站在最外层概览项目设计
阅读源码之前,我比较推荐先站在最外层去熟悉项目整体架构和模块分包。掌控全局之后,我们方能以一个更正确的姿势畅游源码的世界。
比如,我在看 Dubbo 源码之前,我就首先花了大量时间熟悉了 Dubbo 的模块分包,这个在其官方文档上介绍的就非常详细。
从某个功能主线/问题出发研究项目源码
一个比较成熟的项目的源码量是非常多,我们不可能都看完。比较推荐的方式就是通过一个功能主线(比如 Dubbo 是如何暴露服务的?)或者问题(比如 SpringBoot 的自动配置原理?)出发。
学会使用官方提供的 Demo
一般情况下,项目源码已经自带了一些 Demo 我们可以直接使用。这样可以方便我们:
检验源码阅读环境是否搭建成功。
调试项目。
比如在 Dubbo 项目源码中,我们找到 dubbo-demo 这个文件夹,里面包含了 3 种不同类型(xml、api、annotation)使用方式的 demo,可以帮助我们节省掉大量写 Demo 的时间。
有哪些对阅读源码有帮助的建议
学习常见的设计模式、设计原则
一个优秀的开源项目一定会不可避免使用到一些设计模式,如果我们提前不了解这些设计模式的话,会加深自己理解代码的难度。
另外,项目的代码还应该满足一些设计原则。对于,面向对象编程来说,下面这些原则都是我们应该非常熟练的。
面向对象编程的思想(继承、封装、多态、抽象)
面向对象的七大设计原则:
单一职责原则(Single Responsibility Principle, SRP)
开闭原则(Open Closed Principle,OCP)
里氏代换原则(Liskov Substitution Principle,LSP)
接口隔离原则(Interface Segregation Principle,ISP)
依赖反转原则(Dependency Inversion Principle,DIP)
合成/聚合复用原则(Composite/Aggregate Reuse Principle,CARP)
迪米特法则(Principle of Least Knowledge,PLK,也叫最小知识原则)
软件设计的三大原则
DRY(Don’t Repeat Yourself)原则:不要重复你自己
KISS( Keep It Simple/Stupid)原则:保持简单易懂
YAGNI ( You Ain’t Gonna Need It)原则 :不要进行过度设计
学会看测试类
测试类一方面可以说是对代码的稳定性的保障,让我们对自己写的代码更放心。另一方面测试本身也是对代码的一个说明,因此,很多人都会说:“好的测试即是文档”。通过测试类,我们可以很清楚地知道某一块代码具体是在干嘛。
学会调试
通过调试,我们可以更直观地看到调用逻辑关系。通过变量信息,可以更直观地看到数据的变化过程,这对于我们理解代码以及找 bug 都是非常有帮助的。
掌握 IDE 常用的快捷键
熟悉常用的快捷键在源码阅读中非常有必要!这个是必须要会的。
比如在 IDEA 中我们通过 command+o(mac)或者 ctrl+n(win/linux)即可搜索类以及文件
通过 command+f12(mac)或者 ctrl+f12(win/linux)即可查看类的结构(函数、变量)
使用一些插件辅助自己
有一些插件可以帮助我们理清源码的调用逻辑。比如 IDEA 插件 SequenceDiagram 就可以帮助我们一键生成方法的时序图。
相关阅读:《安利一个 IDEA 骚操作:一键生成方法的序列图》
手撸一个简易版
我们可以在学习某个具体的框架源码之前,自己先手撸一个简易版的框架。
就比如我们学习 Dubbo 源码之前,我们自己撸一个简易版的 RPC 框架。
自己动脑思考该怎么设计,功能该如何实现。
做了这些尝试之后,我们再去看别人写的源码,收货一定会非常大!
为什么要准备算法面试?怎么高效刷 Leetcode?
为什么要准备算法面试?
很明显,国内现在的校招面试开始越来越重视算法了,尤其是像字节跳动、腾讯这类大公司。绝大部分公司的校招笔试是有算法题的,如果 AC 率比较低的话,基本就挂掉了。
社招的话,算法面试同样会有。不过,面试官可能会更看重你的工程能力,你的项目经历。如果你的其他方面都很优秀,但是算法很菜的话,不一定会挂掉。不过,还是建议刷下算法题,避免让其成为自己在面试中的短板。
社招往往是在技术面试的最后,面试官给你一个算法题目让你做。
为了能够应对,我们大部分人能做的就是刷 Leetcode 来积累做算法题的经验和套路。
另外, 很多小伙伴特别是已经工作几年的,总觉得说算法这东西没啥用。确实,相比于系统设计能力,工程能力来说,算法对于普通工程师的价值可能并不是那么大。但是,在面试中算法确实很能考验面试者能力的一个环节。单纯靠项目经历以及技术面试的话,还是很容易弄虚作假的。算法能力在某些角度可以反映你解决编程问题的能力以及你的思维能力。
刷 LeetCode 吃力正常吗?
我自己写过框架,写过 web 服务器,给项目造过轮子。
我想说的是太正常不过了!我的工程能力在同龄人中应该还算可以,但是很多 Leetcode 上面的算法题我真做不出来。手撕算法方面我真的没有公司新招进来的应届生强。说实话,公司出的算法题,我自己都不一定能做出来(主要是因为工作之后很久没碰了)。
小声 BB:应届生过来了还是要不断经历我们“老人“的 diss,哈哈哈!鲁迅先生说:没有 diss,哪里来的成长。
刷 Leetcode 需要哪些基础?
编程语言
刷题之前,确保你有一个还算熟悉的编程语言。比较常用的有 Java、C/C++、Python、Go。
数据结构和算法基础
为了让自己更愉快地刷题,一些基本的数据结构和算法知识是必备的。
刷 Leetcode 之前,如果你还没有算法和数据结构方面的基础知识的话,可以先看一些比较适合入门的书籍。我的下面这个回答会推荐一些算法相关的书籍和学习资源(写的用心,觉得不错的话可以点个赞鼓励一下):《有哪些值得推荐的好的算法书?》 。
- 数据结构:数组、链表、栈、队列、堆、二叉树、图、哈希表、并查集
- 算法思想 : 递归、动态规划、二分查找、贪心、分治、回溯、DFS、BFS、KMP、树的广度和深度优先搜索、
- 数学: 位运算、质数、排列组合
另外,对于每一种编程语言都有一些内置的常用数据结构的实现,我们需要提前了解。拿 Java 来说,HashMap、TreeMap,TreeSet,PriorityQueue,Deque
等都是比较常用的。
怎么高效刷 Leetcode?
最重要的还是一定要坚持刷起来!!!
按照类型来刷
我个人比较建议每次刷题助攻一个类型,比如你某一天或者几天就主要刷动态规划相关的题目。
为什么这样建议呢? 也是一点个人经验吧!我当时在刷算法题的时候,发现如果每次做的算法题类型跨度太大的话,非常影响自己速度和体验。当你刷了同一种类型的题目比较多了之后,你就会大概知道这类题型的套路了。
由简入难
很多朋友可能和我一样,刚开始刷 LeetCode 的时候,寸步难行,经常一道算法题一下午写不出来。当时,我甚至开始怀疑自己是不是笨。为此,我专门找到一些算法大佬交流。算法大佬安慰我说:“ 不是你笨,不用灰心!你先从简单地算法题开始做起,多多总结就好了!”。
于是,我每天抽出 1~3 个小时专门用来刷简单类型的算法题。刷了一个多月之后,一些我比较熟悉的题型(简单类型)可以不看答案就能直接 AC 了。
刷算法是一个循序渐进的过程,如果你不是 ACM 大佬这种级别的人物的话,还是建议先从简单开始刷起,慢慢积累经验。
不过,要说明的一点是:很多简单类型的题目甚至还要比中等类型的题目还要难!所以,如果你没办法解决一些简单的算法题,也不要太纠结,不要因此失去信心。
重视高频面试题目/题型
如果你的时间不是很充足的话,建议可以从高频面试题入手。
像 Leetcode 上面就专门把一些最热门的算法面试题给单独整理了出来。
面试中有哪些常见的手撕代码题?
欢迎各位同学在评论区补充完善。
- LRU 缓存实现:https://www.baeldung.com/java-lru-cache
- 栈实现:https://zihengcat.github.io/2019/05/18/java-data-structure-and-algorithm-stack/
- 队列实现:https://cloud.tencent.com/developer/article/1894209
- 加权轮询算法实现:https://mp.weixin.qq.com/s/P25wnGkOjrZiq034UIu2pg
- 死锁:https://www.baeldung.com/java-deadlock-livelock 、https://javaguide.cn/java/concurrent/java-concurrent-questions-01.html(死锁模块有示例代码)
- 单例模式实现:https://segmentfault.com/a/1190000040146574
- 快速排序:http://www.atguigu.com/mst/java/gaopin/17136.html
- 生产者与消费者:https://cloud.tencent.com/developer/article/1824304
- ……
没有项目经验怎么办?跟着视频做的项目会被面试官嫌弃不?
没有项目经验怎么办?
没有项目经验是大部分应届生会碰到的一个问题。甚至说,有很多有工作经验的程序员,对自己在公司做的项目不满意,也想找一个比较有技术含量的项目来做。
说几种我觉得比较靠谱的获取项目经验的方式,希望能够对你有启发。
实战项目视频/专栏
在网上找一个符合自己能力与找工作需求的实战项目视频或者专栏,跟着老师一起做。
你可以通过慕课网、哔哩哔哩、拉勾、极客时间、培训机构(比如黑马、尚硅谷)等渠道获取到适合自己的实战项目视频/专栏。
尽量选择一个适合自己的项目,没必要必须做分布式/微服务项目,对于绝大部分同学来说,能把一个单机项目做好就已经很不错了。
我面试过很多求职者,简历上看着有微服务的项目经验,结果随便问两个问题就知道根本不是自己做的或者说做的时候压根没认真思考。这种情况会给我留下非常不好的印象。
我在《Java 面试指北》的「面试准备篇」中也说过:
个人认为也没必要非要去做微服务或者分布式项目,不一定对你面试有利。微服务或者分布式项目涉及的知识点太多,一般人很难吃透。并且,这类项目其实对于校招生来说稍微有一点超标了。即使你做出来,很多面试官也会认为不是你独立完成的。
其实,你能把一个单体项目做到极致也很好,对于个人能力提升不比做微服务或者分布式项目差。如何做到极致?代码质量这里就不提了,更重要的是你要尽量让自己的项目有一些亮点(比如你是如何提升项目性能的、如何解决项目中存在的一个痛点的),项目经历取得的成果尽量要量化一下比如我使用 xxx 技术解决了 xxx 问题,系统 qps 从 xxx 提高到了 xxx。
跟着老师做的过程中,你一定要有自己的思考,不要浅尝辄止。对于很多知识点,别人的讲解可能只是满足项目就够了,你自己想多点知识的话,对于重要的知识点就要自己学会去深入学习。
实战类开源项目
Github 或者码云上面有很多实战类别项目,你可以选择一个来研究,为了让自己对这个项目更加理解,在理解原有代码的基础上,你可以对原有项目进行改进或者增加功能。
你可以参考 Java 优质开源实战项目 上面推荐的实战类开源项目,质量都很高,项目类型也比较全面,涵盖博客/论坛系统、考试/刷题系统、商城系统、权限管理系统、快速开发脚手架以及各种轮子。
一定要记住: 不光要做,还要改进,改善。不论是实战项目视频或者专栏还是实战类开源项目,都一定会有很多可以完善改进的地方。
从头开始做
自己动手去做一个自己想完成的东西,遇到不会的东西就临时去学,现学现卖。
这个要求比较高,我建议你已经有了一个项目经验之后,再采用这个方法。如果你没有做过项目的话,还是老老实实采用上面两个方法比较好。
参加各种大公司组织的各种大赛
如果参加这种赛事能获奖的话,项目含金量非常高。即使没获奖也没啥,也可以写简历上。
参与实际项目
通常情况下,你有如下途径接触到企业实际项目的开发:
老师接的项目;
自己接的私活;
实习/工作接触到的项目;
老师接的项目和自己接的私活通常都是一些偏业务的项目,很少会涉及到性能优化。这种情况下,你可以考虑对项目进行改进,别怕花时间,某个时间用心做好一件事情就好比如你对项目的数据模型进行改进、引入缓存提高访问速度等等。
实习/工作接触到的项目类似,如果遇到一些偏业务的项目,也是要自己私下对项目进行改进优化。
尽量是真的对项目进行了优化,这本身也是对个人能力的提升。如果你实在是没时间去实践的话,也没关系,吃透这个项目优化手段就好,把一些面试可能会遇到的问题提前准备一下。
我跟着视频做的项目会被面试官嫌弃不?
很多应届生都是跟着视频做的项目,这个大部分面试官都心知肚明。
不排除确实有些面试官不吃这一套,这个也看人。不过我相信大多数面试官都是能理解的,毕竟你在学校的时候实际上是没有什么获得实际项目经验的途径的。
大部分应届生的项目经验都是自己在网上找的或者像你一样买的付费课程跟着做的,极少部分是比较真实的项目。 从你能想着做一个实战项目来说,我觉得初衷是好的,确实也能真正学到东西。 但是,究竟有多少是自己掌握了很重要。看视频最忌讳的是被动接受,自己多改进一下,多思考一下!就算是你跟着视频做的项目,也是可以优化的!
如果你想真正学到东西的话,建议不光要把项目单纯完成跑起来,还要去自己尝试着优化!
简单说几个比较容易的优化点:
- 全局异常处理 :很多项目这方面都做的不是很好,可以参考我的这篇文章:《使用枚举简单封装一个优雅的 Spring Boot 全局异常处理!》 来做优化。
- 项目的技术选型优化 :比如使用 Guava 做本地缓存的地方可以换成 Caffeine 。Caffeine 的各方面的表现要更加好!再比如 Controller 层是否放了太多的业务逻辑。
- 数据库方面 :数据库设计可否优化?索引是否使用使用正确?SQL 语句是否可以优化?是否需要进行读写分离?
- 缓存 :项目有没有哪些数据是经常被访问的?是否引入缓存来提高响应速度?
- 安全 : 项目是否存在安全问题?
- ……
另外,我在星球分享过常见的性能优化方向实践案例,涉及到多线程、异步、索引、缓存等方向,强烈推荐你看看:https://t.zsxq.com/06EqfeMZZ 。
最后,再给大家推荐一个 IDEA 优化代码的小技巧,超级实用!
分析你的代码:右键项目-> Analyze->Inspect Code
扫描完成之后,IDEA 会给出一些可能存在的代码坏味道比如命名问题。
并且,你还可以自定义检查规则。
接触不到高并发场景咋办?如何获得高并发的经验?
不论是应届生还是有几年工作经验的程序员,都可能会面临一个问题:接触不到高并发场景。
不要慌!说实话,能接触到高并发这类业务场景的人还是极少数的。即使在阿里、京东这种公司,你也还是很有可能接触不到。
我在大学那会的时候,自己就比较喜欢捣鼓各种高并发相关的技术了(还是因为太卷,工作之后实际并没有怎么用到,哈哈哈),自己在手动搭建过 Redis 集群、Zookeeeper 集群,动手实现过分库分表、读写分离。
在介绍方法之前,我们首先知道 高并发系统设计的三大目标 :
- 高性能 :系统的处理请求的速度很快,响应时间很短。
- 高可用 :系统几乎可以一直正常提供服务。也就是说系统具备较高的无故障运行的能力。
- 可扩展 :流量高峰时能否在短时间内完成扩容,更平稳地承接峰值流量,比如双 11 活动、明星离婚、明星恋爱等热点事件。
实现高性能的常用手段 :
数据库
- 分库分表&读写分离
- NoSQL
缓存
- 消息队列 (待重构)
- 负载均衡
- 池化技术
- ……
实现高可用的常用手段 :
- 限流
- 降级
- 熔断
- 排队
- 集群
- 超时和重试机制
- 灾备设计
- 异地多活
实现可扩展架构的常用手段:
- 分层架构:面向流程拆分
- SOA、微服务:面向服务拆分
- 微内核架构:面向功能拆分
你可以在 JavaGuide 的在线阅读网站上找到上面这些手段对应的详细讲解。篇幅问题,我这里就不帖原文了。网站地址:https://javaguide.cn/ 。
知道了高并发系统设计相关的概念和手段之后,废话不多说,直接推荐两种我觉得比较靠谱的方法。
第一种方法是自己研究某个技术然后对已有项目进行改进,具体步骤如下:
- 自己研究某个技术比如读写分离。
- 将自己研究的成果应用到自己的项目比如为项目增加读写分离来提高读数据库的速度。
- 想一想项目用了某个技术比如读写分离之后,会不会遇到什么问题,项目的性能到底提升了多少。如果你能模拟一下真实场景就更好了,既能真正学到,又能让自己项目经历更真实。
- 简单复盘总结一下自己对项目所做的完善改进。
这里要说明一点的是:一定不要为了学习实践某个技术而直接用在自己公司的项目上。
技术是为了服务业务的,没必要用的技术就不要用!我之前有个同事天天喜欢在项目上用自己学到的新技术,结果,有一天就出现生产问题了。我现在想着这件事,都想锤他!
你完全可以私下对项目进行改进。 甚至说,你就只是搞懂了这个技术,并没有将其用在真实的项目中。面试官会专门去调查你这个项目么?那大概率是不会。不过,一定要确保自己是真的搞懂了!
我们只是迫于压力,为了找工作而简单润色一下项目经历而已嘛!
第二种方法是你可以跟着视频/教程做一个分布式相关的项目,然后把它吃透,最好还可以对这个项目进行改进,具体步骤如下:
- 跟着视频/教程写一个分布式相关的项目(自己从头开始做也是一样的)。
- 深入研究并搞懂项目涉及到的一些技术。
- 思考有没有可以改进的地方?
- 思考线上环境可能会有一些什么问题出现?该怎么解决?
- 简单复盘总结复盘一下自己从这个项目中学到的什么东西。
我在大学的时候就是通过这种方法获得的高并发相关的经验。我还记得我当时做的那个分布式项目使用到了 Redis 是单机并且消息队列用的是已经淘汰的 ActiveMQ。所以,我就自己搭建了 Redis 集群,模拟各种可能出现的问题。同时,我使用 RocketMQ 替换了 ActiveMQ 并提取封装了消息队列相关的代码。
通过上面这两种方法来获得高并发经验的话,还有一点非常重要: 自己不光要模拟一些生产环境可能会遇到的问题,还要知道这些问题是怎么解决的。 就比如说你用到了 Redis 的话,你自己肯定要私下模拟一下缓存穿透、单机内存不够用、Redis突然宕机等等情况。
基本上,你用到的大部分中间件可能会遇到的问题,你都能够在网上找到对应的案例和解答。多花点时间看看,自己实践一下,研究思考一下。这样的话,在面试中才不会掉链子。
优质 Java 实战项目推荐
业务类开源项目
社区系统
upupor 是一个小众但是功能强大,代码质量也还可以的开源社区,挺适合作为学习的项目。 最主要的是这个项目目前知名度非常非常低,没有项目经历的小伙伴也可以改造升级一下拿来作为自己的项目经历。
技术栈 :
后端:Spring Boot + MySQL + Redis + Undertow(Web 容器)
前端 :Thymeleaf(模板引擎,方便 SEO)+ Bootstrap
相关地址 :
Github 地址:https://github.com/yangrunkang/upupor 。
在线演示:https://upupor.com 。
网站的性能也是不错的:
类似的社区类小众但有两点的项目还有 forest。
不同于其他社区项目,forest 这个知识社区项目主打文章分享,可以自定义专题和作品集。看得出来作者维护比较认真,并且很有想法。根据项目首页介绍,这个项目未来还可能会增加专业知识题库、社区贡献系统、会员系统。
我大概浏览了一下这个项目代码,发现这个项目的代码写的也相对比较规范干净,比很多 star 数量比较多的社区类项目都要好太多!
技术栈 :
- 后端: SpringBoot + Shrio + MyBatis + JWT + Redis
- 前端:Vue + NuxtJS + Element-UI。
相关地址 :
- Github 地址:https://github.com/rymcu 。
- 在线演示:https://rymcu.com/ 。
小说网站
novel-plus 是一个开源的小说网站项目。这个项目的代码质量也是非常不错的,结果清晰,代码结构也比较规范。这是我推荐这个项目很大的一个原因。
- Github 地址:https://github.com/201206030/novel-plus
- Gitee 地址:https://gitee.com/novel_dev_team/novel-plus
另外,除了单体版之外,这个项目还有一个基于 Spring Cloud 的微服务版本供你学习使用。
- GitHub 地址: https://github.com/201206030/novel-cloud
- Gitee 地址: https://gitee.com/novel_dev_team/novel-cloud
技术栈:
- 后端: SpringBoot + MyBatis +Spring Security + Elasticsearch+ 支付宝支付
- 前端:Thymeleaf + Layui。
这个项目还有一个爬虫模块用于系统初期测试使用。对 Java 爬虫感兴趣的朋友,可以简单研究一下。
在线文档管理
document-sharing-site 是一个支持几乎所有类型(Word, Excel, PPT, PDF, Pic 等)的文档存储、在线预览、共享的开源项目。
技术栈 :
- 后端:Spring Boot + Hutool + Tika(内容分析工具包) + Elasticsearch + JWT
- 前端:Vue + axios。
相关地址 :
导航网站
geshanzsq-nav 是一个前后端分离的导航网站。这个项目同样非常小众,撞车的概率非常小,并且,质量也是非常高。
- Github 地址:https://github.com/geshanzsq/geshanzsq-nav
- Gitee 地址 :https://gitee.com/geshanzsq/geshanzsq-nav
技术栈:
- 后端: SpringBoot + MyBatis +Spring Security + Spring Security + Redis + Jwt
- 前端:Thymeleaf + Layui。
在线演示:https://gesdh.cn/ 。
音乐网站
music-website 是一个开源的音乐网站。这个项目的前端写的挺不错的,后端稍微差劲很多,虽然也把功能写出来了,但是很多实现都不太优雅(详见 Controller 层)。
如果你想要将这个项目作为自己的项目经验或者毕业设计的话,可以自行对后端的代码进行优化。
Github 地址:https://github.com/Yin-Hongwei/music-website 。
技术栈:
- 后端 :SpringBoot + MyBatis + MySQL
- 前端 :Vue3.0 + TypeScript + Vue-Router + Vuex + Axios + ElementPlus + Echarts
健身会员管理系统
基于基于 RuoYi-Vue 做的一个健身会员管理系统,实现了 JWT 登录、渠道管理、促销活动等功能,附带详细的教程。
Github 地址:https://github.com/lenve/tienchin
轮子类开源项目
本地缓存
cache 是一个不错的轮子类项目,使用 Java 手写一个类似于 Redis 的单机版本地缓存(附详细教程)。 麻雀虽小五张俱全,支持数据缓存、缓存失效时间、数据淘汰策略(如 FIFO 、 LRU )、RDB 和 AOF 持久化……。 并且,这个项目附带了 6 篇教程来讲解核心功能具体是怎么实现的。
Github 地址:https://github.com/houbb/cache
RPC 框架
guide-rpc-framework 是一款基于 Netty+Kyro+Zookeeper 实现的 RPC 框架。
- Github 地址: https://github.com/Snailclimb/guide-rpc-framework
- Gitee 地址 :https://gitee.com/SnailClimb/guide-rpc-framework
这个项目代码注释详细,结构清晰,并且集成了 Check Style 规范代码结构,非常适合阅读和学习。
并且,这个项目的 README 文档写的也非常认真。从 README 文档中,你就可以大概了解到这个 RPC 框架的设计思路以及前置技术。
数据库
MYDB 是一个 Java 语言实现的简易版数据库,部分原理参照自 MySQL、PostgreSQL 和 SQLite。
麻雀虽小,五脏俱全。MYDB 目前已经实现 MVCC、两种事务隔离级别(读提交和可重复读)、死锁处理、简陋的 SQL 解析等关系型数据库的核心功能。
并且,MYDB 作者写了详细的实现教程,教程地址: https://ziyang.moe/cs/project/mydb/
Github 地址:https://github.com/CN-GuoZiyang/MYDB
编译器
Mini-Compiler 是一个 Mini 版本的入门级编译器,基于 Java 语言编写,有助于初学者了解面向对象编程语言编译器的运行原理。
代码示例:
可以看到,代码注释还是非常清晰的,一共只有 7 个类。
不过,想要搞懂这个项目难度会远大于普通的业务类型项目,像核心类 Parser (语法解析器)的代码量接近有 2000 行(其它 6 个类代码量比较少)。
Github 地址:https://github.com/chenyuwangjs/A-tutorial-compiler-written-in-Java 。
下面是一些相关的学习资料 :
国外公开课 Lab
手写关系型数据库
MIT 6.830/6.814: Database Systems 这门课程的内容非常适合想要深入学习数据库原理的小伙伴。这门课程的 lab 是使用 Java 语言一步一步实现一个关系型数据库。
课程地址:http://db.lcs.mit.edu/6.830/ 。
- 课程代码:https://github.com/MIT-DB-Class/simple-db-hw 。
- 课程讲义:https://github.com/MIT-DB-Class/course-info-2018/ 。
- 课程视频:https://www.youtube.com/playlist?list=PLfciLKR3SgqOxCy1TIXXyfTqKzX2enDjK 。
网络上有一些相关的文章分享:
另外,UCB CS186: Introduction to Database System 的这门课程 lab 也是使用 Java 实现一个关系型数据库。
手写分布式 KV 存储
MIT6.824: Distributed System 这门课程出品自 MIT 大名鼎鼎的 PDOS 实验室,授课老师 Robert Morris 教授。Robert Morris 曾是一位顶尖黑客,世界上第一个蠕虫病毒 Morris 病毒就是出自他之手。
这门课程的 lab 会循序渐进带你实现一个基于 Raft 共识算法的 KV-store 框架,让你在痛苦的 debug 中体会并行与分布式带来的随机性和复杂性。
- 课程网站:https://pdos.csail.mit.edu/6.824/schedule.html 。
- 课程视频(中文翻译):https://www.bilibili.com/video/BV1R7411t71W 。
相关资料:
- MIT6.824: Distributed System(中文翻译 wiki): https://mit-public-courses-cn-translatio.gitbook.io/mit6-824/
- 如何的才能更好地学习 MIT6.824 分布式系统课程?:https://www.zhihu.com/question/29597104
视频类实战项目教程
大家有没有比较好的实战项目视频分享推荐下?慕课网上面的实战课程虽然多,但是,说实话哈,有一些质量都不过关,价格也不便宜。求球友分享优质的实战项目视频教程。
项目经验常见问题解答(补充)
你在项目经历中不涉及的知识点,但在技能介绍中提到的知识点也很大概率会被问到。像 Redis 这种基本是面试 Java 后端岗位必备的技能,我觉得大部分面试官应该都会问。
一个项目的话不是不可以,但你一定要保证这个项目不能太鸡肋。如果太鸡肋的话,简历关可能就直接 pass,而且面试官提问都不好提。
如果你没有项目经验的话,建议你尽量一边学习各种框架和中间件一边做一个完整且有一些亮点的项目作为自己的项目经验。
什么项目算是有亮点的或者是面试官认为有价值的?
最有价值的当然是你参加各种大公司组织的各种大赛(比如阿里的天池软件设计大赛)而做的项目,如果参加这种赛事能获奖的话,项目含金量非常高。即使没获奖也没啥,也可以写简历上。
跟着老师或者普通公司做的项目的话,一般都是面向企业级别,一般很少会用到分布式/微服务,基本都是单机,这种项目的含金量稍低,即使你的业务很复杂。遇到这种情况可以考虑说自己去对项目进行改进,别怕花时间,某个时间用心做好一件事情就好比如你对项目的数据模型进行改进、引入缓存提高访问速度等等。
自己做的项目的话,我觉得一定要:尽量和别人避开,别网上流传一个项目,然后自己名字不改,啥也不做就写简历上了。
项目太简单怎么办?
项目太简单的话,不光是影响简历通过的概率,还会影响到你的面试准备,毕竟面试中的重点就是项目经历涉及到的知识点,如果你的项目经历比较简单的话,面试官直接不知道问啥了。个人建议你可以参考《Java 面试指北》中对应的文章对项目经历进行完善改进!
另外,你还要保证自己的项目的不是烂大街那种(比如商城、博客……),自己参加比赛做的项目或者是企业真实项目是比较好的。
如何优化项目经历性价比更高?
面试之前,你可以跟着网上的教程,从性能优化方向入手去改进一下自己的项目。为什么建议从性能优化方向入手呢?因为性能优化方向改进相比较于业务方向的改进性价比会更高,更容易体现在简历上。并且,更重要的是,性能优化方向更容易在面试之前提前准备,面试官也更喜欢提问这类问题。
你项目没有用到的性能优化手段,只要你搞懂吃透并且觉得合理,你就完全可以写在简历上。不过,建议你还是要实践一下,压测一波,取得的成果也要量化一下比如我使用 xxx 技术解决了 xxx 问题,系统 qps 从 xxx 提高到了 xxx。
必须是微服务项目才有亮点?
个人认为也没必要非要去做微服务或者分布式项目,不一定对你面试有利。微服务或者分布式项目涉及的知识点太多,一般人很难吃透。并且,这类项目其实对于校招生来说稍微有一点超标了。即使你做出来,很多面试官也会认为不是你独立完成的。
其实,你能把一个单体项目做到极致也很好,对于个人能力提升不比做微服务或者分布式项目差。如何做到极致?代码质量这里就不提了,更重要的是你要尽量让自己的项目有一些亮点(比如你是如何提升项目性能的、如何解决项目中存在的一个痛点的),项目经历取得的成果尽量要量化一下比如我使用 xxx 技术解决了 xxx 问题,系统 qps 从 xxx 提高到了 xxx。
能不能推荐一些一些优质线上问题/系统性能优化案例?
请看这个帖子:https://t.zsxq.com/iYZNBmA 。
如何准备项目经历?
你可以从下面几个方面来准备项目的回答(欢迎大家补充):
- 你对项目整体设计的一个感受(面试官可能会让你画系统的架构图)
- 你在这个项目中你负责了什么、做了什么、担任了什么角色。
- 从这个项目中你学会了那些东西,使用到了那些技术,学会了那些新技术的使用。
- 你在这个项目中是否解决过什么问题?怎么解决的?收获了什么?
- 你的项目用到了哪些技术?这些技术你吃透了没有?举个例子,你的项目经历使用了 Seata 来做分布式事务,那 Seata 相关的问题你要提前准备一下吧,比如说 Seata 支持哪些配置中心、Seata 的事务分组是怎么做的、Seata 支持哪些事务模式,怎么选择?
- 你在这个项目中犯过的错误,最后是怎么弥补的?
相关帖子:面试的时候怎么准备项目的回答
去外包对自己的简历有影响么?
有很多初入职场找工作的小伙伴,由于没有什么经验,自己的能力也一般,加上竞争压力太大。导致自己都是基本收到都是各种外包机构的面试邀请,最后,没办法就去了外包公司。
然后,很多小伙伴就会担心自己的外包工作经历会对自己的简历产生影响。
比如下面这个就是一位星球的匿名用户的提问。
其实,去外包对简历的影响,主要还是看你去的公司和经历的项目,比如你在 ThoughtWorks 做外包的话我觉得对你的简历就没啥影响,甚至还可能是加分项。我的很多同事跳槽,都是去了字节、阿里这些大公司。
另外,去了外包之后以后只能混外包这种说法有点自欺欺人。
首先,外包的技术深度确实不比大公司,这点没办法,根本属性决定了。然后,外包公司一般会让你会很多东西,什么东西都想让你了解一下,这可能会导致你没有一门比较精通的技术。你是Java程序员,下个项目需要你是IOS开发,然后你就要自己学,这个还是很坑的。
不过,刚毕业的话在外包干两年还是能学到一些东西的,因为你在外包公司的话,大概率会经历大量的实战项目。。
其实,最重要的是,自己平时要注意多多思考和学习,勿要浮于表面就好了。
很多人抱怨抱怨公司工作强度很多大,就我来看,很多外包公司的工作强度甚至比不上甲方的程序员。
按照大众的话来说,最好是不要去外包公司,这点是没啥问题的。不过, 如果说你目前正在外包公司工作或者你只能找到外包工作的话,不要一味抱怨,只要自己能学到东西就好!
下面是一些读者投稿的真实外包工作经历,感兴趣的小伙伴可以看看:
- 入职外包一个月的感受!(后续:外包一年,拿到某金融上市公司和某鸟的 offer)
- 二本本科,银行外包开发工作 4 个月有余。聊聊外包公司工作的一些真实感受!
- 我在华为外包一年的经历分享。
- 我,法律专业,转行程序员。聊聊我的外包工作经历
- 我在蚂蚁外包的这段时光
- 朋友被裁员之后的工行、华为外包工作经历分享
- 我在国企外包一年的经历和感受
- 转行了!文科生转程序员的外包工作经历分享
- 请不要“妖魔化”外包。
如果面试官问“你有什么问题问我吗?”,该如何回答?
我还记得当时我去参加面试的时候,几乎每一场面试,特别是 HR 面和高管面的时候,面试官总是会在结尾问我:“问了你这么多问题了,你有什么问题问我吗?”。
短暂地思考之后,我的内心会陷入短暂的纠结中:我该问吗?不问的话面试官会不会对我影响不好?问什么问题?问这个问题会不会让面试官对我的影响不好啊?
面试是一个双向选择的过程
就技术面试而言,回答这个问题的时候,只要你不是触碰到你所面试的公司的雷区,那么我觉得这对你能不能拿到最终 offer 来说影响确实是不大的。
但是,我建议还是可以好好回答这个问题。
面试本身就是一个双向选择的过程,你对这个问题的回答也会侧面反映出你对这次面试的上心程度,你的问题是否有价值,也影响了你最终的选择与公司是否选择你。
面试官在技术面试中主要考察的还是你这样个人到底有没有胜任这个工作的能力以及你是否适合公司未来的发展需要,很多公司还需要你认同它的文化,我觉得你只要不是太笨,应该不会栽在这里。除非你和另外一个人在能力上相同,但是只能在你们两个人中选一个,那么这个问题才对你能不能拿到 offer 至关重要。有准备总比没准备好,给面试官留一个好的影响总归是没错的。
但是,就非技术面试来说,我觉得好好回答这个问题对你最终的结果还是比较重要的。
总的来说,不管是技术面试还是非技术面试,如果你想赢得公司的青睐和尊重,我觉得我们都应该重视这个问题。
不要问太 Low 的问题
回答这个问题很重要的一点就是你没有必要放低自己的姿态问一些很虚或者故意讨好面试官的问题,也不要把自己从面经上学到的东西照搬下来使用。
面试官也不是傻子,特别是那种特别有经验的面试官,你是真心诚意的问问题,还是从别处照搬问题来讨好面试官,人家可能一听就听出来了。
总的来说,还是要真诚。
除此之外,不要问太 Low 的问题,会显得你整个人格局比较小或者说你根本没有准备(侧面反映你对这家公司不上心,既然你自己都不上心,公司为什么要录用你呢?)。举例几个比较 Low 的问题,大家看看自己有没有问过其中的问题:
- 贵公司的主要业务是什么?(面试之前自己不知道提前网上查一下吗?)
- 贵公司的男女比例如何?(考虑脱单?记住你是来工作的!)
- 贵公司一年搞几次外出旅游?(你是来工作的,这些娱乐活动先别放在心上!)
- ……
有哪些有价值的问题值得问?
针对这个问题。笔主专门找了几个专门做 HR 工作的小哥哥小姐姐们询问并且查阅了挺多前辈们的回答,然后结合自己的实际经历,我概括了下面几个比较适合问的问题。大家在面试的时候,可以根据自己对于公司或者岗位的了解程度,对下面提到的问题进行适当修饰或者修改。
另外,这些问题只是给没有经验的朋友一个参考,如果你还有其他比较好的问题的话,那当然也更好啦!
面对 HR 或者其他 Level 比较低的面试官时
- 能不能谈谈你作为一个公司老员工对公司的感受? (这个问题比较容易回答,不会让面试官陷入无话可说的尴尬境地。另外,从面试官的回答中你可以加深对这个公司的了解,让你更加清楚这个公司到底是不是你想的那样或者说你是否能适应这个公司的文化。除此之外,这样的问题在某种程度上还可以拉进你与面试官的距离。)
- 能不能问一下,你当时因为什么原因选择加入这家公司的呢或者说这家公司有哪些地方吸引你?有什么地方你觉得还不太好或者可以继续完善吗? (类似第一个问题,都是问面试官个人对于公司的看法,)
- 我觉得我这次表现的不是太好,你有什么建议或者评价给我吗?(这个是我常问的。我觉得说自己表现不好只是这个语境需要这样来说,这样可以显的你比较谦虚好学上进。)
- 接下来我会有一段空档期,有什么值得注意或者建议学习的吗? (体现出你对工作比较上心,自助学习意识比较强。)
- 这个岗位为什么还在招人? (岗位真实性和价值咨询)
- 大概什么时候能给我回复呢? (终面的时候,如果面试官没有说的话,可以问一下)
- ……
面对部门领导
- 部门的主要人员分配以及对应的主要工作能简单介绍一下吗?
- 未来如果我要加入这个团队,你对我的期望是什么? (部门领导一般情况下是你的直属上级了,你以后和他打交道的机会应该是最多的。你问这个问题,会让他感觉你是一个对他的部门比较上心,比较有团体意识,并且愿意倾听的候选人。)
- 公司对新入职的员工的培养机制是什么样的呢? (正规的公司一般都有培养机制,提前问一下是对你自己的负责也会显的你比较上心)
- 以您来看,这个岗位未来在公司内部的发展如何? (在我看来,问这个问题也是对你自己的负责吧,谁不想发展前景更好的岗位呢?)
- 团队现在面临的最大挑战是什么? (这样的问题不会暴露你对公司的不了解,并且也能让你对未来工作的挑战或困难有一个提前的预期。)
面对 Level 比较高的(比如总裁,老板)
- 贵公司的发展目标和方向是什么? (看下公司的发展是否满足自己的期望)
- 与同行业的竞争者相比,贵公司的核心竞争优势在什么地方? (充分了解自己的优势和劣势)
- 公司现在面临的最大挑战是什么?
Java 优质面试视频推荐
文字看累了,还可以看看视频!推荐几个不错的 Java 面试相关的视频。
1、中华石杉老师的《Java 面试突击第一季》
即使是19年那会出来的视频,放到现在依然是适用的!对于想要进 Java 生态为主的公司比如美团、阿里非常有帮助!主要讲的是高并发高可用相关的内容。
地址:https://www.bilibili.com/video/BV1B4411h7Nz
这份资料对应的笔记:https://doocs.github.io/advanced-java/#/
2、图灵学院的《Java 常见面试题详解系列》
涵盖 Java 核心知识、数据库以及常见框架,拿数据库和缓存来说:数据库以面试常问的 MySQL 为例介绍了索引、锁、事务、主从同步、MyISAM 和 InnoDB 的区别、分库分表、慢查询处理等面试题。缓存以面试常问的 Redis 为例介绍了 Redis 常见数据库结构、缓存过期策略、 缓存穿透、缓存击穿、缓存雪崩、数据库和缓存一致性保证、Redis 高可用等面试题。
地址:https://www.bilibili.com/video/BV1XU4y1J7Dr
3、尚硅谷周阳老师的 《Java 面试题第三季》
Java 培训领域比较出名的周阳老师的作品,内容涵盖算法、Java 核心知识、数据库以及常见框架。
可以重点看看并发和 Spring 这块,比其他老师讲的要深入和好理解很多。
地址:https://www.bilibili.com/video/BV1Hy4y1B78T
前两季视频地址 :
- Java 面试题第一季 :https://www.bilibili.com/video/BV1Eb411P7bP
- Java 面试题第二季 :https://www.bilibili.com/video/BV18b411M7xz
4、图灵的《分布式面试核心面试题系列》
主要是分布式相关的内容,涵盖负载均衡、分布式 ID、分布式事务、Dubbo、Zookeeper 、Redis。
地址:https://www.bilibili.com/video/BV1Mz4y1Z7bM
5、享学的《Java 面试全解析系列》
内容比较杂,可以挑选自己比较感兴趣面试题学习。可以重点看看数据库这块,对于常见的 MySQL 面试题比如 MySQL 索引数据结构介绍的比较详细。
地址:https://www.bilibili.com/video/BV1yA411u7WL
二、技术面试题篇
系统设计
如何准备系统设计面试
系统设计在面试中一定是最让面试者头疼的事情之一。因为系统设计相关的问题通常是开放式的,所以没有标准答案。你在和面试官思想的交流碰撞中会慢慢优化自己的系统设计方案。理论上来说,系统设计面试官一起一步一步改进原有系统设计方案的过程。
系统设计题往往也非常能考察岀面试者的综合能力,回答好的话,很易就能在面试中脱颖而岀。不论是对于参加社招还是校招的小伙伴,都很有必要重视起来接下来。
我会带着小伙伴们从我的角度出发来谈谈:如何准备面试中的系统设计
由于文章篇幅有限,就不列举实际例子了,可能会在后面的文章中单独提一些具体的例子
系统设计面试一般怎么问
我简单总下系统设计面试相关问题的问法个
- 设计某某系统比如秒杀系统、微博系统、抢红包系统、短网址系统
- 设计某某系统中一个功能比如哔哩哔哩点赞功能
- 设计一个框架比如RPC框架、消息队列、缓存框架、分布式文件系统等等
- 某某系统的技术选型比如缓存用
Redis
还是Memcached
、网关用Spring Cloud Gateway
还是Netflix Zuul2
系统设计怎么做?
我们将步骤总结成了以下 4 步。
Step1:问清楚系统具体要求
当面试官给出了系统设计题目之后,一定不要立即开始设计解决方案。 你需要先理解系统设计的需求:功能性需求和非功能性需求。
为了避免自己曲解题目所想要解决的问题,你可以先简要地给面试官说说自己的理解,
为啥要询问清楚系统的功能性需求也就是说系统包含哪些功能呢?
毕竟,如果面试官冷不丁地直接让你设计一个微博系统,你不可能把微博系统涵盖的功能比如推荐信息流、会员机制等一个一个都列举出来,然后再去设计吧!你需要筛选出系统所提供的核心功能(缩小边界范围)!
为啥要询问清楚系统的非功能性需求或者说约束条件比如系统需要达到多少QPS呢?
让你设计一个1w人用的微博系统和100w人用的微博系统能一样么?不同的约束系统对应的系统设计方案肯定是不一样的。
Step2:对系统进行抽象设计
我们需要在一个 High Level 的层面对系统进行设计。
你可以画出系统的抽象架构图,这个抽象架构图中包含了系统的一些组件以及这些组件之间的连接。
Step3:考虑系统目前需要优化的点
对系统进行抽象设计之后,你需要思考当前抽象的系统设计有哪些需要优化的点,比如说:
- 当前系统部署在一台机器够吗?是否需要部署在多台机器然后进行负载均衡呢?
- 数据库处理速度能否支撑业务需求?是否需要给指定字段加索引?是否需要读写分离?是否需要缓存?
- 数据量是否大到需要分库分表?
- 是否存在安全隐患?
- 系统是否需要分布式文件系统?
- ……
Step4:优化你的系统抽象设计
根据 Step 3 中的“系统需要优化的点” 对系统的抽象设计做进一步完善。
系统设计该如何准备?
知识储备
系统设计面试非常考察你的知识储备,系统设计能力的提高需要大量的理论知识储备。比如说你要知道大型网站架构设计必备的三板斧:
1.高性能架构设计: 熟悉系统常见性能优化手段比如引入 读写分离、缓存、负载均衡、异步 等等。
2.高可用架构设计 :CAP理论和BASE理论、通过集群来提高系统整体稳定性、超时和重试机制、应对接口级故障:降级、熔断、限流、排队。
3.高扩展架构设计 :说白了就是懂得如何拆分系统。你按照不同的思路来拆分软件系统,就会得到不同的架构。
实战
虽然懂得了理论,但是自己没有进行实践的话,很多东西是无法体会到的!
因此,你还要不断通过实战项目锻炼自己的系统设计能力。
保持好奇心
多思考自己经常浏览的网站是怎么做的。比如:
1你刷微博的时候可以思考一下微博是如何记录点赞数量的?
2你看哔哩哔哩的时候可以思考一下消息提醒系统是如何做的?
3你使用短链系统的时候可以考虑一下短链系统是如何做的?
4……
技术选型
实现同样的功能,一般会有多种技术选择方案,比如缓存用Redis 还是 Memcached
、网关用Spring Cloud Gateway
还是Netflix Zuul2
。 很多时候,面试官在系统设计面过程中会具体到技术的选型,因而,你需要区分不同技术的优缺点。
系统设计面试必知
系统设计的时候必然离不开描述性能相关的指标比如 QPS。
性能相关的指标
响应时间
响应时间RT(Response-time)就是用户发出请求到用户收到系统处理结果所需要的时间。
RT是一个非常重要且直观的指标,RT数值大小直接反应了系统处理用户请求速度的快慢。
并发数
并发数可以简单理解为系统能够同时供多少人访问使用也就是说系统同时能处理的请求数量。
并发数反应了系统的负载能力。
QPS 和 TPS
QPS(Query Per Second) :服务器每秒可以执行的查询次数;
TPS(Transaction Per Second) :服务器每秒处理的事务数(这里的一个事务可以理解为客户发出请求到收到服务器的过程);
书中是这样描述 QPS 和 TPS 的区别的。
QPS vs TPS:QPS 基本类似于 TPS,但是不同的是,对于一个页面的一次访问,形成一个TPS;但一次页面请求,可能产生多次对服务器的请求,服务器对这些请求,就可计入“QPS”之中。如,访问一个页面会请求服务器2次,一次访问,产生一个“T”,产生2个“Q”。
吞吐量
吞吐量指的是系统单位时间内系统处理的请求数量。
一个系统的吞吐量与请求对系统的资源消耗等紧密关联。请求对系统资源消耗越多,系统吞吐能力越低,反之则越高。
TPS、QPS都是吞吐量的常用量化指标。
QPS(TPS) = 并发数/平均响应时间(RT)
并发数 = QPS * 平均响应时间(RT)
系统活跃度
介绍几个描述系统活跃度的常见名词,建议牢牢记住。你不光会在回答系统设计面试题的时候碰到,日常工作中你也会经常碰到这些名词。
PV(Page View)
访问量, 即页面浏览量或点击量,衡量网站用户访问的网页数量;在一定统计周期内用户每打开或刷新一个页面就记录1次,多次打开或刷新同一页面则浏览量累计。UV 从网页打开的数量/刷新的次数的角度来统计的。
UV(Unique Visitor)
独立访客,统计1天内访问某站点的用户数。1天内相同访客多次访问网站,只计算为1个独立访客。UV 是从用户个体的角度来统计的。
DAU(Daily Active User)
日活跃用户数量。
MAU(monthly active users)
月活跃用户人数。
举例:某网站 DAU为 1200w, 用户日均使用时长 1 小时,RT为0.5s,求并发量和QPS。
平均并发量 = DAU(1200w)* 日均使用时长(1 小时,3600秒) /一天的秒数(86400)=1200w/24 = 50w
真实并发量(考虑到某些时间段使用人数比较少) = DAU(1200w)* 日均使用时长(1 小时,3600秒) /一天的秒数-访问量比较小的时间段假设为8小时(57600)=1200w/16 = 75w
峰值并发量 = 平均并发量 * 6 = 300w
QPS = 真实并发量/RT = 75W/0.5=150w/s
常用性能测试工具
后端常用
既然系统设计涉及到系统性能方面的问题,那在面试的时候,面试官就很可能会问:你是如何进行性能测试的?
推荐 4 个比较常用的性能测试工具:
Jmeter :Apache JMeter 是 JAVA 开发的性能测试工具。
LoadRunner:一款商业的性能测试工具。
Galtling :一款基于Scala 开发的高性能服务器性能测试工具。
ab :全称为 Apache Bench 。Apache 旗下的一款测试工具,非常实用。
没记错的话,除了 LoadRunner 其他几款性能测试工具都是开源免费的。
前端常用
Fiddler:抓包工具,它可以修改请求的数据,甚至可以修改服务器返回的数据,功能非常强大,是Web 调试的利器。
HttpWatch: 可用于录制HTTP请求信息的工具。
常见软件的QPS
这里给出的 QPS 仅供参考,实际项目需要进行压测来计算。
Nginx :一般情况下,系统的性能瓶颈基本不会是 Nginx。单机 Nginx 可以达到 30w +。
Redis: Redis 官方的性能测试报告:https://redis.io/topics/benchmarks 。从报告中,我们可以得出 Redis 的单机 QPS 可以达到 8w+(CPU性能有关系,也和执行的命令也有关系比如执行 SET 命令甚至可以达到10w+QPS)。
MySQL: MySQL 单机的 QPS 为 大概在 4k 左右。
Tomcat :单机 Tomcat 的QPS 在 2w左右。这个和你的 Tomcat 配置有很大关系,举个例子Tomcat 支持的连接器有 NIO、NIO.2 和 APR。 AprEndpoint 是通过 JNI 调用 APR 本地库而实现非阻塞 I/O 的,性能更好,Tomcat 配置 APR 为 连接器的话,QPS 可以达到 3w左右。更多相关内容可以自行搜索 Tomcat 性能优化。
系统设计原则
合适优于先进 > 演化优于一步到位 > 简单优于复杂
常见的性能优化策略
性能优化之前我们需要对请求经历的各个环节进行分析,排查出可能出现性能瓶颈的地方,定位问题。
下面是一些性能优化时,我经常拿来自问的一些问题:
当前系统的SQL语句是否存在问题?
当前系统是否需要升级硬件?
系统是否需要缓存?
系统架构本身是不是就有问题?
系统是否存在死锁的地方?
数据库索引使用是否合理?
系统是否存在内存泄漏?(Java 的自动回收内存虽然很方便,但是,有时候代码写的不好真的会造成内存泄漏)
系统的耗时操作进行了异步处理?
……
性能优化必知法则
SQL优化,JVM、DB,Tomcat参数调优 > 硬件性能优化(内存升级、CPU核心数增加、机械硬盘—>固态硬盘等等)> 业务逻辑优化/缓存 > 读写分离、集群等 > 分库分表
系统设计面试的注意事项
想好再说
没必要面试官刚问了问题之后,你没准备好就开始回答。这样不会给面试官带来好印象的!系统设计本就需要面试者结合自己的以往的经验进行思考,这个过程是需要花费一些时间的。
没有绝对的答案
系统设计没有标准答案。重要的是你和面试官一起交流的过程。
一般情况下,你会在和面试官的交流过程中,一步一步完成系统设计。这个过程中,你会在面试官的引导下不断完善自己的系统设计方案。
因此,你不必要在系统设计面试之前找很多题目,然后只是单纯记住他们的答案。
勿要绝对
系统设计没有最好的设计方案,只有最合适的设计方案。这就类比架构设计了:软件开发没有银弹,架构设计的目的就是选择合适的解决方案。 何为银弹? 狼人传说中,只有银弹(银质子弹)才能制服这些猛兽。对应到软件开发活动中,银弹特指开发者们寻求的一种克服软件开发这个难缠的猛兽的“万能钥匙🔑”。
权衡利弊
知道使用某个技术可能会为系统带来的利弊。比如使用消息队列的好处是解耦和削峰,但是,同样也让系统可用性降低、复杂性提高,同时还会存在一致性问题(消息丢失或者消息未被消费咋办)。
慢慢优化
刚开始设计的系统不需要太完美,可以慢慢优化。
不追新技术
使用稳定的、适合业务的技术,不必要过于追求新技术。
追简避杂
系统设计应当追求简单避免复杂。KISS( Keep It Simple, Stupid)原则——保持简单,易于理解。
总结
这篇文章简单带着小伙伴们分析了一下系统设计面试。如果你还想要深入学习的话,可以参考: https://github.com/donnemartin/system-design-primer 。
如何设计一个秒杀系统
大家好,我是 Guide哥!
今天这篇文章咱们就开始从后端的角度来谈谈:“如何设计秒杀系统?”。
在你看这篇文章之前,我想说的是系统设计没有一个标准答案,你需要结合自己的过往经验来回答,我这篇文章也是简单说说自己的看法。
下面是正文!
设计秒杀系统之前,我们首先需要对秒杀系统有一个清晰的认识。
秒杀系统主要为商品(往往是爆款商品)秒杀活动提供支持,这个秒杀活动会限制商品的个数以及秒杀持续时间。
为什么秒杀系统的设计是一个难点呢? 是因为它的业务复杂么? 当然不是!
秒杀系统的业务逻辑非常简单,一般就是下订单减库存,难点在于我们如何保障秒杀能够顺利进行。
- 秒杀开始的时候,会有大量用户同时参与进来,因此秒杀系统一定要满足 高并发 和 高性能 。
- 为了保证秒杀整个流程的顺利进行,整个秒杀系统必须要满足 高可用 。
- 除此之外,由于商品的库存有限,在面对大量订单的情况下,一定不能超卖,我们还需要保证 一致性 。
很多小伙伴可能不太了解当代三高互联网架构:高并发、高性能、高可用。
我这里简单解释一下:高并发简单来说就是能够同时处理很多用户请求。高性能简单来说就是处理用户的请求速度要快。高可用简单来说就是我们的系统要在趋近 100% 的时间内都能正确提供服务。
知道了秒杀系统的特点之后,我们站在技术层面来思考一下:“设计秒杀系统的过程中需要重点关注哪些问题”。
- 参与秒杀的商品属于热点数据,我们该如何处理热点数据?
- 商品的库存有限,在面对大量订单的情况下,如何解决超卖的问题?
- 如果系统用了消息队列,如何保证消息队列不丢失消息?
- 如何保证秒杀系统的高可用?
- 如何对项目进行压测?有哪些工具?
- ……
好的,废话不多说!正式开始!
高并发&高性能
热点数据处理
何为热点数据? 热点数据指的就是某一时间段内被大量访问的数据,比如爆款商品的数据、新闻热点。
为什么要关注热点数据? 热点数据可能仅仅占据系统所有数据的 0.1% ,但是其访问量可能是比其他所有数据之和还要多。不重点处理热点数据,势必会给系统资源消耗带来严峻的挑战。
热点数据的分类? 根据热点数据的特点,我们通常将其分为两类:
静态热点数据 :可以提前预测到的热点数据比如要秒杀的商品。
动态热点数据 : 不能够提前预测到的热点数据,需要通过一些手段动态检测系统运行情况产生。
另外,处理热点数据的问题的关键就在于 我们如何找到这些热点数据(或者说热 key),然后将它们存在 jvm 内存里。 对于并发量非常一般的系统直接将热点数据存放进缓存比如 Redis 中就可以了,不过像淘宝、京东这种级别的并发量,如果把某些热点数据放在 Redis 中,直接可能就将整个 Redis 集群给干掉了。
如何检测热点数据?
我了解到的是市面上也有一些类似的中间件,比如京东零售的 hotkey 就是一款专门用于检测热点数据的中间件,它可以毫秒级探测热点数据,毫秒级推送至服务器集群内存。相关阅读:京东毫秒级热 key 探测框架设计与实践,已完美支撑 618 大促 。
另外,我们平时使用 Redis 做缓存比较多,关于如何快速定位 Redis 热点数据,可以看下如何快速定位 Redis 热 key这篇文章。
如何处理热点数据? 热点数据一定要放在缓存中,并且最好可以写入到 jvm 内存一份(多级缓存),并设置个过期时间。需要注意写入到 jvm 的热点数据不宜过多,避免内存占用过大,一定要设置到淘汰策略。
为什么还要放在 jvm 内存一份? 因为放在 jvm 内存中的数据访问速度是最快的,不存在什么网络开销。
流量削峰
消息队列
秒杀开始之后的流量不是很大,我处理不了嘛!那我就先把这些请求放到消息队列中去。然后,咱后端服务再慢慢根据自己的能力去消费这些消息,这样就避免直接把后端服务打垮掉。
消息队列是一种非常实用的流量削峰手段。只要是涉及到流量削峰,那必然不可缺少消息队列。
回答问题/验证码
我们可以在用户发起秒杀请求之前让其进行答题或者输入验证码。
这种方式一方面可以避免用户请求过于集中,另一方面可以有效解决用户使用脚本作弊。
回答问题/验证码这一步建议除了对答案的正确性做校验,还需要对用户的提交时间做校验,比如提交时间过短(<1s)的话,大概就是使用脚本来处理的。
高可用
集群化
如果我们想要保证系统中某一个组件的高可用,往往需要搭建集群来避免单点风险,比如说 Nginx 集群、Kafka 集群、Redis 集群。
我们拿 Redis 来举例说明。如果我们需要保证 Redis 高可用的话,该怎么做呢?
你直接通过 Redis replication(异步复制) 搞个一主(master)多从(slave)来提高可用性和读吞吐量,slave 的多少取决于你的读吞吐量。
这样的方式有一个问题:一旦 master 宕机,slave 晋升成 master,同时需要修改应用方的主节点地址,还需要命令所有从节点去复制新的主节点,整个过程需要人工干预。
不过,这个问题我们可以通过 Sentinel(哨兵) 来解决。Redis Sentinel 是 Redis 官方推荐的高可用性(HA)解决方案。
Sentinel 是 Redis 的一种运行模式 ,它主要的作用就是对 Redis 运行节点进行监控。当 master 节点出现故障的时候, Sentinel 会帮助我们实现故障转移,确保整个 Redis 系统的可用性。整个过程完全自动,不需要人工介入!
Sentinel 也是一个 Redis 进程,只是不对外提供读写服务,通常哨兵要配置成单数。
限流
限流是从用户访问压力的角度来考虑如何应对系统故障。限流为了对服务端的接口接受请求的频率进行限制,防止服务挂掉。
🌰 举个例子:我们的秒杀接口一秒只能处理 10w 个请求,结果秒杀活动刚开始一下子来了 15w 个请求。这肯定不行啊!我们只能通过限流把 5w 个请求给拦截住,不然系统直接就给整挂掉了!
限流的话可以直接用 Redis 来做(建议基于 Lua 脚本),也可以使用现成的流量控制组件比如 Sentinel 、Hystrix 、Resilience4J 。
Hystrix 是 Netflix 开源的熔断降级组件。
Sentinel 是阿里巴巴体提供的面向分布式服务架构的流量控制组件,经历了淘宝近10年双11(11.11)购物节的所有核心场景(比如秒杀活动)的考验。
Sentinel 主要以流量为切入点,提供 流量控制、熔断降级、系统自适应保护等功能来保护系统的稳定性和可用性。
个人比较建议使用 Sentinel ,更新维护频率更高,功能更强大,并且生态也更丰富(Sentinel 提供与 Spring Cloud、Dubbo 和 gRPC 等常用框架和库的开箱即用集成, Sentinel 未来还会对更多常用框架进行适配,并且会为 Service Mesh 提供集群流量防护的能力)。
排队
你可以把排队看作是限流的一个变种。限流是直接拒绝了用户的请求,而排队则是让用户等待一定的时间(类比现实世界的排队)。
排队虽然没有直接拒绝用户,但用户等了很长时间后进入系统,体验并不一定比限流好。
🌰 举个例子:当请求量达到一个阈值的时候,我们就通知用户让它们排队。等到系统可以继续处理请求之后,再慢慢来处理。
降级
降级是从系统功能优先级的角度考虑如何应对系统故障。
服务降级指的是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行。降级的核心思想就是丢车保帅,优先保证核心业务。例
🌰 举个例子:当请求量达到一个阈值的时候,我们对系统中一些非核心的功能直接关闭或者让它们功能降低。这样的话,系统就有更多的资源留给秒杀功能了!
熔断
熔断和降级是两个比较容易混淆的概念,两者的含义并不相同。降级的目的在于应对系统自身的故障,而熔断的目的在于应对当前系统依赖的外部系统或者第三方系统的故障。
熔断可以防止因为秒杀交易影响到其他正常服务的提供
🌰 举个例子: 秒杀功能位于服务 A 上,服务 A 上同时还有其他的一些功能比如商品管理。如果服务 A 上的商品管理接口响应非常慢的话,其他服务直接不再请求服务 A 上的商品管理这个接口,从而有效避免其他服务被拖慢甚至拖死。
一致性
减库存方案
常见的减库存方案有:
下单即减库存 :只要用户下单了,即使不付款,我们就扣库存。
付款再减库存 :当用户付款了之后,我们在减库存。不过, 这种情况可能会造成用户下订单成功,但是付款失败。
一般情况下都是 下单减扣库存 ,像现在的购物网站比如京东都是这样来做的。
不过,我们还会对业务逻辑做进一步优化,比如说对超过一定时间不付款的订单特殊处理,释放库存。
对应到代码层面,我们应该如何保证不会超卖呢?
我们上面也说,我们一般会提前将秒杀商品的信息放到缓存中去。我们可以通过 Redis 对库存进行原子操作。伪代码如下:
1 | // 第一步:先检查 库存是否充足 |
你也可以通过 Lua 脚本来减少多个命令的网络开销并保证多个命令整体的原子性。伪代码如下:
1 | -- 第一步:先检查 库存是否充足,库存不足,返回0 |
接口幂等
什么是幂等呢? 在分布式系统中,幂等(idempotency)是对请求操作结果的一个描述,这个描述就是不论执行多少次相同的请求,产生的效果和返回的结果都和发出单个请求是一样的。
🌰 举个例子:假如咱们的前后端没有保证接口幂等性,我作为用户在秒杀商品的时候,我同时点击了多次秒杀商品按钮,后端处理了多次相同的订单请求,结果导致一个人秒杀了多个商品。这个肯定是不能出现的,属于非常严重的 bug 了!
保证分布式接口的幂等性对于数据的一致性至关重要,特别是像支付这种涉及到钱的接口。保证幂等性这个操作并不是说前端做了就可以的,后端同样要做。
前端保证幂等性的话比较简单,一般通过当用户提交请求后将按钮致灰来做到。后端保证幂等性就稍微麻烦一点,方法也是有很多种,比如:
同步锁;
分布式锁;
业务字段的唯一索性约束,防止重复数据产生。
……
拿分布式锁来说,我们通过加锁的方式限制用户在第一次请求未结束之前,无法进行第二次请求。
分布式锁一般基于 Redis 来做比较多一些,这也是我比较推荐的一种方式。另外,如果使用 Redis 来实现分布式锁的话,比较推荐基于 Redisson。相关阅读:分布式锁中的王者方案 - Redisson 。
1 | // 1.设置分布式锁 |
当然了,除了 Redis 之外,像 ZooKeeper 等中间也可以拿来做分布式锁。
性能测试
上线之前压力测试是必不可少的。推荐 4 个比较常用的性能测试工具:
- Jmeter :Apache JMeter 是 JAVA 开发的性能测试工具。
- LoadRunner:一款商业的性能测试工具。
- Galtling :一款基于 Scala 开发的高性能服务器性能测试工具。
- ab :全称为 Apache Bench 。Apache 旗下的一款测试工具,非常实用。
没记错的话,除了 LoadRunner 其他几款性能测试工具都是开源免费的。
总结
我简单画了一张图来总结一下上面涉及到的一些技术。
另外,上面涉及到知识点还蛮多的,如果面试官单独挑出一个来深挖还是能够问出很多问题的。
比如面试官想在消息队里上进行深挖,可能会问:
- 常见消息队列的对比
- 如何保证消息的消费顺序?
- 如何保证消息不丢失?
- 如何保证消息不重复消费?
- 如何设计一个消息队列?
- ……
再比如面试官想在 Redis 上深挖的话,可能会问:
- Redis 常用的数据结构了解么?
- Redis 如何保证数据不丢失?
- Redis 内存占用过大导致响应速度变慢怎么解决?
- 缓存穿透、缓存雪崩了解么?怎么解决?
- ……
因此,要想要真正搞懂秒杀系统的设计,你还需要将其涉及到的一些技术给研究透!
如何自己实现一个RPC框架
像设计一个 RPC 框架/消息队列这类问题在面试中还是非常常见的。这是一道你花点精力稍微准备一下就能回答上来的一个问题。如果你回答的比较好的话,那面试官肯定会对你印象非常不错!
消息队列的设计实际上和 RPC 框架/非常类似,我这里就先拿 RPC 框架开涮。
如果让你自己设计 RPC 框架你会如何设计?
一般情况下, RPC 框架不仅要提供服务发现功能,还要提供负载均衡、容错等功能,这样的 RPC 框架才算真正合格的。
为了便于小伙伴们理解,我们先从一个最简单的 RPC 框架使用示意图开始。这也是 guide-rpc-framework 目前的架构 。
从上图我们可以看出:服务提供端 Server 向注册中心注册服务,服务消费者 Client 通过注册中心拿到服务相关信息,然后再通过网络请求服务提供端 Server。
作为 RPC 框架领域的佼佼者Dubbo的架构如下图所示,和我们上面画的大体也是差不多的。
下面我们再来看一个比较完整的 RPC 框架使用示意图如下:
参考上面这张图,我们简单说一下设计一个最基本的 RPC 框架的思路或者说实现一个最基本的 RPC 框架需要哪些东西:
注册中心
注册中心首先是要有的。比较推荐使用 Zookeeper 作为注册中心。
ZooKeeper 为我们提供了高可用、高性能、稳定的分布式数据一致性解决方案,通常被用于实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。并且,ZooKeeper 将数据保存在内存中,性能是非常棒的。 在“读”多于“写”的应用程序中尤其地高性能,因为“写”会导致所有的服务器间同步状态。(“读”多于“写”是协调服务的典型场景)。
关于 ZooKeeper 的更多介绍可以看我总结的这篇文章:《ZooKeeper 相关概念总结》
当然了,如果你想通过文件来存储服务地址的话也是没问题的,不过性能会比较差。
注册中心负责服务地址的注册与查找,相当于目录服务。 服务端启动的时候将服务名称及其对应的地址(ip+port)注册到注册中心,服务消费端根据服务名称找到对应的服务地址。有了服务地址之后,服务消费端就可以通过网络请求服务端了。
我们再来结合 Dubbo 的架构图来理解一下!
上述节点简单说明:
- Provider: 暴露服务的服务提供方
- Consumer: 调用远程服务的服务消费方
- Registry: 服务注册与发现的注册中心
- Monitor: 统计服务的调用次数和调用时间的监控中心
- Container: 服务运行容器
调用关系说明:
- 服务容器负责启动,加载,运行服务提供者。
- 服务提供者在启动时,向注册中心注册自己提供的服务。
- 服务消费者在启动时,向注册中心订阅自己所需的服务。
- 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
- 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
- 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。
网络传输
既然我们要调用远程的方法,就要发送网络请求来传递目标类和方法的信息以及方法的参数等数据到服务提供端。
网络传输具体实现你可以使用 Socket ( Java 中最原始、最基础的网络通信方式。但是,Socket 是阻塞 IO、性能低并且功能单一)。
你也可以使用同步非阻塞的 I/O 模型 NIO ,但是用它来进行网络编程真的太麻烦了。不过没关系,你可以使用基于 NIO 的网络编程框架 Netty ,它将是你最好的选择!
我先简单介绍一下 Netty ,后面的文章中我会详细介绍到。
- Netty 是一个基于 NIO 的 client-server(客户端服务器)框架,使用它可以快速简单地开发网络应用程序。
- 它极大地简化并简化了 TCP 和 UDP 套接字服务器等网络编程,并且性能以及安全性等很多方面甚至都要更好。
- 支持多种协议如 FTP,SMTP,HTTP 以及各种二进制和基于文本的传统协议。
序列化和反序列化
要在网络传输数据就要涉及到序列化。为什么需要序列化和反序列化呢?
因为网络传输的数据必须是二进制的。因此,我们的 Java 对象没办法直接在网络中传输。为了能够让 Java 对象在网络中传输我们需要将其序列化为二进制的数据。我们最终需要的还是目标 Java 对象,因此我们还要将二进制的数据“解析”为目标 Java 对象,也就是对二进制数据再进行一次反序列化。
另外,不仅网络传输的时候需要用到序列化和反序列化,将对象存储到文件、数据库等场景都需要用到序列化和反序列化。
JDK 自带的序列化,只需实现 java.io.Serializable接口即可,不过这种方式不推荐,因为不支持跨语言调用并且性能比较差。
现在比较常用序列化的有 hessian、kyro、protostuff ……。我会在下一篇文章中简单对比一下这些序列化方式。
动态代理
动态代理也是需要的。很多人可能不清楚为啥需要动态代理?我来简单解释一下吧!
我们知道代理模式就是: 我们给某一个对象提供一个代理对象,并由代理对象来代替真实对象做一些事情。你可以把代理对象理解为一个幕后的工具人。 举个例子:我们真实对象调用方法的时候,我们可以通过代理对象去做一些事情比如安全校验、日志打印等等。但是,这个过程是完全对真实对象屏蔽的。
讲完了代理模式,再来说动态代理在 RPC 框架中的作用。
前面第一节的时候,我们就已经提到 :RPC 的主要目的就是让我们调用远程方法像调用本地方法一样简单,我们不需要关心远程方法调用的细节比如网络传输。
怎样才能屏蔽程方法调用的底层细节呢?
答案就是动态代理。简单来说,当你调用远程方法的时候,实际会通过代理对象来传输网络请求,不然的话,怎么可能直接就调用到远程方法。
相关文章: 代理模式详解:静态代理+JDK/CGLIB 动态代理实战
负载均衡
负载均衡也是需要的。为啥?
举个例子:我们的系统中的某个服务的访问量特别大,我们将这个服务部署在了多台服务器上,当客户端发起请求的时候,多台服务器都可以处理这个请求。那么,如何正确选择处理该请求的服务器就很关键。假如,你就要一台服务器来处理该服务的请求,那该服务部署在多台服务器的意义就不复存在了。负载均衡就是为了避免单个服务器响应同一请求,容易造成服务器宕机、崩溃等问题,我们从负载均衡的这四个字就能明显感受到它的意义。
传输协议
我们还需要设计一个私有的 RPC 协议,这个协议是客户端(服务消费方)和服务端(服务提供方)交流的基础。
简单来说:通过设计协议,我们定义需要传输哪些类型的数据, 并且还会规定每一种类型的数据应该占多少字节。这样我们在接收到二进制数据之后,就可以正确的解析出我们需要的数据。这有一点像密文传输的感觉。
通常一些标准的 RPC 协议包含下面这些内容:
魔数 : 通常是 4 个字节。这个魔数主要是为了筛选来到服务端的数据包,有了这个魔数之后,服务端首先取出前面四个字节进行比对,能够在第一时间识别出这个数据包并非是遵循自定义协议的,也就是无效数据包,为了安全考虑可以直接关闭连接以节省资源。
序列化器编号 :标识序列化的方式,比如是使用 Java 自带的序列化,还是 json,kyro 等序列化方式。
消息体长度 : 运行时计算出来。
……
如果你想看 guide-rpc-framework 的 RPC 协议设计的话,可以在 Netty 编解码器相关的类中找到。
实现一个最基本的 RPC 框架需要哪些技术?
刚刚我们已经聊了如何实现一个 RPC 框架,下面我们就来看看实现一个最基本的 RPC 框架需要哪些技术吧!
按照我实现的这一款基于 Netty+Kyro+Zookeeper 实现的 RPC 框架来说的话,你需要下面这些技术支撑:
Java
动态代理机制;
序列化机制以及各种序列化框架的对比,比如 hession2、kyro、protostuff;
线程池的使用;
CompletableFuture 的使用;
……
Netty
使用 Netty 进行网络传输;
ByteBuf 介绍;
Netty 粘包拆包;
Netty 长连接和心跳机制;
……
Zookeeper
基本概念;
数据结构;
如何使用 Netflix 公司开源的 zookeeper 客户端框架 Curator 进行增删改查;
……
总结
实现一个最基本的 RPC 框架应该至少包括下面几部分:
注册中心 :注册中心负责服务地址的注册与查找,相当于目录服务。
网络传输 :既然我们要调用远程的方法,就要发送网络请求来传递目标类和方法的信息以及方法的参数等数据到服务提供端。
序列化和反序列化 :要在网络传输数据就要涉及到序列化。
动态代理 :屏蔽程方法调用的底层细节。
负载均衡 : 避免单个服务器响应同一请求,容易造成服务器宕机、崩溃等问题。
传输协议 :这个协议是客户端(服务消费方)和服务端(服务提供方)交流的基础。
更完善的一点的 RPC 框架可能还有监控模块(拓展:你可以研究一下 Dubbo 的监控模块的设计)。
如何设计一个排行榜
排行榜到处可见,比如直播间送礼物的排行榜、朋友圈的微信步数排行榜、王者荣耀中的段位排行榜等等。
今天让我们从程序设计的角度,来看看如何设计一个排行榜!
我们先从最基础的实现方式来说起。
MySQL 的 ORDER BY 关键字
第一种要介绍的实现方式就是直接使用 MySQL 的 ORDER BY 关键字。 ORDER BY 关键字可以对查询出来的数据按照指定的字段进行排序。
我相信但凡是学过 MySQL 的人,一定都用过 ORDER BY 关键字!没用过的,先不要看下面的文章了,麻烦默默反思 3 分钟。
1 | SELECT column1, column2, ... |
我之前在一个用户数据量不大(6w 用户左右)并且排序需求并不复杂的项目中使用的就是这种方法。
这种方式的优缺点也比较明显。好处是比较简单,不需要引入额外的组件,成本比较低。坏处就是每次生成排行榜都比较耗时,对数据库的性能消耗非常之大,数据量一大,业务场景稍微复杂一点就顶不住了。
我们这里创建一个名为 cus_order 的表,来实际测试一下这种排序方式。为了测试方便, cus_order 这张表只有 id、score、name这 3 个字段。
1 | CREATE TABLE `cus_order` ( |
我们定义一个简单的存储过程(PROCEDURE)来插入 100w 测试数据。
1 | DELIMITER ;; |
存储过程定义完成之后,我们执行存储过程即可!
1 | CALL BatchinsertDataToCusOder(1, 1000000); # 插入100w+的随机数据 |
等待一会,100w 的测试数据就插入完成了!
为了能够对这 100w 数据按照 score 进行排序,我们需要执行下面的 SQL 语句。
1 | SELECT `score`,`name` FROM `cus_order` ORDER BY `score` DESC;#降序排序 |
为了能够查看这套 SQL 语句的执行时间,我们需要通过show profiles命令。
不过,请确保你的 profiling 是开启(on)的状态(可以通过 show variables 命令查看)。
默认情况下, profiling 是关闭(off)的状态,你直接通过set @@profiling=1命令即可开启。
然后,我们就查询到了具体的执行速度。
1 | { |
可以看到,一共耗时了接近 4 s。
如何优化呢? 加索引并且限制排序数据量 是一种比较常见的优化方式。
我们对 score 字段加索引,并限制只排序 score 排名前 500 的数据。
这个时候,我们再执行下面的 SQL 语句,速度就快了很多,只需要 0.01 秒就排序了前 500 名的数据。
1 | { |
当然了,这只是一个最简单的场景,实际项目中的复杂度要比我这里列举的例子复杂很多,执行速度也会慢很多。
不过,能不用 MySQL 的 ORDER BY 关键字还是要看具体的业务场景。如果说你的项目需要排序数据量比较小并且业务场景不复杂的话(比如你对你博客的所有文章按照阅读量来排序),我觉得直接使用 MySQL 的 ORDER BY 关键字就可以了。
Redis 的 sorted set
了解过 Redis 常见数据结构的小伙伴,都知道 Redis 中有一个叫做 sorted set
的数据结构经常被用在各种排行榜的场景下。
通过 sorted set
,我们能够轻松应对百万级别的用户数据排序。这简直就是专门为排行榜设计的数据结构啊!
Redis 中 sorted set 有点类似于 Java 中的 TreeSet
和HashMap
的结合体,sorted set
中的数据会按照权重参数score
的值进行排序。
User | Score |
---|---|
user1 | 112.0 |
user2 | 100.0 |
user3 | 123.0 |
user4 | 100.0 |
user5 | 33.0 |
user6 | 993.0 |
我们这里简单来演示一下。我们把上表中的数据添加到sorted set
中。
1 | # 通过 zadd 命令添加了 6 个元素到 cus_order_set 中 |
sorted set
基本可以满足大部分排行榜的场景。
如果我们要查看包含所有用户的排行榜怎么办? 通过 ZRANGE (从小到大排序) / ZREVRANGE (从大到小排序)
1 | # -1 代表的是全部的用户数据, |
如果我们要查看只包含前 3 名的排行榜怎么办? 限定范围区间即可。
1 | # 0 为 start 2 为 stop |
如果我们需要查询某个用户的分数怎么办呢? 通过 ZSCORE
命令即可。
1 | 127.0.0.1:6379> ZSCORE cus_order_set "user1" |
如果我们需要查询某个用户的排名怎么办呢? 通过ZREVRANK
命令即可。
1 | 127.0.0.1:6379> ZREVRANK cus_order_set "user3" |
如何对用户的排名数据进行更新呢? 通过ZINCRBY
命令即可。
1 | # 对 user1 的分数加2 |
除了我上面提到的之外,还有一些其他的命令来帮助你解决更多排行榜场景的需求,想要深入研究的小伙伴可以仔细学习哦!
不过,需要注意的一点是:Redis 中只保存了排行榜展示所需的数据,需要用户的具体信息数据的话,还是需要去对应的数据库(比如 MySQL)中查。
你以为这样就完事了? 不存在的!还有一些无法仅仅通过 Redis 提供的命令解决的场景。
比如,如何实现多条件排序? 其实,答案也比较简单,对于大部分场景,我们直接对 score 值做文章即可。
更具体点的话就是,我们根据特定的条件来拼接 score 值即可。比如我们还要加上时间先后条件的话,直接在score 值添加上时间戳即可。
再比如,如何实现指定日期(比如最近 7 天)的用户数据排序?
我说一种比较简单的方法:我们把每一天的数据都按照日期为名字,比如 20350305 就代表 2035 年 3 月 5 号。
如果我们需要查询最近 n 天的排行榜数据的话,直接 ZUNIONSTORE来求 n 个 sorted set
的并集即可。
1 | ZUNIONSTORE last_n_days n 20350305 20350306.... |
我不知道大家看懂了没有,我这里还是简单地造一些数据模拟一下吧!
1 | # 分别添加了 3 天的数据 |
通过 ZUNIONSTORE 命令来查看最近 3 天的排行榜情况:
1 | 127.0.0.1:6379> ZUNIONSTORE last_n_days 3 20350305 20350306 20350307 |
现在,这 3 天的数据都集中在了 last_n_days 中。
1 | 127.0.0.1:6379> ZREVRANGE last_n_days 0 -1 |
如果一个用户同时在多个sorted set
中的话,它最终的score
值就等于这些sorted set
中该用户的 score
值之和。
既然可以求并集,那必然也可以求交集。你可以通过 ZINTERSTORE
命令来求多个 n 个 sorted set
的交集。
有哪些场景可以用到多个sorted set
的交集呢? 比如每日打卡的场景,你对某一段时间每天打卡的人进行排序。
这个命令还有一个常用的权重参数weights
(默认为 1)。在进行并集/交集的过程中,每个集合中的元素会将自己的 score
*weights
。
我下面演示一下这个参数的作用。
1 | # staff_set 存放员工的排名信息 |
如果,我们需要将员工和管理者放在一起比较,不过,两者权重分别为 1 和 3。
1 | # staff_set 的权重为1 manager_set的权重为3 |
最终排序的结果如下:
1 | 127.0.0.1:6379> ZREVRANGE all_user_set 0 -1 |
总结
上面我一共提到了两种设计排行榜的方法:
- MySQL 的 ORDER BY 关键字
- Redis 的 sorted set
其实,这两种没有孰好孰坏,还是要看具体的业务场景。如果说你的项目需要排序数据量比较小并且业务场景不复杂的话(比如你对你博客的所有文章按照阅读量来排序),我觉得直接使用 MySQL 的 ORDER BY 关键字就可以了,没必要为了排行榜引入一个 Redis。
另外,在没有分页并且数据量不大的情况下,直接在前端拿到所有需要用到的数据之后再进行排序也是可以的。
如何设计微博Feed流/信息流系统
“如何设计微博 Feed 流/信息流系统? ”是一道比较常见的系统设计问题,面试中比较常见。
这篇文章简单谈谈我的看法。个人能力有限,有些地方大家可以结合自己的经验自行扩展!爱你们哦!
下面是正文!
Feed 流是社交和资讯平台不可缺少的重要组成。TimeLine 时期,Feed 流推送的机制完全基于时间,比如朋友圈动态、几年前的微信订阅号就是这种机制。
现在的 Feed 流主要是基于智能化/个性化的推荐,简单来说,就是你喜欢什么我就给你推荐什么。这样的话,人们被推送的信息会极大地由自己的个人兴趣主导,你自己所处的信息世界就像桎梏于蚕茧一般的“茧房”中一样。这也就是“信息茧房”所表达的意思。
Feed 流基础
何为 Feed 流?
简单来说就是能够实时/智能推送信息的数据流。像咱们的朋友圈动态(timeline)、知乎的推荐(智能化推荐 )、你订阅的 Up 主的动态(timeline)都属于 Feed 流。
几种常见的 Feed 流形式
我总结了 3 种常见的 Feed 流形式。
纯智能推荐
你看到的内容完全是基于你看过的内容而推荐的,比较典型的产品有头条首页推荐、知乎首页推荐。
智能推荐需要依赖 推荐系统 ,推荐质量的好坏和推荐算法有非常大的关系。
推荐系统的相关文献把它们分成三类:协同过滤(仅使用用户与商品的交互信息生成推荐)系统、基于内容(利用用户偏好和/或商品偏好)的系统和 混合推荐模型(使用交互信息、用户和商品的元数据)的系统。
另外,随着深度学习应用的爆发式发展,特别是在计算机视觉、自然语言处理和语音方面的进展,基于深度学习的推荐系统越来越引发大家的关注。循环神经网络(RNN)理论上能够有效地对用户偏好和物品属性的动态性进行建模,基于当前的趋势,预测未来的行为。
纯 Timeline
你看到的内容完全按照时间来排序,比较典型的产品有微信朋友圈、QQ 空间、微博关注者动态。
微信朋友圈:
微博关注者动态:
纯 Timeline 这种方式实现起来最简单,直接按照时间排序就行了。
纯 Timeline 这种形式更适用于好友社交领域,用户关注更多的是人发出的内容,而不仅仅是内容。
智能推荐+Timeline
智能推荐+Timeline 这个也是目前我觉得比较好的一种方式,实现起来比较简单,同时又能一定程度地避免 “信息茧房” 的问题。
设计 Feed 流系统的注意事项
- 实时性 :你关注的人发了微博信息之后,信息需要在短时间之内出现在你的信息流中。
- 高并发 :信息流是微博的主体模块,是用户进入到微博之后最先看到的模块,因此它的并发请求量是最高的,可以达到每秒几十万次请求。
- 性能 : 信息流拉取性能直接影响用户的使用体验。微博信息流系统中需要聚合的数据非常多。聚合这么多的数据就需要查询多次缓存、数据库、计数器,而在每秒几十万次的请求下,如何保证在 100ms 之内完成这些查询操作,展示微博的信息流呢?这是微博信息流系统最复杂之处,也是技术上最大的挑战。
- ……
Feed 流架构设计
我们这里以 微博关注者动态 为例。
Feed 流的 3 种推送模式
推模式
当一个用户发送一个动态(比如微博、视频)之后,主动将这个动态推送给其他相关用户(比如粉丝)。
推模式下,我们需要将这个动态插入到每位粉丝对应的 feed 表中,这个存储成本是比较高的。尤其是对于粉丝数量比较多的大 V 来说,每发一条动态,需要存储的数据量实在太大。
假如狗蛋,有 n 个粉丝 1、2 ~ n。那么,狗蛋发一条微博时,我们需要执行的 SQL 语句如下:
1 | insert into outbox(userId, feedId, create_time) values("goudan", $feedId, $current_time); //写入用户狗蛋的发件箱 |
当我们要查询用户 n 的信息流时,只需要执行下面这条 SQL 就可以了:
1 | select feedId from inbox where userId = "n"; |
可以很明显的看出,推模式最大的问题就是写入数据库的操作太多。
正常情况下,一个微博用户的粉丝大概在 150 左右,挨个写入也还好。不过,微博大 V 的粉丝可能在几百万,几千万,如果挨个给每个写入一条数据的话,是肯定不能接受的!因此,推模式不适合关注者粉丝过多的场景。
拉模式
不同于推模式,拉模式下我们是自己主动去拉取动态(拉取你关注的人的动态),然后将这些动态根据相关指标(比如时间、热度)进行实时聚合。
拉模式存储成本虽然降低,但是查询和聚合这两个操作的成本会比较高。尤其是对于单个用户关注了很多人的情况来说,你需要定时获取他关注的所有人的动态然后再做聚合,这个成本可想而知。
另外,拉模式下的数据流的实时性要比推模式差的。
推垃结合模式
推拉结合的核心是针对微博大 V 和不活跃用户特殊处理。
首先,我们需要区分出系统哪些用户属于微博大 V(10w 粉丝以上?)。其次,我们需要根据登录行为来判断哪些用户属于不活跃用户。
有了这些数据之后,就好办了!当微博大 V 发送微博的时候,我们仅仅将这条微博写入到活跃用户,不活跃的用户自己去拉取。示意图如下(图片来自:《高并发系统设计 40 问》):
推拉结合非常适合用户粉丝数比较大的场景。
存储
我们的存储的数据量会比较大,所以,存储库必须要满足可以水平扩展。
一般情况,通用的存储方案就是 MySQL + Redis 。MySQL 永久保存数据, Redis 作为缓存提高热点数据的访问速度。
如果缓存的数据量太大怎么办? 我们可以考虑使用Redis Cluster,也就是 Redis 集群。Redis Cluster 可以帮助我们解决 Redis 大数据量缓存的问题,并且,也方便我们进行横向拓展(增加 Redis 机器)。
为了提高系统的并发,我们可以考虑对数据进行 读写分离 和 分库分表 。
读写分离主要是为了将数据库的读和写操作分不到不同的数据库节点上。主服务器负责写,从服务器负责读。另外,一主一从或者一主多从都可以。读写分离可以大幅提高读性能,小幅提高写的性能。因此,读写分离更适合单机并发读请求比较多的场景。
分库分表是为了解决由于库、表数据量过大,而导致数据库性能持续下降的问题。常见的分库分表工具有:sharding-jdbc(当当)、TSharding(蘑菇街)、MyCAT(基于 Cobar)、Cobar(阿里巴巴)…。 推荐使用 sharding-jdbc。 因为,sharding-jdbc 是一款轻量级 Java 框架,以 jar 包形式提供服务,不要我们做额外的运维工作,并且兼容性也很好。
《从零开始学架构》 中的有一张图片对于垂直拆分和水平拆分的描述还挺直观的。
另外,如果觉得分库分表比较麻烦的话,可以考虑使用 TiDB 这类分布式数据库。TiDB 是国内 PingCAP 团队开发的一个分布式 SQL 数据库。其灵感来自于 Google 的 F1, TiDB 支持包括传统 RDBMS 和 NoSQL 的特性,具备水平扩容或者缩容、金融级高可用。
参考
Feed 流系统设计-总纲 :写的真心不错!
feed 流设计:那些谋杀你时间 APP :可以让你从产品层面明白 Feed 流的一些概念。
相关问题
微博和知乎中的 feed 流是如何实现的? :知乎的相关提问
如何设计一个短链系统
我平时经常看极客时间上的专栏,上面的每一个专栏 URL 地址都有一个短链与之对应。比如你使用下面两个链接打开的都是 《MySQL 实战 45 讲》这门课程。
有了长链,为什么还要再弄一个短链呢?
- 短链更简洁,更方便传播:过长的链接不利于在互联网传播;
- 方便对链接的点击情况做后续追踪:比如查看短链最近一周的访问量、访客数、访问来源……;
- 对于短信等限制字数的场景来说更加友好:很多社交平台发表动态是有字数限制的,如果你直接使用长链的话,那留给你自己想表达的其他内容的文字就少了很多;
- ……
短链原理
短链的具体原理其实比较简单,说白了就是: 通过短链找到长链(原始链接),然后再重定向到长链地址即可!
我画了一个简单的示意图:
🌰 举个例子:我们来访问 “http://gk.link/a/10q2I” 这个链接,从 HTTP 请求信息可以看到请求被重定向了,返回的状态码为 “302”。
另外还有一个比较常用的重定向状态 “301” , 我们应该用“301” 还是“302”作为状态码更好呢?
答案是:“302” ,绝大部分短链系统也都是使用的 “302” 作为状态码。
这是因为 “301” 状态码代表永久重定向,只要浏览器拿到长链之后就会对其缓存,下次再请求短链就直接从缓存中拿对应的长链地址。这样的话,我们就没办法对短链进行相关分析了。
而“302” 状态码代表资源被临时重定向了,不会存在上面说的这种问题。
🌰 举个例子:你的活动链接通过短链发送给了 10w+用户,你想知道短链后续的点击情况的话,你使用 “301” 状态码就不行了。
唯一短链生成
原始链接必定是唯一的,我们也要确保生成的短链唯一。
如何生成唯一的短链呢?换言之就是我们如何通过唯一的字符串来表示长链。
比较常见的一种方法就是: 通过哈希算法对长链去哈希。
一般建议使用用非加密型哈希算法比如 MurmurHash 。因为,相比于 MD5,SHA 等加密型哈希算法,非加密型哈希算法往往效率更高!
我们拿 MurmurHash 来说,MurmurHash 当前最新的版本是 MurmurHash3,它能够产生出 32-bit 或 128-bit 哈希值。对于绝大部分场景来说,32-bit 的一般就已经够用了。
1 | //Guava 自带的 MurmurHash 算法实现 |
生成的哈希值是 10 进制的,为了缩短它的长度,我们可以将其转变为 62 进制即可。10 进制的 3394174629 转换为 62 进制就是 3HHBS5。
我们将 3HHBS5 作为短链的唯一标识拼接即可。
既然使用了哈希算法,那不可避免会出现哈希冲突(不同的长链生成的短链是一样的),虽然概率比较小,但是我们也还是要解决。
如何判断是否发生了哈希冲突呢?
判断是否发生哈希冲突也就是看我们生成的短链是否是唯一的。
如果我们使用的是 MySQL,PostgreSQL 这类关系型数据库的话,我们可以给存放短链的字段 sort_url 添加唯一索引。
不过,为了提高性能以及应对高并发,还是建议利用布隆过滤器解决这个问题。
如何解决哈希冲突呢?
解决办法其实也很简单。如果发生哈希冲突,我们就在长链后拼接一个随机字符串。如果拼接了随机字符串还是发生哈希冲突那就再拼接一个随机字符串。
并且,我们要将拼接之后得到的字符串和拼接的字符串都存储起来,通过这两者可以获取长链(原始链接)。
一个长链对应一个短链还是多个短链呢?
这个还是要看具体的业务需求。个人建议是一个长链可以在不同的条件(比如生成短链的用户不同)下对应上不同的短链。这样的话,我们可以更好地对短链进行相关分析。
🌰 举个例子:通过小码短连接后台,我们可以看到短连接的访问次数、访问人数等信息。
这样的话,我们对长网址取哈希的时候加上对应的条件信息即可(比如生成短链的用户 ID)。
短链存储
如果我们使用 MySQL,PostgreSQL 这类关系型数据库存储的话,表结构大概是下面这样:
1 | CREATE TABLE `url_map` ( |
当然了,也可以使用 Redis 这类 K-V 内存数据库来做,这样性能也会更好!并且,存放在 Redis 中存放的本就是键值对的数据,刚好满足我们的需求。
当我们存放一个长链的时候,我们首先判断一下这个长链是否已经被转换过短链。
如果需要对长链就行区分的话(比如不同的用户使用同一个长链生成的短链不同),我们在判断的时候加上对应的条件即可(比如这个长链对应的用户)。
这里不能直接根据长链哈希之后得到的短链来判断长链是否已经被转换过短链,因为不同的长链生成的短链可能是一样的(哈希冲突,不过,概率很低)。
我个人建议不论是否使用 Redis 数据库,都要将最近比较活跃的短连接存放在缓存中。为了避免缓存过大,我们可以为这些放在缓存中的短连接设置一个过期时间。
如何设计一个站内消息系统
这篇文章是一位朋友投稿给我的,我简单完善了一下。
各位使用过简书,知乎或 B 站的小伙伴应该都有这样的使用体验:当有其他用户关注我们或者私信我们的行为时,我们会收到相关的消息。
虽然这些功能看上去简单,但其背后的设计是非常复杂的,几乎是一个完成的系统,可以称之为 站内消息系统。
我以 B 站举例(个人认为 B 站的消息系统是我见过的非常完美的,UI 也最为人性化的):
可以看到 B 站把消息大致分为了三类:
- 系统推送的通知(System Notice);
- 回复、@、点赞等用户行为产生的提醒(Remind);
- 用户之间的私信(Chat)。
这样设计不仅分类明确,且处于同一个主体的事件提醒还会做一个聚合,极大的提高了用户体验,不让用户收到太多分散的消息。
举个例子:比如你在某个视频或某篇文章下发表了评论,有 100 个人给你的评论点了赞,那么你希望消息页面呈现的是一个一个用户给你点赞的提醒,还是像以下聚合之后的提醒:
我相信你大概率会选择后者。
我认为对于很多应用来说,这样的设计都是非常合理的,接下来我写写我对于消息系统的设计。
系统通知(System Notice)
系统通知一般是由后台管理员发出,然后指定某一类(全体,个人等)用户接收。基于此设想,可以把系统通知大致分为两张表:
- t_manager_system_notice(管理员系统通知表) :记录管理员发出的通知 ;
- t_user_system_notice(用户系统通知表) : 存储用户接受的通知。
t_manager_system_notice(管理员系统通知表) 表结构如下:
字段名 | 类型 | 描述 |
---|---|---|
system_notice_id | LONG | 系统通知 ID |
title | VARCHAR | 标题 |
content | TEXT | 内容 |
type | VARCHAR | 发给哪些用户:单用户 single;全体用户 all,vip 用户,具体类型各位小伙伴可以根据自己的需求选择 |
state | BOOLEAN | 是否已被拉取过,如果已经拉取过,就无需再次拉取 |
recipient_id | LONG | 接受通知的用户的 ID,如果 type 为单用户,那么 recipient 为该用户的 ID;否则 recipient 为 0 |
manager_id | LONG | 发布通知的管理员 ID |
publish_time | TIMESTAMP | 发布时间 |
t_user_system_notice(用户系统通知表)结构如下:
字段名 | 类型 | 描述 |
---|---|---|
user_notice_id | LONG | 主键 ID |
state | BOOLEAN | 是否已读 |
system_notice_id | LONG | 系统通知的 ID |
recipient_id | LONG | 接受通知的用户的 ID |
pull_time | TIMESTAMP | 拉取通知的时间 |
当管理员发布一条通知后,将通知插入 t_manager_system_notice 表中,然后系统定时的从 t_manager_system_notice 表中拉取通知,然后根据通知的 type 将通知插入 t_user_system_notice 表中。
如果通知的 type 是 single 的,那就只需要插入一条记录到 t_user_system_notice 中。如果是全体用户,那么就需要将一个通知批量根据不同的用户 ID 插入到 t_user_system_notice 中,这个数据量就需要根据平台的用户量来计算。
🌰 举个例子:管理员 A 发布了一个活动的通知,他需要将这个通知发布给全体用户,当拉取时间到来时,系统会将这一条通知取出。随后系统到用户表中查询选取所有用户的 ID,然后将这一条通知的信息根据所有用户的 ID,批量插入 t_user_system_notice 中。用户需要查看系统通知时,从 t_user_system_notice 表中查询就行了。
👉 需要注意的是:
- 因为一次拉取的数据量可能很大,所以两次拉取的时间间隔可以设置的长一些。
- 拉取 t_manager_system_notice 表中的通知时,需要判断 state,如果已经拉取过,就不需要重复拉取,否则会造成重复消费。
- 有的小伙伴可能有疑问: 某条通知已经被拉取过的话,在其后注册的用户是不是不能再接收到这条通知?是的。但如果你想将已拉取过的通知推送给那些后注册的用户,也不是特别大的问题。只需要再写一个定时任务,这个定时任务可以将通知的 push_time 与用户的注册时间比较一下,重新推送即可。
认真思考的小伙伴应该也发现了,当用户量比较大比如上千万的时候,如果发送一个全体用户的通知需要挨个插入数据到一张表的话,是不靠谱的!
常见的解决办法,有两种方式:
- 每位用户单独有一张或者几张专门用来存放站内消息的表,根据 hash(userId)作为表名后缀。
- 对于系统通知类型,只存放一条数据到 t_user_system_notice 表,用户自己拉取数据然后再判断消息是否已经读取过即可。
并且,当一条通知需要发布给全体用户时,我们还应该考虑到用户的活跃度。因为如果有些用户长期不活跃,我们还将通知推送给他(她),这显然会造成空间的浪费。 所以在选取用户 ID 时,我们可以将用户上次登录的时间与推送时间做一个比较,如果用户一年未登陆或几个月未登录,我们就不选取其 ID,进而避免无谓的推送。
以上就是系统通知的设计了,接下来再看看较难的提醒类型的消息。
事件提醒(EventRemind)
之所以称提醒类型的消息为事件提醒,是因为此类消息均是通过用户的行为产生的,如下:
- xxx 在某个评论中@了你;
- xxx 点赞了你的文章;
- xxx 点赞了你的评论;
- xxx 回复了你的文章;
- xxx 回复了你的评论;
- ……
诸如此类事件,我们以单词 action 形容不同的事件(点赞,回复,@(at))。 可以看到除了事件之外,我们还需要了解用户是在哪个地方产生的事件,以便当我们收到提醒时, 点击这条消息就可以去到事件现场,从而增强用户体验,我以事件源 source 来形容事件发生的地方。
- 当 action 为点赞,source 为文章时,我就知道:有用户点赞了我的某篇文章;
- 当 action 为点赞,source 为评论时,我就知道:有用户点赞了我的某条评论;
- 当 action 为@(at), source 为评论时,我就知道:有用户在某条评论里@了我;
- 当 action 为回复,source 为文章时,我就知道:有用户回复了我的某篇文章;
- 当 action 为回复,source 为评论时,我就知道:有用户回复了我的某条评论;
由此可以设计出事件提醒表 t_event_remind,其结构如下:
字段名 | 类型 | 描述 |
---|---|---|
event_remind_id | LONG | 消息 ID |
action | VARCHAR | 动作类型,如点赞、at(@)、回复等 |
source_id | LONG | 事件源 ID,如评论 ID、文章 ID 等 |
source_type | VARCHAR | 事件源类型:”Comment”、”Post”等 |
source_content | VARCHAR | 事件源的内容,比如回复的内容,回复的评论等等 |
url | VARCHAR | 事件所发生的地点链接 url |
state | BOOLEAN | 是否已读 |
sender_id | LONG | 操作者的 ID,即谁关注了你,at 了你 |
recipient_id | LONG | 接受通知的用户的 ID |
remind_time | TIMESTAMP | 提醒的时间 |
消息聚合
消息聚合只适用于事件提醒,以聚合之后的点赞消息来说:
- 100 人 {点赞} 了你的 {文章 ID = 1} :《A》;
- 100 人 {点赞} 了你的 {文章 ID = 2} :《B》;
- 100 人 {点赞} 了你的 {评论 ID = 3} :《C》;
聚合之后的消息明显有两个特征,即: action 和 source type,这是系统消息和私信都不具备的, 所以我个人认为事件提醒的设计要稍微比系统消息和私信复杂。
如何聚合?
稍稍观察下聚合的消息就可以发现:某一类的聚合消息之间是按照 source type 和 source id 来分组的, 因此我们可以得出以下伪 SQL:
1 | SELECT * FROM t_event_remind WHERE recipient_id = 用户ID |
当然,SQL 层面的结果集处理还是很麻烦的,所以我的想法先把用户所有的点赞消息先查出来, 然后在程序里面进行分组,这样会简单不少。
拓展
其实还有一种设计提醒表的做法,即按业务分类,不同的提醒存入不同的表,这样可以分为:
点赞提醒表
回复提醒表
at(@)提醒表。
我认为这种设计比第一种的更松耦合,不必所有类型的提醒都挤在一张表里,但是这也会带来表数量的膨胀。 所以各位小伙伴可以自行选择方案。
私信
站内私信一般都是点到点的,且要求是实时的,服务端可以采用 Netty 等高性能网络通信框架完成请求。 我们还是以 B 站为例,看看它是怎么设计的:
B 站的私信部分可以分为两部分:
- 左边的与不同用户的聊天室;
- 与当前正在对话的用户的对话框,显示了当前用户与目标用户的所有消息。
按照这个设计,我们可以先设计出聊天室表 t_private_chat,因为是一对一,所以聊天室表会包含对话的两个用户的信息:
字段名 | 类型 | 描述 |
---|---|---|
private_chat_id | LONG | 聊天室 ID |
user1_id | LONG | 用户 1 的 ID |
user2_id | LONG | 用户 2 的 ID |
last_message | VARCHAR | 最后一条消息的内容 |
这里 user1_id 和 user2_id 代表两个用户的 ID,并无特定的先后顺序。
接下来是私信表 t_private_message 了,私信自然和所属的聊天室有联系,且考虑到私信可以在记录中删除(删除了只是不显示记录,但是对方会有记录,撤回才是真正的删除),就还需要记录私信的状态,以下是我的设计:
字段名 | 类型 | 描述 |
---|---|---|
private_message_id | LONG | 私信 ID |
content | TEXT | 私信内容 |
state | BOOLEAN | 是否已读 |
sender_remove | BOOLEAN | 发送消息的人是否把这条消息从聊天记录中删除了 |
recipient_remove | BOOLEAN | 接受人是否把这条消息从聊天记录删除了 |
sender_id | LONG | 发送者 ID |
recipient_id | LONG | 接受者 ID |
send_time | TIMESTAMP | 发送时间 |
消息设置
消息设置一般都是针对提醒类型的消息的,且肯定是由用户自己设置的。所以我想到一般有以下设置选项:
- 是否开启点赞提醒;
- 是否开启回复提醒;
- 是否开启@提醒;
下面是 B 站的消息设置:
可以看到 B 站还添加了陌生人选项,也就是说如果给你发送私信的用户不是你关注的用户,那么视之为陌生人私信,就不接受。
以下是我对于消息设置的设计:
字段名 | 类型 | 描述 |
---|---|---|
user_id | LONG | 用户 ID |
like_message | BOOLEAN | 是否接收点赞消息 |
reply_message | BOOLEAN | 是否接收回复消息 |
at_message | BOOLEAN | 是否接收 at 消息 |
stranger_message | BOOLEAN | 是否接收陌生人的私信 |
总结
以上就是我对于整个站内消息系统的大概设计了,我参考了很多文章的内容以及很多网站的设计,但实际项目的需求肯定与我所介绍的有很多出入,所以各位小伙伴可以酌情参考。
如何解决大文件上传问题
如果你的项目涉及到文件上传的话,面试官很可能会问你这个问题。
我们先看第一个场景:大文件上传中途,突然失败!
试想一个,你想上传一个 5g 的视频,上传进度到 99% 的时候,特么的,突然网络断了,这个时候,你发现自己竟然需要重新上传。我就问你抓狂不?
有没有解决办法呢? 答案就是:分片上传!
什么是分片上传呢? 简单来说,我们只需要先将文件切分成多个文件分片(就像我下面绘制的图片所展示的那样),然后再上传这些小的文件分片。
前端发送了所有文件分片之后,服务端再将这些文件分片进行合并即可。
使用分片上传主要有下面 2 点好处:
断点续传 :上传文件中途暂停或失败(比如遇到网络问题)之后,不需要重新上传,只需要上传那些未成功上传的文件分片即可。所以,分片上传是断点续传的基础。
多线程上传 :我们可以通过多线程同时对一个文件的多个文件分片进行上传,这样的话就大大加快的文件上传的速度。
前端怎么生成文件分片呢?后端如何合并文件分片呢?
前端可以通过 Blob.slice() 方法来对文件进行切割(File 对象是继承 Blob 对象的,因此 File 对象也有 slice() 方法)。
生成文件切片的示例代码如下:
RandomAccessFile
类可以帮助我们合并文件分片,示例代码如下:
何为秒传?
秒传说的就是我们在上传某个文件的时候,首先根据文件的唯一标识判断一下服务端是否已经上传过该文件,如果上传过的话,直接就返回给用户文件上传成功即可。
一般情况下,这个唯一标识都是通过对文件的名称、最后修改时间等信息取 MD5 值得到的,这个可以通过使用 spark-md5 这个库来生成。
需要注意的是:你不能根据文件名就决定文件是否已经上传到服务端,因为很可能存在文件名相同,但是,内容不同的情况。另外,体验更好的是文件内容不变,唯一标识就不应该改变。因此,我们可以根据文件的内容来计算 MD5 值。
另外,还存在一种情况是我们要上传的文件已经上传了部分文件切片到服务端。这个时候,我们直接返回已上传的切片列表给前端即可。
然后,前端再将剩余未上传的分片上传到服务端。
我简单画了一张图描述一下断点续传和秒传。
相关阅读:
如何统计网站Uv
我们先来聊聊描述系统活跃度常用的一些指标。
系统活跃度常用指标
我们先来看几个经常用来描述系统活跃度的名词:PV、UV、VV、IP。
🌰 举个栗子:假如你在家用 ADSL 拨号上网,早上 9 点访问了 JavaGuide下的 2 个页面,下午 2 点又访问了 JavaGuide 下的 3 个页面。那么,对于 JavaGuide 来说,今天的 PV、UV、VV、IP 各项指标该如何计算?
PV 等于上午浏览的 2 个页面和下午浏览的 3 个页面之和,即 PV = 2 + 3
UV 指独立访客数,一天内同一访客的多次访问只计为 1 个 UV,即 UV = 1
VV 指访客的访问次数,上午和下午分别有一次访问行为,即 VV = 2
IP 为独立 IP 数,由于 ADSL 拨号上网每次都 IP 不同,即 IP = 2
PV(Page View)
PV(Page View) 即 页面浏览量。每当一个页面被打开或者被刷新,都会产生一次 PV。一般来说,PV 与来访者的数量成正比,但是 PV 并不直接决定页面的真实来访者数量,如果一个来访者通过不断的刷新页面或是使用爬虫访问,也可以制造出非常高的 PV 。
我上面介绍的只是最普通的一个 PV 的计算方式。实际上,PV 的计算规则有很多种。就比如微信公众号的一篇文章,在一段时间内,即使你多次刷新也不会增加阅读量。这样做的好处就是:更能反映出点开文章的真实用户群体的数量了。
总结 :PV 能够反映出网站的页面被网站用户浏览/刷新的次数。
UV(Unique Visitor)
UV(Unique Visitor) 即 独立访客。1 天内相同访客多次访问网站,只计算为 1 个独立访客。UV 是从用户个体的角度来统计的。
总结:UV 主要用来统计 1 天内访问某站点的用户数。
VV (Visit View)
VV (Visit View) 即 访客访问的次数。当访客完成所有的浏览并最终关掉该网站的所有页面时,便完成了一次访问。
总结:VV 主要用来记录网站用户在一天内访问你的站点的次数。
IP
IP 即 独立 IP 访问数。一天内使用不同 IP 地址的用户访问网站的次数,同一 IP 多次访问计数均为 1。
为什么要进行 PV&UV 统计?
大部分网站都会进行 PV&UV 的统计。就比如说咱们的 Github 的项目就自带 PV&UV 统计。下面这张图就是 JavaGuide 这个开源项目最近这段时间的 PV 和 UV 的趋势图。
通过这张图,我可以清楚地知道我的项目访问量的真实情况。
简单来说,网站进行 PV&UV 统计有下面这些好处:
- PV 和 UV 的结合更能反映项目的真实访问量,有助于我们更了解自己的网站,对于我们改进网站有指导意义。比如咱们网站的某个网页访问量最大,那我们就可以对那个网页进行优化改进。再比如我们的网站在周末访问量比较大,那我们周末就可以多部署一个服务来提高网站的稳定性和性能。
- PV 和 UV 的结合可以帮助广告主预计投放广告可以带来的流量。
如何基于 Redis 统计 UV?
PV 的统计不涉及到数据的去重,而 UV 的计算需要根据 IP 地址或者当前登录的用户来作为去重标准。因此,PV 的统计相对于 UV 的统计来说更为简单一些。
因此我会重点介绍 UV 的统计。
最简单的办法就是:为每一个网页维护一个哈希表,网页 ID +日期 为 Key, Value 为看过这篇文章的所有用户 ID 或者 IP(Set 类型的数据结构)。
当我们需要为指定的网页增加 UV ,首先需要判断对应的用户 ID 或者 IP 是否已经存在于对应的 Set 中。
示意图如下:
当我们需要计算对应页面的 UV 的话,直接计算出页面对应的 Set 集合的大小即可!
这种方式在访问量不是特别大的网站,还是可以满足基本需求的。
但是,如果网站的访问量比较大,这种方式就不能够满足我们的需求了!
试想一下:如果网站的一个页面在一天之内就有接近 100w +不同用户访问的话,维护一个包含 100w+ 用户 ID 或者 用户 IP 的 Set 在内存中,还要不断的判断指定的用户 ID 或者 用户 IP 是否在其中,消耗还是比较大的,更何况这还是一个页面!
有没有对内存消耗比较小,又有类似Set
功能的数据结构呢?
答案是有的!这个时候我们就需要用到 HyperLogLog
了!
其实,HyperLogLog
是一种基数计数概率算法 ,并不是 Redis 特有的。Redis 只是实现了这个算法并提供了一些开箱即用的 API。
Redis 提供的 HyperLogLog 占用空间非常非常小(基于稀疏矩阵存储), 12k 的空间就能存储接近2^64个不同元素。
不过,HyperLogLog
的计算结果并不是一个精确值,存在一定的误差,这是由于它本质上是用概率算法导致的。
但是,一般我们在统计 UV 这种数据的时候,是能够容忍一定范围内的误差的(标准误差是 0.81%,这对于 UV 的统计影响不大,可以忽略不计)。我们更关注的是这种方法能够为我们节省宝贵的服务器资源。
使用 RedisHyperloglog
进行 UV 统计,我们主要会使用到以下三个命令:
PFADD key values
: 用于数据添加,可以一次性添加多个。添加过程中,重复的记录会自动去重。PFCOUNT key
: 对 key 进行统计。PFMERGE destkey sourcekey1 sourcekey2
: 合并多个统计结果,在合并的过程中,会自动去重多个集合中重复的元素。
具体是怎么做的呢?
1、将访问指定页面的每个用户 ID 添加到 HyperLogLog 中。
1 | PFADD PAGE_1:UV USER1 USER2 ...... USERn |
2、统计指定页面的 UV。
1 | PFCOUNT PAGE_1:UV |
HyperLogLog 除了上面的 PFADD 和 PFCOIUNT 命令外,还提供了 PFMERGE ,将多个 HyperLogLog 合并在一起形成一个新的 HyperLogLog 值。
1 | PFMERGE destkey sourcekey [sourcekey ...] |
我们来用 Java 写一个简单的程序来实际体验一下,顺便来对比一下 Set 和 HyperLogLog 这两种方式。
我们这里使用 Jedis 提供的相关 API。
直接在项目中引入 Jedis 相关的依赖即可:
1 | <dependency> |
代码如下,我们循环添加了 10w 个用户到指定 Set
和 HyperLogLog
中。
1 | public class HyperLogLogTest { |
输出结果:
1 | 100.00% |
从输出结果可以看出 Set 可以非常精确的存储这 10w 个用户,而 HyperLogLog 有一点点误差,误差率大概在 0.73% 附近。
我们再来对比一下两者的存储使用空间。
1 | 127.0.0.1:6379> debug object PF:PAGE2:2021-12-19 |
我们可以通过
debug object key
命令来查看某个 key 序列化后的长度。输出的项的说明:
- Value at :key 的内存地址
- refcount :引用次数
- encoding :编码类型
- serializedlength:序列化长度(单位是 Bytes)
- lru_seconds_idle:空闲时间
不过,你需要注意的是 serializedlength 仅仅代表 key 序列化后的长度(持久化本地的时候会用到),并不是 key 在内存中实际占用的长度。不过,它也侧面反应了一个 key 所占用的内存,可以用来比较两个 key 消耗内存的大小。
从上面的结果可以看出内存占用上,Hyperloglog 消耗了 10523 bytes ≈ 10kb,而 Set 消耗了
988895 bytes ≈ 965kb (粗略估计,两者实际占用内存大小会更大)。
可以看出,仅仅是 10w 的数据,两者消耗的内存差别就这么大,如果数据量更大的话,两者消耗的内存的差距只会更大!
我们这里再拓展一下: 假如我们需要获取指定天数的 UV 怎么办呢?
其实,思路很简单!我们在 key 上添加日期作为标识即可!
1 | PFADD PAGE_1:UV:2021-12-19 USER1 USER2 ...... USERn |
那假如我们需要获取指定时间(精确到小时)的 UV 怎么办呢?
思路也一样,我们在 key 上添加指定时间作为标识即可!
1 | PFADD PAGE_1:UV:2021-12-19-12 USER1 USER2 ...... USERn |
后记
除了上面介绍到的方案之外,Doris 、ClickHouse 等用于联机分析(OLAP)的列式数据库管理系统(DBMS)现在也经常用在统计相关的场景。比如说百度的百度统计(网站流量分析)就是基于 Doris 做的,再比如说 Yandex(俄罗斯的一家做搜索引擎的公司)的在线流量分析产品就是用自家的 ClickHouse 做的。
Java
Java IO 模型常见面试题总结
面试中经常喜欢问的一个问题,因为通过这个问题,面试官可以顺便了解一下你的操作系统的水平。
IO 模型这块确实挺难理解的,需要太多计算机底层知识。写这篇文章用了挺久,就非常希望能把我所知道的讲出来吧!希望朋友们能有收货!为了写这篇文章,还翻看了一下《UNIX 网络编程》这本书,太难了,我滴乖乖!心痛~
个人能力有限。如果文章有任何需要补充/完善/修改的地方,欢迎在评论区指出,共同进步!
前言
I/O 一直是很多小伙伴难以理解的一个知识点,这篇文章我会将我所理解的 I/O 讲给你听,希望可以对你有所帮助。
I/O
何为 I/O?
I/O(Input/Outpu) 即输入/输出 。
我们先从计算机结构的角度来解读一下 I/O。
根据冯.诺依曼结构,计算机结构分为 5 大部分:运算器、控制器、存储器、输入设备、输出设备。
输入设备(比如键盘)和输出设备(比如显示屏)都属于外部设备。网卡、硬盘这种既可以属于输入设备,也可以属于输出设备。
输入设备向计算机输入数据,输出设备接收计算机输出的数据。
从计算机结构的视角来看的话, I/O 描述了计算机系统与外部设备之间通信的过程。
我们再先从应用程序的角度来解读一下 I/O。
根据大学里学到的操作系统相关的知识:为了保证操作系统的稳定性和安全性,一个进程的地址空间划分为 用户空间(User space) 和 内核空间(Kernel space ) 。
像我们平常运行的应用程序都是运行在用户空间,只有内核空间才能进行系统态级别的资源有关的操作,比如如文件管理、进程通信、内存管理等等。也就是说,我们想要进行 IO 操作,一定是要依赖内核空间的能力。
并且,用户空间的程序不能直接访问内核空间。
当想要执行 IO 操作时,由于没有执行这些操作的权限,只能发起系统调用请求操作系统帮忙完成。
因此,用户进程想要执行 IO 操作的话,必须通过 系统调用 来间接访问内核空间
我们在平常开发过程中接触最多的就是 磁盘 IO(读写文件) 和 网络 IO(网络请求和相应)。
从应用程序的视角来看的话,我们的应用程序对操作系统的内核发起 IO 调用(系统调用),操作系统负责的内核执行具体的 IO 操作。也就是说,我们的应用程序实际上只是发起了 IO 操作的调用而已,具体 IO 的执行是由操作系统的内核来完成的。
当应用程序发起 I/O 调用后,会经历两个步骤:
- 内核等待 I/O 设备准备好数据
- 内核将数据从内核空间拷贝到用户空间。
有哪些常见的 IO 模型?
UNIX 系统下, IO 模型一共有 5 种: 同步阻塞 I/O、同步非阻塞 I/O、I/O 多路复用、信号驱动 I/O 和异步 I/O。
这也是我们经常提到的 5 种 IO 模型。
Java 中 3 种常见 IO 模型
BIO (Blocking I/O)
BIO 属于同步阻塞 IO 模型 。
同步阻塞 IO 模型中,应用程序发起 read 调用后,会一直阻塞,直到在内核把数据拷贝到用户空间。
在客户端连接数量不高的情况下,是没问题的。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
NIO (Non-blocking/New I/O)
Java 中的 NIO 于 Java 1.4 中引入,对应 java.nio 包,提供了 Channel , Selector,Buffer 等抽象。NIO 中的 N 可以理解为 Non-blocking,不单纯是 New。它支持面向缓冲的,基于通道的 I/O 操作方法。 对于高负载、高并发的(网络)应用,应使用 NIO 。
Java 中的 NIO 可以看作是 I/O 多路复用模型。也有很多人认为,Java 中的 NIO 属于同步非阻塞 IO 模型。
跟着我的思路往下看看,相信你会得到答案!
我们先来看看 同步非阻塞 IO 模型。
同步非阻塞 IO 模型中,应用程序会一直发起 read 调用,等待数据从内核空间拷贝到用户空间的这段时间里,线程依然是阻塞的,直到在内核把数据拷贝到用户空间。
相比于同步阻塞 IO 模型,同步非阻塞 IO 模型确实有了很大改进。通过轮询操作,避免了一直阻塞。
但是,这种 IO 模型同样存在问题:应用程序不断进行 I/O 系统调用轮询数据是否已经准备好的过程是十分消耗 CPU 资源的。
这个时候,I/O 多路复用模型 就上场了。
IO 多路复用模型中,线程首先发起 select 调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read 调用。read 调用的过程(数据从内核空间->用户空间)还是阻塞的。
目前支持 IO 多路复用的系统调用,有 select,epoll 等等。select 系统调用,是目前几乎在所有的操作系统上都有支持
- select 调用 :内核提供的系统调用,它支持一次查询多个系统调用的可用状态。几乎所有的操作系统都支持。
- epoll 调用 :linux 2.6 内核,属于 select 调用的增强版本,优化了 IO 的执行效率。
IO 多路复用模型,通过减少无效的系统调用,减少了对 CPU 资源的消耗。
Java 中的 NIO ,有一个非常重要的选择器 ( Selector ) 的概念,也可以被称为 多路复用器。通过它,只需要一个线程便可以管理多个客户端连接。当客户端数据到了之后,才会为其服务。
AIO (Asynchronous I/O)
AIO 也就是 NIO 2。Java 7 中引入了 NIO 的改进版 NIO 2,它是异步 IO 模型。
异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
目前来说 AIO 的应用还不是很广泛。Netty 之前也尝试使用过 AIO,不过又放弃了。这是因为,Netty 使用了 AIO 之后,在 Linux 系统上的性能并没有多少提升。
最后,来一张图,简单总结一下 Java 中的 BIO、NIO、AIO。
参考
- 《深入拆解 Tomcat & Jetty》
- 如何完成一次 IO:https://llc687.top/post/如何完成一次-io/
- 程序员应该这样理解 IO:https://www.jianshu.com/p/fa7bdc4f3de7
- 10 分钟看懂, Java NIO 底层原理:https://www.cnblogs.com/crazymakercircle/p/10225159.html
- IO 模型知多少 | 理论篇:https://www.cnblogs.com/sheng-jie/p/how-much-you-know-about-io-models.html
- 《UNIX 网络编程 卷 1;套接字联网 API 》6.2 节 IO 模型
Java 数据类型常见面试题总结
这篇文章绝对干货!文章涉及到的概念经常会被面试官拿来考察求职者的 Java 基础。
本篇采用大家比较喜欢的面试官问答的形式来展开。
基本数据类型
👨💻面试官 : Java 中有哪 8 种基本数据类型?
🙋 我 :Java 中有 8 种基本数据类型,分别为:
- 6 种数字类型 :byte、short、int、long、float、double
- 1 种字符类型:char
- 1 种布尔型:boolean。
👨💻面试官 : 它们的默认值和占用的空间大小知道不?
🙋 我 :这 8 种基本数据类型的默认值以及所占空间的大小如下:
基本类型 | 位数 | 字节 | 默认值 |
---|---|---|---|
int | 32 | 4 | 0 |
short | 16 | 2 | 0 |
long | 64 | 8 | 0L |
byte | 8 | 1 | 0 |
char | 16 | 2 | ‘u0000’ |
float | 32 | 4 | 0f |
double | 64 | 8 | 0d |
boolean | 1 | false |
另外,对于 boolean,官方文档未明确定义,它依赖于 JVM 厂商的具体实现。逻辑上理解是占用 1 位,但是实际中会考虑计算机高效存储因素。
注意:
- Java 里使用 long 类型的数据一定要在数值后面加上 L,否则将作为整型解析:
- char a = ‘h’char :单引号,String a = “hello” :双引号
包装类型
👨💻面试官 : 说说这 8 种基本数据类型对应的包装类型。
🙋 我 :这八种基本类型都有对应的包装类分别为:Byte、Short、Integer、Long、Float、Double、Character、Boolean
👨💻面试官 :那基本类型和包装类型有啥区别不?
🙋 我 :包装类型不赋值就是 Null ,而基本类型有默认值且不是 Null。
另外,这个问题建议还可以先从 JVM 层面来分析。
基本数据类型直接存放在 Java 虚拟机栈中的局部变量表中,而包装类型属于对象类型,我们知道对象实例都存在于堆中。相比于对象类型, 基本数据类型占用的空间非常小。
《深入理解 Java 虚拟机》 :局部变量表主要存放了编译期可知的基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。
包装类型的常量池技术
👨💻面试官 : 包装类型的常量池技术了解么?
🙋 我 : Java 基本类型的包装类的大部分都实现了常量池技术。
Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character 创建了数值在[0,127]范围的缓存数据,Boolean 直接返回 True Or False。
Integer 缓存源码:
1 | /** |
Character 缓存源码:
1 | public static Character valueOf(char c) { |
Boolean 缓存源码:
1 | public static Boolean valueOf(boolean b) { |
如果超出对应范围仍然会去创建新的对象,缓存的范围区间的大小只是在性能和资源之间的权衡。
两种浮点数类型的包装类 Float,Double 并没有实现常量池技术。
1 | Integer i1 = 33; |
下面我们来看一下问题。下面的代码的输出结果是 true 还是 flase 呢?
1 | Integer i1 = 40; |
Integer i1=40 这一行代码会发生装箱,也就是说这行代码等价于 Integer i1=Integer.valueOf(40) 。因此,i1 直接使用的是常量池中的对象。而Integer i1 = new Integer(40) 会直接创建新的对象。
因此,答案是 false 。你答对了吗?
记住:所有整型包装类对象之间值的比较,全部使用 equals 方法比较。
为什么要有包装类型?
👨💻面试官 : 为什么要有包装类型?
🙋 我 :
Java 本身就是一门 OOP(面向对象编程)语言,对象可以说是 Java 的灵魂。
除了定义一些常量和局部变量之外,我们在其他地方比如方法参数、对象属性中很少会使用基本类型来定义变量。
为什么呢?
我举个例子,假如你有一个对象中的属性使用了 基本类型,那这个属性就必然存在默认值了。这个逻辑不正确的!因为很多业务场景下,对象的某些属性没有赋值,我就希望它的值为 null。你给我默认赋个值,不是帮倒忙么?
另外,像泛型参数不能是基本类型。因为基本类型不是 Object 子类,应该用基本类型对应的包装类型代替。我们直接拿 JDK 中线程的代码举例。
Java 中的集合在定义类型的时候不能使用基本类型的。比如:
1 | public class HashMap<K,V> extends AbstractMap<K,V> |
自动拆装箱
什么是自动拆装箱?原理?
👨💻面试官 : 什么是自动拆装箱?原理了解么?
🙋 我 :
基本类型和包装类型之间的互转。举例:
1 | Integer i = 10; //装箱 |
上面这两行代码对应的字节码为:
1 | L1 |
从字节码中,我们发现装箱其实就是调用了 包装类的valueOf()方法,拆箱其实就是调用了 xxxValue()方法。
因此,
- Integer i = 10 等价于 Integer i = Integer.valueOf(10)
- int n = i 等价于 int n = i.intValue();
自动拆箱引发的 NPE 问题
👨💻面试官 : 自动拆箱可能会引发 NPE 问题,遇到过类似的场景么?
🙋 我 :
案例 1
在《阿里巴巴开发手册》上就有这样一条规定。
我们从上图可以看到,有一条是这样说的:“数据库的查询结果可能是 null,因为自动拆箱,用基本数据类型接收有 NPE 风险”。
我们来模拟一个实际的案例:
1 | public class AutoBoxTest { |
运行代码之后,果然出现了 NPE 的问题。
为什么会这样呢? 我们对 AutoBoxTest.class 进行反编译查看其字节码(我更推荐使用 IDEA 插件 jclasslib 来查看类的字节码)。
1 | javap -c AutoBoxTest.class |
反编译后得到的 should_Throw_NullPointerException() 方法的字节码如下:
1 | 0 aload_0 |
我们可以发现自动拆箱 Long -> long 的过程,不过是调用了 longValue() 方法罢了!
1 | public long longValue() { |
也就是说下面两行的代码实际是等价的:
1 | long id = getNum(); |
因为,getNum()返回的值为 null ,一个 null 值调用方法,当然会有 NPE 的问题了。
案例 2
通过上面的分析之后,我来考了一个不论是平时开发还是面试中都经常会碰到的一个问题:“三目运算符使用不当会导致诡异的 NPE 异常”。
请你回答下面的代码会有 NPE 问题出现吗?如果有 NPE 问题出现的话,原因是什么呢?你会怎么分析呢?
1 | public class Main { |
答案是会有 NPE 问题出现的。
我们还是通过查看其字节码来搞懂背后的原理(这里借助了 IDEA 插件 jclasslib 来查看类字节码)。
从字节码中可以看出,22 行的位置发生了 拆箱操作 。
详细解释下就是:flag ? 0 : i 这行代码中,0 是基本数据类型 int,返回数据的时候 i 会被强制拆箱成 int 类型,由于 i 的值是 null,因此就抛出了 NPE 异常。
1 | Integer i = null; |
如果,我们把代码中 flag 变量的值修改为 true 的话,就不会存在 NPE 问题了,因为会直接返回 0,不会进行拆箱操作。
我们在实际项目中应该避免这样的写法,正确 ✅ 修改之后的代码如下:
1 | Integer i = null; |
这个问题也在 《阿里巴巴开发手册》中 被提到过。
泛型&通配符常见面试题总结
泛型
什么是泛型?有什么作用?
Java 泛型(Generics) 是 JDK 5 中引入的一个新特性。使用泛型参数,可以增强代码的可读性以及稳定性。
编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型。比如 ArrayList<Persion> persons = new ArrayList<Persion>()
这行代码就指明了该 ArrayList 对象只能传入 Persion 对象,如果传入其他类型的对象就会报错。
1 | ArrayList<E> extends AbstractList<E> |
并且,原生 List 返回类型是 Object ,需要手动转换类型才能使用,使用泛型后编译器自动转换。
泛型的使用方式有哪几种?
泛型一般有三种使用方式:泛型类、泛型接口、泛型方法。
1.泛型类:
1 | //此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型 |
如何实例化泛型类:
1 | Generic<Integer> genericInteger = new Generic<Integer>(123456); |
2.泛型接口 :
1 | public interface Generator<T> { |
实现泛型接口,不指定类型:
1 | class GeneratorImpl<T> implements Generator<T>{ |
实现泛型接口,指定类型:
1 | class GeneratorImpl<T> implements Generator<String>{ |
3.泛型方法 :
1 | public static < E > void printArray( E[] inputArray ) |
使用:
1 | // 创建不同类型数组: Integer, Double 和 Character |
项目中哪里用到了泛型?
- 自定义接口通用返回结果 CommonResult
通过参数 T 可根据具体的返回类型动态指定结果的数据类型 - 定义 Excel 处理类 ExcelUtil
用于动态指定 Excel 导出的数据类型 - 构建集合工具类(参考 Collections 中的 sort, binarySearch 方法)。
- ……
什么是泛型擦除机制?为什么要擦除?
Java 的泛型是伪泛型,这是因为 Java 在编译期间,所有的泛型信息都会被擦掉,这也就是通常所说类型擦除 。
编译器会在编译期间会动态地将泛型 T 擦除为 Object 或将 T extends xxx 擦除为其限定类型 xxx 。
因此,泛型本质上其实还是编译器的行为,为了保证引入泛型机制但不创建新的类型,减少虚拟机的运行开销,编译器通过擦除将泛型类转化为一般类。
这里说的可能有点抽象,我举个例子:
1 | List<Integer> list = new ArrayList<>(); |
再来举一个例子 : 由于泛型擦除的问题,下面的方法重载会报错。
1 | public void print(List<String> list) { } |
原因也很简单,泛型擦除之后,List
既然编译器要把泛型擦除,那为什么还要用泛型呢?用 Object 代替不行吗?
这个问题其实在变相考察泛型的作用:
- 使用泛型可在编译期间进行类型检测。
- 使用 Object 类型需要手动添加强制类型转换,降低代码可读性,提高出错概率。
- 泛型可以使用自限定类型如 T extends Comparable 。
什么是桥方法?
桥方法(Bridge Method) 用于继承泛型类时保证多态。
1 | class Node<T> { |
⚠️注意 :桥方法为编译器自动生成,非手写。
泛型有哪些限制?为什么?
泛型的限制一般是由泛型擦除机制导致的。擦除为 Object 后无法进行类型判断
- 只能声明不能实例化 T 类型变量。
- 泛型参数不能是基本类型。因为基本类型不是 Object 子类,应该用基本类型对应的引用类型代替。
- 不能实例化泛型参数的数组。擦除后为 Object 后无法进行类型判断。
- 不能实例化泛型数组。
- 泛型无法使用 Instance of 和 getClass() 进行类型判断。
- 不能实现两个不同泛型参数的同一接口,擦除后多个父类的桥方法将冲突
- 不能使用 static 修饰泛型变量
- ……
以下代码是否能编译,为什么?
1 | public final class Algorithm { |
无法编译,因为 x 和 y 都会被擦除为Object
类型,Object
无法使用>
进行比较
1 | public class Singleton<T> { |
无法编译,因为不能使用 static
修饰泛型T
。
通配符
什么是通配符?有什么作用?
泛型类型是固定的,某些场景下使用起来不太灵活,于是,通配符就来了!通配符可以允许类型参数变化,用来解决泛型无法协变的问题。
举个例子:
1 | // 限制类型为 Person 的子类 |
通配符 ?和常用的泛型 T 之间有什么区别?
- T 可以用于声明变量或常量而 ? 不行。
- T 一般用于声明泛型类或方法,通配符 ? 一般用于泛型方法的调用代码和形参。
- T 在编译期会被擦除为限定类型或 Object,通配符用于捕获具体类型。
什么是无界通配符?
无界通配符可以接收任何泛型类型数据,用于实现不依赖于具体类型参数的简单方法,可以捕获参数类型并交由泛型方法进行处理。
1 | void testMethod(Person<?> p) { |
List<?> 和 List 有区别吗? 当然有!
- List<?> list 表示 list 是持有某种特定类型的 List,但是不知道具体是哪种类型。因此,我们添加元素进去的时候会报错。
- List list 表示 list 是持有的元素的类型是 Object,因此可以添加任何类型的对象,只不过编译器会有警告信息。
1 | List<?> list = new ArrayList<>(); |
什么是上边界通配符?什么是下边界通配符?
在使用泛型的时候,我们还可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。
上边界通配符 extends 可以实现泛型的向上转型即传入的类型实参必须是指定类型的子类型。
举个例子:
1 | // 限制必须是 Person 类的子类 |
类型边界可以设置多个,还可以对 T 类型进行限制。
1 | <T extends T1 & T2> |
下边界通配符 super 与上边界通配符 extends刚好相反,它可以实现泛型的向下转型即传入的类型实参必须是指定类型的父类型。
举个例子:
1 | // 限制必须是 Employee 类的父类 |
? extends xxx
和? super xxx
有什么区别?
两者接收参数的范围不同。并且,使用 ? extends xxx 声明的泛型参数只能调用 get() 方法返回 xxx 类型,调用 set() 报错。使用 ? super xxx 声明的泛型参数只能调用 set() 方法接收 xxx 类型,调用 get() 报错。
T extends xxx
和? extends xxx
又有什么区别?
T extends xxx 用于定义泛型类和方法,擦除后为 xxx 类型, ? extends xxx 用于声明方法形参,接收 xxx 和其子类型。
Class<?>
和Class
的区别?
直接使用 Class 的话会有一个类型警告,使用 Class<?> 则没有,因为 Class 是一个泛型类,接收原生类型会产生警告
以下代码是否能编译,为什么?
1 | class Shape { /* ... */ } |
不能,因为Node<Circle>
不是 Node<Shape>
的子类
1 | class Shape { /* ... */ } |
可以编译,ChildNode<Circle>
是 Node<Circle>
的子类
1 | public static void print(List<? extends Number> list) { |
可以编译,List<? extends Number>
可以往外取元素,但是无法调用add()
添加元素。
参考
- Java 官方文档 : https://docs.oracle.com/javase/tutorial/java/generics/index.html
- Java 基础 一文搞懂泛型:https://www.cnblogs.com/XiiX/p/14719568.html
String 类常见面试题总结
这篇文章是我的一位好朋友 Hydra(公众号码农参上号主)写的原创干货,经他同意,我将其整理到了 《Java 面试指北》的 Java 部分。
String 字符串是我们日常工作中常用的一个类,在面试中也是高频考点,这里精心总结了一波常见但也有点烧脑的 String 面试题,一共 5 道题,难度从简到难,来一起来看看你能做对几道吧。
说明 :本文基于jdk8版本中的 String 进行讨论,文章例子中的代码运行结果基于Java 1.8.0_261-b12
第 1 题,奇怪的 nullnull
下面这段代码最终会打印什么?
1 | public class Test1 { |
运行之后,你会发现打印了nullnull
:
在分析这个结果之前,先扯点别的,说一下为空null
的字符串的打印原理。查看一下PrintStream
类的源码,print
方法在打印null
前进行了处理:
1 | public void print(String s) { |
因此,一个为null的字符串就可以被打印在我们的控制台上了。
再回头看上面这道题,s1
和s2
没有经过初始化所以都是空对象null,需要注意这里不是字符串的”null”,打印结果的产生我们可以看一下字节码文件:
编译器会对String
字符串相加的操作进行优化,会把这一过程转化为StringBuilder
的append
方法。那么,让我们再看看append
方法的源码:
1 | public AbstractStringBuilder append(String str) { |
如果append
方法的参数字符串为null
,那么这里会调用其父类AbstractStringBuilder
的appendNull
方法:
1 | private AbstractStringBuilder appendNull() { |
这里的value
就是底层用来存储字符的char
类型数组,到这里我们就可以明白了,其实StringBuilder
也对null
的字符串进行了特殊处理,在append
的过程中如果碰到是null
的字符串,那么就会以"null"
的形式被添加进字符数组,这也就导致了两个为空null
的字符串相加后会打印为"nullnull"
。
第 2 题,改变 String 的值
如何改变一个 String 字符串的值,这道题可能看上去有点太简单了,像下面这样直接赋值不就可以了吗?
1 | String s="Hydra"; |
恭喜你,成功掉进了坑里!在回答这道题之前,我们需要知道 String 是不可变的,打开 String 的源码在开头就可以看到:
1 | private final char value[]; |
可以看到,String
的本质其实是一个char
类型的数组,然后我们再看两个关键字。先看final,我们知道final在修饰引用数据类型时,就像这里的数组时,能够保证指向该数组地址的引用不能修改,但是数组本身内的值可以被修改。
是不是有点晕,没关系,我们看一个例子:
1 | final char[] one={'a','b','c'}; |
如果你这样写,那么编译器是会报错提示Cannot assign a value to final variable 'one'
,说明被final修饰的数组的引用地址是不可改变的。但是下面这段代码却能够正常的运行:
1 | final char[] one={'a','b','c'}; |
也就是说,即使被final修饰,但是我直接操作数组里的元素还是可以的,所以这里还加了另一个关键字private,防止从外部进行修改。此外,String 类本身也被添加了final关键字修饰,防止被继承后对属性进行修改。
到这里,我们就可以理解为什么 String 是不可变的了,那么在上面的代码进行二次赋值的过程中,发生了什么呢?答案很简单,前面的变量s只是一个 String 对象的引用,这里的重新赋值时将变量s指向了新的对象。
上面白话了一大顿,其实是我们可以通过比较hashCode
的方式来看一下引用指向的对象是否发生了改变,修改一下上面的代码,打印字符串的hashCode
:
1 | public static void main(String[] args) { |
查看结果,发生了改变,证明指向的对象发生了改变:
那么,回到上面的问题,如果我想要改变一个 String 的值,而又不想把它重新指向其他对象的话,应该怎么办呢?答案是利用反射修改char数组的值:
1 | public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException { |
再对比一下hashCode
,修改后和之前一样,对象没有发生任何变化:
最后,再啰嗦说一点题外话,这里看的是jdk8
中String
的源码,到这为止还是使用的char
类型数组来存储字符,但是在jdk9
中这个char
数组已经被替换成了byte
数组,能够使String
对象占用的内存减少。
第 3 题,创建了几个对象?
相信不少小伙伴在面试中都遇到过这道经典面试题,下面这段代码中到底创建了几个对象?
1 | String s = new String("Hydra"); |
其实真正想要回答好这个问题,要铺垫的知识点还真是不少。首先,我们需要了解 3 个关于常量池的概念,下面还是基于jdk8版本进行说明:
- class 文件常量池:在 class 文件中保存了一份常量池(Constant Pool),主要存储编译时确定的数据,包括代码中的字面量(literal)和符号引用
- 运行时常量池:位于方法区中,全局共享,class 文件常量池中的内容会在类加载后存放到方法区的运行时常量池中。除此之外,在运行期间可以将新的变量放入运行时常量池中,相对 class 文件常量池而言运行时常量池更具备动态性
- 字符串常量池:位于堆中,全局共享,这里可以先粗略的认为它存储的是 String 对象的直接引用,而不是直接存放的对象,具体的实例对象是在堆中存放
可以用一张图来描述它们各自所处的位置:
接下来,我们来细说一下字符串常量池的结构,其实在 Hotspot JVM 中,字符串常量池StringTable的本质是一张HashTable,那么当我们说将一个字符串放入字符串常量池的时候,实际上放进去的是什么呢?
以字面量的方式创建 String 对象为例,字符串常量池以及堆栈的结构如下图所示(忽略了 jvm 中的各种OopDesc实例):
实际上字符串常量池HashTable
采用的是数组加链表的结构,链表中的节点是一个个的HashTableEntry
,而HashTableEntry
中的value
则存储了堆上String
对象的引用。
那么,下一个问题来了,这个字符串对象的引用是什么时候被放到字符串常量池中的?具体可为两种情况:
- 使用字面量声明 String 对象时,也就是被双引号包围的字符串,在堆上创建对象,并驻留到字符串常量池中(注意这个用词)
- 调用intern()方法,当字符串常量池没有相等的字符串时,会保存该字符串的引用
注意!我们在上面用到了一个词驻留,这里对它进行一下规范。当我们说驻留一个字符串到字符串常量池时,指的是创建HashTableEntry
,再使它的value
指向堆上的 String 实例,并把HashTableEntry
放入字符串常量池,而不是直接把 String 对象放入字符串常量池中。简单来说,可以理解为将 String 对象的引用保存在字符串常量池中。
我们把intern()
方法放在后面细说,先主要看第一种情况,这里直接整理引用 R 大的结论:
在类加载阶段,JVM 会在堆中创建对应这些 class 文件常量池中的字符串对象实例,并在字符串常量池中驻留其引用。
这一过程具体是在 resolve 阶段(个人理解就是 resolution 解析阶段)执行,但是并不是立即就创建对象并驻留了引用,因为在 JVM 规范里指明了 resolve 阶段可以是 lazy 的。CONSTANT_String 会在第一次引用该项的 ldc 指令被第一次执行到的时候才会 resolve。
就 HotSpot VM 的实现来说,加载类时字符串字面量会进入到运行时常量池,不会进入全局的字符串常量池,即在 StringTable 中并没有相应的引用,在堆中也没有对应的对象产生。
这里大家可以暂时先记住这个结论,在后面还会用到。
在弄清楚上面几个概念后,我们再回过头来,先看看用字面量声明 String 的方式,代码如下:
1 | public static void main(String[] args) { |
反编译生成的字节码文件:
1 | public static void main(java.lang.String[]); |
解释一下上面的字节码指令:
- 0: ldc,查找后面索引为#2对应的项,#2表示常量在常量池中的位置。在这个过程中,会触发前面提到的lazy resolve,在 resolve 过程如果发现StringTable已经有了内容匹配的 String 引用,则直接返回这个引用,反之如果StringTable里没有内容匹配的 String 对象的引用,则会在堆里创建一个对应内容的 String 对象,然后在StringTable驻留这个对象引用,并返回这个引用,之后再压入操作数栈中
- 2: astore_1,弹出栈顶元素,并将栈顶引用类型值保存到局部变量 1 中,也就是保存到变量s中
- 3: return,执行void函数返回
可以看到,在这种模式下,只有堆中创建了一个"Hydra"
对象,在字符串常量池中驻留了它的引用。并且,如果再给字符串s2、s3
也用字面量的形式赋值为"Hydra"
,它们用的都是堆中的唯一这一个对象。
好了,再看一下以构造方法的形式创建字符串的方式:
1 | public static void main(String[] args) { |
同样反编译这段代码的字节码文件:
1 | public static void main(java.lang.String[]); |
看一下和之前不同的字节码指令部分:
- 0: new,在堆上创建一个 String 对象,并将它的引用压入操作数栈,注意这时的对象还只是一个空壳,并没有调用类的构造方法进行初始化
- 3: dup,复制栈顶元素,也就是复制了上面的对象引用,并将复制后的对象引用压入栈顶。这里之所以要进行复制,是因为之后要执行的构造方法会从操作数栈弹出需要的参数和这个对象引用本身(这个引用起到的作用就是构造方法中的this指针),如果不进行复制,在弹出后会无法得到初始化后的对象引用
- 4: ldc,在堆上创建字符串对象,驻留到字符串常量池,并将字符串的引用压入操作数栈
- 6: invokespecial,执行 String 的构造方法,这一步执行完成后得到一个完整对象
到这里,我们可以看到一共创建了两个String 对象,并且两个都是在堆上创建的,且字面量方式创建的 String 对象的引用被驻留到了字符串常量池中。而栈里的s只是一个变量,并不是实际意义上的对象,我们不把它包括在内。
其实想要验证这个结论也很简单,可以使用 idea 中强大的 debug 功能来直观的对比一下对象数量的变化,先看字面量创建 String 方式:
这个对象数量的计数器是在 debug 时,点击下方右侧Memory的Load classes弹出的。对比语句执行前后可以看到,只创建了一个 String 对象,以及一个 char 数组对象,也就是 String 对象中的value。
再看看构造方法创建 String 的方式:
可以看到,创建了两个 String 对象,一个 char 数组对象,也说明了两个 String 中的value指向了同一个 char 数组对象,符合我们上面从字节码指令角度解释的结果。
最后再看一下下面的这种情况,当字符串常量池已经驻留过某个字符串引用,再使用构造方法创建 String 时,创建了几个对象?
1 | public static void main(String[] args) { |
答案是只创建一个对象,对于这种重复字面量的字符串,看一下反编译后的字节码指令:
1 | Code: |
可以看到两次执行ldc
指令时后面索引相同,而ldc
判断是否需要创建新的 String 实例的依据是根据在第一次执行这条指令时,StringTable
是否已经保存了一个对应内容的 String 实例的引用。所以在第一次执行ldc
时会创建 String 实例,而在第二次ldc
就会直接返回而不需要再创建实例了。
第 4 题,烧脑的 intern
上面我们在研究字符串对象的引用如何驻留到字符串常量池中时,还留下了调用intern方法的方式,下面我们来具体分析。
从字面上理解intern这个单词,作为动词时它有禁闭、关押的意思,通过前面的介绍,与其说是将字符串关押到字符串常量池StringTable中,可能将它理解为缓存它的引用会更加贴切。
String 的intern()是一个本地方法,可以强制将 String 驻留进入字符串常量池,可以分为两种情况:
- 如果字符串常量池中已经驻留了一个等于此 String 对象内容的字符串引用,则返回此字符串在常量池中的引用
- 否则,在常量池中创建一个引用指向这个 String 对象,然后返回常量池中的这个引用
好了,我们下面看一下这段代码,它的运行结果应该是什么?
1 | public static void main(String[] args) { |
输出打印:
1 | false |
用一张图来描述它们的关系,就很容易明白了:
其实有了第三题的基础,了解这个结构已经很简单了:
- 在创建s1的时候,其实堆里已经创建了两个字符串对象StringObject1和StringObject2,并且在字符串常量池中驻留了StringObject2
- 当执行s1.intern()方法时,字符串常量池中已经存在内容等于”Hydra”的字符串StringObject2,直接返回这个引用并赋值给s2
- s1和s2指向的是两个不同的 String 对象,因此返回 fasle
- s2指向的就是驻留在字符串常量池的StringObject2,因此s2==”Hydra”为 true,而s1指向的不是常量池中的对象引用所以返回 false
上面是常量池中已存在内容相等的字符串驻留的情况,下面再看看常量池中不存在的情况,看下面的例子:
1 | public static void main(String[] args) { |
执行结果:
1 | true |
简单分析一下这个过程,第一步会在堆上创建"Hy"
和"dra"
的字符串对象,并驻留到字符串常量池中。
接下来,完成字符串的拼接操作,前面我们说过,实际上 jvm 会把拼接优化成StringBuilder
的append
方法,并最终调用toString
方法返回一个 String 对象。在完成字符串的拼接后,字符串常量池中并没有驻留一个内容等于"Hydra"
的字符串。
所以,执行s1.intern()
时,会在字符串常量池创建一个引用,指向前面StringBuilder
创建的那个字符串,也就是变量s1
所指向的字符串对象。在《深入理解 Java 虚拟机》这本书中,作者对这进行了解释,因为从 jdk7 开始,字符串常量池就已经移到了堆中,那么这里就只需要在字符串常量池中记录一下首次出现的实例引用即可。
最后,当执行String s2 = "Hydra"
时,发现字符串常量池中已经驻留这个字符串,直接返回对象的引用,因此s1
和s2
指向的是相同的对象。
第 5 题,还是创建了几个对象?
解决了前面数 String 对象个数的问题,那么我们接着加点难度,看看下面这段代码,创建了几个对象?
1 | String s="a"+"b"+"c"; |
先揭晓答案,只创建了一个对象! 可以直观的对比一下源代码和反编译后的字节码文件:
如果使用前面提到过的 debug 小技巧,也可以直观的看到语句执行完后,只增加了一个 String 对象,以及一个 char 数组对象。并且这个字符串就是驻留在字符串常量池中的那一个,如果后面再使用字面量”abc”的方式声明一个字符串,指向的仍是这一个,堆中 String 对象的数量不会发生变化。
至于为什么源代码中字符串拼接的操作,在编译完成后会消失,直接呈现为一个拼接后的完整字符串,是因为在编译期间,应用了编译器优化中一种被称为常量折叠(Constant Folding)的技术。
常量折叠会将编译期常量的加减乘除的运算过程在编译过程中折叠。编译器通过语法分析,会将常量表达式计算求值,并用求出的值来替换表达式,而不必等到运行期间再进行运算处理,从而在运行期间节省处理器资源。
而上边提到的编译期常量的特点就是它的值在编译期就可以确定,并且需要完整满足下面的要求,才可能是一个编译期常量:
- 被声明为final
- 基本类型或者字符串类型
- 声明时就已经初始化
- 使用常量表达式进行初始化
下面我们通过几段代码加深对它的理解:
1 | public static void main(String[] args) { |
执行结果:
1 | true |
代码中字符串h1
和h2
都使用常量赋值,区别在于是否使用了final
进行修饰,对比编译后的代码,s1
进行了折叠而s2
没有,可以印证上面的理论,final
修饰的字符串变量才有可能是编译期常量。
再看一段代码,执行下面的程序,结果会返回什么呢?
1 | public static void main(String[] args) { |
答案是false
,因为虽然这里字符串h2被final修饰,但是初始化时没有使用常量表达式,因此它也不是编译期常量。那么,有的小伙伴就要问了,到底什么才是常量表达式呢?
在Oracle官网的文档中,列举了很多种情况,下面对常见的情况进行列举(除了下面这些之外官方文档上还列举了不少情况,如果有兴趣的话,可以自己查看):
基本类型和 String 类型的字面量
基本类型和 String 类型的强制类型转换
- 使用+或-或!等一元运算符(不包括++和—)进行计算
- 使用加减运算符+、-,乘除运算符*、 / 、% 进行计算
- 使用移位运算符 >>、 <<、 >>>进行位移操作
- ……
至于我们从文章一开始就提到的字面量(literals),是用于表达源代码中一个固定值的表示法,在 Java 中创建一个对象时需要使用new关键字,但是给一个基本类型变量赋值时不需要使用new关键字,这种方式就可以被称为字面量。Java 中字面量主要包括了以下类型的字面量:
1 | //整数型字面量: |
再说点题外话,和编译期常量相对的,另一种类型的常量是运行时常量,看一下下面这段代码:
1 | final String s1="hello "+"Hydra"; |
编译器能够在编译期就得到s1
的值是hello Hydra
,不需要等到程序的运行期间,因此s1
属于编译期常量。而对s2
来说,虽然也被声明为final
类型,并且在声明时就已经初始化,但使用的不是常量表达式,因此不属于编译期常量,这一类型的常量被称为运行时常量。
再看一下编译后的字节码文件中的常量池区域:
可以看到常量池中只有一个 String 类型的常量hello Hydra
,而s2
对应的字符串常量则不在此区域。对编译器来说,运行时常量在编译期间无法进行折叠,编译器只会对尝试修改它的操作进行报错处理。
总结
最后再强调一下,本文是基于jdk8进行测试,不同版本的jdk可能会有很大差异。例如jdk6之前,字符串常量池存储的是 String 对象实例,而在jdk7以后字符串常量池就改为存储引用,做了非常大的改变。
至于最后一题,其实 Hydra 在以前单独拎出来写过一篇文章,这次总结面试题把它归纳在了里面,省略了一些不重要的部分,大家如果觉得不够详细可以移步看看这篇:String s=”a”+”b”+”c”,到底创建了几个对象?
那么,这次的分享就写到这里,我是 Hydra,我们下篇再见~
参考资料
《深入理解 Java 虚拟机(第三版)》
数据库
MySQL 日志:常见的日志都有什么用?
MySQL 中常见的日志有哪些?
MySQL 中常见的日志类型主要有下面几类(针对的是 InnoDB 存储引擎):
错误日志(error log) :对 MySQL 的启动、运行、关闭过程进行了记录。
二进制日志(binary log) :主要记录的是更改数据库数据的 SQL 语句。
一般查询日志(general query log) :已建立连接的客户端发送给 MySQL 服务器的所有 SQL 记录,因为 SQL 的量比较大,默认是不开启的,也不建议开启。
慢查询日志(slow query log) :执行时间超过 long_query_time秒钟的查询,解决 SQL 慢查询问题的时候会用到。
事务日志(redo log 和 undo log) :redo log 是重做日志,undo log 是回滚日志。
中继日志(relay log) :relay log 是复制过程中产生的日志,很多方面都跟 binary log 差不多。不过,relay log 针对的是主从复制中的从库。
DDL 日志(metadata log) :DDL 语句执行的元数据操作。
二进制日志 binlog (归档日志)和事务日志(redo log 和 undo log)比较重要,需要我们重点关注。
慢查询日志有什么用?
慢查询日志记录了执行时间超过 long_query_time(默认是 10s)的所有查询,在我们解决 SQL 慢查询(SQL 执行时间过长)问题的时候经常会用到。
慢查询日志默认是关闭的,我们可以通过下面的命令将其开启:
1 | SET GLOBAL slow_query_log=ON |
long_query_time
参数定义了一个查询消耗多长时间才可以被定义为慢查询,默认是 10s,通过SHOW VARIABLES LIKE '%long_query_time%'
命令即可查看:
1 | mysql> SHOW VARIABLES LIKE '%long_query_time%'; |
并且,我们还可以对 long_query_time
参数进行修改:
1 | SET GLOBAL long_query_time=1 |
在实际项目中,慢查询日志可能会比较大,直接分析的话不太方便,我们可以借助 MySQL 官方的慢查询分析调优工具 mysqldumpslow。
binlog 主要记录了什么?
MySQL binlog(binary log 即二进制日志文件) 主要记录了 MySQL 数据库中数据的所有变化(数据库执行的所有 DDL 和 DML 语句)。
binlog 有一个比较常见的应用场景就是主从复制,MySQL 主从复制依赖于 binlog 。另外,常见的一些同步 MySQL 数据到其他数据源的工具(比如 canal)的底层一般也是依赖 binlog 。
binlog 通过追加的方式进行写入,大小没有限制。并且,我们可以通过max_binlog_size参数设置每个 binlog 文件的最大容量,当文件大小达到给定值之后,会生成新的 binlog 文件来保存日志,不会出现前面写的日志被覆盖的情况。
关于主从复制的具体步骤和原理,推荐看看我写的读写分离&分库分表这篇文章。
redo log 如何保证事务的持久性?
我们知道 InnoDB 存储引擎是以页为单位来管理存储空间的,我们往 MySQL 插入的数据最终都是存在于页中的,准确点来说是数据页这种类型。为了减少磁盘 IO 开销,还有一个叫做 Buffer Pool(缓冲池) 的区域,存在于内存中。当我们的数据对应的页不存在于 Buffer Pool 中的话, MySQL 会先将磁盘上的页缓存到 Buffer Pool 中,这样后面我们直接操作的就是 Buffer Pool 中的页,这样大大提高了读写性能。
一个事务提交之后,我们对 Buffer Pool 中对应的页的修改可能还未持久化到磁盘。这个时候,如果 MySQL 突然宕机的话,这个事务的更改是不是直接就消失了呢?
很显然是不会的,如果是这样的话就明显违反了事务的持久性。
MySQL InnoDB 引擎使用 redo log 来保证事务的持久性。redo log 主要做的事情就是记录页的修改,比如某个页面某个偏移量处修改了几个字节的值以及具体被修改的内容是什么。redo log 中的每一条记录包含了表空间号、数据页号、偏移量、具体修改的数据,甚至还可能会记录修改数据的长度(取决于 redo log 类型)。
在事务提交时,我们会将 redo log 按照刷盘策略刷到磁盘上去,这样即使 MySQL 宕机了,重启之后也能恢复未能写入磁盘的数据,从而保证事务的持久性。也就是说,redo log 让 MySQL 具备了崩溃回复能力。
不过,我们也要注意设置正确的刷盘策略innodb_flush_log_at_trx_commit
,根据 MySQL 配置的刷盘策略的不同,MySQL 宕机之后可能会存在轻微的数据丢失问题。
刷盘策略innodb_flush_log_at_trx_commit
的默认值为 1,设置为 1 的时候才不会丢失任何数据。为了保证事务的持久性,我们必须将其设置为 1。
下图是 MySQL 5.7 官方文档对于innodb_flush_log_at_trx_commit
参数的详细介绍,我这里就不做过多阐述了。
redo log 采用循环写的方式进行写入,大小固定,当写到结尾时,会回到开头循环写日志,会出现前面写的日志被覆盖的情况。
页修改之后为什么不直接刷盘呢?
很多人可能要问了:为什么每次修改 Buffer Pool 中的页之后不直接刷盘呢?这样不就不需要 redo log 了嘛!
这种方式必然是不行的,性能非常差。最大的问题就是 InnoDB 页的大小一般为 16KB,而页又是磁盘和内存交互的基本单位。这就导致即使我们只修改了页中的几个字节数据,一次刷盘操作也需要将 16KB 大小的页整个都刷新到磁盘中。而且,这些修改的页可能并不相邻,也就是说这还是随机 IO。
采用 redo log 的方式就可以避免这种性能问题,因为 redo log 的刷盘性能很好。首先,redo log 的写入属于顺序 IO。 其次,一行 redo log 记录只占几十个字节。
另外,Buffer Pool 中的页(脏页)在某些情况下(比如 redo log 快写满了)也会进行刷盘操作。不过,这里的刷盘操作会合并写入,更高效地顺序写入到磁盘。
binlog 和 redolog 有什么区别?
- binlog 主要用于数据库还原,属于数据级别的数据恢复,主从复制是 binlog 最常见的一个应用场景。redolog 主要用于保证事务的持久性,属于事务级别的数据恢复。
- redolog 属于 InnoDB 引擎特有的,binlog 属于所有存储引擎共有的,因为 binlog 是 MySQL 的 Server 层实现的。
- redolog 属于物理日志,主要记录的是某个页的修改。binlog 属于逻辑日志,主要记录的是数据库执行的所有 DDL 和 DML 语句。
- binlog 通过追加的方式进行写入,大小没有限制。redo log 采用循环写的方式进行写入,大小固定,当写到结尾时,会回到开头循环写日志。
- ……
undo log 如何保证事务的原子性?
每一个事务对数据的修改都会被记录到 undo log ,当执行事务过程中出现错误或者需要执行回滚操作的话,MySQL 可以利用 undo log 将数据恢复到事务开始之前的状态。
undo log 属于逻辑日志,记录的是 SQL 语句,比如说事务执行一条 DELETE 语句,那 undo log 就会记录一条相对应的 INSERT 语句。
MySQL 索引:索引为什么使用 B+树?
相关面试题 :
- MySQL 的索引结构为什么使用 B+树?
- 红黑树适合什么场景?
在 MySQL 中,无论是 Innodb 还是 MyIsam,都使用了 B+树作索引结构(这里不考虑 hash 等其他索引)。本文将从最普通的二叉查找树开始,逐步说明各种树解决的问题以及面临的新问题,从而说明 MySQL 为什么选择 B+树作为索引结构。
二叉查找树(BST):不平衡
二叉查找树(BST,Binary Search Tree),也叫二叉排序树,在二叉树的基础上需要满足:任意节点的左子树上所有节点值不大于根节点的值,任意节点的右子树上所有节点值不小于根节点的值。如下是一颗 BST(图片来源)。
当需要快速查找时,将数据存储在 BST 是一种常见的选择,因为此时查询时间取决于树高,平均时间复杂度是 O(lgn)。然而,BST可能长歪而变得不平衡,如下图所示(图片来源),此时 BST 退化为链表,时间复杂度退化为 O(n)。
为了解决这个问题,引入了平衡二叉树。
平衡二叉树(AVL):旋转耗时
AVL 树是严格的平衡二叉树,所有节点的左右子树高度差不能超过 1;AVL 树查找、插入和删除在平均和最坏情况下都是 O(lgn)。
AVL 实现平衡的关键在于旋转操作:插入和删除可能破坏二叉树的平衡,此时需要通过一次或多次树旋转来重新平衡这个树。当插入数据时,最多只需要 1 次旋转(单旋转或双旋转);但是当删除数据时,会导致树失衡,AVL 需要维护从被删除节点到根节点这条路径上所有节点的平衡,旋转的量级为 O(lgn)。
由于旋转的耗时,AVL树在删除数据时效率很低;在删除操作较多时,维护平衡所需的代价可能高于其带来的好处,因此 AVL 实际使用并不广泛。
红黑树:树太高
与 AVL 树相比,红黑树并不追求严格的平衡,而是大致的平衡:只是确保从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。从实现来看,红黑树最大的特点是每个节点都属于两种颜色(红色或黑色)之一,且节点颜色的划分需要满足特定的规则(具体规则略)。红黑树示例如下(图片来源):
与 AVL 树相比,红黑树的查询效率会有所下降,这是因为树的平衡性变差,高度更高。但红黑树的删除效率大大提高了,因为红黑树同时引入了颜色,当插入或删除数据时,只需要进行 O(1)次数的旋转以及变色就能保证基本的平衡,不需要像 AVL 树进行 O(lgn)次数的旋转。总的来说,红黑树的统计性能高于 AVL。
因此,在实际应用中,AVL 树的使用相对较少,而红黑树的使用非常广泛。例如,Java 中的 TreeMap 使用红黑树存储排序键值对;Java8 中的 HashMap 使用链表+红黑树解决哈希冲突问题(当冲突节点较少时,使用链表,当冲突节点较多时,使用红黑树)。
对于数据在内存中的情况(如上述的 TreeMap 和 HashMap),红黑树的表现是非常优异的。但是对于数据在磁盘等辅助存储设备中的情况(如MySQL等数据库),红黑树并不擅长,因为红黑树长得还是太高了。当数据在磁盘中时,磁盘 IO 会成为最大的性能瓶颈,设计的目标应该是尽量减少 IO 次数;而树的高度越高,增删改查所需要的 IO 次数也越多,会严重影响性能。
B 树:为磁盘而生
B 树也称 B-树(其中-不是减号),是为磁盘等辅存设备设计的多路平衡查找树,与二叉树相比,B 树的每个非叶节点可以有多个子树。 因此,当总节点数量相同时,B 树的高度远远小于 AVL 树和红黑树(B 树是一颗“矮胖子”),磁盘 IO 次数大大减少。
定义 B 树最重要的概念是阶数(Order),对于一颗 m 阶 B 树,需要满足以下条件:
- 每个节点最多包含 m 个子节点。
- 如果根节点包含子节点,则至少包含 2 个子节点;除根节点外,每个非叶节点至少包含 m/2 个子节点。
- 拥有 k 个子节点的非叶节点将包含 k - 1 条记录。
- 所有叶节点都在同一层中。
可以看出,B 树的定义,主要是对非叶结点的子节点数量和记录数量的限制。
下图是一个 3 阶 B 树的例子(图片来源):
B 树的优势除了树高小,还有对访问局部性原理的利用。所谓局部性原理,是指当一个数据被使用时,其附近的数据有较大概率在短时间内被使用。B 树将键相近的数据存储在同一个节点,当访问其中某个数据时,数据库会将该整个节点读到缓存中;当它临近的数据紧接着被访问时,可以直接在缓存中读取,无需进行磁盘 IO;换句话说,B 树的缓存命中率更高。
B 树在数据库中有一些应用,如 mongodb 的索引使用了 B 树结构。但是在很多数据库应用中,使用了是 B 树的变种 B+树。
B+树
B+树也是多路平衡查找树,其与 B 树的区别主要在于:
- B 树中每个节点(包括叶节点和非叶节点)都存储真实的数据,B+树中只有叶子节点存储真实的数据,非叶节点只存储键。在 MySQL 中,这里所说的真实数据,可能是行的全部数据(如 Innodb 的聚簇索引),也可能只是行的主键(如 Innodb 的辅助索引),或者是行所在的地址(如 MyIsam 的非聚簇索引)。
- B 树中一条记录只会出现一次,不会重复出现,而 B+树的键则可能重复重现——一定会在叶节点出现,也可能在非叶节点重复出现。
- B+树的叶节点之间通过双向链表链接。
- B 树中的非叶节点,记录数比子节点个数少 1;而 B+树中记录数与子节点个数相同。
由此,B+树与 B 树相比,有以下优势:
- 更少的 IO 次数:B+树的非叶节点只包含键,而不包含真实数据,因此每个节点存储的记录个数比 B 数多很多(即阶 m 更大),因此 B+树的高度更低,访问时所需要的 IO 次数更少。此外,由于每个节点存储的记录数更多,所以对访问局部性原理的利用更好,缓存命中率更高。
- 更适于范围查询:在 B 树中进行范围查询时,首先找到要查找的下限,然后对 B 树进行中序遍历,直到找到查找的上限;而 B+树的范围查询,只需要对链表进行遍历即可。
- 更稳定的查询效率:B 树的查询时间复杂度在 1 到树高之间(分别对应记录在根节点和叶节点),而 B+树的查询复杂度则稳定为树高,因为所有数据都在叶节点。
B+树也存在劣势:由于键会重复出现,因此会占用更多的空间。但是与带来的性能优势相比,空间劣势往往可以接受,因此 B+树的在数据库中的使用比 B 树更加广泛。
感受 B+树的威力
前面说到,B 树/B+树与红黑树等二叉树相比,最大的优势在于树高更小。实际上,对于 Innodb 的 B+索引来说,树的高度一般在 2-4 层。下面来进行一些具体的估算。
树的高度是由阶数决定的,阶数越大树越矮;而阶数的大小又取决于每个节点可以存储多少条记录。Innodb 中每个节点使用一个页(page),页的大小为 16KB,其中元数据只占大约 128 字节左右(包括文件管理头信息、页面头信息等等),大多数空间都用来存储数据。
- 对于非叶节点,记录只包含索引的键和指向下一层节点的指针。假设每个非叶节点页面存储 1000 条记录,则每条记录大约占用 16 字节;当索引是整型或较短的字符串时,这个假设是合理的。延伸一下,我们经常听到建议说索引列长度不应过大,原因就在这里:索引列太长,每个节点包含的记录数太少,会导致树太高,索引的效果会大打折扣,而且索引还会浪费更多的空间。
- 对于叶节点,记录包含了索引的键和值(值可能是行的主键、一行完整数据等,具体见前文),数据量更大。这里假设每个叶节点页面存储 100 条记录(实际上,当索引为聚簇索引时,这个数字可能不足 100;当索引为辅助索引时,这个数字可能远大于 100;可以根据实际情况进行估算)。
对于一颗 3 层 B+树,第一层(根节点)有 1 个页面,可以存储 1000 条记录;第二层有 1000 个页面,可以存储 10001000 条记录;第三层(叶节点)有 10001000 个页面,每个页面可以存储 100 条记录,因此可以存储 10001000100 条记录,即 1 亿条。而对于二叉树,存储 1 亿条记录则需要 26 层左右。
总结
最后,总结一下各种树解决的问题以及面临的新问题:
- 二叉查找树(BST) :解决了排序的基本问题,但是由于无法保证平衡,可能退化为链表;
平衡二叉树(AVL) :通过旋转解决了平衡的问题,但是旋转操作效率太低;
红黑树 :通过舍弃严格的平衡和引入红黑节点,解决了 AVL 旋转效率过低的问题,但是在磁盘等场景下,树仍然太高,IO 次数太多;
B 树 :通过将二叉树改为多路平衡查找树,解决了树过高的问题;
B+树 :在 B 树的基础上,将非叶节点改造为不存储数据的纯索引节点,进一步降低了树的高度;此外将叶节点使用指针连接成链表,范围查询更加高效。
参考文献
《MySQL 技术内幕:InnoDB 存储引擎》
《MySQL 运维内参》
https://www.cnblogs.com/gaochundong/p/btree_and_bplustree.html
Redis 基础:为什么要用分布式缓存?
相关面试题 :
- 为什么要用缓存?
- 本地缓存应该怎么做?
- 为什么要有分布式缓存?/为什么不直接用本地缓存?
- 多级缓存了解么?
缓存的基本思想
很多同学只知道缓存可以提高系统性能以及减少请求相应时间,但是,不太清楚缓存的本质思想是什么。
缓存的基本思想其实很简单,就是我们非常熟悉的空间换时间。不要把缓存想的太高大上,虽然,它的确对系统的性能提升的性价比非常高。
其实,我们在学习使用缓存的时候,你会发现缓存的思想实际在操作系统或者其他地方都被大量用到。 比如 CPU Cache 缓存的是内存数据用于解决 CPU 处理速度和内存不匹配的问题,内存缓存的是硬盘数据用于解决硬盘访问速度过慢的问题。 再比如操作系统在 页表方案 基础之上引入了 快表 来加速虚拟地址到物理地址的转换。我们可以把快表理解为一种特殊的高速缓冲存储器(Cache)。
我们知道,缓存中的数据通常存储于内存中,因此访问速度非常快。为了避免内存中的数据在重启或者宕机之后丢失,很多缓存中间件会利用磁盘做持久化。
也就是说,缓存相比较于我们常用的关系型数据库(比如 MySQL)来说访问速度要快非常多。为了避免用户请求数据库中的数据速度过于缓慢,我们可以在数据库之上增加一层缓存。
除了能够提高访问速度之外,缓存支持的并发量也要更大,有了缓存之后,数据库的压力也会随之变小。
缓存的分类
本地缓存
什么是本地缓存?
这个实际在很多项目中用的蛮多,特别是单体架构的时候。数据量不大,并且没有分布式要求的话,使用本地缓存还是可以的。
本地缓存位于应用内部,其最大的优点是应用存在于同一个进程内部,请求本地缓存的速度非常快,不存在额外的网络开销。
常见的单体架构图如下,我们使用 Nginx 来做负载均衡,部署两个相同的应用到服务器,两个服务使用同一个数据库,并且使用的是本地缓存。
本地缓存的方案有哪些?
1、JDK 自带的 HashMap 和 ConcurrentHashMap 了。
ConcurrentHashMap 可以看作是线程安全版本的 HashMap ,两者都是存放 key/value 形式的键值对。但是,大部分场景来说不会使用这两者当做缓存,因为只提供了缓存的功能,并没有提供其他诸如过期时间之类的功能。一个稍微完善一点的缓存框架至少要提供:过期时间、淘汰机制、命中率统计这三点。
2、 Ehcache 、 Guava Cache 、 Spring Cache 这三者是使用的比较多的本地缓存框架。
Ehcache
的话相比于其他两者更加重量。不过,相比于Guava Cache
、Spring Cache
来说,Ehcache
支持可以嵌入到 hibernate 和 mybatis 作为多级缓存,并且可以将缓存的数据持久化到本地磁盘中、同时也提供了集群方案(比较鸡肋,可忽略)。Guava Cache
和Spring Cache
两者的话比较像。Guava
相比于Spring Cache
的话使用的更多一点,它提供了 API 非常方便我们使用,同时也提供了设置缓存有效时间等功能。它的内部实现也比较干净,很多地方都和ConcurrentHashMap
的思想有异曲同工之妙。- 使用
Spring Cache
的注解实现缓存的话,代码会看着很干净和优雅,但是很容易出现问题比如缓存穿透、内存溢出。
3、后起之秀 Caffeine。
相比于Guava
来说Caffeine
在各个方面比如性能要更加优秀,一般建议使用其来替代Guava
。并且, Guava
和 Caffeine
的使用方式很像!
本地缓存有什么痛点?
本地的缓存的优势非常明显:低依赖、轻量、简单、成本低。
但是,本地缓存存在下面这些缺陷:
- 本地缓存应用耦合,对分布式架构支持不友好,比如同一个相同的服务部署在多台机器上的时候,各个服务之间的缓存是无法共享的,因为本地缓存只在当前机器上有。
- 本地缓存容量受服务部署所在的机器限制明显。 如果当前系统服务所耗费的内存多,那么本地缓存可用的容量就很少。
分布式缓存
什么是分布式缓存?
我们可以把分布式缓存(Distributed Cache) 看作是一种内存数据库的服务,它的最终作用就是提供缓存数据的服务。
分布式缓存脱离于应用独立存在,多个应用可直接的共同使用同一个分布式缓存服务。
如下图所示,就是一个简单的使用分布式缓存的架构图。我们使用 Nginx 来做负载均衡,部署两个相同的应用到服务器,两个服务使用同一个数据库和缓存。
使用分布式缓存之后,缓存服务可以部署在一台单独的服务器上,即使同一个相同的服务部署在多台机器上,也是使用的同一份缓存。 并且,单独的分布式缓存服务的性能、容量和提供的功能都要更加强大。
软件系统设计中没有银弹,往往任何技术的引入都像是把双刃剑。 你使用的方式得当,就能为系统带来很大的收益。否则,只是费了精力不讨好。
简单来说,为系统引入分布式缓存之后往往会带来下面这些问题:
- 系统复杂性增加 :引入缓存之后,你要维护缓存和数据库的数据一致性、维护热点缓存、保证缓存服务的高可用等等。
- 系统开发成本往往会增加 :引入缓存意味着系统需要一个单独的缓存服务,这是需要花费相应的成本的,并且这个成本还是很贵的,毕竟耗费的是宝贵的内存。
分布式缓存的方案有哪些?
分布式缓存的话,比较老牌同时也是使用的比较多的还是 Memcached 和 Redis。不过,现在基本没有看过还有项目使用 Memcached 来做缓存,都是直接用 Redis。
Memcached 是分布式缓存最开始兴起的那会,比较常用的。后来,随着 Redis 的发展,大家慢慢都转而使用更加强大的 Redis 了。
另外,腾讯也开源了一款类似于 Redis 的分布式高性能 KV 存储数据库,基于知名的开源项目 RocksDB 作为存储引擎 ,100% 兼容 Redis 协议和 Redis4.0 所有数据模型,名为 Tendis。
关于 Redis 和 Tendis 的对比,腾讯官方曾经发过一篇文章:Redis vs Tendis:冷热混合存储版架构揭秘 ,可以简单参考一下。
从这个项目的 Github 提交记录可以看出,Tendis 开源版几乎已经没有被维护更新了,加上其关注度并不高,使用的公司也比较少。因此,不建议你使用 Tendis 来实现分布式缓存。
多级缓存
我们这里只来简单聊聊 本地缓存 + 分布式缓存 的多级缓存方案。
这个时候估计有很多小伙伴就会问了:既然用了分布式缓存,为什么还要用本地缓存呢? 。
的确,一般情况下,我们也是不建议使用多级缓存的,这会增加维护负担(比如你需要保证一级缓存和二级缓存的数据一致性),并且,实际带来的提升效果对于绝大部分项目来说其实并不是很大。
多级缓存方案中,第一级缓存(L1)使用本地内存(比如 Caffeine)),第二级缓存(L2)使用分布式缓存(比如 Redis)。读取缓存数据的时候,我们先从 L1 中读取,读取不到的时候再去 L2 读取。这样可以降低 L2 的压力,减少 L2 的读次数。并且,本地内存的访问速度是最快的,不存在什么网络开销。
J2Cache 就是一个基于本地内存和分布式缓存的两级 Java 缓存框架,感兴趣的同学可以研究一下。
Redis 基础:常见的缓存更新策略有哪几种?
下面介绍到的三种模式各有优劣,不存在最佳模式,根据具体的业务场景选择适合自己的缓存读写模式即可!
Cache Aside Pattern(旁路缓存模式)
Cache Aside Pattern 是我们平时使用比较多的一个缓存读写模式,比较适合读请求比较多的场景。
Cache Aside Pattern 中服务端需要同时维系数据库(后文简称 db)和缓存(后文简称 cache),并且是以 db 的结果为准。
下面我们来看一下这个策略模式下的缓存读写步骤。
写 :
- 先更新 db;
- 直接删除 cache 。
简单画了一张图帮助大家理解写的步骤。
读 :
- 从 cache 中读取数据,读取到就直接返回;
- cache 中读取不到的话,就从 db 中读取数据返回;
- 再把 db 中读取到的数据放到 cache 中。
简单画了一张图帮助大家理解读的步骤。
你仅仅了解了上面这些内容的话是远远不够的,我们还要搞懂其中的原理。
比如说面试官可能会问你:“为什么删除 cache,而不是更新 cache?”
主要原因有两点:
- 对服务端资源造成浪费 :删除 cache 更加直接,这是因为 cache 中存放的一些数据需要服务端经过大量的计算才能得出,会消耗服务端的资源,是一笔不晓得开销。如果频繁修改 db,就能会导致需要频繁更新 cache,而 cache 中的数据可能都没有被访问到。
- 产生数据不一致问题 :并发场景下,更新 cache 产生数据不一致性问题的概率会更大(后文会解释原因)。
面试官很可能会追问:“在写数据的过程中,可以先删除 cache ,后更新 db 么?”
答案: 那肯定是不行的!因为这样可能会造成 数据库(db)和缓存(Cache)数据不一致的问题。
举例:请求 1 先写数据 A,请求 2 随后读数据 A 的话,就很有可能产生数据不一致性的问题。这个过程可以简单描述为:
请求 1 先把 cache 中的 A 数据删除;
请求 2 从 db 中读取数据;
请求 1 再把 db 中的 A 数据更新。
这就会导致请求 2 读取到的是旧值。
当你这样回答之后,面试官可能会紧接着就追问:“在写数据的过程中,先更新 db,后删除 cache 就没有问题了么?”
答案: 理论上来说还是可能会出现数据不一致性的问题,不过概率非常小,因为缓存的写入速度是比数据库的写入速度快很多。
举例:请求 1 先读数据 A,请求 2 随后写数据 A,并且数据 A 在请求 1 请求之前不在缓存中的话,也有可能产生数据不一致性的问题。这个过程可以简单描述为:
请求 1 从 db 读数据 A;
请求 2 更新 db 中的数据 A(此时缓存中无数据 A ,故不用执行删除缓存操作 );
请求 1 将数据 A 写入 cache。
这就会导致 cache 中存放的其实是旧值。
现在我们再来分析一下 Cache Aside Pattern 的缺陷。
缺陷 1:首次请求数据一定不在 cache 的问题
解决办法:可以将热点数据可以提前放入 cache 中。
缺陷 2:写操作比较频繁的话导致 cache 中的数据会被频繁被删除,这样会影响缓存命中率 。
解决办法:
数据库和缓存数据强一致场景 :更新 db 的时候同样更新 cache,不过我们需要加一个锁/分布式锁来保证更新 cache 的时候不存在线程安全问题。
可以短暂地允许数据库和缓存数据不一致的场景 :更新 db 的时候同样更新 cache,但是给缓存加一个比较短的过期时间,这样的话就可以保证即使数据不一致的话影响也比较小。
Read/Write Through Pattern(读写穿透)
Read/Write Through Pattern 中服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。cache 服务负责将此数据读取和写入 db,从而减轻了应用程序的职责。
这种缓存读写策略小伙伴们应该也发现了在平时在开发过程中非常少见。抛去性能方面的影响,大概率是因为我们经常使用的分布式缓存 Redis 并没有提供 cache 将数据写入 db 的功能。
写(Write Through):
先查 cache,cache 中不存在,直接更新 db。
cache 中存在,则先更新 cache,然后 cache 服务自己更新 db(同步更新 cache 和 db)。
简单画了一张图帮助大家理解写的步骤。
读(Read Through):
- 从 cache 中读取数据,读取到就直接返回 。
- 读取不到的话,先从 db 加载,写入到 cache 后返回响应。
简单画了一张图帮助大家理解读的步骤。
Read-Through Pattern 实际只是在 Cache-Aside Pattern 之上进行了封装。在 Cache-Aside Pattern 下,发生读请求的时候,如果 cache 中不存在对应的数据,是由客户端自己负责把数据写入 cache,而 Read Through Pattern 则是 cache 服务自己来写入缓存的,这对客户端是透明的。
和 Cache Aside Pattern 一样, Read-Through Pattern 也有首次请求数据一定不再 cache 的问题,对于热点数据可以提前放入缓存中。
Write Behind Pattern(异步缓存写入)
Write Behind Pattern 和 Read/Write Through Pattern 很相似,两者都是由 cache 服务来负责 cache 和 db 的读写。
但是,两个又有很大的不同:Read/Write Through 是同步更新 cache 和 db,而 Write Behind 则是只更新缓存,不直接更新 db,而是改为异步批量的方式来更新 db。
很明显,这种方式对数据一致性带来了更大的挑战,比如 cache 数据可能还没异步更新 db 的话,cache 服务可能就就挂掉了。
这种策略在我们平时开发过程中也非常非常少见,但是不代表它的应用场景少,比如消息队列中消息的异步写入磁盘、MySQL 的 Innodb Buffer Pool 机制都用到了这种策略。
Write Behind Pattern 下 db 的写性能非常高,非常适合一些数据经常变化又对数据一致性要求没那么高的场景,比如浏览量、点赞量。
Redis Sentinel:如何实现自动化地故障转移?
普通的主从复制方案下,一旦 master 宕机,我们需要从 slave 中手动选择一个新的 master,同时需要修改应用方的主节点地址,还需要命令所有从节点去复制新的主节点,整个过程需要人工干预。人工干预大大增加了问题的处理时间以及出错的可能性。
我们可以借助 Redis 官方的 Sentinel(哨兵)方案来帮助我们解决这个痛点,实现自动化地故障切换。
建议带着下面这些重要的问题(面试常问)阅读:
- 什么是 Sentinel? 有什么用?
- Sentinel 如何检测节点是否下线?主观下线与客观下线的区别?
- Sentinel 是如何实现故障转移的?
- 为什么建议部署多个 sentinel 节点(哨兵集群)?
- Sentinel 如何选择出新的 master(选举机制)?
- 如何从 Sentinel 集群中选择出 Leader ?
- Sentinel 可以防止脑裂吗?
什么是 Sentinel?
Sentinel(哨兵) 只是 Redis 的一种运行模式 ,不提供读写服务,默认运行在 26379 端口上,依赖于 Redis 工作。Redis Sentinel 的稳定版本是在 Redis 2.8 之后发布的。
Redis 在 Sentinel 这种特殊的运行模式下,使用专门的命令表,也就是说普通模式运行下的 Redis 命令将无法使用。
通过下面的命令就可以让 Redis 以 Sentinel 的方式运行:
1 | redis-sentinel /path/to/sentinel.conf |
Redis 源码中的sentinel.conf
是用来配置 Sentinel 的,一个常见的最小配置如下所示:
1 | // 指定要监视的 master |
Redis Sentinel 实现 Redis 集群高可用,只是在主从复制实现集群的基础下,多了一个 Sentinel 角色来帮助我们监控 Redis 节点的运行状态并自动实现故障转移。
当 master 节点出现故障的时候, Sentinel 会帮助我们实现故障转移,自动根据一定的规则选出一个 slave 升级为 master,确保整个 Redis 系统的可用性。整个过程完全自动,不需要人工介入。
Sentinel 有什么作用?
根据 Redis Sentinel 官方文档的介绍,sentinel 节点主要可以提供 4 个功能:
- 监控:监控所有 redis 节点(包括 sentinel 节点自身)的状态是否正常。
- 故障转移:如果一个 master 出现故障,Sentinel 会帮助我们实现故障转移,自动将某一台 slave 升级为 master,确保整个 Redis 系统的可用性。
- 通知 :通知 slave 新的 master 连接信息,让它们执行 replicaof 成为新的 master 的 slave。
- 配置提供 :客户端连接 sentinel 请求 master 的地址,如果发生故障转移,sentinel 会通知新的 master 链接信息给客户端。
Redis Sentinel 本身设计的就是一个分布式系统,建议多个 sentinel 节点协作运行。这样做的好处是:
- 多个 sentinel 节点通过投票的方式来确定 sentinel 节点是否真的不可用,避免误判(比如网络问题可能会导致误判)。
- Sentinel 自身就是高可用。
如果想要实现高可用,建议将哨兵 Sentinel 配置成单数且大于等于 3 台。
一个最简易的 Redis Sentinel 集群如下所示(官方文档中的一个例子),其中:
- M1 表示 master,R2、R3 表示 slave;
- S1、S2、S3 都是 sentinel;
- quorum 表示判定 master 失效最少需要的仲裁节点数。这里的值为 2 ,也就是说当有 2 个 sentinel 认为 master 失效时,master 才算真正失效。
1 | +----+ |
如果 M1 出现问题,只要 S1、S2、S3 其中的两个投票赞同的话,就会开始故障转移工作,从 R2 或者 R3 中重新选出一个作为 master。
Sentinel 如何检测节点是否下线?
相关的问题:
- 主观下线与客观下线的区别?
- Sentinel 是如何实现故障转移的?
- 为什么建议部署多个 sentinel 节点(哨兵集群)?
Redis Sentinel 中有两个下线(Down)的概念:
- 主观下线(SDOWN) :sentinel 节点认为某个 Redis 节点已经下线了(主观下线),但还不是很确定,需要其他 sentinel 节点的投票。
- 客观下线(ODOWN) :法定数量(通常为过半)的 sentinel 节点认定某个 Redis 节点已经下线(客观下线),那它就算是真的下线了。
也就是说,主观下线 当前的 sentinel 自己认为节点宕机,客观下线是 sentinel 整体达成一致认为节点宕机。
每个 sentinel 节点以每秒钟一次的频率向整个集群中的 master、slave 以及其他 sentinel 节点发送一个 PING 命令。
如果对应的节点超过规定的时间(down-after-millisenconds)没有进行有效回复的话,就会被其认定为是 主观下线(SDOWN) 。注意!这里的有效回复不一定是 PONG,可以是-LOADING 或者 -MASTERDOWN 。
如果被认定为主观下线的是 slave 的话, sentinel 不会做什么事情,因为 slave 下线对 Redis 集群的影响不大,Redis 集群对外正常提供服务。但如果是 master 被认定为主观下线就不一样了,sentinel 整体还要对其进行进一步核实,确保 master 是真的下线了。
所有 sentinel 节点要以每秒一次的频率确认 master 的确下线了,当法定数量(通常为过半)的 sentinel 节点认定 master 已经下线, master 才被判定为 客观下线(ODOWN) 。这样做的目的是为了防止误判,毕竟故障转移的开销还是比较大的,这也是为什么 Redis 官方推荐部署多个 sentinel 节点(哨兵集群)。
随后, sentinel 中会有一个 Leader 的角色来负责故障转移,也就是自动地从 slave 中选出一个新的 master 并执行完相关的一些工作(比如通知 slave 新的 master 连接信息,让它们执行 replicaof 成为新的 master 的 slave)。
如果没有足够数量的 sentinel 节点认定 master 已经下线的话,当 master 能对 sentinel 的 PING 命令进行有效回复之后,master 也就不再被认定为主观下线,回归正常。
Sentinel 如何选择出新的 master?
slave 必须是在线状态才能参加新的 master 的选举,筛选出所有在线的 slave 之后,通过下面 3 个维度进行最后的筛选(优先级依次降低):
- slave 优先级 :可以通过 slave-priority 手动设置 slave 的优先级,优先级越高得分越高,优先级最高的直接成为新的 master。如果没有优先级最高的,再判断复制进度。
- 复制进度 :Sentinel 总是希望选择出数据最完整(与旧 master 数据最接近)也就是复制进度最快的 slave 被提升为新的 master,复制进度越快得分也就越高。
- runid(运行 id) :通常经过前面两轮筛选已经成果选出来了新的 master,万一真有多个 slave 的优先级和复制进度一样的话,那就 runid 小的成为新的 master,每个 redis 节点启动时都有一个 40 字节随机字符串作为运行 id。
如何从 Sentinel 集群中选择出 Leader ?
我们前面说了,当 sentinel 集群确认有 master 客观下线了,就会开始故障转移流程,故障转移流程的第一步就是在 sentinel 集群选择一个 leader,让 leader 来负责完成故障转移。
如何选择出 Leader 角色呢?
这就需要用到分布式领域的 共识算法 了。简单来说,共识算法就是让分布式系统中的节点就一个问题达成共识。在 sentinel 选举 leader 这个场景下,这些 sentinel 要达成的共识就是谁才是 leader 。
大部分共识算法都是基于 Paxos 算法改进而来,在 sentinel 选举 leader 这个场景下使用的是 Raft 算法。这是一个比 Paxos 算法更易理解和实现的共识算法—Raft 算法。更具体点来说,Raft 是 Multi-Paxos 的一个变种,其简化了 Multi-Paxos 的思想,变得更容易被理解以及工程实现。
对于学有余力并且想要深入了解 Raft 算法实践以及 sentinel 选举 leader 的详细过程的同学,推荐阅读下面这两篇文章:
Sentinel 可以防止脑裂吗?
还是上面的例子,如果 M1 和 R2、R3 之间的网络被隔离,也就是发生了脑裂,M1 和 R2 、 R3 隔离在了两个不同的网络分区中。这意味着,R2 或者 R3 其中一个会被选为 master,这里假设为 R2。
但是!这样会出现问题了!!
如果客户端 C1 是和 M1 在一个网络分区的话,从网络被隔离到网络分区恢复这段时间,C1 写入 M1 的数据都会丢失,并且,C1 读取的可能也是过时的数据。这是因为当网络分区恢复之后,M1 将会成为 slave 节点。
1 | +----+ |
想要解决这个问题的话也不难,对 Redis 主从复制进行配置即可。
1 | min-replicas-to-write 1 |
下面对这两个配置进行解释:
- min-replicas-to-write 1:用于配置写 master 至少写入的 slave 数量,设置为 0 表示关闭该功能。3 个节点的情况下,可以配置为 1 ,表示 master 必须写入至少 1 个 slave ,否则就停止接受新的写入命令请求。
- min-replicas-max-lag 10 :用于配置 master 多长时间(秒)无法得到从节点的响应,就认为这个节点失联。我们这里配置的是 10 秒,也就是说 master 10 秒都得不到一个从节点的响应,就会认为这个从节点失联,停止接受新的写入命令请求。
不过,这样配置会降低 Redis 服务的整体可用性,如果 2 个 slave 都挂掉,master 将会停止接受新的写入命令请求。
Redis Cluster:缓存的数据量太大怎么办?
来来来!一起来盘盘 Redis Cluster 常见的问题。如果你的项目用到了 Redis 的话(大部分人的项目都用到了 Redis 来做分布式缓存),为了能比别人更有亮点,Redis Cluster 是一个不错的选择。
这篇文章原本写了接近 8000 字,有点写嗨了,后面删减到了现在的 5000+ 字。为了帮助理解,我手绘了很多张图解,尽可能用大白话的语言来讲。
建议带着下面这些重要的问题(面试常问)阅读:
- 为什么需要 Redis Cluster?解决了什么问题?有什么优势?
- Redis Cluster 是如何分片的?
- 为什么 Redis Cluster 的哈希槽是 16384 个?
- 如何确定给定 key 的应该分布到哪个哈希槽中?
- Redis Cluster 支持重新分配哈希槽吗?
- Redis Cluster 扩容缩容期间可以提供服务吗?
- Redis Cluster 中的节点是怎么进行通信的?
为什么需要 Redis Cluster?
高并发场景下,使用 Redis 主要会遇到的两个问题:
- 缓存的数据量太大 :实际缓存的数据量可以达到几十 G,甚至是成百上千 G;
- 并发量要求太大 :虽然 Redis 号称单机可以支持 10w 并发,但实际项目中,不可靠因素太多,就比如一些复杂的写/读操作就可能会让这个并发量大打折扣。而且,就算真的可以实际支持 10w 并发,达到瓶颈了,可能也没办法满足系统的实际需求。
主从复制和 Redis Sentinel 这两种方案本质都是通过增加主库(master)的副本(slave)数量的方式来提高 Redis 服务的整体可用性和读吞吐量,都不支持横向扩展来缓解写压力以及解决缓存数据量过大的问题。
对于这两种方案来说,如果写压力太大或者缓存数据量太大的话,我们可以考虑提高服务器硬件的配置。不过,提高硬件配置成本太高,能力有限,无法动态扩容缩容,局限性太大。从本质上来说,靠堆硬件配置的方式并没有实质性地解决问题,依然无法满足高并发场景下分布式缓存的要求。
通常情况下,更建议使用 Redis 切片集群 这种方案,更能满足高并发场景下分布式缓存的要求。
简单来说,Redis 切片集群 就是部署多台 Redis 主节点(master),这些节点之间平等,并没有主从之说,同时对外提供读/写服务。缓存的数据库相对均匀地分布在这些 Redis 实例上,客户端的请求通过路由规则转发到目标 master 上。
为了保障集群整体的高可用,我们需要保证集群中每一个 master 的高可用,可以通过主从复制给每个 master 配置一个或者多个从节点(slave)。
Redis 切片集群对于横向扩展非常友好,只需要增加 Redis 节点到集群中即可。
在 Redis 3.0 之前,我们通常使用的是 Twemproxy、Codis 这类开源分片集群方案。Twemproxy、Codis 就相当于是上面的 Proxy 层,负责维护路由规则,实现负载均衡。
不过,Twemproxy、Codis 虽然未被淘汰,但官方已经没有继续维护了。
到了 Redis 3.0 的时候,Redis 官方推出了分片集群解决方案 Redis Cluster 。经过多个版本的持续完善,Redis Cluster 成为 Redis 切片集群的首选方案,满足绝大部分高并发业务场景需求。
Redis Cluster 通过 分片(Sharding) 来进行数据管理,提供 主从复制(Master-Slave Replication)、故障转移(Failover) 等开箱即用的功能,可以非常方便地帮助我们解决 Redis 大数据量缓存以及 Redis 服务高可用的问题。
Redis Cluster 这种方案可以很方便地进行 横向拓展(Scale Out),内置了开箱即用的解决方案。当 Redis Cluster 的处理能力达到瓶颈无法满足系统要求的时候,直接动态添加 Redis 节点到集群中即可。根据官方文档中的介绍,Redis Cluster 支持扩展到 1000 个节点。反之,当 Redis Cluster 的处理能力远远满足系统要求,同样可以动态删除集群中 Redis 节点,节省资源。
可以说,Redis Cluster 的动态扩容和缩容是其最大的优势。
虽说 Redis Cluster 可以扩展到 1000 个节点,但强烈不推荐这样做,应尽量避免集群中的节点过多。这是因为 Redis Cluster 中的各个节点基于 Gossip 协议 来进行通信共享信息,当节点过多时,Gossip 协议的效率会显著下降,通信成本剧增。
最后,总结一下 Redis Cluster 的主要优势:
- 可以横向扩展缓解写压力和存储压力,支持动态扩容和缩容;
- 具备主从复制、故障转移(内置了 Sentinel 机制,无需单独部署 Sentinel 集群)等开箱即用的功能。
一个最基本的 Redis Cluster 架构是怎样的?
为了保证高可用,Redis Cluster 至少需要 3 个 master 以及 3 个 slave,也就是说每个 master 必须有 1 个 slave。master 和 slave 之间做主从复制,slave 会实时同步 master 上的数据。
不同于普通的 Redis 主从架构,这里的 slave 不对外提供读服务,主要用来保障 master 的高可用,当 master 出现故障的时候替代它。
如果 master 只有一个 slave 的话,master 宕机之后就直接使用这个 slave 替代 master 继续提供服务。假设 master1 出现故障,slave1 会直接替代 master1,保证 Redis Cluster 的高可用。
如果 master 有多个 slave 的话,Redis Cluster 中的其他节点会从这个 master 的所有 slave 中选出一个替代 master 继续提供服务。Redis Cluster 总是希望数据最完整的 slave 被提升为新的 master。
Redis Cluster 是去中心化的(各个节点基于 Gossip 进行通信),任何一个 master 出现故障,其它的 master 节点不受影响,因为 key 找的是哈希槽而不是 Redis 节点。不过,Redis Cluster 至少要保证宕机的 master 有一个 slave 可用。
如果宕机的 master 无 slave 的话,为了保障集群的完整性,保证所有的哈希槽都指派给了可用的 master ,整个集群将不可用。这种情况下,还是想让集群保持可用的话,可以将cluster-require-full-coverage 这个参数设置成 no,cluster-require-full-coverage 表示需要 16384 个 slot 都正常被分配的时候 Redis Cluster 才可以对外提供服务。
如果我们想要添加新的节点比如 master4、master5 进入 Redis Cluster 也非常方便,只需要重新分配哈希槽即可。
如果我们想要移除某个 master 节点的话,需要先将该节点的哈希槽移动到其他节点上,这样才可以进行删除,不然会报错。
Redis Cluster 是如何分片的?
类似的问题:
- Redis Cluster 中的数据是如何分布的?
- 如何确定给定 key 的应该分布到哪个哈希槽中?
Redis Cluster 并没有使用一致性哈希,采用的是 哈希槽分区 ,每一个键值对都属于一个 hash slot(哈希槽) 。
Redis Cluster 通常有 16384 个哈希槽 ,要计算给定 key 应该分布到哪个哈希槽中,我们只需要先对每个 key 计算 CRC-16(XMODEM) 校验码,然后再对这个校验码对 16384(哈希槽的总数) 取模,得到的值即是 key 对应的哈希槽。
哈希槽的计算公式如下:
1 | HASH_SLOT = CRC16(key) mod NUMER_OF_SLOTS |
创建并初始化 Redis Cluster 的时候,Redis 会自动平均分配这 16384 个哈希槽到各个节点,不需要我们手动分配。如果你想自己手动调整的话,Redis Cluster 也内置了相关的命令比如ADDSLOTS、ADDSLOTSRANGE
(后面会详细介绍到重新分配哈希槽相关的命令)。
假设集群有 3 个 Redis 节点组成,每个节点负责整个集群的一部分数据,哈希槽可能是这样分配的(这里只是演示,实际效果可能会有差异):
- Node 1 : 0 - 5500 的 hash slots
- Node 2 : 5501 - 11000 的 hash slots
- Node 3 : 11001 - 16383 的 hash slots
在任意一个 master 节点上执行 CLUSTER SLOTS命令即可返回哈希槽和节点的映射关系:
1 | 127.0.0.1:7000>> CLUSTER SLOTS |
客户端连接 Redis Cluster 中任意一个 master 节点即可访问 Redis Cluster 的数据,当客户端发送命令请求的时候,需要先根据 key 通过上面的计算公示找到的对应的哈希槽,然后再查询哈希槽和节点的映射关系,即可找到目标节点。
如果哈希槽确实是当前节点负责,那就直接响应客户端的请求返回结果,如果不由当前节点负责,就会返回 -MOVED 重定向错误,告知客户端当前哈希槽是由哪个节点负责,客户端向目标节点发送请求并更新缓存的哈希槽分配信息。
这个时候你可能就会疑问:为什么还会存在找错节点的情况呢?根据公式计算难道还会出错?
这是因为 Redis Cluster 内部可能会重新分配哈希槽比如扩容缩容的时候(后文中有详细介绍到 Redis Cluster 的扩容和缩容问题),这就可能会导致客户端缓存的哈希槽分配信息会有误。
从上面的介绍中,我们可以简单总结出 Redis Cluster 哈希槽分区机制的优点:解耦了数据和节点之间的关系,提升了集群的横向扩展性和容错性。
为什么 Redis Cluster 的哈希槽是 16384 个?
CRC16 算法产生的校验码有 16 位,理论上可以产生 65536(2^16,0 ~ 65535)个值。为什么 Redis Cluster 的哈希槽偏偏选择的是 16384(2^14)个呢?
2015 年的时候,在 Redis 项目的 issues 区,已经有人提了类似的问题,地址:https://github.com/redis/redis/issues/2576。Redis 作者 antirez 巨佬本人专门对这个问题进行了回复。
antirez 认为哈希槽是 16384(2 的 14 次方) 个的原因是:
- 正常的心跳包会携带一个节点的完整配置,它会以幂等的方式更新旧的配置,这意味着心跳包会附带当前节点的负责的哈希槽的信息。假设哈希槽采用 16384 ,则占空间 2k(16384/8)。假设哈希槽采用 65536, 则占空间 8k(65536/8),这是令人难以接受的内存占用。
- 由于其他设计上的权衡,Redis Cluster 不太可能扩展到超过 1000 个主节点。
也就是说,65536 个固然可以确保每个主节点有足够的哈希槽,但其占用的空间太大。而且,Redis Cluster 的主节点通常不会扩展太多,16384 个哈希槽完全足够用了。
cluster.h
文件中定义了消息结构clusterMsg
(源码地址:https://github.com/redis/redis/blob/7.0/src/cluster.h) :
1 | typedef struct { |
myslots
字段用于存储哈希槽信息, 属于无符号类型的 char 数组,数组长度为 16384/8 = 2048。C 语言中的 char 只占用一个字节,而 Java 语言中 char 占用两个字节,小伙伴们不要搞混了。
这里实际就是通过 bitmap 这种数据结构维护的哈希槽信息,每一个 bit 代表一个哈希槽,每个 bit 只能存储 0/1 。如果该位为 1,表示这个哈希槽是属于这个节点。
消息传输过程中,会对 myslots 进行压缩,bitmap 的填充率越低,压缩率越高。bitmap 的填充率的值是 哈希槽总数/节点数 ,如果哈希槽总数太大的话,bitmap 的填充率的值也会比较大。
最后,总结一下 Redis Cluster 的哈希槽的数量选择 16384 而不是 65536 的主要原因:
- 哈希槽太大会导致心跳包太大,消耗太多带宽;
- 哈希槽总数越少,对存储哈希槽信息的 bitmap 压缩效果越好;
- Redis Cluster 的主节点通常不会扩展太多,16384 个哈希槽已经足够用了。
Redis Cluster 如何重新分配哈希槽?
如果你想自己手动调整的话,Redis Cluster 也内置了相关的命令:
- CLUSTER ADDSLOTS slot [slot …] : 把一组 hash slots 分配给接收命令的节点,时间复杂度为 O(N),其中 N 是 hash slot 的总数;
- CLUSTER ADDSLOTSRANGE start-slot end-slot [start-slot end-slot …] (Redis 7.0 后新加的命令): 把指定范围的 hash slots 分配给接收命令的节点,类似于 ADDSLOTS 命令,时间复杂度为 O(N) 其中 N 是起始 hash slot 和结束 hash slot 之间的 hash slot 的总数。
- CLUSTER DELSLOTS slot [slot …] : 从接收命令的节点中删除一组 hash slots;
- CLUSTER FLUSHSLOTS :移除接受命令的节点中的所有 hash slot;
- CLUSTER SETSLOT slot MIGRATING node-id: 迁移接受命令的节点的指定 hash slot 到目标节点(node_id 指定)中;
- CLUSTER SETSLOT slot IMPORTING node-id: 将目标节点(node_id 指定)中的指定 hash slot 迁移到接受命令的节点中;
- ……
简单演示一下:
1 | # 将 slot 1 2 3 4 5 分配给节点 |
Redis Cluster 扩容缩容期间可以提供服务吗?
类似的问题:
- 如果客户端访问的 key 所属的槽正在迁移怎么办?
- 如何确定给定 key 的应该分布到哪个哈希槽中?
Redis Cluster 扩容和缩容本质是进行重新分片,动态迁移哈希槽。
为了保证 Redis Cluster 在扩容和缩容期间依然能够对外正常提供服务,Redis Cluster 提供了重定向机制,两种不同的类型:
- ASK 重定向
- MOVED 重定向
从客户端的角度来看,ASK 重定向是下面这样的:
- 客户端发送请求命令,如果请求的 key 对应的哈希槽还在当前节点的话,就直接响应客户端的请求。
- 如果客户端请求的 key 对应的哈希槽当前正在迁移至新的节点,就会返回 -ASK 重定向错误,告知客户端要将请求发送到哈希槽被迁移到的目标节点。
- 客户端收到 -ASK 重定向错误后,将会临时(一次性)重定向,自动向目标节点发送一条 ASKING 命令。也就是说,接收到 ASKING 命令的节点会强制执行一次请求,下次再来需要重新提前发送 ASKING 命令。
- 客户端发送真正的请求命令。
- ASK 重定向并不会同步更新客户端缓存的哈希槽分配信息,也就是说,客户端对正在迁移的相同哈希槽的请求依然会发送到原节点而不是目标节点。
如果客户端请求的 key 对应的哈希槽应该迁移完成的话,就会返回 -MOVED 重定向错误,告知客户端当前哈希槽是由哪个节点负责,客户端向目标节点发送请求并更新缓存的哈希槽分配信息。
Redis Cluster 中的节点是怎么进行通信的?
Redis Cluster 是一个典型的分布式系统,分布式系统中的各个节点需要互相通信。既然要相互通信就要遵循一致的通信协议,Redis Cluster 中的各个节点基于 Gossip 协议 来进行通信共享信息,每个 Redis 节点都维护了一份集群的状态信息。
Redis Cluster 的节点之间会相互发送多种 Gossip 消息:
- MEET :在 Redis Cluster 中的某个 Redis 节点上执行 CLUSTER MEET ip port 命令,可以向指定的 Redis 节点发送一条 MEET 信息,用于将其添加进 Redis Cluster 成为新的 Redis 节点。
- PING/PONG :Redis Cluster 中的节点都会定时地向其他节点发送 PING 消息,来交换各个节点状态信息,检查各个节点状态,包括在线状态、疑似下线状态 PFAIL 和已下线状态 FAIL。
- FAIL :Redis Cluster 中的节点 A 发现 B 节点 PFAIL ,并且在下线报告的有效期限内集群中半数以上的节点将 B 节点标记为 PFAIL,节点 A 就会向集群广播一条 FAIL 消息,通知其他节点将故障节点 B 标记为 FAIL 。
- ……
有了 Redis Cluster 之后,不需要专门部署 Sentinel 集群服务了。Redis Cluster 相当于是内置了 Sentinel 机制,Redis Cluster 内部的各个 Redis 节点通过 Gossip 协议互相探测健康状态,在故障时可以自动切换。
cluster.h
文件中定义了所有的消息类型(源码地址:https://github.com/redis/redis/blob/7.0/src/cluster.h) 。Redis 3.0 版本的时候只有 9 种消息类型,到了 7.0 版本的时候已经有 11 种消息类型了。
1 | // 注意,PING 、 PONG 和 MEET 实际上是同一种消息。 |
cluster.h
文件中定义了消息结构 clusterMsg
(源码地址:https://github.com/redis/redis/blob/7.0/src/cluster.h) :
1 | typedef struct { |
clusterMsgData
是一个联合体(union),可以为 PING,MEET,PONG 、FAIL 等消息类型。当消息为 PING、MEET 和 PONG 类型时,都是 ping 字段是被赋值的,这也就解释了为什么我们上面说 PING 、 PONG 和 MEET 实际上是同一种消息。
1 | union clusterMsgData { |
参考
- Redis Cluster 官方规范:https://redis.io/docs/reference/cluster-spec/
- Redis Cluster 官方教程:https://redis.io/topics/cluster-tutorial
- Redis Cluster 官方公开 PDF 讲义:https://redis.io/presentation/Redis_Cluster.pdf
- Redis 集群详述:https://juejin.cn/post/7016865316240097287
- Redis 专题:了解 Redis 集群,这篇就够了:https://juejin.cn/post/6949832776224866340
- Redis Notes - Cluster mode:https://www.stevenchang.tw/blog/2020/12/08/redis-notes-cluster-mode
- 带有详细注释的 Redis 3.0 代码(开源项目):https://github.com/huangz1990/redis-3.0-annotated
Elasticsearch 常见面试题总结
大部分项目都会用到 Elasticsearch ,面试难免会被问到。于是,利用春节时间简单总结了一下 Elasticsearch 常见问题,希望对球友们有帮助。
少部分内容参考了 Elasticsearch 官方文档的描述,在此说明一下。
Elasticsearch 基础
Elasticsearch 是什么?
ElasticSearch 是一个开源的 分布式、RESTful 搜索和分析引擎,可以用来解决使用数据库进行模糊搜索时存在的性能问题,适用于所有类型的数据,包括文本、数字、地理空间、结构化和非结构化数据。
ElasticSearch 使用 Java 语言开发,基于 Lucence。ES 早期版本需要 JDK,在 7.X 版本后已经集成了 JDK,已无需第三方依赖。
Github 地址:https://github.com/elastic/elasticsearch 。
Lucene 是什么?
Lucene 是一个 Java 语言编写的高性能、全功能的文本搜索引擎库,提供强大的索引和搜索功能,以及拼写检查、高亮显示和高级分析功能。
如果我们直接基于 Lucene 开发,会非常复杂。并且,Lucene 并没有分布式以及高可用的解决方案。像 ElasticSearch 就是基于 Lucene 开发的,封装了许多 Lucene 底层功能,提供了简单易用的 RestFul API 接口和多种语言的客户端,开箱即用,自带分布式以及高可用的解决方案。
Github 地址:https://github.com/apache/lucene
Elasticsearch 可以帮助我们做什么?
举几个常见的例子:
实现各种网站的关键词检索功能,比如电商网站的商品检索、维基百科的词条搜索、Github 的项目检索;
本地生活类 APP 比如美团基于你的定位实现附近的一些美食或者娱乐项目的推荐;
结合 Elasticsearch、Kibana、Beats 和 Logstash 这些 Elastic Stack 的组件实现一个功能完善的日志系统。
使用 Elasticsearch 作为地理信息系统 (GIS) 管理、集成和分析空间信息。
……
电商网站检索:
ELK 日志采集系统架构(负责日志的搜索):
为什么需要 Elasticsearch?MySQL 不行吗?
正是谓术业有专攻!Elasticsearch 主要为系统提供搜索功能, MySQL 这类传统关系型数据库主要为系统提供数据存储功能。
MySQL 虽然也能提供简单的搜索功能,但是搜索并不是它擅长的领域。
我们可以从下面两个方面来看:
1)传统关系型数据库的痛点:
- 传统关系型数据库(如 MySQL )在大数据量下查询效率低下, 模糊匹配有可能导致全表扫描。
- MySQL 全文索引只支持 CHAR,VARCHAR 或者 TEXT 字段类型,不支持分词器。
2)Elasticsearch 的优势 :
- 支持多种数据类型,非结构化,数值,地理信息。
- 简单的 RESTful API,天生的兼容多语言开发。
- 提供更丰富的分词器,支持热点词汇查询。
- 近实时查询,Elasticsearch 每隔 1s 把数据存储至系统缓存中,且使用倒排索引提高检索效率。
- 支持相关性搜索,可以根据条件对结果进行打分。
- 天然分布式存储,使用分片支持更大的数据量。
Elasticsearch 中的基本概念
Index(索引) : 作为名词理解的话,索引是一类拥有相似特征的文档的集合比如商品索引、商家索引、订单索引,有点类似于 MySQL 中的数据库表。作为动词理解的话,索引就是将一份文档保存在一个索引中。
Document(文档) :可搜索最小单位,用于存储数据,一般为 JSON 格式。文档由一个或者多个字段(Field)组成,字段类型可以是布尔,数值,字符串、二进制、日期等数据类型。
Type(字段类型) : 每个文档在 ES 中都必须设定它的类型。ES 7.0 之前,一个 Index 可以有多个 Type。6.0 开始,Type 已经被 Deprecated。7.0 开始,一个索引只能创建一个 Type :_doc。8.0 之后,Type 被完全删除,删除的原因看这里:https://www.elastic.co/guide/en/elasticsearch/reference/7.17/removal-of-types.html 。
Mapping(映射) :定义字段名称、数据类型、优化信息(比如是否索引)、分词器,有点类似于数据库中的表结构定义。一个 Index 对应一个 Mapping。
Node(节点) : 相当于一个 ES 实例,多个节点构成一个集群。
Cluster(集群) :多个 ES 节点的集合,用于解决单个节点无法处理的搜索需求和数据存储需求。
Shard(分片): Index(索引)被分为多个碎片存储在不同的 Node 节点上的分片中,以提高性能和吞吐量。
Replica(副本) :Index 副本,每个 Index 有一个或多个副本,以提高拓展功能和吞吐量。
DSL(查询语言) :基于 JSON 的查询语言,类似于 SQL 语句。
MySQL 与 Elasticsearch 的概念简单类比:
MySQL | Elasticsearch |
---|---|
Table(表) | Index |
Row(行) | Document |
Column(列) | Field |
Schema(约束) | Mapping |
SQL(查询语言) | DSL |
倒排索引和正排索引
倒排索引是什么?
倒排索引 也被称作反向索引(inverted index),是用于提高数据检索速度的一种数据结构,空间消耗比较大。倒排索引首先将检索文档进行分词得到多个词语/词条,然后将词语和文档 ID 建立关联,从而提高检索效率。
分词就是对一段文本,通过规则或者算法分出多个词,每个词作为搜索的最细粒度一个个单字或者单词。分词的目的主要是为了搜索,尤其在数据量大的情况下,分词的实现可以快速、高效的筛选出相关性高的文档内容。
如下图所示,倒排索引使用 词语/词条(Term) 来作为索引关键字,并同时记录了哪些 文档(Document) 中有这个词语。
- 文档(Document) :用来搜索的数据,其中的每一条数据就是一个文档。例如一个商品信息、商家信息、一页网页的内容。
- 词语/词条(Term) :对文档数据或用户搜索数据,利用某种算法分词,得到的具备含义的词语就是词条。例如 ‘’数据库索引可以大幅提高查询速度” 这段话被中文分词器 IK Analyzer 细粒度分词后得到[数据库,索引,可以,大幅,提高,查询,速度]。
- 词典(Term Dictionary) :Term 的集合。
Lucene 就是基于倒排索引来做的全文检索,并且 ElasticSearch 还对倒排索引做了进一步优化。
倒排索引的创建和检索流程了解么?
这里只是简单介绍一下倒排索引的创建和检索流程,实际应用中,远比下面介绍的复杂,不过,大体原理还是一样的。
倒排索引创建流程:
建立文档列表,每个文档都有一个唯一的文档 ID 与之对应。
通过分词器对文档进行分词,生成类似于 <词语,文档ID> 的一组组数据。
将词语作为索引关键字,记录下词语和文档的对应关系,也就是哪些文档中包含了该词语。
这里可以记录更多信息比如词语的位置、词语出现的频率,这样可以方便高亮显示以及对搜索结果进行排序(后文会介绍到)。
Lucene 的倒排索引大致是下面这样的(图源:https://segmentfault.com/a/1190000037658997):
倒排索引检索流程:
根据分词查找对应文档 ID
根据文档 ID 找到文档
倒排索引由什么组成?
- 单词字典 :用于存储单词列表。一般用 B+Tree 或 Hash 拉链法存储,提高查询效率。
- 倒排列表 :记录单词对应的文档集合。分为:
- DocID:即文档 id
- TF : 单词出现频率,简称词频
- Position:单词在文档中出现的位置,用于检索
- Offset:偏移量,记录单词开始结束位置,用于高亮显示
正排索引呢?
不同于倒排索引,正排索引将文档 ID 和分词建立关联。
根据词语查询时,必须先逐条获取每个文档,然后判断文档中是否包含所需要的词语,查询效率较低。
倒排索引和正排索引的区别是什么?
正排索引:
- 优点:维护成本低,新增数据的时候,只要在末尾新增一个 ID
- 缺点:以 DocID 为索引,查询时需要扫描所有词语,一个一个比较,直至查到关键词,查询效率较低。
倒排索引:
- 优点:建立分词和 DocID 关系,大大提高查询效率
- 缺点:建立倒排索引的成本高。并且,维护起来也比较麻烦,因为文档的每次更新都意味着倒排索引的重建。还有一些搜索精度的问题,比如搜索dogs 和 dog 想要相同匹配结果,这时就需要合适的分词器了
Elasticsearch 可以针对某些地段不做索引吗?
文档会被序列化为字段组成的 JSON 格式保存在 ES 中。我们可以针对某些地段不做索引。
这样可以节省存储空间,但是,同时也会让字段无法被搜索。
分词器(Analyzer)
Analyzer 翻译成中文叫做分析器,不过,很多人一般习惯称呼其为分词器。
分词器有什么用?
分词器是搜索引擎的一个核心组件,负责对文档内容进行分词(在 ES 里面被称为 Analysis),也就是将一个文档转换成 单词词典(Term Dictionary) 。单词词典是由文档中出现过的所有单词构成的字符串集合。为了满足不同的分词需求,分词器有很多种,不同的分词器分词逻辑可能会不一样。
常用分词器有哪些?
非中文分词器:
Standard Analyzer:标准分词器,也是默认分词器, 英文转换成小写, 中文只支持单字切分。
Simple Analyzer:简单分词器,通过非字母字符来分割文本信息,英文大写转小写,非英文不进行分词。
Stop Analyzer :在 SimpleAnalyzer 基础上去除 the,a,is 等词,也就是加入了停用词。
Whitespace Analyzer : 空格分词器,通过空格来分割文本信息,非英文不进行分词。
上面这些也都是 ES 内置的分词器,详细介绍请看官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-analyzers.html。
这个官方文档为每一个分词器都列举了对应的例子帮助理解,比如 Standard Analyzer 的例子是下面这样的。
输入文本内容:
"The 2 QUICK Brown-Foxes jumped over the lazy dog's bone."
分词结果:
[ the, 2, quick, brown, foxes, jumped, over, the, lazy, dog's, bone ]
中文分词器:
IK Analyzer(推荐): 最常用的开源中文分词器,Github 地址:https://github.com/medcl/elasticsearch-analysis-ik,包括两种分词模式:
ik_max_word:细粒度切分模式,会将文本做最细粒度的拆分,尽可能多的拆分出词语 。
ik_smart:智能模式,会做最粗粒度的拆分,已被分出的词语将不会再次被其它词语占有。
Ansj :基于 n-Gram+CRF+HMM 的中文分词的 Java 实现,分词速度达到每秒钟大约 200 万字左右(mac air 下测试),准确率能达到 96%以上。实现了中文分词、中文姓名识别、用户自定义词典、关键字提取、自动摘要、关键字标记等功能。Github 地址:https://github.com/NLPchina/ansj_seg 。
ICU Analyzer:提供 Unicode 支持,更好地支持亚洲语言。
THULAC(THU Lexical Analyzer for Chinese) : 清华大学推出的一套中文词法分析工具包,具有中文分词和词性标注功能。Github 地址:https://github.com/thunlp/THULAC-Python 。
Jcseg :基于 mmseg 算法的一个轻量级中文分词器,同时集成了关键字提取,关键短语提取,关键句子提取和文章自动摘要等功能。Gitee 地址:https://gitee.com/lionsoul/jcseg 。
IK Analyzer 分词示例:
输入文本内容:
"数据库索引可以大幅提高查询速度"
分词结果:
细粒度切分模式:
[数据库,索引,可以,大幅,提高,查询,速度]
智能模式:
[数据库,数据,索引,可以,大幅,提高,查询,速度]
其他分词器 :
Keyword Analyzer :关键词分词器,输入文本等于输出文本。
Fingerprint Analyzer :指纹分析仪分词器,通过创建标记进行检测。
上面这两个也是 ES 内置的分词器。
Keyword Analyzer 分词示例:
输入文本内容:
"The 2 QUICK Brown-Foxes jumped over the lazy dog's bone."
分词结果:
[ The 2 QUICK Brown-Foxes jumped over the lazy dog's bone. ]
分词器由什么组成?
分析器由三种组件组成:
- Charater Filters:处理原始文本,例如去除 HTMl 标签。
- Tokenizer:按分词器规则切分单词。
- Token Filters:对切分后的单词加工,包括转小写,切除停用词,添加近义词
三者顺序:Character Filters —> Tokenizer —> Token Filter
三者个数:CharFilters(0 个或多个) + Tokenizer(一个) + TokenFilters(0 个或多个)
下图是默认分词器 Standard Analyzer 的分词流程。
Elasticsearch 如何基于拼音搜索?
对于中文内容来说,我们经常需要基于拼音来进行搜索。
在 Elasticsearch 中如何来实现基于拼音来搜索的呢? 我们可以使用 拼音分词器 ,拼音分词器用于汉字和拼音之间的转换,集成了 NLP 工具(https://github.com/NLPchina/nlp-lang),Github 地址:https://github.com/medcl/elasticsearch-analysis-pinyin。
数据类型
Elasticsearch 常见的数据类型有哪些?
常见类型:
关键词:
keyword
、constant_keyword
,和wildcard
数值型:
long
,integer
,short
,byte
,double
,float
,half_float
,scaled_float
布尔型:
boolean
日期型:
date
,date_nanos
二进制:
binary
结构化数据类型:
范围型:
integer_range
,float_range
,long_range
,double_range
,date_range
ip 地址类型 :
ip
软件版本 :
version
文字搜索类型:
非结构化文本 :
text
包含特殊标记的文本:
annotated-text
自动完成建议:
completion
对象和关系类型:
嵌套类型:
nested
、join
对象类型 :
object
、flattened
空间类型:
地理坐标类型 :
geo_point
地理形状类型 :
geo_shape
Elasticsearch 官方文档中有详细介绍到各个数据类型的使用:https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html 。
keyword 和 text 有什么区别?
keyword
不走分词器,而 text
会走分词器,使用keyword
关键字查询效率更高,一般在fields
中定义keyword
类型字段
1 | "name" : { |
Elasticsearch 是否有数组类型?
在 Elasticsearch 中,没有专门的数组数据类型。默认情况下,任何字段都可以包含零个或多个值,但是,数组中的所有值必须具有相同的数据类型。
Elasticsearch 怎么修改索引字段类型?
可以在 Mapping 中直接修改字段类型吗?
不可以!Elasticsearch 中的 Mapping 有点类似于数据库中的表结构定义,Mapping 中的字段类型只能增加不能修改,否则只能reindex
重新索引或者重新进行数据建模并导入数据。
什么是 Nested 数据类型?有什么用?
Elasticsearch 官方文档是这样介绍 Nested 数据类型的:
The
nested
type is a specialised version of the object data type that allows arrays of objects to be indexed in a way that they can be queried independently of each other.Nested (嵌套)类型是对象数据类型的特殊版本,它允许对象数组以一种可以相互独立查询的方式进行索引。
Nested 数据类型可以避免 数组扁平化处理,多个数组的字段会做一个笛卡尔积,导致查询出不存在的数据。
1 | // 会导致查询John White也会匹配,将类型改为nested问题解决 |
将多个字段值合并为一个字段怎么做?
使用 copy_to
,比如将 first_name 和 last_name 合并为 full_name ,但 full_name 不在查询结果中展示
1 | PUT my_index |
Mapping
什么是 Mapping?
Mapping(映射)定义字段名称、数据类型、优化信息(比如是否索引)、分词器,有点类似于数据库中的表结构定义。一个 Index 对应一个 Mapping。
Mapping 分为动态 Mapping 和显示 Mapping 两种:
- 动态 Mapping:根据待索引数据自动建立索引、自动定义映射类型。
- 显示 Mapping:手动控制字段的存储和索引方式比如哪些字符串字段应被视为全文字段。
1 | // 显示映射创建索引 |
动态 Mapping 使用起来比较简单,在初学 Elasticsearch 的时候可以使用。实际项目中,应该尽量手动定义映射关系。
为什么插入数据不用指定 Mapping?
因为在写入文档时,如果索引不存在,Elasticsearch 会自动根据数据类型 自动推断 Mapping 信息 (Dynamic Mapping),但有时候不是很准确。
有自定义过 Mapping 吗?你是怎么做的?
如果纯手写的话,工作量太大,还容易写错,所以可以参考以下步骤:
- 创建临时 Index,插入一些临时数据;
- 访问 Mapping API ,获取相关 Mapping 定义;
- 在此基础上进行修改,如添加 keyword,nested类型;
- 删除临时 Index。
动态 Mapping 有几种属性配置?
4 种,可在 Mapping
中配置 dynamic = true/runtime/false/strict
(默认为true
)。
dynamic = true
: 新字段被添加到映射中(默认)dynamic = runtime
新字段作为运行时字段添加到映射中,这些字段未编入索引,并_source 在查询时加载。dynamic = false
:新字段将被忽略,这些字段不会被索引或可搜索dynamic = strict
: 如果检测到新字段,则会抛出异常并拒绝文档,新字段必须显式添加到映射中。
动态 Mapping 如何防止字段无限增加?
摘自官方文档:Mapping limit settings 。
如果使用了动态映射,插入的每个新文档都可能引入新字段。在索引中定义太多字段会导致 映射爆炸 ,从而导致内存不足的错误和难以恢复的情况。使用 映射限制设置 来限制字段映射的数量(手动或动态创建)并防止映射爆炸。
index.mapping.total_fields.limit
:限制了索引中的字段最大数量。字段、对象映射以及字段别名计入此限制,默认值为 1000。限制的目的是为了防止映射和搜索变得太大。较高的值会导致性能下降和内存问题,尤其是在负载高或资源很少的集群中。index.mapping.depth.limit
:字段的最大深度,以内部对象的数量来衡量。如果所有字段都在根对象级别定义,则深度为 1。如果有一个对象映射,则深度为 2 ,默认为 20。index.mapping.nested_fields.limit:nested
索引中不同映射的最大数量,nested
类型只应在需要相互独立地查询对象数组时使用,默认为 50。index.mapping.nested_objects.limit
:单个文档可以包含的嵌套 JSON 对象(nested
类型)的最大数量,默认为 10000。index.mapping.field_name_length.limit
:设置字段名称的最大长度,默认为Long.MAX_VALUE
(无限制)。index.mapping.dimension_fields.limit
:仅供 Elastic 内部使用,索引的最大时间序列维度数;默认为 16。
想要某个字段不被索引怎么做?
在 Mapping
中设置属性 index = false
,则该字段不可作为检索条件,但结果中还是包含该字段
与此相关的属性还有 index_options
可以控制倒排索引记录内容,属性有:
docs
: 只包括 docIDfreqs
: 包括 docID/词频options
:默认属性,docID/词频/位置offsets
: docID/词频/位置/字符偏移量
记录内容越多,占用空间越大,但是检索越精确
查询语句
查询语句的分类?
1、请求体查询(最常用)
将相关查询条件放在请求体中。
1 | GET /shirts/_search |
请求体查询又称为 Query DSL (Domain Specific Language)
领域特定语言,包括:
- 叶子查询:指定条件指定字段查询,包括
term
分词查询和全文检索(match,match_phrase
) - 复合查询:可包含叶子查询语句和复合查询,主要包括
bool
和dis_max
2、请求 URI
将相关查询条件放在 URI 中,这种方式不常用,了解即可
1 | GET /users/\_search?q=\*&sort=age:asc&pretty |
3、类 SQL 检索
1 | POST /_sql?format=txt |
功能还不完备,不推荐使用。
Term 查询和全文检索区别?
term 查询条件不做分词处理,只有查询词和文档中的词精确匹配才会被搜索到,一般用于非文本字段查询。
1 | # 查询用户名中含有关键词 “张寒” 的人 |
全文检索一般用于 文本查询
,会使用对应分词器,步骤为:分词->词项逐个查询->汇总多个词项得分。
如何实现范围查询?
range 查询用于匹配在某一范围内的数值型、日期类型或者字符串型字段的文档,比如出生日期在 1996-01-01 到 2000-01-01 的人。使用 range 查询只能查询一个字段,不能作用在多个字段上。
range 查询支持的参数有以下几种:
gt
大于,查询范围的最小值,也就是下界,但是不包含临界值。gte
大于等于,和gt
的区别在于包含临界值。lt
小于,查询范围的最大值,也就是上界,但是不包含临界值。lte
小于等于,和lt
的区别在于包含临界值。
1 | # 查询出生日期在 1996-01-01 到 2000-01-01 的人 |
Match 和 Match_phrase 区别?
match
查询多个检索词之间默认是 or 关系,可使用operator
改为 and 关系
match_phrase
查询多个检索词之间默认是 and 关系,并且词的位置关系影响搜索结果
Multi match 有几种匹配策略,都有什么区别?
Multi match 用于单条件多字段查询,有以下几种常用的匹配策略:
best_fields
(默认) :查询结果包含任一查询条件,但最终得分为最佳匹配字段得分most_fields
:查询结果包含任一查询条件,但最终得分 合并所有匹配字段得分,默认查询条件之间是 or 连接cross_fields
:跨字段匹配,解决了most_fields
查询词无法使用and
连接的问题,匹配更加精确,and
相当于整合多个字段为一个字段,但又不像copy_to
占用存储空间。
1 | # 查询域为 title 和 description |
bool 查询有几种查询子句?
bool
一般用于多条件多字段查询,可包含match
,match_phrase
,term
等简单查询语句,主要有以下 4 种查询子句
must
: 结果必须匹配must
查询条件,贡献算分should
: 结果应该匹配should
子句查询的一个或多个,贡献算分must_not
: 结果必须不能匹配该查询条件filter
: 结果必须匹配该过滤条件,但不计算得分,可提高查询效率
比如,你想在北京找一个有房或者有车 ,身高不低于 150 的女朋友,下面这条语句安排上。
1 | GET /users/_search |
数据同步
Elasticsearch 和 MySQL 同步的策略有哪些?
我们可以将同步类型分为 全量同步和增量同步。
全量同步即建好 Elasticsearch 索引后一次性导入 MySQL 所有数据。全量同步有很多现成的工具可以用比如 go-mysql-elasticsearch、Datax。
go-mysql-elasticsearch 是一项将 MySQL 数据自动同步到 Elasticsearch 的服务,同样支持增量同步。Github 地址:https://github.com/go-mysql-org/go-mysql-elasticsearch 。
DataX 是阿里云 DataWorks 数据集成 的开源版本,在阿里巴巴集团内被广泛使用的离线数据同步工具/平台。DataX 实现了包括 MySQL、Oracle、OceanBase、SqlServer、Postgre、HDFS、Hive、ADS、HBase、TableStore(OTS)、MaxCompute(ODPS)、Hologres、DRDS 等各种异构数据源之间高效的数据同步功能。Github 地址: https://github.com/alibaba/DataX。
另外,除了插件之外,像我们比较熟悉的 Canal 除了支持 binlog 实时增量同步 数据库之外也支持全量同步 。
增量同步即对 MySQL 中新增,修改,删除的数据进行同步:
- 同步双写 :修改数据时同步到 Elasticsearch。这种方式性能较差、存在丢数据风险且会耦合大量数据同步代码,一般不会使用。
- 异步双写 :修改数据时,使用 MQ 异步写入 Elasticsearch 提高效率。这种方式引入了新的组件和服务,增加了系统整体复杂性。
- 定时器 :定时同步数据到 Elasticsearch。这种方式时效性差,通常用于数据实时性不高的场景
- binlog 同步组件 Canal(推荐) : 使用 Canal 可以做到业务代码完全解耦,API 完全解耦,零代码实现准实时同步, Canal 通过解析 MySQL 的 binlog 日志文件进行数据同步。
关于增量同步的详细介绍,可以看这篇回答: https://www.zhihu.com/question/47600589/answer/2843488695 。
Canal 增量数据同步 Elasticsearch 的原理了解吗?
这个在 Canal 官方文档中有详细介绍到,原理非常简单:
- Canal 模拟 MySQL Slave 节点与 MySQL Master 节点的交互协议,把自己伪装成一个 MySQL Slave 节点,向 MySQL Master 节点请求 binlog;
- MySQL Master 节点接收到请求之后,根据偏移量将新的 binlog 发送给 MySQL Slave 节点;
- Canal 接收到 binlog 之后,就可以对这部分日志进行解析,获取主库的结构及数据变更。
Elasticsearch 集群
Elasticsearch 集群
Elasticsearch 集群是什么?有什么用?
单台 Elasticsearch 服务器负载能力和存储能力有限,很多时候通过增加服务器配置也没办法满足我们的要求。并且,单个 Elasticsearch 节点会存在单点风险,没有做到高可用。为此,我们需要搭建 Elasticsearch 集群。
Elasticsearch 集群说白了就是多个 Elasticsearch 节点的集合,这些节点共同协作,一起提供服务,这样就可以解决单台 Elasticsearch 服务器无法处理的搜索需求和数据存储需求。出于高可用方面的考虑,集群中节点数量建议 3 个以上,并且其中至少两个节点不是仅投票主节点(后文会介绍到)。
Elasticsearch 集群可以很方便地实现横向扩展,我们可以动态添加或者删除 Elasticsearch 节点。当有节点加入集群中或者从集群中移除节点时,集群将会重新平均分布所有的数据。
Elasticsearch 集群中的节点角色有哪些?
Elasticsearch 7.9 之前的版本中的节点类型:数据节点、协调节点、候选主节点、ingest 节点。在 Elasticsearch 7.9 以及之后,节点类型升级为节点角色(Node roles)。节点角色分的很细:数据节点角色、主节点角色、ingest 节点角色、热节点角色等。
节点角色主要是为了解决基于节点类型配置复杂和用户体验差的问题。
Elasticsearch 集群一般是由多个节点共同组成的分布式集群,节点之间互通,彼此配合,共同对外提供搜索和索引服务(节点之间能够将客户端请求转向到合适的节点)。不同的节点会负责不同的角色,有的负责一个,有的可能负责多个。
在 ES 中我们可以通过配置使一个节点有以下一个或多个角色:
- 主节点(Master-eligible node) :集群层面的管理,例如创建或删除索引、跟踪哪些节点是集群的一部分,以及决定将哪些分片分配给哪些节点。任何不是仅投票主节点的合格主节点都可以通过主选举过程被选为主节点。
- 专用备选主节点(Dedicated master-eligible node) : Elasticsearch 集群中,设置了只能作为主节点的节点。设置专用主节点主要是为了保障集群增大时的稳定性,建议专用主节点个数至少为 3 个。
- 仅投票主节点(Voting-only master-eligible node): 仅参与主节点选举投票,不会被选为主节点,硬件配置可以较低。
- 数据节点(data node) :数据存储和数据处理比如 CRUD、搜索、聚合。
- 预处理节点(ingest node) :执行由预处理管道组成的预处理任务。
- 仅协调节点(coordinating only node) :路由分发请求、聚集搜索或聚合结果。
- 远程节点(Remote-eligible node) :跨集群检索或跨集群复制。
- ……
高可用性 (HA) 集群需要至少三个符合主节点条件的节点,其中至少两个节点不是仅投票主节点。即使其中一个节点发生故障,这样的集群也能够选举出一个主节点。
分片是什么?有什么用?
类似问题:Elasticsearch 集群中的数据是如何被分配的?
分片(Shard) 是集群数据的容器,Index(索引)被分为多个文档碎片存储在分片中,分片又被分配到集群内的各个节点里。当需要查询一个文档时,需要先找到其位于的分片。也就是说,分片是 Elasticsearch 在集群内分发数据的单位。
每个分片都是一个 Lucene 索引实例,您可以将其视作一个独立的搜索引擎,它能够对 Elasticsearch 集群中的数据子集进行索引并处理相关查询。
整个 Elasticsearch 集群的核心就是对所有的分片执行分布存储,索引,负载,路由的工作。
当集群规模扩大或者缩小时, Elasticsearch 会自动的在各节点中迁移分片,使得数据仍然均匀分布在集群里。Elasticsearch 在对数据进行再平衡时移动分片的速度取决于分片的大小和数量,以及网络和磁盘性能。
一个分片可以是 主分片(Primary Shard) 或者 副本分片(Replica Shard) 。一个副本分片只是一个主分片的拷贝。副本分片作为硬件故障时保护数据不丢失的冗余备份,并为搜索和返回文档等读操作提供服务。查询吞吐量可以随着副本分片数量的增加而增长,与此同时,使用分片副本还可以处理查询的发并量。
当我们写索引数据的时候,只能写在主分片上,然后再同步到副本分片。
当主分片出现问题的时候,会从可用的副本分片中选举一个新的主分片。在默认情况下,ElasticSearch 会为主分片创建一个副本分片。由于副本分片同样会占用资源,因此,不建议为一个主分片分配过多的副本分片,应该充分结合业务需求来选定副本分片的数量。
从 Elasticsearch 版本 7 开始,每个索引的主分片数量的默认值为 1,默认的副本分片数为 0。在早期版本中,默认值为 5 个主分片。在生产环境中,副本分片数至少为 1。
最后,简单总结一下:
- 分片是 Elasticsearch 在集群内分发数据的单位。整个 Elasticsearch 集群的核心就是对所有的分片执行分布存储,索引,负载,路由的工作。
- 副本分片主要是为了提高可用性,由于副本分片同样会占用资源,不建议为一个主分片分配过多的副本分片。
- 当我们写索引数据的时候,只能写在主分片上,然后再同步到副本分片。
- 当主分片出现问题的时候,会从可用的副本分片中选举一个新的主分片。
查询文档时如何找到对应的分片?
我们需要查询一个文档的时候,需要先找到其位于那一个分片中。那究竟是如何知道一个文档应该存放在哪个分片中呢?
这个过程是根据路由公式来决定的:
1 | shard = hash(routing) % number_of_primary_shards |
routing
是一个可以配置的变量,默认是使用文档的 id。对 routing
取哈希再除以number_of_primary_shards
(索引创建时指定的分片总数)得到的余数就是对应的分片。
当一个查询请求到达 仅协调节点(coordinating only node) 后,仅协调节点会根据路由公式计算出目标分片,然后再将请求转发到目标分片的主分片节点上。
上面公式也解释了为什么我们要在创建索引的时候就确定好主分片的数量,并且不允许改变索引分片数。因为如果数量变化了, 那么所有之前路由的计算值都会无效,文档也再也找不到了。
自定义路由有什么好处?
默认的路由规则会尽量保证数据会均匀地保存到每一个分片上面。这样做的好处是,一旦某个分片出了故障,ES 集群里的任何索引都不会出现一个文档都查不到的情况,所有索引都只会丢失故障分片上面存储的文档而已,这个给修复故障分片争取了时间。
不过,这种路由规则也有一个弊端,文档均匀分配到多个分片上面了,所以每次查询索引结果都需要向多个分片发送请求,然后再将这些分片返回的结果融合到一起返回到终端。很显然这样一来系统的压力就会增大很多,如果索引数据量不大的情况下,效率会非常差。
如果我们想要让某一类型的文档都被存储到同一分片的话,可以自定义路由规则。所有的文档 API 请求(get,index,delete,bulk,update)都接受一个叫做 routing 的路由参数,通过这个参数我们可以自定义文档到数据分片的映射规则。
如何查看 Elasticsearch 集群健康状态?
在 Kibana 控制台执行以下命令可以查看集群的健康状态:
1 | GET /_cluster/health |
正常情况下,返回如下结果。
1 | { |
接口返回参数解释如下:
指标 | 含义 |
---|---|
cluster_name | 集群的名称 |
status | 集群的运行状况,基于其主要和副本分片的状态。 |
timed_out | 如果 false 响应在 timeout 参数指定的时间段内返回(30s 默认情况下) |
number_of_nodes | 集群中的节点数 |
number_of_data_nodes | 作为专用数据节点的节点数 |
active_primary_shards | 活动主分区的数量 |
active_shards | 活动主分区和副本分区的总数 |
relocating_shards | 正在重定位的分片的数量 |
initializing_shards | 正在初始化的分片数 |
unassigned_shards | 未分配的分片数 |
delayed_unassigned_shards | 其分配因超时设置而延迟的分片数 |
number_of_pending_tasks | 尚未执行的集群级别更改的数量 |
number_of_in_flight_fetch | 未完成的访存数量 |
task_max_waiting_in_queue_millis | 自最早的初始化任务等待执行以来的时间(以毫秒为单位) |
active_shards_percent_as_number | 群集中活动碎片的比率,以百分比表示 |
Elasticsearch 集群健康状态有哪几种?
Elasticsearch 集群健康状态分为三种:
- GREEN (健康状态):最健康的状态,集群中的主分片和副本分片都可用。
- YELLOW (预警状态):主分片都可用,但存在副本分片不可能。
- RED (异常状态):存在不可用的主分片,搜索结果可能会不完整。
如何分析 Elasticsearch 集群异常问题?
1、找到异常索引
1 | # 查看索引情况并根据返回找到状态异常的索引 |
2、查看详细的异常信息
1 | GET /_cluster/allocation/explain |
通过异常信息进一步分析问题的原因。
性能优化
Elasticsearch 如何选择硬件配置?
- 部署 Elasticsearch 对于机器的 CPU 要求并不高,通常选择 2 核或者 4 核的就差不多了。
- Elasticsearch 中的很多操作是比较消耗内存的,如果搜索需求比较大的话,建议选择 16GB 以上的内存。具体如何分配内存呢?通常是 50% 给 ES,50% 留给 Lucene。另外,建议禁止 swap。如果不禁止的话,当内存耗尽时,操作系统就会自动把内存中暂时不使用的数据交换到硬盘中,需要使用的时候再从硬盘交换到内存,频繁硬盘操作对性能影响是致命的。
- 磁盘的速度相对比较慢,尽量使用固态硬盘(SSD)。
Elasticsearch 索引优化策略有哪些?
- ES 提供了 Bulk API 支持批量操作,当我们有大量的写任务时,可以使用 Bulk 来进行批量写入。不过,使用 Bulk 请求时,每个请求尽量不要超过几十 M,因为太大会导致内存使用过大。
- ES 默认副本数量为 3 个,这样可以提高可用性,但会影响写入索引的效率。某些业务场景下,可以设置副本数量为 1 或者 0,提高写入索引的效率。
- ES 在写入数据的时候,采用延迟写入的策略,默认 1 秒之后将内存中 segment 数据刷新到磁盘中,此时我们才能将数据搜索出来。这就是为什么 Elasticsearch 提供的是近实时搜索功能。某些业务场景下,可以增加刷新时间间隔比如设置刷新时间间隔为 30s(
index.refresh_interval=30s
),减少 segment 合并压力,提高写入索引的效率。 - 加大
index_buffer_size
,这个是 ES 活跃分片共享的内存区,官方建议每个分片至少 512MB,且为 JVM 内存的 10%。 - 使用 ES 的默认 ID 生成策略或使用数字类型 ID 做为主键。
- 合理的配置使用 index 属性,
analyzed
和not_analyzed
,根据业务需求来控制字段是否分词或不分词。只有groupby
需求的字段,配置时就设置成not_analyzed
,以提高查询或聚类的效率。 - 加大 Flush 设置。 Flush 的主要目的是把文件缓存系统中的段持久化到硬盘,当 Translog 的数据量达到 512MB 或者 30 分钟时,会触发一次 Flush,我们可以加大
index.translog.flush_threshold_size
,但必须为操作系统的文件缓存系统留下足够的空间。 - ……
Elasticsearch 查询优化策略有哪些?
- 建立冷热索引库(可用固态硬盘存放热库数据,普通硬盘存放冷库数据),热库数据可以提前预热加载至内存,提高检索效率。
- 自定义路由规则,让某一类型的文档都被存储到同一分片。
- 使用
copy_to
将多个字段整合为一个。 - 控制字段的数量,业务中不使用的字段,就不要索引。
- 不要返回无用的字段,使用
_source
进行指定。 - 避免大型文档存储,默认最大长度为 100MB。
- 使用
keyword
数据类型,该类型不会走分词器,效率大大提高。 - 开启慢查询配置定位慢查询。
- ES 查询的时候,使用 filter 查询会使用 query cache, 如果业务场景中的过滤查询比较多,建议将 querycache 设置大一些,以提高查询速度。
- 尽量避免分页过深。
- 增加分片副本提高查询吞吐量,避免使用通配符。
- 加大堆内存,ES 默认安装后设置的内存是 1GB,可以适当加大但不要超过物理内存的 50%,且最好不要超过 32GB。
- 分配一半物理内存给文件系统缓存,以便加载热点数据。
- ……
文章推荐
参考
Elasticsearch 官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/elasticsearch-intro.html
Elasticsearch 中文指南:https://endymecy.gitbooks.io/elasticsearch-guide-chinese/content/index.html
Mastering Elasticsearch(中文版):https://doc.yonyoucloud.com/doc/mastering-elasticsearch/index.html
Elasticsearch Service 相关概念 - 腾讯云:https://cloud.tencent.com/document/product/845/32086
Node - Elasticsearch 官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-node.html
Elasticsearch 有没有数组类型?有哪些坑?:https://mp.weixin.qq.com/s/FCjrn609vYU-URlhVfjD7A
Elasticsearch 实现基于拼音搜索:https://www.cnblogs.com/huan1993/p/17053317.html
Elasticsearch 查询语句语法详解 :https://www.cnblogs.com/Gaimo/p/16036853.html
《Elasticsearch 权威指南》- 集群内的原理:https://www.elastic.co/guide/cn/elasticsearch/guide/current/distributed-cluster.html
Elasticsearch 分布式路由策略:https://zhuanlan.zhihu.com/p/386368763
How to Choose the Correct Number of Shards per Index in Elasticsearch:https://opster.com/guides/elasticsearch/capacity-planning/elasticsearch-number-of-shards/
Elasticsearch 集群异常状态(RED、YELLOW)原因分析:https://cloud.tencent.com/developer/article/1803943
超详细的 Elasticsearch 高性能优化实践 :https://cloud.tencent.com/developer/article/1436787
常见框架
SpringBoot 常见面试题总结
剖析面试最常见问题之 Spring Boot
市面上关于 Spring Boot 的面试题抄来抄去,毫无价值可言。
这篇文章,我会简单就自己这几年使用 Spring Boot 的一些经验,总结一些常见的面试题供小伙伴们自测和学习。少部分关于 Spring/Spring Boot 的介绍参考了官网,其他皆为原创。
1. 简单介绍一下 Spring?有啥缺点?
Spring 是重量级企业开发框架 Enterprise JavaBean(EJB) 的替代品,Spring 为企业级 Java 开发提供了一种相对简单的方法,通过 依赖注入 和 面向切面编程 ,用简单的 Java 对象(Plain Old Java Object,POJO) 实现了 EJB 的功能
虽然 Spring 的组件代码是轻量级的,但它的配置却是重量级的(需要大量 XML 配置) 。
为此,Spring 2.5 引入了基于注解的组件扫描,这消除了大量针对应用程序自身组件的显式 XML 配置。Spring 3.0 引入了基于 Java 的配置,这是一种类型安全的可重构配置方式,可以代替 XML。
尽管如此,我们依旧没能逃脱配置的魔爪。开启某些 Spring 特性时,比如事务管理和 Spring MVC,还是需要用 XML 或 Java 进行显式配置。启用第三方库时也需要显式配置,比如基于 Thymeleaf 的 Web 视图。配置 Servlet 和过滤器(比如 Spring 的DispatcherServlet)同样需要在 web.xml 或 Servlet 初始化代码里进行显式配置。组件扫描减少了配置量,Java 配置让它看上去简洁不少,但 Spring 还是需要不少配置。
光配置这些 XML 文件都够我们头疼的了,占用了我们大部分时间和精力。除此之外,相关库的依赖非常让人头疼,不同库之间的版本冲突也非常常见。
2. 为什么要有 SpringBoot?
Spring 旨在简化 J2EE 企业应用程序开发。Spring Boot 旨在简化 Spring 开发(减少配置文件,开箱即用!)。
3. 说出使用 Spring Boot 的主要优点
- 开发基于 Spring 的应用程序很容易。
- Spring Boot 项目所需的开发或工程时间明显减少,通常会提高整体生产力。
- Spring Boot 不需要编写大量样板代码、XML 配置和注释。
- Spring 引导应用程序可以很容易地与 Spring 生态系统集成,如 Spring JDBC、Spring ORM、Spring Data、Spring Security 等。
- Spring Boot 遵循“固执己见的默认配置”,以减少开发工作(默认配置可以修改)。
- Spring Boot 应用程序提供嵌入式 HTTP 服务器,如 Tomcat 和 Jetty,可以轻松地开发和测试 web 应用程序。(这点很赞!普通运行 Java 程序的方式就能运行基于 Spring Boot web 项目,省事很多)
- Spring Boot 提供命令行接口(CLI)工具,用于开发和测试 Spring Boot 应用程序,如 Java 或 Groovy。
- Spring Boot 提供了多种插件,可以使用内置工具(如 Maven 和 Gradle)开发和测试 Spring Boot 应用程序。
4. 什么是 Spring Boot Starters?
Spring Boot Starters 是一系列依赖关系的集合,因为它的存在,项目的依赖之间的关系对我们来说变的更加简单了。
举个例子:在没有 Spring Boot Starters 之前,我们开发 REST 服务或 Web 应用程序时; 我们需要使用像 Spring MVC,Tomcat 和 Jackson 这样的库,这些依赖我们需要手动一个一个添加。但是,有了 Spring Boot Starters 我们只需要一个只需添加一个spring-boot-starter-web一个依赖就可以了,这个依赖包含的子依赖中包含了我们开发 REST 服务需要的所有依赖。
1 | <dependency> |
5. Spring Boot 支持哪些内嵌 Servlet 容器?
Spring Boot 支持以下嵌入式 Servlet 容器:
Name | Servlet Version |
---|---|
Tomcat 9.0 | 4.0 |
Jetty 9.4 | 3.1 |
Undertow 2.0 | 4.0 |
您还可以将 Spring 引导应用程序部署到任何 Servlet 3.1+兼容的 Web 容器中。
这就是你为什么可以通过直接像运行 普通 Java 项目一样运行 SpringBoot 项目。这样的确省事了很多,方便了我们进行开发,降低了学习难度。
6. 如何在 Spring Boot 应用程序中使用 Jetty 而不是 Tomcat?
Spring Boot (spring-boot-starter-web)使用 Tomcat 作为默认的嵌入式 servlet 容器, 如果你想使用 Jetty 的话只需要修改pom.xml(Maven)或者build.gradle(Gradle)就可以了。
Maven:
1 | <!--从Web启动器依赖中排除Tomcat--> |
Gradle:
1 | compile("org.springframework.boot:spring-boot-starter-web") { |
说个题外话,从上面可以看出使用 Gradle 更加简洁明了,但是国内目前还是 Maven 使用的多一点,我个人觉得 Gradle 在很多方面都要好很多。
7. 介绍一下@SpringBootApplication 注解
1 | package org.springframework.boot.autoconfigure; |
1 | package org.springframework.boot; |
可以看出大概可以把 @SpringBootApplication
看作是@Configuration、@EnableAutoConfiguration、@ComponentScan
注解的集合。根据 SpringBoot 官网,这三个注解的作用分别是:
@EnableAutoConfiguration
:启用 SpringBoot 的自动配置机制@ComponentScan
: 扫描被@Component
(@Service,@Controller
)注解的bean
,注解默认会扫描该类所在的包下所有的类。@Configuration
:允许在上下文中注册额外的bean
或导入其他配置类
8. Spring Boot 的自动配置是如何实现的?
这个是因为@SpringBootApplication
注解的原因,在上一个问题中已经提到了这个注解。我们知道 @SpringBootApplication
看作是 @Configuration、@EnableAutoConfiguration、@ComponentScan
注解的集合。
@EnableAutoConfiguration
:启用 SpringBoot 的自动配置机制@ComponentScan
: 扫描被@Component (@Service,@Controller)
注解的 bean,注解默认会扫描该类所在的包下所有的类。@Configuration
:允许在上下文中注册额外的 bean 或导入其他配置类
@EnableAutoConfiguration是启动自动配置的关键,源码如下(建议自己打断点调试,走一遍基本的流程):
1 | import java.lang.annotation.Documented; |
@EnableAutoConfiguration
注解通过 Spring 提供的 @Import
注解导入了AutoConfigurationImportSelector
类(@Import
注解可以导入配置类或者 Bean 到当前类中)。
AutoConfigurationImportSelector
类中getCandidateConfigurations
方法会将所有自动配置类的信息以List
的形式返回。这些配置信息会被 Spring 容器作 bean
来管理。
1 | protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { |
自动配置信息有了,那么自动配置还差什么呢?
@Conditional
注解。@ConditionalOnClass
(指定的类必须存在于类路径下),@ConditionalOnBean
(容器中是否有指定的 Bean)等等都是对@Conditional
注解的扩展。
拿 Spring Security 的自动配置举个例子:SecurityAutoConfiguration
中导入了WebSecurityEnablerConfiguration
类,WebSecurityEnablerConfiguration
源代码如下:
1 |
|
WebSecurityEnablerConfiguration
类中使用@ConditionalOnBean
指定了容器中必须还有WebSecurityConfigurerAdapter
类或其实现类。所以,一般情况下 Spring Security 配置类都会去实现 WebSecurityConfigurerAdapter
,这样自动将配置就完成了。
9. 开发 RESTful Web 服务常用的注解有哪些?
Spring Bean 相关:
- @Autowired : 自动导入对象到类中,被注入进的类同样要被 Spring 容器管理。
- @RestController : @RestController注解是@Controller和@ResponseBody的合集,表示这是个控制器 bean,并且是将函数的返回值直 接填入 HTTP 响应体中,是 REST 风格的控制器。
- @Component :通用的注解,可标注任意类为 Spring 组件。如果一个 Bean 不知道属于哪个层,可以使用@Component 注解标注。
- @Repository : 对应持久层即 Dao 层,主要用于数据库相关操作。
- @Service : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。
- @Controller : 对应 Spring MVC 控制层,主要用于接受用户请求并调用 Service 层返回数据给前端页面。
处理常见的 HTTP 请求类型:
- @GetMapping : GET 请求、
- @PostMapping : POST 请求。
- @PutMapping : PUT 请求。
- @DeleteMapping : DELETE 请求。
前后端传值:
- @RequestParam以及@Pathvairable :@PathVariable用于获取路径参数,@RequestParam用于获取查询参数。
- @RequestBody :用于读取 Request 请求(可能是 POST,PUT,DELETE,GET 请求)的 body 部分并且 Content-Type 为 application/json 格式的数据,接收到数据之后会自动将数据绑定到 Java 对象上去。系统会使用HttpMessageConverter或者自定义的HttpMessageConverter将请求的 body 中的 json 字符串转换为 java 对象。
详细介绍可以查看这篇文章:《Spring/Spring Boot 常用注解总结》 。
10. Spirng Boot 常用的两种配置文件
我们可以通过 application.properties或者 application.yml 对 Spring Boot 程序进行简单的配置。如果,你不进行配置的话,就是使用的默认配置。
11. 什么是 YAML?YAML 配置的优势在哪里 ?
YAML 是一种人类可读的数据序列化语言。它通常用于配置文件。与属性文件相比,如果我们想要在配置文件中添加复杂的属性,YAML 文件就更加结构化,而且更少混淆。可以看出 YAML 具有分层配置数据。
相比于 Properties 配置的方式,YAML 配置的方式更加直观清晰,简介明了,有层次感。
但是,YAML 配置的方式有一个缺点,那就是不支持 @PropertySource 注解导入自定义的 YAML 配置。
12. Spring Boot 常用的读取配置文件的方法有哪些?
我们要读取的配置文件application.yml 内容如下:
1 | wuhan2020: 2020年初武汉爆发了新型冠状病毒,疫情严重,但是,我相信一切都会过去!武汉加油!中国加油! |
12.1. 通过 @value 读取比较简单的配置信息
使用 @Value("${property}")
读取比较简单的配置信息:
1 |
|
需要注意的是
@value
这种方式是不被推荐的,Spring 比较建议的是下面几种读取配置信息的方式。
12.2. 通过@ConfigurationProperties读取并与 bean 绑定
LibraryProperties 类上加了
@Component
注解,我们可以像使用普通 bean 一样将其注入到类中使用。
1 | import lombok.Getter; |
这个时候你就可以像使用普通 bean 一样,将其注入到类中使用:
1 | package cn.javaguide.readconfigproperties; |
控制台输出:
1 | 湖北武汉加油中国加油 |
12.3. 通过@ConfigurationProperties读取并校验
我们先将application.yml
修改为如下内容,明显看出这不是一个正确的 email 格式:
1 | my-profile: |
ProfileProperties 类没有加 @Component 注解。我们在我们要使用ProfileProperties 的地方使用@EnableConfigurationProperties注册我们的配置 bean:
1 | import lombok.Getter; |
具体使用:
1 | package cn.javaguide.readconfigproperties; |
因为我们的邮箱格式不正确,所以程序运行的时候就报错,根本运行不起来,保证了数据类型的安全性:
1 | Binding to target org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'my-profile' to cn.javaguide.readconfigproperties.ProfileProperties failed: |
我们把邮箱测试改为正确的之后再运行,控制台就能成功打印出读取到的信息:
1 | ProfileProperties(name=Guide哥, email=koushuangbwcx@163.com, handsome=true) |
12.4. @PropertySource读取指定的 properties 文件
1 | import lombok.Getter; |
使用:
1 |
|
13. Spring Boot 加载配置文件的优先级了解么?
Spring 读取配置文件也是有优先级的,直接上图:
14. 常用的 Bean 映射工具有哪些?
我们经常在代码中会对一个数据结构封装成DO、SDO、DTO、VO等,而这些Bean中的大部分属性都是一样的,所以使用属性拷贝类工具可以帮助我们节省大量的 set 和 get 操作。
常用的 Bean 映射工具有:Spring BeanUtils、Apache BeanUtils、MapStruct、ModelMapper、Dozer、Orika、JMapper 。
由于 Apache BeanUtils 、Dozer 、ModelMapper 性能太差,所以不建议使用。MapStruct 性能更好而且使用起来比较灵活,是一个比较不错的选择。
15. Spring Boot 如何监控系统实际运行状况?
我们可以使用 Spring Boot Actuator 来对 Spring Boot 项目进行简单的监控。
1 | <dependency> |
集成了这个模块之后,你的 Spring Boot 应用程序就自带了一些开箱即用的获取程序运行时的内部状态信息的 API。
比如通过 GET 方法访问 /health
接口,你就可以获取应用程序的健康指标。
16. Spring Boot 如何做请求参数校验?
数据的校验的重要性就不用说了,即使在前端对数据进行校验的情况下,我们还是要对传入后端的数据再进行一遍校验,避免用户绕过浏览器直接通过一些 HTTP 工具直接向后端请求一些违法数据。
Spring Boot 程序做请求参数校验的话只需要spring-boot-starter-web 依赖就够了,它的子依赖包含了我们所需要的东西。
16.1. 校验注解
JSR 提供的校验注解:
- @Null 被注释的元素必须为 null
- @NotNull 被注释的元素必须不为 null
- @AssertTrue 被注释的元素必须为 true
- @AssertFalse 被注释的元素必须为 false
- @Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
- @Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
- @DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
- @DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
- @Size(max=, min=) 被注释的元素的大小必须在指定的范围内
- @Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
- @Past 被注释的元素必须是一个过去的日期
- @Future 被注释的元素必须是一个将来的日期
- @Pattern(regex=,flag=) 被注释的元素必须符合指定的正则表达式
Hibernate Validator 提供的校验注解:
- @NotBlank(message =) 验证字符串非 null,且长度必须大于 0
- @Email 被注释的元素必须是电子邮箱地址
- @Length(min=,max=) 被注释的字符串的大小必须在指定的范围内
- @NotEmpty 被注释的字符串的必须非空
- @Range(min=,max=,message=) 被注释的元素必须在合适的范围内
使用示例:
1 |
|
16.2. 验证请求体(RequestBody)
我们在需要验证的参数上加上了@Valid
注解,如果验证失败,它将抛出MethodArgumentNotValidException
。默认情况下,Spring 会将此异常转换为 HTTP Status 400(错误请求)。
1 |
|
16.3. 验证请求参数(Path Variables 和 Request Parameters)
一定一定不要忘记在类上加上 Validated 注解了,这个参数可以告诉 Spring 去校验方法参数。
1 |
|
更多内容请参考我的原创: 如何在 Spring/Spring Boot 中做参数校验?你需要了解的都在这里!
17. 如何使用 Spring Boot 实现全局异常处理?
可以使用 @ControllerAdvice 和 @ExceptionHandler 处理全局异常。
更多内容请参考我的原创 :Spring Boot 异常处理在实际项目中的应用
18. Spring Boot 中如何实现定时任务 ?
我们使用 @Scheduled 注解就能很方便地创建一个定时任务。
1 |
|
单纯依靠 @Scheduled
注解 还不行,我们还需要在 SpringBoot 中我们只需要在启动类上加上@EnableScheduling
注解,这样才可以启动定时任务。@EnableScheduling
注解的作用是发现注解 @Scheduled
的任务并在后台执行该任务。
Netty 常见面试题总结
很多小伙伴搞不清楚为啥要学习 Netty ,正式今天这篇文章开始之前,简单说一下自己的看法:
- Netty 基于 NIO (NIO 是一种同步非阻塞的 I/O 模型,在 Java 1.4 中引入了 NIO ),使用 Netty 可以极大地简化 TCP 和 UDP 套接字服务器等网络编程,并且性能以及安全性等很多方面都非常优秀。
- 我们平常经常接触的 Dubbo、RocketMQ、Elasticsearch、gRPC、Spark 等等热门开源项目都用到了 Netty。
- 大部分微服务框架底层涉及到网络通信的部分都是基于 Netty 来做的,比如说 Spring Cloud 生态系统中的网关 Spring Cloud Gateway。
简单总结一下和 Netty 相关问题。
BIO,NIO 和 AIO 有啥区别?
👨💻面试官 :先来简单介绍一下 BIO,NIO 和 AIO 3 者的区别吧!
🙋 我 :好的!
- BIO (Blocking I/O): 同步阻塞 I/O 模式,数据的读取写入必须阻塞在一个线程内等待其完成。在客户端连接数量不高的情况下,是没问题的。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
- NIO (Non-blocking/New I/O): NIO 是一种同步非阻塞的 I/O 模型,于 Java 1.4 中引入,对应 java.nio包,提供了 Channel , Selector,Buffer 等抽象。NIO 中的 N 可以理解为 Non-blocking,不单纯是 New。它支持面向缓冲的,基于通道的 I/O 操作方法。 NIO 提供了与传统 BIO 模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发
- AIO (Asynchronous I/O): AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的 IO 模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步 IO 的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO 操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。
关于 IO 模型更详细的介绍,你可以看这篇文章:《常见的 IO 模型有哪些?Java 中的 BIO、NIO、AIO 有啥区别?》 这篇文章。
Netty 是什么?
👨💻面试官 :那你再来介绍一下自己对 Netty 的认识吧!小伙子。
🙋 我 :好的!那我就简单用 3 点来概括一下 Netty 吧!
- Netty 是一个 基于 NIO 的 client-server(客户端服务器)框架,使用它可以快速简单地开发网络应用程序。
- 它极大地简化并优化了 TCP 和 UDP 套接字服务器等网络编程,并且性能以及安全性等很多方面甚至都要更好。
- 支持多种协议 如 FTP,SMTP,HTTP 以及各种二进制和基于文本的传统协议。
用官方的总结就是:Netty 成功地找到了一种在不妥协可维护性和性能的情况下实现易于开发,性能,稳定性和灵活性的方法。
网络编程我愿意称中 Netty 为王 。
为啥不直接用 NIO 呢?
👨💻面试官 :你上面也说了 Netty 基于 NIO,那为啥不直接用 NIO 呢?。
不用 NIO 主要是因为 NIO 的编程模型复杂而且存在一些 BUG,并且对编程功底要求比较高。下图就是一个典型的使用 NIO 进行编程的案例:
而且,NIO 在面对断连重连、包丢失、粘包等问题时处理过程非常复杂。Netty 的出现正是为了解决这些问题,更多关于 Netty 的特点可以看下面的内容。
为什么要用 Netty?
👨💻面试官 :为什么要用 Netty 呢?能不能说一下自己的看法。
🙋 我 :因为 Netty 具有下面这些优点,并且相比于直接使用 JDK 自带的 NIO 相关的 API 来说更加易用。
- 统一的 API,支持多种传输类型,阻塞和非阻塞的。
- 简单而强大的线程模型。
- 自带编解码器解决 TCP 粘包/拆包问题。
- 自带各种协议栈。
- 真正的无连接数据包套接字支持。
- 比直接使用 Java 核心 API 有更高的吞吐量、更低的延迟、更低的资源消耗和更少的内存复制。
- 安全性不错,有完整的 SSL/TLS 以及 StartTLS 支持。
- 社区活跃
- 成熟稳定,经历了大型项目的使用和考验,而且很多开源项目都使用到了 Netty, 比如我们经常接触的 Dubbo、RocketMQ 等等。
- ……
Netty 应用场景了解么?
👨💻面试官 :能不能通俗地说一下使用 Netty 可以做什么事情?
🙋 我 :凭借自己的了解,简单说一下吧!理论上来说,NIO 可以做的事情 ,使用 Netty 都可以做并且更好。Netty 主要用来做网络通信 :
- 作为 RPC 框架的网络通信工具 : 我们在分布式系统中,不同服务节点之间经常需要相互调用,这个时候就需要 RPC 框架了。不同服务节点的通信是如何做的呢?可以使用 Netty 来做。比如我调用另外一个节点的方法的话,至少是要让对方知道我调用的是哪个类中的哪个方法以及相关参数吧!
- 实现一个自己的 HTTP 服务器 :通过 Netty 我们可以自己实现一个简单的 HTTP 服务器,这个大家应该不陌生。说到 HTTP 服务器的话,作为 Java 后端开发,我们一般使用 Tomcat 比较多。一个最基本的 HTTP 服务器可要以处理常见的 HTTP Method 的请求,比如 POST 请求、GET 请求等等。
- 实现一个即时通讯系统 : 使用 Netty 我们可以实现一个可以聊天类似微信的即时通讯系统,这方面的开源项目还蛮多的,可以自行去 Github 找一找。
- 实现消息推送系统 :市面上有很多消息推送系统都是基于 Netty 来做的。
- ……
那些开源项目用到了 Netty?
我们平常经常接触的 Dubbo、RocketMQ、Elasticsearch、gRPC 等等都用到了 Netty。
可以说大量的开源项目都用到了 Netty,所以掌握 Netty 有助于你更好的使用这些开源项目并且让你有能力对其进行二次开发。
实际上还有很多很多优秀的项目用到了 Netty,Netty 官方也做了统计,统计结果在这里:https://netty.io/wiki/related-projects.html 。
介绍一下 Netty 的核心组件?
👨💻面试官 :Netty 核心组件有哪些?分别有什么作用?
🙋 我 :表面上,嘴上开始说起 Netty 的核心组件有哪些,实则,内心已经开始 mmp 了,深度怀疑这面试官是存心搞我啊!
简单介绍 Netty 最核心的一些组件(对于每一个组件这里不详细介绍)。通过下面这张图你可以将我提到的这些 Netty 核心组件串联起来。
Bytebuf(字节容器)
网络通信最终都是通过字节流进行传输的。 ByteBuf 就是 Netty 提供的一个字节容器,其内部是一个字节数组。 当我们通过 Netty 传输数据的时候,就是通过 ByteBuf 进行的。
我们可以将 ByteBuf 看作是 Netty 对 Java NIO 提供了 ByteBuffer 字节容器的封装和抽象。
有很多小伙伴可能就要问了 : 为什么不直接使用 Java NIO 提供的 ByteBuffer 呢?
因为 ByteBuffer 这个类使用起来过于复杂和繁琐。
Bootstrap 和 ServerBootstrap(启动引导类)
Bootstrap
是客户端的启动引导类/辅助类,具体使用方法如下:
1 | EventLoopGroup group = new NioEventLoopGroup(); |
ServerBootstrap
是服务端的启动引导类/辅助类,具体使用方法如下:
1 | // 1.bossGroup 用于接收连接,workerGroup 用于具体的处理 |
从上面的示例中,我们可以看出:
- Bootstrap 通常使用 connect() 方法连接到远程的主机和端口,作为一个 Netty TCP 协议通信中的客户端。另外,Bootstrap 也可以通过 bind() 方法绑定本地的一个端口,作为 UDP 协议通信中的一端。
- ServerBootstrap通常使用 bind() 方法绑定本地的端口上,然后等待客户端的连接。
- Bootstrap 只需要配置一个线程组— EventLoopGroup ,而 ServerBootstrap需要配置两个线程组— EventLoopGroup ,一个用于接收连接,一个用于具体的 IO 处理。
Channel(网络操作抽象类)
Channel
接口是 Netty 对网络操作抽象类。通过 Channel
我们可以进行 I/O 操作。
一旦客户端成功连接服务端,就会新建一个 Channel 同该用户端进行绑定,示例代码如下:
1 | // 通过 Bootstrap 的 connect 方法连接到服务端 |
比较常用的Channel
接口实现类是 :
- NioServerSocketChannel(服务端)
- NioSocketChannel(客户端)
这两个Channel
可以和 BIO 编程模型中的ServerSocket
以及Socket
两个概念对应上。
EventLoop(事件循环)
EventLoop 介绍
这么说吧!EventLoop(事件循环)接口可以说是 Netty 中最核心的概念了!
《Netty 实战》这本书是这样介绍它的:
EventLoop 定义了 Netty 的核心抽象,用于处理连接的生命周期中所发生的事件。
是不是很难理解?说实话,我学习 Netty 的时候看到这句话是没太能理解的。
说白了,EventLoop 的主要作用实际就是责监听网络事件并调用事件处理器进行相关 I/O 操作(读写)的处理。
Channel 和 EventLoop 的关系
那 Channel 和 EventLoop 直接有啥联系呢?
Channel 为 Netty 网络操作(读写等操作)抽象类,EventLoop 负责处理注册到其上的Channel 的 I/O 操作,两者配合进行 I/O 操作。
EventloopGroup 和 EventLoop 的关系
EventLoopGroup 包含多个 EventLoop(每一个 EventLoop 通常内部包含一个线程),它管理着所有的 EventLoop 的生命周期。
并且,EventLoop 处理的 I/O 事件都将在它专有的 Thread 上被处理,即 Thread 和 EventLoop 属于 1 : 1 的关系,从而保证线程安全。
下图是 Netty NIO 模型对应的EventLoop
模型。通过这个图应该可以将EventloopGroup、EventLoop、 Channel
三者联系起来。
ChannelHandler(消息处理器) 和 ChannelPipeline(ChannelHandler 对象链表)
下面这段代码使用过 Netty 的小伙伴应该不会陌生,我们指定了序列化编解码器以及自定义的 ChannelHandler 处理消息。
1 | b.group(eventLoopGroup) |
ChannelHandler
是消息的具体处理器,主要负责处理客户端/服务端接收和发送的数据。
当 Channel 被创建时,它会被自动地分配到它专属的 ChannelPipeline
。 一个Channel
包含一个 ChannelPipeline
。 ChannelPipeline
为 ChannelHandler
的链,一个 pipeline
上可以有多个ChannelHandler
。
我们可以在 ChannelPipeline
上通过 addLast()
方法添加一个或者多个ChannelHandler
(一个数据或者事件可能会被多个 Handler 处理) 。当一个 ChannelHandler
处理完之后就将数据交给下一个 ChannelHandler
。
当 ChannelHandler
被添加到的ChannelPipeline
它得到一个 ChannelHandlerContext
,它代表一个 ChannelHandler
和 ChannelPipeline
之间的“绑定”。 ChannelPipeline
通过 ChannelHandlerContext
来间接管理 ChannelHandler
。
ChannelFuture(操作执行结果)
1 | public interface ChannelFuture extends Future<Void> { |
Netty 中所有的 I/O 操作都为异步的,我们不能立刻得到操作是否执行成功。
不过,你可以通过 ChannelFuture
接口的 addListener()
方法注册一个 ChannelFutureListener
,当操作执行成功或者失败时,监听就会自动触发返回结果。
1 | ChannelFuture f = b.connect(host, port).addListener(future -> { |
并且,你还可以通过ChannelFuture 的 channel() 方法获取连接相关联的Channel 。
1 | Channel channel = f.channel(); |
另外,我们还可以通过 ChannelFuture 接口的 sync()方法让异步的操作编程同步的。
1 | //bind()是异步的,但是,你可以通过 sync()方法将其变为同步。 |
NioEventLoopGroup 默认的构造函数会起多少线程?
👨💻面试官 :看过 Netty 的源码了么?NioEventLoopGroup 默认的构造函数会起多少线程呢?
🙋 我 :嗯嗯!看过部分。
回顾我们在上面写的服务器端的代码:
1 | // 1.bossGroup 用于接收连接,workerGroup 用于具体的处理 |
为了搞清楚NioEventLoopGroup
默认的构造函数 到底创建了多少个线程,我们来看一下它的源码。
1 | /** |
一直向下走下去的话,你会发现在 MultithreadEventLoopGroup
类中有相关的指定线程数的代码,如下:
1 | // 从1,系统属性,CPU核心数*2 这三个值中取出一个最大的 |
综上,我们发现 NioEventLoopGroup
默认的构造函数实际会起的线程数为 CPU核心数*2。
另外,如果你继续深入下去看构造函数的话,你会发现每个NioEventLoopGroup
对象内部都会分配一组NioEventLoop
,其大小是nThreads
, 这样就构成了一个线程池, 一个NIOEventLoop
和一个线程相对应,这和我们上面说的EventloopGroup
和 EventLoop
关系这部分内容相对应。
Reactor 线程模型
👨💻面试官 :大部分网络框架都是基于 Reactor 模式设计开发的。你先聊聊 Reactor 线程模型吧!
🙋 我 :好的呀!
Reactor 是一种经典的线程模型,Reactor 模式基于事件驱动,特别适合处理海量的 I/O 事件。
Reactor 线程模型分为单线程模型、多线程模型以及主从多线程模型。
以下图片来源于网络,原出处不明,如有侵权请联系我。
单线程 Reactor
所有的 IO 操作都由同一个 NIO 线程处理。
单线程 Reactor 的优点是对系统资源消耗特别小,但是,没办法支撑大量请求的应用场景并且处理请求的时间可能非常慢,毕竟只有一个线程在工作嘛!所以,一般实际项目中不会使用单线程 Reactor 。
为了解决这些问题,演进出了 Reactor 多线程模型。
多线程 Reactor
一个线程负责接受请求,一组 NIO 线程处理 IO 操作大部分场景下多线程 Reactor 模型是没有问题的,但是在一些并发连接数比较多(如百万并发)的场景下,一个线程负责接受客户端请求就存在性能问题了。
为了解决这些问题,演进出了主从多线程 Reactor 模型。
主从多线程 Reactor
一组 NIO 线程负责接受请求,一组 NIO 线程处理 IO 操作。
Netty 线程模型了解么?
👨💻面试官 :说一下 Netty 线程模型吧!
🙋 我 :大部分网络框架都是基于 Reactor 模式设计开发的。
Reactor 模式基于事件驱动,采用多路复用将事件分发给相应的 Handler 处理,非常适合处理海量 IO 的场景。
在 Netty 主要靠 NioEventLoopGroup
线程池来实现具体的线程模型的 。
我们实现服务端的时候,一般会初始化两个线程组:
- bossGroup :接收连接。
- workerGroup :负责具体的处理,交由对应的 Handler 处理。
下面我们来详细看一下 Netty 中的线程模型吧!
单线程模型
一个线程需要执行处理所有的 accept、read、decode、process、encode、send
事件。对于高负载、高并发,并且对性能要求比较高的场景不适用。
对应到 Netty 代码是下面这样的
使用 NioEventLoopGroup 类的无参构造函数设置线程数量的默认值就是 CPU 核心数 *2 。
1 | //1.eventGroup既用于处理客户端连接,又负责具体的处理。 |
多线程模型
一个 Acceptor 线程只负责监听客户端的连接,一个 NIO 线程池负责具体处理: accept、read、decode、process、encode、send 事件。满足绝大部分应用场景,并发连接量不大的时候没啥问题,但是遇到并发连接大的时候就可能会出现问题,成为性能瓶颈。
对应到 Netty 代码是下面这样的:
1 | // 1.bossGroup 用于接收连接,workerGroup 用于具体的处理 |
主从多线程模型
从一个 主线程 NIO 线程池中选择一个线程作为 Acceptor 线程,绑定监听端口,接收客户端连接的连接,其他线程负责后续的接入认证等工作。连接建立完成后,Sub NIO 线程池负责具体处理 I/O 读写。如果多线程模型无法满足你的需求的时候,可以考虑使用主从多线程模型 。
1 | // 1.bossGroup 用于接收连接,workerGroup 用于具体的处理 |
Netty 服务端和客户端的启动过程了解么?
服务端
1 | // 1.bossGroup 用于接收连接,workerGroup 用于具体的处理 |
简单解析一下服务端的创建过程具体是怎样的:
1.首先你创建了两个 NioEventLoopGroup 对象实例:bossGroup 和 workerGroup。
- bossGroup : 用于处理客户端的 TCP 连接请求。
- workerGroup : 负责每一条连接的具体读写数据的处理逻辑,真正负责 I/O 读写操作,交由对应的 Handler 处理。
举个例子:我们把公司的老板当做 bossGroup,员工当做 workerGroup,bossGroup 在外面接完活之后,扔给 workerGroup 去处理。一般情况下我们会指定 bossGroup 的 线程数为 1(并发连接量不大的时候) ,workGroup 的线程数量为 CPU 核心数 *2 。另外,根据源码来看,使用 NioEventLoopGroup 类的无参构造函数设置线程数量的默认值就是 CPU 核心数 *2 。
2.接下来 我们创建了一个服务端启动引导/辅助类: ServerBootstrap,这个类将引导我们进行服务端的启动工作。
3.通过 .group() 方法给引导类 ServerBootstrap 配置两大线程组,确定了线程模型。
通过下面的代码,我们实际配置的是多线程模型,这个在上面提到过。
1 | EventLoopGroup bossGroup = new NioEventLoopGroup(1); |
4.通过channel()方法给引导类 ServerBootstrap指定了 IO 模型为NIO
- NioServerSocketChannel :指定服务端的 IO 模型为 NIO,与 BIO 编程模型中的ServerSocket对应
- NioSocketChannel : 指定客户端的 IO 模型为 NIO, 与 BIO 编程模型中的Socket对应
5.通过 .childHandler()给引导类创建一个ChannelInitializer ,然后指定了服务端消息的业务处理逻辑 HelloServerHandler对象
6.调用 ServerBootstrap 类的 bind()方法绑定端口
客户端
1 | //1.创建一个 NioEventLoopGroup 对象实例 |
继续分析一下客户端的创建流程:
1.创建一个 NioEventLoopGroup 对象实例
2.创建客户端启动的引导类是 Bootstrap
3.通过 .group() 方法给引导类 Bootstrap 配置一个线程组
4.通过channel()方法给引导类 Bootstrap指定了 IO 模型为NIO
5.通过 .childHandler()给引导类创建一个ChannelInitializer ,然后指定了客户端消息的业务处理逻辑 HelloClientHandler 对象
6.调用 Bootstrap 类的 connect()方法进行连接,这个方法需要指定两个参数:
- inetHost : ip 地址
- inetPort : 端口号
1 | public ChannelFuture connect(String inetHost, int inetPort) { |
connect
方法返回的是一个 Future
类型的对象
1 | public interface ChannelFuture extends Future<Void> { |
也就是说这个方是异步的,我们通过 addListener
方法可以监听到连接是否成功,进而打印出连接信息。具体做法很简单,只需要对代码进行以下改动:
1 | ChannelFuture f = b.connect(host, port).addListener(future -> { |
什么是 TCP 粘包/拆包?有什么解决办法呢?
👨💻面试官 :什么是 TCP 粘包/拆包?
🙋 我 :TCP 粘包/拆包 就是你基于 TCP 发送数据的时候,出现了多个字符串“粘”在了一起或者一个字符串被“拆”开的问题。比如你多次发送:“你好,你真帅啊!哥哥!”,但是客户端接收到的可能是下面这样的:
👨💻面试官 :那有什么解决办法呢?
🙋 我 :
1.使用 Netty 自带的解码器
- LineBasedFrameDecoder : 发送端发送数据包的时候,每个数据包之间以换行符作为分隔,LineBasedFrameDecoder 的工作原理是它依次遍历 ByteBuf 中的可读字节,判断是否有换行符,然后进行相应的截取。
- DelimiterBasedFrameDecoder : 可以自定义分隔符解码器,LineBasedFrameDecoder 实际上是一种特殊的 DelimiterBasedFrameDecoder 解码器。
- FixedLengthFrameDecoder: 固定长度解码器,它能够按照指定的长度对消息进行相应的拆包。如果不够指定的长度,则空格补全
- LengthFieldBasedFrameDecoder:基于长度字段的解码器,发送的数据中有数据长度相关的信息。
2.自定义序列化编解码器
在 Java 中自带的有实现 Serializable 接口来实现序列化,但由于它性能、安全性等原因一般情况下是不会被使用到的。
通常情况下,我们使用 Protostuff、Hessian2、json 序列方式比较多,另外还有一些序列化性能非常好的序列化方式也是很好的选择:
- 专门针对 Java 语言的:Kryo,FST 等等
- 跨语言的:Protostuff(基于 protobuf 发展而来),ProtoBuf,Thrift,Avro,MsgPack 等等
由于篇幅问题,这部分内容会在后续的文章中详细分析介绍~~~
Netty 长连接、心跳机制了解么?
👨💻面试官 :TCP 长连接和短连接了解么?
🙋 我 :我们知道 TCP 在进行读写之前,server 与 client 之间必须提前建立一个连接。建立连接的过程,需要我们常说的三次握手,释放/关闭连接的话需要四次挥手。这个过程是比较消耗网络资源并且有时间延迟的。
所谓,短连接说的就是 server 端 与 client 端建立连接之后,读写完成之后就关闭掉连接,如果下一次再要互相发送消息,就要重新连接。短连接的优点很明显,就是管理和实现都比较简单,缺点也很明显,每一次的读写都要建立连接必然会带来大量网络资源的消耗,并且连接的建立也需要耗费时间。
长连接说的就是 client 向 server 双方建立连接之后,即使 client 与 server 完成一次读写,它们之间的连接并不会主动关闭,后续的读写操作会继续使用这个连接。长连接的可以省去较多的 TCP 建立和关闭的操作,降低对网络资源的依赖,节约时间。对于频繁请求资源的客户来说,非常适用长连接。
👨💻面试官 :为什么需要心跳机制?Netty 中心跳机制了解么?
🙋 我 :
在 TCP 保持长连接的过程中,可能会出现断网等网络异常出现,异常发生的时候, client 与 server 之间如果没有交互的话,它们是无法发现对方已经掉线的。为了解决这个问题, 我们就需要引入 心跳机制 。
心跳机制的工作原理是: 在 client 与 server 之间在一定时间内没有数据交互时, 即处于 idle 状态时, 客户端或服务器就会发送一个特殊的数据包给对方, 当接收方收到这个数据报文后, 也立即发送一个特殊的数据报文, 回应发送方, 此即一个 PING-PONG 交互。所以, 当某一端收到心跳消息后, 就知道了对方仍然在线, 这就确保 TCP 连接的有效性.
TCP 实际上自带的就有长连接选项,本身是也有心跳包机制,也就是 TCP 的选项:SO_KEEPALIVE。 但是,TCP 协议层面的长连接灵活性不够。所以,一般情况下我们都是在应用层协议上实现自定义心跳机制的,也就是在 Netty 层面通过编码实现。通过 Netty 实现心跳机制的话,核心类是 IdleStateHandler 。
Netty 的零拷贝了解么?
👨💻面试官 :讲讲 Netty 的零拷贝?
🙋 我 :
维基百科是这样介绍零拷贝的:
零复制(英语:Zero-copy;也译零拷贝)技术是指计算机执行操作时,CPU 不需要先将数据从某处内存复制到另一个特定区域。这种技术通常用于通过网络传输文件时节省 CPU 周期和内存带宽。
在 OS 层面上的 Zero-copy 通常指避免在 用户态(User-space) 与 内核态(Kernel-space) 之间来回拷贝数据。而在 Netty 层面 ,零拷贝主要体现在对于数据操作的优化。
Netty 中的零拷贝体现在以下几个方面
- 使用 Netty 提供的 CompositeByteBuf 类, 可以将多个ByteBuf 合并为一个逻辑上的 ByteBuf, 避免了各个 ByteBuf 之间的拷贝。
- ByteBuf 支持 slice 操作, 因此可以将 ByteBuf 分解为多个共享同一个存储区域的 ByteBuf, 避免了内存的拷贝。
- 通过 FileRegion 包装的FileChannel.tranferTo 实现文件传输, 可以直接将文件缓冲区的数据发送到目标 Channel, 避免了传统通过循环 write 方式导致的内存拷贝问题.
参考
- netty 学习系列二:NIO Reactor 模型 & Netty 线程模型:https://www.jianshu.com/p/38b56531565d
- 《Netty 实战》
- Netty 面试题整理(2):https://metatronxl.github.io/2019/10/22/Netty-面试题整理-二/
- Netty(3)—源码 NioEventLoopGroup:https://www.cnblogs.com/qdhxhz/p/10075568.html
- 对于 Netty ByteBuf 的零拷贝(Zero Copy) 的理解: https://www.cnblogs.com/xys1228/p/6088805.html
分布式&微服务
服务治理:为什么需要服务注册与发现?
服务注册与发现是分布式以及微服务系统的基石,搞懂它的作用和基本原理对于我们来说非常重要!
为什么需要服务注册与发现?
微服务架构下,一个系统通常由多个微服务组成(比如电商系统可能分为用户服务、商品服务、订单服务等服务),一个用户请求可能会需要多个服务参与,这些服务之间互相配合以维持系统的正常运行。
在没有服务注册与发现机制之前,每个服务会将其依赖的其他服务的地址信息写死在配置文件里(参考单体架构)。假设我们系统中的订单服务访问量突然变大,我们需要对订单服务进行扩容,也就是多部署一些订单服务来分担处理请求的压力。这个时候,我们需要手动更新所有依赖订单服务的服务节点的地址配置信息。同理,假设某个订单服务节点突然宕机,我们又要手动更新对应的服务节点信息。更新完成之后,还要手动重启这些服务,整个过程非常麻烦且容易出错。
有了服务注册与发现机制之后,就不需要这么麻烦了,由注册中心负责维护可用服务的列表,通过注册中心动态获取可用服务的地址信息。如果服务信息发生变更,注册中心会将变更推送给相关联的服务,更新服务地址信息,无需手动更新,也不需要重启服务,这些对开发者来说完全是无感的。
服务注册与发现可以帮助我们实现服务的优雅上下线,从而实现服务的弹性扩缩容。
除此之外,服务注册与发现机制还有一个非常重要的功能:不可用服务剔除 。简单来说,注册中心会通过 心跳机制 来检测服务是否可用,如果服务不可用的话,注册中心会主动剔除该服务并将变更推送给相关联的服务,更新服务地址信息。
最后,我们再来总结补充一下,一个完备的服务注册与发现应该具备的功能:
- 服务注册以及服务查询(最基本的)
- 服务状态变更通知、服务健康检查、不可用服务剔除
- 服务权重配置(权重越高被访问的频率越高)
服务注册与发现的基本流程是怎样的?
这个问题等价于问服务注册与发现的原理。
每个服务节点在启动运行的时候,会向注册中心注册服务,也就是将自己的地址信息(ip、端口以及服务名字等信息的组合)上报给注册中心,注册中心负责将地址信息保存起来,这就是 服务注册。
一个服务节点如果要调用另外一个服务节点,会直接拿着服务的信息找注册中心要对方的地址信息,这就是 服务发现 。通常情况下,服务节点拿到地址信息之后,还会在本地缓存一份,保证在注册中心宕机时仍然可以正常调用服务。
如果服务信息发生变更,注册中心会将变更推送给相关联的服务,更新服务地址信息。
为了保证服务地址列表中都是可用服务的地址信息,注册中心通常会通过 心跳机制 来检测服务是否可用,如果服务不可用的话,注册中心会主动剔除该服务并将变更推送给相关联的服务,更新服务地址信息。
最后,再来一张图简单总结一下服务注册与发现(一个服务既可能是服务提供者也可能是服务消费者)。
常见的注册中心有哪些?
我这里跟多的是从面试角度来说,各类注册中心的详细对比,可以看这篇文章:5 种注册中心如何选型?从原理给你解读! - 楼仔 - 2022 ,非常详细。
比较常用的注册中心有 ZooKeeper、Eureka、Nacos,这三个都是使用 Java 语言开发,相对来说,更适合 Java 技术栈一些。其他的还有像 ETCD、Consul,这里就不做介绍了。
首先,咱们来看 ZooKeeper,大部分同学应该对它不陌生。严格意义上来说,ZooKeeper 设计之初并不是未来做注册中心的,只是前几年国内使用 Dubbo 的场景下比较喜欢使用它来做注册中心。
对于 CAP 理论来说,ZooKeeper 保证的是 CP。 任何时刻对 ZooKeeper 的读请求都能得到一致性的结果,但是, ZooKeeper 不保证每次请求的可用性比如在 Leader 选举过程中或者半数以上的机器不可用的时候服务就是不可用的。
针对注册中心这个场景来说,重要的是可用性,AP 会更合适一些。 ZooKeeper 更适合做分布式协调服,注册中心就交给专业的来做吧!
其次,我们再来看看 Eureka,一款非常值得研究的注册中心。Eureka 是 Netflix 公司开源的一个注册中心,配套的还有 Feign、Ribbon、Zuul、Hystrix 等知名的微服务系统构建所必须的组件。
对于 CAP 理论来说,Eureka 保证的是 AP。 Eureka 集群只要有一台 Eureka 正常服务,整个注册中心就是可用的,只是查询到的数据可能是过期的(集群中的各个节点异步方式同步数据,不保证强一致性)。
不过,可惜的是,Spring Cloud 2020.0.0 版本移除了 Netflix 除 Eureka 外的所有组件。
那为什么 Spring Cloud 这么急着移除 Netflix 的组件呢? 主要是因为在 2018 年的时候,Netflix 宣布其开源的核心组件 Hystrix、Ribbon、Zuul、Eureka 等进入维护状态,不再进行新特性开发,只修 BUG。于是,Spring 官方不得不考虑移除 Netflix 的组件。
我这里也不推荐使用 Eureka 作为注册中心,阿里开源的 Nacos 或许是更好的选择。
最后,我们再来看看 Nacos,一款即可以用来做注册中心,又可以用来做配置中心的优秀项目。
Nacos 属实是后起之秀,借鉴吸收了其他注册中心的有点,与 Spring Boot 、Dubbo、Spring Cloud、Kubernetes 无缝对接,兼容性很好。并且,Nacos 不仅支持 CP 也支持 AP。
Nacos 性能强悍(比 Eureka 能支持更多的服务实例),易用性较强(文档丰富、数据模型简单且自带后台管理界面),支持 99.9% 高可用。
对于 Java 技术栈来说,个人是比较推荐使用 Nacos 来做注册中心。
服务治理:分布式下如何进行配置管理?
为什么要用配置中心?
微服务下,业务的发展一般会导致服务数量的增加,进而导致程序配置(服务地址、数据库参数等等)增多。传统的配置文件的方式已经无法满足当前需求,主要有下面几点原因:
- 安全性得不到保障:配置放在代码库中容易泄露。
- 时效性不行:修改配置需要重启服务才能生效。
- 不支持权限控制 :没有对配置的修改、发布等操作进行严格的权限控制。
- 不支持配置集中管理 : 配置文件过于分散,不方便管理。
- ……
另外,配置中心通常会自带版本跟踪,会记录配置的修改记录,记录的内容包括修改人、修改时间、修改内容等等。
虽然通过 Git 版本管理我们也能追溯配置的修改记录,但是配置中心提供的配置版本管理功能更全面。并且,配置中心通常会在配置版本管理的基础上支持配置一键回滚。
一些功能更全面的配置中心比如Apollo
甚至还支持灰度发布。
常见的配置中心有哪些?
Spring Cloud Config、Nacos 、Apollo、K8s ConfigMap 、Disconf 、Qconf 都可以用来做配置中心。
Disconf 和 Qconf 已经没有维护,生态也并不活跃,并不建议使用,在做配置中心技术选型的时候可以跳过。
如果你的技术选型是 Kubernetes 的话,可以考虑使用 K8s ConfigMap 来作为配置中心。
Apollo 和 Nacos 我个人更喜欢,两者都是国内公司开源的知名项目,项目社区都比较活跃且都还在维护中。Nacos 是阿里开源的,Apollo 是携程开源的。Nacos 使用起来比较简单,并且还可以直接用来做服务发现及管理。Apollo 只能用来做配置管理,使用相对复杂一些。
如果你的项目仅仅需要配置中心的话,建议使用 Apollo 。如果你的项目需要配置中心的同时还需要服务发现及管理的话,那就更建议使用 Nacos。
Spring Cloud Config 属于 Spring Cloud 生态组件,可以和 Spring Cloud 体系无缝整合。由于基于 Git 存储配置,因此 Spring Cloud Config 的整体设计很简单。
Apollo vs Nacos vs Spring Cloud Config
功能点 | Apollo | Nacos | Spring Cloud Config |
---|---|---|---|
配置界面 | 支持 | 支持 | 无(需要通过 Git 操作) |
配置实时生效 | 支持(HTTP 长轮询 1s 内) | 支持(HTTP 长轮询 1s 内) | 重启生效,或手动 refresh 生效 |
版本管理 | 支持 | 支持 | 支持(依赖 Git) |
权限管理 | 支持 | 支持 | 支持(依赖 Git) |
灰度发布 | 支持 | 支持(Nacos 1.1.0 版本开始支持灰度配置) | 不支持 |
配置回滚 | 支持 | 支持 | 支持(依赖 Git) |
告警通知 | 支持 | 支持 | 不支持 |
多语言 | 主流语言,Open API | 主流语言,Open API | 只支持 Spring 应用 |
多环境 | 支持 | 支持 | 不支持 |
监听查询 | 支持 | 支持 | 支持 |
Apollo 和 Nacos 提供了更多开箱即用的功能,更适合用来作为配置中心。
Nacos 使用起来比较简单,并且还可以直接用来做服务发现及管理。Apollo 只能用来做配置管理,使用相对复杂一些。
Apollo 在配置管理方面做的更加全面,就比如说虽然 Nacos 在 1.1.0 版本开始支持灰度配置,但 Nacos 的灰度配置功能实现的比较简单,Apollo 实现的灰度配置功能就相对更完善一些。不过,Nacos 提供的配置中心功能已经可以满足绝大部分项目的需求了。
一个完备配置中心需要具备哪些功能?
如果我们需要自己设计一个配置中心的话,需要考虑哪些东西呢?
简单说说我的看法:
- 权限控制 :配置的修改、发布等操作需要严格的权限控制。
- 日志记录 : 配置的修改、发布等操需要记录完整的日志,便于后期排查问题。
- 配置推送 : 推送模式通常由两种:
- 推 :实时性变更,配置更新后推送给应用。需要应用和配置中心保持长连接,复杂度高。
- 拉 :实时性较差,应用隔一段时间手动拉取配置。
- 推拉结合
- 灰度发布 :支持配置只推给部分应用。
- 易操作 : 提供 Web 界面方便配置修改和发布。
- 版本跟踪 :所有的配置发布都有版本概念,从而可以方便的支持配置的回滚。
- 支持配置回滚 : 我们一键回滚配置到指定的位置,这个需要和版本跟踪结合使用。
- ……
以 Apollo 为例介绍配置中心的设计
Apollo 介绍
根据 Apollo 官方介绍:
Apollo(阿波罗)是携程框架部门研发的分布式配置中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性,适用于微服务配置管理场景。
服务端基于 Spring Boot 和 Spring Cloud 开发,打包后可以直接运行,不需要额外安装 Tomcat 等应用容器。
Java 客户端不依赖任何框架,能够运行于所有 Java 运行时环境,同时对 Spring/Spring Boot 环境也有较好的支持。
Apollo 特性:
- 配置修改实时生效(热发布) (1s 即可接收到最新配置)
- 灰度发布 (配置只推给部分应用)
- 部署简单 (只依赖 MySQL)
- 跨语言 (提供了 HTTP 接口,不限制编程语言)
- ……
关于如何使用 Apollo 可以查看 Apollo 官方使用指南。
相关阅读:
Apollo 架构解析
官方给出的 Apollo 基础模型非常简单:
用户通过 Apollo 配置中心修改/发布配置,
Apollo 配置中心通知应用配置已经更改
应用访问 Apollo 配置中心获取最新的配置
官方给出的架构图如下:
- Client 端(客户端,用于应用获取配置)流程 :Client 通过域名走 slb(软件负载均衡)访问 Meta Server,Meta Server 访问 Eureka 服务注册中心获取 Config Service 服务列表(IP+Port)。有了 IP+Port,我们就能访问 Config Service 暴露的服务比如通过 GET 请求获取配置的接口(
/configs/{appId}/{clusterName}/{namespace:.+}
)即可获取配置。 - Portal 端(UI 界面,用于可视化配置管理)流程 :Portal 端通过域名走 slb(软件负载均衡)访问 Meta Server,Meta Server 访问 Eureka 服务注册中心获取 Admin Service 服务列表(IP+Port)。有了 IP+Port,我们就能访问 Admin Service 暴露的服务比如通过 POST 请求访问发布配置的接口(
/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/releases
)即可发布配置。
另外,杨波老师的微服务架构~携程 Apollo 配置中心架构剖析这篇文章对 Apollo 的架构做了简化,值得一看。
我会从上到下依次介绍架构图中涉及到的所有角色的作用。
Client
Apollo 官方提供的客户端,目前有 Java 和.Net 版本。非 Java 和.Net 应用可以通过调用 HTTP 接口来使用 Apollo。
Client 的作用主要就是提供一些开箱即用的方法方便应用获取以及实时更新配置。
比如你通过下面的几行代码就能获取到 someKey 对应的实时最新的配置值:
1 | Config config = ConfigService.getAppConfig(); |
再比如你通过下面的代码就能监听配置变化:
1 | Config config = ConfigService.getAppConfig(); |
Portal
Portal 实际就是一个帮助我们修改和发布配置的 UI 界面。
(Software) Load Balancer
为了实现 MetaServer 的高可用,MetaServer 通常以集群的形式部署。
Client/Portal 直接访问 (Software) Load Balancer ,然后,再由其进行负载均衡和流量转发。
Meta Server
为了实现跨语言使用,通常的做法就是暴露 HTTP 接口。为此,Apollo 引入了 MetaServer。
Meta Server 其实就是 Eureka 的 Proxy,作用就是将 Eureka 的服务发现接口以 HTTP 接口的形式暴露出来。 这样的话,我们通过 HTTP 请求就可以访问到 Config Service 和 AdminService。
通常情况下,我们都是建议基于 Meta Server 机制来实现 Config Service 的服务发现,这样可以实现 Config Service 的高可用。不过, 你也可以选择跳过 MetaServer,直接指定 Config Service 地址(apollo-client 0.11.0 及以上版本)。
Config Service
主要用于 Client 对配置的获取以及实时更新。
Admin Service
主要用于 Portal 对配置的更新。
参考
- Nacos 1.2.0 权限控制介绍和使用:https://nacos.io/zh-cn/blog/nacos 1.2.0 guide.html
- Nacos 1.1.0 发布,支持灰度配置和地址服务器模式:https://nacos.io/zh-cn/blog/nacos 1.1.0.html
- Apollo 常见问题解答:https://www.apolloconfig.com/#/zh/faq/faq
- 微服务配置中心选型比较:https://www.itshangxp.com/spring-cloud/spring-cloud-config-center/
服务治理:分布式事务解决方案有哪些?
网上已经有很多关于分布式事务的文章了,为啥还要写一篇?
第一是我觉得大部分文章理解起来挺难的,不太适合一些经验不多的小伙伴。这篇文章我的目标就是让即使是没啥工作经验的小伙伴们都能真正看懂分布式事务。
第二是我觉得大部分文章介绍的不够详细,很对分布式事务相关比较重要的概念都没有提到。
开始聊分布式事务之前,我们先来回顾一下事务相关的概念。
事务
我们设想一个场景,这个场景中我们需要插入多条相关联的数据到数据库,不幸的是,这个过程可能会遇到下面这些问题:
- 数据库中途突然因为某些原因挂掉了。
- 客户端突然因为网络原因连接不上数据库了。
- 并发访问数据库时,多个线程同时写入数据库,覆盖了彼此的更改。
- ……
上面的任何一个问题都可能会导致数据的不一致性。为了保证数据的一致性,系统必须能够处理这些问题。事务就是我们抽象出来简化这些问题的首选机制。事务的概念起源于数据库,目前,已经成为一个比较广泛的概念。
何为事务? 一言蔽之,事务是逻辑上的一组操作,要么都执行,要么都不执行。
事务最经典也经常被拿出来说例子就是转账了。假如小明要给小红转账 1000 元,这个转账会涉及到两个关键操作,这两个操作必须都成功或者都失败。
- 将小明的余额减少 1000 元
- 将小红的余额增加 1000 元。
事务会把这两个操作就可以看成逻辑上的一个整体,这个整体包含的操作要么都成功,要么都要失败。这样就不会出现小明余额减少而小红的余额却并没有增加的情况。
数据库事务
大多数情况下,我们在谈论事务的时候,如果没有特指分布式事务,往往指的就是数据库事务。
数据库事务在我们日常开发中接触的最多了。如果你的项目属于单体架构的话,你接触到的往往就是数据库事务了。
那数据库事务有什么作用呢?
简单来说,数据库事务可以保证多个对数据库的操作(也就是 SQL 语句)构成一个逻辑上的整体。构成这个逻辑上的整体的这些数据库操作遵循:要么全部执行成功,要么全部不执行 。
1 | # 开启一个事务 |
另外,关系型数据库(例如:MySQL
、SQL Server
、Oracle
等)事务都有 ACID 特性:
- 原子性(Atomicity) : 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
- 一致性(Consistency): 执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的;
- 隔离性(Isolation): 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
- 持久性(Durabilily): 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
🌈 这里要额外补充一点:只有保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障。也就是说 A、I、D 是手段,C 是目的! 想必大家也和我一样,被 ACID 这个概念被误导了很久! 我也是看周志明老师的公开课《周志明的软件架构课》才搞清楚的(多看好书!!!)。
另外,DDIA 也就是 《Designing Data-Intensive Application(数据密集型应用系统设计)》 的作者在他的这本书中如是说:
Atomicity, isolation, and durability are properties of the database, whereas consis‐ tency (in the ACID sense) is a property of the application. The application may rely on the database’s atomicity and isolation properties in order to achieve consistency, but it’s not up to the database alone.
翻译过来的意思是:原子性,隔离性和持久性是数据库的属性,而一致性(在 ACID 意义上)是应用程序的属性。应用可能依赖数据库的原子性和隔离属性来实现一致性,但这并不仅取决于数据库。因此,字母 C 不属于 ACID 。
《Designing Data-Intensive Application(数据密集型应用系统设计)》这本书强推一波,值得读很多遍!豆瓣有接近 90% 的人看了这本书之后给了五星好评。另外,中文翻译版本已经在 Github 开源,地址:https://github.com/Vonng/ddia 。
数据事务的实现原理呢?
我们这里以 MySQL 的 InnoDB 引擎为例来简单说一下。
MySQL InnoDB 引擎使用 redo log(重做日志) 保证事务的持久性,使用 undo log(回滚日志) 来保证事务的原子性。MySQL InnoDB 引擎通过 锁机制、MVCC 等手段来保证事务的隔离性( 默认支持的隔离级别是 REPEATABLE-READ )。
分布式事务
微服务架构下,一个系统被拆分为多个小的微服务。每个微服务都可能存在不同的机器上,并且每个微服务可能都有一个单独的数据库供自己使用。这种情况下,一组操作可能会涉及到多个微服务以及多个数据库。举个例子:电商系统中,你创建一个订单往往会涉及到订单服务(订单数加一)、库存服务(库存减一)等等服务,这些服务会有供自己单独使用的数据库。
那么如何保证这一组操作要么都执行成功,要么都执行失败呢?
这个时候单单依靠数据库事务就不行了!我们就需要引入 分布式事务 这个概念了!
实际上,只要跨数据库的场景都需要用到引入分布式事务。比如说单个数据库的性能达到瓶颈或者数据量太大的时候,我们需要进行 分库。分库之后,同一个数据库中的表分布在了不同的数据库中,如果单个操作涉及到多个数据库,那么数据库自带的事务就无法满足我们的要求了。
一言蔽之,分布式事务的终极目标就是保证系统中多个相关联的数据库中的数据的一致性!
那既然分布式事务也属于事务,理论上就应该准守事物的 ACID 四大特性。但是,考虑到性能、可用性等各方面因素,我们往往是无法完全满足 ACID 的,只能选择一个比较折中的方案。
针对分布式事务,又诞生了一些新的理论。
分布式事务基础理论
CAP 理论和 BASE 理论
CAP 理论和 BASE 理论是分布式领域非常非常重要的两个理论。不夸张地说,只要问到分布式相关的内容,面试官几乎是必定会问这两个分布式相关的理论。
不论是你面试也好,工作也罢,都非常有必要将这两个理论搞懂,并且能够用自己的理解给别人讲出来。
我这里就不多提这两个理论了,不了解的小伙伴,可以看我前段时间写过的一篇相关的文章:《CAP 和 BASE 理论了解么?可以结合实际案例说下不?》 。
一致性的 3 种级别
我们可以把对于系统一致性的要求分为下面 3 种级别:
- 强一致性 :系统写入了什么,读出来的就是什么。
- 弱一致性 :不一定可以读取到最新写入的值,也不保证多少时间之后读取到的数据是最新的,只是会尽量保证某个时刻达到数据一致的状态。
- 最终一致性 :弱一致性的升级版。系统会保证在一定时间内达到数据一致的状态,
除了上面这 3 个比较常见的一致性级别之外,还有读写一致性、因果一致性等一致性模型,具体可以参考《Operational Characterization of Weak Memory Consistency Models》这篇论文。因为日常工作中这些一致性模型很少见,我这里就不多做阐述(因为我自己也不是特别了解 😅)。
业界比较推崇是 最终一致性,但是某些对数据一致要求十分严格的场景比如银行转账还是要保证强一致性。
柔性事务
互联网应用最关键的就是要保证高可用, 计算式系统几秒钟之内没办法使用都有可能造成数百万的损失。在此场景下,一些大佬们在 CAP 理论和 BASE 理论的基础上,提出了 柔性事务 的概念。 柔性事务追求的是最终一致性。
实际上,柔性事务就是 BASE 理论 +业务实践。 柔性事务追求的目标是:我们根据自身业务特性,通过适当的方式来保证系统数据的最终一致性。 像 TCC、 Saga、MQ 事务 、本地消息表 就属于柔性事务。
刚性事务
与柔性事务相对的就是 刚性事务 了。前面我们说了,柔性事务追求的是最终一致性 。那么,与之对应,刚性事务追求的就是 强一致性。像2PC 、3PC 就属于刚性事务。
分布式事务解决方案
分布式事务的解决方案有很多,比如:2PC、3PC、TCC、本地消息表、MQ 事务(Kafka 和 RocketMQ 都提供了事务相关功能) 、Saga 等等。
2PC、3PC 属于业务代码无侵入方案,都是基于 XA 规范衍生出来的实现,XA 规范是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准。TCC、Saga 属于业务侵入方案,MQ 事务依赖于使用消息队列的场景,本地消息表不支持回滚。
这些方案的适用场景有所区别,我们需要根据具体的场景选择适合自己项目的解决方案。
开始介绍 2PC 和 3PC 之前,我们先来介绍一下 2PC 和 3PC 涉及到的一些角色(XA 规范的角色组成):
- AP(Application Program):应用程序本身。
- RM(Resource Manager) :资源管理器,也就是事务的参与者,绝大部分情况下就是指数据库(后文会以关系型数据库为例),一个分布式事务往往涉及到多个 RM。
- TM(Transaction Manager) :事务管理器,负责管理全局事务,分配事务唯一标识,监控事务的执行进度,并负责事务的提交、回滚、失败恢复等。
2PC(两阶段提交协议)
2PC(Two-Phase Commit)这三个字母的含义:
- 2 -> 指代事务提交的 2 个阶段
- P-> Prepare (准备阶段)
- C ->Commit(提交阶段)
2PC 将事务的提交过程分为 2 个阶段:准备阶段 和 提交阶段 。
准备阶段(Prepare)
准备阶段的核心是“询问”事务参与者执行本地数据库事务操作是否成功。
准备阶段的工作流程:
- 事务协调者/管理者(后文简称 TM) 向所有涉及到的 事务参与者(后文简称 RM) 发送消息询问:“你是否可以执行事务操作呢?”,并等待其答复。
- RM 接收到消息之后,开始执行本地数据库事务预操作比如写 redo log/undo log 日志,此时并不会提交事务 。
- RM 如果执行本地数据库事务操作成功,那就回复“Yes”表示我已就绪,否则就回复“No”表示我未就绪。
提交阶段(Commit)
提交阶段的核心是“询问”事务参与者提交本地事务是否成功。
当所有事务参与者都是“就绪”状态的话:
- TM 向所有参与者发送消息:“你们可以提交事务啦!”(Commit 消息)
- RM 接收到 Commit 消息 后执行 提交本地数据库事务 操作,执行完成之后 释放整个事务期间所占用的资源。
- RM 回复:“事务已经提交” (ACK 消息)。
- TM 收到所有 事务参与者 的 ACK 消息 之后,整个分布式事务过程正式结束。
当任一事务参与者是“未就绪”状态的话:
- TM 向所有参与者发送消息:“你们可以执行回滚操作了!”(Rollback 消息)。
- RM 接收到 Rollback 消息 后执行 本地数据库事务回滚 执行完成之后 释放整个事务期间所占用的资源。
- RM 回复:“事务已经回滚” (ACK 消息)。
- TM 收到所有 RM 的 ACK 消息 之后,中断事务。
总结
简单总结一下 2PC 两阶段中比较重要的一些点:
- 准备阶段 的主要目的是测试 RM 能否执行 本地数据库事务 操作(!!!注意:这一步并不会提交事务)。
- 提交阶段 中 TM 会根据 准备阶段 中 RM 的消息来决定是执行事务提交还是回滚操作。
- 提交阶段 之后一定会结束当前的分布式事务
2PC 的优点:
- 实现起来非常简单,各大主流数据库比如 MySQL、Oracle 都有自己实现。
- 针对的是数据强一致性。不过,仍然可能存在数据不一致的情况。
2PC 存在的问题:
- 同步阻塞 :事务参与者会在正式提交事务之前会一直占用相关的资源。比如用户小明转账给小红,那其他事务也要操作用户小明或小红的话,就会阻塞。
- 数据不一致 :由于网络问题或者TM宕机都有可能会造成数据不一致的情况。比如在第2阶段(提交阶段),部分网络出现问题导致部分参与者收不到 Commit/Rollback 消息的话,就会导致数据不一致。
- 单点问题 : TM在其中也是一个很重要的角色,如果TM在准备(Prepare)阶段完成之后挂掉的话,事务参与者就会一直卡在提交(Commit)阶段。
3PC(三阶段提交协议)
3PC 是人们在 2PC 的基础上做了一些优化得到的。3PC 把 2PC 中的 准备阶段(Prepare) 做了进一步细化,分为 2 个阶段:
- 准备阶段(CanCommit)
- 预提交阶段(PreCommit)
准备阶段(CanCommit)
这一步不会执行事务操作,只是向 RM 发送 准备请求 ,顺便询问一些信息比如事务参与者能否执行本地数据库事务操作。RM 回复“Yes”、“No”或者直接超时。
如果任一 RM 回复“No”或者直接超时的话,就中断事务(向所有参与者发送“Abort”消息),否则进入 预提交阶段(PreCommit) 。
预提交阶段(PreCommit)
TM 向所有涉及到的 RM 发送 预提交请求 ,RM 回复“Yes”、“No”(最后的反悔机会)或者直接超时。
如果任一 RM 回复“No”或者直接超时的话,就中断事务(向所有事务参与者发送“abort”消息),否则进入 执行事务提交阶段(DoCommit) 。
当所有 RM 都返回“Yes”之后, RM 才会执行本地数据库事务预操作比如写 redo log/undo log 日志。
执行事务提交阶段(DoCommit)
执行事务提交(DoCommit) 阶段就开始进行真正的事务提交。
TM 向所有涉及到的 RM 发送 执行事务提交请求 ,RM 收到消息后开始正式提交事务,并在完成事务提交后释放占用的资源。
如果 TM 收到所有 RM 正确提交事务的消息的话,表示事务正常完成。如果任一 RM 没有正确提交事务或者超时的话,就中断事务,TM 向所有 RM 发送“Abort”消息。RM 接收到 Abort 请求后,执行本地数据库事务回滚,后面的步骤就和 2PC 中的类似了。
总结
3PC 除了将2PC 中的准备阶段(Prepare) 做了进一步细化之外,还做了哪些改进?
3PC 还同时在事务管理者和事务参与者中引入了 超时机制 ,如果在一定时间内没有收到事务参与者的消息就默认失败,进而避免事务参与者一直阻塞占用资源。2PC 中只有事务管理者才拥有超时机制,当事务参与者长时间无法与事务协调者通讯的情况下(比如协调者挂掉了),就会导致无法释放资源阻塞的问题。
不过,3PC 并没有完美解决 2PC 的阻塞问题,引入了一些新问题比如性能糟糕,而且,依然存在数据不一致性问题。因此,3PC 的实际应用并不是很广泛,多数应用会选择通过复制状态机解决 2PC 的阻塞问题。
TCC(补偿事务)
TCC 属于目前比较火的一种柔性事务解决方案。TCC 这个概念最早诞生于数据库专家帕特 · 赫兰德(Pat Helland)于 2007 发表的 《Life beyond Distributed Transactions: an Apostate’s Opinion》 这篇论文,感兴趣的小伙伴可以阅读一下这篇论文。
简单来说,TCC 是 Try、Confirm、Cancel 三个词的缩写,它分为三个阶段:
- Try(尝试)阶段 : 尝试执行。完成业务检查,并预留好必需的业务资源。
- Confirm(确认)阶段 :确认执行。当所有事务参与者的 Try 阶段执行成功就会执行 Confirm ,Confirm 阶段会处理 Try 阶段预留的业务资源。否则,就会执行 Cancel 。
- Cancel(取消)阶段 :取消执行,释放 Try 阶段预留的业务资源。
每个阶段由业务代码控制,这样可以避免长事务,性能更好。
我们拿转账场景来说:
- Try(尝试)阶段 : 在转账场景下,Try 要做的事情是就是检查账户余额是否充足,预留的资源就是转账资金。
- Confirm(确认)阶段 : 如果 Try 阶段执行成功的话,Confirm 阶段就会执行真正的扣钱操作。
- Cancel(取消)阶段 :释放 Try 阶段预留的转账资金。
一般情况下,当我们使用TCC
模式的时候,需要自己实现 try
, confirm
, cancel
这三个方法,来达到最终一致性。
正常情况下,会执行 try
, confirm
方法。
出现异常的话,会执行 try
,cancel
方法。
Try 阶段出现问题的话,可以执行 Cancel。那如果 Confirm 或者 Cancel 阶段失败了怎么办呢?
TCC 会记录事务日志并持久化事务日志到某种存储介质上比如本地文件、关系型数据库、Zookeeper,事务日志包含了事务的执行状态,通过事务执行状态可以判断出事务是提交成功了还是提交失败了,以及具体失败在哪一步。如果发现是 Confirm 或者 Cancel 阶段失败的话,会进行重试,继续尝试执行 Confirm 或者 Cancel 阶段的逻辑。重试的次数通常为 6 次,如果超过重试的次数还未成功执行的话,就需要人工介入处理了。
如果代码没有特殊 Bug 的话,Confirm 或者 Cancel 阶段出现问题的概率是比较小的。
事务日志会被删除吗? 会的。如果事务提交成功(没有抛出任何异常),就可以删除对应的事务日志,节省资源。
TCC 模式不需要依赖于底层数据资源的事务支持,但是需要我们手动实现更多的代码,属于 侵入业务代码 的一种分布式解决方案。
TCC 事务模型的思想类似 2PC,我简单花了一张图对比一下二者。
TCC 和 2PC/3PC 有什么区别呢?
2PC/3PC 依靠数据库或者存储资源层面的事务,TCC 主要通过修改业务代码来实现。
2PC/3PC 属于业务代码无侵入的,TCC 对业务代码有侵入。
2PC/3PC 追求的是强一致性,在两阶段提交的整个过程中,一直会持有数据库的锁。TCC 追求的是最终一致性,不会一直持有各个业务资源的锁。
针对 TCC 的实现,业界也有一些不错的开源框架。不同的框架对于 TCC 的实现可能略有不同,不过大致思想都一样。
ByteTCC : ByteTCC 是基于 Try-Confirm-Cancel(TCC)机制的分布式事务管理器的实现。 相关阅读:关于如何实现一个 TCC 分布式事务框架的一点思考
Seata :Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。
Hmily : 金融级分布式事务解决方案。
MQ 事务
RocketMQ 、 Kafka、Pulsar 、QMQ 都提供了事务相关的功能。事务允许事件流应用将消费,处理,生产消息整个过程定义为一个原子操作。
这里我们拿 RocketMQ 来说(图源:《消息队列高手课》)。相关阅读:RocketMQ 事务消息参考文档 。
- MQ 发送方(比如物流服务)在消息队列上开启一个事务,然后发送一个“半消息”给 MQ Server/Broker。事务提交之前,半消息对于 MQ 订阅方/消费者(比如第三方通知服务)不可见
- “半消息”发送成功的话,MQ 发送方就开始执行本地事务。
- MQ 发送方的本地事务执行成功的话,“半消息”变成正常消息,可以正常被消费。MQ 发送方的本地事务执行失败的话,会直接回滚。
从上面的流程中可以看出,MQ 的事务消息使用的是两阶段提交(2PC),简单来说就是咱先发送半消息,等本地事务执行成功之后,半消息才变为正常消息。
如果 MQ 发送方提交或者回滚事务消息时失败怎么办?
RocketMQ 中的 Broker 会定期去 MQ 发送方上反查这个事务的本地事务的执行情况,并根据反查结果决定提交或者回滚这个事务。
事务反查机制的实现依赖于我们业务代码实现的对应的接口,比如你要查看创建物流信息的本地事务是否执行成功的话,直接在数据库中查询对应的物流信息是否存在即可。
如果正常消息没有被正确消费怎么办呢?
消息消费失败的话,RocketMQ 会自动进行消费重试。如果超过最大重试次数这个消息还是没有正确消费,RocketMQ 就会认为这个消息有问题,然后将其放到 死信队列。
进入死信队列的消费一般需要人工处理,手动排查问题。
QMQ 的事务消息就没有 RocketMQ 实现的那么复杂了,它借助了数据库自带的事务功能。其核心思想其实就是 eBay 提出的 本地消息表 方案,将分布式事务拆分成本地事务进行处理。
我们维护一个本地消息表用来存放消息发送的状态,保存消息发送情况到本地消息表的操作和业务操作要在一个事务里提交。这样的话,业务执行成功代表消息表也写入成功。
然后,我们再单独起一个线程定时轮询消息表,把没处理的消息发送到消息中间件。
消息发送成功后,更新消息状态为成功或者直接删除消息。
RocketMQ 的事务消息方案中,如果消息队列挂掉,数据库事务就无法执行了,整个应用也就挂掉了。
QMQ 的事务消息方案中,即使消息队列挂了也不会影响数据库事务的执行。
因此,QMQ 实现的方案能更加适应于大多数业务。不过,这种方法同样适用于其他消息队列,只能说 QMQ 封装的更好,开箱即用罢了!
相关阅读: 面试官:RocketMQ 分布式事务消息的缺点?
Saga
Saga 绝对可以说是历史非常悠久了,Saga 事务理论在 1987 年 Hector & Kenneth 在 ACM 发表的论文 《Sagas》 中就被提出了,早于分布式事务概念的提出。
Saga 属于长事务解决方案,其核心思想是将长事务拆分为多个本地短事务(本地短事务序列)。
- 长事务 —> T1,T2 ~ Tn 个本地短事务
- 每个短事务都有一个补偿动作 —> C1,C2 ~ Cn
下图来自于 微软技术文档—Saga 分布式事务 。
如果 T1,T2 ~ Tn 这些短事务都能顺利完成的话,整个事务也就顺利结束,否则,将采取恢复模式。
反向恢复 :
- 简介:如果 Ti 短事务提交失败,则补偿所有已完成的事务(一直执行 Ci 对 Ti 进行补偿)。
- 执行顺序:T1,T2,…,Ti(失败),Ci(补偿),…,C2,C1。
正向恢复 :
- 简介:如果 Ti 短事务提交失败,则一直对 Ti 进行重试,直至成功为止。
- 执行顺序:T1,T2,…,Ti(失败),Ti(重试)…,Ti+1,…,Tn。
和 TCC 类似,Saga 正向操作与补偿操作都需要业务开发者自己实现,因此也属于 侵入业务代码 的一种分布式解决方案。和 TCC 很大的一点不同是 Saga 没有“Try” 动作,它的本地事务 Ti 直接被提交。因此,性能非常高!
理论上来说,补偿操作一定能够执行成功。不过,当网络出现问题或者服务器宕机的话,补偿操作也会执行失败。这种情况下,往往需要我们进行人工干预。并且,为了能够提高容错性(比如 Saga 系统本身也可能会崩溃),保证所有的短事务都得以提交或补偿,我们还需要将这些操作通过日志记录下来(Saga log,类似于数据库的日志机制)。这样,Saga 系统恢复之后,我们就知道短事务执行到哪里了或者补偿操作执行到哪里了。
另外,因为 Saga 没有进行“Try” 动作预留资源,所以不能保证隔离性。这也是 Saga 比较大的一个缺点。
针对 Saga 的实现,业界也有一些不错的开源框架。不同的框架对于 Saga 的实现可能略有不同,不过大致思想都一样。
- ServiceComb Pack :微服务应用的数据最终一致性解决方案。
- Seata :Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。
分布式事务开源项目
- Seata :Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。经历过双 11 的实战考验。
- Hmily :Hmily 是一款高性能,零侵入,金融级分布式事务解决方案,目前主要提供柔性事务的支持,包含
TCC
,TAC
(自动生成回滚SQL
) 方案,未来还会支持 XA 等方案。个人开发项目,目前在京东数科重启,未来会成为京东数科的分布式事务解决方案。 - Raincat : 2 阶段提交分布式事务中间件。
- Myth : 采用消息队列解决分布式事务的开源框架, 基于 Java 语言来开发(JDK1.8),支持 Dubbo,SpringCloud,Motan 等 rpc 框架进行分布式事务。
服务治理:监控系统如何做?
个人学习笔记,大部分内容整理自书籍、博客和官方文档。
相关文章 &书籍:
相关视频:
监控系统有什么用?
建立完善的监控体系主要是为了:
- 长期趋势分析 :通过对监控样本数据的持续收集和统计,对监控指标进行长期趋势分析。例如,通过对磁盘空间增长率的判断,我们可以提前预测在未来什么时间节点上需要对资源进行扩容。
- 数据可视化 :通过可视化仪表盘能够直接获取系统的运行状态、资源使用情况、以及服务运行状态等直观的信息。
- 预知故障和告警 : 当系统出现或者即将出现故障时,监控系统需要迅速反应并通知管理员,从而能够对问题进行快速的处理或者提前预防问题的发生,避免出现对业务的影响。
- 辅助定位故障、性能调优、容量规划以及自动化运维
出任何线上事故,先不说其他地方有问题,监控部分一定是有问题的。
如何才能更好地使用监控使用?
- 了解监控对象的工作原理:要做到对监控对象有基本的了解,清楚它的工作原理。比如想对 JVM 进行监控,你必须清楚 JVM 的堆内存结构和垃圾回收机制。
- 确定监控对象的指标:清楚使用哪些指标来刻画监控对象的状态?比如想对某个接口进行监控,可以采用请求量、耗时、超时量、异常量等指标来衡量。
- 定义合理的报警阈值和等级:达到什么阈值需要告警?对应的故障等级是多少?不需要处理的告警不是好告警,可见定义合理的阈值有多重要,否则只会降低运维效率或者让监控系统失去它的作用。
- 建立完善的故障处理流程:收到故障告警后,一定要有相应的处理流程和 oncall 机制,让故障及时被跟进处理。
常见的监控对象和指标有哪些?
- 硬件监控 :电源状态、CPU 状态、机器温度、风扇状态、物理磁盘、raid 状态、内存状态、网卡状态
- 服务器基础监控 :CPU、内存、磁盘、网络
- 数据库监控 :数据库连接数、QPS、TPS、并行处理的会话数、缓存命中率、主从延时、锁状态、慢查询
中间件监控 :
- Nginx:活跃连接数、等待连接数、丢弃连接数、请求量、耗时、5XX 错误率
- Tomcat:最大线程数、当前线程数、请求量、耗时、错误量、堆内存使用情况、GC 次数和耗时
- 缓存 :成功连接数、阻塞连接数、已使用内存、内存碎片率、请求量、耗时、缓存命中率
- 消息队列:连接数、队列数、生产速率、消费速率、消息堆积量
应用监控 :
- HTTP 接口:URL 存活、请求量、耗时、异常量
- RPC 接口:请求量、耗时、超时量、拒绝量
- JVM :GC 次数、GC 耗时、各个内存区域的大小、当前线程数、死锁线程数
- 线程池:活跃线程数、任务队列大小、任务执行耗时、拒绝任务数
- 连接池:总连接数、活跃连接数
- 日志监控:访问日志、错误日志
- 业务指标:视业务来定,比如 PV、订单量等
监控的基本流程了解吗?
无论是开源的监控系统还是自研的监控系统,监控的整个流程大同小异,一般都包括以下模块:
- 数据采集:采集的方式有很多种,包括日志埋点进行采集(通过 Logstash、Filebeat 等进行上报和解析),JMX 标准接口输出监控指标,被监控对象提供 REST API 进行数据采集(如 Hadoop、ES),系统命令行,统一的 SDK 进行侵入式的埋点和上报等。
- 数据传输:将采集的数据以 TCP、UDP 或者 HTTP 协议的形式上报给监控系统,有主动 Push 模式,也有被动 Pull 模式。
- 数据存储:有使用 MySQL、Oracle 等 RDBMS 存储的,也有使用时序数据库 RRDTool、OpentTSDB、InfluxDB 存储的,还有使用 HBase 存储的。
- 数据展示:数据指标的图形化展示。
- 监控告警:灵活的告警设置,以及支持邮件、短信、IM 等多种通知通道。
监控系统需要满足什么要求?
- 实时监控&告警 :监控系统对业务服务系统实时监控,如果产生系统异常及时告警给相关人员。
- 高可用 :要保障监控系统的可用性
- 故障容忍 :监控系统不影响业务系统的正常运行,监控系统挂了,应用正常运行。
- 可扩展 :支持分布式、跨 IDC 部署,横向扩展。
- 可视化 :自带可视化图标、支持对接各类可视化组件比如 Grafana 。
监控系统技术选型有哪些?如何选择?
老牌监控系统
Zabbix 和 Nagios 相继出现在 1998 年和 1999 年,目前已经被淘汰,不太建议使用,Prometheus 是更好的选择。
Zabbix
- 介绍 :老牌监控的优秀代表。产品成熟,监控功能很全面,采集方式丰富(支持 Agent、SNMP、JMX、SSH 等多种采集方式,以及主动和被动的数据传输方式),使用也很广泛,差不多有 70%左右的互联网公司都曾使用过 Zabbix 作为监控解决方案。
- 开发语言 : C
- 数据存储 : Zabbix 存储在 MySQL 上,也可以存储在其他数据库服务。Zabbix 由于使用了关系型数据存储时序数据,所以在监控大规模集群时常常在数据存储方面捉襟见肘。所以从 Zabbix 4.2 版本后开始支持 TimescaleDB 时序数据库,不过目前成熟度还不高。
- 数据采集方式 : Zabbix 通过 SNMP、Agent、ICMP、SSH、IPMI 等对系统进行数据采集。Zabbix 采用的是 Push 模型(客户端发送数据给服务端)。
- 数据展示 :自带展示界面,也可以对接 Grafana。
- 评价 :不太建议使用 Zabbix,性能可能会成为监控系统的瓶颈。并且,应用层监控支持有限、二次开发难度大(基于 c 语言)、数据模型不强大。
相关阅读:《zabbix 运维手册》
Nagios
- 介绍 :Nagios 能有效监控 Windows、Linux 和 UNIX 的主机状态(CPU、内存、磁盘等),以及交换机、路由器等网络设备(SMTP、POP3、HTTP 和 NNTP 等),还有 Server、Application、Logging,用户可自定义监控脚本实现对上述对象的监控。Nagios 同时提供了一个可选的基于浏览器的 Web 界面,以方便系统管理人员查看网络状态、各种系统问题以及日志等。
- 开发语言 : C
- 数据存储 : MySQL 数据库
- 数据采集方式 : 通过各种插件采集数据
- 数据展示 :自带展示界面,不过功能简单。
- 评价 :不符合当前监控系统的要求,而且,Nagios 免费版本的功能非常有限,运维管理难度非常大。
新一代监控系统
相比于老牌监控系统,新一代监控系统有明显的优势,比如:灵活的数据模型、更成熟的时序数据库、强大的告警功能。
Open-Falcon
- 介绍 :小米 2015 年开源的企业级监控工具,在架构设计上吸取了 Zabbix 的经验,同时很好地解决了 Zabbix 的诸多痛点。Github 地址:https://github.com/open-falcon 。官方文档:https://book.open-falcon.org/ 。
- 开发语言 :Go、Python。
- 数据存储 : 环型数据库,支持对接时序数据库 OpenTSDB。
- 数据采集方式 : 自动发现,支持 falcon-agent、snmp、支持用户主动 push、用户自定义插件支持、opentsdb data model like(timestamp、endpoint、metric、key-value tags)。Open-Falcon 和 Zabbix 采用的都是 Push 模型(客户端发送数据给服务端)。
- 数据展示 :自带展示界面,也可以对接 Grafana。
- 评价 :用户集中在国内,流行度一般,生态一般。
Open-Falcon 架构图如下:
- Falcon-agent :采集模块。类似 Zabbix 的 agent,Kubernetes 自带监控体系中的 cAdvisor,Nagios 中的 Plugin,使用 Go 语言开发,用于采集主机上的各种指标数据。
- Hearthbeat server :心跳服务。每个 Agent 都会周期性地通过 RPC 方式将自己地状态上报给 HBS,主要包括主机名、主机 IP、Agent 版本和插件版本,Agent 还会从 HBS 获取自己需要执行的采集任务和自定义插件。
- Transfer :负责监控 agent 发送的监控数据,并对数据进行处理,在过滤后通过一致性 Hash 算法将数据发送到 Judge 或者 Graph。为了支持存储大量的历史数据,Transfer 还支持 OpenTSDB。Transfer 本身没有状态,可以随意扩展。
- Jedge :告警模块。Transfer 转发到 Judge 的数据会触发用户设定的告警规则,如果满足,则会触发邮件、微信或者回调接口。这里为了避免重复告警,引入了 Redis 暂存告警,从而完成告警合并和抑制。
- Graph :RRD 数据上报、归档、存储的组件。Graph 在收到数据以后,会以 RRDtool 的数据归档方式存储数据,同时提供 RPC 方式的监控查询接口。
- API : 查询模块。主要提供查询接口,不但可以从 Grapg 里面读取数据,还可以对接 MySQL,用于保存告警、用户等信息。
- Dashboard : 监控数据展示面板。由 Python 开发而成,提供 Open-Falcon 的数据和告警展示,监控数据来自 Graph,Dashboard 允许用户自定义监控面板。
- Aggregator : 聚合模块。聚合某集群下所有机器的某个指标的值,提供一种集群视角的监控体验。 通过定时从 Graph 获取数据,按照集群聚合产生新的监控数据并将监控数据发送到 Transfer。
Prometheus
- 介绍 :Prometheus 受启发于 Google 的 Brogmon 监控系统,由前 Google 员工 2015 年正式发布。截止到 2021 年 9 月 2 日,Prometheus 在 Github 上已经收获了 38.5k+ Star,600+位 Contributors。 Github 地址:https://github.com/prometheus 。
- 开发语言 :Go
- 数据存储 : Prometheus 自研一套高性能的时序数据库,并且还支持外接时序数据库。
- 数据采集方式 : Prometheus 的基本原理是通过 HTTP 协议周期性抓取被监控组件的状态,任意组件只要提供对应的 HTTP 接口就可以接入监控。Prometheus 在收集数据时,采用的 Pull 模型(服务端主动去客户端拉取数据)
- 数据展示 :自带展示界面,也可以对接 Grafana。
- 评价 :目前国内外使用最广泛的一个监控系统,生态也非常好,成熟稳定!
Prometheus 特性 :
- 开箱即用的各种服务发现机制,可以自动发现监控端点;
- 专为监控指标数据设计的高性能时序数据库 TSDB;
- 强大易用的查询语言PromQL以及丰富的聚合函数;
- 可以配置灵活的告警规则,支持告警收敛(分组、抑制、静默)、多级路由等等高级功能;
- 生态完善,有各种现成的开源 Exporter 实现,实现自定义的监控指标也非常简单。
Prometheus 基本架构 :
- Prometheus Server:核心组件,用于收集、存储监控数据。它同时支持静态配置和通过 Service Discovery 动态发现来管理监控目标,并从监控目标中获取数据。此外,Prometheus Server 也是一个时序数据库,它将监控数据保存在本地磁盘中,并对外提供自定义的 PromQL 语言实现对数据的查询和分析。
- Exporter:用来采集数据,作用类似于 agent,区别在于 Prometheus 是基于 Pull 方式拉取采集数据的,因此,Exporter 通过 HTTP 服务的形式将监控数据按照标准格式暴露给 Prometheus Server,社区中已经有大量现成的 Exporter 可以直接使用,用户也可以使用各种语言的 client library 自定义实现。
- Push gateway:主要用于瞬时任务的场景,防止 Prometheus Server 来 pull 数据之前此类 Short-lived jobs 就已经执行完毕了,因此 job 可以采用 push 的方式将监控数据主动汇报给 Push gateway 缓存起来进行中转。
- 当告警产生时,Prometheus Server 将告警信息推送给 Alert Manager,由它发送告警信息给接收方。
- Prometheus 内置了一个简单的 web 控制台,可以查询配置信息和指标等,而实际应用中我们通常会将 Prometheus 作为 Grafana 的数据源,创建仪表盘以及查看指标。
推荐一本 Prometheus 的开源书籍《Prometheus 操作指南》。
总结
- 监控是一项长期建设的事情,一开始就想做一个 All In One 的监控解决方案,我觉得没有必要。从成本角度考虑,在初期直接使用开源的监控方案即可,先解决有无问题。
- Zabbix、Open-Falcon 和 Prometheus 都支持和 Grafana 做快速集成,想要美观且强大的可视化体验,可以和 Grafana 进行组合。
- Open-Falcon 的核心优势在于数据分片功能,能支撑更多的机器和监控项;Prometheus 则是容器监控方面的标配,有 Google 和 k8s 加持。
服务治理:分布式下如何进行日志管理?
因为日志系统在询问项目经历的时候经常会被问到,所以,我就写了这篇文章。
这是一篇日志系统常见概念的扫盲篇~不会涉及到具体架构的日志系统的搭建过程。旨在帮助对于日志系统不太了解的小伙伴,普及一些日志系统常见的概念。
何为日志?
在我看来,日志就是系统对某些行为的一些记录,这些行为包括:系统出现错误(定位问题、解决问题)、记录关键的业务信息(定位问题、解决问题)、记录操作行为(保障安全)等等。
按照较为官方的话来说:“日志是带时间戳的基于时间序列的机器数据,包括 IT 系统信息(服务器、网络设备、操作系统、应用软件)、物联网各种传感器信息。日志可以反映用户/机器的行为,是真实的数据”。
为何要用日志系统?
没有日志系统之前,我们的日志可能分布在多台服务器上。每次需要查看日志,我们都需要登录每台机器。然后,使用 grep
、wc
等 Linux 命令来对日志进行搜索。这个过程是非常麻烦并且耗时的!并且,日志量不大的时候,这个速度还能忍受。当日志量比较多的时候,整个过程就是非常慢。
从上面我的描述中,你已经发现,没有对日志实现集中管理,主要给我们带来了下面这几点问题:
- 开发人员登录线上服务器查看日志比较麻烦并且存在安全隐患
- 日志数据比较分散,难以维护,不方便检索。
- 日志数量比较大的时候,查询速度比较慢。
- 无法对日志数据进行可视化展示。
日志系统就是为了对日志实现集中管理。它也是一个系统,不过主要是负责处理日志罢了。
一个最基本的日志系统要做哪些事情?
为了解决没有日志系统的时候,存在的一些问题,一直最基本的 日志系统需要做哪些事情呢?
采集日志 :支持多种日志格式以及数据源的采集。
日志数据清洗/处理 :采集到的原始日志数据需要首先清洗/处理一波。
存储 :为了方便对清洗后的日志进行处理,我们可以对接多种存储方式比如 ElasticSearch(日志检索) 、Hadoop(离线数据分析)。
展示与搜素 :支持可视化地展示日志,并且能够根据关键词快速的定位到日志并查看日志上下文。
告警 :支持对接常见的监控系统。
我专门画了一张图,展示一下日志系统处理日志的一个基本流程。
另外,一些比较高大上的日志系统甚至还支持 实时分析、离线分析 等功能
ELK 了解么?
ELK 是目前使用的比较多的一个开源的日志系统解决方案,背靠是 Elastic 这家专注搜索的公司。
ELK 老三件套
最原始的时候,ELK 是由 3 个开源项目的首字母构成,分别是 Elasticsearch 、Logstash、Kibana。
下图是一个最简单的 ELK 日志系统架构 :
我们分别来介绍一下这些开源项目以及它们在这个日志系统中起到的作用:
- Logstash :Logstash 主要用于日志的搜集、分析和过滤,支持对多种日志类型进行处理。在 ELK 日志系统中,Logstash 负责日志的收集和清洗。
- Elasticsearch :ElasticSearch 一款使用 Java 语言开发的搜索引擎,基于 Lucence 。可以解决使用数据库进行模糊搜索时存在的性能问题,提供海量数据近实时的检索体验。在 ELK 日志系统中,Elasticsearch 负责日志的搜素。
- Kibana :Kibana 是专门设计用来与 Elasticsearch 协作的,可以自定义多种表格、柱状图、饼状图、折线图对存储在 Elasticsearch 中的数据进行深入挖掘分析与可视化。 ELK 日志系统中,Logstash 主要负责对从 Elasticsearch 中搜索出来的日志进行可视化展示。
新一代 ELK 架构
ELK 属于比较老牌的一款日志系统解决方案,这个方案存在一个问题就是:Logstash 对资源消耗过高。
于是, Elastic 推出了 Beats 。Beats 基于名为libbeat的 Go 框架,一共包含 8 位成员。
这个时候,ELK 已经不仅仅代表 Elasticsearch 、Logstash、Kibana 这 3 个开源项目了。
Elastic 官方将 ELK 重命名为 Elastic Stack(Elasticsearch、Kibana、Beats 和 Logstash)。但是,大家目前仍然习惯将其成为 ELK 。
Elastic 的官方文档是这样描述的(由 Chrome 插件 Mate Translate 提供翻译功能):
现在的 ELK 架构变成了这样:
Beats 采集的数据可以直接发送到 Elasticsearch 或者在 Logstash 进一步处理之后再发送到 Elasticsearch。
Beats 的诞生,也大大地扩展了老三件套版本的 ELK 的功能。Beats 组件除了能够通过 Filebeat 采集日志之外,还能通过 Metricbeat 采集服务器的各种指标,通过 Packetbeat 采集网络数据。
我们不需要将 Beats 都用上,一般对于一个基本的日志系统,只需要 Filebeat 就够了。
Filebeat 是一个轻量型日志采集器。无论您是从安全设备、云、容器、主机还是 OT 进行数据收集,Filebeat 都将为您提供一种轻量型方法,用于转发和汇总日志与文件,让简单的事情不再繁杂。
Filebeat 是 Elastic Stack 的一部分,能够与 Logstash、Elasticsearch 和 Kibana 无缝协作。
Filebeat 能够轻松地将数据传送到 Logstash(对日志进行处理)、Elasticsearch(日志检索)、甚至是 Kibana (日志展示)中。
Filebeat 只是对日志进行采集,无法对日志进行处理。日志具体的处理往往还是要交给 Logstash 来做。
更多关于 Filebeat 的内容,你可以看看 Filebeat 官方文档教程。
Filebeat+Logstash+Elasticsearch+Kibana 架构概览
下图一个最基本的 Filebeat+Logstash+Elasticsearch+Kibana 架构图,图片来源于:《The ELK Stack ( Elasticsearch, Logstash, and Kibana ) Using Filebeat》。
Filebeat 替代 Logstash 采集日志,具体的日志处理还是由 Logstash 来做。
针对上图的日志系统架构图,有下面几个可优化点:
- 在 Kibana 和用户之间,使用 Nginx 来做反向代理,免用户直接访问 Kibana 服务器,提高安全性。
- Filebeat 和 Logstash 之间增加一层消息队列比如 Kafka、RabbitMQ。Filebeat 负责将收集到的数据写入消息队列,Logstash 取出数据做进一步处理。
EFK
EFK 中的 F 代表的是 Fluentd。下图是一个最简单的 EFK 日志系统架构 :
Fluentd 是一款开源的日志收集器,使用 Ruby 编写,其提供的功能和 Logstash 差不多。但是,要更加轻量,性能也更优越,内存占用也更低。具体使用教程,可以参考《性能优越的轻量级日志收集工具,微软、亚马逊都在用!》。
轻量级日志系统 Loki
上面介绍到的 ELK 日志系统方案功能丰富,稳定可靠。不过,对资源的消耗也更大,成本也更高。而且,用过 ELK 日志系统的小伙伴肯定会发现其实很多功能压根都用不上。
因此,就有了 Loki,这是一个 Grafana Labs 团队开源的小巧易用的日志系统,原生支持 Grafana。
并且,Loki 专门为 Prometheus 和 Kubernetes 用户做了相关优化比如 Loki 特别适合存储Kubernetes Pod 日志。
官方的介绍也比较有意思哈!Like Prometheus,But For Logs
. (类似于 Prometheus 的日志系统,不过主要是为日志服务的)。
根据官网 ,Loki 的架构如下图所示
Loki 的整个架构非常简单,主要有 3 个组件组成:
- Loki 是主服务器,负责存储日志和处理查询。
- Promtail 是代理,负责收集日志并将其发送给 Loki 。
- Grafana 用于 UI 展示。
Loki 提供了详细的使用文档,上手相对来说比较容易。并且,目前其流行度还是可以的。你可以很方便在网络上搜索到有关 Loki 的博文。
总结
这篇文章我主要介绍了日志系统相关的知识,包括:
- 何为日志?
- 为何要用日志系统?一个基本的日志系统要做哪些事情?
- ELK、EFK
- 轻量级日志系统 Loki
另外,大部分图片都是我使用 draw.io 来绘制的。一些技术名词的图标,我们可以直接通过 Google 图片搜索即可,方法: 技术名词+图标(示例:Logstash icon)
参考
- ELK 架构和 Filebeat 工作原理详解:https://developer.ibm.com/zh/articles/os-cn-elk-filebeat/
- ELK Introduction-elastic 官方 :https://elastic-stack.readthedocs.io/en/latest/introduction.html
- ELK Stack Tutorial: Learn Elasticsearch, Logstash, and Kibana :https://www.guru99.com/elk-stack-tutorial.html
高并发
高可用:如何设计一个高可用系统?
一篇短小的文章,面试经常遇到的这个问题。本文主要包括下面这些内容:
高可用的定义
哪些情况可能会导致系统不可用?
有些提高系统可用性的方法?只是简单的提一嘴,更具体内容在后续的文章中介绍,就拿限流来说,你需要搞懂:何为限流?如何限流?为什么要限流?如何做呢?说一下原理?。
什么是高可用?可用性的判断标准是啥?
高可用描述的是一个系统在大部分时间都是可用的,可以为我们提供服务的。高可用代表系统即使在发生硬件故障或者系统升级的时候,服务仍然是可用的。
一般情况下,我们使用多少个 9 来评判一个系统的可用性,比如 99.9999% 就是代表该系统在所有的运行时间中只有 0.0001% 的时间是不可用的,这样的系统就是非常非常高可用的了!当然,也会有系统如果可用性不太好的话,可能连 9 都上不了。
哪些情况会导致系统不可用?
黑客攻击;
硬件故障,比如服务器坏掉。
并发量/用户请求量激增导致整个服务宕掉或者部分服务不可用。
代码中的坏味道导致内存泄漏或者其他问题导致程序挂掉。
网站架构某个重要的角色比如 Nginx 或者数据库突然不可用。
自然灾害或者人为破坏。
……
有哪些提高系统可用性的方法?
1. 注重代码质量,测试严格把关
我觉得这个是最最最重要的,代码质量有问题比如比较常见的内存泄漏、循环依赖都是对系统可用性极大的损害。大家都喜欢谈限流、降级、熔断,但是我觉得从代码质量这个源头把关是首先要做好的一件很重要的事情。如何提高代码质量?比较实际可用的就是 CodeReview,不要在乎每天多花的那 1 个小时左右的时间,作用可大着呢!
另外,安利这个对提高代码质量有实际效果的宝贝:
sonarqube :保证你写出更安全更干净的代码!(ps: 目前所在的项目基本都会用到这个插件)。
Alibaba 开源的 Java 诊断工具 Arthas 也是很不错的选择。
IDEA 自带的代码分析等工具进行代码扫描也是非常非常棒的。
2.使用集群,减少单点故障
先拿常用的 Redis 举个例子!我们如何保证我们的 Redis 缓存高可用呢?答案就是使用集群,避免单点故障。当我们使用一个 Redis 实例作为缓存的时候,这个 Redis 实例挂了之后,整个缓存服务可能就挂了。使用了集群之后,即使一台 Redis 实例,不到一秒就会有另外一台 Redis 实例顶上。
3.限流
流量控制(flow control),其原理是监控应用流量的 QPS 或并发线程数等指标,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。——来自 alibaba-Sentinel 的 wiki。
4.超时和重试机制设置
一旦用户请求超过某个时间的得不到响应,就抛出异常。这个是非常重要的,很多线上系统故障都是因为没有进行超时设置或者超时设置的方式不对导致的。我们在读取第三方服务的时候,尤其适合设置超时和重试机制。一般我们使用一些 RPC 框架的时候,这些框架都自带的超时重试的配置。如果不进行超时设置可能会导致请求响应速度慢,甚至导致请求堆积进而让系统无法在处理请求。重试的次数一般设为 3 次,再多次的重试没有好处,反而会加重服务器压力(部分场景使用失败重试机制会不太适合)。
5.熔断机制
超时和重试机制设置之外,熔断机制也是很重要的。 熔断机制说的是系统自动收集所依赖服务的资源使用情况和性能指标,当所依赖的服务恶化或者调用失败次数达到某个阈值的时候就迅速失败,让当前系统立即切换依赖其他备用服务。 比较常用的是流量控制和熔断降级框架是 Netflix 的 Hystrix 和 alibaba 的 Sentinel。
6.异步调用
异步调用的话我们不需要关心最后的结果,这样我们就可以用户请求完成之后就立即返回结果,具体处理我们可以后续再做,秒杀场景用这个还是蛮多的。但是,使用异步之后我们可能需要 适当修改业务流程进行配合,比如用户在提交订单之后,不能立即返回用户订单提交成功,需要在消息队列的订单消费者进程真正处理完该订单之后,甚至出库后,再通过电子邮件或短信通知用户订单成功。除了可以在程序中实现异步之外,我们常常还使用消息队列,消息队列可以通过异步处理提高系统性能(削峰、减少响应所需时间)并且可以降低系统耦合性。
7.使用缓存
如果我们的系统属于并发量比较高的话,如果我们单纯使用数据库的话,当大量请求直接落到数据库可能数据库就会直接挂掉。使用缓存缓存热点数据,因为缓存存储在内存中,所以速度相当地快!
8.其他
核心应用和服务优先使用更好的硬件
监控系统资源使用情况增加报警设置。
注意备份,必要时候回滚。
灰度发布: 将服务器集群分成若干部分,每天只发布一部分机器,观察运行稳定没有故障,第二天继续发布一部分机器,持续几天才把整个集群全部发布完毕,期间如果发现问题,只需要回滚已发布的一部分服务器即可
定期检查/更换硬件: 如果不是购买的云服务的话,定期还是需要对硬件进行一波检查的,对于一些需要更换或者升级的硬件,要及时更换或者升级。
…..(想起来再补充!也欢迎各位欢迎补充!)
高可用:负载均衡的常见算法有哪些?
相关面试题 :
- 服务端负载均衡一般怎么做?
- 四层负载均衡和七层负载均衡的区别?
- 负载均衡的常见算法有哪些?
- 七层负载均衡常见解决方案有哪些?
- 客户端负载均衡的常见解决方案有哪些?
什么是负载均衡?
负载均衡 指的是将用户请求分摊到不同的服务器上处理,以提高系统整体的并发处理能力以及可靠性。负载均衡服务可以有由专门的软件或者硬件来完成,一般情况下,硬件的性能更好,软件的价格更便宜(后文会详细介绍到)。
下图是《Java 面试指北》 「高并发篇」中的一篇文章的配图,从图中可以看出,系统的商品服务部署了多份在不同的服务器上,为了实现访问商品服务请求的分流,我们用到了负载均衡。
负载均衡是一种比较常用且实施起来较为简单的提高系统并发能力和可靠性的手段,不论是单体架构的系统还是微服务架构的系统几乎都会用到。
负载均衡通常分为哪两种?
负载均衡可以简单分为 服务端负载均衡 和 客户端负载均衡 这两种。
服务端负载均衡涉及到的知识点更多,工作中遇到的也比较多,因为,我会花更多时间来介绍。
服务端负载均衡
服务端负载均衡 主要应用在 系统外部请求 和 网关层 之间,可以使用 软件 或者 硬件 实现。
下图是我画的一个简单的基于 Nginx 的服务端负载均衡示意图:
硬件负载均衡 通过专门的硬件设备(比如 F5、A10、Array )实现负载均衡功能。
硬件负载均衡的优势是性能很强且稳定,缺点就是实在是太贵了。像基础款的 F5 最低也要 20 多万,绝大部分公司是根本负担不起的,业务量不大的话,真没必要非要去弄个硬件来做负载均衡,用软件负载均衡就足够了!
在我们日常开发中,一般很难接触到硬件负载均衡,接触的比较多的还是 软件负载均衡 。软件负载均衡通过软件(比如 LVS、Nginx、HAproxy )实现负载均衡功能,性能虽然差一些,但价格便宜啊!像基础款的 Linux 服务器也就几千,性能好一点的 2~3 万的就很不错了。
根据 OSI 模型,服务端负载均衡还可以分为:
- 二层负载均衡
- 三层负载均衡
- 四层负载均衡
- 七层负载均衡
最常见的是四层和七层负载均衡,因此,本文也是重点介绍这两种负载均衡。
- 四层负载均衡 工作在 OSI 模型第四层,也就是传输层,这一层的主要协议是 TCP/UDP,负载均衡器在这一层能够看到数据包里的源端口地址以及目的端口地址,会基于这些信息通过一定的负载均衡算法将数据包转发到后端真实服务器。
- 七层负载均衡 工作在 OSI 模型第七层,也就是应用层,这一层的主要协议是 HTTP 。这一层的负载均衡比四层负载均衡路由网络请求的方式更加复杂,它会读取报文的数据部分(比如说我们的 HTTP 部分的报文),然后根据读取到的数据内容(如 URL、Cookie)做出负载均衡决策。
七层负载均衡比四层负载均衡会消耗更多的性能,不过,也相对更加灵活,能够更加智能地路由网络请求,比如说你可以根据请求的内容进行优化如缓存、压缩、加密。
简单来说,四层负载均衡性能更强,七层负载均衡功能更强!
在工作中,我们通常会使用 Nginx 来做七层负载均衡,LVS(Linux Virtual Server 虚拟服务器, Linux 内核的 4 层负载均衡)来做四层负载均衡。关于 Nginx 的常见知识点总结,《Java 面试指北》 中「技术面试题篇」中已经有对应的内容了,感兴趣的小伙伴可以去看看。
不过,LVS 这个绝大部分公司真用不上,像阿里、百度、腾讯、eBay 等大厂才会使用到,用的最多的还是 Nginx。
客户端负载均衡
客户端负载均衡 主要应用于系统内部的不同的服务之间,可以使用现成的负载均衡组件来实现。
在客户端负载均衡中,客户端会自己维护一份服务器的地址列表,发送请求之前,客户端会根据对应的负载均衡算法来选择具体某一台服务器处理请求。
客户端负载均衡器和服务运行在同一个进程或者说 Java 程序里,不存在额外的网络开销。不过,客户端负载均衡的实现会受到编程语言的限制,比如说 Spring Cloud Load Balancer 就只能用于 Java 语言。
Java 领域主流的微服务框架 Dubbo、Spring Cloud 等都内置了开箱即用的客户端负载均衡实现。Dubbo 属于是默认自带了负载均衡功能,Spring Cloud 是通过组件的形式实现的负载均衡,属于可选项,比较常用的是 Spring Cloud Load Balancer(官方,推荐) 和 Ribbon(Netflix,已被启用)。
下图是我画的一个简单的基于 Spring Cloud Load Balancer(Ribbon 也类似) 的客户端负载均衡示意图:
负载均衡常见的算法有哪些?
随机法
随机法 是最简单粗暴的负载均衡算法。
如果没有配置权重的话,所有的服务器被访问到的概率都是相同的。如果配置权重的话,权重越高的服务器被访问的概率就越大。
未加权重的随机算法适合于服务器性能相近的集群,其中每个服务器承载相同的负载。加权随机算法适合于服务器性能不等的集群,权重的存在可以使请求分配更加合理化。
不过,随机算法有一个比较明显的缺陷:部分机器在一段时间之内无法被随机到,毕竟是概率算法,就算是大家权重一样, 也可能会出现这种情况。
于是,轮询法 来了!
轮询法
轮询法是挨个轮询服务器处理,也可以设置权重。
如果没有配置权重的话,每个请求按时间顺序逐一分配到不同的服务器处理。如果配置权重的话,权重越高的服务器被访问的次数就越多。
未加权重的轮询算法适合于服务器性能相近的集群,其中每个服务器承载相同的负载。加权轮询算法适合于服务器性能不等的集群,权重的存在可以使请求分配更加合理化。
一致性 Hash 法
相同参数的请求总是发到同一台服务器处理,比如同个 IP 的请求。
最小连接法
当有新的请求出现时,遍历服务器节点列表并选取其中活动连接数最小的一台服务器来响应当前请求。活动连接数可以理解为当前正在处理的请求数。
最小连接法可以尽可能最大地使请求分配更加合理化,提高服务器的利用率。不过,这种方法实现起来也最复杂,需要监控每一台服务器处理的请求连接数。
七层负载均衡可以怎么做?
简单介绍两种项目中常用的七层负载均衡解决方案:DNS 解析和反向代理。
除了我介绍的这两种解决方案之外,HTTP 重定向等手段也可以用来实现负载均衡,不过,相对来说,还是 DNS 解析和反向代理用的更多一些,也更推荐一些。
DNS 解析
DNS 解析是比较早期的七层负载均衡实现方式,非常简单。
DNS 解析实现负载均衡的原理是这样的:在 DNS 服务器中为同一个主机记录配置多个 IP 地址,这些 IP 地址对应不同的服务器。当用户请求域名的时候,DNS 服务器采用轮询算法返回 IP 地址,这样就实现了轮询版负载均衡。
现在的 DNS 解析几乎都支持 IP 地址的权重配置,这样的话,在服务器性能不等的集群中请求分配会更加合理化。像我自己目前正在用的阿里云 DNS 就支持权重配置。
反向代理
客户端将请求发送到反向代理服务器,由反向代理服务器去选择目标服务器,获取数据后再返回给客户端。对外暴露的是反向代理服务器地址,隐藏了真实服务器 IP 地址。反向代理“代理”的是目标服务器,这一个过程对于客户端而言是透明的。
Nginx 就是最常用的反向代理服务器,它可以将接收到的客户端请求以一定的规则(负载均衡策略)均匀地分配到这个服务器集群中所有的服务器上。
反向代理负载均衡同样属于七层负载均衡。
客户端负载均衡通常是怎么做的?
我们上面也说了,客户端负载均衡可以使用现成的负载均衡组件来实现。
Netflix Ribbon 和 Spring Cloud Load Balancer 就是目前 Java 生态最流行的两个负载均衡组件。
我更建议你使用 Spring 官方的 Spring Cloud LoadBalancer。Spring Cloud 2020.0.0 版本移除了 Netflix 除 Eureka 外的所有组件。Spring Cloud Hoxton.M2 是第一个支持 Spring Cloud Load Balancer 来替代 Netfix Ribbon 的版本。
我们早期学习微服务,肯定接触过 Netflix 公司开源的 Feign、Ribbon、Zuul、Hystrix、Eureka 等知名的微服务系统构建所必须的组件,直到现在依然有非常非常多的公司在使用这些组件。不夸张地说,Netflix 公司引领了 Java 技术栈下的微服务发展。
那为什么 Spring Cloud 这么急着移除 Netflix 的组件呢? 主要是因为在 2018 年的时候,Netflix 宣布其开源的核心组件 Hystrix、Ribbon、Zuul、Eureka 等进入维护状态,不再进行新特性开发,只修 BUG。于是,Spring 官方不得不考虑移除 Netflix 的组件。
Spring Cloud Alibaba 是一个不错的选择,尤其是对于国内的公司和个人开发者来说。
参考
干货 | eBay 的 4 层软件负载均衡实现:https://mp.weixin.qq.com/s/bZMxLTECOK3mjdgiLbHj-g
HTTP Load Balancing(Nginx 官方文档):https://docs.nginx.com/nginx/admin-guide/load-balancer/http-load-balancer/
深入浅出负载均衡 - vivo 互联网技术:https://www.cnblogs.com/vivotech/p/14859041.html
高性能:池化技术的应用场景
池化技术简介
简单来说,池化技术就是将可重复利用的对象比如连接、线程统一管理起来。线程池、数据库连接池、HTTP、Redis 连接池等等都是对池化技术的应用。
通常来说,池化技术所管理的对象,无论是连接还是线程,它们的创建过程都比较耗时,也比较消耗系统资源 。所以,我们把它们放在一个池子里统一管理起来,以达到 提升性能和资源复用的目的 。
从上面对池化技术的介绍,我们可以得出池化技术的核心思想是空间换时间。它的核心策略是使用已经创建好的对象来减少频繁创建对象的性能开销,同时还可以对对象进行统一的管理。
不过,池化技术也不是并非没有缺点的。如果池子中的对象没有被充分利用的话,也会造成多余的内存浪费(相对于池化技术的优点来说的话,这个缺点几乎可以被忽略)。
池化技术常见应用
线程池和数据库连接池我们平时开发过程中应该接触的非常多。因此,我会以线程池和数据库连接池为例来介绍池化技术的实际应用。
线程池
正如其名,线程池主要负责创建和管理线程。
没有线程池的时候,我们每次用到线程就需要单独创建,用完了之后再销毁。然而,创建线程和销毁线程是比较耗费资源和时间的操作。
有了线程池之后,我们可以重复利用已创建的线程降低线程创建和销毁造成的消耗。并且,线程池还可以方便我们对线程进行统一的管理。
我们拿 JDK 1.5 中引入的原生线程池 ThreadPoolExecutor 来举例说明。
ThreadPoolExecutor 有 3 个最重要的参数:
corePoolSize : 核心线程数线程数定义了最小可以同时运行的线程数量。
maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
线程池
ThreadPoolExecutor
不是上来就是直接初始化corePoolSize
个线程,而是有任务来了才创建线程处理任务。
假如我们需要提交任务给线程池执行的话,整个步骤是这样的:
- 提交新任务
- 判断线程池线程数是否少于 coreThreadCount ,是的话就创新线程处理任务,否则的话就将任务丢到队列中等待执行。
- 当队列中的任务满了之后,继续创建线程,直到线程数量达到 maxThreadCount。
- 当线程数量达到 maxThreadCount还是有任务提交,那我们就直接按照拒绝策略处理。
可以看出,JDK 自带的线程池 ThreadPoolExecutor 会优先将处理不过来的任务放到队列中去,而不是创建更多的线程来处理任务。只有当队列中的等待执行的任务满了之后,线程池才会创建线程,直到线程数达到 maximumPoolSize 。如果任务执行时间过长的话,还会很容易造成队列中的任务堆积。
并且,当线程数大于核心线程数时,如果线程等待 keepAliveTime 没有任务处理的话,该线程会被回收,直到线程数缩小到核心线程数才不会继续对线程进行回收。
可以看出,JDK 自带的的这个线程池 ThreadPoolExecutor 比较适合执行 CPU 密集型的任务,不太适合执行 I/O 密集型任务。
为什么这样说呢? 因此执行 CPU 密集型的任务时 CPU 比较繁忙,只需要创建和 CPU 核数相当的线程就好了,多了反而会造成线程上下文切换。
如何判断是 CPU 密集任务还是 IO 密集任务? CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。但凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。
在看极客时间的专栏《深入拆解 Tomcat & Jetty》的时候,我了解到:Tomcat 扩展了原生的 Java 线程池,来满足 Web 容器高并发的需求。
简单来说,Tomcat 自定义线程池继承了 JDK 线程池 java.util.concurrent.ThreadPoolExecutor 重写了部分方法的逻辑(主要是 execute() 方法)。Tomcat 还通过继承 LinkedBlockingQueue 重写 offer() 方法实现了自定义的队列。
这些改变使得 Tomcat 的线程池在任务量大的情况下会优先创建线程,而不是直接将不能处理的任务放到队列中。
Tomcat 自定义线程池的使用方法如下:
1 | //创建定制版的任务队列 |
下面我们来详细看看 Tomcat 的线程池做了哪些改变。
Tomcat 的线程池通过重写 ThreadPoolExecutor
的 execute()
方法实现了自己的任务处理逻辑。Tomcat 的线程池在线程总数达到最大时,不是立即执行拒绝策略,而是再尝试向自定义的任务队列添加任务,添加失败后再执行拒绝策略。那具体如何实现呢,其实很简单,我们来看一下 Tomcat 线程池的execute()
方法的核心代码。
1 | public class ThreadPoolExecutor extends java.util.concurrent.ThreadPoolExecutor { |
到重点的地方了!Tomcat 自定义队列TaskQueue
重写了 LinkedBlockingQueue
的offer
方法,这是关键所在!
当提交的任务数量大于当前的线程数的时候,offer()
会返回 false,线程池会去创建新的线程,而不是等到任务队列满了之后再创建线程。
1 | public class TaskQueue extends LinkedBlockingQueue<Runnable> { |
LinkedBlockingQueue
默认情况下长度是没有限制的,Tomcat 自定义队列定义了一个capacity
变量来限制队列长度。
1 | public class TaskQueue extends LinkedBlockingQueue<Runnable> { |
TaskQueue
的 capacity
的默认值是 Integer.MAX_VALUE
,也就是说默认情况下 Tomcat 的任务队列是没有长度限制的。不过,你可以通过设置 maxQueueSize
参数来限制任务队列的长度。
如果你想要获取更多关于线程的介绍的话,建议阅读我写的下面这几篇文章:
数据库连接池
数据库连接池属于连接池,类似于 HTTP、Redis 连接池,它们的实现原理类似。连接池的结构示意图,如下所示(图片来自:《Java 业务开发常见错误 100 例》):
连接池负责连接的管理包括连接的建立、空闲连接回收等工作。
我们这里以数据库连接池为例来详细介绍。
没有数据库线程池之前,我们接收到一个需要用到数据库的请求,通常是这样来访问数据库的:
- 装载数据库驱动程序;
- 通过 JDBC 建立数据库连接;
- 访问数据库,执行 SQL 语句;
- 断开数据库连接。
假如我们为每一个请求都建立一次数据库连接然后再断开连接是非常耗费资源和时间的。因为,建立和断开数据库连接本身就是比较耗费资源和时间的操作。
如果我们频繁进行数据库连接的建立和断开操作的话,势必会影响到系统的性能。当请求太多的话,系统甚至会因为创建太多数据库连接而直接宕机。
因此,有了数据库连接池来管理我们的数据库连接。当有请求的时候,我们现在数据库连接池中检查是否有空闲的数据库连接,如果有的话,直接分配给它。
如果我们需要获取数据库连接,整个步骤是这样的:
- 系统首先检查空闲池内有没有空闲的数据库连接。
- 如果有的话,直接获取。
- 如果没有的话,先检查数据库连接池的是否达到所允许的最大连接数,没达到的话就新建一个数据库连接,否则就等待一定的时间(timeout)看是否有数据库连接被释放。
- 如果等待时间超过一定的时间(timeout)还是没有数据库连接被释放的话,就会获取数据库连接失败。
实际开发中,我们使用 HikariCP 这个线程的数据库连接池比较多,SpringBoot 2.0 将它设置为默认的数据源连接池。
HikariCP 为了性能的提升(号称是史上性能最好的数据库连接池),做了非常多的优化,比如 HikariCP 自定义 FastStatementList 来代替 ArrayList 、自定义 ConcurrentBag 来提高并发读写的效率,再比如 HikariCP 通过 Javassist 来优化并精简字节码。
想要继续深入了解 HikariCP 原理的小伙伴,可以看看下面这两篇文章:
HikariCP 是性能超强,在监控方面的话,数据库连接池 Druid 做的不错。
池化技术注意事项
池子的最大值和最小值的设置很重要,初期可以依据经验来设置,后面还是需要根据实际运行情况做调整。
池子中的对象需要在使用之前预先初始化完成,这叫做池子的预热,比方说使用线程池时就需要预先初始化所有的核心线程。如果池子未经过预热可能会导致系统重启后产生比较多的慢请求。
参考
高性能:零拷贝为什么能提升性能?
相关面试题 :
- 简单描述一下传统的 IO 执行流程,有什么缺陷?
- 什么是零拷贝?
- 零拷贝实现的几种方式
- Java 提供的零拷贝方式
作者:程序员田螺 ,公众号:捡田螺的小男孩
《Java 面试指北》已获授权并对其内容进行了完善。
零拷贝算是一个老生常谈的问题啦,很多顶级框架都用到了零拷贝来提升性能,比如我们经常接触到的 Kafka 、RocketMQ、Netty 。
搞懂零拷贝不仅仅可以让自己对这些框架的认识更进一步,还可以让自己在面试中更游刃有余。毕竟,面试中对于零拷贝的考察非常常见,尤其是大厂。
通常情况下,面试官不会直接提问零拷贝,他会先问你 Kafka/RocketMQ/Netty 为什么快,然后你回答到了零拷贝之后,他再去挖掘你对零拷贝的认识。
1.什么是零拷贝
零拷贝字面上的意思包括两个,“零”和“拷贝”:
“拷贝” :就是指数据从一个存储区域转移到另一个存储区域。
“零” :表示次数为 0,它表示拷贝数据的次数为 0。
合起来,那 零拷贝 就是不需要将数据从一个存储区域复制到另一个存储区域。
零拷贝是指计算机执行 IO 操作时,CPU 不需要将数据从一个存储区域复制到另一个存储区域,从而可以减少上下文切换以及 CPU 的拷贝时间。它是一种I/O操作优化技术。
2. 传统 IO 的执行流程
做服务端开发的小伙伴,文件下载功能应该实现过不少了吧。如果你实现的是一个 Web 程序,前端请求过来,服务端的任务就是:将服务端主机磁盘中的文件从已连接的 socket 发出去。关键实现代码如下:
1 | while((n = read(diskfd, buf, BUF_SIZE)) > 0) |
传统的 IO 流程,包括 read 和 write 的过程。
read
:把数据从磁盘读取到内核缓冲区,再拷贝到用户缓冲区。write
:先把数据写入到 socket 缓冲区,最后写入网卡设备。
流程图如下:
- 用户应用进程调用 read 函数,向操作系统发起 IO 调用,上下文从用户态转为内核态(切换 1)
- DMA 控制器把数据从磁盘中,读取到内核缓冲区。
- CPU 把内核缓冲区数据,拷贝到用户应用缓冲区,上下文从内核态转为用户态(切换 2),read 函数返回
- 用户应用进程通过 write 函数,发起 IO 调用,上下文从用户态转为内核态(切换 3)
- CPU 将应用缓冲区中的数据,拷贝到 socket 缓冲区
- DMA 控制器把数据从 socket 缓冲区,拷贝到网卡设备,上下文从内核态切换回用户态(切换 4),write 函数返回
从流程图可以看出,传统 IO 的读写流程,包括了 4 次上下文切换(4 次用户态和内核态的切换),4 次数据拷贝(两次 CPU 拷贝以及两次的 DMA 拷贝),什么是 DMA 拷贝呢?我们一起来回顾下,零拷贝涉及的操作系统知识点哈。
3. 零拷贝相关的知识点回顾
3.1 内核空间和用户空间
我们电脑上跑着的应用程序,其实是需要经过操作系统,才能做一些特殊操作,如磁盘文件读写、内存的读写等等。因为这些都是比较危险的操作,不可以由应用程序乱来,只能交给底层操作系统来。
因此,操作系统为每个进程都分配了内存空间,一部分是用户空间,一部分是内核空间。内核空间是操作系统内核访问的区域,是受保护的内存空间,而用户空间是用户应用程序访问的内存区域。 以 32 位操作系统为例,它会为每一个进程都分配了4G(2 的 32 次方)的内存空间。
内核空间 :主要提供进程调度、内存分配、连接硬件资源等功能
用户空间 :提供给各个程序进程的空间,它不具有访问内核空间资源的权限,如果应用程序需要使用到内核空间的资源,则需要通过系统调用来完成。进程从用户空间切换到内核空间,完成相关操作后,再从内核空间切换回用户空间。
3.2 什么是用户态、内核态
如果进程运行于内核空间,被称为进程的内核态
如果进程运行于用户空间,被称为进程的用户态。
3.3 什么是上下文切换
什么是上下文?
它是指,先把前一个任务的 CPU 上下文(也就是 CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。
一般我们说的上下文切换,就是指内核(操作系统的核心)在 CPU 上对进程或者线程进行切换。进程从用户态到内核态的转变,需要通过系统调用来完成。系统调用的过程,会发生CPU 上下文的切换。
CPU 寄存器里原来用户态的指令位置,需要先保存起来。接着,为了执行内核态代码,CPU 寄存器需要更新为内核态指令的新位置。最后才是跳转到内核态运行内核任务。
3.4 虚拟内存
现代操作系统使用虚拟内存,即虚拟地址取代物理地址,使用虚拟内存可以有 2 个好处:
- 虚拟内存空间可以远远大于物理内存空间
- 多个虚拟内存可以指向同一个物理地址
正是多个虚拟内存可以指向同一个物理地址,可以把内核空间和用户空间的虚拟地址映射到同一个物理地址,这样的话,就可以减少 IO 的数据拷贝次数啦,示意图如下
3.5 DMA 技术
DMA,英文全称是 Direct Memory Access,即直接内存访问。DMA本质上是一块主板上独立的芯片,允许外设设备和内存存储器之间直接进行 IO 数据传输,其过程不需要 CPU 的参与。
我们一起来看下 IO 流程,DMA 帮忙做了什么事情.
- 用户应用进程调用 read 函数,向操作系统发起 IO 调用,进入阻塞状态,等待数据返回。
- CPU 收到指令后,对 DMA 控制器发起指令调度。
- DMA 收到 IO 请求后,将请求发送给磁盘;
- 磁盘将数据放入磁盘控制缓冲区,并通知 DMA
- DMA 将数据从磁盘控制器缓冲区拷贝到内核缓冲区。
- DMA 向 CPU 发出数据读完的信号,把工作交换给 CPU,由 CPU 负责将数据从内核缓冲区拷贝到用户缓冲区。
- 用户应用进程由内核态切换回用户态,解除阻塞状态
可以发现,DMA 做的事情很清晰啦,它主要就是帮忙 CPU 转发一下 IO 请求,以及拷贝数据。为什么需要它的?
主要就是效率,它帮忙 CPU 做事情,这时候,CPU 就可以闲下来去做别的事情,提高了 CPU 的利用效率。大白话解释就是,CPU 老哥太忙太累啦,所以他找了个小弟(名叫 DMA) ,替他完成一部分的拷贝工作,这样 CPU 老哥就能着手去做其他事情。
4. 零拷贝实现的几种方式
零拷贝并不是没有拷贝数据,而是减少用户态/内核态的切换次数以及 CPU 拷贝的次数。零拷贝实现有多种方式,分别是
mmap+write
sendfile
带有 DMA 收集拷贝功能的 sendfile
4.1 mmap+write 实现的零拷贝
mmap 的函数原型如下:
1 | void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); |
addr
:指定映射的虚拟内存地址length
:映射的长度prot
:映射内存的保护模式flags
:指定映射的类型fd
: 进行映射的文件句柄offset
: 文件偏移量
前面一小节,零拷贝相关的知识点回顾,我们介绍了虚拟内存,可以把内核空间和用户空间的虚拟地址映射到同一个物理地址,从而减少数据拷贝次数!mmap 就是用了虚拟内存这个特点,它将内核中的读缓冲区与用户空间的缓冲区进行映射,所有的 IO 都在内核中完成。
mmap+write
实现的零拷贝流程如下:
- 用户进程通过mmap方法向操作系统内核发起 IO 调用,上下文从用户态切换为内核态。
- CPU 利用 DMA 控制器,把数据从硬盘中拷贝到内核缓冲区。
- 上下文从内核态切换回用户态,mmap 方法返回。
- 用户进程通过write方法向操作系统内核发起 IO 调用,上下文从用户态切换为内核态。
- CPU 将内核缓冲区的数据拷贝到的 socket 缓冲区。
- CPU 利用 DMA 控制器,把数据从 socket 缓冲区拷贝到网卡,上下文从内核态切换回用户态,write 调用返回。
可以发现,mmap+write实现的零拷贝,I/O 发生了4次用户空间与内核空间的上下文切换,以及 3 次数据拷贝。其中 3 次数据拷贝中,包括了2 次 DMA 拷贝和 1 次 CPU 拷贝。
mmap是将读缓冲区的地址和用户缓冲区的地址进行映射,内核缓冲区和应用缓冲区共享,所以节省了一次 CPU 拷贝‘’并且用户进程内存是虚拟的,只是映射到内核的读缓冲区,可以节省一半的内存空间。
4.2 sendfile 实现的零拷贝
sendfile是 Linux2.1 内核版本后引入的一个系统调用函数,API 如下:
1 | ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count); |
out_fd
:为待写入内容的文件描述符,一个 socket 描述符。,in_fd
:为待读出内容的文件描述符,必须是真实的文件,不能是 socket 和管道。offset
:指定从读入文件的哪个位置开始读,如果为 NULL,表示文件的默认起始位置。count
:指定在 fdout 和 fdin 之间传输的字节数。
sendfile
表示在两个文件描述符之间传输数据,它是在操作系统内核中操作的,避免了数据从内核缓冲区和用户缓冲区之间的拷贝操作,因此可以使用它来实现零拷贝。
sendfile
实现的零拷贝流程如下:
- 用户进程发起 sendfile 系统调用,上下文(切换 1)从用户态转向内核态
- DMA 控制器,把数据从硬盘中拷贝到内核缓冲区。
- CPU 将读缓冲区中数据拷贝到 socket 缓冲区
- DMA 控制器,异步把数据从 socket 缓冲区拷贝到网卡,
- 上下文(切换 2)从内核态切换回用户态,sendfile 调用返回。
可以发现,sendfile实现的零拷贝,I/O 发生了2次用户空间与内核空间的上下文切换,以及 3 次数据拷贝。其中 3 次数据拷贝中,包括了2 次 DMA 拷贝和 1 次 CPU 拷贝。那能不能把 CPU 拷贝的次数减少到 0 次呢?有的,即带有DMA收集拷贝功能的sendfile!
4.3 sendfile+DMA scatter/gather 实现的零拷贝
linux 2.4 版本之后,对sendfile做了优化升级,引入 SG-DMA 技术,其实就是对 DMA 拷贝加入了scatter/gather操作,它可以直接从内核空间缓冲区中将数据读取到网卡。使用这个特点搞零拷贝,即还可以多省去一次 CPU 拷贝。
sendfile+DMA scatter/gather 实现的零拷贝流程如下:
- 用户进程发起 sendfile 系统调用,上下文(切换 1)从用户态转向内核态
- DMA 控制器,把数据从硬盘中拷贝到内核缓冲区。
- CPU 把内核缓冲区中的文件描述符信息(包括内核缓冲区的内存地址和偏移量)发送到 socket 缓冲区
- DMA 控制器根据文件描述符信息,直接把数据从内核缓冲区拷贝到网卡
- 上下文(切换 2)从内核态切换回用户态,sendfile 调用返回。
可以发现,sendfile+DMA scatter/gather实现的零拷贝,I/O 发生了2次用户空间与内核空间的上下文切换,以及 2 次数据拷贝。其中 2 次数据拷贝都是包DMA 拷贝。这就是真正的 零拷贝(Zero-copy) 技术,全程都没有通过 CPU 来搬运数据,所有的数据都是通过 DMA 来进行传输的。
5. java 提供的零拷贝方式
Java NIO 对 mmap 的支持
Java NIO 对 sendfile 的支持
5.1 Java NIO 对 mmap 的支持
Java NIO 有一个MappedByteBuffer的类,可以用来实现内存映射。它的底层是调用了 Linux 内核的mmap的 API。
mmap 的小 demo如下:
1 | public class MmapTest { |
5.2 Java NIO 对 sendfile 的支持
FileChannel 的transferTo()/transferFrom()
,底层就是 sendfile() 系统调用函数。Kafka 这个开源项目就用到它,平时面试的时候,回答面试官为什么这么快,就可以提到零拷贝sendfile
这个点。
1 |
|
sendfile 的小 demo如下:
1 | public class SendFileTest { |
参考与感谢
高性能:有哪些常见的 SQL 优化手段?
避免使用 SELECT *
SELECT * 会消耗更多的 CPU。
SELECT * 无用字段增加网络带宽资源消耗,增加数据传输时间,尤其是大字段(如 varchar、blob、text)。
SELECT * 无法使用 MySQL 优化器覆盖索引的优化(基于 MySQL 优化器的“覆盖索引”策略又是速度极快,效率极高,业界极为推荐的查询优化方式)
SELECT <字段列表> 可减少表结构变更带来的影响。
分页优化
普通的分页在数据量小的时候耗费时间还是比较短的。
1 | SELECT `score`,`name` FROM `cus_order` ORDER BY `score` DESC LIMIT 10000, 10; |
如果数据量变大,达到百万甚至是千万级别,普通的分页耗费的时间就非常长了。
1 | SELECT `score`,`name` FROM `cus_order` ORDER BY `score` DESC LIMIT 1000000, 10 |
如何优化呢? 可以将上述 SQL 语句修改为子查询。
1 | SELECT `score`,`name` FROM `cus_order` WHERE id >= (SELECT id FROM `cus_order` LIMIT 1000000, 1) LIMIT 10 |
我们先查询出 limit 第一个参数对应的主键值,再根据这个主键值再去过滤并 limit,这样效率会更快。
阿里巴巴《Java 开发手册》中也有对应的描述:
利用延迟关联或者子查询优化超多分页场景。
不过,子查询的结果会产生一张新表,会影响性能,应该尽量避免大量使用子查询。
除了子查询之外,还以采用延迟查询的方式来优化。
1 | SELECT `score`,`name` FROM `cus_order` a, (SELECT id from `cus_order` ORDER BY `score` DESC LIMIT 1000000, 10) b where a.id = b.id |
我们先提取对应的主键,再将这个主键表与原数据表关联。
相关阅读:
尽量避免多表做 join
阿里巴巴《Java 开发手册》中有这样一段描述:
【强制】超过三个表禁止 join。需要 join 的字段,数据类型保持绝对一致;多表关联查询时,保证被关联 的字段需要有索引。
join 的效率比较低,主要原因是因为其使用嵌套循环(Nested Loop)来实现关联查询,三种不同的实现效率都不是很高:
- Simple Nested-Loop Join :没有进过优化,直接使用笛卡尔积实现 join,逐行遍历/全表扫描,效率最低。
- Block Nested-Loop Join :利用 JOIN BUFFER 进行优化,性能受到 JOIN BUFFER 大小的影响,相比于 Simple Nested-Loop Join 性能有所提升。不过,如果两个表的数据过大的话,无论如何优化,Block Nested-Loop Join 对性能的提升都非常有限。
- Index Nested-Loop Join :在必要的字段上增加索引,使 join 的过程中可以使用到这个索引,这样可以让 Block Nested-Loop Join 转换为 Index Nested-Loop Join,性能得到进一步提升。
实际业务场景避免多表 join 常见的做法有两种:
- 单表查询后在内存中自己做关联 :对数据库做单表查询,再根据查询结果进行二次查询,以此类推,最后再进行关联。
- 数据冗余,把一些重要的数据在表中做冗余,尽可能地避免关联查询。很笨的一张做法,表结构比较稳定的情况下才会考虑这种做法。进行冗余设计之前,思考一下自己的表结构设计的是否有问题。
更加推荐第一种,这种在实际项目中的使用率比较高,除了性能不错之外,还有如下优势:
- 拆分后的单表查询代码可复用性更高 :join 联表 SQL 基本不太可能被复用。
- 单表查询更利于后续的维护 :不论是后续修改表结构还是进行分库分表,单表查询维护起来都更容易。
不过,如果系统要求的并发量不大的话,我觉得多表 join 也是没问题的。很多公司内部复杂的系统,要求的并发量不高,很多数据必须 join 5 张以上的表才能查出来。
知乎上也有关于这个问题的讨论:MySQL 多表关联查询效率高点还是多次单表查询效率高,为什么?,感兴趣的可以看看。
建议不要使用外键与级联
阿里巴巴《Java 开发手册》中有这样一段描述:
不得使用外键与级联,一切外键概念必须在应用层解决。
网络上已经有非常多分析外键与级联缺陷的文章了,个人认为不建议使用外键主要是因为对分库分表不友好,性能方面的影响其实是比较小的。
选择合适的字段类型
存储字节越小,占用也就空间越小,性能也越好。
a.某些字符串可以转换成数字类型存储比如可以将 IP 地址转换成整形数据。
数字是连续的,性能更好,占用空间也更小。
MySQL 提供了两个方法来处理 ip 地址
- INET_ATON() : 把 ip 转为无符号整型 (4-8 位)
- INET_NTOA() :把整型的 ip 转为地址
插入数据前,先用 INET_ATON() 把 ip 地址转为整型,显示数据时,使用 INET_NTOA() 把整型的 ip 地址转为地址显示即可。
b.对于非负型的数据 (如自增 ID,整型 IP,年龄) 来说,要优先使用无符号整型来存储。
无符号相对于有符号可以多出一倍的存储空间
1 | SIGNED INT -2147483648~2147483647 |
c.小数值类型(比如年龄、状态表示如 0/1)优先使用 TINYINT 类型。
d.对于日期类型来说, DateTime 类型耗费空间更大且没有时区信息,建议使用 Timestamp。
e.金额字段用 decimal,避免精度丢失。
f.尽量使用自增 id 作为主键。
如果主键为自增 id 的话,每次都会将数据加在 B+树尾部(本质是双向链表),时间复杂度为 O(1)。在写满一个数据页的时候,直接申请另一个新数据页接着写就可以了。
如果主键是非自增 id 的话,为了让新加入数据后 B+树的叶子节点还能保持有序,它就需要往叶子结点的中间找,查找过程的时间复杂度是 O(lgn)。如果这个也被写满的话,就需要进行页分裂。页分裂操作需要加悲观锁,想能非常低。
不过, 像分库分表这类场景就不建议使用自增 id 作为主键,应该使用分布式 ID 比如 uuid 。
相关阅读:数据库主键一定要自增吗?有哪些场景不建议自增?。
尽量用 UNION ALL 代替 UNION
UNION 会把两个结果集的所有数据放到临时表中后再进行去重操作,更耗时,更消耗 CPU 资源。
UNION ALL 不会再对结果集进行去重操作,获取到的数据包含重复的项。
不过,如果实际业务场景中不允许产生重复数据的话,还是可以使用 UNION。
批量操作
对于数据库中的数据更新,如果能使用批量操作就要尽量使用,减少请求数据库的次数,提高性能。
1 | # 反例 |
Show Profile 分析 SQL 执行性能
为了更精准定位一条 SQL 语句的性能问题,需要清楚地知道这条 SQL 语句运行时消耗了多少系统资源。 SHOW PROFILE 和 SHOW PROFILES 展示 SQL 语句的资源使用情况,展示的消息包括 CPU 的使用,CPU 上下文切换,IO 等待,内存使用等。
MySQL 在 5.0.37 版本之后才支持 Profiling,select @@have_profiling
命令返回 YES
表示该功能可以使用。
1 | mysql> SELECT @@have_profiling; |
注意 :
SHOW PROFILE
和SHOW PROFILES
已经被弃用,未来的 MySQL 版本中可能会被删除,取而代之的是使用 Performance Schema。在该功能被删除之前,我们简单介绍一下其基本使用方法。
想要使用 Profiling
,请确保你的 profiling
是开启(on)的状态。
你可以通过SHOW VARIABLES
命令查看其状态:
也可以通过 SELECT @@profiling
命令进行查看:
1 | mysql> SELECT @@profiling; |
默认情况下,Profiling
是关闭(off)的状态,你直接通过SET @@profiling=1
命令即可开启。
开启成功之后,我们执行几条 SQL 语句。执行完成之后,使用 SHOW PROFILES 可以展示当前 Session 下所有 SQL 语句的简要的信息包括 Query_ID(SQL 语句的 ID 编号) 和 Duration(耗时)。
具体能收集多少个 SQL,由参数profiling_history_size
决定,默认值为 15,最大值为 100。如果设置为 0,等同于关闭 Profiling。
如果想要展示一个 SQL 语句的执行耗时细节,可以使用SHOW PROFILE
命令。
SHOW PROFILE
命令的具体用法如下:
1 | SHOW PROFILE [type [, type] ... ] |
在执行SHOW PROFILE
命令时,可以加上类型子句,比如 CPU、IPC、MEMORY 等,查看具体某类资源的消耗情况:
1 | SHOW PROFILE CPU,IPC FOR QUERY 8; |
如果不加 FOR QUERY {n}
子句,默认展示最新的一次 SQL 的执行情况,加了 FOR QUERY {n}
,表示展示 Query_ID 为 n 的 SQL 的执行情况。
优化慢 SQL
为了优化慢 SQL ,我们首先要找到哪些 SQL 语句执行速度比较慢。
MySQL 慢查询日志是用来记录 MySQL 在执行命令中,响应时间超过预设阈值的 SQL 语句。因此,通过分析慢查询日志我们就可以找出执行速度比较慢的 SQL 语句。
出于性能层面的考虑,慢查询日志功能默认是关闭的,你可以通过以下命令开启:
1 | # 开启慢查询日志功能 |
设置成功之后,使用show variables like 'slow%';
命令进行查看。
1 | | Variable_name | Value | |
我们故意在百万数据量的表(未使用索引)中执行一条排序的语句:
1 | SELECT `score`,`name` FROM `cus_order` ORDER BY `score` DESC; |
确保自己有对应目录的访问权限:
1 | chmod 755 /var/lib/mysql/ |
查看对应的慢查询日志:
1 | cat /var/lib/mysql/ranking-list-slow.log |
我们刚刚故意执行的 SQL 语句已经被慢查询日志记录了下来:
1 | # Time: 2022-10-09T08:55:37.486797Z |
这里对日志中的一些信息进行说明:
- Time :被日志记录的代码在服务器上的运行时间。
- User@Host:谁执行的这段代码。
- Query_time:这段代码运行时长。
- Lock_time:执行这段代码时,锁定了多久。
- Rows_sent:慢查询返回的记录。
- Rows_examined:慢查询扫描过的行数。
实际项目中,慢查询日志通常会比较复杂,我们需要借助一些工具对其进行分析。像 MySQL 内置的 mysqldumpslow 工具就可以把相同的 SQL 归为一类,并统计出归类项的执行次数和每次执行的耗时等一系列对应的情况。
找到了慢 SQL 之后,我们可以通过 EXPLAIN
命令分析对应的 SELECT
语句:
1 | mysql> EXPLAIN SELECT `score`,`name` FROM `cus_order` ORDER BY `score` DESC; |
比较重要的字段说明:
- select_type :查询的类型,常用的取值有 SIMPLE(普通查询,即没有联合查询、子查询)、PRIMARY(主查询)、UNION(UNION 中后面的查询)、SUBQUERY(子查询)等。
- table :表示查询涉及的表或衍生表。
- type :执行方式,判断查询是否高效的重要参考指标,结果值从差到好依次是:ALL < index < range ~ index_merge < ref < eq_ref < const < system。
- rows : SQL 要查找到结果集需要扫描读取的数据行数,原则上 rows 越少越好。
- ……
关于 Explain 的详细介绍,请看这篇文章:MySQL 性能优化神器 Explain 使用分析 - 永顺。
正确使用索引
正确使用索引可以大大加快数据的检索速度(大大减少检索的数据量)。
选择合适的字段创建索引
不为 NULL 的字段 :索引字段的数据应该尽量不为 NULL,因为对于数据为 NULL 的字段,数据库较难优化。如果字段频繁被查询,但又避免不了为 NULL,建议使用 0,1,true,false 这样语义较为清晰的短值或短字符作为替代。
被频繁查询的字段 :我们创建索引的字段应该是查询操作非常频繁的字段。
被作为条件查询的字段 :被作为 WHERE 条件查询的字段,应该被考虑建立索引。
频繁需要排序的字段 :索引已经排序,这样查询可以利用索引的排序,加快排序查询时间。
被经常频繁用于连接的字段 :经常用于连接的字段可能是一些外键列,对于外键列并不一定要建立外键,只是说该列涉及到表与表的关系。对于频繁被连接查询的字段,可以考虑建立索引,提高多表连接查询的效率。
被频繁更新的字段应该慎重建立索引
虽然索引能带来查询上的效率,但是维护索引的成本也是不小的。 如果一个字段不被经常查询,反而被经常修改,那么就更不应该在这种字段上建立索引了。
尽可能的考虑建立联合索引而不是单列索引
因为索引是需要占用磁盘空间的,可以简单理解为每个索引都对应着一颗 B+树。如果一个表的字段过多,索引过多,那么当这个表的数据达到一个体量后,索引占用的空间也是很多的,且修改索引时,耗费的时间也是较多的。如果是联合索引,多个字段在一个索引上,那么将会节约很大磁盘空间,且修改数据的操作效率也会提升。
注意避免冗余索引
冗余索引指的是索引的功能相同,能够命中索引(a, b)就肯定能命中索引(a) ,那么索引(a)就是冗余索引。如(name,city )和(name )这两个索引就是冗余索引,能够命中前者的查询肯定是能够命中后者的 在大多数情况下,都应该尽量扩展已有的索引而不是创建新索引。
考虑在字符串类型的字段上使用前缀索引代替普通索引
前缀索引仅限于字符串类型,较普通索引会占用更小的空间,所以可以考虑使用前缀索引带替普通索引。
避免索引失效
索引失效也是慢查询的主要原因之一,常见的导致索引失效的情况有下面这些:
使用 SELECT * 进行查询;
创建了组合索引,但查询条件未准守最左匹配原则;
在索引列上进行计算、函数、类型转换等操作;
% 开头的 LIKE 查询比如 like ‘%abc’;;
查询条件中使用 or,且 or 的前后条件中有一个列没有索引,涉及的索引都不会被使用到;
发生隐式转换;
……
删除长期未使用的索引
删除长期未使用的索引,不用的索引的存在会造成不必要的性能损耗 MySQL 5.7 可以通过查询 sys 库的 schema_unused_indexes 视图来查询哪些索引从未被使用
参考
MySQL 8.2 Optimizing SQL Statements:https://dev.mysql.com/doc/refman/8.0/en/statement-optimization.html
为什么阿里巴巴禁止数据库中做多表 join - Hollis:https://mp.weixin.qq.com/s/GSGVFkDLz1hZ1OjGndUjZg
MySQL 的 COUNT 语句,竟然都能被面试官虐的这么惨 - Hollis:https://mp.weixin.qq.com/s/IOHvtel2KLNi-Ol4UBivbQ
MySQL 性能优化神器 Explain 使用分析:https://segmentfault.com/a/1190000008131735
如何使用 MySQL 慢查询日志进行性能优化 :https://kalacloud.com/blog/how-to-use-mysql-slow-query-log-profiling-mysqldumpslow/
高可用:降级和熔断有什么区别?
什么是降级?
降级是从系统功能优先级的角度考虑如何应对系统故障。
服务降级指的是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行。
降级服务的特征如下 :
原因:整体负荷超出整体负载承受能力。
目的:保证重要或基本服务正常运行,非重要服务延迟使用或暂停使用
大小:降低服务粒度,要考虑整体模块粒度的大小,将粒度控制在合适的范围内
可控性:在服务粒度大小的基础上增加服务的可控性,后台服务开关的功能是一项必要配置(单机可配置文件,其他可领用数据库和缓存),可分为手动控制和自动控制。
次序:一般从外围延伸服务开始降级,需要有一定的配置项,重要性低的优先降级,比如可以分组设置等级 1-10,当服务需要降级到某一个级别时,进行相关配置
降级方式有哪些?
延迟服务:比如发表了评论,重要服务,比如在文章中显示正常,但是延迟给用户增加积分,只是放到一个缓存中,等服务平稳之后再执行。
在粒度范围内关闭服务(片段降级或服务功能降级):比如关闭相关文章的推荐,直接关闭推荐区
页面异步请求降级:比如商品详情页上有推荐信息/配送至等异步加载的请求,如果这些信息响应慢或者后端服务有问题,可以进行降级;
页面跳转(页面降级):比如可以有相关文章推荐,但是更多的页面则直接跳转到某一个地址
写降级:比如秒杀抢购,我们可以只进行 Cache 的更新,然后异步同步扣减库存到 DB,保证最终一致性即可,此时可以将 DB 降级为 Cache。
读降级:比如多级缓存模式,如果后端服务有问题,可以降级为只读缓存,这种方式适用于对读一致性要求不高的场景。
服务降级有哪些分类?
降级按照是否自动化可分为:
自动开关降级(超时、失败次数、故障、限流)
人工开关降级(秒杀、电商大促等)
自动降级分类又分为 :
超时降级:主要配置好超时时间和超时重试次数和机制,并使用异步机制探测回复情况
失败次数降级:主要是一些不稳定的 api,当失败调用次数达到一定阀值自动降级,同样要使用异步机制探测回复情况
故障降级:比如要调用的远程服务挂掉了(网络故障、DNS 故障、http 服务返回错误的状态码、rpc 服务抛出异常),则可以直接降级。降级后的处理方案有:默认值(比如库存服务挂了,返回默认现货)、兜底数据(比如广告挂了,返回提前准备好的一些静态页面)、缓存(之前暂存的一些缓存数据)
限流降级:当我们去秒杀或者抢购一些限购商品时,此时可能会因为访问量太大而导致系统崩溃,此时开发者会使用限流来进行限制访问量,当达到限流阀值,后续请求会被降级;降级后的处理方案可以是:排队页面(将用户导流到排队页面等一会重试)、无货(直接告知用户没货了)、错误页(如活动太火爆了,稍后重试)
大规模分布式系统如何降级?
在大规模分布式系统中,经常会有成百上千的服务。在大促前往往会根据业务的重要程度和业务间的关系批量降级。这就需要技术和产品提前对业务和系统进行梳理,根据梳理结果确定哪些服务可以降级,哪些服务不可以降级,降级策略是什么,降级顺序怎么样。大型互联网公司基本都会有自己的降级平台,大部分降级都在平台上操作,比如手动降级开关,批量降级顺序管理,熔断阈值动态设置,限流阈值动态设置等等。
什么是熔断?
熔断是应对微服务雪崩效应的一种链路保护机制,类似股市、保险丝
微服务之间的数据交互是通过远程调用来完成的。服务 A 调用服务 B,服务 B 调用服务 C,某一时间链路上对服务 C 的调用响应时间过长或者服务 C 不可用,随着时间的增长,对服务 C 的调用也越来越多,然后服务 C 崩溃了,但是链路调用还在,对服务 B 的调用也在持续增多,然后服务 B 崩溃,随之 A 也崩溃,导致雪崩效应
服务熔断是应对雪崩效应的一种微服务链路保护机制。例如在高压电路中,如果某个地方的电压过高,熔断器就会熔断,对电路进行保护。同样,在微服务架构中,熔断机制也是起着类似的作用。当调用链路的某个微服务不可用或者响应时间太长时,会进行服务熔断,不再有该节点微服务的调用,快速返回错误的响应信息。当检测到该节点微服务调用响应正常后,恢复调用链路。
服务熔断的作用类似于我们家用的保险丝,当某服务出现不可用或响应超时的情况时,为了防止整个系统出现雪崩,暂时停止对该服务的调用。
降级和熔断有什么区别?
熔断和降级是两个比较容易混淆的概念,两者的含义并不相同。
降级的目的在于应对系统自身的故障,而熔断的目的在于应对当前系统依赖的外部系统或者第三方系统的故障。
有哪些现成解决方案?
Spring Cloud 官方目前推荐的熔断器组件如下:
Hystrix
Resilience4J
Sentinel
Spring Retry
我们单独拎出 Sentinel 和 Hystrix 来说一下(没记错的话,Hystrix 目前已经没有维护了。)。
Hystrix 是 Netflix 开源的熔断降级组件,Sentinel 是阿里中间件团队开源的一款不光具有熔断降级功能,同时还支持系统负载保护的组件。
简单来说,两者都是主要做熔断降级的 ,那么两者到底有啥异同呢?该如何选择呢?
Sentinel 的 wiki 中已经详细描述了其与 Hystrix 的区别,地址:https://github.com/alibaba/Sentinel/wiki/Sentinel-与-Hystrix-的对比。
下面这个详细的表格就来自 Sentinel 的 wiki。
Sentinel | Hystrix | |
---|---|---|
隔离策略 | 信号量隔离 | 线程池隔离/信号量隔离 |
熔断降级策略 | 基于响应时间或失败比率 | 基于失败比率 |
实时指标实现 | 滑动窗口 | 滑动窗口(基于 RxJava) |
规则配置 | 支持多种数据源 | 支持多种数据源 |
扩展性 | 多个扩展点 | 插件的形式 |
基于注解的支持 | 支持 | 支持 |
限流 | 基于 QPS,支持基于调用关系的限流 | 有限的支持 |
流量整形 | 支持慢启动、匀速器模式 | 不支持 |
系统负载保护 | 支持 | 不支持 |
控制台 | 开箱即用,可配置规则、查看秒级监控、机器发现等 | 不完善 |
常见框架的适配 | Servlet、Spring Cloud、Dubbo、gRPC 等 | Servlet、Spring Cloud Netflix |
如果你想了解 Sentinel、Hystrix、resilience4j 三者的对比的话,可以查看 Sentinel 的相关 wiki :https://github.com/alibaba/Sentinel/wiki/Guideline:-从-Hystrix-迁移到-Sentinel#功能对比。
推荐阅读
参考
高可用:灰度发布和回滚有什么用?
这部分内容为可选内容,你也可以选择不进行学习。
相关面试题 :
- 什么是灰度发布?有什么好处?
- 你的项目是如何做灰度发布的?
- 为什么灰度发布又被称为金丝雀发布呢?
- 回滚通常的做法是怎样的呢?
灰度发布与回滚(可选)
线上的系统通常情况下会一直迭代更新下去,这意味着我们需要不断发布新版本来替换老版本。如何保证新版本稳定运行呢? 必要的测试必不可少,但灰度发布与回滚也是两个制胜法宝!
灰度发布
灰度发布介绍
灰度发布(又名金丝雀发布) 是一种平滑发布新版本系统的方式。
我举一个简单的例子,大家一看应该就明白灰度发布的思想了。
假如我们有一个服务器集群,每个用户固定访问服务器集群中的某一台服务器,当我们需要发布新版本或者上新功能的时候,我们可以将服务器集群分成若干部分,每天只发布新版本到一部分服务器,这样的话,就有一部分用户可以使用最新版本。发布之后,我们需要观察新版本的服务器运行是否稳定且没有故障。如果没问题的话,我们第二天继续发布一部分服务器,通常需要持续几天才把整个集群全部发布完毕。期间如果发现有问题的话,只需要回滚已发布的那部分服务器即可。
上面列举的这个例子其实是灰度发布常用的一种方式 - AB 测试。AB 测试的思想就是就是把用户分成两组,一组用户使用 A 方案(新版本),一组用户使用 B 方案(老版本)。
另外,这个例子是通过服务器来区分的用户,比较粗暴,而且在一些情况下无法使用。一般情况下,我们是建议在进行灰度发布之前对系统用户进行筛选,根据用户的相关信息和各项指标(比如活跃度,违规次数)来筛选出一批可以优先使用新版的用户。我们只需要通过一些手段将这些用户的请求定向到新版本服务即可!为了直观对新版本服务的稳定性进行观测,灰度发布的正确完成还需要依赖可靠的 监控系统 。
好了!相信前面的介绍已经让你搞清了灰度发布是个什么东西。下面,我们来简单总结一下灰度发布的思想: 简单来说,灰度发布的思想就是先分配一小部分请求流量到新版本,看看有没有问题,没问题的话,再一点点地增加流量,最终让所有流量都切换到新版本。
为什么灰度发布又被称为金丝雀发布呢?
金丝雀也被称为瓦斯报警鸟,对于有毒气体非常敏感,在 90 年代的时候经常被拿来检测毒气(有点残忍,后来被禁止了)。为了避免金丝雀直接被毒死了,人们想到了一个办法,把金丝雀放在一个可以控制通气口气体流量的笼子,需要金丝雀预警的时候把通气口慢慢打开,如果笼子中的金丝雀被毒气毒晕,关闭通气口然后让往笼子里充氧气抢救一下金丝雀。
金丝雀预警毒气通过控制通气口气体流量来减小潜在的毒气对金丝雀的影响,金丝雀发布通过控制发布的新版本的使用范围来减小潜在的问题对整体服务的影响,两者思想非常类似。
很多程序员有可能也是为了纪念那些因为毒气而牺牲的金丝雀才把这种发布方式冠上了金丝雀的名称。
灰度发布常见方案
这里介绍几种比较常见的方案,对于 Java 后端开发来说,我觉得了解就行了,一般在公司里这种事情一般是由 Devops 团队来做的。
1、基于 Nginx+OpenResty+Redis+Lua 实现流量动态分流来实现灰度发布,新浪的 ABTestingGateway 就是这种基于这种方案的一个开源项目。
2、使用 Jenkins + Nginx 实现灰度发布策,具体做法可以参考:手把手教你搭建一个灰度发布环境 。这种方案的原理和第一种类似,都是通过对 Nginx 文件的修改来实现流量的定向分流。类似地,如果你用到了其他网关比如 Spring Cloud Gateway 的话,思路也是一样的。另外, Spring Cloud Gateway 配合 Spring Cloud LoadBalancer(官方推荐)/Ribbon 也可以实现简单的灰度发布,核心思想也还是自定义负载均衡策略来分流。
3、基于 Apollo 动态更新配置加上其自带的灰度发布策略来实现灰度发布。
这种方法也是通过修改灰度发布配置的方式来实现灰度发布,如果灰度的配置测试没问题的话,再全量发布配置。
具体做法可以参考:
4、通过一些现成的工具来做,比如说 Rainbond(云原生应用管理平台)就自带了灰度发布解决方案并且还支持滚动发布和蓝绿发布。
5、Flagger
这是之前看马若飞老师的《Service Mesh 实战》这门课的时候看到的一个方法。
Flagger 是一种渐进式交付工具,可自动控制 Kubernetes 上应用程序的发布过程。通过指标监控和运行一致性测试,将流量逐渐切换到新版本,降低在生产环境中发布新软件版本导致的风险。
Flagger 可以使用 Service Mesh(App Mesh,Istio,Linkerd)或 Ingress Controller(Contour,Gloo,Nginx)来实现多种部署策略(金丝雀发布,A/B 测试,蓝绿发布)。
回滚机制
光有灰度发布还不够,如果在灰度发布过程中(灰度期)发现了新版本有问题,我们还需要有回滚机制来应对。类似于数据库事务回滚,系统发布回滚就是将新版本回退到老版本。
回滚通常的做法是怎样的呢?
- 提前备份老版本,新版本遇到问题之后,重新部署老版本。
- 同时部署一套新版本,一套旧版本,两者规模相同新版本出问题之后,流量全部走老版本(蓝绿发布)。
正如余春龙老师在《软件架构设计:大型网站技术架构与业务架构融合之道》这本书中写道:
既然无法避免系统变更,我们能做的就是让这个过程尽可能平滑、受控,这就是灰度与回滚策略。
不过, 灰度发布和回滚也不是银弹,毕竟计算机世界压根不存在银弹。
在一些要求非常严格的系统(如交易系统、消防系统、医疗系统)中,灰度发布和回滚使用不当就会带来非常严重的生产问题。
参考
文章推荐
服务器
Tomcat 常见面试题总结
本文内容主要整理自:
- 《深入拆解 Tomcat & Jetty》
- 《Tomcat 架构解析》
感谢这两份资料,尤其是《深入拆解 Tomcat & Jetty》,写的非常赞,看了之后收货颇多。
虽然这篇文章的内容大部分都不是我的原创,但整理重要的知识点和面试题同样花了不少心思,希望对你有帮助!
Tomcat 介绍
什么是 Web 容器?
早期的 Web 应用主要用于浏览新闻等静态页面,HTTP 服务器(比如 Apache、Nginx)向浏览器返回静态 HTML,浏览器负责解析 HTML,将结果呈现给用户。
随着互联网的发展,我们已经不满足于仅仅浏览静态页面,还希望通过一些交互操作,来获取动态结果,因此也就需要一些扩展机制能够让 HTTP 服务器调用服务端程序。
于是 Sun 公司推出了 Servlet 技术。你可以把 Servlet 简单理解为运行在服务端的 Java 小程序,但是 Servlet 没有 main 方法,不能独立运行,因此必须把它部署到 Servlet 容器中,由容器来实例化并调用 Servlet。
Tomcat 就是 一个 Servlet 容器。为了方便使用,Tomcat 同时具有 HTTP 服务器的功能。
因此 Tomcat 就是一个“HTTP 服务器 + Servlet 容器”,我们也叫它 Web 容器。
什么是 Tomcat?
简单来说,Tomcat 就是一个“HTTP 服务器 + Servlet 容器”,我们通常也称呼 Tomcat 为 Web 容器。
HTTP 服务器 :处理 HTTP 请求并响应结果。
Servlet 容器 :HTTP 服务器将请求交给 Servlet 容器处理,Servlet 容器会将请求转发到具体的 Servlet(Servlet 容器用来加载和管理业务类)。
HTTP 服务器工作原理了解吗?
- 用户通过浏览器进行了一个操作,比如输入网址并回车,或者是点击链接,接着浏览器获取了这个事件。
- 浏览器向服务端发出 TCP 连接请求。
- 服务程序接受浏览器的连接请求,并经过 TCP 三次握手建立连接。
- 浏览器将请求数据打包成一个 HTTP 协议格式的数据包。
- 浏览器将该数据包推入网络,数据包经过网络传输,最终达到端服务程序。
- 服务端程序拿到这个数据包后,同样以 HTTP 协议格式解包,获取到客户端的意图。
- 得知客户端意图后进行处理,比如提供静态文件或者调用服务端程序获得动态结果。
- 服务器将响应结果(可能是 HTML 或者图片等)按照 HTTP 协议格式打包。
- 服务器将响应数据包推入网络,数据包经过网络传输最终达到到浏览器。
- 浏览器拿到数据包后,以 HTTP 协议的格式解包,然后解析数据,假设这里的数据是 HTML。
- 浏览器将 HTML 文件展示在页面上。
什么是 Servlet?有什么作用?
Servlet 指的是任何实现了 Servlet
接口的类。Servlet 主要用于处理客户端传来的 HTTP 请求,并返回一个响应。
Servlet 接口定义了下面五个方法:
1 | public interface Servlet { |
其中最重要是的service
方法,具体业务类在这个方法里实现业务的具体处理逻辑。
Servlet 容器会根据 web.xml
文件中的映射关系,调用相应的 Servlet,Servlet 将处理的结果返回给 Servlet 容器,并通过 HTTP 服务器将响应传输给客户端。
几乎所有的 Java Web 框架(比如 Spring)都是基于 Servlet 的封装。
Tomcat 是如何创建 Servlet 的?
当容器启动时,会读取在 webapps 目录下所有的 web 应用中的 web.xml 文件,然后对 xml 文件进行解析,并读取 servlet 注册信息。然后,将每个应用中注册的 Servlet 类都进行加载,并通过 反射的方式实例化。(有时候也是在第一次请求时实例化)。
<load-on-startup>
元素是 <servlet>
元素的一个子元素,它用于指定 Servlet 被加载的时机和顺序。在 <load-on-startup>
元素中,设置的值必须是一个整数。如果这个值是一个负数,或者没有设定这个元素,Servlet 容器将在客户端首次请求这个 Servlet 时加载它;如果这个值是正整数或 0,Servlet 容器将在 Web 应用启动时加载并初始化 Servlet,并且 <load-on-startup>
的值越小,它对应的 Servlet 就越先被加载。
具体配置方式如下所示:
1 | <servlet> |
Tomcat 文件夹
- /bin:存放 Windows 或 Linux 平台上启动和关闭 Tomcat 的脚本文件。
- /conf:存放 Tomcat 的各种全局配置文件,其中最重要的是 server.xml。
- /lib:存放 Tomcat 以及所有 Web 应用都可以访问的 JAR 文件。
- /logs:存放 Tomcat 执行时产生的日志文件。
- /work:存放 JSP 编译后产生的 Class 文件。
- /webapps:Tomcat 的 Web 应用目录,默认情况下把 Web 应用放在这个目录下。
bin 目录有什么作用?
1 | $ ls tomcat/bin |
bin 目录保存了对 Tomcat 进行控制的相关可执行程序。
上面的文件中,主要分为两类:.bat 和 .sh。.bat 是 window 平台的批处理文件,用于在 window 中执行。而 .sh 则是在 Linux 或者 Unix 上执行的。
比较常用的是下面两个:
- startup.sh(startup.bat)用来启动 Tomcat 服务器。
- shutdown.sh(shutdown.bat)用来关闭已经运行的 Tomcat 服务器。
webapps 目录有什么作用?
webapps 目录用来存放应用程序,当 Tomcat 启动时会去加载 webapps 目录下的应用程序。可以以文件夹、war 包、jar 包的形式发布应用。
当然,你也可以把应用程序放置在磁盘的任意位置,在配置文件中映射好就行。
Tomcat 总体架构
Tomcat 要实现 2 个核心功能:
- 处理 Socket 连接,负责网络字节流与 Request 和 Response 对象的转化。
- 加载和管理 Servlet,以及具体处理 Request 请求。
因此 Tomcat 设计了两个核心组件 连接器(Connector) 和 容器(Container) 来分别做这两件事情。
连接器有什么作用?
连接器对 Servlet 容器屏蔽了协议及 I/O 模型等的区别,无论是 HTTP 还是 AJP,在容器中获取到的都是一个标准的ServletRequest
对象。
我们可以把连接器的功能需求进一步细化,比如:
- 监听网络端口。
- 接受网络连接请求。
- 读取网络请求字节流。
- 根据具体应用层协议(HTTP/AJP)解析字节流,生成统一的 Tomcat Request 对象。
- 将 Tomcat Request 对象转成标准的 ServletRequest。
- 调用 Servlet 容器,得到 ServletResponse。
- 将 ServletResponse 转成 Tomcat Response 对象。
- 将 Tomcat Response 转成网络字节流。
- 将响应字节流写回给浏览器。
通过分析连接器的详细功能列表,我们发现连接器需要完成 3 个高内聚的功能:
- 网络通信。
- 应用层协议解析。
- Tomcat Request/Response 与 ServletRequest/ServletResponse 的转化。
因此 Tomcat 的设计者设计了 3 个组件来实现这 3 个功能,分别是 Endpoint、Processor 和 Adapter (适配器模式)。
Endpoint 负责提供字节流给 Processor,Processor 负责提供 Tomcat Request 对象给 Adapter,Adapter 负责提供 ServletRequest 对象给容器。
连接器用 ProtocolHandler
接口来封装通信协议和 I/O 模型的差异,ProtocolHandler
内部又分为Endpoint
和 Processor
模块,Endpoint
负责底层 Socket
通信,Processor
负责应用层协议解析。连接器通过适配器 Adapter
调用容器。
如果要支持新的 I/O 方案、新的应用层协议,只需要实现相关的具体子类,上层通用的处理逻辑是不变的。
容器是怎么设计的?
Tomcat 设计了 4 种容器,分别是 Engine、Host、Context 和 Wrapper。这 4 种容器不是平行关系,而是父子关系。
- Context 表示一个 Web 应用程序;
- Wrapper 表示一个 Servlet,一个 Web 应用程序中可能会有多个 Servlet;
- Host 代表的是一个虚拟主机,或者说一个站点,可以给 Tomcat 配置多个虚拟主机地址,而一个虚拟主机下可以部署多个 Web 应用程序;
- Engine 表示引擎,用来管理多个虚拟站点,一个 Service 最多只能有一个 Engine。
你可以再通过 Tomcat 的 server.xml
配置文件来加深对 Tomcat 容器的理解。Tomcat 采用了组件化的设计,它的构成组件都是可配置的,其中最外层的是 Server,其他组件按照一定的格式要求配置在这个顶层容器中。
请求是如何定位到 Servlet 的?
Tomcat 是怎么确定请求是由哪个 Wrapper 容器里的 Servlet 来处理的呢?
Mapper`组件的功能就是将用户请求的 URL 定位到一个 Servlet。它的工作原理是:Mapper 组件里保存了 Web 应用的配置信息,其实就是容器组件与访问路径的映射关系,比如 Host 容器里配置的域名、Context 容器里的 Web 应用路径,以及 Wrapper 容器里 Servlet 映射的路径,你可以想象这些配置信息就是一个多层次的 Map。
注意:一个请求 URL 最后只会定位到一个 Wrapper 容器,也就是一个 Servlet。
举个例子:有一个网购系统,有面向网站管理人员的后台管理系统,还有面向终端客户的在线购物系统。这两个系统跑在同一个 Tomcat 上,为了隔离它们的访问域名,配置了两个虚拟域名:manage.shopping.com 和 user.shopping.com 。
假如有用户访问一个 URL,比如图中的http://user.shopping.com:8080/order/buy,Tomcat 如何将这个 URL 定位到一个 Servlet 呢?
- 根据协议和端口号选定 Service 和 Engine : URL 访问的是 8080 端口,因此这个请求会被 HTTP 连接器接收,而一个连接器是属于一个 Service 组件的,这样 Service 组件就确定了
- 根据域名选定 Host : 域名是 user.shopping.com,因此 Mapper 会找到 Host2 这个容器。
- 根据 URL 路径找到 Context 组件 。
- 根据 URL 路径找到 Wrapper(Servlet) : Context 确定后,Mapper 再根据 web.xml 中配置的 Servlet 映射路径来找到具体的 Wrapper 和 Servlet。
Tomcat 为什么要打破双亲委托机制?
Tomcat 自定义类加载器打破双亲委托机制的目的是为了优先加载 Web 应用目录下的类,然后再加载其他目录下的类,这也是 Servlet 规范的推荐做法。
要打破双亲委托机制,需要继承 ClassLoader 抽象类,并且需要重写它的 loadClass 方法,因为 ClassLoader 的默认实现就是双亲委托。
Tomcat 如何隔离 Web 应用?
首先让我们思考这一下这几个问题:
假如我们在 Tomcat 中运行了两个 Web 应用程序,两个 Web 应用中有同名的 Servlet,但是功能不同,Tomcat 需要同时加载和管理这两个同名的 Servlet 类,保证它们不会冲突,因此 Web 应用之间的类需要隔离。
假如两个 Web 应用都依赖同一个第三方的 JAR 包,比如 Spring,那 Spring 的 JAR 包被加载到内存后,Tomcat 要保证这两个 Web 应用能够共享,也就是说 Spring 的 JAR 包只被加载一次,否则随着依赖的第三方 JAR 包增多,JVM 的内存会膨胀。
跟 JVM 一样,我们需要隔离 Tomcat 本身的类和 Web 应用的类。
为了解决上面这些问题,Tomcat 设计了类加载器的层次结构。
我们先来看第 1 个问题: Web 应用之间的类之间如何隔离?
假如我们使用 JVM 默认AppClassLoader
来加载 Web 应用,AppClassLoader
只能加载一个Servlet
类,在加载第二个同名Servlet
类时,AppClassLoader
会返回第一个Servlet
类的 Class
实例,这是因为在 AppClassLoader
看来,同名的 Servlet
类只被加载一次。
Tomcat 的解决方案是自定义一个类加载器 WebAppClassLoader
, 并且给每个 Web 应用创建一个类加载器实例。 我们知道,Context 容器组件对应一个 Web 应用,因此,每个 Context 容器负责创建和维护一个 WebAppClassLoader 加载器实例。这背后的原理是,不同的加载器实例加载的类被认为是不同的类,即使它们的类名相同。这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间,每一个 Web 应用都有自己的类空间,Web 应用之间通过各自的类加载器互相隔离。
我们再来看第 2 个问题: 两个 Web 应用之间怎么共享库类,并且不能重复加载相同的类?
我们知道,在双亲委托机制里,各个子加载器都可以通过父加载器去加载类,那么把需要共享的类放到父加载器的加载路径下不就行了吗,应用程序也正是通过这种方式共享 JRE 的核心类。因此 Tomcat 的设计者又加了一个类加载器 SharedClassLoader,作为 WebAppClassLoader 的父加载器,专门来加载 Web 应用之间共享的类。如果 WebAppClassLoader 自己没有加载到某个类,就会委托父加载器 SharedClassLoader 去加载这个类,SharedClassLoader 会在指定目录下加载共享类,之后返回给 WebAppClassLoader,这样共享的问题就解决了。
我们再来看第 3 个问题:如何隔离 Tomcat 本身的类和 Web 应用的类?
我们知道,要共享可以通过父子关系,要隔离那就需要兄弟关系了。兄弟关系就是指两个类加载器是平行的,它们可能拥有同一个父加载器,但是两个兄弟类加载器加载的类是隔离的。基于此 Tomcat 又设计一个类加载器 CatalinaClassLoader,专门来加载 Tomcat 自身的类。这样设计有个问题,那 Tomcat 和各 Web 应用之间需要共享一些类时该怎么办呢?
老办法,还是再增加一个 CommonClassLoader
,作为CatalinaClassLoader
和 SharedClassLoader
的父加载器。CommonClassLoader
能加载的类都可以被 CatalinaClassLoader
和 SharedClassLoader
使用,而 CatalinaClassLoader
和 SharedClassLoader
能加载的类则与对方相互隔离。WebAppClassLoader
可以使用 SharedClassLoader
加载到的类,但各个 WebAppClassLoader
实例之间相互隔离。
性能优化
如何监控 Tomcat 性能?
Tomcat 的关键的性能指标主要有 吞吐量、响应时间、错误数、线程池、CPU 以及 JVM 内存。
通过 JConsole 监控 Tomcat
命令行查看 Tomcat 指标
prometheus + grafana
JVM GC 原理及调优的基本思路
Tomcat 基于 Java,也是跑在 JVM 中,因此,我们要对 Tomcat 进行调优的话,先要了解 JVM 调优的原理。
JVM 调优主要是对 JVM 垃圾收集的优化。一般来说是因为有问题才需要优化,所以对于 JVM GC 来说,如果你观察到 Tomcat 进程的 CPU 使用率比较高,并且在 GC 日志中发现 GC 次数比较频繁、GC 停顿时间长,这表明你需要对 GC 进行优化了。
在对 GC 调优的过程中,我们不仅需要知道 GC 的原理,更重要的是要熟练使用各种监控和分析工具,具备 GC 调优的实战能力。
CMS 和 G1 是时下使用率比较高的两款垃圾收集器,从 Java 9 开始,采用 G1 作为默认垃圾收集器,而 G1 的目标也是逐步取代 CMS。
如何选择 IO 模型?
I/O 调优实际上是连接器类型的选择,一般情况下默认都是 NIO,在绝大多数情况下都是够用的,除非你的 Web 应用用到了 TLS 加密传输,而且对性能要求极高,这个时候可以考虑 APR,因为 APR 通过 OpenSSL 来处理 TLS 握手和加 / 解密。OpenSSL 本身用 C 语言实现,它还对 TLS 通信做了优化,所以性能比 Java 要高。
那你可能会问那什么时候考虑选择 NIO.2?
如果你的 Tomcat 跑在 Windows 平台上,并且 HTTP 请求的数据量比较大,可以考虑 NIO.2,这是因为 Windows 从操作系统层面实现了真正意义上的异步 I/O,如果传输的数据量比较大,异步 I/O 的效果就能显现出来。
如果你的 Tomcat 跑在 Linux 平台上,建议使用 NIO,这是因为 Linux 内核没有很完善地支持异步 I/O 模型,因此 JVM 并没有采用原生的 Linux 异步 I/O,而是在应用层面通过 epoll 模拟了异步 I/O 模型,只是 Java NIO 的使用者感觉不到而已。因此可以这样理解,在 Linux 平台上,Java NIO 和 Java NIO.2 底层都是通过 epoll 来实现的,但是 Java NIO 更加简单高效。
Nginx 常见面试题总结
什么是 Nginx ?
俄罗斯的工程师 Igor Sysoev,在 Rambler Media 工作期间使用 C 语言开发并开源了 Nginx。
Nginx 同 Apache 一样都是 WEB 服务器,不过,Nginx 更加轻量级,它的内存占用少,启动极快,高并发能力强,在互联网项目中广泛应用。并且,Nginx 可以作为反向代理服务器使用,支持 IMAP/POP3/SMTP 服务。
Web 服务器:负责处理和响应用户请求,一般也称为 HTTP 服务器。
Nginx 的特点是有哪些?
内存占用非常少 :一般情况下,10000 个非活跃的 HTTP Keep-Alive 连接在 Nginx 中仅消耗 2.5MB 的内存,这是 Nginx 支持高并发连接的基础。
高并发 : 单机支持 10 万以上的并发连接
跨平台 :可以运行在 Linux,Windows,FreeBSD,Solaris,AIX,Mac OS 等操作系统上。
扩展性好 :第三方插件非常多!
安装使用简单 :对于简单的应用场景,我们很快就能够上手使用。
稳定性好 :bug 少,不会遇到各种奇葩的问题。
免费 :开源软件,免费使用。
……
Nginx 能用来做什么?
静态资源服务器
Nginx 是一个 HTTP 服务器,可以将服务器上的静态文件(如 HTML、图片)通过 HTTP 协议展现给客户端。因此,我们可以使用 Nginx 搭建静态资源 Web 服务器
不过,记得使用 gzip 压缩静态资源来减少网络传输。
举个例子:我们来使用 Nginx 搭建一个静态网页服务。先将静态网页上传到服务器,然后修改/nginx/conf
目录下的 nginx.conf
文件(Nginx 配置文件)。修改完成之后,重启 Nginx,再请求对应 ip/域名 + 端口 + 资源
地址就可以访问到网页。
1 | server { |
反向代理
客户端将请求发送到反向代理服务器,由反向代理服务器去选择目标服务器,获取数据后再返回给客户端。对外暴露的是反向代理服务器地址,隐藏了真实服务器 IP 地址。反向代理“代理”的是目标服务器,这一个过程对于客户端而言是透明的。
举个例子:公司内网部署了 3 台服务器,客户端请求直接经过代理服务器,由代理服务器将请求转发到内网服务器并最终决定哪一台服务器处理客户端请求。
反向代理隐藏了真实的服务器,为服务器收发请求,使真实服务器对客户端不可见。一般在处理跨域请求的时候比较常用。现在基本上所有的大型网站都设置了反向代理。
Nginx 支持配置反向代理,通过反向代理实现网站的负载均衡。
正向代理
提示 :想要理解正确理解和区分正向代理和反向代理,你要关注的是代理对象,正向代理“代理”的是客户端,反向代理“代理”的是目标服务器。
一位大佬说的一句话挺精辟的:代理其实就是一个中介,A 和 B 本来可以直连,中间插入一个 C,C 就是中介。刚开始的时候,代理多数是帮助内网 client 访问外网 server 用的(比如 HTTP 代理),从内到外 . 后来出现了反向代理,”反向”这个词在这儿的意思其实是指方向相反,即代理将来自外网 client 的请求 forward 到内网 server,从外到内
Nginx 主要被作为反向代理服务器使用,不过,其同样也是正向代理服务器的一个选择。
客户端通过正向代理服务器访问目标服务器。正向代理“代理”的是客户端,目标服务器不知道客户端是谁,也就是说客户端对目标服务器的这次访问是透明的。
为了实现正向代理,客户端需要设置正向代理服务器的 IP 地址以及代理程序的端口。
举个例子:我们无法直接访问外网,但是可以借助科学上网工具 VPN 来访问。VPN 会把访问外网服务器(目标服务器)的客户端请求代理到一个可以直接访问外网的代理服务器上去。代理服务器会把外网服务器返回的内容再转发给客户端。
外网服务器并不知道客户端是通过 VPN 访问的
简单来说: 你可以将正向代理看作是一个位于客户端和目标服务器之间的代理服务器,其主要作用就是转达客户端请求从目标服务器上获取指定的内容。
相关阅读:
负载均衡
如果一台服务器处理用户请求处理不过来的话,一个简单的办法就是增加多台服务器(服务器集群)部署相同的服务来处理用户请求。
Nginx 可以将接收到的客户端请求以一定的规则(负载均衡策略)均匀地分配到这个服务器集群中所有的服务器上,这个就叫做 负载均衡。
可以看出,Nginx 在其中充当的就是反向代理服务器的作用,负载均衡正是 Nginx 作为反向代理服务器最常见的一个应用。
除此之外,Nginx 还带有健康检查(服务器心跳检查)功能,会定期轮询向集群里的所有服务器发送健康检查请求,来检查集群中是否有服务器处于异常状态。
动静分离
动静分离就是把动态请求和静态请求分开,不是讲动态页面和静态页面物理分离,可以理解为 Nginx 处理静态页面,Tomcat 或者其他 Web 服务器处理动态页面。
动静分离可以减轻 Tomcat 或者其他 Web 服务器的压力,提高网站响应速度。
Nginx 有哪些负载均衡策略?
相关参考:
Nginx 的负载均衡策略不止下面介绍的这四种,我这里只是列举几个比较常用的负载均衡策略。
轮询(Round Robin,默认)
轮询为负载均衡中较为基础也较为简单的算法。
如果没有配置权重的话,每个请求按时间顺序逐一分配到不同的服务器处理。
1 | upstream backserver { |
如果配置权重的话,权重越高的服务器被访问的概率就越大。
1 | upstream backserver { |
未加权重的轮询算法适合于服务器性能相近的集群,其中每个服务器承载相同的负载。加权轮询算法适合于服务器性能不等的集群,权重的存在可以使请求分配更加合理化。
IP 哈希
根据发出请求的和护短 ip 的 hash 值来分配服务器,可以保证同 IP 发出的请求映射到同一服务器,或者具有相同 hash 值的不同 IP 映射到同一服务器。
1 | upstream backserver { |
和轮询一样,IP 哈希也可以配置权重,如果有两个活动连接数相同的服务器,权重大的被访问的概率就越大。
该算法在一定程度上解决了集群部署环境下 Session 不共享的问题。
最小连接数
当有新的请求出现时,遍历服务器节点列表并选取其中活动连接数最小的一台服务器来响应当前请求。活动连接数可以理解为当前正在处理的请求数。
1 | upstream backserver { |
Nginx 常用命令有哪些?
- 启动 nginx 。
- 停止 nginx -s stop 或 nginx -s quit 。
- 重载配置 ./sbin/nginx -s reload(平滑重启) 或 service nginx reload 。
- 重载指定配置文件 .nginx -c /usr/local/nginx/conf/nginx.conf 。
- 查看 nginx 版本 nginx -v 。
- 检查配置文件是否正确 nginx -t 。
- 显示帮助信息 nginx -h 。
Nginx 性能优化的常见方式?
设置 Nginx 运行工作进程个数 :一般设置 CPU 的核心数或者核心数 x2;
开启 Gzip 压缩 :这样可以使网站的图片、CSS、JS 等文件在传输时进行压缩,提高访问速度, 优化 Nginx 性能。详细介绍可以参考Nginx 性能优化功能- Gzip 压缩(大幅度提高页面加载速度)这篇文章;
设置单个 worker 进程允许客户端最大连接数 :一般设置为 65535 就足够了;
连接超时时间设置 :避免在建立无用连接上消耗太多资源;
设置缓存 :像图片、CSS、JS 等这类一般不会经常修改的文件,我们完全可以设置图片在浏览器本地缓存,提高访问速度,优化 Nginx 性能。
……
LVS、Nginx、HAproxy 有什么区别?
LVS、Nginx、HAProxy 是目前使用最广泛的三种软件负载均衡软件。
LVS 是 Linux Virtual Server 的简称,也就是 Linux 虚拟服务器。LVS 是四层负载均衡,建立在 OSI 模型的第四层(传输层)之上,性能非常强大。
HAProxy 可以工作在四层和七层(传输层和应用层),是专门用来做代理服务器的。
Nginx 负载均衡主要是对七层网络通信模型中的第七层应用层上的 HTTP、HTTPS 进行支持。Nginx 是以反向代理的方式进行负载均衡的。
Nginx 如何实现后端服务健康检查?
我们可以利用第三方模块 upstream_check_module 来检测后端服务的健康状态,如果后端服务器不可用,则所有的请求不转发到这台服务器。
upstream_check_module 是一款阿里的一位大佬开源的,使用 Perl 和 C 编写而成,Github 地址 :https://github.com/yaoweibin/nginx_upstream_check_module 。
关于 upstream_check_module 实现后端服务健康检查的具体做法可以参考Nginx 负载均衡健康检查功能这篇文章。
如何保证 Nginx 服务的高可用?
Nginx 可以结合 Keepalived 来实现高可用。
什么是 Keepalived ? 根据官网介绍:
Keepalived 是一个用 C 语言编写的开源路由软件,是 Linux 下一个轻量级别的负载均衡和高可用解决方案。Keepalived 的负载均衡依赖于众所周知且广泛使用的 Linux 虚拟服务器 (IPVS 即 IP Virtual Server,内置在 Linux 内核中的传输层负载均衡器) 内核模块,提供第 4 层负载平衡。Keepalived 实现了一组检查器用于根据服务器节点的健康状况动态维护和管理服务器集群。
Keepalived 的高可用性是通过虚拟路由冗余协议(VRRP 即 Virtual Router Redundancy Protocol,实现路由器高可用的协议)实现的,可以用来解决单点故障。
Github 地址:https://github.com/acassen/keepalived
Keepalived 不仅仅可以和 Nginx 搭配使用,还可以和 LVS、MySQL、HAProxy 等软件配合使用。
再来简单介绍一下 Keepalived+Nginx 实现高可用的常见方法:
- 准备 2 台 Nginx 服务器,一台为主服务,一台为备用服务;
- 在两台 Nginx 服务器上安装并配置 Keepalived;
- 为两台 Nginx 服务器绑定同一个虚拟 IP;
- 编写 Nginx 检测脚本用于通过 Keepalived 检测 Nginx 主服务器的状态是否正常;
如果 Nginx 主服务器宕机的话,会自动进行故障转移,备用 Nginx 主服务器升级为主服务。并且,这个切换对外是透明的,因为使用的虚拟 IP,虚拟 IP 不会改变。
相关阅读:
📄友情提示 :下面的内容属于 Nginx 的进阶指点,主要是一些 Nginx 底层原理相关的知识。你可以根据自身情况选择是否掌握这部分内容,如果你的简历没有写熟练掌握 Nginx 使用及原理的话,面试官一般不会问这么深入。
Nginx 总体架构了解吗?
关于 Nginx 总结架构的详细解答,请看这篇文章:最近和 Nginx 杠上了!
对于传统的 HTTP 和反向代理服务器而言,在处理并发请求的时候会使用单进程或线程的模式处理,同时会止网络或输入/输出操作。
这种方式会消耗大量的内存和 CPU 资源。因为每产生一个单独的进程或线程需要准备一套新的运行时环境,包括分配堆和堆栈内存,以及创建新的执行上下文。
可以想象在处理多请求时会生成对应数目的线程或进程,导致由于线程在不断上下文切换上耗费大量资源。
由于上面的原因,Nginx 在设计之初就使用了模块化、事件驱动、异步处理,非阻塞的架构。
一张图来了解 Nginx 的总结架构:
Nginx 进程模型了解么?
关于进程模型的详细解答,请看这篇文章:Nginx 工作模式和进程模型
Nginx 启动后,会产生一个 master 主进程,主进程执行一系列的工作后会产生一个或者多个工作进程 worker 进程。master 进程用来管理 worker 进程, worker 进程负责处理网络请求。也就是说 Nginx 采用的是经典的 master-worker 模型的多进程模型 。
Nginx 如何处理 HTTP 请求?
系统学习
Guide 整理了下面一些文章和书籍帮助你系统学习 Nginx。
文章推荐
书籍推荐
《深入理解 Nginx(第 2 版)》 这本书是初学者学习 Nginx 的首选,讲的非常细致!
Devops
监控系统常见面试题总结
个人学习笔记,大部分内容整理自书籍、博客和官方文档。
相关文章 &书籍:
相关视频:
监控系统有什么用?
建立完善的监控体系主要是为了:
长期趋势分析 :通过对监控样本数据的持续收集和统计,对监控指标进行长期趋势分析。例如,通过对磁盘空间增长率的判断,我们可以提前预测在未来什么时间节点上需要对资源进行扩容。
数据可视化 :通过可视化仪表盘能够直接获取系统的运行状态、资源使用情况、以及服务运行状态等直观的信息。
预知故障和告警 : 当系统出现或者即将出现故障时,监控系统需要迅速反应并通知管理员,从而能够对问题进行快速的处理或者提前预防问题的发生,避免出现对业务的影响。
辅助定位故障、性能调优、容量规划以及自动化运维
出任何线上事故,先不说其他地方有问题,监控部分一定是有问题的。
如何才能更好地使用监控使用?
了解监控对象的工作原理:要做到对监控对象有基本的了解,清楚它的工作原理。比如想对 JVM 进行监控,你必须清楚 JVM 的堆内存结构和垃圾回收机制。
确定监控对象的指标:清楚使用哪些指标来刻画监控对象的状态?比如想对某个接口进行监控,可以采用请求量、耗时、超时量、异常量等指标来衡量。
定义合理的报警阈值和等级:达到什么阈值需要告警?对应的故障等级是多少?不需要处理的告警不是好告警,可见定义合理的阈值有多重要,否则只会降低运维效率或者让监控系统失去它的作用。
建立完善的故障处理流程:收到故障告警后,一定要有相应的处理流程和 oncall 机制,让故障及时被跟进处理。
常见的监控对象和指标有哪些?
硬件监控 :电源状态、CPU 状态、机器温度、风扇状态、物理磁盘、raid 状态、内存状态、网卡状态
服务器基础监控 :CPU、内存、磁盘、网络
数据库监控 :数据库连接数、QPS、TPS、并行处理的会话数、缓存命中率、主从延时、锁状态、慢查询
中间件监控 :
Nginx:活跃连接数、等待连接数、丢弃连接数、请求量、耗时、5XX 错误率
Tomcat:最大线程数、当前线程数、请求量、耗时、错误量、堆内存使用情况、GC 次数和耗时
缓存 :成功连接数、阻塞连接数、已使用内存、内存碎片率、请求量、耗时、缓存命中率
消息队列:连接数、队列数、生产速率、消费速率、消息堆积量
应用监控 :
HTTP 接口:URL 存活、请求量、耗时、异常量
RPC 接口:请求量、耗时、超时量、拒绝量
JVM :GC 次数、GC 耗时、各个内存区域的大小、当前线程数、死锁线程数
线程池:活跃线程数、任务队列大小、任务执行耗时、拒绝任务数
连接池:总连接数、活跃连接数
日志监控:访问日志、错误日志
业务指标:视业务来定,比如 PV、订单量等
监控的基本流程了解吗?
无论是开源的监控系统还是自研的监控系统,监控的整个流程大同小异,一般都包括以下模块:
数据采集:采集的方式有很多种,包括日志埋点进行采集(通过 Logstash、Filebeat 等进行上报和解析),JMX 标准接口输出监控指标,被监控对象提供 REST API 进行数据采集(如 Hadoop、ES),系统命令行,统一的 SDK 进行侵入式的埋点和上报等。
数据传输:将采集的数据以 TCP、UDP 或者 HTTP 协议的形式上报给监控系统,有主动 Push 模式,也有被动 Pull 模式。
数据存储:有使用 MySQL、Oracle 等 RDBMS 存储的,也有使用时序数据库 RRDTool、OpentTSDB、InfluxDB 存储的,还有使用 HBase 存储的。
数据展示:数据指标的图形化展示。
监控告警:灵活的告警设置,以及支持邮件、短信、IM 等多种通知通道。
监控系统需要满足什么要求?
实时监控&告警 :监控系统对业务服务系统实时监控,如果产生系统异常及时告警给相关人员。
高可用 :要保障监控系统的可用性
故障容忍 :监控系统不影响业务系统的正常运行,监控系统挂了,应用正常运行。
可扩展 :支持分布式、跨 IDC 部署,横向扩展。
可视化 :自带可视化图标、支持对接各类可视化组件比如 Grafana 。
监控系统技术选型有哪些?如何选择?
老牌监控系统
Zabbix 和 Nagios 相继出现在 1998 年和 1999 年,目前已经被淘汰,不太建议使用,Prometheus 是更好的选择。
Zabbix
介绍 :老牌监控的优秀代表。产品成熟,监控功能很全面,采集方式丰富(支持 Agent、SNMP、JMX、SSH 等多种采集方式,以及主动和被动的数据传输方式),使用也很广泛,差不多有 70%左右的互联网公司都曾使用过 Zabbix 作为监控解决方案。
开发语言 : C
数据存储 : Zabbix 存储在 MySQL 上,也可以存储在其他数据库服务。Zabbix 由于使用了关系型数据存储时序数据,所以在监控大规模集群时常常在数据存储方面捉襟见肘。所以从 Zabbix 4.2 版本后开始支持 TimescaleDB 时序数据库,不过目前成熟度还不高。
数据采集方式 : Zabbix 通过 SNMP、Agent、ICMP、SSH、IPMI 等对系统进行数据采集。Zabbix 采用的是 Push 模型(客户端发送数据给服务端)。
数据展示 :自带展示界面,也可以对接 Grafana。
评价 :不太建议使用 Zabbix,性能可能会成为监控系统的瓶颈。并且,应用层监控支持有限、二次开发难度大(基于 c 语言)、数据模型不强大。
相关阅读:《zabbix 运维手册》
Nagios
介绍 :Nagios 能有效监控 Windows、Linux 和 UNIX 的主机状态(CPU、内存、磁盘等),以及交换机、路由器等网络设备(SMTP、POP3、HTTP 和 NNTP 等),还有 Server、Application、Logging,用户可自定义监控脚本实现对上述对象的监控。Nagios 同时提供了一个可选的基于浏览器的 Web 界面,以方便系统管理人员查看网络状态、各种系统问题以及日志等。
开发语言 : C
数据存储 : MySQL 数据库
数据采集方式 : 通过各种插件采集数据
数据展示 :自带展示界面,不过功能简单。
评价 :不符合当前监控系统的要求,而且,Nagios 免费版本的功能非常有限,运维管理难度非常大。
新一代监控系统
相比于老牌监控系统,新一代监控系统有明显的优势,比如:灵活的数据模型、更成熟的时序数据库、强大的告警功能。
Open-Falcon
- 介绍 :小米 2015 年开源的企业级监控工具,在架构设计上吸取了 Zabbix 的经验,同时很好地解决了 Zabbix 的诸多痛点。Github 地址:https://github.com/open-falcon 。官方文档:https://book.open-falcon.org/ 。
- 开发语言 :Go、Python。
- 数据存储 : 环型数据库,支持对接时序数据库 OpenTSDB。
- 数据采集方式 : 自动发现,支持 falcon-agent、snmp、支持用户主动 push、用户自定义插件支持、opentsdb data model like(timestamp、endpoint、metric、key-value tags)。Open-Falcon 和 Zabbix 采用的都是 Push 模型(客户端发送数据给服务端)。
- 数据展示 :自带展示界面,也可以对接 Grafana。
- 评价 :用户集中在国内,流行度一般,生态一般。
Open-Falcon 架构图如下:
- Falcon-agent :采集模块。类似 Zabbix 的 agent,Kubernetes 自带监控体系中的 cAdvisor,Nagios 中的 Plugin,使用 Go 语言开发,用于采集主机上的各种指标数据。
- Hearthbeat server :心跳服务。每个 Agent 都会周期性地通过 RPC 方式将自己地状态上报给 HBS,主要包括主机名、主机 IP、Agent 版本和插件版本,Agent 还会从 HBS 获取自己需要执行的采集任务和自定义插件。
- Transfer :负责监控 agent 发送的监控数据,并对数据进行处理,在过滤后通过一致性 Hash 算法将数据发送到 Judge 或者 Graph。为了支持存储大量的历史数据,Transfer 还支持 OpenTSDB。Transfer 本身没有状态,可以随意扩展。
- Jedge :告警模块。Transfer 转发到 Judge 的数据会触发用户设定的告警规则,如果满足,则会触发邮件、微信或者回调接口。这里为了避免重复告警,引入了 Redis 暂存告警,从而完成告警合并和抑制。
- Graph :RRD 数据上报、归档、存储的组件。Graph 在收到数据以后,会以 RRDtool 的数据归档方式存储数据,同时提供 RPC 方式的监控查询接口。
- API : 查询模块。主要提供查询接口,不但可以从 Grapg 里面读取数据,还可以对接 MySQL,用于保存告警、用户等信息。
- Dashboard : 监控数据展示面板。由 Python 开发而成,提供 Open-Falcon 的数据和告警展示,监控数据来自 Graph,Dashboard 允许用户自定义监控面板。
- Aggregator : 聚合模块。聚合某集群下所有机器的某个指标的值,提供一种集群视角的监控体验。 通过定时从 Graph 获取数据,按照集群聚合产生新的监控数据并将监控数据发送到 Transfer。
Prometheus
- 介绍 :Prometheus 受启发于 Google 的 Brogmon 监控系统,由前 Google 员工 2015 年正式发布。截止到 2021 年 9 月 2 日,Prometheus 在 Github 上已经收获了 38.5k+ Star,600+位 Contributors。 Github 地址:https://github.com/prometheus 。
- 开发语言 :Go
- 数据存储 : Prometheus 自研一套高性能的时序数据库,并且还支持外接时序数据库。
- 数据采集方式 : Prometheus 的基本原理是通过 HTTP 协议周期性抓取被监控组件的状态,任意组件只要提供对应的 HTTP 接口就可以接入监控。Prometheus 在收集数据时,采用的 Pull 模型(服务端主动去客户端拉取数据)
- 数据展示 :自带展示界面,也可以对接 Grafana。
- 评价 :目前国内外使用最广泛的一个监控系统,生态也非常好,成熟稳定!
Prometheus 特性 :
- 开箱即用的各种服务发现机制,可以自动发现监控端点;
- 专为监控指标数据设计的高性能时序数据库 TSDB;
- 强大易用的查询语言PromQL以及丰富的聚合函数;
- 可以配置灵活的告警规则,支持告警收敛(分组、抑制、静默)、多级路由等等高级功能;
- 生态完善,有各种现成的开源 Exporter 实现,实现自定义的监控指标也非常简单。
Prometheus 基本架构 :
- Prometheus Server:核心组件,用于收集、存储监控数据。它同时支持静态配置和通过 Service Discovery 动态发现来管理监控目标,并从监控目标中获取数据。此外,Prometheus Server 也是一个时序数据库,它将监控数据保存在本地磁盘中,并对外提供自定义的 PromQL 语言实现对数据的查询和分析。
- Exporter:用来采集数据,作用类似于 agent,区别在于 Prometheus 是基于 Pull 方式拉取采集数据的,因此,Exporter 通过 HTTP 服务的形式将监控数据按照标准格式暴露给 Prometheus Server,社区中已经有大量现成的 Exporter 可以直接使用,用户也可以使用各种语言的 client library 自定义实现。
- Push gateway:主要用于瞬时任务的场景,防止 Prometheus Server 来 pull 数据之前此类 Short-lived jobs 就已经执行完毕了,因此 job 可以采用 push 的方式将监控数据主动汇报给 Push gateway 缓存起来进行中转。
- 当告警产生时,Prometheus Server 将告警信息推送给 Alert Manager,由它发送告警信息给接收方。
- Prometheus 内置了一个简单的 web 控制台,可以查询配置信息和指标等,而实际应用中我们通常会将 Prometheus 作为 Grafana 的数据源,创建仪表盘以及查看指标。
推荐一本 Prometheus 的开源书籍《Prometheus 操作指南》。
总结
- 监控是一项长期建设的事情,一开始就想做一个 All In One 的监控解决方案,我觉得没有必要。从成本角度考虑,在初期直接使用开源的监控方案即可,先解决有无问题。
- Zabbix、Open-Falcon 和 Prometheus 都支持和 Grafana 做快速集成,想要美观且强大的可视化体验,可以和 Grafana 进行组合。
- Open-Falcon 的核心优势在于数据分片功能,能支撑更多的机器和监控项;Prometheus 则是容器监控方面的标配,有 Google 和 k8s 加持。
日志系统常见面试题总结
因为日志系统在询问项目经历的时候经常会被问到,所以,我就写了这篇文章。
这是一篇日志系统常见概念的扫盲篇~不会涉及到具体架构的日志系统的搭建过程。旨在帮助对于日志系统不太了解的小伙伴,普及一些日志系统常见的概念。
何为日志?
在我看来,日志就是系统对某些行为的一些记录,这些行为包括:系统出现错误(定位问题、解决问题)、记录关键的业务信息(定位问题、解决问题)、记录操作行为(保障安全)等等。
按照较为官方的话来说:“日志是带时间戳的基于时间序列的机器数据,包括 IT 系统信息(服务器、网络设备、操作系统、应用软件)、物联网各种传感器信息。日志可以反映用户/机器的行为,是真实的数据”。
为何要用日志系统?
没有日志系统之前,我们的日志可能分布在多台服务器上。每次需要查看日志,我们都需要登录每台机器。然后,使用 grep、wc 等 Linux 命令来对日志进行搜索。这个过程是非常麻烦并且耗时的!并且,日志量不大的时候,这个速度还能忍受。当日志量比较多的时候,整个过程就是非常慢。
从上面我的描述中,你已经发现,没有对日志实现集中管理,主要给我们带来了下面这几点问题:
开发人员登录线上服务器查看日志比较麻烦并且存在安全隐患
日志数据比较分散,难以维护,不方便检索。
日志数量比较大的时候,查询速度比较慢。
无法对日志数据进行可视化展示。
日志系统就是为了对日志实现集中管理。它也是一个系统,不过主要是负责处理日志罢了。
一个最基本的日志系统要做哪些事情?
为了解决没有日志系统的时候,存在的一些问题,一直最基本的 日志系统需要做哪些事情呢?
- 采集日志 :支持多种日志格式以及数据源的采集。
- 日志数据清洗/处理 :采集到的原始日志数据需要首先清洗/处理一波。
- 存储 :为了方便对清洗后的日志进行处理,我们可以对接多种存储方式比如 ElasticSearch(日志检索) 、Hadoop(离线数据分析)。
- 展示与搜素 :支持可视化地展示日志,并且能够根据关键词快速的定位到日志并查看日志上下文。
- 告警 :支持对接常见的监控系统。
我专门画了一张图,展示一下日志系统处理日志的一个基本流程。
另外,一些比较高大上的日志系统甚至还支持 实时分析、离线分析 等功能。
ELK 了解么?
ELK 是目前使用的比较多的一个开源的日志系统解决方案,背靠是 Elastic 这家专注搜索的公司。
ELK 老三件套
最原始的时候,ELK 是由 3 个开源项目的首字母构成,分别是 Elasticsearch 、Logstash、Kibana。
下图是一个最简单的 ELK 日志系统架构 :
我们分别来介绍一下这些开源项目以及它们在这个日志系统中起到的作用:
- Logstash :Logstash 主要用于日志的搜集、分析和过滤,支持对多种日志类型进行处理。在 ELK 日志系统中,Logstash 负责日志的收集和清洗。
- Elasticsearch :ElasticSearch 一款使用 Java 语言开发的搜索引擎,基于 Lucence 。可以解决使用数据库进行模糊搜索时存在的性能问题,提供海量数据近实时的检索体验。在 ELK 日志系统中,Elasticsearch 负责日志的搜素。
- Kibana :Kibana 是专门设计用来与 Elasticsearch 协作的,可以自定义多种表格、柱状图、饼状图、折线图对存储在 Elasticsearch 中的数据进行深入挖掘分析与可视化。 ELK 日志系统中,Logstash 主要负责对从 Elasticsearch 中搜索出来的日志进行可视化展示。
新一代 ELK 架构
ELK 属于比较老牌的一款日志系统解决方案,这个方案存在一个问题就是:Logstash 对资源消耗过高。
于是, Elastic 推出了 Beats 。Beats 基于名为libbeat的 Go 框架,一共包含 8 位成员。
这个时候,ELK 已经不仅仅代表 Elasticsearch 、Logstash、Kibana 这 3 个开源项目了。
Elastic 官方将 ELK 重命名为 Elastic Stack(Elasticsearch、Kibana、Beats 和 Logstash)。但是,大家目前仍然习惯将其成为 ELK 。
Elastic 的官方文档是这样描述的(由 Chrome 插件 Mate Translate 提供翻译功能):
现在的 ELK 架构变成了这样:
Beats 采集的数据可以直接发送到 Elasticsearch 或者在 Logstash 进一步处理之后再发送到 Elasticsearch。
Beats 的诞生,也大大地扩展了老三件套版本的 ELK 的功能。Beats 组件除了能够通过 Filebeat 采集日志之外,还能通过 Metricbeat 采集服务器的各种指标,通过 Packetbeat 采集网络数据。
我们不需要将 Beats 都用上,一般对于一个基本的日志系统,只需要 Filebeat 就够了。
Filebeat 是一个轻量型日志采集器。无论您是从安全设备、云、容器、主机还是 OT 进行数据收集,Filebeat 都将为您提供一种轻量型方法,用于转发和汇总日志与文件,让简单的事情不再繁杂。
Filebeat 是 Elastic Stack 的一部分,能够与 Logstash、Elasticsearch 和 Kibana 无缝协作。
Filebeat 能够轻松地将数据传送到 Logstash(对日志进行处理)、Elasticsearch(日志检索)、甚至是 Kibana (日志展示)中。
Filebeat 只是对日志进行采集,无法对日志进行处理。日志具体的处理往往还是要交给 Logstash 来做。
更多关于 Filebeat 的内容,你可以看看 Filebeat 官方文档教程。
Filebeat+Logstash+Elasticsearch+Kibana 架构概览
下图一个最基本的 Filebeat+Logstash+Elasticsearch+Kibana 架构图,图片来源于:《The ELK Stack ( Elasticsearch, Logstash, and Kibana ) Using Filebeat》。
Filebeat 替代 Logstash 采集日志,具体的日志处理还是由 Logstash 来做。
针对上图的日志系统架构图,有下面几个可优化点:
- 在 Kibana 和用户之间,使用 Nginx 来做反向代理,免用户直接访问 Kibana 服务器,提高安全性。
- Filebeat 和 Logstash 之间增加一层消息队列比如 Kafka、RabbitMQ。Filebeat 负责将收集到的数据写入消息队列,Logstash 取出数据做进一步处理。
EFK
EFK 中的 F 代表的是 Fluentd。下图是一个最简单的 EFK 日志系统架构 :
Fluentd 是一款开源的日志收集器,使用 Ruby 编写,其提供的功能和 Logstash 差不多。但是,要更加轻量,性能也更优越,内存占用也更低。具体使用教程,可以参考《性能优越的轻量级日志收集工具,微软、亚马逊都在用!》。
轻量级日志系统 Loki
上面介绍到的 ELK 日志系统方案功能丰富,稳定可靠。不过,对资源的消耗也更大,成本也更高。而且,用过 ELK 日志系统的小伙伴肯定会发现其实很多功能压根都用不上。
因此,就有了 Loki,这是一个 Grafana Labs 团队开源的小巧易用的日志系统,原生支持 Grafana。
并且,Loki 专门为 Prometheus 和 Kubernetes 用户做了相关优化比如 Loki 特别适合存储Kubernetes Pod 日志。
官方的介绍也比较有意思哈! Like Prometheus,But For Logs. (类似于 Prometheus 的日志系统,不过主要是为日志服务的)。
根据官网 ,Loki 的架构如下图所示
Loki 的整个架构非常简单,主要有 3 个组件组成:
- Loki 是主服务器,负责存储日志和处理查询。
- Promtail 是代理,负责收集日志并将其发送给 Loki 。
- Grafana 用于 UI 展示。
Loki 提供了详细的使用文档,上手相对来说比较容易。并且,目前其流行度还是可以的。你可以很方便在网络上搜索到有关 Loki 的博文。
总结
这篇文章我主要介绍了日志系统相关的知识,包括:
何为日志?
为何要用日志系统?一个基本的日志系统要做哪些事情?
ELK、EFK
轻量级日志系统 Loki
另外,大部分图片都是我使用 draw.io 来绘制的。一些技术名词的图标,我们可以直接通过 Google 图片搜索即可,方法: 技术名词+图标(示例:Logstash icon)
参考
ELK 架构和 Filebeat 工作原理详解:https://developer.ibm.com/zh/articles/os-cn-elk-filebeat/
ELK Introduction-elastic 官方 :https://elastic-stack.readthedocs.io/en/latest/introduction.html
ELK Stack Tutorial: Learn Elasticsearch, Logstash, and Kibana :https://www.guru99.com/elk-stack-tutorial.html
三、技术面试题自测篇
Java基础
可选标题:面了一个应届生,我问了这些 Java 基础问题。
Java 中有哪 8 种基本数据类型?它们的默认值和占用的空间大小知道不? 说说这 8 种基本数据类型对应的包装类型。
💡 提示:Java 中有 8 种基本数据类型,分别为:
- 6 种数字类型 :byte、short、int、long、float、double
- 1 种字符类型:char
- 1 种布尔型:boolean。
包装类型的常量池技术了解么?
💡 提示:Java 基本类型的包装类的大部分(Byte,Short,Integer,Long ,Character,Boolean)都实现了常量池技术。
🌈 拓展:整型包装类对象之间值的比较应该使用 equals 方法
为什么要有包装类型?
💡 提示: 基本类型有默认值、泛型参数不能是基本类型
什么是自动拆装箱?原理?
💡 提示:基本类型和包装类型之间的互转。装箱其实就是调用了 包装类的valueOf()方法,拆箱其实就是调用了 xxxValue()方法。
遇到过自动拆箱引发的 NPE 问题吗?
💡 提示:两个常见的场景:
- 数据库的查询结果可能是 null,因为自动拆箱,用基本数据类型接收有 NPE 风险
- 三目运算符使用不当会导致诡异的 NPE 异常
String、StringBuffer 和 StringBuilder 的区别是什么? String 为什么是不可变的?
💡 提示:可以从可变性、线程安全性、性能这几个角度来回答。
重载和重写的区别?
💡 提示:可以从下面几个角度来回答:
- 发生范围
- 参数列表
- 返回值类型
- 异常
- 访问修饰符
- 发生阶段
== 和 equals() 的区别
💡 提示:== 对于基本类型和引用类型的作用效果是不同的,equals() 不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。equals() 方法存在两种使用情况:
- 类没有重写 equals()方法 :通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的默认是 Object类equals()方法。
- 类重写了 equals()方法 :一般我们都重写 equals()方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。
Java 反射?反射有什么优点/缺点?你是怎么理解反射的(为什么框架需要反射)?
💡 提示: 想想你平时使用框架为啥能够如此方便。想想动态代理以及注解和反射之间的关系。
谈谈对 Java 注解的理解,解决了什么问题?
💡 提示: 想想你平时使用框架为啥能够如此方便。另外,需要注意注解的解析依赖于反射机制,务必要提前把反射机制搞懂。
Java 泛型了解么?泛型的作用?什么是类型擦除?泛型有哪些限制?介绍一下常用的通配符?
💡 提示:
- 好处:编译期间的类型检测(安全)、可读性更好
- Java 的泛型是伪泛型
内部类了解吗?匿名内部类了解吗?
内部类分为下面 4 种:
- 成员内部类
- 静态内部类
- 局部(方法)内部类
- 匿名内部类
BIO,NIO,AIO 有什么区别?
IO 模型这块挺难理解的,需要很多计算机底层知识。建议小伙伴们克服困难,一定要把这个点搞明白。
Java 集合
说说 List,Set,Map 三者的区别?
💡 提示:可以从这些数据结构中的元素是否有序、是否可以重复、存储的元素类型(比如 Map 存储的就是键值对)等方面来回答。
List,Set,Map 在 Java 中分别由哪些对应的实现类?底层的数据结构?
💡 提示:拿 List 来举例, List 的常见实现类以及它们的数据结构 :
- ArrayList: Object[]数组
- Vector:Object[]数组
- LinkedList: 双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环)
有哪些集合是线程不安全的?怎么解决呢?
💡 提示:这块比较常问的是 Arraylist 和 Vector 、HashMap 和 ConcurrentHashMap(高频问题,重要) 。被问到 Vector 的时候, 你紧接着可能会被问到 Arraylist 和 Vector 的区别。被问到 ConcurrentHashMap 的时候,你紧接着就可能会被问到 ConcurrentHashMap 相关的问题比如 ConcurrentHashMap 是如何保证线程安全的。
HashMap 查询,删除的时间复杂度
💡 提示:
- 没有哈希冲突的情况
- 转链表的情况
- 链表转红黑树的情况
HashMap 的底层实现
💡 提示:
- JDK1.8 之前 : 数组和链表
- JDK1.8 之后 : 多了红黑树
HashMap 的长度为什么是 2 的幂次方
💡 提示:提高运算效率。
比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同
💡 提示:
- HashSet、LinkedHashSet 和 TreeSet 都是 Set 接口的实现类,都能保证元素唯一,并且都不是线程安全的。
- HashSet、LinkedHashSet 和 TreeSet 的主要区别在于底层数据结构不同(底层数据结构不同又导致这三者的应用场景不同)。
HashMap 和 Hashtable 的区别?HashMap 和 HashSet 区别?HashMap 和 TreeMap 区别?
ConcurrentHashMap 和 Hashtable 的区别?
💡 提示:
- 底层数据结构
- 实现线程安全的方式的区别
ConcurrentHashMap 线程安全的具体实现方式/底层具体实现
💡 提示:
- JDK 1.7 :Segment 分段锁
- JDK 1.8 : CAS 和 synchronized
Java并发
什么是线程和进程?线程与进程的关系,区别及优缺点?⭐⭐⭐⭐
💡 提示:可以从从 JVM 角度说进程和线程之间的关系
为什么要使用多线程呢? ⭐⭐⭐
💡 提示:从计算机角度来说主要是为了充分利用多核 CPU 的能力,从项目角度来说主要是为了提升系统的性能。
说说线程的生命周期和状态? ⭐⭐⭐⭐
💡 提示: 6 种状态(NEW、RUNNABLE、BLOCKED、WAITING、TIME_WAITING、TERMINATED)。
🌈 拓展:在操作系统中层面线程有 READY 和 RUNNING 状态,而在 JVM 层面只能看到 RUNNABLE 状态。
什么是线程死锁?如何避免死锁?如何预防和避免线程死锁? ⭐⭐⭐⭐
💡 提示: 这里最好能够结合代码来聊,你要确保自己可以写出有死锁问题的代码。
🌈 拓展:项目中遇到死锁问题是比较常见的,除了要搞懂上面这些死锁的基本概念之外,你还要知道线上项目遇到死锁问题该如何排查和解决。
synchronized 关键字 ⭐⭐⭐⭐⭐
💡 提示:synchronized 关键字几乎是面试必问,你需要搞懂下面这些 synchronized 关键字相关的问题:
- synchronized 关键字的作用,自己是怎么使用的。
- synchronized 关键字的底层原理(重点!!!)
- JDK1.6 之后的 synchronized 关键字底层做了哪些优化。synchronized 锁升级流程。
- synchronized 和 ReentrantLock 的区别。
- synchronized 和 volatile 的区别。
并发编程的三个重要特性 ⭐⭐⭐⭐⭐
💡 提示: 原子性、可见性、有序性
JMM(Java Memory Model,Java 内存模型)和 happens-before 原则。 ⭐⭐⭐⭐⭐
volatile 关键字 ⭐⭐⭐⭐⭐
💡 提示:volatile 关键字同样是一个重点!结合 JMM(Java Memory Model,Java 内存模型)和 happens-before 原则来回答就行了。
ThreadLocal 关键字 ⭐⭐⭐⭐⭐
💡 提示:关注ThreadLocal的底层原理、内存泄露问题以及自己是如何在项目中使用ThreadLocal关键字的。
线程池 ⭐⭐⭐⭐⭐
💡 提示:线程池有哪几种,各种线程池的优缺点,线程池的重要参数、线程池的执行流程、线程池的饱和策略、如何设置线程池的大小等等。
ReentrantLock 和 AQS ⭐⭐⭐⭐⭐
💡 提示: ReentrantLock 的特性、实现原理(基于 AQS)。可以从 ReentrantLock 的实现来理解 AQS。
乐观锁和悲观锁的区别 ⭐⭐⭐⭐⭐
CAS 了解么?原理?什么是 ABA 问题?ABA 问题怎么解决? ⭐⭐⭐⭐⭐
💡 提示:多地方都用到了 CAS 比如 ConcurrentHashMap 采用 CAS 和 synchronized 来保证并发安全,再比如java.util.concurrent.atomic包中的类通过 volatile+CAS 重试保证线程安全性。和面试官聊 CAS 的时候,你可以结合 CAS 的一些实际应用来说。
Atomic 原子类 ⭐⭐
JVM
如非特殊说明,本文主要针对的就是 HotSpot VM 。
运行时数据区中包含哪些区域?哪些线程共享?哪些线程独享?哪些区域可能会出现OutOfMemoryError?哪些区域不会出现OutOfMemoryError?【⭐⭐⭐⭐⭐】
💡 提示:把下面两张图记在心里!并且,你还要搞懂这些区域大概的作用是什么。
JDK 1.8 之前:
JDK 1.8 :
线程私有的:程序计数器、虚拟机栈、本地方法栈
线程共享的:堆、方法区、直接内存 (非运行时数据区的一部分)
说一下方法区和永久代的关系。【⭐⭐⭐】
💡 提示:其实就有点像 Java 中接口和类的关系。
Java 对象的创建过程。【⭐⭐⭐⭐】
💡 提示:下图便是 Java 对象的创建过程,我建议最好是能默写出来,并且要掌握每一步在做什么。
对象的访问定位的两种方式知道吗?各有什么优缺点。【⭐⭐⭐⭐】
💡 提示:句柄和直接指针。
如何判断对象是否死亡(两种方法)。 讲一下可达性分析算法的流程。 【⭐⭐⭐⭐】
JDK 中有几种引用类型?分别的特点是什么?【⭐⭐】
💡 提示:JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)。对这些概念简单了解,有印象就可以了。
堆空间的基本结构了解吗?什么情况下对象会进入老年代?【⭐⭐⭐⭐⭐】
提示:
- 大部分情况,对象都会首先在 Eden 区域分配。
- 长期存活的对象将进入老年代。
- 大对象直接进入老年代。
🌈 拓展:动态对象年龄判定。
垃圾收集有哪些算法,各自的特点?【⭐⭐⭐⭐⭐】
💡 提示:
有哪些常见的 GC?谈谈你对 Minor GC、还有 Full GC 的理解。Minor GC 与 Full GC 分别在什么时候发生? Minor GC 会发生 stop the world 现象吗?【⭐⭐⭐⭐⭐】
💡 提示:
针对 HotSpot VM 的实现,它里面的 GC 其实准确分类只有两大种:
部分收集 (Partial GC):
- 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
- 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;
- 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。
整堆收集 (Full GC):收集整个 Java 堆和方法区。
讲一下 CMS 垃圾收集器的四个步骤。CMS 有什么缺点?【⭐⭐⭐⭐】
💡 提示:初始标记、并发标记、重新标记、并发清除。
并发标记要解决什么问题?并发标记带来了什么问题?如何解决并发扫描时对象消失问题?【⭐⭐⭐⭐】
相关阅读:面试官:你说你熟悉 jvm?那你讲一下并发的可达性分析 。
G1 垃圾收集器的步骤。有什么缺点?【⭐⭐⭐⭐】
💡 提示:和 CMS 类似。
ZGC 了解吗?【⭐⭐⭐⭐】
💡 提示: 新一代垃圾回收器 ZGC 的探索与实践(opens new window)
JVM 中的安全点和安全区各代表什么?写屏障你了解吗?【⭐⭐⭐】
虚拟机基础故障处理工具有哪些?【⭐⭐⭐】
💡 提示: 简单了解几个最重要的即可!
什么是字节码?类文件结构的组成了解吗?【⭐⭐⭐⭐】
💡 提示:在 Java 中,JVM 可以理解的代码就叫做字节码(即扩展名为 .class 的文件)。
ClassFile 的结构如下:
1 | ClassFile { |
类的生命周期?类加载的过程了解么?加载这一步主要做了什么事情?初始化阶段中哪几种情况必须对类初始化?【⭐⭐⭐⭐⭐】
💡 提示:
双亲委派模型了解么?如果我们不想用双亲委派模型怎么办?【⭐⭐⭐⭐⭐】
💡 提示:可以参考 Tomcat 的自定义类加载器 WebAppClassLoader
双亲委派模型有什么好处?双亲委派模型是为了保证一个 Java 类在 JVM 中是唯一的? 【⭐⭐⭐⭐⭐】
JDK 中有哪些默认的类加载器? 【⭐⭐⭐⭐】
💡 提示:
JVM 中内置了三个重要的 ClassLoader,除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader:
- BootstrapClassLoader(启动类加载器) :最顶层的加载类,由 C++实现,负责加载 %JAVA_HOME%/lib目录下的 jar 包和类或者被 -Xbootclasspath参数指定的路径中的所有类。
- ExtensionClassLoader(扩展类加载器) :主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类,或被 java.ext.dirs 系统变量所指定的路径下的 jar 包。
- AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。
堆内存相关的 JVM 参数有哪些?你在项目中实际配置过了吗? 【⭐⭐⭐⭐⭐】
💡 提示:一定要自己动手操作一下。
相关问题:
- 如何设置年轻代和老年代的大小?
- 如何调整调整新生代和老年代的比值?
- ……
如何对栈进行参数调优?【⭐⭐⭐⭐】
你在项目中遇到过 GC 问题吗?怎么分析和解决的?【⭐⭐⭐⭐⭐】
💡 提示:比较有含金量的问题!比较能反映出求职者的水平,应该重点准备。
相关阅读:Java 中 9 种常见的 CMS GC 问题分析与解决
GC 性能指标了解吗?调优原则呢?【⭐⭐⭐⭐⭐】
- GC 性能指标通常关注吞吐量、停顿时间和垃圾回收频率。
- GC 优化的目标就是降低 Full GC 的频率以及减少 Full GC 的执行时间。
如何降低 Full GC 的频率?【⭐⭐⭐⭐⭐】
💡 提示: 可以通过减少进入老年代的对象数量可以显著降低 Full GC 的频率。如何减少进入老年代的对象数量呢?JVM 垃圾回收这部分有提到过。
MySQL
注意!!! :下面这些问题的参考答案你几乎都可以在 JavaGuide 在线阅读网站 和 《JavaGuide 面试指北》中找到。并且,这两个参考资料没有给出解答的问题,我也都给了对应的参考文章。
建议你先阅读学习了对应的内容之后再进行自测。
MySQL 基础架构
说说 MySQL 的架构?
👉 重要程度:⭐⭐⭐⭐
💡 提示:面试官一般不会直接问你 MySQL 基础架构,通常会由“一个 SQL 语句在 MySQL 中的执行流程”类似的问题引出。
你需要搞懂下图每一个组件所提供的主要功能。
一条 SQL语句在MySQL中的执行过程
👉 重要程度:⭐⭐⭐⭐
💡 提示:结合 MySQL 的基础架构来回答这个问题。
MySQL 存储引擎
MySQL 提供了哪些存储引擎?
👉 重要程度:⭐⭐
💡 提示:可以通过 show engines; 命令查看 MySQL 提供的所有存储引擎。
MySQL 存储引擎架构了解吗?
👉 重要程度:⭐⭐⭐
💡 提示:MySQL 存储引擎采用的是插件式架构,支持多种存储引擎,我们甚至可以为不同的数据库表设置不同的存储引擎以适应不同场景的需要。存储引擎是基于表的,而不是数据库。
MyISAM 和 InnoDB 的区别
👉 重要程度:⭐⭐⭐⭐⭐
💡 提示:从是否支持行级锁、事务、外键、数据库异常崩溃后的安全恢复、MVCC 等方面回答。
MySQL 事务
何谓事务?
👉 重要程度:⭐⭐⭐⭐
💡 提示:转账的案例。
何谓数据库事务?
👉 重要程度:⭐⭐⭐⭐
💡 提示:数据库事务可以保证多个对数据库的操作(也就是 SQL 语句)构成一个逻辑上的整体。构成这个逻辑上的整体的这些数据库操作遵循:要么全部执行成功,要么全部不执行 。关系型数据库(例如:MySQL、SQL Server、Oracle 等)事务都有 ACID 特性。
ACID 特性指的是什么?
👉 重要程度:⭐⭐⭐⭐⭐
💡 提示:ACID 特性大家都知道,但是,这里有一个绝大部分人可能会理解错误的地方:只有保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障。也就是说 A、I、D 是手段,C 是目的!
面试官可能还会顺便问你:“持久性是如何保证的?”、“原子性是如何保证的?”巴拉巴拉。这个时候你就需要结合 MySQL 日志、MySQL 锁以及 MVCC 来回答。
并发事务带来了哪些问题?
👉 重要程度:⭐⭐⭐⭐⭐
💡 提示:脏读(Dirty read)、丢失修改(Lost to modify)、不可重复读(Unrepeatable read)、幻读(Phantom read)。
不可重复读和幻读区别
👉 重要程度:⭐⭐⭐⭐⭐
💡 提示:
- 不可重复读的重点是内容修改或者记录减少比如多次读取一条记录发现其中某些记录的值被修改;
- 幻读的重点在于记录新增比如多次执行同一条查询语句(DQL)时,发现查到的记录增加了。
幻读其实可以看作是不可重复读的一种特殊情况,单独把区分幻读的原因主要是解决幻读和不可重复读的方案不一样。
举个例子:执行 delete 和 update 操作的时候,可以直接对记录加锁,保证事务安全。而执行 insert 操作的时候,由于记录锁(Record Lock)只能锁住已经存在的记录,为了避免插入新记录,需要依赖间隙锁(Gap Lock)。也就是说执行 insert 操作的时候需要依赖 Next-Key Lock(Record Lock+Gap Lock) 进行加锁来保证不出现幻读。
SQL 标准定义了哪些事务隔离级别?
👉 重要程度:⭐⭐⭐⭐⭐
💡 提示:READ-UNCOMMITTED(读取未提交)、READ-COMMITTED(读取已提交)、REPEATABLE-READ(可重复读)、SERIALIZABLE(可串行化)。
MySQL 的默认隔离级别是什么?能解决幻读问题么?
👉 重要程度:⭐⭐⭐⭐⭐
💡 提示:默认隔离级别是 REPEATABLE-READ(可重读),可以解决幻读问题(两种情况,快照读和当前读)。
什么是 MVCC?有什么用?原理了解么?
👉 重要程度:⭐⭐⭐⭐⭐
💡 提示:RC(Read Committed,读提交)和 RR(Repeatable Read,可重复读)这两个隔离级别的实现都离不开 MVCC。
InnoDB 事务隔离级别实现原理
👉 重要程度:⭐⭐⭐
💡 提示:这个问题研究的比较深入,一般的面试不会问,难度有点大,需要结合 MySQL 锁和 MVCC 来回答。学有余力的朋友,可以通过下面这两篇文章来准备这个问题:
MySQL 锁
表级锁和行级锁了解吗?有什么区别?
👉 重要程度:⭐⭐⭐⭐
💡 提示:表级锁(table-level locking)一锁就锁整张表。行级锁的粒度更小,仅对相关的记录上锁即可(对一行或者多行记录加锁)
行级锁的使用有什么注意事项?
👉 重要程度:⭐⭐⭐⭐
💡 提示:InnoDB 的行锁是针对索引字段加的锁,表级锁是针对非索引字段加的锁。当我们执行 UPDATE、DELETE 语句时,如果 WHERE条件中字段没有命中索引或者索引失效的话,就会导致扫描全表对表中的所有记录进行加锁。
共享锁和排他锁呢?
👉 重要程度:⭐⭐⭐⭐
💡 提示:不论是表级锁还是行级锁,都存在共享锁(Share Lock,S 锁)和排他锁(Exclusive Lock,X 锁)这两类
意向锁有什么作用?
👉 重要程度:⭐⭐⭐⭐
💡 提示:快速判断是否可以对某个表使用表锁。意向锁是表级锁,共有两种。并且,意向锁之间是互相兼容的。
InnoDB 有哪几类行锁?
👉 重要程度:⭐⭐⭐⭐⭐
💡 提示:记录锁(Record Lock)、间隙锁(Gap Lock)、临键锁(Next-key Lock)。
MySQL 索引
何为索引?有什么作用?
👉 重要程度:⭐⭐⭐
💡 提示:索引的作用就相当于书的目录。
索引的优缺点
👉 重要程度:⭐⭐⭐⭐
💡 提示:索引并不都是好的,创建索引和维护索引也需要耗费资源和时间。
索引的底层数据结构
👉 重要程度:⭐⭐⭐⭐
💡 提示:在 MySQL 中,MyISAM 引擎和 InnoDB 引擎都是使用 B+Tree 作为索引结构,但是,两者的实现方式不太一样。
MySQL 的索引结构为什么使用 B+树?
👉 重要程度:⭐⭐⭐⭐
💡 提示:结合二叉查找树、平衡二叉树、红黑树、B 树存在的缺陷来回答这个问题。
主键索引和二级索引
👉 重要程度:⭐⭐⭐⭐
💡 提示:数据表的主键列使用的就是主键索引。二级索引又称为辅助索引,是因为二级索引的叶子节点存储的数据是主键。也就是说,通过二级索引,可以定位主键的位置。唯一索引,普通索引,前缀索引等索引属于二级索引。
聚集索引与非聚集索引
👉 重要程度:⭐⭐⭐⭐
💡 提示:聚集索引即索引结构和数据一起存放的索引。主键索引属于聚集索引。非聚集索引即索引结构和数据分开存放的索引。
覆盖索引
👉 重要程度:⭐⭐⭐⭐
💡 提示:覆盖索引即需要查询的字段正好是索引的字段,那么直接根据该索引,就可以查到数据了,而无需回表查询。
联合索引
👉 重要程度:⭐⭐⭐⭐
💡 提示:MySQL 中的索引可以以一定顺序引用多列。
最左前缀匹配原则
👉 重要程度:⭐⭐⭐⭐⭐
💡 提示:在 MySQL 建立联合索引时会遵守最左前缀匹配原则,即最左优先,在检索数据时从联合索引的最左边开始匹配。
创建索引的注意事项有哪些?
👉 重要程度:⭐⭐⭐⭐
💡 提示:选择合适的字段创建索引、尽可能的考虑建立联合索引而不是单列索引……。
MySQL 日志
MySQL 中常见的日志有哪些?
👉 重要程度:⭐⭐⭐
💡 提示:错误日志(error log)、二进制日志(binary log)、一般查询日志(general query log)、慢查询日志(slow query log) 、事务日志(redo log 和 undo log) ……。
慢查询日志有什么用?
👉 重要程度:⭐⭐⭐
💡 提示:慢查询日志记录了执行时间超过 long_query_time(默认是 10s)的所有查询,在我们解决 SQL 慢查询(SQL 执行时间过长)问题的时候经常会用到。
binlog 主要记录了什么?有什么用?
👉 重要程度:⭐⭐⭐⭐⭐
💡 提示:MySQL binlog(binary log 即二进制日志文件) 主要记录了 MySQL 数据库中数据的所有变化(数据库执行的所有 DDL 和 DML 语句)。
redo log 如何保证事务的持久性?
👉 重要程度:⭐⭐⭐⭐⭐
💡 提示:redo log 主要做的事情就是记录页的修改,比如某个页面某个偏移量处修改了几个字节的值以及具体被修改的内容是什么。在事务提交时,我们会将 redo log 按照刷盘策略刷到磁盘上去,这样即使 MySQL 宕机了,重启之后也能恢复未能写入磁盘的数据,从而保证事务的持久性。
页修改之后为什么不直接刷盘呢?
👉 重要程度:⭐⭐⭐⭐
💡 提示:性能非常差!InnoDB 页的大小一般为 16KB,而页又是磁盘和内存交互的基本单位。这就导致即使我们只修改了页中的几个字节数据,一次刷盘操作也需要将 16KB 大小的页整个都刷新到磁盘中。而且,这些修改的页可能并不相邻,也就是说这还是随机 IO。
binlog 和 redolog 有什么区别?
👉 重要程度:⭐⭐⭐⭐
💡 提示:可以从用途、写入方式、是否是 InnoDB 引擎特有的这几个方面来回答。
undo log 如何保证事务的原子性?
👉 重要程度:⭐⭐⭐⭐⭐
💡 提示:每一个事务对数据的修改都会被记录到 undo log ,当执行事务过程中出现错误或者需要执行回滚操作的话,MySQL 可以利用 undo log 将数据恢复到事务开始之前的状态。
性能优化
SQL 优化
👉 重要程度:⭐⭐⭐⭐⭐
💡 提示:SQL 优化涵盖的内容非常多,像正确使用索引、数据结构、数据库函数等等都属于 SQL 优化的范畴。另外,问到 SQL 优化的时候一定离不开 Explain 命令的使用!你可以参考下面这两篇文章来准备这个问题:
尽量要结合自己的项目来聊,具体说明自己是如何进行 SQL 优化的,具体带来了什么效果。
慢查询问题排查
👉 重要程度:⭐⭐⭐⭐⭐
💡 提示:记几个导致慢查询的常见原因,知道遇到慢查询问题了如何排查。具体做法可以参考这篇那些年我们一起优化的 SQL这篇文章。
这个也要尽量结合自己的项目来聊,具体说明自己如何解决慢查询问题的。
Redis
注意!!! :下面这些问题的参考答案你几乎都可以在 JavaGuide 在线阅读网站 和 《JavaGuide 面试指北》中找到。并且,这两个参考资料没有给出解答的问题,我也都给了对应的参考文章。
建议你先阅读学习了对应的内容之后再进行自测。
Redis 基础
Redis 有什么作用?为什么要用 Redis/为什么要用缓存?
👉 重要程度:⭐⭐⭐
💡 提示:内存数据库,高并发,常用来做缓存。
Redis 除了做缓存,还能做什么?
👉 重要程度:⭐⭐⭐⭐
💡 提示:分布式锁、限流、消息队列(不推荐)。另外,利用 Redis 自带的数据结构我们可以很方便地完成很多复杂的业务场景比如通过 sorted set 维护一份排行榜。
Redis 可以做消息队列么?
👉 重要程度:⭐⭐⭐
💡 提示:Redis 5.0 新增加的一个数据结构 Stream 可以用来做消息队列。不过,和专业的消息对象相比还是有很多欠缺的地方。
分布式缓存常见的技术选型方案有哪些?
👉 重要程度:⭐⭐⭐
💡 提示:Memcached 和 Redis。紧接着面试官可能会让你简单对比一下 Memcached 和 Redis 。
Redis 数据结构
Redis 常用的数据结构有哪些?
👉 重要程度:⭐⭐⭐⭐
💡 提示:
5 种基础数据类型:String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。
3 种特殊数据类型:HyperLogLogs(基数统计)、Bitmap (位存储)、geospatial (地理位置)。
面试官问到 Redis 常用的数据结构之后,可以会顺带问你 Redis 底层数据结构。
使用 Redis 统计网站 UV 怎么做?
👉 重要程度:⭐⭐⭐⭐⭐
💡 提示:可以借助 HyperLogLog 来做,占用空间非常非常小。
使用 Redis 实现一个排行榜怎么做?
👉 重要程度:⭐⭐⭐⭐⭐
💡 提示:Redis 中有一个叫做 sorted set 的数据结构经常被用在各种排行榜的场景下。
Redis 线程模型
Redis 单线程模型了解吗?
👉 重要程度:⭐⭐⭐⭐⭐
💡 提示:Redis 通过 IO 多路复用程序 来监听来自客户端的大量连接(或者说是监听多个 socket),它会将感兴趣的事件及类型(读、写)注册到内核中并监听每个事件是否发生。
类似问题:既然是单线程,那怎么监听大量的客户端连接呢?、为什么 Redis 这么快?
Redis6.0 之前为什么不使用多线程?
👉 重要程度:⭐⭐⭐
💡 提示:单线程编程容易并且更容易维护、Redis 的性能瓶颈不在 CPU 。
Redis6.0 之后为何引入了多线程?
👉 重要程度:⭐⭐⭐⭐
💡 提示:Redis6.0 引入多线程主要是为了提高网络 IO 读写性能。
Redis 内存管理
Redis 给缓存数据设置过期时间有啥用?
👉 重要程度:⭐⭐⭐⭐
💡 提示:内存是有限的,如果缓存中的所有数据都是一直保存的话,分分钟直接 Out of memory。
Redis 是如何判断数据是否过期的呢?
👉 重要程度:⭐⭐⭐⭐
💡 提示:Redis 通过一个叫做过期字典(可以看作是 hash 表)来保存数据过期的时间。
过期的数据的删除策略了解么?
👉 重要程度:⭐⭐⭐⭐
💡 提示:定期删除对内存更加友好,惰性删除对 CPU 更加友好。两者各有千秋,所以 Redis 采用的是 定期删除+惰性/懒汉式删除 。
Redis 内存淘汰机制了解么?
👉 重要程度:⭐⭐⭐⭐
💡 提示:Redis 提供 6 种数据淘汰策略。
类似问题:MySQL 里有 2000w 数据,Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据?
Redis 持久化机制
怎么保证 Redis 挂掉之后再重启数据可以进行恢复?
👉 重要程度:⭐⭐⭐⭐⭐
💡 提示:依赖持久化机制。
什么是 RDB 持久化?
👉 重要程度:⭐⭐⭐⭐⭐
💡 提示:Redis 可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。
什么是 AOF 持久化?
👉 重要程度:⭐⭐⭐⭐⭐
💡 提示:开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入到内存缓存 server.aof_buf 中,然后再根据 appendfsync 配置来决定何时将其同步到硬盘中的 AOF 文件。
Redis 4.0 对于持久化机制做了什么优化?
👉 重要程度:⭐⭐⭐⭐
💡 提示:Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 aof-use-rdb-preamble 开启)。
Redis 事务
如何使用 Redis 事务?
👉 重要程度:⭐⭐⭐⭐
💡 提示:Redis 可以通过 MULTI,EXEC,DISCARD 和 WATCH 等命令来实现事务(transaction)功能。
Redis 事务支持原子性吗?
👉 重要程度:⭐⭐⭐⭐
💡 提示:Redis 事务在运行错误的情况下,除了执行过程中出现错误的命令外,其他命令都能正常执行。并且,Redis 是不支持回滚(roll back)操作的。因此,Redis 事务其实是不满足原子性的(而且不满足持久性)。
Redis 事务还有什么缺陷?
👉 重要程度:⭐⭐⭐⭐
💡 提示:除了不满足原子性之外,事务中的每条命令都会与 Redis 服务器进行网络交互,这是比较浪费资源的行为。明明一次批量执行多个命令就可以了,这种操作实在是看不懂。
如何解决 Redis 事务的缺陷?
👉 重要程度:⭐⭐⭐⭐
💡 提示:Lua 脚本、 Redis functions。
Redis 性能优化
什么是 bigkey?有什么危害?
👉 重要程度:⭐⭐⭐
💡 提示:一个 key 对应的 value 所占用的内存比较大。bigkey 会消耗更多的内存空间,也会影响到性能。
如何发现 bigkey?
👉 重要程度:⭐⭐⭐
💡 提示:使用 Redis 自带的 —bigkeys 参数来查找或者分析 RDB 文件。
如何避免大量 key 集中过期?
👉 重要程度:⭐⭐⭐⭐⭐
💡 提示:给 key 设置随机过期时间 + 开启 lazy-free(惰性删除/延迟释放)。
什么是 Redis 内存碎片?为什么会有 Redis 内存碎片?
👉 重要程度:⭐⭐⭐⭐
💡 提示:内存碎片简单地理解为那些不可用的空闲内存。
Redis 生产问题
什么是缓存穿透?怎么解决?
👉 重要程度:⭐⭐⭐⭐⭐
💡 提示:大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上。常见的解决办法如下:
缓存无效 key
布隆过滤器
什么是缓存雪崩?怎么解决?
👉 重要程度:⭐⭐⭐⭐⭐
💡 提示:缓存在同一时间大面积的失效,后面的请求都直接落到了数据库上,造成数据库短时间内承受大量请求。 如果是 Redis 服务不可用的情况的话,我们应该搭建 Redis 集群来避免单点风险。如果是缓存过期的话,参考“如何避免大量 key 集中过期?”这个问题。
如何保证缓存和数据库数据的一致性?
👉 重要程度:⭐⭐⭐⭐⭐
💡 提示:3 种常见的缓存读写策略。
Redis 集群
如何保证 Redis 服务高可用?
👉 重要程度:⭐⭐⭐⭐⭐
💡 提示:Redis Sentinel 集群。
Sentinel(哨兵) 有什么作用?
👉 重要程度:⭐⭐⭐⭐
💡 提示:监控 Redis 节点的运行状态并自动实现故障转移。
Redis 缓存的数据量太大怎么办?
👉 重要程度:⭐⭐⭐⭐⭐
💡 提示:Redis Cluster。
Redis Cluster 虚拟槽分区有什么优点?
👉 重要程度:⭐⭐⭐⭐
💡 提示:解耦了数据和节点之间的关系,提升了集群的横向扩展性和容错性。
Redis Cluster 中的各个节点是如何实现数据一致性的?
👉 重要程度:⭐⭐⭐⭐
💡 提示:Gossip 协议
四、面经篇
必看
双非本科、0实习、0比赛项目经历。3个月上岸百度
前段时间,小贾在星球向我询问 offer 选择的问题,我才知道小贾已经斩获两个还不错的 offer。
小贾和我一样都是双非本科,学历上面我们和大部分一样都没有任何优势。他的校招经历挺波折的,非常有参考价值。
于是,我就找到小贾让他写一篇文章分享一下自己秋招的一些准备面试的经历以及经验。
贾哥写的太用心了,整篇文章大概有1w+字。我将分为两次来发。觉得内容不错的话,大家记得点赞催更。
希望贾哥的分享对小伙伴们有帮助!
01 关于我
秋招这一路跌跌撞撞的走来,经历了很多心酸,也成长了很多。
从信心满满的开始,到不断地自我怀疑。从一个一无所知的菜鸡,到现在还是一个菜鸟。
我或许没有很多成功的逆袭经验来分享给大家。但是!我从一个秋招的裸奔男孩到理想上岸,收获的更多是失败的经验、成长的阅历和人生的考验吧!
我对计算机并没有激情满满的热爱,更多的是随着投入的时间和学习而产生的兴趣吧!
听过很多秋招大佬的传奇逆袭经历,向往他们将热爱都投身到刷力扣的成就感中,羡慕他们在秋招时斩获大把Offer。
社会遵循着2-8原则,我或许应该被归到8这一类当中。我有时在不断问自己,你真的适合开发这一行吗?你会在这条路上走多远呀?评估自己的实力与大佬们的差距,可能就是我学习的动力吧!
作为一个被秋招毒打的打工人,我想和大家分享我的经历!
02 确立目标
带着高考的些许遗憾,我来到了我的母校,西安某不知名双非一本,专业为数字媒体技术。
这个专业虽然归类在计算机学院下,但是我们的课程方向是游戏动画,影视建模方向。
导致每次面试官问我专业,我都要解释一遍,我是计算机专业的,计算机的公共基础课(数据结构、计算机网络等)我们都会学。
我们的就业方向貌似更加偏向新媒体方向,虽然编程知识也会学,甚至还学了那本西瓜书的《机器学习》。
大学前两年,自己就是一种浑浑噩噩的状态。我没有很明确的目标和方向,每天都是在宿舍-食堂-教室,上好该上的课。
曾经想拿个综测的专业第一,但是好像光靠成绩还是不够的,后来标准降到了考试尽力考个高分就行。
对于学习数据结构、操作系统等等计算机专业课程,我有一个深深的感触:考试分数高不代表你真的“学会了” 。
这些基础课程,我基本都是上课认真听听,考前复习半个月,拿个不错的分数过了,感觉任务就完成了。
现在熬夜补这些知识的时候,眼里都是悔恨的泪水呀🥺。
大三,才意识到自己马上要毕业了,考虑了一个月,放弃考研的打算。我想了很久很久,感觉还是做一个打工人吧!
C/C++中的指针让我头晕眼花,于是我选择了Java。
2019年10月,开始了自己在大学里,真正有目标,有动力的去学习!
在一个失眠焦虑的夜晚,我写下这段话来激励自己:
今年在综测时,拿到了专业第一,可以申请保研(我校保研一般只能保本校)。也动摇过,秋招真的太难了,要不就放弃吧。但是想到自己大三时立下的雄心壮志,既然选择了这条路,就一抹黑的走下去吧,秋招不上岸,春招还能搏一把;这条路实在走不通,那我就考研!
然后,我就开始在B站、慕课网、油管、MOOC上找Java的视频学习。
从JavaSE、JavaWeb、框架的学习。2020年2月份,似乎感觉,把这些内容都过了一遍。
期间一边看网课、博客文章、Guide哥的专栏总结,一边写博客加深理解。寒假租了房,每天按部就班的输入,过年前几天才回家。过年那天晚上,都是一边看春晚,一边在复习。
03 压抑的一段时间
到3月份,认识的几个同学开始投滴滴、百度的实习,我才开始写简历,到牛客看面经,也准备投实习。但是,看到面经的各种提问,我感觉自己像没学一样,全都是知识盲区。
了解的东西不够深入,到不了面试那种深层次提问,还有数据结构、网络、操作系统这些都没怎么复习。自己学过的这些课,脑海里仅仅残留着一点点印象。
更关键的是,我简历写完了技能列表,项目实在没得可写。面对空白乏力的简历,我感觉自己还有好多好多知识要补,完全就是在精卫填海。
本来打算过完年早早去出租屋里学习,年前就定了正月除六的车票打算赶过去。但是,突如其来的疫情,只能让我待在家里,打乱了我安排好的学习计划。
每天,面对面经上满满的知识盲区,自己在家里的效率又比较低,开学又遥遥无期,学习计划一拖再拖。
同时,我的两位伙伴在5月都去到了北京实习,我还在家里天天感觉无所事事。
找实习已经是不可能了,只能直接秋招了。然而,项目经历还是空白,做过的课设项目含金量低,单纯的管理系统实在不想往简历上去写。
对比朋友每天大厂的实习日常,再看看自己的狼狈不堪。每天,整个人都有着巨大的心里压力和焦虑。学校在线的网课都是在后台静音放着,天天跑到教育厅下询问开学时间,“又是不开学的一天!哎,到底什么以后才能去学校呀!”。
那段时间,真的过得非常压抑,每天都是忐忑不安、内心焦躁。自己仿佛在一条漆黑的路上跌跌撞撞的走着,这条路没有光亮,没有尽头。
后来,心态渐渐放平,全国都在众志成城的抗击疫情,大家都在努力着。换个角度想想,自己最大的财富,不就是拥有健康吗?
为了赶上既定的任务安排,我只能每天早早起来学习,虽然中途可能被一些其他事情打断,但是用时间来弥补效率,一直复习到深夜。有时莫名感觉,自己20多年来,第一次真正的这么努力。
2020年6月,我不顾我妈的劝阻,来到了西安,和好基友小贤租了间房。他也没有找到实习,我们都是共赴秋招的裸奔男孩,两个人开始做秋招的最后冲刺!
04 复习基础知识
来到西安后,我便开始集中精力复习基础知识:
- 把多线程、集合类相关的知识重头复习了一遍,专门针对这一块的面试提问看了很多文章;
- 在B站刷了两遍宋红康老师讲的《JVM从入门到精通》,真的良心推荐👍,零零散散看了下《深入理解Java虚拟机》这本圣经;
- 复习了一遍计算机网络,主要是针对TCP-IP体系结构、HTTP协议,看着面经来复习知识点
- 数据库只做了简单复习,基本的SQL能写出来,牛客做了些题
眼看秋招提前批已到来,而且没有笔试,对我来说是个莫大的机会。但是,由于自己项目还没整理,没有可写的内容到简历上。所以只能任之溜走了。
这是对Guide哥之前的一次提问,让我很清楚自己接下来的两个月该做什么!
05 准备项目
7月份的时候,自己的项目经历还是空白,导致简历一直没法完善。
于是我开始着手开始准备项目。顺带着晚上刷题。
学校稍微有代表性的一点就是老师指导我们组做了个国家级的大创项目,但是我负责前端相关的内容。课设都是很基础的类似新闻管理系统、学生管理系统,还有Unity做的两个游戏Demo,实在没法往简历上写。自己学习的方向是后端,只能找有代表性的项目来做!
Github Star了些Java相关的项目,但当我拉下代码导入,发现自己搞不懂有些地方为什么要这样写,项目的架构是怎么设计的?关键的技术点在哪里?可能出现什么问题?如何去改善?
因为这些问题搞不懂,吃不透,虽然简历上写的是你的项目,但面试官一问就被问住了,所以终究还是不属于你。
由于自己底子薄,框架探究没那么深入,自己虽然学了SSM、SpringBoot这些框架,但是也只是能简单上手使用下。当下也没时间来深入探究底层原理学习,只能停留在简单了解和使用上。开源项目我可能没法吃透,我需要找个视频教程跟着做,然后基于自己理解再做拓展。
我把B站所有有关Java的项目都找了一遍,搜索不同的关键字足足过了三遍进行筛选统计。我发现项目大体可以分为两大类:
- 【原理性】:就是造轮子,对已有框架或者协议自己来做个实现;如Guide哥的RPC框架和HTTP的轻量级框架,其他的如实现Tomcat功能、性能基准测试框架、实现网络协议等
- 【功能性】:项目实现具体的业务功能;如各种权限管理系统、博客系统、商城、管理系统等。形式有前后端分离的,有基于微信小程序的后台的、还有客户端的
筛选了大概一周,我找到了适合自己的项目。一个是基于自己之前练手的Demo,跟着视频学习自己做了拓展,一个是前后端分离的项目。
项目没必要功能业务多么复杂,涉及的技术栈有多广,但是一定能够自己吃透,原理性、结构性的层面自己搞懂,还有一定要有亮点!
因为面试官想听的不是你做了什么,而是怎么去做的。就我而言,更多的是考察你发现问题、分析问题、解决问题的能力。即便项目本身简单,但是一些特殊情况要考虑到,为什么这么设计?出现问题了怎么改进?如何去完善?其他技术方式怎么实现?
在百度三面主管面时,全程都在问项目,大概问了50min之久。虽然我觉得准备时自己考虑的很周到了,但是毕竟没参加工作,很多问题根本不知道:
因为基于WebSocket协议做的聊天室,本身是应用层的协议,直接就用TCP来保证消息可靠传输,如果访问量大,为了高效可以改用UDP。这个项目准备的重心没有放在网络层面,而是考虑到多线程下并发聊天,会存在线程安全的问题,准备了很多多线程相关的针对项目的改善、应对策略,消息存储发送。
但是面试官全程都在针对网络层面做拓展,我只能根据已有的知识和对自己项目的拓展了解做回答。面试结束,我感觉自己被按在地上摩擦,又限了入了深深的自我怀疑中~
06 完善简历
到了 8 月份的时候,我才开始完善简历以及刷题。
我的简历大概前前后后改了十二版,最初是改简历的布局,内容块;后面就是字字斟酌,细微调整。
经常删删改改,一句话可能要思考好久;我把我掌握的知识点都很详细的列出来,虽然技能列表看起来很基础,但是我有自信对自己写的内容负责
小伙伴们一定要重视简历!多花点精力在完善简历上!
我的刷题大概从6月就已经开始,断断续续在LeetCode上刷一些题。在8月的时候,我开始每天集中抽出很多时间来刷题。
没错,大佬们天天坚持刷个一年半载,我7、8月才开始每天集中刷题。
我大三就意识到了刷题得重要性,因为做题能力差,报了蓝桥杯比赛没去。
既然意识到重要性,为什么不早点去每天坚持刷题呢?
我尝试过,最终放弃了。这么做可能更多是临时抱佛脚的心态,对刚做完的题有个印象。
对我来说,复习路上最大的阻碍就是刷题了,因为自己的代码能力实在太差了。
三月份,我大概做了半个月题。《剑指Offer》上的常规题,我基本上就是半天一道题,因为自己做这些题实在是想不来,想半个小时尝试去解决,但大多时候都是“差一点”,或者思路正确但又不能用代码实现出来。然后看题解,看别人不同的解法,自己再独立写一遍。
因为时间紧任务重,半天能够让我复习好多知识点了,所以想等复习完提纲之后再来刷题。而且,关键是做的题目,当时感觉自己懂了、会了,但是过一段时间又忘了,只能隐约留下个解题思路,还是不能够独立AC。
七月份,只能是逼着自己来。因为大厂太看重代码能力了,即便是我理论知识掌握的再好,笔试都过不了,根本没得机会去面试。
然后,就开始分类刷题。参考labuladong哥的刷题套路,weiwei哥的刷题分类,小齐姐的刷题经验,剑指OfferKrahets路飞哥的精彩题解,每天花8个小时左右刷题,复习数据结构。
一道单链表反转的题,我整整想了一天半才搞懂。该题下的所有题解全部看了一遍,包括公众号的一些文章。递归的解法,短短几句话,我始终无法理解。
小贤从4月份一直开始刷题,在这期间一直和小贤在一起复习。他是C++方向,算法和代码能力很强,刷题方面我都是请教他的。
单链表递归解法,他画图整整给我解释了一个晚上,从斐波那契的递归,到链表的实现。第二天,我终于搞懂了,在力扣发布了自己写的最认真的一次题解。单链表反转,自己写了不下20遍了吧;这次,可能真的是永远记住了吧。
8月份,小贤由于有事回家了。房间只剩我一个人,我和老板续了房租,继续备战秋招。
期间,刷题有任何问题,我都会立即给小贤打电话过去交流。
【刷题的误区】
开始,我觉得自己不是在刷题,而是不断地重复写,好像在“背代码”。因为有些题说思路,我能够很清晰的表达出来,做的多了发现解题的套路还是比较固定的(虽然也没做多少🤔),但是到实际的动手写,又写不出来了。
针对这个问题,我也很痛苦。一方面觉得“背代码”很可耻,自己真的就这么差吗,做个简单题都写不出来吗?但是,我真的是没办法,只能用做的少,练得少来安慰自己。
就这样,每天逼着自己,刷了大概170题左右,每天将基础的八大排序写一遍
其实,前期的刷题,自己没见过没思路很正常,参考别人的题解,把这种解法引用到类似的题目上。就像写作文一样,针对不同问题有不同的模板,根据具体问题调整边界即可。我自己总结来说,就是两大因素:
- 针对不同问题求解的代码模板,要恰当灵活的应用(如双指针、滑窗、列表DP等)
- 代码熟练度。模板是基于代码的熟练度而存在的,就像写排序算法一样能够很快的写出来
但是,这个量还有我的认知,对秋招来说是远远不够的。这是一项长期的积累和训练,谁也不可能偷懒,达到立竿见影的效果。因此,在后来的秋招笔试中,我重重的摔了跟头😭,这是可预见的。
听学姐说她们去年是互联网的寒冬,找工作难。今年,因为疫情的原因,仿佛一切都变得更难,竞争更加激烈。
八月,2020年的秋招已正式开始,但是我还在刷题复习中,准备即将到来的“金九银十”。这份简历,整整迟投出一个月……
07 开始投递简历
总是喜欢一个人到新田径场静静呆坐着
9 月 1 号返校,在陕西省教育厅下蹲了四个月,终于等到了学校开学。我退了房,回到了宿舍,准备加入秋招得大军中…
9 月 2 日,开始正式投递简历。一开始不敢投大厂,想着先投中小公司刷刷副本。
👇 以下是通过语雀记录的!
我将公司归为四类:小厂、实习、中厂、一线厂。
为了好做做统一管理,我记录了投递时间、岗位、笔试时间、面试时间。
9 月 8 号之前,我一直以中小厂为主,因为自己感觉没实力和自信去投大厂。
但是,到 9 月 9 号时,我才了解到像腾讯、百度、美团、京东、网易这些大厂秋招到 9 月中下旬就会截止网申。
不投就没有机会了!投了起码能有一丝机会进入笔试面试,所以就开始投递大厂了。
我主要投递的都是 Java 研发,自己学的就是这一块的内容。随着学习的深入,也对 Java 后端开发产生了兴趣。
但是!因为投递较晚,好多大厂都没了研发的 HC,我就投测开岗。没 Java 后端开发,我就投移动端,C++,甚至 PHP。
我才发现,原来,今年的秋招 8 月甚至 7 月就开始了。
秋招一般是从暑假那会就已经开始了!很多公司尤其是大厂都有提前批!
我一直想着是等自己复习完,刷些题,准备好了再去投简历;但是,机会是不等人的。
我至今,都没敢投字节和阿里的秋招岗。好几次点入到官网的链接,又退了出来。因为自己知道没能力过得了笔试这一关。
我想:“春招一定要去弥补这个遗憾,通过笔试,一定要去争取到面试机会!”
机会,并不是等你准备好了才来的。 这句话,可能是秋招给我最惨痛的一个教训。机会本来就瞬转即逝,你必须时刻准备着!
期间,陆续有简历被挂的情况。我参考了网上 IT 相关的简历不下 20 份,简历改了那么多次,到底是哪里出问题了呢?
我自己一个字一个字的读了遍简历,还是觉得没有问题。找了个已工作的学长询问,学长说你作为一个双非本,可能更看重实习经历吧!
是呀,我是双非本,还没有实习经历,比赛经历。唯一能写的奖项,就是连续两年获得国家励志奖学金和学校的综测奖学金吧。所以说自己是秋招中裸奔的人,没有任何光环加持,只能跌跌撞撞的摸索。
08 我的第一场正式面试
同秋招的同学,8 月份开始投简历,9 月每天都有平均一场面试。
再看看自己,投了简历,做测评,笔试,然后就没音讯……然后每天一边焦虑,一边自我安慰。
白天投简历,晚上复习…..好希望有个公司能够面我一下,哪怕是给我挂了,我也心甘情愿,面到就是赚到。
终于,在 9 月 13 日,我收到了好未来的面试,也是我人生中一次正式的面试,岗位为测试开发。
我也不奢望自己能进入二面,只要能被面到,积累面试经验就可以了。 而且,今年绝大多数公司都是线上面试,这个还是线下。
我很紧张和畏惧,尤其还是测开岗位,自己对测试的知识根本没有接触过。
但是,我还是积极准备,恶补了下测试的相关知识,看了下面经,第二天早早到了指定地方。
在叫到我进去面试时,我看了一眼堆排,这样感觉更安心些。
我从容的来到面试厅,因为早已知道自己肯定会挂,所以也不那么紧张了。
面试厅有 20 多个面试官在针对到场的同学进行面试,和我一同走进去的,还有个西安交大的小姐姐,也是测开,我们两的面试官也是挨着的。
一面的面试官非常好,也正是他,给了备受打击的我一点自信,给了我很多的建议和指导。
如果将来我有机会做面试官,一定也要做像这位前辈一样谦逊、耐心的人。
他问了我为什么要做测试,我说:“我刚接触测试,自己一直学习 Java 开发线管的知识,觉得测试是从另一个角度来思考问题。测试更加注重问题的细节,逆向的思维,全局的观念,我觉得和研发互为补充,所以想尝试一下”。
期间,面试官问了我登录跳转的一个测试用例,可能对测试的同学来说是入门级的简单题。但是,我之前完全没接触过,只能靠着之前恶补的知识和自己写 Demo 时的经验,把能想到的情况全说了一遍。
一面面试官一直很耐心的听着,并没有因为我说的不正确或者跑偏而打断我。我回答完后,又很耐心的给我指出我错误的地方,同时给我写了测试开发在项目研发中参与的流程和工作,让我有了个主观的认知。又和我谈了些职业规划,给了我宝贵的建议。
剩下的就是一道基础的算法题(括号匹配),我竟然写出来了。还有网络和语言的基础问题,写了三个 SQL 语句。
面试持续了 45 分,面试完,我很真诚的说了感谢。我说:“这是我秋招开始投递简历以来,第一次参加面试。非常感谢您给了我这样的机会,同时谢谢您的建议和指导”。遇到给我带来启迪的面试官,真的十分的荣幸!
很意外,我对测试完全不知道的门外汉,面试官竟然给我过了,等待第二轮面试。
二面是在结果之中,自己没有对测试知识的了解,写算法题也没做出来,就挂了。
当我收拾东西准备推门离开的那一刻,刚好那个小姐姐面试通过,进去参加 HR 面。
我淘汰失败,她晋级成功。不同的方向,不同的结果。虽早已知晓答案,但心中还是羡慕。啥时候自己才能上岸呀!
09 被笔试毒打的日子
好未来面完,我基本上没啥面试了。每天就是在做测评,笔试,然后就没音讯…
期间,大厂的笔试题,对我这个算法菜鸡来说,简直就是被吊着锤 🥺。
大厂的笔试题,基本最多 AC 一道,剩下的只能是 A 一部分,边界情况基本都没改对过。DP 相关的题,我都是只写个框架,过个 0.X 这样。因为我只能保证一个小时左右才能拿下一道题,其他题没时间考虑;要么思路是正确的,本地跑通,提交就是过一半多,边界改不对。
这就导致我根本无缘大厂,第一关笔试就被挡在了门外。
影响最深刻的是美团,5 道题两个小时,我一道都没做出来。剩下的两道有半点思路,也只过了 0.2、0.15。很多大厂和独角兽企业的笔试,基本都是笔试完了,就真的完了!甚至,有好多测评和笔试都没给……
幸运的是,通过了小米和百度的笔试,拿到了面试机会。可能是运气来了吧,均为 3 道题,分别过了 2.1 和 2.4。
中小厂的还凑合,笔试题做的还行,大部分笔试完都能争取到面试,但面试都集中在了 10 月。
唯一笔试完有些许成就感的就是巨人和阅文,4 道题全 AK 了,但至今仍没音讯,可能自己投的太晚了哈哈~
对当晚参加完的笔试,第二天我几乎要花一天的时间来消化昨天的题目,请教牛客评论区的大佬们,自己再试着做一遍,好多还是做不出来。
DP 虐我千百遍,再见了依然是相逢何必曾相识~ 看着几十行的题目描述,有时甚至读多遍题都抽象不出问题模型,20 多分下来,依然无从下手。
九月,真的是非常难熬的一个月。每天都在投简历,做测评,笔试,理解笔试题。
准备了这么久,一次次被笔试挡在了门外,都没机会表达自己了解的知识。面对为数不多的几场面试,自己力不从心,很难把握住。
从开始时希望满满的投递简历,发现都是笔试一轮游。到后面自暴自弃不想去投递。再到自我安慰的去投递,相关公司的岗位都投一遍,因为不投递连一点点机会都没有,我也不期望一次上岸,只希望多积攒些面试经验。
我只想要个面试机会,哪怕面一次挂了也行啊!
没有面试,我就到牛客网上参加模拟面试,对着 AI 讲。及时记录自己的盲区,知道但是表述不完整,还有长时间没复习遗忘的知识。唯一有所慰藉的是,9 月末拿到了钜泉科技的 Offer,偏硬件的测试开发岗。
虽然与预期的岗位不符,但是好歹在 9 月拿到了自己的第一个 Offer,算是给挫败的自己一点小小的鼓励吧!我这菜鸡,还是有公司要的呀。还剩一个多月,加油,一定可以的!
10 获得百度 Offer
国庆八天假期,我给自己放了两天假,其实就是倒头大睡了两天。印象中每年的国庆假期,西安仿佛都在下雨。
睡醒了就躺在床上想:“自己还有什么知识点没掌握牢固?每次开篇的自我介绍该怎么表达才能让面试官印象深刻?结束时针对面试官的“你还有什么要问我的吗” 该怎么去提问?想到了就立刻记下来,自己改怎么去更正”。
10 月 3 日,我起的很早,开始了 10 月份的战斗准备。剩下的 6 天,重点就是在复习数据库。
我把 MySQL 锁,索引,事务等相关的面试高频知识点结合面经总结了一遍。因为 9 月面 CVTE 时,问了很多数据库相关的问题,而我只会写写简单的 SQL 语句,问到时都是一脸懵。
期间,加了个内推群,了解到今年由于疫情的原因,群里好多海归、985/211 研究生大佬们都还是 0-Offer,对比自己目前的境况,又有些释然。
排除最重要的主观因素个人能力不说,今年这样的大环境,竞争真的是异常激烈。 即便是海归或者高校的光环加持,大家求职也和我有类似的情况。
所以,我已经做好了春招的打算。一边投正式岗,一边投大厂的实习岗。
由于实习没笔试,能争取到面试,面到就是赚到!(但是,到现在为止实习被捞起来的,也只有滴滴一家,可能大部分都是针对 22 届的吧。三面时项目回答的不是太好,笔试题写了太久才勉强做出来,最终还是挂了!)
我告诉自己,当机会没来的时候,你一定要做好准备,等待它,把握它! 一定要沉下心来,不断复盘自己之前的面试,及时查漏补缺,巩固知识点。
十月,貌似自己积攒了很久的好运和人品来了。陆续拿到了恒生,大华,泛微,苏宁,闻泰,ThoughtWorks 的 Offer,最意外的,还是收获了百度的 Offer。
点开邮件的那一刻,我没有丝毫的兴奋和激动。因为我觉得这不是真的,一定是 HR 发错邮件了吧!
回想当时一天三轮的技术面,三面主管面时问项目问到我说不出话,感觉答的那么差,当时面试结束就知道没了。
但也不亏,踩了很多坑,这是第一次真正面大厂,果然难度很大。自己也没查过官网的状态变化,因为面试结束的那一刻,我已经知道自己“挂了”的结果。
所以,我觉得这就和我开个玩笑吧!当天夜里,我一直在想这是不是真的。问了百度实习转正的同学,说我拿到 Offer 了,等着 HR 谈薪就好了。
我辗转反侧,一边喜悦,我终于上岸大厂了!一边顾虑,万一是 HR 发错了或者还不能十拿九稳怎么办,因为意向书上并没有我的名字,虽然我可以登录填写信息的入职后台。
就是这种纠结与矛盾,让我理智了下来。我告诉自己:“就当是个以外的惊喜吧,是的话当然如愿了;不是的话,不止于心里落差太大。你还是要全力备战所剩不多的机会,就当这个惊喜不存在”!
后面,也走完了小米,TW,滴滴的技术面试流程。
可能,这就是运气的推波助澜吧!让不可能变成了可能。感谢百度收了我,对我的认可。
我相信此刻还在找工作,或者准备参加春招的小伙伴们,你们一定很焦虑。如人饮水,冷暖自知。
有的时候,并不是你不够努力和优秀,而是属于你的那一份好运和机遇还没到吧。
以我个人为例,我觉得找工作就是 能力 + 机遇。
- 70%是个人实力。因为你的专业素养足够强,你才能胜任你要求职的工作岗位
- 30%是机遇(运气)吧。有的时候,当运气来的时候,以你的能力为支撑,你的求职真的是一帆风顺的。
小贤最近才找到自己理想的工作。以他为例,我觉得就是运气来的稍微晚些吧!
我觉得在算法做题方面,他比我真的厉害很多;专业知识和项目等都掌握的很牢固。但是,面试很多都是最后一轮技术面面完就没音讯了,他也很苦恼,很焦虑,准备去实习春招了。但是,就在昨天,他也理想上岸了。
秋招到现在,已基本结束。
现在,自己的算法、刷题能力依然是一塌糊涂!可能开始刷题对我来说会有一种恐惧感,拿到一道题,首先不是去想这道题该怎么做,而是我能不能做得出来。
但是,自己经历过一天做一道题的那种痛苦期,现在能够很客观的去对待刷题这件事,心里已经消除了这种恐惧感,多刷多积累即可。
就像我开始对编程并不感冒,完全是投入的时间和经历让我觉得做这件事是有意义的,慢慢才产生了兴趣。
11 下一站是未来
我不确定自己能在这条路上走多远,因为人生充满了挑战与无限可能,面对日新月异的技术更迭,终身学习才能保持竞争力而不会被淘汰!
既然做出了选择,就要坚持走下去;技术没有强弱之分,只有接触的先后之差;能力不够,就多花时间和经历来沉淀。
每次当我笔试面试完失意时,就会循环放《追梦赤子心》:“关于理想我从来没选择放弃,即使在灰头土脸的日子里”。
也许每个失意的人,都需要找一个点来慰藉自己。这并不是引人肺腑的鸡汤文,而是迷茫挫败时的自我鼓励,当你内心有了坚定的追求,愿望和希望才会驱使你去奋斗,你才能有勇气和毅力走出眼前的困境。
校园生活即将落下帷幕,打工人的生涯才刚刚开始。秋招对我来说不仅仅是招聘,更重要的是它为我迈出社会的第一步做了警醒。
大学四年,我没有什么很值得骄傲的经历,可能就是一个默默无闻的“平凡带学生”吧。但是,平凡,不能平庸。大学四年里,我好像从来没有把一件事情给做好过,这一次,我想要专心做好一件事,让自己不留遗憾。
我始终坚信一句话:凡事皆有可能,永远别说永远!
2年经验,2021 阿里、头条、美团,滴滴,京东面经
最近一段时间面试了几家互联网公司,陆续通过了阿里、头条、美团,滴滴,京东的面试,基本上面试的公司都通过了,所以在这里想分享一些自己面试的经验给大家,希望能帮助大家拿到心仪的 offer
我的基本情况:19 届本科,现在在一家小公司,毕业一年半,后端开发
面试准备
简历
重点放在专业技能和项目经验两块
- 你的简历就是你给面试官提供的考点,简历上的东西必须自己 Hold 住,万一自己写的东西被问住了,会很尴尬,给面试官留下的印象也不好,所以就是会啥写啥
- 技术栈最好不要写精通,你敢写面试官就敢问,被问倒了很尴尬的,写熟悉,了解就行
怎么投简历
我这里强烈建议找人内推,这样简历通过的概率大些,如果找不到,可以试试脉脉,我就是从脉脉投的简历,把状态改成寻找机会就行,会有很多人找你的
推荐一个简历制作模版,我一直用的: https://www.polebrief.com/index 。
算法
这个该刷还是得刷,别偷懒,我个人感觉刷完下面几个已经够了,大家可以根据自己的基础情况选择
- 剑指 Offer:https://www.nowcoder.com/ta/coding-interviews
- 刷 Leetcode,刷 Leetcode,刷 Leetcode!重要的事情说三遍,Leetcode 前 200 道
- 经典排序算法:https://blog.csdn.net/qq_35508033/article/details/109399281
复习
我复习主要以看书为主,推荐一些我看的书籍和资料,有时间的话尽量看的细一点,多看几遍,没时间的话就挑重点看
- 并发编程:Java 并发编程的艺术,Java 发编程实战
- JVM:深入理解 Java 虚拟机
- Redis:Redis 设计与实现,Redis 开发与运维
- MySQL:高性能 MySQL,MySQL 技术内幕
- SpringBoot 和 SpringCloud:https://blog.didispace.com/
- Kafka:Apache Kafka 实战
- 设计模式:大话设计模式,设计模式之禅
- 分布式:从 Paxos 到 Zookeeper 分布式一致性原理与实践
需要书籍的 pdf 文档可以关注我的公众号,月伴飞鱼,回复 666 获取
项目经验
社招面试项目很重要,不光是你项目本身的技术复杂度,还有业务复杂度,你本身在项目中担任的什么角色,遇到过什么问题,瓶颈在哪,怎么解决的,这几个问题是非常重要的,很多公司到最后基本上都是围绕着你的项目在问,给面试官讲明白你的项目是必须具备的能力
总结下社招面试问项目最主要的问题套路:
- 你项目为什么这么设计,你这样设计有什么好处,解决了什么问题,会产生什么问题,还有什么可以优化的
- 这么设计有什么瓶颈吗,遇到了什么问题,有什么改善的方案
- 项目遇到的难点,技术挑战,你是怎么解决的,为什么用这种方式解决,还有更好的方式么
- 根据你简历上提到的具体功能去扣细节
面试技巧
- 面试得自信且谦虚,声音自信,面试表现谦虚,得给面试官一种你啥都会,很稳的感觉(实际内心很慌),然后语言表达流畅,吐字清晰,回答问题也要有逻辑性,不能支支吾吾半天说不明白,面试官都听不懂,这就很尴尬了,这个可以自己多练习一下
- 面试本质是一个自我优势展示的过程,不要让面试官问一句自己回答一句,主动抛出一些可能的点让面试官来主动问你,还有就是不会的问题就说不会,这个没关系的,千万别瞎说
- 不要眼高手低,不少小伙伴看面经觉得自己啥都会,但是会与面试过程中能清晰有层次的说出来是两回事,费曼学习法可以了解一下,举个例子:比如 sychronized 的原理,能不能说出点面试官眼前一亮的东西,这还是不容易的,其实面试主要是证明你比别人更有技术的深度,广度,不然都是背八股文,那面试官看不出你有什么不一样的,这个面试过的概率就大大降低了
个人建议,面试没准备好,不要随便面试,一些大厂都会有面试评价记录,太多差评影响以后的面试,同时面完之后要多总结,复盘,整理知识点,查漏补缺
面试最后
面试结束时问面试官什么问题
我一般会问:
- 我面试的岗位的具体工作是什么
- 使用的技术栈有哪些
面试总结
阿里的面试更倾向于实用性,基本是从各种场景出发,来给你一个场景,让你来解决实际的问题,那么在解决问题的过程中,对于各种知识的应用就是亮点了
头条更看重计算机基础,算法,以及对各种中间件的了解
面试也有不少的运气成分的,毕竟每个面试官的侧重点可能不一样,大家放平心态就好
学习建议
学习要形成自己的知识体系,不要天天盯着别人的面经(当然,我的面经可以看,哈哈)做碎片化学习,面经只是辅助作用,查漏补缺的,一旦你的知识体系有了,很多问题都能举一反三,这时候面试就很稳了
下面是热乎乎的面经
注意:有些面试的题目比较少,因为有些面试题因为会被多个公司重复问 ,就不重复写了
美团
一面
- 线程安全的类有哪些,平时有使用么,用来解决什么问题
- mysql 日志文件有哪些,分别介绍下作用
- 你们项目为什么用 redis,快在哪,怎么保证高性能,高并发的
- redis 字典结构,hash 冲突怎么办,rehash,负载因子
- jvm 了解哪些参数,用过哪些指令
- zookeeper 的基本原理,数据模型,znode 类型,应用场景有哪些
- 一个热榜功能怎么设计,怎么设计缓存,如何保证缓存和数据库的一致性
- 容器化技术了解么,主要解决什么问题,原理是什么
- 算法:对于一个字符串,计算其中最长回文子串的长度
- 项目介绍
美团
因为之前的部门一面通过后,该部门没有 hc 了,就给我推荐到其他部门了,大厂 hc 还是挺紧张的
一面
- redis 集群,为什么是 16384,哨兵模式,选举过程,会有脑裂问题么,raft 算法,优缺点
- jvm 类加载器,自定义类加载器,双亲委派机制,优缺点,tomcat 类加载机制
- tomcat 热部署,热加载了解么,怎么做到的
- cms 收集器过程,g1 收集器原理,怎么实现可预测停顿的,region 的大小,结构
- 内存溢出,内存泄漏遇到过么,什么场景产生的,怎么解决的
- 锁升级过程,轻量锁可以变成偏向锁么,偏向锁可以变成无锁么,自旋锁,对象头结构,锁状态变化过程
- kafka 重平衡,重启服务怎么保证 kafka 不发生重平衡,有什么方案
- 怎么理解分布式和微服务,为什么要拆分服务,会产生什么问题,怎么解决这些问题
- 你们用的什么消息中间件,kafka,为什么用 kafka,高吞吐量,怎么保证高吞吐量的,设计模型,零拷贝
- 算法 1:给定一个长度为 N 的整形数组 arr,其中有 N 个互不相等的自然数 1-N,请实现 arr 的排序,但是不要把下标 0∼N−1 位置上的数通过直接赋值的方式替换成 1∼N
- 算法 2:判断一个树是否是平衡二叉树
二面
- Innodb 的结构了解么,磁盘页和缓存区是怎么配合,以及查找的,缓冲区和磁盘数据不一致怎么办,mysql 突然宕机了会出现数据丢失么
- redis 字符串实现,sds 和 c 区别,空间预分配
- redis 有序集合怎么实现的,跳表是什么,往跳表添加一个元素的过程,添加和获取元素,获取分数的时间复杂度,为什么不用红黑树,红黑树有什么特点,左旋右旋操作
- io 模型了解么,多路复用,selete,poll,epoll,epoll 的结构,怎么注册事件,et 和 lt 模式
- 怎么理解高可用,如何保证高可用,有什么弊端,熔断机制,怎么实现
- 对于高并发怎么看,怎么算高并发,你们项目有么,如果有会产生什么问题,怎么解决
- 项目介绍
- 算法:给定一个二叉树,请计算节点值之和最大的路径的节点值之和是多少,这个路径的开始节点和结束节点可以是二叉树中的任意节点
三面
- 项目介绍
- 算法:求一个 float 数的立方根,牛顿迭代法
- 什么时候能入职,你对岗位的期望是什么
- 你还在面其他公司么,目前是一个什么流程
阿里
一面
- synchronized 原理,怎么保证可重入性,可见性,抛异常怎么办,和 lock 锁的区别,2 个线程同时访问 synchronized 的静态方法,2 个线程同时访问一个 synchronized 静态方法和非静态方法,分别怎么进行
- volatile 作用,原理,怎么保证可见性的,内存屏障
- 你了解那些锁,乐观锁和悲观锁,为什么读要加锁,乐观锁为什么适合读场景,写场景不行么,会有什么问题,cas 原理
- 什么情况下产生死锁,怎么排查,怎么解决
- 一致性 hash 原理,解决什么问题,数据倾斜,为什么是 2 的 32 次方,20 次方可以么
- redis 缓存穿透,布隆过滤器,怎么使用,有什么问题,怎么解决这个问题
- redis 分布式锁,过期时间怎么定的,如果一个业务执行时间比较长,锁过期了怎么办,怎么保证释放锁的一个原子性,你们 redis 是集群的么,讲讲 redlock 算法
- mysql 事务,acid,实现原理,脏读,脏写,隔离级别,实现原理,mvcc,幻读,间隙锁原理,什么情况下会使用间隙锁,锁失效怎么办,其他锁了解么,行锁,表锁
- mysql 索引左前缀原理,怎么优化,哪些字段适合建索引,索引有什么优缺点
- 线上遇到过慢查询么,怎么定位,优化的,explain,using filesort 表示什么意思,产生原因,怎么解决
- 怎么理解幂等性,有遇到过实际场景么,怎么解决的,为什么用 redis,redis 过期了或者数据没了怎么办
二面
- hashmap 原理,put 和 get,为什么是 8 转红黑树,红黑树节点添加过程,什么时候扩容,为什么是 0.75,扩容步骤,为什么分高低位,1.7 到 1.8 有什么优化,hash 算法做了哪些优化,头插法有什么问题,为什么线程不安全
- arraylist 原理,为什么数组加 transient,add 和 get 时间复杂度,扩容原理,和 linkedlist 区别,原理,分别在什么场景下使用,为什么
- 了解哪些并发工具类
- reentrantlock 的实现原理,加锁和释放锁的一个过程,aqs,公平和非公平,可重入,可中断怎么实现的
- concurrenthashmap 原理,put,get,size,扩容,怎么保证线程安全的,1.7 和 1.8 的区别,为什么用 synchronized,分段锁有什么问题,hash 算法做了哪些优化
- threadlocal 用过么,什么场景下使用的,原理,hash 冲突怎么办,扩容实现,会有线程安全问题么,内存泄漏产生原因,怎么解决
- 垃圾收集算法,各有什么优缺点,gc roots 有哪些,什么情况下会发生 full gc
- 了解哪些设计模式,工厂,策略,装饰者,桥接模式讲讲,单例模式会有什么问题
- 对 spring aop 的理解,解决什么问题,实现原理,jdk 动态代理,cglib 区别,优缺点,怎么实现方法的调用的
- mysql 中有一个索引(a,b,c),有一条 sql,where a = 1 and b > 1 and c =1;可以用到索引么,为什么没用到,B+树的结构,为什么不用红黑树,B 树,一千万的数据大概多少次 io
- mysql 聚簇索引,覆盖索引,底层结构,主键索引,没有主键怎么办,会自己生成主键为什么还要自定义主键,自动生成的主键有什么问题
- redis 线程模型,单线程有什么优缺点,为什么单线程能保证高性能,什么情况下会出现阻塞,怎么解决
- kafka 是怎么保证高可用性的,讲讲它的设计架构,为什么读写都在主分区,这样有什么优缺点
- 了解 DDD 么,不是很了解
- 你平时是怎么学习的
- 项目介绍
三面
- 线程有哪些状态,等待状态怎么产生,死锁状态的变化过程,中止状态,interrupt()方法
- 你怎么理解线程安全,哪些场景会产生线程安全问题,有什么解决办法
- mysql 多事务执行会产生哪些问题,怎么解决这些问题
- 分库分表做过么,怎么做到不停机扩容,双写数据丢失怎么办,跨库事务怎么解决
- 你们用的 redis 集群么,扩容的过程,各个节点间怎么通信的
- 对象一定分配在堆上么,JIT,分层编译,逃逸分析
- es 的写入,查询过程,底层实现,为什么这么设计
- es 集群,脑裂问题,怎么产生的,如何解决
- while(true)里面一直 new thread().start()会有什么问题
- socket 了解么,tcp 和 udp 的实现区别,不了解,用的不多
- 设计一个秒杀系统能承受千万级并发,如果 redis 也扛不住了怎么办
- 项目介绍
四面
- 讲讲你最熟悉的技术,jvm,mysql,redis,具体哪方面
- new Object[100]对象大小,它的一个对象引用大小,对象头结构
- mysql 主从复制,主从延时怎么解决
- 怎么保证 redis 和 mysql 的一致性,redis 网络原因执行超时了会执行成功么,那不成功怎么保证数据一致性
- redis 持久化过程,aof 持久化会出现阻塞么,一般什么情况下使用 rdb,aof
- 线上有遇到大流量的情况么,产生了什么问题,为什么数据库 2000qps 就撑不住了,有想过原因么,你们当时怎么处理的
- 限流怎么做,如果让你设计一个限流系统,怎么实现
- dubbo 和 spring cloud 区别,具体区别,分别什么场景使用
- 给了几个场景解决分布式事务问题
- 项目介绍
- 你觉得你们的业务对公司有什么实际价值,体现在哪,有什么数据指标么
五面
hr 面完后又来了一面,说是交叉面
- 怎么理解用户态,内核态,为什么要分级别,有几种转换的方式,怎么转换的,转换失败怎么办
- 怎么理解异常,它的作用是什么,你们工作中是怎么使用的
- 你们用 redis 么,用来做什么,什么场景使用的,遇到过什么问题,怎么解决的
- jvm 元空间内存结构,永久代有什么问题
- 你平时开发中怎么解决问题,假如现在线上有一个告警,你的解决思路,过程
- 你们为什么要用 mq,遇到过什么问题么,怎么就解决的
- 你觉得和友商相比,你们的优势在哪
- 聊天:炒股么,为什么买 B 站,天天用,看好他
菜鸟
不知道为啥可以同时两个流程,可能真的缺人(想去阿里的大家抓紧机会)
算是给我 2 次选择机会了,面了几面(2 面只用了 11 分钟,哈哈),主要问项目了
抖音
感觉头条不怎么问项目,或许是我项目太 low 了,比较喜欢问计算机基础和中间件知识
一面
- http 请求头,expire,cache-control 字段,状态码,301,302,401,403
- https 原理,数字签名,数字证书,非对称加密算法过程,有什么问题
- tcp 连接 client 和 server 有哪些状态,time_wait 状态
- 虚拟内存,虚拟地址和物理地址怎么转换,内存分段,内存分页,优缺点
- linux 最多可以建立多少个 tcp 连接,client 端,server 端,超过了怎么办
- eureka 原理,强一致性么,为什么,怎么保证强一致性,多级缓存怎么保证一致性,eureka 集群,宕机了服务还能调用么
- hystrix 原理,半开状态知道么,具体的一个转换过程,它的隔离是怎么实现的
- zookeeper 一致性保证,zab 协议原理,半数原则如果查询到另外一半呢,那 zookeeper 属于哪种一致性,强一致性么,还是最终一致性
- zookeeper 选举机制,选举过程有什么问题
- 算法:最长不重复的连续子串
- 聊天:头条为什么用 go,对 java 和 go 怎么看,愿意转 go 么
二面
- 函数 a 调用函数 b 的过程,是怎么传参的
- java 里面的函数调用有哪些,io 流里面有函数调用么
- fork 函数,父子进程的区别,孤儿进程,僵尸进程会有什么问题,进程有哪些状态,进程间怎么同步,通信,消息队列,管道怎么实现的,进程调度算法,各有什么优缺点
- dos 攻击,ddos 攻击,drdos 攻击,怎么解决,syn flood
- 自旋锁,线程上下文切换的开销具体是什么,中断,有哪些中断,用户态和内核态切换过程
- 一张大表怎么更改表的数据结构,字段,用 alter 会有什么问题,怎么解决呢,有什么好的方案,双写的话会有什么问题,还有其他方案么
- redis 管道用过么,用来做什么,它的原理是,保证原子性么,和事务的区别,redis 事务保证原子性么
- redis 强一致性么,怎么保证强一致性,有什么方案
- kafka 怎么保证消息不丢失的
- 算法:找出所有相加之和为 n 的 k 个数的组合,组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字,输入: k = 3, x = 9 输出: [[1,2,6], [1,3,5], [2,3,4]]
三面
感觉面试官很忙,就问了几个电商场景的技术解决方案
算法:一个环上有 10 个点,编号为 0-9,从 0 点出发,每步可以顺时针到下一个点,也可以逆时针到上一个点,求:经过 n 步又回到 0 点有多少种不同的走法
举例:
如果 n=1,则从 0 出发只能到 1 或者 9,不可能回到 0,共 0 种走法
如果 n=2,则从 0 出发有 4 条路径:0->1->2, 0->1->0, 0->9->8, 0->9->0,其中有两条回到了 0 点,故一共有 2 种走法
快手
感觉像刷 kpi 的,一看 JD 发现招的资深研发工程师,我对快手的印象又不好了
一面
手写 hashmap(卒)
滴滴
一面
- 排序算法了解哪些,快排,快排复杂度,优化,堆排序,建堆过程
- 反射了解么,原理是什么
- treemap 和 linkdedhashmap 区别,实现原理
- jvm 类加载的过程讲讲,符号引用是什么,哪些情况会发生初始化
- spring 的循环依赖,怎么解决的,为什么需要加个三级缓存,二级不行么
- springboot 有什么特点,相比与 spring,了解 springboot 的自动装配的一个原理么
- kafka 支持事务么,你们项目中有使用么,它的原理是什么
- 怎么统计一亿用户的日活,hyperloglog 有什么缺点,bitmap 不行么
- 算法:求一个环形链表的环的长度
二面
- redis 的几种数据类型,你们用过哪些,zset 有用来做什么
- 垃圾收集器,cms 垃圾收集过程,为什么停顿时间短,有什么缺点,concurrent mode failure 怎么办,内存碎片怎么解决,为什么不用标记整理法
- 线程池原理,核心参数,线程数设置,参数动态调整后变化过程,Tomcat 线程池原理,常用的线程池,你们一般使用哪种,为什么,会有什么问题,线程抛异常怎么办,阻塞队列原理
- 做过分库分表么,为什么要分库分表,会有什么问题,多少数据适合分库分表,跨库,聚合操作怎么做
- 项目介绍
- 算法:给定一个二叉树, 找到该树中两个指定节点的最近公共祖先
- 你对自己有什么规划,想学习什么技术,最近在看什么书
三面
- nio 讲讲,实现原理,优缺点
- 了解 netty 么,讲讲 netty 的设计模型,架构,使用场景
- zookeeper 读写数据过程
- 项目介绍
京东
一面
- tcp 和 udp 的区别,tcp 怎么保证可靠连接的,出现网络拥塞怎么解决
- tcp 和 udp 的报文结构了解么
- 给了一个业务场景写 sql 语句
- 你们建表会定义自增 id 么,为什么,自增 id 用完了怎么办
- 一般你们怎么建 mysql 索引,基于什么原则,遇到过索引失效的情况么,怎么优化的
- jvm 内存结构,堆结构,栈结构,a+b 操作数栈过程,方法返回地址什么时候回收,程序计数器什么时候为空
- redis 实现分布式锁,还有其他方式么,zookeeper 怎么实现,各有什么有缺点,你们为什么用 redis 实现
- 算法:返回一个树的左视图
二面
- spring 你比较了解哪方面,讲讲,生命周期,bean 创建过程
- 使用过事务么,遇到过事务失效的情况么,原因是什么
- springboot 是怎么加载类的,通过什么方式
- 什么对象会进入老年代,eden 和 survivor 比例可以调整么,参数是什么,调整后会有什么问题
- 微信朋友圈设计,点赞,评论功能实现,拉黑呢,redis 数据没了怎么办
- 项目介绍
- 算法:给你两个非空的链表,表示两个非负的整数。它们每位数字都是按照逆序的方式存储的,并且每个节点只能存储一位数字。
- 请你将两个数相加,并以相同形式返回一个表示和的链表
三面
感觉面试官对 es 很熟悉,一直问 es 问题
- es 倒排索引,原理,lucene,分词,分片,副本
- es 写数据原理,数据实时么,为什么不实时,会丢数据么,segment,cache,buffer,translog 关系
- es 深度分页,优化
- 项目介绍
- 算法:验证二叉搜索树
秋招拿下科大讯飞 SSP 经验分享
这是一位学弟热乎的面经。这位学弟本 211 硕 985,秋招大大小小拿了十几个 offer,三方签了荣耀 15b,两方签了科大讯飞(薪资等同 15a),京东实习转正。
这份面经非常详细,希望对你有帮助!
下面是正文。
个人情况
本 211 硕 985,半科班,一段研究所 C++实习,一段京东 Java 实习。几个专利,几个小比赛。秋招主要面向中小厂拿了十几个 OFFER,终签约科大讯飞(SSP,40W 左右)。
时间路线表
下面是我从开始学习直到现在的时间路线表。
- 2020 年十月到 2021 年五月学习 C++,后续转为学习 Java 后端。
- 2021 年暑期研究所实习 C++。
- 2021 年七月开始学习力扣,至 2022 年 3 月初,300 余道;截止目前近 500 道。学习力扣先看了 B 站 UP 主爱学习的是饲养员的入门课程,另外就是左程云的课程。其他基本是依靠力扣官网的解答。开了一个季度的力扣会员。
- 2021 年七月开始学习 Java 基础,集合,多线程,JavaWeb,SSM,SpringBoot。这些我基本是学习的尚硅谷的课程。当然也有一些是黑马程序员(例如 JUC、Java 集合)的。十月,开始了第一个项目:尚融宝。学习尚融宝我大致明白了 SpringBoot 怎么用,SSM 的一些基础知识,会一些 ElementUI 、Vue,NodeJs,EasyExcel 组件。以及对中间件 MySQL,Redis,单点登录有一定的了解。但尚融宝项目较大,其实我做了一半就面临找实习压力中途终止了。
- 2022 年三月一日,开始了我秋招第一场面试,阿里云一面。我初生牛犊不怕虎,有很多没答上,我依照自己的想法试着猜测底层实现方式,面试官最后给我过了,并且对我进行了一个比较高了评价。然而,阿里云二面,没有这么幸运,由于八股根本没准备过,二十分钟草草结束了。
- 2022 年三月,我开始学习八股文。我找实习期间的资料主要来自公众号代码界的小白、公众号 JavaGuide、美团技术博客、掘金和 CSDN 文章、UP 主跟着 Mic 学架构,以及牛客面经,知识星球 的 《Java 面试指北》。八股文的背诵主要通过面试加深印象,往往被面试官问住的八股你后续记忆是最清楚的。也许你这个时候还没准备好,那么试试面面小公司吧,面试小公司你可以大致了解到 Java 后端面试的重点集中在哪些地方。
- 2022 年三月十日左右,面了一个西安的小公司,外企风格,一场面试手写了四个题,写出了三个半(最后一个是字典树)。我拿到了第一个 OFFER,但规模确实很小,或许并不是我想要的,我就拒了。不过给我找实习路上增加了信心。
- 2022 年四月,我开始了第二个项目:牛客论坛。我选择牛客论坛的原因很简单:论坛具有一定的实用性,论坛包含的技术栈 MQ、ES 我还没了解过,以及 Spring 框架使用我不熟练。于是花了一个多月把牛客论坛做完了。同期,深入学习了 Redis 和 Mysql。
- 2022 年四月,京东一面,面试官上来就是一道回溯题,四个场景,说能写几个场景写几个场景,但写题时间只有半小时。我写了第一个,第二个一直有问题,我同面试官讲“我可能做不完,我讲一下我的思路”。面试官安慰我“我还是希望你把第二个写出来,你可以 debug,多打印一下中间变量”。于是我找到了 arraylist 拷贝的时候我只拷贝了地址,后面改为了构造函数拷贝就对了。总共写了 2 个场景的代码,顺利地,一面过了,二面也过了。五月,HR OC。
- 2022 年暑期,去京东实习了。在京东学习了很多,编码规范,部门的技术文章,组内系统的设计方案,每周的质量周会,跳点分析,以及实习导师对我严格的要求。实习期间主要负责做了一个并发场景下的组件,自己得到了锻炼。很幸运呆在一个氛围很不错的组,实习转正成功。同期,深入学习了 ES,独自开发过程中踩了 Spring 一些常见的坑。
- 2022 年 7 月初,开始了我的秋招。我的意向工作地点是成都。秋招我主要是通过官网投递简历,部分意向一般的企业是通过牛客一键投递的,国企大多是用前程无忧投递,意向度较高的企业是通过官网投递保证能进招聘的系统里。秋招的过程中,系统学习了小林 Codeing 的 Http 协议、TCP/IP 协议、操作系统相关知识,一个名为“Java 全栈知识体系”的网站中的基础知识,也学习了一个叫做“老齐谈架构”UP 主的一些视频,受益匪浅。提升了面试过程中场景题的应对能力。写了半个 Redis,跟着 B 站诸葛老师写了一个仅包含 IOC 和 AOP 功能的 Mini-Spring。同期,深入学习了 Kafka,了解了 SpringCloud 中的部分组件。
- 秋招我给自己定的是循序渐进的目标,初期希望薪资 15w,拿到 15w offer 再找 20w,再 25w,30w。其实我没想到秋招能找个 40w 左右的,主要原因是今年寒气逼人,Java 很卷,成都岗位数量少,大厂难进。后面讯飞打来电话,薪资我很满意,就签约上岸了。
实习面试
小厂实习面试
Java 基础
- HashMap 的底层实现
- 快排有没有了解,归并和快排的区别,快排要递归吗
- 抽象类和接口区别
- Java 线程状态如何变迁
- 线程怎么进入等待
- 用 put get lock unlock notiy 设计阻塞队列
- 什么是协程
- 什么是观察者模式
- 讲一下堆插入元素的详细过程以及堆的应用
JVM
- 讲一下 JVM 内存回收机制
- 讲一下 Stop-The-World
网络与操作系统
- TCP/IP
- TCP 与 UDP 对比
- TCP 哪些机制保证可靠传输
- HTTP 协议
中间件部分
- mybatis 分页插件
- springboot 注解
- springboot 线程池创建
- 微服务和分布式谈一下理解
- 对象存储存什么,OSS 权限管理怎么保证用户隐私
- 微服务远程调用
- 消息队列
- 单点登录
- 说一下事务
- 说一下 redis
- Mybatis 缓存了解吗
- 了解 JPA 吗
- 自定义配置文件的读取方式有哪些
中大厂实习面试面经
实习主要是通过 boss 直聘投递简历,简历单薄,导致投递反馈率比较低。好在东哥给了个机会,不然找实习大概率要灰溜溜收场了。
阿里云实习二面
- Mysql 查询(出生日期,性别)在表(id,性别,年龄,出生日期)中怎么设置索引
- springboot 怎么实现自动装配?用到 springboot 哪些功能
- 进程与线程的区别
- 线程间的通信
- redis 设置过期时间的命令
- 你的使用场景 redis 宕机了怎么办
- 你的数据字典存放有优化方案吗
- 你的 redis 的使用场景
- arraylist 扩容机制?具体怎么扩容
- 如何让 hash 表里的数据 value 排序输出
- treemap 是对 key 还是对 value 进行排序
- 讲一下 TCP 的滑动窗口
蚂蚁支付宝实习一面
- 面向对象的三大特性,讲一下封装
- mysql 索引怎么选择?索引的优缺点?还有什么缺点
- mysql 事务的特性?什么是持久性
- 业务里 redis 的过期策略设置
- hashmap 的扩容机制?为什么扩容选择 2 倍。conhashmap 是线程安全的吗?怎么保证是线程安全的
- 线程的创建方式
- 进程间的通信方式讲一下
- 讲一下 tcp 和 udp 区别
- tcp 建立连接后怎么保证可靠传输的
- 说一下快排,快排是稳定的吗?归并的稳定的吗?哪些排序算法是稳定哪些是不稳定的
- jvm 垃圾回收机制,怎么找到垃圾、怎么回收垃圾
- redis 的缓存击穿、穿透、雪崩各是什么情况
- 乐观锁、悲观锁、讲一下 cas,典型场景
- 读过开源项目源码吗?当项目领导你会怎么安排前后端人员工作
- 业务中的对象存储隐私问题怎么解决
字节暑期实习一面
- 讲一下索引的你的理解
- 事务的特性
- 讲一下存储引擎,各有什么区别
- MyISAM 与 InnoDB 的区别
- 数据库隔离级别
- 讲一下三次握手、四次分手具体
- 如何保证可靠传输
- 点击一个 url 如何处理
- http 状态码讲一下
- 进程和线程的区别
- 进程间的通信,具体应用场景
- 写 sql,查出总成绩排名 3-10 名的 id
- 行升序二维数组的 top k
- 最长不重复子字符串
京东实习一面
- 一道回溯题
- String 是不是基本类型、与 StringBuffer、与 StringBuilder 区别
- 索引失效的场景、场景题的索引设计
- oss 数据库与 mysql 数据库不一致怎么解决
- hashmap 原理、数组和链表的区别
- redis 使用场景
- 异常和错误的区别
京东实习二面
- 个人爱好
- 你觉得好的商业模式
秋招面试
秋招面试概况
秋招部分面试题
以下是我秋招过程中出现的部分面试八股,大多是经过回忆简单记录下,若有八股问题不全请海涵。
Java 基础
- HashMap
- 讲一下 TreeMap、HashMap、HashTable 的区别
- 排序稳定性
- 抽象类和接口的区别
- 继承和重写
- 敏捷开发模型
JVM
- 垃圾回收算法
- GCroot 有哪些
- 垃圾回收器选择原则
- 运行时数据区包含哪些
并发多线程
- 进程和线程的区别
- 什么是死锁,死锁怎么解决
- 线程池参数
- 线程间的同步
- 并发编程包里有哪些常用 API
- 讲一下线程的同步
网络与操作系统
- 网络模型的分层,网络模型为什么要分层
- 讲 https 是否安全
- tcp 为什么是三次握手,而不是两次或者四次握手
- tcp 和 udp 的区别
- udp 的特点
- AWK grep 了解吗
- 是否用过管道
- 多进程编程
- 讲一下进程间的通信
- 并发的锁机制
- 用户态和内核态的区别
- 虚拟内存和管道的选用
- 讲一下 gdb
- 虚拟内存的作用
- 多线程会用到虚拟内存吗
- 虚拟地址
- 软中断了解吗
- 零拷贝拷贝几次
数据库
- 索引是什么,讲下索引类型
- 数据库的隔离级别
- mySQL 怎么用游标
- 慢 sql 优化
- mySQL 死锁怎么解决?mySQL 不能解决死锁的原因
- sql 执行计划 range index 等
- 同一条 sql,不同规模数据会走同一条索引吗
- mysql 删除一列 SQL 语句
SSM
- 如何解决容器初始化 bean A 前初始化 bean B
- 讲一下 SpringCloud
- 微服务的远程调用有哪些可以实现
中间件
redis 的应用场景
redis 的淘汰策略
redis 的过期策略
kafka 丢失消息和重发消息怎么解决
Kafka 消息丢失
Kafka 能否保证幂等性
了解 rabbitMQ 吗
es 为什么快,技术选型为什么不用 mongdb 或者 mysql
场景题
- 上传多个 zip 文件到 oss,设计一个方案,需要前端展示上传进度
- 项目怎么分工的,有几个人,如何安排的方案
- 配置连接的账号密码怎么保证安全性
- 讲一下提交登录信息需要用到哪些注解
- 服务器开发,问安全性如何保证,接口安全性
综合问题
- 你的性格
- 兴趣爱好
- 实习的收获
- 描述最有意义的一件事
- 最自豪的事儿
- 讲下代码的最佳实践
- 操作系统怎么学
- Java 怎么学的,为什么不学其他语言
- 说下金庸或者金庸武侠里的人物,说下最近了解的实事
- 源于创新性的体现,一分钟内说下报纸的用途
手写代码
- 反转每对括号间的子串
- 最长不重复字符串
- 两数之和
- 复原 IP 地址
- 找出最长的对称的字符串
- 二叉搜索树的判定
- 树的层序遍历
最后
我所信奉的秋招原则:永远没有准备好的时候,尽可能早的投递公司;算法题需要多做,大一点的公司(非国企)都会考算法;选择就业方向并充满信心,坚持下去;不要孤军作战,与同学做到互通有无。
我的秋招面试经验:对于某些不会的知识点,你可以用你的猜想去表达而不是不说话。面试官考验的不仅仅是你的知识储备,求职者跟面试官的沟通也同等重要。
2022
双非本秋招总结与心路历程,上岸 OPPO
一位学弟的面经,非常详细地记录了自己的学习过程和心路历程。
下面是正文。
我的学历是双非本、文科学校(非杭电、深大或邮电类计算机强校),科班,leetcode 300+,后端开发方向。
秋招从 7 月中上旬开始到 11 月中旬,共投递近 200 家公司,约面公司约 14 家,有几家面试后来推掉了,今天最后一个池子终于开了,总结最终收获如下:
- 签约:OPPO
- Offer/OC/预录取:4399、政采云、乐刻运动、玄武云
说一下自己的学习过程和心路吧,就当个故事听吧,真心希望对大家有帮助。(长文预警!!!)
兴趣的起源
初中的时候同学都在看 DC 的超英剧闪电侠、绿箭侠之类的,我也跟着看,后来被安利了钢铁侠了,于是就一发不可收拾,我疯狂迷恋上了钢铁侠的高科技战甲,也为我之后职业选择埋下伏笔。
自己高中的时候其实是就读于潮汕地区数一数二的高中,高一高二都不好好学习,因为喜欢钢铁侠的缘故,去参加学校的科技社团搞机器人比赛,当时一直梦想着造出一个类似钢铁侠的外骨骼装甲,和同学一起造的机器人拿了省一等奖还搞了个专利,也因此认识了现在的女朋友。
Guide:这里划重点,认识了现在的女朋友!
之后高三一年也没多拼命学,最后只考上了广州本地一个普通一本。在专业选择上本来也想读自动化、机械电子之类的专业,但是因为想留在广东、女朋友、学校性质等原因,最后选择了现在这个外语学校的计算机专业,也是学校里唯一的工科专业了,也算是和自己兴趣比较接近的专业了,
但是近几年因为疫情和国际形势等的原因,学校的高考分数线不断下降,今年甚至爆冷上了热搜第二,我们自己都开玩笑说要变成带专了。上了这个双非大学心理落差其实非常大,因为自己初中成绩是年级一直第一才考上这么好的高中,高中全级的平均水平也是华工这样的 985,所以自己在高中就是个吊车尾的差生,和高中老同学一起时也会感到些许自卑。
走上后端开发这条路
上了大学后加入了学校里的一个专门做开发的 IT 社团,从大一开始就学习后端开发,从 PHP 到 Go,最后再到 Java,做了大大小小不少项目,也帮学校开发了一个社团管理系统并被学校采用(我们社团其中一项重要工作就是帮学校建设一些数字化系统)。在我们学校某一天举办社团节活动上,这个项目需要被学校里很多同学使用,当天流量 PV 达到 10 万+,当时也非常有成就感,更加坚定了我要走后端开发的路。
还有就是去年社团里很多师兄师姐都进了大厂,而且还有微信的 HR 主动联系我们团队要简历,让我感觉虽然学历差点,但是努力提高技术水平,大厂还是很有希望的,也以此作为自己的一个目标和动力继续前进。自己也一直在这个团队里干了三年才退休,第三年也是担任了团队的最高负责人,参加这个团队是对我大学影响最大的一个经历。
实习经历
第一份实习是大二暑假时找的,面试进入了一个师姐的创业项目,一个小公司,没有办公场地只能线上办公,优点就是可以和学校的事情比较好地平衡时间,最后干了几个月就准备找下一份了实习了,还是想体会线下实习上班的感觉,这次实习的收获主要是积累了个项目经验。离开后我就开始系统化准备八股,冲三四月春招的实习了。
找第二份实习的过程给我挺大的打击,当时大中小厂都有投 ,先拿小厂练手后初步掌握了面试的感觉后,就开始面大中厂了。首先面了阿里,答得还不错,但是等了很久没有消息,问了内推人说过了,等面试官抽时间二面,结果之后又告诉我找到更合适的人选了。。。
之后被另一个部门捞了也是一面挂;之后面了网易又是一面挂,问到挺多自己盲区的,也意识到自己很多知识掌握的漏洞;最难受的是虎牙,一二面体验都不错,一面面试官水平很高,二面聊项目聊得很开心,面试官人也很好,结果苦等两周后 HR 说你很不错,但是我们的 hc 锁了(受腾讯影响),所有面试者的流程都冻结了,又在牛客上问了一个比我早一点面试然后成功进去实习的老哥,他说组里开会的时候说觉得我挺不错的想让我去,可惜被锁 hc 了,还说想帮我申请特批,但是这行情下人人自危最后也就作罢。
得知被锁 hc 后我非常难受,想不通为什么自己这么倒霉,两周里满怀希望、心心念念,结果不是自己实力不够,而是时代尘埃落在自己身上的一座大山,这个时候其实就已经开始感觉到行情非常不对劲了,寒气第一次真真切切地影响到了我。但好在最后拿到了数字广东实习的 Offer,很感谢数广的收留,当时学校里很多人也是去数广实习,因为今年数广真的是广州本地少数有在招实习生而且招得还不少的企业了,就凭这一点我吹爆数广,挽救了寒冬中不少人的实习下落!
实习期间也是第一次体会打工人上班以及企业开发的流程,组里氛围也非常好,办工环境也很 nice,各种下午茶福利也很多,非常难忘快乐的实习经历。因为疫情不能随意进出校门,最后也是和同学一起去外面租房了。同时了解到同级里搞前端、C++、网安、客户端的同学都找到了比我好很多的中大厂实习,年级里基本没几个搞 Java 后端的,所以那时也很郁闷,因为除了算法,Java 在开发领域的卷度说第二没人敢说第一,当时也开始有点后悔选了 Java 这个方向。
艰难的秋招
七月下旬在数广辞职,组里的老大很想留我,甚至后来有确定有转正 hc 还叫我回去,非常感动,但是由于自己还是想专心秋招就还是没有留下了。然后就是正式开启的秋招之旅。
七月的时候最开始投了十多家,然后就开始复习。虽然春招找实习的时候已经感受到了寒气,加上学历差更加举步维艰,但是还是相信秋招作为应届生最大规模的校招,情况肯定还是会好转些的,但是现实让我狠狠打脸。七月剩下的日子和整个八月除了零星测评和笔试外,一个面试都没有。我开始慌了,八月下旬的时候开始海投几十家,而且同时听说另外一个同学早就投超过七八十家了还是 0 面试,所以心态开始有点小崩。之后到九月之前的日子也是测评笔试轮流转,还是一个面试也没有,当情绪到达低谷的时候,终于迎来了些许曙光。。。
九月,我终于开始有第一场面试,先是本地一家做数据库的小厂,再是 4399,广发银行、政采云等,慢慢就好了起来,面试也渐入佳境。9 月底也拿到了第一家意向——政采云,也是 9 月进的几个中小厂池子里最满意的一个,就有点想躺了。而且这个时候其实面了 OPPO 的一面,当时觉得这个时间点约我这个学历的来面是铁 KPI,但是这是我秋招所有有面试机会的公司中最大的一家,怎么能暴殄天物呢?所以也老老实实面了,但是心里属实没有报太大希望。国庆时也和女朋友出去耍了两天。
十月过完国庆后,又有多了几家中小厂的面试,比如途虎、百奥、拖了一个多月的 4399 终面、宁波银行深分等,同时也 9 月积攒的池子又开了一两个小厂,虽然有政采云保底,但是因为面试也不少,加上国庆又摆了几天,所以精神还是比较紧绷的。同时 OPPO 也迎来二面综合面,面试官非常和蔼可亲,会引导我,而且第一次有面试官愿意看我 GitHub,最后面完 11 点多了还赶紧让我去吃饭,当时面完心情也是非常好,想着就算是挂也是值了,有一个好的面试官真的真的对面试体验起到非常大的影响,非常感谢这个二面面试官,当时一整天心里都暖暖的。
之后不久也约了 HR 面,然后看到大家都在说 HR 面秒挂,我也没有抱太大的希望了,但是作为一场面试无论如何还是要尽力去准备和表现的,这是态度问题。看到别人的面经中会问是否用过或了解 OPPO 产品等的问题时,想到自己作为一个前 OPPO 用户,高中毕业典礼时还把当时的 OPPO A59m 给摔碎屏了哈哈哈。在这个问题上自己还是认真去准备了的,包括去官网了解各种手机型号、不同的产品线、自研芯片、生态等,了解了 OPPO 的三大发展战略,马里亚纳、潘塔纳尔等等。。。但是最后居然没问到,有点可惜。
过了一周后,进了池子并且很快就开到了(应该刚好赶上那一批),发了意向并在两天后正式 Offer,自己非常惊讶,本来已经做好去杭州的准备了,思考了几个被捞到的原因:除了前面的大佬拒了(一般是拿到了别的大厂),还有可能是和二面面试官聊得很来,最后就是之前虎牙锁我 hc 给的我攒下的人品吧。。。
之后零零散散还有些厂的约面,游卡、CVTE 等都拒了,开摆了,笔试也是,然后等到 11 月中旬最后一家 4399 才开出来,也是等了很久。最后也没啥悬念签下绿厂,结束了 23 届秋招这场异常艰难的战役。
所想与感悟
所以秋招也让我明白一个道理,找工作不像高考一样,可以精确地用分数来量化你是否达到了录用的门槛,每个高校都有自己非常明确的分数线,过了就是过了,没过就是没过;而找工作则是除了自己技术能力上达到要求之外(问的问题都基本答得出来),可能还和过往的经历、面试过程中对自己能力的展现、遇到的面试官的态度、与面试官是否有眼缘、其它竞争者的情况、前面是否有大佬拒了、公司的经营情况、hc 数量等等因素综合影响下,最后得出的结果。所以其实大家无论最后秋招确定去了哪里,符合预期也好,不符合预期也好,都要相信技术实力永远是决定你的下限的兜底,只要你技术摆在那,无论如何之后结果都不会太差的,校招不理想可以社招再跳,人生不会仅仅因为一场秋招而被定性,就像不会被一场高考而定性一样。
回看自己走过的长长的路,可以说是在寒冬中蹒跚前进,最后到达一个冰雪暂时消融的地方小憩,并期盼着春暖花开来临之际。在最后还算顺利地结束秋招之后,自己心里还是比较惶恐的,毕竟很怕进去之后身边都是各种 92 硕大佬被吊打,所以还是要保持一个持续学习不断前进的状态,同时也要搞毕设了。当然还是好好地享受一下成为社畜之前为数不多的假期,重拾下健身、好好打游戏之类的。
一些建议
如果是要给 24 届或之后的师弟师妹们一些建议的话,我觉得网上一些总所周知的就不说了,毕竟 Java 人从来不缺学习资料,比如刷题用代码随想录、八股看 JavaGuide 这些我就不再赘述了,应该说只要是行内人都非常熟悉了吧,说下几个我个人觉得比较实用的建议吧:
- 关于提高面试通过率和项目准备上,我也是看了一个 b 站一个 up 主的分享觉得非常一针见血,打算分享给大家。大概意思就是说在项目中要体现自己的思考,通过思考的过程把项目的发展串联起来,引起面试官的兴趣,层层诱敌深入,让自己掌握面试的主导权,既然面试官被你引导得基本在聊你最熟悉的项目,问到你不会的八股的概率就大大减少了,面试通过率自然也就提高了。具体大家可以仔细品品原视频:https://www.bilibili.com/video/BV1oP4y1U769/
- 关于赛道选择上,如果自己学历普通比如双非本科,想提高自己进大厂的概率,并且对自己实力比较有信心,可以不做我们普通的前后端开发,转而选择做基础架构方向,搞开源,例如牛客上最有名的 A 佬和 hzh 佬,他们都是我非常崇拜的大神,也是今年寒冬还可以疯狂收割大厂的本科生。当然难度上自然会高不少,但是也意味着能和你卷同个方向的人也少了很多。
- 关于软实力上,我觉得可能也是面试中很多人会忽略的一个点。比如你面试时表现出来的精神面貌、你的沟通表达或者说语言的运用能力、你的礼貌、你对一个问题是否有自己独到的看法(独立思考能力)等等。。。可能有人认为这不是非技术岗才会看的东西吗,我认为就算是技术岗这些同样不可忽视,因为等你步入职场后,人际交往、沟通表达等软实力也是非常重要的(更深的含义自己体会了),程序员不能仅仅只会写那么几个代码。
最后
最后非常感谢你能看到这里,希望我的经历和建议对你有些许帮助,不管是能力提升上的作用也好还是精神上的激励也好,最后送给大家一句计算机之神 Knuth 老爷子的名言,希望真正热爱技术的大家可以坚持自己的热爱:
A programmer who subconsciously views himself as an artist will enjoy what he does and will do it better.
NLP 转后端开发,顺利拿到字节实习 offer!!
简单介绍一下我的基本情况,上海双非本 + 985 硕,目前在读研二。本科 + 研究生大部分时间都在搞算法, NLP 相关,今年终于认识到了理想和现实的差距,遂在 5 月份下定决心转后端开发。之后处理一些事情,暑假 7 月底的时候正式开始学习 Java,历时三个月, 9 月底开始投日常实习,10 月 17 结束。面试过的公司包括: B 站,蔚来,百度以及字节,B 站挂在二面,其他均拿到日常实习的 Offer,最终选择了字节跳动。
从以上我的基本情况大家能看出,我的战线比较短。但是在这些时间里,我的作息基本上都是早 7 晚 11,很辛苦。
这篇文章首先分享我的面试经历,后面的部分与大家分享我的学习经验。
面经部分
下面的部分总结了面试过程中被问到的知识点(还记得的部分),以及我个人的一些心得体会,供大家参考。
百度
百度给我的面试体验还是蛮好的,是我面试的所有公司中唯一一家对八股考察非常详细的。一面是非常详细的八股面试,涉及到 Java 基础知识、数据库、spring、jvm、多线程、场景设计等等,基本上准备的都被问过了,但是整体来说难度不高,知识面比较广但是不会深挖。二面来说区别就非常大了,注重实践能力的考查,而且会深入到底层原理。
面试之前会有自我介绍的环节,包括项目介绍。百度两面对我的项目提问都不多,可能是我介绍项目的时候就比较详细的原因。
接下来的部分是知识点整理:
百度一面(1h+):
- 常用 GC 算法,常用的垃圾收集器, G1 了解吗
- 场景题: cpu 打满且频繁 full GC,怎么解决?
- 有 jvm 调优的经验吗?实际工作中遇到过内存相关的问题吗?用过哪些堆栈工具调试?
- Mysql 索引,数据结构为什么使用 B+ 树
- 索引覆盖了解吗
- 索引失效的场景
- 简单描述一下数据库的四种隔离级别以及对应的三种相关问题
- MVCC + 锁 保证隔离性
- 造成幻读的原因了解吗,快照读、当前读。
- 数据库自增 ID 和 UUID 对比
- HashMap 源码,数据结构,如何避免哈希冲突,对比 HashTable
- HashMap 源码中,计算 hash 值为什么有一个 高 16 位 和 低 16 位异或的过程?
- 为什么重写 equals 还要重写 hashCode,不重写会有什么问题
- ConcurrentHashMap 底层实现,扩容问题。
- 如果让你自己实现哈希表,你会考虑什么问题?
- 场景题:亿级别黑名单、短链接,你考虑使用什么数据结构?布隆过滤器、前缀树。其中布隆过滤器问了基本的原理和实现方式
- Java 引用类型,强软弱虚
- Java 是引用传递还是值传递
- Object 类你了解哪些方法
- 接口和抽象类的区别
- 线程池核心参数,以及工作原理
- ReentrantLock 对比 sync 锁
- lockInterruptibly()、acquire()、tryAcquire() 方法
- CAS 机制了解吗,存在什么问题
- 对象锁和类锁的区别
- 如果让你自己实现阻塞队列,如何实现?阻塞唤醒这一部分,如何实现?
- ThreadLocal ,Volatile
- 看你项目中用到了 Netty,简单介绍下吧。这里还有个 问题是问到 Netty 和 SpringBoot 整合的,但我一直都没理解她想问什么
- 粘包拆包问题,Netty 解决粘包拆包的 Decoder
- Spring 事务了解吗,Spring 事务的注解不生效,是什么原因
- 算法题: 手写快速排序,时间复杂度,稳定性
整理感觉不错,基本都答上来了,按照 Guide 哥星球里的内容,认真准备就好。
百度二面(45 min):
- 看你项目中用了一致性哈希做负载均衡,简单介绍一下
- 项目中 CompletableFuture 如何使用的
- 算法题:给定一个字符串,找到其中最长回文串
- 计网和组成原理学过吧,你认为哪个掌握的好? 我选了计网。这一部分问的很深入,我没来得及记录,以下部分只是一些零散片段,但是整体问的时候是有逻辑的。
- OSI 七层模型
- TCP 三次握手,四次挥手整个过程包括状态的转换。为什么是三次握手、四次挥手。发送 Fin ,实际的意义代表什么?(发送方没有数据要发送了,可以断开连接)
- 四次挥手,为什么等待 2 MSL
- 流量控制、拥塞控制
- 后面关于网络就更深入了,TCP 底层是怎么实现的,如果让你用 Java 模拟 TCP 的过程,做一个仿真,你有什么想法。大学学习计网的时候,协议栈之类的了解过吗(这部分我都不懂,认栽了)
- 看你项目中用到了 Spring,自动装配的过程了解吗。
- Spring 启动类的注解,介绍一下
- 因为我项目中用到了,所以被提问了 Spring 二次开发常用的扩展点,还涉及到了 Bean 的生命周期。 BeanPostProcessor,在你项目中如何使用的
- Spring 中你常用哪些注解? Autowired 实现原理
计网仿真 TCP 以及后面深入的部分我不懂, Autowired 实现原理 没说清除,其余的都答上来了。
字节跳动
在我整个的面试过程中,字节给我的体验是最好的。一面二面的面试官都非常好,面试的问题、要求都说的很清楚,需要注意的点都提前告诉了我,甚至二面的面试官会提醒我,”在回答问题的时候这边会有敲键盘的声音,是我在记录,不要影响你回答问题。”对于初次求职面试,体验感拉满。
除此之外,字节的面试和百度思路不一样。百度是从八股出发,引出一些实际场景遇到的问题。字节几乎没有八股,是从项目出发,结合工程经验,主要考察思考的过程,关键点答出来之后,结果对错可能不是很重要(这里是我主观臆断的)。
字节一面(1h):
- 自我介绍,项目部分主要介绍了 rpc 项目,后续的问题都是基于这个项目
- rpc 远程调用的整个流程
- 项目中的 SPI 机制,介绍一下原理以及你做了哪些改进
- 项目中用到了负载均衡算法,详细介绍一下
- 一致性哈希的原理,虚拟结点
- 项目中的序列化方案,为什么序列化,你都了解哪些常用的序列化方法。
- 你项目中使用了 Kyro 序列化,优点你提到了,缺点了解吗
- 通信协议是你自己设计的,假如后面需要变更,比如添加新的字段,你项目中如何处理的?
- 服务的灰度发布介绍一下,如何实现的?
- Zookeeper 作为注册中心,假如崩溃了怎么办?这里开始连环问了
- 你提到了 Zookeeper 的一致性,它是如何保证的?
- ZAB 协议,选举的过程,这里问的很详细
- Zookeeper 是强一致性吗?
- 网络分区了解吗,CAP 理论
- Zookeeper 如何应对网络分区的,脑裂问题了解吗,如何解决?
- 假如我同一时间有大量服务发布,你提到了 Zookeeper 只有主节点负责写, 怎么解决?假如主节点崩溃了,新选举出的主节点仍然没办法面对我的大流量,也崩溃了,如何解决?
- MQ 的原理,你知道哪些 MQ,各自有什么特点,什么时候需要用 MQ
- 你刚才提到了服务端保护机制,如何实现的?这里我答了限制连接数以及接口限流,基于责任链模式。之后问了用到的令牌桶以外的常用限流算法。
- 算法题:链表反转,你知道的所有实现方式。这里我写了递归和非递归两种。
整个面试的过程中大脑都是高速思考的,甚至从面试官的问题中得到了好多启发,是背八股掌握不到的,体验非常好。面试的问题几乎都答上来了,有一些不太熟悉的在面试官的提醒下也都回忆起来了(这点非常 nice,其他面试不会就直接过了,而字节会认真引导你,看你究竟掌握到什么程度)。事后 hr 小姐姐还告诉我面评非常好,鼓励我认真准备二面。
字节二面(1h):
字节二面的经历比较魔幻了, 面试官在伦敦有时差,因此是晚上九点开始面试的,由于面试官比较忙,整个面试过程比较简单,自我介绍 + 项目介绍之后简单提问了几个问题(没有印象深刻的技术问题,这里就不重复整理了),沟通了一下实习时间,直接做算法题了。
- 算法题:有一个 n * n 的棋盘,每个格子有 RB@ 三个状态,R 表示红色,B 表示蓝色,@ 表示此路不通。机器人从左上角走到右下角,每次只有上下左右四个方向选择,相同颜色之间没有代价,跨越不同颜色代价为 1,求解机器人从左上角走到右下角,最少的代价。
- 这里我用回溯求解的,很快就写出来了,思路也没问题。但是复杂度计算卡住了,在面试官多次且反复的提示下,算出来了。 最后提问环节面试官跟我说后续优化可以加一些剪枝操作
二面没有遇到难度比较大的问题,大部分时间都被我卡在了算时间复杂度(很菜勿喷)….
B 站
综合来说,B 站面试给我的体验是非常差的,一面的时候我感觉还没进行比较深入的交流,问了一些八股,很快就结束了。二面上来之后,问了你觉得自己项目有什么亮点吗,然后就来了一道 Hard 算法,又结束了…….
B 站一面(30 min)
- Rpc 远程调用的流程
- 一致性哈希算法详细介绍
- 为什么选用 Zookeeper 作为注册中心,注册中心作用是什么
- 动态代理
- Redis 在你项目中如何使用的,穿透、雪崩、击穿了解吗
- 你项目中用的是 RabbitMQ,为什么,和其他 MQ 对比如何?
- RabbitMQ 的原理
- 你项目中的 灰度发布、分组管理如何实现的
- 无算法题
之后在我以为他准备深入提问的时候,面试官告诉我面试结束了,整个过程不到 30 min,而且没有算法题。
B 站二面(30 min)
- 简单介绍下你的项目,是工程项目、学校项目还是自己学习的
- 你认为项目中有什么亮点?
- 算法题:K 个有序数组,输出最终排序后的数组 (K merge)。
B 站是我第一个走面试流程的公司,二面在字节一面的前一天。B 站二面是我第一次在面试过程中写算法题,结果就遇到个这,当时心态是崩掉的。因为第一次确实有些紧张,思路不清晰。虽然是力扣 Hard 难度吧,但是事后觉得也没有很难,做不出来还是大多归因于自己。但是复盘的过程中,我发现在面试的过程中,虽然太紧张了没实现出来,我把两种解题思路都思考到了,并且面试官提问时间复杂度,在提示下也求解出来了(很菜勿喷),整体表现自我感觉也算可圈可点吧。
给大家的经验就是,平时刷题的时候时间复杂度求解一定要重视!
备战部分
下面是我从七月底写出第一行 Java HelloWorld 直到现在的大致时间表:
- 七月底,正式开始投入时间学 Java,在师兄、师姐的推荐下选择了 JavaGuide 作为主线的学习资料,之后加入了知识星球,认真阅读了关于学习路线的内容。
- 七月份用了一周多的时间熟悉 Java 语法。前期主要跟随 小码哥恋上数据结构课程,一边复习算法数据结构,一遍熟悉 Java 语法。
- 八月份开始,选择了 Guide 哥推荐的千峰商城项目作为入门,大概用了两周时间,全程跟随视频敲完代码,收获非常大。这一过程中,恋上数据结构这门课程是同步学习的,基本就是早晚做项目,下午学算法。
- 多线程、Jvm 方面的知识,我选择了马士兵的课程。这两部分是同时学习的,理解为主。选择马士兵课程的原因之一,是因为马老师讲课是以面试为导向的,一边理解一边掌握八股文了,效率比较高,总共耗时两周左右。
- 网络 IO 部分的知识 以及 Zookeeper、MQ 等中间件,这三部分是一起学习的,参考的资料包括马士兵课程、稀土掘金的 Zookeeper 课程、慕课网的 MQ 课程以及尚硅谷的一些资料,耗时大概一周左右吧。
- 以上打基础大概花费了一个月的时间。
- 九月份返校之后,开始着手准备简历上的项目,花了一些时间在 Guide 哥的知识星球里翻看优秀开源项目介绍,选择了 Guide 哥的手写 Rpc 项目以及星球推荐的 IM 项目。
- 有了基础之后,项目做起来还是比较快的,加上有源码可以参考,各自用了一周就基本实现完成了。这里总共耗时两周。
- 后面的时间里,我针对这两个项目做了深入研究。在极客时间和稀土掘金里,我分别找到了 rpc 和 IM 的相关课程。由于都是文字的形式,加上自己实现过基本功能,读起来非常快,快速整理出了课程内作者对于项目深入思考的部分,之后融合到自己的项目中。这里我认为是非常关键的一步,在面试的时候我能够顶住面试官的连环问,和这些课程中的相关内容以及思考题的深度是分不开的。在这一过程中,我还有幸加了几个作者大大的微信,不停地和作者交流自己的思考,甚至发现了课程中的一些小瑕疵。非常幸运他们都很有耐心,给予了我很多指导,尤其是 crossoverJie 大佬,几乎是有问必答,甚至在我面试之前,还给予了我很大鼓励(相当感动)。
- 大概到九月中旬,我就开始整理简历并且投递了,之后一边复习八股,一边完善项目。
- 九月底,我的项目已经基本整理完成了。我花了一周的时间系统梳理八股文,制作了很长的脑图帮助我回忆知识点。
- 十月份,国庆节的假期里,我保持着每天 15+题的速度,快速找回了算法题的手感。由于时间真的太仓促了,时间复杂度这一块我没有重视,后面也付出了惨痛的代价。提醒各位读者,算法复杂度的计算一定要重视起来。
- 最终功夫不负有心人,我收获了百度、蔚来以及字节的日常实习 offer。JavaGuide 以及知识星球内部的 《Java 面试指北》在我整个备战的过程中起了很大作用,是我的指路明灯。
写在最后
由于从本科开始就一直做算法 NLP 相关的工作,对后端开发了解甚少,加上时间紧迫,我不得不采取一种囫囵吞枣的方式进行学习。对我而言,在不到三个月的时间里,从 Java 的入门阶段到通过日常实习面试实在付出了太多,每天早 7 晚 11 的作息时间对身体也产生了一些伤害,如果时间允许,我更希望节奏慢下来,把每个知识点都学扎实、学透彻。
本篇面经实际上没有太多东西可以分享给大家,因为笔者实际上也只是一个才学了不到三个月 Java 的新手小白。如果说文章里有什么是值得大家参考的话,我希望是面对目标绝对坚持的毅力以及面对困难永不退缩的决心,是它们支撑着我逐渐越过一个又一个的“不可能”。
2 年经验大厂(网易、字节、B站、阿里…)面经分享
这是一位20届球友的社招面经,获得了滴滴、网易、字节、B站、携程等公司的 offer,最终选择了字节。
下面是正文。
北京滴滴(offer)
一面
自我介绍
介绍自己做的项目,难点有哪些,怎么处理的?
拆分读服务是微服务的什么思想?
拆新的服务和之前服务水平扩展 有什么不一样?
数据库层面有没有数据扩展?
QPS 8W 总单量是多少 ?
本地缓存怎么保证数据一致性?
MQ 如果挂了 怎么办?
Redis 集群了解吗?
数据清洗怎么做的?
如何保证最终一致性?
顺序消息如何保证?
ES 怎么用的?数据量级多少?为什么用ES 不用Hbase?
Zookeeper 作为注册中心有什么问题?如果 海量服务同时重启会出现什么问题
算法:环形链表 II
二面
项目介绍
大促期间服务总QPS , 多少个服务,每个服务多少个线程
服务器线程数量根据什么来配置?
Redis 集群的工作原理? gossip协议? 写和读的流程? CRC16 再取余 这个计算 在client 还是服务端?可以决定哪个key 放在哪个节点吗?
Redis 主从同步流程?
Redis 的 hash结构 怎么 rehash的?如果渐进式时,这些的key突然都不访问了 会有什么问题
MySQL innodb 引擎的索引结构,B+树一般都多高? 层高怎么计算?
联合索引 abc where a = 3 and b > 3 and c= 3 怎么走索引?
如果MySQL 表中有一个字段很大有几K会有什么问题?
索引下推了解吗?
场景设计:如何设计一个会议室预定系统?
算法: 给数组arry 和值 x 计算 数组 array 中差值绝对值为X的数对;
三面(HRBP)
离职原因;
用三个词评价一下你的领导;
未来规划;
你有什么缺点;
遇到过最大的问题;
总结
一面整体上全是项目和场景考虑,因为他们是用go开发,我之前是用Java,所以一直在问中间件,没有Java八股文,不过中间件问的蛮深,面试体验很好;
杭州网易(offer)
一面
讲一下JUC 下的线程池,线程池参数以及提交任务后怎么执行
Lock 的加锁和解锁过程和公平锁和非公平锁实现原理
Conditional 源码有没有看过
阻塞队列 源码有没有看过
JVM 调优讲一下?非常细 什么命令 怎么分析的 面板什么样子都有问
CMS + ParNew 算法的对象分配和垃圾回收流程
什么时候会出发full gc
old区什么时候触发CMS GC 什么参数 配置大了会怎么样 配置小了会怎么样
为什么会产生浮动垃圾
MySQL 的隔离级别,MVCC 原理 ,乐观锁 在什么隔离级别才能使用?
Kafka 的ISR是什么,HW呢?怎么保证可靠性, Kafka 怎么实现顺序消息?为什么Kafka的broker上topic越多 效率越慢?
讲一下项目的完整流程 数据模型,多个版本经常变化怎么控制的?(每个校验模块提供原子能力 可以配置化,如何设计)
分布式事务 是怎么保证的, MQ的方式 如果本地执行成功同时服务挂掉了 这个MQ没有记录 怎么办?
二面
Zookeeper工作原理讲一下,有没有看过源码;
讲一下你负责的业务的服务架构,以及你们部门的服务架构;
你觉得现在架构有什么不合理的地方?
有没有看过什么中间件的源码?
区块链了解吗?
总结
因为简历投错了部门,投到了区块链,所以问我很多源码,比较底层的东西,因为做区块链开发可能会难一点,所以会问有没有看过源码,整体面试体验很好;
Shopee(offer)
一面
项目问题( 聊了 30分钟);
MySQL 主从同步原理;
MySQL 索引优化;
线上问题定位 以及优化过程;
Redis 集群 的工作原理,集群写入数据原理, 增删节点 如何数据同步?Redis的hash过程;
Kafka 讲一下,offset存储原理;
算法: 1.栈实现队列 2.三数之和
二面
介绍一下自己的亮点;
讲一下做的项目;
Kafka 讲一下;
MySQL 的索引讲一下;
Redis 的key过期 怎么删除的 ?主动删除 和被动删除;
Redis 击穿 雪崩 穿透 和解决办法;
MQ 同步信息怎么保证数据的一致性和实时性?
JVM调优过程说一下;
算法:1.二叉树的前序,中序,后序遍历; 2.最长重复子数组
总结
参加的是周末专场面试,一个周末面完+出结果,整体是项目+中间件,算法每一面都是两道一个easy,一个mid,整体面试体验很不错;
B站(offer)
一面
MySQL 行级锁,表级锁。意向锁 加锁时机是什么? 项目中有没有使用过意向锁?
如果查询语句有没有索引,SQL调优过程?
Spring 事务注解原理,事务传播机制,使用过什么传播机制?
RocketMq 消费者重平衡 会有什么问题?重复消费? 消费失败? 这些场景如何处理
数据一致性怎么保证? 分布式事务怎么实现?
动态代理有哪些,什么区别,使用注意方式;
二面
项目主要负责什么?
数据清洗怎么做的?
Kafka 怎么保证消息一定被消费?
qps8W 多少台机器? 什么配置? 总qps? 线程数量? mysql 版本? 走的什么索引? 会不会回表?
说了自己加redis和本地缓存 然后问我本地缓存的配置?
如何保证Redis,DB数据一致性?双删策略 为什么要多删除一次?
HashMap的扩容过程? 会发生什么问题?
MyBatis 的接口和mapper怎么对应执行?
还做过什么技术优化?
分布式锁怎么实现? 分布式锁怎么实现阻塞队列?
本地锁 怎么实现阻塞队列的唤醒的?
Zookeeper 怎么实现Cp?
ZK 怎么选举的 怎么投票的?
算法:1.多线程循环打印ABC; 2反转链表。因为是现场面试,纸上手写,所以给了两道题目难度都还好
三面
你们部门的服务架构讲一下?
接口优化怎么做?
什么场景下,都用过哪些并发?你用多线程的时候 Synchrionzed和ReentranLock怎么选择的?选择原则是什么?
压测和故障演练做过吗? 你都扮演什么角色?有什么收获?
用的什么rpc和注册中心?有什么优缺点?
未来三五年规划?期望薪资?
总结
因为是使用Java的,所以Spring问的比较多,一面比较贴合实际,都是面试官开发中常见的问题;二面对项目整体做个梳理和一些中间件知识;三面从架构和优化,压测等角度去问看看广度和高度吧主要;
携程(offer)
一面
讲一下ArrayList和HashMap 底层数据结构,优缺点,使用方式;
CuurentHashMap有用过吗?
CAS 设计思路和原理?
ThreadLocal底层原理?
什么场景使用的ThreadLocal?
什么场景使用了多并发?
用到了Java jdk8的哪些新特性?
Lambda怎么用的,Stream的实现原理?
除了刚刚的场景 还有什么场景使用过异步任务,并发任务计算结果后做聚合 怎么做?
网络编程 用过吗? IO讲一下
你开发中都用到了什么设计模式?
工厂模式的设计理念是什么?有什么好处?体现了什么编程思想?
适配器模式了解吗? 策略和适配器模式有什么区别,你为什么选择用策略模式而不是适配器原因是什么?
设计模式都有什么开发原则?
JVM调优经验 说一下做了什么?
JVM 知识你讲一下?
Spring 事务注解Transaction 实现原理?
A方法调用B方法,如果B方法开启事务 则直接用B方法的事务,如果是你 你怎么设计怎么做?
InnoDb的默认隔离级别,可重复读,解决了什么问题 没有解决什么问题?
什么场景下使用了ES?
倒排索引 是什么讲一下?
为什么ES检索比较快?
你使用MQ(RocketMq和Kafka)的应用场景什么?
二面
服务器都多少线程,发起一个请求去调用第三方,是新增加一个请求吗?如果服务器线程使用完了怎么办?
灰度上线流程怎么做的?
数据洗刷 怎么做的 双写 怎么避免循环写?
ES 数据结构是怎么样的?
25匹马 5个赛道 怎么跑 才能最少批次找到最快的三匹马?
分裤分表下 怎么做业务逻辑查询?怎么生成一个全局唯一的id?
场景设计:给一个10G的文件,里面只有两行记录是一样的,如何找出(电脑内存只有500M)
算法:有效的括号
三面
负责的·项目 业务流程和 服务架构都说一下?(20min)
bigdecimal 使用需要注意什么,还有什么其他编程时需要注意的规范?
MySQL 库表上线之前需要做什么工作?
索引为什么是B+树结构,MySQL都有哪些引擎,有什么区别?
MySQL深度分页怎么解决?
ThreadLocal原理?使用需要注意什么?
如果做海外的业务,使用数据库需要注意什么地方?时区
有没有做过海外业务?多语言,多币种有没有什么解决方案?
总结
感觉携程的面试比较中规中矩,基本上所有知识面都有考察到,也比较符合实际,项目,基础知识,场景设计,算法,代码规范都有,但是相应更看重 项目和基础知识和代码规范,面试体验很好;
阿里(曲折的面试经历)
阿里一共面了三个部门:淘宝,饿了吗,供应链
年前面淘宝两面,到三面没有hc了,搁浅;饿了吗有同学联系,面完两面,结合自己情况不再考虑这个机会;年后又面了供应链,后来有offer,没有继续走流程;
淘宝一面
先聊了20分钟项目 问难点 如何解决
HashMap在使用时需要注意什么地方 至少说出四点;
你的多并发控制是怎么使用的,都有哪些多并发控制手段
线上有死循环代码 你怎么排查定位到
MySQL的 事务 实现原理和隔离级别
对于索引,你觉得在开发中需要注意什么?
分布式锁的实现 和 底层原理 以及都有什么问题?
NIO 和 AIO 的区别
Kafka 的 架构和工作原理?
Kafka为什么这么快,顺序写 是这么实现的?
你觉得你做的业务的价值是什么 解决了什么问题?
你觉得 你做的对业务最有价值的一件事情是什么?
笔试:1.找出代码的bug 一段多线程代码 找出三个bug
给你Memcached Clinet 实现一个消息队列
淘宝二面
设计方案主要做哪些事
线程池的阻塞队列有几种,你们用的那种,拒绝策略有几种,你们用的哪个,为什么
线程池里面的execute 和 submit 方法有什么区别
线上线程池打满,如何优化的?JVM 你了解什么? CMS 为什么会有浮动垃圾? 什么时候会进行CMS GC? 什么时候会进行Full GC?CMSGC和 FULL GC有什么不一样?
spring中bean的生命周期
explain 执行分析,你们主要关注哪些字段,为什么
线程池核心参数
java内存模型讲讲,内存屏障是干嘛的
Zookeeper 作注册中心 和nacos 和eruka 有什么差异 ?基于什么理论选择?
JVM调优经验说一下
总结
很遗憾面到三面没有hc了(拥抱变化),面试更贴合实际,比如:HashMap在使用时需要注意什么地方 至少说出四点,看起来很简单,但是需要知道HasHMap的结构和工作原理,JVM和Spring问题会多一点,也会关注你的业务Sense,对业务有没有推动;
字节(offer)
一面
项目问题(20min)
binlog 和 redolog 有什么区别?
MySQL 不同存储引擎有什么区别
Kafka 为什么这么快,主从同步怎么做的?HW 和LEO分别是什么;
让你实现一个消息中间件,你会设计哪些模块?
ES查询流程?
B+树的特性?
Select / Poll / Epoll 的区别?
Redis 集群工作原理? 如何通信?MOVED和ASKED 有什么区别?
服务设计:设计一个短链系统;
算法:接雨水
二面
项目问题(10min)
JVM 的内存模型
G1 和CMS GC过程都说一下,分别适用什么场景? JVM调优过程说一下?
内存溢出和内存泄漏?什么情况下会出现? 怎么避免?
HTTPS 的工作原理? 有哪些常见的加密算法?
顺序消息 如何实现?
数据库索引的原理?联合索引和索引下推?
Redis数据结构?为什么用快表而不用平衡查找树?
Redis主从复制过程?
一个数组 int[10] 在JVM内存上怎么分配的?多大空间?
场景设计:设计一个分布式限流器
算法:寻找重复的子树
三面
项目介绍;
DDD了解吗,讲一下?
RocketMQ有什么缺点? Kafka有什么缺点?使用场景分别是什么?
用过什么设计模式?
使用Redis 需要注意哪些地方?
工作遇到过什么问题? 如何解决的?
工作中和同事遇到冲突,如何解决?
场景设计:公司的各系统都有计数需求(如头条文章的阅读数、评论数、点赞数等),请设计一个统一计数服务。
算法:LFU 缓存
还有一些忘记了。。。
总结
一二面考察比较全面,以技术问题为主,涉及面较广;具体包括:计算机基础、编程语言、数据结构与算法、系统设计题等一些问题会涉及到原理与细节;三面也会看反应力、方法论。面试体验比较好。
腾讯云Java工程师一面 + 被捞一面 + 二面面经
【一面】全程55min
先自我介绍一下吧
Java1.8 的新特性?你说到了 Lambda 表达式,你说说它的优缺点?
Java 8 的 Stream 流用过吗?有什么特点?
线程池创建的方式有哪些?ThreadPoolExecuter 的参数有哪些?
ArrayList 和 LinkedList 的区别在哪里?Queue 与 Deque 的区别?
HashMap 和 TreeMap 区别?
假设有一个10W的数据请求,你会有什么方法来实现这些数据的增删改查?
数据库的三大范式是什么?
MyISAM 和 InnoDB 的区别?
MySQL主键索引和普通索引的区别是什么?谁的性能更好一些?如果是在10W级的数据下,谁的性能更好些?
介绍一下联合索引吧?
以(a,b,c)为例,在什么情况下,单查 b 也能够命中联合索引?
算法题:手写代码实现单向链表的结构体,完成增删改查。
反问环节:你有什么问题想问我的不?
面试总结:感觉问题的题目不是太难,只怪自己太菜,基础没掌握好,面试官人不错,要是有人捞,我还想再来一次!第二天流程已结束。。
好家伙,不知道发生了什么,我又开始了新的流程,这难道就是传说中的二战!!!还是腾讯云。
【一面】全程46min
自我介绍,
接着就是来俩算法题:题1:统计一串字符中,重叠字符出现的次数。(如AAABBBCC,输出A_3_B_3_C_2)题2:求两个字符的最长公共子串
你用过哪些数据库?MySQL数据库的存储引擎了解过哪些?这些存储引擎的特点是什么?
MySQL 默认是什么存储引擎,为啥用这个?
InnoDB和MyISAM的有啥区别?和MEMORY呢?
InnoDB为啥索引的数据结果要用B+树?
为啥不用Hash索引?Hash索引查找的时候不是更快吗?
网络编程了解过吗?说说如何创建一个Socket连接
C++的基本数据结构了解过吗?
反问环节:你有什么问题想问我的不?
【二面】全程43min
今天上午突然给我发邮件让我下午二面,这霸道总裁的通知风格。。。
看你有个比赛,你这个比赛里面做的什么?(15min)
你还写过哪些应用程序,说说看?数据存储用的什么,web服务器用的什么?
C++了解过吗?
软件设计师什么时候考的??
写过多进程和多进程吗?都在什么情况下用过?遇到了什么困难不?
有没有 Linux 下的编程经验?
Linux常用命令了解哪些?
网络编程用过吗?Sokect?
Linux中的epoll和select这些多路复用了解过吗?
说说五层网络模型吧,说说对应的协议吧
抓过网络包吗?
如果要设计一个快速插入和查找的数据结构,你会用什么结构?Hash冲突的解决方法?
有一个容量为N的数组,里面存放了N个数,每个数的取值范围是1~N,有没有什么快速办法判断是否有重复元素,哪个元素重复了?空间复杂度要求是O(1)
求二叉树深度的算法呢?说说看?时间和空间尽可能的小
设计模式了解过吗?了解一下
UML类图理解的怎么样?画个类图吗?
云计算的知识了解吗?容器用过吗?大数据套件了解过吗?ELK了解过吗?看过开源架构系统的代码吗?(这全是知识盲区啊。。。)
分布式存储了解过吗?
一个进程的栈大小是多少知道吗?打开文件的上限是多少?
你觉得你擅长做什么业务?
反问环节
总结:整个过程主动补充有点少,导致场面数次陷入安静的尴尬场面,最长的一次甚至长达30多秒。以后的面试还是要多多扩展自己讲的内容。
文|牛客网:牛客71576213号
参考答案
上面的绝大部分问题,你都可以在下面的这几篇文章中找到答案。
步步高 Java 后端校招 6 面面经
我对步步高的记忆主要还停留在小时候,那时候步步高点点读机是真的火爆,广告直接都给洗脑了。
步步高虽然不是什么大型互联网公司,但是这份面经总体还是非常高质量的!这侧面说明了这家公司的的技术栈也还是比较主流的。
一面(50 分钟)3 月 3 号
介绍一下项目
项目是视频还是通过什么途径学习的?
你认为项目中复杂的点是什么?
Redis 的使用场景?
Redis 的高并发是依靠什么去保证的?
ThreadLocal 用在哪,为什么选择 ThreadLocal 呢?
项目上线了嘛?部署在哪里?怎么部署的?
注解实现缓存和日志统一处理是怎么做的?
SpringBoot 分哪些模块?
项目中的分页是怎么实现的?
项目中都有哪些 sql 表说一下吧?
消息队列 MQ 用过吗?说一下?-
分布式锁这块有用到吗?-
说一下常用的一些集合?
说一下 HashSet 的原理?
说一下 HashSet 与 HashMap 的区别?
线程安全的集合类有哪些?
锁重入了解过嘛?那些锁支持锁重入?
说一下锁升级的过程?
数据库中的锁有哪些?
Java8 的新特性 Stream 流、Lambda 表达式说一下?
TCP 和 HTTP 协议之间的关系,有什么区别?
TCP/IP 参考模型,每层都是封装的什么?
TCP 是可靠的嘛?那么 UDP 呢?
TCP 如何保证我们的可靠传输的?
说一下 TCP 中拥塞控制的一个过程?
Linux 查看 ip 地址的命令?
说一下聚簇索引和非聚簇的区别?
事务的隔离级别和每个级别所产生的问题?
实际开发中最常使用的隔离界别
创建线程的方式?说一下?
讲一下同步和异步的区别?
说一下项目中 Nginx 的作用?
说一下 JMM 吧
说一下常见的垃圾回收算法吧?
JMM 的三个特性是哪三个?
如何保证原子性,volatile 的作用呢?
ThreadLocal 和 synchronized 的区别
Redis 的 rdb 和 aof 说一下吧,区别呢?
为什么 fork 一个子进程呢?
Redis 有持久化为什么还要用 MySQL 呢?
MySQL 数据也会有丢失的情况呀?是如何保证的呢?
单节点和集群的区别,集群解决了什么问题?
主从复制解决了什么问题?
Redis 集群的原理
MySQL 为什么要采用读写分离呢?
除了 MySQL、Redis 外还了解过其他数据库嘛?
用过 Docker 嘛?
使用 Docker 部署的好处是什么?相比原始部署?
最近看了哪些书呢?
反问
二面(25 分钟) 3 月 5 号
Redis 为什么快?
线程的创建方式?
怎么在 Linux 服务器上部署项目?
使用过 Docker 嘛?
Docker 与 Linux 相比为什么性能更好?
如何进行 sql 优化?你自己实践哪些手段?
我们 MySQL 读写压力很大,怎么解决?
说一下 TCP 三次握手、四次挥手?
MQ 是什么?
项目是怎么做的?实习项目还是自己做的?
如何设计秒杀系统
实际开发中如何解决高并发的问题?你知道哪些手段?实践过哪些手段?
校园的实践经历
为什么来参加春招,是没 offer 吗?
手里有几个 offer ?
以后的发展方向是走技术管理,还是架构方向?
说一下在你眼里技术管理和技术架构的区别?
如果领导让你 3 天完成一个任务,但是你 4 天才能完成
谈一下你对加班的看法?
你将来计划打算学到什么,提升 Java 哪方面技能?
反问
三面(HR 面,20 分钟) 3 月 8 号
为什么会有写博客的习惯呢,出发点是什么?
这个博客是有粉丝的吗?你有多少粉丝呢?
大学校园经历中有意义的一些事情?
大学当中跟室友的关系怎么样?
为什么没有参加秋招呢?
找工作跟考研之间是怎么权衡的呢?
讲一下在自己的个人项目中学到了什么呢?
大学期间有没有低谷期间
手里有其他的公司的 offer 吗?
offer 是哪家公司的?
期望薪资是多少,年薪呢?
反问
四面(终面,7 分钟) 3 月 11 号
一个非常让人讨厌的领导,说话阴阳怪气的,开头第一句话就是你的成绩不咋地啊(无挂科平均成绩在 80+)。
1。 有没有实习的经验 ?
2。 为什么秋招没有找到工作?
3。 你是怎么学习一个技术的?说一个擅长的
结果
面试结束半个月 3 月 25 号,收到消息未通过面试的消息,理由是:因与人才画像不匹配 !
作者:持续学习爪洼
参考答案
你可以在下面这两份资料中找到上面绝大部分面试问题的准确答案:
JavaGuide:https://javaguide.cn/home.html
《Java 面试指北》:https://t.zsxq.com/Uv3ByZn
金蝶 Java 后端校招三面面经(已OC)
一位球友的金蝶面试经历,已经拿到了 offer,不过,后面因为觉得公司的风评不好就拒掉了。
一面
自我介绍
项目的架构图画一下
项目是怎么部署到服务器的
为什么要用 Docker
做项目的过程中遇到了什么问题没有,如何解决的,学到了什么。
项目数据库表怎么设计的
项目的日志怎么做的
项目有没有做权限管理,怎么做的
说一下自己对 IoC、AOP 的理解
网络协议说一下
数据库优化
有没有用过针对多表查询如何优化
HR面
自我介绍
学校的成绩,有没有获得过什么奖项
介绍一下项目,业务情况,当时是怎么做这个项目的
项目中充当的角色,负责做什么
项目带给你最大的收获是什么
说一件你在校园中做过对自己来说最有价值的事情
你觉得一个好的开发工程师应该具备怎样的素质
平时有健身运动的习惯么,频率怎么样
讲讲你的个人优势
手里的 offer 情况
反问
二面
Spring,Spring MVC,Spring Boot 之间什么关系?
@Autowired 和 @Resource 的区别是什么?
静态代理和动态代理的区别
除了 JDK 提供的动态代理实现还有其他实现方式么(CGLIB )
谈谈对 MySQL 索引的了解,哪些字段应该考虑创建索引,哪些字段尽量不要创建索引
为什么 InnoDB 引擎要选择 B+Tree 作为索引数据结构?
MySQL 中 一条 SQL 语句的执行流程
从执行流程的层面说说如何优化一条 SQL 语句的查询速度,发生在哪个部分
多表联合查询的时候,SQL语句的执行流程
目前正在学习什么知识
反问:新人培训体系是怎么样的
三面
自我介绍
简单介绍一下自己的项目
项目中用了哪些设计模式
单例模式有什么好处
项目中用了线程池干什么
为什么实际生产建议使用 ThreadPoolExecutor 构造函数来创建线程池
如何理解线程安全和不安全
平时怎么学习的
反问:公司目前的技术栈,是否有 CodeReview
三面这个面试官基本没怎么问题技术,后面找我唠嗑半天就闲聊一些大学生活啥的。
总结
金蝶的八股文整体还是挺简单的,没有问到特别难的问题的,整体体验一般。
参考答案
你可以在下面这两份资料中找到上面绝大部分面试问题的准确答案:
JavaGuide:https://javaguide.cn/home.html
《Java 面试指北》:https://t.zsxq.com/Uv3ByZn
美团、华为、字节(已获 offer)春招面经(附参考答案)
一位球友的 2022 春招面经,拿到了美团、字节、华为等公司的 offer。
面经中涵盖的问题,我几乎都找到了对应的参考答案,希望可以帮助到你。
美团
一面
挖项目,问的太多了,这里就不一一列举了,大部分是某个功能是怎么实现的或者如果要加某个功能应该怎么实现。
进程线程区别。
死锁,死锁条件。
知不知道中断和轮询的区别。
数据库索引,讨论了一下B+树能存多少数据。
数据库存储引擎知道哪些,有什么区别。
数据库锁。
算法题:起始点到终点最短路径。
部分问题参考答案 :
- Java 并发常见知识点&面试题总结(基础篇)
- Java 并发常见知识点&面试题总结(进阶篇)
- 选中断还是轮询方式?深究其中的区别
- MySQL 索引知识点总结
- MySQL面试题/知识点总结!
- MySQL锁总结
- 《Java 面试指北》 - 技术面试题篇
- LCP 35. 电动车游城市 - LeetCode
二面
- 问项目。
- 什么是序列化反序列化。
- 负载均衡,知道哪些负载均衡 。
- 什么时候会OOM,服务OOM怎么办,如何排查。
- Spring 启动流程。
- Spring 设计模式。
- 对于模版模式的理解,应用场景,你在项目中是怎么使用的。
- HTTP 请求过程 。
- TCP 和 UDP 区别。
- Linux知道哪些命令。
- 设置索引有什么注意的地方。
- 最近看了哪些书,有什么收获。
- 算法题:合并有序数组 O(N)时间 O(1)空间。
- 数据库设计:只能以半小时为单位订会议室。
部分问题参考答案 :
- 招银网络二面:什么是序列化?常见的序列化协议有哪些?
- 《Java 面试指北》 - 技术面试题篇 - 高并发模块
- 系统稳定性——OutOfMemoryError 常见原因及解决方法 - 3.2.1 异常诊断
- Spring常见问题总结
- 设计模式最佳套路4 —— 愉快地使用模板模式
- 一次 HTTP 请求的完整过程
- TCP和UDP的区别
- Linux 基础知识总结
- MySQL 索引知识点总结
- 88. 合并两个有序数组 - LeetCode
华为
一面
项目、论文。
String 能否被继承。
- Java 内存泄露和排查。
- Hash 方式和 Hash 冲突解决。
- 静态代理和动态代理。
- 线程通信方式。
- Volitate关键字。
- Java 高效拷贝数组。
- 算法题 跳跃游戏 leetcode 55。
部分问题参考答案 :
二面
- 简单说说项目、论文。
- 项目是自己学习的还是落地项目。
- 本科保研绩点高,为啥研究生期间没有刷绩点。
- 对华为的了解,这个聊了比较久 因为我本身就是华为用户,比较了解,主管也给我介绍和补充。
- 实习时间。
- 反问部门、技术栈,是否可以自己选项目。
字节
一面
- 问项目,聊怎么实现,从项目里学到什么。
- 手写单例模式,和 Spring 的单例有什么区别。
- 算法题:给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
- SQL 题:根据题目要求写出对应的 SQL,由于太久没写,不会做。
- 智力题 1-N批次其中有一批次重量不合格 用最少称重次数找到
1) 刚开始说二分 面试官说不是最优
2) 提示可以从每批次拿不同数量:第N批拿N个 算重量差值就可以确定
部分问题参考答案 :
二面
- 问项目。
- volitate 关键字。
- JVM新生代怎么划分,大对象怎么分配。
- 新生代有哪些垃圾回收器。
- ParNew 原理。
- Innodb 默认隔离级别,RR能防止幻读吗,RR默认使用间隙锁吗。
- 怎么理解最终一致性,有哪些实现方案。
- 分布式事务。
- 算法题:数组里每个数右边第一个比他大的数。
部分问题参考答案 :
- Java 并发常见知识点&面试题总结(基础篇)
- Java 并发常见知识点&面试题总结(进阶篇)
- Java 内存区域详解
- JVM 垃圾回收详解
- MySQL面试题/知识点总结!
- 最终一致性,一致只会迟到,但绝不会缺席
- 《Java 面试指北》 - 技术面试题篇 - 分布式模块
- 496. 下一个更大元素 I - LeetCode
三面
- 问项目。
- RAFT 脑裂、一致性。
- 负载均衡。
- 各种排序算法,分析复杂度和稳定性。
- 其他想不起来了,八股很少,一直问项目。
- 算法题:旋转图像(90度旋转矩阵)。
部分问题参考答案 :
- Raft实战系列,集群成员如何变更?日志怎么压缩?
- 《Java 面试指北》 - 技术面试题篇 - 高并发模块
- 十大经典排序算法最强总结(含 Java、Python 码实现)
- 48. 旋转图像 - LeetCode
字节跳动 Java 后端实习面经(附参考答案)
这是一位读者的 2022 字节跳动 Java 后端实习面经,已经拿到了 offer。字节虽然用 Go 居多,但也是有挺多 Java 岗位的招聘。
我对他在面试遇到的问题进行了整理并给出了详细的参考答案,希望对准备面试的小伙伴有帮助!另外,建议准备面试的小伙伴一定要多看一些面经,根据自己的简历多多自测,这对于面试非常有帮助!
个人情况
末流 211,软件工程专业,2023 届毕业生。本来没抱多大希望,没想到最后过了。
一面(60分钟)
- 自我介绍。
- 问项目:登录鉴权是怎么做的?为什么采用 JWT 的方式?有什么好处?如何防止 Token 被篡改?
- 问项目:如何使用缓存的?技术选型的考虑?为什么要用 Sorted Set 实现排行榜?Redis 数据同步和数据迁移如何做?
- 问项目:如何防止表单重复提交?
- 问 Spring:怎么理解 AOP的?你在项目中是怎么使用的?Spring AOP 和 AspectJ AOP 有什么区别?
- 问线程池:如何理解线程池、参数、拒绝策略、原理?你的项目是如何使用线程池的?如果然你设计一个线程池,你会怎么做?
- 问 Java 并发(这块问的太深入了,顶不住啊。。。):进程和线程,了解协程吗?JMM 的理解,作用。happens-before 原则的理解,作用。Java 里面的锁你知道哪些? synchronized 关键字的理解、原理、锁升级过程。AQS 了解、原理。ReentrantLock 源码。ThreadLocal 理解、原理、内存泄露问题。
- 继续问 Java 并发:手写 DCL(Double Check Lock) 线程安全方法。为什么需要加 volatile? volatile 的作用、底层原理。
- 问计算机网络: HTTPS 和HTTP 区别、 HTTPS 加密过程。
- Leetcode 199. 二叉树的右视图
- ……
参考答案 :
- 虾皮二面:什么是 JWT? 如何基于 JWT 进行身份验证?、虾皮二面后续:JWT 身份认证优缺点
- Redis 5 种基础数据结构总结、Redis 数据同步和数据迁移如何做?
- 招银网络一面:AOP 了解吗?有什么用?切面执行顺序如何控制?
- Java 线程池详解、面试题 — 如何设计一个线程池
- JMM(Java 内存模型)详解
- AQS 详解
- Java 并发常见面试题总结(上)、Java 并发常见面试题总结(中)
- ReentrantLock源码详细解读
- HTTP vs HTTPS(应用层)
二面(50分钟)
- 自我介绍。
- 问计算机网络: HTTPS 和HTTP 区别、SSL/TLS 的工作原理、中间人攻击了解吗?
- 问计算机网络(比较深入,有一些没有回答上来,还是要多补补基础):TCP 与 UDP 的区别,TCP 三次握手四次挥手,TIME-WAIT 和 CLOSE-WAIT 是干什么的? 为什么要三次握手两次不行吗?有大量连接处于 TIME-WAIT 的原因? TCP 是长连接还是短连接?
- 问计算机网络:从输入URL到浏览器显示页面的流程。
- 问数据库: MySQL 索引的理解、底层数据结构。如何看 SQL 语句是否使用了索引?
- 问数据库:如何理解事务?表级锁和行级锁的理解,幻读、脏读问题的解决。
- 问 JVM: GC 如何判断回收的垃圾对象?GC 算法有哪些?Minor Gc 和 Full GC 有什么不同呢?ZGC 垃圾回收器了解吗?
- 问 JVM:双亲委派模型的理解,有没有在项目中实践过自定义类加载器。
- 问场景题:几十G的数据都是URL,内存空间只有1G,磁盘空间无限,统计频率最高的Top 10;
- Leetcode 32. 最长有效括号、Leetcode 110. 平衡二叉树
- ……
参考答案 :
- HTTP vs HTTPS(应用层)、你连 HTTPS 原理都不懂,还讲“中间人攻击”?
- TCP 与 UDP 的区别?、TCP 为什么要三次握手?
- 从输入URL到浏览器显示页面的流程
- 深入理解 MySQL 索引底层原理、最完整的Explain总结,SQL优化不再困难
- MySQL 事务的默认隔离级别是什么?可以解决幻读问题么?、MySQL 中有哪些锁?表级锁和行级锁有什么区别
- JVM 垃圾回收详解、新一代垃圾回收器ZGC的探索与实践
- 类加载器详解、JVM自定义类加载器在代码扩展性的实践
- 10 道 BAT 大厂海量数据面试题(附题解+方法总结)、海量大数据处理面试题和思路总结
三面
这一面问的技术问题变少了很多,更多的是和面试官交流技术思维。
- 自我介绍。
- 你感觉你一二面表现的怎么样?
- 除了 Java 你还学习过什么其他的编程语言么?我说了 C 语言。面试官紧接着让我说说 Java 和 C 的使用感受,应用场景。
- 爬虫有了解吗?大学的时候写过爬虫没有?如何构建一个爬虫代理服务?
- 分布式缓存设计、缓存问题解决思路(雪崩、穿透)。
- 自己做过印象最深的一个项目,学到了什么。
- Leetcode 44.二叉树每层找最大值
- ……
参考答案 :
HR 面
随便聊了一下。
自我介绍。
项目里面做了什么,担任什么角色,最有成就感的事情。
对于公司的了解?还面了哪些公司?为什么要选择实习?
平时是怎么学习的?
你有什么要问我的?
平时除了技术喜欢干什么?
……
总结
字节的面试难度还是比较大的,不过,效率很高,体验也很不错。几个面试官给我的感觉还是不错的,一看就是做技术的,不整一些虚头巴脑的东西。
面试之前,我一直对照着 JavaGuide 网站(地址:javaguide.cn)和 《Java 面试指北》复习知识点,准备对应的八股文。Java 后端的知识点比较多,我主要是根据自己的简历来进行针对性地复习。面试的时候,不出意外,面试官问的几乎都是简历上写的东西。
2021
从考研失败到收获到自己满意的Offer
关于我
我现在是本科大三学生,在电子科大就读软件工程专业,在我大一大二的时候其实也并没有找到所谓的方向,将来想要从事什么岗位。只是一心想着先学好学校的专业课程,工作就业的事以后再说。我就一直用自己在学校课程上取得的一点点成绩在麻痹自己,逃避就业的现实。其实大家也都非常清楚,现在高校里面讲授的内容很多都是偏向于底层的一些理论知识,并不会具体教你框架、怎么做项目、怎么样写代码、即使有很多实验课程也都是非常地老套和实际情况差距非常大。这就直接导致一个很大的问题:我的编程能力很差,没有一点自信。
由于我们学院特殊的安排,我们基本所有必修专业课程的学习都在大一和大二修完,大三上半学期有少量的专业选修课程和思政课。大三下整个学期都是要去企业完成 6 个月的实习。了解到很多优秀的学长在大三实习的时候就拿到了非常厉害的 offer 和优厚实习待遇,我当然是非常的心动,希望能够在大三下学期的时候能拿到一个不错的实习岗位。由于我个人是非常不愿意去做测试开发,算法开发的门槛又相对较高,然后就选择了 Java 这个方向。
准备面试
我其实在大二上半学期的时候修了 Java 这门课程,但是学校的 Java 课程是非常老套,和实际企业里的开发是完全脱节。在大三上半学期我当时就在网上找各种 Java 的学习路线,但我发现有很多学习路线看完都是“实力劝退”的感觉,因为内容太多太杂,对于一个想要入门开发的 Javaer 非常不友好。也是机缘巧合,在一个学长(很厉害的一个学长,目前在华科直博)推荐下,了解到 JavaGuide 这个开源项目,从那时起我才算是打开了新世界的大门。学习路线非常清楚,特别对于我们这种初学者的人来说非常友好,知识点的总结也在我后来面试过程帮了大忙。
看到身边的大佬们手拿多个大厂实习 offer 不知道怎么选时,一方面是非常羡慕,另一方面就是觉得自己是在还以前欠下的债,所以大三上整个学期我的压力都是挺大的,边学习 Java 的技术栈边准备面试。前前后后面试的公司有百度、成都 SAP、京东(京东数科)、新浪微博等,最终也算是如愿以偿,马上准备入职京东。
至于我怎么准备的面试?我觉得很重要的一点就是根据自己写的简历和所投递岗位的 JD 有针对性地复习。在简历上最为重要的版块就是项目经历和技能清单这两块,这两部分直接决定了能不能拿到面试资格和面试官怎样提问。所以我当时就遇到了一种窘境,因为我是边学 Java 边面试,项目这部分可写的非常少,基本就没有。
我看过各大公司的招聘需求:Java 开发现在基本都是 SSM、SpringBoot 框架等等,当我学完了这部分之后,我就跟着学校老师那边做了一个 Java 后端的项目把学的框架练习了一遍,写在了简历上,之后我就对项目中的技术点进行复盘。
在当时我确实有着投机的心态,但是必须要有这样一个项目,否则我可能连面试的机会都没有,在参加了多次面试之后我的感受就是:作为实习生,项目这一方面重点在于面试官他要确认你是实实在在地做了,并且有你自己的思考和收获。面试的重点其实是在很多基础的问题上(面试题放在后面),在基础这部分,我反复地复习 JavaGuide 上面的基础知识点,在这里必须感谢 JavaGuide,这可以说直接影响了我在面试中的表现。
面试真题
下面的面试题是来自百度、京东、新浪微博,我进行了一个总结,希望能帮到大家,划重点的部分表示反复被问到
数据结构与算法篇
B 树和 B+树的区别
你了解哪些排序算法?算法的思想、时间复杂度、空间复杂度?
LeetCode 第 1 题及第 15 题:两数之和及三数之和问题
计算机网络篇
- TCP 三次握手、四次挥手流程?为什么三次,为什么四次?
- TCP 和 UDP 区别,有 TCP 为什么还要有 UDP?
- TCP 粘包和拆包问题有了解吗?
- TCP 是怎样保持连接的?
操作系统篇
并发编程中死锁有了解吗?死锁产生的条件是什么?你在项目中是怎样解除避免和解除死锁的?
进程的都有哪些状态?怎么转换的?
Linux 下文件的操作命令
数据库篇
数据库范式了解吗?在你的项目中怎么运用的?会出现什么问题?
数据库索引了解吗?MySQL 中索引底层是怎么实现的?
- MySQL 中存储引擎 InnoDB 和 MyISAM 有什么区别?分别用于什么场景?
- 数据库事务有了解吗?事务的隔离级别?你在项目中使用的隔离级别是什么?
- SQL 优化有什么思路?
- 项目中使用到外键了吗?外键作用?使用外键要注意些什么问题?
- 除了 MySQL 数据库你还用到哪些数据库?Redis 数据库和 MySQL 数据库的区别?
- 设计一个数据库表
Java 基础篇
类和对象的区别?
讲讲 static 关键字和 final 关键字
- synchronized 关键字是怎么用的?底层实现有了解吗?还有用过其他的锁吗?
- BIO、NIO、AIO 区别有哪些?项目中有用到吗?Netty 了解吗?
- 接口和抽象类的区别?什么时候用接口,什么时候用抽象类?接口可以继承接口吗?
- HashMap 和 HashTable 的区别是什么?
- ConcurrentHashMap和HashMap的区别是什么?ConcurrentHashMap为什么线程安全?
- HashMap 和 HashSet 的区别?HashSet 是如何检查重复的?
- Java 中线程的状态?join()、yield()方法是干什么?
- Object 类下有哪些方法?
- 字符串”123”转换成整型123的API是什么?整型123转换成字符串“123”的 API 又是什么?
- 创建线程有几种方式?分别是怎么做的?
- 线程池用过吗?如何创建一个线程池?其中各个参数的含义是什么?为什么要用线程池?coreSize?
- synchronized、ReentrantLock 区别?
- CountDownLatch 和 Semaphore 用过吗?他们的区别是什么?CountDownLatch 应用场景?比如现在要让第 5 个线程等待前 4 个线程执行完毕再执行,具体怎么做?
- 使用 synchronized 来实现单缓冲区的生产者消费者模型?
- JVM 有了解吗?JVM 中参数–Xms和-Xmx是什么意思?
- 设计模式有了解过哪些?单例设计模式知道哪几种写法?策略设计模式了解吗?你在项目中用到了哪些设计模式?
- Spring 中依赖注入有几种方式?怎么做的?
- Spring 框架中有哪些组件了解吗?分别做什么的?
- SpringMVC 的这种 MVC 模式了解吗?他的工作原理是什么?用到了哪些设计模式?(基本每轮面试都被问到)
- SpringMVC 中要接受用户传来的参数要怎么做?REST 的风格呢?
- Spring 中 bean 的创建过程了解吗?
- SpringBoot 和 SpringMVC 的区别和联系是什么?了解 SpringBoot 的启动流程吗?SpringBoot 自动配置是如何实现的?
总结:其实我们看上面的问题,整体来说还是非常地基础,尤其对于实习生和应届生来说,基础是第一位的,就包括百度和京东的面试官都在面试最后给我强调基础的重要性
写在最后
以前觉得自己还小还早,告诉自己才大一大二,可是当突然把自己推向生活的洪流,我仿佛什么都做不了。有了这段找实习的经历,我觉得自己成长了不少,要勇敢地跳出自己的舒适圈,当自己不知道做什么的时候就去面试,让社会对你进行评价。
在这个过程中,我也眼看着很多好的机会从我身边流走,都是因为自己还不够优秀,虽然现在有幸拿到了实习机会,但我也时刻告诫自己要保持学习,沉淀自己,当有更好的机会来临时我能够抓的住。
在 Java 开发这条路上,我也算是刚刚入门,要学的还很多,作为 JavaGuide 的忠实粉丝,再次感谢 JavaGuide! (Guide 哥故意加粗了一下,开心 😄)
Guide 哥注:生活要继续,学习也要继续。对我而言,JavaGuide 还有太多太多不足的地方,后面的日子会继续完善下去。
五面阿里,终获 offer!
前言
在接触 Java 之前我接触的比较多的是硬件方面,用的比较多的语言就是 C 和 C++。到了大三我才正式选择 Java 方向,到目前为止使用 Java 到现在大概有一年多的时间,所以 Java 算不上很好。刚开始投递的时候,实习刚辞职,也没准备笔试面试,很多东西都忘记了。所以,刚开始我并没有直接就投递阿里,毕竟心里还是有一点点小害怕的。于是,我就先投递了几个不算大的公司来练手,就是想着刷刷经验而已或者说是练练手(ps:还是挺对不起那些公司的)。面了一个月其他公司后,我找了我实验室的学长内推我,后面就有了这 5 次面试。
下面简单的说一下我的这 5 次面试:4 次技术面+1 次 HR 面,希望我的经历能对你有所帮助。
一面(技术面)
- 自我介绍(主要讲自己会的技术细节,项目经验,经历那些就一语带过,后面面试官会问你的)。
- 聊聊项目(就是一个很普通的分布式商城,自己做了一些改进),让我画了整个项目的架构图,然后针对项目抛了一系列的提高性能的问题,还问了我做项目的过程中遇到了那些问题,如何解决的,差不读就这些吧。
- 可能是我前面说了我会数据库优化,然后面试官就开始问索引、事务隔离级别、悲观锁和乐观锁、索引、ACID、MVVC 这些问题。
- 浏览器输入 URL 发生了什么? TCP 和 UDP 区别? TCP 如何保证传输可靠性?
- 讲下跳表怎么实现的?哈夫曼编码是怎么回事?非递归且不用额外空间(不用栈),如何遍历二叉树
- 后面又问了很多 JVM 方面的问题,比如 Java 内存模型、常见的垃圾回收器、双亲委派模型这些
- 你有什么问题要问吗?
二面(技术面)
- 自我介绍(主要讲自己会的技术细节,项目经验,经历那些就一语带过,后面面试官会问你的)。
- 操作系统的内存管理机制
- 进程和线程的区别
- 说下你对线程安全的理解
- volatile 有什么作用 ,sychronized 和 lock 有什么区别
- ReentrantLock 实现原理
- 用过 CountDownLatch 么?什么场景下用的?
- AQS 底层原理。
- 造成死锁的原因有哪些,如何预防?
- 加锁会带来哪些性能问题。如何解决?
- HashMap、ConcurrentHashMap 源码。HashMap 是线程安全的吗?Hashtable 呢?ConcurrentHashMap 有了解吗?
- 是否可以实习?
- 你有什么问题要问吗?
三面(技术面)
- 有没有参加过 ACM 或者他竞赛,有没有拿过什么奖?( 我说我没参加过 ACM,本科参加过数学建模竞赛,名次并不好,没拿过什么奖。面试官好像有点失望,然后我又赶紧补充说我和老师一起做过一个项目,目前已经投入使用。面试官还比较感兴趣,后面又和他聊了一下这个项目。)
- 研究生期间,做过什么项目,发过论文吗?有什么成果吗?
- 你觉得你有什么优点和缺点?你觉得你相比于那些比你更优秀的人欠缺什么?
- 有读过什么源码吗?(我说我读过 Java 集合框架和 Netty 的,面试官说 Java 集合前几面一定问的差不多,就不问了,然后就问我 Netty 的,我当时很慌啊!)
- 介绍一下自己对 Netty 的认识,为什么要用。说说业务中,Netty 的使用场景。什么是 TCP 粘包/拆包,解决办法。Netty 线程模型。Dubbo 在使用 Netty 作为网络通讯时候是如何避免粘包与半包问题?讲讲 Netty 的零拷贝?巴拉巴拉问了好多,我记得有好几个我都没回答上来,心里想着凉凉了啊。
- 用到了那些开源技术、在开源领域做过贡献吗?
- 常见的排序算法及其复杂度,现场写了快排。
- 红黑树,B 树的一些问题。
- 讲讲算法及数据结构在实习项目中的用处。
- 自己的未来规划(就简单描述了一下自己未来的设想啊,说的还挺诚恳,面试官好像还挺满意的)
- 你有什么问题要问吗?
四面(半个技术面)
三面面完当天,晚上 9 点接到面试电话,感觉像是部门或者项目主管。 这个和之前的面试不大相同,感觉面试官主要考察的是你解决问题的能力、学习能力和团队协作能力。
- 让我讲一个自己觉得最不错的项目。然后就巴拉巴拉的聊,我记得主要是问了项目是如何进行协作的、遇到问题是如何解决的、与他人发生冲突是如何解决的这些。感觉聊了挺久。
- 出现 OOM 后你会怎么排查问题?
- 自己平时是如何学习新技术的?除了 Java 还回去了解其他技术吗?
- 上一段实习经历的收获。
- NginX 如何做负载均衡、常见的负载均衡算法有哪些、一致性哈希的一致性是什么意思、一致性哈希是如何做哈希的
- 你有什么问题问我吗?
- 还有一些其他的,想不起来了,感觉这一面不是偏向技术来问。
五面(HR 面)
- 自我介绍(主要讲能突出自己的经历,会的编程技术一语带过)。
- 你觉得你有什么优点和缺点?如何克服这些缺点?
- 说一件大学里你自己比较有成就感的一件事情,为此付出了那些努力。
- 你前面跟其他面试官讲过一些你做的项目吧?可以给我讲讲吗?你要考虑到我不是一个做技术的人,怎么让我也听得懂。项目中有什么问题,你怎么解决的?你最大的收获是什么?
- 你目前有面试过其他公司吗?如果让你选,这些公司和阿里,你选哪个?(送分题,回答不好可能送命)
- 你期望的工作地点是哪里?
- 你有什么问题吗?
总结
- 可以看出面试官问我的很多问题都是比较常见的问题,所以记得一定要提前准备,还要深入准备,不要回答的太皮毛。很多时候一个问题可能会牵扯出很多问题,遇到不会的问题不要慌,冷静分析,如果你真的回答不上来,也不要担心自己是不是就要挂了,很可能这个问题本身就比较难。
- 表达能力和沟通能力太重要了,一定要提前练一下,我自身就是一个不太会说话的人,所以,面试前我对于自我介绍、项目介绍和一些常见问题都在脑子里练了好久,确保面试的时候能够很清晰和简洁的说出来。
- 等待面试的过程和面试的过程真的好熬人,那段时间我压力也比较大,好在我私下找到学长聊了很多,心情也好了很多。
- 面试之后及时总结,面的好的话,不要得意,尽快准备下一场面试吧!
我觉得我还算是比较幸运的,最后也祝大家都能获得心仪的 Offer。
2021 华为|字节|腾讯|京东|网易|滴滴面经分享(6个offer)
本文是一位读者的面经分享。希望这篇文章的内容可以对小伙伴们有帮助!
每个人成功的经历都不可复制, 我们可以借鉴吸收别人的经验为己所用。
另外,把自己上岸的经历分享出来是一件非常棒的事情,我在这里实名为这位读者点个赞👍
个人介绍
目前大三,本科就读于电子科技大学。
我在大一进入学校实验室学习,负责数据收集、日常开发、NLP。用到的技术包括:
- 语言:Java、Python
- 技术:
- 爬虫:协程、异步OI、正则表达式
- 后端:SpringBoot、MyBatis、MySQL
- 前端:HTML、CSS、JavaScript、BootStrap
- 深度学习:Pytorch、Keras
在实验室接触的比较广泛,不过感觉不够深入,于是在大二下开始深入后端技术。
我在大二下开始做了些开源项目并深入Java相关技术,深入学习了: Java核心技术、Java虚拟机、Java并发编程、设计模式、MySQL、Spring、SpringBoot、Mybatis。
在大三上期,11月开始准备Java实习相关事务:
一个月的面试后,陆续拿到了字节,网易、京东、滴滴、腾讯和某区块链公司的6个实习offer。
复习经历
因为之前就深入学习过,所以总的复习时间也不长,大概是一周左右,后面是通过边面试边查漏补缺的方式来补短板。
前两天的复习内容:
Java基础
面向对象特性:封装,多态(动态绑定,向上转型),继承
泛型,类型擦除
- 反射,原理,优缺点
- static,final 关键字
- String,StringBuffer,StringBuilder底层区别
- BIO、NIO、AIO
- Object 类的方法
- 自动拆箱和自动装箱
Java集合框架
List :ArrayList、LinkedList、Vector、CopyOnWriteArrayList
Set:HashSet、TreeSet、LinkedHashSet
- Queue:PriorityQueue
- Map:HashMap,TreeMap,LinkedHashMap
- fast-fail,fast-safe机制
- 源码分析(底层数据结构,插入、扩容过程)、线程安全。
Java虚拟机
- 类加载机制、双亲委派模式、3种类加载器(BootStrapClassLoader,ExtensionClassLoader,ApplicationClassLoader)
- 运行时内存分区(PC,Java虚拟机栈,本地方法栈,堆,方法区(永久代,元空间))
- JMM:Java内存模型
- 引用计数、可达性分析
- 垃圾回收算法:标记-清除,标记-整理,复制
- 垃圾回收器:比较,区别(Serial,ParNew,Parallel Scavenge ,CMS,G1)Stop The World
- 强、软、弱、虚引用
- 内存溢出、内存泄漏排查
- JVM调优,常用命令
Java并发
三种线程初始化方法(Thread、Callable,Runnable)区别
线程池(ThreadPoolExecutor,7大参数,原理,四种拒绝策略,四个变型:Fixed,Single,Cached,Scheduled)
有界、无界任务队列,手写BlockingQueue。
乐观锁:CAS(优缺点,ABA问题,DCAS)
悲观锁:
Synchronized:
使用:方法(静态,一般方法),代码块(this,ClassName.class)
1.6优化:锁粗化,锁消除,自适应自旋锁,偏向锁,轻量级锁
锁升级的过程和细节:无锁->偏向锁->轻量级锁->重量级锁(不可逆)
重量级锁的原理(monitor对象,monitorenter,monitorexit)
ReentrantLock:和Synchronized区别?(公平锁、非公平锁、可中断锁….)、原理、用法
ThreadLocal :底层数据结构:ThreadLocalMap、原理、应用场景。
Atomic 类(原理,应用场景)
AQS:原理、Semaphore、CountDownLatch、CyclicBarrier
Volatile:原理:有序性,可见性
第三天的复习内容:
MySQL
- 架构:Server层,引擎层(缓存,连接器,分析器,优化器,处理器)
- 引擎:InnoDB,MyISAM,Memory区别
- 聚簇索引,非聚簇索引区别(从二叉平衡搜索树复习(AVL,红黑树)到B树,最后B+树)
- MySQL、SQL优化方法
- 覆盖索引,最左前缀匹配
- 当前读,快照读
- MVCC原理(事务ID,隐藏字段,Undo,ReadView)
- Gap Lock、Next-Key Lock、Record Lock
- 三大范式
SQL
- 常用SQL
- 连接:自连接,内连接(等值,非等值,自然连接),外连接(左,右,全)
- Group BY 和 Having
- Explain
第四天的复习内容:
Spring
- AOP原理(JDK动态代理,CGLIB动态代理)和 IOC原理
- Spring Bean生命周期
- SpringMVC 原理
- SpringBoot常用注解
设计模式
- 三种类型:创建、结构、行为
- 单例模式:饿汉,懒汉,DCL
- 简单工厂,工厂方法,抽象工厂
- 代理模式
- 装饰器模式
- 观察者模式
- 策略模式
- 迭代器模式
- ….
第五天的复习内容:
计算机网络
- OSI模型、TCP/IP模型
- TCP和UDP区别
- TCP可靠性传输原理:重传、流量控制、拥塞控制、序列号与确认应达号、校验和
- 三次握手、四次挥手过程、原理
- timewait、closewait
- HTTP
- 报文格式
- 1.0 1.1 2.0
- 状态码
- 无状态解决(Cookie Session原理)
- HTTPS
- CA证书
- 对称加密
- 非对称加密
- DNS解析过程,原理
- IP协议、ICMP协议(Ping、Tracert)、ARP协议、路由协议
- 攻击手段与防范:XSS、CSRF、SQL注入、DOS、DDOS
第六天的复习内容:
操作系统
- 进程、线程和协程区别
- 进程通信方式(管道,消息队列,共享内存,信号,信号量,socket)
- 进程调度算法(先来先服务,短作业优先,时间片轮换,多级反馈队列,优先级调度)
- 内存管理:分页(页面置换算法:手写LRU)、分段、虚拟内存
第七天和以后的复习内容:
每天做点刷算法题(剑指offer、LeetCode 面试Hot题) +查漏补缺。
字节跳动
第一面
自我介绍,介绍项目
协程、线程、进程区别
手写LRU(要求用泛型写)、手写DCL
DNS解析过程
输入一个URL到浏览器,整体流程
谈谈Java虚拟机你的认识?垃圾回收算法?垃圾回收器
知道哪些Java的锁?CAS的缺点?
第二面
自我介绍、介绍项目
手写最大堆
设计模式了解吗?几大类型?谈谈工厂模式?
谈一下Java集合框架?HashMap线程安全的吗?会出现什么问题?
说说MySQL的架构?
InnoDB和MyISAM区别?
知道聚簇索引和非聚簇索引吗?B树和B+树区别?
一道LeetCode难问题:接雨水(动态规划解决)
第三面
自我介绍、介绍开源项目
线程池了解吗?原理?可以写个BlockingQueue吗?
说说fast-fail和fast-safe?
了解死锁吗?怎么解决?
进程间通信方式?哪种最高效?
说说MYSQL优化策略?
说了一下部门介绍,主要业务,说可能会转GO等等
第四面(HR)
介绍自己
团队怎么协作?有没有矛盾?怎么解决的?
入职时间?实习多久?
华为
第一面
- 自我介绍
- 谈项目(谈了很久)
- HTTP 的无状态怎么解决?(Cookie Session)
- TCP如何保证可靠性传输?(校验和,序列号和确认应答号,重传,流量控制,拥塞控制)
- ARP过程?
- 进程调度算法?
- 一道动态规划题目:不同路径
第二面
自我介绍
谈项目(你觉得收获最大的项目)
谈谈Spring AOP 和 IOC
谈谈你知道的MySQL所有内容
手写个归并排序
谈谈你对分布式系统的认识?
谈谈你对华为的认识?华为的文化和价值观?
HR
技术面试都通过了,问HR怎么样,说应该没问题,等了一星期offer,最后发offer的时候,HR说我的性格测试没通过,Offer审批不下来,人傻了。因为华为在成都,字节在北京,而且技术官的意向是很稳能进华为,我想着在家近的地方实习,在等待的一周中就把字节拒了,最后华为没发到offer,直接架空,崩溃!第一次找实习没太多经验,策略不对,心里很难受,不过调整了一下,继续了新的面试
网易
第一面
自我介绍
介绍一个对自己影响深刻的项目
说说进程间调度的算法
说说匿名函数
说说协程、线程、进程。
你对游戏引擎了解多少?
手写地杰斯特拉算法?
了解A*算法吗?
说说Python和Java的区别?
Java是怎么进行垃圾回收的?
然后聊了很多生活上的问题,非技术问题。
第二面
自我介绍
介绍项目
说说深度优先搜索算法、回溯算法
一道算法题:一个走迷宫问题,DFS+回溯解决。
你对C熟悉吗?Lua使用过吗?
介绍业务,主要工作内容。
HR面
自我介绍
介绍一个项目中遇到的问题,怎么解决的?
介绍一下博客?开源项目?为什么花时间做这些?
大学最成功的一件事?
滴滴
第一面
自我介绍、介绍项目
Java面向对象的三大特性?
了解Java哪些锁?Synchronized优化内容?锁升级过程?
谈谈Java虚拟机?类加载机制?
知道双亲委派模式吗?有什么好处?
Java运行时内存分区?
死锁了解吗?如何解决?
哪些对象可以作为GC ROOTS?
了解的设计模式?手写一下DCL吧
第二面
自我介绍
介绍项目(难点以及怎么解决的?)
谈谈MySQL的各种引擎?
覆盖索引和非覆盖索引区别?
MYSQL优化方法有哪些?
讲讲HashMap的原理,put过程?resize过程?线程安全吗?死循环问题?
了解什么中间件吗?
讲讲Java里面的锁?
一道算法题:最长公共子串
HR面
自我介绍
到岗时间
自己的优势
大学最失败的一件事
对加班的看法
京东
第一面
自我介绍
谈项目
TCP如何保证可靠传输?拥塞控制算法?
讲讲Spring的AOP?
SpringBoot常用哪些注解?
谈谈Java虚拟机?
垃圾回收算法有哪些?
了解哪些垃圾回收器?讲一下CMS垃圾回收过程
算法题:
a. 两个栈实现队列
b. 最近公共祖先节点
第二面
自我介绍
讲讲Java集合框架,HashMap原理。
知道哪些锁?
谈谈公平锁和非公平锁?
Synchronized和ReentrantLock区别
MySQL的索引为什么快?有哪些索引?原理数据结构?
MySQL有哪些优化的策略?
死锁了解吗?
ThreadLocal了解吗?原理?
手写一个堆排序。
一道算法题:完全平方数(动态规划)
HR面
自我介绍
多久可以到岗?实习时间?
对加班看法?
如何团队分工的?
腾讯
第一面
自我介绍
介绍项目
说说协程和线程区别?
Java虚拟机的作用?垃圾回收的过程?
了解的垃圾回收器?
手写快排
算法题:按K位反转链表
一百亿个数,n个机器,怎么排序?(桶排序)
第二面
自我介绍
介绍项目
TCP和UDP区别?如何保证可靠性?
HTTP的状态码记得哪些?
ICMP是哪层的?有什么用?
会哪些框架?
Spring的AOP认识?
MySQL InnoDB和MyISAM区别?
谈谈各种索引?为什么用B+树不用B树?
死锁的条件?如何解决?
OOM怎么排查?
介绍业务
HR面
自我介绍
多久能来实习?实习多久?
加班看法?
看你掌握技术挺多,如何快速学习一个技术的?
总结
因为之前学的也比较深入,复习时间也没用太多,主要就是写点算法题保持手感。
面试中遇到的问题,9成都已经复习了,而且也比较基础,也都在掌握之中。
像中间件、微服务这些我没写在简历上,不是很会,面试官也不会刻意刁难你,实习的话,感觉大厂可能更注重基础和对知识的深入度,面试了一个月收货还是挺多的,希望总结一下面经,帮到更多的人~
准备大厂面试的话,注重基础,多练算法题,基本上就没问题了!加油!
2021 虾皮,网易云,京东,阿里校招面经!附参考答案
虾皮 sg 三轮面经(通过)
一面(2021.7.8)
【项目】介绍下百度的实习经历
用户登录密码存储,哈希和加盐的过程是在前端还是后端
浏览器输入 URL 过程
tcp 连接建立过程
http 与 https 的区别
【项目】介绍下分片降低 redis 热键访问压力
写 db 数据如何同步到 cache
cache key 失效后大量流量请求 db 如何处理(数据存在于 db 种)
【项目】介绍下数据库慢查询的优化
MySQL 索引默认数据结构
B+树相对 B 树优点
MySQL 里的主键,外键以及组合索引分别在什么场景下使用
为什么实际项目里建议不用外键
【设计题】根据查询场景设计索引
常见的用来计算哈希的方法
如何解决哈希冲突
【算法题】实现 LRU cache
【算法题】给定数字 N,打印 1~N 中心螺旋矩阵
参考答案 :
如何加密传输和存储用户密码 : https://zhuanlan.zhihu.com/p/36603247
HTTP vs HTTPS(应用层):https://javaguide.cn/cs-basics/network/http&https.html
- 计算机网络常见知识点&面试题(补充):https://javaguide.cn/cs-basics/network/other-network-questions.html
- 安全系列之——主流 Hash 散列算法介绍和使用:https://cloud.tencent.com/developer/news/682510
- Redis 知识点&面试题总结:https://javaguide.cn/database/redis/redis-questions-01.html
- MySQL 知识点&面试题总结 : https://javaguide.cn/database/mysql/mysql-questions-01.html
- 如何在 Java 中实现 LRU 缓存 :https://www.baeldung.com/java-lru-cache
二面(2021.7.16)
团队介绍
自我介绍
印象最深刻的项目
redis 的 zset 数据结构
加盐的目的
重放指的是什么
介绍下彩虹表,彩虹表为什么叫彩虹表
用户登录状态怎么保持
https 为什么需要证书
加盐过程,盐如何存储
实现一个向用户展示商品历史价格的网站,
百万级别商品,爬虫脚本怎么解决
什么样的分布式方案
布隆过滤器原理
用户量增大,如何提升系统容量
消息队列作用
loadbalancer 如何实现分布式
缓存如何实现分布式
哈希如何减少重哈希代价
如何分析热键
增强 db 能力的方案
网站会面临的安全问题
介绍下 csrf,攻击者如何拿到用户身份,csrf 预防方法,csrf token 如何实现无法伪造
如何应对 shopee 钓鱼网站
为什么选 sg shopee,国内与 sg 的倾向,有通过渠道了解过 sg 吗
字节与百度工作方式的区别,眼下喜欢哪一种
反问环节
参考答案(部分参考答案和一面中的重合了,这里就不多放一编了):
- 什么是彩虹表?:https://www.zhihu.com/question/19790488 、密码破解的利器——彩虹表(rainbow table):https://www.jianshu.com/p/732d9d960411
- TikTok 三面:“聊聊 TCP/IP 常见的攻击手段”:https://mp.weixin.qq.com/s/U8S8IEb_rH5FHUKvoBJCUg
- 消息队列知识点&面试题总结 : https://javaguide.cn/high-performance/message-queue/message-queue.html
- 布隆过滤器:https://javaguide.cn/cs-basics/data-structure/bloom-filter/
- 谈谈 redis 的热 key 问题如何解决:https://www.cnblogs.com/rjzheng/p/10874537.html、如何快速定位 Redis 热 key:https://www.infoq.cn/article/3l3zaq4h8xpnom2glsyi
HR 面(2021.7.26)
英文自我介绍
为什么投递 shopee 职位
为什么选择 sg
sg 介绍
反问环节
网易云音乐三轮后端(通过)
一面(2021.8.30)
自我介绍
【项目】实习项目在技术架构上,除了语言的差别,还有哪些更深入的差异
【项目】选择一个实习项目,介绍下具体做的事情
【项目】随着活动事件越来越多,如何从后端进行设计上的优化
使用 mq 对业务进行异步解耦之后,在消息消费上有哪些需要注意的点
【项目】在发放奖励场景下,如何保证消费的幂等性
【redis】有了解过 redis 集群如何部署的吗
【redis】用 redis 如何实现分布式锁
【redis】zset 实现原理是什么
【redis】跳表优化的理念是什么
【redis】为什么采用跳表,而不使用哈希表或平衡树实现呢?
【redis】在数据量比较小时,跳表相较其他数据结构的缺点是什么
【项目】数据库是单节点还是分布式的,有做分库分表吗
【项目】在你的业务场景下,是怎么进行分库分表的?
【分库分表】在查询分库分表的数据时,没有带分库分表的 key,底层查询是怎么样的,对性能有影响吗
【分库分表】分库分表下 ID 全局唯一是如何做的?
【MySQL】MySQL 联合索引查询时需要注意哪些问题
【MySQL】从数据结构角度分析为何需要最左匹配原则
【项目】有性能优化案例吗
【IO】BIO 和 NIO 的区别是什么
【JVM】java gc 算法了解哪些
【JVM】可达性分析里哪些对象可以作为 gc root
【JVM】类的 static 变量时 gc root 吗,一个普通 map 对象的 key,value 可以被回收吗
【JVM】想要 map 里的 value 在 gc 时可以被及时回收,应该对 map 做什么样的改造呢
【多线程】java 多线程下的变量可见性有什么解决方案
【多线程】阻塞队列里锁的如何实现的,设计阻塞队列时,主要阻塞在哪些操作上
【多线程】线程安全的数组和链表有哪些
【网络】tcp 协议的连接、断开过程
【网络】挥手时为什么需要等待 2 倍 MSL
反问环节
参考答案 :
- Redis 知识点&面试题总结:https://javaguide.cn/database/redis/redis-questions-01.html
- 【Redis】拼多多面试官问我 zset 底层是如何实现的,我反手就把跳表的数据结构画了出来:https://segmentfault.com/a/1190000037473381
- 为啥 redis 使用跳表(skiplist)而不是使用 red-black?:https://www.zhihu.com/question/20202931
- 分布式 ID:https://javaguide.cn/distributed-system/distributed-id.html
- 读写分离&分库分表:https://javaguide.cn/high-performance/read-and-write-separation-and-library-subtable.html
- MySQL 知识点&面试题总结:https://javaguide.cn/database/mysql/mysql-questions-01.html
- JVM 垃圾回收详解:https://javaguide.cn/java/jvm/jvm-garbage-collection.html
- Java 并发常见知识点&面试题总结(进阶篇):https://javaguide.cn/java/concurrent/java-concurrent-questions-02.html
- JDK 提供的并发容器总结:https://javaguide.cn/java/concurrent/java-concurrent-collections.html
- 「为什么这么设计系列」为什么 TCP 建立连接需要三次握手:https://draveness.me/whys-the-design-tcp-three-way-handshake/
- 用 Java 如何设计一个阻塞队列,然后说说 ArrayBlockingQueue 和 LinkedBlockingQueue:https://www.cnblogs.com/jimoer/p/14887921.html
二面(2021.9.4)
自我介绍
哪个实习项目成长比较大
【项目】实习项目里主要做了哪些的工作
【项目】了解任务系统等项目相关信息
【项目】项目中最大的难点是什么
【项目】如何保证缓存和数据库的一致性
【项目】分布式锁是怎么用的
【分布式锁】如果需要一个严格的分布式锁,需要怎么做
【分布式锁】如何处理分布式锁因为超时被提前释放的问题
【设计题】高并发场景下评论点赞功能的设计(点赞数量须持久化到 db)
反问环节
参考答案 :
- 拜托,面试请不要再问我 Redis 分布式锁的实现原理!【石杉的架构笔记】:https://juejin.im/post/5bf3f15851882526a643e207
- 微博架构组面试:类微博点赞系统设计:https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247505978&idx=2&sn=d9190cb5345c5798a07d460811d51c74&chksm=cea197f1f9d61ee772de9c02f2b04f432b4493078152c67626c7bccb48b5ca31b17ca4ec6412&token=1069133552&lang=zh_CN#rd
HR 面(2021.9.8)
自我介绍
base 地偏好
还有哪些公司在面试中,面试进展
有什么爱好或热爱
未来职业规划
为什么选择互联网
求职过程中在团队或工作氛围上有什么样的想法
平时有使用过网易云音乐产品吗
平时在什么场景下使用到我们的产品
听歌听哪种风格多一点
了解乡村音乐吗
基于哪些维度对面试公司做出最终的选择
期望第一年的收入达到什么范围
反问环节
京东后端三轮面经(通过)
一面 (2021.7.28)
自我介绍
【项目】收获最大的项目,两个项目有什么不一样,自己承担什么角色,实现哪些内容
【项目】项目为什么引入 etcd
【Java 基础】Java 中的==和 equals 区别
【Java 基础】hashmap 的实现原理、1.8 之后的改变
【Java 基础】接口和抽象类的区别
【Java 基础】可变长参数
【Java 新特性】java8 有哪些新特性
【Java 多线程】介绍下 ThreadLocal 和使用场景
【JVM】jvm 优化工具
【设计模式】简单工厂和抽象工厂的区别
【设计模式】熟悉的设计模式有哪些
【设计模式】优化 if-else 方法
【MySQL】MySQL 里行锁和表锁及其特性
【MySQL】介绍乐观锁、悲观锁、重入锁、排他锁
对方屏幕共享项目中一个源文件,找代码里的优缺点(单例实现的策略模式)
【算法题】递归实现单链表反转
反问环节
参考答案 :
- Java 基础知识&面试题总结: https://javaguide.cn/java/basis/java-basic-questions-01.html
- 接口和抽象类有什么区别?:https://www.zhihu.com/question/20149818
- Java 集合框架基础知识&面试题总结: https://javaguide.cn/java/collection/java-collection-questions-01.html
- Java 并发常见知识点&面试题总结(进阶篇):https://javaguide.cn/java/concurrent/java-concurrent-questions-02.html
- 设计模式之工厂模式(factory pattern):https://www.cnblogs.com/yssjun/p/11102162.html
- 设计模式总结 :https://www.cnblogs.com/chenssy/p/3357683.html
- 条件语句的多层嵌套问题优化:https://mp.weixin.qq.com/s/7i-TPFovLwrSmbWaIiX8dQ
- MySQL 锁:灵魂七拷问 :https://tech.youzan.com/seven-questions-about-the-lock-of-mysql/、通俗易懂 悲观锁、乐观锁、可重入锁、自旋锁、偏向锁、轻量/重量级锁、读写锁、各种锁及其 Java 实现!:https://zhuanlan.zhihu.com/p/71156910
二面(2021.8.5)
自我介绍
字节和有取向吗
【项目】介绍下 redis 在项目中是怎么使用的?
【项目】介绍下 redis 分布式锁项目中有什么作用?怎么实现的?
【MySQL】MySQL 优化的方法
【Java 多线程】介绍下 ThreadLocal 的原理和使用场景
【Java 基础】equals 和 hashcode 区别
【Java 基础】java 的基本类型和空间大小
【Spring Boot】Spring Boot 自动装配原理
【Java 基础】Java I/O 流有用过吗
【设计模式】软件设计原则有哪些?
【设计模式】介绍下模板方法
【设计模式】装饰器模式
找工作的标准
像从事的方向偏业务还是偏底层框架
反问环节
参考答案 :
- MySQL 高性能优化规范建议:https://javaguide.cn/database/mysql/mysql-high-performance-optimization-specification-recommendations.html
- Java 基础知识&面试题总结: https://javaguide.cn/java/basis/java-basic-questions-01.html
- Java 并发常见知识点&面试题总结(进阶篇):https://javaguide.cn/java/concurrent/java-concurrent-questions-02.html
- Spring Boot 自动装配原理:https://javaguide.cn/system-design/framework/spring/spring-boot-auto-assembly-principles.html
- 设计模式总结 :https://www.cnblogs.com/chenssy/p/3357683.html
阿里巴巴后端面经(通过)
【cto线-业务平台事业部】一面(2021.8.11)
【闲聊】去年为什么没有接实习offer
【闲聊】为什么没有选择直接去字节和百度?两家公司感觉有什么区别?
【闲聊】对阿里的印象怎么样
【项目】介绍下你在百度实习做的项目,做这个项目过程中有什么难点
【项目】讲讲你对依赖倒置原则的理解,你是如何基于这个原则来重构项目代码的?
【项目】如何保证重构不会影响正常的业务?
【项目】单测的覆盖率有多少?
【编程语言】Go 跟 Java 的区别在哪?为什么Docker/Kubernetes们选择Go?
【编程语言】协程和线程有什么区别
【项目】简单说下单点登录的实现
【认证授权】如果 Cookie 禁用的话如何解决?Cookie 和 Session 有什么区别?
【认证授权】什么是 Token?什么是 JWT??如何解决token方案无法主动过期的问题?
【认证授权】RBAC 模型了解吗?
【多线程】Java线程池的原理
【JVM】jvm的内存区域?
【JVM】垃圾收集算法有哪些?如何判断一个对象是否已经死亡?
【设计模式】设计模式了解哪些
参考答案 :
- 再读《重构》- ThoughtWorks 洞见:https://insights.thoughtworks.cn/reread-refactoring/
- 单元测试到底是什么?应该怎么做? - 腾讯技术工程的回答 - 知乎 :https://www.zhihu.com/question/28729261/answer/1058317111
- 为什么Docker/Kubernetes们选择Go?:https://zhuanlan.zhihu.com/p/446697672
- Go 面试官:什么是协程,协程和线程的区别和联系?:https://segmentfault.com/a/1190000040373756
- JWT 身份认证优缺点分析以及常见问题解决方案:https://javaguide.cn/system-design/security/advantages&disadvantages-of-jwt.html
- 认证授权基础:https://javaguide.cn/system-design/security/basis-of-authority-certification.html
- Java线程池学习总结: https://javaguide.cn/java/concurrent/java-thread-pool-summary.html
- Java 内存区域详解:https://javaguide.cn/java/jvm/memory-area.html
- JVM 垃圾回收详解:https://javaguide.cn/java/jvm/jvm-garbage-collection.html
- 设计模式总结 :https://www.cnblogs.com/chenssy/p/3357683.html
【cto线-业务平台事业部】二面(2021.8.20)
【闲聊】自我介绍
【闲聊】学校里有无发表过论文、专利或者参加过竞赛
【闲聊】百度、字节有没有给offer?还有其他offer吗?
【项目】百度项目的业务效果怎么样、QPS怎么样?
【项目】介绍下用户邀请分享的实现?
【JVM】jvm内存分为哪几个区域,哪些是线程私有的,哪些是线程共享的
【Java基础】ClassNotFound 与 NoClassDefinedError 有什么区别
【多线程】能说下什么是CAS吗?什么是 ABA 问题?ABA 问题怎么解决?
【MySQL】MySQL数据库的索引为什么使用B+树而不是B树
【Redis】Redis中有哪些数据结构?为啥 redis 使用跳表(skiplist)而不是使用红黑树?
【Redis】有没有遇到过缓存被击穿的情况
【算法题】leetcode 1478. 安排邮筒
参考答案 :
- ClassNotFoundException 和 NoClassDefFoundError 的区别:https://cloud.tencent.com/developer/article/1153789
- 如何理解ABA问题:https://elsef.com/2020/03/08/如何理解ABA问题/
- AQS 原理以及 AQS 同步组件总结:https://javaguide.cn/java/concurrent/aqs.html
- 深入理解 MySQL 索引底层原理:https://zhuanlan.zhihu.com/p/113917726
- Redis 知识点&面试题总结 :https://javaguide.cn/database/redis/redis-questions-01.html
- 为啥 redis 使用跳表(skiplist)而不是使用红黑树?:https://www.zhihu.com/question/20202931
【cto线-零售云事业部】终面(2021.9.18)
(流程转到该部门)
【闲聊】自我介绍
【实习】百度实习团队规模,项目流量怎么样
【项目】采用哪些方法应对高并发流量
【项目】配置缓存的时候有哪些考虑,哪些数据放哪些不放入缓存
(HR入会)介绍/宣传部门业务
反问环节
五、练级攻略篇
如何成为一个合格的程序员?
对于下面的每一点建议的理解,每个人可能都不一样。
如果你觉得某一点对你有用的话,不要关了这篇文章之后你就忘记了,建议你一定要记录下来。从当下开始就去努力践行。
本文概览 :
- 用好 Google
- 修改代码要慎重
- 谨慎使用网上搜索的代码片段
- Code Review 很重要
- 尽量减少 TODO
- 不要放任破窗
- 不要孤立地写代码
- 试着从更高的层面去了解大部分代码的功能
- 尽量多沟通交流,提高表达能力
- 你永远无法写出完美的软件
- 工作经验 != 能力
- 提高自己的核心竞争力
用好 Google
相比于百度,更建议使用 Google。如果你无法访问 Google 的话,必应也是不错的。
分享一些个人使用 Google 搜索的实用建议, 这里就不专门介绍各种繁杂的搜索参数了,说了也记不住,实用性不强。
1、选择合适的关键词,多个关键词手动使用空格进分割。
如果搜索出来的内容你不满意的话,建议重新更换/删减关键词进行搜索或者调整关键词的顺序。
2、利用好 Google 图片搜索,一张好的技术配图有更大概率带你进入更优质的页面。
3、往往同时需要多打开多个页面之后,才有可能找到自己需要的内容。
你可以先从第一页的搜索结果中选择打开 5 个页面,内容差的直接关闭,全平台采集文章类的盗文网站直接选择屏蔽掉即可。
像下面这个网站就是一个典型的需要被屏蔽的垃圾文章收集网站,文章排版和网站体验极差且文章都是从其他平台收集整理过来的。
你可以使用 uBlacklist 这个 Chrome 插件屏蔽特定的网站。
4、搜索参数上加上 site:网站或域名 搜索指定网站或者域名下的内容
搜索参数有很多,个人比较常用的是 site:网站或域名 ,更多搜索参数你可以在这篇文章中找到:Google Search Operators: The Complete List (42 Advanced Operators)。
5、过滤搜索结果
你可以通过 Google 高级搜索过滤搜索结果,缩小搜索结果的范围,地址:https://www.google.com/advanced_search 。
我们上面讲到的 site:网站或域名 功能也可以在这个高级搜索页面上完成。
修改代码要慎重
修改代码之前,一定要思考清楚,不要自以为很简单,结果改了之后出现了大问题。这个在我们写代码的时候也一样,一定要思考清楚之后再写。
就拿我自己举例子,我们一般项目上都是开发做完相关功能之后,测试随后会对你做的功能进行一系列测试。很多时候,QA 测出一些问题之后,我都自以为很简单,并没有太多思考,然后修改之后发现又出现了其他问题。
代码很多时候就是这样的,这个地方的 Bug 补上了,说不定另外一个地方的 Bug 又出现了。所以说,修改代码和写代码的时候一定要慎重,一定要思考清楚一点。
谨慎使用网上搜索的代码片段
作为一个 CRUD 程序员,我们经常需要在网上搜索各种代码片段用在项目上。
不过,如果使用不当,这些代码片段就会在潜移默化中引起项目腐化以及代码变质。因此,在使用别人的代码片段的时候,一定先要搞懂了这段代码之后再使用,一定不要直接复制粘贴!
并且,一定不要无脑信 StackOverflow ,上面很多问题的回答以及代码片段也不是 100% 准确的,依然还有很多存在问题或者可以优化的回答以及代码片段。在国内的话,大部分程序员都是通过在 CSDN 上找答案,那你就更需要多留点心思上,上面的低质量文章太多了,能不用就不用。
Code Review 很重要
代码复查或者说 Code Review 很重要!这是一项成本不大,但是做好了之后收益非常非常大的活动。
一般情况下,大部分项目定期都是要做 Code Review(一天一次最好)的 ,尽量细致到每一行代码或者每一行重要的代码。对于代码中存在的问题,不论是命名问题、潜在的 Bug 还是某部分代码有更好的写法都要当场指出。
我听到过很多人说平时工作太忙,根本没有时间 Code Review,我觉得这只是一个逃避 Code Review 的接口。孤尽大佬在他分享《Code Review 是一场苦涩但有意思的修行》 这篇文章中也说到:
业务跑得快,代码写得快,可能写的是一堆没有营养甚至是有毒的代码。我们需要追求的是 Code Review 的效能,而不是逃避 Code Review 。Code Review 是一种修行,对于双方都是一样的收获。
尽量减少 TODO
TODO 描述的是那些我们应该做,但是出于某些原因暂时还没有做的事情。
随着项目的发展,你们项目的 TODO 是不是越来越多了呢?你自己写的 TODO 最后是不是到了项目结束或者上线还没有做呢?
实际上这是一个不那么好的习惯,现实工作中尽量做到记得定期查看 TODO 注释,能完成的尽量完成!不能完成的呢?emmm….留着以后接手代码的人来做吧(开个玩笑~能做还是要尽量做)!
不要放任破窗
这是《程序员修炼之道》这本书中的一个建议,这里分享一下原文的描述:
熵在软件中定义和解释:虽然软件开发不受绝大多数物理法则的约束,但我们无法躲避来自熵的增加的重击。熵是一个物理学术语,它定义了一个系统的“无序”总量。不幸的是,热力学法则决定了宇宙中的熵会趋向最大化。当软件中的无序化增加时,程序员会说“软件在腐烂”。有些人可能会用更乐观的术语来称呼它,即“技术债”,潜台词是说他们总有一天会偿还的——恐怕不会还了。
不要搁置“破窗”(糟糕的设计、错误的决定、低劣的代码)不去修理。每发现一个就赶紧修一个。 如果没有足够的时间完全修好,那么就把它钉起来。也许你可以注释掉那些糟糕的代码,显示一行“尚未实现”的信息,或用假数据先替代一下。采取行动,预防进一步的损害发生,表明一切尽在你的掌握中。
不要孤立地写代码
一定不要孤立地写代码,多看看别人的代码。 这样我觉得有下面几方面的好处:
- 避免了团队的单点因素,比如某一部分的代码只有某个人懂;
- 提高了代码质量;
- 从别人的代码中或许也能学到一些东西;
- ……
另外,国外很多公司都是结对编程,这玩意好像在国内行不通啊!
结对编程(英语:Pair programming)是一种敏捷软件开发的方法,两个程序员在一个计算机上共同工作。一个人输入代码,而另一个人审查他输入的每一行代码。输入代码的人称作驾驶员,审查代码的人称作观察员(或导航员)。两个程序员经常互换角色。
试着从更高的层面去了解大部分代码的功能
大型系统几乎没有一个人能够明白所有代码或者功能。除了你正在开发的功能之外,试着从更高的层面去了解大部分代码的功能,这样你就可以理解各个功能块之间是如何交互的了。 这个建议在我经历的上一个项目(学生答题类)中感受颇深。整个项目虽然不是很庞大,但是业务功能点还是比较多,初期的时候,我没有搞懂学生教材选择那块的逻辑 ,导致后面我做学生答题统计模块的时候又花了很久询问相关的同事才搞清楚。
你永远无法写出完美的软件
这是《程序员修炼之道》这本书中的一个建议,这里分享一下原文的描述:
软件不可能是完美的。对于在所难免的错误,要保护代码和用户免受其影响。
没有完美的软件!!!
工作经验 != 能力
对于咱程序员来说,有一个很现实但又不得不面对的问题:“你的工作经验是否匹配你自己当前的能力?”。
我们刚从学校毕业的时候,最大的优势就是“年轻”。说好听点,年轻意味着你未来可发展空间要稍微更大一点。然而!现实中,互联网公司更偏爱年轻人,往往是因为年轻人更有精力加班、成本也更低。
对于工作时间比较长时间的朋友来说,“年轻”这个优势就不复存在了。我们需要依靠我们的工作经验来为自己打开一片天地。然而!咱这一行又存在很多劳动密集型的那些工作,工作经验并不代表你的真实能力/水平。
如果你工作了5年,甚至是10年,都是在做一些简单的业务系统,每天的工作都是 CRUD 的话。我觉得你实际的工作经验,可能只有 1年左右。那你出去找工作的话,别人肯定不愿意招聘你了。
提高自己的核心竞争力
那很多小伙伴都要说了:“我们公司业务比较简单,基本都是 CRUD 的任务,没办法提高自己的能力啊!”。其实,解决这类问题的办法也很简单,关键要看我们是否愿意跳出自己的舒适区。我们作为一个正常人,往往都是会更倾向于过比较安逸的生活嘛!人之常情,无可厚非!
如果工作无法给你足够的锻炼,那你就要自己多留点心,工作之外多提高一下自己的核心竞争力。 比如你可以课外多去研究一些优秀的开源项目(比如 Kafka、sharding-jdbc)、多看看自己平时经常使用的框架(比如SpringBoot、MyBatis)的源码。
我还推荐你没事就要多造轮子,多写点框架层面的东西,而不是天天用别人的框架。
我们实际项目开发中是比较忌讳造轮子的,但是,自己在学习过程中造轮子绝对是对自己百利而无一害的!造轮子是一种特别能够提高自己系统编程能力的手段。
通过自己造轮子,你更能体会到框架底层的原理,更有机会接触到一些底层的东西,这对你以后的发展绝对是百利而无一害的!
如果说你从你的工作中学习不到什么对你有价值的东西,每天的工作强度又很大,你连自己充电的时间都没有的话。那我建议你可以直接跳槽,跳槽到一家对你的发展更有帮助的公司。
人生路漫漫,不要过于在意短期的利益,眼光要放的更长远一些。
另外,在我们平时日常工作中,有一个非常重要的能力,经常会被我们忽略。这个能力就是系统设计能力 。
不要把自己局限在技术上
技术作为我们程序员的核心竞争力,毋庸置疑,非常重要!但是,不要把自己的“束缚”在“技术”上,被“技术”绑架。
技术本身往往不会产生价值,必须依托于产品才能体现。 比如你是一个提供技术服务的公司,你创造的技术产品有人买单或者有人使用。再比如你是一个普通的互联网公司,你们通过技术创造了某个热门 App 为公司创造了营收。
但是,我们大多数人喜欢在技术上自嗨,这当然也包括我自己。
拿我自己来说,我觉得在技术之外,我还需要提高自己的产品设计能力、演讲能力、理财能力……。
产品设计能力,一是为以后自己可能独立做产品做下铺垫,二是这个在日常工作中也会用到。
演讲能力和理财能力就不用多说了吧!当代社会必备的能力。
往美好的方向讲,技术是为了让人们的生活更好。现实来说,技术就是为了帮助公司创造更多利润。
另外,技术更新换代太快,但是,底层技术比如数据结构和算法、计算机组成原理、操作系统的内容其实一直没怎么么改变的。就那些东西。
当自己年龄上来之后或者成家之后,自己投入在技术上的时间一定是会减少的。为了避免自己未来产生“技术焦虑” ,还是要把这些底层东西给吃透啊!
如何更有效地提高编程能力?
对于下面的每一点建议的理解,每个人可能都不一样。
如果你觉得某一点对你有用的话,不要关了这篇文章之后你就忘记了,建议你一定要记录下来。从当下开始就去努力践行,知行合一。
本文概览 :
- 练好基本功,勿过于追赶技术时髦
- 选择值得投入的技术
- 深入学习,学会总结沉淀
- 避免货物崇拜编程
- 批判性地分析你读到和听到的东西
- 提高系统设计能力
- 不要让技术栈限制住了手脚
- 造轮子
练好基本功,勿过于追赶技术时髦
一定要把基本功的修炼放在首位。高楼大厦起于坚实的地基,顶尖的程序员同样起于过硬的基本功。
哪些算是程序员的基本功呢?
- 技术方面: 计算机技术基础知识、优秀的编码实践、系统设计、设计模式、各种技术的原理,定位问题的能力等等。
- 非技术方面 : 对业务的理解能力、抗压能力、表达能力等等。
一定不要把自己的精力都花在各种工具库、框架和中间件的使用以及配置上!从投资角度来说,这些东西的投资价值并不高,有很大概率过几年就过时或者被淘汰了。举点例子:Struts2 被 Spring 干掉、Spring 又被 Spring Boot 替代、ActiveMQ 被 Kafka,RabbitMQ等优秀的消息队列干掉,太多太多这样的例子了。就算是 Spring Boot 目前依然存在着被其他框架替代的可能性,没有什么永恒不变,尤其对于工具库、框架和中间件来说。
不过,这些工具库和中间件的底层原理还是值得学习的。
基础以及原理性的知识一般不会被淘汰,只会被更先进的技术给颠覆。
如何修炼自己的基本功呢?
- 不断学习,提升自己的认知。
- 不要单纯为了完成需求而完成需求,还要考虑代码质量比如可读性、bug 数量、能否对扩展友好等等
- 经常总结复盘。
- 理论+实践并行。
选择值得投入的技术
在我大学刚学 Java 后台开发的时候,我学习过什么呢?实话实说是 JSP、Struts2….这些现在看起来老掉牙的技术,这些技术放在现在确实没有学习的理由了。
我自己当时学这些实际也是踩了坑,被一个学长忽悠了,他对我说很多公司做项目还是用这些技术。奈何他当时比我厉害,所以,我选择相信了他。
我们每个人的时间都是有限的,这个在工作之后的感触尤其明显,所以,我们一定要尽量在有限的时间去学习那些值得我们长期投入学习的技术。
一项技术是否值得长期投入学习,简单来说,我觉得主要可以下面 3 点:
- 这个技术的学习成本。
- 这个技术的发展势头如何(Google trends 能很好的反映一项技术的发展势头)。
- 看看一些业界比较权威的技术大佬对这个技术的看法。
深入学习,学会总结沉淀
做咱们这一行,很多人最喜欢抱怨的就是:“我每天都是在做重复的 CRUD 工作啊!没啥意思。”、“这个公司的项目不行,没用到某某高大上的技术”……
然而,很多这样抱怨的人连特么 CRUD 都写不好,写个基本的业务功能一测贼多 Bug。
我在刚工作那会也是这样的。不过,现在再听到别人这样抱怨的时候,我一般都会首先觉得这个人有点浮躁,不知道如何学习提升自己。
单纯把业务代码写好真的没那么容易,抱怨自己天天做 CRUD 工作之前,一定要先看看自己 CRUD 的代码写好没。
另外,就单纯一个 CRUD 的工作,只要你善于学习,还是能从项目中挖掘到很多值得你学习的点。 举个例子,你项目用的是 JPA ,你把 JPA 玩的很溜了之后,是不是可以考虑去研究一下 JPA 的底层原理呢!还比如说,项目某个模块的响应速度太慢,自己是不是可以考虑通过某些手段比如 SQL 优化、DB 参数调优、JVM 参数调优、索引、读写分离、缓存等手段来优化一下呢!
真的!就单纯一个最基本的 CRUD 的项目要考虑到的点就已经够多了。一定不要眼高手低,整天就想着微服务、高并发,总觉得“低级”的开发工作配不上自己的身份了。
再来聊一下回顾总结。
很多时候,我们做一个项目,做完了之后就感觉自己就和这个项目没有关系了。项目上学到的一些东西或者可以改进的地方,完全不想花时间总结。
以至于,很多年之后,你学到的东西还是比较零散的,不成体系。 别人询问你“有没有从上个项目学到点什么?”的时候,自己却没法回答。
不会进行思考总结,你做再多的项目,了解再多的技术又如何?可能就只是表面上好看而已,有些东西永远都成为不了自己的。
对应到我们平时学习技术的时候也是一样,记得一定要多总结思考!
不要让技术栈限制住了手脚
一定不要有那种学了一种编程语言或者框架就想着用这一种编程语言或者框架做任何事情的想法。
一定不要让技术栈限制住了自己!!!
很多时候你用这种编程语言很难做到的事情,使用其他编程语言可能很简单就解决了,就比如说我们项目平时如果有爬虫场景,基本都是用 Python 写的,又快有简单。
避免货物崇拜编程
何为货物编程?
维基百科是这样解释的:
货物崇拜编程(Cargo Cult Programming)是一种计算机程序设计中的反模式,其特征为不明就里地、仪式性地使用代码或程序架构。货物崇拜编程通常是程序员既没理解他要解决的 bug、也没理解表面上的解决方案的典型表现。
简单来说,货物编程就是我们不明就理地使用各种框架/优秀实践(比如设计模式)/软件架构,最后把项目搞得像个四不像。
列举一些我身边发生过的实际的例子吧!
- 看到一些比较火的框架就直接套用在自己的项目上,而不知道这个框架究竟能解决项目上的什么问题?是否适合项目?有没有什么风险?
- 学习了某个设计模式/工程实践之后,不顾项目实际情况,刻意使用在项目上!
- 直接复制从网上(比如 Stack Overflow )找到的代码,只要运行 OK 就好。
- 看到一些比较火的概念就魔怔了,比如前两年开始爆火的中台概念。
批判性地分析你读到和听到的东西
这是《程序员修炼之道》这本书中的一个建议,这里分享一下原文的描述:
批判性思维本身就是一门完整的学科,非常值得仔细研究和学习!
我最喜欢的咨询技巧是:至少问五次“为什么”。就是说,每当有了答案后,还要追问“为什么”。像个烦人的四岁小孩那样经常性重复提问,不过记得要比小朋友更有礼貌。这样做可以让你更接近本源。
提高系统设计能力
哪些考察系统设计能力的问题
不论是面试应届生还是高级开发,系统设计能力是大部分面试官会重点关注的对象。比如面试官可能会问题你:
- 如何设计一个 RPC 框架?消息队列?
- 如何设计一个秒杀系统?
- 如何设计一个排行榜?
- 如何设计一个视频网站?有哪些需要注意的地方?(比如如何解决大文件上传问题、如何保证视频的安全性)
- 如何设计微博 Feed 流?
- ……
这些问题都是非常能够考验你的工程能力的,相比于理论性的题目,这种问题的细节点较多,要更难准备一些。
如何提高系统设计能力
想要提供系统设计能力,需要我们的刻意训练。那到底该怎么训练呢?
简单说说我自己的看法,欢迎大家补充:
- 多对你做过的系统进行复盘总结,思考一下这个系统有哪些需要改进/完善的地方。
- 多进行系统设计实战,比如你可以多问问自己:“如果让你去设计 xx 系统,你该怎么做?”。你最好把这个系统设计的过程记录下来,以便后续再完善改进。
系统设计不一定非要我们实际写代码去实现,系统设计好了之后,写代码并不是什么难事。我这样说并不是代码实践不重要,只是每个人的精力都有限,你应该把你的精力用在最值得你投入时间的地方。
造轮子
何为造轮子
在编程领域,你可以把造轮子中的“轮子”简单地理解为各种框架、标准库或者软件。
造轮子说的就是我们对现有的各种框架、标准库或者软件进行改进或者重新创造一个类似的。就比如说已经有了现成的任务调度框架,你自己又创造了一个更满足自己需求的任务调度框架。
不知道何时起,“重复造轮子”被大家看作是一个很傻叉的行为。我却不是这么认为的!在我看来,不论是对于个人还是公司,亦或是技术本身来说,造轮子都有其重要的意义存在。
虽然,造轮子很有意义。但是,有一点不可否认的是:我们在实际项目开发中,会从成本、稳定性、成熟度等方面优先考虑使用比较可靠的开源项目。
另外,我们不是每个人能够写出一个被广泛使用的框架或者标准库。这个需要坚持,也需要我们长期积累的经验。我认识到的很多优秀开源库的作者,他们大部分都是工作中遇到一个问题,现有的开源库没办法很好地解决,最后自己经过很长时间才写出来的。比如安全框架 sureness 的作者,自己在使用 shiro 的时候,不太满意,就花了 2 年多写了这个框架。再比如 sa-token 这个项目的作者公司的项目需要用到踢人下线、账号封禁等功能,现有的权限认证框架没有现成的功能,于是他就自己写了这个框架。
那造轮子会为我们带来什么呢?
为什么要造轮子
从个人角度来说
第一,造轮子能够非常有效地提高自己的系统编程能力。
我之前在搞懂了 RPC 的原理之后,就自己动手写了一个简单的 RPC 框架。我的 RPC 框架肯定是无法和 Dubbo 这类已经这么成熟的相提并论。但是,在自己去写 RPC 框架的时候,更加加深了自己对于 RPC 框架的认识。实现的过程中,遇到了很多问题,解决问题的过程中也提高了自己的编程能力。
第二,造轮子可以提高自己的影响力。
那我自己来说,我写的建议一个建议的 RPC 框架 guide-rpc-framework 虽然功能很简陋,但是,凭借这详细的 README 介绍以及清晰的代码结构还是被很多热爱技术的小伙伴喜欢。
一年不到,这个项目的 star 数量就达到了 1.5k, 有 700 位小伙伴 fork 了这个项目。
第三,造轮子可以倒逼自己学习。
造轮子的过程中,我们往往需要做大量的功课,学习很多自己之前没有接触过的东西。
就比如我在写 guide-rpc-framework 之前,自己对于 Netty 的使用仅仅停留在发送和接收消息。在写 guide-rpc-framework 的过程中,我就学习了很多关于 Netty 更高级的使用比如粘包/半包处理、 心跳机制。
从项目/公司角度来说
第一,造轮子可以更好地适应项目需求
当项目业务比较复杂和庞大之后,很可能存在现有的轮子不满足我们的需求的情况。这个时候,我们就需要自己造一个更适合自己的轮子了。
第二,一个好的轮子的诞生可以提高公司的技术影响力
像现在国内的很多公司都在搞开源,甚至有的公司的部门还有开源项目的 KPI。
不可否认的是,我们程序员在找工作的时候很看重这个公司有没有比较好的开源项目的。
拿 Java 来说,为什么大家觉得阿里的技术很厉害。主要原因其实并不是因为阿里的业务场景的技术挑战有多大,而是阿里开源了很多还不错的框架比如 Dubbo、Spring Cloud Alibaba。
第三,造轮子可以让公司的技术得到沉淀。
公司可以把自己解决某一领域的问题通过造轮子的方式沉淀下来,这样的话,以后再遇到类似的问题就可以直接使用现成的轮子解决了。就比如很多公司内部都有一套适合自己公司的框架,使用这套框架可以帮助开发者节省很多开发时间。
程序员如何快速上手一个新项目?
今天的文章标题就是我平时被问过的一个高频问题。
确实,很多小伙伴在学习或者接手一个项目的时候,不知道如何快速了解项目。
今天这篇文章我就简单聊聊“如何快速上手一个新项目?面试被问项目经历有哪些小技巧?”。
下面是正文!
项目学习五步走
一般项目都会有遗留文档,不论是传统的项目开发模式还是敏捷开发模式。上项目之前自己抽时间看一下相关文档,大概了解一下这个项目整体的情况比如基本的业务还有技术选型啊这些。
如果项目是单机的话,大部分就是增删改查的逻辑,主要是对于业务的理解。
如果项目是分布式或者微服务的话,会涉及各个服务之间的调用以及一些其他问题比如限流、分布式锁、分布式 ID 这些,稍微会复杂一些。
不过不论是什么类型的项目,上手的姿势大概是下面几步。
第一步:了解业务
先搞清你接受的新项目:
- 是做什么的? 主要面向什么人群使用?
- 主要提供了哪些功能?
- 项目背景是什么样的?
- 项目涉及的关键业务流程是怎么样的?
- 项目目前面临的挑战是什么?未来规划是什么?
- ……
技术本身就是为了业务而服务,只有首先搞清楚了业务之后你才真正算是步入了这个项目的大门。
第二步:搭建项目开发环境
是骡子是马总要拉出来溜溜。所以,第二步我推荐你简单把开发环境搭建一下。搭建的步骤一般都在项目的 README 文档里面。
搭建完成之后,需要确保项目能够在自己的电脑上正确运行。
第三步:看项目技术架构
这个直接看项目的相关依赖就好。拿 Java 后端项目举例子,如果是 Maven 项目的话看 pom.xml,如果是 Gradle 项目就看 build.gradle。
可能会涉及下面这几部分,但是并不完全。
- 项目最底层框架是什么?是 Spring 还是 Spring Boot,又或者是其他框架呢?
- 项目依赖了哪些相关的包?挑重点看,比如数据库是 MyBatis 还是 JPA 或者是公司自研的框架呢?
- 项目使用的什么数据库?是 MySQL 还是 PostgreSQL,又或者是其他数据库呢?
- 项目用到了缓存吗?是 Redis 缓存吗?有没有用到本地缓存呢?
- 项目用到了消息队列吗? Kafka 还是 RocketMQ?
- 项目的权限管理这块是怎么做的呢?
- …….
第四步:看项目的代码结构
项目的代码结构是怎么划分的,比如常见的项目可能会分为下面三层(复杂的系统分层可能会更多)。
- Repository(数据库操作)
- Service(业务操作)
- Controller(数据交互)
如果是 DDD 分层架构的话,可能是下面这样的:
- User Interface(用户界面层)
- Application(应用层)
- Domain(模型层)
- Infrastructure(基础实施层)
不同的公司对于项目的结构的划分可能也不同,不过大体都是类似的。比如《阿里巴巴 Java 开发手册》中所推荐的项目代码结构是下面这样的。
第五步:从功能主线/问题出发研究项目源码
一个比较成熟的项目的源码量是非常多,我们不可能都完完整整地看完,也没有必要。
你可以通过 debug 调试,研究项目核心代码逻辑。比较推荐的方式就是通过一个功能主线(比如 Dubbo 是如何暴露服务的?)或者问题(比如 ?)出发。
对于企业项目来说,大部分还是知道如何进行 debug 调试的。但是,对于 Spring 这种顶级开源框架来说,很多人就不知道怎么打断点了。
我比较推荐的是你可以先把源码拷贝到本地,然后运行源码中提供的 Demo。对于你想研究的问题比如 Spring 的 IoC 源码,你先去找找对应的 API 调用方式的 Demo,然后根据 Demo 中的方法调用来研究整个过程。如果你觉得这种方法比较难的话,你也可以先去网上看看别人的分析。
项目经历的四个小技巧
面试中,对于项目经历的考察是重中之重。下面我就分享 4 个面试被问项目经历的小技巧:
1.提前搞清楚项目的架构图、技术选型等等。
比如下面这个就是我之前写的一个简易 RPC 框架(guide-rpc-framework)的架构图。
再比如下面这个是一个微服务的电商网站的架构图。
2.提前想好项目的亮点,针对项目涉及的关键技术进行深度复习。
比如说,你的项目用了消息队列的话,你就很有必要提前想好怎么回答消息队列相关的一些问题:消息队列解决了什么问题、常见消息队列对比、如何保证消息只被消费一次、如何保证消息不被重复消费……。
3.引导面试官问你熟悉的技术。
比如说,你对消息队列比较了解的话,介绍项目的时候就可以多介绍一下自己通过消息队列解决了什么问题。
4.突出个人的贡献比如自己在项目中解决了什么问题,而不只是叙述自己做了什么。
程序员如何有效地提高工作效率?
对于下面的每一点建议的理解,每个人可能都不一样。
如果你觉得某一点对你有用的话,不要关了这篇文章之后你就忘记了,建议你一定要记录下来。从当下开始就去努力践行。
本文概览:
- 根据事情的重要程度安排优先级
- 会安排自己任务,学会制定计划
- 工作之外有点自己感兴趣的东西
- 学会使用工具提升工作效率
- 学会休息
- 如何保证精力充沛
根据事情的重要程度安排优先级
说实话,在这一点上,我自己刚工作那会做的并不好,也经常因为没有处理好事情的优先级被 diss。
不知道大家会不会有时候在一个不那么重要的事情上,耽搁很久,虽然这件事情不是很重要,自己也知道要先去做最重要的事情,但就是想把当前的事情做完为止。
如何安排工作上的事情的优先级?
给几点建议大家参考一下,总体原则还是重要的事情优先。
- 客户、线上、安全问题最优先
- 对于后续开发依赖比较大的业务优先
- 工作量小,流程比较长的优先比如账户认证,资源申请等等
学会安排自己任务,学会制定计划
工作之后,你会发现自己的时间少了太多太多太多。大部分时间都会感觉每天忙忙碌碌,后头看,却不知道自己究竟做了啥!
前几天自己刚想学习的某个技术、刚想看某本书,忙着忙着却又忘记了。所以,你需要学会合理安排自己的任务。
我个人比较推荐 Trello 作为个人任务管理工具。
据我所知国内外很多项目都是用 Trello 来做项目管理的。
我平时使用 Trello 记录一些自己想写的文章或者代码,以及一些读者的投稿情况和个人突然冒出来的想法。
下图是我平时用 Trello 记录自己要写的文章或者代码的效果。我还会按照优先级来排列每一个任务和想法。
然后,平时的一些小任务我是通过 Microsoft To Do来记的(Windows、Mac、Android)。
我之前使用的是滴答清单,但是,后来发现 Microsoft To Do 用着更舒服点,界面也更加符合我的审美。
最后,再来安利一下番茄工作法! 番茄工作法是我一直在用,并且也经常安排给身边朋友的一个时间管理方法,简单易操作,并且效果极好。
注意:番茄工作法不一定适合每个人,并且,有的公司根本没条件让你用番茄工作法。
维基百科是这样介绍番茄工作法的:
番茄工作法原理:每次专注一段时间(一般是 25 分钟)结束搭配一次休息(一般是 5 分钟),多次专注(一般为 4 次)结束搭配一次长休息(一般为 15 分钟)。劳逸结合,有助于提高工作效率。
我每天会根据事情的重要程度以及难易程度给我当天要做的所有事情排一个优先级,然后按照番茄工作法一个一个地去完成。每一次专注的 25 分钟时间内,我都会保证自己只做这一件事情。空余的 5 分钟休息时间,我一般会简单看看邮件、做做眼保健操或者起来站一会放松一下。
我的番茄任务管理工具是在 Apple Store 上 花钱购买的 Be Foucused 的 Pro 版。
不是 Mac 电脑的也没关系,再给小伙伴们再推荐一个多平台(ios、andriod、mac、win)都可以使用的任务管理工具:番茄土豆 。
工作之外有点自己感兴趣的东西
工作之外要有自己的生活,这样的日子才不会太单调,比如我工作之外喜欢打打游戏放松一下,周末的时候喜欢自己烹饪做好吃的东西给自己。有人可能觉得这个比较浪费时间,不过,在我而言这也是对自己的一种放松,或许在某种程度还能帮助我们提升效率。
学会使用工具提升工作效率
就比如我上面推荐的几款效率工具,就我个人而言真的是方便了我太多,在工具效率上给我带来了很大的帮助。
再拿我们平时编程来说,选好编程工具也真的太重要了,比如在我看来 Java 最好的 IDE 当属 IDEA 了,随随便便提升 30%以上的开发效率不是吹的。
另外,我平时也会经常给大家推荐一些不错的工具比如:浏览 Github 必备的 5 款神器级别的 Chrome 插件,IDEA 插件 ,这些工具/插件它不香么?
学会休息
别打时间战,少熬夜,休息好了,工作效率才高。熬夜的危害就不用多说了,秃头加内分泌失调,你懂得!
拿我个人来说,我平时如果 12 点前睡的话,白天就是 7 点起来,如果 12 点后睡的话,一般都是 8 点左右起来。没睡好的话,一天真的效率会降低很多。
看电脑 45 分钟之后,起来走 5 分钟,看看远方放松一下。不要觉得这 5 分钟浪费时间,相反,这 5 分钟可能为你带来更大的效率提升。
电脑架子不贵,但是很有用,保护好自己脊椎的同时,办公体验也会提升很多。
如何保证精力充沛
除了上面提到的 学会休息 之外,还有哪些能够让我们的经历更充沛的好习惯呢?
正所谓 :选择大于努力,效率大于堆时间。
只有我们从下层打好基础,才能稳步上升,最后登顶。
- 不要吃太多碳水,容易瞌睡。
- 一定要吃早餐。
- 咀嚼,具有促进头脑清醒的作用。吃饭的时候,不要太急,多咀嚼一下。
- 尽量午睡,控制在半小时左右。
- 运动!拒绝久坐!
- 早上起来太困的话,洗个澡!
- 锻炼!
- 保持积极的心态,减少消极情绪。
相关阅读:
- 如何拥有旺盛精力? - 知乎 https://www.zhihu.com/question/21671881
- 如何保持精力充沛,有效适应困、倦、疲、乏等周期型生理状况? - 知乎 https://www.zhihu.com/question/21097892
- 低碳水食物清单 :https://lowcarbfasthealth.com/low-carb-food-visual-guides/
如何更高效地自学编程?
我的学校是荆州一所双非一本。整个大一,我都没有怎么认真学习编程,每天就是出去玩,还有参加各种社团活动。
在大二上学期末,最终确定了自己以后要走的技术方向是走 Java 后端。于是,我就开始制定学习计划,开始了自己的 Java 后端领域的打怪升级之路。
到了大三,我基本把 Java 后端领域一些必备的技术都给过了一遍,还用自己学的东西做了两个实战项目。
这篇文章就从下面几个切入点来简单聊聊“如何更高效地自学?”:
- 有哪些学习的途径
- 如何获取技术最新动向?
- 自学过程中有哪些需要注意的地方?
有哪些学习的途径?
一般来说,有了一个具体的学习路线,知道学习什么之后,我们通常有下面几个方向来学习:
视频
初学编程的小伙伴尽量多看视频,因为,视频教程比较容易理解。不过,对于经验已经比较丰富的小伙伴来说,视频教程相比于文档教程学习起来会更慢一些。
像慕课网和哔哩哔哩上面有挺多学习视频可以看,只直接在上面搜索关键词(比如 Java、MySQL)就可以了。
提个醒哈!在哔哩哔哩上学习的时候,不要学一会就跑到别的分区去了,有点顶不住啊!
书籍
书籍的内容更成体系,更系统。任何时候,书籍都是我们最重要的学习途径!!!
不过,目前绝大部分高质量的技术书籍还是国外出版的,等到翻译成中文的时候可能已经过了几年了。因此,提高英文阅读能力是每个想要成为优秀工程师的程序员必须要做的。
不过,书籍存在时效问题,你可以通过一些手段来获取技术的最新动向(后面会详细介绍到)。
博客
网上的博客大多没有体系,推荐你在解决某一知识点或者问题的时候可以在网上找一些相关的博客看。就比如我在学习消息队列 Pulsar 的时候,先把Pulsar 官方文档看了一遍。然后,自己在网上找了一些相关的文章来深入学习。
- 《Kafka vs. Pulsar vs. RabbitMQ: Performance, Architecture, and Features Compared》
- 《为什么放弃 Kafka,选择 Pulsar?》
- 《7 Reasons We Chose Apache Pulsar over Apache Kafka》
- 《比拼 Kafka, 大数据分析新秀 Pulsar 到底好在哪》
- 《从 Kafka 到 Pulsar,BIGO 打造实时消息系统之路》
- 《Apache Pulsar 在 BIGO 的性能调优实战(上)》、《Apache Pulsar 在 BIGO 的性能调优实战(下)》
- 《Apache Pulsar 在能源互联网领域的落地实践》
- ……
Java 领域比较成体系的博客,推荐 JavaGuide 。
官网
官方文档我们一定是要看的。 除非是一些国产项目的官方文档提供了中文版本,否则大概率是英文的。并且,官方文档介绍的往往也比较粗糙,不太适合初学者作为学习资料。当然了,如果你经验比较丰富的话,直接看官方文档也是没问题的。
通过官方文档你才能知道你学习的技术最新的技术动态,才能知道这个技术有哪些模块需要学习,才能知道这个技术具体可以帮你解决什么问题。
比如下面是 Spring 的官网,通过网站首页你就可以大概知道 Spring 可以帮助你:
- 快速开发网站
- 开发微服务架构的软件
- 开发响应式架构的软件
- ……
如何获取技术最新动向?
Github Trending
Github Trending 我几乎每天必看,通过 Github Trending 我可以大概知道最近有哪些项目比较火,有哪些框架比较热门,有哪些新的中间件被开源了。
并且,Github 的 Trending 可以按照语言和日期来进行筛选,你可以根据自己的需要来选择查看对应的信息。
公开的技术分享
你可以留意一些公开的技术分享比如 InfoQ 技术大会、思否技术活动汇总 、。
通过这些公开的技术分享,你可以了解到当下热门的创新技术、实践案例、产品思维和管理心得。
技术大佬
技术无国界,国内外都有很多优秀的工程师。多关注一下他们在干什么,在研究什么技术,或许能给你很大的启发和动力。
国内比较值得关注的技术大佬有:
- Liang Zhang:Apache ShardingSphere,ElasticJob 创始人 & 项目管理委员会主席。
- xiaoyu : 作为主要作者开源了 soul(网关)、hmily(分布式事务框架)等等顶级开源项目,并且参与了apache/shardingsphere等开源项目。
- immking :Apache Dubbo/ShardingSphere PMC。前某集团高级技术总监/阿里架构师/某商业银行北京研发中心负责人,阿里云 MVP、腾讯 TVP、TGO 鲲鹏会会员。
- Juan Pan :京东数科高级 DBA&Apache ShardingSphere PMC,主要负责京东数科分布式数据库开发、数据库运维自动化平台开发等工作。
- Jintao Zhang :《Kubernetes 从上手到实践》 《Docker 核心知识必知必会》 作者、API7.AI任技术专家,负责 Apache APISIX Ingress 和 Service Mesh 等云原生技术方向。
- …..
技术社区
技术社区也是一个了解技术动向的好办法,国内外有很多优质的社区比如 Reddit 上的 Java 社区,InfoQ 中文社区 (近几年质量有所下降)、Medium 上的技术社区。
技术博客
关注或者订阅一些干货比较多的技术博客,不光能够获取到技术最新动向,还可以让自己深入学习很多知识点。
如果你不知道国内有哪些值得推荐的技术博客的话,可以看看这篇文章:国内有哪些顶级技术团队的博客值得推荐? - JavaGuide - 知乎 。
自学过程中有哪些需要注意的地方?
英语阅读能力
大部分优秀的技术书籍都是国外的,几乎都是英文,并且,大部分技术的官方文档也都是英文的。
所以,提高自己的英文阅读能力很重要。英文阅读能力暂时比较差的也不要紧,有道翻译和谷歌翻译就是你最好的老师。如果是使用 Chrome 浏览器的话,我还推荐你安装一个 Mate Translate 插件。 这个插件对于网页阅读英文文档太友好了,可以一站式翻译您的网页以及标记的文字段落。
多练!多记!多实践!多实战!
不论是看视频还是看书,最好都要跟着一起练!
学习编程,不动手实践那都是扯淡。你是不是经常听别人讲的时候感觉自己似乎懂了,好像也并不难,结果,自己写的时候就不会了,过了没几天自己就忘了。
比如说我们学习 Spring Boot 整合其他常见框架的时候,你不光要看对应的 Spring Boot 教程,一定还要动手去实践,去写一些 Spring Boot 的小 Demo。动手实践的过程中,你会发现有很多被自己忽略的细节,遇到一些需要解决的问题。解决问题的过程中,同样也是学习的过程。
再比如说我们学习 Tomcat 原理的时候,我们发现 Tomcat 的自定义线程池挺有意思,那我们自己也可以手写一个定制版的线程池。再比如我们学习 Dubbo 原理的时候,可以自己动手造一个简易版的 RPC 框架。
学习过程中没弄懂的知识点一定要尽快解决。如何解决?首选百度/Google,通过搜索引擎解决不了的话就找身边的朋友或者网上认识的一些人。
另外,一定要进行项目实战!很多人这时候就会问没有实际项目让我做怎么办?我觉得可以通过下面这几种方式:
- 实战项目视频/专栏 : 在网上找一个符合自己能力与找工作需求的实战项目视频或者专栏,跟着老师一起做。跟着老师做的过程中,你一定要有自己的思考,不要浅尝辄止。对于很多知识点,别人的讲解可能只是满足项目就够了,你自己想多点知识的话,对于重要的知识点就要自己学会去深入学习。
- 实战类开源项目 : Github 或者码云上面有很多实战类别项目,你可以选择一个来研究,为了让自己对这个项目更加理解,在理解原有代码的基础上,你可以对原有项目进行改进或者增加功能。Java 类的实战项目,你可以从 awesome-java 这个仓库里面找,里面有很多非常赞的项目。
- 从头开始做 :自己动手去做一个自己想完成的东西,遇到不会的东西就临时去学,现学现卖。这个要求比较高,我建议你已经有了一个项目经验之后,再采用这个方法。如果你没有做过项目的话,还是老老实实采用上面两个方法比较好。
- ……
做项目不光要做,还要改进,改善。另外,如果你的老师有相关 Java 后台项目的话,你也可以主动申请参与进来。
一定要学会分配自己时间,要学的东西很多,真的很多,搞清楚哪些东西是重点,哪些东西仅仅了解就够了。一定不要把精力都花在了学各种框架上,算法和数据结构真的很重要!
不要把学习编程还当做学生时代的应试考试来看
你或许也发现了。很多成绩特别特别优异的同学,他们的编程能力其实并不好。在大学的时候,那些编程能力最强的往往是那些成绩比较一般的。
为什么会这样呢?
我觉得主要是一个思维的转变问题。很多人学习编程的时候,总是想着我要把这个 API 记下来,把这个库的用法记下来。这样学习,导致的结果只有一个那就是你会很难受!因为,这些根本不是要死记硬背的东西啊!真还当这是上课考试啊!你要从如何用你学的东西来解决实际编程问题出发,站在做一个实际的项目的角度来学习。
拿我自己来说:我平时也会写 Python,基本就是自己看着官方文档或者一些书籍的教的语法跟着写。如果哪个地方不会了,我就去查一下。
多看优秀的代码
不看优秀的代码,你写的代码质量很难提高。
虽然要多看优秀的代码,但是也不要被 “最佳实践” 所束缚,很多时候实际是根本不存在适用于任何场景的“最佳实践”,没有银弹。
有哪些优秀的代码值得学习呢?
拿 Java 来说,不知道阅读什么源码的话,可以先从 JDK 的几个常用集合看起。另外,我比较推荐看 Dubbo 的,因为感觉会稍微相对容易一点,模块划分清晰,注释也比较详细。搞清楚了 RPC 的基本的原理,知道如何自己实现一个 RPC 框架之后,看起来就没那么吃力了。
另外,随便一个框架的源码都 10w+行了,都看一遍是不可能的。要挑选比较重要的地方看,就比如看 Spring 源码的话你一定要看 IoC 和 AOP,要知道一个 Spring Bean 是如何一步一步被创建出来的。你要看 Spring Boot 源码的话就要知道 Spring Boot 的启动机制是啥,Spring Boot 是如何实现自动配置的。
不要死记硬背
学习理论知识的时候,我们可以多花点时间整理笔记。
但是,在学习框架使用的时候,就没有太大必要花大量时间的整理做笔记了。
你完全可以随时查文档,记住关键词即可!比如 Spring Boot 你不知道如何接受 Query Param 的话,你直接搜 Spring Boot Query Param 即可!
再比如你不会使用 Redis ,你做的 Spring Boot 项目需要用到的话,你直接搜“Spring Boot+ Redis”就出来了各种详细的教程。
你要做的就是把常用的东西串联起来,知道有这个东西就好。根据自己的实际能力,再对底层的东西进行学习就好。
程序员如何快速学习新技术?
很多时候,我们因为工作原因需要快速学习某项技术,进而在项目中应用。或者说,我们想要去面试的公司要求的某项技术我们之前没有接触过,为了应对面试需要,我们需要快速掌握这项技术。
作为一个人纯自学出生的程序员,这篇文章简单聊聊自己对于如何快速学习某项技术的看法。
文章内容仅代表个人观点,如果你有更好的学习方法,还请在评论区多多和我交流。希望我们都能有所收货!
学习任何一门技术的时候,一定要先搞清楚这个技术是为了解决什么问题的。深入学习这个技术的之前,一定先从全局的角度来了解这个技术,思考一下它是由哪些模块构成的,提供了哪些功能,和同类的技术想必它有什么优势。
比如说我们在学习 Spring 的时候,通过 Spring 官方文档你就可以知道 Spring 最新的技术动态,Spring 包含哪些模块 以及 Spring 可以帮你解决什么问题。
再比如说我在学习消息队列的时候,我会先去了解这个消息队列一般在系统中有什么作用,帮助我们解决了什么问题。消息队列的种类很多,具体学习研究某个消息队列的时候,我会将其和自己已经学习过的消息队列作比较。像我自己在学习 RocketMQ 的时候,就会先将其和自己曾经学习过的第 1 个消息队列 ActiveMQ 进行比较,思考 RocketMQ 相对于 ActiveMQ 有了哪些提升,解决了 ActiveMQ 的哪些痛点,两者有哪些相似的地方,又有哪些不同的地方。
学习一个技术最有效最快的办法就是将这个技术和自己之前学到的技术建立连接,形成一个网络。
然后,我建议你先去看看官方文档的教程,运行一下相关的 Demo ,做一些小项目。
不过,官方文档通常是英文的,通常只有国产项目以及少部分国外的项目提供了中文文档。并且,官方文档介绍的往往也比较粗糙,不太适合初学者作为学习资料。
如果你看不太懂官网的文档,你也可以搜索相关的关键词找一些高质量的博客或者视频来看。 一定不要一上来就想着要搞懂这个技术的原理。
就比如说我们在学习 Spring 框架的时候,我建议你在搞懂 Spring 框架所解决的问题之后,不是直接去开始研究 Spring 框架的原理或者源码,而是先实际去体验一下 Spring 框架提供的核心功能 IoC(Inverse of Control:控制反转) 和 AOP(Aspect-Oriented Programming:面向切面编程),使用 Spring 框架写一些 Demo,甚至是使用 Spring 框架做一些小项目。
一言以蔽之, 在研究这个技术的原理之前,先要搞懂这个技术是怎么使用的。
这样的循序渐进的学习过程,可以逐渐帮你建立学习的快感,获得即时的成就感,避免直接研究原理性的知识而被劝退。
研究某个技术原理的时候,为了避免内容过于抽象,我们同样可以动手实践。
比如说我们学习 Tomcat 原理的时候,我们发现 Tomcat 的自定义线程池挺有意思,那我们自己也可以手写一个定制版的线程池。再比如我们学习Dubbo 原理的时候,可以自己动手造一个简易版的 RPC 框架。
另外,学习项目中需要用到的技术和面试中需要用到的技术其实还是有一些差别的。
如果你学习某一项技术是为了在实际项目中使用的话,那你的侧重点就是学习这项技术的使用以及最佳实践,了解这项技术在使用过程中可能会遇到的问题。你的最终目标就是这项技术为项目带来了实际的效果,并且,这个效果是正面的。
如果你学习某一项技术仅仅是为了面试的话,那你的侧重点就应该放在这项技术在面试中最常见的一些问题上,也就是我们常说的八股文。
很多人一提到八股文,就是一脸不屑。在我看来,如果你不是死记硬背八股文,而是去所思考这些面试题的本质。那你在准备八股文的过程中,同样也能让你加深对这项技术的了解。
最后,最重要同时也是最难的还是 知行合一!知行合一!知行合一! 不论是编程还是其他领域,最重要不是你知道的有多少,而是要尽量做到知行合一。
如何提高个人编程硬实力?
我们平时要拿大厂的要求来鞭策自己,尽量避免一直待在自己的舒适区。
那大厂想要什么样的人才呢?
先从已经有两年左右开发经验的工程师角度来看: 我们来看一下阿里官网支付宝 Java 高级开发工程师的招聘要求,从下面的招聘信息可以看出,除去 Java 基础/集合/多线程这些,这些能力格外重要:
- 底层知识比如 jvm :不只是懂理论更会实操;
- 面向对象编程能力 :我理解这个不仅包括“面向对象编程”,还有 SOLID 软件设计原则,相关阅读:《写了这么多年代码,你真的了解 SOLID 吗?》(我司大佬的一篇文章)
- 框架能力 :不只是使用那么简单,更要搞懂原理和机制!搞懂原理和机制的基础是要学会看源码。
- 分布式系统开发能力 :缓存、消息队列等等都要掌握,关键是还要能使用这些技术解决实际问题而不是纸上谈兵。
- 不错的 sense :喜欢和尝试新技术、追求编写优雅的代码等等。
再从应届生的角度来看: 我们还是看阿里巴巴的官网相关应届生 Java 工程师招聘岗位的相关要求。
结合阿里、腾讯等大厂招聘官网对于 Java 后端方向/后端方向的应届实习生的要求下面几点也提升你的个人竞争力:
- 参加过竞赛( 含金量超高的是 ACM );
- 对数据结构与算法非常熟练;
- 参与过实际项目(比如学校网站)
- 熟悉 Python、Shell、Perl 其中一门脚本语言;
- 熟悉如何优化 Java 代码、有写出质量更高的代码的意识;
- 熟悉 SOA 分布式相关的知识尤其是理论知识;
- 熟悉自己所用框架的底层知识比如 Spring;
- 有高并发开发经验;
- 有大数据开发经验等等。
从来到大学之后,我的好多阅历非常深的老师经常就会告诫我们:“ 一定要有一门自己的特长,不管是技术还好还是其他能力 ” 。我觉得这句话真的非常有道理!
刚刚也提到了要有一门特长,所以在这里再强调一点:公司不需要你什么都会,但是在某一方面你一定要有过于常人的优点。换言之就是我们不需要去掌握每一门技术(你也没精力去掌握这么多技术),而是需要去深入研究某一门技术,对于其他技术我们可以简单了解一下。
我觉得一个好的 Java 程序员应该具备下面这些素质:
- Java 基础 :掌握 Java 基础知识(可以看《Java 核心技术卷 1》或者《Head First Java》这两本书在我看来都是入门 Java 的很不错的书籍),当然你也可以边看视频边看书学习(推荐黑马或者尚硅谷的视频)。一定要记得多总结!打好基础!把自己重要的东西都记录下来。
- 多线程 :掌握多线程的简单实用(推荐《Java 并发编程之美》或者《实战 Java 高并发程序设计》)。
- JVM(可选) :如果想去大厂,JVM 的一些知识也是必学的(Java 内存区域、虚拟机垃圾算法、虚拟垃圾收集器、JVM 内存管理)推荐《深入理解 Java 虚拟机》。
- 算法和数据结构:如果你想进入大厂的话,我推荐你在学习完 Java 基础或者多线程之后,就开始每天抽出一点时间来学习算法和数据结构。为了提高自己的编程能力,你也可以坚持刷 Leetcode。
- 前端知识 :学习前端基础(HTML、CSS、JavaScript),当然 BootStrap、VUE 等等前端框架你也可以了解一下。
- Git : 版本控制工具 Git 绝对比必须的。你可以自己去 Github 上下载一些项目看,然后自己也上传一个项目到 Github 上去。
- MySQL : 学习 MySQL 的基本使用,基本的增删改查,索引需要重点关注,存储过程可以简单了解一下。
- Maven : 建议学习各种框架之前可以提前花半天时间学习一下 Maven 的使用。(到处找 Jar 包,下载 Jar 包是真的麻烦费事,使用 Maven 可以为你省很多事情)
- 框架 :学习 Spring、SpringMVC、Hibernate、Mybatis 等框架的使用,(可选)熟悉 Spring 原理(大厂面试必备),然后很有必要学习一下 SpringBoot。我也遇到很多公司对于应届生直接上手 SpringBoot,不过我还是推荐你有时间还是可以把 Spring、SpringMVC 好好学一下。不过 SpringBoot 优先级最高!
- Linux :学习 Linux 的基本使用(常见命令、基本概念)
- 分布式 :RPC、服务注册于发现、API 网关、配置中心、分布式 ID、分布式事务……。
- 高并发 : 消息队列、读写分离&分库分表、负载均衡、缓存……这些。
- 高可用 : 主要就是限流&降级&熔断、集群……这些。
- 微服务 :微服务的一些基本概念、SpringCloud 和 Spring Cloud Alibaba 那一套都可以学习一下。我比较推荐的是学习 Spring Cloud Alibaba,因为首先它是阿里开源的,文档比较丰富,另外,它比较新,各种组件都可以说很不错。
- 进阶 :操作系统底层知识、计算机组成原理、Java 编码优秀实践、SQL 调优、定位解决线上问题的能力等等
知道要学什么之后,如何去学呢?
我觉得学习每个知识点可以考虑这样去入手:
- 官网(大概率是英文,不推荐初学者看)
- 书籍(知识更加系统完全,推荐)
- 视频(比较容易理解,比较推荐,特别是初学的时候),另外,大家不要说自己工作很多年,技术也比较厉害了就不能看视频学习了。我认识的很多大佬,包括我经历的几个项目组的技术 Leader,他们都有看视频学习技术的习惯。
- 网上博客(解决某一知识点的问题的时候可以看看)。
最后,有一个建议是:看视频的过程中最好跟着一起练,要做笔记!!!最好可以边看视频边找一本书籍看,看视频没弄懂的知识点一定要尽快解决,如何解决?首先百度/Google,通过搜索引擎解决不了的话就找身边的朋友或者认识的一些人。
六、工作篇
如何选择职业方向?
发现身边有一些朋友看到哪一个方向工资高就转哪个方向,看到大数据库工资高,他们就立马转大数据方向,看到算法工程师工资高,他们就转算法方向……。
实际这种行为是非常不可取的,不利于个人发展,尤其是对于已经在某个领域工作了多年的朋友来说。
如果你在某个领域比如 Java 后端有了几年的工作经验,那你再换其他方向几乎相当于是从头开始。虽然你可能或多或少也能用到部分 Java 后端的工作经验,但这并不能让你在新方向上有太大的竞争力,你几乎就是这个领域的新人。
另外,如果你仅仅是因为对某个方向感兴趣就想要转变职业方向的话,那我建议你先在这个方向深入学习一下。就像很多后端的朋友想要转前端一样,在他们眼里前端的工作所见即所得,可以很快就能看到自己的成果,入门也要相对容易一些。但是,他们并不清楚想要做好前端真的很难,各种新框架,各种新工具,适配各种尺寸的屏幕是真的有难度且麻烦。
学技术也一样,不要今天看别人学了某某框架,掌握了某某技术原理,你也要去学。按照自己的节奏来就好,没必要和其他人比,每个人的情况不同,擅长的领域也不同。
过于从众,没有主见,只能让自己在技术这个道路上走的很累。
不过,有一些情况下,换一个职业方向对你来说还是值得考虑的:
- 如果你本身就刚工作不久的话,那我觉得你换一个职业方向对你影响也不大,毕竟你本身就没有什么工作经验。
- 如果你所从事的方向真的是夕阳产业,已经进入了长期下行的趋势(比如曾经很火的 Flash 开发方向),那你要尽快考虑换一个职业方向了。
- 如果你真的对新的职业方向有信心,感兴趣,清楚自己一定会在新方向坚持下来。这种情况下,我相信你是可以在新的职业方向上成功的
转换职业方向也要乘早,尽快跳出舒适区,在下一个有前景的方向上努力。
如何判断一个职业方向好不好?你可以从下面 2 个方向来判断:
- 天花板高度:你这个职业方向最厉害的那批人能够到达的高度。
- 前景:夕阳行业还是未来趋势,你可以结合国家产业发展、供需关系、社会需要等角度来分析。
- 竞争程度:如果一个行业竞争人数太多的话,也会造成内卷的问题,进而导致这个行业的性价比降低。
通常来说,一个好的行业必然会竞争加剧。但是!如果你所从事的行业护城河够高(对求职者的硬性要求比较高)或者你的能力足够强的话,那这个行业注定不会太卷。就比如说顶级软件工程师、数据库内核资深开发、资深芯片设计师等等在任何时候在职场求职都是非常有竞争力的存在。
一个好的职业方向,一定是职业天花板够高,前景不错且竞争不太剧烈。
新入职一家公司如何快速进入工作状态?
朋友投稿的一篇文章!强烈建议每一位即将入职/在职的小伙伴看看这篇文章,看完之后可以帮助你少踩很多坑。整篇文章逻辑清晰,内容全面!
原文:https://www.cnblogs.com/hunternet/p/14675348.html
每到一个新的公司面临的可能都是新的业务、新的技术、新的团队……这些可能会打破你原来工作思维、编码习惯、合作方式……
而于公司而言,又不能给你几个月的时间去慢慢的熟悉。这个时候,如何快速进入工作状态,尽快发挥自己的价值是非常重要的。
有些人可能会很幸运,入职的公司会有完善的流程与机制,通过一带一、各种培训等方式可以在短时间内快速的让新人进入工作状态。有些人可能就没有那么幸运了,就比如我在几年前跳槽进入某厂的时候,当时还没有像我们现在这么完善的带新人融入的机制,又赶上团队最忙的一段时间,刚一入职的当天下午就让给了我几个线上问题去排查,也没有任何的文档和培训。遇到情况,很多人可能会因为难以快速适应,最终承受不起压力而萌生退意。
那么,我们应该如何去快速的让自己进入工作状态,适应新的工作节奏呢?
新的工作面对着一堆的代码仓库,很多人常常感觉无从下手。但回顾一下自己过往的工作与项目的经验,我们可以发现它们有着异曲同工之处。当开始一个新的项目,一般会经历几个步骤:需求->设计->开发->测试->发布,就这么循环往复,我们完成了一个又一个的项目。
而在这个过程中主要有四个方面的知识那就是业务、技术、项目与团队贯穿始终。新入职一家公司,我们第一阶段的目标就是要具备能够跟着团队做项目的能力,因此我们所应尽快掌握的知识点也要从这四个方面入手。
业务
很多人可能会认为作为一个技术人,最应该了解的不应该是技术吗?于是他们在进入一家公司后,就迫不及待的研究起来了一些技术文档,系统架构,甚至抱起来源代码就开始“啃”,如果你也是这么做的,那就大错特错了!在几乎所有的公司里,技术都是作为一个工具存在的,虽然它很重要,但是它也是为了承载业务所存在的,技术解决了如何做的问题,而业务却告诉我们,做什么,为什么做。一旦脱离了业务,那么技术的存在将毫无意义。
想要了解业务,有两个非常重要的方式
一是靠问
如果你加入的团队,有着完善的业务培训机制,详尽的需求文档,也许你不需要过多的询问就可以了解业务,但这只是理想中的情况,大多数公司是没有这个条件的。因此我们只能靠问。
这里不得不提的是,作为一个新人一定要有一定的脸皮厚度,不懂就要问。我见过很多新人会因为内向、腼腆,遇到疑问总是不好意思去问,这导致他们很长一段时间都难以融入团队、承担更重要的责任。不怕要怕挨训、怕被怼,而且我相信绝对多数的程序员还是很好沟通的!
二是靠测试
我认为测试绝对是一个人快速了解团队业务的方式。通过测试我们可以走一走自己团队所负责项目的整体流程,如果遇到自己走不下去或想不通的地方及时去问,在这个过程中我们自然而然的就可以快速的了解到核心的业务流程。
在了解业务的过程中,我们应该注意的是不要让自己过多的去追求细节,我们的目的是先能够整体了解业务流程,我们面向哪些用户,提供了哪些服务……
技术
在我们初步了解完业务之后,就该到技术了,也许你已经按捺不住翻开源代码的准备了,但还是要先提醒你一句先不要着急。
这个时候我们应该先按照自己了解到的业务,结合自己过往的工作经验去思考一下如果是自己去实现这个系统,应该如何去做?这一步很重要,它可以在后面我们具体去了解系统的技术实现的时候去对比一下与自己的实现思路有哪些差异,为什么会有这些差异,哪些更好,哪些不好,对于不好我们可以提出自己的意见,对于更好的我们可以吸收学习为己用!
接下来,我们就是要了解技术了,但也不是一上来就去翻源代码。 应该按照从宏观到细节,由外而内逐步地对系统进行分析。
首先,我们应该简单的了解一下 自己团队/项目的所用到的技术栈 ,Java 还是.NET、亦或是多种语言并存,项目是前后端分离还是服务端全包,使用的数据库是 MySQL 还是 PostgreSQL……,这样我们可能会对所用到的技术和框架,以及自己所负责的内容有一定的预期,这一点有的人可能在面试的时候就会简单了解过。
下一步,我们应该了解的是 系统的宏观业务架构 。自己的团队主要负责哪些系统,每个系统又主要包含哪些模块,又与哪些外部系统进行交互……对于这些,最好可以通过流程图或者思维导图等方式整理出来。
然后,我们要做的是看一下 自己的团队提供了哪些对外的接口或者服务 。每个接口和服务所提供功能是什么。这一点我们可以继续去测试自己的系统,这个时候我们要看一看主要流程中主要包含了哪些页面,每个页面又调用了后端的哪些接口,每个后端接口又对应着哪个代码仓库。(如果是单纯做后端服务的,可以看一下我们提供了哪些服务,又有哪些上游服务,每个上游服务调用自己团队的哪些服务……),同样我们应该用画图的形式整理出来。
接着,我们要了解一下 自己的系统或服务又依赖了哪些外部服务 ,也就是说需要哪些外部系统的支持,这些服务也许是团队之外、公司之外,也可能是其他公司提供的。这个时候我们可以简单的进入代码看一下与外部系统的交互是怎么做的,包括通讯框架(REST、RPC)、通讯协议……
到了代码层面,我们首先应该了解每个模块代码的层次结构,一个模块分了多少层,每个层次的职责是什么,了解了这个就对系统的整个设计有了初步的概念,紧接着就是代码的目录结构、配置文件的位置。
最后,我们可以寻找一个示例,可以是一个接口,一个页面,让我们的思路跟随者代码的运行的路线,从入参到出参,完整的走一遍来验证一下我们之前的了解。
到了这里我们对于技术层面的了解就可以先告一段落了,我们的目的知识对系统有一个初步的认知,更细节的东西,后面我们会有大把的时间去了解
项目与团队
上面我们提到,新入职一家公司,第一阶段的目标是有跟着团队做项目的能力,接下来我们要了解的就是项目是如何运作的。
我们应该把握从需求设计到代码编写入库最终到发布上线的整个过程中的一些关键点。例如项目采用敏捷还是瀑布的模式,一个迭代周期是多长,需求的来源以及展现形式,有没有需求评审,代码的编写规范是什么,编写完成后如何构建,如何入库,有没有提交规范,如何交付测试,发布前的准备是什么,发布工具如何使用……
关于项目我们只需要观察同事,或者自己亲身经历一个迭代的开发,就能够大概了解清楚。
在了解项目运作的同时,我们还应该去了解团队,同样我们应该先从外部开始,我们对接了哪些外部团队,比如需求从哪里来,是否对接公司外部的团队,提供服务的上游团队有哪些,依赖的下游团队有哪些,团队之间如何沟通,常用的沟通方式是什么…….
接下来则是团队内部,团队中有哪些角色,每个人的职责是什么,这样遇到问题我们也可以清楚的找到对应的同事寻求帮助。是否有一些定期的活动与会议,例如每日站会、周例会,是否有一些约定俗成的规矩,是否有一些内部评审,分享机制……
总结
新入职一家公司,面临新的工作挑战,能够尽快进入工作状态,实现自己的价值,将会给你带来一个好的开始。
作为一个程序员,能够尽快进入工作状态,意味着我们首先应该具备跟着团队做项目的能力,这里我站在了一个后端开发的角度上从业务、技术、项目与团队四个方面总结了一些方法和经验。
关于如何快速进入工作状态,如果你有好的方法与建议,欢迎在评论区留言。
最后我们用一张思维导图来回顾一下这篇文章的内容。如果你觉得这篇文章对你有所帮助,可以关注文末公众号,我会经常分享一些自己成长过程中的经验与心得,与大家一起学习与进步。
如何在绩效考核中脱颖而出?
对于职场人来说,绩效考核是一个绕不开的话题,几乎所有公司都有一套绩效考核方法。
如果你想要在职场走的更远,爬的更高的话,绩效考核对你来说还是非常重要的。在绝大部分公司,绩效考核都会和你的职级晋升、薪水涨幅、年终奖挂钩。并且,很多公司是的确有末尾淘汰机制的,很残酷。
公司给员工升职加薪,一是看你过去的表现,二是看你未来的潜力。未来的潜力主要也是基于你过去的表现以及个人进步速度。只有你能够证明自己能够做的越来越好,你才能在升职加薪上获得更大的主动权。
如何提升自己在工作中的表现? 核心肯定是持续稳定的在工作上产出有价值的东西。
如果想要持续稳定的在工作上产出,入职之后一定一定一定要先熟悉公司的技术栈、内部系统以及各种常用工具的。熟悉了这些东西之后,你的工作效率才会拉满,你才能充分利用公司内部的资源。
熟悉了之后,更进一步就是熟练掌握那些对你当前工作比较重要的技术以及工具。
勇于认领有挑战的任务
我们在工作中的产出,通常是通过完成开发任务方式来体现的。
日常开发中,建议要勇于认领比较有挑战的任务,解决有难度的问题。一定不要一碰到不会的就退缩了,碰到没有接触过的技术就怂了。
但也不要过于自信,还是要对自己的能力有个清楚且客观地认识。
如果你当前正在做的任务实在没办法完成的话,可以找技术 Leader 或者其他同事聊聊,沟通一下,寻求一下他们的建议和指导。一定不要憋着不说,等到截止日期的时候,才让大家知道你没有完成这个任务。
深入思考业务
平时要养成深入思考业务的习惯,敢于提出自己的想法和建议,而不是业务负责人说什么就是什么,技术负责人说用什么技术就用什么技术。不要被牵着鼻子走,要有自己的想法。但切忌不要过于偏执,一切的争论要建立在完善的逻辑之上。就比如说你觉得某一块的业务设计不合理的话,那就要有理有据地和业务负责人说出自己的想法。再比如说你觉得有更好的技术框架可以解决当前项目的问题,那你就要把这个技术框架调研清楚之后再和技术负责人沟通。
如果你能够对项目的发展提出一些有用的建议,大家对你的看法肯定会不一样。
善于分享
平时工作中要乐于帮助其他同事,也要学会寻求同事的帮助。
如果公司崇尚技术分享的话。试着学会技术分享即使你讲的东西比较简单,讲好就行了。
多要反馈
尽量要多找你的技术 Leader/其他上级要反馈,多和他们聊聊自己做了什么,后面有什么什么建议给自己。
一定一定不要只埋头搞事,搞了事产生了效果,还要让其他人知道你的贡献。
持续学习
一定要有持续学习的意识!你的日常一定不是只有工作,想要走的更远,工作之外一定也要抽时间用学习武装自己。
举个例子你的项目用到了消息队列,那你就要一定要搞清楚:
- 常见消息队列之间的对比?如何选择?
- 如何确保消息不会丢失?
- 如何确保消息不被重复消费?
- 消息积压如何处理?
- 消息队列如何实现分布式事务?
- 消息队列(任选一个主流的消息队列进行研究)的底层原理是什么(网络通信、高性能 IO、数据压缩……)?如果让你设计的话,你会如何设计?(学习优秀的消息队列的底层原理)
- ……
如果刚毕业之后就没有持续学习的意识的话,那大概率未来的工作中也要不回养成这种意识。刚工作的那 1-3 年是个人能力提升最快的阶段。
要有 Owner 意识
什么叫有 Owner 意识呢? 我举几个例子大家应该就明白了。
- 某天客户突然在群里询问了一个问题,你及时在群里回应了客户。这就叫有 Owner 意识。
- 觉得项目某个模块的数据库表设计有问题,自己私下进行了深度思考,并给出了优化方案,之后找到技术 Leader 说明了自己想法。这就叫有 Owner 意识
- 觉得项目某个模块的技术方案有问题,自己找到技术 Leader 进行了沟通。这就叫有 Owner 意识
什么叫没有 Owner 意识呢? 我举几个反例大家应该就明白了。
- 某天客户突然在群里询问了一个问题,你看到了问题,但是觉得自己的工作还没做完或者觉得这事不重要,干脆就假装没看见。这就叫缺乏 Owner 意识。正确的做法是积极主动地推动问题的解决。如果自己没能力解决或者实现没时间解决的话,可以联系相关的同事帮忙解决。
- 觉得项目某个模块的数据库表设计有问题,自己就直接找到 Leader 开始抱怨:“这特么表设计的什么鬼啊!”。这就叫缺乏 Owner 意识。
- 觉得项目某个模块的技术方案有问题,自己睁一只眼闭一只眼,没有找技术 Leader 沟通,技术方案确定之后,却经常抱怨技术方案设计的不够好。这就叫缺乏 Owner 意识。
有 Owner 意识,并不是说让大家都去当“奋斗逼”,故意在上级面前多表现一下。而是说,希望自己能够对工作更加负责,更加积极主动地参与项目的建设。
要有全链路意识
什么是全链路? 全链路可以理解为一个请求在系统中经过的完整路径。
我们这里的全链路意识说的是:不仅仅要对你自己模块负责,还要尝试对了解整个系统涉及到的所有模块,将它们串联起来。
全链路意识是项目技术 Leader 的必备。如果你以后想要往项目技术 Leader 的方向发展,那就先从培养自己的全链路意识开始吧!
多沟通交流
不喜欢沟通交流和表达的人,一般也会更难受到上级的青睐。很多时候你做的工作比别人多,你本以为你可以获得更多认可和奖励,但是,到最后往往收获的认可度和奖励却没有别人高。
做好本质工作是我们的分内之事,如果你能偶尔抽出一些时间,多和你的同事、上级或者 leader 交流问题的话,你所能得到的肯定是远远超过你所付出的那一会时间。