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


Writer:BYSocket(泥沙磚瓦漿木匠)

微博:BYSocket

豆瓣:BYSocket

 

一、前言

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

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

 

二、故事是這樣的

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

 

然后我們寫了一個極為簡單的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
image
可見后台程序報錯了。什么錯誤呢?

?
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,控制台沒報錯。

image

數據庫結果也是很不錯。

image

 

五、加大壓力

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

image

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

少update了很多值。為什么呢?

 

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

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

QQ截圖20150322124739

發現圖中 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測試。輕松見圖:

UFRJFLLO6@F1)LWQ6EQJ8P8

image

七、總結

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM