字节飞书后端一面不知道凉不凉经|讲解

挑选了一篇牛客上的面经给大家做讲解分析和学习指引,期望对大家有所帮助~ 本文也是《热门面经讲解》系列文章之一,大家可以持续关注

原帖链接 alt

本文主要涉及大量数据库mysql的考察,也比较有参考意义。所以结合这些题目,给大家指引了系统且高效的学习资料

下面开始~

1. 实现一个cache,包括LRU算法和在x秒后过期

解析:

LRU 属于常考题, 大家需要重点掌握;Java 可以用 LinkedHashMap + ScheduledExecutorService实现

参考回答:

import java.util.LinkedHashMap;  
import java.util.Map;  
import java.util.concurrent.ScheduledExecutorService;  
import java.util.concurrent.Executors;  
import java.util.concurrent.TimeUnit;  
  
public class LRUCacheWithExpiration<K, V> {  
    // 缓存的最大容量  
    private final int capacity;  
    // 缓存项的过期时间(秒)  
    private final long expirationTimeInSeconds;  
    // 使用LinkedHashMap实现LRU缓存,accessOrder设置为true以启用LRU顺序  
    private final LinkedHashMap<K, CacheEntry<V>> cacheMap;  
    // 定时任务执行器,用于清理过期的缓存项  
    private final ScheduledExecutorService expirationExecutor;  
  
    // 内部类CacheEntry,用于存储缓存值和过期时间  
    private static class CacheEntry<V> {  
        V value;  
        long expirationTime;  
  
        CacheEntry(V value, long expirationTime) {  
            this.value = value;  
            this.expirationTime = expirationTime;  
        }  
    }  
  
    // 构造函数  
    public LRUCacheWithExpiration(int capacity, long expirationTimeInSeconds) {  
        this.capacity = capacity;  
        this.expirationTimeInSeconds = expirationTimeInSeconds;  
        // 初始化LRU缓存,设置accessOrder为true,以便按访问顺序进行LRU操作  
        this.cacheMap = new LinkedHashMap<>(capacity, 0.75f, true) {  
            // 重写removeEldestEntry方法,当Map大小超过指定容量时,删除最老的元素  
            @Override  
            protected boolean removeEldestEntry(Map.Entry<K, CacheEntry<V>> eldest) {  
                return size() > LRUCacheWithExpiration.this.capacity;  
            }  
        };  
        // 初始化定时任务执行器  
        this.expirationExecutor = Executors.newSingleThreadScheduledExecutor();  
        // 安排定时任务,每隔expirationTimeInSeconds秒执行一次清理过期缓存项的操作  
        this.expirationExecutor.scheduleAtFixedRate(this::evictExpiredEntries, expirationTimeInSeconds, expirationTimeInSeconds, TimeUnit.SECONDS);  
    }  
  
    // 获取缓存项  
    public synchronized V get(K key) {  
        CacheEntry<V> entry = cacheMap.get(key);  
        if (entry == null || isExpired(entry.expirationTime)) {  
            // 缓存项不存在或已过期,返回null  
            return null;  
        }  
        // 更新缓存项的访问顺序(移到尾部,表示最近访问)  
        cacheMap.remove(key);  
        cacheMap.put(key, entry);  
        // 返回缓存值  
        return entry.value;  
    }  
  
    // 添加或更新缓存项  
    public synchronized void put(K key, V value) {  
        // 创建新的缓存项,设置过期时间  
        CacheEntry<V> newEntry = new CacheEntry<>(value, System.currentTimeMillis() + (expirationTimeInSeconds * 1000));  
        // 将新的缓存项添加到LRU缓存中,如果缓存已满,则会自动删除最老的元素  
        cacheMap.put(key, newEntry);  
    }  
  
    // 清理过期缓存项的方法  
    private void evictExpiredEntries() {  
        long currentTime = System.currentTimeMillis();  
        // 遍历缓存,移除已过期的缓存项  
        cacheMap.entrySet().removeIf(entry -> isExpired(entry.getValue().expirationTime));  
    }  
  
    // 判断缓存项是否过期  
    private boolean isExpired(long expirationTime) {  
        return expirationTime <= System.currentTimeMillis();  
    }  
  
    // 关闭缓存,停止定时任务执行器  
    public void close() {  
        expirationExecutor.shutdown();  
        try {  
            if (!expirationExecutor.awaitTermination(60, TimeUnit.SECONDS)) {  
                expirationExecutor.shutdownNow();  
            }  
        } catch (InterruptedException e) {  
            expirationExecutor.shutdownNow();  
            Thread.currentThread().interrupt();  
        }  
    }  
}

学习指引: 牛客题目:BM100 设计LRU缓存结构

2. MySQL事务:ACID四个特性?原子性是如何保证的?隔离性是怎么保证的?四个隔离级别?

解析: mysql事务,简单且重要,校招考察频率高,必须掌握。

参考回答:

MySQL事务的ACID四个特性:

1.原子性(Atomicity):事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。

2.一致性(Consistency):事务必须使数据库从一个一致性状态变换到另一个一致性状态。

3.隔离性(Isolation):通常,一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。

4.持久性(Durability):一旦事务提交,则其结果永久保存在数据库中。

原子性是如何保证的?

原子性是通过日志(如MySQL的redo log和undo log)和锁机制来保证的。当事务开始时,MySQL会先记录redo log,表示事务执行过程中的修改。如果事务成功,这些修改会被永久写入数据文件。如果事务失败,MySQL会使用undo log来回滚事务,恢复到事务开始前的状态,从而保证原子性。

隔离性是怎么保证的?

隔离性是通过数据库的多版本并发控制(MVCC)锁机制 来实现的。MVCC允许读取者在不阻塞写入者的情况下读取数据,并通过保存数据的多个版本来确保事务之间的隔离。锁机制则用于在必要时阻止其他事务访问特定数据,直到当前事务完成。

四个隔离级别:

1.读未提交(Read Uncommitted):事务可以读取尚未提交的其他事务的数据。这是隔离级别最低的一种,可能会导致很多问题,如脏读、不可重复读和幻读。

2.读已提交(Read Committed):一个事务只能读取已经提交的事务的数据。这可以避免脏读,但可能会导致不可重复读和幻读。

3.可重复读(Repeatable Read):这是MySQL的默认隔离级别。在这个级别,事务在开始时取得的快照数据在事务内是保持一致的,即使其他事务修改了数据。这可以避免脏读和不可重复读,但在某些情况下仍可能导致幻读。但在InnoDB存储引擎中,由于使用了MVCC,可重复读级别实际上可以防止幻读。

4.串行化(Serializable):这是隔离级别最高的一种。事务串行化执行,事务之间不可能产生干扰。这可以避免脏读、不可重复读和幻读,但会大大降低并发性能。

学习指引

小林 coding|图解Mysql|事务篇

3. 为什么读已提交会的时候其他事务读取不到修改了的数据?

解析:

考察“读已提交”这种隔离级别下是如何工作的,常考面试题,大家需要理解原理之后用自己的话来描述。同时还有“可重复读”隔离级别下是如何工作的也要重点掌握。

参考回答:

读已提交(Read Committed)隔离级别下,其他事务读取不到当前事务修改的数据,是因为在当前事务提交之前,其对数据的修改对其他事务是不可见的。InnoDB通过多版本并发控制(MVCC)机制来实现这种隔离性。在每个事务开始时,InnoDB会为其创建一个唯一的事务ID,并使用这个ID来标记事务中所有读取和修改的数据行。当其他事务尝试读取这些数据行时,InnoDB会根据其事务ID和行上的标记来判断这些行是否对当前事务可见。如果数据行的修改是在当前事务开始之后提交的,那么这些修改对其他事务是不可见的,因为它们属于不同的版本。这样,即使在读已提交隔离级别下,InnoDB也能保证事务之间的隔离性,避免脏读问题。

学习指引

Mysql事务:可重复读是如何工作的?

Mysql事务:读提交是如何工作的?

4. 索引的数据结构,为什么查询快?

解析: 考察mysql InnoDB引擎的数据结构B+树,需要回答出来为什么要选用B+树,而不是B树,也不是二叉树。 为什么B+树比他们合适,比他们块。

参考回答:

MySQL 默认的存储引擎 InnoDB 采用的是 B+ 作为索引的数据结构,原因有:

1.B+ 树的非叶子节点不存放实际的记录数据,仅存放索引,因此数据量相同的情况下,相比存储即存索引又存记录的 B 树,B+树的非叶子节点可以存放更多的索引,因此 B+ 树可以比 B 树更「矮胖」,查询底层节点的磁盘 I/O次数会更少。(为什么不选二叉树也有类似原因)

2.B+ 树有大量的冗余节点(所有非叶子节点都是冗余索引),这些冗余索引让 B+ 树在插入、删除的效率都更高,比如删除根节点的时候,不会像 B 树那样会发生复杂的树的变化;

3.B+ 树叶子节点之间用链表连接了起来,有利于范围查询,而 B 树要实现范围查询,因此只能通过树的遍历来完成范围查询,这会涉及多个节点的磁盘 I/O 操作,范围查询效率不如 B+ 树。

学习指引: 系统学习 小林 coding|为什么 MySQL 采用 B+ 树作为索引?

5.聚族索引和非聚族索引有什么区别,是不是聚族索引更好?

解析: 属于mysql中的两个重要概念,必须掌握。另外在innodb中,非聚族索引也指二级索引。

参考回答:

聚簇索引(Clustered Index)和非聚簇索引(Non-Clustered Index)是数据库中两种常见的索引类型,它们在数据结构、数据存储方式和性能上有显著的区别。

聚簇索引

1.数据存储:聚簇索引决定了表中数据的物理存储顺序。换句话说,聚簇索引中的键值与数据行在磁盘上是存放在一起的。因此,一个表只能有一个聚簇索引。

2.性能:由于聚簇索引的键值和数据行存储在一起,所以范围查询非常快。当查询连续的数据时,磁盘I/O操作可以最小化。

3.主键:在InnoDB存储引擎中,如果表定义了主键,InnoDB会自动为主键创建聚簇索引,除非明确指定其他列。

非聚簇索引:

1.数据存储:非聚簇索引与数据的物理存储顺序无关。它只包含键值和指向数据行的指针。这意味着非聚簇索引和表数据是分开存储的。

2.性能:非聚簇索引需要额外的磁盘空间来存储索引结构,并且在执行查询时可能需要更多的磁盘I/O操作,因为索引和数据可能分布在不同的磁盘位置。

3.多个索引:一个表可以有多个非聚簇索引,因为非聚簇索引不决定数据的物理存储顺序。

聚簇索引和非聚簇索引的比较

查询性能:聚簇索引通常对范围查询更为高效,因为它将数据按照索引键值的顺序存储。然而,非聚簇索引可能在某些情况下提供更好的性能,特别是当索引覆盖查询所需的所有列时(即覆盖索引)。

更新开销:当数据行的键值(即聚簇索引的键值)发生变化时,聚簇索引可能需要重新组织数据的存储顺序,这可能会导致更大的更新开销。而非聚簇索引只需要更新索引条目。

学习指引 系统学习 Mysql:索引的分类

6.出现慢查询要怎么优化?

解析: 面试重点:索引优化。可以先讲一些策略明,不深入细节,面试官细问了再详细说。

参考回答:

常见优化索引的方法: 1.前缀索引优化; 2.覆盖索引优化; 3.主键索引最好是自增的; 4.索引最好设置为 NOT NULL 5.防止索引失效;

学习指引:

如果不清楚每一种优化方法原理,看这篇就清楚了

有什么优化索引的方法?

7. a b c三个字段怎么设计他们的索引顺序?

解析: 这种题目面试官一般会给出一条sql查询的条件语句,然后让你据此来分析联合索引应该怎么建立,你在回答的时候除了要答出具体的顺序,还要讲出每个字段的作用。 例如"where a>50 and b=20 and c=20 order by d 怎么建立联合索引?" 例如: 参考回答:

索引顺序(bcda)。因为这时候 b 和 c 字段都能走索引,而且 d 能利用索引有序性,避免 filesort,最后的 a 字段虽然无法走索引,但是可以利用索引下推,减少回表的次数。

学习指引 一文搞懂 MySQL索引应用篇:建立索引的正确姿势与使用索引的最佳指南!

8. 数据库中的锁

解析: 数据库中的锁也是面试重点。看一篇资料就够了

参考回答:

基于锁的属性分类: 1.共享锁(Share Lock,S锁):当一个事务为数据加上读锁之后,其他事务只能对该数据加读锁,而不能对数据加写锁,直到所有的读锁释放之后其他事务才能对其进行加持写锁。共享锁主要用于支持并发的读取数据,读取数据的时候不支持修改,避免出现重复读的问题。 2.排他锁(Exclusive Lock,X锁):当一个事务为数据加上写锁时,其他请求将不能再为数据加任何锁,直到该锁释放之后,其他事务才能对数据进行加锁。排他锁避免了出现脏数据和脏读的问题。

基于锁的粒度分类: 1.行级锁(InnoDB):对数据库表中的某一行或多行记录进行加锁,使得其他事务在当前事务释放锁之前无法修改这些记录。 2.表级锁(InnoDB、MyISAM):对整个数据库表进行加锁,使得其他事务在当前事务释放锁之前无法对该表进行访问或修改。表级锁包括表共享读锁和表独占写锁。 3.页级锁(InnoDB、BDB引擎):是一种介于行级锁和表级锁之间的锁,它锁定的是数据页而不是整个表或单个行。页级锁在某些情况下可以减少锁的竞争,提高并发性能。 4.记录锁:行级锁的一种,单条记录上加的锁。 5.间隙锁(InnoDB):在索引记录之间的间隙上加的锁,主要用于防止两个事务同时向一个范围内插入新的记录,产生幻读的情况。 6.临键锁(Next-Key Locks):是InnoDB的行级锁和间隙锁的结合,即除了锁住记录本身,还要再锁住索引记录前面的间隙。

基于锁的状态分类: 1.意向共享锁(Intention Shared Lock):事务有意向对表中的某些行加共享锁。 2.意向排他锁(Intention Exclusive Lock):事务有意向对表中的某些行加排他锁。

全局锁: 对整个数据库进行加锁,阻止其他用户对该数据库的访问和修改。例如,当需要让整个数据库进行备份时,可以使用全局锁。但全局锁会严重影响并发性能,应谨慎使用。

学习指引 小林coding 一文搞懂。 MySQL 有哪些锁?

9. 缓存穿透、缓存雪崩说一下,缓存击穿要怎么处理

解析: 缓存本省是一个优化利器,也是面试当中的重点。引入缓存之后,缓存本身也会带来一些问题,缓存穿透、缓存雪崩就是需要解决的问题。

参考回答:

缓存穿透:指查询一个一定不存在的数据,由于缓存不命中时需要从数据库查询,而数据库中也无此数据,因此无法写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,给数据库带来压力。对于恶意利用此漏洞的攻击,甚至可能压垮数据库。其解决方案包括但不限于:

  1. 对查询的key进行合法性校验,比如长度、格式等,对不合法的key直接拒绝服务。
  2. 对不存在的key也进行缓存,设置一个较短的过期时间即可。
  3. 使用布隆过滤器,将所有可能存在的key预先加载到过滤器中,在查询前先进行过滤,如果不存在则直接返回,避免对数据库的查询。

缓存雪崩:当缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至宕机。解决方案包括:

  1. 给缓存的失效时间加上一个随机值,避免集体失效。
  2. 使用互斥锁,当缓存失效的时候,不是立即去load db,而是先使用缓存工具的某个机制,比如Redis的SETNX去设置一个锁,当操作返回成功时,再去load db并放入缓存;否则,就重试获取缓存值。
  3. 采用双缓存策略,即设置两级缓存,原始缓存和拷贝缓存,原始缓存失效时,可以访问拷贝缓存,原始缓存与拷贝缓存,定期同步数据,保证一致。

缓存击穿:是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。其解决方案包括:

  1. 设置热点数据永远不过期。
  2. 使用互斥锁,当缓存失效的时候,不是立即去load db,而是通过缓存工具的某个机制(如Redis的SETNX)去设置一个锁,当操作返回成功时,再去load db并放入缓存;否则,就重试获取缓存值。

学习指引 系统学习“缓存篇”: 什么是缓存雪崩、缓存击穿?

10. 为什么Redis速度比较快

解析: redis高频面试题之一

参考回答:

Redis速度快的原因主要有以下几点:首先,Redis是一个内存数据库,所有的数据都存储在内存中,这大大减少了磁盘I/O的延迟,使得数据读写速度非常快;其次,Redis使用单线程模型,避免了线程切换和锁竞争等开销,能够更高效地利用CPU资源;此外,Redis提供了多种高效的数据结构,如字符串、哈希表、列表、集合等,这些数据结构都经过了高度优化,能够实现快速的读写操作;最后,Redis还使用了异步I/O模型,能够同时处理多个并发请求,提高了系统的吞吐量。这些因素共同作用,使得Redis在处理大量读写请求时表现出色,成为了一个高性能的键值数据库。

学习指引 系统学习Redis Redis为什么快?

往期相关文章推荐阅读

面经讲解系列:

某跨境电商独角兽Java实习面经|讲解

阿里云 实习面经(已OC) 一面面经|讲解

师兄经验分享:

一篇真正的面经:你真的准备好实习“面试”了吗?

文中学习指引提到的很多经典书籍(Java,JVM,数据库,Redis等等),我都有电子版,需要的同学可以私信我。其他 学习指引/简历修改相关问题也可以咨询我,抽空安排!

#面试##面经##实习##春招##我的实习求职记录#
热门面经讲解 文章被收录于专栏

挑选近期热门真实后端面经进行讲解分析,给出:个人分析+参考回答+学习资料指引。

全部评论

相关推荐

17 81 评论
分享
牛客网
牛客企业服务