java八股面试(2)

1.设计一个URL短路链接系统

URL 短链系统的核心是将"长链接"映射为"唯一 ID",再将 ID 转换为"62 进制字符串"作为短码。在读取时,通过短码查库获取长链接,利用 302 重定向跳转。面对百亿级数据,我们需要采用"分库分表 + NoSQL"存储方案,并利用 Redis 缓存热点数据。

一、 核心架构设计逻辑

我们可以把整个系统拆解为两个核心动作:"生成"和"访问"。

整体流程
  • 生成短链: 用户发送长 URL -> 只有当该 URL 第一次出现时(去重),系统生成全局唯一 ID -> 转换为 62 进制短码 -> 存入数据库(KV 结构:短码 -> 长 URL) -> 返回短链接。

  • 访问跳转: 用户访问短链接 -> 负载均衡 -> 缓存层(Redis)查找 -> 没命中则查数据库 -> 获取长 URL -> 返回 HTTP 302 重定向。

二、 针对追问的深度解析

追问一:如何将长 URL 转换为短码?(Hash 算法 vs 发号器)
  • **方案 A:Hash 算法(如 ****MD5 / SHA)**

    • 做法: 对长 URL 进行 MD5 运算,得到 32 位 16 进制字符串。

    • 问题: 结果太长了,我们通常只需要 6-7 位。如果截取前 6 位,会有哈希冲突的风险。虽然概率低,但处理冲突会增加系统复杂度。

    • 结论: 不太推荐,除非对短码长度不敏感。

  • 方案 B:分布式 ID 发号器 + Base62 编码(推荐)

    • 原理: 维护一个全局唯一的自增 ID(比如 1, 2, 3... 10000)。

    • 转换: 将这个 10 进制的 ID 转换为 62 进制(0-9, a-z, A-Z 共 62 个字符)。

    • 为什么是 62 进制? 因为 URL 安全的字符正好是这 62 个。

    • 容量计算:

      • 6 位 Base62: 62\^6 \\approx 568 亿。足够覆盖百亿级数据。

      • 7 位 Base62: 62\^7 \\approx 3.5万亿。

    • ID 怎么生成? 为了高性能,不能依赖 MySQL 的自增主键。通常使用 Snowflake 雪花算法 或者 Redis 的 **INCR**** 预取号段模式**。

追问二:301 跳转与 302 跳转的区别?对数据分析有什么影响?
  • 301:永久重定向。

    • 机制: 浏览器第一次拿到跳转地址后,会把映射关系缓存在本地。下次用户再访问短链,浏览器直接跳长链,不再请求短链服务器。

    • 后果: 服务器负载低,但我们失去了统计数据的能力。无法知道用户点了多少次,无法做来源分析,这对于短链服务商是致命的。

  • 302 :临时重定向。

    • 机制: 告诉浏览器"你这次先跳这里,但下次还得来问我"。

    • 后果: 浏览器每次都会请求短链服务器。虽然增加了服务器压力,但我们能精准捕获每一次点击,从而实现数据分析(UV/PV、地理位置、设备统计)。

追问三:如果数据量达到百亿级,如何存储?如何处理哈希冲突?
  • 存储选型:

    • 这种场景是典型的 K-V 结构(ShortKey -> LongURL)。

    • 关系型数据库(MySQL)虽然稳定,但对于海量 K-V 读写略显笨重。

    • 更优解: 使用 NoSQL,如 HBase 或者 MongoDB 。如果团队技术栈受限必须用 MySQL,则必须做分库分表。

  • 哈希冲突的处理(如果必须用 Hash 算法):

    • 虽然我推荐 ID 生成器方案(天然无冲突),但如果必须用 Hash:

    • 方案: 拿长 URL 做 Hash -> 截取前 6 位 -> 去数据库查询是否存在(或用布隆过滤器预判)。

    • 如果冲突(存在但长 URL 不一样): 在原长 URL 后面加一个固定的"盐"(如 [DUPLICATE]),重新 Hash,直到不冲突为止。

  • 性能优化(缓存策略):

    • 短链系统是极端的读多写少。

    • 必须引入 Redis。将热门的短码存入 Redis。

    • 利用 LRU 淘汰策略,保证内存中留存的都是热点链接。

2.什么是Lucene

一、 知识点讲解

Lucene 不是一个像 MySQL 那样可以直接启动的"软件"或"服务",它本质上是一个 Java 类库。

你可以把它理解为搜索引擎界的"乐高积木"或者"发动机"。如果你想在自己的 Java 代码里实现全文检索功能(比如给一个单机版的 APP 做个搜索框),你直接引入 Lucene 的包,调用它的 API 就能实现建索引和搜索。

Elasticsearch 其实就是把 Lucene 这个"发动机"装进了车壳子里,配上了 HTTP 接口、分布式管理、高可用架构,变成了开箱即用的"汽车"。

倒排索引

这是 Lucene 最核心的数据结构。

复制代码
- **正向索引**:类似书的章节内容,你要找某个词,得从头翻到尾(遍历),速度慢。
- **倒排索引**:类似书后的“术语索引表”。它记录了“某个词出现在哪几页”。
1.   在 Lucene 中,它维护了一个词典,记录了所有切分出来的词,以及这些词对应的文档 ID 列表。当你搜“手机”时,它直接在词典里找到这个词,立刻就能把所有包含该词的文档 ID 给你,速度极快。
段与不可变性

Lucene 里的索引并不是一整块巨大的文件,而是由很多个段组成的。

  1. 不可变性:一旦一个段被写在这个磁盘上,它就不能被修改了。这样做的好处是不需要锁,读写性能极高。

  2. 删除与更新:因为不能修改,所以"删除"其实只是在一个 .del 文件里打个标记说"这行数据废了";"更新"就是"先标记删除,再新增一条"。

  3. 合并:为了防止段文件越来越多,Lucene 会在后台偷偷把几个小段合并成一个大段,并顺便把标记删除的数据真正清理掉。


二、 面试标准回答(实战篇)

Lucene 本质上是一个高性能、全功能的文本搜索引擎类库。

它不是一个可以独立运行的服务,而是一个工具包。我们可以通过调用它的 API 来实现数据的索引和检索。

关于 Lucene 的内部原理,三个点最关键:

  1. 倒排索引:这是它性能强大的根基。Lucene 通过维护'词项'到'文档 ID 列表'的映射,实现了毫秒级的关键词检索,避免了全表扫描。

  2. 段式存储 :Lucene 的索引是由多个 Segment 组成的。每个 Segment 一旦生成就是不可变 的。这种设计极大地简化了并发控制,不需要复杂的锁机制。数据的删除和更新是通过'标记删除'文件和'追加新文档'来实现的,系统会定期进行 Merge 操作来合并段并回收空间。

3.什么是DDD

DDD 是一种软件开发的方法论。它的核心思想是"先业务,后技术"。它让我们把复杂的业务拆解成一个个独立的"限界上下文",然后在代码层面通过"充血模型"让代码真实地反映业务逻辑,而不是简单的 CRUD。

一、 核心体系深度解析

战略设计

这是 DDD 最重要、也是最难的部分,主要解决"业务边界"问题。

  • 限界上下文:

    • 比如电商系统,我们拆分为"订单上下文"、"库存上下文"、"支付上下文"。

    • 关键点: 同一个词(比如"商品"),在订单上下文里关注的是价格,在物流上下文里关注的是重量。只有划清界限,概念才不会混淆。

  • 通用语言

    • 产品经理、开发、测试必须说一样的话。不能产品经理说"修改收货地址",开发说"Update User Table"。代码里的类名、方法名必须和业务术语完全一致。
战术设计

这是代码落地的层面,主要解决"代码实现"问题。

  • 实体 (Entity): 有唯一 ID 的对象(如 User, Order)。

  • 值对象 (Value Object): 没有 ID,由属性决定身份的对象(如 Address, Color)。两个地址如果省市区都一样,那就是同一个地址。

  • 聚合 (Aggregate) & 聚合根 (Aggregate Root):

    • 这是为了保证数据一致性。比如"订单"和"订单项"。你不能直接单独修改"订单项",必须通过"订单(聚合根)"来操作。订单是管事的。
  • 领域服务 (Domain Service): 当一个动作不属于任何一个对象时(比如"转账"涉及两个账户),就放在领域服务里。

核心价值 ------ 拒绝"贫血"
  • 传统 CRUD(贫血模型): Service 层写满了 if-else 逻辑,Entity 只是个只有 Getter/Setter 的数据容器。

  • DDD(充血模型): 业务逻辑(比如"判断库存够不够")直接写在 Entity/Aggregate 内部。对象是"活"的,自己管好自己的数据。

二、 面试标准回答

它的核心目的是为了解决复杂业务逻辑与技术实现之间的脱节问题。

具体来说,我可以从"战略设计"和"战术设计"两个层面来解释:

第一,在战略设计层面:

它的重点是"划分边界"和"统一语言"。

  • 我们通过定义 限界上下文,把一个庞大的单体系统拆解成若干个独立的业务领域(比如订单域、用户域)。这其实也是微服务拆分的理论基础。

  • 同时,它强调 通用语言,要求产品、业务和技术人员使用同一套术语,并且这套术语要直接反映在代码的类名和方法名中,降低沟通成本。

第二,在战术设计层面:

它的重点是"充血模型"。

  • 我们不再把业务逻辑全堆在 Service 层(贫血模型),而是回归面向对象的本质。

  • 我们利用 聚合 和 聚合根 来保证业务数据的一致性边界。

  • 我们区分 实体 和 值对象来精细化建模。

4.什么是Mybatis和ORM框架

一、 核心概念深度解析

我们可以把数据库操作想象成"翻译工作":Java 讲的是"对象语言",数据库讲的是"表格语言"。

什么是 ORM?
复制代码
- **痛点:** 在没有 ORM 之前(JDBC 时代),我们需要手动建立连接、手写 SQL、手动把数据库查出来的 `ResultSet` 结果集一行行赋值给 Java 对象。代码又臭又长,全是体力活。
- **作用:** ORM 就是一个“自动翻译机”。
    * 它把 Java 类映射成数据库表。
    * 它把 Java 对象映射成表里的一行记录。
    * **结果:** 程序员只需要操作 Java 对象,ORM 框架在底层自动帮你生成 SQL 并操作数据库。
什么是 MyBatis?
  • 定义: MyBatis 是一个优秀的持久层框架。

  • 本质: 它是一个**半自动化的 ****ORM 框架**(这是它和 Hibernate 最大的区别)。

  • 工作方式:

    • 它不像 Hibernate 那样全自动生成 SQL(黑盒)。

    • 它把 SQL 语句"剥离"出来,写在独立的 XML 配置文件 或者注解里。

    • 它负责把 Java 方法的参数自动映射到 SQL 参数上,再把 SQL 执行的结果自动映射回 Java 对象。

  • 核心优势:**SQL 可控**。你想怎么写就怎么写,想怎么优化就怎么优化,非常适合互联网高并发、业务复杂的场景。

二、 面试标准回答

首先,讲一下 ORM:

它的全称是 Object Relational Mapping。

在 Java 开发中,我们操作的是对象,而数据库存储的是关系型数据(表),ORM 的作用就是充当一座桥梁,建立起 Java 对象和数据库表之间的映射关系。

它的核心价值在于解放生产力:让我们不再需要手写繁琐的 JDBC 代码,而是通过操作对象就能间接操作数据库。

其次,关于 MyBatis:

MyBatis 本质上是一个 Java 持久层框架,但我更愿意称它为"半自动化的 ORM 框架"。

为什么叫"半自动"呢?这是它的核心特点:

  • 像 Hibernate 这种全自动 ORM,它会帮我们自动生成 SQL,开发者甚至不需要懂 SQL。

  • 而 MyBatis 仅仅帮我们做了"输入参数映射"和"输出结果映射",但具体的 SQL 语句还是需要我们自己编写的(通常写在 XML 里)。

虽然需要手写 SQL 听起来麻烦,但它给了开发者对 SQL 的绝对控制权。在复杂的业务场景下,我们可以轻松地对 SQL 进行手动优化,而不用去研究框架底层生成了什么奇怪的语句。

5.什么是Reids

一、 知识点讲解

Redis (Remote Dictionary Server) 本质上是一个基于内存的非关系型(NoSQL)数据库。

与 MySQL 不同,它将所有数据存储在内存中,这使得它的读写速度达到了纳秒级别。同时,它不仅仅是一个简单的 Key-Value 存储),它是一个数据结构服务器,支持在服务端直接操作复杂的结构(如链表、哈希表、集合)。

速度的核心来源:内存 + IO 多路复用
  • 内存存储: 这是快的基础。磁盘的寻址是毫秒级的,内存是纳秒级的,两者差了几个数量级。

  • 单线程模型: Redis 的网络 IO 和键值对读写是由一个主线程串行完成的(6.0 以后引入了多线程处理网络 IO,但执行命令依然是单线程)。

    • 为什么单线程还快? 因为 Redis 的瓶颈通常不在 CPU,而在网络带宽和内存。单线程避免了多线程上下文切换 的昂贵开销,也就不需要各种锁来保证线程安全。

    • IO 多路复用: 它利用系统的 Epoll 机制,让一个线程同时监听成千上万个客户端连接,哪个连接有数据来了就处理哪个,绝不阻塞。

丰富的数据结构

Redis 不是简单地把 value 当作一个字符串。它内部实现了针对不同场景优化的特定数据结构:

  • String: 最基本类型,底层是 SDS(简单动态字符串),安全且高效。

  • List: 双向链表,常用于消息队列。

  • Hash: 哈希表,适合存储对象。

  • Set/ZSet: 集合与有序集合,底层用到跳表,常用于排行榜。

这些结构使得计算(如排序、去重)可以直接在 Redis 内存中完成,而不用把数据拉到应用层处理。

持久化与高可用

虽然是内存数据库,但 Redis 提供了机制防止数据丢失:

  • RDB (快照): 每隔一段时间把内存数据 dump 到磁盘。

  • AOF (日志): 把每一条写命令追加到文件里。

  • 同时,通过 Sentinel(哨兵) 和 Cluster(集群) 实现了自动故障转移和数据分片。

二、 面试标准回答

Redis 是一个基于内存的高性能 Key-Value 数据库。在工程实践中,我们主要利用它极其快速的读写能力(QPS 可达 10 万+)来做缓存,同时利用它丰富的数据结构来实现复杂的业务逻辑(如排行榜、计数器)。

第一,关于它的性能核心。

Redis 之所以快,主要基于三点:

  1. 纯内存操作,避开了磁盘 I/O 的瓶颈。

  2. 基于 IO 多路复用模型,实现了非阻塞的网络 I/O,能高效处理海量并发连接。

  3. 单线程执行模型,避免了多线程频繁的上下文切换开销和竞态条件,不需要加锁。

第二,关于数据结构与场景。

它不仅仅存字符串,更重要的是支持复杂数据类型:

  • 用 String 做缓存、计数器(INCR)或分布式锁(SETNX)。

  • 用 Hash 存储用户对象,修改单个字段不用读取整个对象。

  • 用 List 做简单的消息队列。

  • 用 ZSet 实现实时排行榜,利用其底层的跳表结构进行快速排序。

第三,关于高可用。

Redis 虽然是内存数据库,但支持 RDB 和 AOF 两种持久化方式保证重启数据不丢。在生产环境中,我们通常配合 Sentinel(哨兵) 做主从切换的高可用,或者使用 Redis Cluster 做分片集群,以支持海量数据存储和水平扩展。

相关推荐
CesareCheung1 小时前
高级软件测试工程师面试锦集
面试·单元测试·压力测试
虚无的纽扣2 小时前
【力扣刷题】第五天:将 x 减到 0 的最小操作数、最大连续1的个数 III
算法·leetcode·职场和发展
子林super15 小时前
MySQL三大日志(binlog、redo log和undo log)详解
面试
救救孩子把1 天前
AI面试八股文-03_3-RAG 系统面试题
人工智能·redis·面试·agent·rag
我命由我123451 天前
动火作业极简理解
学习·职场和发展·产品运营·求职招聘·职场发展·产品经理·学习方法
十年Java程序媛1 天前
Java 内部类全解|成员 / 静态 / 匿名内部类,理清和 Lambda 的区别
java·spring boot·面试
用户676081149111 天前
个人图书室
面试
柒和远方1 天前
企业级多模态知识库:从文件入库、跨模态检索到可追溯回答
面试