spring boot 核心概念

IoC 控制反转 和 DI 依赖注入

IOC 是一种设计思想,是将对象的创建和管理权转给了 spring 容器,让 spring 容器来管理对象,也即控制权反转。

DI (依赖注入,Dependency Injection)是 IoC 的实现方式之一,由 spring 容器来创建并注入所依赖的对象,如下(UserService 就是依赖):

@Service
class OrderService { 
	private UserService userService;
}

在类上添加 @Controller、@Service 、@Component 等注解,将其声明为 Bean ,让 spring 来管理,就能实现自动注入。


DI工作流程

如下,如果某个依赖有多个实现,则必须通过 @Primary 来声明默认优先的实现;否则在依赖注入时,就会因为无法确定使用哪一个实现而报错;

public interface UserService {}

@Service
public class UserServiceImpl1 implements UserService {}

@Primary
@Service
public class UserServiceImpl2 implements UserService {}


如下,也可以主动指定依赖注入时使用哪个一个实现

publicOrderService(@Qualifier("userServiceImpl2") UserServiceuserService) {
    this.userService=userService;
}


如下,明确声明要注入的实现

private final UserServiceuserServiceImpl1;


如下,如果依赖对象有多个构造器,可通过 @Autowred 来指定默认的构造器

@Autowired
publicOrderService(UserServiceuserService) {
    this.userService=userService;
}


Spring 判断寻找依赖的顺序如下:

1>. 按类型找
2>. 如果多个 → 按 @Qualifier
3>. 如果没写 → 按变量名
4>. 再不行 → 报错


AOP

AOP 就是在业务逻辑前、后插入“通用功能”:

@Aspect
@Component
public class LogAspect {


    @Before("execution(* com.example.service.*.*(..))")
    public void before() {
        System.out.println("方法执行前");
    }


    @After("execution(* com.example.service.*.*(..))")
    public void after() {
        System.out.println("方法执行后");
    }
}

@Transactional 就是 AOP 的典型实现:在方法前后自动包一层事务逻辑。具体流程是:方法调用 → AOP代理 → 开启事务 → 执行业务 → 提交/回滚


@Transactional 事务

@Transactional 通常用在 Service 层类的方法上,如下:

@Transactional
public void createOrder() {}

执行流程如下:

调用 createOrder()
   ↓
进入代理对象
   ↓
TransactionInterceptor 拦截
   ↓
1>. 开启事务(begin)
2>. 执行业务方法
3>. 判断是否异常
    - 无异常 → commit
    - 有异常 → rollback


1>. 默认只回滚 RuntimeException

默认情况下,Spring 只对“运行时异常”(RuntimeException)及其子类,以及“错误”(Error)进行回滚。在 Java 中,Exception 是所有异常的父类,它包含 受检异常(Checked Exception)。手动抛出一个显式的 Exception(属于受检异常)时,Spring 的事务拦截器会捕获它,但发现它不在回滚列表中,因此依然会提交事务,导致数据没有回滚。(受检异常 (Checked Exception) 是指在编译阶段必须处理的异常,否则无法通过编译)

@Transactional
public void test() throws Exception {
    throw new Exception(); // ❌ 不会回滚
}

正确写法

## 显式指示 Spring 框架,只要方法抛出 Exception(及其任何子类),就立即回滚当前数据库事务
@Transactional(rollbackFor = Exception.class)


2>. 传播行为(Propagation)

传播行为(Propagation) 定义了当一个事务方法被另一个事务方法调用时,事务该如何生存;它决定了业务方法之间是共用一个事务,还是开启新事务。

@Transactional(propagation = Propagation.REQUIRED)

👉 含义:

有事务 → 加入
没事务 → 创建

常见几种类型说明:

- REQUIRED 默认:必须有事务;如果调用者已经开启了事务,则被调用者方法直接加入这个事务,所有参与的方法都在同一个事务里,任何一个地方报错,整个链路全部回滚。如果调用者没有事务,则被调用者就自己开启一个独立的新事务,且不会将调用者包含到事务中去。

被调用者开启新事务的前提是,被调用者方法有 @Transactional 声明,否则被调用者就是一个普通方法,也就不会开始事务。

如果调用者有事务,但被调用都没有事务,则被调用者会包含到调用者的事务中。如果调用者没有事务,但被调用者有 @Transactional 声明,则被调用者会开启自己的独立事务,且不会将调用者包含到事务中去。

- REQUIRES_NEW 不管调用者有没有事务,被调用都要新开一个独立事务;

- SUPPORTS :如果调用者有事务,则在事务里运行;否则就以非事务(普通逻辑)方式运行;


3>. 隔离级别(Isolation)

隔离级别(Isolation) 是定义“一个事务对数据库的操作,在什么程度上对其他事务可见”;也即决定了当多个事务并发执行时,它们之间如何“防干扰”。

脏读 (Dirty Read):是指事务 A 改了数据但还没提交,事务 B 就读到了这个“脏数据”;如果事务 A 随后回滚了,事务 B 读到的就是不存在的假数据。

不可重复读 (Non-repeatable Read):是事务 A 读了一行数据,事务 B 随后修改并提交了这行;事务 A 再次读,发现数据变了;针对的是同一条数据的 Update(修改)

幻读 (Phantom Read):事务 A 查了一次范围,事务 B 随后插入并提交了新记录;事务 A 再次查时,发现多出了一行;针对的是 Insert/Delete(增删) 导致的行数变化。

// READ_COMMITTED 大多数主流数据库的默认级别,意思是一个事务只能读取到其他事务已经提交的数据。它彻底解决了 脏读,但无法防止不可重复读 和 幻读。
@Transactional(isolation = Isolation.READ_COMMITTED)


Spring Boot 本身不解决脏读、不可重复读和幻读问题,而是通过 @Transactional 设置事务隔离级别,将问题交给数据库处理。

脏读:读取未提交事务的数据;在 PostgreSQL 中,默认的 READ COMMITTED 及以上隔离级别只允许读取已提交数据,因此直接避免了脏读;

不可重复读:同一行数据,两次读取结果不同;在默认隔离级别 READ COMMITTED 下,每次查询都会获取最新快照,因此不可重复读是可能发生的;

幻读:同一条件查询返回的行数不同;在默认隔离级别 READ COMMITTED 下,每次查询都会获取最新快照,因此幻读也是可能发生的;


如果使用隔离级别是 REPEATABLE READ,则事务级快照会固定下来,从而同时避免不可重复读和幻读。快照固定是指:同一个事务中首次读取数据后,数据会以快照形式被记住;之后在当前事务中读取到的永远都这个固定的快照数据,由此避免了不可重复读取和幻读问题。


在 PostgreSQL 中,READ COMMITTED 是“语句级快照”,每条 SQL 都读取当前已提交的最新数据,因此可能出现不可重复读和幻读;而 MySQL innerDB 用的是 REPEATABLE READ “事务级快照”,事务开始后读取的数据版本被固定,从而避免不可重复读和幻读。PostgreSQL 之所以默认选择 READ COMMITTED,是因为它对大多数业务来说在一致性已足够,具有更低的资源占用和更高的并发性能,同时减少长事务导致的版本堆积和性能问题。且 REPEATABLE READ 并不能完全避免 不可重复读 幻读。事实上,通过固定快照来解决事务中数据不一致的问题,并不适合所有业务,因为不是所有业务都想读取一个假的快照数据。


默认情况下,读取数据时不会锁,删除或修改数据时会加行锁,表锁非常少见;

悲观锁(默认) 是先锁数据,再操作;乐观锁是先操作,再在提交时检查。即:悲观锁用等待换安全,乐观锁用失败重试换并发;


如果一个数据依赖多个数据源,会被多处并发修改,则绝不能仅依赖数据库锁,必须在应用层设计完整、一致性的组合方案。例如,账户余额来自多个数据源,则应该如处理:

a. 多个数据源,各自更新的数据要统一沉淀为账本(或等价结构);再汇总数据至数据库余额表中,作为后续计算的依据;此举可避免多个数据源更新同一个“账户余额”;

b. 多个数据源,必须同时更新时一个账户余额时,应该以审计日志或追加式账簿的方式来记录每一笔出入账,然后账户余额来自这些数据的汇总;重要数据一般都要求加审计日志,以便追溯和满足合规要求;

c. 用幂等保证“不重复”;有些类似支付回调的场景下,会有 MQ 重复投递、HTTP 重试、服务超时重放等情况;这些情况要就要用 业务唯一ID 来避免数据被重复插入;

d. 所有数据的数据写入,必须是统一服务、单入口写入;

幂等是数学与计算机领域的重要概念,核心是:对同一个操作执行一次与执行多次,结果完全相同

综上,处理多源高并发,本身就是一个复杂的问题;因此通过账本记录变更、汇总等方式,直接规避,是最可靠、性能价比最高的方式。

注:账本或审计日志不允许 UPDATE / DELETE


4>. 同类方法调用失效

@Service
public class OrderService {

    public void A() {
        B(); // ❌ 事务不会生效
    }

    @Transactional
    public void B() {}
}

Spring 的 @Transactional 是通过 AOP(面向切面编程) 实现的。正常情况,当从外部调用 orderService.B() 时,你拿到的其实是 Spring 生成的代理对象。代理对象会先开启事务,再调用实际的 B() 方法。但如果在 A() 内部调用 B() 时,本质上执行的是 this.B()。this 指向的是目标类实例本身,而不是 Spring 的 代理对象;因此,Spring 拦截不到 B() 的执行,B() 的注解自然就成了摆设,事务也就不会生效。这种情况下,最好为 A() 加注解 @Transactional 。


5> private 方法无效

AOI 无法代理 private 方法,因为 spring 的代理对象无法调用目标实例的私有方法;

@Transactional
private void doSomething() {} ❌


6> try-catch 吃掉异常

如果在事务方法中自行捕获、吞没异常,会导致异常无法传导至 AOP 代理对象,从而使事务无法回滚,如下:

@Transactional
public void test() {
    try {
        // 异常
    } catch (Exception e) {
        // 吞掉异常  ❌
    }
}


7> 多线程失效

Spring 的事务管理是基于线程绑定的(ThreadLocal),跨线程调用会导致事务上下文丢失。 ThreadLocal 里的变量是线程隔离的,当开启 new Thread() 时,新线程里没有任何事务信息,也拿不到主线程的那个数据库连接。新线程会去自己当前线程中的连接池中申请一个新的数据库连接;也就是主线程和子线程完全不在一个数据库连接中,子线程的回滚主线程也无法感知的到。

@Transactional
public void test() {
    new Thread(() -> {
        // ❌ 不在事务中
    }).start();
}


6. 只读事务误用

不要在读取事务中做写操作(有些数据库会优化甚至忽略写)

@Transactional(readOnly = true)

当数据库检测到 readOnly = true 时就会跳过某些为了支持“写操作”而设计的内部锁逻辑,甚至优化事务 ID 的分配,跳过修改检查,从而减轻数据库负担,提升查询速度;同时,readOnly = true 也能让语义清晰。


7. 大事务(性能问题)

尽量不要在一个事务中进行大量操作,这会导致锁时间长,回滚成本高;因此尽量要拆成小事务(批处理)

@Transactional
public void bigTask() {
    // 处理1万条数据
}
举报

© 著作权归作者所有


0