forked from gzc426/Java-Interview
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbackend
More file actions
332 lines (202 loc) · 12.8 KB
/
backend
File metadata and controls
332 lines (202 loc) · 12.8 KB
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
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
面试题1:Java中实现线程安全有哪些方式?请举例说明。
考察点: 线程安全的基本概念、同步机制、并发工具类
答案要点:
线程安全定义:多个线程访问共享资源时,不会产生不确定的结果。
实现方式:
使用 synchronized 关键字:修饰方法或代码块,保证同一时刻只有一个线程执行。
java
public synchronized void increment() { count++; }
使用 Lock 接口:如 ReentrantLock,提供更灵活的锁操作(可中断、可超时、公平锁)。
java
Lock lock = new ReentrantLock();
lock.lock();
try { count++; } finally { lock.unlock(); }
使用原子类:如 AtomicInteger,基于CAS(无锁)实现线程安全。
java
AtomicInteger count = new AtomicInteger();
count.incrementAndGet();
使用并发容器:如 ConcurrentHashMap、CopyOnWriteArrayList,内部已经处理好线程安全。
使用 ThreadLocal:每个线程拥有自己的变量副本,避免共享。
使用 volatile 关键字:保证可见性,但不保证原子性(适合状态标志位)。
选择依据:根据并发度、竞争激烈程度、代码复杂度选择合适的方式。
面试题2:什么是死锁?如何避免死锁?
考察点: 死锁的四个必要条件、预防与避免策略
答案要点:
死锁定义:两个或更多线程互相持有对方所需的资源,导致彼此永远等待。
产生死锁的四个必要条件(缺一不可):
互斥:资源一次只能被一个线程占用。
持有并等待:线程持有资源的同时等待其他资源。
非抢占:线程已获得的资源不能被强行剥夺。
循环等待:存在线程间的循环等待链。
避免方法:
破坏“持有并等待”:一次性申请所有资源。
破坏“非抢占”:如果线程申请不到新资源,则释放已占有的资源(Lock 的 tryLock 支持超时释放)。
破坏“循环等待”:按固定顺序申请资源(如锁顺序),避免环路。
使用锁超时:lock.tryLock(timeout) 在规定时间内获取不到就放弃。
使用并发工具:如 Semaphore、CountDownLatch 等高级工具,减少手动锁的使用。
🔒 1. 乐观锁、悲观锁,如何实现
面试题3:乐观锁和悲观锁的区别是什么?在Java中如何实现?
考察点: 两种锁的思想、适用场景、实现技术
答案要点:
悲观锁:
思想:认为并发冲突的概率很高,所以在操作数据前先加锁,阻塞其他线程。
实现:Java 中的 synchronized、ReentrantLock;数据库中的 select ... for update。
适用场景:写操作频繁、冲突严重的情况。
乐观锁:
思想:认为并发冲突的概率较低,不加锁,在更新时检查数据是否被修改过,如果被修改则重试或失败。
实现:
版本号机制:在数据表中加一个 version 字段,更新时检查 version 是否匹配。
sql
update table set count = ?, version = version + 1 where id = ? and version = oldVersion;
CAS(Compare And Swap):Java 原子类(如 AtomicInteger)底层使用 Unsafe 的 CAS 指令实现。
适用场景:读多写少、冲突概率低的场景,可以提高并发性能。
📦 2. JPA 的 N+1 问题
面试题4:什么是 JPA 的 N+1 问题?如何解决?
考察点: ORM 性能问题、懒加载、查询优化
答案要点:
N+1 问题定义:当查询一个实体及其关联集合时,如果先查主实体(1条SQL),然后遍历每个主实体去懒加载关联集合(N条SQL),总共执行了 N+1 条SQL,造成性能低下。
例如:查询所有用户(1条SQL),然后对每个用户查询其订单(N条SQL)。
产生原因:默认的懒加载策略(FetchType.LAZY)在访问关联属性时触发额外查询。
解决方法:
使用 JOIN FETCH:在 JPQL 中显式指定关联抓取。
sql
select u from User u join fetch u.orders
使用 @EntityGraph:通过注解定义抓取计划。
java
@EntityGraph(attributePaths = "orders")
List<User> findAll();
使用批量抓取(Batch Fetch):设置 @BatchSize(size = 10),将 N 次查询合并为 N/size 次。
使用 DTO 投影:只查询需要的字段,避免加载整个实体和关联。
二级缓存:如果数据变动少,可以缓存关联对象,减少重复查询。
注意:FetchType.EAGER 虽然能避免 N+1,但可能导致不必要的关联加载,需权衡使用。
♻️ 3. 幂等
面试题5:什么是幂等性?在分布式系统中如何保证接口的幂等性?
考察点: 幂等概念、常见实现方案
答案要点:
幂等性定义:无论调用一次还是多次,对系统的副作用(即状态变化)是相同的。例如 GET 请求天然幂等,DELETE 重复删除同一资源也是幂等的(结果都是删除成功或资源已不存在)。
为什么需要:防止重复提交(如订单创建、支付扣款)导致数据错误。
常见实现方案:
唯一索引:数据库表中对关键字段(如订单号)建唯一索引,重复插入时抛异常。
去重表:利用数据库的唯一约束,插入前先查去重表,若存在则直接返回结果。
状态机:业务状态流转只能单向变化,例如订单状态“待支付”只能改为“支付成功”,不能重复支付。
Token 机制:服务端生成 token 返回给前端,每次请求携带 token,服务端校验 token 是否存在(redis),存在则处理并删除 token,重复请求 token 已失效。
分布式锁:使用 Redis 或 Zookeeper 实现锁,在业务执行前加锁,执行后释放,防止并发重复处理。
全局唯一请求 ID:客户端生成 UUID 作为请求 ID,服务端根据请求 ID 进行幂等校验(结合 Redis 存储处理过的 ID)。
🐘 4. PostgreSQL 索引与优化
面试题6:PostgreSQL 中有哪些索引类型?如何选择合适的索引?
考察点: 索引类型、适用场景
答案要点:
B-Tree 索引(默认):
适用于等值查询和范围查询(=、<、>、BETWEEN、LIKE 'abc%')。
支持排序和唯一约束。
Hash 索引:
仅适用于等值查询(=),但不常用(PostgreSQL 10 后改进,但仍不如 B-Tree 通用)。
GIN(Generalized Inverted Index)索引:
适用于包含多个值的列,如数组、JSONB、全文检索。
例如:查询数组包含某元素 WHERE arr @> '[1,2]'。
GiST(Generalized Search Tree)索引:
适用于几何类型、范围类型、全文检索(相比 GIN 更灵活但性能稍低)。
支持位置搜索、重叠判断等。
BRIN(Block Range INdex)索引:
适用于超大型表且数据与物理存储顺序相关性高的列(如时间序列)。
只存储每块数据的摘要,占用空间极小,但扫描效率低于 B-Tree。
选择原则:
根据查询条件(等值、范围、全文)、数据类型、数据分布选择。
对于高基数列(如主键),B-Tree 最合适。
对于多值类型,GIN 是首选。
对于只追加的日志表,BRIN 可以大幅节省空间。
面试题7:如何分析 PostgreSQL 中的慢查询?请列出关键步骤和常用命令。
考察点: SQL 调优、执行计划解读
答案要点:
启用慢查询日志:在 postgresql.conf 中设置:
text
log_min_duration_statement = 1000 # 记录超过1秒的SQL
log_connections = on
log_disconnections = on
使用 EXPLAIN 分析执行计划:
EXPLAIN (ANALYZE, BUFFERS) your_sql; 实际执行 SQL 并输出真实统计信息。
关键指标解读:
Seq Scan:全表扫描,通常需要优化(除非表很小)。
Index Scan:使用索引扫描,效率较高。
Bitmap Heap Scan:结合多个索引扫描的结果。
cost:启动成本和总成本,估算值。
actual time:实际执行时间(ms)。
rows:预估行数与实际行数(若偏差大,说明统计信息过时,需执行 ANALYZE)。
Buffers:读取的缓存块数,可判断是否大量 I/O。
优化措施:
创建合适的索引。
更新统计信息:ANALYZE table;
调整 work_mem、shared_buffers 等参数。
改写 SQL(避免函数在索引列上,使用 JOIN 代替子查询等)。
🏗️ 5. Java 设计模式
面试题8:你熟悉哪些设计模式?请举例说明在 Spring 框架中的应用。
考察点: 设计模式掌握程度、与实际框架结合
答案要点:
单例模式(Singleton):
Spring 中 Bean 默认作用域为单例,容器中只存在一个实例。
工厂模式(Factory):
BeanFactory 和 ApplicationContext 负责创建和管理 Bean 实例。
代理模式(Proxy):
Spring AOP 基于动态代理(JDK 动态代理或 CGLIB)实现切面功能。
模板方法模式(Template Method):
JdbcTemplate、RestTemplate 封装固定流程,子类或回调实现可变部分。
观察者模式(Observer):
Spring 事件机制(ApplicationEvent 和 ApplicationListener)。
策略模式(Strategy):
Resource 接口的不同实现(如 ClassPathResource、FileSystemResource)。
适配器模式(Adapter):
Spring MVC 中的 HandlerAdapter 适配不同类型的处理器(Controller、HttpRequestHandler 等)。
装饰器模式(Decorator):
BeanWrapper 对 Bean 实例进行包装,添加属性编辑功能。
🔍 6. 接口与抽象类的区别及使用场景
面试题9:Java 中接口和抽象类有什么区别?各自在什么场景下使用?
考察点: 面向对象基础、设计原则
答案要点:
语法区别:
抽象类:用 abstract 修饰,可以有抽象方法和具体方法,可以有成员变量、构造方法。
接口:用 interface 修饰,方法默认 public abstract(Java 8 后可以有 default 和 static 方法),变量默认 public static final。
设计思想区别:
抽象类:表示“是什么”(is-a)关系,定义一类事物的共同属性和行为,可以有状态(成员变量)。
接口:表示“能做什么”(like-a)关系,定义行为规范,强调能力,不能有状态。
使用场景:
抽象类适用场景:
多个类有公共代码,需要代码复用。
需要控制子类的访问权限或提供默认行为。
例如:HttpServlet 抽象类提供 service() 模板方法。
接口适用场景:
需要定义多个不相关类的共同行为(如 Comparable、Runnable)。
需要实现多继承(一个类可以实现多个接口)。
解耦调用方与实现方,定义契约(如 API 接口)。
Java 8 后变化:接口可以有 default 方法,可以在不破坏实现类的情况下新增方法;可以有 static 方法,提供工具方法。但接口仍不能有实例变量。
📋 7. 数据库事务
面试题10:什么是数据库事务的 ACID 特性?事务隔离级别有哪些?MySQL 默认隔离级别是什么?
考察点: 事务基本概念、隔离级别、并发问题
答案要点:
ACID 特性:
原子性(Atomicity):事务是一个不可分割的单元,要么全部成功,要么全部失败回滚。
一致性(Consistency):事务执行前后,数据库完整性约束没有被破坏(数据状态一致)。
隔离性(Isolation):多个事务并发执行时,相互隔离,互不干扰。
持久性(Durability):事务一旦提交,对数据的修改是永久性的。
事务并发问题:
脏读:读到另一个事务未提交的数据。
不可重复读:同一事务内多次读取同一数据,结果不一致(因其他事务修改并提交)。
幻读:同一事务内多次查询,返回的记录数不同(因其他事务插入或删除)。
事务隔离级别(由低到高):
读未提交(Read Uncommitted):可能发生脏读、不可重复读、幻读。
读已提交(Read Committed):避免脏读,但可能发生不可重复读和幻读(Oracle 默认)。
可重复读(Repeatable Read):避免脏读和不可重复读,但可能发生幻读(MySQL InnoDB 默认,通过 MVCC 和间隙锁解决幻读)。
可串行化(Serializable):最高级别,所有事务串行执行,无并发问题,但性能最低。
MySQL InnoDB 默认隔离级别:可重复读(Repeatable Read)。
如何选择:根据业务对一致性的要求与并发性能的权衡,选择合适的隔离级别。
补充题:Spring 中如何使用事务?@Transactional 的常用属性有哪些?
考察点: Spring 事务管理
答案要点:
声明式事务:使用 @Transactional 注解,可作用于类或方法。
常用属性:
propagation:事务传播行为(REQUIRED、REQUIRES_NEW、NESTED 等)。
isolation:事务隔离级别(DEFAULT、READ_COMMITTED 等)。
timeout:事务超时时间(秒)。
readOnly:是否为只读事务(优化数据库连接,适合查询)。
rollbackFor:指定哪些异常触发回滚(默认运行时异常回滚,受检异常不回滚)。
noRollbackFor:指定哪些异常不回滚。
原理:基于 AOP 动态代理,在方法前后开启、提交或回滚事务