学成在线-面试

1.面试环节

项目介绍

image-20260227120744271

2.核心业务流程

课程发布对象

登录 –>

课程发布 –> 章节设置

课程管理 –> 提交审核 –> 课程发布(分布式事务)

课程搜索 –> 课程支付(支付宝沙箱环境) –> 支付服务 –> 选课服务(消息队列)

3.技术架构

image-20260227124648461

  • 技术栈

    image-20260227124844524

redis

1
2
26739
Yh3xEtrjTG5rwRi7

4.git代码冲突

场景一:两人修改了同一文件的同一行(最常见)

这是 90% 以上冲突的来源两个不同的分支对同一个文件的同一个位置进行了不同的修改

  • 举个例子: 在 main 分支中,第 15 行代码是:let buttonColor = "blue";
    • 开发者 A 在 feature-A 分支中把这行改成了:let buttonColor = "red";
    • 开发者 B 在 feature-B 分支中把这行改成了:let buttonColor = "green";
    • 当把 feature-Afeature-B 都要合并到 main 分支时,Git 看到同一个位置既要变红又要变绿,它不知道谁说了算,于是产生冲突

场景二:一方修改了文件,另一方删除了该文件

当一个开发者对某个文件进行了认真的代码更新,而另一个开发者在另一个分支里直接把这个文件删除了

  • Git 的困惑: 合并时,Git 发现一边有新代码,另一边文件都没了。它是应该保留修改后的文件,还是尊重删除操作?Git 无法决定,产生冲突(通常提示为 CONFLICT (modify/delete)

减少冲突

虽然冲突无法 100% 避免,但良好的团队习惯可以极大地减少处理冲突的痛苦:

  1. 频繁地 Pull 和 Push: 不要在一个分支上闭门造车写了一个月才合并。每天(甚至每次完成小功能后)都去拉取一下主分支的最新代码。这样即使有冲突,也是非常微小的、容易解决的。
  2. 职责划分明确(模块化): 尽量避免多个人同时修改同一个巨大的文件。把代码拆分成独立的组件和模块,每个人负责不同的部分。

处理冲突

  • 打开冲突文件,找到 <<<<<<< 标记,做决策并修改

    • 只保留当前分支版本(HEAD)
    • 只保留对方分支版本
    • 两边都要,手动融合
    • 甚至重写成第三种更合理的写法(最常见)

    保存后,标记“这个文件我已经解决”
    git add 冲突文件

    完成合并

  • 使用可视化工具:不要只用纯文本编辑器找 <<<<<<< 标记。现代 IDE(如 VS Code、IntelliJ IDEA、WebStorm)都有非常强大的内置 Git 冲突解决界面。它们通常提供 “Accept Current Change”(保留当前更改)“Accept Incoming Change”(保留传入更改)“Accept Both Changes”(保留两者) 的快捷按钮。

5.git 分支开发

image-20260227161416067

具体开发时在一个单独的dev分支,开发完成之后由项目经历合并

6.maven

6.1、maven命令

image-20260227162142156

6.2、maven依赖冲突

Maven 依赖冲突并不会像 Git 那样在合并代码时直接把过程卡死,它往往在代码编译通过后,在程序运行期间突然报出 ClassNotFoundException(找不到类)或 NoSuchMethodError(找不到方法)之类的致命错误

举例如下:

image-20260227162745742

解决方法

方法 A:使用 <exclusions> 排除依赖(最常用) 如果你确定依赖 A 传递过来的 Guava v20.0 是不需要的(太旧了),你可以直接在引入 A 的时候把它“踢掉”。

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>com.example</groupId>
<artifactId>dependency-A</artifactId>
<version>1.0</version>
<exclusions>
<exclusion>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</exclusion>
</exclusions>
</dependency>

方法 B:使用 <dependencyManagement> 锁定版本(最优雅,适合多模块项目) 这是企业级项目中最标准的做法。在父级的 pom.xml 中直接锁定该组件的版本号。一旦锁定,无论底层的子依赖怎么折腾,全都会无条件强制使用这个锁定的版本。

1
2
3
4
5
6
7
8
9
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.0-jre</version>
</dependency>
</dependencies>
</dependencyManagement>

方法 C:直接在项目中显式声明想要的版本 利用 Maven 的“路径最短优先”规则。直接在你的 pom.xml 顶层写入你想要的版本。因为直接写在当前项目里的深度为 1,是绝对的最短路径,Maven 会直接采用。

1
2
3
4
5
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.0-jre</version>
</dependency>

7.mysql数据库

7.1、 常见的三大存储引擎

  • InnoDB(目前绝对的王者与默认引擎): 从 MySQL 5.5 版本开始,InnoDB 成为了默认的存储引擎。它被设计用来处理大量的短期事务(short-lived transactions),具备极高的可靠性和高性能。如果是日常业务开发,99% 的表都应该无脑选择 InnoDB。
  • MyISAM(曾经的老大哥): 在 MySQL 5.5 之前,MyISAM 是默认引擎。它提供了大量的特性(如全文索引、压缩等),但最大的缺陷是不支持事务和行级锁。现在主要用于极少数以读为主、对数据完整性要求不高的历史遗留系统或报表系统。
  • Memory(极速缓存): 顾名思义,它将所有数据保存在内存中。不需要进行磁盘 I/O 操作,所以速度极快。但是一旦数据库重启或崩溃,数据就会全部丢失。通常用于存储临时数据或作为极速查询的缓存字典表。

7.2、建表

1.命名规范(约定大于配置)

清晰的命名能极大降低团队的沟通成本。

  • 使用蛇形命名法(snake_case): 表名、字段名全部小写,单词之间用下划线分割,例如 user_login_log
  • 禁用保留字: 绝对不要使用 MySQL 的系统关键字作为字段名(如 descordergroupkey),否则每次写 SQL 都要加反引号,非常容易引发语法错误。
  • 业务前缀: 如果是一个大系统,建议表名加上模块前缀,例如订单模块用 oms_order,系统设置用 sys_config
  • 布尔值命名: 表达是与否的概念时,字段名建议以 is_ 开头,数据类型使用 TINYINT(1)(例如 is_deletedis_vip)。

2.字段类型选择(合适才是最好)

选择数据类型的核心原则是:在满足业务需求的前提下,尽量选择占用空间最小的类型。

数据类型场景 推荐做法 避坑指南
主键 ID BIGINT UNSIGNED AUTO_INCREMENT 数据量小可用 INT,但为长远考虑首选 BIGINT
金额 / 财务数据 DECIMAL(10,2) 或将金额乘100存 INT 严禁使用 FLOATDOUBLE,一定会导致精度丢失。
定长字符串 CHAR(n) (如手机号、身份证号、MD5密码) 查询速度比 VARCHAR 快,且不会产生内存碎片。
变长字符串 VARCHAR(n) (如姓名、地址) 长度 n 刚好够用就行,不要无脑写 VARCHAR(255),会浪费内存。
时间日期 DATETIMETIMESTAMP 跨时区业务用 TIMESTAMP,时间跨度大用 DATETIME
大文本 TEXTLONGTEXT 尽量避免。如果必须用,建议将这些大字段垂直拆分到一张单独的扩展表中。

3. 主键与索引设计(性能的命脉)

InnoDB 引擎的数据是按照主键顺序聚集存储的(聚簇索引),这决定了主键的设计至关重要。

  • 强制设置主键: 每张表必须有且只有一个主键。如果没有,InnoDB 会自动找一个不允许为空的唯一索引做主键;如果还没找到,会生成一个隐藏的自增字段,这会带来额外的性能开销。
  • 推荐单调递增主键: 强烈建议使用自增数字(AUTO_INCREMENT)或类似雪花算法生成的趋势递增 ID。
  • 严禁使用 UUID 作为主键: UUID 是完全无序的字符串。由于 InnoDB 底层是 B+ 树,插入无序的 UUID 会导致频繁的“页分裂”(Page Split)和数据碎片,极其消耗 I/O 性能,严重拖慢插入速度。
  • 控制索引数量: 索引可以提升查询速度,但会降低插入和更新的速度。单表索引数量建议控制在 5 个以内。

4. 字段约束与规范(保障数据质量)

  • 尽量设置为 NOT NULL: 除非业务确实有“未知”状态,否则所有字段都建议加上 NOT NULL DEFAULT '默认值'。NULL 值会影响索引的统计和优化,且比较时需要用 IS NULL 特殊语法,容易写出 Bug。
  • 必须添加注释 (COMMENT): 表的定义和每一个字段的定义,都必须加上清晰的中文注释。这是最基础的职业素养。
  • 必备审计字段: 生产环境的业务表通常强制要求包含三个基本字段,用于追溯和运维:
    • create_time (创建时间)
    • update_time (更新时间)
    • is_deleted (逻辑删除标志,通常 0 表示正常,1 表示删除。生产环境严禁物理删除数据)

5. 字符集与存储引擎

  • 字符集选择 utf8mb4: 绝对不要用 MySQL 原本的 utf8(它最多只支持 3 个字节,存不了 Emoji 表情和部分生僻字)。一定要用 utf8mb4,配合排序规则 utf8mb4_general_ciutf8mb4_unicode_ci
  • 引擎选择: 显式声明 ENGINE=InnoDB(虽然默认是它,但写明更严谨)。

8.@RestController

1
@RestController` = `@Controller` + `@ResponseBody

image-20260301202150749

特点总结:

  • 全局生效: 只要类上加了 @RestController,这个类里面的所有方法都会自动附带 @ResponseBody 的效果
  • 跳过视图解析: 这就是为什么截图里说“不能返回 html 页面”。因为即使你 return "index";,浏览器收到的也是纯文本字符串 "index",而不是一个网页,因为它跳过了 Spring 的视图解析器
  • 最佳拍档: 它天生就是用来开发 RESTful 风格的 API 接口的,专门给前端或者第三方系统提供 JSON/XML 数据

9.开发流程

image-20260301202129186

10.mapper返回字段

resultType这个是指返回数据可以和数据库中定义类一一对应

resultMap则代表类型与属性名不一样时,手工映射

11.事务失效原因

  • 异常必须被抛出,且类型要匹配(不能被吞掉)

    image-20260312212302294

  • 非事务方法调用事务方法

    image-20260312212401167

  • 事务方法调用事务方法

    • image-20260312212749586
    • image-20260312212825124

12.断点续传

1
2
3
4
5
前端物理切片 (File Slicing)
计算文件的“身份证” (File Hash / MD5)
上传前的“探路” (秒传与续传检查)
并发上传分片 (Chunk Upload)
后端最终合并 (Merge)

分块文件删除

  • image-20260312213340313
  • image-20260312213354794
  • image-20260312213409831

13.配置本地优先

1
2
3
4
spring:
cloud:
config:
override-none: true

这样配置优先级就是本地命令

image-20260313185815175

14.XXL-JOB

部署调度中心

  • 执行器

    • pom文件添加依赖
      nacos配置文件
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14

      * 调度中心注册

      * 调度任务策略配置

      * 任务启动

      **保证不重复执行**

      分片广播与乐观锁

      ~~~shell
      过期策略为忽略
      阻塞策略为丢弃

幂等性的保证

image-20260315190524772

  • 方案一:数据库唯一索引—— 最简单粗暴的防重复插入

    1
    2
    3
    场景: 用户注册、创建订单、MQ 消费记录。
    做法: 给数据库表中能代表业务唯一性的字段(比如 order_sn 订单号)建一个唯一索引。
    效果: 第一次 INSERT 成功。第二次同样的并发请求过来,数据库直接报错 Duplicate entry。程序捕获这个异常,不当作错误处理,而是直接返回“处理成功”即可。
  • 方案二:状态机 / 乐观锁 —— 完美的防重复更新

    1
    2
    3
    4
    5
    6
    7
    8
    9
    这个就用到我们前面学的乐观锁知识了!

    场景: 订单支付成功后修改状态、扣减库存。

    做法: 业务数据通常都有状态流转(比如:待支付 0 -> 已支付 1 -> 已发货 2)。我们在更新时,必须带上前置状态。

    -- 只有当状态是“待支付(0)”时,才能更新为“已支付(1)”
    UPDATE orders SET status = 1 WHERE order_id = '123' AND status = 0;
    效果: 第一次请求执行成功,status 变成了 1。第二次重复请求过来,WHERE status = 0 匹配不到任何数据,修改行数为 0,直接忽略。
  • 防重 Token 机制 (Token + Redis) —— 解决前端连击的利器

    1
    2
    3
    4
    5
    6
    7
    场景: 表单提交、下单操作。

    做法: 1. 申请 Token: 用户进入“确认订单”页面时,前端先向后端请求一个唯一的 Token(比如 UUID),后端把这个 Token 存进 Redis。
    2. 提交请求: 用户点击“提交订单”,前端把这个 Token 放在请求头(Header)里一起发给后端。
    3. 验证并删除 Token: 后端收到请求,去 Redis 里执行删除该 Token 的操作。如果删除成功,说明是第一次请求,放行去建订单;如果删除失败(Redis里找不到Token了),说明是重复请求,直接拦截拒绝!

    注意: 验证和删除 Token 的动作必须是原子性的(通常用 Redis 的 Lua 脚本配合实现)。
  • 分布式锁 (Redis SETNX) —— 复杂业务的终极护盾

    1
    2
    3
    4
    5
    场景: 极其复杂的业务逻辑,不仅有查库,还要调第三方接口,且无法简单用唯一索引或状态机解决。

    做法: 以业务 ID(比如 user_id + action_name)作为 Redis 的 Key,使用 Redisson 加锁。

    效果: 两个相同的请求同时到达,只有一个能抢到锁往下执行。没抢到锁的请求,直接返回“请勿重复操作”即可。

15.熔断降级

image-20260318175115091

具体详细解释

image-20260318175204633

降级的俩种方式

image-20260318175811252

fallback

直接写一个类实现 FeignClient 接口,每个方法写死返回值,触发降级时调哪个接口就走哪个方法。拿不到是什么异常导致的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 1. 定义 FeignClient,指定 fallback 类
@FeignClient(name = "order-service", fallback = OrderClientFallback.class)
public interface OrderClient {
@GetMapping("/order/{id}")
OrderDTO getOrder(@PathVariable Long id);
}

// 2. 实现降级类(注册成 Bean)
@Component
public class OrderClientFallback implements OrderClient {
@Override
public OrderDTO getOrder(Long id) {
// 不知道为什么失败,只能返回固定兜底
return OrderDTO.defaultEmpty();
}
}

fallbackFactory

工厂的 create(Throwable cause) 方法会把异常传进来,你可以拿到具体的错误类型,做差异化处理或打日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 1. 定义 FeignClient,指定 fallbackFactory 类
@FeignClient(name = "order-service", fallbackFactory = OrderClientFallbackFactory.class)
public interface OrderClient {
@GetMapping("/order/{id}")
OrderDTO getOrder(@PathVariable Long id);
}

// 2. 实现工厂类
@Component
public class OrderClientFallbackFactory implements FallbackFactory<OrderClient> {

private static final Logger log = LoggerFactory.getLogger(OrderClientFallbackFactory.class);

@Override
public OrderClient create(Throwable cause) {
// cause 就是触发降级的异常
return new OrderClient() {
@Override
public OrderDTO getOrder(Long id) {
// 可以按异常类型走不同逻辑
if (cause instanceof FeignException.ServiceUnavailable) {
log.error("order-service 不可用, orderId={}", id, cause);
return OrderDTO.serviceDown();
}
log.warn("getOrder 降级, orderId={}, reason={}", id, cause.getMessage());
return OrderDTO.defaultEmpty();
}
};
}
}

16.搜索功能实现

创建索引

实现搜索

elasticsearch搜索