跳转到内容

Blog

redis核心原理与实战应用

Redis 常被用作缓存、分布式锁、排行榜、计数器和会话存储。它的核心优势不是“能存数据”这么简单,而是用内存访问、丰富数据结构、持久化和高可用机制,支撑高并发场景下的读写性能与系统稳定性。

这篇文章按实际使用路径整理 Redis:先看基础能力,再看高可用与缓存问题,最后整理一致性、性能优化和典型场景。

Redis 支持多种常用数据结构:

  • String:适合缓存简单值、计数器、分布式锁标记。
  • Hash:适合存储对象字段,例如用户信息。
  • List:适合队列、消息列表、时间线等场景。
  • Set:适合去重、集合交并差。
  • Sorted Set(ZSet):适合排行榜、权重排序、延迟队列。

这些结构让 Redis 不只是一个 Key-Value 缓存,而是可以承接一部分高频读写业务逻辑。

Redis 主要有两种持久化方式:RDB 和 AOF。

RDB(快照)

  • 定期生成内存快照,例如通过 bgsave 命令。
  • 优点:文件体积较小,数据恢复速度快。
  • 缺点:如果 Redis 异常退出,可能丢失最后一次快照之后的数据。

AOF(追加日志)

  • 记录每一次写操作命令,例如 SET key value
  • 优点:数据安全性更高,可以配置更高频率的落盘。
  • 缺点:文件体积更大,恢复速度通常慢于 RDB。

实际生产中常见策略是 RDB + AOF 组合使用:RDB 用于快速恢复,AOF 用于降低数据丢失风险。

Redis 主从同步分为全量同步和增量同步。

全量同步

通常发生在从节点初次连接主节点,或断线时间过长无法增量追赶时:

  1. 主节点生成 RDB 文件。
  2. 主节点把 RDB 文件发送给从节点。
  3. 从节点清空旧数据并加载 RDB。
  4. 主节点继续把同步期间缓冲区中的写操作发送给从节点。

增量同步

当从节点短暂断线后重新连接,可以通过 psync 根据复制偏移量(offset)同步缺失的写操作,避免重新做一次全量同步。

Redis 常见高可用方案主要有两类:

  • 哨兵模式(Sentinel):监控主从节点状态,在主节点故障时自动完成故障转移。
  • 集群模式(Cluster):通过分片存储数据,提升容量上限,并支持水平扩展。

哨兵更偏向主从高可用,集群更偏向容量扩展和分片治理。

缓存击穿指的是一个热点 Key 在高并发访问时突然过期,大量请求同时打到数据库。

常见解决方案:

  • 热点 Key 不设置过期时间,通过后台任务主动刷新。
  • 互斥锁或分布式锁,只允许一个请求回源重建缓存。
  • 逻辑过期,缓存中保存过期时间,由后台异步刷新数据。

缓存穿透指的是大量请求访问不存在的数据,缓存无法命中,请求直接落到数据库。

常见解决方案:

  • 布隆过滤器:在请求进入缓存和数据库之前过滤明显不存在的 Key。
  • 缓存空对象:对不存在的数据短时间缓存空值,避免重复打到数据库。
  • 参数校验:拦截非法 ID、异常参数和明显无效请求。

缓存雪崩指的是大量 Key 在同一时间过期,导致请求集中打到数据库。

常见解决方案:

  • 随机过期时间,避免大量 Key 同时失效。
  • 限流降级,在数据库压力过高时保护核心服务。
  • 多级缓存或集群部署,减少单点缓存失效带来的冲击。

四、数据一致性与 Redis-MySQL 同步

Section titled “四、数据一致性与 Redis-MySQL 同步”

业务中常见的数据源组合是 MySQL 存储主数据,Redis 作为缓存。这里的核心问题是:数据库更新后,缓存如何保持一致。

常见方案:

  • 先写 MySQL,再更新 Redis:逻辑直接,但并发下可能出现旧值覆盖新值。
  • 先写 MySQL,再删除 Redis:更常见,后续请求回源数据库并重建缓存。
  • 延迟双删:写库后删除缓存,短暂延迟后再次删除,降低并发读写导致的脏缓存概率。
  • 最终一致性:通过消息队列或 Canal 监听 MySQL Binlog,把数据变更异步同步到 Redis。
  • 对实时一致性要求很高的场景,需要更严格的事务或强一致方案。
  • 对短暂延迟可接受的场景,通常使用缓存删除、消息队列、Binlog 同步等最终一致性方案。

缓存一致性没有绝对通用答案,重点是结合业务对“延迟、正确性、复杂度”的要求做取舍。

Redis 提供 slowlog 用于排查慢查询。常见优化方向:

  • 避免 KEYS *SORT、大范围 SUNION 等高复杂度命令。
  • 对批量操作使用 Pipeline,减少网络往返。
  • 控制单次命令处理的数据规模,避免阻塞主线程。

当内存达到 maxmemory 限制时,Redis 会根据配置的淘汰策略处理 Key。

常见策略:

  • noeviction:内存不足时拒绝写入。
  • volatile-lru:只在设置了过期时间的 Key 中淘汰最近最少使用的 Key。
  • volatile-lfu:只在设置了过期时间的 Key 中淘汰最近最不常用的 Key。
  • allkeys-lru:在所有 Key 中淘汰最近最少使用的 Key。
  • allkeys-lfu:在所有 Key 中淘汰最近最不常用的 Key。
  • allkeys-random:在所有 Key 中随机淘汰。

缓存场景通常更常见 allkeys-lruallkeys-lfu,但具体选择要看访问分布和业务容忍度。

BigKey 指单个 Key 占用过大内存或包含过多元素,会导致网络传输、删除、迁移和持久化变慢。

HotKey 指少数 Key 被高频访问,容易造成单点压力。

优化方向:

  • 拆分过大的 Hash、List、Set、ZSet。
  • 避免一次性读取或删除大 Key。
  • 对热点数据做本地缓存、多副本缓存或请求合并。

Redis 的命令执行主要是单线程模型,这让它避免了大量锁竞争,命令执行顺序也更容易理解。

Redis 6.0 之后引入多线程处理网络 I/O,但命令执行本身仍然保持单线程语义。因此,慢命令、大 Key 操作、复杂聚合仍然可能阻塞 Redis。

Redis 对过期 Key 主要使用两种删除策略:

惰性删除

  • 获取 Key 时检查是否过期,过期则删除。
  • 优点:不主动消耗额外资源。
  • 缺点:如果过期 Key 长时间不被访问,可能继续占用内存。

定期删除

  • Redis 定期随机抽样部分 Key,删除其中已经过期的 Key。
  • 优点:可以主动清理过期数据。
  • 缺点:不保证所有过期 Key 都会立刻被删除。

Redis 默认使用 惰性删除 + 定期删除 的组合策略。

Redis 变慢时,可以从以下方向排查:

  • 检查内存是否耗尽,是否发生 Swap,例如使用 free -m
  • 检查磁盘 I/O,尤其是 AOF 写入和重写期间。
  • 使用 SSD 替代传统磁盘,降低持久化带来的 I/O 压力。
  • 排查是否使用了 KEYSSORTSUNION 等高复杂度命令。
  • 检查是否存在大批量读取、大批量删除或大集合遍历。
  • 对批量请求使用 Pipeline,减少网络延迟。

AOF 重写期间可能与 fsync 竞争磁盘资源。可以根据场景配置:

no-appendfsync-on-rewrite yes

这个配置可以降低 AOF 重写期间的写入阻塞风险,但也会增加极端情况下的数据丢失窗口,需要结合业务容忍度评估。

如果数据库和 Redis 同时出现压力波动,需要排查是否存在:

  • 大量 Key 同时过期。
  • 热点 Key 突然失效。
  • 缓存重建逻辑没有互斥保护。
特性RedisMemcached
数据类型String、Hash、List、Set、ZSet 等主要支持简单 Key-Value
持久化支持 RDB、AOF不支持持久化
内存回收支持多种淘汰策略支持 LRU 等缓存淘汰机制
原子操作支持较丰富的原子操作支持有限原子操作
线程模型命令执行以单线程为主,Redis 6.0 后支持 I/O 多线程多线程网络模型
场景适用复杂缓存、计数、排行榜、分布式锁简单缓存、高吞吐 Key-Value

简单说:如果只是做非常简单的缓存,Memcached 也可以胜任;如果需要更丰富的数据结构、持久化、高可用和分布式能力,Redis 更适合。

Redis 常见业务场景包括:

  • 缓存:存储热点数据,减少数据库压力。
  • 会话保持:存储用户 Session 或登录态。
  • 排行榜:通过 ZSet 实现 Top N 榜单。
  • 限流降级:通过计数器、滑动窗口或令牌桶实现接口限流。
  • 分布式锁:使用 SET key value NX PX 等命令实现基础锁能力。
  • 延迟队列:通过 ZSet 分数存储执行时间,实现定时任务调度。

Redis 的核心价值在于 高性能访问、灵活数据结构、持久化能力和分布式支持

实际使用 Redis 时,需要重点关注四件事:

  1. 数据结构是否选对。
  2. 缓存异常场景是否有兜底方案。
  3. Redis 与数据库之间是否能接受最终一致性。
  4. 是否避免了慢命令、BigKey、HotKey 和集中过期。

理解这些核心机制后,Redis 就不只是一个缓存组件,而是高并发系统中非常重要的性能与稳定性基础设施。

接触 Vibe Coding 八个多月后的感受

接触 Vibe Coding 已经八个多月了。

回头看,这段时间给我带来的变化非常大。它不只是让我多认识了一些 AI 工具,也不只是让我写代码的速度变快了。更重要的是,它改变了我看待开发这件事的方式。

以前我更关注技术本身。

我会想这个功能应该怎么实现,代码怎么写,框架怎么选,接口怎么设计,数据库结构怎么拆。很多时候,注意力会自然落在“如何把代码写出来”这件事上。

而现在,我越来越多地开始关注产品本身。

这个功能为什么要做?用户会怎么使用?流程是不是顺?页面是不是清楚?这个需求背后真正要解决的业务问题是什么?这些问题慢慢变得比“代码怎么写”更靠前。

这就是 Vibe Coding 对我最大的影响。

在更早的时候,我使用 AI 的方式其实很简单。

去年的上半年,AI 对我来说更多还是一个开发辅助工具。它主要停留在 Web 式的 Chat 形态里,我会问它一些问题,让它帮我解释概念、检索资料、分析报错、生成一些代码片段。

那个阶段的 AI 很像一个随时在线的助手。

它能帮我查东西,也能帮我补充思路,但大多数时候,真正的开发过程还是由我自己主导。我要自己拆任务、自己打开项目、自己修改文件、自己调试和验证。

AI 参与了过程,但没有真正进入开发工作流的中心。

那时候我对它的理解也很朴素:它可以提高效率,可以减少搜索成本,可以帮我更快理解一些不熟悉的知识。

但我还没有意识到,它会在后面改变整个开发方式。

后来,Agent 的概念越来越火。

我开始看到各种 CLI 工具出现,也开始频繁听到 token、上下文、模型网关、提示词、工具调用、代码代理这些词。AI 不再只是一个聊天窗口,它开始进入终端、进入编辑器、进入项目目录,甚至可以直接阅读代码、修改文件、运行命令、检查结果。

这和以前完全不一样。

以前是我把问题复制给 AI,然后把答案再搬回项目里。现在则更像是 AI 直接坐进了项目现场,和我一起看代码、改代码、验证代码。

这时我也开始加入 Vibe Coding 的行列。

刚开始的时候,我其实并不知道这些工具应该怎么用。面对各种模型、API、CLI、代理配置,我会有点茫然。它们看起来都很强,但真正落到自己的项目里,还是需要一段适应过程。

我需要理解它们的边界。

哪些事情可以交给它?哪些事情必须自己判断?什么时候应该让它改代码?什么时候只是让它分析?上下文应该怎么给?任务应该怎么拆?

这些并不是看一篇教程就能立刻掌握的。

后来我逐渐知道了 New API 这类整合型网关,也开始理解它们在 AI 工作流中的意义。

模型越来越多,不同模型有不同能力、价格和使用限制。如果每一个工具都单独配置,就会很分散。整合型网关的意义在于,它能把不同模型入口统一起来,让工具调用变得更稳定,也更容易管理。

再后来,我接触到了 Claude Code 这类工具。

它让我真正感受到“AI 参与编码”这件事和普通问答的区别。

普通问答更像是你问一句,它答一句。CLI 编码工具则更像是你把它放进项目里,它可以沿着任务往前走:阅读文件、理解结构、修改代码、运行检查、再根据结果继续调整。

这时候,AI 就不只是回答问题,而是在参与完成工作。

当然,这并不意味着我可以完全放手。

相反,我越来越感觉到,使用这类工具时,人要承担更高层次的判断。你要知道目标是什么,知道验收标准是什么,知道哪里不能乱动,知道生成的代码是否符合项目长期维护的方向。

AI 可以很快,但方向仍然要由人来定。

过去我做一个东西,第一反应常常是技术问题。

页面怎么写?接口怎么接?状态怎么管理?样式怎么调?

现在我会先想产品问题。

这个页面存在的目的是什么?用户第一眼应该看到什么?如果他想继续阅读,路径是不是顺?如果他在移动端打开,会不会困惑?如果内容越来越多,列表是否还能承载?如果未来要部署、维护、持续写文章,流程是不是足够轻?

这种变化很明显。

因为当 AI 可以承担大量具体编码工作后,我的注意力就被释放出来了。我不再需要把所有精力都压在每一行代码上,而是可以站得稍微高一点,看整个产品的结构和体验。

这并不是说技术不重要。

技术仍然重要,而且越到后面越重要。只是技术不再是唯一中心。它更像是实现产品目标的手段,而不是最终目的。

以前我可能会因为某个技术点很有意思就想做点东西。现在我会先问:这个东西解决了什么问题?它对用户、对内容、对长期维护有什么价值?

Vibe Coding 也让我更关注业务流程。

一个功能不是孤立存在的。它前面有入口,后面有结果,中间有状态变化和用户决策。只把某个页面写出来,并不代表功能真的完成。

比如一个博客站,不只是能展示文章就够了。

还要考虑:

  • 新文章怎么创建。
  • 分类和标签怎么维护。
  • 首页如何呈现内容价值。
  • 列表页如何让读者快速判断是否要点进去。
  • 文章页如何让阅读体验稳定。
  • 部署后如何只专注维护内容。

这些都不是单纯的代码问题,而是产品流程问题。

当 AI 能帮我更快完成具体实现后,我反而会花更多时间思考这些流程是否合理。

我会更在意一个功能放在系统里是不是自然,一个页面是不是为后续内容增长留好了空间,一个交互是不是符合读者直觉。

这其实是更接近产品视角的思考。

这八个多月里,我最大的感受是:AI 把很多“执行层面的阻力”变小了。

以前想到一个功能,可能要先考虑技术栈、查文档、写样板代码、调样式、修报错。很多时候,还没真正验证想法,就已经被实现细节消耗掉了。

现在不同了。

我可以更快把想法变成可运行的东西,再通过实际效果判断它是否值得继续优化。

这会让开发节奏发生变化。

以前更像是先想很久,再动手实现。现在更像是先做出一个版本,然后不断观察、调整、迭代。

AI 给我的不是简单的偷懒,而是更短的反馈周期。

当反馈周期变短,人就更容易围绕结果做判断,而不是长时间停留在假设里。

使用 Vibe Coding 越久,我越觉得人并没有变得不重要。

相反,人变得更重要。

因为 AI 可以生成代码,但它不一定知道什么是适合你的。它可以给出方案,但它不知道你真正想要的产品气质。它可以完成任务,但它不会天然理解你的长期规划。

所以,人需要做这些事情:

  • 定义目标。
  • 拆解任务。
  • 判断取舍。
  • 控制范围。
  • 验收结果。
  • 维护产品方向。

如果没有这些判断,AI 很容易把事情做得很快,但不一定做得正确。

这也是我慢慢学到的一点:Vibe Coding 不是随便让 AI 写代码,而是学会用清晰的目标和上下文引导它,把人的判断和 AI 的执行力结合起来。

这段经历让我发生了几个明显变化。

第一,我更愿意从产品角度看问题。

我不再只问“这个功能怎么写”,而是会先问“这个功能为什么存在”。

第二,我更重视流程。

页面、内容、工具、部署、维护,它们应该连成一条顺畅的链路,而不是一个个孤立的点。

第三,我对学习 AI 相关知识更有动力。

从模型到上下文,从 CLI 工具到网关,从提示词到任务拆解,这些内容不再只是概念,而是会真实影响我每天的开发方式。

第四,我开始更相信个人项目的可能性。

以前一个人做完整产品会觉得很重。现在虽然仍然不轻松,但至少很多原本消耗人的细节可以被 AI 分担。一个人的上限,正在被工具重新拉高。

接触 Vibe Coding 的这八个多月,对我来说像是一次开发方式的迁移。

我从把 AI 当作问答工具,慢慢转向把它当作开发协作者。也从更关注技术实现,逐渐转向关注产品本身、业务流程和最终结果。

这种变化不是一夜之间发生的。

它是在一次次尝试工具、配置模型、修改项目、验证结果的过程中慢慢形成的。

现在的我依然还在学习 AI,也还在摸索更适合自己的工作流。但有一点已经很明确:未来的开发不会再回到过去那种完全依赖手工推进的状态。

AI 会继续进入开发流程,而我需要做的,是学会站在更高的位置使用它。

把注意力从代码细节里适当抽出来,更多地放到产品、体验、流程和价值上。

这可能就是 Vibe Coding 最吸引我的地方。

它不是让我不再关心技术,而是让我终于有更多精力去关心技术背后真正要完成的事情。

梦开始的地方

这是我创建项目的第一篇博客文章。

在这个博客里,我将分享我的技术学习历程、项目经验以及一些随笔思考。希望通过这个平台,能够记录下我的成长轨迹,也能与志同道合的朋友们交流和分享。

我的名字叫做 veyliss,这是我“梦开始的地方”。我希望在这里能够记录下我的梦想和努力的过程,也希望能够激励自己不断前行。无论是技术上的突破,还是生活中的点滴,我都希望能够在这里留下足迹。

在许多年前我就曾想着自己搭建真正属于自己的博客网站,记录自己的学习和成长。如今这个想法终于实现了,我感到非常兴奋和满足。这个博客不仅是一个记录工具,更是一个激励自己不断前进的动力源泉。

我相信,通过这个博客,我能够更好地总结和反思自己的学习过程,也能够与更多的人分享我的经验和见解。无论是技术上的问题,还是生活中的思考,我都希望能够在这里找到共鸣和支持。

最后,感谢每一个来到这个博客的朋友们,希望你们能够在这里找到有价值的内容,也希望我们能够在这里一起成长和进步。让我们一起在这个梦开始的地方,书写属于我们的故事吧!

回顾 2025

回望 2025 年,时间像被按下了快进键。

这一年走得很快,也走得并不轻松。很多事情在开始时都带着热情,真正走到最后却发现,能完整收尾的并不算多。想做的项目、想沉淀的知识、想持续推进的计划,有些还停留在半成品阶段,有些甚至只是短暂地闪过念头。

这并不是一个让人完全满意的年份。

但它也不是毫无意义的一年。

这一年里,有一段连续而紧张的工作经历。

那几个月很充实,也很消耗人。每天都在处理新的任务,面对新的问题,试着把一些看起来并不容易的事情往前推进。工作中,我尽力认真对待每一项安排,也不断尝试挑战自己原本觉得困难的部分。

这段经历让我收获了不少东西。

我积累了更真实的工作经验,也接触到了很多优秀的同事和前辈。从他们身上,我看到了更成熟的处理方式、更稳定的职业节奏,以及面对复杂问题时更清晰的判断。

这些东西不是单靠看教程、写练习就能得到的。它们来自具体的工作现场,来自一次次沟通、交付、修改和复盘。

不过遗憾也很明显。

个人能力确实在提升,但还没有达到自己期待中的飞跃。和行业里真正优秀的人相比,差距依然存在,而且并不小。意识到这一点的时候,会有一点失落,但也会更清楚自己接下来应该往哪里用力。

下班后的疲惫,是这一年很真实的一部分。

这种疲惫不只是身体上的,更是精神上的。忙碌一天之后,大脑长时间处在紧绷状态。回到住处,整个人像被抽空了能量,明明知道还有很多东西值得学习,却很难再把自己重新拉回专注状态。

我曾经计划利用业余时间继续学习新知识,补足能力短板,也想持续推进一些个人项目。可是很多时候,打开资料、看到待办列表,就会先感到一阵无力。

于是一些计划被推迟,一些想法被搁置,一些原本应该继续打磨的东西,也慢慢停在了半路。

这并不值得美化。

它只是提醒我:人的精力是有限的,自我提升也不能只靠一时热情。真正能走得远的,应该是更稳定、更可持续的节奏。

这一年,我也多次想过要构建自己的知识体系。

我越来越能感受到,零散学习带来的问题很明显。今天看一点后端,明天补一点前端,后天又去了解新的工具和概念,短期内似乎学了很多,但如果没有整理和连接,这些知识很容易散落在各处。

我希望能把这些零散的知识点串联起来。

不是为了把自己包装得很厉害,而是希望在未来遇到问题时,能更快找到方向,知道某个知识点在整个技术体系里处于什么位置,也能把过去踩过的坑、做过的项目、解决过的问题留下来。

这也是我后来越来越想认真维护博客和知识库的原因。

文字不是为了证明什么,而是为了给自己留下路径。现在的记录,也许会成为未来某个阶段重新出发时的坐标。

2025 年,我结识了很多圈内朋友。

他们来自不同领域,有交易所、Web3、外卖行业、传统行业、SaaS 领域,也有来自大厂的朋友。还有一些自由职业者,他们的工作方式和生活状态让我产生过很多羡慕与向往。

和这些人交流,会明显感觉到世界比自己日常接触到的范围更大。

不同的人在不同赛道里寻找机会,也在用各自的方式处理工作、生活和成长之间的关系。有的人很专注,有的人很灵活,有的人已经找到了相对自由的节奏,也有人还在变化里持续摸索。

这些交流让我意识到,职业发展不是只有一条路。

稳定工作是一种选择,持续深耕是一种选择,做项目、做产品、做自由职业,也都是不同的选择。重要的是,自己要逐渐知道想过怎样的生活,并为之积累足够的能力。

2025 年也是 AI 快速爆发的一年。

越来越多行业开始投入 AI 相关研发,尝试用 AI 提升效率、优化流程、创造新的业务价值。无论是技术研发、内容生产,还是产品设计、数据分析,AI 都在逐渐进入日常工作。

这不是一个遥远的趋势,而是正在发生的变化。

我能明显感觉到,未来互联网生态会和 AI 更紧密地连接在一起。很多岗位的工作方式会被改变,很多工具会被重做,很多原本依赖人工经验的流程,也会被新的方式重新组织。

这对个人来说既是压力,也是机会。

压力在于,原本掌握的技能可能很快变得不够用。机会在于,如果能更早理解这些变化,并把 AI 当成能力放大器,就有可能在新的阶段找到更好的位置。

2025 年并不是一个完成度很高的年份。

它有遗憾,有拖延,有没有收尾的项目,也有很多没有真正落实的计划。但它同样留下了工作经验、人际连接、行业观察和对自我节奏的重新认识。

我不想把这一年写得过于漂亮。

因为真实的成长并不总是热血的。很多时候,它只是一次次发现自己的不足,然后在疲惫里慢慢调整方向。

如果要给 2025 年一个总结,我想它更像是一个提醒:

不要只依赖热情,也不要害怕缓慢。

真正重要的,是在每一次停顿之后,还能重新开始。

Java 校招面试题复盘清单

这是一份 Java 校招面试题复盘清单。

它不适合当作“背诵稿”逐字记忆,更适合当作复习地图:先知道面试会问哪些方向,再把每个问题整理成可以讲清楚的核心答案。

Java 校招面试通常会围绕三部分展开:

模块占比重点
技术基础约 40%Java、Spring、MySQL、Redis、JVM、并发
项目深挖约 40%项目背景、技术选型、难点、优化、问题排查
学习能力约 20%最近在学什么、为什么做 Java、如何解决问题

如果项目里有 Elasticsearch,那么 ES 往往会成为面试官重点追问的方向。

HashMap 底层主要是数组、链表和红黑树。

当放入一个 key-value 时,会先根据 key 的 hashCode() 计算 hash,再定位到数组下标。如果该位置没有元素,就直接放入;如果已经有元素,就会形成链表或红黑树。

在 Java 8 之后,当链表长度达到一定阈值,并且数组容量足够大时,链表会转换为红黑树,用来提高查询效率。

可以这样回答:

HashMap 通过 hash 定位数组下标,数组中每个位置叫 bucket。发生 hash 冲突时,Java 8 以前主要用链表,Java 8 之后链表过长会转为红黑树。扩容时会重新计算元素位置,所以 HashMap 的性能和初始容量、负载因子、hash 分布都有关系。

HashMap 没有做同步控制,多线程同时读写时可能出现数据覆盖、状态不一致、扩容异常等问题。

常见风险:

  • 多个线程同时 put,可能覆盖彼此写入。
  • 扩容时结构变化,其他线程同时访问可能拿到异常结果。
  • 统计数量 size 可能不准确。

所以多线程场景通常使用 ConcurrentHashMap

ConcurrentHashMap 如何实现线程安全

Section titled “ConcurrentHashMap 如何实现线程安全”

Java 8 中,ConcurrentHashMap 主要通过 CAS 和 synchronized 保证线程安全。

它不是给整张表加一把大锁,而是尽量缩小锁粒度。

常见回答:

Java 8 的 ConcurrentHashMap 底层也是数组、链表和红黑树。插入时,如果桶为空,会通过 CAS 放入节点;如果桶不为空,会对桶头节点加 synchronized,只锁当前桶。这样既保证线程安全,又比 Hashtable 整表加锁性能更好。

ArrayList 底层是动态数组,LinkedList 底层是双向链表。

对比项ArrayListLinkedList
底层结构动态数组双向链表
随机访问快,按下标访问 O(1)慢,需要遍历
中间插入删除需要移动元素找到节点后修改指针
内存占用相对较少每个节点要存前后指针
常用场景查询多插入删除多,但实际也要看位置

面试里要注意:不要简单说 LinkedList 插入删除一定快。因为如果要先按索引找到位置,遍历本身也有成本。

String、StringBuilder、StringBuffer 的区别

Section titled “String、StringBuilder、StringBuffer 的区别”

String 是不可变对象,每次修改都会产生新字符串。

StringBuilder 是可变字符序列,线程不安全,但性能较好。

StringBuffer 也是可变字符序列,方法加了同步,线程安全,但性能通常低于 StringBuilder

常见选择:

  • 少量字符串拼接:直接用 String
  • 单线程大量拼接:用 StringBuilder
  • 多线程共享拼接对象:用 StringBuffer,但实际业务中较少这样用。

== 比较的是两边是否相等。

对于基本类型,比较的是值。

对于引用类型,比较的是对象地址。

equals() 是对象方法,默认实现也是比较地址,但很多类会重写它,比如 String 会比较字符串内容。

equals-demo.java
String a = new String("hello");
String b = new String("hello");
System.out.println(a == b); // false
System.out.println(a.equals(b)); // true

Java 异常体系的顶层是 Throwable

它下面主要分为:

  • Error:严重错误,程序通常不主动处理,比如 OutOfMemoryError
  • Exception:程序可以捕获和处理的异常。

Exception 又分为:

  • checked exception:编译期异常,必须处理或声明抛出。
  • unchecked exception:运行时异常,继承自 RuntimeException,例如空指针、数组越界。

面试里可以补一句:业务开发中不要滥用异常控制正常流程,异常更适合表示非预期情况。

反射是 Java 在运行时获取类信息、创建对象、调用方法、访问字段的能力。

常见应用场景:

  • Spring 创建 Bean、依赖注入。
  • MyBatis 映射对象字段。
  • 注解解析。
  • 测试框架调用测试方法。
  • 动态代理。

反射灵活,但也有缺点:性能相对普通调用更低,可读性和安全性更差。

进程是操作系统资源分配的基本单位。一个应用程序运行起来通常就是一个进程。

线程是 CPU 调度的基本单位,一个进程中可以包含多个线程。

可以这样回答:

进程拥有独立的内存空间,线程共享同一进程的内存资源。线程切换成本通常低于进程,但共享数据也会带来线程安全问题。

synchronized 可以修饰方法或代码块,用来保证同一时间只有一个线程进入临界区。

它依赖对象监视器锁,也就是 monitor。

进入同步代码块时,线程尝试获取对象锁;执行完或异常退出时释放锁。

可以补充:

  • 修饰普通方法,锁的是当前对象 this
  • 修饰静态方法,锁的是当前类的 Class 对象。
  • 修饰代码块,可以指定锁对象。

volatile 主要有两个作用:

  • 保证变量对多线程的可见性。
  • 禁止指令重排序。

它不能保证复合操作的原子性。

比如 count++ 包含读取、加一、写回,不是一个原子操作,所以只加 volatile 仍然不安全。

常见场景:

  • 状态标志位。
  • 单例模式双重检查锁中的实例变量。

线程池是提前创建并管理一组线程,任务来了以后交给线程池执行。

使用线程池的原因:

  • 避免频繁创建和销毁线程。
  • 控制并发线程数量。
  • 提高响应速度。
  • 统一管理任务队列、拒绝策略和线程生命周期。

ThreadPoolExecutor 常见核心参数:

参数作用
corePoolSize核心线程数
maximumPoolSize最大线程数
keepAliveTime非核心线程空闲存活时间
unit时间单位
workQueue任务队列
threadFactory线程创建工厂
handler拒绝策略

执行流程可以概括为:

核心线程未满 -> 创建核心线程
核心线程已满 -> 放入任务队列
队列满了 -> 创建非核心线程
线程数达到最大且队列也满 -> 执行拒绝策略

死锁是多个线程互相持有对方需要的资源,导致都无法继续执行。

死锁常见四个条件:

  • 互斥。
  • 请求并保持。
  • 不可剥夺。
  • 循环等待。

避免方式:

  • 固定加锁顺序。
  • 减少锁范围。
  • 使用超时锁。
  • 避免嵌套锁。
  • 使用并发工具类代替手写锁。

JVM 运行时数据区主要包括:

  • 程序计数器。
  • Java 虚拟机栈。
  • 本地方法栈。
  • 堆。
  • 方法区。

线程私有:

  • 程序计数器。
  • Java 虚拟机栈。
  • 本地方法栈。

线程共享:

  • 堆。
  • 方法区。

栈主要存放方法调用相关信息,比如局部变量表、操作数栈、方法出口等。

堆主要存放对象实例,是垃圾回收重点关注的区域。

对比项
线程关系线程私有线程共享
存储内容方法调用、局部变量对象实例
生命周期随方法调用入栈出栈由 GC 管理
常见错误StackOverflowErrorOutOfMemoryError

垃圾回收是 JVM 自动回收不再使用对象的机制。

判断对象是否可回收,主流方法是可达性分析:从 GC Roots 出发,能到达的对象是存活对象,不能到达的对象可以被回收。

常见 GC Roots:

  • 虚拟机栈中的引用。
  • 方法区中的静态变量引用。
  • 常量引用。
  • 本地方法栈中的引用。

常见算法:

  • 标记-清除:先标记垃圾,再清除,可能产生内存碎片。
  • 复制算法:把存活对象复制到另一块区域,适合新生代。
  • 标记-整理:标记后整理存活对象,减少碎片。
  • 分代收集:按对象生命周期分区,不同区域用不同算法。

OOM 是内存不足导致的错误。

常见原因:

  • 创建了大量对象,堆空间不足。
  • 大对象过多。
  • 内存泄漏,旧对象一直被引用。
  • 线程过多导致栈空间不足。
  • 元空间加载类过多。

排查时通常会看日志、堆 dump、GC 情况和对象引用链。

常见关注点:

  • 堆大小:-Xms-Xmx
  • 新生代大小:-Xmn
  • 元空间大小:-XX:MetaspaceSize
  • GC 收集器选择。
  • GC 日志。
  • 停顿时间和吞吐量。

调优不是盲目改参数,而是先看现象:内存是否够、GC 是否频繁、停顿是否过长、对象是否异常增长。

IoC 是控制反转。

原本对象由程序自己创建和管理,现在交给 Spring 容器创建和管理。

DI 依赖注入是 IoC 的一种实现方式。比如一个 Service 依赖 Mapper,不需要自己 new,而是由 Spring 注入。

AOP 是面向切面编程,用来把通用逻辑从业务代码中抽离出来。

常见场景:

  • 日志。
  • 权限校验。
  • 事务。
  • 监控统计。
  • 接口耗时。

核心思想是:不修改业务方法本身,在方法执行前后织入增强逻辑。

简化流程:

实例化 -> 属性注入 -> 初始化前后处理 -> 初始化方法 -> 使用 -> 销毁

常见扩展点:

  • 构造方法。
  • 属性填充。
  • BeanPostProcessor
  • InitializingBeaninit-method
  • DisposableBeandestroy-method

Spring Boot 主要解决传统 Spring 项目配置繁琐的问题。

优势:

  • 自动配置。
  • 内置 Web 容器。
  • starter 依赖简化。
  • 快速创建项目。
  • 方便监控和部署。

一句话回答:

Spring Boot 让 Spring 项目更容易启动和维护,它通过自动配置和 starter 机制减少大量 XML 或手动配置。

自动配置的核心是根据类路径、配置文件和条件注解,自动创建合适的 Bean。

常见关键词:

  • starter。
  • auto configuration。
  • @EnableAutoConfiguration
  • 条件注解,比如 @ConditionalOnClass@ConditionalOnMissingBean

面试中可以这样说:

Spring Boot 会根据引入的依赖和当前环境判断是否满足条件,如果满足,就把对应配置类里的 Bean 注册到容器中。

RESTful API 是一种接口设计风格。

核心思想:

  • 使用 URL 表示资源。
  • 使用 HTTP 方法表示操作。
  • 使用状态码表示结果。

示例:

GET /users/1 查询用户
POST /users 创建用户
PUT /users/1 更新用户
DELETE /users/1 删除用户

索引是帮助 MySQL 快速查找数据的数据结构。

可以理解为书的目录。没有索引时,数据库可能要全表扫描;有索引时,可以更快定位数据。

索引能提高查询效率,但会增加写入成本和存储空间。

B+Tree 适合磁盘存储和范围查询。

原因:

  • 树高度低,减少磁盘 IO。
  • 非叶子节点只存索引,能放更多 key。
  • 叶子节点之间有链表,范围查询效率高。
  • 查询性能稳定。

覆盖索引指查询需要的字段都能从索引中获得,不需要回表查询。

比如有联合索引 (name, age)

SELECT name, age FROM user WHERE name = 'xiaoxi';

如果查询字段都在索引里,就可能走覆盖索引。

常见情况:

  • 对索引列使用函数。
  • 对索引列做计算。
  • 使用左模糊匹配,例如 LIKE '%abc'
  • 联合索引不符合最左前缀原则。
  • 隐式类型转换。
  • OR 条件使用不当。

事务是一组数据库操作的集合,要么全部成功,要么全部失败。

比如下单时:

  • 创建订单。
  • 扣减库存。
  • 扣减余额。

这些操作应该作为一个整体处理。

特性含义
Atomicity 原子性事务要么全部成功,要么全部失败
Consistency 一致性事务前后数据满足约束
Isolation 隔离性并发事务之间互相隔离
Durability 持久性事务提交后数据持久保存

MVCC 是多版本并发控制。

它通过保存数据的多个版本,让读写尽量不互相阻塞。

在 InnoDB 中,MVCC 主要依赖:

  • 隐藏字段。
  • undo log。
  • ReadView。

常见作用是支持可重复读和快照读。

常见原因:

  • 数据主要在内存中。
  • 使用高效数据结构。
  • 单线程命令执行避免大量锁竞争。
  • IO 多路复用。
  • C 语言实现,执行效率高。

常见类型:

  • String。
  • Hash。
  • List。
  • Set。
  • Sorted Set。
  • Bitmap。
  • HyperLogLog。
  • Geo。
  • Stream。

缓存穿透是请求的数据在缓存和数据库中都不存在,导致请求持续打到数据库。

解决方式:

  • 缓存空对象。
  • 布隆过滤器。

缓存击穿是热点 Key 过期瞬间,大量并发请求同时打到数据库。

解决方式:

  • 互斥锁。
  • 逻辑过期。

缓存雪崩是大量 Key 同时过期,或者 Redis 整体不可用,导致大量请求涌向数据库。

解决方式:

  • 过期时间加随机值。
  • 多级缓存。
  • 限流降级。
  • Redis 高可用。

Elasticsearch 是一个分布式搜索和分析引擎,常用于全文搜索、日志检索、商品搜索等场景。

它底层基于 Lucene,对外提供 REST API,支持分布式、倒排索引和复杂查询。

倒排索引是从词到文档的映射。

普通索引更像:

文档 -> 包含哪些词

倒排索引更像:

词 -> 出现在哪些文档中

这就是 ES 做全文搜索快的重要原因。

index 类似一类数据的集合。

document 是一条 JSON 数据。

type 在早期版本中用于区分类型,但新版本已经逐步移除,不建议在新项目中依赖 type。

可以类比:

ES关系型数据库
indextable 或 database 的某种集合概念
documentrow
fieldcolumn

DSL 是 Elasticsearch 的查询语言,使用 JSON 描述查询条件。

示例:

es-dsl.json
{
"query": {
"match": {
"title": "redis"
}
}
}

常见条件:

  • must:必须匹配,影响评分。
  • should:可选匹配,可能影响评分。
  • filter:必须匹配,但不参与评分,适合过滤条件。
  • must_not:必须不匹配。

为什么 Elasticsearch 搜索比 MySQL 快

Section titled “为什么 Elasticsearch 搜索比 MySQL 快”

ES 在全文搜索场景下更快,主要因为它使用倒排索引。

MySQL 更擅长结构化数据查询和事务处理,全文检索不是它最核心的场景。

可以这样回答:

ES 会先对文本分词,再建立词到文档的倒排索引。查询关键词时,可以快速定位包含该词的文档。MySQL B+Tree 索引更适合精确匹配和范围查询,对复杂全文搜索、相关性评分和分词检索不如 ES 合适。

Docker 是容器化技术,可以把应用和依赖打包成镜像,再以容器方式运行。

它解决了“本地能跑,服务器不能跑”的环境一致性问题。

对比项Docker虚拟机
隔离方式进程级隔离硬件级虚拟化
启动速度
资源占用较少较多
系统内核共享宿主机内核每个虚拟机有完整操作系统
适用场景应用部署、微服务强隔离、多系统环境

常见命令:

linux-commands.sh
ls
cd
pwd
mkdir
rm
cp
mv
cat
tail -f app.log
grep "error" app.log
ps -ef
top
df -h
free -m
chmod
chown

面试时如果结合项目部署经历回答,会比单纯背命令更好。

REST API 是基于 REST 风格设计的接口。

它通常使用 HTTP 协议,通过 URL 表示资源,通过 HTTP 方法表示操作。

这个问题和 Spring 里的 RESTful API 本质相同。

微服务是把一个大系统拆成多个小服务,每个服务负责一个相对独立的业务能力。

优点:

  • 服务独立部署。
  • 技术栈可以更灵活。
  • 方便水平扩展。
  • 团队边界更清晰。

缺点:

  • 服务间调用复杂。
  • 分布式事务困难。
  • 监控、链路追踪、部署复杂度提高。

API 网关是系统入口,负责把外部请求转发到内部服务。

常见功能:

  • 路由转发。
  • 鉴权。
  • 限流。
  • 熔断。
  • 日志。
  • 跨域处理。

如果你的项目重点是 Elasticsearch,面试官很可能会围绕项目继续追问。

可以提前准备这些问题:

问题准备方向
为什么项目要用 ESMySQL 搜索能力不足、全文检索、分词、排序、性能
数据怎么同步到 ES同步写、异步消息、定时补偿
ES 和 MySQL 数据不一致怎么办重试、补偿任务、最终一致性
索引怎么设计index、mapping、分词器、字段类型
搜索结果怎么排序相关性评分、业务权重、时间、热度
查询慢怎么优化filter、分页限制、字段选择、索引设计

面试时不要只说“我用了 ES”。更好的说法是:

我在项目中用 Elasticsearch 解决全文检索问题。MySQL 更适合事务和结构化查询,但对分词搜索、相关性排序支持有限。所以把需要搜索的数据同步到 ES,通过倒排索引提升搜索效率。项目里还需要考虑 MySQL 和 ES 的数据一致性,比如通过消息队列异步同步,并配合定时任务补偿。

学习能力问题通常不会太难,但很考验真实感。

常见问题:

  • 最近在学什么技术?
  • 为什么选择 Java?
  • 遇到不会的问题怎么解决?
  • 看过哪些技术文档?
  • 项目里最有收获的地方是什么?

回答建议:

  • 不要只说“我在学 Java”。
  • 要说具体学了什么、为什么学、怎么实践。
  • 最好能和项目或面试岗位关联起来。

例如:

最近我在补 Redis 和 Elasticsearch。Redis 主要关注缓存穿透、击穿、雪崩以及数据结构的使用场景;Elasticsearch 主要学习倒排索引、DSL 查询和数据同步。因为我的项目里有搜索场景,所以我想把搜索链路和缓存链路都理解得更完整。

如果时间有限,可以按这个顺序准备:

  1. Java 基础:集合、字符串、异常、反射。
  2. MySQL:索引、事务、MVCC、索引失效。
  3. Redis:数据结构、缓存问题、常用命令。
  4. Spring Boot:IoC、AOP、自动配置、REST API。
  5. JVM:内存结构、GC、OOM。
  6. 并发:线程池、锁、volatile、死锁。
  7. Elasticsearch 项目:倒排索引、DSL、数据同步、搜索优化。
  8. Docker 和 Linux:能讲清楚部署和常用命令即可。

Java 校招面试不是只背八股。

基础题要能答清楚概念,项目题要能讲清楚自己做了什么、为什么这样做、遇到了什么问题、怎么解决。

这份清单的重点不是一次性背完,而是把每个问题都整理成三层:

一句话结论
核心原理
项目或使用场景

这样回答时会更稳,也更像真的理解过。