一、数据库事务基本概念
1.1 事务的定义
事务(Transaction)是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。事务是数据库并发控制和恢复的基本单位,具有不可分割的特性------事务中的所有操作要么全部执行成功,要么全部不执行。
在关系型数据库中,事务通常由一条或多条SQL语句组成。以银行转账为例:从账户A扣款100元并向账户B增加100元,这两个操作必须作为一个整体------要么都成功,要么都失败回滚,绝不能出现"钱扣了但没到账"的情况。
1.2 ACID特性
事务的可靠性由ACID四个核心特性来保证:
**原子性(Atomicity):**事务是一个不可分割的最小工作单元。事务中的所有操作要么全部完成,要么全部不执行。如果在执行过程中发生错误,系统会将已经执行的操作回滚到事务开始前的状态。
**一致性(Consistency):**事务执行前后,数据库必须从一个一致性状态转换到另一个一致性状态。一致性是由业务规则和数据库约束(如主键、外键、唯一约束、CHECK约束等)共同定义的。
**隔离性(Isolation):**多个事务并发执行时,每个事务都感觉不到其他事务的存在,仿佛自己是数据库中唯一运行的事务。隔离性通过锁机制和多版本并发控制(MVCC)来实现。
**持久性(Durability):**一旦事务被提交,其结果就是永久性的,即使系统发生崩溃也不会丢失。InnoDB通过Redo Log(重做日志)和Double Write Buffer(双写缓冲区)来保证持久性
二、事务隔离级别
2.1 并发事务引发的问题
当多个事务同时操作同一数据时,如果不加以控制,会出现以下三类典型问题:
**脏读(Dirty Read):**一个事务读取到另一个未提交事务的修改数据。如果那个事务最终回滚,读取到的就是"脏数据"------这些数据从未真正存在于数据库中。
**不可重复读(Non-Repeatable Read):**一个事务内两次读取同一行数据,但两次读取的结果不同。这是因为在两次读取之间,另一个事务修改并提交了该行数据。
**幻读(Phantom Read):**一个事务按照相同条件两次查询,第二次查询结果的行数发生了变化(增多或减少)。这是因为另一个事务在此期间插入或删除了满足条件的行。幻读与不可重复读的核心区别:幻读关注的是行数的变化(INSERT/DELETE),不可重复读关注的是同一行内容的变化(UPDATE)。
2.2 四种隔离级别
SQL标准定义了四种事务隔离级别,从低到高依次为:
**READ UNCOMMITTED(读未提交):**最低级别。事务可以读取其他事务未提交的修改。存在脏读、不可重复读、幻读问题。几乎不用于生产环境,仅在某些需要非阻塞读的日志/监控场景中有零星应用。
**READ COMMITTED(读已提交):**事务只能读取其他事务已提交的数据,解决了脏读问题。但不可重复读和幻读仍然存在。这是Oracle和PostgreSQL的默认隔离级别。
**REPEATABLE READ(可重复读):**事务在执行期间看到的数据始终保持一致------同一事务内多次读取同一行数据结果相同。解决了脏读和不可重复读问题,但理论上仍存在幻读。这是MySQL InnoDB的默认隔离级别。
**SERIALIZABLE(可串行化):**最高级别。事务完全串行执行,通过强制排序彻底杜绝所有并发问题。代价是并发性能极低,通常只在金融等对数据一致性要求极高的场景中使用。
三、@Transactional 用法
1.准备条件
在非springboot项目中,想要使用@Transactional 注解,要在配置类中开启事务注解的支持
java
@Configuration
@ComponentScan("com.xxx")
@PropertySource("classpath:jdbc.properties")
@EnableAspectJAutoProxy //开启aspectj注解的支持 aop动态代理
@EnableTransactionManagement //开启事务注解的支持
public class JavaConfig {
......
2. 基本使用
@Transactional注解可用于类 或public方法上,用于声明该类/方法需要被事务管理,Spring会自动完成"开启事务→执行方法→提交事务/回滚事务"的流程。
java
@Transactional(rollbackFor = Exception.class)
public void changAge(){
dao.updateAgeById(110,1);
}
3.关键属性
- rollbackFor:指定需要回滚的异常类型,最常用 rollbackFor = Exception.class。默认只回滚RuntimeException和Error, checked异常(如IOException)不会回滚,必须显式指定。
- propagation:事务传播机制,默认REQUIRED(有事务则加入,无则新建),日常开发无需修改,特殊场景(如嵌套事务)可调整。
- readOnly:设为true表示只读事务,仅用于查询场景,可优化数据库性能(避免开启写事务)。
- timeout:超时时间,默认:永远不超时 -1,设置timeout = 时间 秒数 超时时间,就会回滚事务和释放异常
3.1事务的传播机制
- REQUIRED (默认):当前有事务就加入,没有就新建一个。
- 最常用,适合大多数业务场景。
- 内外层方法共享同一个事务,任意一方失败都会回滚。
- **SUPPORTS** :当前有事务就加入,没有就以非事务方式执行。
- 方法本身不强制要求事务。
- 适合查询类操作,有事务时能参与,没有也不影响。
- **MANDATORY** :必须在现有事务中运行,没有事务就抛异常。
- 强制要求调用方已开启事务。
- 适合内部辅助方法,确保不会被单独调用。
- **REQUIRES_NEW** :总是新建事务,如果有现有事务就挂起。
- 内外层事务独立,互不影响。
- 适合日志记录、审计等需要独立提交的操作。
- **NOT_SUPPORTED** :以非事务方式执行,如果有现有事务就挂起。
- 方法本身不需要事务。
- 适合耗时较长且不需要回滚的操作。
- **NEVER** :必须以非事务方式执行,有事务就抛异常。
- 与 MANDATORY 相反。
- 适合明确不需要事务的场景。
- **NESTED** :当前有事务就创建嵌套事务,没有就新建事务。
- 嵌套事务可以独立回滚,不影响外层。
- 适合需要部分回滚的复杂业务场景。
3.2 显示的给事务添加传播机制
java
@Transactional(propagation = Propagation.REQUIRED)
public void changAge(){
dao.updateAgeById(110,1);
}
4.什么时候该用?什么时候不用?
核心原则:有多条写操作(增/删/改),需要原子性(要么全成功、要么全回滚),就必须加;单条写操作、纯查询,坚决不加。
| 场景 | 是否加@Transactional | 说明 |
| 多条增删改(如订单创建+扣库存) | 是 | 保证原子性,避免部分成功、部分失败 |
| 单条增/删/改 | 否 | 数据库单条SQL本身就是原子性,加注解多余且耗性能 |
| 纯查询(查列表、查详情) | 否 | 查询无需提交/回滚,加注解会开启无用事务 |
| 复杂多表联查(大数据量) | 可选 | 可加@Transactional(readOnly = true)优化性能 |
| 无DB操作(仅参数校验、调用第三方接口) | 否 | 无事务需要管理,加注解无意义 |
|---|
5.@Transactional 失效场景(高频坑,必避)
事务失效的核心本质只有一个:没有通过Spring生成的代理对象调用带事务的方法,或代理机制被破坏。以下是100%会遇到的失效场景,按出现频率排序。
1. 同类内部方法调用(自调用,最常见)
这是最容易踩的坑!同一个类中,无事务方法调用有事务方法,事务必失效。
一个方法加了事务注解,那么spring会对这个类生成一个对应的代理对象,放入spring容器。
如果同类自己调用自己,使用的this对象。不是用的代理对象。this对象调用其他方式不会对其他方法增强的。只有代理对象才会对要调用的方法增强。所以事务会失效。
只有代理对象里面才有方法被事务加持。
java
@Service
public class UserService {
// 无事务方法
public void methodA() {
// 自调用:this.methodB(),不走代理,事务失效
methodB();
}
// 有事务方法
@Transactional(rollbackFor = Exception.class)
public void methodB() {
// 数据库操作
}
}
原因:methodA直接调用methodB,是原生对象(this)调用,绕过了Spring代理,AOP无法拦截事务逻辑。
注意:哪怕两个方法都加@Transactional,自调用依然失效!
2. 方法修饰符错误(private/final/static)
java
// 失效:private方法,动态代理无法重写
@Transactional
private void save() {}
// 失效:final方法,动态代理无法重写
@Transactional
public final void save() {}
// 失效:static方法,不属于对象实例,代理无法拦截
@Transactional
public static void save() {}
规则:@Transactional只能用在public、非final、非static方法上。
3. 异常被try-catch"吃掉"
java
@Transactional(rollbackFor = Exception.class)
public void save() {
try {
// 数据库操作
insertData();
} catch (Exception e) {
// 异常被捕获,未抛出,Spring无法感知异常,不会回滚
e.printStackTrace();
}
}
原因:Spring事务默认只有"未捕获的异常"才会触发回滚,捕获异常后不抛出,事务会正常提交。
4. 类未被Spring管理(无@Component/@Service等)
java
// 失效:未加@Service,不是Spring Bean,AOP无法代理
public class UserService {
@Transactional
public void save() {}
}
原因:只有Spring管理的Bean,才会被AOP代理,事务注解才会生效。
四、用法总结:
场景1:同一个类中,A→B→C 多层调用,均有写库操作(需整体原子性)
最优方案:只给最外层入口方法加事务,内部方法改为private,直接调用(无需注入自己、无需AopContext,无循环依赖)。
java
@Service public class BizService {
// 唯一事务入口:只给最外层public方法加注解
@Transactional(rollbackFor = Exception.class) public void methodA() {
// A自身写库操作
insertA();
// 内部直接调用private方法,无代理问题,事务生效
methodB();
}
// 内部方法:private,不加事务注解
private void methodB() {
// B自身写库操作
insertB();
methodC();
}
// 内部方法:private,不加事务注解
private void methodC() {
// C自身写库操作
insertC();
// 任意环节抛异常,整体回滚
if (true) { throw new RuntimeException("异常回滚");}
}
// 私有数据库操作方法
private void insertA() { /* mapper操作 */ }
private void insertB() { /* mapper操作 */ }
private void insertC() { /* mapper操作 */ } }
}
场景2:跨类调用(A→B→C,分属不同Service)
最优方案:只给最外层入口方法加事务,内部跨类调用天然走代理,无需额外处理。