原來一個 Map 就能搞定注冊表了
本篇從源碼角度帶你學習 Eureka 服務端接收注冊的流程。另外我從源碼中也發現了一些值得我們學習的地方,如 Eureka 存儲注冊表的數據結構、利用讀寫鎖來控制更細粒度的并發性,提高程序的運行效率。
接下來,會從以下幾個方面講解:
客戶端發送注冊請求。
Eureka 注冊中心接收注冊請求。
服務端將客戶端注冊信息保存到一個 Map 里面。
關于源碼的獲取直接到官網下載就好了。https://github.com/Netflix/eureka
本文已收錄到我的 github:https://github.com/Jackson0714/PassJava-Learning
一、注冊入口
上一講我們知道了 Eureka Client 是通過發送 http 請求來注冊的,那么肯定是有一個地方來接收這個 http 請求的,也就是注冊入口。這是怎么玩的呢?
其實是用到了 jersey 框架,這個框架不用深究,我們只需要知道這個框架在哪引用以及做什么事情的就可以了。
可以把 jersey 類比 mvc 框架,jersey 有 servlet 專門處理 http 請求。引用 jersey 框架的地方:
- \eureka\eureka-server\src\main\webapp\WEB-INF\web.xml
然后處理 HTTP 請求的 controller 在哪呢?
其實是在 eureka-core 項目的 resources 目錄下,里面定義了很多的 Resource 結尾的類,它們就是用來處理 HTTP 請求的。
- \eureka\eureka-core\src\main\java\com\netflix\eureka\resources
通過XxResource 類的英文注釋我們也可以知道,這個 jersey resource 類是用來處理 HTTP 請求的。
- A jersey resource that handles request
ApplicationsResource
最后找到了 ApplicationResource 類的 addInstance 方法就是我們要找的處理注冊請求的方法。
二、接收注冊請求
整體流程如下:
2.1 接收注冊請求的方法
addInstance 方法里面的核心代碼就是
- registry.register(info, true);
registry 就是 PeerAwareInstanceRegistryImpl 的實例對象。它實現了 PeerAwareInstanceRegistry 接口。
調用它的 register() 方法后會調用抽象類 AbstractInstanceRegistry 的 register() 方法,核心代碼就是在這個抽象類的 register() 方法。另外要說下的就是上面的抽象類和接口分別實現和繼承了接口 InstanceRegistry。
接口和類的關系圖如下:
那么注冊信息會放到哪個里面呢?
三、存放注冊信息的地方
我們看到源碼里面定義了一個 gNewMap,是 ConcurrentHashMap,然后賦值給了 gMap 變量
- ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap
所以其實是用 gMap 變量來存注冊信息的。我們來分析 gMap 的結構。
首先 gMap 是 ConcurrentHashMap 結構,所以就是 key-value 這種鍵值對的。
- key 就是一個 唯一 id,String 類型。值類似這種:i-00000004
- value 里面存的是 Lease。
- Lease是一個類,里面持有一個 instanceInfo 的 holder。這個 instanceInfo 就是注冊過來的服務實例信息,包含 ip 地址,端口號等。
把服務實例信息放到 gMap 中也很簡單,調用 put 方法就可以了。
- gMap.put(registrant.getId(), lease);
下面是我注冊了兩個服務實例的狀態:
四、值得學習的地方
4.1 ConcurrentHashMap?
上面講到 ConcurrentHashMap,為什么不是用 hashmap ?
- ConcurrentHashMap<String, Lease<InstanceInfo>>()
原因:
在并發編程中使用 HashMap 可能造成死循環 ( JDK 1.7 和 1.8 可能會造成數據丟失)
HashTable 效率非常較低。
簡單說下 ConcurrentHashMap 的底層原理是怎么樣的?
ConcurrentHashMap 內部細分了若干個小的 HashMap,稱之為段(Segment)。默認情況下一個 ConcurrentHashMap 被進一步細分為 16 個段,既就是鎖的并發度。如果需要在 ConcurrentHashMap 中添加一個新的表項,并不是將整個 HashMap 加鎖,而是首先根據 hashcode 得到該表項應該存放在哪個段中,然后對該段加鎖,并完成 put 操作。在多線程環境中,如果多個線程同時進行put操作,只要被加入的表項不存放在同一個段中,則線程間可以做到真正的并行。
4.2 readWriteLock?
我們看到源碼中有用到讀鎖 ReentrantReadWriteLock,如下所示
- readWriteLock = new ReentrantReadWriteLock();
- Lock read = readWriteLock.readLock();
- read.lock();
- ...
- read.unlock();
4.2.1 為什么分為讀鎖和寫鎖?
原因:
在沒有讀寫鎖之前,假設使用普通的 ReentrantLock,那么雖然保證了線程安全,但是也浪費了一定的資源,因為如果多個讀操作同時進行,其實并沒有線程安全問題,可以允許讓多個讀操作并行,以便提高程序效率。
但是寫操作不是線程安全的,如果多個線程同時寫,或者在寫的同時進行讀操作,便會造成線程安全問題。
讀寫鎖就解決了這樣的問題,它設定了一套規則,既可以保證多個線程同時讀的效率,同時又可以保證有寫入操作時的線程安全。
讀鎖: 允許多個線程獲取讀鎖,同時訪問同一個資源。
讀鎖
寫鎖: 只允許一個線程獲取寫鎖,不允許同時訪問同一個資源。
寫鎖
整體思路:
是它有兩把鎖,第 1 把鎖是寫鎖,獲得寫鎖之后,既可以讀數據又可以修改數據,而第 2 把鎖是讀鎖,獲得讀鎖之后,只能查看數據,不能修改數據。讀鎖可以被多個線程同時持有,所以多個線程可以同時查看數據。
在讀的地方合理使用讀鎖,在寫的地方合理使用寫鎖,靈活控制,可以提高程序的執行效率。
4.2.2 讀寫鎖的獲取規則
在使用讀寫鎖時遵守下面的獲取規則:
- 如果有一個線程已經占用了讀鎖,則此時其他線程如果要申請讀鎖,可以申請成功。
- 如果有一個線程已經占用了讀鎖,則此時其他線程如果要申請寫鎖,則申請寫鎖的線程會一直等待釋放讀鎖,因為讀寫不能同時操作。
- 如果有一個線程已經占用了寫鎖,則此時其他線程如果申請寫鎖或者讀鎖,都必須等待之前的線程釋放寫鎖,同樣也因為讀寫不能同時,并且兩個線程不應該同時寫。
讀寫鎖互斥總結:
- 讀讀共享。
- 寫寫互斥、讀寫互斥、寫讀互斥。
五、總結
本篇從源碼的角度,分析了 Eureka 服務端接收注冊信息的流程,核心邏輯就是將服務實例的注冊信息放到 ConcurrentHashMap 里面,同時利用讀鎖來控制細粒度的并發注冊。另外介紹了下我們不太熟悉的 Jersey 框架,它是用來處理 HTTP 請求的,比如用來處理客戶端注冊的 HTTP 請求。
從源碼分析中,我學到了 Eureka 存儲注冊表用到的數據結構 ConcurrentHashMap
本文轉載自微信公眾號「悟空聊架構」,可以通過以下二維碼關注。轉載本文請聯系悟空聊架構公眾號。