Spring方法嵌套调用事务失效原因及解决办法

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、总结
这个问题在面试中经常会问到的,而且也是工作中时常会发生的。程序员需要学的东西很多,时间久了会遗忘很多之前遇到过的坑,然后再踩一遍,所以以后在工作或学习中遇到一些问题,记录下来,自己偶尔看看。作为同行,如果我的经验有帮助到你,我也很开心,本人才疏学浅,如果文章中有错误的地方,还望大神不吝指教。
 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
当在同一个中的方法相互调用时,如果希望事务能够生效,可以采取以下解决办法: 1. 使用代理调用方法:由于Spring事务管理是通过AOP代理实现的,所以可以通过使用代理对象调用方法来触发事务管理。可以通过将方法调用委托给代理对象来确保事务的生效。 2. 将被调用方法抽取到另一个中:将被调用的方法抽取到另一个中,并确保在被调用方法上添加@Transactional注解。这样,在调用方方法中调用被抽取的方法时,事务将能够生效。 3. 使用AspectJ模式的事务管理:Spring还提供了AspectJ模式的事务管理,可以在同一个中的方法相互调用时保持事务的生效。通过配置AspectJ的切面来实现事务的管理,可以细粒度地控制事务的传播行为和回滚条件。 需要注意的是,以上解决办法需要根据具体情况选择合适的方式,并确保在调用的方法上正确地添加@Transactional注解。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [Spring中同一个service中方法相互调用事务不生效问题解决方案](https://blog.csdn.net/a1036645146/article/details/107469578)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值