SpringBoot事务管理_AOP思想详解
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 启动流程