字节跳动也不过如此,狠心拒了。

共 11279字,需浏览 23分钟

 ·

2024-05-22 14:04

大家好,我是二哥呀。

字节跳动之所以被称为“宇宙厂”,就是因为字节的员工数量非常多,业务覆盖的范围非常广。再加上字节的薪资待遇给的非常可观,也就成为了很多同学的心仪之选,不只是校招还有社招哦(香,真香)。

23 届的一个球友去字节,直接开到了 SSP,让我印象非常深刻,所以每次提到字节我第一时间就想到了他。要知道,23 届号称是史上最难的一届哦,他还是双非本,年包直接给到了 50 万以上,非常夸张:开发一个小软件,月入2000

所以当我看到牛客上这位牛友直接拒绝了字节跳动的 offer,我是无所谓的,但我的一个朋友已经开始汗流浃背了,他有点破防了。。。。。。

截图来自牛客

不管网上的声音怎么说,我还是希望有能力的同学多冲一冲,尤其是字节这种愿意给钱多的厂,大不了干几年跑路嘛,这年头,在一家公司干一辈子估计也不太现实吧(😂)?

就像我昨天在 VIP 群里说的,怀揣希望,仰望星空,做一个爱自己、有思想、懂感恩,能实现自己人生抱负的人最好,做不到,也无所谓啊。

这次我们以《Java 面试指南-字节跳动面经》同学 1 的后端实习二面为例, 来看看字节的面试官都喜欢问哪些问题,好做到知彼知己百战不殆~

让天下所有的面渣都能逆袭 😁

题目不少,火箭造的飞起。主要围绕 MySQL、Redis 和计算机网络展开,所以大家在准备的时候一定要有的放矢,知道哪些是重点。

字节跳动面经(题目来自牛客)

对redis的数据结构是否熟悉?

Redis 的底层数据结构有动态字符串(sds)链表(list)字典(ht)跳跃表(skiplist)整数集合(intset)压缩列表(ziplist) 等。

三分恶面渣逆袭:Redis Object对应的映射

比如说 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;

什么是缓存穿透?如何解决?

缓存穿透是指查询不存在的数据,由于缓存没有命中(因为数据根本就不存在),请求每次都会穿过缓存去查询数据库。如果这种查询非常频繁,就会给数据库造成很大的压力。

三分恶面渣逆袭:缓存穿透

缓存穿透意味着缓存失去了减轻数据压力的意义。缓存穿透可能有两种原因:

  1. 自身业务代码问题
  2. 恶意攻击,爬虫造成空命中

它主要有两种解决办法:

①、缓存空值/默认值

在数据库无法命中之后,把一个空对象或者默认值保存到缓存,之后再访问这个数据,就会从缓存中获取,这样就保护了数据库。

三分恶面渣逆袭:缓存空值/默认值

缓存空值有两大问题:

  1. 空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间(如果是攻击,问题更严重),比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除。
  2. 缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。

例如过期时间设置为 5 分钟,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和存储层数据的不一致。

这时候可以利用消息队列或者其它异步方式清理缓存中的空对象。

②、布隆过滤器

除了缓存空对象,我们还可以在存储和缓存之前,加一个布隆过滤器,做一层过滤。

布隆过滤器里会保存数据是否存在,如果判断数据不存在,就不会访问存储。

三分恶面渣逆袭:布隆过滤器

两种解决方案的对比:

三分恶面渣逆袭:缓存空对象和布隆过滤器方案

什么是缓存击穿?如何解决?

缓存击穿是指某一个或少数几个数据被高频访问,当这些数据在缓存中过期的那一刻,大量请求就会直接到达数据库,导致数据库瞬间压力过大。

三分恶面渣逆袭:缓存击穿

解决⽅案:

①、加锁更新,⽐如请求查询 A,发现缓存中没有,对 A 这个 key 加锁,同时去数据库查询数据,写⼊缓存,再返回给⽤户,这样后⾯的请求就可以从缓存中拿到数据了。

三分恶面渣逆袭:加锁更新

②、将过期时间组合写在 value 中,通过异步的⽅式不断的刷新过期时间,防⽌此类现象。

什么是缓存雪崩?如何解决?

缓存雪崩是指在某一个时间点,由于大量的缓存数据同时过期或缓存服务器突然宕机了,导致所有的请求都落到了数据库上(比如 MySQL),从而对数据库造成巨大压力,甚至导致数据库崩溃的现象。

总之就是,崩了,崩的非常严重,就叫雪崩了(电影电视里应该看到过,非常夸张)。

三分恶面渣逆袭:缓存雪崩

如何解决缓存雪崩呢?

第一种:提高缓存可用性

01、集群部署:采用分布式缓存而不是单一缓存服务器,可以降低单点故障的风险。即使某个缓存节点发生故障,其他节点仍然可以提供服务,从而避免对数据库的大量直接访问。

可以利用 Redis Cluster。

Rajat Pachauri:Redis Cluster

或者第三方集群方案 Codis。

极客时间: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原子命令
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 从主键索引中找到对应的数据行。

三分恶面渣逆袭:InnoDB 回表

回表记录越多好吗?(回表的代价)

回表记录越多并不是一件好事。事实上,回表的代价是很高的,尤其在记录较多时,回表操作会显著影响查询性能。

因为每次回表操作都需要进行一次磁盘 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 列的最好,最好级别?都代表了什么意思?

二哥的 Java 进阶之路

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 刷题的私密圈子,你可以阅读星球专栏、向二哥提问、帮你制定学习计划、和球友一起打卡成长。

两个置顶帖「球友必看」和「知识图谱」里已经沉淀了非常多优质的学习资源,相信能帮助你走的更快、更稳、更远

欢迎点击左下角阅读原文了解二哥的编程星球,这可能是你学习求职路上最有含金量的一次点击。

最后,把二哥的座右铭送给大家:没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟。共勉 💪。

浏览 5786
5点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
5点赞
评论
收藏
分享

手机扫一扫分享

分享
举报