挑选了一篇牛客上的面经给大家做讲解分析和学习指引,期望对大家有所帮助~本文也是《热门面经讲解》系列文章之一,大家可以持续关注原帖链接本文主要涉及大量数据库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 NULL5.防止索引失效;学习指引:如果不清楚每一种优化方法原理,看这篇就清楚了有什么优化索引的方法?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. 缓存穿透、缓存雪崩说一下,缓存击穿要怎么处理解析:缓存本省是一个优化利器,也是面试当中的重点。引入缓存之后,缓存本身也会带来一些问题,缓存穿透、缓存雪崩就是需要解决的问题。参考回答:缓存穿透:指查询一个一定不存在的数据,由于缓存不命中时需要从数据库查询,而数据库中也无此数据,因此无法写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,给数据库带来压力。对于恶意利用此漏洞的攻击,甚至可能压垮数据库。其解决方案包括但不限于:对查询的key进行合法性校验,比如长度、格式等,对不合法的key直接拒绝服务。对不存在的key也进行缓存,设置一个较短的过期时间即可。使用布隆过滤器,将所有可能存在的key预先加载到过滤器中,在查询前先进行过滤,如果不存在则直接返回,避免对数据库的查询。缓存雪崩:当缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至宕机。解决方案包括:给缓存的失效时间加上一个随机值,避免集体失效。使用互斥锁,当缓存失效的时候,不是立即去load db,而是先使用缓存工具的某个机制,比如Redis的SETNX去设置一个锁,当操作返回成功时,再去load db并放入缓存;否则,就重试获取缓存值。采用双缓存策略,即设置两级缓存,原始缓存和拷贝缓存,原始缓存失效时,可以访问拷贝缓存,原始缓存与拷贝缓存,定期同步数据,保证一致。缓存击穿:是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。其解决方案包括:设置热点数据永远不过期。使用互斥锁,当缓存失效的时候,不是立即去load db,而是通过缓存工具的某个机制(如Redis的SETNX)去设置一个锁,当操作返回成功时,再去load db并放入缓存;否则,就重试获取缓存值。学习指引系统学习“缓存篇”:什么是缓存雪崩、缓存击穿?10. 为什么Redis速度比较快解析:redis高频面试题之一参考回答:Redis速度快的原因主要有以下几点:首先,Redis是一个内存数据库,所有的数据都存储在内存中,这大大减少了磁盘I/O的延迟,使得数据读写速度非常快;其次,Redis使用单线程模型,避免了线程切换和锁竞争等开销,能够更高效地利用CPU资源;此外,Redis提供了多种高效的数据结构,如字符串、哈希表、列表、集合等,这些数据结构都经过了高度优化,能够实现快速的读写操作;最后,Redis还使用了异步I/O模型,能够同时处理多个并发请求,提高了系统的吞吐量。这些因素共同作用,使得Redis在处理大量读写请求时表现出色,成为了一个高性能的键值数据库。学习指引系统学习RedisRedis为什么快?往期相关文章推荐阅读面经讲解系列:某跨境电商独角兽Java实习面经|讲解阿里云 实习面经(已OC) 一面面经|讲解师兄经验分享:一篇真正的面经:你真的准备好实习“面试”了吗?文中学习指引提到的很多经典书籍(Java,JVM,数据库,Redis等等),我都有电子版,需要的同学可以私信我。其他 学习指引/简历修改相关问题也可以咨询我,抽空安排!
点赞 18
评论 0
全部评论

相关推荐

点赞 收藏 评论
分享
牛客网
牛客企业服务