专业技能:
- 熟练掌握Java基础知识,集合,多线程等;
- 熟练掌握SpringMVC、SpringBoot、Spring cloud、Mybatis等开源框架;
- 熟练使用Mysql数据库,能熟练使用相关数据库函数,掌握SQL语言;
- 熟练使用explain工具对SQL执行过程进行分析,优化SQL;
- 熟悉分布式架构相关内容,熟悉微服务原理,对服务调用、消息中间件、分布式缓存有一定了解;
- 熟悉JVM内存模型和GC垃圾回收机制,了解回收算法;
- 了解HTML5、CSS、JavaScript、Vue等前端网页开发技术;
业务场景------遇到的问题-----解决方案
项目经历:
-
java基础八股文(因为我申请的大部分是Java,所以就是对应的Java八股文)。相应的八股文资料网上数不胜数,靠谱的就是被star了几万次的JavaGuide以及Aobing Java的git。
-
SQL(数据结构,磁盘相关,隔离级别及问题和场景,引擎,ACID,MVCC,锁,explain,慢查询,索引优化,数据库优化,三大范式)。
加分点1:说到B+树,面试官常常会引申出其他的树。此时你最好说出树的发展:AVL tree->平衡二叉树->红黑树->B树->B+树->跳表。
加分点2:让你说说mysql里用到的各种锁,尽量扯,能扯多少是多少。从ACID到MVCC到快照读/当前读到当前读里的各种锁机制,最后再说下死锁和死锁场景,说的完整绝对的加分项。
加分点3:不仅了解RDBMS,你还了解KV型NoSQL(Redis),搜索型NoSQL(ElasticSearch),文档型NoSQL(MongoDB)。NoSQL,RDBMS各自的简单原理,优劣和使用场景以及各自的瓶颈。另外,讲到这种分布式中间件就会涉及到脑裂问题!
- 计算机网络(OSI,老tcp/udp了重中之重, DNS,ARP,http状态码,synflood攻击,域名劫持,dns欺骗)
加分点1:面试官要你说出http各版本的发展,厉害的你要说出http1.0->http1.1->http2.0->http3.0的改进。
加分点2:了解tcp的每个汗毛每个毛孔每一滴血液。
加分点3:经典题目——在浏览器中输入一个url之后会发生什么?除了老生常谈的dns寻址那些,最好把OSI layer中涉及的每一层都提到,包括用到的协议。我每一次回答这个问题都精确到了细节,比如涉及的ARP协议,路由协议,交换机,路由器,网卡,缓冲,如何如何加报头等等。另外,数据包到达后,你可以再提Nginx通过负载均衡才能把请求映射到真正的服务器上!还可以提CDN用来加速响应!这是妥妥的加分项!
加分点4:Nagle算法,延迟确认(这个其实只有当初面腾讯实习的时候问到过,确实一点儿不了解,不过面试还是过了,算是个加分项)。
加分点5:ChatGPT。无论是对chatgpt的方方面面有个广泛的认知,或是对其某个点有较为深入或独到的点,都会成为现阶段面试巨大的加分项。
-
计算机安全:除了在计算机网络里提到的几种攻击之外,问的主要是非对称/对称加密以及https。问到https的话,从https的三个指标(加密,数据一致性,身份认证)答起,答的过程中肯定会提到摘要,加密算法,以及数字签名,数字证书。之后再延伸到https握手的详细过程。
-
操作系统(用户态vs内核态,用户态内核态切换,串行并行并发,中断,通信方式,select vs poll vs epoll(无数次问到),虚拟内存(重中之重),内存规划,进程/线程/协程,IO操作(重中之重),典型的sig信号,常见的linux指令,top指令和内容,nohup指令):
-
Java程序员一定要看的源码(不想看1.7版本的只看1.8也可):
-
HashMap1.7 & 1.8 (put/get/扩容/capacity, loadFactor)
-
ConcurrentHashMap1.7 & 1.8 (和上面一样,主要看如何做的同步)
-
ArrayList源码(喜欢问扩容机制)
-
跳表源码(大厂深度面的亲儿子)
-
Spring Boot的启动源码(其实问的不多,但可能会问)
-
著名开源项目的源码(这个只被问过一次,只是个加分项,不一定要看。你可以挑一个你喜欢的或者借鉴过的项目源码读一下。之前字节四面面试官希望我能和他讨论自己看过的开源项目源码,我直接说我没看过啥项目源码,不过最后还是给过了。。。)
除了这些必问的外,以下中间件主要看你的项目/课程是否涉及到。你没用过就大大方方的说没用过,良心的面试官不会因此而为难你,你只要把你会的那部分做到出类拔萃就行了,面试官会觉得你态度认真,善于思考,学习能力强。
- Spring相关(Java面试的宠儿,IoC原理,AoP原理,事务何时失效,Spring vs Spring MVC vs Spring Boot(多次问到),设计模式(重中之重),Spring Boot启动流程分析)。
加分点1:Spring Boot设计模式中最喜欢问的就是代理模式和适配器模式,Spring AoP和Spring MVC中用到的适配器模式都应掌握。
- Redis相关(数据结构,工作原理,为什么快,删除策略,淘汰策略,持久化机制并没有怎么被问到,缓存问题,主动复制/哨兵/集群,一致性哈希,使用场景,分布式锁,此时可以和zk的分布式锁做对比)
加分点1:Sorted set底层跳表被多家大厂都问到过,不下四次,绝对的重中之重。包括复杂度,底层代码,使用场景,为什么mysql里使用B+树不使用跳表,为什么redis里sorted set使用跳表不使用B+树,以及各式各样的衍生题。所以跳表一定要大量的阅读相关面试题。如果你觉得跳表这块你已经烂熟于心了,回答B+树相关或者redis相关尽量把面试官的问题往跳表方向上引,回答的好妥妥是个加分项。
-
Zookeeper相关(是什么,应用场景(很重要),监听器,分布式算法,消息广播,崩溃恢复,选举)
-
RPC和Dubbo(是什么,解决什么问题,项目中没使用的话简单了解即可)
-
消息队列(是什么,解决什么问题,Kafka,Rocket mq,稍微微了解即可,一般问不到)
-
云原生+Docker+K8s+CI/CD (可以稍作了解)
-
高可用高并发相关,大型网站系统架构(可以为场景题提供思路,JavaGuide git上有相关资料)
-
智力题(面到现在只有字节三场面试每次都问到了智力题,从看过的面经上似乎百度,美团,蔚来也可能问到,看面试官的喜好),没什么特殊的方法,只能大量的看面经和智力题相关资料。
-
另外,大厂到了二/三/四面可能会问一些场景题。于我而言,问的不多,亮色标注的题目是我被问到过的。
场景题其实有些套路,得去搜常问的场景题,理解且背下来,学习那些场景题的回答思路。面试官如果问到场景题不会的情况下,就和他一层一层的扯,提出你觉得可能的solution,思维越发散越好。我个人觉得场景题回答的特别好的人要不是特别聪明要不就是看的相关题特别多。
经典场景题/设计题:
-
如何实现分布式全局id?
-
如何生成邀请码?
-
海量数据处理(经常被问到,一定要看有关的各种题)
-
如何设计一个线程池/数据连接池?
-
数据库设计(设计微博关注/被关注,微信朋友圈,学生老师课程,图书tag等)
-
微信抢红包
-
淘宝微信扫码登录
1:缓存技术和框架的重要性最全面的缓存架构设计(全是干货) - 知乎 (zhihu.com)
互联网的一些高并发,高性能的项目和系统中,缓存技术是起着功不可没的作用。缓存不仅仅是key-value的简单存取,它在具体的业务场景中,还是很复杂的,需要很强的架构设计能力。我曾经就遇到过因为缓存架构设计不到位,导致了系统崩溃的案例。
2:缓存的技术方案分类
1)是做实时性比较高的那块数据,比如说库存,销量之类的这种数据,我们采取的实时的缓存+数据库双写的技术方案,双写一致性保障的方案。
2)是做实时性要求不高的数据,比如说商品的基本信息,等等,我们采取的是三级缓存架构的技术方案,就是说由一个专门的数据生产的服务,去获取整个商品详情页需要的各种数据,经过处理后,将数据放入各级缓存中。
3:高并发以及高可用的复杂系统中的缓存架构都有哪些东西
1)在大型的缓存架构中,redis是最最基础的一层。高并发,缓存架构中除了redis,还有其他的组成部分,但是redis至关重要。
• 如果你的数据量不大(10G以内),单master就可以。redis持久化+备份方案+容灾方案+replication(主从+读写分离)+sentinal(哨兵集群,3个节点,高可用性)
• 如果你的数据量很大(1T+),采用redis cluster。多master分布式存储数据,水平扩容,自动进行master -> slave的主备切换。
2)最经典的缓存+数据库读写的模式,cache aside pattern。读的时候,先读缓存,缓存没有的话,那么就读数据库。更新缓存分以下两种方式:
• 数据发生变化时,先更新缓存,然后再更新数据库。这种适用于缓存的值相对简单,和数据库的值一一对应,这样更新比较快。
• 数据发生变化时,先删除缓存,然后再更新数据库,读数据的时候再设置缓存。这种适用于缓存的值比较复杂的场景。比如可能更新了某个表的一个字段,然后其对应的缓存,是需要查询另外两个表的数据,并进行运算,才能计算出缓存最新的值的。这样更新缓存的代价是很高的。如果你频繁修改一个缓存涉及的多个表,那么这个缓存会被频繁的更新,频繁的更新缓存代价很高。而且这个缓存的值如果不是被频繁访问,就得不偿失了。
大部分情况下,建议适用删除更新的方式。其实删除缓存,而不是更新缓存,就是一个lazy计算的思想,不要每次都重新做复杂的计算,不管它会不会用到,而是让它到需要被使用的时候再重新计算。
举个例子,一个缓存涉及的表的字段,在1分钟内就修改了20次,或者是100次,那么缓存跟新20次,100次; 但是这个缓存在1分钟内就被读取了1次,有大量的冷数据。28黄金法则,20%的数据,占用了80%的访问量。实际上,如果你只是删除缓存的话,那么1分钟内,这个缓存不过就重新计算一次而已,开销大幅度降低。每次数据过来,就只是删除缓存,然后修改数据库,如果这个缓存,在1分钟内只是被访问了1次,那么只有那1次,缓存是要被重新计算的。
3)数据库与缓存双写不一致问题的解决方案
问题:并发请求的时候,数据发生了变更,先删除了缓存,然后要去修改数据库,此时还没修改。另一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中。
方案:数据库与缓存更新与读取操作进行异步串行化。(引入队列)
更新数据的时候,将相应操作发送到一个jvm内部的队列中。读取数据的时候,如果发现数据不在缓存中,那么将重新读取数据的操作也发送到同一个jvm内部的队列中。队列消费者串行拿到对应的操作,然后一条一条的执行。这样的话,一个数据变更的操作,先执行删除缓存,然后再去更新数据库,但是还没完成更新。此时如果一个读请求过来,读到了空的缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,然后同步等待缓存更新完成。
这里有两个可以优化的点:
• 一个队列中,其实多个读缓存,更新缓存的请求串在一起是没意义的,而且如果读同一缓存的大量请求到来时,会依次进入队列等待,这样会导致队列最后一个的请求响应时间超时。因此可以做过滤,如果发现队列中已经有一个读缓存,更新缓存的请求了,那么就不用再放个新请求操作进去了,直接等待前面的更新操作请求完成即可。如果请求还在等待时间范围内,不断轮询发现可以取到值了,那么就直接返回; 如果请求等待的时间超过一定时长,那么这一次直接从数据库中读取当前的旧值。
• 如果请求量特别大的时候,可以用多个队列,每个队列对应一个线程。每个请求来时可以根据请求的标识id进行hash路由进入到不同的队列。
最后,一定要做根据实际业务系统的运行情况,去进行一些压力测试,和模拟线上环境,去看看最繁忙的时候,内存队列可能会挤压多少更新操作,可能会导致最后一个更新操作对应的读请求,会hang多少时间,如果读请求在200ms返回,如果你计算过后,哪怕是最繁忙的时候,积压10个更新操作,最多等待200ms,那还可以的。如果一个内存队列可能积压的更新操作特别多,那么你就要加机器,让每个机器上部署的服务实例处理更少的数据,那么每个内存队列中积压的更新操作就会越少。其实根据之前的项目经验,一般来说数据的写频率是很低的,因此实际上正常来说,在队列中积压的更新操作应该是很少的。
举个例子:一秒就100个写操作。单台机器,20个内存队列,每个内存队列,可能就积压5个写操作,每个写操作性能测试后,一般在20ms左右就完成,那么针对每个内存队列中的数据的读请求,也就最多hang一会儿,200ms以内肯定能返回了。如果把写QPS扩大10倍,但是经过刚才的测算,就知道,单机支撑写QPS几百没问题,那么就扩容机器,扩容10倍的机器,10台机器,每个机器20个队列,200个队列。大部分的情况下,应该是这样的,大量的读请求过来,都是直接走缓存取到数据的,少量情况下,可能遇到读跟数据更新冲突的情况,如上所述,那么此时更新操作如果先入队列,之后可能会瞬间来了对这个数据大量的读请求,但是因为做了去重的优化,所以也就一个更新缓存的操作跟在它后面。
4)大型缓存全量更新问题的解决方案
问题:缓存数据很大时,可能导致redis的吞吐量就会急剧下降,网络耗费的资源大。如果不维度化,就导致多个维度的数据混合在一个缓存value中。而且不同维度的数据,可能更新的频率都大不一样。拿商品详情页来说,如果现在只是将1000个商品的分类批量调整了一下,但是如果商品分类的数据和商品本身的数据混杂在一起。那么可能导致需要将包括商品在内的大缓存value取出来,进行更新,再写回去,就会很坑爹,耗费大量的资源,redis压力也很大
方案:缓存维度化。举个例子:商品详情页分三个维度:商品维度,商品分类维度,商品店铺维度。将每个维度的数据都存一份,比如说商品维度的数据存一份,商品分类的数据存一份,商品店铺的数据存一份。那么在不同的维度数据更新的时候,只要去更新对应的维度就可以了。大大减轻了redis的压力。
5)通过多级缓存,达到高并发极致,同时给缓存架构最后的安全保护层。具体可以参照上一篇文章【亿级流量的商品详情页架构分析】。
6)分布式并发缓存重建的冲突问题的解决方案
问题:假如数据在所有的缓存中都不存在了(LRU算法弄掉了),就需要重新查询数据写入缓存。对于分布式的重建缓存,在不同的机器上,不同的服务实例中,去做上面的事情,就会出现多个机器分布式重建去读取相同的数据,然后写入缓存中。
方案:分布式锁:如果你有多个机器在访问同一个共享资源,那么这个时候,如果你需要加个锁,让多个分布式的机器在访问共享资源的时候串行起来。分布式锁当然有很多种不同的实现方案,redis分布式锁,zookeeper分布式锁。
zookeeper分布式锁的解决并发冲突的方案
• (1)变更缓存重建以及空缓存请求重建,更新redis之前,都需要先获取对应商品id的分布式锁
• (2)拿到分布式锁之后,需要根据时间版本去比较一下,如果自己的版本新于redis中的版本,那么就更新,否则就不更新
• (3)如果拿不到分布式锁,那么就等待,不断轮询等待,直到自己获取到分布式的锁
7)缓存冷启动的问题的解决方案
问题:新系统第一次上线,此时在缓存里可能是没有数据的。或者redis缓存全盘崩溃了,数据也丢了。导致所有请求打到了mysql。导致mysql直接挂掉。
方案:缓存预热。
• 提前给redis中灌入部分数据,再提供服务
• 肯定不可能将所有数据都写入redis,因为数据量太大了,第一耗费的时间太长了,第二根本redis容纳不下所有的数据,需要根据当天的具体访问情况,实时统计出访问频率较高的热数据,然后将访问频率较高的热数据写入redis中,肯定是热数据也比较多,我们也得多个服务并行读取数据去写,并行的分布式的缓存预热。
8)恐怖的缓存雪崩问题的解决方案
问题:缓存服务大量的资源全部耗费在访问redis和源服务无果,最后自己被拖死,无法提供服务。
方案:相对来说,考虑的比较完善的一套方案,分为事前,事中,事后三个层次去思考怎么来应对缓存雪崩的场景。
• 事前:高可用架构。主从架构,操作主节点,读写,数据同步到从节点,一旦主节点挂掉,从节点跟上。
• 事中:多级缓存。redis cluster已经彻底崩溃了,缓存服务实例的ehcache的缓存还能起到作用。
• 事后:redis数据可以恢复,做了备份,redis数据备份和恢复,redis重新启动起来。
9)缓存穿透问题的解决方案
问题:缓存中没有这样的数据,数据库中也没有这样的数据。由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。
方案:有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
分类功能实现逻辑
1.从数据库中查询出所有分类信息,一次性全查
2.构建分类信息的父子结构,实现查询返回父子结构的分类信息
3.将查询到的结果保存在Redis中,以备后续用户直接获取
代码中要判断Redis中是否包含全部分类数据,不包含的话做上面操作
包含分类数据的话直接获得之后返回
三级分类树
本项目使用固定的三级分类树
是自关联分类(所有分类信息在一张表中)
实现思路
1.一次性查询出所有分类对象(List集合)
2.遍历集合将当前分类对象以父分类id为Key,以当前对象作为值,保存在一个Map中,这个Map对象的Key(父级分类ID)对应的Key,会包含它的所有子分类对象
3.遍历所有分类对象,以当前分类对象id为key,从Map中获取它的子分类,关联到三级分类树对象中
最后返回包含三级分类树结构的集合
【【自关联分类是指在分类系统中,某个分类可以同时属于同一分类系统中的其他分类。这种关联关系可以是层级的,即一个分类可以是另一个分类的父级或子级,也可以是同级的,即一个分类可以与其他分类并列存在。
自关联分类常见于具有层级结构的分类系统,例如商品分类、文件夹分类等。在这些分类系统中,一个分类可以包含其他子分类,同时也可以属于一个或多个更高级的父级分类。
例如,在电子商务网站的商品分类系统中,一个分类可以同时属于多个父级分类。比如,一个商品可以同时属于"电子产品"和"数码相机"两个分类,其中"数码相机"可以是"电子产品"的子分类。
自关联分类的使用可以帮助组织和管理大量的分类数据,提供更细致的分类和检索功能。同时,它也增加了分类系统的灵活性,使得分类可以根据实际需求进行调整和扩展。】】
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class CategoryTreeBuilder {
public List<Category> buildCategoryTree(List<Category> categories) {
// 使用Map保存每个分类的子分类列表
Map<Long, List<Category>> categoryMap = new HashMap<>();
// 遍历所有分类对象,将每个分类对象以父分类id为Key,以当前对象作为值保存在Map中
for (Category category: categories) {
Long parentId = category.getParentId();
List<Category> childCategories = categoryMap.getOrDefault(parentId, new ArrayList<>());
childCategories.add(category);
categoryMap.put(parentId, childCategories);
}
// 构建三级分类树
List<Category> categoryTree = new ArrayList<>();
for (Category category: categories) {
Long categoryId = category.getId();
List<Category> childCategories = categoryMap.get(categoryId);
category.setChildCategories(childCategories);
if (category.getParentId() == 0) {
categoryTree.add(category);
}
}
return categoryTree;
}
public static void main(String[] args) {
// 示例数据
List<Category> categories = new ArrayList<>();
categories.add(new Category(1L, "分类1", 0L));
categories.add(new Category(2L, "分类2", 0L));
categories.add(new Category(3L, "分类3", 1L));
categories.add(new Category(4L, "分类4", 2L));
categories.add(new Category(5L, "分类5", 3L));
categories.add(new Category(6L, "分类6", 3L));
categories.add(new Category(7L, "分类7", 4L));
// 构建三级分类树
CategoryTreeBuilder builder = new CategoryTreeBuilder();
List<Category> categoryTree = builder.buildCategoryTree(categories);
// 输出分类树结构
for (Category category: categoryTree) {
printCategory(category, 0);
}
}
private static void printCategory(Category category, int level) {
StringBuilder prefix = new StringBuilder();
for (int i = 0; i < level; i++) {
prefix.append(" ");
}
System.out.println(prefix + category.getName());
List<Category> childCategories = category.getChildCategories();
if (childCategories != null) {
for (Category childCategory : childCategories) {
printCategory(childCategory, level + 1);
}
}
}
}
class Category {
private Long id;
private String name;
private Long parentId;
private List childCategories;
public Category(Long id, String name, Long parentId) {
this.id = id;
this.name = name;
this.parentId = parentId;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public Long getParentId() {
return parentId;
}
public List<Category> getChildCategories() {
return childCategories;
}
public void setChildCategories(List<Category> childCategories) {
this.childCategories = childCategories;
}
}
输出结果:
分类1
分类3
分类5
分类6
分类4
分类7
分类2
布隆过滤器
什么是布隆过滤器
布隆过滤器能够实现使用较少的空间来判断一个指定的元素是否包含在一个集合
布隆过滤器并不保存这些数据,所以只能判断是否存在,而并不能取出改元素
布隆过滤器常见使用场景
idea中编写代码,一个单词是否包含在正确拼写的词库中(拼写不正确划绿线的提示)
公安系统,根据身份证号\人脸信息,判断该人是否在追逃名单中
爬虫检查一个网址是否被爬取过
…
宗旨凡是判断一个元素是否在一个集合中的操作,都可以使用它
设计布隆过滤器
我们在启动布隆过滤器时,需要给它分配一个合理大小的内存
这个大小应该满足
1.一个可接受范围的大小
2.不能有太高的误判率
内存约节省,误判率越高
内存越大,误判率越低
Spring mvc boot 原理和实现机制
SSM框架
JVM相关调优 解决生产问题
中间件的优缺点和使用场景 redis MQ