文章摘要:名下),虽然已凉,但可以给大家打个样。所以想冲腾讯暑期实习的小伙伴可以放心冲一波。腾讯面经单例模式能确保一个类仅有一个实例,并提供一个全局访问点来访问这个实例。三分恶面渣逆袭:缓存雪崩如何解决缓存雪崩···
大家好,我是二哥呀。
鹅厂在整个互联网大厂中,属于工作环境非常舒适的那种,所以很多小伙伴都很向往~
之前有一位读者从腾讯出来,拿到 80 万的年包,却很不满意,觉得不如当时在鹅厂的 60 万年包。那今天我们就分享一个腾讯的 Java 暑期实习一面面经(收录到了Java面试指南中同学 22 名下),虽然已凉,但可以给大家打个样。
能看得出,面试的题目围绕着二哥一直给大家强调的 Java 后端四大件展开,所以准备秋招或者春招的时候,一定要以这些为主,知道轻重缓急。
对,腾讯也招 Java 的,不要以为腾讯只招 Go 和 CPP,大厂的产品线非常多,Java 自然是有一席之地的。所以想冲腾讯暑期实习的小伙伴可以放心冲一波。
腾讯面经
内容较长,撰写硬核面经不容易,建议大家先收藏起来,我会尽量用通俗易懂+手绘图的方式,让大家不仅能背会,还能理解和掌握。
什么情况下会内存泄漏
使用 发生内存泄露的原因可能是:
①、 的生命周期过长,在使用线程池等长生命周期的线程时,线程不会立即销毁。
如果变量在使用后没有被及时清理(通过调用的()方法),那么中的键值对会一直存在,即使外部已经没有对对象的引用。
这意味着中的键值对无法被垃圾收集器回收,从而导致内存泄露。
三分恶面渣逆袭:内存分配
②、 对象生命周期结束,线程继续运行。
如果一个对象已经不再被使用,但是线程仍然在运行,并且其中还保留着对这个对象的键的引用,这会导致对象所引用的数据也无法被回收,因为中的键是对对象的弱引用(),但值(存储的数据)是强引用。
AOP实现原理
的 AOP 是通过来实现的,动态代理主要有两种方式:JDK 动态代理和 CGLIB 代理。
①、JDK 动态代理是基于接口的代理方式,它使用 Java 原生的 java.lang..Proxy 类和 java.lang.. 接口来创建和管理代理对象。
基于 :JDK 动态代理要求目标对象必须实现一个或多个接口。代理对象不是直接继承自目标对象,而是实现了与目标对象相同的接口。
使用 :在调用代理对象的任何方法时,调用都会被转发到一个 实例的 方法。可以在这个 方法中定义拦截逻辑,比如方法调用前后执行的操作。
基于 Proxy:Proxy 利用 动态创建一个符合目标类实现的接口实例,生成目标类的代理对象。
②、CGLIB(Code )是一个第三方代码生成库,它通过继承方式实现代理,不需要接口,被广泛应用于 AOP 中,用于提供方法拦截操作。
图片来源于网络
基于继承,CGLIB 通过在运行时生成目标对象的子类来创建代理对象,并在子类中覆盖非 final 的方法。因此,它不要求目标对象必须实现接口。
基于 ASM,ASM 是一个 Java 字节码操作和分析框架,CGLIB 可以通过 ASM 读取目标类的字节码,然后修改字节码生成新的类。它在运行时动态生成一个被代理类的子类,并在子类中覆盖父类的方法,通过方法拦截技术插入增强代码。
选择 CGLIB 还是 JDK 动态代理?单例模式的好处
单例模式( )是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取该实例。单例模式主要用于控制对某些共享资源的访问,例如配置管理器、连接池、线程池、日志对象等。
:单例模式
使用枚举(Enum)实现单例是最简单的方式,也能防止反射攻击和序列化问题。
public enum Singleton {
INSTANCE;
// 可以添加实例方法
}
单例模式能确保一个类仅有一个实例,并提供一个全局访问点来访问这个实例。
这对于需要控制资源使用或需要共享资源的情况非常有用,比如数据库连接池,通过单例模式,可以避免对资源的重复创建和销毁,从而提高资源利用率和系统性能。
Redis key删除策略
Redis 处理过期数据(即键值对)的回收策略主要有两种:惰性删除和定期删除。
惰性删除
当某个键被访问时,如果发现它已经过期,Redis 会立即删除该键。这意味着如果一个已过期的键从未被访问,它不会被自动删除,可能会占用额外的内存。
定期删除
Redis 会定期随机测试一些键,并删除其中已过期的键。这个过程是 Redis 内部自动执行的,旨在减少过期键对内存的占用。
可以通过 get hz 命令查看当前的 hz 值。
结果显示 hz 的值为 "10"。这意味着 Redis 服务器每秒执行其内部定时任务(如过期键的清理)的频率是 10 次。
可以通过 SET hz 20 进行调整,或者直接通过配置文件中的 hz 设置。
二哥本地 Redis 的配置文件路径和 hz 的默认值缓存雪崩,如何解决
缓存雪崩是指在某一个时间点,由于大量的缓存数据同时过期或缓存服务器突然宕机了,导致所有的请求都落到了数据库上(比如 MySQL),从而对数据库造成巨大压力,甚至导致数据库崩溃的现象。
总之就是,崩了,崩的非常严重,就叫雪崩了(电影电视里应该看到过,非常夸张)。
三分恶面渣逆袭:缓存雪崩如何解决缓存雪崩呢?
第一种:提高缓存可用性
01、集群部署:采用分布式缓存而不是单一缓存服务器,可以降低单点故障的风险。即使某个缓存节点发生故障,其他节点仍然可以提供服务,从而避免对数据库的大量直接访问。
可以利用 Redis 。
Rajat :Redis
或者第三方集群方案 Codis。
极客时间:Codis
02、备份缓存:对于关键数据,除了在主缓存中存储,还可以在备用缓存中保存一份。当主缓存不可用时,可以快速切换到备用缓存,确保系统的稳定性和可用性。
第二种:过期时间
对于缓存数据,设置不同的过期时间,避免大量缓存数据同时过期。可以通过在原有过期时间的基础上添加一个随机值来实现,这样可以分散缓存过期时间,减少同一时间对数据库的访问压力。
第三种:限流和降级
通过设置合理的系统限流策略,如令牌桶或漏斗算法,来控制访问流量,防止在缓存失效时数据库被打垮。
此外,系统可以实现降级策略,在缓存雪崩或系统压力过大时,暂时关闭一些非核心服务,确保核心服务的正常运行。
MySQL 为什么选用 B+树
MySQL 的默认存储引擎是 ,它采用的是 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+树就来了,B+树的非叶子节点只存储键值,不存储数据,而叶子节点存储了所有的数据,并且构成了一个有序链表。
用户:B+树
这样做的好处是,非叶子节点上由于没有存储数据,就可以存储更多的键值对,树就变得更加矮胖了,于是就更有劲了,每次搬的砖也就更多了()。
由此一来,查找数据进行的磁盘 IO 就更少了,查询的效率也就更高了。
再加上叶子节点构成了一个有序链表,范围查询时就可以直接通过叶子节点间的指针顺序访问整个查询范围内的所有记录,而无需对树进行多次遍历。
总结一下, 之所以选择 B+树是因为:
查询优化、联合索引、覆盖索引
我在进行慢SQL 优化的时候,主要通过以下几个方面进行优化:
沉默王二:SQL 优化如何避免不必要的列?
比如说尽量避免使用 *,只查询需要的列,减少数据传输量。
SELECT * FROM employees WHERE department_id = 5;
改成:
SELECT employee_id, first_name, last_name FROM employees WHERE department_id = 5;
如何进行分页优化?
当数据量巨大时,传统的LIMIT和可能会导致性能问题,因为数据库需要扫描 + LIMIT数量的行。
延迟关联(Late Row )和书签(Seek )是两种优化分页查询的有效方法。
①、延迟关联
延迟关联适用于需要从多个表中获取数据且主表行数较多的情况。它首先从索引表中检索出需要的行ID,然后再根据这些ID去关联其他的表获取详细信息。
SELECT e.id, e.name, d.details
FROM employees e
JOIN department d ON e.department_id = d.id
ORDER BY e.id
LIMIT 1000, 20;
延迟关联后:
SELECT e.id, e.name, d.details
FROM (
SELECT id
FROM employees
ORDER BY id
LIMIT 1000, 20
) AS sub
JOIN employees e ON sub.id = e.id
JOIN department d ON e.department_id = d.id;
首先对表进行分页查询,仅获取需要的行的ID,然后再根据这些ID关联获取其他信息,减少了不必要的JOIN操作。
②、书签(Seek )
书签方法通过记住上一次查询返回的最后一行的某个值,然后下一次查询从这个值开始,避免了扫描大量不需要的行。
假设需要对用户表进行分页,根据用户ID升序排列。
SELECT id, name
FROM users
ORDER BY id
LIMIT 1000, 20;
书签方式:
SELECT id, name
FROM users
WHERE id > last_max_id -- 假设last_max_id是上一页最后一行的ID
ORDER BY id
LIMIT 20;
优化后的查询不再使用,而是直接从上一页最后一个用户的ID开始查询。这里的是上一次查询返回的最后一行的用户ID。这种方法有效避免了不必要的数据扫描,提高了分页查询的效率。
如何进行索引优化?
正确地使用索引可以显著减少 SQL 的查询时间,通常可以从索引覆盖、避免使用 != 或者 操作符、适当使用前缀索引、避免列上函数运算、正确使用联合索引等方面进行优化。
①、利用覆盖索引
使用非主键索引查询数据时需要回表,但如果索引的叶节点中已经包含要查询的字段,那就不会再回表查询了,这就叫覆盖索引。
举个例子,现在要从 test 表中查询 city 为上海的 name 字段。
select name from test where city='上海'
如果仅在 city 字段上添加索引,那么这条查询语句会先通过索引找到 city 为上海的行,然后再回表查询 name 字段,这就是回表查询。
为了避免回表查询,可以在 city 和 name 字段上建立联合索引,这样查询结果就可以直接从索引中获取。
alter table test add index index1(city,name);
②、避免使用 != 或者 操作符
!= 或者 操作符会导致 MySQL 无法使用索引,从而导致全表扫描。
例如,可以把'aaa',改成>'aaa' or 、