作者:李玉亮
引言
資料庫事務與大多數后端軟體開發人員的作業密不可分,本文從事務理論、事務技術、事務實踐等方面對常用的相關事務知識進行整理總結,供大家參考,
事務理論介紹
事務定義
在資料庫管理系統中,事務是單個邏輯或作業單元,有時由多個操作組成,在資料庫中以一致模式完成的邏輯處理稱為事務,一個例子是從一個銀行賬戶轉賬到另一個賬戶:完整的交易需要減去從一個賬戶轉賬的金額,然后將相同的金額添加到另一個賬戶,
事務特性
原子性( 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寫代碼控制事務,
示例一、JTA的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='https://www.cnblogs.com/Jcloud/p/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層也可有,
總結
以上對事務的常用知識進行了總結整理,相關實踐規范有的并無完美固定答案,需要結合實際而論,歡迎大家留言溝通!
轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/539427.html
標籤:其它
下一篇:事務相關知識集錦
