JavaWeb 併發:FOR UPDATE 實戰,監測並解決。

一、前言

針對併發,老生常談了。目前一個通用的做法有兩種:鎖機制:1.悲觀鎖;2.樂觀鎖。

但是這篇我主要用於記錄我這次處理的經歷,另外希望能看的大神,大牛,技師者,學長,兄長,大哥們能在評論中發表自己的看法和解決技巧等。

 

二、故事是這樣的

一個表,暫且叫 wallet,其中3個字段是 金額。初始值爲0,如下圖所示:

 

然後我們寫了一個極爲簡單的Controller,並寫了下面的Service代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
    public void testLock(int lockId)
    {
        Wallet wallet = walletMapper.selectByPrimaryKey(4);
         
        BigDecimal one = new BigDecimal(1.00);
        BigDecimal two = new BigDecimal(2.00);
        BigDecimal three = new BigDecimal(3.00);
         
        wallet.setWalletAmount(wallet.getWalletAmount().add(one));
        wallet.setWalletAvailableAmount(wallet.getWalletAvailableAmount().subtract(two));
        wallet.setOldAmount(wallet.getOldAmount().add(three));     
         
        walletMapper.updateByPrimaryKeySelective(wallet);
    }

就簡單的通過主鍵讀取到一個對象,注意這個對象是沒加鎖的。也就是說,所對應的SQL如下:

1
2
3
4
SELECT
    <include refid="Base_Column_List" />
    FROM wallet
    WHERE wallet_id = #{walletId,jdbcType=INTEGER}

我這邊是MyBiatis,大家應該看得懂的。然後一個增加1 一個減少2 一個增加 3。

 

三、測試是這樣

我用了Web應用壓力測試工具:Boomhttps://github.com/rakyll/boom Go編寫的HTTP(S)負載生成器,ApacheBench(AB)的替代工具。Boom是一個微型程序,能夠對Web應用程序進行負載測試。它類似於 Apache Bench ,但在不同的平臺上有更好的可用性,安裝使用也比較簡單。

簡單使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
boom -n 1000 -c 200 http://www.baidu.com
 
Options:
  -n  Number of requests to run.
  -c  Number of requests to run concurrently. Total number of requests cannot
      be smaller than the concurency level.
  -q  Rate limit, in seconds (QPS).
  -o  Output type. If none provided, a summary is printed.
      "csv" is the only supported alternative. Dumps the response
      metrics in comma-seperated values format.
  
  -m  HTTP method, one of GET, POST, PUT, DELETE, HEAD, OPTIONS.
  -h  Custom HTTP headers, name1:value1;name2:value2.
  -d  HTTP request body.
  -T  Content-type, defaults to "text/html".
  -a  Basic authentication, username:password.
  
  -allow-insecure Allow bad/expired TLS/SSL certificates.

 

所以我就如圖進行壓力測試,可見這個小工具還挺美的,這裏我連接數1000,併發數100

可見後臺程序報錯了。什麼錯誤呢?

1
Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction

 

原來併發導致update死表了。數據庫的數據不用看了肯定是錯誤的。

 

四、FOR UPDATE的使用

先補一下其知識:利用select * for update 可以鎖表/鎖行。自然鎖表的壓力遠大於鎖行。所以我們採用鎖行。什麼時候鎖表呢?

假設有個表單products ,裏面有id跟name二個欄位,id是主鍵。
例1: (明確指定主鍵,並且有此筆資料,row lock)
SELECT * FROM wallet WHERE id=’3′ FOR UPDATE;
例2: (明確指定主鍵,若查無此筆資料,無lock)
SELECT * FROM wallet WHERE id=’-1′ FOR UPDATE;
例2: (無主鍵,table lock)
SELECT * FROM wallet WHERE name=’Mouse’ FOR UPDATE;
例3: (主鍵不明確,table lock)
SELECT * FROM wallet WHERE id<>’3′ FOR UPDATE;
例4: (主鍵不明確,table lock)
SELECT * FROM wallet WHERE id LIKE ‘3’ FOR UPDATE;

 

因此我們更新了下Service層的Mapper方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
    public void testLock(int lockId)
    {
        Wallet wallet = walletMapper.selectForUpdate(4);
         
        BigDecimal one = new BigDecimal(1.00);
        BigDecimal two = new BigDecimal(2.00);
        BigDecimal three = new BigDecimal(3.00);
         
        wallet.setWalletAmount(wallet.getWalletAmount().add(one));
        wallet.setWalletAvailableAmount(wallet.getWalletAvailableAmount().subtract(two));
        wallet.setOldAmount(wallet.getOldAmount().add(three));     
         
        walletMapper.updateByPrimaryKeySelective(wallet);
    }

所對應的SQL如下:

1
2
3
4
5
6
7
<select id="selectForUpdate" resultMap="BaseResultMap" parameterType="java.lang.Integer" >
  SELECT
  <include refid="Base_Column_List" />
  FROM wallet
  WHERE wallet_id = #{walletId,jdbcType=INTEGER}
  FOR UPDATE
</select>

自然大家可以看到,我這邊加了鎖,是通過主鍵鎖行。

 

按着上面的測試連接數1000,併發數100,控制檯沒報錯。

數據庫結果也是很不錯。

 

五、加大壓力

按着上面的測試連接數5000,併發數350,控制檯還是沒報錯。

數據庫結果卻是很出錯了!!!

少update了很多值。爲什麼呢?

 

六、jvisualvm 小工具檢測,發現Tomcat線程連接數默認不夠

然後我用jvisualvm 小工具檢測。多測了幾次,發現連接數5000,併發數350,併發數上升。有一個圖的值始終不變。如圖:

發現圖中 tomcat的守護線程一直在200左右。後來我去找了下tomcat的server.xml發現了,使用了默認,大概就是200左右。

 

所以就配置了一下,大致配置方法有兩種如下:

第1種方式:配置Connector
maxThreads:tomcat可用於請求處理的最大線程數
minSpareThreads:tomcat初始線程數,即最小空閒線程數
maxSpareThreads:tomcat最大空閒線程數,超過的會被關閉
acceptCount:當所有可以使用的處理請求的線程數都被使用時,可以放到處理隊列中的請求數,超過這個數的請求將不予處理

1
<Connectorport="8080"maxHttpHeaderSize="8192"maxThreads="150"minSpareThreads="25"maxSpareThreads="75"enableLookups="false"redirectPort="8443"acceptCount="100"connectionTimeout="20000"disableUploadTimeout="true"/>

 

第2種方式:配置Executor和Connector

name:線程池的名字
class:線程池的類名
namePrefix:線程池中線程的命名前綴
maxThreads:線程池的最大線程數
minSpareThreads:線程池的最小空閒線程數
maxIdleTime:超過最小空閒線程數時,多的線程會等待這個時間長度,然後關閉
threadPriority:線程優先級

1
2
3
<Executorname="tomcatThreadPool"namePrefix="req-exec-"maxThreads="1000"minSpareThreads="50"maxIdleTime="60000"/>
 
<Connectorport="8080"protocol="HTTP/1.1"executor="tomcatThreadPool"/>

 

maxThreads:線程池的最大線程數,直接配置1000,然後用連接數10000,併發數800測試。輕鬆見圖:

七、總結

感謝幫助我的人。希望有大牛在此討論相關。小生感激不盡。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章