# Java后端核心技术面试指南
> 🎯 涵盖:Spring、Spring Boot、MyBatis、MVC、MySQL、Redis
> 特点:企业级实战 + 源码原理 + 业务场景举例
---
## 📚 目录
1. [Spring核心面试题](#一spring核心面试题)
2. [Spring Boot面试题](#二spring-boot面试题)
3. [MyBatis面试题](#三mybatis面试题)
4. [Spring MVC面试题](#四spring-mvc面试题)
5. [MySQL面试题](#五mysql面试题)
6. [Redis面试题](#六redis面试题)
---
# 一、Spring核心面试题
## 1.1 IOC容器相关
### Q1:什么是IOC?什么是DI?
**答**:
- **IOC(Inversion of Control)控制反转**:把对象的创建和依赖关系的管理交给Spring容器,而不是程序员手动new。
- **DI(Dependency Injection)依赖注入**:IOC的具体实现方式,通过构造器、setter、字段注入依赖。
**业务举例**:
```java
// ❌ 传统方式:手动创建依赖
public class OrderService {
private OrderMapper orderMapper = new OrderMapper(); // 强耦合
private StockService stockService = new StockService();
}
// ✅ IOC方式:Spring管理依赖
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper; // Spring注入
@Autowired
private StockService stockService;
}
// 好处:
// 1. 解耦:更换实现只需改配置
// 2. 测试:方便Mock依赖
// 3. 管理:统一管理Bean生命周期
```
---
### Q2:Spring Bean的生命周期?
**答**:
```
1. 实例化(Instantiation)
└─ 通过反射创建Bean实例
2. 属性填充(Populate Properties)
└─ 注入依赖(@Autowired等)
3. Aware接口回调
├─ BeanNameAware.setBeanName()
├─ BeanFactoryAware.setBeanFactory()
└─ ApplicationContextAware.setApplicationContext()
4. BeanPostProcessor.postProcessBeforeInitialization()
└─ 初始化前置处理
5. 初始化
├─ @PostConstruct 注解方法
├─ InitializingBean.afterPropertiesSet()
└─ init-method 指定的方法
6. BeanPostProcessor.postProcessAfterInitialization()
└─ 初始化后置处理(AOP代理在这里创建)
7. 使用Bean
8. 销毁
├─ @PreDestroy 注解方法
├─ DisposableBean.destroy()
└─ destroy-method 指定的方法
```
**业务举例**:
```java
@Service
public class PaymentService implements InitializingBean, DisposableBean {
@Autowired
private WxPayConfig wxPayConfig;
private WxPayClient wxPayClient;
// 初始化:创建微信支付客户端
@Override
public void afterPropertiesSet() {
this.wxPayClient = new WxPayClient(wxPayConfig);
log.info("微信支付客户端初始化完成");
}
// 销毁:释放资源
@Override
public void destroy() {
if (wxPayClient != null) {
wxPayClient.close();
log.info("微信支付客户端已关闭");
}
}
}
```
---
### Q3:Spring Bean的作用域有哪些?
**答**:
| 作用域 | 描述 | 使用场景 |
|--------|------|---------|
| **singleton** | 默认,单例,整个容器只有一个实例 | 无状态的Service、DAO |
| **prototype** | 每次获取都创建新实例 | 有状态的Bean |
| **request** | 每个HTTP请求一个实例 | Web应用 |
| **session** | 每个HTTP Session一个实例 | 用户会话相关 |
| **application** | 整个ServletContext一个实例 | 跨用户共享 |
**业务举例**:
```java
// 单例(默认):无状态,线程安全
@Service
public class OrderService {
// 所有请求共享同一个实例
}
// 原型:有状态,每次创建新实例
@Component
@Scope("prototype")
public class ShoppingCart {
private List<CartItem> items = new ArrayList<>();
// 每个用户需要独立的购物车
}
// Request作用域:每个请求一个
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext {
private Long userId;
private String requestId;
// 存储当前请求的上下文信息
}
```
---
### Q4:@Autowired和@Resource的区别?
**答**:
| 对比项 | @Autowired | @Resource |
|--------|-----------|-----------|
| 来源 | Spring注解 | JSR-250规范(Java标准) |
| 注入方式 | 默认按类型(byType) | 默认按名称(byName) |
| required | 支持required=false | 不支持 |
| 指定名称 | 配合@Qualifier | name属性 |
```java
// @Autowired:按类型注入
@Autowired
private OrderService orderService;
// @Autowired + @Qualifier:按名称注入
@Autowired
@Qualifier("wxPayService")
private PayService payService;
// @Resource:按名称注入
@Resource(name = "aliPayService")
private PayService payService;
```
**推荐**:构造器注入(更安全,便于测试)
```java
@Service
@RequiredArgsConstructor // Lombok自动生成构造器
public class OrderService {
private final OrderMapper orderMapper; // final字段必须注入
private final StockService stockService;
}
```
---
### Q5:Spring循环依赖是怎么解决的?
**答**:
Spring通过**三级缓存**解决单例Bean的循环依赖:
```java
// 三级缓存
singletonObjects // 一级:完整的Bean实例
earlySingletonObjects // 二级:提前暴露的Bean(未完成属性填充)
singletonFactories // 三级:ObjectFactory,用于创建早期引用
// 解决流程(A依赖B,B依赖A)
1. 创建A → 实例化A → 放入三级缓存(ObjectFactory)
2. A填充属性 → 发现依赖B → 去创建B
3. 创建B → 实例化B → 放入三级缓存
4. B填充属性 → 发现依赖A → 从三级缓存获取A的早期引用
5. B完成创建 → 放入一级缓存
6. A拿到B → A完成创建 → 放入一级缓存
```
**无法解决的场景**:
```java
// ❌ 构造器注入的循环依赖无法解决
@Service
public class A {
public A(B b) { } // 构造器依赖B
}
@Service
public class B {
public B(A a) { } // 构造器依赖A
}
// 解决方案:
// 1. 改用@Autowired字段注入
// 2. 使用@Lazy延迟加载
@Service
public class A {
public A(@Lazy B b) { } // 延迟加载B
}
```
---
## 1.2 AOP相关
### Q6:什么是AOP?有哪些应用场景?
**答**:
**AOP(Aspect Oriented Programming)面向切面编程**:将横切关注点(日志、事务、权限等)从业务逻辑中分离出来。
**核心概念**:
- **切面(Aspect)**:横切关注点的模块化
- **切点(Pointcut)**:定义在哪些方法执行切面
- **通知(Advice)**:切面的具体逻辑
- **连接点(JoinPoint)**:可以被切入的点
**企业级应用场景**:
```java
// 场景1:统一日志记录
@Aspect
@Component
@Slf4j
public class ApiLogAspect {
@Around("@annotation(apiLog)")
public Object logApi(ProceedingJoinPoint pjp, ApiLog apiLog) throws Throwable {
long startTime = System.currentTimeMillis();
String methodName = pjp.getSignature().getName();
log.info("API调用开始: {}, 参数: {}", methodName, Arrays.toString(pjp.getArgs()));
try {
Object result = pjp.proceed();
long costTime = System.currentTimeMillis() - startTime;
log.info("API调用成功: {}, 耗时: {}ms", methodName, costTime);
return result;
} catch (Exception e) {
log.error("API调用异常: {}, 错误: {}", methodName, e.getMessage());
throw e;
}
}
}
// 场景2:权限校验
@Aspect
@Component
public class PermissionAspect {
@Before("@annotation(requirePermission)")
public void checkPermission(JoinPoint jp, RequirePermission requirePermission) {
String permission = requirePermission.value();
Long userId = SecurityUtils.getCurrentUserId();
if (!permissionService.hasPermission(userId, permission)) {
throw new AccessDeniedException("无权限: " + permission);
}
}
}
// 场景3:分布式锁
@Aspect
@Component
public class DistributedLockAspect {
@Around("@annotation(distributedLock)")
public Object lock(ProceedingJoinPoint pjp, DistributedLock distributedLock) throws Throwable {
String lockKey = parseLockKey(distributedLock.key(), pjp);
RLock lock = redissonClient.getLock(lockKey);
try {
if (!lock.tryLock(distributedLock.waitTime(), distributedLock.leaseTime(), TimeUnit.SECONDS)) {
throw new BusinessException("操作太频繁,请稍后再试");
}
return pjp.proceed();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
```
---
### Q7:Spring AOP和AspectJ的区别?
**答**:
| 对比项 | Spring AOP | AspectJ |
|--------|-----------|---------|
| 实现方式 | 动态代理(运行时) | 字节码织入(编译时/加载时) |
| 性能 | 略低(代理调用) | 更高(直接调用) |
| 功能 | 仅支持方法级别 | 支持字段、构造器等 |
| 使用复杂度 | 简单 | 需要特殊编译器 |
| 适用场景 | 大多数场景 | 性能要求极高的场景 |
**Spring AOP代理方式**:
```java
// 1. JDK动态代理:目标类实现了接口
// 代理类:$Proxy0
public interface UserService { }
@Service
public class UserServiceImpl implements UserService { }
// 2. CGLIB代理:目标类没有实现接口
// 代理类:UserService$$EnhancerBySpringCGLIB$$xxx
@Service
public class UserService { }
```
---
# 二、Spring Boot面试题
### Q8:Spring Boot的核心注解@SpringBootApplication包含什么?
**答**:
```java
@SpringBootApplication
= @SpringBootConfiguration // 标识为配置类
+ @EnableAutoConfiguration // 开启自动配置
+ @ComponentScan // 组件扫描
// 详细说明:
@SpringBootConfiguration
└─ @Configuration // 配置类
@EnableAutoConfiguration
├─ @AutoConfigurationPackage // 自动配置包
└─ @Import(AutoConfigurationImportSelector.class) // 导入自动配置类
└─ 从META-INF/spring.factories加载自动配置类
@ComponentScan
└─ 默认扫描主类所在包及子包
```
---
### Q9:Spring Boot自动配置原理?
**答**:
```
启动流程:
1. @EnableAutoConfiguration 触发
│
2. AutoConfigurationImportSelector 加载
│
3. 读取 META-INF/spring.factories 文件
│
4. 获取所有 EnableAutoConfiguration 对应的配置类
│
5. 根据 @Conditional 条件过滤
│
6. 将满足条件的配置类加载到容器
条件注解:
@ConditionalOnClass // 类存在时生效
@ConditionalOnMissingBean // Bean不存在时生效
@ConditionalOnProperty // 配置属性满足时生效
```
**业务举例:自定义Starter**
```java
// 1. 创建自动配置类
@Configuration
@ConditionalOnClass(WxPayClient.class)
@EnableConfigurationProperties(WxPayProperties.class)
public class WxPayAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public WxPayClient wxPayClient(WxPayProperties properties) {
return new WxPayClient(properties);
}
}
// 2. 创建配置属性类
@ConfigurationProperties(prefix = "wx.pay")
@Data
public class WxPayProperties {
private String appId;
private String mchId;
private String apiKey;
}
// 3. 在META-INF/spring.factories中配置
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.WxPayAutoConfiguration
```
---
### Q10:Spring Boot配置文件加载顺序?
**答**:
```
优先级从高到低:
1. 命令行参数 (--server.port=8080)
2. SPRING_APPLICATION_JSON 环境变量
3. ServletConfig/ServletContext 初始化参数
4. JNDI属性
5. Java系统属性 (System.getProperties())
6. 操作系统环境变量
7. RandomValuePropertySource
8. jar包外的 application-{profile}.yml
9. jar包内的 application-{profile}.yml
10. jar包外的 application.yml
11. jar包内的 application.yml
12. @PropertySource 注解
13. 默认属性 (SpringApplication.setDefaultProperties)
```
**多环境配置**:
```yaml
# application.yml(主配置)
spring:
profiles:
active: dev # 激活dev环境
---
# application-dev.yml
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/dev_db
---
# application-prod.yml
server:
port: 80
spring:
datasource:
url: jdbc:mysql://prod-server:3306/prod_db
```
---
### Q11:Spring Boot如何实现热部署?
**答**:
**1. spring-boot-devtools(开发环境)**
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
```
**2. JRebel(商业工具,生产级)**
**原理**:
- devtools:双ClassLoader机制,重启时只加载变化的类
- 触发方式:IDEA设置Build Project自动编译
---
# 三、MyBatis面试题
### Q12:MyBatis的#{}和${}区别?
**答**:
| 对比项 | #{} | ${} |
|--------|-----|-----|
| 处理方式 | 预编译(PreparedStatement) | 字符串拼接 |
| SQL注入 | ✅ 防止 | ❌ 有风险 |
| 类型处理 | 自动类型转换 | 原样拼接 |
| 使用场景 | 参数值 | 表名、列名、排序 |
```xml
<!-- #{}:参数值,安全 -->
<select id="selectUser" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>
<!-- 生成:SELECT * FROM user WHERE id = ? -->
<!-- ${}:字符串拼接,用于动态表名/列名 -->
<select id="selectByTable" resultType="User">
SELECT * FROM ${tableName} WHERE id = #{id}
</select>
<!-- 生成:SELECT * FROM user_2024 WHERE id = ? -->
<!-- ⚠️ 排序必须用${} -->
<select id="selectWithOrder" resultType="User">
SELECT * FROM user ORDER BY ${orderColumn} ${orderType}
</select>
```
**业务举例**:分表查询
```java
// 按月份分表
public List<Order> getOrders(String month, Long userId) {
String tableName = "order_" + month; // order_202601
return orderMapper.selectByTable(tableName, userId);
}
```
---
### Q13:MyBatis一级缓存和二级缓存?
**答**:
| 对比项 | 一级缓存 | 二级缓存 |
|--------|---------|---------|
| 作用域 | SqlSession级别 | Mapper级别(跨SqlSession) |
| 默认状态 | 开启 | 关闭 |
| 存储结构 | HashMap | 可配置(默认HashMap) |
| 失效条件 | 增删改、手动清除、Session关闭 | 增删改、手动清除 |
```java
// 一级缓存示例
SqlSession session = sqlSessionFactory.openSession();
UserMapper mapper = session.getMapper(UserMapper.class);
User user1 = mapper.selectById(1); // 查数据库
User user2 = mapper.selectById(1); // 走一级缓存,不查数据库
System.out.println(user1 == user2); // true,同一个对象
session.close(); // 关闭Session,缓存清空
```
```xml
<!-- 二级缓存配置 -->
<mapper namespace="com.example.mapper.UserMapper">
<!-- 开启二级缓存 -->
<cache
eviction="LRU"
flushInterval="60000"
size="1024"
readOnly="true"/>
</mapper>
```
**生产环境建议**:
```
1. 一级缓存:保持默认
2. 二级缓存:通常关闭,使用Redis替代
原因:
- 分布式环境下缓存不一致
- 缓存粒度粗(整个Mapper)
- 无法设置过期时间
```
---
### Q14:MyBatis如何实现分页?
**答**:
**方式1:物理分页(推荐)**
```xml
<!-- MySQL -->
<select id="selectPage" resultType="User">
SELECT * FROM user
LIMIT #{offset}, #{pageSize}
</select>
```
**方式2:PageHelper插件(最常用)**
```java
@Service
public class UserService {
public PageInfo<User> getUserPage(int pageNum, int pageSize) {
// 设置分页参数(必须紧跟查询语句)
PageHelper.startPage(pageNum, pageSize);
// 执行查询
List<User> users = userMapper.selectAll();
// 封装分页信息
return new PageInfo<>(users);
}
}
```
**方式3:MyBatis-Plus分页**
```java
@Service
public class UserService {
public IPage<User> getUserPage(int pageNum, int pageSize) {
Page<User> page = new Page<>(pageNum, pageSize);
return userMapper.selectPage(page,
new QueryWrapper<User>().eq("status", 1));
}
}
```
---
### Q15:MyBatis如何执行批量操作?
**答**:
**方式1:foreach标签**
```xml
<!-- 批量插入 -->
<insert id="batchInsert">
INSERT INTO user (name, age) VALUES
<foreach collection="list" item="user" separator=",">
(#{user.name}, #{user.age})
</foreach>
</insert>
<!-- 批量更新 -->
<update id="batchUpdate">
<foreach collection="list" item="user" separator=";">
UPDATE user SET name = #{user.name} WHERE id = #{user.id}
</foreach>
</update>
```
**方式2:BATCH模式(性能更好)**
```java
@Service
public class UserService {
@Autowired
private SqlSessionFactory sqlSessionFactory;
public void batchInsert(List<User> users) {
try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
UserMapper mapper = session.getMapper(UserMapper.class);
for (int i = 0; i < users.size(); i++) {
mapper.insert(users.get(i));
// 每1000条提交一次,防止内存溢出
if (i % 1000 == 0) {
session.flushStatements();
}
}
session.commit();
}
}
}
```
**业务举例**:订单批量导入
```java
// 每批1000条,分批处理
public void importOrders(List<Order> orders) {
List<List<Order>> batches = Lists.partition(orders, 1000);
for (List<Order> batch : batches) {
orderMapper.batchInsert(batch);
}
}
```
---
# 四、Spring MVC面试题
### Q16:Spring MVC的执行流程?
**答**:
```
用户请求
│
▼
1. DispatcherServlet(前端控制器)
│ 接收请求,协调各组件
▼
2. HandlerMapping(处理器映射器)
│ 根据URL找到对应的Handler(Controller方法)
│ 返回HandlerExecutionChain(Handler + 拦截器)
▼
3. HandlerAdapter(处理器适配器)
│ 适配不同类型的Handler
│ 调用Controller方法
▼
4. Controller(处理器)
│ 执行业务逻辑
│ 返回ModelAndView
▼
5. ViewResolver(视图解析器)
│ 解析逻辑视图名为物理视图
▼
6. View(视图)
│ 渲染视图,返回响应
▼
响应用户
```
---
### Q17:@Controller和@RestController区别?
**答**:
```java
@Controller
= @Component + 返回视图名
@RestController
= @Controller + @ResponseBody(返回JSON)
// @Controller:返回视图
@Controller
public class PageController {
@GetMapping("/index")
public String index(Model model) {
model.addAttribute("name", "张三");
return "index"; // 返回视图名,解析为index.html
}
}
// @RestController:返回JSON
@RestController
public class ApiController {
@GetMapping("/user")
public User getUser() {
return userService.getById(1); // 自动转JSON
}
}
```
---
### Q18:Spring MVC如何处理异常?
**答**:
**方式1:@ExceptionHandler(局部)**
```java
@Controller
public class UserController {
@ExceptionHandler(BusinessException.class)
@ResponseBody
public Result handleBusinessException(BusinessException e) {
return Result.error(e.getCode(), e.getMessage());
}
}
```
**方式2:@ControllerAdvice(全局,推荐)**
```java
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// 业务异常
@ExceptionHandler(BusinessException.class)
public Result handleBusinessException(BusinessException e) {
log.warn("业务异常: {}", e.getMessage());
return Result.error(e.getCode(), e.getMessage());
}
// 参数校验异常
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result handleValidException(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining(", "));
return Result.error(400, message);
}
// 系统异常
@ExceptionHandler(Exception.class)
public Result handleException(Exception e) {
log.error("系统异常", e);
return Result.error(500, "系统繁忙,请稍后再试");
}
}
```
---
### Q19:Spring MVC拦截器和过滤器区别?
**答**:
| 对比项 | Filter(过滤器) | Interceptor(拦截器) |
|--------|----------------|---------------------|
| 规范 | Servlet规范 | Spring规范 |
| 作用范围 | 所有请求(包括静态资源) | 只拦截Controller |
| 依赖 | 不依赖Spring | 依赖Spring容器 |
| 注入Bean | 不方便 | 可以直接注入 |
| 执行顺序 | 先执行 | 后执行 |
**业务举例**:登录拦截器
```java
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
// 1. 获取Token
String token = request.getHeader("Authorization");
if (StringUtils.isBlank(token)) {
response.setStatus(401);
return false;
}
// 2. 验证Token
String userId = redisTemplate.opsForValue().get("token:" + token);
if (userId == null) {
response.setStatus(401);
return false;
}
// 3. 存入ThreadLocal
UserContext.setUserId(Long.valueOf(userId));
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
// 清理ThreadLocal,防止内存泄漏
UserContext.clear();
}
}
// 配置拦截器
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/login", "/api/register");
}
}
```
---
# 五、MySQL面试题
### Q20:MySQL索引类型有哪些?
**答**:
| 索引类型 | 说明 | 使用场景 |
|---------|------|---------|
| **主键索引** | 唯一且不为空 | 主键 |
| **唯一索引** | 值唯一 | 邮箱、手机号 |
| **普通索引** | 无限制 | 常用查询字段 |
| **组合索引** | 多个字段组合 | 多条件查询 |
| **全文索引** | 文本搜索 | 文章内容搜索 |
**业务举例**:订单表索引设计
```sql
CREATE TABLE `order` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`order_no` VARCHAR(32) NOT NULL,
`user_id` BIGINT NOT NULL,
`status` TINYINT NOT NULL,
`create_time` DATETIME NOT NULL,
`pay_time` DATETIME,
-- 唯一索引:订单号
UNIQUE KEY `uk_order_no` (`order_no`),
-- 组合索引:用户+状态+时间(符合最左前缀)
KEY `idx_user_status_time` (`user_id`, `status`, `create_time`),
-- 单列索引:支付时间
KEY `idx_pay_time` (`pay_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 常见查询都能走索引:
-- 1. SELECT * FROM order WHERE user_id = 1; ✅
-- 2. SELECT * FROM order WHERE user_id = 1 AND status = 1; ✅
-- 3. SELECT * FROM order WHERE user_id = 1 AND status = 1 AND create_time > '2024-01-01'; ✅
-- 4. SELECT * FROM order WHERE status = 1; ❌ 不走索引(不符合最左前缀)
```
---
### Q21:MySQL索引失效的场景?
**答**:
```sql
-- 1. 对索引列使用函数或运算
SELECT * FROM user WHERE YEAR(create_time) = 2024; ❌
SELECT * FROM user WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01'; ✅
-- 2. 隐式类型转换
SELECT * FROM user WHERE phone = 13800138000; ❌ phone是varchar
SELECT * FROM user WHERE phone = '13800138000'; ✅
-- 3. LIKE以%开头
SELECT * FROM user WHERE name LIKE '%张'; ❌
SELECT * FROM user WHERE name LIKE '张%'; ✅
-- 4. OR条件有非索引列
SELECT * FROM user WHERE id = 1 OR remark = 'xxx'; ❌ remark无索引
SELECT * FROM user WHERE id = 1 OR name = '张三'; ✅ 都有索引
-- 5. 组合索引不符合最左前缀
-- 索引:(a, b, c)
SELECT * FROM t WHERE b = 1; ❌
SELECT * FROM t WHERE a = 1 AND c = 1; ❌ 只用到a
SELECT * FROM t WHERE a = 1 AND b = 1; ✅
-- 6. NOT IN、NOT EXISTS、!=、<>
SELECT * FROM user WHERE status NOT IN (0, 1); ❌
SELECT * FROM user WHERE status IN (2, 3); ✅
-- 7. IS NULL / IS NOT NULL(取决于数据分布)
SELECT * FROM user WHERE name IS NULL; -- 可能不走索引
```
---
### Q22:如何优化慢SQL?
**答**:
**1. 使用EXPLAIN分析**
```sql
EXPLAIN SELECT * FROM order WHERE user_id = 1 AND status = 1;
-- 关注字段:
-- type: 访问类型(ALL < index < range < ref < eq_ref < const)
-- key: 实际使用的索引
-- rows: 扫描行数
-- Extra: 额外信息(Using filesort、Using temporary需优化)
```
**2. 优化策略**
```sql
-- 问题1:全表扫描
SELECT * FROM order WHERE DATE(create_time) = '2024-01-01';
-- 优化:
SELECT * FROM order
WHERE create_time >= '2024-01-01' AND create_time < '2024-01-02';
-- 问题2:SELECT *
SELECT * FROM order WHERE id = 1;
-- 优化:只查需要的字段
SELECT id, order_no, status FROM order WHERE id = 1;
-- 问题3:大OFFSET分页
SELECT * FROM order LIMIT 100000, 10;
-- 优化:延迟关联
SELECT o.* FROM order o
INNER JOIN (SELECT id FROM order LIMIT 100000, 10) t ON o.id = t.id;
-- 问题4:IN包含太多值
SELECT * FROM user WHERE id IN (1, 2, 3, ..., 10000);
-- 优化:分批查询
```
**业务举例**:订单列表优化
```sql
-- 原SQL(慢)
SELECT * FROM order
WHERE user_id = 1 AND status IN (1, 2)
ORDER BY create_time DESC
LIMIT 100000, 20;
-- 优化后
-- 1. 添加组合索引
ALTER TABLE order ADD INDEX idx_user_status_time(user_id, status, create_time);
-- 2. 使用游标分页(推荐)
SELECT * FROM order
WHERE user_id = 1 AND status IN (1, 2)
AND id < #{lastId} -- 上一页最后一条的ID
ORDER BY id DESC
LIMIT 20;
```
---
### Q23:MySQL事务隔离级别?
**答**:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---------|------|-----------|------|
| READ UNCOMMITTED | ✅ | ✅ | ✅ |
| READ COMMITTED | ❌ | ✅ | ✅ |
| REPEATABLE READ(默认) | ❌ | ❌ | ✅(MVCC解决) |
| SERIALIZABLE | ❌ | ❌ | ❌ |
```sql
-- 查看当前隔离级别
SELECT @@transaction_isolation;
-- 设置隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
```
**业务举例**:库存扣减防超卖
```java
// 方案1:乐观锁
UPDATE product_stock
SET quantity = quantity - 1, version = version + 1
WHERE product_id = 1 AND quantity >= 1 AND version = #{version};
// 方案2:悲观锁
@Transactional
public void deductStock(Long productId) {
// SELECT FOR UPDATE 加行锁
Stock stock = stockMapper.selectForUpdate(productId);
if (stock.getQuantity() < 1) {
throw new BusinessException("库存不足");
}
stockMapper.deduct(productId, 1);
}
```
---
### Q24:MySQL主从复制原理?
**答**:
```
Master Slave
│ │
│ 1. 写入binlog │
│────────────────────────────────> │
│ (IO Thread拉取binlog) │
│ │
│ 2. 写入relay log
│ │
│ 3. SQL Thread重放
│ │
```
**读写分离配置**(ShardingSphere):
```yaml
spring:
shardingsphere:
datasource:
names: master,slave
master:
url: jdbc:mysql://master:3306/db
slave:
url: jdbc:mysql://slave:3306/db
rules:
readwrite-splitting:
data-sources:
ds:
write-data-source-name: master
read-data-source-names: slave
load-balancer-name: round_robin
```
---
# 六、Redis面试题
### Q25:Redis的数据类型及使用场景?
**答**:
| 类型 | 使用场景 | 命令示例 |
|------|---------|---------|
| **String** | 缓存、计数器、分布式锁 | SET、GET、INCR |
| **Hash** | 对象存储、购物车 | HSET、HGET、HINCRBY |
| **List** | 消息队列、最新列表 | LPUSH、RPOP、LRANGE |
| **Set** | 去重、共同好友 | SADD、SMEMBERS、SINTER |
| **ZSet** | 排行榜、延迟队列 | ZADD、ZRANGE、ZRANGEBYSCORE |
**业务举例**:
```java
// 1. String:库存计数
redisTemplate.opsForValue().set("stock:1001", "100");
redisTemplate.opsForValue().decrement("stock:1001");
// 2. Hash:购物车
redisTemplate.opsForHash().put("cart:user:123", "product:1001", "2");
redisTemplate.opsForHash().increment("cart:user:123", "product:1001", 1);
// 3. List:最新消息
redisTemplate.opsForList().leftPush("message:user:123", message);
List<String> messages = redisTemplate.opsForList().range("message:user:123", 0, 9);
// 4. Set:点赞用户
redisTemplate.opsForSet().add("like:post:1001", "user:123");
Long count = redisTemplate.opsForSet().size("like:post:1001");
// 5. ZSet:排行榜
redisTemplate.opsForZSet().add("rank:sales", "product:1001", 1000);
Set<String> top10 = redisTemplate.opsForZSet().reverseRange("rank:sales", 0, 9);
```
---
### Q26:Redis缓存穿透、击穿、雪崩?
**答**:
| 问题 | 描述 | 解决方案 |
|------|------|---------|
| **穿透** | 查询不存在的数据,缓存不命中 | 布隆过滤器、空值缓存 |
| **击穿** | 热点Key过期,大量请求打到DB | 互斥锁、永不过期+异步更新 |
| **雪崩** | 大量Key同时过期 | 随机过期时间、多级缓存 |
```java
// 缓存穿透:空值缓存
public Product getProduct(Long productId) {
String key = "product:" + productId;
String json = redisTemplate.opsForValue().get(key);
if (json != null) {
if ("NULL".equals(json)) {
return null; // 空值缓存
}
return JSON.parseObject(json, Product.class);
}
Product product = productMapper.selectById(productId);
if (product != null) {
redisTemplate.opsForValue().set(key, JSON.toJSONString(product), 1, TimeUnit.HOURS);
} else {
// 缓存空值,防止穿透
redisTemplate.opsForValue().set(key, "NULL", 5, TimeUnit.MINUTES);
}
return product;
}
// 缓存击穿:互斥锁
public Product getProductWithLock(Long productId) {
String key = "product:" + productId;
String json = redisTemplate.opsForValue().get(key);
if (json != null) {
return JSON.parseObject(json, Product.class);
}
// 获取分布式锁
String lockKey = "lock:product:" + productId;
RLock lock = redissonClient.getLock(lockKey);
try {
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
// 双重检查
json = redisTemplate.opsForValue().get(key);
if (json != null) {
return JSON.parseObject(json, Product.class);
}
// 查数据库
Product product = productMapper.selectById(productId);
redisTemplate.opsForValue().set(key, JSON.toJSONString(product), 1, TimeUnit.HOURS);
return product;
}
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
return null;
}
// 缓存雪崩:随机过期时间
public void cacheProducts(List<Product> products) {
for (Product product : products) {
String key = "product:" + product.getId();
// 基础过期时间 + 随机时间,避免同时过期
int expire = 3600 + new Random().nextInt(600);
redisTemplate.opsForValue().set(key, JSON.toJSONString(product), expire, TimeUnit.SECONDS);
}
}
```
---
### Q27:Redis持久化机制?
**答**:
| 对比项 | RDB | AOF |
|--------|-----|-----|
| 方式 | 快照 | 追加日志 |
| 触发 | 手动/自动 | 每秒/每命令 |
| 恢复速度 | 快 | 慢 |
| 数据安全 | 可能丢失 | 最多丢1秒 |
| 文件大小 | 小 | 大 |
```bash
# RDB配置
save 900 1 # 900秒内有1个key变化则保存
save 300 10 # 300秒内有10个key变化则保存
save 60 10000 # 60秒内有10000个key变化则保存
# AOF配置
appendonly yes
appendfsync everysec # 每秒同步
```
**生产建议**:RDB + AOF 混合使用
---
### Q28:Redis集群方案?
**答**:
| 方案 | 说明 | 适用场景 |
|------|------|---------|
| **主从复制** | 一主多从,读写分离 | 读多写少 |
| **哨兵模式** | 主从+自动故障转移 | 高可用 |
| **Cluster** | 分片集群,16384个槽 | 大数据量 |
```yaml
# Spring Boot配置Redis Cluster
spring:
redis:
cluster:
nodes:
- 192.168.1.1:6379
- 192.168.1.2:6379
- 192.168.1.3:6379
max-redirects: 3
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
```
---
### Q29:如何保证缓存和数据库一致性?
**答**:
**方案对比**:
| 方案 | 一致性 | 复杂度 | 适用场景 |
|------|--------|--------|---------|
| Cache Aside | 最终一致 | 低 | 大多数场景 |
| 延迟双删 | 最终一致 | 中 | 主从延迟场景 |
| 监听binlog | 最终一致 | 高 | 强一致要求 |
```java
// Cache Aside模式(推荐)
@Service
public class ProductService {
// 读:先缓存,后数据库
public Product getProduct(Long id) {
String key = "product:" + id;
Product product = cache.get(key);
if (product == null) {
product = productMapper.selectById(id);
if (product != null) {
cache.set(key, product, 1, TimeUnit.HOURS);
}
}
return product;
}
// 写:先更新数据库,后删除缓存
@Transactional
public void updateProduct(Product product) {
// 1. 更新数据库
productMapper.updateById(product);
// 2. 删除缓存
cache.delete("product:" + product.getId());
}
}
// 延迟双删(解决主从延迟)
@Transactional
public void updateProductWithDelayDelete(Product product) {
String key = "product:" + product.getId();
// 1. 删除缓存
cache.delete(key);
// 2. 更新数据库
productMapper.updateById(product);
// 3. 延迟再次删除(异步)
executor.schedule(() -> cache.delete(key), 500, TimeUnit.MILLISECONDS);
}
```
---
### Q30:Redis分布式锁如何实现?
**答**:
**Redisson实现(推荐)**:
```java
@Service
public class DistributedLockService {
@Autowired
private RedissonClient redissonClient;
/**
* 可重入锁
*/
public void doWithLock(String lockKey, Runnable task) {
RLock lock = redissonClient.getLock(lockKey);
try {
// 等待3秒,持有锁10秒
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
task.run();
} else {
throw new BusinessException("获取锁失败");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
/**
* 红锁(多节点)
*/
public void doWithRedLock(String lockKey, Runnable task) {
RLock lock1 = redissonClient1.getLock(lockKey);
RLock lock2 = redissonClient2.getLock(lockKey);
RLock lock3 = redissonClient3.getLock(lockKey);
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
try {
if (redLock.tryLock(3, 10, TimeUnit.SECONDS)) {
task.run();
}
} finally {
redLock.unlock();
}
}
}
```
**原生Redis实现**:
```java
// SET NX EX 原子操作
public boolean tryLock(String key, String value, long expireSeconds) {
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(key, value, expireSeconds, TimeUnit.SECONDS);
return Boolean.TRUE.equals(result);
}
// 释放锁(Lua脚本保证原子性)
public boolean releaseLock(String key, String value) {
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
Long result = redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(key),
value
);
return Long.valueOf(1).equals(result);
}
```
---
## 📝 面试速记卡
```
【Spring】
IOC = 控制反转,对象创建交给容器
DI = 依赖注入,IOC的实现方式
AOP = 面向切面,横切关注点模块化
Bean生命周期 = 实例化 → 属性填充 → Aware → 前置 → 初始化 → 后置 → 使用 → 销毁
循环依赖 = 三级缓存解决(构造器注入除外)
【Spring Boot】
@SpringBootApplication = @Configuration + @EnableAutoConfiguration + @ComponentScan
自动配置 = spring.factories + @Conditional条件装配
【MyBatis】
#{} = 预编译,防SQL注入
${} = 字符串拼接,用于表名列名
一级缓存 = SqlSession级别,默认开启
二级缓存 = Mapper级别,生产一般关闭用Redis
【MySQL】
索引失效 = 函数运算、隐式转换、LIKE%开头、OR非索引、不符合最左前缀
优化思路 = EXPLAIN分析 → 加索引 → 避免SELECT * → 优化分页
【Redis】
数据类型 = String/Hash/List/Set/ZSet
三大问题 = 穿透(空值缓存)、击穿(互斥锁)、雪崩(随机过期)
持久化 = RDB快照 + AOF日志
一致性 = Cache Aside(先更新DB,后删缓存)
```
---
**文档版本**:v1.0
**创建时间**:2026-01-19
**适用场景**:Java后端面试