springcloud之自定義簡易消費服務元件

神牛003發表於2018-06-04

  本次和大家分享的是怎麼來消費服務,上篇文章講了使用Feign來消費,本篇來使用rest+ribbon消費服務,並且通過輪詢方式來自定義了個簡易消費元件,本文分享的宗旨是:自定義消費服務的思路;思路如果有可取之處還請“贊”一下:

  • Rest+Ribbon實現消費服務
  • Rest+輪詢自定義簡易消費元件
  • 使用Scheduled重新整理服務提供者資訊

Rest+Ribbon實現消費服務

  做為服務消費方準確的來說進行了兩種主流程區分1)獲取可以服務2)呼叫服務,那麼又是如何獲取服務的並且又是通過什麼來呼叫服務的,下面我們來看一副手工圖:

  

  手工圖上能夠看出消費方先獲取了服務方的真實介面地址,然後再通過地址去呼叫介面;然後對於微服務架構來說獲取某一個類ip或埠然後去呼叫介面肯定是不可取的,因此微服務中產生了一種serviceid的概念;簡單流程介紹完了,下面通過例項來分析;首先新增依賴如:

1 <dependency>
2     <groupId>org.springframework.boot</groupId>
3     <artifactId>spring-boot-starter-web</artifactId>
4 </dependency>
5 <dependency>
6     <groupId>org.springframework.cloud</groupId>
7     <artifactId>spring-cloud-starter-eureka</artifactId>
8 </dependency>

  再來我們通過上篇文章搭建的eureka_server(服務中心),eureka_provider(服務提供者)來做測試用例,這裡我重新定義eureka_consumer_ribbon模組做為消費服務;先建立service層類和程式碼:

 1 @Service
 2 public class UserService implements UserInterface {
 3 
 4     @Autowired
 5     protected RestTemplate restTemplate;
 6 
 7     @Override
 8     public MoRp<List<MoUser>> getUsers(MoRq rq) {
 9         return null;
10     }
11 
12     @Override
13     public String getMsg() {
14 
15         String str = restTemplate.getForObject("http://EUREKA-PROVIDER/msg", String.class);
16         return str;
17     }
18 }

  主要用到了RestTemplate的restTemplate.getForObject函式,然後需要定義個Controller來吧獲取到的資料響應到頁面上,為了簡單這裡僅僅只拿getMsg服務介面測試:

 1 @RestController
 2 public class UserController {
 3 
 4     @Autowired
 5     private UserService userService;
 6 
 7     @GetMapping("/msg")
 8     public String getMsg(){
 9 
10         return userService.getMsg();
11     }
12 }

  最後我們在啟動類新增入下程式碼,注意@LoadBalanced標記必須加,因為咋們引入的eureka依賴裡面包含了ribbon(Dalston.RELEASE版本),ribbon封裝了負載均衡的演算法,如果不加這個註解,那後面rest方法的url就必須是可用的url路徑了,當然這裡加了註解就可以使用上面說的serviceId:

 1 @SpringBootApplication
 2 @EnableDiscoveryClient //消費客戶端
 3 public class EurekaConsumerRibbonApplication {
 4 
 5     @Bean 
 6     @LoadBalanced //負載均衡
 7     RestTemplate restTemplate(){
 8         return new RestTemplate();
 9     }
10     
11     public static void main(String[] args) {
12         SpringApplication.run(EurekaConsumerRibbonApplication.class, args);
13     }
14 }

  下面來消費方顯示的效果:

  

Rest+輪詢自定義簡易消費元件

  自定義消費元件原來和麵手工圖差不多,就是先想法獲取服務提供端真實的介面地址,然後通過rest去呼叫這個url,得到相應的結果輸出;這裡自定義了一個ShenniuBanlance的元件類:

  1 /**
  2  * Created by shenniu on 2018/6
  3  * <p>
  4  * rest+eureka+自定義client端
  5  */
  6 @Component
  7 public class ShenniuBanlance {
  8 
  9     @Autowired
 10     private RestTemplate restTemplate;
 11 
 12     @Autowired
 13     private DiscoveryClient discoveryClient;
 14 
 15     /**
 16      * 服務真實地址 ConcurrentHashMap<"服務應用名稱", ("真實介面ip", 被訪問次數)>
 17      */
 18     public static ConcurrentHashMap<String, List<MoService>> sericesMap = new ConcurrentHashMap<>();
 19 
 20     /**
 21      * 設定服務提供者資訊到map
 22      */
 23     public void setServicesMap() {
 24         //獲取所有服務提供者applicationName
 25         List<String> appNames = discoveryClient.getServices();
 26 
 27         //儲存真實地址到map
 28         for (String appName :
 29                 appNames) {
 30             //獲取某個服務提供者資訊
 31             List<ServiceInstance> instanceInfos = discoveryClient.getInstances(appName);
 32             if (instanceInfos.isEmpty()) {
 33                 continue;
 34             }
 35 
 36             List<MoService> services = new ArrayList<>();
 37             instanceInfos.forEach(b -> {
 38                 MoService service = new MoService();
 39                 //被訪問次數
 40                 service.setWatch(0L);
 41                 //真實介面地址
 42                 service.setUrl(b.getUri().toString());
 43                 services.add(service);
 44             });
 45 
 46             //如果存在就更新
 47             sericesMap.put(appName.toLowerCase(), services);
 48         }
 49     }
 50 
 51     /**
 52      * 根據app獲取輪詢方式選中後的service
 53      *
 54      * @param appName
 55      * @return
 56      */
 57     public MoService choiceServiceByAppName(String appName) throws Exception {
 58         appName = appName.toLowerCase();
 59         //某種app的服務service集合
 60         List<MoService> serviceMap = sericesMap.get(appName);
 61         if (serviceMap == null) {
 62             //初始化所有app服務
 63             setServicesMap();
 64             serviceMap = sericesMap.get(appName);
 65             if (serviceMap == null) {
 66                 throw new Exception("未能找到" + appName + "相關服務");
 67             }
 68         }
 69 
 70         //篩選出被訪問量最小的service  輪詢的方式
 71         MoService moService = serviceMap.stream().min(
 72                 Comparator.comparing(MoService::getWatch)
 73         ).get();
 74 
 75         //負載記錄+1
 76         moService.setWatch(moService.getWatch() + 1);
 77         return moService;
 78     }
 79 
 80     /**
 81      * 自動重新整理 服務提供者資訊到map
 82      */
 83     @Scheduled(fixedDelay = 1000 * 10)
 84     public void refreshServicesMap() {
 85         setServicesMap();
 86     }
 87 
 88     /**
 89      * get請求服務獲取返回資料
 90      *
 91      * @param appName     應用名稱 ApplicationName
 92      * @param serviceName 服務名稱 ServiceName
 93      * @param map         url上請求引數
 94      * @param tClass      返回型別
 95      * @param <T>
 96      * @return
 97      */
 98     public <T> T getServiceData(
 99             String appName, String serviceName,
100             Map<String, ?> map,
101             Class<T> tClass) {
102         T result = null;
103         try {
104             //篩選獲取真實Service
105             MoService service = choiceServiceByAppName(appName);
106 
107             //請求該service的url
108             String apiUrl = service.getUrl() + "/" + serviceName;
109             System.out.println(apiUrl);
110             result = map != null ?
111                     restTemplate.getForObject(apiUrl, tClass, map) :
112                     restTemplate.getForObject(apiUrl, tClass);
113         } catch (Exception ex) {
114             ex.printStackTrace();
115         }
116         return result;
117     }
118 
119     /**
120      * Service資訊
121      */
122     public class MoService {
123         /**
124          * 負載次數記錄數
125          */
126         private Long watch;
127         /**
128          * 真實介面地址: http://xxx.com/api/add
129          */
130         private String url;
131 
132         public Long getWatch() {
133             return watch;
134         }
135 
136         public void setWatch(Long watch) {
137             this.watch = watch;
138         }
139 
140         public String getUrl() {
141             return url;
142         }
143 
144         public void setUrl(String url) {
145             this.url = url;
146         }
147     }
148 }

  以上就是主要的實現程式碼,程式碼邏輯:設定服務提供者資訊到map-》根據app獲取輪詢方式選中後的service-》請求服務獲取返回資料;輪詢實現的原理是使用了一個負載記錄數,每次被請求後自動+1,當要獲取某個服務提供者時,通過記錄數篩選出最小值的一個例項,裡面儲存有真實介面地址url;呼叫只需要這樣(當然可以弄成註解來呼叫):

 1     @Override
 2     public String getMsg() {
 3 
 4         String str = banlance.getServiceData(
 5                 "EUREKA-PROVIDER", "msg",
 6                 null,
 7                 String.class
 8         );
 9         return str;
10     }

  這裡需要注意由於我們在前面RestTemplate使用加入了註解@LoadBalanced,這樣使得rest請求時必須用非ip的訪問方式(也就是必須serviceid)才能正常響應,不然會提示錯誤如:

  

  簡單來說就是不用再使用ip了,因為有負載均衡機制;當我們去掉這個註解後,我們自定義的元件就能執行成功,效果圖和例項1一樣就不貼圖了;

使用Scheduled重新整理服務提供者資訊

  在微服務架構中,如果某臺服務掛了之後,必須要及時更新client端的服務快取資訊,不然就可能請求到down的url去,基於這種考慮我這裡採用了EnableSched標記來做定時重新整理;首先在啟動類增加 @EnableScheduling ,然後定義一個刷行服務資訊的服務如:

1 /**
2      * 自動重新整理 服務提供者資訊到map 
3      */
4     @Scheduled(fixedDelay = 1000 * 10)
5     public void refreshServicesMap() {
6         setServicesMap();
7     }

  為了方便看測試效果,我們在server,provider(2個),consumer已經啟動的情況下,再啟動一個埠為2005的provider服務;然後重新整理consumer介面看下效果:

  

  這個時候能夠看到呼叫2005埠的介面成功了,通過@Scheduled定時服務吧最新或者失效的服務加入|移除掉,就達到了咋們的需求了;如果你覺得該篇內容對你有幫助,不防贊一下,謝謝。

相關文章