Spring事務的介紹,以及基於註解@Transactional的宣告式事務

碼上遇見你發表於2021-11-02
前言

事務是一個非常重要的知識點,前面的文章已經有介紹了關於SpringAOP代理的實現過程;事務管理也是AOP的一個重要的功能。

事務的基本介紹
資料庫事務特性:
  • 原子性
  • 一致性
  • 隔離性
  • 永續性
事務的隔離級別

SQL 標準定義了四種隔離級別,MySQL 全都支援。這四種隔離級別分別是:

  • 讀未提交(READ UNCOMMITTED)
  • 讀已提交(READ COMMITTED)
  • 可重複讀(REPEATABLE READ)
  • 序列化(SERIALIZABLE)

至於為什麼會設定資料庫的隔離級別,原因是由於在併發運算元據庫的時候可能會引起髒讀、不可重複讀、幻讀、第一類丟失更新、第二類更新丟失等現象。

髒讀:

事物A讀取事物B尚未提交的更改資料,並做了修改;此時如果事物B回滾,那麼事物A讀取到的資料是無效的,此時就發生了髒讀。

不可重複讀:

一個事務執行相同的查詢兩次或兩次以上,每次都得到不同的資料。如:A事物下查詢賬戶餘額,此時恰巧B事物給賬戶裡轉賬100元,A事物再次查詢賬戶餘額,那麼A事物的兩次查詢結果是不一致的。

幻讀:

A事物讀取B事物提交的新增資料,此時A事物將出現幻讀現象。幻讀與不可重複讀容易混淆,如何區分呢?幻讀是讀取到了其他事物提交的新資料,不可重複讀是讀取到了已經提交事物的更改資料(修改或刪除)

第一類丟失更新現象:

撤銷一個事務的時候,把其它事務已提交的更新資料覆蓋了。這是完全沒有事務隔離級別造成的。如果事務1被提交,另一個事務被撤銷,那麼會連同事務1所做的更新也被撤銷。

第二類丟失更新現象:

它和不可重複讀本質上是同一類併發問題,通常將它看成不可重複讀的特例。當兩個或多個事務查詢相同的記錄,然後各自基於查詢的結果更新記錄時會造成第二類丟失更新問題。每個事務不知道其它事務的存在,最後一個事務對記錄所做的更改將覆蓋其它事務之前對該記錄所做的更改。

針對以上問題,其實可以有其它的解決方法,設定資料庫隔離級別就是其中的一種,簡單說一下資料庫四個隔離級別的作用,見下表

image

簡單總結:

  • Read Uncommitted存在:髒讀、不可重複讀、第二類丟失更新和幻讀問題。
  • Read committed存在:不可重複讀、第二類丟失更新和幻讀問題。
  • Repeatable Read存在:幻讀問題。
  • Serializable 不存在問題。
接下來我們看一下Spring支援事務的核心介面:

概要圖:

image

TransactionDefinition
  • 看原始碼(TransactionDefinition.java)
public interface TransactionDefinition {

	/**
	 * 如果當前沒有事物,則新建一個事物;如果已經存在一個事物,則加入到這個事物中。
	 */
	int PROPAGATION_REQUIRED = 0;

	/**
	 * 支援當前事物,如果當前沒有事物,則以非事物方式執行。
	 */
	int PROPAGATION_SUPPORTS = 1;

	/**
	 * 使用當前事物,如果當前沒有事物,則丟擲異常
	 */
	int PROPAGATION_MANDATORY = 2;
	/**
	 * 新建事物,如果當前已經存在事物,則掛起當前事物。
	 */
	int PROPAGATION_REQUIRES_NEW = 3;

	/**
	 * 以非事物方式執行,如果當前存在事物,則掛起當前事物。
	 */
	int PROPAGATION_NOT_SUPPORTED = 4;
	/**
	 * 以非事物方式執行,如果當前存在事物,則丟擲異常。
	 */
	int PROPAGATION_NEVER = 5;
	/**
	 * 如果當前存在事物,則在巢狀事物內執行;如果當前沒有事物,則與PROPAGATION_REQUIRED傳播特性相同
	 */
	int PROPAGATION_NESTED = 6;

	/**
	 * 使用後端資料庫預設的隔離級別。
	 */
	int ISOLATION_DEFAULT = -1;

	/**
	 * READ_UNCOMMITTED 隔離級別
	 */
	int ISOLATION_READ_UNCOMMITTED = 1; // same as java.sql.Connection.TRANSACTION_READ_UNCOMMITTED;

	/**
	 *  READ_COMMITTED 隔離級別
	 */
	int ISOLATION_READ_COMMITTED = 2; // same as java.sql.Connection.TRANSACTION_READ_COMMITTED;

	/**
	 * REPEATABLE_READ 隔離級別
	 */
	int ISOLATION_REPEATABLE_READ = 4; // same as java.sql.Connection.TRANSACTION_REPEATABLE_READ;

	/**
	 * SERIALIZABLE 隔離級別
	 */
	int ISOLATION_SERIALIZABLE = 8; // same as java.sql.Connection.TRANSACTION_SERIALIZABLE;


	/**
	 * 預設超時時間
	 */
	int TIMEOUT_DEFAULT = -1;

	/**
	 *  獲取事物傳播特性
	 * @return
	 */
	default int getPropagationBehavior() {
		return PROPAGATION_REQUIRED;
	}
	/**
	 * 獲取事物隔離級別
	 * @return
	 */
	default int getIsolationLevel() {
		return ISOLATION_DEFAULT;
	}
	/**
	 * 獲取事物超時時間
	 * @return
	 */
	default int getTimeout() {
		return TIMEOUT_DEFAULT;
	}

	/**
	 * 判斷事物是否可讀
	 * @return
	 */
	default boolean isReadOnly() {
		return false;
	}

	/**
	 *  獲取事物名稱
	 * @return
	 */
	@Nullable
	default String getName() {
		return null;
	}

	static TransactionDefinition withDefaults() {
		return StaticTransactionDefinition.INSTANCE;
	}

}
Spring事務傳播行為
事務傳播行為型別 說明
PROPAGATION_REQUIRED 如果當前沒有事務,就新建一個事務,如果已經存在一個事務中,加入到這個事務中。這是最常見的選擇。
PROPAGATION_SUPPORTS 支援當前事務,如果當前沒有事務,就以非事務方式執行。
PROPAGATION_MANDATORY 使用當前的事務,如果當前沒有事務,就丟擲異常。
PROPAGATION_REQUIRES_NEW 新建事務,如果當前存在事務,把當前事務掛起。
PROPAGATION_NOT_SUPPORTED 以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。
PROPAGATION_NEVER 以非事務方式執行,如果當前存在事務,則丟擲異常。
PROPAGATION_NESTED 如果當前存在事務,則在巢狀事務內執行。如果當前沒有事務,則執行與PROPAGATION_REQUIRED類似的操作。
Spring支援的隔離級別
隔離級別 描述
DEFAULT 使用資料庫本身使用的隔離級別 ORACLE(讀已提交) MySQL(可重複讀)
READ_UNCOMMITTED 讀未提交(髒讀)最低的隔離級別,一切皆有可能。
READ_COMMITTEN 讀已提交,ORACLE預設隔離級別,有幻讀以及不可重複讀風險。
REPEATABLE_READ 可重複讀,解決不可重複讀的隔離級別,但還是有幻讀風險。MySQL預設隔離級別
SERLALIZABLE 序列化,最高的事務隔離級別,不管多少事務,挨個執行完一個事務的所有子事務之後才可以執行另外一個事務裡面的所有子事務,這樣就解決了髒讀、不可重複讀和幻讀的問題了
Spring事務基礎結構中的中心介面(PlatformTransactionManager.JAVA)
  • 看原始碼
/**
 * Spring事務基礎結構中的中心介面
 */
public interface PlatformTransactionManager extends TransactionManager {

	/**
	 * 根據指定的傳播行為,返回當前活動的事務或建立新事務。
	 * @param definition
	 * @return
	 * @throws TransactionException
	 */
	TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
	throws TransactionException;

	/**
	 * 就給定事務的狀態提交給定事務
	 * @param status
	 * @throws TransactionException
	 */
	void commit(TransactionStatus status) throws TransactionException;
	/**
	 * 執行給定事務的回滾
	 * @param status
	 * @throws TransactionException
	 */
	void rollback(TransactionStatus status) throws TransactionException;

}
  • 原始碼分析

    Spring將事務管理委託給底層的持久化框架來完成,因此,Spring為不同的持久化框架提供了不同的PlatformTransactionManager介面實現類,我們看一下具體有哪些事務管理器:

image

簡單說一下圖中標註的幾個事務管理器:

事務管理器 描述
DataSourceTransactionManager 提供對單個javax.sql.DataSource事務管理,用於Spring JDBC抽象框架、iBATIS或MyBatis框架的事務管理
JpaTransactionManager 提供對單個javax.persistence.EntityManagerFactory事務支援,用於整合JPA實現框架時的事務管理
JtaTransactionManager 提供對分散式事務管理的支援,並將事務管理委託給Java EE應用伺服器事務管理器

看完事務管理器,我們看一下概覽圖中的第三部分TransactionStatus事務狀態描述介面類

Spring事務狀態描述
  • 看原始碼(TransactionStatus.java)
// 事務狀態描述
public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable {

	/**
	 * 返回該事務是否在內部攜帶儲存點,也就是說,已經建立為基於儲存點的巢狀事務。
	 * @return
	 */
	boolean hasSavepoint();
	/**
	 *  將會話重新整理到資料儲存區
	 */
	@Override
	void flush();

}

繼續看一下它的父類TransactionExecution.javaSavepointManager.java

public interface TransactionExecution {
	/**
	 * 返回當前事務是否為新事務(否則將參與到現有事務中,或者可能一開始就不在實際事務中執行)
	 * @return
	 */
	boolean isNewTransaction();
	/**
	 * 設定事務僅回滾。
	 */
	void setRollbackOnly();
	/**
	 * 返回事務是否已標記為僅回滾
	 * @return
	 */
	boolean isRollbackOnly();
	/**
	 * 返回事物是否已經完成,無論提交或者回滾。
	 * @return
	 */
	boolean isCompleted();

}
public interface SavepointManager {
	/**
	 * 建立一個新的儲存點。
	 * @return
	 * @throws TransactionException
	 */
	Object createSavepoint() throws TransactionException;
	/**
	 * 回滾到給定的儲存點。
	 *   注意:呼叫此方法回滾到給定的儲存點之後,不會自動釋放儲存點,
	 *        可以通過呼叫releaseSavepoint方法釋放儲存點。
	 * @param savepoint
	 * @throws TransactionException
	 */
	void rollbackToSavepoint(Object savepoint) throws TransactionException;
	/**
	 * 顯式釋放給定的儲存點。(大多數事務管理器將在事務完成時自動釋放儲存點)
	 * @param savepoint
	 * @throws TransactionException
	 */
	void releaseSavepoint(Object savepoint) throws TransactionException;

}

到這裡Spring的事務相關概念已經大概介紹完了,我們先來熟悉一下Spring的程式設計式事務的應用例項:

Spring程式設計式事務
package com.vipbbo.spring.transaction;

import org.apache.commons.dbcp2.BasicDataSource;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

import javax.sql.DataSource;

public class MyTransaction {

	private JdbcTemplate jdbcTemplate;
	private DataSourceTransactionManager transactionManager;
	private DefaultTransactionDefinition transactionDefinition;
	private String insertSql = "insert into account (balance) values ('100')";

	public void save(){
		// 初始化jdbcTemplate
		DataSource dataSource = getDataSource();
		jdbcTemplate = new JdbcTemplate(dataSource);

		// 建立事務管理器
		transactionManager = new DataSourceTransactionManager();
		transactionManager.setDataSource(dataSource);

		// 定義事務屬性
		transactionDefinition = new DefaultTransactionDefinition();
		transactionDefinition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

		// 開啟事務
		TransactionStatus transactionStatus = transactionManager.getTransaction(transactionDefinition);

		// 執行業務邏輯
		try {
			jdbcTemplate.execute(insertSql);
			//int i = 1/0;
			jdbcTemplate.execute(insertSql);
			transactionManager.commit(transactionStatus);
		} catch (DataAccessException e) {
			e.printStackTrace();
		} catch (TransactionException e) {
			transactionManager.rollback(transactionStatus);
			e.printStackTrace();
		}

	}

	public DataSource getDataSource(){
		BasicDataSource dataSource = new BasicDataSource();
		dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
		dataSource.setUrl("jdbc:mysql://192.168.1.100:3306/spring_aop?" +
				"useSSL=false&useUnicode=true&characterEncoding=UTF-8");
		dataSource.setUsername("root");
		dataSource.setPassword("root");
		return dataSource;
	}
}

測試類:

package com.vipbbo.spring.transaction;

import org.junit.jupiter.api.Test;

public class MyTransactionTest {

	@Test
	public void test1() {
		MyTransaction myTransaction = new MyTransaction();
		myTransaction.save();
	}
}

分析

執行測試類,一旦放開int i = 1/0;這段程式碼,再丟擲異常之後手動回滾事務,所以資料庫表不會增加記錄。

基於@Transactional註解的宣告式事務
其底層建立在`AOP`的基礎之上,對方法前後進行攔截,然後在目標方法開始之前建立一個或者加入一個事務,在執行完目標方法之後根據執行情況提交或者回滾事務。通過宣告式事務,無需在業務邏輯程式碼中摻雜事務管理的程式碼,只需在配置檔案中做相應的事務規則宣告(或通過等價的基於標註的方式),便可以將事務規則應用到業務邏輯中。

非XMl方式配置宣告式事務


package com.vipbbo.spring.transaction;


import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Transactional(propagation = Propagation.REQUIRED)
public interface AccountByAnnotationService {
	void save() throws RuntimeException;
}

實現類:

package com.vipbbo.spring.transaction;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

/**
 * 賬戶介面實現
 *
 * @author paidaxing
 */
@Service("accountByAnnotationService")
public class AccountByAnnotationServiceImpl implements AccountByAnnotationService {


	@Autowired
	private JdbcTemplate jdbcTemplate;
	private static String insertSql = "insert into account(balance) values (100)";

	@Override
	public void save() throws RuntimeException {
		System.out.println("======開始執行sql======");
		jdbcTemplate.execute(insertSql);
		System.out.println("======sql執行結束======");
		System.out.println("======準備丟擲異常======");
		throw new RuntimeException("手動丟擲異常");
	}

}

註解開啟宣告式事務

配置類

package com.vipbbo.spring.config;


import org.apache.commons.dbcp2.BasicDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;
import java.net.ProtocolException;

@ComponentScan(basePackages = {"com.vipbbo"})
@Configuration
//開啟基於註解的宣告式事務
@EnableTransactionManagement
public class SpringConfig {


	/**
	 * 註解資料來源
	 * @return
	 * @throws ProtocolException
	 */
	@Bean
	public DataSource dataSource() throws ProtocolException {
		BasicDataSource dataSource = new BasicDataSource();
		dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
		dataSource.setUsername("root");
		dataSource.setPassword("root");
		dataSource.setUrl("jdbc:mysql://192.168.1.100:3306/spring_aop?" +
				"useSSL=false&useUnicode=true&characterEncoding=UTF-8");
		return dataSource;
	}

	/**
	 * 註冊JdbcTemplate
	 * @return
	 * @throws ProtocolException
	 */
	@Bean
	public JdbcTemplate jdbcTemplate() throws ProtocolException{
		// 兩種方式獲取DataSOurce
		//1. 直接在方法上放置引數 public JdbcTemplate jdbcTemplate(DataSource dataSource)
		// 預設會去容器去取
		// 2. 如下: 呼叫上面的方法
		//spring對@Configuration類有特殊處理,註冊元件的方法多次呼叫只是在IOC容器中找元件
		return new JdbcTemplate(dataSource());
	}

	/**
	 * 註冊事務管理器
	 * @return
	 * @throws ProtocolException
	 */
	@Bean
	public PlatformTransactionManager transactionManager() throws ProtocolException{
		//需要傳入dataSource
		return new DataSourceTransactionManager(dataSource());
	}
}

測試程式碼類

package com.vipbbo.spring.transaction;

import com.vipbbo.spring.config.SpringConfig;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class MyTransactionByAnnotationTest {

	@Test
	public void test(){
		ApplicationContext tx = new AnnotationConfigApplicationContext(SpringConfig.class);
		AccountByAnnotationService annotationService =
				tx.getBean("accountByAnnotationService",AccountByAnnotationService.class);
		annotationService.save();
	}
}

測試類執行截圖:

image
我們在上述實現類中手動丟擲了一個異常,Spring會自動回滾事務,我們檢視資料庫可以知道並沒有新增資料。。

注意重中之重

預設情況下Spring中的事務處理只對RuntimeException方法進行回滾,所以,如果此處將RuntimeException替換成普通的Exception不會產生回滾效果

參考文章:https://cloud.tencent.com/developer/article/1589894

微信搜尋【碼上遇見你】第一時間獲取更多精彩內容!

相關文章