Writer:BYSocket(泥沙磚瓦漿木匠)
微博:BYSocket
豆瓣:BYSocket
一、前言
針對並發,老生常談了。目前一個通用的做法有兩種:鎖機制: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應用壓力測試工具:Boom。https://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測試。輕松見圖:
七、總結
感謝幫助我的人。希望有大牛在此討論相關。小生感激不盡。