事務相關知識集錦

京東雲開發者發表於2022-12-07

作者:李玉亮

引言

資料庫事務與大多數後端軟體開發人員的工作密不可分,本文從事務理論、事務技術、事務實踐等方面對常用的相關事務知識進行整理總結,供大家參考。

事務理論介紹

事務定義

在資料庫管理系統中,事務是單個邏輯或工作單元,有時由多個操作組成,在資料庫中以一致模式完成的邏輯處理稱為事務。一個例子是從一個銀行賬戶轉賬到另一個賬戶:完整的交易需要減去從一個賬戶轉賬的金額,然後將相同的金額新增到另一個賬戶。

事務特性

原子性( atomicty)

事務中的全部操作在資料庫中是不可分割的,要麼全部完成,要麼全部不執行。

一致性(consistency)

事務的執行不能破壞資料庫資料的完整性和一致性。一致性指資料滿足所有資料庫的條件,比如欄位約束、外來鍵約束、觸發器等,事務從一致性開始,以一致性結束。

隔離性( isolation)

事務的執行不受其他事務的干擾,事務執行的中間結果對其他事務是透明的。

永續性(durability)

對於提交事務,系統必須保證該事務對資料庫的改變不被丟失,即使資料庫出現故障。

注:DBMS一般採用日誌來保證事務的原子性、一致性和永續性。

事務隔離級別

併發事務帶來的問題

不可重複讀的重點是資料修改場景,幻讀的重點在於新增或者刪除場景。

事務隔離級別

SQL92標準定義了4種隔離級別的事務

大多數資料庫系統如oracle的預設隔離級別都是 Read committed,mysql預設為可重複讀,InnoDB 和 XtraDB 儲存引擎透過多版併發控制(MVCC,Multivesion Concurrency Control)解決了幻讀問題,Repeatable read 是 Mysql 預設的事務隔離級別,其中 InnoDB主 要透過使用 MVVC 獲得高併發,使用一種被稱為 next-key-locking 的策略來避免幻讀。

事務模型

事務提交模型

顯式事務:又稱自定義事務,是指用顯式的方式定義其開始和結束的事務,當使用start transaction和 commit語句時表示發生顯式事務。

隱式事務:隱式事務是指每一條資料操作語句都自動地成為一個事務,事務的開始是隱式的,事務的結束有明確的標記。即當使用者進行資料操作時,系統自動開啟一個事務,事務的結束則需手動呼叫 commit或 rollback語句來結束當前事務,在當前事務結束後又自動開啟一個新事務。

自動事務:自動事務是指能夠自動開啟事務並且能夠自動結束事務。在事務執行過程中,如果沒有出現異常,事務則自動提交;當執行過程產生錯誤時,則事務自動回滾;一條SQL語句一個事務。

事務程式設計模型

本地事務模型:事務由本地資源管理器來管理。簡單理解就是直接使用JDBC的事務API。

connection.setAutoCommit(false);// 自動提交關閉
//XXXX資料庫的增刪改查操作
connection.commit(); //提交事務

程式設計式事務模型:事務透過JTA以及底層的JTS實現來管理,對於開發人員而言,管理的是“事務”,而非“連線”。簡單理解就是使用事務的API寫程式碼控制事務。

示例一、J他的API程式設計

UserTransaction txn = sessionCtx.getUserTransaction();
txn.begin();
txn.commit();

示例二、Spring的事務模版

transactionTemplate.execute(
    new TransactionCallback<Object>() {
        @Override
        public Object doInTransaction(TransactionStatus status) {
            // 事務相關處理
           return null;
        }
    }
);

宣告式事務:事務由容器進行管理,對於開發人員而言,幾乎不管理事務。簡單理解就是加個事務註解或做個AOP切面。

@Transactional(rollbackFor = Exception.class)
public void updateStatus(String applyNo){
    cashierApplyMapper.updateStatus(applyNo, CANCEL_STATUS, CANCEL_STATUS_DESC);
}

比較

附:SQL相關小知識

**SQL的全稱:**Structured Query Language。中文翻譯:結構化查詢語言。

關聯式資料庫理論之父:埃德加·科德。是一位計算機的大牛,他憑藉關係資料模型理論獲得了圖靈獎,核心思想就兩個:關係代數和關係演算,發表了一篇牛逼的論文“A Relational Model of Data for Large Shared Data Banks”。

寫第一句SQL的人:Donald D. Chamberlin 和 Raymond F. Boyce。埃德加·科德的兩個同事Donald D. Chamberlin和Raymond F. Boyce根據論文,發明出了簡單好用的SQL語言。

**SQL 標準:**有兩個主要的標準,分別是 SQL92 和 SQL99 。92 和 99 代表了標準提出的時間。除了 SQL92 和 SQL99 以外,還存在 SQL-86、SQL-89、SQL:2003、SQL:2008、SQL:2011 和 SQL:2016 等其他的標準。

事務技術介紹

以Spring+Mybatis+JDBC+Mysql為例,常見的事務類請求的呼叫鏈路如下圖。請求呼叫應用服務,應用服務中開啟事務並進行業務操作,操作過程中呼叫Mybatis進行資料庫類操作,Mybatis透過JDBC驅動與底層資料庫互動。

因此接下來先按Mysql、JDBC、Mybatis、Spring來介紹各層的事務相關知識;最後進行全鏈路的呼叫分析。

Mysql事務相關

Mysql邏輯架構

架構圖如下(InnoDB儲存引擎):

MySQL事務是由儲存引擎實現的,MySQL支援事務的儲存引擎有InnoDB、NDB Cluster等,其中InnoDB的使用最為廣泛,其他儲存引擎如MyIsam、Memory等不支援事務。

Mysql的事務保證

Mysql的4個特性中有3個與 WAL(Write-Ahead Logging,先寫日誌,再寫磁碟)有關係,需要透過 Redo、Undo 日誌來保證等,而一致性需要透過DBMS的功能邏輯及原子性、隔離性、永續性共同來保證。

MVCC

MVCC最大的好處是讀不加鎖,讀寫不衝突,在讀多寫少的系統應用中,讀寫不衝突是非常重要的,可極大提升系統的併發效能,這也是為什麼現階段幾乎所有的關係型資料庫都支援 MVCC 的原因,目前MVCC只在 Read Commited 和 Repeatable Read 兩種隔離級別下工作。它是透過在每行記錄的後面儲存兩個隱藏列來實現的,這兩個列, 一個儲存了行的建立時間,一個儲存了行的過期時間, 儲存的並不是實際的時間值,而是系統版本號。MVCC在mysql中的實現依賴的是undo log與read view。

read view

在 MVCC 併發控制中,讀操作可以分為兩類: 快照讀(Snapshot Read)與當前讀 (Current Read)。

•快照讀:讀取的是記錄的快照版本(有可能是歷史版本)不用加鎖(select)。

•當前讀:讀取的是記錄的最新版本,並且當前讀返回的記錄,都會加鎖,保證其他事務不會再併發修改這條記錄(select… for update 、lock或insert/delete/update)。

redo log

redo log叫做重做日誌。mysql 為了提升效能不會把每次的修改都實時同步到磁碟,而是會先存到Buffer Pool(緩衝池)裡,當作快取來用以提升效能,使用後臺執行緒去做緩衝池和磁碟之間的同步。那麼問題來了,如果還沒來及的同步的時候當機或斷電了怎麼辦?這樣會導致丟部分已提交事務的修改資訊!所以引入了redo log來記錄已成功提交事務的修改資訊,並且會把redo log持久化到磁碟,系統重啟之後再讀取redo log恢復最新資料。redo log是用來恢復資料的,保障已提交事務的持久化特性。

undo log

undo log 叫做回滾日誌,用於記錄資料被修改前的資訊。他正好跟前面所說的重做日誌所記錄的相反,重做日誌記錄資料被修改後的資訊。undo log主要記錄的是資料的邏輯變化。為了在發生錯誤時回滾之前的操作,需要將之前的操作都記錄下來,然後在發生錯誤時才可以回滾。undo log 記錄事務修改之前版本的資料資訊,假如由於系統錯誤或者rollback操作而回滾的話可以根據undo log的資訊來進行回滾到沒被修改前的狀態。undo log是用來回滾資料的,保障未提交事務的原子性。

示例

假設 F1~F6 是表中欄位的名字,1~6 是其對應的資料。後面三個隱含欄位分別對應該行的隱含ID、事務號和回滾指標,如下圖所示。

具體的更新過程如下:

假如一條資料是剛 INSERT 的,DB_ROW_ID 為 1,其他兩個欄位為空。當事務 1 更改該行的資料值時,會進行如下操作,如下圖所示。

•用排他鎖鎖定該行,記錄 Redo log;

•把該行修改前的值複製到 Undo log,即圖中下面的行;

•修改當前行的值,填寫事務編號,並回滾指標指向 Undo log 中修改前的行。

如果再有事務2操作,過程與事務 1 相同,此時 Undo log 中會有兩行記錄,並且透過回滾指標連在一起,透過當前記錄的回滾指標回溯到該行建立時的初始內容,如下圖所示,這裡的undolog不會一直增加,purge thread在後面會進行undo page的回收,也就是清理undo log。

JDK事務相關

JDBC規範

java定義了統一的JDBC驅動API,各資料庫廠商按規範實現。jdbc驅動相關包在java.sql包下:

使用示例:

// 建立資料庫連線
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/easyflow", "root", "12345678");
// 自動提交設定
connection.setAutoCommit(false);
// 只讀設定
connection.setReadOnly(false);
// 事務隔離級別設定
connection.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
// 建立查詢語句
PreparedStatement statement = connection.prepareStatement("update config set cfg_value='1' where id=11111");
// 執行SQL
int num = statement.executeUpdate();
System.out.println("更新行數:" + num);
// 事務提交
connection.commit();

JDBC驅動序號產生器制

之前需要呼叫Class.forName或其他方式顯式載入驅動,現在有了SPI機制後可不寫。

public class DriverManager {


// List of registered JDBC drivers
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
private static volatile int loginTimeout = 0;
private static volatile java.io.PrintWriter logWriter = null;
private static volatile java.io.PrintStream logStream = null;
// Used in println() to synchronize logWriter
private final static Object logSync = new Object();

/* Prevent the DriverManager class from being instantiated. */
private DriverManager(){}


/**
* Load the initial JDBC drivers by checking the System property
* jdbc.properties and then use the {@code ServiceLoader} mechanism
*/
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
……

JTA規範

JTA 全稱 Java Transaction API,是 X/OPEN CAE 規範中分散式事務 XA 規範在 Java 中的對映,是 Java 中使用事務的標準 API,同時支援單機事務與分散式事務。

作為 J2EE 平臺規範的一部分,JTA 與 JDBC 類似,自身只提供了一組 Java 介面,需要由供應商來實現這些介面,與 JDBC 不同的是這些介面需要由不同的供應商來實現。

相關程式碼在jta jar的javax.transaction包下。

Mybatis事務相關

Mybatis核心是提供了sql查詢方法、結果集與應用方法及物件之間的對映關係,便於開發人員進行資料庫操作。

整體模組如下:

各模組與下面的各子包一一對應:

Mybatis執行的核心類如下:

Mysql的核心入口類為SqlSession,事務相關的操作透過TransactionFactory來處理,可選擇使用Spring事務(SpringManagedTransaction)還是內建事務管理。

事務相關的控制處理可見SqlSessionInterceptor類,主要邏輯如下:

原始碼見下:

  private class SqlSessionInterceptor implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
          SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
      try {
        Object result = method.invoke(sqlSession, args);
        if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
          // force commit even on non-dirty sessions because some databases require
          // a commit/rollback before calling close()
          sqlSession.commit(true);
        }
        return result;
      } catch (Throwable t) {
        Throwable unwrapped = unwrapThrowable(t);
        if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
          // release the connection to avoid a deadlock if the translator is no loaded. See issue #22
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
          sqlSession = null;
          Throwable translated = SqlSessionTemplate.this.exceptionTranslator
              .translateExceptionIfPossible((PersistenceException) unwrapped);
          if (translated != null) {
            unwrapped = translated;
          }
        }
        throw unwrapped;
      } finally {
        if (sqlSession != null) {
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
        }
      }
    }
  }

Spring事務相關

spring事務相程式碼主要位於spring-tx包,如TransactionInterceptor。spring-jdbc包中有spring jdbc對事務的相關支援實現,如JdbcTransactionManager。核心類如下圖,主要有三大部分:事務管理器(TransactionManager)、事務定義(TransactionDefinition)、事務狀態(TtransactionStatus),這也是經常見的一種架構思維,將功能模組抽象為配置態定義、執行態例項和執行引擎,在開源元件jd-easyflow(
https://github.com/JDEasyFlow/jd-easyflow) 中也是此種設計理念。從下面的類圖可以Spring的設計非常有層次化,很有美感。

Spring程式設計式事務

常用類為TransacitonTemplate,執行邏輯為:獲取事務狀態->在事務中執行業務->提交或回滾,原始碼見下:

@Override
@Nullable
public <T> T execute(TransactionCallback<T> action) throws TransactionException {
Assert.state(this.transactionManager != null, "No PlatformTransactionManager set");

if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager) {
return ((CallbackPreferringPlatformTransactionManager) this.transactionManager).execute(this, action);
}
else {
TransactionStatus status = this.transactionManager.getTransaction(this);
T result;
try {
result = action.doInTransaction(status);
}
catch (RuntimeException | Error ex) {
// Transactional code threw application exception -> rollback
rollbackOnException(status, ex);
throw ex;
}
catch (Throwable ex) {
// Transactional code threw unexpected exception -> rollback
rollbackOnException(status, ex);
throw new UndeclaredThrowableException(ex, "TransactionCallback threw undeclared checked exception");
}
this.transactionManager.commit(status);
return result;
}
}

Spring宣告式事務

宣告式事務實現原理就是透過AOP/動態代理。

在Bean初始化階段建立代理物件:Spring容器在初始化每個單例bean的時候,會遍歷容器中的所有BeanPostProcessor實現類,並執行其
postProcessAfterInitialization方法,在執行AbstractAutoProxyCreator類的postProcessAfterInitialization方法時會遍歷容器中所有的切面,查詢與當前例項化bean匹配的切面,這裡會獲取事務屬性切面,查詢@Transactional註解及其屬性值,然後根據得到的切面建立一個代理物件,預設是使用JDK動態代理建立代理,如果目標類是介面,則使用JDK動態代理,否則使用Cglib。

在執行目標方法時進行事務增強操作:當透過代理物件呼叫Bean方法的時候,會觸發對應的AOP增強攔截器,宣告式事務是一種環繞增強,對應介面為MethodInterceptor,事務增強對該介面的實現為TransactionInterceptor,類圖如下:

事務攔截器TransactionInterceptor在invoke方法中,透過呼叫父類TransactionAspectSupport的invokeWithinTransaction方法進行事務處理,包括開啟事務、事務提交、異常回滾 。

宣告式事務有5個配置項,說明如下:

事務配置一、事務隔離級別

配置該事務的隔離級別,一般情況資料庫或應用統一設定,不需要單獨設值。

事務配置二、事務傳播屬性

事務傳播屬性是spring事務模組的一個重要屬性。簡單理解,他控制一個方法在進入事務時,在外層方法有無事務的場景下,自己的事務的處理策略,如是複用已有事務還是建立新事務。

spring支援的傳播屬性有7種,如下:

事務配置三、事務超時

事務的超時設定是為了解決什麼問題呢?

在資料庫中,如果一個事務長時間執行,這樣的事務會佔用不必要的資料庫資源,還可能會鎖定資料庫的部分資源,這樣在生產環境是非常危險的。這時就可以宣告一個事務在特定秒數後自動回滾,不必等它自己結束。

事務超時時間的設定

由於超時時間在一個事務開啟的時候建立的,因此,只有對於那些具有啟動一個新事務的傳播行為(PROPAGATION_REQUIRES_NEW、PROPAGATION_REQUIRED、ROPAGATION_NESTED),宣告事務超時才有意義。

事務配置四、事務只讀

如果一個事務只對資料庫進行讀操作,資料庫可以利用事務的只讀特性來進行一些特定的最佳化。我們可以透過將事務宣告為只讀,讓資料庫對我們的事務操作進行最佳化。

事務配置五、回滾規則

回滾規則,就是程式發生了什麼會造成回滾,這裡我們可以進行設定RuntimeException或者Error。

預設情況下,事務只有遇到執行期異常時才會回滾,而在遇到檢查型異常時不會回滾。

我們可以宣告事務在遇到特定的異常進行回滾。同樣,我們也可以宣告事務遇到特定的異常不回滾,即使這些異常是執行期異常。

宣告式事務失效的場景

事務同步管理器

Spring中有一個事務同步管理器類
TransactionSynchronizationManager,它提供了事務提交後處理等相關回撥註冊的方法。當我們有業務需要在事務提交過後進行某一項或者某一系列的業務操作時候我們就可以使用
TransactionSynchronizationManager。

事務請求處理鏈路示例

下圖為全鏈路的從應用發起到開啟事務,到業務邏輯處理(SQL執行),最後關閉事務的正向鏈路。

事務實踐相關

資料一致性

同一個資料來源的操作在一個事務內可保證一致,但實際場景中會因為不同事務或不同資料來源(不同關聯式資料庫、快取或遠端服務)而導致資料不能強一致。在CAP理論框架下,我們一般是保證可用性、分割槽容錯性,基於BASE理論達到最終一致性。但如何達到資料的最終一致性需要合理設計。

資料庫的提交、快取的更新、RPC的執行、訊息的傳送的先後順序

一般我們以資料庫資料為準,先資料庫提交,再更新快取或傳送訊息,透過非同步輪詢補償的方式保證異常情況下的最終一致性。

不建議用法:

1、事務回滾會導致快取和資料庫不一致

2、事務回滾會導致訊息接收方收到的資料狀態錯誤

建議用法:

1、先更新資料庫,事務提交後再更新快取或傳送訊息

2、透過非同步異常重試或批處理同步來保證資料的最終一致性

3、核心交易以資料庫資料為準

系統健壯性增強,但程式設計模型複雜一些

長事務

如果事務中有耗時長的SQL或有RPC操作可能會導致事務時間變長,會導致併發量大的情況下資料庫連線池被佔滿,應用無法獲取連線資源,在主從架構中會導致主從延時變大。

建議事務粒度儘量小,事務中儘量少包含RPC操作。事務儘量放在下層。

不建議用法

建議用法

這種方式需要應用程式保證多個事務操作的最終一致性,一般可透過異常重試來實現。

事務程式碼層級

事務該加在哪一層?放在上層的優點是程式設計簡單,放在底層則需要需要在一個事務的操作封裝在一起沉澱到底層。

對於傳統架構(如下圖),建議在DAO層和Manager層加事務。Service層可以有,但重的Service或有rpc的Service操作慎用。

對於領域設計類架構(如下圖),從DDD的思想上,建議放在APP層(基礎設施不應是領域層關注的),但考慮到長事務問題,不建議放在APP層,更建議優先放在基礎設施層,domain的service層也可有。

總結

以上對事務的常用知識進行了總結整理,相關實踐規範有的並無完美固定答案,需要結合實際而論,歡迎大家留言溝通!

相關文章