1. 什么是微服務的注冊中心
注冊中心:服務管理,核心是有個服務注冊表,心跳機制動態維護。
為什么要用?
微服務應用和機器越來越多,調用方需要知道接口的網絡地址,如果靠配置文件的方式去控制網絡地址,對於動態新增機器,維護帶來很大問題。
主流的注冊中心:Zookeeper、Eureka、Consul、ETCD 等。
服務提供者 Provider:啟動的時候向注冊中心上報自己的網絡信息。
服務消費者 Consumer:啟動的時候向注冊中心上報自己的網絡信息,拉取 Provider 的相關網絡信息。
2. 分布式應用知識CAP理論知識
CAP定理:指的是在一個分布式系統中,Consistency(一致性)、 Availability(可用性)、Partition Tolerance(分區容錯性),三者不可同時獲得。
一致性(C):在分布式系統中的所有數據備份,在同一時刻是否同樣的值。(所有節點在同一時間的數據完全一致,越多節點,數據同步越耗時)
可用性(A):負載過大后,集群整體是否還能響應客戶端的讀寫請求。(服務一直可用,而且是正常響應時間)
分區容錯性(P):分區容忍性,就是高可用性,一個節點崩了,並不影響其它的節點。(100個節點,掛了幾個,不影響服務,越多機器越好)
CAP理論就是說在分布式存儲系統中,最多只能實現上面的兩點。而由於當前的網絡硬件肯定會出現延遲丟包等問題,所以分區容忍性是我們必須需要實現的。所以我們只能在一致性和可用性之間進行權衡。
原因:
CA 滿足的情況下,P 不能滿足的原因:數據同步(C)需要時間,也要正常的時間內響應(A),那么機器數量就要少,所以P就不滿足。
CP 滿足的情況下,A 不能滿足的原因:數據同步(C)需要時間,,機器數量也多(P),但是同步數據需要時間,所以不能再正常時間內響應,所以A就不滿足。
AP 滿足的情況下,C不能滿足的原因:機器數量也多(P),正常的時間內響應(A),那么數據就不能及時同步到其他節點,所以C不滿足。
注冊中心選擇:
Zookeeper:CP 設計,保證了一致性,集群搭建的時候,某個節點失效,則會進行選舉行的 leader,或者半數以上節點不可用,則無法提供服務,因此可用性沒法滿足。
Eureka:AP 原則,無主從節點,一個節點掛了,自動切換其他節點可以使用,去中心化。
結論:
分布式系統中P,肯定要滿足,所以只能在 C 和 A 中二選一。沒有最好的選擇,最好的選擇是根據業務場景來進行架構設計,如果要求一致性,則選擇 Zookeeper,如金融行業;如果要去可用性,則 Eureka,如電商系統。
Eureka 原理圖:
Spring Cloud 體系官方地址:http://projects.spring.io/spring-cloud/
參考:
https://www.jianshu.com/p/d32ae141f680
https://blog.csdn.net/zjcjava/article/details/78608892
3. 使用 IDEA 搭建 Eureka 服務中心 Server 端並啟動
第一步:創建項目
和創建普通的 Spring Boot 項目是一樣的,只是需要選擇下圖所示依賴
第二步:添加注解 @EnableEurekaServer
@SpringBootApplication @EnableEurekaServer public class EurekaServerApplication { public static void main(String[] args) { SpringApplication.run(EurekaServerApplication.class, args); } }
第三步:增加配置 application.yml(其實可以使用 application.properties,但官網上使用 yml,這里我們照抄官網)
server:
port: 8761
eureka:
instance:
hostname: localhost
client:
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
第四步:訪問注冊中心頁面
使用 http://localhost:8761/ 訪問注冊中心頁面,這個地址按照配置文件中的來,這里注意下,按照配置文件說的,訪問地址為:http://localhost:8761/eureka/
但不同版本,可能不一樣,我所使用的版本是 Greenwich,它就不能添加 /eureka/ 否則會報 404 錯誤
Eureka 管理后台出現一串紅色字體:EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY'RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.
這個是警告,說明有服務上線率低。
關閉檢查方法:Eureka 服務端配置文件加入
server: enable-self-preservation: false
注意:自我保護模式禁止關閉,默認是開啟狀態 true
4. 創建商品服務,並將服務注冊到注冊中心
第一步:創建一個 Spring Boot 應用,增加服務注冊和發現依賴
第二步:模擬商品信息,存儲在內存中
第三步:開發商品列表接口,商品詳情接口
第四步:配置文件加入注冊中心地址
server: port: 8771 #指定注冊中心地址 eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/ #服務的名稱 spring: application: name: product-service
我們可以用當前項目啟動多個實例,參考教程:https://blog.csdn.net/zhou520yue520/article/details/81167841
啟動后,我們可以登錄下訪問注冊中心頁面,看到如下效果:
為什么只加一個注冊中心地址,就可以注冊?
官網解釋:By having spring-cloud-starter-netflix-eureka-client on the classpath, your application automatically registers with the Eureka Server.(也就是說,這樣 Jar 包在類路徑上,就可以自動識別)
5.常用的服務間的調用方式
RPC:遠程過程調用,像調用本地服務(方法)一樣調用服務器的服務。支持同步、異步調用。客戶端和服務器之間建立 TCP 連接,可以一次建立一個,也可以多個調用復用一次鏈接。PRC 數據包小。
Rest:Http 請求,支持多種協議和功能。開發方便成本低。Http 數據包大。類似 HttpClient,URLConnection。
6.訂單服務調用商品服務獲取商品信息
第一步:創建 order_service 項目
注意:調用方需要引入 Ribbon 依賴
第二步:使用 Ribbon(類似 HTTPClient,URLConnection)
啟動類增加注解
@Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); }
第三步:開發偽下單接口
第四步:根據名稱進行調用商品,獲取商品詳情
注意:紅字標識的就是上面商品服務的 spring.application.name
@Service public class ProductOrderServiceImpl implements ProductOrderService { @Autowired private RestTemplate restTemplate; @Override public ProductOrder save(int userId, int productId) { Object obj = restTemplate.getForObject("http://product-service/api/v1/product/find?id="+productId, Object.class); System.out.println(obj); ProductOrder productOrder = new ProductOrder(); productOrder.setCreateTime(new Date()); productOrder.setUserId(userId); productOrder.setTradeNo(UUID.randomUUID().toString()); return productOrder; } }
當我們用請求多次訪問時,可以從控制台看到端口號的不同,說明這是從不同端口的應用返回的數據
商品服務的 Controller 如下:
@RestController @RequestMapping("/api/v1/product") public class ProductController { @Value("${server.port}") private String port; @Autowired private ProductService productService; /** * 獲取所有商品列表 * @return */ @RequestMapping("list") public Object list(){ return productService.listProduct(); } /** * 根據id查找商品詳情 * @param id * @return */ @RequestMapping("find") public Object findById(int id){ Product product = productService.findById(id); Product result = new Product(); BeanUtils.copyProperties(product,result); result.setName( result.getName() + " data from port="+port ); return result; } }
我們還可以通過另一種調用方式進行調用
//調用方式二 ServiceInstance instance = loadBalancer.choose("product-service"); String url = String.format("http://%s:%s/api/v1/product/find?id="+productId, instance.getHost(),instance.getPort()); RestTemplate restTemplate = new RestTemplate(); Object obj = restTemplate.getForObject(url, Object.class); //Map<String,Object> productMap = restTemplate.getForObject(url, Map.class);
調用原理:
1)首先從注冊中心獲取 Provider 的列表
2)通過一定的策略選擇其中一個節點
3)再返回給 restTemplate 調用
我們也可以自定義負載均衡策略,官網說明:https://cloud.spring.io/spring-cloud-static/spring-cloud-netflix/2.2.0.M3/reference/html/#customizing-the-ribbon-client-by-setting-properties
server:
port: 8781
#指定注冊中心地址
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
#服務的名稱
spring:
application:
name: order-service
#自定義負載均衡策略 product-service: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
策略選擇:
1)如果每個機器配置一樣,則建議不修改策略 (推薦)
2)如果部分機器配置強,則可以改為 WeightedResponseTimeRule
7.使用 Feign 改造訂單服務
Feign: 偽 RPC 客戶端(本質還是用http)
官方文檔: https://cloud.spring.io/spring-cloud-openfeign/
第一步:加入依賴
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
第二步:啟動類增加 @EnableFeignClients
@SpringBootApplication @EnableFeignClients public class OrderServiceApplication { public static void main(String[] args) { SpringApplication.run(OrderServiceApplication.class, args); } }
第三步:增加一個接口並使用注解 @FeignClient(name="product-service")
package com.jwen.order_service.service; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; /** * 商品服務客戶端 */ @FeignClient(name = "product-service") public interface ProductClient { @GetMapping("/api/v1/product/find") String findById(@RequestParam(value = "id") int id); }
注意點:
1)服務名和 Http 方法必須對應
2)使用 RequestBody,應該使用 @PostMapping
3)多個參數的時候,通過 @RequestParam(value = "id") int id 方式調用
第四步:更改調用方式編碼
package com.jwen.order_service.service.impl; import com.fasterxml.jackson.databind.JsonNode; import com.jwen.order_service.domain.ProductOrder; import com.jwen.order_service.service.ProductClient; import com.jwen.order_service.service.ProductOrderService; import com.jwen.order_service.utils.JsonUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Date; import java.util.UUID; @Service public class ProductOrderServiceImpl implements ProductOrderService { @Autowired private ProductClient productClient; @Override public ProductOrder save(int userId, int productId) { String response = productClient.findById(productId); JsonNode jsonNode = JsonUtils.str2JsonNode(response); ProductOrder productOrder = new ProductOrder(); productOrder.setCreateTime(new Date()); productOrder.setUserId(userId); productOrder.setTradeNo(UUID.randomUUID().toString()); productOrder.setProductName(jsonNode.get("name").toString()); productOrder.setPrice(Integer.parseInt(jsonNode.get("price").toString())); return productOrder; } }
JsonUtils 工具類的代碼
package com.jwen.order_service.utils; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; /** * json工具類 */ public class JsonUtils { private static final ObjectMapper objectMappper = new ObjectMapper(); /** * json字符串轉JsonNode對象的方法 */ public static JsonNode str2JsonNode(String str){ try { return objectMappper.readTree(str); } catch (IOException e) { return null; } } }
Ribbon 和 Feign 兩個之間,應該選擇 Feign。Feign 默認集成了 Ribbon。寫起來更加思路清晰和方便。采用注解方式進行配置,配置熔斷等方式方便
超時配置
#修改調用超時時間
feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
模擬接口響應慢,線程睡眠新的方式
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }