Administrator
发布于 2026-03-16 / 3 阅读
0
0

Java后端核心技术面试指南

# 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后端面试


评论