前言java
本篇主要講解的是前陣子的一個壓測問題.那麼就直接開門見山面試
可能有的朋友不併不知道forceTransactionTemplate這個是幹嗎的,首先這裏先普及一下,在Java中,咱們通常開啓事務就有三種方式spring
咱們先不糾結爲何使用第三種,後面在講事務傳播機制的時候我會專門介紹,咱們聚焦一下主題,你如今只要知道,那個是開啓事務的意思就好了.我特地用紅色和藍色把日誌代碼圈起來,意思就是,進入方法的時候打印日誌,而後開啓事務後,再打印一個日誌.一波壓測以後,發現接口頻繁超時,數據一致壓不上去.咱們查看日誌以下:數據庫
咱們發現.這兩個日誌輸出時間間隔,居然用了接近5秒!開個事務爲什麼用了5秒?事出反常必有妖!bash
如何切入解決問題網絡
線上遇到高併發的問題,因爲通常高併發問題重現難度比較大,因此通常肥朝都是採用眼神編譯,九淺一深靜態看源碼的方式來分析.具體能夠參考本地可跑,上線就崩?慌了!.可是考慮到肥朝公衆號仍然有小部分新關注的粉絲還沒有掌握分析問題的技巧,本篇就再講一些遇到此類問題的一些常見分析方式,不至於遇到問題時,慌得一比!併發
好在這個併發問題的難度並不大,本篇案例排查很是適合小白入門,咱們能夠經過本地模擬場景重現,將問題範圍縮小,從而逐步定位問題.分佈式
本地重現ide
首先咱們能夠準備一個併發工具類,經過這個工具類,能夠在本地環境模擬併發場景.手機查看代碼並不友好,可是不要緊,如下代碼均是給你複製粘貼進項目重現問題用的,並非給你手機上看的.至於這個工具類爲何能模擬併發場景,因爲這個工具類的代碼全是JDK中的代碼,核心就是CountDownLatch類,這個原理你根據我提供的關鍵字對着你喜歡的搜索引擎搜索便可.高併發
CountDownLatchUtil.java
1public 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
1public interface HelloService {
2
3 void sayHello(long timeMillis);
4
5}
複製代碼
HelloServiceImpl.java
1@Service
2public 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 //模擬業務執行時間爲1s
16 Thread.sleep(1000);
17 } catch (Exception e) {
18 e.printStackTrace();
19 }
20 }
21}
複製代碼
HelloServiceTest.java
1@RunWith(SpringRunner.class)
2@SpringBootTest
3public 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個線程併發
12 CountDownLatchUtil countDownLatchUtil = new CountDownLatchUtil(1000);
13 countDownLatchUtil.latch(() -> {
14 helloService.sayHello(currentTimeMillis);
15 });
16 }
17
18}
複製代碼
咱們從本地調試的日誌中,發現了大量超過5s的接口,而且還有一些規律,肥朝特意用不一樣顏色的框框給你們框起來
爲何這些時間,都是5個爲一組,且每組數據相差是1s左右呢?
真相大白
@Transactional的核心代碼以下(後續我會專門一個系列分析這部分源碼,關注肥朝以避免錯過核心內容).這裏簡單說就是retVal = invocation.proceedWithInvocation()方法會去獲取數據庫鏈接.
1if (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}
複製代碼
而後肥朝爲了更好的演示這個問題,將數據庫鏈接池(本篇用的是Druid)的參數作了如下設置
1//初始鏈接數
2spring.datasource.initialSize=1
3//最大鏈接數
4spring.datasource.maxActive=5
複製代碼
因爲最大鏈接數是5.因此當1000個線程併發進來的時候,你能夠想象是一個隊伍有1000我的排隊,最前面的5個,拿到了鏈接,而且執行業務時間爲1秒.那麼隊伍中剩下的995我的,就在門外等候.等這5個執行完的時候.釋放了5個鏈接,依次向後的5我的又進來,又執行1秒的業務操做.經過簡單的小學數學,均可以計算出最後5個執行完,須要多長時間.經過這裏分析,你就知道,爲何上面的日誌輸出,是5秒爲一組了,而且每組間隔爲1s了.
怎麼解決
看過肥朝源碼實戰的粉絲都知道,肥朝歷來不耍流氓,凡是拋出問題,都會相應給出其中一種解決方案.固然方案沒有最優只有更優!
好比看到這裏有的朋友可能會說,你最大鏈接數設置得就像平時讚揚肥朝的金額同樣小,若是設置大一點,天然就不會有問題了.固然這裏爲了方便向你們演示問題,設置了最大鏈接數是5.正常生產的鏈接數是要根據業務特色和不斷壓測才能得出合理的值,固然肥朝也瞭解到,部分同窗公司機器的配置,居然比不過市面上的千元手機!!!
可是其實當時壓測的時候,數據庫的最大鏈接數設置的是200,而且當時的壓測壓力並不大.那爲何還會有這個問題呢?那麼仔細看前面的代碼
其中這個校驗的代碼是RPC調用,該接口的同事並無像肥朝同樣值得託付終身般的高度可靠,致使耗時時間較長,從而致使後續線程獲取數據庫鏈接等待的時間過長.你再根據前面說的小學數學來算一下就很容易明白該壓測問題出現的緣由.
敲黑板劃重點
以前肥朝就反覆說過,遇到問題,要通過深度思考.好比這個問題,咱們能獲得什麼拓展性的思考呢?咱們來看一下以前一位粉絲的面試經歷
其實他面試遇到的這個問題,和咱們這個壓測問題基本是同一個問題,只不過面試官的結論其實並不夠準確.咱們來一塊兒看一下阿里巴巴的開發手冊
那麼什麼樣叫作濫用呢?其實肥朝認爲,即便這個方法常常調用,可是都是單表insert、update操做,執行時間很是短,那麼承受較大併發問題也不大.關鍵是,這個事務中的全部方法調用,是不是有意義的,或者說,事務中的方法是不是真的要事務保證,纔是關鍵.由於部分同窗,在一些比較傳統的公司,作的可能是能用就行的CRUD工做,很容易一個service方法,就直接打上事務註解開始事務,而後在一個事務中,進行大量和事務一毛錢關係都沒有的無關耗時操做,好比文件IO操做,好比查詢校驗操做等.例如本文中的業務校驗就徹底不必放在事務中.平時工做中沒有相應的實戰場景,加上並無關注肥朝的公衆號,對原理源碼真實實戰場景一無所知.面試稍微一問原理就喊痛,面試官也只好換個方向再繼續深刻!
經過這個經歷咱們又有什麼拓展性的思考呢?由於問題是永遠解決不完的,可是咱們能夠經過不斷的思考,把這個問題壓榨出更多的價值!咱們再來看一下阿里規範手冊
用大白話歸納就是,儘可能減小鎖的粒度.而且儘可能避免在鎖中調用RPC方法,由於RPC方法涉及網絡因素,他的調用時間存在很大的不可控,很容易就形成了佔用鎖的時間過長.
其實這個和咱們這個壓測問題是同樣的.首先你本地事務中調用RPC既不能起到事務做用(RPC須要分佈式事務保證),可是又會由於RPC不可控因素致使數據庫鏈接佔用時間過長.從而引發接口超時.固然咱們也能夠經過APM工具來梳理接口的耗時拓撲,將此類問題在壓測前就暴露.
寫在最後
更多專題系列式源碼解析、真實場景源碼原理實戰與你分享,掃描下方二維碼關注肥朝,讓天生就該造火箭的你,無須委屈擰螺絲!