一、前言
針對併發,老生常談了。目前一個通用的做法有兩種:鎖機制: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測試。輕鬆見圖:
七、總結
感謝幫助我的人。希望有大牛在此討論相關。小生感激不盡。