spring方法调用导致事务失效原因及解决方案
1、事务失效场景复现
背景
我们在平时的工作中写业务逻辑的时候,有可能会遇到这么一个场景:在一个循环中处理事务问题。在使用声明式事务的情况下我们有两种选择,要么把@Transanal注解放在整个循环的方法上,这样的话每次循环的事务都会被管理到,缺点是使用了长事务,会导致锁表问题,影响效率。另一种方案是将每一次循环抽出一个方法,然后把@Transanal注解加在这个方法上。这样spring只管理了本次循环的事务,解决了长事务问题,但是有事务失效的风险。下面我将会模拟这个场景,这种事务失效也是工作中最常遇到的情况。
场景模拟
某公司要给疫情期间不回家过年的员工补贴。每个人发放1000元到员工的账户
使用技术栈
spring-boot 2.2.2.RELEASE
jpa、mysql、lombok 1.18.10
实体类Employee,代表公司的员工:
import lombok.Data;
import javax.persistence.*;
@Data
@Entity
@Table(name = "employee")
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
/**
* 补贴是否已发放
*/
private Boolean hasGiveOut;
}
实体类EmployeeAccount代表员工的账户:
import javax.persistence.*;
import java.math.BigDecimal;
@Data
@Entity
@Table(name = "employee_account")
public class EmployeeAccount {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long employeeId;
@Column(name = "account")
private BigDecimal employeeAccount;
}
下面是两个实体类对应的DAO:
import org.springframework.data.jpa.repository.JpaRepository;
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
}
import org.springframework.data.jpa.repository.JpaRepository;
public interface EmployeeAccountRepository extends JpaRepository<EmployeeAccount, Long> {
EmployeeAccount findByEmployeeId(Long employeeId);
}
下面是业务处理方法:
因为要保证钱发放到账户之后才能将employee的状态改为已发放,所以要使用事务。我们选择使用spring的声明式事务。
@Service
@AllArgsConstructor
public class EmployeeServiceImpl {
private EmployeeRepository employeeRepository;
private EmployeeAccountRepository employeeAccountRepository;
/**
* 方案1:将事件整个方法作为一个事务,保证了一致性
* 但是当员工很多的时候,是一个长事务,有效率问题。不推荐
*/
@Transactional(rollbackFor = Exception.class)
public void giveOutBonusForAllEmployees(){
final List<Employee> allEmployees = employeeRepository.findAll();
for (Employee employee : allEmployees) {
employee.setHasGiveOut(true);
employeeRepository.save(employee);
final EmployeeAccount account = employeeAccountRepository.findByEmployeeId(employee.getId());
account.setEmployeeAccount(account.getEmployeeAccount().add(new BigDecimal("1000")));
employeeAccountRepository.save(account);
}
}
}
测试方法:
@Autowired
private EmployeeServiceImpl employeeService;
@Test
public void testTransaction(){
employeeService.giveOutBonusForAllEmployees();
}
方案一可以被spring事务管理,朋友们可以自行验证。为了解决长事务,我们将业务处理方法抽出来,单独做成一个短事务。
package com.jack.transaction;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.List;
@Service
@AllArgsConstructor
public class EmployeeServiceImpl {
private EmployeeRepository employeeRepository;
private EmployeeAccountRepository employeeAccountRepository;
public void giveOutBonusForAllEmployees(){
final List<Employee> allEmployees = employeeRepository.findAll();
for (Employee employee : allEmployees) {
try{
giveOutOne(employee, new BigDecimal("1000"));
}catch (Exception e){
//一个出错了不管,继续发放其他的
}
}
}
@Transactional(rollbackFor = Exception.class)
public void giveOutOne(Employee employee, BigDecimal money){
employee.setHasGiveOut(true);
employeeRepository.save(employee);
final EmployeeAccount account = employeeAccountRepository.findByEmployeeId(employee.getId());
account.setEmployeeAccount(account.getEmployeeAccount().add(money));
// 模拟抛出异常
if ("jack".equals(employee.getName())){
throw new RuntimeException("保存失败了");
}
employeeAccountRepository.save(account);
}
}
下面我们来验证结果:
原始数据:
select employee.name,employee.has_give_out,ea.account from employee join employee_account ea on employee.id = ea.employee_id;
1
调用测试方法之后的数据:
事务失效了!!!jack损失了1000块大洋
2、声明式事务失效原因
有很多类似的博客贴出了源码来解释这个问题。笔者表达能力有限,不想用太多源码来说明这个原因。我想尽量用自己的语言将这个事务失效的原因告诉读者,源码还是要靠自己去深究。
我们都知道,spring声明式事务是通过AOP实现的。简单点说,所有在方法上加了@Transactional注解的方法,已经被spring代理了,成为了有事务能力的代理方法,只有真正调用到了这个代理方法才能实现事务。而我们代码中的方法调用giveOutOne(employee, new BigDecimal("1000"));实际上是this.giveOutOne(employee, new BigDecimal("1000"));,它调用的仅仅是原对象的原方法,并没有调用到代理方法,自然也就没有事务能力了。
3、解决方案
事务失效的原因大概清楚了,那么解决事务失效的方法的思路也就很清楚了,只要想办法调用到这个代理方法即可。
方法一:从容器中取出代理类,调用它的代理方法。当然也也可以使用@Autowired注入EmployeeServiceImpl,但是这样会显得很奇怪。
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.List;
@Service
public class EmployeeServiceImpl implements ApplicationContextAware {
private EmployeeRepository employeeRepository;
private EmployeeAccountRepository employeeAccountRepository;
public EmployeeServiceImpl(EmployeeRepository employeeRepository, EmployeeAccountRepository employeeAccountRepository) {
this.employeeRepository = employeeRepository;
this.employeeAccountRepository = employeeAccountRepository;
}
private ApplicationContext applicationContext;
public void giveOutBonusForAllEmployees(){
final EmployeeServiceImpl employeeService = applicationContext.getBean(EmployeeServiceImpl.class);
final List<Employee> allEmployees = employeeRepository.findAll();
for (Employee employee : allEmployees) {
try{
employeeService.giveOutOne(employee, new BigDecimal("1000"));
}catch (Exception e){
//一个出错了不管,继续发放其他的
}
}
}
@Transactional(rollbackFor = Exception.class)
public void giveOutOne(Employee employee, BigDecimal money){
employee.setHasGiveOut(true);
employeeRepository.save(employee);
final EmployeeAccount account = employeeAccountRepository.findByEmployeeId(employee.getId());
account.setEmployeeAccount(account.getEmployeeAccount().add(money));
// 模拟抛出异常
if ("jack".equals(employee.getName())){
throw new RuntimeException("保存失败了");
}
employeeAccountRepository.save(account);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
将数据复原,再跑一次测试方法看看:
方法二:使用AOP暴露出来的代理对象,其本质也跟上面的一样。
第一步:在启动类上加注解EnableAspectJAutoProxy,指定使用AspectJ作动态代理,并将代理对象暴露出来exposeProxy = true。
@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true)
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
第二步:在代码中使用AopContext获取代理对象
import lombok.AllArgsConstructor;
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.List;
@Service
@AllArgsConstructor
public class EmployeeServiceImpl{
private EmployeeRepository employeeRepository;
private EmployeeAccountRepository employeeAccountRepository;
public void giveOutBonusForAllEmployees(){
final List<Employee> allEmployees = employeeRepository.findAll();
for (Employee employee : allEmployees) {
try{
((EmployeeServiceImpl)AopContext.currentProxy()).giveOutOne(employee, new BigDecimal("1000"));
}catch (Exception e){
//一个出错了不管,继续发放其他的
}
}
}
@Transactional(rollbackFor = Exception.class)
public void giveOutOne(Employee employee, BigDecimal money){
employee.setHasGiveOut(true);
employeeRepository.save(employee);
final EmployeeAccount account = employeeAccountRepository.findByEmployeeId(employee.getId());
account.setEmployeeAccount(account.getEmployeeAccount().add(money));
// 模拟抛出异常
if ("jack".equals(employee.getName())){
throw new RuntimeException("保存失败了");
}
employeeAccountRepository.save(account);
}
}
验证结果:
4、总结
这个问题在面试中经常会问到的,而且也是工作中时常会发生的。程序员需要学的东西很多,时间久了会遗忘很多之前遇到过的坑,然后再踩一遍,所以以后在工作或学习中遇到一些问题,记录下来,自己偶尔看看。作为同行,如果我的经验有帮助到你,我也很开心,本人才疏学浅,如果文章中有错误的地方,还望大神不吝指教。