在前面的章節,併發操做要麼發生在單個應用內,通常使用基於JVM的lock解決併發問題,要麼發生在數據庫,能夠考慮使用數據庫層面的鎖,而在分佈式場景下,須要保證多個應用實例都可以執行同步代碼,則須要作一些額外的工做,一個最典型分佈式同步方案即是使用分佈式鎖。html
分佈式鎖由不少種實現,但本質上都是相似的,即依賴於共享組件實現鎖的詢問和獲取,若是說單體式應用中的Monitor是由JVM提供的,那麼分佈式下Monitor即是由共享組件提供,而典型的共享組件你們其實並不陌生,包括但不限於:Mysql,Redis,Zookeeper。同時他們也表明了三種類型的共享組件:數據庫,緩存,分佈式協調組件。基於Consul的分佈式鎖,其實和基於Zookeeper的分佈式鎖大同小異,都是藉助於分佈式協調組件實現鎖,大而化之,這三種類型的分佈式鎖,原理也都差很少,只不過,鎖的特性和實現細節有所差別。java
定義需求:A應用須要完成添加庫存的操做,部署了A1,A2,A3多個實例,實例之間的操做要保證同步。程序員
分析需求:顯然,此時依賴於JVM的lock已經沒辦法解決問題了,A1添加鎖,沒法保證A2,A3的同步,這種場景能夠考慮使用分佈式鎖應對。redis
創建一張Stock表,包含id,number兩個字段,分別讓A1,A2,A3併發對其操做,保證線程安全。sql
1
2
3
4
5
6
|
@Entity
public
class
Stock {
@Id
private
String id;
private
Integer number;
}
|
定義數據庫訪問層:數據庫
1
2
|
public
interface
StockRepository
extends
JpaRepository<Stock,String> {
}
|
若是你的項目中Redis是多機部署的,那麼能夠嘗試使用Redisson實現分佈式鎖,這是Redis官方提供的Java組件。
這一節的主角,redis分佈式鎖,使用開源的redis分佈式鎖實現:Redisson。緩存
引入Redisson依賴:安全
1
2
3
4
5
|
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>
3.5
.
4
</version>
</dependency>
|
定義測試類:架構
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
@RestController
public
class
StockController {
@Autowired
StockRepository stockRepository;
ExecutorService executorService = Executors.newFixedThreadPool(
10
);
@Autowired
RedissonClient redissonClient;
final
static
String id =
"1"
;
@RequestMapping
(
"/addStock"
)
public
void
addStock() {
RLock lock = redissonClient.getLock(
"redisson:lock:stock:"
+ id);
for
(
int
i =
0
; i <
100
; i++) {
executorService.execute(() -> {
lock.lock();
try
{
Stock stock = stockRepository.findOne(id);
stock.setNumber(stock.getNumber() +
1
);
stockRepository.save(stock);
}
finally
{
lock.unlock();
}
});
}
}
}
|
上述的代碼使得併發發生在多個層面。其一,在應用內部,啓用線程池完成庫存的加1操做,自己即是線程不安全的,其二,在多個應用之間,這樣的加1操做更加是不受約束的。若初始化id爲1的Stock數量爲0。分別在本地啓用A1(8080),A2(8081),A3(8082)三個應用,同時併發執行一次addStock(),若線程安全,必然可使得數據庫中的Stock爲300,這即是咱們的檢測依據。併發
簡單解讀下上述的代碼,使用redisson獲取一把RLock,RLock是java.util.concurrent.locks.Lock接口的實現類,Redisson幫助咱們屏蔽Redis分佈式鎖的實現細節,使用過java.util.concurrent.locks.Lock的朋友都會知道下述的代碼能夠被稱得上是同步的起手範式,畢竟這是Lock的java doc中給出的代碼:
1
2
3
4
5
6
7
|
Lock l = ...;
l.lock();
try
{
// access the resource protected by this lock
}
finally
{
l.unlock();
}
|
而redissonClient.getLock(「redisson:lock:stock:」 + id)則是以」redisson:lock:stock:」 + id該字符串做痛同步的Monitor,保證了不一樣id之間是互相不阻塞的。
爲了保證發生併發,實際測試中我加入了Thread.sleep(1000),使競爭得以發生。測試結果:
Redis分佈式鎖的確起了做用。
若是僅僅是實現一個可以用於demo的Redis分佈式鎖並不難,但爲什麼你們更偏向於使用開源的實現呢?主要仍是可用性和穩定性,we make things work是我在寫博客,寫代碼時牢記在腦海中的,若是真的要細究如何本身實現一個分佈式鎖,或者平時使用鎖保證併發,須要有哪些注意點呢?列舉幾點:阻塞,超時時間,可重入,可用性,其餘特性。
意味着各個操做之間的等待,A1正在執行增長庫存時,A1其餘的線程被阻塞,A2,A3中全部的線程被阻塞,在Redis中可使用輪詢策略以及redis底層提供的CAS原語(如setnx)來實現。(初學者能夠理解爲:在redis中設置一個key,想要執行lock代碼時先詢問是否有該key,若是有則表明其餘線程在執行過程當中,若沒有,則設置該key,而且執行代碼,執行完畢,釋放key,而setnx保證操做的原子性)
在特殊狀況,可能會致使鎖沒法被釋放,如死鎖,死循環等等意料以外的狀況,鎖超時時間的設置是有必要的,一個很直觀的想法是給key設置過時時間便可。
如在Redisson中,lock提供了一個重載方法lock(long t, TimeUnit timeUnit);能夠自定義過時時間。
這個特性很容易被忽視,可重入其實並不難理解,顧名思義,一個方法在調用過程當中是否能夠被再次調用。實現可重入須要知足三個特性:
好比下述的代碼引用了全局變量,即是不可重入的:
1
2
3
4
5
6
7
|
int
t;
void
swap(
int
x,
int
y) {
t = x;
x = y;
y = t;
System.out.println(
"x is"
+ x +
" y is "
+ y);
}
|
一個更加直觀的例子即是,同一個線程中,某個方法的遞歸調用不該該被阻塞,因此若是要實現這個特性,簡單的使用某個key做爲Monitor是欠妥的,能夠加入線程編號,來保證可重入。
使用可重入分佈式鎖的來測試計算斐波那契數列(只是爲了驗證可重入性):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
@RequestMapping
(
"testReentrant"
)
public
void
ReentrantLock() {
RLock lock = redissonClient.getLock(
"fibonacci"
);
lock.lock();
try
{
int
result = fibonacci(
10
);
System.out.println(result);
}
finally
{
lock.unlock();
}
}
int
fibonacci(
int
n) {
RLock lock = redissonClient.getLock(
"fibonacci"
);
try
{
if
(n <=
1
)
return
n;
else
return
fibonacci(n -
1
) + fibonacci(n -
2
);
}
finally
{
lock.unlock();
}
}
|
最終輸出:55,能夠發現,只要是在同一線程以內,不管是遞歸調用仍是外部加鎖(同一把鎖),都不會形成死鎖。
藉助於第三方中間件實現的分佈式鎖,都有這個問題,中間件掛了,會致使鎖不可用,因此須要保證鎖的高可用,這就須要保證中間件的可用性,如redis可使用哨兵+集羣,保證了中間件的可用性,便保證了鎖的可用性、
除了可重入鎖,鎖的分類還有不少,在分佈式下也一樣能夠實現,包括但不限於:公平鎖,聯鎖,信號量,讀寫鎖。Redisson也都提供了相關的實現類,其餘的特性如併發容器等能夠參考官方文檔。
基本算是把項目中遇到的併發過了一遍了,案例其實不少,再簡單羅列下一些新手可能會遇到的問題。
使用了線程安全的容器就是線程安全了嗎?不少新手誤覺得使用了併發容器如:concurrentHashMap就萬事大吉了,殊不知道,只知其一;不知其二的隱患可能比全然不懂更大。來看下面的代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
public
class
ConcurrentHashMapTest {
static
Map<String, Integer> counter =
new
ConcurrentHashMap();
public
static
void
main(String[] args)
throws
InterruptedException {
counter.put(
"stock1"
,
0
);
ExecutorService executorService = Executors.newFixedThreadPool(
10
);
CountDownLatch countDownLatch =
new
CountDownLatch(
100
);
for
(
int
i =
0
; i <
100
; i++) {
executorService.execute(
new
Runnable() {
@Override
public
void
run() {
counter.put(
"stock1"
, counter.get(
"stock1"
) +
1
);
countDownLatch.countDown();
}
});
}
countDownLatch.await();
System.out.println(
"result is "
+ counter.get(
"stock1"
));
}
}
|
counter.put(「stock1″, counter.get(「stock1″) + 1)並非原子操做,併發容器保證的是單步操做的線程安全特性,這一點每每初級程序員特別容易忽視。
項目中的併發場景是很是多的,而根據場景不一樣,同一個場景下的業務需求不一樣,以及數據量,訪問量的不一樣,都會影響到鎖的使用,架構中常常被提到的一句話是:業務決定架構,放到併發中也一樣適用:業務決定控制併發的手段,如本文未涉及的隊列的使用,本質上是化併發爲串行,也解決了併發問題,都是控制的手段。瞭解鎖的使用很簡單,但若是使用,在什麼場景下使用什麼樣的鎖,這纔是價值所在。
同一個線程之間的遞歸調用不該該被阻塞,因此若是要實現這個特性,簡單的使用某個key做爲Monitor是欠妥的,能夠加入線程編號,來保證可重入。
(原文地址:http://www.importnew.com/27278.html 。 尊重原創,感謝做者!)