SpringBoot事务管理_AOP思想详解

O泡李华 5

SpringBoot 事务管理 + AOP 思想详解

本章位置:第二阶段 Java 核心框架
前置知识:SpringBoot、MyBatis、MySQL 事务、RBAC 综合案例、注解、反射
下一篇:SpringBoot 底层原理
学习目标:系统掌握 Spring 事务管理、@Transactional、事务传播、事务隔离、事务失效、Spring AOP、动态代理、切点、通知、切面,以及事务和 AOP 的底层联系。


一、为什么事务管理和 AOP 要放在一起学

因为 Spring 中很多常见功能,本质上都和:

代理

有关。

例如:

@Transactional

@Cacheable

@Async

部分权限注解

操作日志

它们并不是:

Java 关键字

而是:

Spring 通过代理对象
在方法执行前后
插入额外逻辑

事务就是非常经典的例子。


二、先回顾数据库事务

事务:

一组数据库操作,要么全部成功,要么全部失败。

例如转账:

A 扣 100
B 加 100

如果:

A 成功
B 失败

数据就错误。

所以必须:

一起成功
或者
一起回滚

三、事务 ACID

四个核心特性:

Atomicity
原子性

Consistency
一致性

Isolation
隔离性

Durability
持久性

四、原子性

一组操作:

不可拆分

要么:

全部提交

要么:

全部回滚

五、一致性

事务前后:

业务规则保持正确

例如转账前:

A+B 总金额 1000

转账后:

A+B 仍然 1000

六、隔离性

并发事务之间:

尽量互不干扰

七、持久性

事务一旦:

COMMIT

就应该持久保存。


八、原生 JDBC 事务

JDBC:

Connection connection =
        dataSource.getConnection();

try {

    connection.setAutoCommit(
            false
    );

    // SQL 1

    // SQL 2

    connection.commit();

} catch (
        Exception e
) {

    connection.rollback();

    throw e;

} finally {

    connection.close();
}

九、原生 MyBatis 事务

SqlSession sqlSession =
        sqlSessionFactory.openSession();

try {

    UserMapper userMapper =
            sqlSession.getMapper(
                    UserMapper.class
            );

    UserRoleMapper userRoleMapper =
            sqlSession.getMapper(
                    UserRoleMapper.class
            );

    userMapper.insert(
            user
    );

    userRoleMapper.batchInsert(
            user.getId(),
            roleIds
    );

    sqlSession.commit();

} catch (
        Exception e
) {

    sqlSession.rollback();

    throw e;

} finally {

    sqlSession.close();
}

十、为什么 Spring 要管理事务

如果每个 Service 都手工:

begin

commit

rollback

close

会产生:

大量重复代码

所以 Spring 把事务抽出来统一管理。


十一、Spring 声明式事务

最常见:

@Transactional
public void assignRoles(
        Long userId,
        List<Long> roleIds
) {

    userRoleMapper.deleteByUserId(
            userId
    );

    userRoleMapper.batchInsert(
            userId,
            roleIds
    );
}

开发者只声明:

这个方法需要事务

Spring 自动:

开启事务

提交

回滚

十二、@Transactional 属于声明式事务

声明式:

告诉框架“我要事务”

而不是自己手工:

connection.commit()

十三、编程式事务

Spring 也支持:

TransactionTemplate

PlatformTransactionManager

手动控制事务。

例如:

transactionTemplate.execute(
        status -> {

            // 业务代码

            return null;
        }
);

十四、声明式和编程式事务

声明式:

代码少

易维护

最常用

编程式:

控制更细

某些复杂业务使用

普通 SpringBoot 项目:

优先 @Transactional

十五、@Transactional 通常放哪里

推荐:

Service public 方法

因为:

Service
表示完整业务操作

十六、为什么不推荐 Controller 开事务

Controller 负责:

HTTP 请求

参数

响应

事务属于:

业务层

所以:

Service 更合适

十七、为什么不推荐 Mapper 单独开事务

如果每个 Mapper:

自己一个事务

那么一个业务调用多个 Mapper 时:

无法保证整体原子性

十八、RBAC 分配角色案例

删除旧角色

插入新角色

这两个操作必须:

一个事务

所以:

@Transactional
public void assignRoles(
        Long userId,
        List<Long> roleIds
) {

    userRoleMapper.deleteByUserId(
            userId
    );

    userRoleMapper.batchInsert(
            userId,
            roleIds
    );
}

十九、正常情况下 Spring 做了什么

调用:

assignRoles()

Spring 大致:

代理对象
↓
开启事务
↓
调用真实方法
↓
方法正常结束
↓
commit

二十、异常情况下

代理对象
↓
开启事务
↓
调用真实方法
↓
抛异常
↓
rollback

二十一、@Transactional 默认回滚什么

默认常见:

RuntimeException

Error

会回滚。


二十二、Checked Exception 默认可能不回滚

例如:

IOException

属于:

Checked Exception

默认行为和 RuntimeException 不同。


二十三、rollbackFor

如果希望所有异常:

都回滚

常写:

@Transactional(
        rollbackFor = Exception.class
)

二十四、什么时候使用 rollbackFor

例如 Service 明确可能抛:

IOException

而业务必须:

回滚

就可以:

@Transactional(
        rollbackFor = Exception.class
)

二十五、noRollbackFor

也可以指定:

@Transactional(
        noRollbackFor =
                BusinessWarningException.class
)

表示:

某种异常不回滚

实际项目使用要谨慎。


二十六、事务失效是非常重要的面试题

常见失效原因:

方法不是 public

同类内部 self-invocation

对象不是 Spring Bean

异常被 catch 吞掉

异常类型不触发默认 rollback

数据库表不支持事务

多线程切换

事务传播设置不合理

手工创建对象绕过 Spring

代理方式限制

二十七、事务失效 1:方法不是 public

例如:

@Transactional
private void saveUser() {

}

很多 Spring 声明式事务场景下:

不会按预期生效

推荐:

public Service 方法

二十八、事务失效 2:同类内部调用

经典:

@Service
public class UserService {

    public void a() {

        this.b();
    }

    @Transactional
    public void b() {

        // DB
    }
}

调用:

a()

再:

this.b()

可能导致:

b 的事务失效

二十九、为什么 self-invocation 会失效

Spring 事务通常依赖:

代理对象

外部调用:

Proxy.b()

可以经过:

事务拦截器

但:

this.b()

调用的是:

当前真实对象自身方法

绕过:

Proxy

三十、代理调用图

正常:

Controller
↓
UserService Proxy
↓
Transaction Interceptor
↓
UserService Target

自调用:

UserService Target.a()
↓
this.b()

没有再次经过:

Proxy

三十一、解决 self-invocation

常见方案:

1. 把事务方法拆到另一个 Service

2. 让调用经过 Spring Bean

3. 使用 AspectJ 等更高级方案

4. 谨慎使用 AopContext

初学最推荐:

拆 Service

三十二、事务失效 3:自己 new 对象

错误:

UserService userService =
        new UserService();

userService.save();

这个对象:

不是 Spring 管理的 Bean

所以:

没有事务代理

三十三、正确方式

通过 Spring:

依赖注入

例如:

private final UserService
        userService;

三十四、事务失效 4:异常被吞掉

错误:

@Transactional
public void save() {

    try {

        mapper.insert(...);

        throw new RuntimeException();

    } catch (
            Exception e
    ) {

        log.error(
                "error",
                e
        );
    }
}

方法最后:

正常返回

Spring 认为:

没有异常

可能:

commit

三十五、正确做法

如果希望事务回滚:

catch (
        Exception e
) {

    log.error(
            "error",
            e
    );

    throw e;
}

三十六、事务失效 5:Checked Exception

@Transactional
public void importFile()
        throws IOException {

    // DB

    throw new IOException();
}

默认可能:

不回滚

三十七、解决

@Transactional(
        rollbackFor = Exception.class
)

三十八、事务失效 6:线程切换

例如:

@Transactional
public void save() {

    CompletableFuture.runAsync(
            () -> mapper.insert(...)
    );
}

异步线程:

不自动继承当前线程数据库事务

三十九、为什么

Spring 事务上下文通常:

绑定当前线程

切换线程后:

事务上下文丢失

四十、事务失效 7:表引擎不支持

MySQL:

InnoDB
支持事务

如果使用某个:

不支持事务的存储引擎

Spring 再怎么 rollback:

也无法真正回滚

四十一、事务失效 8:多个数据源

如果一个业务同时:

DataSource A

DataSource B

普通单数据源事务管理器:

不一定能保证两个库一起事务

这属于:

分布式事务

后面再学。


四十二、事务传播是什么

事务传播:

一个带事务的方法调用另一个带事务的方法时,应该如何处理事务关系。

这就是:

Propagation

四十三、为什么需要传播行为

例如:

@Transactional
public void createOrder() {

    saveOrder();

    saveLog();
}

其中:

saveOrder
saveLog

也可能各自带事务。

那么问题:

共用一个事务?

还是开新事务?

还是禁止事务?

四十四、Spring 常见传播行为

重点:

REQUIRED

REQUIRES_NEW

SUPPORTS

NOT_SUPPORTED

MANDATORY

NEVER

NESTED

最重要:

REQUIRED

REQUIRES_NEW

四十五、REQUIRED

默认:

@Transactional(
        propagation =
                Propagation.REQUIRED
)

意思:

外面有事务
→ 加入外部事务

外面没事务
→ 新建事务

四十六、REQUIRED 最常用

Service A:

@Transactional
public void a() {

    serviceB.b();
}

Service B:

@Transactional
public void b() {

}

通常:

a 和 b
使用同一个事务

四十七、REQUIRED 图

A Transaction
├─ SQL A1
├─ B()
│  ├─ SQL B1
│  └─ SQL B2
└─ SQL A2

其中 B:

加入 A 的事务

四十八、REQUIRES_NEW

@Transactional(
        propagation =
                Propagation.REQUIRES_NEW
)

意思:

不管外面有没有事务

都开一个新事务

外事务:

暂时挂起

四十九、REQUIRES_NEW 例子

业务:

删除用户

同时:

记录审计日志

希望:

主业务失败
日志仍然保存

可以考虑:

日志使用 REQUIRES_NEW

五十、REQUIRES_NEW 图

Transaction A
↓
暂停
↓
Transaction B
commit / rollback
↓
恢复 Transaction A

五十一、为什么 REQUIRES_NEW 要谨慎

因为会:

占用额外数据库连接

增加事务复杂度

可能造成连接池压力

不是所有日志都:

必须新事务

五十二、SUPPORTS

意思:

外面有事务
→ 加入

外面没事务
→ 非事务运行

五十三、MANDATORY

意思:

必须存在外部事务

否则:

报错

五十四、NOT_SUPPORTED

意思:

以非事务方式运行

如果外面有事务:

挂起事务

五十五、NEVER

意思:

必须没有事务

如果外面有:

报错

五十六、NESTED

嵌套事务:

使用保存点 Savepoint

可以:

内部部分回滚

但具体行为受:

事务管理器
数据库支持

影响。


五十七、初学传播行为记忆

REQUIRED
有就加入,没有就建

REQUIRES_NEW
永远新建

SUPPORTS
有就加入,没有也行

MANDATORY
必须有

NOT_SUPPORTED
必须非事务

NEVER
禁止事务

NESTED
嵌套保存点

五十八、事务传播最大的坑:异常处理

A:

@Transactional
public void a() {

    try {

        serviceB.b();

    } catch (
            Exception e
    ) {

        // 吞掉
    }

    mapper.update(...);
}

B:

@Transactional
public void b() {

    throw new RuntimeException();
}

五十九、UnexpectedRollbackException

即使 A catch 了异常,

B 使用:

REQUIRED

加入 A 事务后,

事务可能已经标记:

rollback-only

最后 A 想 commit:

可能抛 UnexpectedRollbackException

六十、为什么会这样

因为:

B 出现 RuntimeException

事务拦截器认为:

整个事务应该回滚

虽然 A:

catch 了 Java 异常

但事务状态:

已经被标记 rollback-only

六十一、事务传播不是简单 try/catch

这是非常重要的理解:

Java 异常控制

和:

Spring 事务状态

不是完全同一件事。


六十二、事务隔离级别

Spring:

@Transactional(
        isolation =
                Isolation.READ_COMMITTED
)

可以指定事务隔离级别。


六十三、常见 Isolation

DEFAULT

READ_UNCOMMITTED

READ_COMMITTED

REPEATABLE_READ

SERIALIZABLE

六十四、DEFAULT

表示:

使用数据库默认隔离级别

MySQL InnoDB 常见默认:

REPEATABLE READ

六十五、READ_UNCOMMITTED

可能:

脏读

不可重复读

幻读

六十六、READ_COMMITTED

解决:

脏读

仍可能:

不可重复读

六十七、REPEATABLE_READ

保证:

同事务普通快照读
重复读取通常一致

MySQL InnoDB:

默认常见

六十八、SERIALIZABLE

最严格:

并发能力最低

六十九、不要为了安全无脑 SERIALIZABLE

隔离越强:

并发能力通常越低

要平衡:

一致性

性能

七十、事务 timeout

可以:

@Transactional(
        timeout = 5
)

表示:

事务超时限制

七十一、事务 readOnly

@Transactional(
        readOnly = true
)
public List<User> list() {

}

表示:

只读事务提示

七十二、readOnly 是绝对禁止写吗

不要简单理解为:

数据库绝对无法写

它更像:

事务管理与数据库驱动优化提示

具体行为:

取决于实现

七十三、事务越短越好

事务中尽量不要:

调用第三方 API

上传大文件

等待用户操作

sleep

执行大量慢计算

七十四、为什么事务要短

因为事务可能持有:

数据库连接

锁

undo 信息

事务越长:

锁竞争越严重

连接占用越久

死锁概率越高

七十五、错误事务设计

@Transactional
public void createOrder() {

    mapper.insertOrder();

    callPaymentRemoteApi();

    uploadFile();

    Thread.sleep(
            5000
    );

    mapper.updateOrder();
}

整个过程:

事务太长

七十六、更合理

事务内:
必要 DB 操作

事务外:
远程调用 / 非关键耗时逻辑

复杂一致性:

后面消息队列 / 分布式事务

再解决。


七十七、事务和锁

事务不是:

只有 commit / rollback

还会影响:

锁持有时间

七十八、事务里更新一行

UPDATE account
SET balance = balance - 100
WHERE id = 1;

未提交前:

相关锁可能继续持有

七十九、所以不要事务中暂停

例如:

debug 断点停很久

有时候也会让其他请求:

一直等待数据库锁

八十、Spring AOP 是什么

AOP:

Aspect-Oriented Programming

中文:

面向切面编程

八十一、AOP 解决什么

把大量业务都需要的:

公共逻辑

统一抽取。

例如:

日志

事务

权限

性能统计

缓存

审计

八十二、没有 AOP

每个方法:

long start =
        System.currentTimeMillis();

try {

    // 业务

} finally {

    long cost =
            System.currentTimeMillis()
            - start;

    log.info(
            "cost={}",
            cost
    );
}

大量重复。


八十三、有 AOP

业务:

@OperationLog(
        "删除用户"
)
public void deleteUser() {

    // 只写业务
}

切面统一:

记录时间

执行方法

记录结果

记录异常

八十四、AOP 核心术语

必须掌握:

Target

Proxy

Join Point

Pointcut

Advice

Aspect

Weaving

八十五、Target

Target:

目标对象

也就是:

真正业务对象

例如:

UserService

八十六、Proxy

Proxy:

代理对象

Controller 实际注入的:

可能不是原始 UserService

而是:

UserService Proxy

八十七、Join Point

连接点:

可以被增强的位置

Spring AOP 中最主要:

方法执行

八十八、Pointcut

切点:

从所有连接点中,选择哪些方法需要增强。

例如:

所有 service 包的方法

或者:

带 @OperationLog 的方法

八十九、Advice

通知:

切面具体要执行的逻辑。

常见:

@Before

@After

@AfterReturning

@AfterThrowing

@Around

九十、Aspect

切面:

Pointcut
+
Advice

组成:

完整横切逻辑

九十一、Weaving

织入:

把增强逻辑应用到目标对象

Spring AOP:

通常运行时通过代理完成

九十二、@Aspect

@Aspect
@Component
public class LogAspect {

}

九十三、@Before

@Before(
        "execution(* com.example.service..*(..))"
)
public void before() {

    log.info(
            "before"
    );
}

在:

目标方法执行前

九十四、@After

无论:

正常

异常

一般都会执行。

类似:

finally

九十五、@AfterReturning

只有:

正常返回

才执行。

可以获得:

返回值

九十六、@AfterThrowing

只有:

抛异常

才执行。

适合:

异常日志

九十七、@Around

最强:

可以控制方法前后

例如:

@Around(
        "@annotation(operationLog)"
)
public Object around(
        ProceedingJoinPoint joinPoint,
        OperationLog operationLog
)
        throws Throwable {

    long start =
            System.currentTimeMillis();

    try {

        return joinPoint.proceed();

    } finally {

        long cost =
                System.currentTimeMillis()
                - start;
    }
}

九十八、joinPoint.proceed()

非常重要。

它表示:

继续执行目标方法

如果没有:

joinPoint.proceed()

目标方法:

根本不会运行

九十九、Around 类似什么

可以理解:

代理包装

结构:

before
↓
target method
↓
after

一百、Pointcut 表达式 execution

例如:

execution(
    * com.example.service..*(..)
)

一百零一、execution 含义

可以粗略拆:

*
任意返回值

com.example.service..
service 包及子包

*
任意类/方法匹配部分

(..)
任意参数

一百零二、注解切点

相比 execution,企业项目常用:

@Around(
        "@annotation(operationLog)"
)

表示:

只有带指定注解的方法
才拦截

一百零三、自定义 @OperationLog

@Target(
        ElementType.METHOD
)
@Retention(
        RetentionPolicy.RUNTIME
)
public @interface OperationLog {

    String module();

    String operation();
}

一百零四、使用

@OperationLog(
        module = "用户管理",
        operation = "删除用户"
)
public void deleteUser(
        Long id
) {

}

一百零五、为什么 AOP 和注解经常一起用

注解负责:

声明“这里需要某功能”

AOP 负责:

真正执行功能

一百零六、事务也是这种思想

@Transactional
public void save() {

}

注解:

声明事务

Spring:

代理拦截方法

执行:

begin
↓
method
↓
commit / rollback

一百零七、所以 @Transactional 本质就是 AOP 场景

这也是为什么:

self-invocation

会失效。

因为:

绕过了代理

一百零八、Spring AOP 动态代理

主要:

JDK Dynamic Proxy

CGLIB

一百零九、JDK 动态代理

通常要求:

目标对象实现接口

代理:

接口

一百一十、CGLIB

通过:

生成目标类子类

实现代理。


一百一十一、SpringBoot 中常见

现代 Spring:

会根据情况创建代理

开发者大多数时候:

不用手工决定

但要理解:

代理对象存在

一百一十二、final 方法为什么值得注意

CGLIB 基于:

子类重写

所以:

final 类 / final 方法

会影响:

基于继承的代理能力

具体 Spring 版本和配置需要结合实际。


一百一十三、private 方法为什么难增强

private 方法:

不能被子类覆盖

也不是接口公开调用点。

所以:

不适合作为普通 Spring AOP 增强入口

一百一十四、AOP 自调用问题

和事务同理:

public void a() {

    this.b();
}

@OperationLog(...)
public void b() {

}

如果切面依赖代理:

b 可能不被增强

一百一十五、为什么

this.b()

没有经过:

Spring Proxy

一百一十六、AOP 与 Interceptor 区别

Interceptor:

Spring MVC Web 层

主要:

HTTP 请求

HandlerMethod

一百一十七、AOP

AOP:

Bean 方法层

不局限于:

Controller

可以拦截:

Service
Mapper 外围 Bean

一百一十八、什么时候用 Interceptor

适合:

登录检查

请求级权限

请求 Header

访问路径

一百一十九、什么时候用 AOP

适合:

业务操作日志

方法耗时

事务

方法级权限

审计

一百二十、Filter vs Interceptor vs AOP

顺序可以简单理解:

Filter
↓
DispatcherServlet
↓
Interceptor
↓
Controller
↓
Service AOP

一百二十一、Filter

属于:

Servlet 规范

更加底层。

适合:

CORS

请求包装

安全过滤

一百二十二、Interceptor

属于:

Spring MVC

可以拿:

HandlerMethod

一百二十三、AOP

属于:

Spring Bean 方法调用

关注:

方法

一百二十四、操作日志 Aspect 实战

@Aspect
@Component
public class OperationLogAspect {

    private final OperationLogService
            operationLogService;

    public OperationLogAspect(
            OperationLogService operationLogService
    ) {

        this.operationLogService =
                operationLogService;
    }
}

一百二十五、Around 记录耗时

@Around(
        "@annotation(operationLog)"
)
public Object around(
        ProceedingJoinPoint joinPoint,
        OperationLog operationLog
)
        throws Throwable {

    long start =
            System.currentTimeMillis();

    boolean success =
            false;

    String errorMessage =
            null;

    try {

        Object result =
                joinPoint.proceed();

        success =
                true;

        return result;

    } catch (
            Throwable e
    ) {

        errorMessage =
                e.getMessage();

        throw e;

    } finally {

        long cost =
                System.currentTimeMillis()
                - start;

        // 保存日志
    }
}

一百二十六、为什么 catch 后还必须 throw

因为:

主业务异常不能被日志切面吞掉

否则:

Controller 看起来成功

事务可能不回滚

错误被隐藏

一百二十七、AOP 记录参数要谨慎

不要无脑:

所有方法参数转 JSON

因为可能包含:

password

token

身份证

文件

一百二十八、日志脱敏

例如:

password
直接忽略

token
只记录前几位或 hash

手机号
138****8000

一百二十九、AOP 中异常日志

可以:

log.error(
        "operation failed, method={}",
        joinPoint.getSignature(),
        e
);

完整异常:

只写日志

不要:

直接返回前端

一百三十、AOP 和事务执行顺序

如果一个方法同时:

@Transactional
@OperationLog

会出现:

多个代理增强

谁先谁后:

与 Order 有关

一百三十一、@Order

可以:

@Order(1)

控制切面优先级。

数字一般:

越小
优先级越高

一百三十二、Around 嵌套图

如果:

Log Aspect
外层

Transaction Aspect
内层

可能:

Log before
↓
Transaction begin
↓
Business
↓
Transaction commit
↓
Log after

一百三十三、如果顺序反过来

Transaction begin
↓
Log before
↓
Business
↓
Log after
↓
Transaction commit

结果不同。


一百三十四、为什么日志和事务顺序重要

如果你在:

事务 commit 前

就记录:

success=true

但最后 commit 失败:

日志会误判

所以复杂系统:

要考虑事务完成后的状态

一百三十五、事务同步回调

Spring 提供事务同步机制,

可以:

事务提交后
执行某些操作

当前阶段:

知道思想即可

例如:

提交后删缓存

提交后发消息

一百三十六、为什么缓存最好提交后删

如果:

事务还没提交
↓
先删缓存
↓
其他请求进来
↓
查数据库旧值
↓
重新写旧缓存
↓
事务再提交新值

可能出现:

缓存旧数据

一百三十七、这就是事务与缓存一致性问题

后面 Redis / 分布式阶段会继续深入:

Cache Aside

延迟双删

消息通知

Binlog/Canal

一百三十八、事务方法中不要随意捕获所有 Exception

错误:

@Transactional
public void save() {

    try {

        ...

    } catch (
            Exception e
    ) {

        return;
    }
}

这会:

隐藏异常

可能导致提交

一百三十九、如果业务确实要 catch

可以:

catch (
        Exception e
) {

    // 补偿 / 日志

    throw new BusinessException(...);
}

确保:

事务能看到异常

一百四十、手工标记 rollback-only

Spring 允许:

TransactionAspectSupport
        .currentTransactionStatus()
        .setRollbackOnly();

但学习阶段:

不要依赖这种写法

更推荐:

正确抛异常

一百四十一、事务边界不要过大

错误:

@Transactional
public void fullBusiness() {

    // 30 个操作
    // 5 个远程请求
    // 10 秒
}

一百四十二、事务边界要围绕一致性

问自己:

哪些数据库操作必须一起成功?

只有这些:

放一个事务

一百四十三、只读查询要不要事务

普通简单查询:

可以不显式加

复杂一致性读取:

可以使用 readOnly 事务

一百四十四、查询是否完全不需要事务

数据库每条 SQL:

本身也运行在数据库事务机制下

只是应用层:

未必需要显式长事务

一百四十五、并发更新案例

库存:

stock = 1

两个请求同时:

扣库存

只加:

@Transactional

不代表:

自动解决所有并发问题

一百四十六、为什么事务不等于并发安全

事务解决:

原子性
一致性
隔离

但具体业务仍可能需要:

锁

乐观锁

条件 UPDATE

一百四十七、库存安全更新

例如:

UPDATE product

SET stock = stock - 1

WHERE
    id = #{id}
    AND stock > 0;

返回:

1
成功

0
库存不足

一百四十八、事务 + 乐观锁

UPDATE product

SET
    stock = #{stock},
    version = version + 1

WHERE
    id = #{id}
    AND version = #{version};

一百四十九、事务 + FOR UPDATE

悲观锁:

SELECT *
FROM account
WHERE id = #{id}
FOR UPDATE;

然后:

修改
提交

一百五十、@Transactional 不是万能锁

这是必须记住的:

加了事务
≠
所有并发问题自动解决

一百五十一、事务与数据库连接

Spring 事务底层仍然要:

获取 Connection

关闭 autoCommit

commit / rollback

只不过:

Spring 帮你管理

一百五十二、事务和 ThreadLocal

Spring 常通过:

当前线程上下文

绑定:

Connection

Transaction Status

所以线程切换后:

事务通常不会自动跟过去

一百五十三、为什么 @Async + @Transactional 要小心

@Async
@Transactional
public void asyncSave() {

}

或者事务方法调用异步方法,

要理解:

新线程
新事务上下文

不能假设:

共享外部事务

一百五十四、Spring AOP 的局限

Spring AOP 主要:

方法级代理

不像完整 AspectJ:

可以织入字段、构造器等更多连接点

一百五十五、Spring AOP 为什么够用

企业 Spring 项目常见:

事务

日志

权限

缓存

方法性能

基本都是:

方法级别

所以足够。


一百五十六、切点不要过宽

错误:

execution(* com.example..*(..))

可能:

整个项目所有方法都被增强

性能和排错成本高。


一百五十七、推荐切点明确

例如:

指定包

指定注解

一百五十八、操作日志推荐注解切点

@Around(
        "@annotation(operationLog)"
)

非常清楚:

只记录明确标注的方法

一百五十九、性能统计可以包切点

例如:

execution(
    * com.example.service..*(..)
)

统计 Service 方法耗时。


一百六十、不要把 AOP 变成业务垃圾桶

AOP 适合:

横切关注点

不适合:

用户注册核心业务

订单计算

库存规则

这些仍然属于:

Service

一百六十一、什么叫横切关注点

很多模块都需要:

日志

事务

认证

缓存

监控

它们“横切”:

多个业务模块

一百六十二、AOP 的最大价值

不是:

代码炫酷

而是:

减少重复

职责分离

业务代码更干净

一百六十三、事务本质完整流程

Controller
↓
Service Proxy
↓
TransactionInterceptor
↓
TransactionManager
↓
获取 Connection
↓
关闭 autoCommit
↓
Target Service
↓
Mapper
↓
SQL
↓
正常
→ commit

异常
→ rollback

一百六十四、PlatformTransactionManager

Spring 事务抽象核心之一:

PlatformTransactionManager

负责:

begin

commit

rollback

一百六十五、不同技术可以有不同事务管理器

例如:

JDBC

JPA

JTA

Spring 通过:

统一事务抽象

屏蔽差异。


一百六十六、SpringBoot + JDBC/MyBatis 常见

底层常见:

DataSourceTransactionManager

在新版本 Spring 中类名/实现体系可能进一步演化,

但思想不变:

围绕 DataSource Connection 管理事务

一百六十七、事务管理器和连接池区别

连接池:

管理 Connection 资源

事务管理器:

管理一个业务期间 Connection 的事务状态

一百六十八、事务和 MyBatis SqlSession

Spring 集成 MyBatis 后:

SqlSession
会配合 Spring 事务

同一事务中 Mapper 调用:

使用同一事务上下文中的 Connection

开发者通常:

不用手动 commit

一百六十九、为什么 SpringBoot MyBatis 不写 commit

因为:

Spring 事务管理器

统一控制。

如果手工:

sqlSession.commit()

反而会破坏:

事务边界

一百七十、事务传播案例 1:用户创建 + 默认角色

@Transactional
public Long createUser(
        UserCreateDTO dto
) {

    userMapper.insert(
            user
    );

    defaultRoleService.assignDefaultRole(
            user.getId()
    );

    return user.getId();
}

一百七十一、DefaultRoleService

@Transactional
public void assignDefaultRole(
        Long userId
) {

    userRoleMapper.insert(
            userId,
            defaultRoleId
    );
}

默认:

REQUIRED

所以:

两个方法同一事务

一百七十二、如果默认角色插入失败

整个:

用户创建

也应该:

回滚

这就是 REQUIRED 的价值。


一百七十三、传播案例 2:独立审计日志

@Transactional
public void deleteUser(
        Long userId
) {

    userMapper.deleteById(
            userId
    );

    auditService.writeLog(
            ...
    );
}

如果日志:

必须独立保存

audit:

@Transactional(
        propagation =
                Propagation.REQUIRES_NEW
)

一百七十四、但日志一定要 REQUIRES_NEW 吗

不一定。

更常见高并发系统:

异步消息

日志不阻塞主业务。

学习阶段:

理解传播即可

一百七十五、传播案例 3:内部异常被 catch

这是事务最容易踩坑的地方。

必须结合:

传播行为

rollback-only

异常传播

整体分析。


一百七十六、事务优先级建议

实际开发前问:

1. 哪些操作必须原子?

2. 谁是事务边界?

3. 异常是否会抛出?

4. 是否跨线程?

5. 是否跨数据库?

6. 是否有远程调用?

7. 是否存在缓存?

一百七十七、AOP 开发前问

1. 这是业务逻辑还是横切逻辑?

2. 哪些方法需要增强?

3. 用注解切点还是包切点?

4. Advice 放 before 还是 around?

5. 异常是否继续抛?

6. 是否会记录敏感参数?

7. 切面顺序是否重要?

一百七十八、练习 1:事务回滚

创建:

@Transactional
public void testRollback() {

    userMapper.insert(
            user
    );

    throw new RuntimeException(
            "测试回滚"
    );
}

执行后检查:

用户是否插入

应该:

没有

一百七十九、练习 2:异常吞掉

把异常 catch:

try {

    throw new RuntimeException();

} catch (
        Exception e
) {

}

观察:

事务是否提交

理解:

为什么异常传播重要

一百八十、练习 3:Checked Exception

抛:

IOException

分别测试:

默认 @Transactional

rollbackFor=Exception.class

观察区别。


一百八十一、练习 4:self-invocation

同一个 Service:

a()
调用
this.b()

b:

@Transactional

观察事务。


一百八十二、练习 5:拆 Service

把 b:

移动到 OtherService

再由:

Spring 注入调用

观察事务恢复。


一百八十三、练习 6:REQUIRED

Service A:

事务

调用 Service B:

REQUIRED

B 抛异常。

观察:

A/B 数据一起回滚

一百八十四、练习 7:REQUIRES_NEW

A:

插入用户

B:

独立写日志

B 使用:

REQUIRES_NEW

设计不同异常场景观察。


一百八十五、练习 8:Around

创建:

@OperationLog

和:

OperationLogAspect

记录:

方法名

耗时

一百八十六、练习 9:AOP 异常

目标方法:

故意抛异常

确认:

Aspect 能记录失败

异常仍然继续抛出

一百八十七、练习 10:切点

分别使用:

execution

@annotation

理解:

包切点

注解切点

一百八十八、练习 11:权限 AOP 思考

尝试设计:

@RequirePermission(
        "user:delete"
)

用 AOP 读取注解。

然后比较:

Interceptor 实现
AOP 实现

一百八十九、练习 12:事务 + 缓存

用户分配角色:

更新 DB
↓
清 LoginUser 缓存

思考:

如果 DB rollback
缓存怎么办?

一百九十、必须掌握事务注解参数

propagation

isolation

timeout

readOnly

rollbackFor

noRollbackFor

一百九十一、必须掌握传播行为

重点:

REQUIRED

REQUIRES_NEW

了解:

SUPPORTS

MANDATORY

NOT_SUPPORTED

NEVER

NESTED

一百九十二、必须掌握事务失效

非 public

self-invocation

手工 new

异常被吞

Checked Exception

线程切换

非事务存储引擎

多数据源

一百九十三、必须掌握 AOP 术语

Target

Proxy

Join Point

Pointcut

Advice

Aspect

Weaving

一百九十四、必须掌握通知

@Before

@After

@AfterReturning

@AfterThrowing

@Around

一百九十五、必须掌握代理

JDK Dynamic Proxy

CGLIB

一百九十六、面试题 1

问:

Spring 事务为什么会失效?

答:

Spring 声明式事务通常基于 AOP 代理。

如果方法调用没有经过代理,例如同类内部 this 调用,
或者对象不是 Spring Bean,
事务增强就可能无法执行。

另外异常被吞、Checked Exception 默认不回滚、
线程切换等也会造成事务行为不符合预期。

一百九十七、面试题 2

问:

@Transactional 默认回滚什么?

答:

默认通常对 RuntimeException 和 Error 回滚。

Checked Exception 默认不一定回滚,
如果业务要求所有异常回滚,
可以使用 rollbackFor = Exception.class。

一百九十八、面试题 3

问:

REQUIRED 和 REQUIRES_NEW 区别?

答:

REQUIRED 是默认传播行为。

外部有事务就加入,
没有事务就创建。

REQUIRES_NEW 无论外部有没有事务,
都会创建一个新事务,
外部事务会被挂起。

一百九十九、面试题 4

问:

为什么同类内部调用事务会失效?

答:

因为 this.method() 是目标对象内部直接调用,
没有经过 Spring 创建的代理对象。

事务拦截器位于代理链上,
绕过代理就无法执行事务增强。

二百、面试题 5

问:

什么是 AOP?

答:

AOP 是面向切面编程。

它用于把日志、事务、权限、监控等
多个业务模块都需要的横切逻辑抽取出来,
通过代理统一织入目标方法。

二百零一、面试题 6

问:

Spring AOP 核心概念有哪些?

答:

Target:目标对象

Proxy:代理对象

Join Point:连接点

Pointcut:切点

Advice:通知

Aspect:切面

Weaving:织入

二百零二、面试题 7

问:

Around 和 Before 有什么区别?

答:

Before 只能在目标方法执行前增加逻辑。

Around 可以包围整个目标方法,
能在执行前、执行后、异常时做处理,
并通过 ProceedingJoinPoint.proceed()
决定是否继续执行目标方法。

二百零三、面试题 8

问:

Spring AOP 使用什么代理?

答:

主要有 JDK 动态代理和 CGLIB 代理。

JDK 动态代理主要基于接口,
CGLIB 通过生成目标类子类实现代理。

Spring 会根据实际情况创建代理对象。

二百零四、面试题 9

问:

事务为什么通常放 Service?

答:

因为 Service 表示完整业务操作。

一个业务可能调用多个 Mapper,
这些数据库操作往往需要一起提交或一起回滚。

所以 Service 是更合理的事务边界。

二百零五、面试题 10

问:

事务越大越好吗?

答:

不是。

长事务会长期占用连接和锁,
增加锁等待、死锁和数据库压力。

事务应该只包含必须保证一致性的数据库操作。

二百零六、Spring 事务知识结构

Spring Transaction
│
├─ @Transactional
│  ├─ rollbackFor
│  ├─ propagation
│  ├─ isolation
│  ├─ timeout
│  └─ readOnly
│
├─ Propagation
│  ├─ REQUIRED
│  ├─ REQUIRES_NEW
│  ├─ SUPPORTS
│  ├─ MANDATORY
│  ├─ NOT_SUPPORTED
│  ├─ NEVER
│  └─ NESTED
│
├─ Failure
│  ├─ self invocation
│  ├─ non-public
│  ├─ catch exception
│  ├─ checked exception
│  ├─ new object
│  └─ thread switch
│
└─ Underlying
   ├─ Proxy
   ├─ TransactionInterceptor
   ├─ TransactionManager
   ├─ Connection
   ├─ commit
   └─ rollback

二百零七、Spring AOP 知识结构

Spring AOP
│
├─ Target
├─ Proxy
├─ Join Point
├─ Pointcut
├─ Advice
│  ├─ Before
│  ├─ After
│  ├─ AfterReturning
│  ├─ AfterThrowing
│  └─ Around
├─ Aspect
├─ Weaving
├─ Proxy
│  ├─ JDK
│  └─ CGLIB
└─ Use Cases
   ├─ Transaction
   ├─ Log
   ├─ Permission
   ├─ Cache
   └─ Monitor

二百零八、事务和 AOP 最终联系

一定记住这一张图:

调用 Service
↓
实际上调用 Proxy
↓
Transaction Advice
↓
begin transaction
↓
Target Service Method
↓
Mapper
↓
Database
↓
method return
↓
commit

如果异常
↓
rollback

所以:

@Transactional
本质上就是 Spring AOP 的典型应用

二百零九、从 RBAC 回看事务

RBAC 中:

用户分配角色

角色分配权限

删除角色

删除用户

创建用户 + 默认角色

都需要:

事务

现在我们知道:

为什么 @Transactional 能工作
为什么有时会失效
为什么 self-invocation 是坑

二百一十、从 RBAC 回看 AOP

RBAC 中:

@OperationLog

@RequirePermission

都可以和:

AOP / Interceptor

结合。

现在我们知道:

注解只是声明

真正增强来自代理

二百一十一、本章总结

事务管理最核心:

事务边界
+
异常传播
+
代理

@Transactional 并不是:

贴上就一定生效

必须满足:

Spring Bean

代理调用

正确异常传播

正确数据库事务环境

最重要传播行为:

REQUIRED
默认

REQUIRES_NEW
新事务

事务失效重点:

self-invocation

异常被吞

Checked Exception

自己 new 对象

线程切换

AOP 核心:

Target

Proxy

Join Point

Pointcut

Advice

Aspect

最重要通知:

@Around

最重要代理思想:

外部调用
↓
Proxy
↓
增强
↓
Target

一定牢记:

this.method()

可能绕过:

Proxy

所以事务、日志、权限切面都可能:

不生效

到这里,你已经能从底层解释:

为什么 @Transactional 会生效

为什么它会失效

为什么 Spring AOP 能统一做日志

为什么代理对象如此重要

按照课程表,下一篇进入:

SpringBoot 底层原理

后面会系统讲:

@SpringBootApplication

自动配置

组件扫描

条件装配

Starter

Bean 生命周期

IOC

依赖注入

SpringBoot 启动流程