讓人頭痛的大事務問題到底要如何解決?

前言

最近有個網友問了我一個問題:系統中大事務問題要如何處理?mysql

正好前段時間我在公司處理過這個問題,咱們當時因爲項目初期時間比較緊張,爲了快速完成業務功能,忽略了系統部分性能問題。項目順利上線後,專門抽了一個迭代的時間去解決大事務問題,目前已經優化完成,而且順利上線。現給你們總結了一下,咱們當時使用的一些解決辦法,以便你們被相同問題困擾時,能夠參考一下。web

大事務引起的問題

在分享解決辦法以前,先看看系統中若是出現大事務可能會引起哪些問題面試

從上圖能夠看出若是系統中出現大事務時,問題還不小,因此咱們在實際項目開發中應該儘可能避免大事務的狀況。若是咱們已有系統中存在大事務問題,該如何解決呢?redis

解決辦法

少用@Transactional註解

你們在實際項目開發中,咱們在業務方法加上@Transactional註解開啓事務功能,這是很是廣泛的作法,它被稱爲聲明式事務spring

部分代碼以下:sql

@Transactional(rollbackFor=Exception.class)
public void save(User user)
{
doSameThing...
}

然而,我要說的第一條是:少用@Transactional註解。mongodb

爲何?數據庫

  1. 咱們知道 @Transactional註解是經過 springaop起做用的,可是若是使用不當,事務功能可能會失效。若是恰巧你經驗不足,這種問題不太好排查。至於事務哪些狀況下會失效,能夠參考我以前寫的《 spring事務的這10種坑,你稍不注意可能就會踩中!!!》這篇文章。
  2. @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();
}

能夠將queryData1queryData2兩個查詢方法放在事務外執行,將真正須要事務執行的代碼才放到事務中,好比:addData1updateData2方法,這樣就能有效的減小事務的粒度。

若是使用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種辦法:

  1. 少用@Transactional註解
  2. 將查詢(select)方法放到事務外
  3. 事務中避免遠程調用
  4. 事務中避免一次性處理太多數據
  5. 非事務執行
  6. 異步處理

最後說一句

若是這篇文章對您有所幫助,或者有所啓發的話,幫忙掃描下發二維碼關注一下,或者點贊、轉發、在看。在公衆號中回覆:面試、代碼神器、開發手冊、時間管理有超讚的粉絲福利,另外回覆:加羣,能夠跟不少大廠的前輩交流和學習。

推薦閱讀:

mysql的這幾個坑你踩過沒?真是防不勝防
線程池最佳線程數量到底要如何配置?
這8種保證線程安全的技術你都知道嗎?
springboot面試殺手鐗-自動配置原理
11張圖讓你完全明白jdk1.7 hashmap的死循環是如何產生的
硬核 | 使用spring cache讓個人接口性能瞬間提高了100倍
實戰|如何消除又臭又長的if...else判斷更優雅的編程?
老司機手把手教你編寫本身的springboot starter
面試前看了這篇spring事務的文章,讓我多要了2k的工資


本文分享自微信公衆號 - 蘇三說技術(gh_9f551dfec941)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索