麻了,這讓人絕望的大事務(wù)提交
一、背景
繼上次的if else優(yōu)化也有段時(shí)間了,最近小貓又又又著道了,接手的那個(gè)項(xiàng)目又遇到了坑爹的地方,經(jīng)常性的報(bào)死鎖異常,經(jīng)常性的主從延遲......通過報(bào)錯(cuò)信息按圖索驥,發(fā)現(xiàn)代碼是這樣的。
這是一段商品發(fā)布的邏輯,我們可以看到參數(shù)校驗(yàn)、查詢、最終的insert以及update全部揉在一個(gè)事務(wù)中。遇到批量發(fā)布商品的時(shí)候就經(jīng)常出現(xiàn)問題了,數(shù)據(jù)庫主從延遲是肯定少不了的。
二、開啟優(yōu)化
其實(shí)像上述小貓遇到的這種狀況我們就稱其為大事務(wù),那么我們就大概有這么一個(gè)定義。我們將執(zhí)行時(shí)間長(zhǎng),并且操作數(shù)據(jù)比較多的事務(wù)叫做大事務(wù)。
1.大事務(wù)產(chǎn)生的原因
在我們?nèi)粘i_發(fā)過程中,其實(shí)經(jīng)常會(huì)遇到大事務(wù),老貓總結(jié)了一下,往往原因其實(shí)總結(jié)下來有這么幾點(diǎn)(當(dāng)然存在紕漏的地方,也歡迎大家評(píng)論區(qū)留言補(bǔ)充):
- 一次性操作的數(shù)據(jù)量確實(shí)多,大量的鎖競(jìng)爭(zhēng),比如批量操作這種行為。
- 事務(wù)粒度過大,代碼中的 @Transactional使用不當(dāng),其他非DB操作比較多,耗時(shí)久。比如調(diào)用RPC接口,在例如上述小貓遇到的check邏輯甚至都揉在一起等等。
2.造成的影響
那么大事務(wù)造成的影響又是什么呢?
從開發(fā)者的角度來看的話,部分大事務(wù)必定對(duì)應(yīng)的復(fù)雜的業(yè)務(wù)邏輯,代碼封裝事務(wù)拆解不合理,研發(fā)側(cè)維護(hù)困難,維護(hù)成本高。
從最終系統(tǒng)以及運(yùn)維角度來看:
- 出現(xiàn)了死鎖。
- 造成了主從延遲。
- 大事務(wù)消耗更多的磁盤空間,回滾成本高。
- 大事務(wù)發(fā)生的過程中,由于連接池持續(xù)被打開,很容易造成數(shù)據(jù)庫連接池被沾滿。
- 接口響應(yīng)慢導(dǎo)致接口超時(shí),甚至導(dǎo)致服務(wù)不可用等等 (歡迎大家補(bǔ)充)
3.優(yōu)化方案
大事務(wù)既然有這么多坑,那么我們來看一下我們?nèi)粘i_發(fā)過程中,應(yīng)該如何做到盡量規(guī)避呢?老貓整理了以下幾種優(yōu)化方法。
a.降低事務(wù)顆粒度,大事務(wù)拆解小事務(wù):
- 編程式事務(wù)代替@Transactional。
- 非update以及insert動(dòng)作外移。
b.大數(shù)據(jù)量一次性提交盡可能拆解分批處理。
c.拆解原始事務(wù),異步化處理。
(1) 降低事務(wù)顆粒度
1、我們對(duì)@Transactional的事務(wù)粒度把控不好,有時(shí)候如果使用不當(dāng)?shù)脑捠聞?wù)功能可能會(huì)失效,如果經(jīng)驗(yàn)不足,很難排查,那么我們不如直接使用粗細(xì)粒度更好把控的編程式事務(wù)。TransactionTemplate。這樣的話咱們的優(yōu)化代碼就可以寫好才能如下方式。
@Autowired
private TransactionTemplate transactionTemplate;
public boolean publishProduct(PublishProductRequest request) {
externalSellerAuthorizeService.checkAuthorizeValid(request.getSellerId(),request.getThirdCategoryId(),request.getBrandId());
......
transactionTemplate.execute((status) -> {
try{
//執(zhí)行insert
productDao.insert(productDO);
productDescDao.insert(productDescDO);
....
//其他insert以及update操作
}catch (Exception e) {
//回滾
status.setRollbackOnly();
return true;
}
return false;
});
return true;
}
非update以及insert動(dòng)作外移。
原始代碼:
@Transactional(rollbackFor=Exception.class)
public void save(Req req) {
checkParam(req);
saveData1(req);
updateData2(req);
}
private void checkParam(Req req){
Data1 data = selectData1();
Data2 data2 = selectData2();
if(data.getSomeThing() != STATUS_YES){
throw new BusinessTimeException(.....);
}
}
然后部分小伙伴就覺得外移么,如果不用@Transactional的情況,那直接這樣不就行了么。
錯(cuò)誤改造案例:
class ServiceAImpl implements ServiceA {
@Transactional(rollbackFor=Exception.class)
public void save(Req req) {
saveData1(req);
updateData2(req);
}
private void checkParam(Req req){
Data1 data = selectData1();
Data2 data2 = selectData2();
if(data.getSomeThing() != STATUS_YES){
throw new BusinessTimeException(.....);
}
}
public void save(Req req){
checkParam(req);
doSave(req);
}
}
這個(gè)例子是非常經(jīng)典的錯(cuò)誤,這種直接方法調(diào)用的做法事務(wù)不會(huì)生效,老貓以前也踩過這樣的坑。因?yàn)?@Transactional 注解的聲明式事務(wù)是通過 spring aop 起作用的, 而 spring aop 需要生成代理對(duì)象,直接方法調(diào)用使用的還是原始對(duì)象,所以事務(wù)不會(huì)生效。那么我們應(yīng)該如何改造呢?我們看下正確的改造。
正確改造方案1,當(dāng)然還是利用上面的TransactionTemplate:
@Autowired
private TransactionTemplate transactionTemplate;
public void save(Req req) {
checkParam(req);
transactionTemplate.execute((status) -> {
try{
saveData1(req);
updateData2(req);
....
//其他insert以及update操作
}catch (Exception e) {
//回滾
status.setRollbackOnly();
return true;
}
return false;
});
}
private void checkParam(Req req){
Data1 data = selectData1();
Data2 data2 = selectData2();
if(data.getSomeThing() != STATUS_YES){
throw new BusinessTimeException(.....);
}
}
正確改造方案2,把 @Transactional 注解加到新Service方法上,把需要事務(wù)執(zhí)行的代碼移到新方法中。
@Servcie
public class ServiceA {
@Autowired
private ServiceB serviceB;
private void checkParam(Req req){
Data1 data = selectData1();
Data2 data2 = selectData2();
if(data.getSomeThing() != STATUS_YES){
throw new BusinessTimeException(.....);
}
}
public void save(Req req) {
checkParam(req);
serviceB.save(req);
}
}
@Servcie
public class ServiceB {
@Transactional(rollbackFor=Exception.class)
public void save(Req req) {
saveData1(req);
updateData2(req);
}
}
正確改造方案3:將ServiceA 再次注入到自身(老貓覺得這種方式不優(yōu)雅,不太推薦,這里就不寫了)
(2) 大數(shù)據(jù)量一次性提交盡可能拆解分批處理
我們?cè)賮砜创髷?shù)量批量請(qǐng)求的場(chǎng)景,咱們具體來分析一下,假設(shè)上游系統(tǒng)存在一個(gè)批量導(dǎo)入2w的數(shù)據(jù)操作。如果我們讀取到上游導(dǎo)入的數(shù)據(jù),并且直接執(zhí)行DB一次性執(zhí)行肯定是不合適的。這種情況就需要我們對(duì)其請(qǐng)求的數(shù)據(jù)量做一個(gè)拆解。我們可以采用Lists.partition等等方式將數(shù)據(jù)拆成多個(gè)小的批量然后再進(jìn)行入庫操作處理。
@Servcie
public class ServiceA {
@Autowired
private ServiceB serviceB;
private void batchAdd(List<Long> inventorySkuIdList){
List<List<Long>> partition = Lists.partition(inventorySkuIdList, 1000);
for (List<Long> idList : partition) {
List<InventorySkuDO> inventorySkuDOList = inventorySkuDao.selectByIdList(idList, null);
if (CollectionUtils.isNotEmpty(inventorySkuDOList)) {
serviceB.doInsertUpdate(inventorySkuDOList);
}
}
}
}
@Servcie
public class ServiceB {
@Transactional(rollbackFor=Exception.class)
private void doInsertUpdate(List<InventorySkuDO> inventorySkuDOList){
for (InventorySkuDO inventorySkuDO : inventorySkuDOList) {
doInsert(inventorySkuDO);
doUpdate(inventorySkuDO)
}
}
}
(3) 拆解原始事務(wù),異步化處理
這種異步化處理的方案其實(shí)有兩種方式進(jìn)行異步化操作。尤其是涉及到第三方RPC調(diào)用或者HTTP調(diào)用的時(shí)候,這種方案就更加適合。
方案一,采用CompletableFuture異步編排特性,當(dāng)業(yè)務(wù)流程比較長(zhǎng)的時(shí)候,我們可以將一個(gè)大業(yè)務(wù)拆解成多個(gè)小的任務(wù)進(jìn)行異步化執(zhí)行。比如咱們有個(gè)批量支付的業(yè)務(wù)邏輯,因?yàn)檎麄€(gè)流程是同步的,所以大概有了下面這樣的流程。(關(guān)于CompletableFeature老貓覺得挺有意思的,后續(xù)老貓會(huì)出專門的文章來理透該特性,歡迎大家持續(xù)關(guān)注)。
completeFeature
對(duì)應(yīng)轉(zhuǎn)換成代碼邏輯的話,大概是這樣的:
void doBatchPay() {
CompletableFuture<Object> task1 = CompletableFuture.supplyAsync(() -> {
return "訂單信息";
});
CompletableFuture<Object> task2 = CompletableFuture.supplyAsync(() -> {
try {
return doPay();
} catch (InterruptedException e) {
//log add
}
});
//task1、task2 執(zhí)行完執(zhí)行task3 ,需要感知task1和task2的執(zhí)行結(jié)果
CompletableFuture<Object> future = task1.thenCombineAsync(task2, (t1, t2) -> {
return "郵件發(fā)送成功";
});
}
方案二,Mq異步化處理,還是針對(duì)上述業(yè)務(wù)邏輯,我們是否可以將最終的發(fā)送郵件的動(dòng)作剝離出來,最終再去統(tǒng)一執(zhí)行發(fā)送郵件。
關(guān)于偽代碼這里不展開了,有興趣的小伙伴可以自行實(shí)現(xiàn)一下。