Spring 事务管理是一种确保数据一致性和完整性的机制。它允许开发者在操作数据库时将多个步骤封装在一个事务中,要么全部成功,要么在出错时全部回滚。Spring 提供了声明式和编程式事务管理方式,通常使用 @Transactional 注解进行声明式事务管理,以简化事务处理。事务具有四个基本特性:原子性、一致性、隔离性和持久性(ACID),确保在并发环境中数据的可靠性。
事务基本特性(ACID 原则)
原子性 (Atomicity)
事务是一个不可分割的操作单位,要么全部执行成功,要么全部不执行。原子性确保了在事务执行过程中,若发生错误,所有已执行的操作都会被回滚,数据库将恢复到事务开始之前的状态。
一致性 (Consistency)
在事务执行前后,数据库必须保持一致性状态。也就是说,事务的执行不会破坏数据的完整性约束。完成一个事务后,数据库的状态应该是有效的,不论是在事务开始前还是结束后。
隔离性 (Isolation)
隔离性确保多个事务并发执行时,互不干扰。一个事务的执行不应受到其他事务的影响。隔离级别决定了事务间的可见性,通常分为以下几种:
持久性 (Durability)
一旦事务被提交,其结果是永久性的,即使系统崩溃也不会丢失。持久性保证了数据库在事务提交后的数据将被持久保存,通常通过日志或其他持久存储机制实现。
事务隔离级别
会出现的问题
@Transactional 注解是 Spring Framework 中用于声明式事务管理的关键注解。它允许开发者在方法上或者类上标记事务的边界,使得 Spring 在执行这些方法时自动管理数据库事务。
@Transactional 可以标注在类上或者方法上:
类上:表示该类中的所有public方法都应该在一个事务中执行。
方法上:仅表示该方法在事务中执行,类中的其他方法不受影响。
@Transactional 提供了多个属性,帮助开发者更精细地控制事务的行为:
value:指定事务管理器的名称。如果没有指定,默认使用唯一的事务管理器。
propagation:指定事务的传播行为,默认为 Propagation.REQUIRED,表示如果存在一个事务,则加入这个事务;如果没有,则创建一个新的事务。
Propagation.REQUIRED:
说明:如果当前存在事务,则加入该事务;如果没有,则创建一个新的事务。
示例:方法 A 调用方法 B。如果方法 A 在事务中,方法 B 也在同一个事务中执行;如果方法 A 没有事务,方法 B 会创建一个新的事务
Propagation.SUPPORTS
说明:如果当前存在事务,则加入该事务;如果没有,则以非事务方式执行。
示例:方法 A 调用方法 B。如果方法 A 在事务中,方法 B 将在同一事务中执行;如果方法 A 没有事务,方法 B 将以非事务方式执行。
Propagation.MANDATORY:
说明:如果当前存在事务,则加入该事务;如果当前不存在事务,则抛出异常。
示例:方法 A 调用方法 B。如果方法 A 没有事务,调用将失败并抛出异常;如果有,则方法 B 在该事务中执行。
Propagation.REQUIRES_NEW:
说明:重新创建一个新的事务,如果当前存在事务,暂停当前的事务。
示例:方法 A 调用方法 B。如果方法 A 有事务,方法 B 将创建一个新的事务,方法 A 的事务会被挂起。两者的事务相互独立。 A方法中调用 B方法操作数据库, A方法抛出异常后,B方法不会进行回滚。
Propagation.NOT_SUPPORTED:
说明:以非事务的方式运行,如果当前存在事务,暂停当前的事务。
示例:方法 A 调用方法 B。如果方法 A 有事务,方法 B 将在非事务状态下执行,方法 A 的事务会被挂起。
Propagation.NEVER:
说明:以非事务的方式运行,如果当前存在事务,则抛出异常。
示例:方法 A 调用方法 B。如果方法 A 有事务,将抛出异常;如果没有事务,则正常执行。
Propagation.NESTED :
说明:如果当前存在事务,则创建一个嵌套事务;如果没有事务,则行为与 PROPAGATION_REQUIRED 相同。
示例:方法 A 调用方法 B。如果方法 A 有事务,方法 B 将创建一个嵌套事务,可以独立提交或回滚。如果方法 A 没有事务,则方法 B 会创建一个新事务。
isolation:指定事务的隔离级别,默认为 Isolation.DEFAULT。常用的隔离级别有:
timeout:设置事务的超时时间,单位为秒。超过该时间,事务会被强制回滚。
readOnly:设置为 true 表示该事务是只读的,通常用于查询操作,优化性能。
rollbackFor:指定哪些异常会导致事务回滚。可以是异常类或异常类的数组。
noRollbackFor:指定哪些异常不会导致事务回滚。
代码示例:
public class UserService {
@Transactional(
propagation = Propagation.REQUIRES_NEW,
isolation = Isolation.READ_COMMITTED,
readOnly = true,
timeout = 5, // 事务超时时间
rollbackFor = Exception.class // 指定回滚的异常类型
)
public User getUserById(Long id) {
// 查询用户
...
}
}
场景:A类中,有两个方法,方法A1和A2;方法A1调用方法A2;方法A1被其他类调用。
示例1:方法A1和A2都添加@Transactional注解
@Service
public class AClass {
@Transactional(rollbackFor = Exception.class)
public void A1() {
// 业务逻辑
A2();
}
@Transactional
public void A2() {
// 执行数据库操作,假设抛了异常
// ...
throw new RuntimeException("函数异常!");
}
}
结果:A1被注解为 @Transactional,那么在 A1被调用时会开启一个事务。由于 A1内部调用 A2是在同一个实例内,A2会在同一个事务上下文中执行,事务会正常生效,同类调用,不涉及事务传播,相当于A2的代码加到了A1方法内。
整个调用过程都在 A1的事务控制下,包括对 A2的调用。这意味着,如果 A1中发生异常,事务将会回滚,包括 A2的操作。
如果 A1被标记为 @Transactional,而 A2没有被标记为事务,则事务依然会生效,因为它是在同一个事务上下文中运行。
示例2:方法A1不添加@Transactional注解,A2添加@Transactional注解
@Service
public class AClass {
public void A1() {
// 业务逻辑
A2();
}
@Transactional(rollbackFor = Exception.class)
public void A2() {
// 执行数据库操作,假设抛了异常
// ...
throw new RuntimeException("函数异常!");
}
}
结果:当其他类调用 A1时,A1运行在没有事务的上下文中。随后在 A1中调用 A2,由于 A2是在 A1的上下文中直接调用的,所以它不会启动一个新的事务。
在这种情况下,A2的 @Transactional 注解不会生效,因为 Spring 的事务管理机制依赖于代理模式,同类方法调用不会调用代理对象的方法。只有当事务方法通过 Spring 的代理进行调用时,事务管理才会生效。因此,A2中的数据库操作将会直接执行,而不受事务控制。
如果 A2中发生异常,A1也不会回滚,因为 A1本身没有事务。如果需要进行回滚,aFunction 应该被标记为 @Transactional
场景:两个类分别为A类、B类;A类有方法A、B类有方法B;A类方法A调用B类方法B;A类方法A被另一个C类调用。
示例1:方法A添加注解,方法B不添加注解
@Service
public class AClass {
@Autowired
private BClass bClass;
@Transactional(rollbackFor = Exception.class)
public void A() {
// 执行数据库操作
bClass.B();
}
}
@Service
public class BClass {
public void B() {
// 执行数据库操作,假设抛了异常
// ...
throw new RuntimeException("函数执行有异常!");
}
}
结果:两个函数对数据库的操作都回滚了,因为 A被标记为 @Transactional,当它被调用时,会开启一个事务,当 A调用 B时,因为 B没有事务控制,所以它将直接在当前的事务上下文中执行。如果 B抛出异常,异常将会传播回 A,因为 A处于事务管理之下,如果 B抛出一个异常,A将会捕获到这个异常,并且会导致整个事务回滚。
示例2:方法A不添加注解,方法B添加注解
@Service
public class AClass {
@Autowired
private BClass bClass;
public void A() {
// 执行数据库操作
bClass.B();
}
}
@Service
public class BClass {
@Transactional(rollbackFor = Exception.class)
public void B() {
// 执行数据库操作,假设抛了异常
// ...
throw new RuntimeException("函数执行有异常!");
}
}
结果:A、B都不会回滚。因为方法 A 运行在没有事务的上下文中,方法 B 的 @Transactional 注解不会生效,如果方法 B 抛出异常,方法 A 也会捕获到这个异常,但不会导致任何事务回滚,因为方法 A 没有事务控制。
示例3:A、B两个函数都添加事务注解;B抛异常;A抓出异常
@Service
public class AClass {
@Autowired
private BClass bClass;
@Transactional(rollbackFor = Exception.class)
public void A() {
try {
// 执行数据库操作
bClass.B();
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Service
public class BClass {
@Transactional(rollbackFor = Exception.class)
public void B() {
// 执行数据库操作,假设抛了异常
// ...
throw new RuntimeException("函数异常!");
}
}
结果:整个事务不会提交,且抛异常org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only,所有在方法A和方法B中的数据库操作都会被回滚。
因为两个函数用的是同一个事务;B函数抛了异常,调了事务的rollback函数,并且事务被标记了只能rollback了;程序继续执行,A函数里面把异常给抓出来了,这个时候A函数没有抛出异常,既然没有异常那事务就需要提交,会调事务的commit函数;而之前这个事务已经被标记了只能rollback-only(因为是同一个事务),因此直接就抛异常了。
示例4:A、B两个函数都添加注解;B抛异常,A抓出异常;这里B函数@Transactional注解加了一个参数propagation = Propagation.REQUIRES_NEW,控制事务的传播行为,表明是一个新的事务
@Service
public class AClass {
@Autowired
private BClass bClass;
@Transactional(rollbackFor = Exception.class)
public void A() {
try {
// 执行数据库操作
bClass.B();
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Service
public class BClass {
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void B() {
// 执行数据库操作,假设抛了异常
// ...
throw new RuntimeException("函数异常!");
}
}
结果:B函数里面的操作回滚了,A函数里面的操作成功了;因为两个函数不是同一个事务了,所以B函数抛异常只会导致B的回滚,不影响A所在事务的正常执行。
场景: 如果在同一个类内部的方法中调用被 @Transactional 注解的方法,事务可能不会生效。
解决办法: 将事务方法放在不同的服务类中,或者使用 Spring 的 AOP 代理机制,使其能够通过代理进行调用。
场景: 当通过非代理对象(例如直接使用 this)调用 @Transactional 方法时,事务将失效。
解决办法: 确保通过 Spring 容器管理的代理对象调用事务方法,避免使用 this。
场景: 默认情况下,只有未检查的异常(RuntimeException)会导致事务回滚,检查异常不会导致回滚。
解决办法: 如果需要针对检查异常进行回滚,可以使用 rollbackFor 属性指定异常类型。
场景: 选择不当的事务传播行为可能导致事务失效,例如使用 PROPAGATION_NOT_SUPPORTED 会以非事务方式执行。
解决办法: 根据业务需求选择合适的传播行为,确保方法调用之间的事务关系。
场景: 嵌套事务可能不会按照预期工作,特别是在使用 PROPAGATION_NESTED 时。
解决办法: 了解嵌套事务的行为,并确保使用合适的事务传播策略。
场景: 在多线程环境中,事务可能无法正常传播。
解决办法: 确保在同一线程中处理事务,或者使用合适的方式管理跨线程事务。