1. 程式人生 > 其它 >spring方法呼叫導致事務失效原因及解決方案

spring方法呼叫導致事務失效原因及解決方案

技術標籤:踩坑記錄springjava

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;

在這裡插入圖片描述呼叫測試方法之後的資料:
在這裡插入圖片描述事務失效了!!!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、總結

這個問題在面試中經常會問到的,而且也是工作中時常會發生的。程式設計師需要學的東西很多,時間久了會遺忘很多之前遇到過的坑,然後再踩一遍,所以以後在工作或學習中遇到一些問題,記錄下來,自己偶爾看看。作為同行,如果我的經驗有幫助到你,我也很開心,本人才疏學淺,如果文章中有錯誤的地方,還望大神不吝指教。