spring 篇

spring IOC (控制反转)

📖 定义:
IoC(Inversion of Control,控制反转)是一种 设计思想,指的是对象的创建与依赖关系的管理不再由程序员控制,而是交由容器(如 Spring)来管理。

  • 🔧 二、传统写法 vs IoC 思想
    👎 传统写法(耦合度高):
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class UserDao {
    public void save() {
    System.out.println("Saving user");
    }
    }

    class UserService {
    private UserDao userDao = new UserDao(); // 手动创建依赖

    public void doBusiness() {
    userDao.save();
    }
    }
    👍 使用 IoC:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @Component
    class UserDao {
    public void save() {
    System.out.println("Saving user");
    }
    }

    @Service
    class UserService {
    @Autowired
    private UserDao userDao; // 自动注入

    public void doBusiness() {
    userDao.save();
    }
    }

⚙️ 五、Spring IoC 容器的底层流程(重要)

1
2
3
4
5
6
1. 扫描(@ComponentScan)
2. 注册(@Component、@Service 等注解)
3. 创建 Bean(反射)
4. 依赖注入(@Autowired)
5. 初始化(@PostConstruct)
6. 容器准备好,开发者调用 getBean() 获取 Bean 使用

🔄 更准确的说法:
IoC 容器启动时,会:

  • 扫描或加载所有配置的类(如 @Component、XML)。
  • 将这些类信息 注册为 BeanDefinition。
  • 当程序需要某个对象(比如 UserService)时,Spring 会:
  • 检查它的依赖(比如 UserRepository)。
  • 自动创建依赖(通过反射)并注入(DI)进来。
  • 这整个流程,是 IoC 的体现:我们不再手动 new 依赖,而是让容器帮我们创建并注入。

spring事务的传播机制

Spring 提供了 7 种传播行为,定义在 org.springframework.transaction.annotation.Propagation 枚举中。

传播行为描述
REQUIRED(默认)有事务则加入,没有则新建事务。常用。
REQUIRES_NEW挂起当前事务新建事务,互不影响。
NESTED嵌套事务,内层异常只回滚自身,不影响外层。
SUPPORTS有事务就加入,没事务就以非事务方式运行。
NOT_SUPPORTED不支持事务,有事务则挂起,用非事务方式运行。
NEVER不允许有事务,否则抛异常。
MANDATORY必须在事务中运行,没有事务就抛异常。

1️⃣ REQUIRED(默认)
A 有事务,B 就加入;
A 无事务,B 就新建事务。

1
2
3
4
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
methodB(); // 会加入 methodA 的事务
}

事务失效的场景

  • 1️⃣ 方法不是 public 的
    1
    2
    @Transactional
    private void doSomething() { ... } // ❌ 不生效
    原因:Spring AOP 只能拦截 public 方法。非 public 方法不会被代理,自然事务不生效。
  • 2️⃣ 方法被内部调用(在同一个java类中)
    1
    2
    3
    4
    5
    6
    7
    @Transactional
    public void methodA() {
    methodB(); // 内部方法调用,事务不生效
    }

    @Transactional
    public void methodB() { ... }
    原因:Spring AOP 是基于代理对象实现的,this.methodB() 这种内部调用不会走代理,事务不会生效。
    • ✅ 正确做法:
      1
      2
      3
      4
      5
      6
      @Autowired
      private Service self; // 注入自身代理对象

      public void methodA() {
      self.methodB(); // 用代理调用才能生效
      }
  • 3️⃣ 异常被捕获了
    1
    2
    3
    4
    5
    6
    7
    8
    @Transactional
    public void method() {
    try {
    // 可能抛异常的代码
    } catch (Exception e) {
    // 异常被吃掉
    }
    }
    原因:Spring 默认只在 运行时异常(RuntimeException)或 Error 时才回滚,且异常要能抛出,不能被吞掉。
    ✅ 正确做法:
  1. 异常不要随便吃掉,或者手动 throw
  2. 或者加 @Transactional(rollbackFor = Exception.class) 明确声明回滚类型
  • 4️⃣ 事务方法没有被 Spring 管理(比如 new 出来的类)
    1
    2
    UserService userService = new UserService(); // ❌ 非 Spring Bean
    userService.saveUser(); // 事务不会生效
    原因:Spring 只能代理容器内的 Bean,new 出来的对象是普通对象,事务无效。
  • 5️⃣多线程导致事务失效
    1
    2
    3
    4
    5
    6
    7
    @Transactional
    public void saveData() {
    new Thread(() -> {
    // 数据库操作
    }).start();
    }

    原因:Spring 的事务是线程绑定的,开启事务的线程和新线程不是同一个,事务无效。

bean的生命周期

实例化(new)

属性注入(依赖注入)

BeanPostProcessor(初始化前置处理)

初始化方法(如 @PostConstruct / init-method)

BeanPostProcessor(初始化后置处理)

Bean 开始使用

销毁前处理(如 @PreDestroy / destroy-method)

Bean 被销毁(容器关闭)

springbean的但历史怎么实现的

Spring 容器通过 一个 Map 来缓存单例 Bean 实例,在创建 Bean 时先查缓存,如果已存在就直接返回,不再重新创建。这样有点像CAS一样,保证了bean的单例。

Spring 单例 Bean 的生命周期流程:
Spring 容器启动 → 解析配置 → 实例化 Bean → 放入单例池(Map) → 后续使用都从池中取。

mybatis

mybatis的二级缓存

MyBatis 提供了两级缓存:

缓存级别缓存范围生命周期
一级缓存SqlSession 级别当前会话内有效
二级缓存Mapper 级别SqlSession 有效(多个会话共享)

流程:

1
2
3
4
5
6
7
用户请求 -> Mapper 查询方法

一级缓存 (SqlSession)
↓(查不到)
二级缓存 (基于 Mapper 命名空间)
↓(查不到)
数据库查询

  • 开启 MyBatis 二级缓存
    1
    2
    3
    <settings>
    <setting name="cacheEnabled" value="true"/>
    </settings>

rpc篇

八股文

什么是grpc

  • 定义:
    gRPC 是一种高性能、开源、跨语言的远程过程调用(RPC)框架,由 Google 开发,基于 HTTP/2 和 Protobuf 协议。
特点说明
📦 基于 Protobuf 序列化使用 Google 的 Protocol Buffers(比 JSON/XML 更小更快)
🛣️ 使用 HTTP/2 协议支持多路复用、流控制、头压缩,性能远超 HTTP/1.1
🔁 支持 4 种调用方式普通调用、服务器流、客户端流、双向流
🧬 跨语言支持支持 Java、Go、Python、C++、Node.js 等十几种语言
🧱 接口强类型.proto 文件定义服务和消息,代码自动生成,接口规范统一
🔐 支持认证和加密支持 TLS/SSL,支持 token、OAuth2 等安全机制
🔔 支持流式通信天然支持 Server Push、双向长连接、实时消息等场景
🚀 高性能、低延迟Protobuf + HTTP/2 = 高吞吐 + 低延迟
📦 支持负载均衡、服务发现(可配合 etcd)在大型微服务架构中易于扩展

如何使用grpc

  • 大致过程:
    • 通过 gRPC 代理,把方法和参数序列化为 Protobuf 二进制;
    • 用 HTTP/2 发送给远程服务端;
    • 服务端解析请求,执行对应方法;
    • 返回结果(也是 Protobuf 格式);
    • 客户端反序列化,获得最终结果。
  • 🔧 示例:gRPC 使用流程(Java / Go 都类似)
    • 1️⃣ 定义 proto 文件(接口说明)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      syntax = "proto3";

      service UserService {
      rpc GetUserById (UserRequest) returns (UserResponse);
      }

      message UserRequest {
      int32 id = 1;
      }

      message UserResponse {
      string name = 1;
      int32 age = 2;
      }
    • 2️⃣ 使用 protoc 工具生成代码(Stub)
      1
      protoc --java_out=. user.proto
    • 3️⃣ 实现服务端逻辑
      1
      2
      3
      4
      5
      6
      7
      8
      9
      public class UserServiceImpl extends UserServiceGrpc.UserServiceImplBase {
      @Override
      public void getUserById(UserRequest request, StreamObserver<UserResponse> responseObserver) {
      UserResponse resp = UserResponse.newBuilder()
      .setName("Tom").setAge(22).build();
      responseObserver.onNext(resp);
      responseObserver.onCompleted();
      }
      }
    • 4️⃣ 客户端调用服务
      1
      2
      3
      ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 50051).usePlaintext().build();
      UserServiceGrpc.UserServiceBlockingStub stub = UserServiceGrpc.newBlockingStub(channel);
      UserResponse resp = stub.getUserById(UserRequest.newBuilder().setId(1).build());

etcd

八股文

什么是etcd

etcd 是一个开源的、强一致性、高可用的分布式键值数据库,广泛用于服务注册、配置中心、选主、分布式协调等场景。

等级etcd 用于Redis 用于
📦 轻量配置 / 注册中心❌ Redis 不推荐做配置中心
💽 高频缓存 / 业务数据✅ Redis 优于 etcd
⚠️ 一致性要求高的协调场景✅ etcd 更安全稳定

设计模式

八股文

单例模式

ChatGPT 说:
单例模式(Singleton Pattern)是一种创建型设计模式,它的主要目的是确保一个类只有一个实例,并且提供一个全局访问点来获得该实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Singleton {
private static volatile Singleton instance;

private Singleton() {
// 私有构造方法,防止外部实例化
}

public static Singleton getInstance() {
if (instance == null) { // 第一重检查,避免不必要的同步开销
synchronized (Singleton.class) { // 同步锁住代码块,保证线程安全
if (instance == null) { // 第二重检查,避免多个线程同时进入同步块时创建多个实例
instance = new Singleton(); // 创建实例
}
}
}
return instance;
}

}

其它篇

什么是幂等性,接口幂等性

✅ 一句话理解幂等性:
一次和多次执行的效果是一样的,不论你执行多少次,结果都是一样的。

请求类型示例 URL幂等性说明
GET/user/1001每次获取数据,不改变状态 → ✅ 幂等
DELETE/user/1001多次删除同一个用户,结果一样 → ✅ 幂等
PUT/user/1001 设置 age=20多次设置同一个值,结果一样 → ✅ 幂等
POST/order 新建订单每次都创建一个新订单 → ❌ 不幂等

如果 接口不幂等,就会导致:

  • 重复扣款
  • 重复创建订单
  • 数据不一致
    所以设计接口时,最好让它幂等,或者用幂等机制来保护。

JWT 如何使用续约toekn避免用户登录过期的

🔁 JWT 续约机制:

类型用途有效期存储方式
accessToken访问业务接口用,权限控制短(如15分钟)Authorization 头 / localStorage
refreshToken用来刷新 accessToken长(如7天~30天)HttpOnly Cookie(推荐)或 localStorage
  1. 🧠 为什么需要 refreshToken?
  • 如果只用一个长期有效的 accessToken,一旦泄露无法阻止使用;
  • 如果 accessToken 很短,又不能刷新,用户就要频繁登录,体验差;
  • 因此,需要一个长期有效的 refreshToken,专门用于续签新的 accessToken。
  1. 流程图

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    登录:
    Client ----> Server
    <---- 返回 accessToken + refreshToken

    访问接口:
    Client 携带 accessToken ----> Server 验证通过

    accessToken 过期:
    Client 携带 refreshToken ----> /auth/refresh
    <---- 返回新的 accessToken

    refreshToken 也过期:
    Client 重新登录

    当返回token过期时,需要前端去捕捉,捕捉成功后让前端主动调刷新accessToken的接口,再调用一次失败的接口

  2. JWT 流程示意

部分说明内容示例
Header头部,描述令牌类型和签名算法{"alg":"HS256","typ":"JWT"}
Payload载荷,存放声明(claims){"sub":"1234567890","name":"Alice","iat":1516239022}
Signature签名,对前两部分进行签名校验HMACSHA256(base64UrlEncode(header) + “.” + base64UrlEncode(payload), secret)
  • 将 Header 和 Payload 转为 JSON 后,进行 Base64Url 编码;
  • 用密钥和算法对编码后的头部和载荷拼接字符串签名,生成 Signature;
  • 拼接成 header.payload.signature 格式的 JWT 字符串。
  • 面试官: ThreadLocal的内存泄漏的原因
  • 我:
    • ThreadLocalMap 存储结构
      ThreadLocalMap 存储结构
      每个线程内部(Thread 类)有一个 ThreadLocalMap,它以 弱引用(WeakReference) 来引用 ThreadLocal 对象(key),而 value 是强引用。
      1
      2
      3
      4
      5
      6
      7
      Thread -> ThreadLocalMap
      key (弱引用): ThreadLocal对象
      value (强引用): 线程绑定的变量值

      // 例如
      ThreadLocal<String> threadLocal = new ThreadLocal<>();
      threadLocal.set("Hello, ThreadLocal!"); // 绑定变量值
    1. key:就是 threadLocal 这个对象(弱引用)
    2. value:就是 “Hello, ThreadLocal!” 这个字符串
    • 内存泄漏的关键点:

      • 当 ThreadLocal 对象被垃圾回收(key 为弱引用,没被外部引用了),
      • 但是 ThreadLocalMap 中对应的 value 是强引用,不会被自动回收,
      • 导致这个 value 对象一直存在,造成内存泄漏。
    • 必须在使用完 ThreadLocal 后调用 remove()