Spring事务
1. 引言
在企业级应用中,事务管理是保证数据一致性和完整性的核心机制。Spring Boot作为一款主流的Java后端开发框架,提供了便捷的事务管理支持。本篇文章将深入探讨Spring Boot事务管理的各个方面,包括不同的事务隔离级别、使用场景、注意事项以及基于注解和编程式事务的实现方法。我们还将比较这两种事务管理方式的优缺点,以帮助开发者在实际应用中选择最合适的事务管理策略。
2. 什么是事务?
事务(Transaction)是指一系列操作的集合,这些操作要么全部成功,要么全部失败,确保数据的一致性和完整性。事务的四个基本特性通常简称为ACID:
- 原子性(Atomicity):事务中的所有操作要么全部成功,要么全部失败。
- 一致性(Consistency):事务结束后,数据必须保持一致状态。
- 隔离性(Isolation):一个事务的执行不能被其他事务干扰。
- 持久性(Durability):事务一旦提交,其结果是永久的。
3. 事务隔离级别
事务的隔离性确保了并发事务
的正确执行。数据库系统通常提供四种隔离级别,每种隔离级别在并发事务处理时都能防止不同类型的数据一致性问题。
3.1 读未提交(Read Uncommitted)
描述:允许一个事务读取另一个未提交
事务的数据。这是最低的隔离级别。
使用场景:通常用于不太关注数据一致性的场景,如日志收集等。这种隔离级别可以导致"脏读”(Dirty Read)。
注意事项:
- 脏读:可能会读到其他事务未提交的数据,导致数据不一致。
- 风险:较高,不适合大多数业务场景。
示例:
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void readUncommittedExample() {
// 执行数据库操作
}
测试:
/**
* 正常事务
*/
@Transactional
public void test1() {
userMapper.insert(getUser());
List<User> users = userMapper.selectList(null);
users.forEach(System.out::println);
try {
// 暂停100s,用于测试读未提交
Thread.sleep(100000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 手动抛出异常,测试事务回滚
throw new RuntimeException();
}
// 测试读未提交
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void readUncommittedTest() {
List<User> users = userMapper.selectList(null);
// test1暂停,事务未提交,数据库中数据为空,但是能查出1条数据
users.forEach(System.out::println);
}
测试结果:
test1()暂停期间,readUncommittedTest()方法能够查询到test1()中未提交的数据。
3.2 读已提交(Read Committed)
描述:保证一个事务只能读取另一个事务已提交
的数据,防止脏读。这是大多数数据库系统的默认隔离级别,如SQL Server。
使用场景:适合需要避免脏读,但可以接受不可重复读(Non-repeatable Read)的场景。
注意事项:
- 不可重复读:同一事务中的两次相同查询可能得到不同的结果。
- 风险:适中,适合大多数在线事务处理系统。
示例:
@Transactional(isolation = Isolation.READ_COMMITTED)
public void readCommittedExample() {
// 执行数据库操作
}
测试:
@Transactional
public void test1() {
userMapper.insert(getUser());
try {
// 暂停10s,用于测试读已提交
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("事务已经提交");
}
// 测试读已提交
@Transactional(isolation = Isolation.READ_COMMITTED)
public void readCommittedTest() {
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
// test1 未提交
List<User> users1 = mapper.selectList(null);
System.out.println();
// 清空一级缓存,保证第二次查询是查询数据库
sqlSession.clearCache();
try {
// 暂停20s,保证test1已经执行完成,事务已经提交
Thread.sleep(20000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// test1 已提交
List<User> users2 = userMapper.selectList(null);
System.out.println();
}
测试结果:
两次查询结果不一致,user1查询到1条数据,user2在test1执行完成提交事务后查询,查询到2条数据。说明读已提交级别可以读取到其他事务中已经提交的数据,有可能造成多次查询结果不一致的情况。
3.3 可重复读(Repeatable Read)
描述:保证同一事务中多次读取的数据一致
。即使其他事务修改了数据,当前事务的结果也不会改变。
使用场景:适用于需要确保数据一致性,防止不可重复读的场景,例如财务应用。
注意事项:
- 幻读:一个事务中多次查询时,如果数据被其他事务插入或删除,可能会得到不同的数据行集。
- 风险:较低,适合需要严格数据一致性的应用场景。
示例:
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void repeatableReadExample() {
// 执行数据库操作
}
测试:
@Transactional
public void test1() {
userMapper.insert(getUser());
try {
// 暂停10s,用于测试读已提交
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("事务已经提交");
}
// 测试可重复读
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void repeatableReadTest() {
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
// test1 未提交
List<User> users1 = mapper.selectList(null);
System.out.println();
// 清空一级缓存,保证第二次查询是查询数据库
sqlSession.clearCache();
try {
// 暂停20s,保证test1已经执行完成,事务已经提交
Thread.sleep(20000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// test1 已提交
List<User> users2 = userMapper.selectList(null);
System.out.println();
}
测试结果:
可重复读的隔离级别下,事务内的多次读取结果都是一样的,即使其他事务已经提交修改数据。
3.4 串行化(Serializable)
描述:最高的隔离级别,确保事务串行执行
。这意味着在一个事务完成之前,其他事务不能操作同一数据。
使用场景:适用于需要最高数据一致性和完整性的场景,但性能开销大。
注意事项:
- 性能:严重影响性能,因为会锁住很多数据,导致并发度降低。
- 风险:最低,适用于金融系统和其他需要严格数据完整性的系统。
示例:
@Transactional(isolation = Isolation.SERIALIZABLE)
public void serializableExample() {
// 执行数据库操作
}
测试:
// 测试串行化
@Transactional(isolation = Isolation.SERIALIZABLE)
public void serializableTest() {
User user = new User();
user.setUserId(1853288506208837634L);
user.setUsername("Test");
// 使用串行化的隔离级别修改数据
userMapper.updateById(user);
System.out.println();
try {
// 暂停100s,给其他线程测试
Thread.sleep(100000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println();
}
测试结果:
在串行化的隔离级别下,其他事务可以查询数据,但是修改、删除数据的操作将被阻塞,知道串行化事务提交之后,才能执行。
4. Spring Boot中事务管理的实现
在Spring Boot中,事务管理可以通过注解
和编程式
两种方式实现。
4.1 基于注解的事务管理
基于注解的事务管理是Spring Boot中最常见的事务管理方式,使用@Transactional
注解来声明事务的行为。
4.1.1 使用方法
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserService {
@Transactional
public void createUser(User user) {
// 业务逻辑
}
}
4.1.2 优点
简单易用:只需在方法上加上@Transactional注解即可,代码更简洁。
自动管理:Spring框架自动处理事务的开始和提交/回滚。
4.1.3 缺点
灵活性较差:无法精细控制事务的各个阶段和行为。
不适合复杂场景:对于需要动态决定事务行为的场景不太适用。
4.1.4 注意事项
事务传播属性:@Transactional注解提供了[[事务传播属性]](https://blog.csdn.net/qq_27184497/article/details/116525588)选项(如REQUIRED, REQUIRES_NEW等),需要根据具体业务需求进行配置。
方法可见性:事务管理只在public方法上有效。private方法上的@Transactional注解不会被Spring代理。
4.1.5 事务失效的场景
在Spring Boot中使用基于注解的事务管理时,有一些特定场景可能导致事务失效。这些场景大多源于Spring事务管理的工作原理和Spring AOP(面向切面编程)的特性。下面我们详细探讨几个常见的导致事务失效的场景。
方法的可见性(非 public 方法)
描述:Spring的事务管理基于AOP(Aspect-Oriented Programming),而AOP代理仅适用于public方法
。如果你在一个private、protected或包级可见性的方法上使用@Transactional注解,该注解将不会生效。
示例:
@Service
public class UserService {
// 事务不会生效,因为方法不是public的
@Transactional
private void saveUser(User user) {
// 业务逻辑
}
}
解决方案:确保@Transactional注解仅应用于public方法。
自调用(Self-invocation)
描述:自调用是指在同一个类中一个方法调用另一个方法
。Spring的事务管理是基于代理的,当一个事务性方法调用另一个同样有事务注解的方法时,如果是通过this调用,即自调用,事务将不会生效,因为Spring AOP的代理机制在这种情况下不会拦截调用。
示例:
@Service
public class UserService {
@Transactional
public void publicMethod() {
// 自调用
this.privateMethod();
}
@Transactional
private void privateMethod() {
// 事务不会生效
}
}
解决方案:将两个事务性方法拆分到不同的类中,或者在外部通过注入的方式调用。
非代理对象调用(Direct Method Call)
描述:如果在非代理对象上直接调用事务性方法,事务将不会生效
。例如,当你在同一个类中调用另一个带有@Transactional注解的方法时,由于不是通过Spring代理调用,事务将不会生效。
示例:
@Service
public class UserService {
@Transactional
public void methodA() {
// 方法B事务不会生效,因为是直接调用
methodB();
}
@Transactional
public void methodB() {
// 业务逻辑
}
}
解决方案:确保事务性方法调用是通过Spring管理的代理对象。
异常未被正确抛出
描述:默认情况下,Spring只对未被捕获的RuntimeException
或Error类型的异常进行回滚。如果事务性方法中抛出了CheckedException
(如Exception
类或其子类),事务不会回滚,除非明确指定。
示例:
@Transactional
public void saveUser(User user) {
try {
// 业务逻辑
} catch (IOException e) {
// 捕获了CheckedException,事务不会回滚
logger.error("Exception occurred", e);
}
}
解决方案:修改方法以抛出异常,或者在@Transactional
注解中指定rollbackFor
属性。
多线程环境中使用事务
描述:Spring的事务管理是基于线程绑定的
。因此,如果你在一个事务性方法中启动了一个新线程,那么新线程中执行的操作不在原始事务的控制范围内。
示例:
@Transactional
public void saveUser(User user) {
new Thread(() -> {
// 新线程,事务不生效
doSomething();
}).start();
}
解决方案:避免在事务性方法中启动新线程,或者使用Spring的异步支持(@Async
)并确保在相同的上下文中使用事务。
事务传播行为配置错误
描述:事务传播行为决定了一个事务方法是如何与当前事务进行关联的。如果配置不正确,事务可能不会按预期工作。例如,如果一个REQUIRED
传播的事务性方法调用了一个REQUIRES_NEW
传播的事务性方法,那么原始事务将被挂起,新事务将被创建。
示例:
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void newTransactionMethod() {
// 这个方法会挂起外部事务
}
解决方案:根据业务需求正确配置事务传播行为。
使用了不支持事务的数据库操作
描述:某些数据库操作如DDL(Data Definition Language)操作和非事务性数据源(如某些NoSQL数据库),并不支持事务。在这些操作上使用事务管理是无效的。
解决方案:在使用事务时,确保数据库和数据源支持事务。
4.2 基于编程式的事务管理
编程式事务管理提供了更大的灵活性,可以在代码中显式地控制事务的开始、提交和回滚。
4.2.1 使用方法
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
@Service
public class UserService {
@Autowired
private PlatformTransactionManager transactionManager;
public void createUser(User user) {
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus status = transactionManager.getTransaction(def);
try {
// 业务逻辑
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
throw e;
}
}
}
4.2.2 优点
灵活性高:开发者可以精细控制事务的各个阶段及行为。
适用于复杂场景:例如,动态决定事务行为、嵌套事务等场景。
4.2.3 缺点
代码冗长:需要显式地管理事务状态,代码量大且复杂。
易出错:开发者需自行管理事务的提交和回滚,容易在错误处理中遗漏。
4.2.4 注意事项
事务管理器的配置:需确保配置正确的PlatformTransactionManager
实例,通常是DataSourceTransactionManager
。
异常处理:要在事务管理代码块中精细地进行异常捕获和处理,避免遗漏导致事务不正确提交或回滚。
5. 基于注解和编程式事务管理的比较
5.1 优缺点总结
5.2 实践中的选择
在实际项目中,大多数情况下使用基于注解的事务管理方式,因为它简单且易于维护,适合大多数的CRUD操作。而在一些复杂的业务场景中,例如需要根据不同条件动态决定事务行为或涉及多个数据源的分布式事务,编程式事务管理提供了更高的灵活性和控制力。
6. 事务管理的注意事项
选择合适的隔离级别:根据具体的业务需求和性能要求选择合适的事务隔离级别,以平衡数据一致性和并发性能。
事务传播机制的合理配置:不同的传播行为(如REQUIRED, REQUIRES_NEW等)会对事务的执行产生不同影响,需根据实际场景配置。
避免过长的事务:过长的事务会占用数据库资源,导致其他事务等待甚至死锁,应尽量缩短事务的执行时间。
正确处理异常:确保在事务中正确捕获和处理异常,以防止事务未能正确提交或回滚。
7. 总结
事务管理是Spring Boot中确保数据一致性和完整性的重要机制。在本文中,我们详细介绍了Spring Boot事务管理的各个方面,包括不同的事务隔离级别及其使用场景、基于注解和编程式的事务管理实现方式及其优缺点。通过合理配置和选择事务管理策略,可以有效提升系统的稳定性和性能。开发者应根据具体的业务需求和场景,灵活应用Spring Boot事务管理功能,确保数据的准确性和一致性。
评论区