用了這么久的@Transactional,你真的了解嗎?
點(diǎn)擊上方“程序員大白”,選擇“星標(biāo)”公眾號
重磅干貨,第一時(shí)間送達(dá)

來源:juejin.cn/post/6942651630554710046
數(shù)據(jù)庫的事務(wù)
數(shù)據(jù)庫事務(wù)(Transaction,簡寫為 TX)是數(shù)據(jù)庫管理系統(tǒng)執(zhí)行過程中的一個(gè)邏輯單位,是可以提交或回滾的工作的原子單元。當(dāng)事務(wù)對數(shù)據(jù)庫進(jìn)行多次更改時(shí),要么在提交事務(wù)時(shí)所有更改都成功,要么在回滾事務(wù)時(shí)所有更改都被撤消。
數(shù)據(jù)庫(包括但不限于關(guān)系型)事務(wù)一般擁有以下 4 個(gè)特性,稱之為 ACID 特性
ACID
原子性(Atomicity):事務(wù)作為一個(gè)整體被執(zhí)行,包含在其中的對數(shù)據(jù)庫的操作要么全部被執(zhí)行,要么都不執(zhí)行。 一致性(Consistency):事務(wù)應(yīng)確保數(shù)據(jù)庫的狀態(tài)從一個(gè)一致狀態(tài)轉(zhuǎn)變?yōu)榱硪粋€(gè)一致狀態(tài)。_一致狀態(tài)_的含義是數(shù)據(jù)庫中的數(shù)據(jù)應(yīng)滿足完整性約束。 隔離性(Isolation):多個(gè)事務(wù)并發(fā)執(zhí)行時(shí),一個(gè)事務(wù)的執(zhí)行不應(yīng)影響其他事務(wù)的執(zhí)行。 持久性(Durability):已被提交的事務(wù)對數(shù)據(jù)庫的修改應(yīng)該永久保存在數(shù)據(jù)庫中。
Mysql 中的事務(wù)
START TRANSACTION
[transaction_characteristic [, transaction_characteristic] ...]
transaction_characteristic: {
WITH CONSISTENT SNAPSHOT
| READ WRITE
| READ ONLY
}
BEGIN [WORK]
COMMIT [WORK] [AND [NO] CHAIN] [[NO] RELEASE]
ROLLBACK [WORK] [AND [NO] CHAIN] [[NO] RELEASE]
SET autocommit = {0 | 1}
START TRANSACTION或 BEGIN開始新事務(wù)。 COMMIT 提交當(dāng)前事務(wù)。 ROLLBACK 回滾當(dāng)前事務(wù)。 SET autocommit 禁用或啟用當(dāng)前會(huì)話的默認(rèn)自動(dòng)提交模式。
默認(rèn)情況下,Mysql 是自動(dòng)提交的模式,所有語句會(huì)立即提交
JDBC 中的事務(wù)
JDBC 是 Java 語言中用來規(guī)范客戶端程序如何來訪問數(shù)據(jù)庫的應(yīng)用程序接口,提供了查詢和更新數(shù)據(jù)庫中數(shù)據(jù)的方法。JDBC 也是 Sun Microsystems 的商標(biāo)(現(xiàn)在屬于 Oracle),是面向關(guān)系型數(shù)據(jù)庫的。
上面說到,Mysql 是默認(rèn)自動(dòng)提交的,所以 JDBC 中事務(wù)事務(wù)的第一步,需要禁用自動(dòng)提交:
con.setAutoCommit(false);
提交事務(wù):
con.commit();
回滾事務(wù):
con.rollback();
一個(gè)完整流程的例子(摘自 Oracle JDBC 文檔):
public void updateCoffeeSales(HashMap<String, Integer> salesForWeek)
throws SQLException {
PreparedStatement updateSales = null;
PreparedStatement updateTotal = null;
String updateString =
"update " + dbName + ".COFFEES " +
"set SALES = ? where COF_NAME = ?";
String updateStatement =
"update " + dbName + ".COFFEES " +
"set TOTAL = TOTAL + ? " +
"where COF_NAME = ?";
try {
con.setAutoCommit(false);
updateSales = con.prepareStatement(updateString);
updateTotal = con.prepareStatement(updateStatement);
for (Map.Entry<String, Integer> e : salesForWeek.entrySet()) {
updateSales.setInt(1, e.getValue().intValue());
updateSales.setString(2, e.getKey());
updateSales.executeUpdate();
updateTotal.setInt(1, e.getValue().intValue());
updateTotal.setString(2, e.getKey());
updateTotal.executeUpdate();
con.commit();
}
} catch (SQLException e ) {
JDBCTutorialUtilities.printSQLException(e);
if (con != null) {
try {
System.err.print("Transaction is being rolled back");
con.rollback();
} catch(SQLException excep) {
JDBCTutorialUtilities.printSQLException(excep);
}
}
} finally {
if (updateSales != null) {
updateSales.close();
}
if (updateTotal != null) {
updateTotal.close();
}
con.setAutoCommit(true);
}
}
為什么需要事務(wù)管理器
如果沒有事務(wù)管理器的話,我們的程序可能是這樣:
Connection connection = acquireConnection();
try{
int updated = connection.prepareStatement().executeUpdate();
connection.commit();
}catch (Exception e){
rollback(connection);
}finally {
releaseConnection(connection);
}
也有可能是這樣 "優(yōu)雅的事務(wù)":
execute(new TxCallback() {
@Override
public Object doInTx(Connection var1) {
//do something...
return null;
}
});
public void execute(TxCallback txCallback){
Connection connection = acquireConnection();
try{
txCallback.doInTx(connection);
connection.commit();
}catch (Exception e){
rollback(connection);
}finally {
releaseConnection(connection);
}
}
# lambda版
execute(connection -> {
//do something...
return null;
});
但是以上兩種方式,針對一些復(fù)雜的場景是很不方便的。在實(shí)際的業(yè)務(wù)場景中,往往有比較復(fù)雜的業(yè)務(wù)邏輯,代碼冗長,邏輯關(guān)聯(lián)復(fù)雜,如果一個(gè)大操作中又全是這種代碼的話我想開發(fā)人員可能會(huì)瘋吧。更不用提定制化的隔離級別,以及嵌套 / 獨(dú)立事務(wù)的處理了。
Spring 事務(wù)管理器(Transaction Manager)簡介
Spring 作為 Java 最強(qiáng)框架,事務(wù)管理也是其核心功能之一。Spring 為事務(wù)管理提供了統(tǒng)一的抽象,有以下優(yōu)點(diǎn):
跨不同事務(wù) API(例如 Java 事務(wù) API(JTA),JDBC,Hibernate,Java 持久性 API(JPA)和 Java 數(shù)據(jù)對象(JDO))的一致編程模型。 支持聲明式事務(wù)管理(注解形式) 與 JTA 之類的復(fù)雜事務(wù) API 相比, 用于程序化事務(wù)管理的 API 更簡單 和 Spring 的 Data 層抽象集成方便(比如 Spring - Hibernate/Jdbc/Mybatis/Jpa...)
Spring 的事務(wù)管理器只是一個(gè)接口 / 抽象,不同的 DB 層框架(其實(shí)不光是 DB 類框架,支持事務(wù)模型的理論上都可以使用這套抽象) 可能都需要實(shí)現(xiàn)此標(biāo)準(zhǔn)才可以更好的工作,核心接口是org.springframework.transaction.support.AbstractPlatformTransactionManager,其代碼位于spring-tx模塊中,比如 Hibernate 中的實(shí)現(xiàn)為:org.springframework.orm.hibernate4.HibernateTransactionManager
使用方式
事務(wù),自然是控制業(yè)務(wù)的,在一個(gè)業(yè)務(wù)流程內(nèi),往往希望保證原子性,要么全成功要么全失敗。所以事務(wù)一般是加載@Service層,一個(gè) Service 內(nèi)調(diào)用了多個(gè)操作數(shù)據(jù)庫的操作(比如 Dao),在 Service 結(jié)束后事務(wù)自動(dòng)提交,如有異常拋出則事務(wù)回滾。
這也是 Spring 事務(wù)管理的基本使用原則。
注解
在被 Spring 管理的類頭上增加@Transactional注解,即可對該類下的所有方法開啟事務(wù)管理。事務(wù)開啟后,方法內(nèi)的操作無需手動(dòng)開啟 / 提交 / 回滾事務(wù),一切交給 Spring 管理即可。
@Service
@Transactional
public class TxTestService{
@Autowired
private OrderRepo orderRepo;
public void submit(Order order){
orderRepo.save(order);
}
}
也可以只在方法上配置,方法配置的優(yōu)先級是大于類的
@Service
public class TxTestService{
@Autowired
private OrderRepo orderRepo;
@Transactional
public void submit(Order order){
orderRepo.save(order);
}
}
TransactionTemplate
TransactionTemplate 這中方式,其實(shí)和使用注解形式的區(qū)別不大,其核心功能也是由 TransactionManager 實(shí)現(xiàn)的,這里只是換了個(gè)入口
public <T> T execute(TransactionCallback<T> action) throws TransactionException {
if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager) {
return ((CallbackPreferringPlatformTransactionManager) this.transactionManager).execute(this, action);
}
else {
//獲取事務(wù)信息
TransactionStatus status = this.transactionManager.getTransaction(this);
T result;
try {
//執(zhí)行業(yè)務(wù)代碼
result = action.doInTransaction(status);
}
//處理異?;貪L
catch (RuntimeException ex) {
// Transactional code threw application exception -> rollback
rollbackOnException(status, ex);
throw ex;
}
catch (Error err) {
// Transactional code threw error -> rollback
rollbackOnException(status, err);
throw err;
}
catch (Exception ex) {
// Transactional code threw unexpected exception -> rollback
rollbackOnException(status, ex);
throw new UndeclaredThrowableException(ex, "TransactionCallback threw undeclared checked exception");
}
//提交事務(wù)
this.transactionManager.commit(status);
return result;
}
}
XML 配置 tx:advice
過于古老,不做解釋
隔離級別 (Isolation Level)
事務(wù)隔離級別是數(shù)據(jù)庫最重要的特性之一,他保證了臟讀 / 幻讀等問題不會(huì)發(fā)生。作為一個(gè)事務(wù)管理框架自然也是支持此配置的,在 @Transactional 注解中有一個(gè) isolation 配置,可以很方便的配置各個(gè)事務(wù)的隔離級別,等同于connection.setTransactionIsolation()
Isolation {
DEFAULT(-1),
READ_UNCOMMITTED(1),
READ_COMMITTED(2),
REPEATABLE_READ(4),
SERIALIZABLE(8);
}
傳播行為 (Propagation behavior)
可能沒有接觸過 Spring 的人聽到傳播行為會(huì)奇怪,這是個(gè)什么東西。
其實(shí)這個(gè)傳播行為和數(shù)據(jù)庫功能無關(guān),只是事務(wù)管理器為了處理復(fù)雜業(yè)務(wù)而設(shè)計(jì)的一個(gè)機(jī)制。比如現(xiàn)在有這樣一個(gè)調(diào)用場景,A Service -> B Service -> C Service,但是希望 A/B 在一個(gè)事務(wù)內(nèi),C 是一個(gè)獨(dú)立的事務(wù),同時(shí) C 如果出錯(cuò),不影響 AB 所在的事務(wù)。
此時(shí),就可以通過傳播行為來處理;將 C Service 的事務(wù)配置為@Transactional(propagation = Propagation.REQUIRES_NEW)即可
Spring 支持以下幾種傳播行為:
REQUIRED 默認(rèn)策略,優(yōu)先使用當(dāng)前事務(wù)(及當(dāng)前線程綁定的事務(wù)資源),如果不存在事務(wù),則開啟新事務(wù) SUPPORTS 優(yōu)先使用當(dāng)前的事務(wù)(及當(dāng)前線程綁定的事務(wù)資源),如果不存在事務(wù),則以無事務(wù)方式運(yùn)行 MANDATORY 優(yōu)先使用當(dāng)前的事務(wù),如果不存在事務(wù),則拋出異常 REQUIRES_NEW 創(chuàng)建一個(gè)新事務(wù),如果存在當(dāng)前事務(wù),則掛起(Suspend) NOT_SUPPORTED 以非事務(wù)方式執(zhí)行,如果當(dāng)前事務(wù)存在,則掛起當(dāng)前事務(wù)。 NEVER 以非事務(wù)方式執(zhí)行,如果當(dāng)前事務(wù)存在,則拋出異常
回滾策略
@Transactional 中有 4 個(gè)配置回滾策略的屬性,分為 Rollback 策略,和 NoRollback 策略
默認(rèn)情況下,RuntimeException 和 Error 這兩種異常會(huì)導(dǎo)致事務(wù)回滾,普通的 Exception(需要 Catch 的)異常不會(huì)回滾。
Rollback
配置需要回滾的異常類
# 異常類Class
Class<? extends Throwable>[] rollbackFor() default {};
# 異常類ClassName,可以是FullName/SimpleName
String[] rollbackForClassName() default {};
NoRollback
針對一些要特殊處理的業(yè)務(wù)邏輯,比如插一些日志表,或者不重要的業(yè)務(wù)流程,希望就算出錯(cuò)也不影響事務(wù)的提交。(搜索公眾號Java知音,回復(fù)“2021”,送你一份Java面試題寶典)
可以通過配置 NoRollbackFor 來實(shí)現(xiàn),讓某些異常不影響事務(wù)的狀態(tài)。
# 異常類Class
Class<? extends Throwable>[] noRollbackFor() default {};
# 異常類ClassName,可以是FullName/SimpleName
String[] noRollbackForClassName() default {};
只讀控制
設(shè)置當(dāng)時(shí)事務(wù)的只讀標(biāo)示,等同于connection.setReadOnly()
關(guān)鍵名詞解釋

基本原理
public void execute(TxCallback txCallback){
//獲取連接
Connection connection = acquireConnection();
try{
//執(zhí)行業(yè)務(wù)代碼
doInService();
//提交事務(wù)
connection.commit();
}catch (Exception e){
//回滾事務(wù)
rollback(connection);
}finally {
//釋放連接
releaseConnection(connection);
}
}
Spring 事務(wù)管理的基本原理就是以上代碼,獲取連接 -> 執(zhí)行代碼 -> 提交 / 回滾事務(wù)。Spring 只是將這個(gè)流程給抽象出來了,所有事務(wù)相關(guān)的操作都交由 TransactionManager 去實(shí)現(xiàn),然后封裝一個(gè)模板形式的入口來執(zhí)行 t
比如org.springframework.transaction.support.TransactionTemplate的實(shí)現(xiàn):
@Override
public <T> T execute(TransactionCallback<T> action) throws TransactionException {
if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager) {
return ((CallbackPreferringPlatformTransactionManager) this.transactionManager).execute(this, action);
}
else {
//通過事務(wù)管理器獲取事務(wù)
TransactionStatus status = this.transactionManager.getTransaction(this);
T result;
try {
//執(zhí)行業(yè)務(wù)代碼
result = action.doInTransaction(status);
}
//處理異?;貪L
catch (RuntimeException ex) {
// Transactional code threw application exception -> rollback
rollbackOnException(status, ex);
throw ex;
}
catch (Error err) {
// Transactional code threw error -> rollback
rollbackOnException(status, err);
throw err;
}
catch (Exception ex) {
// Transactional code threw unexpected exception -> rollback
rollbackOnException(status, ex);
throw new UndeclaredThrowableException(ex, "TransactionCallback threw undeclared checked exception");
}
//提交事務(wù)
this.transactionManager.commit(status);
return result;
}
}
注解形式的事務(wù)(@Transactional),實(shí)現(xiàn)機(jī)制也是一樣,基于 Spring 的 AOP,將上面 Template 的模式換成了自動(dòng)的 AOP,在 AOP 的 Interceptor(org.springframework.transaction.interceptor.TransactionInterceptor)中來執(zhí)行這套流程:
protected Object invokeWithinTransaction(Method method, Class<?> targetClass, final InvocationCallback invocation)
throws Throwable {
// If the transaction attribute is null, the method is non-transactional.
final TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(method, targetClass);
//獲取事務(wù)管理器
final PlatformTransactionManager tm = determineTransactionManager(txAttr);
final String joinpointIdentification = methodIdentification(method, targetClass);
if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
// Standard transaction demarcation with getTransaction and commit/rollback calls.
//創(chuàng)建事務(wù)
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
Object retVal = null;
try {
// This is an around advice: Invoke the next interceptor in the chain.
// This will normally result in a target object being invoked.
//執(zhí)行被“AOP”的代碼
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// target invocation exception
//處理異常回滾
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
//清除資源
cleanupTransactionInfo(txInfo);
}
//提交事務(wù)
commitTransactionAfterReturning(txInfo);
return retVal;
}
....
}
復(fù)雜流程下的事務(wù)傳播 / 保持相同事務(wù)的關(guān)鍵:
對于復(fù)雜一些的業(yè)務(wù)流程,會(huì)出現(xiàn)各種類之間的調(diào)用,Spring 是如何做到保持同一個(gè)事務(wù)的?
其實(shí)基本原理很簡單,只需要將當(dāng)前事務(wù)(Connection)隱式的保存至事務(wù)管理器內(nèi),后續(xù)方法在執(zhí)行 JDBC 操作前,從事務(wù)管理器內(nèi)獲取即可:
比如HibernateTemplate中的SessionFactory中的getCurrentSession,這里的getCurrentSession就是從(可能是間接的)Spring 事務(wù)管理器中獲取的 Spring 事務(wù)管理器將處理事務(wù)時(shí)的相關(guān)臨時(shí)資源(Connection 等)存在org.springframework.transaction.support.TransactionSynchronizationManager中,通過 ThreadLocal 維護(hù)
public abstract class TransactionSynchronizationManager {
private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<Map<Object, Object>>("Transactional resources");
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
new NamedThreadLocal<Set<TransactionSynchronization>>("Transaction synchronizations");
private static final ThreadLocal<String> currentTransactionName =
new NamedThreadLocal<String>("Current transaction name");
private static final ThreadLocal<Boolean> currentTransactionReadOnly =
new NamedThreadLocal<Boolean>("Current transaction read-only status");
private static final ThreadLocal<Integer> currentTransactionIsolationLevel =
new NamedThreadLocal<Integer>("Current transaction isolation level");
private static final ThreadLocal<Boolean> actualTransactionActive =
new NamedThreadLocal<Boolean>("Actual transaction active");
...
}
針對一些復(fù)雜場景,嵌套事務(wù) + 獨(dú)立事務(wù),涉及到掛起(suspend),恢復(fù)(resume)的情況,相關(guān)資源也是存儲在TransactionSynchronizationManager 中的,方便嵌套事務(wù)的處理。比如 A->B 時(shí),A 方法已經(jīng)開啟了事務(wù),并將當(dāng)前事務(wù)資源綁定在TransactionSynchronizationManager,那么執(zhí)行 B 之前,會(huì)檢測當(dāng)前是否已經(jīng)存在事務(wù);檢測方式就是從TransactionSynchronizationManager查找并檢測狀態(tài),如果已經(jīng)在事務(wù)內(nèi),那么就根據(jù)不同的傳播行為配置來執(zhí)行不同的邏輯,對于 REQUIRES_NEW 等傳播行為的處理會(huì)麻煩一些,會(huì)涉及到 “掛起(suspend)” 和恢復(fù) (resume) 的操作,原理打通小異,這里就不做過多解釋了
常見問題
事務(wù)沒生效
有下列代碼,入口為 test 方法,在 testTx 方法中配置了 @Transactional 注解,同時(shí)在插入數(shù)據(jù)后拋出 RuntimeException 異常,但是方法執(zhí)行后插入的數(shù)據(jù)并沒有回滾,竟然插入成功了
public void test(){
testTx();
}
@Transactional
public void testTx(){
UrlMappingEntity urlMappingEntity = new UrlMappingEntity();
urlMappingEntity.setUrl("http://www.baidu.com");
urlMappingEntity.setExpireIn(777l);
urlMappingEntity.setCreateTime(new Date());
urlMappingRepository.save(urlMappingEntity);
if(true){
throw new RuntimeException();
}
}
這里不生效的原因是因?yàn)槿肟诘姆椒?/ 類沒有增加 @Transaction 注解,由于 Spring 的事務(wù)管理器也是基于 AOP 實(shí)現(xiàn)的,不管是 Cglib(ASM) 還是 Jdk 的動(dòng)態(tài)代理,本質(zhì)上也都是子類機(jī)制;在同類之間的方法調(diào)用會(huì)直接調(diào)用本類代碼,不會(huì)執(zhí)行動(dòng)態(tài)代理曾的代碼;所以在這個(gè)例子中,由于入口方法test沒有增加代理注解,所以textTx方法上增加的事務(wù)注解并不會(huì)生效
異步后事務(wù)失效
比如在一個(gè)事務(wù)方法中,開啟了子線程操作庫,那么此時(shí)子線程的事務(wù)和主線程事務(wù)是不同的。
因?yàn)樵?Spring 的事務(wù)管理器中,事務(wù)相關(guān)的資源(連接,session,事務(wù)狀態(tài)之類)都是存放在 TransactionSynchronizationManager 中的,通過 ThreadLocal 存放,如果跨線程的話就無法保證一個(gè)事務(wù)了
# TransactionSynchronizationManager.java
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
new NamedThreadLocal<>("Transaction synchronizations");
private static final ThreadLocal<String> currentTransactionName =
new NamedThreadLocal<>("Current transaction name");
private static final ThreadLocal<Boolean> currentTransactionReadOnly =
new NamedThreadLocal<>("Current transaction read-only status");
private static final ThreadLocal<Integer> currentTransactionIsolationLevel =
new NamedThreadLocal<>("Current transaction isolation level");
private static final ThreadLocal<Boolean> actualTransactionActive =
new NamedThreadLocal<>("Actual transaction active");
事務(wù)提交失敗
org.springframework.transaction.UnexpectedRollbackException:
Transaction silently rolled back because it has been marked as rollback-only
這個(gè)異常是由于在同一個(gè)事務(wù)內(nèi),多個(gè)事務(wù)方法之間調(diào)用,子方法拋出異常,但又被父方法忽略了導(dǎo)致的。
因?yàn)樽臃椒⊕伋隽水惓#琒pring 事務(wù)管理器會(huì)將當(dāng)前事務(wù)標(biāo)為失敗狀態(tài),準(zhǔn)備進(jìn)行回滾,可是當(dāng)子方法執(zhí)行完畢出棧后,父方法又忽略了此異常,待方法執(zhí)行完畢后正常提交時(shí),事務(wù)管理器會(huì)檢查回滾狀態(tài),若有回滾標(biāo)示則拋出此異常。
具體可以參考org.springframework.transaction.support.AbstractPlatformTransactionManager#processCommit
示例代碼:
A -> B
# A Service(@Transactional):
public void testTx(){
urlMappingRepo.deleteById(98l);
try{
txSubService.testSubTx();
}catch (Exception e){
e.printStackTrace();
}
}
# B Service(@Transactional)
public void testSubTx(){
if(true){
throw new RuntimeException();
}
}
參考
https://docs.spring.io/spring/docs/4.2.x/spring-framework-reference/html/transaction.html https://docs.oracle.com/javase/tutorial/jdbc/basics/transactions.html https://www.cnblogs.com/micrari/p/7612962.html
推薦閱讀
國產(chǎn)小眾瀏覽器因屏蔽視頻廣告,被索賠100萬(后續(xù))
年輕人“不講武德”:因看黃片上癮,把網(wǎng)站和786名女主播起訴了
關(guān)于程序員大白
程序員大白是一群哈工大,東北大學(xué),西湖大學(xué)和上海交通大學(xué)的碩士博士運(yùn)營維護(hù)的號,大家樂于分享高質(zhì)量文章,喜歡總結(jié)知識,歡迎關(guān)注[程序員大白],大家一起學(xué)習(xí)進(jìn)步!

