字节跳动也不过如此,狠心拒了。
共 11279字,需浏览 23分钟
·
2024-05-22 14:04
大家好,我是二哥呀。
字节跳动之所以被称为“宇宙厂”,就是因为字节的员工数量非常多,业务覆盖的范围非常广。再加上字节的薪资待遇给的非常可观,也就成为了很多同学的心仪之选,不只是校招还有社招哦(香,真香)。
23 届的一个球友去字节,直接开到了 SSP,让我印象非常深刻,所以每次提到字节我第一时间就想到了他。要知道,23 届号称是史上最难的一届哦,他还是双非本,年包直接给到了 50 万以上,非常夸张:开发一个小软件,月入2000
所以当我看到牛客上这位牛友直接拒绝了字节跳动的 offer,我是无所谓的,但我的一个朋友已经开始汗流浃背了,他有点破防了。。。。。。
不管网上的声音怎么说,我还是希望有能力的同学多冲一冲,尤其是字节这种愿意给钱多的厂,大不了干几年跑路嘛,这年头,在一家公司干一辈子估计也不太现实吧(😂)?
就像我昨天在 VIP 群里说的,怀揣希望,仰望星空,做一个爱自己、有思想、懂感恩,能实现自己人生抱负的人最好,做不到,也无所谓啊。
这次我们以《Java 面试指南-字节跳动面经》同学 1 的后端实习二面为例, 来看看字节的面试官都喜欢问哪些问题,好做到知彼知己百战不殆~
题目不少,火箭造的飞起。主要围绕 MySQL、Redis 和计算机网络展开,所以大家在准备的时候一定要有的放矢,知道哪些是重点。
1、二哥的 Linux 速查备忘手册.pdf 下载 2、三分恶面渣逆袭在线版:https://javabetter.cn/sidebar/sanfene/nixi.html
字节跳动面经(题目来自牛客)
对redis的数据结构是否熟悉?
Redis 的底层数据结构有动态字符串(sds)、链表(list)、字典(ht)、跳跃表(skiplist)、整数集合(intset)、压缩列表(ziplist) 等。
比如说 string 是通过 SDS 实现的,list 是通过链表实现的,hash 是通过字典实现的,set 是通过字典实现的,zset 是通过跳跃表实现的。
讲一下Sorted set的底层数据结构实现?
跳跃表(也称跳表)是有序集合 Zset 的底层实现之⼀。在 Redis 7.0 之前,如果有序集合的元素个数小于 128 个,并且每个元素的值小于 64 字节时,Redis 会使用压缩列表作为 Zset 的底层实现,否则会使用跳表;在 Redis 7.0 之后,压缩列表已经废弃,交由 listpack 来替代。
跳表由 zskiplist 和 zskiplistNode 组成,zskiplist ⽤于保存跳表的基本信息(表头、表尾、⻓度、层高等)。
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
zskiplistNode ⽤于表示跳表节点,每个跳表节点的层⾼是不固定的,每个节点都有⼀个指向保存了当前节点的分值和成员对象的指针。
typedef struct zskiplistNode {
sds ele;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned int span;
} level[];
} zskiplistNode;
什么是缓存穿透?如何解决?
缓存穿透是指查询不存在的数据,由于缓存没有命中(因为数据根本就不存在),请求每次都会穿过缓存去查询数据库。如果这种查询非常频繁,就会给数据库造成很大的压力。
缓存穿透意味着缓存失去了减轻数据压力的意义。缓存穿透可能有两种原因:
-
自身业务代码问题 -
恶意攻击,爬虫造成空命中
它主要有两种解决办法:
①、缓存空值/默认值
在数据库无法命中之后,把一个空对象或者默认值保存到缓存,之后再访问这个数据,就会从缓存中获取,这样就保护了数据库。
缓存空值有两大问题:
-
空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间(如果是攻击,问题更严重),比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除。 -
缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。
例如过期时间设置为 5 分钟,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和存储层数据的不一致。
这时候可以利用消息队列或者其它异步方式清理缓存中的空对象。
②、布隆过滤器
除了缓存空对象,我们还可以在存储和缓存之前,加一个布隆过滤器,做一层过滤。
布隆过滤器里会保存数据是否存在,如果判断数据不存在,就不会访问存储。
两种解决方案的对比:
什么是缓存击穿?如何解决?
缓存击穿是指某一个或少数几个数据被高频访问,当这些数据在缓存中过期的那一刻,大量请求就会直接到达数据库,导致数据库瞬间压力过大。
解决⽅案:
①、加锁更新,⽐如请求查询 A,发现缓存中没有,对 A 这个 key 加锁,同时去数据库查询数据,写⼊缓存,再返回给⽤户,这样后⾯的请求就可以从缓存中拿到数据了。
②、将过期时间组合写在 value 中,通过异步的⽅式不断的刷新过期时间,防⽌此类现象。
什么是缓存雪崩?如何解决?
缓存雪崩是指在某一个时间点,由于大量的缓存数据同时过期或缓存服务器突然宕机了,导致所有的请求都落到了数据库上(比如 MySQL),从而对数据库造成巨大压力,甚至导致数据库崩溃的现象。
总之就是,崩了,崩的非常严重,就叫雪崩了(电影电视里应该看到过,非常夸张)。
如何解决缓存雪崩呢?
第一种:提高缓存可用性
01、集群部署:采用分布式缓存而不是单一缓存服务器,可以降低单点故障的风险。即使某个缓存节点发生故障,其他节点仍然可以提供服务,从而避免对数据库的大量直接访问。
可以利用 Redis Cluster。
或者第三方集群方案 Codis。
02、备份缓存:对于关键数据,除了在主缓存中存储,还可以在备用缓存中保存一份。当主缓存不可用时,可以快速切换到备用缓存,确保系统的稳定性和可用性。
在技术派实战项目中,我们采用了多级缓存的策略,其中就包括使用本地缓存 Guava Cache 和 Caffeine 来作为二级缓存,在 Redis 出现问题时,系统会自动切换到本地缓存。
这个过程称为“降级”,意味着系统在失去优先级高的资源时仍能继续提供服务。
当从 Redis 获取数据失败时,尝试从本地缓存读取数据。
LoadingCache<String, UserPermissions> permissionsCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(this::loadPermissionsFromRedis);
public UserPermissions loadPermissionsFromRedis(String userId) {
try {
return redisClient.getPermissions(userId);
} catch (Exception ex) {
// Redis 异常处理,尝试从本地缓存获取
return permissionsCache.getIfPresent(userId);
}
}
第二种:过期时间
对于缓存数据,设置不同的过期时间,避免大量缓存数据同时过期。可以通过在原有过期时间的基础上添加一个随机值来实现,这样可以分散缓存过期时间,减少同一时间对数据库的访问压力。
第三种:限流和降级
通过设置合理的系统限流策略,如令牌桶或漏斗算法,来控制访问流量,防止在缓存失效时数据库被打垮。
此外,系统可以实现降级策略,在缓存雪崩或系统压力过大时,暂时关闭一些非核心服务,确保核心服务的正常运行。
什么是缓存预热?如何解决?
缓存预热是指在系统启动时,提前将一些预定义的数据加载到缓存中,以避免在系统运行初期由于缓存未命中(cache miss)导致的性能问题。
通过缓存预热,可以确保系统在上线后能够立即提供高效的服务,减少首次访问时的延迟。
缓存预热的方法有多种,在技术派实战项目中,我们采用了项目启动时自动加载和定时预热两种方式,比如说每天定时更新站点地图到 Redis 缓存中。
/**
* 采用定时器方案,每天5:15分刷新站点地图,确保数据的一致性
*/
@Scheduled(cron = "0 15 5 * * ?")
public void autoRefreshCache() {
log.info("开始刷新sitemap.xml的url地址,避免出现数据不一致问题!");
refreshSitemap();
log.info("刷新完成!");
}
@Override
public void refreshSitemap() {
initSiteMap();
}
private synchronized void initSiteMap() {
long lastId = 0L;
RedisClient.del(SITE_MAP_CACHE_KEY);
while (true) {
List<SimpleArticleDTO> list = articleDao.getBaseMapper().listArticlesOrderById(lastId, SCAN_SIZE);
// 刷新站点地图信息
Map<String, Long> map = list.stream().collect(Collectors.toMap(s -> String.valueOf(s.getId()), s -> s.getCreateTime().getTime(), (a, b) -> a));
RedisClient.hMSet(SITE_MAP_CACHE_KEY, map);
if (list.size() < SCAN_SIZE) {
break;
}
lastId = list.get(list.size() - 1).getId();
}
}
Redis如何实现分布式锁?
Redis 实现分布式锁的本质,就是在 Redis 里面占一个“茅坑”,当别的客户端也来占坑时,发现已经有客户端蹲在那里了,就只好放弃或者稍后再试。
可以使用 Redis 的 SET 命令实现分布式锁。SET 命令支持设置键值对的同时添加过期时间,这样可以防止死锁的发生。
SET key value NX PX 30000
-
key
是锁名。 -
value
是锁的持有者标识,可以使用 UUID 作为 value。 -
NX
只在键不存在时设置。 -
PX 30000
:设置键的过期时间为 30 秒(防止死锁)。
上面这段命令其实是 setnx 和 expire 组合在一起的原子命令,算是比较完善的一个分布式锁了。
当然,实际的开发中,没人会去自己写分布式锁的命令,因为有专业的轮子——Redisson。
什么是回表?
回表是指在数据库查询过程中,通过非聚簇索引(secondary index)查找到记录的主键值后,再根据这个主键值到聚簇索引(clustered index)中查找完整记录的过程。
回表操作通常发生在使用非聚簇索引进行查询,但查询的字段不全在该索引中,必须通过主键进行再次查询以获取完整数据。
换句话说,数据库需要先查找索引,然后再根据索引回到数据表中去查找实际的数据。
因此,使用非聚簇索引查找数据通常比使用聚簇索引要慢,因为需要进行两次磁盘访问。当然,如果索引所在的数据页已经被加载到内存中,那么非聚簇索引的查找速度也可以非常快。
例如:select * from user where name = '张三';
,会先从辅助索引中找到 name='张三' 的主键 ID,然后再根据主键 ID 从主键索引中找到对应的数据行。
回表记录越多好吗?(回表的代价)
回表记录越多并不是一件好事。事实上,回表的代价是很高的,尤其在记录较多时,回表操作会显著影响查询性能。
因为每次回表操作都需要进行一次磁盘 I/O 读取操作。如果回表记录很多,会导致大量的磁盘 I/O。
索引覆盖(Covering Index)可以减少回表操作,将查询的字段都放在索引中,这样不需要回表就可以获取到查询结果了。
性别字段要建立索引吗?为什么?
性别字段通常不适合建立索引。因为性别字段的选择性(区分度)较低,独立索引效果有限。
如果性别字段又很少用于查询,表的数据规模较小,那么建立索引反而会增加额外的存储空间和维护成本。
如果性别字段确实经常用于查询条件,数据规模也比较大,可以将性别字段作为复合索引的一部分,与选择性较高的字段一起加索引,会更好一些。
什么是区分度?
区分度(Selectivity)是衡量一个字段在数据库表中唯一值的比例,用来表示该字段在索引优化中的有效性。
区分度 = 字段的唯一值数量 / 字段的总记录数;接近 1,字段值大部分是唯一的。例如,用户的唯一 ID,一般都是主键索引。接近 0,则说明字段值重复度高。
例如,一个表中有 1000 条记录,其中性别字段只有两个值(男、女),那么性别字段的区分度只有 0.002。
高区分度的字段更适合拿来作为索引,因为索引可以更有效地缩小查询范围。
MySQL查看字段区分度的命令?
在 MySQL 中,可以通过 COUNT(DISTINCT column_name)
和 COUNT(*)
的比值来计算字段的区分度。例如:
SELECT
COUNT(DISTINCT gender) / COUNT(*) AS gender_selectivity
FROM
users;
MySQL主从复制流程和原理?
MySQL 的主从复制(Master-Slave Replication)是一种数据同步机制,用于将数据从一个主数据库(master)复制到一个或多个从数据库(slave)。
广泛用于数据备份、灾难恢复和数据分析等场景。
复制过程的主要步骤有:
-
在主服务器上,所有修改数据的语句(如 INSERT、UPDATE、DELETE)会被记录到二进制日志中。 -
主服务器上的一个线程(二进制日志转储线程)负责读取二进制日志的内容并发送给从服务器。 -
从服务器接收到二进制日志数据后,会将这些数据写入自己的中继日志(Relay Log)。中继日志是从服务器上的一个本地存储。 -
从服务器上有一个 SQL 线程会读取中继日志,并在本地数据库上执行,从而将更改应用到从数据库中,完成同步。
MySQL如何查看查询是否用到了索引?
可以通过 EXPLAIN
关键字来查看是否使用了索引。
EXPLAIN SELECT * FROM table WHERE column = 'value';
其结果中的 key
值显示了查询是否使用索引,如果使用了索引,会显示索引的名称。
type 列的最好,最好级别?都代表了什么意思?
从 EXPLAIN
输出结果来看,我们可以得到 MySQL 是如何执行查询的一些关键信息:
-
id: 查询标识符,这里是 1
。 -
select_type: 查询的类型,这里是 SIMPLE
,表示这是一个简单的查询,没有使用子查询或复杂的联合查询。 -
table: 正在查询的表名,这里是 tbn
。 -
type: 查询类型,这里是 range
,表示 MySQL 使用了范围查找。这是因为查询条件包含了>
操作符,使得 MySQL 需要在索引中查找满足范围条件的记录。 -
possible_keys: 可能被用来执行查询的索引,这里是 idx_abc
,表示 MySQL 认为idx_abc
索引可能会用于优化查询。 -
key: 实际用来执行查询的索引,也是 idx_abc
,这意味着 MySQL 实际上使用了idx_abc
联合索引来优化查询。 -
key_len: 使用索引的长度,这里是 15
字节,这提供了关于索引使用情况的一些信息,比如哪些列被用在了索引中。 -
ref: 显示哪些列或常量被用作索引查找的参考。 -
rows: MySQL 估计为了找到结果需要检查的行数,这里是 2
。 -
filtered: 表示根据表的条件过滤后,剩余多少百分比的结果,这里是 100.00
%,意味着所有扫描的行都会被返回。 -
Extra: 提供了关于查询执行的额外信息。 Using index condition
表示 MySQL 使用了索引条件推送(Index Condition Pushdown,ICP),这是 MySQL 的一个优化方式,它允许在索引层面过滤数据,减少访问表数据的需要。
计网的内容
由于全部内容都贴出来实在太多了,我就把计网的答案直接放到了《Java 面试指南》中,当然也可以通过面渣逆袭按图索骥。
内容来源
-
星球嘉宾三分恶的面渣逆袭:https://javabetter.cn/sidebar/sanfene/nixi.html -
二哥的 Java 进阶之路(GitHub 已有 12000+star):https://javabetter.cn
ending
一个人可以走得很快,但一群人才能走得更远。二哥的编程星球已经有 5300 多名球友加入了,如果你也需要一个良好的学习环境,戳链接 🔗 加入我们吧。这是一个编程学习指南 + Java 项目实战 + LeetCode 刷题的私密圈子,你可以阅读星球专栏、向二哥提问、帮你制定学习计划、和球友一起打卡成长。
两个置顶帖「球友必看」和「知识图谱」里已经沉淀了非常多优质的学习资源,相信能帮助你走的更快、更稳、更远。
欢迎点击左下角阅读原文了解二哥的编程星球,这可能是你学习求职路上最有含金量的一次点击。
最后,把二哥的座右铭送给大家:没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟。共勉 💪。