面经

第一次面试

笔试

  1. 对List排序(倒叙)?
  2. redis缓存中需要注意什么 ?
  3. 请设计一个用户管理和权限管理模块,要数据库表的设计,登录功能和权限校验功能的实现,中间会遇到什么问题 ?
  4. 单表查询,联表查询等SQL语句。

面试

1. 你在项目中开发了文件上传模块,解决了大文件的传输问题,是如何解决的

前端部分
1.1 切文件(前端)

1.2 判定切片是否完成上传完成(前端)

  • 客户端记录切片的上传状态,只需要上传未成功的切片

1.3 断点、错误续传(前端)

  • 客户端上传文件时,记录已上传的切片位置
  • 下次上传时,根据记录的位置,继续上传

后端部分
1.1 收切片、存切片

  • 将相关切片保存在目标文件夹

1.2 合并切片

  • 服务端根据切片的顺序,将切片合并成完整文件

1.3 文件是否存在校验

  • 服务端根据文件md5值、文件名,校验该文件是否已经上传(解决重复提交问题)。

pAtL5bd.png

2. 百万级数据导入导出场景问题、大数据处理策略及优化方案?

场景问题分析解决

百万级或者千万级数据量导入导出的场景面临的一些问题,拆开解决:

导入问题解决方案

内存溢出(分批导入)

问题:传统的Apache POI在读取Excel文件时会创建大量的Java对象来表示文件中的每一个单元格和行,当数据量超级大,使用传统的POI方式来完成导入会内存溢出,并且效率会非常低;

解决:EasyExcel通过流式读取和写入数据,只在内存中处理当前的数据块,避免了一次性加载整个文件,从而有效降低了内存消耗。分批读取读取Excel中的百万级的数据,这一点EasyExcel只需要把它分批的参数3000调大即可。我是用的20w;

EasyExcel底层采用了什么技术解决的这个问题:

  • 基于流的API:EasyExcel使用了流式API来处理Excel文件。它使用了InputStream和OutputStream来逐步读取和写入数据,而不是一次性将整个文件加载到内存中。这样可以处理数据块,逐步读取和写入文件。
  • 事件驱动模型:EasyExcel采用了事件驱动的方式,特别是在读取时,库会触发事件(如行读取事件),开发者可以在这些事件发生时处理数据。这样,只有当前正在处理的行会被加载到内存中,其他数据仍然保留在文件中。
  • 按需加载:在读取过程中,EasyExcel只会加载当前需要处理的数据,而不是整个文件。它会在读取数据时动态地从磁盘加载数据块,然后处理完这些数据块后将其从内存中清除。
  • 低级别的文件操作:EasyExcel使用了底层的文件操作技术,如BufferedInputStream,来高效地读取文件内容,减少内存占用和提高读取速度。

DB插入(分批插入)

问题:其次就是往DB里插入,怎么去插入这20w条数据,当然不能一条一条的循环,应该批量插入这20w条数据,同样也不能使用Mybatis的批量插入,因为效率也低。

解决:使用JDBC+事务的批量操作将数据插入到数据库。(分批读取+JDBC分批插入+手动事务控制)

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
// EasyExcel的读取Excel数据的API
@Test
public void import2DBFromExcel10wTest() {
String fileName = "D:\\StudyWorkspace\\JavaWorkspace\\java_project_workspace\\idea_projects\\SpringBootProjects\\easyexcel\\exportFile\\excel300w.xlsx";
//记录开始读取Excel时间,也是导入程序开始时间
long startReadTime = System.currentTimeMillis();
System.out.println("------开始读取Excel的Sheet时间(包括导入数据过程):" + startReadTime + "ms------");
//读取所有Sheet的数据.每次读完一个Sheet就会调用这个方法
EasyExcel.read(fileName, new EasyExceGeneralDatalListener(actResultLogService2)).doReadAll();
long endReadTime = System.currentTimeMillis();
System.out.println("------结束读取Excel的Sheet时间(包括导入数据过程):" + endReadTime + "ms------");
}
// 事件监听
public class EasyExceGeneralDatalListener extends AnalysisEventListener<Map<Integer, String>> {
/**
* 处理业务逻辑的Service,也可以是Mapper
*/
private ActResultLogService2 actResultLogService2;

/**
* 用于存储读取的数据
*/
private List<Map<Integer, String>> dataList = new ArrayList<Map<Integer, String>>();

public EasyExceGeneralDatalListener() {
}

public EasyExceGeneralDatalListener(ActResultLogService2 actResultLogService2) {
this.actResultLogService2 = actResultLogService2;
}

@Override
public void invoke(Map<Integer, String> data, AnalysisContext context) {
//数据add进入集合
dataList.add(data);
//size是否为100000条:这里其实就是分批.当数据等于10w的时候执行一次插入
if (dataList.size() >= ExcelConstants.GENERAL_ONCE_SAVE_TO_DB_ROWS) {
//存入数据库:数据小于1w条使用Mybatis的批量插入即可;
saveData();
//清理集合便于GC回收
dataList.clear();
}
}

/**
* 保存数据到DB
*
* @param
* @MethodName: saveData
* @return: void
*/
private void saveData() {
actResultLogService2.import2DBFromExcel10w(dataList);
dataList.clear();
}

/**
* Excel中所有数据解析完毕会调用此方法
*
* @param: context
* @MethodName: doAfterAllAnalysed
* @return: void
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
saveData();
dataList.clear();
}
}
//JDBC工具类
public class JDBCDruidUtils {
private static DataSource dataSource;

/*
创建数据Properties集合对象加载加载配置文件
*/
static {
Properties pro = new Properties();
//加载数据库连接池对象
try {
//获取数据库连接池对象
pro.load(JDBCDruidUtils.class.getClassLoader().getResourceAsStream("druid.properties"));
dataSource = DruidDataSourceFactory.createDataSource(pro);
} catch (Exception e) {
e.printStackTrace();
}
}

/*
获取连接
*/
public static Connection getConnection() throws SQLException {
return dataSource.getConnection();
}


/**
* 关闭conn,和 statement独对象资源
*
* @param connection
* @param statement
* @MethodName: close
* @return: void
*/
public static void close(Connection connection, Statement statement) {
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (statement != null) {
try {
statement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}

/**
* 关闭 conn , statement 和resultset三个对象资源
*
* @param connection
* @param statement
* @param resultSet
* @MethodName: close
* @return: void
*/
public static void close(Connection connection, Statement statement, ResultSet resultSet) {
close(connection, statement);
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}

/*
获取连接池对象
*/
public static DataSource getDataSource() {
return dataSource;
}

}
# druid.properties配置
driverClassName=oracle.jdbc.driver.OracleDriver
url=jdbc:oracle:thin:@localhost:1521:ORCL
username=mrkay
password=******
initialSize=10
maxActive=50
maxWait=60000
// Service中具体业务逻辑

/**
* 测试用Excel导入超过10w条数据,经过测试发现,使用Mybatis的批量插入速度非常慢,所以这里可以使用 数据分批+JDBC分批插入+事务来继续插入速度会非常快
*
* @param
* @MethodName: import2DBFromExcel10w
* @return: java.util.Map<java.lang.String, java.lang.Object>
*/
@Override
public Map<String, Object> import2DBFromExcel10w(List<Map<Integer, String>> dataList) {
HashMap<String, Object> result = new HashMap<>();
//结果集中数据为0时,结束方法.进行下一次调用
if (dataList.size() == 0) {
result.put("empty", "0000");
return result;
}
//JDBC分批插入+事务操作完成对10w数据的插入
Connection conn = null;
PreparedStatement ps = null;
try {
long startTime = System.currentTimeMillis();
System.out.println(dataList.size() + "条,开始导入到数据库时间:" + startTime + "ms");
conn = JDBCDruidUtils.getConnection();
//控制事务:默认不提交
conn.setAutoCommit(false);
String sql = "insert into ACT_RESULT_LOG (onlineseqid,businessid,becifno,ivisresult,createdby,createddate,updateby,updateddate,risklevel) values";
sql += "(?,?,?,?,?,?,?,?,?)";
ps = conn.prepareStatement(sql);
//循环结果集:这里循环不支持"烂布袋"表达式
for (int i = 0; i < dataList.size(); i++) {
Map<Integer, String> item = dataList.get(i);
ps.setString(1, item.get(0));
ps.setString(2, item.get(1));
ps.setString(3, item.get(2));
ps.setString(4, item.get(3));
ps.setString(5, item.get(4));
ps.setTimestamp(6, new Timestamp(System.currentTimeMillis()));
ps.setString(7, item.get(6));
ps.setTimestamp(8, new Timestamp(System.currentTimeMillis()));
ps.setString(9, item.get(8));
//将一组参数添加到此 PreparedStatement 对象的批处理命令中。
ps.addBatch();
}
//执行批处理
ps.executeBatch();
//手动提交事务
conn.commit();
long endTime = System.currentTimeMillis();
System.out.println(dataList.size() + "条,结束导入到数据库时间:" + endTime + "ms");
System.out.println(dataList.size() + "条,导入用时:" + (endTime - startTime) + "ms");
result.put("success", "1111");
} catch (Exception e) {
result.put("exception", "0000");
e.printStackTrace();
} finally {
//关连接
JDBCDruidUtils.close(conn, ps);
}
return result;
}

导出问题解决方案

问题:如果一次性查询数据库百万条数据会很慢

解决:

  • 首先在查询数据库层面,需要分批进行查询(我使用的是每次查询20w)
  • 每查询一次结束,就使用EasyExcel工具将这些数据写入一次
  • 当一个Sheet写满了100w条数据,开始将查询的数据写入到另一个Sheet中
  • 如此循环直到数据全部导出到Excel完毕

3. 如何设计一个权限管理模块(这里用的是RBAC模式)

在RBAC 模型中,通过将权限分配给角色,当该角色被指定给用户时,用户就拥有了该角色所分配的权限。每个角色至少具备一个权限,每个用户至少扮演一个角色。

用户 n:<——>:n:角色n:<——>n:权限

pAtO8MD.png

只要对角色设置权限,属于该角色的人员就自动拥有这些权限。

如果某一权限范围太大了,想修改怎么弄:通过删除该角色的权限缩小范围

RBAC1:角色继承的RBAC模型

系统中存在的角色太多了,因为只要有权限不一样的用户加入系统,就需要新建一个角色,当用户权限分得很细的时候,甚至比ACL还繁琐

他发现角色之间存在着类似组织架构一样的上下级关系,比如COO>运营经理>运营主管>运营组长>运营人员>运营实习生,并且上级拥有下级的所有权限,同时可以额外拥有其他权限,于是他在现有的角色基础上又抽象出一层角色等级。

这种在角色中引入上下级关系的RBAC模型就叫做角色继承的RBAC模型(RBAC1),通过给角色分级,高级别的角色可继承低级别角色的权限,一定程度上简化了权限管理工作。

pAtXW0H.webp

RBAC2:角色限制的RBAC模型

这种针对角色进行限制的模型就叫做角色限制的RBAC模型(RBAC2),可以把限制具体地分为静态职责分离(SSD)和动态职责分离(DSD):

  • SSD:
  • 互斥角色:同一用户只能分配到一组互斥角色集合中至多一个角色,比如用户不能同时拥有会计和审计两个角色
  • 基数约束:一个用户可拥有的角色数目受限;一个角色可被分配的用户数量受限;一个角色对应的权限数目受限
  • 先决条件角色:用户想要成为上级角色,必须先成为下一级角色,比如游戏中的转职
  • DSD:允许一个用户具有多个角色,但在运行时只能激活其中某些角色,比如BOSS直聘,一个用户既可以是招聘者也可以是应聘者,但同时只能选择一种身份进行操作

RBAC3:统一的RBAC模型

RBAC3=RBAC1+RBAC2,既引入了角色间的继承关系,又引入了角色限制关系。

完善的RBAC模型

现有的权限管理模型虽然已经对“角色”进行了层级优化,但并没有优化“用户”方,这就意味着每入职一个新员工,小王得单独为其设置权限,还是很麻烦,于是他利用抽象的思想将相同属性的用户进行归类。在公司里,最简单的相同属性就是“部门”了,如果给部门赋予了角色和权限,那么这个部门中的所有用户都有了部门权限,而不需要为每一个用户再单独指定角色。

用户可以分组,权限同样也可以分组。在权限特别多的情况下,可以把同一层级的权限合并为一个权限组,如二级菜单下面有十几个按钮,如果对按钮的操作没有角色限制,可以把这些按钮权限与二级菜单权限合并为一个权限组,然后再把权限组赋予角色就可以了。

“组”概念的引入完善了RBAC模型,在简化操作的同时更贴近了实际业务,便于理解。

pAtXRne.webp

4. 什么是笛卡尔积,及其在SQL中的应用?

在关系型数据库中,笛卡尔积用于生成两个或多个表之间的所有可能组合。

假设我们有两个表:studentscourses

他们各有两个字段

使用以下 SQL 查询来生成 studentscourses 表之间的笛卡尔积:

1
select * from stu cross join cou;

每个学生与每门课程的所有可能组合都被列出了

student_id student_name course_id course_name
1 Alice 101 Math
1 Alice 102 Science
2 Bob 101 Math
2 Bob 102 Science

我们可以生成这三个表之间的笛卡尔积,查询如下:

1
2
3
4
5
SELECT *
FROM employees
CROSS JOIN departments
CROSS JOIN projects;
1234

结果集

employee_id employee_name department_id department_name project_id project_name
1 John 10 HR 1001 Alpha
1 John 10 HR 1002 Beta
1 John 20 IT 1001 Alpha
1 John 20 IT 1002 Beta
2 Jane 10 HR 1001 Alpha
2 Jane 10 HR 1002 Beta
2 Jane 20 IT 1001 Alpha
2 Jane 20 IT 1002 Beta

这生成了每个员工、每个部门和每个项目的所有可能组合。

使用 WHERE子句限制结果集

虽然笛卡尔积会生成所有可能的组合,但在实际查询中,我们常常需要限制结果集。通过结合 WHERE 子句,我们可以筛选掉不需要的结果。

例如,我们只对 IT 部门的员工和项目感兴趣。可以使用如下查询:

1
2
3
4
5
6
SELECT *
FROM employees
CROSS JOIN departments
CROSS JOIN projects
WHERE departments.department_name = 'IT';
12345

结果集

employee_id employee_name department_id department_name project_id project_name
1 John 20 IT 1001 Alpha
1 John 20 IT 1002 Beta
2 Jane 20 IT 1001 Alpha
2 Jane 20 IT 1002 Beta

这样,我们只保留了 IT 部门的所有组合。

笛卡尔积的性能考虑

笛卡尔积虽然功能强大,但在处理大数据集时可能会导致性能问题。生成的结果集的大小是输入表行数的乘积,因此在数据量大的情况下,结果集的大小会迅速增长,从而对数据库性能产生重大影响。这个时候可以加限制条件。

MySQL怎么连表查询

  1. 内连接

    1
    select a.name,b.name form a inner join b on a.id=b.id
  2. 左外连接

    1
    select a.name,b.name form a left join b on a.id=b.id
  3. 右外连接

1
select a.name,b.name form a right join b on a.id=b.id
  1. 全外连接
1
2
3
select a.name,b.name form a left join b on a.id=b.id
union
select a.name,b.name form a right join b on a.id=b.id

5. 使用联表查询比较慢如何进行优化?

  1. 使用正确的连接类型:根据查询需求,选择合适的连接类型,如 INNER JOIN、LEFT JOIN 或 RIGHT JOIN。INNER JOIN 通常性能较好,因为只返回匹配的记录。

    使用连接操作时,应该尽量使用 INNER JOIN,因为它通常性能最好。

  2. 优化 WHERE 子句:减少查询结果集的大小,可以有效提高关联查询的性能。在 WHERE 子句中添加筛选条件,以尽可能地缩小结果集。

  3. 为关联字段创建索引:为关联操作中用到的字段创建索引,可以大大提高查询性能。确保在所有参与连接操作的表中的关联字段上都创建了合适的索引。

  4. 减少查询的字段:尽量只查询需要的字段,而不是使用 SELECT *。返回更少的数据可以减轻数据库和网络负担,从而提高性能。

  5. 考虑使用分布式查询:对于非常大的数据集,可以考虑将数据分布在多个服务器上,然后使用分布式查询(如 MySQL Cluster 或分片技术)来提高关联查询的性能。

  6. 尽量避免子查询:子查询可能导致性能下降。尽可能使用连接操作替换子查询,因为 MySQL 在执行连接操作时通常性能更好。

  7. 利用 EXPLAIN 分析查询:使用 EXPLAIN 命令分析查询计划,找出性能瓶颈,然后针对性地进行优化。EXPLAIN 可以帮助你识别需要添加索引的字段、连接顺序等问题。

  8. 分解复杂查询:将复杂的多表关联查询分解成多个简单查询,可以降低查询复杂度,提高性能。通过将查询结果保存到临时表或内存表,然后再执行其他查询操作,可以有效地降低查询的复杂度。

总结:联表查询较慢可以从以下方面解决使用内连接优化where语句,为查询字段创建索引,减少查询字段(不要用select*),采用分布式,避免子查询等。