分佈式(1)-- 分佈式鎖

分佈式遭遇併發

在前面的章節,併發操做要麼發生在單個應用內,通常使用基於JVM的lock解決併發問題,要麼發生在數據庫,能夠考慮使用數據庫層面的鎖,而在分佈式場景下,須要保證多個應用實例都可以執行同步代碼,則須要作一些額外的工做,一個最典型分佈式同步方案即是使用分佈式鎖。html

分佈式鎖由不少種實現,但本質上都是相似的,即依賴於共享組件實現鎖的詢問和獲取,若是說單體式應用中的Monitor是由JVM提供的,那麼分佈式下Monitor即是由共享組件提供,而典型的共享組件你們其實並不陌生,包括但不限於:Mysql,Redis,Zookeeper。同時他們也表明了三種類型的共享組件:數據庫,緩存,分佈式協調組件。基於Consul的分佈式鎖,其實和基於Zookeeper的分佈式鎖大同小異,都是藉助於分佈式協調組件實現鎖,大而化之,這三種類型的分佈式鎖,原理也都差很少,只不過,鎖的特性和實現細節有所差別。java

Redis實現分佈式鎖

定義需求: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. 被打斷以後,在該函數一次調用執行完以前,能夠再次被調用(或進入,reentered)。
  3. 再次調用執行完以後,被打斷的上次調用能夠繼續恢復執行,並正確執行。

好比下述的代碼引用了全局變量,即是不可重入的:

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 。 尊重原創,感謝做者!)

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息