linux命令之字串擷取-cut命令
對於從事 java 開發工作的同學來說,spring 的事務肯定再熟悉不過了。
在某些業務場景下,如果一個請求中,需要同時寫入多張表的資料。為了保證操作的原子性(要麼同時成功,要麼同時失敗),避免資料不一致的情況,我們一般都會用到 spring 事務。
確實,spring 事務用起來賊爽,就用一個簡單的註解:@Transactional
,就能輕鬆搞定事務。我猜大部分小夥伴也是這樣用的,而且一直用一直爽。
但如果你使用不當,它也會坑你於無形。
今天我們就一起聊聊,事務失效的一些場景,說不定你已經中招了。不信,讓我們一起看看
1、事務不生效
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;
}
}
也就是說,如果我們自定義的事務方法(即目標方法),它的訪問許可權不是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 方法擁有事務的能力是因為 spring aop 生成代理了物件,但是這種方法直接呼叫了 this 物件的方法,所以 updateStatus 方法不會生成事務。
由此可見,在同一個類中的方法直接內部呼叫,會導致事務失效。
那麼問題來了,如果有些場景,確實想在同一個類的某個方法中,呼叫它自己的另外一個方法,該怎麼辦呢?
3.1、新新增一個service方法
這個方法非常簡單,只需要新加一個 Service 方法,把 @Transactional 註解加到新 Service 方法上,把需要事務執行的程式碼移到新方法中。具體程式碼如下:
@Servcie
public class ServiceA {
@Autowired
prvate ServiceB serviceB;
public void save(User user) {
queryData1();
queryData2();
serviceB.doSave(user);
}
}
@Servcie
public class ServiceB {
@Transactional(rollbackFor=Exception.class)
public void doSave(User user) {
addData1();
updateData2();
}
}
3.2、在該service中自己注入自己
如果不想再新加一個 Service 類,在該 Service 類中注入自己也是一種選擇。具體程式碼如下:
@Servcie
public class ServiceA {
@Autowired
prvate ServiceA serviceA;
public void save(User user) {
queryData1();
queryData2();
serviceA.doSave(user);
}
@Transactional(rollbackFor=Exception.class)
public void doSave(User user) {
addData1();
updateData2();
}
}
可能有些人會有這樣的疑問:這種做法會不會出現迴圈依賴問題?
答案:不會。
其實 spring ioc 內部的三級快取保證了它不會出現迴圈依賴問題。
3.3、通過AopConponent類
在該 Service 類中使用 AopContext.currentProxy() 獲取代理物件。
上面的方法 2 確實可以解決問題,但是程式碼看起來並不直觀,還可以通過在該 Service 類中使用 AOPProxy 獲取代理物件,實現相同的功能。具體程式碼如下:
@Servcie
public class ServiceA {
public void save(User user) {
queryData1();
queryData2();
// 這個我試過,感覺不太行
((ServiceA)AopContext.currentProxy()).doSave(user);
}
@Transactional(rollbackFor=Exception.class)
public void doSave(User user) {
addData1();
updateData2();
}
}
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 還不支援行鎖和外來鍵。
所以在實際業務場景中,myisam 使用的並不多。在 mysql5 以後,myisam 已經逐漸退出了歷史的舞臺,取而代之的是 innodb。
有時候我們在開發的過程中,發現某張表的事務一直都沒有生效,那不一定是 spring 事務的鍋,最好確認一下你使用的那張表,是否支援事務。
7、未開啟事務
有時候,事務沒有生效的根本原因是沒有開啟事務。
你看到這句話可能會覺得好笑。
開啟事務不是一個專案中,最最最基本的功能嗎?
為什麼還會沒有開啟事務?
沒錯,如果專案已經搭建好了,事務功能肯定是有的。
但如果你是在搭建專案 demo 的時候,只有一張表,而這張表的事務沒有生效。那麼會是什麼原因造成的呢?
當然原因有很多,但沒有開啟事務,這個原因極其容易被忽略。
如果你使用的是 springboot 專案,那麼你很幸運。因為 springboot 通過DataSourceTransactionManagerAutoConfiguration
類,已經默默地幫你開啟了事務。
你所要做的事情很簡單,只需要配置spring.datasource
相關引數即可。
但如果你使用的還是傳統的 spring 專案,則需要在 applicationContext.xml 檔案中,手動配置事務相關引數。如果忘了配置,事務肯定是不會生效的。
<!-- 配置事務管理器 -->
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<tx:advice id="advice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<!-- 用切點把事務切進去 -->
<aop:config>
<aop:pointcut expression="execution(* com.susan.*.*(..))" id="pointcut"/>
<aop:advisor advice-ref="advice" pointcut-ref="pointcut"/>
</aop:config>
默默地說一句,如果在 pointcut 標籤中的切入點匹配規則,配錯了的話,有些類的事務也不會生效。
2、事務不回滾
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 事務能夠正常回滾,必須丟擲它能夠處理的異常。如果沒有拋異常,則 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。
5、巢狀事務回滾多了
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleService roleService;
@Transactional
public void add(UserModel userModel) throws Exception {
userMapper.insertUser(userModel);
roleService.doOtherThing();
}
}
@Service
public class RoleService {
@Transactional(propagation = Propagation.NESTED)
public void doOtherThing() {
System.out.println("儲存role表資料");
}
}
這種情況使用了巢狀的內部事務,原本是希望呼叫 roleService.doOtherThing 方法時,如果出現了異常,只回滾 doOtherThing 方法裡的內容,不回滾 userMapper.insertUser 裡的內容,即回滾儲存點。但事實是,insertUser 也回滾了。
why?因為 doOtherThing 方法出現了異常,沒有手動捕獲,會繼續往上拋,到外層 add 方法的代理方法中捕獲了異常。所以,這種情況是直接回滾了整個事務,不只回滾單個儲存點。
怎麼樣才能只回滾儲存點呢?
@Slf4j
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleService roleService;
@Transactional
public void add(UserModel userModel) throws Exception {
userMapper.insertUser(userModel);
try {
roleService.doOtherThing();
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}
可以將內部巢狀事務放在 try/catch 中,並且不繼續往上拋異常。這樣就能保證,如果內部巢狀事務中出現異常,只回滾內部事務,而不影響外部事務。
那麼這裡針對於事務註解巢狀的方式,也有著對應的幾種解決方式。
如果說兩個都有,那麼使用依賴注入的方式來進行操作。這種方式比較笨拙,但是也需要考慮到兩個事務之間的問題,
若果想要一個丟擲,另外一個不需要進行回滾的操作,那麼應當如何來進行操作。。。。
2、其他
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
的程式設計式事務。主要原因如下:
- 避免由於 spring aop 問題導致事務失效的問題。
- 能夠更小粒度地控制事務的範圍,更直觀。
建議在專案中少使用 @Transactional 註解開啟事務。但並不是說一定不能用它,如果專案中有些業務邏輯比較簡單,而且不經常變動,使用 @Transactional 註解開啟事務也無妨,因為它更簡單,開發效率更高,但是千萬要小心事務失效的問題