最近公司的下單接口有些慢,老闆擔憂沒法支撐雙11,想讓我優化一把,可是前提是不容許大改,由於下單接口太複雜了,若是改動太大,怕有風險。另外開發成本和測試成本也很是大。對於這種有挑戰性的任務,我向來是很是喜歡的,由於在解決問題的過程當中,能夠學習到不少東西。java
當時我只是知道下單接口慢,可是沒人告訴我慢在哪裏,也便是說,哪些瓶頸致使下單接口慢了。其實沒人知道也不要緊的,由於咱們能夠經過壓測來找到具體的瓶頸。mysql
下面會詳細介紹一下,在本次壓測中遇到的問題以及如何解決,期間用了什麼工具。sql
騰訊雲Mysql數據庫
騰訊雲2核4g的服務器1臺apache
下單屬於寫接口,大部分狀況下,瓶頸都出在DB
裏,程序可能都在等待DB
鎖的釋放。爲了驗證這個想法,咱們可使用Jmeter
和jvisualvm
來驗證一下。服務器
爲了監控服務器和服務器中JAVA進程,咱們須要開啓JMX,能夠在JAVA進程啓動的時候,添加以下幾個參數:併發
JMX_OPTS="-Dcom.sun.management.jmxremote.port=7969 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=xx.xx.xx.xx" nohup java ${JMX_OPTS} -jar xxxxx.jar
Djava.rmi.server.hostname
填寫JAVA
進程所在服務器的IP
地址,-Dcom.sun.management.jmxremote.port=7969
是指定JMX
監控端口的,這裏是7969。工具
從新啓動進程後,打開本地的(我用的是Window10)jvisualvm
,添加JMX
配置。配置成功後,能夠點擊線程那個tab
,由於咱們要作線程dump
,觀察線程的執行狀況。性能
好了,如今咱們可使用Jmeter
來對下單接口進行壓測了。能夠先用50線程併發壓,執行時間是1分鐘。 學習
在壓測的過程當中,作一下線程dump
,同時利用nmon
觀察應用服務器CPU
的負載狀況。
負載很低,將線程併發調整到100後,CPU仍是上不去,這樣的話,初步能夠判斷,代碼裏有鎖。 經過觀察dump文件,發現以下信息:
- locked <22f6e7f3> (a com.mysql.cj.core.io.ReadAheadInputStream) - at com.sun.proxy.$Proxy231.reduceSkuStock(Unknown Source)
觸發這個lock的業務代碼是reduceSkuStock
方法。經過閱讀代碼,發現reduceSkuStock
被包在一個大事務裏面。
@Transactional(rollbackFor = {Exception.class}) createOrder() { //一、扣減庫存 reduceSkuStock(); //二、建立訂單 insertOrder(); //三、其餘寫操做 。。。。 }
庫存記錄一般存在一張獨立的庫存表,因爲建立訂單的方法,是一個大事務,這樣就會致使某條庫存記錄只有當整個createorder()
方法執行完後,數據庫行鎖纔會被釋放,在這個期間,其餘線程是沒法對這條庫存記錄進行寫操做的。所以咱們能夠在reduceSkuStock()
中,再開一個事務,操做完這條庫存記錄後,馬上釋放鎖,這樣應該能夠提升一些性能。爲了驗證是不是由於事務的緣由致使下單接口慢,咱們能夠直接將createOrder()
方法的事務去掉,再壓測一下。
壓測結果發現,下單接口的TPS
提升了一倍,CPU
也上去了很多,可是仍然不夠理想,代碼裏,應該還有其餘的鎖。再次作線程dump
,又發現了一個鎖。
- locked <438be230> (a org.apache.http.pool.AbstractConnPool$2) - at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:108)
致使鎖的代碼是HttpClient
的execute
方法,該方法在執行的時候,一直在等待獲取HTTP
鏈接,經過查看源代碼,發現竟然沒有使用鏈接池,醉了。趕忙加上以下代碼:
PoolingHttpClientConnectionManager pool = new PoolingHttpClientConnectionManager(); pool.setDefaultMaxPerRoute(400); httpClient = HttpClients.custom().setConnectionManager(pool).build();
再次壓測後,發現代碼裏已經沒有鎖了。TPS
提高了5倍。可是接下來還得作幾件事情:
一、打印下單接口的全部SQL
,而後逐一進行explain
操做,看看有沒有全表掃描的語句或者沒用到索引的SQL
語句;
二、觀察下單接口執行的過程當中,FULL GC
發生的次數;
三、增長應用的MYSQL
鏈接數;
好了,到了這地方,咱們能夠回到前面,來解決庫存問題了。因爲老闆說,不能大改,所以我就在reduceSkuStock
方法上,再開一個事務。
@Transactional(propagation = Propagation.REQUIRES_NEW) reduceSkuStock(){}
讓執行庫存操做的線程執行完後,趕忙釋放行鎖。這樣作也有個風險,就是庫存扣減成功後,下單失敗了。不過這種狀況比較少,由於當時的下單接口中,大部分業務邏輯都在前面作好判斷了,到達插入訂單的代碼時,就只是單獨的insert了,除非數據庫掛了,否則不會出現下單失敗的狀況。
在開發環境下,通過調優後,下單接口的TPS提高了3倍左右,固然因爲開發環境的數據庫和應用服務器都比較差,也會對TPS有影響的。當時優化完後,在生產上進行了壓測,發現TPS提高了10倍。
這個是下單接口的邏輯不能大改的狀況下的優化方案,通常來講,庫存操做應該是單獨的服務,能夠單獨優化的。而單純的下單邏輯也是能夠優化的。