黑马学成在线

莫道桑榆晚,为霞尚满天。

——刘禹锡 《酬乐天咏老见示》

一、面试部分

1. 面试话术

  1. 好的面试官,我介绍一个源自于我自己想法的一个项目,这个想法来源于我一对一辅导考研学生的时候,当时因为我的成绩比较高,然后一个机构找到我,联系我给考研的学生上专业课一对一辅导,后来通过了解发现,我给学生上课机构付给我的教学费用还没有学生交给机构的费用的一半,然而机构做的事情仅仅只是找到了我,他们靠这个来赚取信息差,所以当时就产生了想做一个平台,用于链接考研学生和已经考上的同学们,来消除中间商赚差价。于是就有了这个项目,这个项目目前正在开发,采用的是B2B2C业务模式,第一个B呢就是我们团队,第二个B指的就是已经上岸的同学,他们可发布课程收取费用,C就是正在准备考研的同学,项目采用的是微服务架构,目前已经完成了内容管理模块,媒资管理模块,课程发布模块。其中 内容管理模块可以实现对课程的搜索、添加课程基本信息,课程营销信息,然还可以添加课程计划等 媒资管理模块可以实现上传视频、图片等,对大文件实现了断点续传、使用分布式任务调度XXL-JOB实现了视频转码等。 课程发布模块通过分布式事务实现了上传课程静态页面到MinIO,向Redis写缓存,包括后续还考虑写ElasticSerach索引库,实现课程的快速搜索。
  2. 面试官问我对视频转码的内容感兴趣,我说我就是调用了FFMpeg的工具包,他说可以了解一下谷歌的转码工具,如果能了解原理并且深入的话会很加分。 三面用友的时候,面试让我共享屏幕,打开ideal给他讲解分布式事务的相关代码,就是本地消息表+任务调度机制那个,然后如何用模板设计模式封装成SDK,并且现场手写关键地方代码,完了之后考察JUC中的countdownlathch如何实现的。

2. 简要介绍一下你的项目

  • 我最近参与的项目是我们公司自研的专门针对成人职业技能教育的网络课堂系统,网站提供了成人职业技能培训的相关课程,如:软件开发培训、职业资格证书培训、成人学历教育培训等课程。项目基于B2B2C的业务模式,培训机构可以在平台入驻、发布课程,我们公司作为运营方由专门的人员对发布的课程进行审核,审核通过后课程才可以发布成功,课程包括免费和收费两种形式,对于免费课程普通用户可以直接选课学习,对于收费课程在选课后需要支付成功才可以继续学习。
  • 本项目包括三个端:用户端(学生端)、机构端、运营端。工 核心模块包括:内容管理、媒资管理、课程搜索、订单支付、选课管理、认证授权等。
  • 本项目采用前后端分离架构,后端采用SpringEoot.SpringCloud技术栈开发,数据库使用了MySQL,还使用的Redis、消息队列、分布式文件系统、Elasticsearch等中间件系统。
  • 划分的微服务包括:内容管理服务、媒资管理服务、搜索服务、订单支付服务、学习中心服务、系统管理服务.认证授权服务、网关服务、注册中心服务、配置中心服务等。
  • 我在这个项目中负责了内容管理、媒资管理、订单支付模块的设计与开发。
  • 内容管理模块,是对平台上的课程进行管理,课程的相关信息比较多这里在数据库设计了课程基本信息表、课程营销表、课程计划、课程师资表进行存储,培训机构要发布一门课程需要填写课程基本信息、课程营销信息、课程计划信息、课程师资信息,填写完毕后需要提交审核,由运营人员进行课程信息的审核,整个审核过程是程序自动审核加人工确认的方式,通常24小时审核完成。课程审核通过即可发布课程,课程的相关信息会聚合到课程发布表中,这里不仅要将课程信息写到课程发布表还要将课程信息写到索引库、分布式文件系统中,所以这里存在分布式事务的问题,项目使用本地消息表加任务调度的方式去解决这里的分布式事务,保存数据的最终一致性。

3. B2B2C 是什么

  • 1B是服务的供应商,2B就是培训机构,C是消费者.B2B是指服务的供应商和培训机构的运作方式,B2C是指培训机构和消费者之间的运作方式

4. maven依赖版本冲突了怎么解决?

1、使用exclusions排除依赖

比如:我们只依赖B的1.0版本,此时可以在依赖C时排除对B的依赖。

2、使用dependencyManagement锁定版本号。 通常在父工程对依赖的版本统一管理。 比如:我们只依赖B的1.0版本,此时可以在父工程中限定B的版本为1.0。

5. MySQL 的常见引擎以及区别

InnoDB

1、支持事务。

2、使用的锁粒度默认为行级锁,可以支持更高的并发;也支持表锁。

3、支持外键约束;外键约束其实降低了表的查询速度,增加了表之间的耦合度。

MyISAM

1、不提供事务支持

2、只支持表级锁

3、不支持外键

memory

数据存储在内存中

总结:

  • ·MyIlSAM管理非事务表,提供高速存储和检索以及全文搜索能力,如果在应用中执行大量select操作,应该选择MylSAM
  • lnnoDB用于事务处理,具有ACID事务支持等特性,如果在应用中执行大量insert和update操作,应该选择lnnoDB

拓展:

什么是表锁什么是行为锁

  1. 行级别锁

    • 是针对数据表中单行记录的锁。当事务对某一行数据进行操作(如增删改查)时,仅锁定该特定行,其他事务可以同时操作表中其他未被锁定的行,不相互阻塞。
    • 适用于并发量高、写操作频繁的场景,能最大限度减少锁冲突,提高并发性能。
  2. 表级别锁

    • 是针对整个数据表的锁。当事务持有表级锁时,会锁定整张表,此时其他事务对该表的所有操作(包括读写任何行)都会被阻塞,直到锁释放。
    • 适用于需要对整张表进行批量操作(如全表更新、结构修改)的场景,锁机制简单,开销小,但并发性能较低。

6. 建表应该注意什么

1、注意选择存储引擎,如果要支持事务需要选择InnoDB。

2、注意字段类型的选择,对于日期类型如果要记录时分秒建议使用datetime,只记录年月日使用date类型,对于字符类型的选择,固定长度字段选择char,不固定长度的字段选择varchar,varchar比char节省空间但速度没有char快;对于内容介绍类的长广文本字段使用text或longtext类型;如果存储图片等二进制数据使用blob或 longblob类型;对金额字段建议使用DECIMAL;对于数值类型的字段在确保取值范围足够的前提下尽量使用占用空间较小的类型,

3、主键字段建议使用自然主键,不要有业务意义,建议使用int unsigned类型,特殊场景使用bigint类型。4、如果要存储text、blob字段建议单独建—张表,使用外键关联。

5、尽量不要定义外键,保证表的独立性,可以存在外键意义的字段。

6、设置字段默认值,比如:状态、创建时间等。

7、每个字段写清楚注释。

8、注意字段的约束,比如:非空、唯一、主键等。

7. 数据库的三大范式

  • 第一范式:数据表中的每一列(每个字段)必须是不可拆分的最小单元,也就是确保每一列的原子性;
  • 第二范式(2NF):满足 1NF 后,要求表中的所有列,都必须依赖于主键,而不能有任何一列与主键没有关系,也就是说一个表只描述一件事情;
  • 第三范式:必须先满足第二范式(2NF),要求:表中的每一列只与主键直接相关而不是间接相关,(表中的每一列只能依赖于主键);

8. SpringBoot接口开发的常用注解有哪些

  • @Controller标记此类是一个控制器,可以返回视图解析器指定的html页面,通过@ResponseBody可以将结果返回Json. xml数据。
  • @RestController相当于@ResponseBody加@Controller,实现rest接口开发,返回ison数据,不能返回html页面。
  • @RequestMapping定义接口地址,可以标记在类上也可以标记在方法上,支持http的post、put、get等方法。
  • @PostMapping定义post接口,只能标记在方法上,用于添加记录,复杂条件的查询接口。
  • @GetMapping定义get接口,只能标记在方法上,用于查询接口的定义。
  • @PutMapping定义put接口,只能标记在方法上,用于修改接口的定义。
  • @DeleteMapping定义delete接口,只能标记在方法上,用于删除接口的定义。
  • @RequestBody定义在方法上,用于将json串转成java对象。
  • @Pathvarible接收请求路径中占位符的值.
  • @ApiOperation swagger注解,对接口方法进行说明。
  • @Api wagger注解,对接口类进行说明。
  • @Autowired基于类型注入。
  • @Resourc基于名称注入,如果基于名称注入失败转为基于类型注入。

9. 项目开发流程是什么

1、产品人员设计产品原型。

2、讨论需求。

3、分模块设计接口。

4、出接口文档。

5、将接口文档给到前端人员,前后端分离开发。

6、开发完毕进行测试。

7、测试完毕发布项目,由运维人员进行部署安装。

10. MyBatis分页插件的原理

  • 首先分页参数放到ThreadLocal中,拦截执行的sql,根据数据库类型添加对应的分页语句重写sql,例如: (select* from table where a)转换为(select count(*) from table where a)和(select * from table where a limit,)
  • 计算出了total总条数、pageNum当前第几页、pageSize每页大小和当前页的数据,是否为首页,是否为尾页,总页数等。

11. 树型表的标记字段是什么?如何查询MySQl的树型表

标记字段就是parentid,即父节点

  • 当层级固定可以用数据库的自连接的方式
  • 如果想要灵活查询可以使用MySQL的递归的方式(使用with表达式,里面的recusive关键字可以使用关键字的方式)

12. MyBatis的ResultType和ResultMap的区别

ResultType:指定映射类型,只要查询的字段名和类型的属性名匹配可以自动映射。

ResultMap:自定义映射规则,当查询的字段名和映射类型的属性不匹配时可以通过ResultMap自定义映射规则,也可以实现一对多、一对一映射。

13. MyBatis中的#{}和${}有什么区别

#{}是标记一个占位符,可以防止sql注入。 ${}用于在动态sql中拼接字符串,可能导致s.ql注入。

14. 系统如何处理自定义异常

我们自定义一个统一的异常处理器去捕获并处理异常。

使用控制器增加注解@ControllerAdvice和异常处理注解@ExceptionHandler来实现。

1)处理自定义异常

  • 程序在编写代码时根据校验结果主动抛出自定义异常类对象,抛出异常时指定详细的异常信息,异常处理器捕获异常信息记录异常日志并响应给用户。

2)处理未知异常

  • 接口执行过程中的一些运行时异常也会由异常处理器统一捕获,记录异常日志,统一响应给用户500错误。在异常处理器中还可以针对某个异常类型进行单独处理。

15. 请求参数合法性该如何校验

使用基于ISR303的校验框架实现,SpringBoot提供了ISR-303的支持,它就是spring-boot-starter-validation,它包括了很多校验规则,只需要在模型类中通过注解指定校验规则,在controller方法上开启校验

16. Spring事务什么时候会失效

1)在方法中捕获异常没有抛出去

2)非事务方法调用事务方法

3)事务方法内部调用事务方法

4)@Transactional标记的方法不是public

5)抛出的异常与rollbackFor指定的异常不匹配,默认rollbackFor指定的异常为RuntimeException

6)数据库表不支持事务,比如MySQL的MyISAM

7)Spring的传播行为导致事务失效,比如:PROPAGATION_NEVER、PROPAGATION_NOT_SUPPORTED

17. 断点续传是怎么做的

我们是基于分块上传的模式实现断点续传的需求,当文件上传一部分断网后前边已经上传过的不再上传。

1)前端对文件分块。

2)前端使用多线程一块一块上传,上传前给服务端发一个消息校验该分块是否上传,如果已上传则不再上传。

3)等所有分块上传完毕,服务端合并所有分块,校验文件的完整性。

因为分块全部上传到了服务器,服务器将所有分块按顺序进行合并,就是写每个分块文件内容按顺序依次写入一个文件中。使用字节流去读写文件。

4)前端给服务传了一个md5值,服务端合并文件后计算合并后文件的md5是否和前端传的一样,如果一样则说文件完整,如果不一样说明可能由于网络丢包导致文件不完整,这时上传失败需要重新上传。

18. 分块文件清理问题

上传一个文件进行分块上传,上传一半不传了,之前上传到minio的分块文件要清理吗?怎么做的?

1、在数据库中有一张文件表记录minio中存储的文件信息。

2、文件开始上传时会写入文件表,状态为上传中,上传完成会更新状态为上传完成。

3、当一个文件传了一半不再上传了说明该文件没有上传完成,会有定时任务去查询文件表中的记录,如果文件未上传完成则删除minio中没有上传成功的文件目录。

19 XXL-job的工作原理是什么?

XXL-JOB分布式任务调度服务由调用中心和执行器组成,调用中心负责按任务调度策略向执行器下发任务,执行器负责接收任务执行任务。

1)首先部署并启动xxl-job调度中心。(一个java工程)

2)首先在微服务添加xxl-job依赖,在微服务中配置执行器

3)启动微服务,执行器向调度中心上报自己。

4)在微服务中写一个任务方法并用xxl-job的注解去标记执行任务的方法名称。

5)在调度中心配置任务调度策略,调度策略就是每隔多长时间执行还是在每天或每月的固定时间去执行,比如每天0点执行,或每隔1小时执行一次等。

6)在调度中心启动任务。

7)调度中心根据任务调度策略,到达时间就开始下发任务给执行器。

8)执行器收到任务就开始执行任务。

如何保证任务不重复执行

1)调度中心按分片广播的方式去下发任务

2)执行器收到作业分片广播的参数:分片总数和分片序号,计算任务id 除以分片总数得到一个余数,如果余数等于分片序号这时就去执行这全任务,这里保证了不同的执行器执行不同的任务。

3)配置调度过期策略为“忽略”,避免同一个执行器多次重复执行同一个任务

4)配置任务阻塞处理策略为“丢弃后续调度”,注意:丢弃也没事下一次调度就又可以执行了

5)另外还要保证任务处理的幂等性,执行过的任务可以打一个状态标记已完成,下次再调度执行该任务判断该任务已完成就不再执行

如何保证任务的幂等性

1)数据库约束,比如:唯一索引,主键。同一个主键不可能两次都插入成功。

2)乐观锁,常用于数据库,更新数据时根据乐观锁状态去更新。

3)唯一序列号,请求前生成唯一的序列号,携带序列号去请求,执行时在redis记录该序列号表示以该序列号的请求执行过了,如果相同的序列号再次来执行说明是重复执行。

这里我们在数据库视频处理表中添加处理状态字段,视频处理完成更新状态为完成,执行视频处理前判断状态是否完成,如果完成则不再处理。

二 .内容管理模块

1. 课程查询

接口请求示例:

POST /content/course/list?pageNo=2&pageSize=1
Content-Type: application/json

{
"auditStatus": "202002",
"courseName": "",
"publishStatus":""
}
###成功响应结果
{
"items": [
{
"id": 26,
"companyId": 1232141425,
"companyName": null,
"name": "spring cloud实战",
"users": "所有人",
"tags": null,
"mt": "1-3",
"mtName": null,
"st": "1-3-2",
"stName": null,
"grade": "200003",
"teachmode": "201001",
"description": "本课程主要从四个章节进行讲解: 1.微服务架构入门 2.spring cloud 基础入门 3.实战Spring Boot 4.注册中心eureka。",
"pic": "https://cdn.educba.com/academy/wp-content/uploads/2018/08/Spring-BOOT-Interview-questions.jpg",
"createDate": "2019-09-04 09:56:19",
"changeDate": "2021-12-26 22:10:38",
"createPeople": null,
"changePeople": null,
"auditStatus": "202002",
"auditMind": null,
"auditNums": 0,
"auditDate": null,
"auditPeople": null,
"status": 1,
"coursePubId": null,
"coursePubDate": null
}
],
"counts": 23,
"page": 2,
"pageSize": 1
}
  1. 由请求路径可知:前端在路径传递分页VO,以及条件查询的VO,因为分页的功能很多业务都要用,那么我们可不可以抽取到更高的模块里面去呢?答案当然可用,这里就抽取到了base里面了

  2. 第二步就要定义DTO(注意: 前端传参DTO,返回前端VO);因为这里的DTO是特有的,所以不用定义在base中在model中定义即可

  3. 然后就可以在api中定义查询的方法

    @RestController
    @Api(value = "课程信息编辑接口",tags = "课程信息编辑接口")
    public class CourseBaseInfoController {

    /**
    * 查询课程
    */
    @ApiOperation("查询课程")
    @PostMapping("/course/list")
    public PageResult<CourseBase> queryCourseBaseInfo(PageParams pageParams, @RequestBody (required=false) QueryCourseParamsDto queryCourseParamsDto) {


    return null;
    }
    }

  4. mapping是自动生成的

解决跨域问题

/**
* @author 李阳
* @version 1.0
* @description 解决跨域1
* @date 2025/8/12 11:27
*/
@Configuration
public class GlobalCorsConfig {

@Bean
public CorsFilter corsFilter() {

CorsConfiguration config = new CorsConfiguration();
//允许白名单域名进行跨域调用
config.addAllowedOrigin("*");
//允许跨越发送cookie
config.setAllowCredentials(true);
//放行全部原始头信息
config.addAllowedHeader("*");
//允许所有请求方法跨域调用
config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);

}
}

递归查询数据库

image-20250806141600174

树形结构的MySQL改怎么查询结果呢?

{
"id" : "1-2",
"isLeaf" : null,
"isShow" : null,
"label" : "移动开发",
"name" : "移动开发",
"orderby" : 2,
"parentid" : "1",
"childrenTreeNodes" : [
{
"childrenTreeNodes" : null,
"id" : "1-2-1",
"isLeaf" : null,
"isShow" : null,
"label" : "微信开发",
"name" : "微信开发",
"orderby" : 1,
"parentid" : "1-2"
}
}

如果树的层级不确定,此时可以使用MySQL递归实现,使用with语法,如下:

WITH [RECURSIVE]
cte_name [(col_name [, col_name] ...)] AS (subquery)
[, cte_name [(col_name [, col_name] ...)] AS (subquery)] ...

cte_name :公共表达式的名称,可以理解为表名,用来表示as后面跟着的子查询

col_name :公共表达式包含的列名,可以写也可以不写

下边是一个递归的简单例子:

with RECURSIVE t1  AS
(
SELECT 1 as n
UNION ALL
SELECT n + 1 FROM t1 WHERE n < 5
)
SELECT * FROM t1;

在 SQL 中,UNION ALL 操作用于结合两个或更多 SELECT 语句的结果集,包括所有匹配的行,甚至包括重复的行。这与 UNION 不同,因为 UNION 会自动删除重复的行。

满足条件:

1、两个select查询的列的数量必须相同。

2、每个列的数据类型需要相似。

下边我们使用递归实现课程分类的查询

with recursive t1 as (
select * from course_category p where id= '1'
union all
select t.* from course_category t inner join t1 on t1.id = t.parentid
)
select * from t1 order by t1.id

image-20250806144803080

那么我们在Java的SQL中改怎么写呢

课程添加

全局异常处理器

全局异常处理器

​ 从 Spring 3.0 - Spring 3.2 版本之间,对 Spring 架构和 SpringMVC 的Controller 的异常捕获提供了相应的异常处理。

  • @ExceptionHandler
  • Spring3.0提供的标识在方法上或类上的注解,用来表明方法的处理异常类型。
  • @ControllerAdvice
  • Spring3.2提供的新注解,从名字上可以看出大体意思是控制器增强, 在项目中来增强SpringMVC中的Controller。通常和**@ExceptionHandler** 结合使用,来处理SpringMVC的异常信息。
  • @ResponseStatus
  • Spring3.0提供的标识在方法上或类上的注解,用状态代码和应返回的原因标记方法或异常类。 调用处理程序方法时,状态代码将应用于HTTP响应。

JSR303

image-20250807134656451

docker run \
-e MINIO_ROOT_USER=minioadmin \
-e MINIO_ROOT_PASSWORD=minioadmin \
-v minio-data:/data \
--name minio \
--hostname minio \
-p 9000:9000 \
-p 9001:9001 \
--network hm-net \
-d \
minio/minio:RELEASE.2023-05-04T21-44-30Z server /data --console-address ":9001"