Bootstrap

SpringBoot 使用 SpringDataJpa 报错事务回滚失效原因排查

回顾

近期遇到的了这样的问题,在使用 SpringBoot + SpringDataJpa 的时候,明明在方法上添加了 @Transactional 注解,但是在操作数据库的时候却没有启用事务,方法中所作的每一条 SQL 操作都直接提交了.而不是合并为同一个事务进行提交.下面对我遇到的问题做一下场景回顾.

代码回顾

首先对我代码做一下伪代码描述:

@RestController
@RequestMapping("/")
public class MainController{
	@Resource
	MainService mainService;
	@RequestMapping("/")
	public String index(){
		mainService.sync();
		return "success";
	}
}
@Service
public class MainServcie(){
	@Autowired
	UserRepository userRepository;
	public void sync(){
		doSync();
	}
	@Transactional(rollbackFor = Exception.class)
	public void doSync(){
		userRepository.delete();
		User user1 = User.builder().name("zhangsan").gender("female").build();
		userRepository.save(user1);
	}
}
public interface UserRepository extends SimpleJpaRepository<User>{
	@Query(value="delete from user" nativeQuery = true)
	@Modify
	@Transactional(rollbackFor=Exceptional.class)
	void delete();
}
@Data
@Builder
public class User{
	@Id
	private Long id;
	private String name;
	private String gender;
}

从代码中可以看到,我在 MainController 中调用了 MainService 中的 sync 方法,然后在 sync 方法中又调用了添加了事务注解的 doSync 方法,其中在 doSync 方法中执行了对于 SQL 的删除操作和添加操作.

我在写完这套逻辑之后再执行的过程中却发现对 SQL 的删除和添加操作并没有在事务中进行,当我在 SQL 的删除操作之后添加断点并去数据库中执行查询逻辑时可以看到删除逻辑已经执行了,即 mysql 的会话隔离级别表现为 “读未提交” .

在这里插入图片描述

读未提交(READ_UNCOMMITED) A事务中可以读取到 B 事务中已经执行但是还没有提交的数据.事务隔离界别为该级别时有可能造成不可重复度,脏读和幻读等问题.

但并非是我的 mysql 事务隔离级别的设置问题.那么这个问题是怎么产生的呢?能想到的大概就是方法中的 删除 和 添加 操作作为独立的事务执行了,当删除操作执行后事务已经提交,此时在执行添加操作前去数据库中执行查询操作时,因为删除操作的事务已经提交,即在其他事务中删除操作是可见的.

经过上述的分析大概可以找到原因了,是我添加在 doSync 方法上面的 @Transactional 注解并没有生效,使得本来应该在 doSync 方法中的两个事务合并在 doSync 方法中作为一个事务执行,但是却没有这样,从而导致 doSync 方法中的两个事务是分别执行的.最终产生了上述现象

但是明明添加了事务注解但是为什么没有生效呢?Spring的事务不是开箱即用的吗?为什么怀疑人生?是这样,导致事务失效的原因有很多,但是上述代码中有一个很明显的问题从而导致 doSync 方法的事务注解失效,那就是显式调用了 doSync 方法.

显式调用导致的事务注解失效

从上述代码中可以看到,我并没有通过bean mainService.doSync() 方法去调用 doSync 这个事务方法的,而是通过调用了 mainService.sync 方法间接调用了 doSync 方法,姑且不讨论这样写的代码是否多此一举.在我们的正常调用过程中这两种调用感觉并没有什么区别.但是当我们把 mainService 这个 bean 交由 Spring 管理时,这两种调用方式就会出现些许的不同.

在这里插入图片描述
简单来说,当我们在通过 sync 方法去调用 doSync 方法时,是并没有走动态代理的.而动态代理中存在着 Spring 实现事务注解的全部逻辑,因此当我们通过 sync 方法显式调用 doSync 方法时,就会发现 doSync 方法的事务注解失效了.

解决方案也很简单,通过bean调用是可以实现事务注解的.

参考资料

At a high level, Spring creates proxies for all the classes annotated with @Transactional – either on the class or on any of the methods. The proxy allows the framework to inject transactional logic before and after the running method – mainly for starting and committing the transaction.

What’s important to keep in mind is that, if the transactional bean is implementing an interface, by default the proxy will be a Java Dynamic Proxy. This means that only external method calls that come in through the proxy will be intercepted. Any self-invocation calls will not start any transaction, even if the method has the @Transactional annotation.

Another caveat of using proxies is that only public methods should be annotated with @Transactional. Methods of any other visibilities will simply ignore the annotation silently as these are not proxied.

Transactions with Spring and JPA

数据库引擎配置异常导致事务失效

如果说上面的错误是不了解代码导致的注解失效,那么引擎配置异常导致事务注解失效我觉得怎么找也应该给个注解失效的日志报错之类的也好,SpringDataJpa不是全自动的ORM映射框架吗,保姆级别的提示应该做好才对.

我这里主要使用的数据库是 MySQL,虽然现在用 MyIsam 人很少(我自己从来没用过,印象中好像也没见周围人用过).由于 MyIsam 引擎的实现原理,使得它在并没有提供事务的支持.因为没有过多的研究,这里不做更多的深入,简单的说一下配置的问题:
我最早的配置是这样子的:

#jpa
spring.jpa.database-platform=org.hibernate.dialect.MySQL5Dialect

通过一番查找,发现这样配置方言是不行的,于是按照网上的教程又修改为下面的配置:

#jpa
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect

说实话,并没有搞清楚这两种配置有什么区别,但是在 MySQL5InnoDBDialect 类中可以明显看到这货重写了一个方法:

@Override
public String getTableTypeString() {
	return " ENGINE=InnoDB";
}

而这货重写之前的样子是这样的:

public String getTableTypeString() {
	// grrr... for differentiation of mysql storage engines
	return "";
}

查看源码,这个方法似乎是和 SpringDataJpa 建表存在很大的关系,但是和事务有什么关系就不清楚了.头发多的童鞋可以再往下深究一下.

参考资料

Spring Boot 2.0 使用data JPA @Transactional 报错事务不回滚

上面就是我在使用 SpringDataJpa 遇到的事务失效问题的分析,简单看一下出错原因都比较幼稚.希望可以帮到你.

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;