作者:肥朝 出處:公眾號【肥朝】
前言本篇主要講解的是前陣子的一個壓測問題.那么就直接開門見山 
可能有的朋友不并不知道 forceTransactionTemplate 這個是干嘛的,首先這里先普及一下,在Java中,我們一般開啟事務(wù)就有三種方式 XML中根據(jù)service及方法名配置切面,來開啟事務(wù)(前幾年用的頻率較高,現(xiàn)在基本很少用) @Transactional注解開啟事務(wù)(使用頻率最高) 采用spring的事務(wù)模板(截圖中的方式,幾乎沒什么人用)
我們先不糾結(jié)為什么使用第三種,后面在講 事務(wù)傳播機(jī)制 的時候我會專門介紹,我們聚焦一下主題,你現(xiàn)在只要知道,那個是開啟事務(wù)的意思就行了.我特意用紅色和藍(lán)色把日志代碼圈起來,意思就是,進(jìn)入方法的時候打印日志,然后開啟事務(wù)后,再打印一個日志.一波壓測之后,發(fā)現(xiàn)接口頻繁超時,數(shù)據(jù)一致壓不上去.我們查看日志如下: 我們發(fā)現(xiàn).這兩個日志輸出時間間隔,竟然用了接近5秒!開個事務(wù)為何用了5秒? 事出反常必有妖! 如何切入解決問題線上遇到高并發(fā)的問題,由于一般高并發(fā)問題重現(xiàn)難度比較大,所以一般肥朝都是采用眼神編譯,九淺一深靜態(tài)看源碼的方式來分析.具體可以參考本地可跑,上線就崩?慌了!.但是考慮到肥朝公眾號仍然有小部分新關(guān)注的粉絲尚未掌握分析問題的技巧,本篇就再講一些遇到此類問題的一些常見分析方式,不至于遇到問題時, 慌得一比! 好在這個并發(fā)問題的難度并不大,本篇案例排查非常適合小白入門,我們可以通過本地模擬場景重現(xiàn),將問題范圍縮小,從而逐步定位問題. 本地重現(xiàn)首先我們可以準(zhǔn)備一個并發(fā)工具類,通過這個工具類,可以在本地環(huán)境模擬并發(fā)場景.手機(jī)查看代碼并不友好,但是沒關(guān)系,以下代碼均是給你復(fù)制粘貼進(jìn)項目重現(xiàn)問題用的, 并不是給你手機(jī)上看的 .至于這個工具類為什么能模擬并發(fā)場景,由于這個工具類的代碼 全是JDK中的代碼 ,核心就是 CountDownLatch 類,這個原理你根據(jù)我提供的關(guān)鍵字對著你喜歡的搜索引擎搜索即可. CountDownLatchUtil.java
1 public class CountDownLatchUtil {
2
3 private CountDownLatch start;
4 private CountDownLatch end;
5 private int pollSize = 10;
6
7 public CountDownLatchUtil() {
8 this(10);
9 }
10
11 public CountDownLatchUtil(int pollSize) {
12 this.pollSize = pollSize;
13 start = new CountDownLatch(1);
14 end = new CountDownLatch(pollSize);
15 }
16
17 public void latch(MyFunctionalInterface functionalInterface) throws InterruptedException {
18 ExecutorService executorService = Executors.newFixedThreadPool(pollSize);
19 for (int i = 0; i < pollSize; i++) {
20 Runnable run = new Runnable() {
21 @Override
22 public void run() {
23 try {
24 start.await();
25 functionalInterface.run();
26 } catch (InterruptedException e) {
27 e.printStackTrace();
28 } finally {
29 end.countDown();
30 }
31 }
32 };
33 executorService.submit(run);
34 }
35
36 start.countDown();
37 end.await();
38 executorService.shutdown();
39 }
40
41 @FunctionalInterface
42 public interface MyFunctionalInterface {
43 void run();
44 }
45}
HelloService.java
1 public interface HelloService {
2
3 void sayHello(long timeMillis);
4
5}
HelloServiceImpl.java
1 @Service
2 public class HelloServiceImpl implements HelloService {
3
4 private final Logger log = LoggerFactory.getLogger(HelloServiceImpl.class);
5
6 @Transactional
7 @Override
8 public void sayHello(long timeMillis) {
9 long time = System.currentTimeMillis() - timeMillis;
10 if (time > 5000) {
11 //超過5秒的打印日志輸出
12 log.warn('time : {}', time);
13 }
14 try {
15 //模擬業(yè)務(wù)執(zhí)行時間為1s
16 Thread.sleep(1000);
17 } catch (Exception e) {
18 e.printStackTrace();
19 }
20 }
21}
HelloServiceTest.java
1 @RunWith(SpringRunner.class)
2 @SpringBootTest
3 public class HelloServiceTest {
4
5 @Autowired
6 private HelloService helloService;
7
8 @Test
9 public void testSayHello() throws Exception {
10 long currentTimeMillis = System.currentTimeMillis();
11 //模擬1000個線程并發(fā)
12 CountDownLatchUtil countDownLatchUtil = new CountDownLatchUtil(1000);
13 countDownLatchUtil.latch(() -> {
14 helloService.sayHello(currentTimeMillis);
15 });
16 }
17
18 }
我們從本地調(diào)試的日志中,發(fā)現(xiàn)了大量超過5s的接口,并且還有一些規(guī)律,肥朝特地用不同顏色的框框給大家框起來 
為什么這些時間,都是5個為一組,且每組數(shù)據(jù)相差是1s左右呢? 真相大白@Transactional 的核心代碼如下(后續(xù)我會專門一個系列分析這部分源碼,關(guān)注肥朝以免錯過核心內(nèi)容).這里簡單說就是 retVal=invocation.proceedWithInvocation() 方法會去獲取數(shù)據(jù)庫連接.
1 if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
2 // Standard transaction demarcation with getTransaction and commit/rollback calls.
3 TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
4 Object retVal = null;
5 try {
6 // This is an around advice: Invoke the next interceptor in the chain.
7 // This will normally result in a target object being invoked.
8 retVal = invocation.proceedWithInvocation();
9 }
10 catch (Throwable ex) {
11 // target invocation exception
12 completeTransactionAfterThrowing(txInfo, ex);
13 throw ex;
14 }
15 finally {
16 cleanupTransactionInfo(txInfo);
17 }
18 commitTransactionAfterReturning(txInfo);
19 return retVal;
20}
然后肥朝為了更好的演示這個問題,將數(shù)據(jù)庫連接池(本篇用的是Druid)的參數(shù)做了以下設(shè)置 1 //初始連接數(shù)
2 spring.datasource.initialSize=1
3 //最大連接數(shù)
4 spring.datasource.maxActive=5
由于最大連接數(shù)是5.所以當(dāng)1000個線程并發(fā)進(jìn)來的時候,你可以想象是一個隊伍有1000個人排隊,最前面的5個,拿到了連接,并且執(zhí)行業(yè)務(wù)時間為1秒.那么隊伍中剩下的995個人,就在門外等候.等這5個執(zhí)行完的時候.釋放了5個連接,依次向后的5個人又進(jìn)來,又執(zhí)行1秒的業(yè)務(wù)操作.通過簡單的小學(xué)數(shù)學(xué),都可以計算出最后5個執(zhí)行完,需要多長時間.通過這里分析,你就知道,為什么上面的日志輸出,是5秒為一組了,并且每組間隔為1s了. 怎么解決看過肥朝源碼實戰(zhàn)的粉絲都知道,肥朝從來不耍流氓,凡是拋出問題,都會相應(yīng)給出 其中一種 解決方案.當(dāng)然方案 沒有最優(yōu)只有更優(yōu)! 比如看到這里有的朋友可能會說,你最大連接數(shù)設(shè)置得 就像平時贊賞肥朝的金額一樣小 ,如果設(shè)置大一點,自然就不會有問題了.當(dāng)然這里為了方便向大家演示問題,設(shè)置了最大連接數(shù)是5.正常生產(chǎn)的連接數(shù)是要根據(jù)業(yè)務(wù)特點和不斷壓測才能得出合理的值,當(dāng)然肥朝也了解到,部分同學(xué)公司機(jī)器的配置,竟然比不過市面上的 千元手機(jī)!!! 但是其實當(dāng)時壓測的時候,數(shù)據(jù)庫的最大連接數(shù)設(shè)置的是200,并且當(dāng)時的壓測壓力并不大.那為什么還會有這個問題呢?那么仔細(xì)看前面的代碼 
其中這個 校驗 的代碼是RPC調(diào)用,該接口的同事并沒有像肥朝一樣 值得托付終身般的高度可靠 ,導(dǎo)致耗時時間較長,從而導(dǎo)致后續(xù)線程獲取數(shù)據(jù)庫連接等待的時間過長.你再根據(jù)前面說的小學(xué)數(shù)學(xué)來算一下就很容易明白該壓測問題出現(xiàn)的原因. 敲黑板劃重點之前肥朝就反復(fù)說過,遇到問題,要經(jīng)過深度思考.比如這個問題,我們能得到什么拓展性的思考呢?我們來看一下之前一位粉絲的面試經(jīng)歷 
其實他面試遇到的這個問題,和我們這個壓測問題基本是同一個問題,只不過面試官的結(jié)論其實并不夠準(zhǔn)確.我們來一起看一下阿里巴巴的開發(fā)手冊 那么什么樣叫做濫用呢?其實肥朝認(rèn)為,即使這個方法經(jīng)常調(diào)用,但是都是單表insert、update操作,執(zhí)行時間非常短,那么承受較大并發(fā)問題也不大.關(guān)鍵是,這個事務(wù)中的所有方法調(diào)用,是否是有意義的,或者說,事務(wù)中的方法是否是真的要事務(wù)保證,才是關(guān)鍵.因為部分同學(xué),在一些比較傳統(tǒng)的公司,做的多是 能用就行 的CRUD工作,很容易一個service方法,就直接打上事務(wù)注解開始事務(wù),然后在一個事務(wù)中,進(jìn)行大量和事務(wù)一毛錢關(guān)系都沒有的無關(guān)耗時操作,比如文件IO操作,比如查詢校驗操作等.例如本文中的 業(yè)務(wù)校驗 就完全沒必要放在事務(wù)中.平時工作中沒有相應(yīng)的實戰(zhàn)場景,加上并沒有關(guān)注肥朝的公眾號,對原理源碼真實實戰(zhàn)場景一無所知.面試稍微一問原理就喊痛,面試官也只好換個方向再繼續(xù)深入! 通過這個經(jīng)歷我們又有什么拓展性的思考呢?因為問題是永遠(yuǎn)解決不完的,但是我們可以通過不斷的思考,把這個問題壓榨出更多的價值!我們再來看一下阿里規(guī)范手冊 用大白話概括就是,盡量減少鎖的粒度.并且盡量避免在鎖中調(diào)用RPC方法,因為RPC方法涉及網(wǎng)絡(luò)因素,他的調(diào)用時間存在很大的不可控,很容易就造成了占用鎖的時間過長. 其實這個和我們這個壓測問題是一樣的.首先你本地事務(wù)中調(diào)用RPC既不能起到事務(wù)作用(RPC需要分布式事務(wù)保證),但是又會因為RPC不可控因素導(dǎo)致數(shù)據(jù)庫連接占用時間過長.從而引起接口超時.當(dāng)然我們也可以通過 APM 工具來梳理接口的耗時拓?fù)?將此類問題在壓測前就暴露.
|