Spring事務詳解
什么叫事務?
關於事務,最簡單最常見的例子就是取錢。ATM機取錢有兩個步驟,第一步輸入金額密碼,銀行卡扣掉1000元,第二步ATM出鈔1000元,這兩個步驟必須要么都執行成功,要么都不執行。如果其中一個步驟失敗了,必須把整個過程回滾,取消掉所有操作,這就是事務最基本的應用,事務就是用來解決類似問題的。
代碼舉例:
Connection conn = DriverManager.getConnection();
try {
conn.setAutoCommit(false); //將自動提交設置為false
//執行CRUD操作
conn.commit(); //當兩個操作成功后手動提交
} catch (Exception e) {
conn.rollback(); //一旦其中一個操作出錯都將回滾,所有操作都不成功
e.printStackTrace();
} finally {
conn.colse();
}
事務的四個特性(ACID)
ACID,指數據庫事務正確執行的四個基本要素的縮寫。包含:原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)、持久性(Durability)。
原子性
整個事務中的所有操作,要么全部完成,要么全部不完成,不可能停滯在中間某個環節。事務在執行過程中發生錯誤,會被回滾(Rollback)到事務開始前的狀態,就像這個事務從來沒有執行過一樣。
一致性
一個事務可以封裝狀態改變(除非它是一個只讀的)。事務必須始終保持系統處於一致的狀態,不管在任何給定的時間並發事務有多少。
也就是說:如果事務是並發多個,系統也必須如同串行事務一樣操作。其主要特征是保護性和不變性(Preserving an
Invariant),以轉賬案例為例,假設有五個賬戶,每個賬戶余額是100元,那么五個賬戶總額是500元,如果在這個5個賬戶之間同時發生多個轉賬,無論並發多少個,比如在A與B賬戶之間轉賬5元,在C與D賬戶之間轉賬10元,在B與E之間轉賬15元,五個賬戶總額也應該還是500元,這就是保護性和不變性。
隔離性
隔離狀態執行事務,使它們好像是系統在給定時間內執行的唯一操作。如果有兩個事務,運行在相同的時間內,執行相同的功能,事務的隔離性將確保每一事務在系統中認為只有該事務在使用系統。這種屬性有時稱為串行化,為了防止事務操作間的混淆,必須串行化或序列化請求,使得在同一時間僅有一個請求用於同一數據。
持久性
在事務完成以后,該事務對數據庫所作的更改便持久的保存在數據庫之中,並不會被回滾。
由於一項操作通常會包含許多子操作,而這些子操作可能會因為硬件的損壞或其他因素產生問題,要正確實現ACID並不容易。ACID建議數據庫將所有需要更新以及修改的資料一次操作完畢,但實際上並不可行。
目前主要有兩種方式實現ACID:第一種是Write ahead
logging,也就是日志式的方式(現代數據庫均基於這種方式)。第二種是Shadow paging。
Spring核心接口
Spring並不直接管理事務,而是提供了多種事務管理器,他們將事務管理的職責委托給Hibernate或者JTA等持久化機制所提供的相關平台框架的事務來實現。
基本事務屬性的定義
傳播行為
隔離規則
回滾規則
事務超時
是否只讀
傳播行為
傳播行為 含義
PROPAGATION_REQUIRED 表示當前方法必須運行在事務中。如果當前事務存在,方法將會在該事務中運行。否則,會啟動一個新的事務
PROPAGATION_SUPPORTS 表示當前方法不需要事務上下文,但是如果存在當前事務的話,那么該方法會在這個事務中運行
PROPAGATION_MANDATORY 表示該方法必須在事務中運行,如果當前事務不存在,則會拋出一個異常
PROPAGATION_REQUIRED_NEW 表示當前方法必須運行在它自己的事務中。一個新的事務將被啟動。如果存在當前事務,在該方法執行期間,當前事務會被掛起。
PROPAGATION_NOT_SUPPORTED 表示該方法不應該運行在事務中。如果存在當前事務,在該方法運行期間,當前事務將被掛起。
PROPAGATION_NEVER 表示當前方法不應該運行在事務上下文中。如果當前正有一個事務在運行,則會拋出異常。
PROPAGATION_NESTED 表示如果當前已經存在一個事務,那么該方法將會在嵌套事務中運行。嵌套的事務可以獨立於當前事務進行單獨地提交或回滾。如果當前事務不存在,那么其行為與PROPAGATION_REQUIRED一樣。
隔離規則
在企業級應用中,多用戶訪問數據庫是常見的場景,這就是所謂的事務的並發。事務並發所可能存在的問題:
1.臟讀:一個事務讀到另一個事務未提交的更新數據。
2.不可重復讀:一個事務兩次讀同一行數據,可是這兩次讀到的數據不一樣。
3.幻讀:一個事務執行兩次查詢,但第二次查詢比第一次查詢多出了一些數據行。
4.丟失更新:撤消一個事務時,把其它事務已提交的更新的數據覆蓋了。
不可重復讀:事務1第一次查找記錄A,事務2對記錄A進行修改並提交事務,事務1第二次查找記錄A時發現兩次結果不一致,這就是不可重復讀。
幻讀:事務1第一次查找表的總數量為B,事務2對表增加了一條記錄,事務1第二次查找表的總數量變成了B+1,兩次的結果也不一致,這是幻讀。
不可重復讀的重點是修改,幻讀的重點是插入和刪除;對於前者, 只需要鎖住滿足條件的記錄,對於后者, 要鎖住滿足條件及其相近的記錄。
隔離級別 含義
ISOLATION_DEFAULT 使用后端數據庫默認的隔離級別
ISOLATION_READ_UNCOMMITTED 最低的隔離級別,允許讀取尚未提交的數據變更,可能會導致臟讀、幻讀或不可重復讀
ISOLATION_READ_COMMITTED 允許讀取並發事務已經提交的數據,可以阻止臟讀,但是幻讀或不可重復讀仍有可能發生
ISOLATION_REPEATABLE_READ 對同一字段的多次讀取結果都是一致的,除非數據是被本身事務自己所修改,可以阻止臟讀和不可重復讀,但幻讀仍有可能發生
ISOLATION_SERIALIZABLE 最高的隔離級別,完全服從ACID的隔離級別,確保阻止臟讀、不可重復讀以及幻讀,也是最慢的事務隔離級別,因為它通常是通過完全鎖定事務相關的數據庫表來實現的
回滾規則
回滾規則定義了哪些異常會導致事務回滾而哪些不會。默認情況下,事務只有遇到運行期異常時才會回滾,而在遇到檢查型異常時不會回滾(這一行為與EJB的回滾行為是一致的)
但是你可以聲明事務在遇到特定的檢查型異常時像遇到運行期異常那樣回滾。同樣,你還可以聲明事務遇到特定的異常不回滾,即使這些異常是運行期異常。
事務超時
為了使應用程序很好地運行,事務不能運行太長的時間。因為事務可能涉及對后端數據庫的鎖定,所以長時間的事務會不必要的占用數據庫資源。事務超時就是事務的一個定時器,在特定時間內事務如果沒有執行完畢,那么就會自動回滾,而不是一直等待其結束。
是否只讀
如果事務只對后端的數據庫進行該操作,數據庫可以利用事務的只讀特性來進行一些特定的優化。通過將事務設置為只讀,你就可以給數據庫一個機會,讓它應用它認為合適的優化措施。
注解方式的事務使用注意事項
正確的設置@Transactional 的 propagation 屬性
需要注意下面三種 propagation 可以不啟動事務。如果期望對目標方法進行事務管理,但若是錯誤的配置這三種 propagation,事務將不會發生回滾。
TransactionDefinition.PROPAGATION_SUPPORTS:如果當前存在事務,則加入該事務;如果當前沒有事務,則以非事務的方式繼續運行。
TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事務方式運行,如果當前存在事務,則把當前事務掛起。
TransactionDefinition.PROPAGATION_NEVER:以非事務方式運行,如果當前存在事務,則拋出異常。
正確的設置@Transactional 的 rollbackFor 屬性
默認情況下,如果在事務中拋出了未檢查異常(繼承自 RuntimeException 的異常)或者 Error,則 Spring 將回滾事務;除此之外,Spring 不會回滾事務。
如果在事務中拋出其他類型的異常,並期望 Spring 能夠回滾事務,可以指定 rollbackFor。例:
@Transactional(propagation= Propagation.REQUIRED,rollbackFor= MyException.class)
@Transactional 只能應用到 public 方法才有效
只有@Transactional 注解應用到 public 方法,才能進行事務管理。這是因為在使用 Spring AOP 代理時,AbstractFallbackTransactionAttributeSource的 computeTransactionAttribute 方法會檢查目標方法的修飾符是不是 public,若不是 public,就不會獲取@Transactional 的屬性配置信息,最終會造成不會用 TransactionInterceptor 來攔截該目標方法進行事務管理。
避免 Spring 的 AOP 的自調用問題
在 Spring 的 AOP 代理下,只有目標方法由外部調用,目標方法才由 Spring 生成的代理對象來管理,這會造成自調用問題。若同一類中的其他沒有@Transactional 注解的方法內部調用有@Transactional 注解的方法,有@Transactional 注解的方法的事務被忽略,不會發生回滾。
@Service
public class OrderService {
private void insert() {
insertOrder();
}
@Transactional
public void insertOrder() {
// do something
}
}
insertOrder 盡管有@Transactional 注解,但它被內部方法 insert 調用,事務被忽略,出現異常事務不會發生回滾。
對於從事java開發工作的同學來說,spring的事務肯定再熟悉不過了。在某些業務場景下,如果一個請求中,需要同時寫入多張表的數據。為了保證操作的原子性
(要么同時成功,要么同時失敗),避免數據不一致的情況,我們一般都會用到spring事務。
確實,spring事務用起來賊爽,就用一個簡單的注解:@Transactional
,就能輕松搞定事務。我猜大部分小伙伴也是這樣用的,而且一直用一直爽。
但如果你使用不當,它也會坑你於無形。今天我們就一起聊聊,事務失效的一些場景,說不定你已經中招了。
一、事務不生效
1、訪問權限問題
眾所周知,java的訪問權限主要有四種:private、default、protected、public,它們的權限從左到右,依次變大。但如果我們在開發過程中,把有某些事務方法,定義了錯誤
的訪問權限,就會導致事務功能出問題,例如:
@Service public class UserService { @Transactional private void add(UserModel userModel) { saveData(userModel); updateData(userModel); } }
我們可以看到add方法的訪問權限被定義成了 private,這樣會導致事務失效,spring要求被代理方法必須是 public的
。
說白了,在 AbstractFallbackTransactionAttributeSource類的 computeTransactionAttribute方法中有個判斷,如果目標方法不是public,則 TransactionAttribute
返回null,即不支持事務。
protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) { // Don't allow no-public methods as required. if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) { return null; } // The method may be on an interface, but we need attributes from the target class. // If the target class is null, the method will be unchanged. Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); // First try is the method in the target class. TransactionAttribute txAttr = findTransactionAttribute(specificMethod); if (txAttr != null) { return txAttr; } // Second try is the transaction attribute on the target class. txAttr = findTransactionAttribute(specificMethod.getDeclaringClass()); if (txAttr != null && ClassUtils.isUserLevelMethod(method)) { return txAttr; } if (specificMethod != method) { // Fallback is to look at the original method. txAttr = findTransactionAttribute(method); if (txAttr != null) { return txAttr; } // Last fallback is the class of the original method. txAttr = findTransactionAttribute(method.getDeclaringClass()); if (txAttr != null && ClassUtils.isUserLevelMethod(method)) { return txAttr; } } return null; }
總結
: 也就是說,如果我們自定義的事務方法(即目標方法),它的訪問權限不是 public,而是private、default或protected的話,spring則不會提供事務功能。
2. 方法用final修飾
有時候,某個方法不想被子類重新,這時可以將該方法定義成final的。普通方法這樣定義是沒問題的,但如果將事務方法定義成final,例如:
@Service public class UserService { @Transactional public final void add(UserModel userModel){ saveData(userModel); updateData(userModel); } }
我們可以看到add方法被定義成了 final 的,這樣會導致事務失效。
為什么?
如果你看過spring事務的源碼,可能會知道spring事務底層使用了aop,也就是通過jdk動態代理或者cglib,幫我們生成了代理類,在代理類中實現的事務功能。但如果某個方法
用final修飾了,那么在它的代理類中,就無法重寫該方法,而添加事務功能。
注意
:如果某個方法是static的,同樣無法通過動態代理,變成事務方法。
3、方法內部調用
有時候我們需要在某個Service類的某個方法中,調用另外一個事務方法,比如:
@Service public class UserService { @Autowired private UserMapper userMapper; public void add(UserModel userModel) { userMapper.insertUser(userModel); updateStatus(userModel); } @Transactional public void updateStatus(UserModel userModel) { doSameThing(); } }
我們看到在事務方法add中,直接調用事務方法updateStatus。從前面介紹的內容可以知道,updateStatus方法擁有事務的能力是因為springaop生成代理了對象,
但是這種方法直接調用了this對象的方法,所以updateStatus方法不會生成事務。由此可見,在同一個類中的方法直接內部調用,會導致事務失效。
4、未被spring管理
在我們平時開發過程中,有個細節很容易被忽略。即使用spring事務的前提是:對象要被spring管理,需要創建bean實例。通常情況下,我們通過@Controller、@Service、
@Component、@Repository等注解,可以自動實現bean實例化和依賴注入的功能。
如果有一天,你匆匆忙忙的開發了一個Service類,但忘了加@Service注解,比如:
//@Service public class UserService { @Transactional public void add(UserModel userModel) { saveData(userModel); updateData(userModel); } }
從上面的例子,我們可以看到UserService類沒有加 @Service注解,那么該類不會交給spring管理,所以它的add方法也不會生成事務。
5、多線程調用
在實際項目開發中,多線程的使用場景還是挺多的。如果spring事務用在多線程場景中,會有問題嗎?
@Slf4j @Service public class UserService { @Autowired private UserMapper userMapper; @Autowired private RoleService roleService; @Transactional public void add(UserModel userModel) throws Exception { userMapper.insertUser(userModel); new Thread(() -> { roleService.doOtherThing(); }).start(); } } @Service public class RoleService { @Transactional public void doOtherThing() { System.out.println("保存role表數據"); } }
從上面的例子中,我們可以看到事務方法add中,調用了事務方法doOtherThing,但是事務方法doOtherThing是在另外一個線程中調用的。
這樣會導致兩個方法不在同一個線程中,獲取到的數據庫連接不一樣,從而是兩個不同的事務。如果想doOtherThing方法中拋了異常,add方法也回滾是不可能的。
如果看過spring事務源碼的朋友,可能會知道spring的事務是通過數據庫連接來實現的。當前線程中保存了一個map,key是數據源,value是數據庫連接。
private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");
我們說的同一個事務,其實是指同一個數據庫連接,只有擁有同一個數據庫連接才能同時提交和回滾。如果在不同的線程,拿到的數據庫連接肯定是不一樣的,所以是不同的事務。
6、表不支持事務
周所周知,在mysql5之前,默認的數據庫引擎是 myisam。它的好處就不用多說了:索引文件和數據文件是分開存儲的,對於查多寫少的單表操作,性能比innodb更好。
有些老項目中,可能還在用它。在創建表的時候,只需要把 ENGINE參數設置成 MyISAM即可:
CREATE TABLE `category` ( `id` bigint NOT NULL AUTO_INCREMENT, `one_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL, `two_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL, `three_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL, `four_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=MyISAM AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin
myisam好用,但有個很致命的問題是:不支持事務。
如果只是單表操作還好,不會出現太大的問題。但如果需要跨多張表操作,由於其不支持事務,數據極有可能會出現不完整的情況。所以在實際業務場景中,myisam使用
的並不多。在mysql5以后,myisam已經逐漸退出了歷史的舞台,取而代之的是innodb。
有時候在開發的過程中,發現某張表的事務一直都沒有生效,那不一定是spring事務的鍋,最好確認一下你使用的那張表是否支持事務。
二、事務不回滾
1、錯誤的傳播特性
其實,我們在使用 @Transactional注解時,是可以指定 propagation參數的。該參數的作用是指定事務的傳播特性,spring目前支持7種傳播特性:
REQUIRED #如果當前上下文中存在事務,那么加入該事務,如果不存在事務,創建一個事務,這是默認的傳播屬性值。 SUPPORTS #如果當前上下文存在事務,則支持事務加入事務,如果不存在事務,則使用非事務的方式執行。 MANDATORY #如果當前上下文中存在事務,否則拋出異常。 REQUIRES_NEW #每次都會新建一個事務,並且同時將上下文中的事務掛起,執行當前新建事務完成以后,上下文事務恢復再執行。 NOT_SUPPORTED #如果當前上下文中存在事務,則掛起當前事務,然后新的方法在沒有事務的環境中執行。 NEVER #如果當前上下文中存在事務,則拋出異常,否則在無事務環境上執行代碼。 NESTED #如果當前上下文中存在事務,則嵌套事務執行,如果不存在事務,則新建事務。
如果我們在手動設置propagation參數的時候,把傳播特性設置錯了,比如:
@Service public class UserService { @Transactional(propagation = Propagation.NEVER) public void add(UserModel userModel) { saveData(userModel); updateData(userModel); } }
我們可以看到add方法的事務傳播特性定義成了Propagation.NEVER,這種類型的傳播特性不支持事務,如果有事務則會拋異常。
目前只有這三種傳播特性才會創建新事務:REQUIRED,REQUIRES_NEW,NESTED。
2、自己吞了異常
事務不會回滾,最常見的問題是:開發者在代碼中手動try...catch了異常。比如:
@Slf4j @Service public class UserService { @Transactional public void add(UserModel userModel) { try { saveData(userModel); updateData(userModel); } catch (Exception e) { log.error(e.getMessage(), e); } } }
這種情況下spring事務當然不會回滾,因為開發者自己捕獲了異常,又沒有手動拋出,換句話說就是把異常吞掉了。
如果想要spring事務能夠正常回滾,必須拋出它能夠處理的異常。如果沒有拋異常,則spring認為程序是正常的。
3、手動拋了別的異常
即使開發者沒有手動捕獲異常,但如果拋的異常不正確,spring事務也不會回滾。
@Slf4j @Service public class UserService { @Transactional public void add(UserModel userModel) throws Exception { try { saveData(userModel); updateData(userModel); } catch (Exception e) { log.error(e.getMessage(), e); throw new Exception(e); } } }
上面的這種情況,開發人員自己捕獲了異常,又手動拋出了異常:Exception,事務同樣不會回滾。
因為spring事務,默認情況下只會回滾 RuntimeException
(運行時異常)和 Error
(錯誤),對於普通的Exception(非運行時異常)它不會回滾。
4、自定義了回滾異常
在使用@Transactional注解聲明事務時,有時我們想自定義回滾的異常,spring也是支持的。可以通過設置rollbackFor參數來完成這個功能。
但如果這個參數的值設置錯了,就會引出一些莫名其妙的問題,例如:
@Slf4j @Service public class UserService { @Transactional(rollbackFor = BusinessException.class) public void add(UserModel userModel) throws Exception { saveData(userModel); updateData(userModel); } }
如果在執行上面這段代碼,保存和更新數據時,程序報錯了,拋了SqlException、DuplicateKeyException等異常。而BusinessException是我們自定義的異常,報錯的
異常不屬於BusinessException,所以事務也不會回滾。即使rollbackFor有默認值,但阿里巴巴開發者規范中,還是要求開發者重新指定該參數。
這是為什么呢?
因為如果使用默認值,一旦程序拋出了Exception,事務不會回滾,這會出現很大的bug。所以,建議一般情況下,將該參數設置成:Exception或Throwable。
三、其它
1、大事務問題
在使用spring事務時,有個讓人非常頭疼的問題,就是大事務問題。通常情況下,我們會在方法上 @Transactional注解,填加事務功能,比如:
@Service public class UserService { @Autowired private RoleService roleService; @Transactional public void add(UserModel userModel) throws Exception { query1(); query2(); query3(); roleService.save(userModel); update(userModel); } } @Service public class RoleService { @Autowired private RoleService roleService; @Transactional public void save(UserModel userModel) throws Exception { query4(); query5(); query6(); saveData(userModel); } }
但 @Transactional注解,如果被加到方法上,有個缺點就是整個方法都包含在事務當中了。上面的這個例子中,在UserService類中,其實只有這兩行才需要事務:
roleService.save(userModel);
update(userModel);
在RoleService類中,只有這一行需要事務:
saveData(userModel);
現在的這種寫法,會導致所有的query方法也被包含在同一個事務當中。
如果query方法非常多,調用層級很深,而且有部分查詢方法比較耗時的話,會造成整個事務非常耗時,而從造成大事務問題。
2、編程式事務
上面聊的這些內容都是基於 @Transactional注解的,主要說的是它的事務問題,我們把這種事務叫做:聲明式事務。
其實,spring還提供了另外一種創建事務的方式,即通過手動編寫代碼實現的事務,我們把這種事務叫做:編程式事務。例如:
@Autowired private TransactionTemplate transactionTemplate; ... public void save(final User user) { queryData1(); queryData2(); transactionTemplate.execute((status) => { addData1(); updateData2(); return Boolean.TRUE; }) }
在spring中為了支持編程式事務,專門提供了一個類:TransactionTemplate,在它的execute方法中,就實現了事務的功能。
相較於 @Transactional注解聲明式事務,在某些場景下其實更適合使用基於 TransactionTemplate的編程式事務。主要原因如下:
1、避免由於spring aop問題,導致事務失效的問題。 2、能夠更小粒度的控制事務的范圍,更直觀。