ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Java八股面试(5)

Java八股面试(5)

1.#和$的区别是什么?什么情况必须用$

**#**** 是预编译处理,相当于 JDBC 的**?**占位符,专门用来防注入;**$**是字符串替换,相当于直接拼接 SQL,有注入风险。**

一、 核心区别深度解析

1.#{}(预编译)

  • 原理:MyBatis 在处理#{}时,会把它替换成一个**?**** (问号)**。

  • 执行:然后调用 JDBC 的PreparedStatement,通过setString()等方法把参数填进去。

  • 效果:假如你传的参数是Tom,它会自动加上单引号变成'Tom'。

  • 安全:因为是预编译,数据库知道这个?只是一个参数,不会把它当成 SQL 命令去执行。所以能防止 SQL 注入。

2.${}(字符串替换)

  • 原理:MyBatis 在处理${}时,就是简单的字符串拼接。

  • 执行:假如你传的参数是Tom,它就直接把Tom拼在 SQL 里,不加引号。

  • 风险:如果恶意用户传了1 OR 1=1,这句 SQL 就变成了逻辑真,所有数据都会被查出来。这就是SQL 注入。

二、 面试标准回答

它们的核心区别在于“处理机制”和“安全性”。

首先,关于它们的区别:

  1. 处理机制不同:

    1. **#{}**** 是预编译处理:** MyBatis 在处理它时,会将 SQL 中的#{}替换为 JDBC 的占位符**?**。接着调用PreparedStatement的set方法来赋值。

    2. **${}**** 是字符串替换:** MyBatis 在处理它时,就是做纯粹的字符串拼接。它会把参数的值直接拼接到 SQL 语句中,不会自动添加引号。

  2. 安全性不同(最关键点):

    1. **#{}**因为使用了预编译,能够有效防止 SQL 注入,所以我们在传递查询参数(如where id = #{id})时,绝大多数情况都必须用这个。

    2. **${}**因为是直接拼接,如果接收前端传入的不可信数据,极易引发 SQL 注入风险。

2.介绍一下ES是什么

一、 知识点讲解

ES (Elasticsearch) 说白了就是一个基于 Lucene 的分布式搜索和分析引擎。你可以把它理解为是一个对 Lucene 做了封装的“超级壳子”,把原本复杂难用的 Lucene 变成了支持分布式、高可用、提供 RESTful API 接口的开箱即用的服务。它主要解决的是传统关系型数据库在海量数据全文检索和复杂聚合分析上性能极其低下的问题。

倒排索引

这是 ES 的灵魂。MySQL 用的是 B+ 树,适合精确查找;而 ES 用的是倒排索引。一般的数据库是“通过 ID 找内容”。ES 是反过来的,它是“通过内容找 ID”。

  1. 当你存数据时,ES 会先把内容分词(比如把“华为手机”拆成“华为”、“手机”),然后建立一个字典,记录每个词出现在哪些文档 ID 里。当你搜“华为”时,它直接查字典就能告诉你哪些文档里有,不用去遍历每一行数据。这就是为什么它能在亿级数据中毫秒级响应。

分片与副本

ES 天生就是为了分布式设计的。

分片:数据量太大,一台机器存不下怎么办?切开了存。一个索引切成几个分片,分散在不同的机器(节点)上,并行查询,提高吞吐量。

  1. 副本:机器坏了数据丢了怎么办?备份。每个分片都有副本,主分片挂了,副本立马顶上,保证高可用。

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

Elasticsearch 是一个基于 Lucene 开发的、分布式的、实时的搜索和分析引擎。它对外提供了 RESTful 风格的接口,非常易于使用。在目前的架构中,我们主要用它来解决海量数据的全文检索和实时数据分析的需求。

它之所以被广泛使用,主要有两点核心优势:

第一是查询性能极强。

这主要归功于它底层的倒排索引机制。和传统数据库不同,ES 在写入数据时就会对内容进行分词建立索引,这让它在处理模糊搜索、多条件组合过滤时,比 MySQL 的 Like 查询快几个数量级,基本能做到亿级数据毫秒级返回。

第二是天生的分布式架构。

它支持水平扩展,通过分片机制,我们可以把庞大的数据切分存储在多台节点上,提升系统的吞吐量;同时通过副本机制,保证了即使某台节点宕机,数据也不会丢失,服务也不会中断,天然具备高可用性。

3.如何理解BASE理论

一、知识点讲解

BASE 理论是对 CAP 中一致性(C)和可用性(A)权衡的结果。

在分布式系统中,因为网络分区(P)是必然存在的,我们往往无法同时做到强一致性和高可用性。

BASE 理论的核心思想是:我们不追求强一致性,而是通过牺牲强一致性,来获得基本可用性,并允许数据在一段时间内是不一致的,但最终会达到一致状态。

它对应的是这三个短语的缩写:

Basically Available(基本可用):

系统出现故障时,允许损失部分可用性,但保证核心功能可用。

  1. 比如:响应时间变长(原本 0.5秒 返回,现在 2秒),或者功能降级(淘宝双11流量洪峰时,你可能无法看到具体的物流详情,只能看到简单的“已发货”状态,或者无法立刻修改收货地址,但你依然可以下单买东西)。

Soft State(软状态):

允许系统存在中间状态,而该中间状态不会影响系统的整体可用性。

  1. 实质:允许不同节点间的数据同步存在延时。比如你刚付完款,状态可能显示“支付中”而不是立刻变为“支付成功”,这就是一种软状态。

Eventually Consistent(最终一致性):

这是 BASE 的最终目标。系统不需要实时保持各节点数据一致,但在经过一段时间的同步后,所有节点的数据最终必须达成一致。

  1. 关键:这个“一段时间”取决于网络延迟、系统负载等因素。

二、面试标准回答

BASE 理论是分布式系统设计中,为了解决大规模互联网系统的高可用问题,对 CAP 理论中一致性和可用性权衡后的结果。它主要针对的是对数据强一致性要求没那么高,但对并发和可用性要求极高的场景。

它和传统数据库的 ACID 特性是相反的,ACID 追求的是“强一致性”,而 BASE 追求的是“最终一致性”。

第一是基本可用。这意味着在系统出现不可预知的故障时,我们允许损失部分可用性来保全大局。比如响应时间上的损失,或者功能上的降级,保证核心业务(如电商的下单)不受影响。

第二是软状态。这是相对于原子性中的“硬状态”而言的。我们允许系统存在中间状态,也就是说,允许系统在不同节点的数据副本之间进行同步时存在延时,这个延时不会阻塞业务的进行。

第三是最终一致性。这是 BASE 的核心目标。虽然我们允许中间状态和短暂的不一致,但系统必须保证在经过一段时间的同步后,所有节点的数据最终能够达到一致。这个时间窗口取决于网络状况和系统负载。

4.如何理解IOC

IOC(控制反转)是一种设计思想,它的核心是将对象的创建权和管理权从代码中剥离出来,交给 Spring 容器去统一管理。这样做最大的好处就是实现了代码的解耦。

一、 核心原理深度解析

我们必须搞清楚两个问题:谁控制了谁?反转了什么?

正向控制

  • 场景:假设对象 A 依赖于对象 B。

  • 做法:在 A 的代码内部,我们必须显式地new B()。

  • 弊端:

    • 耦合度高:如果 B 的构造函数变了,A 的代码也得改。

    • 难以替换:如果想把 B 换成子类 C,必须修改 A 的源代码。

    • 难以测试:想单独测 A,还得先把 B 搞定,没法 Mock。

反转控制

  • 做法:对象 A 不再去new B()了,它只声明“我需要一个 B”。

  • 谁来控制:**Spring 容器。容器在启动时,会自动创建好 B,然后通过反射把 B注入**到 A 里面。

  • 反转了什么:**对象的创建权和依赖关系的维护权**,从程序员的手里(代码内部),反转到了容器的手里。

IOC 与 DI 的关系

这是面试常考的辨析。

  • IOC :是思想,是目标(我要让控制权反转)。

  • DI:是手段,是实现(通过依赖注入来实现控制反转)。

  • 结论:IOC 是理论,DI 是实践。

二、 面试标准回答

IOC 是 Spring 框架最核心的设计思想。可以总结为一句话:IOC 是为了解耦,将对象的创建权移交给容器。

具体来说,可以从以下三个层面来理解:

第一,“谁控制谁,控制什么”:

在传统的 Java 开发中,如果 A 类依赖 B 类,我们需要在 A 类里手动通过 new 关键字来创建 B 的实例。这时候,依赖对象的创建和生命周期是由程序员在代码里主动控制的。

而在 Spring 中,我们不再手动 new 对象,而是把对象交给 Spring 容器去管理。这就是“控制”。

第二,什么是“反转”:

所谓的反转,就是对象的创建权和依赖关系的维护权发生了转移。

以前是我们主动去“找”资源,现在变成了被动等待容器把资源“送”过来。

第三,为什么要这么做:

这么做的最大好处就是解耦。

5.如何在分布式环境下实现精确限流

在分布式场景下,要实现精确限流,最通用的方案是“Redis + Lua 脚本”组合。针对“精确”二字,如果要求极致的时间窗口平滑度,使用“ZSet 滑动窗口”;如果追求性能与精度的平衡,使用基于 Lua 实现的“令牌桶算法”。

一、 核心难点与架构选型

在分布式环境下,流量打在不同的服务器上,单机的AtomicInteger或RateLimiter只能管住自己这台机器,管不住全局。比如你要限制全局 QPS 为 1000,你有 10 台机器,简单的每台限流 100 是不准确的(因为负载均衡不一定绝对均匀)。

要做到精确,必须把所有机器的计数收敛到一个地方。

  • 存储介质:**Redis** 是不二之选。因为它是单线程处理命令,天然防并发,且读写极快。

  • 一致性保障:**Lua 脚本**。因为“读取当前令牌数”和“扣减令牌数”是两个网络请求,在高并发下会有竞争,必须把逻辑打包在 Lua 脚本里发给 Redis,保证原子执行。

二、 两种主流的“精确”实现方案

针对“精确”定义的程度不同,有两种做法:

极致精确:Redis ZSet 实现滑动窗口

如果你说的“精确”,是指要解决“固定窗口”算法在临界点流量翻倍的问题(比如 0.9秒来了 100 个请求,1.1秒又来了 100 个请求,虽然每秒都没超,但 0.2秒内压垮了系统),那么ZSet 滑动窗口是唯一的解法。

  • 原理:

    • 我们将 Key 设为限流的资源名(如 API 接口)。

    • Value 是一个 Redis 的ZSet(有序集合)。

    • 每当一个请求进来:

      • 记录:将当前请求的时间戳作为 member 和 score 写入 ZSet。

      • 清理:移除当前时间窗口之外的旧记录。

      • 计数:统计 ZSet 中剩下的元素个数。

      • 判断:如果个数 <= 阈值,放行;否则拒绝。

  • 优缺点:

    • 优点:非常精确。它可以任意滑动,彻底消除了临界突刺问题。

    • 缺点:内存消耗大。你需要存储过去 1秒内所有请求的时间戳。如果限流阈值是 10万 QPS,那 ZSet 里就要存 10万条数据,这对 Redis 内存和 CPU 都是巨大压力。

  • 适用场景:强一致性要求高,但 QPS 阈值不高的场景(如短信发送频率限制)。

生产通用:Redis + Lua 实现令牌桶

如果你说的“精确”,是指严格控制速率,且能抗住高并发,那么令牌桶算法是工业界标准。它比固定窗口精确,比滑动窗口省资源。

  • 原理:

    • 在 Redis 中存储两个键:tokens(当前剩余令牌数)和timestamp(上次刷新时间)。

    • 每次请求进来,执行一段 Lua 脚本:

      • 计算生成:now - timestamp,计算出这段时间理论上应该生成多少个新令牌。

      • 更新桶:Math.min(capacity, tokens + generated),填满为止。

      • 判断扣减:如果tokens >= 1,则tokens - 1并返回成功;否则返回失败。

      • 更新时间:将timestamp更新为当前时间。

  • 为什么精确?

    • 它不是简单地按秒清零,而是基于时间流逝线性地补充令牌。

    • Redis Lua 保证了这整个计算过程是原子的,不会出现“多扣”或“少扣”。

  • 优缺点:

    • 优点:性能极高,内存占用极小(只存几个数字),且支持预热和突发流量。

    • 缺点:实现逻辑比计数器复杂,需要维护 Lua 脚本。

三、 补充点

  1. 时间同步问题:

    1. 在分布式环境下,应用服务器的时间可能不准。在使用 ZSet 滑动窗口时,尽量使用Redis 服务器的时间(Lua 脚本中获取redis.call('TIME')),而不是应用服务器的System.currentTimeMillis(),以保证基准一致。

  2. Redis 挂了怎么办?(降级策略)

    1. 这是分布式限流最大的软肋。如果 Redis 崩了或者网络抖动,限流器会抛异常。

    2. 策略:必须要有本地降级。可以在本地内存(如 Guava)预设一个更大的兜底阈值。一旦 Redis 链接超时,自动切换到单机限流模式,虽然不准,但能保命,别让限流组件把业务搞挂了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进