GitVP开源文摘
全部文章/后端开发

JavaGuide

Java 面试 & 后端通用面试指南

作者Snailclimb(Guide 哥) 仓库Snailclimb/JavaGuide ↗ 星标★ 159,029 字数144,758 许可Apache-2.0 阅读12
摘要开源多年的中文后端面试知识库,覆盖计算机基础、数据库、分布式、高并发、系统设计与 AI 应用开发,是 GitHub 上中文技术仓库里星标最高的几本之一。

English | 日本語 | 简体中文

logo

GitHub | Gitee

Snailclimb%2FJavaGuide | Trendshift
- 大模型实战项目: ⭐AI 智能面试辅助平台 + RAG 知识库(基于 Spring Boot 4.0 + Java 21 + Spring AI 2.0,非常适合作为学习和简历项目,学习门槛低)。 - 面试资料补充: - 《Java 面试指北》:四年打磨,和 JavaGuide 开源版的内容互补,带你从零开始系统准备面试! - 《后端面试高频系统设计&场景题》:30+ 道高频系统设计和场景面试,助你应对当下中大厂面试趋势。 - 使用建议 :如果你想要系统准备 Java 后端面试但又不知道如何开始的,可以参考 Java 后端面试通关计划(后端通用)。 - 求个 Star:如果觉得 JavaGuide 的内容对你有帮助的话,还请点个免费的 Star,这是对我最大的鼓励,感谢各位一起同行,共勉!传送门:GitHub | Gitee。 - 转载须知:以下所有文章如非文首说明为转载皆为 JavaGuide 原创,转载请在文首注明出处。如发现恶意抄袭/搬运,会动用法律武器维护自己的权益。让我们一起维护一个良好的技术创作环境!

AI 应用开发面试指南

面向后端开发者的 AI 应用开发、AI 编程实战与面试指南已开源,涵盖 LLM、Agent、RAG、MCP、Claude Code、Codex 等核心技术与工程实践。对标 JavaGuide!有帮助的话,欢迎 Star!

后端面试准备

Java

基础

知识点/面试题总结(必看:+1:):

重要知识点详解:

集合

知识点/面试题总结:

源码分析:

IO

并发

知识点/面试题总结(必看 :+1:)

重要知识点详解:

JVM(必看 :+1:)

JVM 这部分内容主要参考 JVM 虚拟机规范-Java8 和周志明老师的《深入理解 Java 虚拟机(第 3 版)》 (强烈建议阅读多遍!)。

新特性

计算机基础

操作系统

网络

知识点/面试题总结:

重要知识点详解:

数据结构

图解数据结构:

其他常用数据结构:

算法

算法这部分内容非常重要,如果你不知道如何学习算法的话,可以看下我写的:

常见算法问题总结:

另外,GeeksforGeeks 这个网站总结了常见的算法,比较全面系统。

数据库

基础

MySQL

知识点/面试题总结:

重要知识点:

Redis

知识点/面试题总结(必看:+1:):

重要知识点:

MongoDB

搜索引擎

Elasticsearch 常见面试题总结(付费)

JavaGuide 官方公众号

开发工具

Maven

Gradle

Gradle 核心概念总结(可选,目前国内还是使用 Maven 普遍一些)

Docker

Git

系统设计

基础

常用框架

Spring/SpringBoot(必看 :+1:)

知识点/面试题总结 :

重要知识点详解:

MyBatis

MyBatis 常见面试题总结

安全

认证授权

数据安全

定时任务

Java 定时任务详解

Web 实时消息推送

Web 实时消息推送详解

分布式

理论&算法&协议

RPC

ZooKeeper

这两篇文章可能有内容重合部分,推荐都看一遍。

API 网关

分布式 ID

分布式锁

分布式事务

分布式事务常见知识点&面试题总结

分布式配置中心

分布式配置中心常见知识点&面试题总结

高性能

数据库优化

负载均衡

负载均衡常见知识点&面试题总结

CDN

CDN(内容分发网络)常见知识点&面试题总结

消息队列

高可用

高可用系统设计指南

冗余设计

冗余设计详解

限流

服务限流详解

降级&熔断

降级&熔断详解

超时&重试

超时&重试详解

集群

相同的服务部署多份,避免单点故障。

灾备设计和异地多活

灾备 = 容灾 + 备份。

  • 备份:将系统所产生的所有重要数据多备份几份。
  • 容灾:在异地建立两个完全相同的系统。当某个地方的系统突然挂掉,整个应用系统可以切换到另一个,这样系统就可以正常提供服务了。

异地多活 描述的是将服务部署在异地并且服务同时对外提供服务。和传统的灾备设计的最主要区别在于“多活”,即所有站点都是同时在对外提供服务的。异地多活是为了应对突发状况比如火灾、地震等自然或者人为灾害。

Star 趋势

Star History Chart

公众号

如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。

JavaGuide 公众号


backend interview plan


title: 2026 最新版 Java 后端面试通关计划(涵盖后端通用体系) description: Java 后端面试复习计划,提供 4 周压缩版和 8 周标准版,覆盖项目与简历、Java 核心、MySQL、Redis、Spring、计算机基础、分布式、高可用与 JVM,并给出各阶段的产出和自测方法。 category: 面试准备 icon: mdi:star-outline head:

  • - meta
- name: keywords
  content: Java后端面试,面试准备计划,面试指南,八股文,校招,社招,项目经验,Java面试

把 JavaGuide 里的面试题从头看到尾,只完成了资料阅读。面试官通常从简历和项目开始问,再顺着里面的 Java、MySQL、Redis、Spring 或消息队列继续追问。只按知识库目录复习,很容易看了很多文章,轮到自己回答时仍然不知道从哪里讲起。

这份计划把项目和简历放在前面,技术知识跟着简历与目标岗位展开。计划分为 4 周压缩版和 8 周标准版,时间不够时删掉与岗位无关的扩展内容,不要把每个专题都压成走马观花。

先选 4 周还是 8 周

阶段4 周压缩版8 周标准版
前期检查第 1~2 天第 1~2 天
项目与简历第 1 周剩余时间第 1 周
Java、MySQL、Redis第 2 周第 2~4 周
Spring 与系统设计第 3 周前半段第 5 周
计算机基础与算法每天穿插,按岗位取舍第 6 周集中复习,算法每天保持练习
分布式、JVM 与线上排查第 3 周后半段至第 4 周,按简历选择第 7 周
模拟面试与查漏补缺最后 2 天第 8 周

4 周版本适合学过主要知识、现在需要集中复习的人;第一次系统学习 Java 后端知识,8 周也只是一个起点。每天能稳定拿出的时间不到 2 小时,优先选 8 周版本。已经开始投递或面试临近,可以走 4 周版本,但复习范围要跟着简历收缩:简历没有写 Kafka,岗位描述也没有相关要求,就不必在消息队列实现细节上花掉两三天。

算法不要留到最后突击。有笔试或代码题要求的岗位,从第一周开始保持练习;不考算法的岗位,把这部分时间留给项目、数据库和场景题。

什么程度才算会了

“看过”和“面试时能答”差得很远。同一个问题至少要经过下面三层:

层级自测方式
能回答不看资料,用 30~60 秒说出结论和关键词
能追问继续解释实现原理、适用条件、常见失败方式和替代方案
能落到项目说明项目中是否使用、为什么这样选、遇到过什么限制、如何验证结果

复习记录不必做得很复杂,保留“问题、资料链接、当前层级、没答好的点”四列即可。当天读完的内容至少做一次脱稿回答;答不上来再回原文查,不要用反复阅读代替回忆。

第 0 阶段:先把范围定下来

用 1~2 天完成三件事:确定目标岗位、检查简历、做一次摸底自测。

先找几份准备投递的岗位描述,记录反复出现的技能,再和简历逐项核对。复习范围主要来自两处:岗位明确要求什么,简历主动写了什么。简历上出现“熟悉 Redis”“负责订单模块”“使用 Kafka 处理异步任务”,后面就应该有对应问题和项目细节可以接住。

这一阶段至少留下四份材料:

  • 一份可以投递的 PDF 简历。
  • 30~60 秒的自我介绍提纲。
  • 每个项目的一张项目底稿。
  • 一份按优先级排列的待复习问题列表。

准备方法可以参考如何高效准备 Java 面试?和Java 后端面试重点总结。

简历还没定稿时,先看程序员简历编写指南;不要一边复习一边频繁往简历里增加新技术,否则复习范围会不断扩大。

第一阶段:项目与简历深挖

项目通常是技术追问的入口。项目讲不清,背再多组件原理也很难把回答接回自己的经历。

给每个重点项目整理下面这些内容:

内容要回答的问题
业务背景项目给谁使用,解决什么问题,核心链路是什么
个人职责哪些接口、表、任务或模块由自己负责,参与到什么程度
请求链路一次请求经过哪些服务、缓存、数据库和消息队列
技术选型为什么采用当前方案,比较过什么,付出了什么代价
难点或故障现象是什么,怎样定位、修复和验证
项目指标数据来自生产还是测试,统计口径和对照条件是什么
职责范围哪些部分由其他同事或团队负责

每个项目准备 30 秒和 3 分钟两个版本。30 秒版本讲业务、职责和一个重点;3 分钟版本补上核心链路、技术选型以及一个可以继续追问的问题。不要背逐字稿,记住顺序和关键词即可。具体写法见后端项目面试怎么讲?。

顺着项目里的每项技术继续列问题。例如使用 Redis 缓存商品信息,至少要准备 Key 设计、过期策略、缓存未命中、数据一致性和 Redis 不可用时的处理;写了线程池,就要能解释任务类型、核心参数、队列、拒绝策略以及下游承载能力。

项目结果可以量化,但数字必须有来源。没有生产指标时,可以在测试环境补测,并注明机器配置、数据量、并发模型和测试时长。不要给练手项目编造生产 QPS,也不要把只看过的模块写成自己负责。

没有实习或正式项目也可以准备。跟着课程完成的项目、二次开发的开源项目、课程设计和比赛项目都能写,重点是自己做过哪些改动:增加功能、调整表结构、补测试、修复缺陷或比较过不同方案。可以继续阅读项目经验指南、校招没有实习经历怎么办?和Java 优质开源实战项目。

完成这一阶段后,随机挑一个项目,脱稿回答下面四个问题:

  1. 这个项目解决什么问题,你负责什么?
  2. 一条核心请求怎样流转?
  3. 哪个技术选择最值得解释,为什么?
  4. 遇到过什么问题,结论由什么证据支持?

第二阶段:Java、MySQL 与 Redis

这三部分覆盖面很大,不适合平均分配时间。先做一轮随机抽题,哪个专题只能说出定义,就把时间补到哪里;已经能结合项目回答的内容只做复盘。

Java 基础、集合与并发

先看 Java 基础、集合和并发这三组文章:

基础部分要能解释常见概念和代码行为;集合重点放在选型、扩容、线程安全和常见误用;并发要能把线程状态、锁、JMM、ThreadLocal、线程池和异步任务串起来。简历涉及并发编程时,再深入看 JMM、线程池详解、ThreadLocal、AQS 和 CompletableFuture。

MySQL

MySQL 常见面试题总结适合作为主线。读到索引、事务和锁时,再进入专题文章:

索引题不要停在最左匹配和索引失效。给一条项目 SQL,说明查询条件、数据分布、执行计划、扫描行数以及最终怎样修改。事务题也要能落到代码:事务范围为什么过大、哪些调用不该放在事务里、Spring 事务在哪些情况下会失效。

Redis

先读Redis 常见面试题(上)和(下),再根据项目选择专题:

准备 Redis 时,不要只背数据类型。从项目里挑一条真实的缓存链路,试着完整讲一遍:请求如何读取缓存,未命中后从哪里查数据,查到后怎样回填,缓存多久过期,Redis 出故障时业务怎么兜底。

复习完后,把 Java、MySQL 和 Redis 混着抽题,每类各抽 5 道。每道题先用一两句话给出结论,然后继续追问两轮:“为什么?”“项目里怎么用?”哪道题卡住,就回到对应文章补那一个知识点,不必整章重读。

第三阶段:Spring 与系统设计

Spring、Spring Boot 和 MyBatis

Spring 的准备重点是项目里真实使用的功能。先看Spring 常见面试题和Spring Boot 常见面试题,再补下面这些专题:

自测时不要只解释注解含义。结合项目说明 Bean 怎样创建、AOP 用在哪里、事务边界如何划分、某个事务为什么会失效,以及 MyBatis 最终执行了什么 SQL。项目没有使用 Netty、响应式编程或复杂扩展点,不必为了覆盖面临时补进简历。

认证、授权与常见安全问题

简历涉及登录、权限或开放接口时,准备认证授权基础、JWT、SSO和权限系统设计。回答时讲清认证信息放在哪里、权限在什么位置校验、Token 如何失效,以及接口怎样防止越权和重复提交。

系统设计与场景题

系统设计题先确认需求和约束,再开始画组件。回答按这条顺序展开:

  1. 明确用户规模、请求量、延迟、可用性和一致性要求。
  2. 找出核心业务流程、数据模型和接口。
  3. 给出能工作的基础方案。
  4. 根据瓶颈增加缓存、异步、分片、限流或降级。
  5. 说明失败场景、数据一致性、监控和容量验证。

入门先看系统设计常见面试题总结、高性能系统设计面试题和高可用系统设计面试题。短链、秒杀、海量数据处理等完整场景可参考后端面试高频系统设计与场景题。

完成后选择两个题目口述,不看现成架构图。第一次先给基础方案,面试官增加流量、故障或一致性要求后再调整,重点讲清方案为什么变化。

第四阶段:计算机基础与算法

计算机基础的复习深度由岗位和面试流程决定。有笔试、算法面或手写代码环节,算法需要从第一周持续练习;岗位更关注业务开发,仍要保证网络、操作系统和常见数据结构能够回答。

算法与数据结构

先用算法专题确定范围,再练习二分查找、双指针与滑动窗口、DFS/BFS、回溯、动态规划和 Top K。

刷题时保留错题和边界条件,不追求只记模板。至少能解释时间复杂度,手写常见链表、树遍历、二分、哈希和堆相关题;简历写了某种数据结构,还要能说明它为什么适合当前场景。

计算机网络和操作系统

网络先过计算机网络常见面试题(上)和(下),再重点看从输入 URL 到页面展示的过程、HTTP 与 HTTPS、TCP 三次握手和四次挥手以及TCP 如何保证可靠传输。

操作系统以操作系统常见面试题(上)和(下)为主,重点检查进程与线程、虚拟内存、I/O、死锁和系统调用。不要只背定义,尝试把它们和 Java 线程、文件 I/O、网络请求、OOM 以及上下文切换联系起来。

第五阶段:分布式、高性能与高可用

这一阶段跟着简历和岗位走。项目是单体应用,岗位也没有分布式要求,掌握常见问题即可;简历写了微服务、消息队列、分布式锁或分库分表,对应专题就要能扛住追问。

简历或岗位出现的内容复习入口至少准备到什么程度
微服务、RPC微服务面试题、RPC 基础服务如何拆分,调用怎样超时和重试,故障如何隔离
网关、配置中心API 网关、分布式配置中心请求路由、鉴权、限流、配置推送与故障处理
分布式 ID、锁、事务分布式 ID、分布式锁、分布式事务选型条件、正确性风险、超时与失败恢复
消息队列消息队列面试题发送失败、重复消费、顺序、积压和下游容量
高并发与数据库优化高性能系统设计、SQL 优化瓶颈位置、容量上限、缓存与异步带来的代价
稳定性建设高可用系统设计、超时和重试、限流、幂等故障怎样传播,怎样止损,临时措施有什么副作用

CAP、BASE、一致性哈希、Raft 等理论用来解释具体设计,不必脱离项目背成长篇定义。从项目中挑一个分布式方案,回答为什么需要、为什么这样选、失败时会怎样,以及怎样证明它真的生效。

第六阶段:JVM 与线上问题排查

简历写了 JVM 调优、GC 优化、OOM 排查,或者岗位强调生产问题处理,这一阶段应提前到 Java 并发之后。缺少线上经验的校招生,至少要掌握内存区域、对象回收、类加载和常见诊断思路。

先用 JVM 常见面试题总结列出需要回答的问题,再补下面这些专题:

自测不要停在“堆里放对象、栈里放局部变量”。给自己一个具体告警,例如 CPU 飙高、Full GC 频繁或 OOM,说明先确认哪些指标、怎样保留现场、使用什么工具缩小范围、哪些操作可能扩大故障,以及修复后如何验证。

一周内怎样安排复习

每天的时间大致分成三块:一半用来阅读和理解,四分之一脱稿回答,剩余时间练项目表达或算法。当天读了多少页不重要,至少留下一个能复述的问题和一个仍然答不好的点。

每周安排一次 30~60 分钟的模拟面试。让对方从简历开始问,项目追问后再进入 Java、数据库和场景题。没有同伴时可以录音,也可以使用 AI 模拟追问,但回答结束后仍要回到文章、代码或官方文档核对事实。

复习过程中不断增加新资料,很容易让计划失控。一个专题保留一份主线资料和少量专题文章即可;同一道题看了三份答案仍然说不出来,应该开始脱稿回答,而不是继续收藏第四份。

面试前 1~2 天做什么

临近面试不要再开新专题,按简历和错题收口:

事项怎么做
自我介绍讲一遍 30~60 秒版本,确认经历、技术栈和求职方向一致
项目每个重点项目讲一遍 30 秒和 3 分钟版本,卡住的位置立即补材料
简历技术栈抽查写了“熟悉”或“掌握”的技术,确认能回答原理、限制和项目用法
高频错题只复盘自己的错题和薄弱点,不重新刷完整题库
代码与设备线上面试提前检查网络、摄像头、麦克风、共享屏幕和编程环境
岗位信息再看一次岗位描述,准备与岗位最相关的项目和问题

紧张会影响发挥时,可以参考面试太紧张怎么办?。

面试结束后怎么复盘

面试结束后尽快记下问题,不必追求完整还原。每道没答好的题记录五项:题目、当时怎么答、缺了什么、正确依据在哪里、下次怎样回答。项目追问卡住时,还要回到项目底稿补职责、代码位置、指标口径或方案限制。

下一场面试前只看这份复盘和原来的高优先级问题。连续几场都没有被问到、简历和岗位也没有出现的扩展内容,可以降级;反复出现的问题则进入主清单。复习范围会随着真实面试逐渐收敛。


teach you how to prepare for the interview hand in hand


title: 如何高效准备Java面试? description: 如何高效准备Java面试:从求职导向学习、技能清单制定到简历优化与面试冲刺,提供系统化备战方法,帮助你少走弯路、提高面试通过率。 category: 知识星球 icon: "mdi:map-marker-path" head:

  • - meta
- name: keywords
  content: Java面试准备,高效备战面试,求职导向学习,面试冲刺,简历优化,项目准备,校招,Java后端

::: tip 友情提示 本文节选自 《Java 面试指北》。这是一份教你如何更高效地准备面试的专栏,内容和 JavaGuide 互补,涵盖常见八股文(系统设计、常见框架、分布式、高并发 ……)、优质面经等内容。 :::

你身边是否有这样的朋友:编程能力比你强,求职结果却不如你?其实技术好≠面试能过 —— 如今的面试早已不是 “会写代码就行”,不做准备就去面,大概率是 “撞枪口”。

我们大多是普通开发者,没有顶会论文或竞赛大奖加持,面对 “面试造火箭,工作拧螺丝钉” 的常态,只能靠扎实准备突围。但准备面试不等于耍小聪明或者死记硬背面试题。 一定不要对面试抱有侥幸心理。打铁还需自身硬! 千万不要觉得自己看几篇面经,看几篇面试题解析就能通过面试了。一定要静下心来深入学习!

这篇文章就从宏观视角,带你搞懂程序员该如何系统准备面试:从求职导向学习,到简历优化、面试冲刺,帮你少走弯路,高效拿下心仪 offer。

尽早以求职为导向来学习

我是比较建议还在学校的同学尽可能早一点以求职为导向来学习的。

这样更有针对性,并且可以大概率减少自己处在迷茫的时间,很大程度上还可以让自己少走很多弯路。

但是!不要把“以求职为导向学习”理解为“我就不用学课堂上那些计算机基础课程了”!

我在之前的很多次分享中都强调过:一定要用心学习计算机基础知识!操作系统、计算机组成原理、计算机网络真的不是没有实际用处的学科!!!

你会发现大厂面试你会用到,以后工作之后你也会用到。我分别列举 2 个例子吧!

  • 面试中:像字节、腾讯这些大厂的技术面试以及几乎所有公司的笔试都会考操作系统相关的问题。
  • 工作中:在实际使用缓存的时候,软件层次而言的缓存思想,则是源自数据库速度、Redis(内存中间件)速度、本地内存速度之间的不匹配;而在计算机存储层次结构设计中,我们也能发现同样的问题及缓存思想的使用:内存用于解决磁盘访问速度过慢的问题,CPU 用三级缓存缓解寄存器和内存之间的速度差异。它们面临的都是同一个问题(速度不匹配)和同一个思想,那么计算机先驱者在存储层次结构设计上对缓存性能的优化措施,同样也适用于软件层次缓存的性能优化。

如何求职为导向学习呢? 简答来说就是:根据招聘要求整理一份目标岗位的技能清单,然后按照技能清单去学习和提升。

  1. 你首先搞清楚自己要找什么工作
  2. 然后根据招聘岗位的要求梳理一份技能清单
  3. 根据技能清单写好最终的简历
  4. 最后再按照简历的要求去学习和提升。

这其实也是 以终为始 思想的运用。

何为以终为始? 简单来说,以终为始就是我们可以站在结果来考虑问题,从结果出发,根据结果来确定自己要做的事情。

你会发现,其实几乎任何领域都可以用到 以终为始 的思想。

了解投递简历的黄金时间

面试之前,你肯定是先要搞清楚春招和秋招的具体时间的。

正所谓金三银四,金九银十,错过了这个时间,很多公司都没有 HC 了。

秋招一般 7 月份就开始了,大概一直持续到 9 月底。

春招一般 3 月份就开始了,大概一直持续到 4 月底。

很多公司(尤其大厂)到了 9 月中旬(秋招)/3 月中旬(春招),很可能就会没有 HC 了。面试的话一般都是至少是 3 轮起步,一些大厂比如阿里、字节可能会有 5 轮面试。面试失败话的不要紧,某一面表现差的话也不要紧,调整好心态。又不是单一选择对吧?你能投这么多企业呢! 调整心态。 今年面试的话,因为疫情原因,有些公司还是可能会还是集中在线上进行面试。然后,还是因为疫情的影响,可能会比往年更难找工作(对大厂影响较小)。

知道如何获取招聘信息

下面是常见的获取招聘信息的渠道:

  • 目标企业的官网/公众号:最及时最权威的获取招聘信息的途径。
  • 招聘网站:BOSS 直聘、智联招聘、拉勾招聘……。
  • 牛客网:每年秋招/春招,都会有大批量的公司会到牛客网发布招聘信息,并且还会有大量的公司员工来到这里发内推的帖子。地址: 。
  • 超级简历:超级简历目前整合了各大企业的校园招聘入口,地址:
  • 认识的朋友:如果你有认识的朋友在目标企业工作的话,你也可以找他们了解招聘信息,并且可以让他们帮你内推。
  • 宣讲会:宣讲会也是一个不错的途径,不过,好的企业通常只会去比较好的学校,可以留意一下意向公司的宣讲会安排或者直接去到一所比较好的学校参加宣讲会。像我当时校招就去参加了几场宣讲会。不过,我是在荆州上学,那边没什么比较好的学校,一般没有公司去开宣讲会。所以,我当时是直接跑到武汉来了,参加了武汉理工大学以及华中科技大学的几场宣讲会。总体感觉还是很不错的!
  • 其他:校园就业信息网、学校论坛、班级 or 年级 QQ 群。

校招的话,建议以官网为准,有宣讲会的话更好。社招的话,可以多留意一下各大招聘网站比如 BOSS 直聘、拉勾上的职位信息。

不论校招和社招,如果能找到比较靠谱的内推机会的话,获得面试的机会的概率还是非常大的。而且,你可以让内推你的人定向地给你一些建议。找内推的方式有很多,首选比较熟悉的朋友、同学,还可以留意技术交流社区和公众号上的内推信息。

一般是只能投递一个岗位,不过,也有极少数投递不同部门两个岗位的情况,这个应该不会有影响,但你的前一次面试情况可能会被记录,也就是说就算你投递成功两个岗位,第一个岗位面试失败的话,对第二个岗位也会有影响,很可能直接就被 pass。

多花点时间完善简历

一定一定一定要重视简历啊!朋友们!至少要花 2~3 天时间来专门完善自己的简历。

最近看了很多份简历,满意的很少,我简单拿出一份来说分析一下(欢迎在评论区补充)。

1.个人介绍没太多实用的信息。

技术博客、GitHub 以及在校获奖经历的话,能写就尽量写在这里。 你可以参考下面 👇 的模板进行修改:

2.项目经历过于简单,完全没有质量可言

每一个项目经历真的就一两句话可以描述了么?还是自己不想写?还是说不是自己做的,不敢多写。

如果有项目的话,技术面试第一步,面试官一般都是让你自己介绍一下你的项目。你可以从下面几个方向来考虑:

  1. 你对项目整体设计的一个感受(面试官可能会让你画系统的架构图)
  2. 你在这个项目中你负责了什么、做了什么、担任了什么角色。
  3. 从这个项目中你学会了那些东西,使用到了那些技术,学会了那些新技术的使用。
  4. 你在这个项目中是否解决过什么问题?怎么解决的?收获了什么?
  5. 你的项目用到了哪些技术?这些技术你吃透了没有?举个例子,你的项目经历使用了 Seata 来做分布式事务,那 Seata 相关的问题你要提前准备一下吧,比如说 Seata 支持哪些配置中心、Seata 的事务分组是怎么做的、Seata 支持哪些事务模式,怎么选择?
  6. 你在这个项目中犯过的错误,最后是怎么弥补的?

3.计算机二级这个证书对于计算机专业完全不用写了,没有含金量的。

4.技能介绍问题太大。

  • 技术名词最好规范大小写比较好,比如 java->Java ,spring boot -> Spring Boot 。这个虽然有些面试官不会介意,但是很多面试官都会在意这个细节的。
  • 技能介绍太杂,没有亮点。不需要全才,某个领域做得好就行了!
  • 对 Java 后台开发的部分技能比如 Spring Boot 的熟悉度仅仅为了解,无法满足企业的要求。

详细的程序员简历编写指南请参考:程序员简历到底该怎么写?。

岗位匹配度很重要

校招通常会对你的项目经历的研究方向比较宽容,即使你的项目经历和对应公司的具体业务没有关系,影响其实也并不大。

社招的话就不一样了,毕竟公司是要招聘可以直接来干活的人,你有相关的经验,公司会比较省事。社招通常会比较重视你的过往工作经历以及项目经历,HR 在筛选简历的时候会根据这两方面信息来判断你是否满足他们的招聘要求。就比如说你投递电商公司,而你之前的并没有和电商相关的工作经历以及项目经历,那 HR 在筛简历的时候很可能会直接把你 Pass 掉。

不过,这个也并不绝对,也有一些公司在招聘的时候更看重的是你的过往经历,较少地关注岗位匹配度,优秀公司的工作经历以及有亮点的项目经验都是加分项。这类公司相信你既然在某个领域(比如电商、支付)已经做的不错了,那应该也可以在另外一个领域(比如流媒体平台、社交软件)很快成为专家。这个领域指的不是技术领域,更多的是业务方向。横跨技术领域(比如后端转算法、后端转大数据)找工作,你又没有相关的经验,几乎是没办法找到的。即使找到了,也大概率会面临 HR 压薪资的问题。

提前准备技术面试

面试之前一定要提前准备一下常见的面试题也就是八股文:

  • 自己面试中可能涉及哪些知识点、那些知识点是重点。
  • 面试中哪些问题会被经常问到、面试中自己该如何回答。(强烈不推荐死记硬背,第一:通过背这种方式你能记住多少?能记住多久?第二:背题的方式的学习很难坚持下去!)

Java 后端面试复习的重点请看这篇文章:Java 面试重点总结(重要)。

不同类型的公司对于技能的要求侧重点是不同的比如腾讯、字节可能更重视计算机基础比如网络、操作系统这方面的内容。阿里、美团这种可能更重视你的项目经历、实战能力。

一定不要抱着一种思想,觉得八股文或者基础问题的考查意义不大。如果你抱着这种思想复习的话,那效果可能不会太好。实际上,个人认为还是很有意义的,八股文或者基础性的知识在日常开发中也会需要经常用到。例如,线程池这块的拒绝策略、核心参数配置什么的,如果你不了解,实际项目中使用线程池可能就用的不是很明白,容易出现问题。而且,其实这种基础性的问题是最容易准备的,像各种底层原理、系统设计、场景题以及深挖你的项目这类才是最难的!

八股文资料首推我的 《Java 面试指北》 (配合 JavaGuide 使用,会根据每一年的面试情况对内容进行更新完善)和 JavaGuide 。里面不仅仅是原创八股文,还有很多对实际开发有帮助的干货。除了我的资料之外,你还可以去网上找一些其他的优质的文章、视频来看。

《Java 面试指北》内容概览

提前准备手撕算法

很明显,国内现在的校招面试开始越来越重视算法了,尤其是像字节跳动、腾讯这类大公司。绝大部分公司的校招笔试是有算法题的,如果 AC 率比较低的话,基本就挂掉了。

社招的话,算法面试同样会有。不过,面试官可能会更看重你的工程能力,你的项目经历。如果你的其他方面都很优秀,但是算法很菜的话,不一定会挂掉。不过,还是建议刷下算法题,避免让其成为自己在面试中的短板。

社招往往是在技术面试的最后,面试官给你一个算法题目让你做。

关于如何准备算法面试《Java 面试指北》 的面试准备篇有详细介绍到。

《Java 面试指北》面试准备篇

提前准备自我介绍

自我介绍一般是你和面试官的第一次面对面正式交流,换位思考一下,假如你是面试官的话,你想听到被你面试的人如何介绍自己呢?一定不是客套地说说自己喜欢编程、平时花了很多时间来学习、自己的兴趣爱好是打球吧?

我觉得一个好的自我介绍至少应该包含这几点要素:

  • 用简洁的话说清楚自己主要的技术栈于擅长的领域;
  • 把重点放在自己在行的地方以及自己的优势之处;
  • 重点突出自己的能力比如自己的定位的 bug 的能力特别厉害;

简单来说就是用简洁的语言突出自己的亮点,也就是推销自己嘛!

  • 如果你去过大公司实习,那对应的实习经历就是你的亮点。
  • 如果你参加过技术竞赛,那竞赛经历就是你的亮点。
  • 如果你大学就接触过企业级项目的开发,实战经验比较多,那这些项目经历就是你的亮点。
  • ……

从社招和校招两个角度来举例子吧!我下面的两个例子仅供参考,自我介绍并不需要死记硬背,记住要说的要点,面试的时候根据公司的情况临场发挥也是没问题的。另外,网上一般建议的是准备好两份自我介绍:一份对 hr 说的,主要讲能突出自己的经历,会的编程技术一语带过;另一份对技术面试官说的,主要讲自己会的技术细节和项目经验。

社招:

面试官,您好!我叫独秀儿。我目前有 1 年半的工作经验,熟练使用 Spring、MyBatis 等框架、了解 Java 底层原理比如 JVM 调优并且有着丰富的分布式开发经验。离开上一家公司是因为我想在技术上得到更多的锻炼。在上一个公司我参与了一个分布式电子交易系统的开发,负责搭建了整个项目的基础架构并且通过分库分表解决了原始数据库以及一些相关表过于庞大的问题,目前这个网站最高支持 10 万人同时访问。工作之余,我利用自己的业余时间写了一个简单的 RPC 框架,这个框架用到了 Netty 进行网络通信, 目前我已经将这个项目开源,在 GitHub 上收获了 2k 的 Star! 说到业余爱好的话,我比较喜欢通过博客整理分享自己所学知识,现在已经是多个博客平台的认证作者。 生活中我是一个比较积极乐观的人,一般会通过运动打球的方式来放松。我一直都非常想加入贵公司,我觉得贵公司的文化和技术氛围我都非常喜欢,期待能与你共事!

校招:

面试官,您好!我叫秀儿。大学时间我主要利用课外时间学习了 Java 以及 Spring、MyBatis 等框架 。在校期间参与过一个考试系统的开发,这个系统的主要用了 Spring、MyBatis 和 shiro 这三种框架。我在其中主要担任后端开发,主要负责了权限管理功能模块的搭建。另外,我在大学的时候参加过一次软件编程大赛,我和我的团队做的在线订餐系统成功获得了第二名的成绩。我还利用自己的业余时间写了一个简单的 RPC 框架,这个框架用到了 Netty 进行网络通信, 目前我已经将这个项目开源,在 GitHub 上收获了 2k 的 Star! 说到业余爱好的话,我比较喜欢通过博客整理分享自己所学知识,现在已经是多个博客平台的认证作者。 生活中我是一个比较积极乐观的人,一般会通过运动打球的方式来放松。我一直都非常想加入贵公司,我觉得贵公司的文化和技术氛围我都非常喜欢,期待能与你共事!

减少抱怨

就像现在的技术面试一样,大家都说内卷了,抱怨现在的面试真特么难。然而,单纯抱怨有用么?你对其他求职者说:“大家都不要刷 Leetcode 了啊!都不要再准备高并发、高可用的面试题了啊!现在都这么卷了!”

会有人听你的么?你不准备面试,但是其他人会准备面试啊!那你是不是傻啊?还是真的厉害到不需要准备面试呢?

因此,准备 Java 面试的第一步,我们一定要尽量减少抱怨。抱怨的声音多了之后,会十分影响自己,会让自己变得十分焦虑。

面试之后及时复盘

如果失败,不要灰心;如果通过,切勿狂喜。面试和工作实际上是两回事,可能很多面试未通过的人,工作能力比你强的多,反之亦然。

面试就像是一场全新的征程,失败和胜利都是平常之事。所以,劝各位不要因为面试失败而灰心、丧失斗志。也不要因为面试通过而沾沾自喜,等待你的将是更美好的未来,继续加油!

总结

这篇文章内容有点多,如果这篇文章只能让你记住 7 句话,那请记住下面这 7 句:

  1. 一定要提前准备面试!技术面试不同于编程,编程厉害不代表技术面试就一定能过。
  2. 一定不要对面试抱有侥幸心理。打铁还需自身硬!千万不要觉得自己看几篇面经,看几篇面试题解析就能通过面试了。一定要静下心来深入学习!尤其是目标是大厂的同学,那更要深挖原理!
  3. 建议大学生尽可能早一点以求职为导向来学习的。这样更有针对性,并且可以大概率减少自己处在迷茫的时间,很大程度上还可以让自己少走很多弯路。 但是,不要把“以求职为导向学习”理解为“我就不用学课堂上那些计算机基础课程了”!
  4. 一定不要抱着一种思想,觉得八股文或者基础问题的考查意义不大。如果你抱着这种思想复习的话,那效果可能不会太好。实际上,个人认为还是很有意义的,八股文或者基础性的知识在日常开发中也会需要经常用到。例如,线程池这块的拒绝策略、核心参数配置什么的,如果你不了解,实际项目中使用线程池可能就用的不是很明白,容易出现问题。
  5. 手撕算法是当下技术面试的标配,尽早准备!
  6. 岗位匹配度很重要。校招通常会对你的项目经历的研究方向比较宽容,即使你的项目经历和对应公司的具体业务没有关系,影响其实也并不大。社招的话就不一样了,毕竟公司是要招聘可以直接来干活的人,你有相关的经验,公司会比较省事。
  1. 面试之后及时复盘。面试就像是一场全新的征程,失败和胜利都是平常之事。所以,劝各位不要因为面试失败而灰心、丧失斗志。也不要因为面试通过而沾沾自喜,等待你的将是更美好的未来,继续加油!

key points of interview


title: 2026最新版Java后端面试重点总结 description: Java后端面试重点总结:梳理校招/社招高频考点与复习优先级,覆盖Java基础、集合、并发、MySQL、Redis、Spring/Spring Boot、JVM与项目经验准备,帮你抓重点高效备战。 category: 面试准备 icon: mdi:star-outline head:

  • - meta
- name: keywords
  content: Java后端面试,面试重点,八股文,Java基础,Java集合,Java并发,MySQL,Redis,Spring Boot,项目经验

::: tip 友情提示 本文节选自 《Java 面试指北》。这是一份教你如何更高效地准备面试的专栏,内容和 JavaGuide 互补,涵盖常见八股文(系统设计、常见框架、分布式、高并发 ……)、优质面经等内容。 :::

Java 后端面试哪些知识点是重点?

准备面试的时候,具体哪些知识点是重点呢?如何把握重点?

先看下面这张全局图(后续会详细解读):

Java 后端面试重点

给你几点靠谱的建议:

  1. Java 基础、集合、并发、MySQL、Redis 、Spring、Spring Boot 这些 Java 后端开发必备的知识点(MySQL + Redis >= Java > Spring + Spring Boot)。大厂以及中小厂的面试问的比较多的就是这些知识点。Spring 和 Spring Boot 这俩框架类的知识点相对前面的知识点来说重要性要稍低一些,但一般面试也会问一些,尤其是中小厂。并发知识一般中大厂提问更多也更难,尤其是大厂喜欢深挖底层,很容易把人问倒。计算机基础相关的内容会在下面提到。
  2. 你的项目经历涉及到的知识点是重中之重,有水平的面试官都是会根据你的项目经历来问的。举个例子,你的项目经历使用了 Redis 来做限流,那 Redis 相关的八股文(比如 Redis 常见数据结构)以及限流相关的八股文(比如常见的限流算法)你就应该多花更多心思来搞懂吃透!你把项目经历上的知识点吃透之后,再把你简历上哪些写熟练掌握的技术给吃透,最后再去花时间准备其他知识点。
  3. 针对自身找工作的需求,你又可以适当地调整复习的重点。像中小厂一般问计算机基础比较少一些,有些大厂比如字节比较重视计算机基础尤其是算法。这样的话,如果你的目标是中小厂的话,计算机基础就准备面试来说不是那么重要了。如果复习时间不够的话,可以暂时先放放,腾出时间给其他重要的知识点。
  4. 一般校招的面试不会强制要求你会分布式/微服务、高并发的知识(不排除个别岗位有这方面的硬性要求),所以到底要不要掌握还是要看你个人当前的实际情况。如果你会这方面的知识的话,对面试相对来说还是会更有利一些(想要让项目经历有亮点,还是得会一些性能优化的知识。性能优化的知识这也算是高并发知识的一个小分支了)。如果你的技能介绍或者项目经历涉及到分布式/微服务、高并发的知识,那建议你尽量也要抽时间去认真准备一下,面试中很可能会被问到,尤其是项目经历用到的时候。不过,也还是主要准备写在简历上的那些知识点就好。
  5. JVM 相关的知识点,一般是大厂(例如美团、阿里)和一些不错的中厂(例如携程、顺丰、招银网络)才会问到,面试国企、差一点的中厂和小厂就没必要准备了。JVM 面试中比较常问的是 Java 内存区域、JVM 垃圾回收、类加载器和双亲委派模型 以及 JVM 调优和问题排查(我之前分享过一些常见的线上问题案例,里面就有 JVM 相关的)。
  6. 不同的大厂面试侧重点也会不同。比如说你要去阿里这种公司的话,项目和八股文就是重点,阿里笔试一般会有代码题,进入面试后就很少问代码题了,但是对原理性的问题问的比较深,经常会问一些你对技术的思考。再比如说你要面试字节这种公司,那计算机基础,尤其是算法是重点,字节的面试十分注重代码功底,有时候开始面试就会直接甩给你一道代码题,写出来再谈别的。也会问面试八股文,以及项目,不过,相对来说要少很多。
  7. 多去找一些面经看看,尤其你目标公司或者类似公司对应岗位的面经。这样可以实现针对性的复习,还能顺便自测一波,检查一下自己的掌握情况。

看似 Java 后端八股文很多,实际把复习范围一缩小,重要的东西就是那些。考虑到时间问题,你不可能连一些比较冷门的知识点也给准备了。这没必要,主要精力先放在那些重要的知识点即可。

如何更高效地准备八股文?

对于技术八股文来说,尽量不要死记硬背,这种方式非常枯燥且对自身能力提升有限!但是!想要一点不背是不太现实的,只是说要结合实际应用场景和实战来理解记忆。

我一直觉得面试八股文最好是和实际应用场景和实战相结合。很多同学现在的方向都错了,上来就是直接背八股文,硬生生学成了文科,那当然无趣了。

举个例子:你的项目中需要用到 Redis 来做缓存,你对照着官网简单了解并实践了简单使用 Redis 之后,你去看了 Redis 对应的八股文。你发现 Redis 可以用来做限流、分布式锁,于是你去在项目中实践了一下并掌握了对应的八股文。紧接着,你又发现 Redis 内存不够用的情况下,还能使用 Redis Cluster 来解决,于是你就又去实践了一下并掌握了对应的八股文。

一定要记住你的主要目标是理解和记关键词,而不是像背课文一样一字一句地记下来,这样毫无意义!效率最低,对自身帮助也最小!

还要注意适当“投机取巧”,不要单纯死记八股,有些技术方案的实现有很多种,例如分布式 ID、分布式锁、幂等设计,想要完全记住所有方案不太现实,你就重点记忆你项目的实现方案以及选择该种实现方案的原因就好了。当然,其他方案还是建议你简单了解一下,不然也没办法和你选择的方案进行对比。

想要检测自己是否搞懂或者加深印象,记录博客或者用自己的理解把对应的知识点讲给别人听也是一个不错的选择。

另外,准备八股文的过程中,强烈建议你花个几个小时去根据你的简历(主要是项目经历部分)思考一下哪些地方可能被深挖,然后把你自己的思考以面试问题的形式体现出来。面试之后,你还要根据当下的面试情况复盘一波,对之前自己整理的面试问题进行完善补充。这个过程对于个人进一步熟悉自己的简历(尤其是项目经历)部分,非常非常有用。这些问题你也一定要多花一些时间搞懂吃透,能够流畅地表达出来。面试问题可以参考 Java 面试常见问题总结(2024 最新版),记得根据自己项目经历去深入拓展即可!

最后,准备技术面试的同学一定要定期复习(自测的方式非常好),不然确实会遗忘的。

详细面试准备计划(后端通用)

Java 后端面试重点和详细准备计划


java roadmap


title: 2026最新版Java学习路线(4w+字) description: Java学习路线最新版:结合当下 Java 后端招聘要求,提供从基础到进阶的系统学习路径与资料建议,覆盖Java核心、数据库、缓存、中间件、框架与面试重点,帮助高效规划与提速上岸。 category: 面试准备 icon: mdi:map-marker-path head:

  • - meta
- name: keywords
  content: Java学习路线,Java后端路线,Java学习计划,校招准备,面试路线,Spring Boot,MySQL,Redis,JVM

::: tip 重要说明

本学习路线保持年度系统性修订,严格同步 Java 技术生态与招聘市场的最新动态,确保内容时效性与前瞻性。

:::

历时一个月精心打磨,笔者基于当下 Java 后端开发岗位招聘的最新要求,对既有学习路线进行了全面升级。本次升级涵盖技术栈增删、学习路径优化、配套学习资源更新等维度,力争构建出更符合 Java 开发者成长曲线的知识体系。

亮色板概览:

Java 学习路线 PDF 概览 - 亮色板

暗色板概览:

Java 学习路线 PDF 概览 - 暗色版

这可能是你见过的最用心、最全面的 Java 后端学习路线。这份学习路线共包含 4w+ 字,但你完全不用担心内容过多而学不完。我会根据学习难度,划分出适合找小厂工作必学的内容,以及适合逐步提升 Java 后端开发能力的学习路径。

Java 学习路线图

对于初学者,你可以按照这篇文章推荐的学习路线和资料进行系统性的学习;对于有经验的开发者,你可以根据这篇文章更一步地深入学习 Java 后端开发,提升个人竞争力。

在看这份学习路线的过程中,建议搭配 Java 面试重点总结(重要),可以让你在学习过程中更有目的性。

由于这份学习路线内容太多,因此我将其整理成了 PDF 版本(共 55 页),方便大家阅读。这份 PDF 有黑夜和白天两种阅读版本,满足大家的不同需求。

这份学习路线的获取方法很简单:直接在公众号「JavaGuide」后台回复“路线”即可获取。

JavaGuide 官方公众号


resume guide


title: 程序员简历编写指南 description: 程序员简历编写指南:从筛选逻辑出发讲清简历结构、项目经历与技能描述写法,提供简历模板与避坑建议,帮助你提高简历通过率并让面试官更好地深挖你的亮点。 category: 面试准备 icon: "mdi:account-tie-outline" head:

  • - meta
- name: keywords
  content: 程序员简历,Java简历,简历优化,项目经历写法,简历模板,校招简历,社招简历,面试准备

::: tip 友情提示 本文节选自 《Java 面试指北》。这是一份教你如何更高效地准备面试的小册,涵盖常见八股文(系统设计、常见框架、分布式、高并发 ……)、优质面经等内容。 :::

前言

一份好的简历可以在整个申请面试以及面试过程中起到非常重要的作用。

为什么说简历很重要呢? 我们可以从下面几点来说:

1、简历就像是我们的一个门面一样,它在很大程度上决定了是否能够获得面试机会。

  • 假如你是网申,你的简历必然会经过 HR 的筛选,一张简历 HR 可能也就花费 10 秒钟左右看一下,然后决定你能否进入面试。
  • 假如你是内推,如果你的简历没有什么优势的话,就算是内推你的人再用心,也无能为力。

另外,就算你通过了第一轮的筛选获得面试机会,后面的面试中,面试官也会根据你的简历来判断你究竟是否值得他花费很多时间去面试。

2、简历上的内容很大程度上决定了面试官提问的侧重点。

  • 一般情况下你的简历上注明你会的东西才会被问到(Java 基础、集合、并发、MySQL、Redis 、Spring、Spring Boot 这些算是每个人必问的),比如写了你熟练使用 Redis,那面试官就很大概率会问你 Redis 的一些问题,再比如你写了你在项目中使用了消息队列,那面试官大概率问很多消息队列相关的问题。
  • 技能熟练度在很大程度上也决定了面试官提问的深度。

在不夸大自己能力的情况下,写出一份好的简历也是一项很棒的能力。一般情况下,技术能力和学习能力比较厉害的,写出来的简历也比较棒!

简历模板

简历的样式真的非常非常重要!!!如果你的简历样式丑到没朋友的话,面试官真的没有看下去的欲望。一天处理上百份的简历的痛苦,你不懂!

我这里的话,推荐大家使用 Markdown 语法写简历,然后再将 Markdown 格式转换为 PDF 格式后进行简历投递。如果你对 Markdown 语法不太了解的话,可以花半个小时简单看一下 Markdown 语法说明: 。

下面是我收集的一些还不错的简历模板:

  • 适合中文的简历模板收集(推荐,开源免费):
  • 木及简历(推荐,部分免费) :
  • 简单简历(推荐,部分免费):
  • 极简简历(免费):
  • Markdown 简历排版工具(开源免费):
  • 站长简历(收费,支持 AI 生成):
  • typora+markdown+css 自定义简历模板 :
  • 超级简历(部分收费) :

上面这些简历模板大多是只有 1 页内容,很难展现足够的信息量。如果你不是顶级大牛(比如 ACM 大赛获奖)的话,我建议还是尽可能多写一点可以突出你自己能力的内容(校招生 2 页之内,社招生 3 页之内,记得精炼语言,不要过多废话)。

再总结几点 简历排版的注意事项:

  • 尽量简洁,不要太花里胡哨。
  • 技术名词最好规范大小写比较好,比如 java->Java ,spring boot -> Spring Boot 。这个虽然有些面试官不会介意,但是很多面试官都会在意这个细节的。
  • 中文和数字英文之间加上空格的话看起来会舒服一点。

另外,知识星球里还有真实的简历模板可供参考,地址: (需加入知识星球获取)。

简历内容

个人信息

  • 最基本的 :姓名(身份证上的那个)、年龄、电话、籍贯、联系方式、邮箱地址
  • 潜在加分项 : Github 地址、博客地址(如果技术博客和 Github 上没有什么内容的话,就不要写了)

示例:

简历要不要放照片呢? 很多人写简历的时候都有这个问题。

其实放不放都行,影响不大,完全不用在意这个问题。除非,你投递的岗位明确要求要放照片。 不过,如果要放的话,不要放生活照,还是应该放正规一些的照片比如证件照。

求职意向

你想要应聘什么岗位,希望在什么城市。另外,你也可以将求职意向放到个人信息这块写。

示例:

教育经历

教育经历也不可或缺。通过教育经历的介绍,你要确保能让面试官就可以知道你的学历、专业、毕业学校以及毕业的日期。

示例:

北京理工大学 硕士,软件工程 2019.09 - 2022.01 湖南大学 学士,应用化学 2015.09 ~ 2019.06

专业技能

先问一下你自己会什么,然后看看你意向的公司需要什么。一般 HR 可能并不太懂技术,所以他在筛选简历的时候可能就盯着你专业技能的关键词来看。对于公司有要求而你不会的技能,你可以花几天时间学习一下,然后在简历上可以写上自己了解这个技能。

下面是一份最新的 Java 后端开发技能清单,你可以根据自身情况以及岗位招聘要求做动态调整,核心思想就是尽可能满足岗位招聘的所有技能要求。

Java 后端技能模板

我这里再单独放一个我看过的某位同学的技能介绍,我们来找找问题。

上图中的技能介绍存在的问题:

  • 技术名词最好规范大小写比较好,比如 java->Java ,spring boot -> Spring Boot 。这个虽然有些面试官不会介意,但是很多面试官都会在意这个细节的。
  • 技能介绍太杂,没有亮点。不需要全才,某个领域做得好就行了!
  • 对 Java 后台开发的部分技能比如 Spring Boot 的熟悉度仅仅为了解,无法满足企业的要求。

实习经历/工作经历(重要)

工作经历针对社招,实习经历针对校招。

工作经历建议采用时间倒序的方式来介绍。实习经历和工作经历都需要简单突出介绍自己在职期间主要做了什么。

示例:

XXX 公司 (201X 年 X 月 ~ 201X 年 X 月 ) - 职位:Java 后端开发工程师 - 工作内容:主要负责 XXX

项目经历(重要)

简历上有一两个项目经历很正常,但是真正能把项目经历很好的展示给面试官的非常少。

很多求职者的项目经历介绍都会面临过于啰嗦、过于简单、没突出亮点等问题。

项目经历介绍模板如下:

项目名称(字号要大一些) 2017-05~2018-06 淘宝 Java 后端开发工程师 - 项目描述 : 简单描述项目是做什么的。 - 技术栈 :用了什么技术(如 Spring Boot + MySQL + Redis + Mybatis-plus + Spring Security + Oauth2) - 工作内容/个人职责 : 简单描述自己做了什么,解决了什么问题,带来了什么实质性的改善。突出自己的能力,不要过于平淡的叙述。 - 个人收获(可选) : 从这个项目中你学会了那些东西,使用到了那些技术,学会了那些新技术的使用。通常是可以不用写个人收获的,因为你在个人职责介绍中写的东西已经表明了自己的主要收获。 - 项目成果(可选) :简单描述这个项目取得了什么成绩。

1、项目经历应该突出自己做了什么,简单概括项目基本情况。

项目介绍尽量压缩在两行之内,不需要介绍太多,但也不要随便几个字就介绍完了。

另外,个人收获和项目成果都是可选的,如果选择写的话,也不要花费太多篇幅,记住你的重点是介绍工作内容/个人职责。

2、技术架构直接写技术名词就行,不要再介绍技术是干嘛的了,没意义,属于无效介绍。

3、尽量减少纯业务的个人职责介绍,对于面试不太友好。尽量再多挖掘一些亮点(6~8 条个人职责介绍差不多了,做好筛选),最好可以体现自己的综合素质,比如你是如何协调项目组成员协同开发的或者在遇到某一个棘手的问题的时候你是如何解决的又或者说你在这个项目优化了某个模块的性能。

即使不是你做的功能模块或者解决的问题,你只要搞懂吃透了就能拿来自己用,适当润色即可!

像性能优化方向上的亮点面试之前也比较容易准备,但也不要都是性能优化相关的,这种也算是一个极端。

另外,技术优化取得的成果尽量要量化一下:

  • 使用 xxx 技术解决了 xxx 问题,系统 QPS 从 xxx 提高到了 xxx。
  • 使用 xxx 技术了优化了 xxx 接口,系统 QPS 从 xxx 提高到了 xxx。
  • 使用 xxx 技术解决了 xxx 问题,查询速度优化了 xxx,系统 QPS 达到 10w+。
  • 使用 xxx 技术优化了 xxx 模块,响应时间从 2s 降低到 0.2s。
  • ……

个人职责介绍示例(这里只是举例,不要照搬,结合自己项目经历自己去写,不然面试的时候容易被问倒) :

  • 基于 Spring Cloud Gateway + Spring Security OAuth2 + JWT 实现微服务统一认证授权和鉴权,使用 RBAC 权限模型实现动态权限控制。
  • 参与项目订单模块的开发,负责订单创建、删除、查询等功能,基于 Spring 状态机实现订单状态流转。
  • 商品和订单搜索场景引入 Elasticsearch,并且实现了相关商品推荐以及搜索提示功能。
  • 整合 Canal + RabbitMQ 将 MySQL 增量数据(如商品、订单数据)同步到 Elasticsearch。
  • 利用 RabbitMQ 官方提供的延迟队列插件实现延时任务场景比如订单超时自动取消、优惠券过期提醒、退款处理。
  • 消息推送系统引入 RabbitMQ 实现异步处理、削峰填谷和服务解耦,最高推送速度 10w/s,单日最大消息量 2000 万。
  • 使用 MAT 工具分析 dump 文件解决了广告服务新版本上线后导致大量的服务超时告警的问题。
  • 排查并解决扣费模块由于扣费父任务和反作弊子任务使用同一个线程池导致的死锁问题。
  • 基于 EasyExcel 实现广告投放数据的导入导出,通过 MyBatis 批处理插入数据,基于任务表实现异步。
  • 负责用户统计模块的开发,使用 CompletableFuture 并行加载后台用户统计模块的数据信息,平均相应时间从 3.5s 降低到 1s。
  • 基于 Sentinel 对核心场景(如用户登入注册、收货地址查询等)进行限流、降级,保护系统,提升用户体验。
  • 热门数据(如首页、热门博客)使用 Redis+Caffeine 两级缓存,解决了缓存击穿和穿透问题,查询速度毫秒级,QPS 30w+。
  • 使用 CompletableFuture 优化购物车查询模块,对获取用户信息、商品详情、优惠券信息等异步 RPC 调用进行编排,响应时间从 2s 降低为 0.2s。
  • 搭建 EasyMock 服务,用于模拟第三方平台接口,方便了在网络隔离情况下的接口对接工作。
  • 基于 SkyWalking + Elasticsearch 搭建分布式链路追踪系统实现全链路监控。

4、如果你觉得你的项目技术比较落后的话,可以自己私下进行改进。重要的是让项目比较有亮点,通过什么方式就无所谓了。

项目经历这部分对于简历来说非常重要,《Java 面试指北》的面试准备篇有好几篇关于优化项目经历的文章,建议你仔细阅读一下,应该会对你有帮助。

5、避免个人职责介绍都是围绕一个技术点来写,非常不可取。

6、避免模糊性描述,介绍要具体(技术+场景+效果),也要注意精简语言(避免堆砌技术词,省略不必要的描述)。

荣誉奖项(可选)

如果你有含金量比较高的竞赛(比如 ACM、阿里的天池大赛)的获奖经历的话,荣誉奖项这块内容一定要写一下!并且,你还可以将荣誉奖项这块内容适当往前放,放在一个更加显眼的位置。

校园经历(可选)

如果有比较亮眼的校园经历的话就简单写一下,没有就不写!

个人评价

个人评价就是对自己的解读,一定要用简洁的语言突出自己的特点和优势,避免废话! 像勤奋、吃苦这些比较虚的东西就不要扯了,面试官看着这种个人评价就烦。

我们可以从下面几个角度来写个人评价:

  • 文档编写能力、学习能力、沟通能力、团队协作能力
  • 对待工作的态度以及个人的责任心
  • 能承受的工作压力以及对待困难的态度
  • 对技术的追求、对代码质量的追求
  • 分布式、高并发系统开发或维护经验

列举 3 个实际的例子:

  • 学习能力较强,大三参加国家软件设计大赛的时候快速上手 Python 写了一个可配置化的爬虫系统。
  • 具有团队协作精神,大三参加国家软件设计大赛的时候协调项目组内 5 名开发同学,并对编码遇到困难的同学提供帮助,最终顺利在 1 个月的时间完成项目的核心功能。
  • 项目经验丰富,在校期间主导过多个企业级项目的开发。

STAR 法则和 FAB 法则

STAR 法则(Situation Task Action Result)

相信大家一定听说过 STAR 法则。对于面试,你可以将这个法则用在自己的简历以及和面试官沟通交流的过程中。

STAR 法则由下面 4 个单词组成(STAR 法则的名字就是由它们的首字母组成):

  • Situation: 情景。 事情是在什么情况下发生的?
  • Task: 任务。你的任务是什么?
  • Action: 行动。你做了什么?
  • Result: 结果。最终的结果怎样?

FAB 法则(Feature Advantage Benefit)

除了 STAR 法则,你还需要了解在销售行业经常用到的一个叫做 FAB 的法则。

FAB 法则由下面 3 个单词组成(FAB 法则的名字就是由它们的首字母组成):

  • Feature: 你的特征/优势是什么?
  • Advantage: 比别人好在哪些地方;
  • Benefit: 如果雇佣你,招聘方会得到什么好处。

简单来说,FAB 法则主要是让你的面试官知道你的优势和你能为公司带来的价值。

建议

避免页数过多

精简表述,突出亮点。校招简历建议不要超过 2 页,社招简历建议不要超过 3 页。如果内容过多的话,不需要非把内容压缩到一页,保持排版干净整洁就可以了。

看了几千份简历,有少部分同学的简历页数都接近 10 页了,让我头皮发麻。

简历页数过多

避免语义模糊

尽量避免主观表述,少一点语义模糊的形容词。表述要简洁明了,简历结构要清晰。

举例:

  • 不好的表述:我在团队中扮演了很重要的角色。
  • 好的表述:我作为后端技术负责人,领导团队完成后端项目的设计与开发。

注意简历样式

简历样式同样很重要,一定要注意!不必追求花里胡哨,但要尽量保证结构清晰且易于阅读。

其他

  • 一定要使用 PDF 格式投递,不要使用 Word 或者其他格式投递。这是最基本的!
  • 不会的东西就不要写在简历上了。注意简历真实性,适当润色没有问题。
  • 工作经历建议采用时间倒序的方式来介绍,实习经历建议将最有价值的放在最前面。
  • 将自己的项目经历完美的展示出来非常重要,重点是突出自己做了什么(挖掘亮点),而不是介绍项目是做什么的。
  • 项目经历建议以时间倒序排序,另外项目经历不在于多(精选 2~3 即可),而在于有亮点。
  • 准备面试的过程中应该将你写在简历上的东西作为重点,尤其是项目经历上和技能介绍上的。
  • 面试和工作是两回事,聪明的人会把面试官往自己擅长的领域领,其他人则被面试官牵着鼻子走。虽说面试和工作是两回事,但是你要想要获得自己满意的 offer ,你自身的实力必须要强。

简历修改

到目前为止,我至少帮助 6000+ 位球友提供了免费的简历修改服务。由于个人精力有限,修改简历仅限加入星球的读者,需要帮看简历的话,可以加入 JavaGuide 官方知识星球(点击链接查看详细介绍)。

img

虽然收费只有培训班/训练营的百分之一,但是知识星球里的内容质量更高,提供的服务也更全面,非常适合准备 Java 面试和学习 Java 的同学。

下面是星球提供的部分服务(点击下方图片即可获取知识星球的详细介绍):

星球服务

这里再提供一份限时专属优惠卷:

知识星球30元优惠卷


project experience guide


title: 项目经验指南 description: 项目经验指南:针对没有项目/项目平淡的求职者,给出获取实战项目经验的方法与选择建议,并讲清如何做出项目亮点、如何复盘与表达,提升简历与面试竞争力。 category: 面试准备 icon: "mdi:projector-screen-outline" head:

  • - meta
- name: keywords
  content: 项目经验,校招项目,实战项目,项目亮点,简历项目描述,后端项目,面试项目准备,项目复盘

::: tip 友情提示 本文节选自 《Java 面试指北》。这是一份教你如何更高效地准备面试的专栏,内容和 JavaGuide 互补,涵盖常见八股文(系统设计、常见框架、分布式、高并发 ……)、优质面经等内容。 :::

没有项目经验怎么办?

没有项目经验是大部分应届生会碰到的一个问题。甚至说,有很多有工作经验的程序员,对自己在公司做的项目不满意,也想找一个比较有技术含量的项目来做。

说几种我觉得比较靠谱的获取项目经验的方式,希望能够对你有启发。

实战项目视频/专栏

在网上找一个符合自己能力与找工作需求的实战项目视频或者专栏,跟着老师一起做。

你可以通过慕课网、哔哩哔哩、拉勾、极客时间、培训机构(比如黑马、尚硅谷)等渠道获取到适合自己的实战项目视频/专栏。

慕课网实战课

尽量选择一个适合自己的项目,没必要必须做分布式/微服务项目,对于绝大部分同学来说,能把一个单机项目做好就已经很不错了。

我面试过很多求职者,简历上看着有微服务的项目经验,结果随便问两个问题就知道根本不是自己做的或者说做的时候压根没认真思考。这种情况会给我留下非常不好的印象。

我在 《Java 面试指北》 的「面试准备篇」中也说过:

个人认为也没必要非要去做微服务或者分布式项目,不一定对你面试有利。微服务或者分布式项目涉及的知识点太多,一般人很难吃透。并且,这类项目其实对于校招生来说稍微有一点超标了。即使你做出来,很多面试官也会认为不是你独立完成的。 其实,你能把一个单体项目做到极致也很好,对于个人能力提升不比做微服务或者分布式项目差。如何做到极致?代码质量这里就不提了,更重要的是你要尽量让自己的项目有一些亮点(比如你是如何提升项目性能的、如何解决项目中存在的一个痛点的),项目经历取得的成果尽量要量化一下比如我使用 xxx 技术解决了 xxx 问题,系统 qps 从 xxx 提高到了 xxx。

跟着老师做的过程中,你一定要有自己的思考,不要浅尝辄止。对于很多知识点,别人的讲解可能只是满足项目就够了,你自己想多点知识的话,对于重要的知识点就要自己学会去深入学习。

实战类开源项目

GitHub 或者码云上面有很多实战类别项目,你可以选择一个来研究,为了让自己对这个项目更加理解,在理解原有代码的基础上,你可以对原有项目进行改进或者增加功能。

你可以参考 Java 优质开源实战项目 上面推荐的实战类开源项目,质量都很高,项目类型也比较全面,涵盖博客/论坛系统、考试/刷题系统、商城系统、权限管理系统、快速开发脚手架以及各种轮子。

Java 优质开源实战项目

一定要记住:不光要做,还要改进,改善。不论是实战项目视频或者专栏还是实战类开源项目,都一定会有很多可以完善改进的地方。

从头开始做

自己动手去做一个自己想完成的东西,遇到不会的东西就临时去学,现学现卖。

这个要求比较高,我建议你已经有了一个项目经验之后,再采用这个方法。如果你没有做过项目的话,还是老老实实采用上面两个方法比较好。

参加各种大公司组织的各种大赛

如果参加这种赛事能获奖的话,项目含金量非常高。即使没获奖也没啥,也可以写简历上。

阿里云天池大赛

参与实际项目

通常情况下,你有如下途径接触到企业实际项目的开发:

  1. 老师接的项目;
  2. 自己接的私活;
  3. 实习/工作接触到的项目;

老师接的项目和自己接的私活通常都是一些偏业务的项目,很少会涉及到性能优化。这种情况下,你可以考虑对项目进行改进,别怕花时间,某个时间用心做好一件事情就好比如你对项目的数据模型进行改进、引入缓存提高访问速度等等。

实习/工作接触到的项目类似,如果遇到一些偏业务的项目,也是要自己私下对项目进行改进优化。

尽量是真的对项目进行了优化,这本身也是对个人能力的提升。如果你实在是没时间去实践的话,也没关系,吃透这个项目优化手段就好,把一些面试可能会遇到的问题提前准备一下。

有没有还不错的项目推荐?

《Java 面试指北》 的「面试准备篇」中有一篇文章专门整理了一些比较高质量的实战项目,包含业务项目、轮子项目、国外公开课 Lab 和视频类实战项目教程推荐,非常适合用来学习或者作为项目经验。

优质 Java 实战项目推荐

这篇文章一共推荐了 15+ 个实战项目,有业务类的,也有轮子类的,有开源项目、也有视频教程。对于参加校招的小伙伴,我更建议做一个业务类项目加上一个轮子类的项目。

我跟着视频做的项目会被面试官嫌弃不?

很多应届生都是跟着视频做的项目,这个大部分面试官都心知肚明。

不排除确实有些面试官不吃这一套,这个也看人。不过我相信大多数面试官都是能理解的,毕竟你在学校的时候实际上是没有什么获得实际项目经验的途径的。

大部分应届生的项目经验都是自己在网上找的或者像你一样买的付费课程跟着做的,极少部分是比较真实的项目。 从你能想着做一个实战项目来说,我觉得初衷是好的,确实也能真正学到东西。 但是,究竟有多少是自己掌握了很重要。看视频最忌讳的是被动接受,自己多改进一下,多思考一下!就算是你跟着视频做的项目,也是可以优化的!

如果你想真正学到东西的话,建议不光要把项目单纯完成跑起来,还要去自己尝试着优化!

简单说几个比较容易的优化点:

  1. 全局异常处理:很多项目这方面都做的不是很好,可以参考我的这篇文章:《使用枚举简单封装一个优雅的 Spring Boot 全局异常处理!》 来做优化。
  2. 项目的技术选型优化:比如使用 Guava 做本地缓存的地方可以换成 Caffeine 。Caffeine 的各方面的表现要更加好!再比如 Controller 层是否放了太多的业务逻辑。
  3. 数据库方面:数据库设计可否优化?索引是否使用使用正确?SQL 语句是否可以优化?是否需要进行读写分离?
  4. 缓存:项目有没有哪些数据是经常被访问的?是否引入缓存来提高响应速度?
  5. 安全:项目是否存在安全问题?
  6. ……

另外,我在星球分享过常见的性能优化方向实践案例,涉及到多线程、异步、索引、缓存等方向,强烈推荐你看看: 。

最后,再给大家推荐一个 IDEA 优化代码的小技巧,超级实用!

分析你的代码:右键项目-> Analyze->Inspect Code

扫描完成之后,IDEA 会给出一些可能存在的代码坏味道比如命名问题。

并且,你还可以自定义检查规则。

项目做完后怎么准备项目深挖?

把项目做完,只解决了“有没有项目”的问题。技术面试还会继续追问项目架构、个人职责、技术选型、核心链路、性能指标和线上故障。建议给简历上的每个重点项目准备 30 秒和 3 分钟两个版本的介绍,再沿着自己使用的数据库、缓存、消息队列和线程池逐项追问。

具体准备方法可以参考:《后端项目面试怎么讲?从项目介绍到技术难点和故障复盘》。文章中的订单系统只用于演示回答结构,项目职责和指标仍然要换成自己的真实材料。

AI Agent 项目怎么准备?

如果准备的是 AI Agent 项目,除了讲清自己做了什么,还要能回答为什么使用 Agent、请求如何流转、工具写操作如何兜底,以及一次失败是怎么定位和修复的。可以继续看 《Agent 项目面试怎么讲?从系统架构、技术选型到 Badcase 复盘》。文章里的案例只能作为组织答案的参考,项目职责和指标仍然要以自己的真实经历为准。


how to handle interview nerves


title: 面试太紧张怎么办? description: 面试太紧张影响发挥怎么办?从心态调整、提前准备到模拟面试与表达训练,提供一套可落地的方法,帮助你降低焦虑、提升临场表现,更稳定地通过技术面试。 category: 面试准备 icon: "mdi:shield-lock-outline" head:

  • - meta
- name: keywords
  content: 面试紧张,技术面试,面试心态,临场发挥,模拟面试,表达训练,面试准备,校招

很多小伙伴在第一次技术面试时都会感到紧张甚至害怕,遇到稍微刁钻的问题大脑就一片空白,面试结束后还会有种“懵懵的”感觉。我也经历过类似的状况,对这种手心出汗、语无伦次的窘境深有体会。

其实,紧张是非常正常的生理和心理反应——它代表你对这次机会的重视,也源于人类对未知结果的天然担忧。但如果任由过度紧张蔓延,绝对会大幅折损你的临场发挥水平。

下面,我将结合自己的实战经验,从心态重塑、战术准备、临场应对、面后复盘四个维度,分享一套可落地的“抗紧张”指南。

试着接受紧张情绪,调整心态

首先要明白,紧张是正常情绪,特别是初次或前几次面试时,多少都会有点忐忑。不要过分排斥这种情绪,可以适当地“拥抱”它:

  • 搞清楚面试的本质:面试本质上是一场与面试官的深入交流,是一个双向选择的过程。面试失败并不意味着你的价值和努力被否定,而可能只是因为你与目标岗位暂时不匹配,或者仅仅是一次 KPI 面试,这家公司可能压根就没有真正的招聘需求。失败的原因也可能是某些知识点、项目经验或表达方式未能充分展现出你的能力。即便这次面试未通过,也不妨碍你继续尝试其他公司,完全不慌!
  • 不要害怕面试官:很多求职者平时和同学朋友交流沟通的蛮好,一到面试就害怕了。面试官和求职者双方是平等的,以后说不定就是同事关系。也不要觉得面试官就很厉害,实际上,面试官的水平也参差不齐。他们提出的问题,可能自己也没有完全理解。
  • 给自己积极的心理暗示:告诉自己“有点紧张没关系,这只能让我更专注,心跳加快是我在给自己打气,我一定可以回答的很好!”。

提前准备,减少不确定性

不确定性越多,越容易紧张。 如果你能够在面试前做充分的准备,很多“未知”就会消失,紧张情绪自然会减轻很多。

认真准备技术面试

  • 优先梳理核心知识点:比如计算基础、数据库、Java 基础、Java 集合、并发编程、SpringBoot(这里以 Java 后端方向为例)等。如果时间不够,可以分轻重缓急,有重点地复习。如果你想要系统准备 Java 后端面试但又不知道如何开始的,可以参考 Java 后端面试通关计划(后端通用)。
  • 精心准备项目经历:认真思考你简历上最重要的项目(面试以前两个项目为主,尤其是第一个),它们的技术难点、业务逻辑、架构设计,以及可能被面试官深挖的点。把你的思考总结成可能出现的面试问题,并尝试回答。

模拟面试和自测

  • 约朋友或同学互相提问:以真实的面试场景来进行演练,并及时对回答进行诊断和反馈。
  • 线上练习:直接利用 AI 来进行模拟面试即可,免费且高效。把自己的简历投喂给它,让它根据你的简历,尤其是项目经历生成面试问题。
  • 面经:平时可以多看一些前辈整理的面经,尤其是目标岗位或目标公司的面经,总结高频考点和常见问题。
  • 技术面试题自测:在 《Java 面试指北》 的 「技术面试题自测篇」 ,我总结了 Java 面试中最重要的知识点的最常见的面试题并按照面试提问的方式展现出来。其中,每一个问题都有提示和重要程度说明,非常适合用来自测。

《Java 面试指北》 的 「技术面试题自测篇」概览:

技术面试题自测篇

多表达

平时要多说,多表达出来,不要只是在心里面想,不然真正面试的时候会发现想的和说的不太一样。

我前面推荐的模拟面试和自测,有一部分原因就是为了能够多多表达。

多面试

  • 先小厂后大厂:可以先去一些规模较小或者对你来说压力没那么大的公司试试手,积累一些实战经验,增加一些信心;等熟悉了面试流程、能够更从容地回答问题后,再去挑战自己心仪的大厂或热门公司。
  • 积累“失败经验”:不要怕被拒,有些时候被拒绝却能从中学到更多。多复盘,多思考到底是哪个环节出了问题,再用更好的状态迎接下一次面试。

保证休息

  • 留出充裕时间:面试前尽量不要排太多事情,保证自己能有个好状态去参加面试。
  • 保证休息:充足睡眠有助于情绪稳定,也能让你在面试时更清晰地思考问题。

遇到不会的问题不要慌

一场面试,不太可能面试官提的每一个问题你都能轻松应对,除非这场面试非常简单。

在面试过程中,遇到不会的问题,首先要做的是快速回顾自己过往的知识,看是否能找到突破口。如果实在没有思路的话,可以真诚地向面试要一些提示比如谈谈你对这个问题的理解以及困惑点。一定不要觉得向面试官要提示很可耻,只要沟通没问题,这其实是很正常的。最怕的就是自己不会,还乱回答一通,这样会让面试官觉得你技术态度有问题。

面试结束后的复盘

很多人关注面试前的准备,却忽略了面试后的复盘,这一步真的非常非常非常重要:

  1. 记录面试中的问题:无论回答得好坏,都把它们写下来。如果问到了一些没想过的问题,可以认真思考并在面试后补上答案。
  2. 反思自己的表现:有没有遇到卡壳的地方?是知识没准备到还是过于紧张导致表达混乱?下次如何改进?
  3. 持续完善自己的“面试题库”:把新的问题补充进去,不断拓展自己的知识面,也逐步降低对未知问题的恐惧感。

internship experience


title: 校招没有实习经历怎么办?实习经历怎么写? description: 校招没有实习经历也能上岸:从补强项目经验、持续优化简历到系统准备技术面试,给出可执行的提升路径与注意事项,帮助你在没有大厂实习的情况下提高面试通过率。 category: 面试准备 icon: "mdi:chart-timeline-variant" head:

  • - meta
- name: keywords
  content: 校招,实习经历,没有实习怎么办,项目经验,简历优化,技术面试准备,Java后端,秋招

由于目前的面试太卷,对于犹豫是否要找实习的同学来说,个人建议不论是本科生还是研究生都应该在参加校招面试之前,争取一下不错的实习机会,尤其是大厂的实习机会,日常实习或者暑期实习都可以。当然,如果大厂实习面不上,中小厂实习也是可以接受的。

不过,现在的实习是真难找,这两年有非常多的同学没有找到实习,有一部分甚至是 211/985 名校的同学。实习难找是一方面原因,国内很多学校的导师压根不放实习,这也是很棘手的问题。

没有实习经历怎么办?

如果实在是找不到合适的实习的话,那也没办法,我们应该多花时间去把下面这三件事情给做好:

  1. 补强项目经历
  2. 持续完善简历
  3. 准备技术面试

补强项目经历

校招没有实习经历的话,找工作比较吃亏(没办法,太卷了),需要在项目经历部分多发力弥补一下。

建议你尽全力地去补强自己的项目经历,完善现有的项目或者去做更有亮点的项目,尽可能地通过项目经历去弥补一些。

你面试中的重点就是你的项目经历涉及到的知识点,如果你的项目经历比较简单的话,面试官直接不知道问啥了。另外,你的项目经历中不涉及的知识点,但在技能介绍中提到的知识点也很大概率会被问到。像 Redis 这种基本是面试 Java 后端岗位必备的技能,我觉得大部分面试官应该都会问。

推荐阅读一下网站的这篇文章:项目经验指南。

完善简历

一定一定一定要重视简历啊!建议至少花 2~3 天时间来专门完善自己的简历。并且,后续还要持续完善。

对于面试官来说,筛选简历的时候会比较看重下面这些维度:

  1. 实习/工作经历:看你是否有不错的实习经历,大厂且与面试岗位相关的实习/工作经历最佳。
  2. 获奖经历:如果有含金量比较高(知名度较高的赛事比如 ACM、阿里云天池)的获奖经历的话,也是加分点,尤其是对于校招来说,这类求职者属于是很多大厂争抢的对象(但不是说获奖了就能进大厂,还是要面试表现还可以)。对于社招来说,获奖经历作用相对较小,通常会更看重过往的工作经历和项目经验。
  3. 项目经验:项目经验对于面试来说非常重要,面试官会重点关注,同时也是有水平的面试提问的重点。
  4. 技能匹配度:看你的技能是否满足岗位的需求。在投递简历之前,一定要确认一下自己的技能介绍中是否缺少一些你要投递的对应岗位的技能要求。
  5. 学历:相对其他行业来说,程序员求职面试对于学历的包容度还是比较高的,只要你在其他方面有过人之出的话,也是可以弥补一下学历的缺陷的。你要知道,很多行业比如律师、金融,学历就是敲门砖,学历没达到要求,直接面试机会都没有。不过,由于现在面试越来越卷,一些大厂、国企和研究所也开始卡学历了,很多岗位都要求 211/985,甚至必须需要硕士学历。总之,学历很难改变,学校较差的话,就投递那些对学历没有明确要求的公司即可,努力提升自己的其他方面的硬实力。

对于大部分求职者来说,实习/工作经历、项目经验、技能匹配度更重要一些。不过,不排除一些公司会因为学历卡人。

详细的程序员简历编写指南可以参考这篇文章:程序员简历编写指南(重要)。

准备技术面试

面试之前一定要提前准备一下常见的面试题也就是八股文:

  • 自己面试中可能涉及哪些知识点、那些知识点是重点。
  • 面试中哪些问题会被经常问到、面试中自己该如何回答。(强烈不推荐死记硬背,第一:通过背这种方式你能记住多少?能记住多久?第二:背题的方式的学习很难坚持下去!)

不同类型的公司对于技能的要求侧重点是不同的比如腾讯、字节可能更重视计算机基础比如网络、操作系统这方面的内容。阿里、美团这种可能更重视你的项目经历、实战能力。

一定不要抱着一种思想,觉得八股文或者基础问题的考查意义不大。如果你抱着这种思想复习的话,那效果可能不会太好。实际上,个人认为还是很有意义的,八股文或者基础性的知识在日常开发中也会需要经常用到。例如,线程池这块的拒绝策略、核心参数配置什么的,如果你不了解,实际项目中使用线程池可能就用的不是很明白,容易出现问题。而且,其实这种基础性的问题是最容易准备的,像各种底层原理、系统设计、场景题以及深挖你的项目这类才是最难的!

八股文资料首推我的 《Java 面试指北》 和 JavaGuide 。里面不仅仅是原创八股文,还有很多对实际开发有帮助的干货。除了我的资料之外,你还可以去网上找一些其他的优质的文章、视频来看。

如果你想要系统准备 Java 后端面试但又不知道如何开始的,可以参考 Java 后端面试通关计划(后端通用)。

实习经历在简历上一般怎么写比较出彩?

实习经历的描述一定要避免空谈,尽量列举出你在实习期间取得的成就和具体贡献,使用具体的数据和指标来量化你的工作成果。

示例(这里假设项目细节放在实习经历这里介绍,你也可以选择将实习经历参与的项目放到项目经历中):

  1. 负责订单模块核心流程开发,实现订单状态的精确流转,并保障与库存、支付等模块的数据一致性。
  2. 负责行为风控黑名单看板的开发,支持查看拉黑用户、批量拉黑以及取消拉黑。
  3. 基于 Redisson + AOP 封装限流组件,实现对核心接口(如付费、课程搜索)的限流,有效防止恶意请求冲击。
  4. 优化用户统计模块性能,利用 CompletableFuture 并行加载多维度数据(如用户增长、课程活跃度),,平均相应时间从 3.5s 降低到 1s。
  5. 封装通用数据脱敏组件,通过自定义 Jackson 注解实现对手机号、邮箱等敏感信息的自动、无侵入式脱敏。
  6. 优化文件上传模块,基于 MinIO 实现了文件的分片上传、断点续传以及极速秒传功能。
  7. 排查并解决扣费模块由于扣费父任务和反作弊子任务使用同一个线程池导致的死锁问题,通过线程池隔离策略根除该隐患。
  8. 实习期间独立负责 7 个功能需求与 3 个线上问题修复,代码均一次性通过评审与测试。

下面是星球一位球友分享的实习经历介绍,整体写的还是非常不错的:

实习经历模板

📌关于实习经历这块再多提一点:很多同学实习期间可能接触不到什么实际的开发任务,大部分时间可能都是在熟悉和维护项目。

对于这种情况,应对思路是一套组合拳:首先,你肯定是要和 mentor 沟通继续争取做一些有价值的工作,这样你的实习经历才更有价值,简历上自然就能够有东西可写。记得找一个 mentor 不那么忙的时候沟通,放低姿态,真诚一些,表明自己现有的工作已经认真完成,想要承担更多责任的意愿。其次,不管是否能够争取到这种机会,你都要自己有意识地寻找项目中适合自己研究的功能点(比如同组其他实习生干的活),进行深度挖掘。重点关注以下几个方面:

  1. 这个功能是干嘛的? 它解决了什么业务痛点?给哪个业务方用的?整个流程是怎样的?
  2. 它是怎么实现的? 用了哪些关键技术、框架或者设计模式?核心代码的逻辑是怎样的?
  3. 为什么要这么设计? 当初设计的时候有没有别的方案?现在这个方案好在哪,又有什么潜在的坑?如果让你来做,你会怎么设计?

只要你把具体的功能点彻底搞懂,那就可以在简历上合理包装成自己的成果。除了功能点开发之外,也可以包装一些合适的问题排查解决经历,这样能够体现你解决问题的能力。 面试时也不用太担心自己“露馅”,只要你选择的内容不属于那些显然不会交给实习生完成的高难度任务,并且能清晰地讲明白,就不会有问题。


basis


title: 数据库基础常见面试题总结 description: 数据库基础面试题和知识点总结,包括数据库、DBMS、数据库系统、DBA的概念区别,DBMS核心功能,元组、码、主键外键等关系型数据库核心概念,以及ER图的使用方法。 category: 数据库 tag:

  • 数据库基础

head:

  • - meta
- name: keywords
  content: 数据库,数据库管理系统,DBMS,数据库系统,DBA,SQL,DDL,DML,数据模型,关系型数据库,主键,外键,ER图

数据库知识基础,这部分内容一定要理解记忆。虽然这部分内容只是理论知识,但是非常重要,这是后面学习 MySQL 数据库的基础。PS: 这部分内容由于涉及太多概念性内容,所以参考了维基百科和百度百科相应的介绍。

什么是数据库, 数据库管理系统, 数据库系统, 数据库管理员?

这四个概念描述了从数据本身到管理整个体系的不同层次,我们常用一个图书馆的例子来把它们串联起来理解。

  • 数据库 (Database - DB): 它就像是图书馆里,书架上存放的所有书籍和资料。从技术上讲,数据库就是按照一定数据模型组织、描述和储存起来的、可以被各种用户共享的结构化数据的集合。它就是我们最终要存取的核心——信息本身。
  • 数据库管理系统 (Database Management System - DBMS): 它就像是整个图书馆的管理系统,包括图书的分类编目规则、借阅归还流程、安全检查系统等等。从技术上讲,DBMS 是一种大型软件,比如我们常用的 MySQL、Oracle、PostgreSQL 软件。它的核心职责是科学地组织和存储数据、高效地获取和维护数据;为我们屏蔽了底层文件操作的复杂性,提供了一套标准接口(如 SQL)来操纵数据,并负责并发控制、事务管理、权限控制等复杂问题。
  • 数据库系统 (Database System - DBS): 它就是整个正常运转的图书馆。这是一个更大的概念,不仅包括书(DB)和管理系统(DBMS),还包括了硬件、应用和使用的人。
  • 数据库管理员 (Database Administrator - DBA ): 他就是图书馆的馆长,负责整个数据库系统正常运行。他的职责非常广泛,包括数据库的设计、安装、监控、性能调优、备份与恢复、安全管理等等,确保整个系统的稳定、高效和安全。

DB 和 DBMS 我们通常会搞混,这里再简单提一下:通常我们说“用 MySQL 数据库”,其实是用 MySQL(DBMS)来管理一个或多个数据库(DB)。

DBMS 有哪些主要的功能

graph TD
    DBMS["🗄️ DBMS<br/><b>数据库管理系统</b>"]

    subgraph define["数据定义"]
        DDL["📐 DDL<br/>Data Definition Language"]
        DDL_Items["• 创建/修改/删除对象<br/>• 定义表结构<br/>• 定义视图、索引<br/>• 定义触发器<br/>• 定义存储过程"]
    end

    subgraph operate["数据操作"]
        DML["⚡ DML<br/>Data Manipulation Language"]
        CRUD["<b>CRUD 操作</b><br/>• Create 创建<br/>• Read 读取<br/>• Update 更新<br/>• Delete 删除"]
    end

    subgraph control["数据控制"]
        DCL["🔐 数据控制功能"]
        Control_Items["• 并发控制<br/>• 事务管理<br/>• 完整性约束<br/>• 权限控制<br/>• 安全性限制"]
    end

    subgraph maintain["数据库维护"]
        Maintenance["🛠️ 维护功能"]
        Maintain_Items["• 数据导入/导出<br/>• 备份与恢复<br/>• 性能监控与分析<br/>• 系统日志管理"]
    end

    DBMS --> DDL
    DBMS --> DML
    DBMS --> DCL
    DBMS --> Maintenance

    DDL --> DDL_Items
    DML --> CRUD
    DCL --> Control_Items
    Maintenance --> Maintain_Items

    style DBMS fill:#005D7B,stroke:#00838F,stroke-width:4px,color:#fff

    style DDL fill:#4CA497,stroke:#00838F,stroke-width:3px,color:#fff
    style DDL_Items fill:#f0fffe,stroke:#4CA497,stroke-width:2px,color:#333

    style DML fill:#E99151,stroke:#C44545,stroke-width:3px,color:#fff
    style CRUD fill:#fff5e6,stroke:#E99151,stroke-width:2px,color:#333

    style DCL fill:#00838F,stroke:#005D7B,stroke-width:3px,color:#fff
    style Control_Items fill:#e6f7ff,stroke:#00838F,stroke-width:2px,color:#333

    style Maintenance fill:#C44545,stroke:#8B0000,stroke-width:3px,color:#fff
    style Maintain_Items fill:#ffe6e6,stroke:#C44545,stroke-width:2px,color:#333

    style define fill:#E4C189,stroke:#E99151,stroke-width:2px,stroke-dasharray: 5 5,opacity:0.3
    style operate fill:#E4C189,stroke:#E99151,stroke-width:2px,stroke-dasharray: 5 5,opacity:0.3
    style control fill:#E4C189,stroke:#E99151,stroke-width:2px,stroke-dasharray: 5 5,opacity:0.3
    style maintain fill:#E4C189,stroke:#E99151,stroke-width:2px,stroke-dasharray: 5 5,opacity:0.3

DBMS 通常提供四大核心功能:

  1. 数据定义: 这是 DBMS 的基础。它提供了一套数据定义语言(Data Definition Language - DDL),让我们能够创建、修改和删除数据库中的各种对象。这不仅仅是定义表的结构(比如字段名、数据类型),还包括定义视图、索引、触发器、存储过程等。
  2. 数据操作: 这是我们作为开发者日常使用最多的功能。它提供了一套数据操作语言(Data Manipulation Language - DML),核心就是我们熟悉的增、删、改、查(CRUD)操作。它让我们能够方便地对数据库中的数据进行操作和检索。
  3. 数据控制: 这是保证数据正确、安全、可靠的关键。通常包含并发控制、事务管理、完整性约束、权限控制、安全性限制等功能。
  4. 数据库维护: 这部分功能是为了保障数据库系统的长期稳定运行。它包括了数据的导入导出、数据库的备份与恢复、性能监控与分析、以及系统日志管理等。

你知道哪些类型的 DBMS?

关系型数据库

除了我们最常用的关系型数据库(RDBMS),比如 MySQL(开源首选)、PostgreSQL(功能最全)、Oracle(企业级),它们基于严格的表结构和 SQL,非常适合结构化数据和需要事务保证的场景,例如银行交易、订单系统。

近年来,为了应对互联网应用带来的海量数据、高并发和多样化数据结构的需求,涌现出了一大批 NoSQL 和 NewSQL 数据库。

NoSQL 数据库

它们的共同特点是为了极致的性能和水平扩展能力,在某些方面(通常是事务)做了妥协。

1. 键值数据库,代表是 Redis。

  • 特点: 数据模型极其简单,就是一个巨大的 Map,通过 Key 来存取 Value。内存操作,性能极高。
  • 适用场景: 非常适合做缓存、会话存储、计数器等对读写性能要求极高的场景。

2. 文档数据库,代表是 MongoDB。

  • 特点: 它存储的是半结构化的文档(比如 JSON/BSON),结构灵活,不需要预先定义表结构。
  • 适用场景: 特别适合那些数据结构多变、快速迭代的业务,比如用户画像、内容管理系统、日志存储等。

3. 列式数据库,代表是 HBase, Cassandra。

  • 特点: 数据是按列族而不是按行来存储的。这使得它在对大量行进行少量列的读取时,性能极高。
  • 适用场景: 专为海量数据存储和分析设计,非常适合做大数据分析、监控数据存储、推荐系统等需要高吞吐量写入和范围扫描的场景。

4. 图形数据库,代表是 Neo4j。

  • 特点: 数据模型是节点(Nodes)和边(Edges),专门用来存储和查询实体之间的复杂关系。
  • 适用场景: 在社交网络(好友关系)、推荐引擎(用户-商品关系)、知识图谱、欺诈检测(资金流动关系)等场景下,表现远超关系型数据库。

NewSQL 数据库

由于 NoSQL 不支持事务,很多对于数据安全要求非常高的系统(比如财务系统、订单系统、交易系统)就不太适合使用了。不过,这类系统往往有存储大量数据的需求。

这些系统往往只能通过购买性能更强大的计算机,或者通过数据库中间件来提高存储能力。不过,前者的金钱成本太高,后者的开发成本太高。

于是,NewSQL 就来了!

简单来说,NewSQL 就是:分布式存储+SQL+事务 。NewSQL 不仅具有 NoSQL 对海量数据的存储管理能力,还保持了传统数据库支持 ACID 和 SQL 等特性。因此,NewSQL 也可以称为 分布式关系型数据库。

NewSQL 数据库设计的一些目标:

  1. 横向扩展(Scale Out) : 通过增加机器的方式来提高系统的负载能力。与之类似的是 Scale Up(纵向扩展),升级硬件设备的方式来提高系统的负载能力。
  2. 强一致性(Strict Consistency):在任意时刻,所有节点中的数据是一样的。
  3. 高可用(High Availability):系统几乎可以一直提供服务。
  4. 支持标准 SQL(Structured Query Language) :PostgreSQL、MySQL、Oracle 等关系型数据库都支持 SQL 。
  5. 事务(ACID) : 原子性(Atomicity)、一致性(Consistency)、 隔离性(Isolation); 持久性(Durability)。
  6. 兼容主流关系型数据库 : 兼容 MySQL、Oracle、PostgreSQL 等常用关系型数据库。
  7. 云原生 (Cloud Native):可在公有云、私有云、混合云中实现部署工具化、自动化。
  8. HTAP(Hybrid Transactional/Analytical Processing) :支持 OLTP 和 OLAP 混合处理。

NewSQL 数据库代表:Google 的 F1/Spanner、阿里的 OceanBase、PingCAP 的 TiDB 。

什么是元组, 码, 候选码, 主码, 外码, 主属性, 非主属性?

在关系型数据库理论中,理解元组、码、候选码、主码、外码、主属性和非主属性这些核心概念,对于数据库设计和规范化至关重要。这些概念构成了关系数据库的理论基础。

graph TD
    A[关系数据库概念] --> B[数据组织]
    A --> C[码的类型]
    A --> D[属性分类]

    B --> B1[元组<br/>表中的行记录]
    B --> B2[属性<br/>表中的列]

    C --> C1[码<br/>唯一标识]
    C1 --> C2[候选码<br/>最小唯一标识集]
    C2 --> C3[主码<br/>选定的候选码]
    C1 --> C4[外码<br/>引用其他表主码]

    D --> D1[主属性<br/>候选码中的属性]
    D --> D2[非主属性<br/>不在候选码中的属性]

    C3 -.关联.-> C4
    C2 -.构成.-> D1

    style A fill:#4CA497,stroke:#00838F,stroke-width:3px,color:#fff
    style B fill:#00838F,stroke:#005D7B,stroke-width:2px,color:#fff
    style C fill:#E99151,stroke:#005D7B,stroke-width:2px,color:#fff
    style D fill:#005D7B,stroke:#00838F,stroke-width:2px,color:#fff

    style B1 fill:#E4C189,stroke:#00838F,stroke-width:1px
    style B2 fill:#E4C189,stroke:#00838F,stroke-width:1px

    style C1 fill:#E4C189,stroke:#E99151,stroke-width:1px
    style C2 fill:#E4C189,stroke:#E99151,stroke-width:1px
    style C3 fill:#C44545,stroke:#005D7B,stroke-width:2px,color:#fff
    style C4 fill:#E4C189,stroke:#E99151,stroke-width:1px

    style D1 fill:#E4C189,stroke:#005D7B,stroke-width:1px
    style D2 fill:#E4C189,stroke:#005D7B,stroke-width:1px

基础概念

  • 元组(Tuple): 元组是关系数据库中的基本单位,在二维表中对应一行记录。每个元组包含了一个实体的完整信息。例如,在学生表中,每个学生的完整信息(学号、姓名、年龄等)构成一个元组。
  • 码(Key): 码是能够唯一标识关系中元组的一个或多个属性的集合。码的主要作用是保证数据的唯一性和完整性。

码的分类

  • 候选码(Candidate Key): 候选码是能够唯一标识元组的最小属性集合,其任何真子集都不能唯一标识元组。一个关系可能有多个候选码。例如,在学生表中,如果"学号"能唯一标识学生,同时"身份证号"也能唯一标识学生,那么{学号}和{身份证号}都是候选码。
  • 主码/主键(Primary Key): 主码是从候选码中选择的一个,用于唯一标识关系中的元组。每个关系只能有一个主码,但可以有多个候选码。选择主码时通常考虑:简单性、稳定性、无业务含义等因素。
  • 外码/外键(Foreign Key): 外码是一个关系中的属性或属性组,它对应另一个关系的主码。外码用于建立和维护两个关系之间的联系,是实现参照完整性的重要机制。例如,在选课表中的"学号"如果引用学生表的主码"学号",则选课表中的"学号"就是外码。

属性分类

  • 主属性(Prime Attribute): 主属性是包含在任何一个候选码中的属性。如果一个关系有多个候选码,那么这些候选码中出现的所有属性都是主属性。例如,工人关系(工号,身份证号,姓名,性别,部门)中,如果{工号}和{身份证号}都是候选码,那么"工号"和"身份证号"都是主属性。
  • 非主属性(Non-prime Attribute): 非主属性是不包含在任何候选码中的属性。这些属性完全依赖于候选码来确定其值。在上述工人关系中,"姓名"、"性别"、"部门"都是非主属性。

什么是 ER 图?

我们做一个项目的时候一定要试着画 ER 图来捋清数据库设计,这个也是面试官问你项目的时候经常会被问到的。

ER 图 全称是 Entity Relationship Diagram(实体联系图),提供了表示实体类型、属性和联系的方法。

ER 图由下面 3 个要素组成:

  • 实体:通常是现实世界的业务对象,当然使用一些逻辑对象也可以。比如对于一个校园管理系统,会涉及学生、教师、课程、班级等等实体。在 ER 图中,实体使用矩形框表示。
  • 属性:即某个实体拥有的属性,属性用来描述组成实体的要素,对于产品设计来说可以理解为字段。在 ER 图中,属性使用椭圆形表示。
  • 联系:即实体与实体之间的关系,在 ER 图中用菱形表示,这个关系不仅有业务关联关系,还能通过数字表示实体之间的数量对照关系。例如,一个班级会有多个学生就是一种实体间的联系。

下图是一个学生选课的 ER 图,每个学生可以选若干门课程,同一门课程也可以被若干人选择,所以它们之间的关系是多对多(M: N)。另外,还有其他两种实体之间的关系是:1 对 1(1:1)、1 对多(1: N)。

erDiagram
    STUDENT {
        string student_id PK "学号"
        string name "姓名"
        string gender "性别"
        date birth_date "出生日期"
        string department "学院名称"
    }

    COURSE {
        string course_id PK "课程编号"
        string course_name "课程名称"
        string location "课程地点"
        string instructor "开课教师"
        float credits "成绩"
    }

    ENROLLMENT {
        string student_id FK "学号"
        string course_id FK "课程编号"
        float grade "成绩"
    }

    STUDENT ||--o{ ENROLLMENT : "选课"
    COURSE ||--o{ ENROLLMENT : "被选"

    style STUDENT fill:#4CA497,stroke:#00838F,stroke-width:2px
    style COURSE fill:#005D7B,stroke:#00838F,stroke-width:2px
    style ENROLLMENT fill:#E99151,stroke:#C44545,stroke-width:2px

数据库范式了解吗?

数据库范式有 3 种:

  • 1NF(第一范式):属性不可再分。
  • 2NF(第二范式):1NF 的基础之上,消除了非主属性对于码的部分函数依赖。
  • 3NF(第三范式):3NF 在 2NF 的基础之上,消除了非主属性对于码的传递函数依赖 。

1NF(第一范式)

属性(对应于表中的字段)不能再被分割,也就是这个字段只能是一个值,不能再分为多个其他的字段了。1NF 是所有关系型数据库的最基本要求 ,也就是说关系型数据库中创建的表一定满足第一范式。

2NF(第二范式)

2NF 在 1NF 的基础之上,消除了非主属性对于码的部分函数依赖。如下图所示,展示了第一范式到第二范式的过渡。第二范式在第一范式的基础上增加了一个列,这个列称为主键,非主属性都依赖于主键。

第二范式

一些重要的概念:

  • 函数依赖(functional dependency):若在一张表中,在属性(或属性组)X 的值确定的情况下,必定能确定属性 Y 的值,那么就可以说 Y 函数依赖于 X,写作 X → Y。
  • 部分函数依赖(partial functional dependency):如果 X→Y,并且存在 X 的一个真子集 X0,使得 X0→Y,则称 Y 对 X 部分函数依赖。比如学生基本信息表 R 中(学号,身份证号,姓名)当然学号属性取值是唯一的,在 R 关系中,(学号,身份证号)->(姓名),(学号)->(姓名),(身份证号)->(姓名);所以姓名部分函数依赖于(学号,身份证号);
  • 完全函数依赖(Full functional dependency):在一个关系中,若某个非主属性数据项依赖于全部关键字称之为完全函数依赖。比如学生基本信息表 R(学号,班级,姓名)假设不同的班级学号有相同的,班级内学号不能相同,在 R 关系中,(学号,班级)->(姓名),但是(学号)->(姓名)不成立,(班级)->(姓名)不成立,所以姓名完全函数依赖与(学号,班级);
  • 传递函数依赖:在关系模式 R(U)中,设 X,Y,Z 是 U 的不同的属性子集,如果 X 确定 Y、Y 确定 Z,且有 X 不包含 Y,Y 不确定 X,(X∪Y)∩Z=空集合,则称 Z 传递函数依赖(transitive functional dependency) 于 X。传递函数依赖会导致数据冗余和异常。传递函数依赖的 Y 和 Z 子集往往同属于某一个事物,因此可将其合并放到一个表中。比如在关系 R(学号 , 姓名, 系名,系主任)中,学号 → 系名,系名 → 系主任,所以存在非主属性系主任对于学号的传递函数依赖。

3NF(第三范式)

3NF 在 2NF 的基础之上,消除了非主属性对于码的传递函数依赖 。符合 3NF 要求的数据库设计,基本上解决了数据冗余过大,插入异常,修改异常,删除异常的问题。比如在关系 R(学号 , 姓名, 系名,系主任)中,学号 → 系名,系名 → 系主任,所以存在非主属性系主任对于学号的传递函数依赖,所以该表的设计,不符合 3NF 的要求。

主键和外键有什么区别?

从定义和属性上看,它们的区别是:

  • 主键 (Primary Key): 它的核心作用是唯一标识表中的每一行数据。因此,主键列的值必须是唯一的 (Unique) 且不能为空 (Not Null)。一张表只能有一个主键。主键保证了实体完整性。
  • 外键 (Foreign Key): 它的核心作用是建立并强制两张表之间的关联关系。一张表中的外键列,其值必须对应另一张表中某行的候选键值(通常是主键,也可以是唯一键),或者是一个 NULL 值。因此,外键的值可以重复,也可以为空。一张表可以有多个外键,分别关联到不同的表。外键保证了引用完整性。

用一个简单的电商例子来说明:假设我们有两张表:users (用户表) 和 orders (订单表)。

  • 在 users 表中,user_id 列是主键。每个用户的 user_id 都是独一无二的,我们用它来区分张三和李四。
  • 在 orders 表中,order_id 是它自己的主键。同时,它会有一个 user_id 列,这个列就是一个外键,它引用了 users 表的 user_id 主键。

这个外键约束就保证了:

  1. 你不能创建一个不属于任何已知用户的订单( user_id 在 users 表中不存在)。
  2. 你不能删除一个已经下了订单的用户(除非设置了级联删除等特殊规则)。

为什么不推荐使用外键与级联?

对于外键和级联,阿里巴巴开发手册这样说到:

【强制】不得使用外键与级联,一切外键概念必须在应用层解决。 说明: 以学生和成绩的关系为例,学生表中的 student_id 是主键,那么成绩表中的 student_id 则为外键。如果更新学生表中的 student_id,同时触发成绩表中的 student_id 更新,即为级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风险;外键影响数据库的插入速度

为什么不要用外键呢?大部分人可能会这样回答:

  1. 增加了复杂性: a. 每次做 DELETE 或者 UPDATE 都必须考虑外键约束,会导致开发的时候很痛苦, 测试数据极为不方便; b. 外键的主从关系是定的,假如哪天需求有变化,数据库中的这个字段根本不需要和其他表有关联的话就会增加很多麻烦。
  2. 增加了额外工作:数据库需要增加维护外键的工作,比如当我们做一些涉及外键字段的增,删,更新操作之后,需要触发相关操作去检查,保证数据的一致性和正确性,这样会不得不消耗数据库资源。如果在应用层面去维护的话,可以减小数据库压力;
  3. 对分库分表不友好:因为分库分表下外键是无法生效的。
  4. ……

我个人觉得上面这种回答不是特别的全面,只是说了外键存在的一个常见的问题。实际上,我们知道外键也是有很多好处的,比如:

  1. 保证了数据库数据的一致性和完整性;
  2. 级联操作方便,减轻了程序代码量;
  3. ……

所以说,不要一股脑的就抛弃了外键这个概念,既然它存在就有它存在的道理,如果系统不涉及分库分表,并发量不是很高的情况还是可以考虑使用外键的。

什么是存储过程?

graph LR
    A[存储过程] --> B[定义特征]
    A --> C[优势]
    A --> D[劣势]
    A --> E[应用现状]

    B --> B1[SQL语句集合]
    B --> B2[包含逻辑控制]
    B --> B3[预编译机制]

    C --> C1[执行速度快]
    C --> C2[运行稳定]
    C --> C3[简化复杂操作]

    D --> D1[调试困难]
    D --> D2[扩展性差]
    D --> D3[无移植性]
    D --> D4[占用数据库资源]

    E --> E1[传统企业<br/>使用较多]
    E --> E2[互联网公司<br/>很少使用]
    E --> E3[阿里规范<br/>明确禁用]

    style A fill:#4CA497,stroke:#00838F,stroke-width:3px,color:#fff
    style B fill:#00838F,stroke:#005D7B,stroke-width:2px,color:#fff
    style C fill:#E99151,stroke:#C44545,stroke-width:2px,color:#fff
    style D fill:#C44545,stroke:#005D7B,stroke-width:2px,color:#fff
    style E fill:#005D7B,stroke:#00838F,stroke-width:2px,color:#fff

    style B1 fill:#E4C189,stroke:#00838F,stroke-width:1px
    style B2 fill:#E4C189,stroke:#00838F,stroke-width:1px
    style B3 fill:#E4C189,stroke:#00838F,stroke-width:1px

    style C1 fill:#E4C189,stroke:#E99151,stroke-width:1px
    style C2 fill:#E4C189,stroke:#E99151,stroke-width:1px
    style C3 fill:#E4C189,stroke:#E99151,stroke-width:1px

    style D1 fill:#E4C189,stroke:#C44545,stroke-width:1px
    style D2 fill:#E4C189,stroke:#C44545,stroke-width:1px
    style D3 fill:#E4C189,stroke:#C44545,stroke-width:1px
    style D4 fill:#E4C189,stroke:#C44545,stroke-width:1px

    style E1 fill:#E4C189,stroke:#005D7B,stroke-width:1px
    style E2 fill:#E4C189,stroke:#005D7B,stroke-width:1px
    style E3 fill:#E4C189,stroke:#005D7B,stroke-width:1px

存储过程是数据库中预编译的SQL语句集合,它将多条SQL语句和程序逻辑控制语句(如IF-ELSE、WHILE循环等)封装在一起,形成一个可重复调用的数据库对象。

存储过程的优势:

在传统企业级应用中,存储过程具有一定的实用价值。当业务逻辑复杂时,需要执行大量SQL语句才能完成一个业务操作,此时可以将这些语句封装成存储过程,简化调用过程。由于存储过程在创建时就已经编译并存储在数据库中,执行时无需重新编译,因此相比动态SQL语句具有更好的执行性能。同时,一旦存储过程调试完成,其运行相对稳定可靠。

存储过程的局限性:

然而,在现代互联网架构中,存储过程的使用越来越少。主要原因包括:调试困难,缺乏成熟的调试工具;扩展性差,修改业务逻辑需要直接修改数据库对象;移植性差,不同数据库系统的存储过程语法差异较大;占用数据库资源,增加数据库服务器负担;版本管理困难,不便于进行代码版本控制。

行业规范:

基于以上原因,许多互联网公司的开发规范中明确限制或禁止使用存储过程。例如,《阿里巴巴Java开发手册》中明确规定禁止使用存储过程,推荐将业务逻辑放在应用层实现,保持数据库的简单和高效。

阿里巴巴Java开发手册: 禁止存储过程

DROP、DELETE、TRUNCATE 有什么区别?

在数据库操作中,DROP、DELETE 和 TRUNCATE 是三个常用的数据删除命令,它们在功能、性能和使用场景上存在显著差异。

DROP命令:

  • 语法:DROP TABLE 表名
  • 作用:完全删除整个表,包括表结构、数据、索引、触发器、约束等所有相关对象
  • 使用场景:当表不再需要时使用

TRUNCATE命令:

  • 语法:TRUNCATE TABLE 表名
  • 作用:清空表中所有数据,但保留表结构
  • 特点:自增长字段(AUTO_INCREMENT)会重置为初始值(通常为1)
  • 使用场景:需要快速清空表数据但保留表结构时使用

DELETE命令:

  • 语法:DELETE FROM 表名 WHERE 条件
  • 作用:删除满足条件的数据行,不带WHERE子句时删除所有数据
  • 特点:自增长字段不会重置,继续从之前的值递增
  • 使用场景:需要有选择地删除部分数据时使用

TRUNCATE 和不带 WHERE子句的 DELETE、以及 DROP 都会删除表内的数据,但是 TRUNCATE 和 DELETE 只删除数据不删除表的结构(定义),执行 DROP 语句,此表的结构也会删除,也就是执行DROP 之后对应的表不复存在。

对表结构的影响

  • DROP:删除表结构和所有数据,表将不复存在
  • TRUNCATE:仅删除数据,保留表结构和定义
  • DELETE:仅删除数据,保留表结构和定义

触发器

  • DELETE 操作会触发相关的DELETE触发器
  • TRUNCATE 和 DROP 不会触发DELETE触发器

事务和回滚

  • DROP 和 TRUNCATE 属于DDL操作,执行后立即生效,不能回滚
  • DELETE 属于DML操作,可以回滚(在事务中)

执行速度

一般来说:DROP > TRUNCATE > DELETE(这个我没有实际测试过)。

  • DELETE命令执行的时候会产生数据库的binlog日志,而日志记录是需要消耗时间的,但是也有个好处方便数据回滚恢复。
  • TRUNCATE命令执行的时候不会产生数据库日志,因此比DELETE要快。除此之外,还会把表的自增值重置和索引恢复到初始大小等。
  • DROP命令会把表占用的空间全部释放掉。

Tips:你应该更多地关注在使用场景上,而不是执行效率。

DML 语句和 DDL 语句区别是?

  • DML 是数据库操作语言(Data Manipulation Language)的缩写,是指对数据库中表记录的操作,主要包括表记录的插入、更新、删除和查询,是开发人员日常使用最频繁的操作。
  • DDL (Data Definition Language)是数据定义语言的缩写,简单来说,就是对数据库内部的对象进行创建、删除、修改的操作语言。它和 DML 语言的最大区别是 DML 只是对表内部数据的操作,而不涉及到表的定义、结构的修改,更不会涉及到其他对象。DDL 语句更多的被数据库管理员(DBA)所使用,一般的开发人员很少使用。

另外,由于SELECT不会对表进行破坏,所以有的地方也会把SELECT单独区分开叫做数据库查询语言 DQL(Data Query Language)。

数据库设计通常分为哪几步?

graph TD
    A[数据库设计流程] --> B[1.需求分析]
    B --> C[2.概念结构设计]
    C --> D[3.逻辑结构设计]
    D --> E[4.物理结构设计]
    E --> F[5.数据库实施]
    F --> G[6.运行和维护]

    B --> B1[数据需求<br/>功能需求<br/>性能需求]
    C --> C1[E-R建模<br/>实体关系图]
    D --> D1[关系模型<br/>表结构设计<br/>规范化]
    E --> E1[存储结构<br/>索引设计<br/>分区策略]
    F --> F1[编程开发<br/>测试部署<br/>数据迁移]
    G --> G1[性能监控<br/>备份恢复<br/>优化调整]

    G -.反馈.-> B

    style A fill:#4CA497,stroke:#00838F,stroke-width:3px,color:#fff
    style B fill:#00838F,stroke:#005D7B,stroke-width:2px,color:#fff
    style C fill:#E99151,stroke:#005D7B,stroke-width:2px,color:#fff
    style D fill:#005D7B,stroke:#00838F,stroke-width:2px,color:#fff
    style E fill:#C44545,stroke:#005D7B,stroke-width:2px,color:#fff
    style F fill:#E99151,stroke:#005D7B,stroke-width:2px,color:#fff
    style G fill:#00838F,stroke:#005D7B,stroke-width:2px,color:#fff

    style B1 fill:#E4C189,stroke:#00838F,stroke-width:1px
    style C1 fill:#E4C189,stroke:#E99151,stroke-width:1px
    style D1 fill:#E4C189,stroke:#005D7B,stroke-width:1px
    style E1 fill:#E4C189,stroke:#C44545,stroke-width:1px
    style F1 fill:#E4C189,stroke:#E99151,stroke-width:1px
    style G1 fill:#E4C189,stroke:#00838F,stroke-width:1px

1. 需求分析阶段

目标: 深入了解和分析用户需求,明确系统边界 主要工作:

  • 收集和分析数据需求:确定需要存储哪些数据,数据量大小,数据更新频率
  • 明确功能需求:系统需要支持哪些业务操作,各操作的优先级
  • 定义性能需求:响应时间要求,并发用户数,数据吞吐量
  • 确定安全需求:数据访问权限,加密要求,审计要求 产出物: 需求规格说明书、数据字典初稿

2. 概念结构设计阶段

目标: 将需求转化为信息世界的概念模型 主要工作:

  • 识别实体:确定系统中的主要对象
  • 定义属性:明确每个实体的特征
  • 建立联系:确定实体之间的关系(一对一、一对多、多对多)
  • 绘制E-R图(实体-关系图) 产出物: E-R图、概念数据模型文档

3. 逻辑结构设计阶段

目标: 将概念模型转换为特定DBMS支持的逻辑模型 主要工作:

  • E-R图向关系模型转换:将实体转换为表,属性转换为字段
  • 规范化处理:通过范式化消除数据冗余和更新异常(通常达到3NF)
  • 定义完整性约束:主键、外键、唯一性约束、检查约束
  • 优化模型:根据性能需求进行适当的反规范化 产出物: 逻辑数据模型、表结构设计文档

4. 物理结构设计阶段

目标: 确定数据的物理存储方案和访问方法 主要工作:

  • 选择存储引擎:如MySQL的InnoDB、MyISAM等
  • 设计索引策略:确定需要建立的索引类型和字段
  • 分区设计:对大表进行分区以提高性能
  • 确定存储参数:表空间大小、数据文件位置、缓冲区配置
  • 制定备份策略:全量备份、增量备份的频率和方式 产出物: 物理设计文档、索引设计方案

5. 数据库实施阶段

目标: 将设计转化为实际运行的数据库系统 主要工作:

  • 创建数据库和表结构:编写和执行DDL语句
  • 开发存储过程和触发器(如需要)
  • 编写应用程序接口
  • 导入初始数据
  • 系统集成测试:功能测试、性能测试、压力测试
  • 用户培训和文档编写 产出物: 数据库脚本、测试报告、用户手册

6. 运行和维护阶段

目标: 确保数据库系统稳定高效运行 主要工作:

  • 日常监控:性能监控、空间监控、错误日志分析
  • 性能优化:查询优化、索引调整、参数调优
  • 数据备份和恢复:定期备份、恢复演练
  • 安全管理:权限管理、安全补丁更新、审计
  • 容量规划:预测数据增长,提前扩容
  • 变更管理:需求变更的评估和实施 产出物: 运维报告、优化方案、变更记录

设计原则

在整个设计过程中应遵循:数据独立性原则、完整性原则、安全性原则、可扩展性原则和标准化原则。

参考


nosql


title: NoSQL基础常见面试题总结 description: NoSQL数据库基础面试题和知识总结,包括NoSQL与SQL的区别、NoSQL的优势、四种NoSQL数据库类型(键值、文档、图形、宽列)及其代表产品Redis、MongoDB、Neo4j等的应用场景。 category: 数据库 tag:

  • NoSQL
  • MongoDB
  • Redis

head:

  • - meta
- name: keywords
  content: NoSQL,Redis,MongoDB,HBase,Cassandra,键值数据库,文档数据库,图数据库,宽列存储,SQL与NoSQL区别

NoSQL 是什么?

NoSQL(Not Only SQL 的缩写)泛指非关系型的数据库,主要针对的是键值、文档以及图形类型数据存储。并且,NoSQL 数据库天生支持分布式,数据冗余和数据分片等特性,旨在提供可扩展的高可用高性能数据存储解决方案。

一个常见的误解是 NoSQL 数据库或非关系型数据库不能很好地存储关系型数据。NoSQL 数据库可以存储关系型数据—它们与关系型数据库的存储方式不同。

NoSQL 数据库代表:HBase、Cassandra、MongoDB、Redis。

SQL 和 NoSQL 有什么区别?

SQL 数据库NoSQL 数据库
数据存储模型结构化存储,具有固定行和列的表格非结构化存储。文档:JSON 文档,键值:键值对,宽列:包含行和动态列的表,图:节点和边
发展历程开发于 1970 年代,重点是减少数据重复开发于 2000 年代后期,重点是提升可扩展性,减少大规模数据的存储成本
例子Oracle、MySQL、Microsoft SQL Server、PostgreSQL文档:MongoDB、CouchDB,键值:Redis、DynamoDB,宽列:Cassandra、 HBase,图表:Neo4j、 Amazon Neptune、Giraph
ACID 属性提供原子性、一致性、隔离性和持久性 (ACID) 属性通常不支持 ACID 事务,为了可扩展、高性能进行了权衡,少部分支持比如 MongoDB 。不过,MongoDB 对 ACID 事务 的支持和 MySQL 还是有所区别的。
性能性能通常取决于磁盘子系统。要获得最佳性能,通常需要优化查询、索引和表结构。性能通常由底层硬件集群大小、网络延迟以及调用应用程序来决定。
扩展垂直(使用性能更强大的服务器进行扩展)、读写分离、分库分表横向(增加服务器的方式横向扩展,通常是基于分片机制)
用途普通企业级的项目的数据存储用途广泛比如图数据库支持分析和遍历连接数据之间的关系、键值数据库可以处理大量数据扩展和极高的状态变化
查询语法结构化查询语言 (SQL)数据访问语法可能因数据库而异

NoSQL 数据库有什么优势?

NoSQL 数据库非常适合许多现代应用程序,例如移动、Web 和游戏等应用程序,它们需要灵活、可扩展、高性能和功能强大的数据库以提供卓越的用户体验。

  • 灵活性: NoSQL 数据库通常提供灵活的架构,以实现更快速、更多的迭代开发。灵活的数据模型使 NoSQL 数据库成为半结构化和非结构化数据的理想之选。
  • 可扩展性: NoSQL 数据库通常被设计为通过使用分布式硬件集群来横向扩展,而不是通过添加昂贵和强大的服务器来纵向扩展。
  • 高性能: NoSQL 数据库针对特定的数据模型和访问模式进行了优化,这与尝试使用关系数据库完成类似功能相比可实现更高的性能。
  • 强大的功能: NoSQL 数据库提供功能强大的 API 和数据类型,专门针对其各自的数据模型而构建。

NoSQL 数据库有哪些类型?

NoSQL 数据库主要可以分为下面四种类型:

  • 键值:键值数据库是一种较简单的数据库,其中每个项目都包含键和值。这是极为灵活的 NoSQL 数据库类型,因为应用可以完全控制 value 字段中存储的内容,没有任何限制。Redis 和 DynanoDB 是两款非常流行的键值数据库。
  • 文档:文档数据库中的数据被存储在类似于 JSON(JavaScript 对象表示法)对象的文档中,非常清晰直观。每个文档包含成对的字段和值。这些值通常可以是各种类型,包括字符串、数字、布尔值、数组或对象等,并且它们的结构通常与开发者在代码中使用的对象保持一致。MongoDB 就是一款非常流行的文档数据库。
  • 图形:图形数据库旨在轻松构建和运行与高度连接的数据集一起使用的应用程序。图形数据库的典型使用案例包括社交网络、推荐引擎、欺诈检测和知识图形。Neo4j 和 Giraph 是两款非常流行的图形数据库。
  • 宽列:宽列存储数据库非常适合需要存储大量的数据。Cassandra 和 HBase 是两款非常流行的宽列存储数据库。

下面这张图片来源于 微软的官方文档 | 关系数据与 NoSQL 数据。

NoSQL 数据模型

参考

  • NoSQL 是什么?- MongoDB 官方文档:
  • 什么是 NoSQL? - AWS:
  • NoSQL vs. SQL Databases - MongoDB 官方文档:


character set


title: 字符集详解:字符集是什么?怎么用? description: 详解字符集与字符编码原理,深入分析ASCII、GB2312、GBK、UTF-8、UTF-16等常见编码,解释MySQL中utf8与utf8mb4的区别以及emoji存储问题的解决方案。 category: 数据库 tag:

  • 数据库基础

head:

  • - meta
- name: keywords
  content: 字符集,字符编码,UTF-8,UTF-16,GBK,GB2312,utf8mb4,ASCII,Unicode,MySQL字符集,emoji存储

MySQL 字符编码集中有两套 UTF-8 编码实现:utf8 和 utf8mb4。

如果使用 utf8 的话,存储 emoji 符号和一些比较复杂的汉字、繁体字就会出错。

为什么会这样呢?这篇文章可以从源头给你解答。

字符集是什么?

字符是各种文字和符号的统称,包括各个国家文字、标点符号、表情、数字等等。 字符集 就是一系列字符的集合。字符集的种类较多,每个字符集可以表示的字符范围通常不同,就比如说有些字符集是无法表示汉字的。

计算机只能存储二进制的数据,那英文、汉字、表情等字符应该如何存储呢?

我们要将这些字符和二进制的数据一一对应起来,比如说字符“a”对应“01100001”,反之,“01100001”对应 “a”。我们将字符对应二进制数据的过程称为"字符编码",反之,二进制数据解析成字符的过程称为“字符解码”。

字符编码是什么?

字符编码是一种将字符集中的字符与计算机中的二进制数据相互转换的方法,可以看作是一种映射规则。也就是说,字符编码的目的是为了让计算机能够存储和传输各种文字信息。

每种字符集都有自己的字符编码规则,常用的字符集编码规则有 ASCII 编码、 GB2312 编码、GBK 编码、GB18030 编码、Big5 编码、UTF-8 编码、UTF-16 编码等。

有哪些常见的字符集?

常见的字符集有:ASCII、GB2312、GB18030、GBK、Unicode……。

不同的字符集的主要区别在于:

  • 可以表示的字符范围
  • 编码方式

ASCII

ASCII (American Standard Code for Information Interchange,美国信息交换标准代码) 是一套主要用于现代美国英语的字符集(这也是 ASCII 字符集的局限性所在)。

为什么 ASCII 字符集没有考虑到中文等其他字符呢? 因为计算机是美国人发明的,当时,计算机的发展还处于比较雏形的时代,还未在其他国家大规模使用。因此,美国发布 ASCII 字符集的时候没有考虑兼容其他国家的语言。

ASCII 字符集至今为止共定义了 128 个字符,其中有 33 个控制字符(比如回车、删除)无法显示。

一个 ASCII 码长度是一个字节也就是 8 个 bit,比如“a”对应的 ASCII 码是“01100001”。不过,最高位是 0 仅仅作为校验位,其余 7 位使用 0 和 1 进行组合,所以,ASCII 字符集可以定义 128(2^7)个字符。

由于,ASCII 码可以表示的字符实在是太少了。后来,人们对其进行了扩展得到了 ASCII 扩展字符集 。ASCII 扩展字符集使用 8 位(bits)表示一个字符,所以,ASCII 扩展字符集可以定义 256(2^8)个字符。

ASCII字符编码

GB2312

我们上面说了,ASCII 字符集是一种现代美国英语适用的字符集。因此,很多国家都捣鼓了一个适合自己国家语言的字符集。

GB2312 字符集是一种对汉字比较友好的字符集,共收录 6700 多个汉字,基本涵盖了绝大部分常用汉字。不过,GB2312 字符集不支持绝大部分的生僻字和繁体字。

对于英语字符,GB2312 编码和 ASCII 码是相同的,1 字节编码即可。对于非英字符,需要 2 字节编码。

GBK

GBK 字符集可以看作是 GB2312 字符集的扩展,兼容 GB2312 字符集,共收录了 20000 多个汉字。

GBK 中 K 是汉语拼音 Kuo Zhan(扩展)中的“Kuo”的首字母。

GB18030

GB18030 完全兼容 GB2312 和 GBK 字符集,纳入中国国内少数民族的文字,且收录了日韩汉字,是目前为止最全面的汉字字符集,共收录汉字 70000 多个。

BIG5

BIG5 主要针对的是繁体中文,收录了 13000 多个汉字。

Unicode & UTF-8

为了更加适合本国语言,诞生了很多种字符集。

我们上面也说了不同的字符集可以表示的字符范围以及编码规则存在差异。这就导致了一个非常严重的问题:使用错误的编码方式查看一个包含字符的文件就会产生乱码现象。

就比如说你使用 UTF-8 编码方式打开 GB2312 编码格式的文件就会出现乱码。示例:“牛”这个汉字 GB2312 编码后的十六进制数值为 “C5A3”,而 “C5A3” 用 UTF-8 解码之后得到的却是 “ţ”。

你可以通过这个网站在线进行编码和解码:

这样我们就搞懂了乱码的本质:编码和解码时用了不同或者不兼容的字符集 。

为了解决这个问题,人们就想:“如果我们能够有一种字符集将世界上所有的字符都纳入其中就好了!”。

然后,Unicode 带着这个使命诞生了。

Unicode 字符集中包含了世界上几乎所有已知的字符。不过,Unicode 字符集并没有规定如何存储这些字符(也就是如何使用二进制数据表示这些字符)。

然后,就有了 UTF-8(8-bit Unicode Transformation Format)。类似的还有 UTF-16、 UTF-32。

UTF-8 使用 1 到 4 个字节为每个字符编码, UTF-16 使用 2 或 4 个字节为每个字符编码,UTF-32 固定位 4 个字节为每个字符编码。

UTF-8 可以根据不同的符号自动选择编码的长短,像英文字符只需要 1 个字节就够了,这一点 ASCII 字符集一样 。因此,对于英语字符,UTF-8 编码和 ASCII 码是相同的。

UTF-32 的规则最简单,不过缺陷也比较明显,对于英文字母这类字符消耗的空间是 UTF-8 的 4 倍之多。

UTF-8 是目前使用最广的一种字符编码。

MySQL 字符集

MySQL 支持很多种字符集的方式,比如 GB2312、GBK、BIG5、多种 Unicode 字符集(UTF-8 编码、UTF-16 编码、UCS-2 编码、UTF-32 编码等等)。

查看支持的字符集

你可以通过 SHOW CHARSET 命令来查看,支持 like 和 where 子句。

默认字符集

在 MySQL5.7 中,默认字符集是 latin1 ;在 MySQL8.0 中,默认字符集是 utf8mb4

字符集的层次级别

MySQL 中的字符集有以下的层次级别:

  • server(MySQL 实例级别)
  • database(库级别)
  • table(表级别)
  • column(字段级别)

它们的优先级可以简单的认为是从上往下依次增大,也即 column 的优先级会大于 table 等其余层次的。如指定 MySQL 实例级别字符集是utf8mb4,指定某个表字符集是latin1,那么这个表的所有字段如果不指定的话,编码就是latin1。

server

不同版本的 MySQL 其 server 级别的字符集默认值不同,在 MySQL5.7 中,其默认值是 latin1 ;在 MySQL8.0 中,其默认值是 utf8mb4 。

当然也可以通过在启动 mysqld 时指定 --character-set-server 来设置 server 级别的字符集。

mysqld
mysqld --character-set-server=utf8mb4
mysqld --character-set-server=utf8mb4 \
  --collation-server=utf8mb4_0900_ai_ci

或者如果你是通过源码构建的方式启动的 MySQL,你可以在 cmake 命令中指定选项:

cmake . -DDEFAULT_CHARSET=latin1
或者
cmake . -DDEFAULT_CHARSET=latin1 \
  -DDEFAULT_COLLATION=latin1_german1_ci

此外,你也可以在运行时改变 character_set_server 的值,从而达到修改 server 级别的字符集的目的。

server 级别的字符集是 MySQL 服务器的全局设置,它不仅会作为创建或修改数据库时的默认字符集(如果没有指定其他字符集),还会影响到客户端和服务器之间的连接字符集,具体可以查看 MySQL Connector/J 8.0 - 6.7 Using Character Sets and Unicode。

database

database 级别的字符集是我们在创建数据库和修改数据库时指定的:

CREATE DATABASE db_name
    [[DEFAULT] CHARACTER SET charset_name]
    [[DEFAULT] COLLATE collation_name]

ALTER DATABASE db_name
    [[DEFAULT] CHARACTER SET charset_name]
    [[DEFAULT] COLLATE collation_name]

如前面所说,如果在执行上述语句时未指定字符集,那么 MySQL 将会使用 server 级别的字符集。

可以通过下面的方式查看某个数据库的字符集:

USE db_name;
SELECT @@character_set_database, @@collation_database;
SELECT DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME
FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = 'db_name';

table

table 级别的字符集是在创建表和修改表时指定的:

CREATE TABLE tbl_name (column_list)
    [[DEFAULT] CHARACTER SET charset_name]
    [COLLATE collation_name]]

ALTER TABLE tbl_name
    [[DEFAULT] CHARACTER SET charset_name]
    [COLLATE collation_name]

如果在创建表和修改表时未指定字符集,那么将会使用 database 级别的字符集。

column

column 级别的字符集同样是在创建表和修改表时指定的,只不过它是定义在列中。下面是个例子:

CREATE TABLE t1
(
    col1 VARCHAR(5)
      CHARACTER SET latin1
      COLLATE latin1_german1_ci
);

如果未指定列级别的字符集,那么将会使用表级别的字符集。

连接字符集

前面说到了字符集的层次级别,它们是和存储相关的。而连接字符集涉及的是和 MySQL 服务器的通信。

连接字符集与下面这几个变量息息相关:

  • character_set_client :描述了客户端发送给服务器的 SQL 语句使用的是什么字符集。
  • character_set_connection :描述了服务器接收到 SQL 语句时使用什么字符集进行翻译。
  • character_set_results :描述了服务器返回给客户端的结果使用的是什么字符集。

它们的值可以通过下面的 SQL 语句查询:

SELECT * FROM performance_schema.session_variables
WHERE VARIABLE_NAME IN (
'character_set_client', 'character_set_connection',
'character_set_results', 'collation_connection'
) ORDER BY VARIABLE_NAME;
SHOW SESSION VARIABLES LIKE 'character\_set\_%';

如果要想修改前面提到的几个变量的值,有以下方式:

1、修改配置文件

[mysql]
# 只针对MySQL客户端程序
default-character-set=utf8mb4

2、使用 SQL 语句

set names utf8mb4
# 或者一个个进行修改
# SET character_set_client = utf8mb4;
# SET character_set_results = utf8mb4;
# SET collation_connection = utf8mb4;

JDBC 对连接字符集的影响

不知道你们有没有碰到过存储 emoji 表情正常,但是使用类似 Navicat 之类的软件的进行查询的时候,发现 emoji 表情变成了问号的情况。这个问题很有可能就是 JDBC 驱动引起的。

根据前面的内容,我们知道连接字符集也是会影响我们存储的数据的,而 JDBC 驱动会影响连接字符集。

mysql-connector-java (JDBC 驱动)主要通过这几个属性影响连接字符集:

  • characterEncoding
  • characterSetResults

以 DataGrip 2023.1.2 来说,在它配置数据源的高级对话框中,可以看到 characterSetResults 的默认值是 utf8 ,在使用 mysql-connector-java 8.0.25 时,连接字符集最后会被设置成 utf8mb3 。那么这种情况下 emoji 表情就会被显示为问号,并且当前版本驱动还不支持把 characterSetResults 设置为 utf8mb4 ,不过换成 mysql-connector-java driver 8.0.29 却是允许的。

具体可以看一下 StackOverflow 的 DataGrip MySQL stores emojis correctly but displays them as?这个回答。

UTF-8 使用

通常情况下,我们建议使用 UTF-8 作为默认的字符编码方式。

不过,这里有一个小坑。

MySQL 字符编码集中有两套 UTF-8 编码实现:

  • utf8:utf8编码只支持1-3个字节 。 在 utf8 编码中,中文是占 3 个字节,其他数字、英文、符号占一个字节。但 emoji 符号占 4 个字节,一些较复杂的文字、繁体字也是 4 个字节。
  • utf8mb4:UTF-8 的完整实现,正版!最多支持使用 4 个字节表示字符,因此,可以用来存储 emoji 符号。

为什么有两套 UTF-8 编码实现呢? 原因如下:

因此,如果你需要存储emoji类型的数据或者一些比较复杂的文字、繁体字到 MySQL 数据库的话,数据库的编码一定要指定为utf8mb4 而不是utf8 ,要不然存储的时候就会报错了。

演示一下吧!(环境:MySQL 5.7+)

建表语句如下,我们指定数据库 CHARSET 为 utf8 。

CREATE TABLE `user` (
  `id` varchar(66) CHARACTER SET utf8mb3 NOT NULL,
  `name` varchar(33) CHARACTER SET utf8mb3 NOT NULL,
  `phone` varchar(33) CHARACTER SET utf8mb3 DEFAULT NULL,
  `password` varchar(100) CHARACTER SET utf8mb3 DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

当我们执行下面的 insert 语句插入数据到数据库时,果然报错!

INSERT INTO `user` (`id`, `name`, `phone`, `password`)
VALUES
 ('A00003', 'guide哥😘😘😘', '181631312312', '123456');

报错信息如下:

Incorrect string value: '\xF0\x9F\x98\x98\xF0\x9F...' for column 'name' at row 1

参考

  • 字符集和字符编码(Charset & Encoding):
  • 十分钟搞清字符集和字符编码:
  • Unicode-维基百科:
  • GB2312-维基百科:
  • UTF-8-维基百科:
  • GB18030-维基百科:
  • MySQL8 文档:
  • MySQL5.7 文档:
  • MySQL Connector/J 文档:


system design questions


title: 系统设计常见面试题总结(付费) description: 系统设计高频面试题解析,涵盖短链系统、秒杀系统、海量数据处理等场景题的设计思路与解决方案。 category: Java面试指北 icon: "mdi:palette-swatch-outline" head:

  • - meta
- name: keywords
  content: 系统设计面试题,场景题,短链系统,秒杀系统,海量数据,限流,缓存,分布式锁,一致性

系统设计 相关的面试题为我的知识星球(点击链接即可查看详细介绍以及加入方法)专属内容,已经整理到了《后端面试高频系统设计&场景题》中。

《后端面试高频系统设计&场景题》 包含了常见的系统设计案例比如短链系统、秒杀系统以及高频的场景题比如海量数据去重、第三方授权登录。

《后端面试高频系统设计&场景题》

近年来,随着国内的技术面试越来越卷,越来越多的公司开始在面试中考察系统设计和场景问题,以此来更全面的考察求职者,不论是校招还是社招。不过,正常面试全是场景题的情况还是极少的,面试官一般会在面试中穿插一两个系统设计和场景题来考察你。

于是,我总结了这份《后端面试高频系统设计&场景题》,包含了常见的系统设计案例比如短链系统、秒杀系统以及高频的场景题比如海量数据去重、第三方授权登录。

即使不是准备面试,我也强烈推荐你认真阅读这一系列文章,这对于提升自己系统设计思维和解决实际问题的能力还是非常有帮助的。并且,涉及到的很多案例都可以用到自己的项目上比如抽奖系统设计、第三方授权登录、Redis 实现延时任务的正确方式。

《后端面试高频系统设计&场景题》本身是属于《Java 面试指北》的一部分,后面由于内容篇幅较多,因此被单独提了出来。


schedule task


title: Java 定时任务详解 category: 系统设计 icon: "mdi:clock-outline" description: 系统讲解 Java 定时任务与延时任务:Timer、ScheduledThreadPoolExecutor、DelayQueue、时间轮、Spring @Scheduled(Cron 表达式),以及 Quartz、XXL-JOB、ElasticJob、PowerJob 等分布式任务调度框架的选型对比与适用场景(订单超时取消/定时备份/定时抓取)。 head:

  • - meta
- name: keywords
  content: 定时任务,Quartz,Elastic-Job,XXL-JOB,PowerJob

为什么需要定时任务?

我们来看一下几个非常常见的业务场景:

  1. 某系统凌晨 1 点要进行数据备份。
  2. 某电商平台,用户下单半个小时未支付的情况下需要自动取消订单。
  3. 某媒体聚合平台,每 10 分钟动态抓取某某网站的数据为自己所用。
  4. 某博客平台,支持定时发送文章。
  5. 某基金平台,每晚定时计算用户当日收益情况并推送给用户最新的数据。
  6. ……

这些场景往往都要求我们在某个特定的时间去做某个事情,也就是定时或者延时去做某个事情。

  • 定时任务:在指定时间点执行特定的任务,例如每天早上 8 点,每周一下午 3 点等。定时任务可以用来做一些周期性的工作,如数据备份,日志清理,报表生成等。
  • 延时任务:一定的延迟时间后执行特定的任务,例如 10 分钟后,3 小时后等。延时任务可以用来做一些异步的工作,如订单取消,推送通知,红包撤回等。

尽管二者的适用场景有所区别,但它们的核心思想都是将任务的执行时间安排在未来的某个点上,以达到预期的调度效果。

单机定时任务

Timer

java.util.Timer是 JDK 1.3 开始就已经支持的一种定时任务的实现方式。

Timer 内部使用一个叫做 TaskQueue 的类存放定时任务,它是一个基于最小堆实现的优先级队列。TaskQueue 会按照任务距离下一次执行时间的大小将任务排序,保证在堆顶的任务最先执行。这样在需要执行任务时,每次只需要取出堆顶的任务运行即可!

Timer 使用起来比较简单,通过下面的方式我们就能创建一个 1s 之后执行的定时任务。

// 示例代码:
TimerTask task = new TimerTask() {
    public void run() {
        System.out.println("当前时间: " + new Date() + "\n" +
                "线程名称: " + Thread.currentThread().getName());
    }
};
System.out.println("当前时间: " + new Date() + "\n" +
        "线程名称: " + Thread.currentThread().getName());
Timer timer = new Timer("Timer");
long delay = 1000L;
timer.schedule(task, delay);


//输出:
当前时间: Fri May 28 15:18:47 CST 2021
线程名称: main
当前时间: Fri May 28 15:18:48 CST 2021
线程名称: Timer

不过其缺陷较多。每个 Timer 只使用一个后台线程串行执行所有任务,某个任务执行过久会推迟其他任务。如果 TimerTask#run() 抛出未捕获的运行时异常或错误,该 Timer 的唯一执行线程会终止,后续任务也无法继续调度。这不是“Timer 只捕获 InterruptedException”可以准确概括的行为。

Timer 类上的有一段注释是这样写的:

 * This class does not offer real-time guarantees: it schedules
 * tasks using the <tt>Object.wait(long)</tt> method.
 *Java 5.0 introduced the {@code java.util.concurrent} package and
 * one of the concurrency utilities therein is the {@link
 * java.util.concurrent.ScheduledThreadPoolExecutor
 * ScheduledThreadPoolExecutor} which is a thread pool for repeatedly
 * executing tasks at a given rate or delay.  It is effectively a more
 * versatile replacement for the {@code Timer}/{@code TimerTask}
 * combination, as it allows multiple service threads, accepts various
 * time units, and doesn't require subclassing {@code TimerTask} (just
 * implement {@code Runnable}).  Configuring {@code
 * ScheduledThreadPoolExecutor} with one thread makes it equivalent to
 * {@code Timer}.

大概的意思就是:ScheduledThreadPoolExecutor 支持多线程执行定时任务并且功能更强大,是 Timer 的替代品。

ScheduledExecutorService

ScheduledExecutorService 是一个接口,有多个实现类,比较常用的是 ScheduledThreadPoolExecutor 。

ScheduledThreadPoolExecutor 本身就是一个线程池,支持任务并发执行。并且,其内部使用 DelayedWorkQueue 作为任务队列。

// 示例代码:
TimerTask repeatedTask = new TimerTask() {
    @SneakyThrows
    public void run() {
        System.out.println("当前时间: " + new Date() + "\n" +
                "线程名称: " + Thread.currentThread().getName());
    }
};
System.out.println("当前时间: " + new Date() + "\n" +
        "线程名称: " + Thread.currentThread().getName());
ScheduledExecutorService executor = Executors.newScheduledThreadPool(3);
long delay  = 1000L;
long period = 1000L;
executor.scheduleAtFixedRate(repeatedTask, delay, period, TimeUnit.MILLISECONDS);
Thread.sleep(delay + period * 5);
executor.shutdown();
//输出:
当前时间: Fri May 28 15:40:46 CST 2021
线程名称: main
当前时间: Fri May 28 15:40:47 CST 2021
线程名称: pool-1-thread-1
当前时间: Fri May 28 15:40:48 CST 2021
线程名称: pool-1-thread-1
当前时间: Fri May 28 15:40:49 CST 2021
线程名称: pool-1-thread-2
当前时间: Fri May 28 15:40:50 CST 2021
线程名称: pool-1-thread-2
当前时间: Fri May 28 15:40:51 CST 2021
线程名称: pool-1-thread-2
当前时间: Fri May 28 15:40:52 CST 2021
线程名称: pool-1-thread-2

不论是使用 Timer 还是 ScheduledExecutorService 都无法使用 Cron 表达式指定任务执行的具体时间。

DelayQueue

DelayQueue 是 JUC 包(java.util.concurrent)为我们提供的延迟队列,用于实现延时任务比如订单下单 15 分钟未支付直接取消。它是 BlockingQueue 的一种,底层是一个基于 PriorityQueue 实现的一个无界队列,是线程安全的。关于PriorityQueue可以参考笔者编写的这篇文章:PriorityQueue 源码分析 。

BlockingQueue 的实现类

DelayQueue 和 Timer/TimerTask 都可以作为延时调度的基础。DelayQueue 使用优先级队列管理实现了 Delayed 接口的元素,只有延迟到期的元素才能被取出,但它本身不负责创建线程执行任务;通常还需要编写消费循环并选择合适的执行器。Timer 则自带一个执行线程。两者都可以在创建后继续添加任务,也都支持取消或移除任务,“Timer 只能在创建时指定任务”并不成立。

关于 DelayQueue 的详细介绍,请参考我写的这篇文章:DelayQueue 源码分析。

Spring Task

我们直接通过 Spring 提供的 @Scheduled 注解即可定义定时任务,非常方便!

/**
 * cron:使用Cron表达式。 每分钟的1,2秒运行
 */
@Scheduled(cron = "1-2 * * * * ? ")
public void reportCurrentTimeWithCronExpression() {
  log.info("Cron Expression: The time is now {}", dateFormat.format(new Date()));
}

我在大学那会做的一个 SSM 的企业级项目,就是用的 Spring Task 来做的定时任务。

并且,Spring Task 还是支持 Cron 表达式 的。Cron 表达式主要用于定时作业(定时任务)系统定义执行时间或执行频率的表达式,非常厉害,你可以通过 Cron 表达式进行设置定时任务每天或者每个月什么时候执行等等操作。咱们要学习定时任务的话,Cron 表达式是一定是要重点关注的。推荐一个在线 Cron 表达式生成器:http://cron.qqe2.com/ 。

但是,Spring 自带的定时调度只支持单机,并且提供的功能比较单一。之前写过一篇文章:《5 分钟搞懂如何在 Spring Boot 中 Schedule Tasks》 ,不了解的小伙伴可以参考一下。

Spring 通过 TaskScheduler 提供调度抽象,并不固定只有一种底层实现。常用的 ThreadPoolTaskScheduler 内部委托给 ScheduledExecutorService;在 Jakarta EE 等环境中也可以使用容器管理的调度器。@Scheduled 本身不提供集群协调,同一应用部署多个实例时,每个实例都可能触发任务。

优缺点总结:

  • 优点:简单,轻量,支持 Cron 表达式
  • 缺点:功能单一

时间轮

Kafka、Dubbo、ZooKeeper、Netty、Caffeine、Akka 中都有对时间轮的实现。

时间轮简单来说就是一个环形的队列(底层一般基于数组实现),队列中的每一个元素(时间格)都可以存放一个定时任务列表。

时间轮中的每个时间格代表了时间轮的基本时间跨度或者说时间精度,假如时间一秒走一个时间格的话,那么这个时间轮的最高精度就是 1 秒(也就是说 3 s 和 3.9s 会在同一个时间格中)。

下图是一个有 12 个时间格的时间轮,转完一圈需要 12 s。当我们需要新建一个 3s 后执行的定时任务,只需要将定时任务放在下标为 3 的时间格中即可。当我们需要新建一个 9s 后执行的定时任务,只需要将定时任务放在下标为 9 的时间格中即可。

那当我们需要创建一个 13s 后执行的定时任务怎么办呢?这个时候可以引入 圈数/轮数 的概念。任务仍位于下标为 1 的时间格,同时记录它需要等待的剩余轮数,完整走过一轮再经过 1s 后才执行。不同实现对“当前格是否计入轮数”的约定可能不同,不应脱离具体实现固定写成 2 圈。

除了增加圈数这种方法之外,还有一种 多层次时间轮 (类似手表),Kafka 采用的就是这种方案。

针对下图的时间轮,我来举一个例子便于大家理解。

上图的时间轮(ms -> s),第 1 层的时间精度为 1 ,第 2 层的时间精度为 20 ,第 3 层的时间精度为 400。假如我们需要添加一个 350s 后执行的任务 A 的话(当前时间是 0s),这个任务会被放在第 2 层(因为第二层的时间跨度为 20\*20=400>350)的第 350/20=17 个时间格子。

当第一层转了 17 圈之后,时间过去了 340s ,第 2 层的指针此时来到第 17 个时间格子。此时,第 2 层第 17 个格子的任务会被移动到第 1 层。

任务 A 当前是 10s 之后执行,因此它会被移动到第 1 层的第 10 个时间格子。

这里在层与层之间的移动也叫做时间轮的升降级。参考手表来理解就好!

时间轮比较适合管理大量定时器的场景。在时间轮层数和每层槽数有界的典型实现中,定位槽位、插入和推进指针的开销通常可以做到接近 O(1);但某个刻度的实际执行开销仍取决于到期任务数、级联迁移和具体实现。

分布式定时任务

Redis

Redis 是可以用来做延时任务的,基于 Redis 实现延时任务的功能无非就下面两种方案:

  1. Redis 过期事件监听
  2. Redisson 内置的延时队列

这部分内容的详细介绍我放在了《后端面试高频系统设计&场景题》中,有需要的同学可以进入星球后阅读学习。篇幅太多,这里就不重复分享了。

《后端面试高频系统设计&场景题》

MQ

大部分消息队列,例如 RocketMQ、RabbitMQ,都支持定时/延时消息。定时消息和延时消息本质其实是相同的,都是服务端根据消息设置的定时时间在某一固定时刻将消息投递给消费者消费。

不过,在使用 MQ 定时消息之前一定要看清具体产品和版本的限制。例如,RocketMQ 4.x 的官方实现提供 18 个固定延时级别,最长为 2 小时;RocketMQ 5.x 改为按毫秒级 Unix 时间戳设置投递时间,默认允许的最大定时范围为 24 小时。不能把这两套版本机制合并为同一条限制。

优缺点总结:

  • 优点:可以与 Spring 集成、支持分布式、支持集群、性能不错
  • 缺点:功能性较差、不灵活、需要保障消息可靠性

分布式任务调度框架

如果我们需要一些高级特性比如支持任务在分布式场景下的分片和高可用的话,我们就需要用到分布式任务调度框架了。

通常情况下,一个分布式定时任务的执行往往涉及到下面这些角色:

  • 任务:首先肯定是要执行的任务,这个任务就是具体的业务逻辑比如定时发送文章。
  • 调度器:其次是调度中心,调度中心主要负责任务管理,会分配任务给执行器。
  • 执行器:最后就是执行器,执行器接收调度器分派的任务并执行。

Quartz

一个很火的开源任务调度框架,完全由 Java 写成。Quartz 可以说是 Java 定时任务领域的老大哥或者说参考标准,其他的任务调度框架基本都是基于 Quartz 开发的,比如当当网的elastic-job就是基于 Quartz 二次开发之后的分布式调度解决方案。

使用 Quartz 可以很方便地与 Spring 集成,并且支持动态添加任务和集群。但是,Quartz 使用起来也比较麻烦,API 繁琐。

并且,Quartz 并没有内置 UI 管理控制台,不过你可以使用 quartzui 这个开源项目来解决这个问题。

另外,Quartz 虽然也支持分布式任务。但是,它是在数据库层面,通过数据库的锁机制做的,有非常多的弊端比如系统侵入性严重、节点负载不均衡。有点伪分布式的味道。

优缺点总结:

  • 优点:可以与 Spring 集成,并且支持动态添加任务和集群。
  • 缺点:分布式支持不友好,不支持任务可视化管理、使用麻烦(相比于其他同类型框架来说)

Elastic-Job

ElasticJob 最初由当当网开源,历史上曾分为 ElasticJob-Lite 和 ElasticJob-Cloud 两个子项目。这种分类以及 ElasticJob-Cloud 对 Mesos 的依赖属于早期版本语境,不应作为当前选型表。当前官方将 ElasticJob 描述为提供分布式任务分片的轻量级、去中心化解决方案,注册中心支持 ZooKeeper 和 etcd。

ElasticJob 支持任务在分布式场景下的分片和高可用、任务可视化管理等功能。

下面是早期 ElasticJob-Lite 以 ZooKeeper 为注册中心的架构图,用于理解其去中心化调度思路;当前版本还支持 etcd,不能把图中的组件当成唯一部署方式。

ElasticJob-Lite 的架构设计

在这种部署中,ElasticJob 不设置中心化调度服务,而是使用 ZooKeeper 协调各节点的任务分片。当前版本也可以选择 etcd 作为注册中心。

Elastic-Job 中的定时调度都是由执行器自行触发,这种设计也被称为去中心化设计(调度和处理都是执行器单独完成)。

当前官方 Spring Boot Starter 通过 Spring Bean 和配置文件注册任务,不提供 @ElasticJobConf 注解。下面以 ZooKeeper 注册中心为例:

@Component
public class TestJob implements SimpleJob {
    @Override
    public void execute(ShardingContext context) {
        System.out.printf("任务名:%s,分片总数:%d,当前分片参数:%s%n",
                context.getJobName(),
                context.getShardingTotalCount(),
                context.getShardingParameter());
    }
}
elasticjob:
  regCenter:
    serverLists: localhost:2181
    namespace: elasticjob-demo
  jobs:
    dayJob:
      elasticJobClass: com.example.job.TestJob
      cron: 0/10 * * * * ?
      shardingTotalCount: 2
      shardingItemParameters: 0=AAAA,1=BBBB

配置属性和注册中心类型会随版本演进,接入时应以所用版本的 Spring Boot Starter 文档和注册中心文档为准。

相关地址:

  • GitHub 地址:。
  • 官方网站: 。

优缺点总结:

  • 优点:可以与 Spring 集成、支持分布式、支持集群、性能不错、支持任务可视化管理
  • 缺点:需要额外部署 ZooKeeper 或 etcd 等注册中心,会增加系统复杂度和维护成本

XXL-JOB

XXL-JOB 于 2015 年开源,是一款优秀的轻量级分布式任务调度框架,支持任务可视化管理、弹性扩容缩容、任务失败重试和告警、任务分片等功能,

根据 XXL-JOB 官网介绍,其解决了很多 Quartz 的不足。

Quartz 作为开源作业调度中的佼佼者,是作业调度的首选。但是集群环境中 Quartz 采用 API 的方式对任务进行管理,从而可以避免上述问题,但是同样存在以下问题: - 问题一:调用 API 的方式操作任务,不人性化; - 问题二:需要持久化业务 QuartzJobBean 到底层数据表中,系统侵入性相当严重。 - 问题三:调度逻辑和 QuartzJobBean 耦合在同一个项目中,这将导致一个问题,在调度任务数量逐渐增多,同时调度任务逻辑逐渐加重的情况下,此时调度系统的性能将大大受限于业务; - 问题四:quartz 底层以“抢占式”获取 DB 锁并由抢占成功节点负责运行任务,会导致节点负载悬殊非常大;而 XXL-JOB 通过执行器实现“协同分配式”运行任务,充分发挥集群优势,负载各节点均衡。 XXL-JOB 弥补了 quartz 的上述不足之处。

XXL-JOB 的架构设计如下图所示:

从上图可以看出,XXL-JOB 由 调度中心 和 执行器 两大部分组成。调度中心主要负责任务管理、执行器管理以及日志管理。执行器主要是接收调度信号并处理。另外,调度中心进行任务调度时,是通过自研 RPC 来实现的。

不同于 Elastic-Job 的去中心化设计, XXL-JOB 的这种设计也被称为中心化设计(调度中心调度多个执行器执行任务)。

和 Quzrtz 类似 XXL-JOB 也是基于数据库锁调度任务,存在性能瓶颈。不过,一般在任务量不是特别大的情况下,没有什么影响的,可以满足绝大部分公司的要求。

当前版本推荐在 Spring Bean 的无参 void 方法上使用 @XxlJob。任务参数、日志和执行结果通过 XxlJobHelper 处理;@JobHandler、带 String 入参并返回 ReturnT 的写法属于旧版 API。

@Component
public class MyApiJobHandler {

    @XxlJob("myApiJobHandler")
    public void execute() {
        String param = XxlJobHelper.getJobParam();
        XxlJobHelper.log("任务参数:{}", param);
        // 执行业务逻辑;默认执行结果为成功,失败时可调用 XxlJobHelper.handleFail(...)
    }
}

相关地址:

  • GitHub 地址:。
  • 官方介绍: 。

优缺点总结:

  • 优点:开箱即用(学习成本比较低)、与 Spring 集成、支持分布式、支持集群、支持任务可视化管理。
  • 缺点:不支持动态添加任务(如果一定想要动态创建任务也是支持的,参见:xxl-job issue277)。

PowerJob

非常值得关注的一个分布式任务调度框架,分布式任务调度领域的新星。目前,已经有很多公司接入比如 OPPO、京东、中通、思科。

这个框架的诞生也挺有意思的,PowerJob 的作者当时在阿里巴巴实习过,阿里巴巴那会使用的是内部自研的 SchedulerX(阿里云付费产品)。实习期满之后,PowerJob 的作者离开了阿里巴巴。想着说自研一个 SchedulerX,防止哪天 SchedulerX 满足不了需求,于是 PowerJob 就诞生了。

更多关于 PowerJob 的故事,小伙伴们可以去看看 PowerJob 作者的视频 《我和我的任务调度中间件》。简单点概括就是:“游戏没啥意思了,我要扛起了新一代分布式任务调度与计算框架的大旗!”。

由于 SchedulerX 属于人民币产品,我这里就不过多介绍。PowerJob 官方也对比过其和 QuartZ、XXL-JOB 以及 SchedulerX。下表是项目方的功能对比,不是独立基准测试;性能和容量仍需结合版本、数据库、部署规模与业务负载验证。

QuartZxxl-jobSchedulerX 2.0PowerJob
定时类型CRONCRONCRON、固定频率、固定延迟、OpenAPICRON、固定频率、固定延迟、OpenAPI
任务类型内置 Java内置 Java、GLUE Java、Shell、Python 等脚本内置 Java、外置 Java(FatJar)、Shell、Python 等脚本内置 Java、外置 Java(容器)、Shell、Python 等脚本
分布式计算无静态分片MapReduce 动态分片MapReduce 动态分片
在线任务治理不支持支持支持支持
日志白屏化不支持支持不支持支持
调度方式及性能基于数据库锁,有性能瓶颈基于数据库锁,有性能瓶颈不详项目方称采用无锁化设计,实际容量需压测
报警监控无邮件短信WebHook、邮件、钉钉与自定义扩展
系统依赖JDBC 支持的关系型数据库(MySQL、Oracle...)MySQL人民币任意 Spring Data Jpa 支持的关系型数据库(MySQL、Oracle...)
DAG 工作流不支持不支持支持支持

定时任务方案总结

单机定时任务的常见解决方案有 Timer、ScheduledExecutorService、DelayQueue、Spring Task 和时间轮。对普通 Java 周期或延时任务,通常优先使用 ScheduledExecutorService 或 Spring Task;时间轮更适合大量定时器、允许一定时间精度换取调度性能的场景,并不是所有单机任务的默认最佳方案。

Redis 和 MQ 虽然可以实现分布式延时触发,但它们通常不提供完整的任务编排、分片、失败补偿和可视化管理。可靠 MQ 的常见交付语义是“至少一次”,在超时、重试或故障切换时可能出现重复投递,因此消费端必须做幂等处理。周期任务需要调度器持续产生新的触发事件,与一条消息能否被“消费多次”没有直接关系。MQ 仍然很适合订单超时取消等一次性延时触发,也能用于解耦调度与执行。

无论选择哪种方案,上生产前都应明确以下语义:

  • 任务是否幂等,如何处理重复执行;
  • 应用停机或错过执行时间后,是补执行、跳过还是合并执行;
  • 超时、重试、退避、最大尝试次数和死信/人工补偿策略;
  • 时区、夏令时、时钟回拨以及集群时钟偏差的处理方式;
  • 集群中如何避免非预期的重复调度,并对延迟、成功率、重试和积压建立监控告警。

Quartz、Elastic-Job、XXL-JOB 和 PowerJob 这几个是专门用来做分布式调度的框架,提供的分布式定时任务的功能更为完善和强大,更加适合执行周期性的定时任务。除了 Quartz 之外,另外三者都是支持任务可视化管理的。

XXL-JOB 2015 年推出,使用门槛相对较低,采用中心化调度;ElasticJob 采用去中心化调度,并通过 ZooKeeper 或 etcd 协调任务分片。两者的架构和运维依赖不同,不能据此直接断言某一个框架“性能更好”。选型时应针对实际版本、任务数量、触发频率、分片规模、故障恢复要求和数据库或注册中心负载进行压测。PowerJob 等其他框架也应按相同维度验证,不宜只依据项目方的功能对比表下结论。

这篇文章并没有介绍到实际使用,但是,并不代表实际使用不重要。我在写这篇文章之前,已经动手写过相应的 Demo。像 Quartz,我在大学那会就用过。不过,当时用的是 Spring 。为了能够更好地体验,我自己又在 Spring Boot 上实际体验了一下。如果你并没有实际使用某个框架,就直接说它并不好用的话,是站不住脚的。


web real time message push


title: Web 实时消息推送详解 description: 消息推送通常是指网站的运营工作等人员,通过某种工具对用户当前网页或移动设备 APP 进行的主动消息推送。 category: 系统设计 icon: "mdi:message-text-outline" head:

  • - meta
- name: keywords
  content: Web消息推送,实时消息,WebSocket,SSE,长轮询,短轮询,MQTT,实时通信方案

原文地址: 对本文进行了完善总结。

我有一个朋友做了一个小破站,现在要实现一个站内信 Web 消息推送的功能,对,就是下图这个小红点,一个很常用的功能。

站内信 Web 消息推送

不过他还没想好用什么方式做,这里我帮他整理了一下几种方案,并简单做了实现。

什么是消息推送?

推送的场景比较多,比如有人关注我的公众号,这时我就会收到一条推送消息,以此来吸引我点击打开应用。

消息推送通常是指网站的运营工作等人员,通过某种工具对用户当前网页或移动设备 APP 进行的主动消息推送。

消息推送一般又分为 Web 端消息推送和移动端消息推送。

移动端消息推送示例:

移动端消息推送示例

Web 端消息推送示例:

Web 端消息推送示例

在具体实现之前,咱们再来分析一下前边的需求,其实功能很简单,只要触发某个事件(主动分享了资源或者后台主动推送消息),Web 页面的通知小红点就会实时的 +1 就可以了。

通常在服务端会有若干张消息推送表,用来记录用户触发不同事件所推送不同类型的消息,前端主动查询(拉)或者被动接收(推)用户所有未读的消息数。

消息推送表

消息推送无非是推(push)和拉(pull)两种形式,下边我们逐个了解下。

消息推送常见方案

Web 实时消息推送方案总览

短轮询

轮询(polling) 应该是实现消息推送方案中最简单的一种,这里我们暂且将轮询分为短轮询和长轮询。

短轮询很好理解,指定的时间间隔,由浏览器向服务器发出 HTTP 请求,服务器实时返回未读消息数据给客户端,浏览器再做渲染显示。

一个简单的 JS 定时器就可以搞定,每秒钟请求一次未读消息数接口,返回的数据展示即可。

setInterval(() => {
  // 方法请求
  messageCount().then((res) => {
    if (res.code === 200) {
      this.messageCount = res.data;
    }
  });
}, 1000);

效果还是可以的,短轮询实现固然简单,缺点也是显而易见,由于推送数据并不会频繁变更,无论后端此时是否有新的消息产生,客户端都会进行请求,势必会对服务端造成很大压力,浪费带宽和服务器资源。

长轮询

长轮询是对上边短轮询的一种改进版本,在尽可能减少对服务器资源浪费的同时,保证消息的相对实时性。长轮询在中间件中应用的很广泛,比如 Nacos 和 Apollo 配置中心,消息队列 Kafka、RocketMQ 中都有用到长轮询。

Nacos 配置中心交互模型是 push 还是 pull?一文中我详细介绍过 Nacos 长轮询的实现原理,感兴趣的小伙伴可以瞅瞅。

长轮询其实原理跟轮询差不多,都是采用轮询的方式。不过,如果服务端的数据没有发生变更,会 一直 hold 住请求,直到服务端的数据发生变化,或者等待一定时间超时才会返回。返回后,客户端又会立即再次发起下一次长轮询。

这次我使用 Apollo 配置中心实现长轮询的方式,应用了一个类DeferredResult,它是在 Servlet3.0 后经过 Spring 封装提供的一种异步请求机制,直意就是延迟结果。

长轮询示意图

DeferredResult 可以让容器先释放处理当前请求的 Servlet 线程,稍后再由应用选择的任意线程、消息回调或其他事件源调用 setResult() 恢复响应处理。DeferredResult 本身不会自动启动一个工作线程,实际业务在哪个线程上执行由应用决定。

下边我们用长轮询来实现消息推送。

因为一个 ID 可能会被多个长轮询请求监听,所以我采用了 Guava 包提供的 Multimap 结构存放长轮询,一个 key 可以对应多个 value。一旦监听到 key 发生变化,对应的所有长轮询都会响应。前端收到新的版本号后,主动查询未读消息数接口并更新页面数据。

@Controller
@RequestMapping("/polling")
public class PollingController {

    // 存放监听某个Id的长轮询集合
    // 线程同步结构
    private static final Multimap<String, DeferredResult<String>> watchRequests =
            Multimaps.synchronizedMultimap(HashMultimap.create());
    // 演示用的内存版本号;生产环境通常应使用业务数据自身的持久化版本
    private static final Map<String, Long> versions = new HashMap<>();

    /**
     * 设置监听
     */
    @GetMapping(path = "watch/{id}")
    @ResponseBody
    public DeferredResult<String> watch(@PathVariable String id,
                                        @RequestParam(defaultValue = "0") long version) {
        // 延迟对象设置超时时间
        DeferredResult<String> deferredResult = new DeferredResult<>(TIME_OUT, "timeout");
        // 异步请求完成时移除 key,防止内存溢出
        deferredResult.onCompletion(() -> {
            watchRequests.remove(id, deferredResult);
        });
        // 版本比较和监听注册必须处于同一临界区,避免发布发生在二者之间而丢通知
        synchronized (watchRequests) {
            long currentVersion = versions.getOrDefault(id, 0L);
            if (currentVersion != version) {
                deferredResult.setResult(Long.toString(currentVersion));
            } else {
                watchRequests.put(id, deferredResult);
            }
        }
        return deferredResult;
    }

    /**
     * 变更数据
     */
    @PostMapping(path = "publish/{id}")
    @ResponseBody
    public String publish(@PathVariable String id) {
        // 在同一同步块内先更新版本,再移除并复制监听快照
        Collection<DeferredResult<String>> deferredResults;
        long currentVersion;
        synchronized (watchRequests) {
            currentVersion = versions.merge(id, 1L, Long::sum);
            deferredResults = new ArrayList<>(watchRequests.removeAll(id));
        }
        for (DeferredResult<String> deferredResult : deferredResults) {
            deferredResult.setResult(Long.toString(currentVersion));
        }
        return "success";
    }
}

这里通过 DeferredResult 的超时结果返回约定好的 "timeout",前端收到后携带原版本号立即发起下一次长轮询。收到新版本号时,前端先查询最新业务数据,再把该版本号用于下一次监听。版本比较、监听注册和发布递增版本都在同一个临界区内完成,因此即使更新恰好发生在两次请求交接期间,下一次监听也会立即发现版本变化。示例中的内存版本会随进程重启丢失,生产环境应优先使用数据库版本、消息位点等持久化标识,并设计清理策略。

不要把 HTTP 304 用作普通的“请求超时”状态:304 专用于条件请求中表示已缓存的表现仍未修改,且不能包含响应内容。生产项目也可以约定 204 等无内容响应,关键是让服务端和客户端对超时语义保持一致。

我们来测试一下,首先页面携带已知版本发起长轮询请求 /polling/watch/10086?version=0 监听消息变更,请求被挂起;紧接着手动变更数据 /polling/publish/10086,长轮询返回新版本。前端查询最新数据后,再携带新版本发起下一次请求,如此循环往复。

长轮询相比于短轮询在性能上提升了很多,但依然会产生较多的请求,这是它的一点不完美的地方。

iframe 流

iframe 流就是在页面中插入一个隐藏的`


服务端需要保持响应并在有新数据时写入、及时刷新缓冲区。不能在 Servlet 请求线程中使用不带等待、中断和异常处理的 `while (true)` 循环持续写响应:这会空转消耗 CPU、长时间占用容器线程,还无法在客户端断开时正常收敛。如果必须维护旧式 iframe 流,应使用容器异步 I/O 和事件驱动的写入模型;新系统通常直接选择 SSE 或 WebSocket。

iframe 流的服务器开销很大,而且 IE、Chrome 等浏览器一直会处于 loading 状态,图标会不停旋转,简直是强迫症杀手。

![iframe 流效果](https://oss.javaguide.cn/github/javaguide/system-design/web-real-time-message-push/1460000042192389.png)

iframe 流非常不友好,强烈不推荐。

### SSE (推荐)

很多人可能不知道,服务端向客户端推送消息,其实除了可以用`WebSocket`这种耳熟能详的机制外,还有一种服务器发送事件(Server-Sent Events),简称 SSE。这是一种服务器端到客户端(浏览器)的单向消息推送。

流式对话是 SSE 的一个典型应用场景。服务端可以把已经生成的部分内容持续写入事件流,用户无需等到全部计算完成才看到结果。

![ChatGPT 使用 SSE 实现对话](https://oss.javaguide.cn/github/javaguide/system-design/web-real-time-message-push/chatgpt-sse.png)

SSE 基于 HTTP,它不是让服务端在没有请求的情况下凭空建立连接,而是让客户端先发起请求,服务端保持该 HTTP 响应并持续写入事件。

![SSE 图解](https://oss.javaguide.cn/github/javaguide/system-design/web-real-time-message-push/1460000042192390.png)

SSE 在服务器和客户端之间打开一个单向通道,服务端响应的不再是一次性的数据包而是`text/event-stream`类型的数据流信息,在有数据变更时从服务器流式传输到客户端。

整体的实现思路有点类似于在线视频播放,视频流会连续不断的推送到浏览器,你也可以理解成,客户端在完成一次用时很长(网络不畅)的下载。

![SSE 示意图](https://oss.javaguide.cn/github/javaguide/system-design/web-real-time-message-push/1460000042192391.png)

SSE 与 WebSocket 作用相似,都可以建立服务端与浏览器之间的通信,实现服务端向客户端推送消息,但还是有些许不同:

- SSE 使用 HTTP 和 `text/event-stream`,通常更容易接入现有 Web 服务;WebSocket 需要服务器或容器支持协议升级和 WebSocket 帧。它不要求必须单独部署一台服务器。
- SSE 单向通信,只能由服务端向客户端单向通信;WebSocket 全双工通信,即通信的双方可以同时发送和接受信息。
- SSE 实现简单开发成本低,无需引入其他组件;WebSocket 传输数据需做二次解析,开发门槛高一些。
- SSE 默认支持断线重连;WebSocket 则需要自己实现。
- SSE 只能传送文本消息,二进制数据需要经过编码后传送;WebSocket 默认支持传送二进制数据。

![SSE 和 WebSocket 对比](https://oss.javaguide.cn/github/javaguide/system-design/web-real-time-message-push/sse-vs-websocket-comparison.webp)

**SSE 与 WebSocket 该如何选择?**

> 技术并没有好坏之分,只有哪个更合适。

SSE 好像一直不被大家所熟知,一部分原因是出现了 WebSocket,这个提供了更丰富的协议来执行双向、全双工通信。对于游戏、即时通信以及需要双向近乎实时更新的场景,拥有双向通道更具吸引力。

但是,在某些情况下,不需要从客户端发送数据。而你只需要一些服务器操作的更新。比如:站内信、未读消息数、状态更新、股票行情、监控数量等场景,SSE 不管是从实现的难易和成本上都更加有优势。此外,SSE 具有 WebSocket 在设计上缺乏的多种功能,例如:自动重新连接、事件 ID 和发送任意事件的能力。

前端只需进行一次 HTTP 请求,带上唯一 ID,打开事件流,监听服务端推送的事件就可以了

javascript


服务端的实现更简单,创建一个`SseEmitter`对象放入`sseEmitterMap`进行管理

java private static Map sseEmitterMap = new ConcurrentHashMap<>();

/**

  • 创建连接

*/ public static SseEmitter connect(String userId) {

try {
    // 0 表示不设置应用层超时,并非“默认 30 秒”
    SseEmitter sseEmitter = new SseEmitter(0L);
    // 注册回调
    sseEmitter.onCompletion(completionCallBack(userId));
    sseEmitter.onError(errorCallBack(userId));
    sseEmitter.onTimeout(timeoutCallBack(userId));
    sseEmitterMap.put(userId, sseEmitter);
    count.getAndIncrement();
    return sseEmitter;
} catch (Exception e) {
    log.info("创建新的sse连接异常,当前用户:{}", userId);
}
return null;

}

/**

  • 给指定用户发送消息

*/ public static void sendMessage(String userId, String message) {

if (sseEmitterMap.containsKey(userId)) {
    try {
        sseEmitterMap.get(userId).send(message);
    } catch (IOException e) {
        log.error("用户[{}]推送异常:{}", userId, e.getMessage());
        removeUser(userId);
    }
}

}


上面的 `Map<String, SseEmitter>` 示例每个用户只保留一个连接,新连接会覆盖旧连接。如果要支持多标签页或多设备,应为每个用户维护一组 `SseEmitter`,并在完成、超时和错误回调中移除对应连接。`0L` 也不代表代理、网关和容器永不会断开连接,生产环境需要配置心跳、超时和客户端重连策略。

**注意:** SSE 不支持 IE 浏览器,对其他主流浏览器兼容性做的还不错。

![SSE 兼容性](https://oss.javaguide.cn/github/javaguide/system-design/web-real-time-message-push/1460000042192393.png)

### Websocket

Websocket 应该是大家都比较熟悉的一种实现消息推送的方式,上边我们在讲 SSE 的时候也和 Websocket 进行过比较。

这是一种在 TCP 连接上进行全双工通信的协议,建立客户端和服务器之间的通信渠道。浏览器和服务器仅需一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

![Websocket 示意图](https://oss.javaguide.cn/github/javaguide/system-design/web-real-time-message-push/1460000042192394.png)

WebSocket 的工作过程可以分为以下几个步骤:

1. 客户端向服务器发送一个 HTTP 请求,请求头中包含 `Upgrade: websocket` 和 `Sec-WebSocket-Key` 等字段,表示要求升级协议为 WebSocket;
2. 服务器收到这个请求后,会进行升级协议的操作,如果支持 WebSocket,它将回复一个 HTTP 101 状态码,响应头中包含 ,`Connection: Upgrade`和 `Sec-WebSocket-Accept: xxx` 等字段、表示成功升级到 WebSocket 协议。
3. 客户端和服务器之间建立了一个 WebSocket 连接,可以进行双向的数据传输。数据以帧(frames)的形式进行传送,而不是传统的 HTTP 请求和响应。WebSocket 的每条消息可能会被切分成多个数据帧(最小单位)。发送端会将消息切割成多个帧发送给接收端,接收端接收消息帧,并将关联的帧重新组装成完整的消息。
4. 客户端或服务器可以主动发送一个关闭帧,表示要断开连接。另一方收到后,也会回复一个关闭帧,然后双方关闭 TCP 连接。

另外,建立 WebSocket 连接之后,通过心跳机制来保持 WebSocket 连接的稳定性和活跃性。

SpringBoot 整合 WebSocket,先引入 WebSocket 相关的工具包,和 SSE 相比有额外的开发成本。

xml org.springframework.boot spring-boot-starter-websocket


服务端使用 `@ServerEndpoint` 注解标注当前类为一个 WebSocket 端点,客户端可以通过 `ws://localhost:7777/websocket/10086` 连接到服务端。路径中的 `userId` 只适合用于演示路由;生产环境必须在握手时验证用户身份,并把连接绑定到已认证的主体,不能相信客户端自行填写的 `userId`。

java @Component @Slf4j @ServerEndpoint("/websocket/{userId}") public class WebSocketServer {

//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
private String userId;
private static final CopyOnWriteArraySet<WebSocketServer> webSockets = new CopyOnWriteArraySet<>();
// 用来存在线连接数
private static final Map<String, Session> sessionPool = new ConcurrentHashMap<>();
/**
 * 链接成功调用的方法
 */
@OnOpen
public void onOpen(Session session, @PathParam(value = "userId") String userId) {
    try {
        this.session = session;
        this.userId = userId;
        webSockets.add(this);
        sessionPool.put(userId, session);
        log.info("websocket消息: 有新的连接,总数为:" + webSockets.size());
    } catch (Exception e) {
        log.error("WebSocket 连接初始化失败,userId={}", userId, e);
    }
}
/**
 * 连接关闭时清理当前会话
 */
@OnClose
public void onClose() {
    webSockets.remove(this);
    if (userId != null && session != null) {
        sessionPool.remove(userId, session);
    }
}
@OnError
public void onError(Throwable error) {
    log.error("WebSocket 连接异常,userId={}", userId, error);
}
/**
 * 收到客户端消息后调用的方法
 */
@OnMessage
public void onMessage(String message) {
    log.info("websocket消息: 收到客户端消息:" + message);
}
/**
 * 此为单点消息
 */
public static boolean sendOneMessage(String userId, String message) {
    Session session = sessionPool.get(userId);
    if (session != null && session.isOpen()) {
        try {
            log.info("websocket消: 单点消息:" + message);
            session.getAsyncRemote().sendText(message);
            return true;
        } catch (Exception e) {
            log.error("WebSocket 消息发送失败,userId={}", userId, e);
        }
    }
    return false;
}

}


该示例的 `sessionPool` 同样只保留每个 `userId` 的一个连接。支持多标签页或多设备时,应将值改为会话集合,并为每个连接独立执行发送、心跳、背压和清理逻辑。

如果希望通过 HTTP 接口触发一次服务端推送,可以增加一个与前端参数保持一致的控制器:

java @RestController @RequestMapping("/socket") public class SocketController {

@PostMapping("/publish")
public ResponseEntity<Void> publish(@RequestParam String userId,
                                    @RequestParam String message) {
    return WebSocketServer.sendOneMessage(userId, message)
            ? ResponseEntity.accepted().build()
            : ResponseEntity.notFound().build();
}

}


这个控制器只用于演示请求契约。生产环境还必须对发布接口进行身份认证和授权,不能允许调用方通过任意 `userId` 向其他用户推送消息;同时应限制消息大小和请求频率。

在使用内嵌 Servlet 容器的 Spring Boot 应用中,通常还需要注入 `ServerEndpointExporter`,由它注册使用了 `@ServerEndpoint` 注解的 WebSocket 端点。

java @Configuration public class WebSocketConfiguration {

/**
 * 用于注册使用了 @ServerEndpoint 注解的 WebSocket 服务器
 */
@Bean
public ServerEndpointExporter serverEndpointExporter() {
    return new ServerEndpointExporter();
}

}


前端初始化打开 WebSocket 连接,并监听连接状态,接收服务端数据或向服务端发送数据。

javascript


页面初始化建立 WebSocket 连接,之后就可以进行双向通信了,效果还不错。

![](https://oss.javaguide.cn/github/javaguide/system-design/web-real-time-message-push/1460000042192395.png)

### MQTT

**什么是 MQTT 协议?**

MQTT 是一种基于发布/订阅(publish/subscribe)模式的轻量级消息协议,通过订阅主题来获取消息,广泛应用于物联网场景。当前 OASIS 规范直接使用“MQTT”这一名称,不再将其展开为“Message Queue Telemetry Transport”。

该协议将消息的发布者(publisher)与订阅者(subscriber)进行分离,因此可以在不可靠的网络环境中,为远程连接的设备提供可靠的消息服务,使用方式与传统的 MQ 有点类似。

![MQTT 协议示例](https://oss.javaguide.cn/github/javaguide/system-design/web-real-time-message-push/1460000022986325.png)

MQTT 位于应用层,需要运行在有序、无损、双向的字节流传输上。最常见的承载方式是 TCP(生产环境通常配合 TLS),也可以通过 WebSocket 等能提供这种字节流语义的传输承载,因此不应简化为“只要有 TCP/IP 就一定能直接使用”。

**为什么要用 MQTT 协议?**

MQTT 协议为什么在物联网(IOT)中如此受偏爱?而不是其它协议,比如我们更为熟悉的 HTTP 协议呢?

- 经典的短连接 HTTP 请求-响应模式需要设备定期请求或保持长连接才能获取服务端更新;MQTT 则直接提供长连接上的异步发布/订阅模型。HTTP 本身不能笼统定义为“同步协议”,HTTP/2、HTTP/3、SSE 和 WebSocket 升级等机制的行为并不等同于经典的短轮询。
- HTTP 请求由客户端发起,但不意味着服务端永远无法流式返回数据,也不意味着设备不能接收命令。MQTT 的优势是把长连接、主题路由、订阅、QoS 和会话状态等能力标准化了。
- 需要向多个设备发送命令时,MQTT broker 可以按主题将消息分发给所有订阅者;HTTP 也能实现类似功能,但往往需要应用自行管理连接、设备组和重试语义。

具体的 MQTT 协议介绍和实践,这里我就不再赘述了,大家可以参考我之前的两篇文章,里边写的也都很详细了。

- MQTT 协议的介绍:[我也没想到 SpringBoot + RabbitMQ 做智能家居,会这么简单](https://mp.weixin.qq.com/s/udFE6k9pPetIWsa6KeErrA)
- MQTT 实现消息推送:[未读消息(小红点),前端 与 RabbitMQ 实时消息推送实践,贼简单~](https://mp.weixin.qq.com/s/U-fUGr9i1MVa4PoVyiDFCg)

## 总结

> 以下内容为 JavaGuide 补充

|           | 介绍                                                                                                          | 优点                   | 缺点                                                 |
| --------- | ------------------------------------------------------------------------------------------------------------- | ---------------------- | ---------------------------------------------------- |
| 短轮询    | 客户端定时向服务端发送请求,服务端直接返回响应数据(即使没有数据更新)                                        | 简单、易理解、易实现   | 实时性太差,无效请求太多,频繁建立连接太耗费资源     |
| 长轮询    | 与短轮询不同是,长轮询接收到客户端请求之后等到有数据更新才返回请求                                            | 减少了无效请求         | 挂起请求会导致资源浪费                               |
| iframe 流 | 服务端和客户端之间创建一条长连接,服务端持续向`iframe`传输数据。                                              | 简单、易理解、易实现   | 维护一个长连接会增加开销,效果太差(图标会不停旋转) |
| SSE       | 一种服务器端到客户端(浏览器)的单向消息推送。                                                                  | 简单、易实现,功能丰富 | 不支持双向通信                                       |
| WebSocket | 除了最初建立连接时用 HTTP 协议,其他时候都是直接基于 TCP 协议进行通信的,可以实现客户端和服务端的全双工通信。 | 性能高、开销小         | 对开发人员要求更高,实现相对复杂一些                 |
| MQTT      | 基于发布/订阅(publish/subscribe)模式的轻量级通讯协议,通过订阅相应的主题来获取消息。                        | 成熟稳定,轻量级       | 对开发人员要求更高,实现相对复杂一些                 |

<!-- @include: @article-footer.snippet.md -->


---

## spring cloud gateway questions

---
title: Spring Cloud Gateway 面试题总结:路由、Predicate、Filter、限流熔断与工作原理
category: 分布式
description: Spring Cloud Gateway 高频面试题总结,覆盖核心概念、路由匹配、Predicate、GatewayFilter、GlobalFilter、限流熔断、负载均衡、跨域处理和常见生产问题。
tag:
  - API 网关
  - Spring Cloud
head:
  - - meta
    - name: keywords
      content: Spring Cloud Gateway,Spring Cloud Gateway 面试题,API 网关,Predicate,GatewayFilter,GlobalFilter,网关限流,网关熔断,微服务网关
---

> 本文重构完善自[6000 字 | 16 图 | 深入理解 Spring Cloud Gateway 的原理 - 悟空聊架构](https://mp.weixin.qq.com/s/XjFYsP1IUqNzWqXZdJn-Aw)这篇文章。

这篇文章只展开 Spring Cloud Gateway 的面试高频点。如果你还没搞清楚 API 网关为什么存在、网关和 RPC 的关系、Zuul / Gateway / Kong / APISIX 怎么选,建议先看 [API 网关详解](https://raw.githubusercontent.com/Snailclimb/JavaGuide/HEAD/api-gateway.md)。

## 什么是 Spring Cloud Gateway?

Spring Cloud Gateway 属于 Spring Cloud 生态系统中的网关,其诞生的目标主要是为了替代 **Zuul 1.x**。Zuul 1.x 基于 Servlet 阻塞 I/O 架构,在高并发场景下性能有限。而 Zuul 2.x 虽然采用了 Netty 非阻塞架构,但 Spring Cloud 官方并未正式集成 Zuul 2.x。Spring Cloud Gateway 起步要比 Zuul 2.x 更早。

为了提升网关的性能,Spring Cloud Gateway 基于 Spring WebFlux 。Spring WebFlux 使用 Reactor 库来实现响应式编程模型,底层基于 Netty 实现同步非阻塞的 I/O。

![](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway/springcloud-gateway-%20demo.png)

Spring Cloud Gateway 不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/指标,限流。

Spring Cloud Gateway 和 Zuul 2.x 的差别不大,也是通过过滤器来处理请求。不过,目前更加推荐使用 Spring Cloud Gateway 而非 Zuul,Spring Cloud 生态对其支持更加友好。

- GitHub 地址: <https://github.com/spring-cloud/spring-cloud-gateway>
- 官网: <https://spring.io/projects/spring-cloud-gateway>

## Spring Cloud Gateway 的工作流程?

Spring Cloud Gateway 的工作流程如下图所示:

![Spring Cloud Gateway 的工作流程](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway/spring-cloud-gateway-workflow.png)

这是 Spring 官方博客中的一张图,原文地址:<https://spring.io/blog/2022/08/26/creating-a-custom-spring-cloud-gateway-filter>。

具体的流程分析:

1. **路由判断**:客户端的请求到达网关后,先经过 Gateway Handler Mapping 处理,这里面会做断言(Predicate)判断,看下符合哪个路由规则,这个路由映射后端的某个服务。
2. **请求过滤**:然后请求到达 Gateway Web Handler,这里面有很多过滤器,组成过滤器链(Filter Chain),这些过滤器可以对请求进行拦截和修改,比如添加请求头、参数校验等等,有点像净化污水。然后将请求转发到实际的后端服务。这些过滤器逻辑上可以称作 Pre-Filters,Pre 可以理解为“在...之前”。
3. **服务处理**:后端服务会对请求进行处理。
4. **响应过滤**:后端处理完结果后,返回给 Gateway 的过滤器再次做处理,逻辑上可以称作 Post-Filters,Post 可以理解为“在...之后”。
5. **响应返回**:响应经过过滤处理后,返回给客户端。

总结:客户端的请求先通过匹配规则找到合适的路由,就能映射到具体的服务。然后请求经过过滤器处理后转发给具体的服务,服务处理后,再次经过过滤器处理,最后返回给客户端。

## Spring Cloud Gateway 的断言是什么?

断言(Predicate)这个词听起来比较抽象,它可以理解为对请求条件做一次判断:结果为真就匹配当前路由,结果为假就继续匹配其他路由。

在 Gateway 中,如果客户端发送的请求满足了断言的条件,则映射到指定的路由器,就能转发到指定的服务上进行处理。

断言配置的示例如下,配置了两个路由规则,有一个 predicates 断言配置,当请求 url 中包含 `api/thirdparty`,就匹配到了第一个路由 `route_thirdparty`。

![断言配置示例](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway/spring-cloud-gateway-predicate-example.png)

常见的路由断言规则如下图所示:

![Spring Cloud GateWay 路由断言规则](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway/spring-cloud-gateway-predicate-rules.png)

## Spring Cloud Gateway 的路由和断言是什么关系?

Route 路由和 Predicate 断言的对应关系如下::

![路由和断言的对应关系](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway/spring-cloud-gateway-predicate-route.png)

- **一对多**:一个路由规则可以包含多个断言。如上图中路由 Route1 配置了三个断言 Predicate。
- **同时满足**:如果一个路由规则中有多个断言,则需要同时满足才能匹配。如上图中路由 Route2 配置了两个断言,客户端发送的请求必须同时满足这两个断言,才能匹配路由 Route2。
- **第一个匹配成功**:如果一个请求可以匹配多个路由,则映射第一个匹配成功的路由。如上图所示,客户端发送的请求满足 Route3 和 Route4 的断言,但是 Route3 的配置在配置文件中靠前,所以只会匹配 Route3。

## Spring Cloud Gateway 如何实现动态路由?

在使用 Spring Cloud Gateway 的时候,官方文档提供的方案总是基于配置文件或代码配置的方式。

Spring Cloud Gateway 作为微服务的入口,需要尽量避免重启,而现在配置更改需要重启服务不能满足实际生产过程中的动态刷新、实时变更的业务需求,所以我们需要在 Spring Cloud Gateway 运行时动态配置网关。

实现动态路由的方式有很多种,其中一种推荐的方式是基于 Nacos 注册中心来做。 Spring Cloud Gateway 可以从注册中心获取服务的元数据(例如服务名称、路径等),然后根据这些信息自动生成路由规则。这样,当你添加、移除或更新服务实例时,网关会自动感知并相应地调整路由规则,无需手动维护路由配置。

其实这些复杂的步骤并不需要我们手动实现,通过 Nacos Server 和 Spring Cloud Alibaba Nacos Config 即可实现配置的动态变更,官方文档地址:<https://github.com/alibaba/spring-cloud-alibaba/wiki/Nacos-config> 。

## Spring Cloud Gateway 的过滤器有哪些?

过滤器 Filter 按照请求和响应可以分为两种:

- **Pre 类型**:在请求被转发到微服务之前,对请求进行拦截和修改,例如参数校验、权限校验、流量监控、日志输出以及协议转换等操作。
- **Post 类型**:微服务处理完请求后,返回响应给网关,网关可以再次进行处理,例如修改响应内容或响应头、日志输出、流量监控等。

另外一种分类是按照过滤器 Filter 作用的范围进行划分:

- **GatewayFilter**:局部过滤器,应用在单个路由或一组路由上的过滤器。标红色表示比较常用的过滤器。
- **GlobalFilter**:全局过滤器,应用在所有路由上的过滤器。

### 局部过滤器

常见的局部过滤器如下图所示:

![](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway/spring-cloud-gateway-gatewayfilters.png)

具体怎么用呢?这里有个示例,如果 URL 匹配成功,则去掉 URL 中的 “api”。

yaml filters: #过滤器

  • RewritePath=/api/(?.*),/$\{segment} # 将跳转路径中包含的 “api” 替换成空

当然我们也可以自定义过滤器,本篇不做展开。

### 全局过滤器

常见的全局过滤器如下图所示:

![](https://oss.javaguide.cn/github/javaguide/system-design/distributed-system/api-gateway/spring-cloud-gateway-globalfilters.png)

全局过滤器最常见的用法是进行负载均衡。配置如下所示:

yaml spring: cloud:

gateway:
  routes:
    - id: route_member # 第三方微服务路由规则
      uri: lb://passjava-member # 负载均衡,将请求转发到注册中心注册的 passjava-member 服务
      predicates: # 断言
        - Path=/api/member/** # 如果前端请求路径包含 api/member,则应用这条路由规则
      filters: #过滤器
        - RewritePath=/api/(?<segment>.*),/$\{segment} # 将跳转路径中包含的api替换成空

这里有个关键字 `lb`,用到了全局过滤器 `LoadBalancerClientFilter`,当匹配到这个路由后,会将请求转发到 passjava-member 服务,且支持负载均衡转发,也就是先将 passjava-member 解析成实际的微服务的 host 和 port,然后再转发给实际的微服务。

## Spring Cloud Gateway 支持限流吗?

Spring Cloud Gateway 自带了限流过滤器,对应的接口是 `RateLimiter`,`RateLimiter` 接口只有一个实现类 `RedisRateLimiter` (基于 Redis + Lua 实现的限流),提供的限流功能比较简易且不易使用。

从 Sentinel 1.6.0 版本开始,Sentinel 引入了 Spring Cloud Gateway 的适配模块,可以提供两种资源维度的限流:route 维度和自定义 API 维度。也就是说,Spring Cloud Gateway 可以结合 Sentinel 实现更强大的网关流量控制。

## Spring Cloud Gateway 如何自定义全局异常处理?

在 SpringBoot 项目中,我们捕获全局异常只需要在项目中配置 `@RestControllerAdvice`和 `@ExceptionHandler`就可以了。不过,这种方式在 Spring Cloud Gateway 下不适用。

Spring Cloud Gateway 提供了多种全局处理的方式,比较常用的一种是实现`ErrorWebExceptionHandler`并重写其中的`handle`方法。

java @Order(-1) @Component @RequiredArgsConstructor public class GlobalErrorWebExceptionHandler implements ErrorWebExceptionHandler {

private final ObjectMapper objectMapper;
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
// ...
}

}


## 参考

- Spring Cloud Gateway 官方文档:<https://cloud.spring.io/spring-cloud-gateway/reference/html/>
- Creating a custom Spring Cloud Gateway Filter:<https://spring.io/blog/2022/08/26/creating-a-custom-spring-cloud-gateway-filter>
- 全局异常处理: <https://zhuanlan.zhihu.com/p/347028665>

<!-- @include: @article-footer.snippet.md -->


---

## distributed configuration center

---
title: 分布式配置中心详解:Apollo、Nacos、Spring Cloud Config 与 K8s ConfigMap 对比
description: 分布式配置中心原理与选型详解,涵盖 Apollo、Nacos、Spring Cloud Config、Kubernetes ConfigMap 的架构差异、配置推送机制、灰度发布、高可用设计和面试高频考点。
category: 分布式
keywords:
  - 配置中心
head:
  - - meta
    - name: keywords
      content: 配置中心,分布式配置中心,Apollo,Nacos,Spring Cloud Config,Kubernetes ConfigMap,配置推送,长轮询,灰度发布,配置中心面试题
---

## 为什么要用配置中心?

微服务架构下,应用被拆分为大量独立部署的服务,每个服务都有自己的配置(服务地址、数据库参数、功能开关等)。配置项数量会随着服务数量、环境数量和集群数量一起增长。传统配置文件方式存在以下问题:

- **修改需重启**:无论配置在代码库还是外部文件中,很多应用都需要重启进程才能让新配置生效。
- **与发版耦合**:如果配置放在代码库中,配置变更往往要跟代码发版绑定,难以独立灰度和回滚。
- **安全性不足**:敏感配置(数据库密码、API Key)直接写在代码库中容易泄露。
- **缺乏权限控制**:无法对配置的查看、修改、发布等操作进行细粒度权限管控。
- **配置分散难管理**:多环境(开发/测试/生产)、多集群的配置分散在各处,难以统一维护。

此外,配置中心通常提供以下增强能力:

- **版本管理**:记录每次配置变更的修改人、修改时间、修改内容,支持一键回滚。
- **灰度发布**:先将配置推送给部分实例验证,降低变更风险(Apollo、Nacos 1.1.0+ 支持)。

![Apollo 配置中心](https://oss.javaguide.cn/github/javaguide/config-center/view-release-history.png)

当然,不是所有系统都需要上配置中心。单体应用、单环境、配置项很少且变更频率低的场景,`application-{profile}.yml`、环境变量或 Kubernetes ConfigMap + 滚动重启通常就够了。配置中心会带来额外的运维成本、故障域和排查链路,小团队或低频配置场景不必过度工程化。

从分布式系统视角看,配置中心属于典型控制面:它负责决定“当前应该使用哪一份配置”,客户端负责拉取、缓存、监听和刷新。配置发布、灰度、回滚、客户端本地快照,处理的都是集中决策和客户端容灾之间的取舍。这个取舍可以和 [分布式协调详解](https://raw.githubusercontent.com/Snailclimb/JavaGuide/HEAD/protocol/centralized-and-decentralized.md) 放在一起理解。

配置中心还经常和 [RPC](https://raw.githubusercontent.com/Snailclimb/JavaGuide/HEAD/rpc/) 以及 [API 网关](https://raw.githubusercontent.com/Snailclimb/JavaGuide/HEAD/api-gateway.md) 一起出现:RPC 框架需要拿到服务地址、超时、重试、降级等治理配置,网关也可能依赖配置中心做动态路由和灰度规则。它们解决的问题不同,但在真实微服务体系里通常会连在同一条链路上。

## 常见的配置中心有哪些?如何选择?

| 方案                                                                | 状态       | 特点                                   |
| ------------------------------------------------------------------- | ---------- | -------------------------------------- |
| [Spring Cloud Config](https://cloud.spring.io/spring-cloud-config/) | 活跃       | Spring 生态原生支持,基于 Git 存储     |
| [Nacos](https://github.com/alibaba/nacos)                           | 活跃       | 阿里开源,配置中心 + 服务发现二合一    |
| [Apollo](https://github.com/apolloconfig/apollo)                    | 活跃       | 携程开源,配置管理、权限和审计能力较强 |
| K8s ConfigMap                                                       | 活跃       | Kubernetes 原生方案                    |
| Disconf / Qconf                                                     | 长期不活跃 | 不建议新项目使用                       |

**选型建议**:

- 只需配置中心 → **Apollo**(管理能力更细)或 **Nacos**(单机启动更轻)
- 需要配置中心 + 服务发现 → **Nacos**
- Spring Cloud 体系且追求简单 → **Spring Cloud Config**
- Kubernetes 环境 → **K8s ConfigMap 挂载 + 应用层文件监听**。ConfigMap 以 Volume 挂载时会被 kubelet 周期同步,最终可见时间取决于 kubelet 同步周期和本地缓存传播方式;环境变量方式和 `subPath` 挂载不会自动更新。热重载可以用 inotify 监听挂载文件,也可以用 Spring Cloud Kubernetes 通过 K8s Watch API 监听 ConfigMap 变更并触发刷新。

**Apollo vs Nacos vs Spring Cloud Config**

> **版本说明**:以下对比基于 Apollo 2.x、Nacos 2.x、Spring Cloud Config 4.x/5.x。Spring Boot 3 体系通常对应 Spring Cloud Config 4.x,Spring Boot 4 体系对应更新的 Spring Cloud 2025.x 发行列车;如果仍在 Spring Boot 2 体系,对应的是 Spring Cloud Config 3.x。

| 功能点       | Apollo                                     | Nacos                                        | Spring Cloud Config                  |
| ------------ | ------------------------------------------ | -------------------------------------------- | ------------------------------------ |
| 配置界面     | 支持(权限、审计、发布流程较完整)         | 支持                                         | 无(通常通过 Git 平台操作)          |
| 配置实时生效 | 支持(HTTP 长轮询,通常秒级感知)          | 支持(gRPC 变更通知 + 客户端拉取)           | 半实时(需触发 refresh 或 Bus 广播) |
| 版本管理     | 原生支持                                   | 原生支持                                     | 依赖 Git                             |
| 权限管理     | 支持(应用/命名空间/环境等多层粒度)       | 支持                                         | 依赖 Git 平台                        |
| 灰度发布     | 支持(规则更细)                           | 支持(1.1.0+,能力相对基础)                 | 不支持                               |
| 配置回滚     | 支持                                       | 支持                                         | 依赖 Git                             |
| 告警通知     | 支持                                       | 支持                                         | 不支持                               |
| 多语言       | 支持(Open API / 多语言客户端)            | 支持(Open API / 多语言客户端)              | 更偏 Spring 应用                     |
| 多环境       | 支持(通常物理隔离)                       | 支持(多用 Namespace 逻辑隔离)              | 需配合多 Git 仓库                    |
| 依赖组件     | MySQL(注册中心默认内嵌在 Config Service) | 外部 MySQL(生产推荐)/ 嵌入式 Derby + JRaft | Git + 可选消息队列                   |

**深度对比**:

1. **Apollo**:在权限模型、发布审计、发布前 diff、灰度规则等管理特性上更细,适合对配置治理要求较高的团队。多环境(FAT/UAT/PROD)物理隔离场景下,需为每个环境部署 Config Service、Admin Service 和独立数据库,运维门槛中等偏高
2. **Nacos**:配置 + 注册中心二合一,部署简单(单机模式仅一个 Jar 包)。生产集群推荐使用外部 MySQL;嵌入式 Derby + JRaft 更适合测试或小规模场景。Nacos 的 Namespace/Group/DataId 模型上手快,但环境隔离通常偏逻辑隔离
3. **Spring Cloud Config**:架构最简单(基于 Git),但实时性差,需要额外组件实现自动刷新

## 配置中心、注册中心与 K8s ConfigMap 的边界

- **应用配置中心(Apollo/Nacos/Spring Cloud Config)**:主要解决业务参数、开关、阈值、连接信息等应用配置的集中管理、审计、灰度和动态刷新。
- **服务注册中心(Eureka/Nacos/Consul)**:主要解决服务实例注册、发现和健康状态同步。Nacos 同时提供配置中心和注册中心能力,但两类职责仍然不同。
- **Kubernetes ConfigMap**:主要解决 Pod 启动参数、环境变量、挂载文件等容器运行时配置管理,不天然提供发布审批、灰度规则和应用内对象刷新。
- **Service Mesh / Ingress 配置**:主要解决流量路由、熔断、重试、超时、灰度流量等治理策略,配置对象通常是 CRD 或控制平面资源。

## 配置中心核心设计要点

设计或选型配置中心时,需关注以下能力:

### 1. 配置推送机制

| 模式       | 实时性          | 服务端压力                   | 实现复杂度 | 适用场景     |
| ---------- | --------------- | ---------------------------- | ---------- | ------------ |
| **推模式** | 高(毫秒级)    | 高(需维护连接)             | 高         | 强实时性要求 |
| **拉模式** | 低(秒~分钟级) | 高(无效轮询)               | 低         | 配置变更极少 |
| **长轮询** | 中高(秒级)    | 中等(海量连接时内存压力大) | 中         | **主流方案** |

> **推送机制说明**:
>
> - **Apollo**:采用 HTTP 长轮询。客户端发起请求,服务端若有变更立即返回;无变更则挂起请求(服务端默认约 60s,客户端 read timeout 通常更长),期间一旦有变更立即响应。
> - **Nacos 2.x**:服务发现链路升级为 gRPC 双向流,实时性更好;配置中心链路更准确地说是“变更通知 + 客户端拉取”的两阶段模型,服务端通知配置发生变化,客户端再按需拉取最新配置内容。
>
> **注意**:严格说,长轮询仍是客户端发起的拉取请求,只是服务端通过挂起请求实现近实时;本表按行业惯例将其单列以突出运行特征。长轮询虽然比短轮询节省 CPU 和网络开销,但当客户端规模达到十万级时,服务端仍需维持海量挂起请求。以 Apollo 为例,服务端基于 Spring MVC `DeferredResult` 挂起请求,底层依托 Servlet 3.0 异步特性和 Tomcat NIO Connector 承载,对内存和连接数上限仍有要求。

### 2. 必备功能清单

- **权限控制**:配置的查看、修改、发布需分级授权
- **审计日志**:完整记录配置变更的操作人、时间、内容
- **版本管理**:每次发布生成版本号,支持回滚到任意历史版本
- **灰度发布**:配置先推送到部分实例,验证通过后全量发布
- **多环境隔离**:开发、测试、生产环境配置独立管理
- **高可用部署**:配置中心自身需要集群化部署,避免单点故障

### 3. 客户端容灾与启动顺序

配置中心是基础设施,一旦不可用会影响大量业务应用。因此客户端必须具备容灾能力:

- **多级缓存**:优先读内存配置;配置中心不可用时读取本地快照;本地快照也不存在时使用代码里的兜底默认值或拒绝启动。
- **降级启动**:对于非关键配置,可以先用本地快照启动,再异步连接配置中心;对于数据库地址、加密密钥这类关键配置,可以选择“无配置不启动”。
- **断线重连**:长轮询或长连接断开后,客户端应带退避策略重连,避免配置中心恢复时被瞬时流量打满。
- **刷新边界**:动态刷新不等于所有对象都会自动改变。比如 Spring 中已注入到普通字段、`final` 字段或条件装配逻辑里的值,可能不会按预期刷新,需要配合 `@RefreshScope`、监听器或重新设计 Bean 生命周期。

## 以 Apollo 为例介绍配置中心的设计

### Apollo 介绍

根据 Apollo 官方介绍:

> [Apollo](https://github.com/apolloconfig/apollo)(阿波罗)是携程框架部门研发的分布式配置中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性,适用于微服务配置管理场景。
>
> 服务端基于 Spring Boot 和 Spring Cloud 开发,打包后可以直接运行,不需要额外安装 Tomcat 等应用容器。
>
> Java 客户端不依赖任何框架,能够运行于所有 Java 运行时环境,同时对 Spring/Spring Boot 环境也有较好的支持。

Apollo 核心特性:

- **配置修改实时生效(热发布)**:基于长轮询,1s 内即可接收到最新配置
- **灰度发布**:配置只推给部分应用,降低变更风险
- **部署简单**:单环境仅依赖 MySQL;Apollo 自带的注册中心(默认为 Eureka)以内嵌方式运行于 Config Service 进程内,无需独立部署。多环境物理隔离时,需要为每个环境部署一套 Config Service、Admin Service 和独立数据库
- **跨语言**:提供了 HTTP 接口,不限制编程语言

关于如何使用 Apollo 可以查看 [Apollo 官方使用指南](https://www.apolloconfig.com/#/zh/)。

### Apollo 架构解析

官方给出的 Apollo 基础模型(图片来源:Apollo 官方文档 - Apollo Design):

![](https://img-blog.csdnimg.cn/a75ccb863e4a401d947c87bb14af7dc3.png)

1. 用户在 Apollo 配置中心修改/发布配置
2. Apollo 配置中心通知应用配置已更改
3. 应用访问 Apollo 配置中心获取最新配置

官方架构图(图片来源:Apollo 官方文档 - Apollo Design):

![](https://img-blog.csdnimg.cn/79c7445f9dbc45adb45699d40ef50f44.png)

### 组件说明

| 组件               | 作用                                                                                    | 默认端口               |
| ------------------ | --------------------------------------------------------------------------------------- | ---------------------- |
| **Portal**         | Web 管理界面,提供配置的可视化管理                                                      | 8070                   |
| **Client**         | 客户端 SDK,提供配置获取和变更监听能力                                                  | -                      |
| **Meta Server**    | 服务发现入口,与 Config Service 同进程,供 Client/Portal 获取服务地址                   | 8080                   |
| **Config Service** | 提供配置读取和长轮询通知接口,供 Client 调用;同时内嵌注册中心                          | 8080                   |
| **Admin Service**  | 提供配置管理接口,供 Portal 调用                                                        | 8090                   |
| **Eureka(内嵌)** | Config Service 同进程内嵌的注册中心实例,供 Config/Admin Service 注册发现;无需独立部署 | 与 Config Service 相同 |
| **MySQL**          | 存储配置数据和元数据                                                                    | 3306                   |

Apollo 2.0+ 支持通过 SPI 替换服务注册发现实现,例如接入 Nacos、Consul、Polaris 等。但在默认部署模型下,Eureka 是 Config Service 内嵌能力,不应把它理解为需要单独运维的外部 Eureka 集群。

### 核心流程

**Client 端(获取配置)**:

1. Client 启动时访问 Meta Server 获取 Config Service 地址列表
2. Client 本地缓存服务地址(Eureka 故障时仍可用)
3. Client 发起长轮询请求获取配置
4. Config Service 检测到配置变更后立即响应
5. Client 更新内存缓存、触发变更回调,并**异步持久化到本地文件系统**。Linux/Mac 默认缓存目录位于 `/opt/data/{appId}/config-cache/`,Windows 默认位于 `C:\opt\data\{appId}\config-cache\`,也可以通过系统属性 `apollo.cache-dir` 自定义

> **灾备机制**:即使 Config Service 全部宕机且应用重启,Client 仍可从本地磁盘读取缓存的配置完成启动,确保应用可用性不强依赖配置中心。

**Portal 端(发布配置)**:

1. 用户在 Portal 修改配置并点击发布
2. Portal 调用 Admin Service 发布接口
3. Admin Service 将配置写入 MySQL 并生成发布版本
4. Config Service 通过长轮询通知 Client 配置已变更
5. Client 重新拉取最新配置

### Client 使用示例

获取配置:

java Config config = ConfigService.getAppConfig(); String someKey = "someKeyFromDefaultNamespace"; String someDefaultValue = "someDefaultValueForTheKey"; String value = config.getProperty(someKey, someDefaultValue);


监听配置变化:

java Config config = ConfigService.getAppConfig(); config.addChangeListener(new ConfigChangeListener() {

@Override
public void onChange(ConfigChangeEvent changeEvent) {
    // 处理配置变更
    for (String key : changeEvent.changedKeys()) {
        ConfigChange change = changeEvent.getChange(key);
        System.out.println(String.format(
            "Key: %s, Old: %s, New: %s",
            key, change.getOldValue(), change.getNewValue()));
    }
}

}); ```

在 Spring Boot 项目中,生产代码通常不会直接到处调用底层 API,而是通过 Apollo Spring 集成完成配置注入和刷新,例如使用 @EnableApolloConfig 启用 Apollo,通过 @Value、@ConfigurationProperties 或 @ApolloConfigChangeListener 监听变更。需要注意的是,条件装配类(例如 @ConditionalOnProperty)和已经初始化完成的复杂 Bean 不一定会因为配置变化自动重建,关键配置变更仍要结合业务刷新策略验证。

Nacos 配置中心核心模型

Nacos 同时提供配置中心和服务发现能力,这也是它与 Apollo 的主要差异之一。从配置中心角度看,Nacos 常用三层模型来定位一份配置:

  • Namespace:通常用于环境或租户隔离,例如 dev、test、prod。
  • Group:通常用于业务域或应用分组,默认是 DEFAULT_GROUP。
  • DataId:具体配置文件或配置项标识,例如 order-service.yaml。

Nacos 的配置存储也要区分部署形态:

  • 生产集群:推荐使用外部 MySQL 存储配置数据,数据一致性主要由 MySQL 自身的高可用方案保障。
  • 嵌入式存储:Nacos 也支持 Derby 等嵌入式存储。集群模式下,Nacos 通过 JRaft 将各节点的嵌入式存储组成逻辑集群,适合测试、小规模或对运维成本特别敏感的场景,但排障复杂度更高。

Nacos 2.x 引入 gRPC 长连接后,客户端与服务端之间的连接开销比 1.x HTTP 长轮询更低。配置变更时,服务端会通知客户端“某个配置发生变化”,客户端再拉取最新配置内容并回调监听器。这样可以避免把大配置内容直接塞进通知链路,也便于客户端做本地快照和容灾。

Nacos 客户端同样会维护本地快照。配置中心不可用时,客户端可以读取本地 snapshot/failover 文件继续启动或运行;具体缓存路径会随客户端版本、命名空间、服务端地址、Group 和 DataId 变化,排障时建议以目标版本客户端日志和本地 nacos/config 目录为准。

参考

本文由 GitVP 从 GitHub 收录并在站内全文呈现,版权归原作者所有(Apache-2.0)。

← 回到全部文章

同分类还有