【日常开发问题】在同一个对象中无法创建两个事务的坑

本贴最后更新于 2222 天前,其中的信息可能已经时异事殊

情景:有个朋友 A,最近给我说了一个问题,说是在同一个对象 OBJECT1#METHOD1 调用 OBJECT1#METHOD2 时无法开启新的事务。

1.先上代码


代码1:
@Transactional
public void saveA(){
	User user = new User();
	user.setPassword("admin1");
	user.setUsername("26041467711");
	user.setAppAuth("asdfasd1");
	user.setNickname("asd1");
	userRepository.save(user);
	try{
			saveB();
	}catch (RuntimeException e){
			e.printStackTrace();
	}
}

//挂起当前事务,创建一个新事务
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveB(){
		User user = new User();
		user.setPassword("admin2");
		user.setUsername("26041467712");
		user.setAppAuth("asdfasd2");
		user.setNickname("asd2");
		userRepository.save(user);
		throw new RuntimeException();
}

通过这段代码,我们能理解出他想做什么:在方法 B 抛出异常时,B 的事务能回滚,而 A 的事物则不回滚。

我们运行这段代码后得到结果:

可以看到,这不是我们想要的结果。

2.问题排查

首先我们知道的:

1.spring 通过 AOP编程实现事务的管理
2.spring 实现AOP的模式为代理模式
3.spring 实现代理模式的两种方式:jdk动态代理与CGLIB

我们知道,这个 Service 方法被 Controller 调用时,Controller 中的 Service 对象是通过 @Autowired 自动注入的,也就是说这个对象确实是由 spring 容器在管理。

我们直接调用这个方法,按理说 B 方法的事务应该会生效的。当我们把代码改成如下样子时:

代码 2:

@Transactional
@Override
public void saveA(){
    User user = new User();
    user.setPassword("admin1");
    user.setUsername("26041467711");
    user.setAppAuth("asdfasd1");
    user.setNickname("asd1");
    userRepository.save(user);
    saveB();
}

@Transactional
@Override
public void saveB(){
    User user = new User();
    user.setPassword("admin2");
    user.setUsername("26041467712");
    user.setAppAuth("asdfasd2");
    user.setNickname("asd2");
    userRepository.save(user);
    throw new RuntimeException();
}
	

执行这段代码:会发现,两个方法的插入都没有成功,说明事物成功回滚。

正因为这段代码没问题,所以许多没遇坑的开发者会觉得文章开始的那段代码也是没问题的。

简单的代理模式

这幅图简单的描述了 spring 如何使用 mysql 的事务,通过为目标对象创建一个代理对象来完成事务的。

通过这张图,我们可以得出以下假设:
  1. 代码 1 的 this.saveB()的 this 并非 spring 容器里面的对象。
现在我们去验证这个结论:

结果为 false,证明了我们的观点,虽然在直观上,我调用我这个真实的方法,但是其实在使用 spring @Autowired 时,对象就已经成为代理对象了。

有一个误区,很多人在判断是否为一个对象的时候会将两个对象直接打印进行对比。

public void test(){
    IUserService userService = Util.getBean(IUserService.class);
    System.out.println(String.format("userService:%s", userService));
    System.out.println(String.format("this:%s", this));
}
	

结果:

userService:net.onemost.web.service.impl.UserServiceImpl@57e03347
this:net.onemost.web.service.impl.UserServiceImpl@57e03347

发现居然是一样的,然后就认为这是同一个对象,这是不对的,如果要比较两个对象是否为同一个对象:

1.使用==或者equals(),如果没有对Object的equals()进行重写,那么equals()就是将==封装了
2.也可以通过hashCode()方法来比较,前提是hashCode()方法没有被重写。

我们再来看看两个对象的超类:

public void test(){
    IUserService userService = Util.getBean(IUserService.class);
    System.out.println(String.format("userService:%s", userService.getClass().getSuperclass().getName()));
    System.out.println(String.format("this:%s", this.getClass().getSuperclass().getName()));
}

结果:

	userService:net.onemost.web.service.impl.UserServiceImpl
	this:java.lang.Object

看到了吧,getBean 得到的对象,是一个由 CGLIB(因为 JDK 的动态代理只能通过实现接口的方式,而 CGLIB 可以通过继承原来的实现类来实现代理)生成的动态代理类。

结论

如果要开启一个新事务,必须使用 spring 的代理类,所以在调用会开启新事务的方法时,要确保这个对象是代理对象而不是真实对象。

  • 数据库

    据说 99% 的性能瓶颈都在数据库。

    340 引用 • 708 回帖
  • 事务
    23 引用 • 21 回帖 • 1 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...