数据库事务( 五 ) Spring管理事务的几道面试题
1.Spring中的事务管理是如何实现的?
Spring 框架提供了两种机制来管理应用程序中的事务。分别为编程式事务管理和声明式事务管理, 建议使用非入侵式的声明式事务管理, 通过配置文件或者注解来管理事务。
声明式事务管理底层是基于数据库事务和AOP机制的。
1.1.实现步骤
1.1.1.配置事务管理器 (Transaction Manager)
- Spring 需要一个事务管理器来实际执行事务的开启、提交、回滚等操作。
- 通常会配置一个与持久化技术(如 JDBC、JPA、Hibernate 等)相匹配的事务管理器。
- 对于 JDBC 和 JdbcTemplate,可以配置
DataSourceTransactionManager
。 - 对于 MyBatis , 默认事务管理器也是
DataSourceTransactionManager
, 因为 MyBatis 通常使用 JDBC 方式访问数据库,所以事务管理也依赖于DataSource
。 - 对于 JPA,可以配置
JpaTransactionManager
。这个事务管理器使用实体管理器工厂(EntityManagerFactory
)来管理事务。 - 对于 Hibernate,默认事务管理器也是
JpaTransactionManager
,因为 Hibernate 是 JPA 的一种实现。当使用 Hibernate 作为 JPA 提供者时,Spring Boot 会自动配置JpaTransactionManager
。 - 对于 NoSQL 数据库如 MongoDB , 默认事务管理器是
MongoTransactionManager
。
- 对于 JDBC 和 JdbcTemplate,可以配置
1.1.2.启用事务注解驱动 (Enable Transaction Annotation Support)
-
要让 Spring 识别并处理
@Transactional
注解,需要在配置中启用事务注解驱动。 -
在 XML 配置中,可以通过
<tx:annotation-driven>
标签实现。<!-- 配置事务管理器 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"><property name="dataSource" ref="dataSource"/> </bean><!-- 启用事务注解 --> <tx:annotation-driven transaction-manager="transactionManager"/>
-
在 Java 配置中,通过
@EnableTransactionManagement
注解启用。@Configuration @EnableTransactionManagement public class AppConfig {@Beanpublic PlatformTransactionManager transactionManager(DataSource dataSource) {return new DataSourceTransactionManager(dataSource);}// 其他 Bean 定义... }
1.1.3.使用事务注解 (@Transactional
)
-
Spring 支持在类或方法上使用特定的事务注解来声明事务边界。
-
最常见的注解是
@Transactional
,它可以放在类或方法上。@Service public class UserService {@Autowiredprivate UserRepository userRepository;@Transactionalpublic void createUser(User user) {userRepository.save(user);} }
1.1.4.AOP 代理与拦截 (AOP Proxy and Interception)
-
首先对于使用了
@Transactional
注解的Bean,Spring会创建一个代理对象作为Bean, 当调用代理对象的方法时,会先判断该方法上是否加了@Transactional
注解 -
如果加了,那么则利用事务管理器创建一个数据库连接, 并且修改数据库连接的autocommit属性为false, 禁止此连接的自动提交,这是实现Spring事务非常重要的一步
-
然后执行当前方法,方法中会执行sql。执行完当前方法后,如果没有出现异常就直接提交事务
-
如果出现了异常,并且这个异常是需要回滚的就会回滚事务,否则仍然提交事务
1.2.Spring事务管理的隔离级别
Spring事务管理的隔离级别对应的就是数据库的隔离级别
1.3.Spring事务的传播机制
Spring事务的传播机制是Spring事务管理自己实现的,也是Spring事务管理中最复杂的。Spring事务的传播机制是基于数据库连接来做的,一个数据库连接一个事务,如果传播机制配置为需要新开一个事务,那么实际上就是先建立一个数据库连接,在此新数据库连接上执行sql
2.说下 spring 的事务隔离级别
Spring的事务隔离级别是指在并发环境下,事务之间相互隔离的程度。Spring框架支持多种事务隔高级别,可以根据具体的业务需求来选择适合的隔离级别。以下是常见的事务隔离级别:
1.DEFAULT(默认):使用数据库默认的事务隔离级别。通常为数据库的默认隔离级别, 如Oracle为READ COMIMITTED, MySQL为 REPEATABLE READ.
2.**READ UNCOMMITTED(可读取未提交) **: 最低的隔离级别,允许读取未提交的数据。事务可以读取其他事务未提交的数据,可能会导致脏读、不可重复读和幻读的问题。
3.READ COMMITTED(可读已提交):保证一个事务只能读取到已提交的教据,事务读取的数据是其他事务已经提交的教据,避免了脏读的问题,但可能会出现不可重复读和幻读的问题,
4.REPEATABLE READ(可重复读):保证一个事务在同一个査询中多次读取的数据是一致的。事务期间,其他事务对数据的修改不可见,避免了脏读和不可重复读的问题,但可能会出现幻读的问题,
5.SERIALIZABLE(序列化):最高的隔离级别,保证事务串行执行,避免了脏读、不可重复读和幻读的问题。但会降低并发性能,因为事务需要串行执行。
2.1.设置事务隔离级别
1. 在 @Transactional
注解中设置
在方法或类上使用 @Transactional
注解时,可以通过 isolation
属性指定隔离级别。例如:
@Service
public class UserService {@Autowiredprivate UserRepository userRepository;@Transactional(isolation = Isolation.REPEATABLE_READ)public void createUser(User user) {userRepository.save(user);}
}
在这个例子中,createUser
方法的事务隔离级别被设置为 REPEATABLE_READ
。
2. 在 XML 配置中设置
如果您使用 XML 配置,可以在 <tx:method>
标签中设置隔离级别:
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"><property name="dataSource" ref="dataSource"/>
</bean><tx:advice id="txAdvice" transaction-manager="transactionManager"><tx:attributes><tx:method name="createUser*" isolation="REPEATABLE_READ"/></tx:attributes>
</tx:advice><aop:config><aop:pointcut id="txPointcut" expression="execution(* com.example.service.UserService.*(..))"/><aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
</aop:config>
这里,我们设置了名为 createUser*
的方法(以 createUser
开头的方法)的事务隔离级别为 REPEATABLE_READ
。
3. 在 Java 配置中设置
如果您使用 Java 配置,可以在 @Bean
方法中定义事务管理器,并通过 TransactionManagementConfigUtils
设置全局的默认隔离级别,或者在 @Transactional
注解中为特定的方法设置隔离级别:
@Configuration
@EnableTransactionManagement
public class AppConfig {@Beanpublic PlatformTransactionManager transactionManager(DataSource dataSource) {DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();transactionManager.setDataSource(dataSource);return transactionManager;}@Beanpublic TransactionAttributeSource transactionAttributeSource() {NameMatchTransactionAttributeSource source = new NameMatchTransactionAttributeSource();RuleBasedTransactionAttribute readOnlyAttr = new RuleBasedTransactionAttribute();readOnlyAttr.setReadOnly(true);readOnlyAttr.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);readOnlyAttr.setIsolationLevel(Isolation.READ_COMMITTED.value());RuleBasedTransactionAttribute readWriteAttr = new RuleBasedTransactionAttribute();readWriteAttr.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);readWriteAttr.setIsolationLevel(Isolation.REPEATABLE_READ.value());source.addTransactionalMethodMap("com.example.service.UserService", "createUser*", readWriteAttr);return source;}// 其他 Bean 定义...
}
在这个例子中,createUser
方法的事务隔离级别被设置为 REPEATABLE_READ
,并且我们还定义了一个全局的 readOnlyAttr
用于设置只读事务的隔离级别。
2.2.注意事项
- 在设置隔离级别时,请考虑性能和数据一致性之间的权衡。
- 如果您的数据库不支持某些隔离级别,可能会导致异常或不受支持的行为。
- 默认情况下,Spring 使用数据库的默认隔离级别。
3.说下Spring的事务传播特性
事务的传播特性指的是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行?Spring框架提供了多种事务传播行为:
1.REQUIRED: 如果当前存在事务,则加入该事务,如果当前没有事务,则创建一个新的事务。这是最常用的传播行为,也是默认的,适用于大多数情况。
2.REQUIRES NEW:无论当前是否存在事务,都创建一个新的事务。如果当前存在事务,则将当前事务挂起。适用于需要独立事务执,行的场景,不受外部事务的影响。
3.SUPPORTS:如果当前存在事务,则加入该事务,如果当前没有事务,则以非事务方式执行。适用于不需要强制事务的场录,可以与其他事务方法共享事务。
4.NOT SUPPORTED:以非事务方式执行,如果当前存在事务,则将当前事务挂起。适用于不需要事务支持的场景,可以在方法执行期间暂时禁用事务。
5.MANDATORY:如果当前存在事务,则加入该事务,如果当前没有事务,则抛出异常。适用于必须在事务中执行的场景,如果没有事务则会抛出异常。
6.NESTED:如果当前存在事务,则在嵌衰事务中执行,如果当前没有事务,则创建一个新的事务。嵌套事务是外部事务的一部分,可以独立提交或回流。适用于需要在嵌套事务中执行的场景。
7.NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。适用于不允许在事务中执行的场景,如果存在事务则会抛出异常。
3.1.设置事务传播行为
您可以在 @Transactional
注解中使用 propagation
属性来设置事务的传播行为。例如:
@Service
public class UserService {@Autowiredprivate UserRepository userRepository;@Transactional(propagation = Propagation.REQUIRED)public void createUser(User user) {userRepository.save(user);}@Transactional(propagation = Propagation.REQUIRES_NEW)public void updateUser(User user) {userRepository.update(user);}
}
在这个例子中:
createUser
方法使用PROPAGATION_REQUIRED
作为传播行为,这意味着如果当前存在事务,则加入该事务;如果没有,则创建一个新的事务。updateUser
方法使用PROPAGATION_REQUIRES_NEW
作为传播行为,这意味着无论当前是否存在事务,都会创建一个新的事务,并且如果当前有事务,那么这个新的事务将是独立的。
4.哪些情况下会导致Spring事务失效,对应的原因是什么?
1.方法内的自调用 : Spring事务是基于AOP的,只有使用代理对象调用某个方法时,Spring事务才能生效,而在一个方法中使用this.xxx()调用方法时,this并不是代理对象,所以会导致事务失效。 解决办法如下:
a. 把调用方法拆分到另外一个Bean中
b. 自己注入自己
c. AopContext.currentProxy() + @EnableAspectJAutoProxy(exposeProxy = true)
AopContext.currentProxy()
是一个方法,用于返回当前方法调用的 AOP 代理对象。这个方法主要用于在类内部方法调用时,通过代理对象来调用自身的方法,从而使得事务管理生效。
@EnableAspectJAutoProxy
是一个注解,用于启用 AspectJ 风格的 AOP 支持。当使用 exposeProxy = true
选项时,它允许在被通知的方法中访问当前的 AOP 代理对象。
2.方法是private的 : Spring 使用 AOP 代理来实现事务管理。对于 @Transactional
注解的方法,Spring 会创建一个代理对象,当这个方法被调用时,实际上是调用代理对象上的方法。当一个方法被声明为 private
时,它不能从外部类中被直接访问,也就意味着 Spring 无法为其创建代理对象。 解决 : 改成 public
3.方法是final 终态 或者 static 静态 的 : 原因和private
是一样的,也是由于子类不能重写父类中的final
的方法, static 不是由对象而是类直接调用
4.自己捕获了异常 : Spring事务管理是基于抛出未捕获的运行时异常来触发事务回滚。如果您在方法内部捕获了异常并处理了它, 那么也就不会回滚了, 默认情况下Spring会捕获RuntimeException
和 Error
.
5.单独的线程调用方法 : 当Mybatis或JdbcTemplate执行SQL时,会从ThreadLocal中去获取数据库连接对象,如果开启事务的线程和执行SQL的线程是同一个,那么就能拿到数据库连接对象,如果不是同一个线程,那就拿不到数据库连接对象. 这样,Mybatis或JdbcTemplate就会自己去新建一个数据库连接用来执行SQL,此数据库连接的autocommit
为true
,那么执行完SQL就会提交,后续再抛异常也就不能再回滚之前已经提交了的SQL了
6.没加@Configuration注解 : 如果用SpringBoot基本没有这个问题,但是如果用的Spring,那么可能会有这个问题,这个问题的原因其实也是由于Mybatis或JdbcTemplate会从ThreadLocal中去获取数据库连接,但是ThreadLocal中存储的是一个MAP,MAP的key为DataSource对象,value为连接对象,而如果我们没有在AppConfig上添加@Configuration注解的话,会导致MAP中存的DataSource对象和Mvbatis和JdbcTemplate中的DataSource对象不相等, 从而也拿不到数据库连接,导致自己去创建数据库连接了。
7.跨越多个线程的事务管理 : 如果应用程序在多个线程之间共享数据库连接和事务上下文,事务可能会失效, 除非适当地配置事务传播属性。解决: 使用编程式事务或分布式事务
8.类没有被Spring管理 : 如果目标类没有被配置为Spring Bean, 那么事务将无法被应用,解决方法是确保目标类被正确配置为Spring Bean.
9.数据库不支持事务
5.@Transactional(readonly=true)真的能提高性能吗?
5.1.是否提高性能
@Transactional(readonly = true)
注解确实可以在某些情况下提高性能,但这取决于您使用的数据库和持久化技术。
-
减少锁定:
- 当一个事务被标记为只读时,数据库可以减少或消除锁定操作。这是因为只读事务不修改数据,因此不需要担心数据的一致性问题。
- 减少锁定意味着事务之间发生的等待时间更短,从而提高了并发性能。
-
优化查询计划:
- 一些数据库管理系统(DBMS)可能会针对只读事务优化查询计划。例如,只读事务可以利用缓存的结果集,而不必重新执行查询。
- 优化的查询计划可以减少 CPU 和 I/O 的消耗,从而提高性能。
-
隔离级别调整:
- 当事务被标记为只读时,某些数据库可能会自动降低事务的隔离级别。例如,MySQL 可以将只读事务的隔离级别调整为
READ UNCOMMITTED
,从而减少锁定。 - 较低的隔离级别通常意味着较少的锁定,从而提高性能。
- 当事务被标记为只读时,某些数据库可能会自动降低事务的隔离级别。例如,MySQL 可以将只读事务的隔离级别调整为
-
减少日志记录:
- 一些数据库系统在只读事务中可以减少或跳过日志记录操作。例如,Oracle 数据库在只读事务中不会生成重做日志(redo log)。
- 减少日志记录可以降低 I/O 操作的需求,从而提高性能。
5.2.注意事项
- 兼容性:
- 不是所有的数据库都支持只读事务,或者支持的程度不同。您需要查看所使用的数据库文档以了解其支持的特性。
- 对于一些数据库,如 PostgreSQL,需要在数据库层面配置只读事务的支持。
- 性能提升的程度:
- 性能提升的程度取决于多个因素,包括数据库配置、事务的工作负载、并发量等。
- 在一些低并发的情况下,性能提升可能不明显。
- 测试:
- 在生产环境中启用只读事务之前,最好进行详细的测试以评估性能改进的效果。
- 测试时应模拟实际生产环境中的负载,以获得准确的结果。
5.3.与不设置事务比较
@Transactional(readonly=true)
是利用数据库的SET TRANSACTION READ ONLY
,开启只读事务,那只读事务对数据库意味着什么呢?
- 通过执行
SET TRANSACTION READ ONLY
, 将当前事务设置为只读事务。这意味着在此事务内部, 任何修改数据的操作 ( 如INSERT
、UPDATE
、DELETE
) 都将被禁止,只能执行读取操作(如SELECT
)。 - 只读事务依然会运用隔离级别(MVCC),需要事务隔离级别就需要一定性能开销。
所以到底要不要设置只读,不设置是不是一样的?
一般情况下,执行查询时不开启事务的性能可能会稍微优于开启只读事务。这是因为不开启事务的査询操作不会涉及事务管理和隔离级别的开销,因此可能更为轻量级和高效。
当执行查询操作时,如果不需要事务的隔离级别和一致性保证,并且不需要使用事务管理的功能,那么不开启事务可能是更为合适的选择。这种情况下,查询操作将立即执行并返回结果,不会受到事务管理和隔离级别的开销影响。然而,需要注意的是,如果应用需要保证数据的一致性和隔离性,**并且希望査询操作与其他事务的修改行为相互独立,那么开启只读事务是必要的。**在这种情况下,虽然可能会有一些额外的性能开销,但可以保证数据的一致性和隔离性,避免了并发操作可能引起的数据不一致问题。
综上所述,选择是否开启只读事务还是不开启事务,取决于具体的应用场景和需求。如果应用需要保证数据的一致性和隔离性那么开启只读事务是必要的;如果不需要这些保证,并且追求查询操作的性能优化,那么不开启事务可能更为合适。
6.Spring 事务回滚什么类型的异常?
在 Spring 中,事务回滚是由事务管理器自动处理的,它基于异常类型来决定是否回滚事务。Spring 的事务管理默认只对未被捕获的运行时异常(unchecked exceptions)进行回滚。这意味着,除非您明确配置事务管理器,否则事务将在遇到以下类型的异常时自动回滚:
-
运行时异常 (
RuntimeException
):NullPointerException
IllegalArgumentException
IndexOutOfBoundsException
- 等等
-
错误 (
Error
):OutOfMemoryError
StackOverflowError
- 等等
对于检查型异常(checked exceptions),如 IOException
或 SQLException
等,默认情况下事务不会自动回滚。这是因为检查型异常通常需要被捕获并处理,而运行时异常则表示程序存在逻辑错误,应该回滚事务以防止数据不一致。
用户可以自定义事务回滚策略
如果您希望改变默认的事务回滚策略,可以通过以下几种方式来实现:
-
使用
@Transactional(rollbackFor = ...)
:- 您可以在
@Transactional
注解中使用rollbackFor
属性来指定回滚事务的异常类型。
import org.springframework.transaction.annotation.Transactional;@Service public class UserService {@Transactional(rollbackFor = Exception.class)public void createUser(User user) {// 数据库操作} }
在这个例子中,事务将在遇到任何类型的异常时回滚。
- 您可以在
-
使用
noRollbackFor
属性:- 您也可以使用
noRollbackFor
属性来指定不应该回滚事务的异常类型。
@Transactional(rollbackFor = Exception.class, noRollbackFor = IOException.class) public void createUser(User user) {// 数据库操作 }
在这个例子中,事务将回滚所有异常,除了
IOException
。 - 您也可以使用
默认情况下,Spring 的事务管理器在遇到运行时异常时会自动回滚事务。如果您希望改变这一行为,可以通过在 @Transactional
注解中使用 rollbackFor
和 noRollbackFor
属性来自定义事务回滚策略。此外,您还可以通过 TransactionDefinition
更细粒度地控制事务的回滚规则。