前言
最近有個網友問了我一個問題:系統中大事務問題要如何處理?mysql
正好前段時間我在公司處理過這個問題,咱們當時因爲項目初期時間比較緊張,爲了快速完成業務功能,忽略了系統部分性能問題。項目順利上線後,專門抽了一個迭代的時間去解決大事務問題,目前已經優化完成,而且順利上線。現給你們總結了一下,咱們當時使用的一些解決辦法,以便你們被相同問題困擾時,能夠參考一下。web
大事務引起的問題
在分享解決辦法以前,先看看系統中若是出現大事務可能會引起哪些問題面試
從上圖能夠看出若是系統中出現大事務時,問題還不小,因此咱們在實際項目開發中應該儘可能避免大事務的狀況。若是咱們已有系統中存在大事務問題,該如何解決呢?redis
解決辦法
少用@Transactional註解
你們在實際項目開發中,咱們在業務方法加上@Transactional
註解開啓事務功能,這是很是廣泛的作法,它被稱爲聲明式事務
。spring
部分代碼以下:sql
@Transactional(rollbackFor=Exception.class)
public void save(User user) {
doSameThing...
}
然而,我要說的第一條是:少用@Transactional
註解。mongodb
爲何?數據庫
-
咱們知道 @Transactional
註解是經過spring
的aop
起做用的,可是若是使用不當,事務功能可能會失效。若是恰巧你經驗不足,這種問題不太好排查。至於事務哪些狀況下會失效,能夠參考我以前寫的《 spring事務的這10種坑,你稍不注意可能就會踩中!!!》這篇文章。 -
@Transactional
註解通常加在某個業務方法上,會致使整個業務方法都在同一個事務中,粒度太粗,很差控制事務範圍,是出現大事務問題的最多見的緣由。
那咱們該怎麼辦呢?編程
可使用編程式事務
,在spring
項目中使用TransactionTemplate
類的對象,手動執行事務。緩存
部分代碼以下:
@Autowired
private TransactionTemplate transactionTemplate;
...
public void save(final User user) {
transactionTemplate.execute((status) => {
doSameThing...
return Boolean.TRUE;
})
}
從上面的代碼中能夠看出,使用TransactionTemplate
的編程式事務
功能本身靈活控制事務的範圍,是避免大事務問題的首選辦法。
固然,我說少使用@Transactional
註解開啓事務,並非說必定不能用它,若是項目中有些業務邏輯比較簡單,並且不常常變更,使用@Transactional
註解開啓事務開啓事務也無妨,由於它更簡單,開發效率更高,可是千萬要當心事務失效的問題。
將查詢(select)方法放到事務外
若是出現大事務,能夠將查詢(select)方法放到事務外,也是比較經常使用的作法,由於通常狀況下這類方法是不須要事務的。
好比出現以下代碼:
@Transactional(rollbackFor=Exception.class)
public void save(User user) {
queryData1();
queryData2();
addData1();
updateData2();
}
能夠將queryData1
和queryData2
兩個查詢方法放在事務外執行,將真正須要事務執行的代碼才放到事務中,好比:addData1
和updateData2
方法,這樣就能有效的減小事務的粒度。
若是使用TransactionTemplate
的編程式事務
這裏就很是好修改。
@Autowired
private TransactionTemplate transactionTemplate;
...
public void save(final User user) {
queryData1();
queryData2();
transactionTemplate.execute((status) => {
addData1();
updateData2();
return Boolean.TRUE;
})
}
可是若是你實在仍是想用@Transactional
註解,該怎麼拆分呢?
public void save(User user) {
queryData1();
queryData2();
doSave();
}
@Transactional(rollbackFor=Exception.class)
public void doSave(User user) {
addData1();
updateData2();
}
這個例子是很是經典的錯誤,這種直接方法調用的作法事務不會生效,給正在坑中的朋友提個醒。由於@Transactional
註解的聲明式事務是經過spring aop
起做用的,而spring aop
須要生成代理對象,直接方法調用使用的仍是原始對象,因此事務不會生效。
有沒有辦法解決這個問題呢?
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();
}
}
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
內部的三級緩存保證了它,不會出現循環依賴問題。若是你想進一步瞭解循環依賴問題,能夠看看我以前文章《spring解決循環依賴爲何要用三級緩存?》。
3.在該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();
}
}
事務中避免遠程調用
咱們在接口中調用其餘系統的接口是不能避免的,因爲網絡不穩定,這種遠程調的響應時間可能比較長,若是遠程調用的代碼放在某個事物中,這個事物就多是大事務。固然,遠程調用不只僅是指調用接口,還有包括:發MQ消息,或者鏈接redis、mongodb保存數據等。
@Transactional(rollbackFor=Exception.class)
public void save(User user) {
callRemoteApi();
addData1();
}
遠程調用的代碼可能耗時較長,切記必定要放在事務以外。
@Autowired
private TransactionTemplate transactionTemplate;
...
public void save(final User user) {
callRemoteApi();
transactionTemplate.execute((status) => {
addData1();
return Boolean.TRUE;
})
}
有些朋友可能會問,遠程調用的代碼不放在事務中如何保證數據一致性呢?這就須要創建:重試
+補償機制
,達到數據最終一致性
了。
事務中避免一次性處理太多數據
若是一個事務中須要處理的數據太多,也會形成大事務問題。好比爲了操做方便,你可能會一次批量更新1000條數據,這樣會致使大量數據鎖等待,特別在高併發的系統中問題尤其明顯。
解決辦法是分頁處理,1000條數據,分50頁,一次只處理20條數據,這樣能夠大大減小大事務的出現。
非事務執行
在使用事務以前,咱們都應該思考一下,是否是全部的數據庫操做都須要在事務中執行?
@Autowired
private TransactionTemplate transactionTemplate;
...
public void save(final User user) {
transactionTemplate.execute((status) => {
addData();
addLog();
updateCount();
return Boolean.TRUE;
})
}
上面的例子中,其實addLog
增長操做日誌方法 和 updateCount
更新統計數量方法,是能夠不在事務中執行的,由於操做日誌和統計數量這種業務容許少許數據不一致的狀況。
@Autowired
private TransactionTemplate transactionTemplate;
...
public void save(final User user) {
transactionTemplate.execute((status) => {
addData();
return Boolean.TRUE;
})
addLog();
updateCount();
}
固然大事務中要鑑別出哪些方法能夠非事務執行,其實沒那麼容易,須要對整個業務梳理一遍,才能找出最合理的答案。
異步處理
還有一點也很是重要,是否是事務中的全部方法都須要同步執行?咱們都知道,方法同步執行須要等待方法返回,若是一個事務中同步執行的方法太多了,勢必會形成等待時間過長,出現大事務問題。
看看下面這個列子:
@Autowired
private TransactionTemplate transactionTemplate;
...
public void save(final User user) {
transactionTemplate.execute((status) => {
order();
delivery();
return Boolean.TRUE;
})
}
order
方法用於下單,delivery
方法用於發貨,是否是下單後就必定要立刻發貨呢?
答案是否認的。
這裏發貨功能其實能夠走mq異步處理邏輯。
@Autowired
private TransactionTemplate transactionTemplate;
...
public void save(final User user) {
transactionTemplate.execute((status) => {
order();
return Boolean.TRUE;
})
sendMq();
}
總結
本人從網友的一個問題出發,結合本身實際的工做經驗分享了處理大事務的6種辦法:
-
少用@Transactional註解 -
將查詢(select)方法放到事務外 -
事務中避免遠程調用 -
事務中避免一次性處理太多數據 -
非事務執行 -
異步處理
最後說一句
若是這篇文章對您有所幫助,或者有所啓發的話,幫忙掃描下發二維碼關注一下,或者點贊、轉發、在看。在公衆號中回覆:面試、代碼神器、開發手冊、時間管理有超讚的粉絲福利,另外回覆:加羣,能夠跟不少大廠的前輩交流和學習。
推薦閱讀:
本文分享自微信公衆號 - 蘇三說技術(gh_9f551dfec941)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。