服務(wù)調(diào)用方式
常見(jiàn)的遠(yuǎn)程調(diào)用方式有RPC和HTTP
RPC:Remote Produce Call遠(yuǎn)程過(guò)程調(diào)用,類似的還有RMI苔咪。自定義數(shù)據(jù)格式,基于原生TCP通信柳骄,速度快团赏,效率高舔清。早期的webservice曲初,現(xiàn)在熱門的dubbo,都是RPC的典型代表抒痒。
HTTP:http其實(shí)是一種網(wǎng)絡(luò)傳輸協(xié)議目锭,基于TCP痢虹,規(guī)定了數(shù)據(jù)傳輸?shù)母袷健主儡,F(xiàn)在客戶端瀏覽器與服務(wù)端通信基本都是采用http協(xié)議糜值,也可以用來(lái)進(jìn)行遠(yuǎn)程服務(wù)調(diào)用坯墨。缺點(diǎn)是消息封裝臃腫捣染,優(yōu)勢(shì)是對(duì)服務(wù)的提供和調(diào)用方?jīng)]有任何技術(shù)限定停巷,自由靈活,更符合微服務(wù)理念蕾各。
RestTemplate的使用
跨服務(wù)調(diào)用:restTemplate的getForObject(地址式曲,結(jié)果)
@Autowired
private RestTemplate restTemplate;
@Test
public void httpGet(){
User user = restTemplate.getForObject("http://localhost:80/user/2", User.class);
System.out.println("user = " + user);
}
初始SpringCloud
dependencyManagement和dependencies區(qū)別:dependencies:自動(dòng)引入聲明在dependencies里的所有依賴吝羞,并默認(rèn)被所有的子項(xiàng)目繼承内颗。如果項(xiàng)目中不寫(xiě)依賴項(xiàng),則會(huì)從父項(xiàng)目繼承(屬性全部繼承)聲明在父項(xiàng)目dependencies里的依賴項(xiàng)卖氨。dependencyManagement里只是聲明依賴筒捺,并不實(shí)現(xiàn)引入纸厉,因此子項(xiàng)目需要顯示的聲明需要的依賴颗品。
可以給大家看下我搭好的微服務(wù)調(diào)用場(chǎng)景
user-service對(duì)外提供了查詢用戶的接口躯枢;consumer-demo通過(guò)RestTemplate訪問(wèn)http://localhost:8081/user/{id}接口,查詢用戶數(shù)據(jù)氓仲。
哈哈哈哈敬扛,yang嗯嗯邁出了萬(wàn)里長(zhǎng)征第一步。
但是這個(gè)遠(yuǎn)程服務(wù)調(diào)用案例存在問(wèn)題:在consumer中谍珊,把url地址硬編碼到了代碼中抬驴,不方便后期維護(hù)缆巧;consumer需要記憶user-service的地址陕悬,如果出現(xiàn)變更,可能得不到通知胧卤,地址將失效枝誊;consumer不清楚user-service的狀態(tài)惜纸,服務(wù)宕機(jī)也不知道耐版;user-service只有一臺(tái)服務(wù),不具備高可用性古瓤;即便user-service形成集群落君,consumer還需自己實(shí)現(xiàn)負(fù)載均衡亭引。
注冊(cè)中心原理圖如下
心跳(續(xù)約):提供者定期通過(guò)http方式向Eureka刷新自己的狀態(tài)痛侍。
(1)引入依賴
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
(2)在啟動(dòng)類上加@EnableEurekaServer
@EnableEurekaServer
@SpringBootApplication
public class EurekaServer {
public static void main(String[] args) {
SpringApplication.run(EurekaServer.class);
}
}
(3)改端口
server:
port: 10086
設(shè)置應(yīng)用名主届;將自己注冊(cè)到自己
spring:
application:
name: eureka-server
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
OK君丁,此時(shí)eureka-server啟動(dòng)起來(lái)了。
user-service服務(wù)要注冊(cè)到eureka-server橡庞,改造user-service
<!--eureka客戶端-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-eureka-client</artifactId>
<version>2.0.1.RELEASE</version>
</dependency>
添加@EnableDiscoveryClient注解
@EnableDiscoveryClient
@SpringBootApplication
@MapperScan("enen.user.mapper")
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class);
}
}
要知道注冊(cè)中心的位置及給應(yīng)用命名
server:
port: 8081
spring:
application:
name: user-service
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/eesy
username: root
password: 12345678
mybatis:
type-aliases-package: enen.user.pojo
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
ok 扒最,接下來(lái)改造服務(wù)的調(diào)用方吧趣,同樣引依賴耙厚,加注解薛躬,加配置。
server:
port: 8088
spring:
application:
name: consumer-service
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
接下來(lái)看下高可用的Eureka
server:
port: 10087
spring:
application:
name: eureka-server
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
user-service向注冊(cè)中心寫(xiě)入時(shí)
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka
Eureka客戶端:
服務(wù)提供者要向EurekaServer注冊(cè)服務(wù)八匠,并且完成服務(wù)續(xù)約等工作臀叙。
服務(wù)注冊(cè):
服務(wù)提供者在啟動(dòng)時(shí)劝萤,會(huì)檢測(cè)配置屬性中的:eureka.client.register-with-eureka=true參數(shù)是否正確慎璧,事實(shí)上就是true胸私。如果值為true,則會(huì)向EurekaServer發(fā)起一個(gè)Rest請(qǐng)求阔涉,并攜帶自己的元數(shù)據(jù)信息瑰排,EurekaServer會(huì)把這些信息保存到一個(gè)雙層Map結(jié)構(gòu)中。
服務(wù)續(xù)約:
在注冊(cè)服務(wù)完成以后崇渗,服務(wù)提供者會(huì)維持一個(gè)心跳(定時(shí)向EurekaServer發(fā)起Rest請(qǐng)求)宅广,告訴EurekaServer:“我還或者”些举。這個(gè)稱為服務(wù)的續(xù)約(renew):有兩個(gè)重要的參數(shù)可以修改服務(wù)續(xù)約的行為:
eureka:
instance:
lease-renewal-interval-in-seconds: 30
lease-expiration-duration-in-seconds: 90
lease-renewal-interval-in-seconds服務(wù)續(xù)約的間隔户魏,默認(rèn)30秒绪抛。lease-expiration-duration-in-seconds服務(wù)失效時(shí)間,默認(rèn)90秒笤休。也就是說(shuō)症副,默認(rèn)情況下每隔30秒服務(wù)就會(huì)向注冊(cè)中心發(fā)送一次心跳贞铣,證明自己還活著辕坝。如果超過(guò)90秒沒(méi)有發(fā)送心跳,EurekaServer就會(huì)認(rèn)為該服務(wù)宕機(jī)琳袄,會(huì)從服務(wù)列表中移除窖逗,這兩個(gè)值在生產(chǎn)環(huán)境不要修改餐蔬,默認(rèn)即可。
獲取服務(wù)列表:
當(dāng)服務(wù)消費(fèi)者啟動(dòng)時(shí)音同,會(huì)檢測(cè)eureka.client.fetch-registry=true參數(shù)的值痴鳄,如果為true痪寻,則會(huì)從EurekaServer服務(wù)的列表只讀備份橡类,然后緩存在本地芽唇,并且每隔30秒會(huì)重新獲取更新數(shù)據(jù)匆笤。
失效剔除:有時(shí)我們的服務(wù)可能由于內(nèi)存溢出或網(wǎng)絡(luò)故障等原因使服務(wù)不能正常的工作炮捧,而服務(wù)注冊(cè)中心并未收到“服務(wù)下線”的請(qǐng)求。相對(duì)于服務(wù)提供者的“服務(wù)續(xù)約”操作末誓,服務(wù)注冊(cè)中心在啟動(dòng)時(shí)會(huì)創(chuàng)建一個(gè)定時(shí)服務(wù)喇澡,默認(rèn)每隔一段時(shí)間(默認(rèn)60秒)將當(dāng)前清單中超時(shí)(默認(rèn)90秒)沒(méi)有續(xù)約的服務(wù)剔除殊校∥鳎可以通過(guò)eureka.server.eviction-interval-timer-in-ms參數(shù)對(duì)其進(jìn)行修改艺谆,單位是毫秒。
負(fù)載均衡Ribbon:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
@RestController
@RequestMapping("consumer")
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
@Autowired
// private DiscoveryClient discoveryClient;
private RibbonLoadBalancerClient client;
@GetMapping("{id}")
public User queryById(@PathVariable("id") int id){
//根據(jù)服務(wù)id獲取實(shí)例
// List<ServiceInstance> instances = discoveryClient.getInstances("user-service");
//從實(shí)例中取出ip和端口
// ServiceInstance ins = instances.get(0);
//使用負(fù)載均衡(默認(rèn)輪詢)
ServiceInstance ins = client.choose("user-service");
String url = "http://"+ins.getHost()+":"+ins.getPort()+"/user/" + id;
User user = restTemplate.getForObject(url, User.class);
return user;
}
}
第二種方式:
在啟動(dòng)類上加注解@LoadBalanced
@EnableDiscoveryClient
@SpringBootApplication
public class ConsumerApplication {
@LoadBalanced
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args){
SpringApplication.run(ConsumerApplication.class, args);
}
}
@RestController
@RequestMapping("consumer")
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
@Autowired
// private DiscoveryClient discoveryClient;
// private RibbonLoadBalancerClient client;
@GetMapping("{id}")
public User queryById(@PathVariable("id") int id){
//根據(jù)服務(wù)id獲取實(shí)例
// List<ServiceInstance> instances = discoveryClient.getInstances("user-service");
//從實(shí)例中取出ip和端口
// ServiceInstance ins = instances.get(0);
//使用負(fù)載均衡(默認(rèn)輪詢)
// ServiceInstance ins = client.choose("user-service");
// String url = "http://"+ins.getHost()+":"+ins.getPort()+"/user/" + id;
String url = "http://user-service/user/"+id;
User user = restTemplate.getForObject(url, User.class);
return user;
}
}
采用隨機(jī)而不是輪詢
user-service:
ribbon:
NFLoadBalancerRulerClassName: com.netflix.loadbalancer.RandomRule
Hystrix:
解決雪崩問(wèn)題的手段有兩個(gè):線程隔離和服務(wù)熔斷。
線程隔離侠碧,服務(wù)降級(jí)
Hystrix為每個(gè)依賴服務(wù)調(diào)用分配一個(gè)小的線程池缠黍,如果線程池已滿調(diào)用將被立即拒絕瓷式,默認(rèn)不采用排隊(duì)贸典,加速失敗判定時(shí)間。
用戶的請(qǐng)求將不再直接訪問(wèn)服務(wù)据过,而是通過(guò)線程池中的空閑線程來(lái)訪問(wèn)服務(wù)绳锅,如果線程池已滿酝掩,或者超時(shí)庸队,則會(huì)進(jìn)行降級(jí)處理彻消。
服務(wù)降級(jí):優(yōu)先保證核心服務(wù),而非核心服務(wù)不可用或弱可用丙笋。
觸發(fā)Hystrix服務(wù)降級(jí)的情況:線程池已滿御板;請(qǐng)求超時(shí)牛郑。
實(shí)踐服務(wù)的消費(fèi)方進(jìn)行降級(jí)處理:
(1)引依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
(2)開(kāi)啟熔斷淹朋,加注解@EnableCircuitBreaker
@EnableCircuitBreaker
@EnableDiscoveryClient
@SpringBootApplication
public class ConsumerApplication {
@LoadBalanced
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args){
SpringApplication.run(ConsumerApplication.class, args);
}
}
@SpringCloudApplication=@EnableCircuitBreaker+@EnableDiscoveryClient+@SpringBootApplication
(3)編寫(xiě)降級(jí)邏輯
@GetMapping("{id}")
//成功和失敗的方法返回值和參數(shù)列表必須一樣
@HystrixCommand(fallbackMethod = "queryByIdFallback")
public String queryById(@PathVariable("id") int id){
String url = "http://user-service/user/"+id;
String user = restTemplate.getForObject(url, String.class);
return user;
}
public String queryByIdFallback(@PathVariable("id") int id){
return "不好意思杈抢,服務(wù)器太擁擠了!";
}
通用的fallback
@RestController
@RequestMapping("consumer")
@DefaultProperties(defaultFallback = "defaultFallback")
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("{id}")
//成功和失敗的方法返回值和參數(shù)列表必須一樣
//@HystrixCommand(fallbackMethod = "queryByIdFallback")
@HystrixCommand
public String queryById(@PathVariable("id") int id){
String url = "http://user-service/user/"+id;
String user = restTemplate.getForObject(url, String.class);
return user;
}
public String queryByIdFallback(int id){
return "不好意思,服務(wù)器太擁擠了歼捐!";
}
public String defaultFallback(){
return "不好意思窥岩,服務(wù)器太擁擠了颂翼!";
}
}
自定義超時(shí)時(shí)長(zhǎng)配置
@HystrixCommand(commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value = "2000")
})
feign
feign可以把Rest請(qǐng)求進(jìn)行隱藏朦乏,偽裝成類似SpringMVC的controller一樣氧骤,你不用再自己拼接url筹陵,拼接參數(shù)等操作,一切都交給feign去做并思。
引依賴宋彼,加注解输涕,寫(xiě)接口
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
在啟動(dòng)類上慨畸,添加注解寸士,開(kāi)啟feign功能
@EnableFeignClient
@FeignClient("user-service")
public interface UserClient {
@GetMapping("user/{id}")
User queryById(@PathVariable("id") int id);
}
在遠(yuǎn)程調(diào)用時(shí),直接注入U(xiǎn)serClient
@Autowird
private UserClient userClient;
@GetMapping("{id}")
public User queryById(@PathVariable("id") int id){
return userClient.queryById(id);
}
feign開(kāi)啟熔斷:
在application.yml中開(kāi)啟hystrix
feign:
hystrix:
enabled: true
feign中的Fallback配置不像Ribbon中那樣簡(jiǎn)單了螟深。
zuul
不管是來(lái)自客戶端的請(qǐng)求界弧,還是服務(wù)內(nèi)部調(diào)用垢箕,一切對(duì)服務(wù)的請(qǐng)求都會(huì)經(jīng)過(guò)Zuul這個(gè)網(wǎng)關(guān)兑巾,然后再由網(wǎng)關(guān)來(lái)實(shí)現(xiàn)鑒權(quán)蒋歌、動(dòng)態(tài)路由等等操作堂油。Zuul就是我們服務(wù)的統(tǒng)一入口。
快速入門:
引依賴
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
</dependencies>
加注解
@EnableZuulProxy
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class);
}
}
配置
server:
port: 10010
zuul:
routes:
hehe:
path: /user-service/**
url: http://127.0.0.1:8081
訪問(wèn)http://127.0.0.1:10010/user-service/user/1
ok了。
這里地址是固定的不好院峡,需要從eureka中拉取服務(wù)系宜。
引依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
最終配置為:
server:
port: 10010
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
zuul:
routes:
hehe:
path: /user-service/**
serviceId: user-service
http://127.0.0.1:10010/user-service
成功映射到/user-service/**实抡,然后轉(zhuǎn)發(fā)至serviceId: user-service這個(gè)服務(wù)欢策,從eureka中去查找到8081踩寇,然后訪問(wèn)8081。
OK辣卒,以上就是面向服務(wù)的路由荣茫。
簡(jiǎn)化配置方案:
服務(wù)ID:服務(wù)路徑
zuul:
routes:
user-service: /user-service/**
默認(rèn)情況下啡莉,一切服務(wù)的映射路徑就是服務(wù)名本身咧欣。例如服務(wù)名為:user-service,則默認(rèn)的映射路徑就是:/user-service/**衩椒。也就是說(shuō)毛萌,剛才的映射規(guī)則不用配置也是ok的朝聋。
如果想禁用某個(gè)路由規(guī)則囤躁,可以:
server:
port: 10010
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
spring:
application:
name: gateway
zuul:
routes:
user-service: /user/**
ignored-services:
-consumer-service
過(guò)濾器
zuul作為網(wǎng)關(guān)的其中一個(gè)重要功能狸演,就是實(shí)現(xiàn)請(qǐng)求的鑒權(quán)宵距。而這個(gè)動(dòng)作我們往往是通過(guò)Zuul提供的過(guò)濾器來(lái)實(shí)現(xiàn)的满哪。
filterType()//過(guò)濾器類型劝篷;filterOrder()//過(guò)濾器順序(數(shù)字越小優(yōu)先級(jí)越高)娇妓;shouldFilter()//要不要過(guò)濾哈恰;run()//過(guò)濾邏輯
過(guò)濾器執(zhí)行生命周期
正常流程:請(qǐng)求到達(dá)首先會(huì)經(jīng)過(guò)pre類型過(guò)濾器,而后到達(dá)routing類型蛔钙,進(jìn)行路由吁脱,請(qǐng)求就到達(dá)真正的服務(wù)提供者豫喧,執(zhí)行請(qǐng)求,返回結(jié)果后讲衫,會(huì)達(dá)到post過(guò)濾器涉兽,而后返回響應(yīng)枷畏。
異常流程:整個(gè)過(guò)程中拥诡,pre或routing過(guò)濾器出現(xiàn)異常氮发,都會(huì)直接進(jìn)入error過(guò)濾器渴肉,在error處理完畢后,會(huì)將請(qǐng)求交給post過(guò)濾器爽冕,最后返回給用戶仇祭;如果是error過(guò)濾器自己出現(xiàn)異常,最終也會(huì)進(jìn)入post過(guò)濾器颈畸,而后返回乌奇;如果是post過(guò)濾器出現(xiàn)異常,會(huì)跳轉(zhuǎn)到error過(guò)濾器眯娱,但是與pre和touting不同的是請(qǐng)求不會(huì)再到達(dá)post過(guò)濾器了。
自定義一個(gè)過(guò)濾器困乒,模擬登陸校驗(yàn)寂屏。基本邏輯:如果請(qǐng)求中有access-token參數(shù),則認(rèn)為請(qǐng)求有效迁霎,放行吱抚。
@Component//自動(dòng)加入到spring中
public class LoginFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return FilterConstants.PRE_DECORATION_FILTER_ORDER-1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
//獲取請(qǐng)求上下文
RequestContext ctx = RequestContext.getCurrentContext();
// 獲取request
HttpServletRequest request = ctx.getRequest();
//獲取請(qǐng)求參數(shù)access-token
String token = request.getParameter("access-token");
//判斷是否存在
if(StringUtils.isBlank(token)){
//不存在,未登錄考廉,則攔截
ctx.setSendZuulResponse(false);
//返回403
ctx.setResponseStatusCode(HttpStatus.SC_FORBIDDEN);
}
return null;
}
}
負(fù)載均衡和熔斷:
zuul中默認(rèn)就已經(jīng)集成了Ribbon負(fù)載均衡和Hystix熔斷機(jī)制秘豹,但是所有的超時(shí)策略都是走的默認(rèn)值,比如熔斷超時(shí)時(shí)間只有1s昌粤,很容易就觸發(fā)了既绕。因此建議手動(dòng)進(jìn)行配置:
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 6000
ribbon:
ConnectionTimeout: 500
ReadTimeOut: 2000
ribbon的超時(shí)時(shí)長(zhǎng),真實(shí)值是(read+connect)*2涮坐,必須小于hystrix時(shí)長(zhǎng)凄贩。
zuul的高可用:
啟動(dòng)多個(gè)Zuul服務(wù),自動(dòng)注冊(cè)到Eureka袱讹,形成集群疲扎。如果是服務(wù)內(nèi)部訪問(wèn),你訪問(wèn)Zuul捷雕,自動(dòng)負(fù)載均衡椒丧,沒(méi)問(wèn)題。但是救巷,Zuul更多是外部訪問(wèn)壶熏,PC端愕宋、移動(dòng)端等翰意。他們無(wú)法通過(guò)Eureka進(jìn)行負(fù)載均衡,那么該怎么辦烂瘫?此時(shí)管怠,使用其他的服務(wù)網(wǎng)關(guān)淆衷,來(lái)對(duì)Zuul進(jìn)行代理,比如Nginx渤弛。