文章目录
概述
在使用Spring框架时,可以有两种使用事务的方式,一种是编程式的,一种是声明式的,@Transactional
注解就是声明式的。
首先,事务这个概念是数据库层面的,Spring只是基于数据库中的事务进行了扩展,以及提供了一些方便程序员更加容易操作事务的方式。
在一个方法加上了 @Transactional
注解之后,Spring会基于这个类生成代理对象,会将这个代理对象作为bean,当在使用这个代理对象的方法时,如果方法上存在 @Transactional
注解,那么代理逻辑会先把事务的自动提交设置为false,然后再去执行原本的业务逻辑方法,如果执行业务逻辑方法没有出现异常,那么代理逻辑就会将事务提交;反之,当业务逻辑方法发生异常,则会将事务回滚。
当然,针对哪些异常回滚事务也是可以配置的,可以通过 @Transactional
注解中的rollbackFor属性进行指定,默认情况下会对 RuntimeException和Error 进行回滚。
示例
针对下文用例,定义:
数据库表:
CREATE TABLE `user` (
`id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` VARCHAR(16) NOT NULL COMMENT 'name',
`age` INT NOT NULL COMMENT 'age',
PRIMARY KEY(`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='用户表'
实体类:
public class User{
private Long id;
private String name;
private Integer age;
// 略get/set方法
}
事务注解失效原因列举
-
@Transactional
注解标注的方法修饰符为非public时,@Transactional
注解将不会生效例如以下代码:定义一个
@Transactional
注解实现,修饰一个默认访问修饰符的方法@Service public class UserService { @Autowired private UserMapper mapper; @Transactional void defaultMethodInsert() { int count = mapper.insert(new User(1L,"Tom", 19)); if (count > 0) { throw new RuntimeException("exception intercept"); } mapper.insert(new User(2L,"Mary", 18)); } }
测试用例:
@RunWith(SpringRunner.class) @SpringBootTest(classes = {CustomerApplication.class}) public class TransactionFailureTest { @Autowired private UserService userService; @Test public void testInvoke() { userService.methodInsert(); } }
输出结果:
测试用例结果显示抛出异常,但数据库表写入 Tom。事务没有开启,因此在方法抛出异常时,Tom的写入没有进行回滚;如果
InvokcationService#invokeInsertTestWrongModifier
方法的访问修饰符改为public的话,将会正常开启事务,Tom和Mary的写入会同时进行回滚。 -
@Transactional
注解标注的方法被final或static修饰事务是通过AOP实现,即通过代理的方式完成,将一个方法定义成final意味着方法不能被重写,那么事务就失效了。
我们将 UserService#defaultMethodInsert的修饰符改成final,同样测试出当异常抛出,Tom的写入没有被回滚。
注意:如果某个方法是static时,意味着该方法属于类本身,同样无法通过动态代理,变成事务方法。 -
在类内部调用类内部
@Transactional
标注的方法;业务逻辑方法新增
UserService#testInnerInvoke
调用类内部的事务方法UserService#methodInsert
@Service public class UserService { @Autowired private UserMapper mapper; @Transactional public void methodInsert() { int count = mapper.insert(new User(1L,"Tom", 19)); if (count > 0) { throw new RuntimeException("exception intercept"); } mapper.insert(new User(2L,"Mary", 18)); } public void testInnerInvoke() { this.methodInsert(); } }
测试用例:
@RunWith(SpringRunner.class) @SpringBootTest(classes = {CustomerApplication.class}) public class TransactionFailureTest { @Autowired private UserService userService; @Test public void methodInnerInvoke() { System.out.println("methodInnerInvoke >>>"); userService.testInnerInvoke(); } @Test public void methodOuterInvoke() { System.out.println("methodOuterInvoke >>>"); userService.methodInsert(); } }
输出结果:
执行测试用例中
TransactionFailureTest#methodInnerInvoke
方法测试方法内部调用@Transactional
,Tom的写入事务操作同样不会进行回滚,即事务已失效。执行测试用例中
TransactionFailureTest#methodOuterInvoke
方法测试,Tom和Mary的写入事务都已进行回滚,即事务生效; -
未被Spring管理
在开发过程中容易忽略一些细节问题,比如忘记了@Controller、@Service、@Component、@Repository等注解。
没使用以上注解交给spring进行管理,那么事务就不会生效。
应用@Transactional
的类必须是Spring管理的bean,如果该类没有被Spring容器管理,则事务不会生效。 -
数据库引擎不支持事务
mysql5之前,默认的数据库引擎是MYISAM,它的好处是:索引文件和数据文件是分开存储的,对于查多写少的单表操作,性能比innodb更好。
但是数据库引擎(如MyISAM)不支持事务,此时@Transactional
注解将不会生效。 -
异常处理不当
如果在事务方法中捕获了异常但是没有抛出,或者抛出了检查型异常(即继承自
Exception
而非RuntimeException
),那么事务可能不会回滚。
在业务逻辑方法内修改UserService#methodInsert
,捕捉异常后只打印文字;@Service public class UserService { @Autowired private UserMapper mapper; @Transactional public void methodInsert() { try { int count = mapper.insert(new User(1L,"Tom", 19)); if (count > 0) { throw new RuntimeException("exception intercept"); } mapper.insert(new User(2L,"Mary", 18)); } catch (Exception e) { System.out.println("catch exception >>>"); } } }
测试用例:
@RunWith(SpringRunner.class) @SpringBootTest(classes = {CustomerApplication.class}) public class TransactionFailureTest { @Test public void methodCatchExInvoke() { System.out.println("methodCatchExInvoke >>>"); userService.methodInsert(); } }
输出结果:
执行测试用例方法,结果显示Tom写入事务操作未回滚,即事务失效。
当然,@Transactional注解的rollbackFor属性可以指定异常类型进行回滚,当方法抛出的异常非rollbackFor指定的类型,事务操作同样不会进行回滚。
-
数据源未配置事务管理器
Spring事务需要使用支持事务的
DataSource
并配置PlatformTransactionManager
,如果未正确配置,则事务无法生效。 -
传播行为不匹配
事务的传播行为(propagation behavior)需要匹配调用栈,如果配置不当,可能会导致事务意外提交或回滚。
其实,在使用@Transactional注解时,是可以指定propagation参数的。
该参数得作用是指定事务的传播特性,spring目前支持7中传播特性:
REQUIRED;SUPPORTS;MANDATORY;REQUIRES_NEW;NOT_SUPPORTED;NEVER;NESTED
如果事务设置成Propagation.NEVER,这种类型的传播特性不支持事务,如果有事务则抛出异常。 -
AOP切面顺序问题
如果项目中存在多个AOP切面,并且它们的顺序配置不当,可能会导致事务切面未能正确执行,从而影响事务的生效。
-
注解标注位置问题
如果将
@Transactional
注解标注在接口方法上,并且使用CGLIB作为代理方式,则无法解析到该注解,导致事务失效。通常建议将注解标注在接口的实现类方法上。
总结
总之为了避免@Transactional
注解失效,需要确保方法的访问权限正确、没有被final或static修饰、正确配置Spring容器和数据库支持事务、正确处理异常、以及正确配置AOP切面顺序等。同时,在编写代码时,应尽量避免在同一个类中通过this
调用事务方法,而是应该通过Spring容器或其他方式调用。