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

前言

事務是一個非常重要的知識點,前面的文章已經有介紹了關於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不會產生回滾效果

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

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