四、負(fù)載均衡Ribbon

上一章講述了服務(wù)注冊和發(fā)現(xiàn)組件Eureka,同時(shí)追蹤源碼深入講解了Eureka的機(jī)制,并通過案例講解了如何構(gòu)建高可用的EurekaServer。本章講解如何使用 RestTemplate和Ribbon相結(jié)合作為服務(wù)消費(fèi)者去消費(fèi)服務(wù),同時(shí)從源碼的角度來深入講解 Ribbon。

一、RestTemplate簡介

??RestTemplate是Spring Resources中一個(gè)訪問第三方RESTful API接口的網(wǎng)絡(luò)請求框架。RestTemplate的設(shè)計(jì)原則和其他SpringTemplate (例如 JdbcTemplate、 JmsTemplate)類似,都是為執(zhí)行復(fù)雜任務(wù)提供了一個(gè)具有默認(rèn)行為的簡單方法。
??RestTemplate是用來消費(fèi)REST服務(wù)的,所以RestTemplate 的主要方法都與REST的Http協(xié)議的一些方法緊密相連,例如HEAD、GET、POST、PUT、DELETE和OPTIONS等方法, 這些方法在RestTemplate類對應(yīng)的方法為headForHeaders()、getForObject()、postForObject()、put()和delete()等。

二、Ribbon簡介

??負(fù)載均衡是指將負(fù)載分?jǐn)偟蕉鄠€(gè)執(zhí)行單元上,常見的負(fù)載均衡有兩種方式。一種是獨(dú)立進(jìn)程單元,通過負(fù)載均衡策略,將請求轉(zhuǎn)發(fā)到不同的執(zhí)行單元上,例如Ngnix。另一種是將負(fù)載均衡邏輯以代碼的形式封裝到服務(wù)消費(fèi)者的客戶端上,服務(wù)消費(fèi)者客戶端維護(hù)了一份服務(wù)提供者的信息列表,有了信息列表,通過負(fù)載均衡策略將請求分?jǐn)偨o多個(gè)服務(wù)提供者,從而達(dá)到負(fù)載均衡的目的。
??Ribbon是Netflix公司開源的一個(gè)負(fù)載均衡的組件,它屬于上述的第二種方式,是將負(fù)載均衡邏輯封裝在客戶端中,并且運(yùn)行在客戶端的進(jìn)程里。Ribbon是一個(gè)經(jīng)過了云端測試的IPC庫,可以很好地控制HTTP和TCP 客戶端的負(fù)載均衡行為。
??在SpringCloud構(gòu)建的微服務(wù)系統(tǒng)中, Ribbon作為服務(wù)消費(fèi)者的負(fù)載均衡器,有兩種使用方式,一種是和RestTemplate相結(jié)合,另一種是和Feign相結(jié)合。Feign已經(jīng)默認(rèn)集成了Ribbon, 關(guān)于Feign的內(nèi)容將會在下一章進(jìn)行詳細(xì)講解。
??Ribbon有很多子模塊,但很多模塊沒有用于生產(chǎn)環(huán)境 ,目前Netflix公司用于生產(chǎn)環(huán)境的Ribbon子模塊如下:

  • ribbon-loadbalancer: 可以獨(dú)立使用或與其他模塊一起使用的負(fù)載均衡器API。
  • ribbon-eureka: Ribbon結(jié)合Eureka客戶端的API,為負(fù)載均衡器提供動態(tài)服務(wù)注冊列表信息。
  • ribbon-core: Ribbon的核心API

三、使用RestTemplate和Ribbo和Ribbon來消費(fèi)服務(wù)

??本案例是上一節(jié)案例的基礎(chǔ)上進(jìn)行改造的,先回顧一下上一節(jié)中的代碼結(jié)構(gòu),它包括一個(gè)服務(wù)注冊中心eureka-server、一個(gè)服務(wù)提供者eureka-client。eureka-client向eureka-server注冊服務(wù),并且eureka-client提供了一個(gè)“ /hi"API接口,用于提供服務(wù)。
??啟動eureka-server, 端口為8671。啟動兩個(gè)eureka-client實(shí)例,端口分別為 8762和8763。 啟動完成后,在瀏覽器上訪問 http://localhost:8671/,瀏覽器顯示 eureka-client 的兩個(gè)實(shí)例已經(jīng)成功向服務(wù)注冊中心注冊,它們的端口分別為8672和8673,如下所示。

??創(chuàng)建完成 eureka-ribbon-client 的 Module 工程之后 , 在其pom文件中引入相關(guān)的依賴,包括繼承了主Maven工程的pom文件,引入了EurekaClient的起步依賴spring-cloud-starter- eureka、Ribbon的起步依賴spring-cloud-starter-ribbon,以及Web的起步依賴spring-boot-starter-web, 代碼如下:

<parent>
        <groupId>com.hand</groupId>
        <artifactId>macro-service</artifactId>
        <version>1.0-SNAPSHOT</version>
        <relativePath/>
</parent>
<dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-ribbon</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

macro-service為主模塊的配置,內(nèi)容如下:

<groupId>com.hand</groupId>
   <artifactId>macro-service</artifactId>
   <version>1.0-SNAPSHOT</version>
   <packaging>pom</packaging>
  <parent>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-parent</artifactId>
       <version>1.5.10.RELEASE</version>
       <relativePath/>
   </parent>
   <properties>
       <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
       <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
       <spring.cloud.vension>Dalston.SR1</spring.cloud.vension>
       <java.version>1.8</java.version>
   </properties>
   <dependencies>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-test</artifactId>
           <scope>test</scope>
       </dependency>
   </dependencies>
   <dependencyManagement>
       <dependencies>
           <dependency>
               <groupId>org.springframework.cloud</groupId>
               <artifactId>spring-cloud-dependencies</artifactId>
               <version>${spring.cloud.vension}</version>
               <type>pom</type>
               <scope>import</scope>
           </dependency>
       </dependencies>
   </dependencyManagement>

??在工程的配置文件appIication.yml做程序的相關(guān)配置,包括指定程序名為 eureka-ribbon--client,程序的端口號為8674,服務(wù)的注冊地址http://localhost:8761/eureka/,代碼如下:

spring:
  application:
    name: eureka-ribbon-client
server:
  port: 8674
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8671/eureka/

??另外,作為EurekaClient需要在程序的入口類加上注解@EnableEurekaClient開啟EurekaClient功能,代碼如下:

@SpringBootApplication
@EnableEurekaClient
public class EurekaRibbonClientApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaRibbonClientApplication.class, args);
    }
}

??寫一個(gè)RESTful API接口,在該API接口內(nèi)部需要調(diào)用eureka-client的API接口"/hi”, 即服務(wù)消費(fèi)。由于eureka-client為兩個(gè)實(shí)例,它們的端口為8672和8673。在調(diào)用eureka-client的API接口“/hi”時(shí)希望做到輪流訪問這兩個(gè)實(shí)例,這時(shí)就需要將RestTemplate和Ribbon相結(jié)合,進(jìn)行負(fù)載均衡。
??首先需要在程序的IoC容器中注入一個(gè) restTemplate的Bean,并在這個(gè)Bean上加上@LoadBalanced注解,此時(shí)RestTemplate就結(jié)合了Ribbon開啟了負(fù)載均衡功能 ,代碼如下:

@Configuration
public class RibbonConfig {
    
    @Bean
    @LoadBalanced
    RestTemplate restTemplate(){
        return new RestTemplate();
    }
}

??寫一個(gè)RibbonService類,在該類的 hi()方法用 restTemplate 調(diào)用eureka-client的API接口,此時(shí)Uri上不需要使用硬編碼(例如IP地址),只需要寫服務(wù)名eureka-client 即可,代碼如下:

@Service
public class RibbonService {

    @Autowired
    private RestTemplate restTemplate;

    public String hi(String name) {
        return restTemplate.getForObject("http://eureka-client/hi?name=" + name, String.class);
    }
}

寫一個(gè)RibbonController類,為該類加上@RestController注解,開啟RestController的功能, 寫一個(gè)“/hi” Get方法的接口,調(diào)用RibbonService類的hi()方法,代碼如下:

@RestController
public class RibbonController {

    @Autowired
    private RibbonService ribbonService;

    @GetMapping(value = "/hi")
    public String hi(@RequestParam String name){
        return ribbonService.hi(name);
    }
}

啟動eureka-ribbon-client工程,在瀏覽器上訪問http://localhost:8671,顯示的EurekaServer 的主界面如下圖所示。在主界面上發(fā)現(xiàn)有兩個(gè)服務(wù)被注冊,分別為eureka-client和eureka-ribbon-client,其中eureka-client有兩個(gè)實(shí)例,端口為8672和8673,而eureka-ribbon-client的端口為8674


在瀏覽器上多次訪問 http://localhost:8674/hi?name=ben,瀏覽器會輪流顯示如下內(nèi)容:

hi ben, i am from port:8672
hi ben, i am from port:8673

四、LoadBalancerClient簡介

??負(fù)載均衡器的核心類為LoadBalancerClient, LoadBalancerCiient可以獲取負(fù)載均衡的服務(wù)提供者的實(shí)例信息。為了演示,在RibbonController重新寫一個(gè)接口“/testRibbon”,通過LoadBalancerCIient去選擇一個(gè)eureka-client的服務(wù)實(shí)例的信息,并將該信息返回,繼續(xù)在eureka-ribbon-client工程上修改,代碼如下:

@RestController
public class RibbonController {
    ...//省略代碼
    @Autowired
    private LoadBalancerClient loadBalancer;

    @GetMapping ("/testRibbon")
    public String testRibbon() {

        ServiceInstance instance = loadBalancer.choose("eureka-client");
        return instance.getHost() + ":" + instance.getPort();
    }
}

??重新啟動工程,在瀏覽器上多次訪問http://localhost:8764/testRibbon,瀏覽器會輪流顯示如下內(nèi)容 :

localhost:8672
localhost:8673

??可見,LoadBalancerClient 的 choose(”eureka-client'’)方法可以輪流得到 eureka-client 的兩個(gè) 服務(wù)實(shí)例的信息。
??負(fù)載均衡器LoadBalancerClient是從EurekaClient獲取服務(wù)注冊列表信息的,并將服務(wù)注冊列表信息緩存了一份。在LoadBalancerCJient 調(diào)用choose()方法時(shí),根據(jù)負(fù)載均衡策略選擇一個(gè)服務(wù)實(shí)例的信息,從而進(jìn)行了負(fù)載均衡。 LoadBalancerClient也可以不從EurekaClient獲取注冊列表信息, 這時(shí)需要自己維護(hù)一份服務(wù)注冊列表信息。需要修改application.xml的配置信息,通過stores.ribbon.listOfServers來配置這些服務(wù)實(shí)例的Uri。

#禁止Ribbon從Eureka獲取注冊列表信息
ribbon:
  eureka:
    enabled: false

#手動配置服務(wù)列表
stores:
  ribbon:
    listOfServers: example1.com,example2.com
@RestController
public class RibbonController {
    @Autowired
    private LoadBalancerClient loadBalancer;

    @GetMapping ("/testSelfConfigRibbon")
    public String testSelfConfigRibbon() {
        ServiceInstance instance = loadBalancer.choose("stores");
        return instance.getHost() + ":" + instance.getPort();
    }

啟動工程 ,在瀏覽器上多次訪問內(nèi)容:http://localhost:8769/testRibbon,瀏覽器會交替出現(xiàn)以下內(nèi)容:

example1.com:80
example2.com:80

??由此,我們知道在Ribbon中的負(fù)載均衡客戶端為LoadBalancerClient。在SpringCloud項(xiàng)目中,負(fù)載均衡器Ribbon會默認(rèn)從EurekaClient的服務(wù)注冊列表中獲取服務(wù)的信息,并緩存一份。根據(jù)緩存的服務(wù)注冊列表信息,可以通過LoadBalancerClient來選擇不同的服務(wù)實(shí)例, 從而實(shí)現(xiàn)負(fù)載均衡。如果禁止Ribbon從Eureka獲取注冊列表信息,則需要自己去維護(hù)一份服務(wù)注冊列表信息。根據(jù)自己維護(hù)服務(wù)注冊列表的信息,Ribbon也可以實(shí)現(xiàn)負(fù)載均衡。

五、源碼解析Ribbon

??為了深入理解Ribbon,通過查看源碼來分析Ribbon如何和RestTemplate相結(jié)合來做負(fù)載均衡。開啟負(fù)載均衡的關(guān)鍵在@LoadBalanced這個(gè)注解上,首先從這個(gè)注解入手。

@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LoadBalanced {
}

??上面是@LoadBalanced的定義,這就是一個(gè)普通的標(biāo)記注解,作用就是修飾RestTemplate讓其擁有負(fù)載均衡的能力,全局搜索發(fā)現(xiàn)在LoadBalancerAutoConfiguration.java這個(gè)類里用到了。

@Configuration
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {

    @LoadBalanced
    @Autowired(required = false)
    private List<RestTemplate> restTemplates = Collections.emptyList();

    @Bean
    public SmartInitializingSingleton loadBalancedRestTemplateInitializer(
            final List<RestTemplateCustomizer> customizers) {
        return new SmartInitializingSingleton() {
            @Override
            public void afterSingletonsInstantiated() {
                for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
                    for (RestTemplateCustomizer customizer : customizers) {
                        customizer.customize(restTemplate);
                    }
                }
            }
        };
    }
    //這里的restTemplates是所有的被@LoadBalanced注解的集合,這就是標(biāo)記注解的作用(Autowired是可以集合注入的)
    @Autowired(required = false)
    private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList();

    @Bean
    @ConditionalOnMissingBean
    public LoadBalancerRequestFactory loadBalancerRequestFactory(
            LoadBalancerClient loadBalancerClient) {
        return new LoadBalancerRequestFactory(loadBalancerClient, transformers);
    }
    //生成一個(gè)LoadBalancerInterceptor的Bean
    @Configuration
    @ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
    static class LoadBalancerInterceptorConfig {
        @Bean
        public LoadBalancerInterceptor ribbonInterceptor(
                LoadBalancerClient loadBalancerClient,
                LoadBalancerRequestFactory requestFactory) {
            return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
        }
        //給注解了@LoadBalanced的RestTemplate加上攔截器
        @Bean
        @ConditionalOnMissingBean
        public RestTemplateCustomizer restTemplateCustomizer(
                final LoadBalancerInterceptor loadBalancerInterceptor) {
            return new RestTemplateCustomizer() {
                @Override
                public void customize(RestTemplate restTemplate) {
                    List<ClientHttpRequestInterceptor> list = new ArrayList<>(
                            restTemplate.getInterceptors());
                    list.add(loadBalancerInterceptor);
                    restTemplate.setInterceptors(list);
                }
            };
        }
    }
        ...//省略后面代碼
}

??看到這里,我們應(yīng)該大致知道@loadBalanced的作用了,就是起到一個(gè)標(biāo)記RestTemplate的作用,當(dāng)服務(wù)啟動時(shí),標(biāo)記了的RestTemplate對象里面就會被自動加入LoadBalancerInterceptor攔截器,這樣當(dāng)RestTemplate像外面發(fā)起http請求時(shí),會被LoadBalancerInterceptor的intercept函數(shù)攔截,而intercept里面又調(diào)用了LoadBalancerClient接口實(shí)現(xiàn)類execute方法,我們接著往下看;

public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {

    ...//省略部分代碼
    @Override
    public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
            final ClientHttpRequestExecution execution) throws IOException {
        //這是以服務(wù)名為地址的原始請求:例:http://HI-SERVICE/hi
        final URI originalUri = request.getURI();
        String serviceName = originalUri.getHost();
        Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
        return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
    }
}

這里的LoadBalancerClient的實(shí)現(xiàn)是RibbonLoadBalancerClient,調(diào)用的是RibbonLoadBalancerClient.execute()方法。在execute內(nèi)首先執(zhí)行g(shù)etLoadBalancer(serviceId)獲取ILoadBalancer的實(shí)現(xiàn)者,然后調(diào)用getServer(loadBalancer)方法通過負(fù)載均衡策略獲取服務(wù)。

@Override
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
    //通過serviceId找到ILoadBalancer的實(shí)現(xiàn)者
    ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
    Server server = getServer(loadBalancer);
    if (server == null) {
        throw new IllegalStateException("No instances available for " + serviceId);
    }
    RibbonServer ribbonServer = new RibbonServer(serviceId, server, isSecure(server,
            serviceId), serverIntrospector(serviceId).getMetadata(server));

    return execute(serviceId, ribbonServer, request);
}

繼續(xù)看getServer(loadBalancer)方法,發(fā)現(xiàn)是調(diào)用ILoadBalancer實(shí)現(xiàn)類對象的chooseServer()方法。

protected Server getServer(ILoadBalancer loadBalancer) {
        if (loadBalancer == null) {
            return null;
        }
        return loadBalancer.chooseServer("default"); // TODO: better handling of key
    }

這里ILoadBalancer接口有三個(gè)實(shí)現(xiàn)類,通過查看源碼發(fā)現(xiàn),BaseLoadBalancer和ZoneAwareLoadBalancer類里都有具體的實(shí)現(xiàn)方法,到底調(diào)用的是哪個(gè)類的方法呢?


查看RibbonClientConfiguration.java類發(fā)現(xiàn)如下代碼:

@Bean
@ConditionalOnMissingBean
public ILoadBalancer ribbonLoadBalancer(IClientConfig config,
       ServerList<Server> serverList, ServerListFilter<Server> serverListFilter,
           IRule rule, IPing ping) {
       if (this.propertiesFactory.isSet(ILoadBalancer.class, name)) {
           return this.propertiesFactory.get(ILoadBalancer.class, config, name);
       }
       ZoneAwareLoadBalancer<Server> balancer = LoadBalancerBuilder.newBuilder()
                .withClientConfig(config).withRule(rule).withPing(ping)
                .withServerListFilter(serverListFilter).withDynamicServerList(serverList)
                .buildDynamicServerListLoadBalancer();
       return balancer;
}

由此可知,攔截器里默認(rèn)調(diào)用的是ZoneAwareLoadBalancer.chooseServer()方法。

    public Server chooseServer(Object key) {
        //ENABLED默認(rèn)值為true
        if (!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <= 1) {
            logger.debug("Zone aware logic disabled or there is only one zone");
            return super.chooseServer(key);
        }
        Server server = null;
        try {
            LoadBalancerStats lbStats = getLoadBalancerStats();
            Map<String, ZoneSnapshot> zoneSnapshot = ZoneAvoidanceRule.createSnapshot(lbStats);
            logger.debug("Zone snapshots: {}", zoneSnapshot);
            if (triggeringLoad == null) {
                triggeringLoad = DynamicPropertyFactory.getInstance().getDoubleProperty(
                        "ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".triggeringLoadPerServerThreshold", 0.2d);
            }

            if (triggeringBlackoutPercentage == null) {
                triggeringBlackoutPercentage = DynamicPropertyFactory.getInstance().getDoubleProperty(
                        "ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".avoidZoneWithBlackoutPercetage", 0.99999d);
            }
            Set<String> availableZones = ZoneAvoidanceRule.getAvailableZones(zoneSnapshot, triggeringLoad.get(), triggeringBlackoutPercentage.get());
            logger.debug("Available zones: {}", availableZones);
            if (availableZones != null &&  availableZones.size() < zoneSnapshot.keySet().size()) {
                String zone = ZoneAvoidanceRule.randomChooseZone(zoneSnapshot, availableZones);
                logger.debug("Zone chosen: {}", zone);
                if (zone != null) {
                    BaseLoadBalancer zoneLoadBalancer = getLoadBalancer(zone);
                    server = zoneLoadBalancer.chooseServer(key);
                }
            }
        } catch (Exception e) {
            logger.error("Error choosing server using zone aware logic for load balancer={}", name, e);
        }
        if (server != null) {
            return server;
        } else {
            logger.debug("Zone avoidance logic is not invoked.");
            return super.chooseServer(key);
        }
    }

但是由于我是模擬單服務(wù)器測試的,所以是單區(qū)域,通過調(diào)試可以看到空間數(shù)為1,如下圖。所以這里會去調(diào)用ZoneAwareLoadBalancer父類的chooseServer()方法,也就是BaseLoadBalancer的chooseServer()方法。


//BaseLoadBalancer.chooseServer()
public Server chooseServer(Object key) {
        if (counter == null) {
            counter = createCounter();
        }
        counter.increment();
        if (rule == null) {
            return null;
        } else {
            try {
                return rule.choose(key);
            } catch (Exception e) {
                logger.warn("LoadBalancer [{}]:  Error choosing server for key {}", name, key, e);
                return null;
            }
        }
    }

接下來就是調(diào)用rule.choose(key);這里是選擇負(fù)載均衡策略。IRule的實(shí)現(xiàn)類如下:


IRule的默認(rèn)實(shí)現(xiàn)類有以下7種。在大多數(shù)情況下,這些默認(rèn)的實(shí)現(xiàn)類是可以滿足需求的,如果有特殊的諦求,可以自己實(shí)現(xiàn)。

  • BestAvailableRule: 選擇最小請求數(shù)。
  • ClientConfigEnabledRoundRobinRule:輪詢。
  • RandornRule: 隨機(jī)選擇一個(gè)server。
  • RoundRobinRule: 輪詢選擇server。
  • RetryRule: 根據(jù)輪詢的方式重試。
  • ZoneAvoidanceRule:根據(jù)server的zone區(qū)域和可用性來輪詢選擇。
  • WeightedResponseTirneRule: 根據(jù)響應(yīng)時(shí)間去分配一個(gè)weight,weight越低,被選擇的可能性就越低。

??綜上所述,Ribbon的負(fù)載均衡,主要通過LoadBalancerClient來實(shí)現(xiàn)的,而LoadBalancerClient具體交給了ILoadBalancer來處理,ILoadBalancer通過配置IRule等信息,并向EurekaClient獲取注冊列表的信息,得到注冊列表后,ILoadBalancer根據(jù)IRule的策略進(jìn)行負(fù)載均衡。
??RestTemplate 被@LoadBalance注解后,能使用負(fù)載均衡,主要是維護(hù)了一個(gè)被@LoadBalance注解的RestTemplate列表,并給列表中的RestTemplate添加攔截器,進(jìn)而交給負(fù)載均衡器去處理。

總結(jié):本章節(jié)學(xué)習(xí)了Ribbon負(fù)載均衡搭配RestTemplate實(shí)現(xiàn)的方式,通過學(xué)習(xí)Ribbon的源碼,深入了解了Ribbon的實(shí)現(xiàn)原理和方式,使自己受益匪淺。下一章學(xué)習(xí)聲明式調(diào)用Feign的有關(guān)內(nèi)容。

源代碼:https://github.com/Cheerman/macro-service.git

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,578評論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,701評論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,691評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,974評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,694評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,026評論 1 329
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,015評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,193評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,719評論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,442評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,668評論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,151評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,846評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,255評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,592評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,394評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內(nèi)容