Spring Boot中的事務是如何實現的

1. 概述

一直在用SpringBoot中的@Transactional來作事務管理,可是不多沒想過SpringBoot是如何實現事務管理的,今天從源碼入手,看看@Transactional是如何實現事務的,最後咱們結合源碼的理解,本身動手寫一個相似的註解來實現事務管理,幫助咱們加深理解。java

閱讀說明:本文假設你具有Java基礎,同時對事務有基本的瞭解和使用。spring

2. 事務的相關知識

開始看源碼以前,咱們先回顧下事務的相關知識。sql

2.1 事務的隔離級別

事務爲何須要隔離級別呢?這是由於在併發事務狀況下,若是沒有隔離級別會致使以下問題:數據庫

  • 髒讀(Dirty Read) :當A事務對數據進行修改,可是這種修改尚未提交到數據庫中,B事務同時在訪問這個數據,因爲沒有隔離,B獲取的數據有可能被A事務回滾,這就致使了數據不一致的問題。segmentfault

  • 丟失修改(Lost To Modify): 當A事務訪問數據100,而且修改成100-1=99,同時B事務讀取數據也是100,修改數據100-1=99,最終兩個事務的修改結果爲99,可是實際是98。事務A修改的數據被丟失了。緩存

  • 不可重複讀(Unrepeatable Read):指A事務在讀取數據X=100的時候,B事務把數據X=100修改成X=200,這個時候A事務第二次讀取數據X的時候,發現X=200了,致使了在整個A事務期間,兩次讀取數據X不一致了,這就是不可重複讀。bash

  • 幻讀(Phantom Read):幻讀和不可重複讀相似。幻讀表如今,當A事務讀取表數據時候,只有3條數據,這個時候B事務插入了2條數據,當A事務再次讀取的時候,發現有5條記錄了,無緣無故多了2條記錄,就像幻覺同樣。併發

不可重複讀 VS 幻讀ide

不可重複讀的重點是修改 : 一樣的條件 , 你讀取過的數據 , 再次讀取出來發現值不同了,重點在更新操做。 幻讀的重點在於新增或者刪除:一樣的條件 , 第 1 次和第 2 次讀出來的記錄數不同,重點在增刪操做。spring-boot

因此,爲了不上述的問題,事務中就有了隔離級別的概念,在Spring中定義了五種表示隔離級別的常量:

常量 說明
TransactionDefinition.ISOLATION_DEFAULT 數據庫默認的隔離級別,MySQL默認採用的 REPEATABLE_READ隔離級別
TransactionDefinition.ISOLATION_READ_UNCOMMITTED 最低的隔離級別,容許讀取未提交的數據變動,可能會致使髒讀、幻讀或不可重複讀
TransactionDefinition.ISOLATION_READ_COMMITTED 容許讀取併發事務已經提交的數據,能夠阻止髒讀,可是幻讀或不可重複讀仍有可能發生
TransactionDefinition.ISOLATION_REPEATABLE_READ 對同一字段的屢次讀取結果都是一致的,除非數據是被自己事務本身所修改,**能夠阻止髒讀和不可重複讀,但幻讀仍有可能發生。**MySQL中經過MVCC解決了該隔離級別下出現幻讀的可能。
TransactionDefinition.ISOLATION_SERIALIZABLE 串行化隔離級別,該級別能夠防止髒讀、不可重複讀以及幻讀,可是串行化會影響性能。

2.2 Spring中事務的傳播機制

爲何Spring中要搞一套事務的傳播機制呢?這是Spring給咱們提供的事務加強工具,主要是解決方法之間調用,事務如何處理的問題。好比有方法A、方法B和方法C,在A中調用了方法B和方法C。僞代碼以下:

MethodA{
	MethodB;
	MethodC;
}
MethodB{

}
MethodC{

}
複製代碼

假設三個方法中都開啓了本身的事務,那麼他們之間是什麼關係呢?MethodA的回滾會影響MethodBMethodC嗎?Spring中的事務傳播機制就是解決這個問題的。

Spring中定義了七種事務傳播行爲:

類型 說明
PROPAGATION_REQUIRED 若是當前沒有事務,就新建一個事務,若是已經存在一個事務中,加入到這個事務中。這是最多見的選擇
PROPAGATION_SUPPORTS 支持當前事務,若是當前沒有事務,就以非事務方式執行。
PROPAGATION_MANDATORY 使用當前的事務,若是當前沒有事務,就拋出異常。
PROPAGATION_REQUIRES_NEW 新建事務,若是當前存在事務,把當前事務掛起。
PROPAGATION_NOT_SUPPORTED 以非事務方式執行操做,若是當前存在事務,就把當前事務掛起。
PROPAGATION_NEVER 以非事務方式執行,若是當前存在事務,則拋出異常。
PROPAGATION_NESTED 若是當前存在事務,則在嵌套事務內執行。若是當前沒有事務,則執行與PROPAGATION_REQUIRED相似的操做。

這七種傳播機制是如何影響事務的,感興趣的同窗能夠閱讀這篇文章

3. 如何實現異常回滾的

回顧完了事務的相關知識,接下來咱們正式來研究下Spring Boot中如何經過@Transactional來管理事務的,咱們重點看看它是如何實現回滾的。

在Spring中TransactionInterceptorPlatformTransactionManager這兩個類是整個事務模塊的核心,TransactionInterceptor負責攔截方法執行,進行判斷是否須要提交或者回滾事務。PlatformTransactionManager是Spring 中的事務管理接口,真正定義了事務如何回滾和提交。咱們重點研究下這兩個類的源碼。

TransactionInterceptor類中的代碼有不少,我簡化一下邏輯,方便說明:

//如下代碼省略部份內容
	public Object invoke(MethodInvocation invocation) throws Throwable {
	//獲取事務調用的目標方法
		Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
	//執行帶事務調用
		return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
	}

複製代碼

invokeWithinTransaction 簡化邏輯以下:

//TransactionAspectSupport.class
	//省略了部分代碼
	protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
			final InvocationCallback invocation) throws Throwable {
			Object retVal;
			try {
			//調用真正的方法體
				retVal = invocation.proceedWithInvocation();
			}
			catch (Throwable ex) {
				// 若是出現異常,執行事務異常處理
				completeTransactionAfterThrowing(txInfo, ex);
				throw ex;
			}
			finally {
			//最後作一下清理工做,主要是緩存和狀態等
				cleanupTransactionInfo(txInfo);
			}
			//若是沒有異常,直接提交事務。
			commitTransactionAfterReturning(txInfo);
			return retVal;
	
	}
複製代碼

事務出現異常回滾的邏輯completeTransactionAfterThrowing以下:

//省略部分代碼
protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
				//判斷是否須要回滾,判斷的邏輯就是看有沒有聲明事務屬性,同時判斷是否是在目前的這個異常中執行回滾。
			if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
				//執行回滾
					txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
			} 
			else {
						//不然不須要回滾,直接提交便可。
					txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
			
			}
		}
	}
複製代碼

上面的代碼已經把Spring的事務的基本原理說清楚了,如何進行判斷執行事務,如何回滾。下面到了真正執行回滾邏輯的代碼中PlatformTransactionManager接口的子類,咱們以JDBC的事務爲例,DataSourceTransactionManager就是jdbc的事務管理類。跟蹤上面的代碼rollback(txInfo.getTransactionStatus())能夠發現最終執行的代碼以下:

@Override
	protected void doRollback(DefaultTransactionStatus status) {
		DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
		Connection con = txObject.getConnectionHolder().getConnection();
		if (status.isDebug()) {
			logger.debug("Rolling back JDBC transaction on Connection [" + con + "]");
		}
		try {
		//調用jdbc的 rollback進行回滾事務。
			con.rollback();
		}
		catch (SQLException ex) {
			throw new TransactionSystemException("Could not roll back JDBC transaction", ex);
		}
	}
複製代碼

3.1 小結

這裏小結下Spring 中事務的實現思路,Spring 主要依靠 TransactionInterceptor 來攔截執行方法體,判斷是否開啓事務,而後執行事務方法體,方法體中catch住異常,接着判斷是否須要回滾,若是須要回滾就委託真正的TransactionManager 好比JDBC中的DataSourceTransactionManager來執行回滾邏輯。提交事務也是一樣的道理。

這裏用個流程圖展現下思路:

流程圖

4. 手寫一個註解實現事務回滾

咱們弄清楚了Spring的事務執行流程,那咱們能夠模仿着本身寫一個註解,實現遇到指定異常就回滾的功能。這裏持久層就以最簡單的JDBC爲例。咱們先梳理下需求,首先註解咱們能夠基於Spring 的AOP來實現,接着既然是JDBC,那麼咱們須要一個類來幫咱們管理鏈接,用來判斷異常是否回滾或者提交。梳理完就開幹吧。

4.1 首先加入依賴

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
複製代碼

4.2 新增一個註解

/** * @description: * @author: luozhou * @create: 2020-03-29 17:05 **/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface MyTransaction {
    //指定異常回滾
    Class<? extends Throwable>[] rollbackFor() default {};
}
複製代碼

4.3 新增鏈接管理器

該類幫助咱們管理鏈接,該類的核心功能是把取出的鏈接對象綁定到線程上,方便在AOP處理中取出,進行提交或者回滾操做。

/** * @description: * @author: luozhou * @create: 2020-03-29 21:14 **/
@Component
public class DataSourceConnectHolder {
    @Autowired
    DataSource dataSource;
    /** * 線程綁定對象 */
    ThreadLocal<Connection> resources = new NamedThreadLocal<>("Transactional resources");

    public Connection getConnection() {
        Connection con = resources.get();
        if (con != null) {
            return con;
        }
        try {
            con = dataSource.getConnection();
            //爲了體現事務,所有設置爲手動提交事務
            con.setAutoCommit(false);
        } catch (SQLException e) {
            e.printStackTrace();
        }
        resources.set(con);
        return con;
    }

    public void cleanHolder() {
        Connection con = resources.get();
        if (con != null) {
            try {
                con.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        resources.remove();
    }
}
複製代碼

4.4 新增一個切面

這部分是事務處理的核心,先獲取註解上的異常類,而後捕獲住執行的異常,判斷異常是否是註解上的異常或者其子類,若是是就回滾,不然就提交。

/** * @description: * @author: luozhou * @create: 2020-03-29 17:08 **/
@Aspect
@Component
public class MyTransactionAopHandler {
    @Autowired
    DataSourceConnectHolder connectHolder;
    Class<? extends Throwable>[] es;

    //攔截全部MyTransaction註解的方法
    @org.aspectj.lang.annotation.Pointcut("@annotation(luozhou.top.annotion.MyTransaction)")
    public void Transaction() {

    }

    @Around("Transaction()")
    public Object TransactionProceed(ProceedingJoinPoint proceed) throws Throwable {
        Object result = null;
        Signature signature = proceed.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        if (method == null) {
            return result;
        }
        MyTransaction transaction = method.getAnnotation(MyTransaction.class);
        if (transaction != null) {
            es = transaction.rollbackFor();
        }
        try {
            result = proceed.proceed();
        } catch (Throwable throwable) {
            //異常處理
            completeTransactionAfterThrowing(throwable);
            throw throwable;
        }
        //直接提交
        doCommit();
        return result;
    }
		/** * 執行回滾,最後關閉鏈接和清理線程綁定 */
    private void doRollBack() {
        try {
            connectHolder.getConnection().rollback();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            connectHolder.cleanHolder();
        }

    }
		/** *執行提交,最後關閉鏈接和清理線程綁定 */
    private void doCommit() {
        try {
            connectHolder.getConnection().commit();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            connectHolder.cleanHolder();
        }
    }
		/** *異常處理,捕獲的異常是目標異常或者其子類,就進行回滾,不然就提交事務。 */
    private void completeTransactionAfterThrowing(Throwable throwable) {
        if (es != null && es.length > 0) {
            for (Class<? extends Throwable> e : es) {
                if (e.isAssignableFrom(throwable.getClass())) {
                    doRollBack();
                }
            }
        }
        doCommit();
    }
}
複製代碼

4.5 測試驗證

建立一個tb_test表,表結構以下:

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for tb_test
-- ----------------------------
DROP TABLE IF EXISTS `tb_test`;
CREATE TABLE `tb_test` (
  `id` int(11) NOT NULL,
  `email` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

SET FOREIGN_KEY_CHECKS = 1;
複製代碼

4.5.1 編寫一個Service

saveTest方法調用了2個插入語句,同時聲明瞭@MyTransaction事務註解,遇到NullPointerException就進行回滾,最後咱們執行了除以0操做,會拋出ArithmeticException。咱們用單元測試看看數據是否會回滾。

/** * @description: * @author: luozhou kinglaw1204@gmail.com * @create: 2020-03-29 22:05 **/
@Service
public class MyTransactionTest implements TestService {
    @Autowired
    DataSourceConnectHolder holder;
		//一個事務中執行兩個sql插入
   @MyTransaction(rollbackFor = NullPointerException.class)
    @Override
    public void saveTest(int id) {
        saveWitharamters(id, "luozhou@gmail.com");
        saveWitharamters(id + 10, "luozhou@gmail.com");
        int aa = id / 0;
    }
		//執行sql
   private void saveWitharamters(int id, String email) {
        String sql = "insert into tb_test values(?,?)";
        Connection connection = holder.getConnection();
        PreparedStatement stmt = null;
        try {
            stmt = connection.prepareStatement(sql);
            stmt.setInt(1, id);
            stmt.setString(2, email);
            stmt.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    
}
複製代碼

4.5.2 單元測試

@SpringBootTest
@RunWith(SpringRunner.class)
class SpringTransactionApplicationTests {
    @Autowired
    private TestService service;

    @Test
    void contextLoads() throws SQLException {
        service.saveTest(1);
    }

}
複製代碼

上圖代碼聲明瞭事務對NullPointerException異常進行回滾,運行中遇到了ArithmeticException異常,因此是不會回滾的,咱們在右邊的數據庫中刷新發現數據正常插入成功了,說明並無回滾。

咱們把回滾的異常類改成ArithmeticException,把原數據清空再執行一次,出現了ArithmeticException異常,這個時候查看數據庫是沒有記錄新增成功了,這說明事物進行回滾了,代表咱們的註解起做用了。

5. 總結

本文最開始回顧了事務的相關知識,併發事務會致使髒讀丟失修改不可重複讀幻讀,爲了解決這些問題,數據庫中就引入了事務的隔離級別,隔離級別包括:讀未提交讀提交可重複讀串行化

Spring中加強了事務的概念,爲了解決方法A、方法B和方法C之間的事務關係,引入了事務傳播機制的概念。

Spring中的@Transactional註解的事務實現主要經過TransactionInterceptor攔截器來進行實現的,攔截目標方法,而後判斷異常是否是目標異常,若是是目標異常就行進行回滾,不然就進行事務提交。

最後咱們本身經過JDBC結合Spring的AOP本身寫了個@MyTransactional的註解,實現了遇到指定異常回滾的功能。

相關文章
相關標籤/搜索