能看得出,面试的题目围绕着二哥一直给大家强调的Java后端四大件展开,所以准备秋招或者春招的时候,一定要以这些为主,知道轻重缓急。
对,腾讯也招Java的,不要以为腾讯只招Go和CPP,大厂的产品线非常多,Java自然是有一席之地的。所以想冲腾讯暑期实习的小伙伴可以放心冲一波。
内容较长,撰写硬核面经不容易,建议大家先收藏起来,我会尽量用通俗易懂+手绘图的方式,让大家不仅能背会,还能理解和掌握。
使用ThreadLocal发生内存泄露的原因可能是:
①、ThreadLocalMap的生命周期过长,在使用线程池等长生命周期的线程时,线程不会立即销毁。
如果ThreadLocal变量在使用后没有被及时清理(通过调用ThreadLocal的remove()方法),那么ThreadLocalMap中的键值对会一直存在,即使外部已经没有对ThreadLocal对象的引用。
这意味着ThreadLocalMap中的键值对无法被垃圾收集器回收,从而导致内存泄露。
②、ThreadLocal对象生命周期结束,线程继续运行。
如果一个ThreadLocal对象已经不再被使用,但是线程仍然在运行,并且其ThreadLocalMap中还保留着对这个ThreadLocal对象的键的引用,这会导致ThreadLocal对象所引用的数据也无法被回收,因为ThreadLocalMap中的键是对ThreadLocal对象的弱引用(WeakReference),但值(存储的数据)是强引用。
①、JDK动态代理是基于接口的代理方式,它使用Java原生的java.lang.reflect.Proxy类和java.lang.reflect.InvocationHandler接口来创建和管理代理对象。
②、CGLIB(CodeGenerationLibrary)是一个第三方代码生成库,它通过继承方式实现代理,不需要接口,被广泛应用于SpringAOP中,用于提供方法拦截操作。
单例模式(SingletonPattern)是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取该实例。单例模式主要用于控制对某些共享资源的访问,例如配置管理器、连接池、线程池、日志对象等。
publicenumSingleton{INSTANCE;//可以添加实例方法}单例模式能确保一个类仅有一个实例,并提供一个全局访问点来访问这个实例。
这对于需要控制资源使用或需要共享资源的情况非常有用,比如数据库连接池,通过单例模式,可以避免对资源的重复创建和销毁,从而提高资源利用率和系统性能。
Redis处理过期数据(即键值对)的回收策略主要有两种:惰性删除和定期删除。
当某个键被访问时,如果发现它已经过期,Redis会立即删除该键。这意味着如果一个已过期的键从未被访问,它不会被自动删除,可能会占用额外的内存。
Redis会定期随机测试一些键,并删除其中已过期的键。这个过程是Redis内部自动执行的,旨在减少过期键对内存的占用。
可以通过configgethz命令查看当前的hz值。
结果显示hz的值为"10"。这意味着Redis服务器每秒执行其内部定时任务(如过期键的清理)的频率是10次。
可以通过CONFIGSEThz20进行调整,或者直接通过配置文件中的hz设置。
总之就是,崩了,崩的非常严重,就叫雪崩了(电影电视里应该看到过,非常夸张)。
第一种:提高缓存可用性
01、集群部署:采用分布式缓存而不是单一缓存服务器,可以降低单点故障的风险。即使某个缓存节点发生故障,其他节点仍然可以提供服务,从而避免对数据库的大量直接访问。
可以利用RedisCluster。
或者第三方集群方案Codis。
02、备份缓存:对于关键数据,除了在主缓存中存储,还可以在备用缓存中保存一份。当主缓存不可用时,可以快速切换到备用缓存,确保系统的稳定性和可用性。
第三种:限流和降级
通过设置合理的系统限流策略,如令牌桶或漏斗算法,来控制访问流量,防止在缓存失效时数据库被打垮。
此外,系统可以实现降级策略,在缓存雪崩或系统压力过大时,暂时关闭一些非核心服务,确保核心服务的正常运行。
MySQL的默认存储引擎是InnoDB,它采用的是B+树索引。
那在说B+树之前,必须得先说一下B树(B-tree)。
B树是一种自平衡的多路查找树,和红黑树、二叉平衡树不同,B树的每个节点可以有m个子节点,而红黑树和二叉平衡树都只有2个。
换句话说,红黑树、二叉平衡树是细高个,而B树是矮胖子。
好,我继续说。
内存和磁盘在进行IO读写的时候,有一个最小的逻辑单元,叫做页(Page),页的大小一般是4KB。
那为了提高读写效率,从磁盘往内存中读数据的时候,一次会读取至少一页的数据,比如说读取2KB的数据,实际上会读取4KB的数据;读取5KB的数据,实际上会读取8KB的数据。我们要尽量减少读写的次数。
因为读的次数越多,效率就越低。就好比我们在工地上搬砖,一次搬10块砖肯定比一次搬1块砖的效率要高,反正我每次都搬10块()。
对于红黑树、二叉平衡树这种细高个来说,每次搬的砖少,因为力气不够嘛,那来回跑的次数就越多。
是这个道理吧,树越高,意味着查找数据时就需要更多的磁盘IO,因为每一层都可能需要从磁盘加载新的节点。
B树的节点大小通常与页的大小对齐,这样每次从磁盘加载一个节点时,可以正好是一个页的大小。因为B树的节点可以有多个子节点,可以填充更多的信息以达到一页的大小。
B树的一个节点通常包括三个部分:
不过,正所谓“祸兮福所倚,福兮祸所伏”,正是因为B树的每个节点上都存了数据,就导致每个节点能存储的键值和指针变少了,因为每一页的大小是固定的,对吧?
于是B+树就来了,B+树的非叶子节点只存储键值,不存储数据,而叶子节点存储了所有的数据,并且构成了一个有序链表。
这样做的好处是,非叶子节点上由于没有存储数据,就可以存储更多的键值对,树就变得更加矮胖了,于是就更有劲了,每次搬的砖也就更多了()。
由此一来,查找数据进行的磁盘IO就更少了,查询的效率也就更高了。
再加上叶子节点构成了一个有序链表,范围查询时就可以直接通过叶子节点间的指针顺序访问整个查询范围内的所有记录,而无需对树进行多次遍历。
总结一下,InnoDB之所以选择B+树是因为:
我在进行慢SQL优化的时候,主要通过以下几个方面进行优化:
比如说尽量避免使用select*,只查询需要的列,减少数据传输量。
SELECT*FROMemployeesWHEREdepartment_id=5;改成:
SELECTemployee_id,first_name,last_nameFROMemployeesWHEREdepartment_id=5;如何进行分页优化?当数据量巨大时,传统的LIMIT和OFFSET可能会导致性能问题,因为数据库需要扫描OFFSET+LIMIT数量的行。
延迟关联(LateRowLookups)和书签(SeekMethod)是两种优化分页查询的有效方法。
①、延迟关联
延迟关联适用于需要从多个表中获取数据且主表行数较多的情况。它首先从索引表中检索出需要的行ID,然后再根据这些ID去关联其他的表获取详细信息。
SELECTe.id,e.name,d.detailsFROMemployeeseJOINdepartmentdONe.department_id=d.idORDERBYe.idLIMIT1000,20;延迟关联后:
SELECTe.id,e.name,d.detailsFROM(SELECTidFROMemployeesORDERBYidLIMIT1000,20)ASsubJOINemployeeseONsub.id=e.idJOINdepartmentdONe.department_id=d.id;首先对employees表进行分页查询,仅获取需要的行的ID,然后再根据这些ID关联获取其他信息,减少了不必要的JOIN操作。
②、书签(SeekMethod)
书签方法通过记住上一次查询返回的最后一行的某个值,然后下一次查询从这个值开始,避免了扫描大量不需要的行。
假设需要对用户表进行分页,根据用户ID升序排列。
SELECTid,nameFROMusersORDERBYidLIMIT1000,20;书签方式:
SELECTid,nameFROMusersWHEREid>last_max_id--假设last_max_id是上一页最后一行的IDORDERBYidLIMIT20;优化后的查询不再使用OFFSET,而是直接从上一页最后一个用户的ID开始查询。这里的last_max_id是上一次查询返回的最后一行的用户ID。这种方法有效避免了不必要的数据扫描,提高了分页查询的效率。
①、利用覆盖索引
使用非主键索引查询数据时需要回表,但如果索引的叶节点中已经包含要查询的字段,那就不会再回表查询了,这就叫覆盖索引。
举个例子,现在要从test表中查询city为上海的name字段。
selectnamefromtestwherecity='上海'如果仅在city字段上添加索引,那么这条查询语句会先通过索引找到city为上海的行,然后再回表查询name字段,这就是回表查询。
为了避免回表查询,可以在city和name字段上建立联合索引,这样查询结果就可以直接从索引中获取。
③、适当使用前缀索引
适当使用前缀索引可以降低索引的空间占用,提高索引的查询效率。
altertabletestaddindexindex2(email(6));需要注意的是,MySQL无法利用前缀索引做orderby和groupby操作。
④、避免列上使用函数
在where子句中直接对列使用函数会导致索引失效,因为数据库需要对每行的列应用函数后再进行比较,无法直接利用索引。
selectnamefromtestwheredate_format(create_time,'%Y-%m-%d')='2021-01-01';可以改成:
⑤、正确使用联合索引
正确地使用联合索引可以极大地提高查询性能,联合索引的创建应遵循最左前缀原则,即索引的顺序应根据列在查询中的使用频率和重要性来安排。
select*frommessageswheresender_id=1andreceiver_id=2andis_read=0;那就可以为sender_id、receiver_id和is_read这三个字段创建联合索引,但是要注意索引的顺序,应该按照查询中的字段顺序来创建索引。
altertablemessagesaddindexindex3(sender_id,receiver_id,is_read);如何进行JOIN优化?对于JOIN操作,可以通过优化子查询、小表驱动大表、适当增加冗余字段、避免join太多表等方式来进行优化。
①、优化子查询
子查询,特别是在select列表和where子句中的子查询,往往会导致性能问题,因为它们可能会为每一行外层查询执行一次子查询。
使用子查询:
selectnamefromAwhereidin(selectidfromB);使用JOIN代替子查询:
selectA.namefromAjoinBonA.id=B.id;②、小表驱动大表
在执行JOIN操作时,应尽量让行数较少的表(小表)驱动行数较多的表(大表),这样可以减少查询过程中需要处理的数据量。
比如leftjoin,左表是驱动表,所以A表应小于B表,这样建立连接的次数就少,查询速度就快了。
selectnamefromAleftjoinB;③、适当增加冗余字段
在某些情况下,通过在表中适当增加冗余字段来避免JOIN操作,可以提高查询效率,尤其是在高频查询的场景下。
比如,我们有一个订单表和一个商品表,查询订单时需要显示商品名称,如果每次都通过JOIN操作查询商品表,会降低查询效率。这时可以在订单表中增加一个冗余字段,存储商品名称,这样就可以避免JOIN操作。
selectorder_id,product_namefromorders;④、避免使用JOIN关联太多的表
因为join太多表会降低查询的速度,返回的数据量也会变得非常大,不利于后续的处理。
如果业务逻辑允许,可以考虑将复杂的JOIN查询分解成多个简单查询,然后在应用层组合这些查询的结果。
MySQL生成有序结果的方式有两种:一种是对结果集进行排序操作,另外一种是按照索引顺序扫描得出的自然有序结果。
因此在设计索引的时候要充分考虑到排序的需求。
selectid,namefromusersorderbyname;如果name字段上有索引,那么MySQL可以直接利用索引的有序性,避免排序操作。
UNION操作用于合并两个或者多个SELECT语句的结果集。
①、条件下推
条件下推是指将where、limit等子句下推到union的各个子查询中,以便优化器可以充分利用这些条件进行优化。
假设我们有两个查询分支,需要合并结果并过滤:
SELECT*FROM(SELECT*FROMAUNIONSELECT*FROMB)ASsubWHEREsub.id=1;可以改写成:
SELECT*FROMAWHEREid=1UNIONSELECT*FROMBWHEREid=1;通过将查询条件下推到UNION的每个分支中,每个分支查询都只处理满足条件的数据,减少了不必要的数据合并和过滤。
在SpringBoot中,开启自动装配的注解是@EnableAutoConfiguration。
SpringBoot项目为了进一步简化,直接通过@SpringBootApplication注解一步搞定,这个注解包含了@EnableAutoConfiguration注解。
①、@EnableAutoConfiguration只是一个简单的注解,但是它的背后却是一个非常复杂的自动装配机制,它的核心是AutoConfigurationImportSelector类。
③、获取注入类的方法是selectImports(),它实际调用的是getAutoConfigurationEntry(),这个方法是获取自动装配类的关键。
SpringBoot的默认包扫描路径是以启动类@SpringBootApplication注解所在的包为根目录的,即默认情况下,SpringBoot会扫描启动类所在包及其子包下的所有组件。
@SpringBootApplication是一个组合注解,它里面的@ComponentScan注解可以指定要扫描的包路径,默认扫描启动类所在包及其子包下的所有组件。
@ComponentScan(excludeFilters={@Filter(type=FilterType.CUSTOM,classes=TypeExcludeFilter.class), @Filter(type=FilterType.CUSTOM,classes=AutoConfigurationExcludeFilter.class)})public@interfaceSpringBootApplication{}比如说带有@Component、@Service、@Controller、@Repository等注解的类都会被SpringBoot扫描到,并注册到Spring容器中。
如果需要自定义包扫描路径,可以在@SpringBootApplication注解上添加@ComponentScan注解,指定要扫描的包路径。
@SpringBootApplication@ComponentScan(basePackages={"com.github.paicoding.forum"})publicclassQuickForumApplication{publicstaticvoidmain(String[]args){SpringApplication.run(QuickForumApplication.class,args);}}这种方式会覆盖默认的包扫描路径,只扫描com.github.paicoding.forum包及其子包下的所有组件。
最后,把二哥的座右铭送给大家:没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟。共勉。