面试模拟-202506

6.11模拟面试

1.什么是 Java 的面向对象编程?它的核心特性有哪些?

面向对象编程是OOP是说就是用“对象”来组织代码,java就是一门面向对象编程的语言。

java的面向对象的三大特征是封装继承多态

封装:反射,修饰符

继承:构造方法调用,final

多态:方法重载,方法重写

抽象:抽象和接口

静态:

2.说说 Java 中 ArrayList 和 LinkedList 的区别,以及各自的适用场景

底层:

遍历和插入:

arrayList扩容:1.5,数组的复制

内存占比:

使用场景:

线程安全:

3.HashMap 的底层原理

特点:

底层实现,1.7/1.7之后,头插和尾插

put过程

转换红黑树/扩容:当链表长度≥8 且数组容量≥64 时转红黑树 为什么用红黑树

线程安全:

concurrenthashmap,底层,put过程

4.Spring 框架中,Bean 的作用域有哪些?请分别说明它们的生命周期和应用场景

5 大标准作用域

  • Singleton:全局唯一实例,Spring 容器加载时创建,适用于无状态服务;
  • Prototype:每次获取新实例,适用于有状态对象(如 DAO 层);
  • Request:每次 HTTP 请求一个实例,需 Web 环境,适用于请求上下文数据;
  • Session:每个用户会话一个实例,需 Web 环境,适用于会话级数据;
  • Application:全局 ServletContext 共享实例,需 Web 环境。

生命周期:实例化→属性注入→@PostConstruct→初始化完成→使用→@PreDestroy→销毁。

循环依赖:setter单例注入,三级缓存,

  • 一级缓存(singletonObjects):存储完全初始化的单例 Bean;
  • 二级缓存(earlySingletonObjects):存储早期曝光的 Bean(解决 A→B→A 的循环依赖);
  • 三级缓存(singletonFactories):存储 Bean 的工厂,用于生成代理对象(解决 A→B(代理)→A 的循环依赖)。

5.AOP 的实现原理

概念:适用场景

作用点:

  • 切面(Aspect):封装横切逻辑的类,用@Aspect注解标识;
  • 切点(Pointcut):定义横切逻辑作用的目标方法(如@Pointcut("execution(* com.service.*.*(..))"));
  • 通知(Advice):横切逻辑的具体实现,包括 5 种类型
  • 连接点(Joinpoint):程序执行中的具体点(如方法调用、字段修改),AOP 中主要指方法调用

动态代理:

  • JDK 动态代理:基于接口实现,生成InvocationHandler的代理对象,适用于目标类有接口的情况;
  • CGLIB 代理:基于子类继承,生成目标类的子类代理对象,适用于无接口的类(需引入cglib依赖);
  • Spring 默认策略:有接口用 JDK 代理,无接口用 CGLIB 代理(可通过proxy-target-class属性强制使用 CGLIB)。
  • 流程:
  • Spring 容器扫描到@Aspect注解的切面类,解析切点表达式和通知类型;
  • 对目标类判断是否适用 JDK/CGLIB 代理,生成代理对象;
  • 当调用代理对象的方法时,实际执行的是通知逻辑 + 目标方法的组合逻辑。

细节:

  1. 开启 AOP 支持:在配置类中添加@EnableAspectJAutoProxy注解,或在 XML 中配置<aop:aspectj-autoproxy/>

  2. 代理模式选择

    • 默认proxyTargetClass=false(优先 JDK 代理);
    • 若需强制 CGLIB 代理,设置proxyTargetClass=true@EnableAspectJAutoProxy(proxyTargetClass = true)
  3. 循环依赖与 AOP:若目标类被代理,注入的是代理对象,需注意this.方法()调用不会触发 AOP(因this指向原始对象)。

场景:

  • 日志记录:在方法执行前后记录入参 / 结果,避免业务代码污染;

  • 事务管理@Transactional本质是 AOP 实现,在方法调用时开启 / 提交 / 回滚事务;

  • 权限校验:在接口调用前校验用户权限,拒绝非法请求;
  • 性能监控:统计方法执行耗时,用于性能优化。

流程:

  1. 调用代理方法:客户端调用代理对象的方法(如userService.save());
  2. 匹配切点:Spring 判断该方法是否匹配切面的切点表达式;
  3. 执行通知逻辑
    • @Around中先执行前置逻辑(如日志记录开始);
    • 调用proceed()触发目标方法执行;
    • 执行后置逻辑(如日志记录结束、统计耗时);
  4. 返回结果:通知逻辑执行完毕后,将结果返回给客户端。

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 1. 定义切面类
@Aspect
@Component
public class LogAspect {

// 2. 定义切点(匹配所有Service方法)
@Pointcut("execution(* com.service..*.*(..))")
public void servicePointcut() {}

// 3. 环绕通知实现耗时统计
@Around("servicePointcut()")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
// 调用目标方法
Object result = joinPoint.proceed();
long end = System.currentTimeMillis();
// 记录日志
String methodName = joinPoint.getSignature().getName();
System.out.println("方法 " + methodName + " 执行耗时:" + (end - start) + "ms");
return result;
}
}

6.MySQL 中,索引是什么?常见的索引类型有哪些?请说明它们的适用场景和优缺点。

数据库中用于加快数据查询速度的一种数据结构,可以类比为书本的目录。通过索引,数据库可以更快地定位到目标数据,而无需全表扫描。

为什么使用b+树,层高,叶子节点/非叶子节点

聚簇索引,非聚簇索引,索引下推

联合索引,覆盖索引

普通索引,唯一索引,前缀索引,全文索引。

索引失效:

explain:

7.MyBatis 的一级缓存和二级缓存机制?如何禁用缓存?

sqlsession:

在一次 SqlSession 生命周期内,相同的查询语句(SQL + 参数)会被缓存,第二次执行时直接从缓存中取值,不会发起数据库请求。当前会话有效,执行 insert/update/delete:任何更新操作会清空缓存。执行sqlSession.clearCache()。不同参数的时候也会失效

mapper:

二级缓存是 Mapper 级别(namespace)共享缓存。

多个 SqlSession 之间共享该 Mapper 的缓存数据。

启用条件:

在 MyBatis 配置中启用全局缓存:

1
2
3
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>

在对应 Mapper 文件中配置 <cache> 标签:

查询的 POJO 实体类必须实现 Serializable

查询语句不能使用 flushCache="true"(默认查询是 false,更新是 true)。

SqlSession 必须关闭后,一级缓存的数据才会被写入二级缓存。

默认缓存实现是 PerpetualCache + LRU

可自定义缓存策略(如 EhCache、Redis)。

8.MyBatis Plus 的 Wrapper 和原生 XML 写 SQL 的区别?什么时候该用 Wrapper?

Wrapper 是 MyBatis Plus 提供的条件构造器,用于构建 SQL 的 WHERE、ORDER BY 等子句,简化代码书写。

QueryWrapper:用于普通查询。

LambdaQueryWrapper:使用 lambda 表达式避免写字段名字符串。

UpdateWrapper / LambdaUpdateWrapper:用于更新条件构造。

XML 方式是传统 MyBatis 写 SQL 的方式,通过 Mapper.xml 文件自定义 SQL 语句,更加灵活和强大,支持复杂的多表连接、子查询等。

对比项 Wrapper(构造器) XML 原生 SQL
语法风格 Java 代码风格,链式调用 SQL 语法,放在 XML 中
可读性 简洁、类型安全(特别是 Lambda) 接近原生 SQL,清晰直观
编写速度 快速开发,尤其适合单表 CRUD 编写略慢,需额外维护 Mapper.xml 文件
SQL 灵活性 支持简单查询(单表、分页、排序) 支持复杂 SQL(多表连接、子查询、聚合)
维护性 逻辑分散在代码中,不易集中查看 逻辑集中在 XML,更适合团队协作维护
运行效率 两者本质上都由 MyBatis 执行,性能差异不大 性能主要看 SQL 写得是否合理
调试与日志 SQL 日志可查看 也可通过日志查看

✅ 使用 Wrapper 的场景:

  • 快速开发、原型项目。
  • 简单的单表查询、分页、筛选。
  • Controller/Service 中构造简单业务逻辑。
  • 需要链式调用构造条件,代码更加优雅。
  • 使用 Lambda 避免字段拼写错误风险。

推荐:日常开发中能用 Wrapper 就用 Wrapper,提高开发效率。


✅ 使用 XML 写 SQL 的场景:

  • 涉及复杂 SQL(多表关联、聚合函数、子查询、动态 SQL)。
  • 查询语句过长、不适合写在 Java 代码中。
  • 项目追求清晰的逻辑分层、SQL 可维护性。
  • 性能调优场景,需要手写精细 SQL。
  • 团队需要 DBA 审查 SQL。

推荐:复杂业务、线上稳定项目,用 XML 更清晰、更可控。

6.12模拟面试

1.单例模式有哪些实现方式?各自的优缺点是什么?(比如饿汉式、懒汉式、双重检查锁,还有静态内部类和枚举方式,有没有线程安全的坑呀?)

饿汉式:

同步模式

volatile+synchronized

静态内部类

枚举

2.说下Spring 的ApplicationContext是单例模式”“策略模式在排序算法中的应用”

ApplicationContext 本质上采用了单例模式来保证 Spring 容器中 Bean 的唯一性和全局访问能力

Spring Boot 启动时会初始化一个 ApplicationContext(如 AnnotationConfigApplicationContext),作为IoC 容器的核心上下文

这个容器对象在整个应用中只创建一次(单例),所有组件(Controller、Service、Repository)都从这个容器中获取 Bean 实例。

避免了重复初始化 Bean 的性能开销,也方便了依赖管理、统一配置、事件发布等功能的实现。

1
2
3
ApplicationContext context = SpringApplication.run(MyApp.class, args);
MyService service = context.getBean(MyService.class);

开发中我们只需注入一次上下文或通过 @Autowired 注解获取 Bean,即可全局复用。

Spring 默认的 Bean 是单例的(@Scope("singleton")),这与 ApplicationContext 单例模型相辅相成,进一步保证了资源一致性与管理效率。

策略模式允许在运行时选择算法逻辑,在排序算法中可用于根据用户选择动态切换不同排序策略。

定义统一的排序接口(策略抽象),不同的排序方式(快排、归并、冒泡等)实现这个接口。

运行时根据条件动态切换策略,无需修改原有代码(遵循开闭原则)。

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
public interface SortStrategy {
void sort(int[] arr);
}

public class QuickSort implements SortStrategy {
public void sort(int[] arr) {
// 快速排序实现
}
}

public class BubbleSort implements SortStrategy {
public void sort(int[] arr) {
// 冒泡排序实现
}
}

public class SortContext {
private SortStrategy strategy;
public SortContext(SortStrategy strategy) {
this.strategy = strategy;
}
public void executeSort(int[] arr) {
strategy.sort(arr);
}
}

1
2
3
SortContext ctx = new SortContext(new QuickSort());
ctx.executeSort(myArray); // 快速排序执行

  • 电商平台商品排序(按价格、销量、上架时间)
  • 数据可视化工具的排序规则切换

3.Java 线程池的核心参数有哪些?各自的作用是什么?比如corePoolSizemaximumPoolSize,如何根据业务场景设置这些参数呀?

核心线程

最大线程

最大线程存活事件

单位

阻塞队列

拒绝策略

工厂

4.你项目里用了JWT加双 Token,具体是怎么设计的?两个 Token 各自的作用是什么?拦截器又分别处理什么逻辑?比如有没有考虑过 Token 过期、刷新机制,或者防重放攻击的问题?

token:

  • access token(7 天有效期):放在请求头,用于日常接口认证,用 HS256 加密(密钥服务端持有);放在前端的localStorage/jwt
  • refresh token(15 天有效期):存在 Redis,值是随机字符串,且绑定用户 ID ,防止盗用。放在redis中
安全措施 说明
使用 HTTPS 防止中间人劫持 token
access token 只读 JWT 使用 HS256 加密,防篡改
refresh token 存 Redis 结合用户 ID 存储,支持设置 TTL,有效控制生命周期
黑名单机制(登出) 用户退出登录时可删除 Redis 中 refresh token,立即失效
防止多端同时登录 Redis 可使用 userId 为 key,限制 refresh token 单个登录会话
Token 刷新接口限流 防止 refresh token 被滥用(加防重放、限频)
Token 刷新重发保护 Redis 设置短期 refresh token 使用标记,防止并发重复刷新

退出策略:

用户点击退出:

  • 删除 Redis 中的 refresh token(或设置为无效标记);
  • access token 因为是 JWT,不可被撤销,可考虑实现黑名单机制(例如 Redis 中维护一张 token 黑名单);

黑名单适用于高安全场景(如后台管理系统),但会略增加接口访问时的 Redis 查询压力。

jwt拦截器:

  1. 认证拦截器:先检查请求头是否有 access token,没有就返回 401;有则解析 token,校验用户信息是否存在。
  • 第一个拦截器先校验请求头是否有 access token,没有的话直接返回 401;第二个拦截器在 access token 有效时,额外检查是否快过期(比如剩余时间 < 10 分钟),如果是就用 refresh token 去 Redis 换全新的 access token 和 refresh token(这里要注意刷新时的原子性,避免并发问题)。
  1. 刷新拦截器:如果 access token 剩余有效期 < 10 分钟,就用请求头中的 refresh token 去 Redis 校验:
    • 校验通过的话,生成新的 access token 和 refresh token(新 refresh token 会覆盖 Redis 中的旧值,保证单设备登录);
    • 校验失败的话,直接让用户重新登录。

危险:

前端或攻击者拿到密钥(如部署泄露、浏览器调试泄露),就可以伪造合法 token。

token 内容是可解密的,攻击者可猜测或修改 payload,然后重新签名。

如果服务端未验证签名(有些误用场景会只解析不校验),更容易被利用。

但还是sh256更快,计算算开销低;对称密钥管理更简单,适用于内部系统或中小型项目;

开发成本更低,不涉及证书管理、公私钥分发;

措施 说明
✅ 使用强密钥 至少 256 bit 的复杂密钥,不可硬编码进前端或代码中
✅ 启用 HTTPS 防止 token 被中间人劫持
✅ token 签名校验 每次都校验 JWT 的签名,防止伪造
✅ 缩短 access token 有效期 缩小攻击窗口
✅ refresh token 存 Redis 结合用户 ID 防重放、防伪造
✅ 黑名单机制 + 登出清除 登出时使 refresh token 无效
✅ 检查 UA/IP 等指纹 防止 token 被别人拿去复用

5.看你在优惠劵的发放的时候使用限流,怎么设计的

我们在优惠券领取接口中用 AOP + 令牌桶实现限流

  • 注解定义@RateLimit 注解标注需要限流的方法,参数包括 limitCount=5(每分钟 5 次)、time=1(时间单位分钟)、keyType="USER_IP"(按用户 IP+ID 限流)。
  • 切面实现:通过 @Around 切面拦截注解方法,获取参数后生成唯一限流 key(例如 rate_limit:user_123:192.168.1.1),然后调用 Redis 令牌桶服务校验。
  • Redis 令牌桶:用 Lua 脚本实现原子性校验,核心是根据时间戳计算可生成的新令牌数,不足时拒绝请求。比如用户抢优惠券时,同一 IP+ID 每分钟最多 5 次请求,防止恶意刷接口。
  1. 防重复领取:避免用户短时间内多次点击接口,导致优惠券超发
  2. 保护服务端:峰值流量时限制请求频率,防止 Redis 或数据库被击穿。线上遇到过同一 WiFi 下多个用户被误限的情况,后来把 key 改为`IP+用户ID,减少了误判率。”

切面类如何获取注解:

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
@Aspect
@Component
public class RateLimitAspect {
@Pointcut("@annotation(com.example.RateLimit)")
public void rateLimitPointcut() {}

@Around("rateLimitPointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取方法上的注解
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
RateLimit rateLimit = method.getAnnotation(RateLimit.class);
int limitCount = rateLimit.limitCount();
long time = rateLimit.time();
String keyType = rateLimit.keyType();

// 生成限流key(IP+用户ID)
String key = generateKey(joinPoint, keyType);
// 调用Redis令牌桶校验
boolean allow = redisBucketService.tryAcquire(key, limitCount, time, TimeUnit.MINUTES);
if (!allow) {
throw new RuntimeException("请求频繁,请稍后再试");
}
return joinPoint.proceed();
}
}

为什么选令牌桶而不是漏桶?

令牌桶适合突发流量,漏桶适合平滑流量

使用redis+lua原子的存储令牌桶,使用时间戳来校验。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-- 令牌桶Lua脚本(简化版)
local key = KEYS[1] -- 限流key
local capacity = tonumber(ARGV[1]) -- 桶容量
local rate = tonumber(ARGV[2]) -- 令牌生成速率(个/秒)
local now = tonumber(ARGV[3]) -- 当前时间戳
local requested = tonumber(ARGV[4]) -- 请求令牌数

-- 读取上次更新时间和剩余令牌数
local last = redis.call('hget', key, 'last')
local tokens = tonumber(redis.call('hget', key, 'tokens') or 0)
last = last or now

-- 计算可生成的新令牌数
local delta = now - last
local newTokens = math.min(capacity, tokens + delta * rate)
if newTokens >= requested then
-- 够发令牌,更新状态
redis.call('hset', key, 'last', now)
redis.call('hset', key, 'tokens', newTokens - requested)
return 1 -- 允许访问
else
return 0 -- 拒绝访问
end

6.那么优惠劵的发放呢?一些列的问题呢

✅ 一、关于优惠券类型的常见问题

❓1. 如何区分和应用不同类型的优惠券逻辑?

面试点:策略模式、优惠金额计算方式。

答法

我们用策略模式来封装每种优惠券的优惠逻辑(如满减、折扣、无门槛)。每种类型对应一个策略类,实现统一的接口方法 calculateDiscount(OrderInfo order),这样能在运行时动态选择策略,便于扩展和维护。


✅ 二、关于接口设计的追问

❓2. 为什么接口是这样的?有没有考虑幂等性和安全性?

  • POST /use 接口可能会被重复调用,是否幂等
  • 有没有做权限控制,防止其他用户伪造请求?

答法

在使用优惠券时,为防止重复使用,我们使用 Redis + Lua 实现原子操作,同时标记优惠券为“已使用”。此外接口采用登录态校验,验证当前操作用户是否拥有该优惠券。敏感操作都要求用户登录,并记录操作日志。


✅ 三、关于并发处理和限领逻辑

❓3. 如果多用户并发领取优惠券,如何防止超发?

答法

发放流程中,我们使用 Redis 的 DECR 或 Lua 脚本实现优惠券库存的原子扣减,保证不会发放超过设定数量。同时加锁防止并发条件下库存扣减不一致。


❓4. 如何限制每个用户只能领取一次?

答法

Redis 中设置一个标识键:user:coupon:received:{couponId}:{userId},发放前先判断这个键是否存在,避免重复发放。也可以结合布隆过滤器提前过滤无效请求。


✅ 四、关于时间与状态校验

❓5. 限时券怎么判断是否有效?服务端怎么处理时间逻辑?

答法

每张优惠券记录中保存有效时间范围。使用时,服务端比对当前时间是否在有效期内。同时定时任务每天扫描过期优惠券,标记为“已过期”。


✅ 五、关于优惠券使用过程的逻辑判断

❓6. 使用时如果有多个优惠券,怎么选最优?

答法

前端可以请求一个“推荐最优券”的接口,我们在服务端遍历用户可用券,调用各个策略类计算预期优惠,返回最高优惠金额对应的券。


✅ 六、关于优惠券与订单系统结合问题

❓7. 如果下单失败了,优惠券是否要恢复?

答法

下单失败(如支付失败、库存不足)时,我们会在事务回滚后将优惠券状态重置为“未使用”。为了避免并发问题,使用分布式事务(如消息队列、TCC)或 Redis 回滚标记。


✅ 七、进阶设计问题

❓8. 优惠券支持“异步发放”和“定时生效”吗?

答法

是的,可以结合定时任务(如 Quartz 或 Spring Schedule)实现定时发放;发放时间和有效时间字段分离,发放时写入 Redis 延时队列,当生效时间到达时插入到用户优惠券表中。


✅ 八、可能让你设计代码结构的问题

❓9. 优惠券模块的结构划分是怎样的?

答法(示意):

1
2
3
4
5
- entity(Coupon, UserCoupon)
- controller(CouponController)
- service(CouponService、UseStrategy接口及实现)
- repository(MyBatis-Plus Mapper)
- util(金额计算工具类)

✅ 九、安全性问题

❓10. 用户伪造优惠券 ID,试图套用别人的优惠券怎么办?

答法

每次使用时不仅要校验优惠券 ID 是否存在,还要校验当前登录用户是否是该优惠券持有人,只有归属校验通过才能使用。

7.AOP记录日志

“我们用 AOP 实现了登录日志和业务日志的分类记录,核心设计是:

  • 注解与策略:定义@Log注解标注需要记录的方法,通过type()参数区分LOGINBUSINESS类型。
  • 切面逻辑:拦截方法执行前后,获取用户信息、操作参数、接口耗时等数据,封装成日志对象。例如登录日志会记录 IP、设备信息,业务日志会记录操作类型(如下单、领券)和参数(如商品 ID、优惠券 ID)。

  • 存储优化:用 Kafka 异步写入日志,避免影响接口性能,再通过 Logstash 同步到 Elasticsearch,方便用 Kibana 按时间、用户 ID 等维度检索。

举个实际场景:用户领取优惠券时,业务日志会记录{user_id:123, operation:"领取满100减20", params:{coupon_id:567}, cost:87ms}。有次线上发现某个接口耗时突然增加,通过 ELK 检索该接口的日志,很快定位到是参数校验逻辑异常导致的。

我们对敏感信息做了脱敏处理,比如用户手机号会存成138****5678,既满足日志追溯需求,又符合数据安全规范。”

存储的信息,使用json进行存储

  • 登录日志:用户 ID、登录 IP、设备信息、登录时间、状态(成功 / 失败)、失败原因(如密码错误);
  • 业务日志:用户 ID、操作类型(如 “领取优惠券”“下单”)、操作内容、操作时间、接口耗时、请求参数(脱敏处理)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Aspect
@Component
public class LogAspect {
@Around("operationLogPointcut()")//切入点就是注解
public Object logOperation(ProceedingJoinPoint joinPoint) throws Throwable{
Method method = ((MethodSignature)joinPoint.getSignature()).getMethod();
OperationLog logAnno = method.getAnnotation(OperationLog.class);


long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
long duration =System.currentTimeMillis()-start;

LogDTO log = buildLog(joinPoint,logAnno.type(),logAnno.action(),duration);
asyncLogUtils.record(log);
return result;
}

Method获取主机额上的方法,然后建立实体类,设置参数,然后利用DTO传参,然后执行service方法,然后返回,service就是调用log,info

DTO层负责service和controller直接传输数据

6.14模拟面试

1.Spring 事务的实现原理是什么?@Transactional 注解在什么情况下会失效?

spring事务的实现原理是基于AOP的动态代理和TransacationInterceptorh还有底层依赖

AOP:Spring 通过 ProxyFactoryBean 生成代理对象,默认对接口用 JDK 动态代理,对类用 CGLIB 代理;是采用cglib继承目标类的方式去创建代理类,非pulic的方法不能能继承。

拦截器:核心拦截器,在方法调用前后开启 / 提交 / 回滚事务,基于 ThreadLocal 存储事务状态;基于运行时异常来回滚的,所以把运行时异常给catch或者返回没指定的异常

底层依赖:通过 PlatformTransactionManager 接口适配不同事务管理器(如 JDBC、JPA)。

使用ThreadLocal存储事务的状态,(如连接、隔离级别)通过TransactionSynchronizationManager存在线程本地变量,保证线程安全。

所以基于这个情况,spring事务失效的场景有:

1.吃掉运行时异常没抛出:

2.未配置回滚规则,要配置rollbackFor=Exception.class指定类型

3.调用this

4.非public方法

5.事务的传播属性设置为never,not_support这种不支持事务的

6.调用了不支持事务的数据库

7.事务嵌套:

REQUIRES_NEW 会挂起外部事务,提前提交

嵌套事务用 NESTED 会创建 savepoint,支持回滚子事务(但数据库需支持)

1
2
3
4
5
6
7
8
9
10
11
// 外部事务
@Transactional
public void outer() {
inner(); // REQUIRES_NEW
throw new RuntimeException(); // 外部异常不会影响 inner 提交的事务
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void inner() {
// 已提交
}

Transactional 属性详解:

  • propagation:传播行为(如 REQUIRED:当前有事务则加入,无则新建);
  • isolation:隔离级别(如 READ_COMMITTED 避免脏读);
  • timeout:事务超时时间(如timeout=30秒);
  • rollbackFor/rollbackForClassName:指定回滚的异常类型;
  • noRollbackFor/noRollbackForClassName:指定不回滚的异常类型。

2.Java 中 HashMap 在 JDK1.8 的底层实现是什么?相对于 JDK1.7 做了哪些优化?

1.8:数组+链表+红黑树,1.7数组+链表

尾插 头插

转换条件,<64先扩容

为什么不用平衡二叉树,

尾插解决环的问题(线程 A 和线程 B 同时扩容 HashMap,A 处理链表时被挂起,B 完成扩容后,A 继续处理时按头插法插入元素,导致链表成环;)。

扩容机制 <64先扩容

哈希算法优化:

  • 1.7:通过hashCode() ^ (hashCode() >>> 16)扰动函数打散高 16 位,避免低位冲突;
  • 1.8:直接使用hashCode(),但计算下标时用(n-1) & hash(n 为数组长度,需是 2 的幂),例:

实战:

  • 性能提升:存储 1000 个哈希冲突的键时,1.7 查询需遍历链表(O (1000)),1.8 用红黑树只需 O (log2 (1000))≈10 次查询;
  • 内存占用:红黑树节点比链表节点多存储父节点、左右子节点指针,但若哈希冲突少,链表长度 <8 时仍用链表,节省内存。”

红黑树特点:

  1. 节点非红即黑;
  2. 根节点和叶节点(null)是黑色;
  3. 红色节点不能相邻;
  4. 任意节点到叶节点的路径上黑节点数相同;
    插入时最多旋转 2 次即可平衡,适合 HashMap 的高频插入场景。

场景:

“线上曾遇到 HashMap 性能问题,通过 JProfiler 发现某接口频繁操作哈希冲突严重的 Map,将 JDK1.7 升级到 1.8 后,接口响应时间从 500ms 降至 50ms,主要得益于红黑树对高冲突场景的优化。”

3.Spring Bean 的生命周期是怎样的?@PostConstruct 和 @PreDestroy 的作用是什么?

实例化->初始化->使用->销毁

实例化:通过构造器创建 Bean 实例(无参构造或工厂方法);

属性赋值:依赖注入(@Autowired、setter 方法等);

初始化前:执行 BeanPostProcessor 的postProcessBeforeInitialization

初始化:

  1. 执行 @PostConstruct 标注的方法;
  2. 实现 InitializingBean 接口的afterPropertiesSet
  3. 自定义 init-method(XML 配置或 @Bean 的 initMethod 属性);

初始化后:执行 BeanPostProcessor 的postProcessAfterInitialization

使用:Bean 放入容器,供应用获取使用;

销毁前:执行 @PreDestroy 标注的方法;

销毁:

  1. 实现 DisposableBean 接口的destroy
  2. 自定义 destroy-method。

三级缓存解决循环依赖:

@PostConstruct 和 @PreDestroy 的执行时机

@PostConstruct:在依赖注入完成后,初始化方法(如 init-method)之前执行;

@PreDestroy:在 Bean 销毁前执行,先于 destroy-method;

应用场景:

  • 初始化:数据库连接池初始化、缓存预热;
  • 销毁:释放资源(关闭文件流、释放锁)。

关键点:

  • BeanPostProcessor:初始化前后的拦截器(如 AOP 代理在此阶段创建);
  • InstantiationAwareBeanPostProcessor:属性赋值前的拦截器(可修改属性值);
  • SmartInitializingSingleton:所有单例 Bean 初始化完成后执行(适合初始化需要依赖其他 Bean 的场景)。

问题解决:

  • 问题:ServiceA 依赖 ServiceB,ServiceB 的 @PostConstruct 方法未完成时,ServiceA 已使用 ServiceB;
  • 解决:通过@DependsOn("serviceB")强制 ServiceA 在 ServiceB 之后初始化,或实现Ordered接口指定初始化顺序。”或者是使用懒加载加载一个,或者是重新构建方法

4.Java 中 synchronized 和 volatile 的区别是什么?各自的应用场景有哪些?

  • synchronized:

    • 原子性:通过 Monitor 锁确保代码块同一时间只能被一个线程执行;
    • 可见性:解锁时将工作内存变量刷新到主内存;
    • 有序性:通过 happens-before 原则,禁止指令重排(锁的获取 / 释放形成 happens-before 关系)。
  • volatile:

    • 可见性:写操作时强制刷新主内存,读操作时强制从主内存读取;
    • 有序性:通过内存屏障(Memory Barrier)禁止指令重排,确保 happens-before 关系;
    • 不保证原子性:仅保证单次读 / 写操作的原子性,如i++(实际是读 - 改 - 写三步,非原子)。

内存屏障使用的时间:读写屏障,写写屏障,写读屏障

使用场景:

s:原子操作,线程安全比如hashtable

v:状态标记,interput

联合使用:单例的DCL模式,concurrenthashmap,

问题:

  • 问题:多线程读取配置开关不生效;
  • 原因:配置开关未用 volatile 修饰,线程读取的是本地缓存值;
  • 解决volatile boolean configSwitch = false;,修改后立即通知所有线程。

5.Java 中为什么要使用线程池?线程池如何处理异常?

线程池的核心优势系统化梳理

  • 资源复用:避免频繁创建销毁线程(创建线程耗时约 3ms,复用可降低开销);
  • 控制并发数:防止并发过高导致的 OOM(如秒杀场景限制线程数保护数据库);
  • 统一管理:统一设置线程名称、优先级,方便日志追踪和故障排查;
  • 异步处理:将耗时任务放入线程池,避免主线程阻塞(如日志异步写入)。

比较单线程,多线程,和线程池

异常处理:

使用try-catch包裹任务

1
2
3
4
5
6
7
executor.execute(() -> {
try {
doTask();
} catch (Exception e) {
log.error("任务异常", e);
}
});

通过Future获取异常

1
2
3
4
5
6
7
8
Future<?> future = executor.submit(() -> {
throw new RuntimeException("任务异常");
});
try {
future.get(); // 阻塞获取结果,抛出异常
} catch (ExecutionException e) {
log.error("任务异常", e.getCause());
}

拒绝策略AbortPolicy会抛出RejectedExecutionException,需在提交任务时捕获:

1
2
3
4
5
try {
executor.execute(task);
} catch (RejectedExecutionException e) {
log.error("任务被拒绝", e);
}

场景:

  • 电商下单场景
    用线程池异步处理库存扣减、积分计算等耗时任务,主线程快速返回订单创建结果,提升用户体验;
  • 日志系统
    用线程池异步写入日志到 Kafka,避免 IO 阻塞影响业务接口响应时间。

6.19模拟面试

1.在 “校园餐饮系统” 项目里,你提到用了 Redis 存储 token 缓解压力,能讲讲具体怎么设计 key 的结构,以及如何保障 token 存储和读取的高效性与安全性不?

我们使用 Redis 存储用户的 refresh token,以提升系统性能并减少数据库压力,整体设计分为三部分:Key 设计、存储结构、安全机制

结构设计:

1
token:refresh:{userId}

这种格式能明确标识 token 类型和所属用户,保证唯一性和可读性,避免 key 冲突。

Value 的存储结构:

我们使用 Redis 的 字符串结构(String)哈希结构(Hash) 存储 refresh token,取决于业务是否需要额外附加字段(如生成时间、IP、设备信息)。

1
HSET token:refresh:123 userId 123 token abc123 createdAt 2025-06-19

设置合理的过期时间(如 7 天),使用 EXPIRESETEX 命令控制 token 生命周期。

Token 的生成与安全性保障:

Token 使用 JWT 生成,签名采用 HS256 算法(HMAC + SHA256),密钥保存在服务端配置中心或安全的 KMS(Key Management Service)中。

JWT 本身包含用户信息、过期时间等,生成后只将 access token 下发到前端,refresh token 则存于 Redis 中服务端管理,防止伪造。

认证流程:

登录时生成 access + refresh token:

  • access token 返回给前端;
  • refresh token 存入 Redis,key 为 token:refresh:{userId}

前端 token 过期后,携带 refresh token 请求刷新接口

后端校验 Redis 中的 refresh token,校验通过后重新生成新 token 对。

提前防止问题:

防缓存穿透:使用 布隆过滤器 记录有效用户 ID 或常见请求 Key,防止恶意请求频繁查询 Redis 失败并打爆数据库。

防击穿:为热点用户的 token 设置合理 TTL,避免同一时间大批用户同时失效。

防雪崩:TTL 设置加入随机因子,避免大批 key 同时过期。

权限校验:请求时验证 token 是否属于当前 userId,防止越权操作。

2.在 “MinaDB - 自研轻量级数据库系统” 项目里,你实现了基于 Java NIO 的数据页读写管理模块,提升了 3 倍访问效率,讲讲 Java NIO 是咋在这个模块里发挥作用的,和传统 I/O 相比,具体优化点在哪呀?

BIO 与 NIO 的区别:

BIO(Blocking I/O):每个请求需要一个线程处理,线程阻塞在 I/O 操作上,连接数一多就容易造成线程资源耗尽,系统响应慢。

NIO(Non-blocking I/O):通过 Selector + Channel + Buffer 实现多路复用,单线程可监听多个 Channel,避免大量线程阻塞等待,提高了资源利用率。

NIO 工作机制:

  • 客户端连接通过 ServerSocketChannel 接收;
  • 每个 Channel 注册到 Selector 上,监听感兴趣的事件(如 READWRITE);
  • Selector 使用 select() 非阻塞轮询就绪事件;
  • 当某个 Channel 有事件到达,就通过 Buffer 读写数据,由工作线程处理请求。

这种模型的优势是:少量线程可处理高并发请求,适合 I/O 密集型场景。

数据库应用:

数据库系统天然是多并发场景,传统 BIO 每个连接对应一个线程,会造成线程浪费甚至上下文切换频繁;

使用 NIO 模型后,我们采用 Reactor 模式,用一个主线程监听所有连接事件,用线程池异步处理真正的读写请求;

每个客户端连接通过 Channel 注册到 Selector 上,提升了连接并发处理能力;

底层页式读写通过 Buffer 显著减少了系统调用次数和数据拷贝成本,提高了磁盘 I/O 效率。

并发控制说明:

因为是数据库系统,多用户可能同时对同一数据页进行访问;

在读写路径上,我们结合了读写锁机制和自实现的MVCC(多版本并发控制),确保事务隔离的一致性;

NIO 负责连接层的高效事件处理,MVCC 负责数据访问层的并发安全。

3.在 “校园餐饮系统” 项目里,你用 AOP 实现了操作日志自动记录,讲讲 AOP 切点是怎么定义的,怎么确保只拦截到需要记录日志的关键业务方法,又不会过度拦截影响系统性能呀?

我们系统中使用了 Spring AOP 来统一记录用户的操作日志,包括登录日志和业务行为日志。整体设计思路是基于 自定义注解 + 切面编程 + 策略模式 来实现灵活、可扩展的日志记录体系。

我们定义了一个自定义注解 @LogRecord,用于标记需要记录的操作方法,注解中可以配置以下元信息:

1
2
3
4
5
6
7
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogRecord {
LogType type(); // 登录日志 or 业务日志
String action(); // 操作行为描述,如 "删除用户"
}

切面实现日志拦截逻辑:

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
@Aspect
@Component
public class LogAspect {

@Around("@annotation(logRecord)")
public Object recordLog(ProceedingJoinPoint pjp, LogRecord logRecord) throws Throwable {
// 1. 获取方法签名与参数
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();

// 2. 根据注解元信息决定日志类型
LogType type = logRecord.type();
String action = logRecord.action();

// 3. 执行方法并记录执行结果
Object result = pjp.proceed();

// 4. 将日志对象封装后交由策略类处理
LogContext context = new LogContext(...);
logStrategyFactory.getStrategy(type).record(context);

return result;
}
}

日志记录策略设计(策略模式):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public interface LogStrategy {
void record(LogContext context);
}

@Component
public class LoginLogStrategy implements LogStrategy {
public void record(LogContext context) {
// 记录用户登录成功/失败,IP、时间等
}
}

@Component
public class BizLogStrategy implements LogStrategy {
public void record(LogContext context) {
// 记录用户执行的业务操作,如“删除用户”
}
}

策略选择通过 LogStrategyFactory 实现,支持按日志类型动态扩展。

如何保证只拦截关键方法、避免性能问题:

只对使用 @LogRecord 注解的方法进行拦截,确保业务无关方法不被扫描,避免性能浪费;

切面逻辑尽量精简,只做元信息解析和日志上下文构建,真正的日志落盘交给异步任务处理或消息队列;

对不需要记录的查询类接口,不加注解,完全绕过切面执行,保障系统整体响应效率。

日志可选择输出到文件(使用 SLF4J + Logback),或存入数据库 / Elasticsearch,供后期审计;

特殊场景下(如操作异常),支持记录异常栈与执行耗时。

追问:::

  • :@Around 和 @Before 有什么区别,为什么你选用 Around?
  • :Around 能拿到方法执行前后的控制权,包括返回值和异常,更适合记录执行结果和耗时。
  • :怎么处理日志失败、落盘慢的问题?
  • :日志落盘可异步执行或通过 MQ 异步解耦,避免阻塞主流程。

4.Mysql的两段提交的三个步骤

1.prepare

InnoDB 写入 redo log 的 prepare 状态并刷盘:

InnoDB 引擎将事务修改的数据写入内存(Buffer Pool);

然后生成一条 redo log,标记为 "prepare" 状态;

调用 fsync 将 redo log 刷入磁盘,保证崩溃后能恢复;

2.binlogcommit:

Server 层将整个事务的逻辑操作记录为一条 binlog;

将 binlog 持久化刷盘,fsync() 确保落地。

3.redocommit:

Server 层写 binlog 成功后,通知 InnoDB;

InnoDB 将之前 prepare 状态的 redo log 更新为 "commit" 状态;

这一步是关键确认步骤,表示事务已完全提交。

如果 先写 binlog 再写 redo log:InnoDB 崩溃了但 binlog 有记录,主从同步出错;

如果 只写 redo log,不写 binlog:主库恢复没问题,但从库根本不会同步这条事务,数据不一致;

所以必须保证 redo log 与 binlog 一致性落盘,2PC 正是为了解决这一问题。

5.springboot启动类注解

@SpringBootApplication

这个注解是 Spring Boot 应用的入口,是一个组合注解,等价于:

1
2
3
@Configuration
@EnableAutoConfiguration
@ComponentScan

@Configuration

  • 表示这是一个配置类,等同于传统的 applicationContext.xml
  • 可在该类中使用 @Bean 定义 Bean。

@EnableAutoConfiguration

  • 开启 Spring Boot 的自动配置机制
  • 会根据项目依赖的 jar 自动配置 Spring Bean(例如依赖了 spring-boot-starter-web,就会自动配置 Tomcat、Spring MVC 等);
  • 可通过 exclude 属性排除某些自动配置类:
1
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})

它通过 SpringFactoriesLoader + SPI机制 + 条件注解,在启动时动态加载并生效配置类。

@Import(AutoConfigurationImportSelector.class)
这个 Import 会触发 Spring 的自动导入机制;

AutoConfigurationImportSelector 会加载所有自动配置类

内部使用 SpringFactoriesLoader 加载 spring.factories 文件中所有自动配置类

每个自动配置类都配合有 @ConditionalOn... 注解,例如:

Spring 会根据当前 classpath已有 Bean配置文件内容等条件判断是否真正加载配置类;

从而实现了“按需加载”的自动配置。

在springboot中约定大于配置,

约定大于配置是Spring Boot的核心设计理念,它通过预设合理的默认行为和项目规范,大幅减少开发者需要手动配置的步骤,从而提升开发效率和项目标准化程度。

理解 Spring Boot 中的“约定大于配置”原则,可以从以下几个方面来解释:

  • 自动化配置:Spring Boot 提供了大量的自动化配置,通过分析项目的依赖和环境,自动配置应用程序的行为。开发者无需显式地配置每个细节,大部分常用的配置都已经预设好了。例如,引入spring-boot-starter-web后,Spring Boot会自动配置内嵌Tomcat和Spring MVC,无需手动编写XML。
  • 默认配置:Spring Boot 为诸多方面提供大量默认配置,如连接数据库、设置 Web 服务器、处理日志等。开发人员无需手动配置这些常见内容,框架已做好决策。例如,默认的日志配置可让应用程序快速输出日志信息,无需开发者额外繁琐配置日志级别、输出格式与位置等。
  • 约定的项目结构:Spring Boot 提倡特定项目结构,通常主应用程序类(含 main 方法)置于根包,控制器类、服务类、数据访问类等分别放在相应子包,如com.example.demo.controller放控制器类,com.example.demo.service放服务类等。此约定使团队成员更易理解项目结构与组织,新成员加入项目时能快速定位各功能代码位置,提升协作效率。

@ComponentScan

  • 扫描当前类所在包及其子包下的所有类,识别注解如 @Component@Service@Repository@Controller 并注册到 Spring 容器;
  • 可指定自定义扫描路径:
1
@ComponentScan(basePackages = {"com.example.service", "com.example.controller"})

扫描这个下面的包和他的子类

常见注解:

@EnableScheduling

  • 开启定时任务支持(配合 @Scheduled 使用)

@EnableAsync

  • 开启异步任务支持(配合 @Async 使用)

@EnableTransactionManagement

  • 开启事务注解支持(如 @Transactional

6.BeanFactoryFactoryBean 的区别

BeanFactory 是 Spring 容器本身,负责管理所有 Bean;
FactoryBean 是你自己定义的 Bean,用来创建其他 Bean。

对比点 BeanFactory FactoryBean
类型 接口(Spring 顶层 IOC 容器) 接口(自定义 Bean 的工厂)
位置 Spring 框架提供 用户自定义实现
作用 提供获取 Bean 的基础功能,如 getBean() 用于控制某个复杂 Bean 的创建逻辑
返回对象 返回注册的原始 Bean 实例 返回由 getObject() 方法创建的对象
示例用途 XML 加载、懒加载场景用到 MyBatis 中用于创建 Mapper 接口代理
典型实现类 DefaultListableBeanFactory 用户自定义,如 MyCarFactoryBean

1️⃣ BeanFactory 示例(Spring 提供)

1
2
BeanFactory factory = new ClassPathXmlApplicationContext("beans.xml");
UserService userService = factory.getBean("userService", UserService.class);
  • BeanFactory 就是容器,负责“拿 Bean”。
  • 常用于早期的 XML 配置方式。

FactoryBean 示例(用户自定义)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component
public class MyCarFactoryBean implements FactoryBean<Car> {
@Override
public Car getObject() throws Exception {
// 返回一个自定义创建的对象
return new Car("BMW", 2025);
}

@Override
public Class<?> getObjectType() {
return Car.class;
}
}
@Autowired
private Car car; // 实际注入的是 MyCarFactoryBean.getObject() 返回的 Car 实例
  • Spring 启动时会识别 MyCarFactoryBean,但注入时拿的是 getObject() 的返回值;
  • 如果你想拿到 FactoryBean 本身,可以用:
1
CarFactoryBean factoryBean = (CarFactoryBean) context.getBean("&carFactoryBean");

beanFactory 是 Spring 的底层 IOC 容器接口,提供了 Bean 的获取、懒加载等功能。
FactoryBean 是一个用于创建 Bean 的“工厂类”,由我们自定义创建逻辑,它本身是一个 Bean,但它返回的对象是 getObject() 产生的。
例如:MyBatis 就是通过 FactoryBean 动态创建 Mapper 接口代理对象。

7.MyBatis Mapper 注册过程

动态代理、FactoryBean、Configuration 注册流程的理解

MyBatis 使用 MapperFactoryBean接口注册为代理对象,启动时会把接口方法解析成 MappedStatement 存入 Configuration 中,运行时由 MapperProxy 通过反射动态执行 SQL。

1
2
3
4
5
6
7
8
       XML or @MapperScan

【Mapper接口注册】 → 注册到 Configuration.mapperRegistry

【创建代理类】 MapperProxyFactory → MapperProxy

【执行 SQL】 通过 SqlSession 执行对应 MappedStatement

1.@MapperScan 启动时扫描 Mapper 接口

@MapperScan 会注册 MapperScannerConfigurerMapperScannerRegistrar

它们会扫描包下所有接口,对每个接口注册一个 MapperFactoryBean 到 Spring 容器中

2.每个 Mapper 接口注册成一个 MapperFactoryBean

MapperFactoryBean 实现了 Spring 的 FactoryBean 接口;

它不会直接注入自身,而是调用 getObject() 返回 Mapper 接口的代理类。

3.MapperProxyFactory 创建代理类

返回的是一个 MapperProxy 的动态代理对象,底层通过 Proxy.newProxyInstance(...) 实现;

所以你注入的 UserMapper 实际是 MapperProxy 代理类。

4.代理对象调用方法时执行 SQL

MapperMethod 会查找 Configuration 中注册的 MappedStatement

调用 sqlSession.selectOne(...)insert(...) 等执行 SQL;

SQL 语句和参数映射都来源于 XML 或注解注册的 MappedStatement。

5.在 SqlSessionFactoryBuilder.build() 构建过程中,MyBatis 会:

  • 加载 XML 映射文件或注解 Mapper;
  • 把每个 <mapper> 里的 SQL 语句解析为 MappedStatement
  • 调用 Configuration.addMappedStatement() 注册到全局配置里;
  • 最终保存在 configuration.getMappedStatements() 的 Map 中。