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 里的索引并不是一整块巨大的文件,而是由很多个段组成的。

-
不可变性:一旦一个段被写在这个磁盘上,它就不能被修改了。这样做的好处是不需要锁,读写性能极高。
-
删除与更新:因为不能修改,所以"删除"其实只是在一个 .del 文件里打个标记说"这行数据废了";"更新"就是"先标记删除,再新增一条"。
-
合并:为了防止段文件越来越多,Lucene 会在后台偷偷把几个小段合并成一个大段,并顺便把标记删除的数据真正清理掉。
二、 面试标准回答(实战篇)
Lucene 本质上是一个高性能、全功能的文本搜索引擎类库。
它不是一个可以独立运行的服务,而是一个工具包。我们可以通过调用它的 API 来实现数据的索引和检索。
关于 Lucene 的内部原理,三个点最关键:
-
倒排索引:这是它性能强大的根基。Lucene 通过维护'词项'到'文档 ID 列表'的映射,实现了毫秒级的关键词检索,避免了全表扫描。
-
段式存储 :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 之所以快,主要基于三点:
-
纯内存操作,避开了磁盘 I/O 的瓶颈。
-
基于 IO 多路复用模型,实现了非阻塞的网络 I/O,能高效处理海量并发连接。
-
单线程执行模型,避免了多线程频繁的上下文切换开销和竞态条件,不需要加锁。
第二,关于数据结构与场景。
它不仅仅存字符串,更重要的是支持复杂数据类型:
-
用 String 做缓存、计数器(
INCR)或分布式锁(SETNX)。 -
用 Hash 存储用户对象,修改单个字段不用读取整个对象。
-
用 List 做简单的消息队列。
-
用 ZSet 实现实时排行榜,利用其底层的跳表结构进行快速排序。
第三,关于高可用。
Redis 虽然是内存数据库,但支持 RDB 和 AOF 两种持久化方式保证重启数据不丢。在生产环境中,我们通常配合 Sentinel(哨兵) 做主从切换的高可用,或者使用 Redis Cluster 做分片集群,以支持海量数据存储和水平扩展。