深刻淺出 Spring Cloud 之 Eureka

什麼是 Eureka

Eureka is a REST (Representational State Transfer) based service that is primarily used in the AWS cloud for locating services for the purpose of load balancing and failover of middle-tier servers. We call this service, the Eureka Server. Eureka also comes with a Java-based client component,the Eureka Client, which makes interactions with the service much easier. The client also has a built-in load balancer that does basic round-robin load balancing. At Netflix, a much more sophisticated load balancer wraps Eureka to provide weighted load balancing based on several factors like traffic, resource usage, error conditions etc to provide superior resiliency.java

這是 Netflix 官方的說明,若是英文不是很熟練的能夠對照着下面的翻譯看。web

譯:Eureka是基於REST(表明性狀態轉移)的服務,主要在AWS雲中用於定位服務,以實現負載均衡和中間層服務器的故障轉移。咱們稱此服務爲Eureka服務器。Eureka還帶有一個基於Java的客戶端組件Eureka Client,它使與服務的交互變得更加容易。客戶端還具備一個內置的負載平衡器,能夠執行基本的循環負載平衡。在Netflix,更復雜的負載均衡器將Eureka包裝起來,以基於流量,資源使用,錯誤條件等多種因素提供加權負載均衡,以提供出色的彈性。spring

簡而言之, Eureka 就是 Netflix服務發現框架json

服務發現:其實就是一個「中介」,整個過程當中有三個角色:服務提供者(賣房子出租房子的)、服務消費者(租客買主)、服務中介(房屋中介)。緩存

服務提供者: 就是提供一些本身可以執行的一些服務給外界。服務器

服務消費者: 就是須要使用一些服務的「用戶」。網絡

服務中介: 其實就是服務提供者和服務消費者之間的「橋樑」,服務提供者能夠把本身註冊到服務中介那裏,而服務消費者如須要消費一些服務(使用一些功能)就能夠在服務中介中尋找註冊在服務中介的服務提供者。架構

能夠充當服務發現的組件有不少:ZookeeperConsulEureka 等。併發

Eureka 某些基礎概念

  • 服務註冊 Register:當 Eureka 客戶端向 Eureka Server 註冊時,它提供自身的元數據,好比IP地址、端口,運行情況指示符URL,主頁等。app

  • 服務續約 RenewEureka 客戶會每隔30秒(默認狀況下)發送一次心跳來續約。 經過續約來告知 Eureka ServerEureka 客戶仍然存在,沒有出現問題。 正常狀況下,若是 Eureka Server 在90秒沒有收到 Eureka 客戶的續約,它會將實例從其註冊表中刪除。

  • 獲取註冊列表信息 Fetch RegistriesEureka 客戶端從服務器獲取註冊表信息,並將其緩存在本地。客戶端會使用該信息查找其餘服務,從而進行遠程調用。該註冊列表信息按期(每30秒鐘)更新一次。每次返回註冊列表信息可能與 Eureka 客戶端的緩存信息不一樣, Eureka 客戶端自動處理。若是因爲某種緣由致使註冊列表信息不能及時匹配,Eureka 客戶端則會從新獲取整個註冊表信息。 Eureka 服務器緩存註冊列表信息,整個註冊表以及每一個應用程序的信息進行了壓縮,壓縮內容和沒有壓縮的內容徹底相同。Eureka 客戶端和 Eureka 服務器可使用JSON / XML格式進行通信。在默認的狀況下 Eureka 客戶端使用壓縮 JSON 格式來獲取註冊列表的信息。

  • 服務下線 Cancel:Eureka客戶端在程序關閉時向Eureka服務器發送取消請求。 發送請求後,該客戶端實例信息將從服務器的實例註冊表中刪除。該下線請求不會自動完成,它須要調用如下內容:DiscoveryManager.getInstance().shutdownComponent();

  • 服務剔除 Eviction: 在默認的狀況下,當Eureka客戶端連續90秒(3個續約週期)沒有向Eureka服務器發送服務續約,即心跳,Eureka服務器會將該服務實例從服務註冊列表刪除,即服務剔除。

參考自 深刻理解Eureka

咱們能夠這麼理解,轉換爲現實中的問題就是 房屋中介問題

服務註冊: 房東或者房屋的主人 (提供者 Eureka Client Provider)在中介 (服務器 Eureka Server) 那裏登記房屋的信息,好比面積,價格,地段等等(元數據 metaData)。

服務續約: 房東或者房屋的主人 (提供者 Eureka Client Provider) 按期告訴中介 (服務器 Eureka Server) 個人房子還租或者還賣 (續約) ,中介 (服務器Eureka Server) 收到以後繼續保留房屋的信息。

獲取註冊列表信息:租客或者買主(消費者 Eureka Client Consumer) 去中介 (服務器 Eureka Server) 那裏獲取全部的房屋信息列表 (客戶端列表 Eureka Client List) ,並且租客或者買主爲了獲取最新的信息會按期向中介 (服務器 Eureka Server) 那裏獲取並更新本地列表。

服務下線:房東或者房屋的主人 (提供者 Eureka Client Provider) 告訴中介 (服務器 Eureka Server) 個人房子不賣了不租了,中介以後就將註冊的房屋信息從列表中剔除。

服務剔除:房東或者房屋的主人 (提供者 Eureka Client Provider) 會按期聯繫 中介 (服務器 Eureka Server) 告訴他個人房子還租還賣(續約),若是中介 (服務器 Eureka Server) 長時間沒收到提供者的信息,那麼中介會將他的房屋信息給下架(服務剔除)。

Eureka架構

Eureka架構圖
Eureka架構圖

藍色的 Eureka ServerEureka 服務器,這三個表明的是集羣,並且他們是去中心化的。

綠色的 Application ClientEureka 客戶端,其中能夠是消費者提供者,最左邊的就是典型的提供者,它須要向 Eureka 服務器註冊本身和發送心跳包進行續約,而其餘消費者則經過 Eureka 服務器來獲取提供者的信息以調用他們

Eureka 與 Zookeeper 對比

  • Eureka: 符合AP原則 爲了保證了可用性,Eureka 不會等待集羣全部節點都已同步信息完成,它會無時無刻提供服務。
  • Zookeeper: 符合CP原則 爲了保證一致性,在全部節點同步完成以前是阻塞狀態的。

Eureka經常使用配置

服務端配置

eureka:
  instance:
    hostname: xxxxx    # 主機名稱
    prefer-ip-address: true/false   # 註冊時顯示ip
  server:
    enableSelfPreservation: true   # 啓動自我保護
    renewalPercentThreshold: 0.85  # 續約配置百分比
複製代碼

還須要在spring boot啓動類中設置 @EnableEurekaServer 註解開啓 Eureka 服務

客戶端配置

eureka:
  client:
    register-with-eureka: true/false  # 是否向註冊中心註冊本身
    fetch-registry: # 指定此客戶端是否能獲取eureka註冊信息
    service-url:    # 暴露服務中心地址
      defaultZone: http://xxxxxx   # 默認配置
  instance:
    instance-id: xxxxx # 指定當前客戶端在註冊中心的名稱
複製代碼

用服務發現來查找服務(實戰)

1.使用Spring DiscoveryClient

這種方法使用的比較少,由於它其中不會使用 Ribbon 來作 負載均衡 而且開發人員編寫了過多的代碼,不利於開發和維護。其實本質就是經過 DiscoveryClient 去獲取全部實例的列表,而後從中獲取一個的 service url 再經過 RestTemplate 進行遠程調用。若是感興趣能夠去閱讀 《微服務實戰》的第四章。

下面兩種方式,若是你作 Eureka Server 集羣的話你會體驗到 Ribbon 帶來的 負載均衡 功能,由於這裏只是簡單的入門,若是讀者感興趣能夠本身嘗試一下。

2.使用啓用了 RestTemplate 的 Spring DiscoveryClient

首先咱們建立 Eureka Server

1. 建立 `spring boot` 項目而且勾選 `Eureka Server`
建立項目
建立項目
2. 編寫配置文件
server:
  port: 8000
eureka:
  instance:
    hostname: localhost   # 指定Eureka主機
  client:
    register-with-eureka: false  # 是否向服務中心註冊本身
    fetch-registry: false        # 是否可以獲取Eureka註冊信息
    service-url:    # 暴露本身的服務中心地址
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka
複製代碼
3. 開啓 `Eureka Server` 並啓動項目訪問
服務端註解
服務端註解

Eureka服務端網站效果
Eureka服務端網站效果

而後建立一個 ``Provider` 的客戶端

1. 新建項目並勾選 `web` 和 `Eureka Discovery Client`
建立客戶端項目
建立客戶端項目
2. 編寫客戶端配置文件
server:
  port: 9001
spring:
  application:
    name: provider-application     # 指定當前應用名 若是不配置 instance-id 那麼此項爲客戶端在註冊中心的名稱默認值
eureka:
  instance:
    instance-id: provider-application  # 指定當前客戶端在註冊中心的名稱
  client:
    service-url:
      defaultZone: http://localhost:8000/eureka  # 服務中心的地址 就是咱們在 Eureka Server 中配置的暴露地址
複製代碼
3. 編寫服務代碼並配置啓動類
// 直接在啓動類中
@SpringBootApplication
@RestController
public class EurekaProviderApplication {

    public static void main(String[] args) {
        SpringApplication.run(EurekaProviderApplication.class, args);
    }
    // 隨便編寫一個服務代碼 主要是給消費者調用的
    @RequestMapping(value = "/provider/{id}")
    public Map providerMethod(@PathVariable(value = "id")Integer id) {
        Map map = new HashMap<>();
        map.put(id.toString(), "Provider");
        return map;
    }

}
複製代碼

建立一個 `Consumer` 客戶端

1. 新建項目,和前面提供者同樣
2. 編寫消費者客戶端配置信息
server:
  port: 9002
spring:
  application:
    name: consumer-application
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8000/eureka
複製代碼
3. 編寫調用提供者代碼的邏輯並設置啓動類
@SpringBootApplication
@RestController
public class EurekaConsumerApplication {
    // 這個註解告訴spring cloud 建立一個支持 Ribbon 的 RestTemplate
    @LoadBalanced
    @Bean
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }

    @Autowired
    RestTemplate restTemplate;

    @RequestMapping(value = "/consumer/{id}")
    public Map consumerTest(@PathVariable(value = "id")Integer id) {
        // 經過帶有 Ribbon 功能的 RestTemplate 調用服務
        ResponseEntity<Map> responseEntity
                // 注意這裏的url是提供者的名稱
                = restTemplate.exchange("http://provider-application/provider/" + id, HttpMethod.GET,
                null, Map.class, id);
        return responseEntity.getBody();
    }

    public static void main(String[] args) {
        SpringApplication.run(EurekaConsumerApplication.class, args);
    }
}
複製代碼
4. 測試結果
測試結果
測試結果

3.使用`Open Feign`

NetflixOpen FeignSpring 棄用 RibbionRestTemplate 的替代方案。
你能夠在消費者端定義與服務端映射的接口,而後你就能夠經過調用消費者端的接口方法來調用提供者端的服務了(目標REST服務),除了編寫接口的定義,開發人員不須要編寫其餘調用服務的代碼,是如今經常使用的方案。

如今咱們只須要對上面的消費者項目進行簡單的修改就好了。

增長 `open Feign` 依賴並配置接口

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
複製代碼
@Service
// 使用@FeignClient表示服務 這裏的值是 提供者的名稱
@FeignClient("provider-application")
// 這裏的值是提供者相應的路徑
@RequestMapping("/provider")
public interface FeginService {
    // 這裏的路徑也要和提供者相同 參數也須要同樣
    @GetMapping("/{id}")
    Map providerMethod(@PathVariable(value = "id") int id);
}
複製代碼

建立 `Controller` 實現類

@RestController
public class FeignController {
    // 調用服務
    @Autowired
    private FeginService feginService;

    @RequestMapping(value = "/consumer/{id}")
    public Map consumerTest(@PathVariable(value = "id")Integer id) {
        return feginService.providerMethod(id);
    }
}
複製代碼

增長 `Feign` 配置

# 固然你能夠不進行配置 這裏不影響主要功能
feign:
  client:
    config:
      default:
        connectTimeout: 5000  # 指定Feign客戶端鏈接提供者的超時時限   取決於網絡環境
        readTimeout: 5000   # 指定Feign客戶端從請求到獲取到提供者給出的響應的超時時限  取決於業務邏輯運算時間
  compression:
    request:
      enabled: true   # 開啓對請求的壓縮
      mime-types: text/xml, application/xml
      min-request-size: 2048   # 指定啓用壓縮的最小文件大小
    response:
      enabled: true   # 開啓對響應的壓縮
複製代碼

配置啓動類

@SpringBootApplication
@EnableFeignClients   // 這裏須要使用 @EnableFeignClients 來啓用 Feign客戶端
public class EurekaConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaConsumerApplication.class, args);
    }
}
複製代碼

Eureka 的自我保護機制

Eureka Server 在某種特定狀況下 Eureka Server 不會剔除其註冊列表中的實例,那就是 Eureka 的自我保護時期。

何爲自我保護? 假想一下,當一個 server 節點出現了網絡分區等不可抗力緣由,那麼它會所以收不到 client 的續約心跳,若是網絡波動比較大,也就可能致使 server 由於一次網絡波動剔除了全部或者絕大部分 Client 。這種狀況是咱們不想看見的。

因此 Eureka 會有一種自我保護機制,默認是15分鐘內收到的續約低於原來的85%(這是上面的續約配置比例)那麼就會開啓 自我保護 。這階段 Eureka Server 不會剔除其列表中的實例,即便過了 90秒 也不會。

Eureka 部分源碼淺析

這裏作一部分的源碼分析,主要涉及 DiscoveryClientInstanceInfoReplicator 兩個類。

上面咱們講到 Spring 中的 DiscoveryClient 能夠用來做爲發現服務,只不過比較麻煩。

而在 Netflix 中也有一個 DiscoveryClient , 這個類的功能更增強大。咱們來看一下官方文檔對它的描述。

The class that is instrumental for interactions with Eureka Server.

Eureka Client is responsible for a) Registering the instance with Eureka Server b) Renewalof the lease with Eureka Server c) Cancellation of the lease from Eureka Server during shutdown

大概意思就是這個類負責了 Eureka Client 的註冊,下線,剔除,更新,查詢實例列表等操做。

如今咱們來看一下這個類是如何構造的。

@Inject
DiscoveryClient(ApplicationInfoManager applicationInfoManager,
    EurekaClientConfig config, 
    AbstractDiscoveryClientOptionalArgs args,
    Provider<BackupRegistry> backupRegistryProvider, EndpointRandomizer endpointRandomizer){
        // 前面作一些校驗和預註冊處理
        .........省略代碼
        // 很重要 初始化定時任務
        initScheduledTasks();
        // 後面作一些其餘處理 好比時間日誌的打印
        .........省略代碼
}
複製代碼

能夠看到裏面最重要的是調用了 initScheduledTasks() 函數,而且主要的初始化還在這裏。

private void initScheduledTasks() {
    // 若是定義能夠獲取實例列表信息
    if (clientConfig.shouldFetchRegistry()) {
        // registry cache refresh timer
        // 這裏默認獲取的30秒 也就是說這裏是
        // 客戶端每三十秒獲取一次實例列表信息的代碼實現
        int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
        int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
        scheduler.schedule(
                new TimedSupervisorTask(
                        "cacheRefresh",
                        scheduler,
                        cacheRefreshExecutor,
                        registryFetchIntervalSeconds,
                        TimeUnit.SECONDS,
                        expBackOffBound,
                        new CacheRefreshThread()
                ),
                registryFetchIntervalSeconds, TimeUnit.SECONDS);
    }
    // 若是是想服務中心註冊本身的 則註冊本身
    if (clientConfig.shouldRegisterWithEureka()) {
        // 這是續約
        int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
        int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();
        logger.info("Starting heartbeat executor: " + "renew interval is: {}", renewalIntervalInSecs);
        // 發送心跳續約 每10秒一次
        // Heartbeat timer
        scheduler.schedule(
                new TimedSupervisorTask(
                        "heartbeat",
                        scheduler,
                        heartbeatExecutor,
                        renewalIntervalInSecs,
                        TimeUnit.SECONDS,
                        expBackOffBound,
                        new HeartbeatThread()
                ),
                renewalIntervalInSecs, TimeUnit.SECONDS);

        // 這裏初始化了 實例信息複製器 很重要
        instanceInfoReplicator = new InstanceInfoReplicator(
                this,
                instanceInfo,
                clientConfig.getInstanceInfoReplicationIntervalSeconds(),
                2); // burstSize
        // 作一些狀態上的監聽和更新操做
        .......省略代碼
        // 這裏啓動了實例信息複製器
        instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
    } else {
        ... 打印註冊失敗日誌
    }
}
複製代碼

咱們能夠看到 initScheduledTasks() 主要就是初始化全部的定時任務,好比 多長時間獲取實例信息列表多長時間發送心跳包多長時間進行一次續約多長時間複製實例變化信息到eureka服務器 等等。

你可能有疑問,爲何是三十秒,幹嗎要這麼慢。喜歡閱讀文檔的同窗會發現,官方給了一些說明,而且他推薦使用默認值。

1.10. Why Is It so Slow to Register a Service?

Being an instance also involves a periodic heartbeat to the registry (through the client’s serviceUrl) with a default duration of 30 seconds. A service is not available for discovery by clients until the instance, the server, and the client all have the same metadata in their local cache (so it could take 3 heartbeats). You can change the period by setting eureka.instance.leaseRenewalIntervalInSeconds. Setting it to a value of less than 30 speeds up the process of getting clients connected to other services. In production, it is probably better to stick with the default, because of internal computations in the server that make assumptions about the lease renewal period.

翻譯:成爲實例還涉及到註冊表的按期心跳(經過客戶端的serviceUrl),默認持續時間爲30秒。
直到實例,服務器和客戶端在其本地緩存中都具備相同的元數據後,客戶端才能發現該服務(所以可能須要3個心跳)。
您能夠經過設置eureka.instance.leaseRenewalIntervalInSeconds來更改週期。
將其設置爲小於30的值可加快使客戶端鏈接到其餘服務的過程。
在生產中,最好使用默認值,由於服務器中的內部計算對租約續訂期進行了假設。

在最後還調用了 InstanceInfoReplicator 這個類的啓動方法,而且傳入了一個 初始複製實例變化信息到eureka服務器的時間間隔(40s),咱們繼續查看 InstanceInfoReplicator 這個類中的方法。

public void start(int initialDelayMs) {
    // CAS無鎖
    if (started.compareAndSet(falsetrue)) {
        // 初始化須要40秒
        instanceInfo.setIsDirty();  // for initial register
        Future next = scheduler.schedule(this, initialDelayMs, TimeUnit.SECONDS);
        scheduledPeriodicRef.set(next);
    }
}

public void run() {
    try {
        // 刷新實例信息
        discoveryClient.refreshInstanceInfo();
        Long dirtyTimestamp = instanceInfo.isDirtyWithTime();
        if (dirtyTimestamp != null) {
            // 最終仍是會調用到 DiscoveryClient 的註冊方法
            discoveryClient.register();
            instanceInfo.unsetIsDirty(dirtyTimestamp);
        }
    } catch (Throwable t) {
        logger.warn("There was a problem with the instance info replicator", t);
    } finally {
        Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);
        scheduledPeriodicRef.set(next);
    }
}
複製代碼
boolean register() throws Throwable {
    logger.info(PREFIX + "{}: registering service...", appPathIdentifier);
    EurekaHttpResponse<Void> httpResponse;
    try {
        // 深刻裏面會調用到 AbstractJerseyEurekaHttpClient 的 註冊方法
        // 其實裏面就是調用到 服務端給客戶端暴露出來的 註冊接口
        httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
    } catch (Exception e) {
        logger.warn(PREFIX + "{} - registration failed {}", appPathIdentifier, e.getMessage(), e);
        throw e;
    }
    if (logger.isInfoEnabled()) {
        logger.info(PREFIX + "{} - registration status: {}", appPathIdentifier, httpResponse.getStatusCode());
    }
    return httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode();
}
複製代碼

大體整理了一下上面的流程,若是不深刻其實很簡單。

註冊流程
註冊流程

固然,上面我說到註冊方法其實就是經過 RestHttp 最終調用 Eureka Server 暴露出來的註冊接口,那麼這個註冊接口在哪呢?

就在 ApplicationResource 中的 addInstance()

// 這裏是暴露了接口
@POST
@Consumes({"application/json""application/xml"})
public Response addInstance(InstanceInfo info,
                            @HeaderParam(PeerEurekaNode.HEADER_REPLICATION)
 String isReplication) 
{
    // 作一些日誌和校驗
    .....省略代碼
    // 到註冊中心去註冊
    registry.register(info, "true".equals(isReplication));
    // 返回 204 即無內容的成功
    return Response.status(204).build();  // 204 to be backwards compatible
}
複製代碼

主要還在這個 register 方法中,最終調用的是 PeerAwareInstanceImpl 類中的註冊方法。

@Override
public void register(final InstanceInfo info, final boolean isReplication) {
    int leaseDuration = Lease.DEFAULT_DURATION_IN_SECS;
    if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) {
        leaseDuration = info.getLeaseInfo().getDurationInSecs();
    }
    // 主要在這裏
    super.register(info, leaseDuration, isReplication);
    // 給同輩進行復制 這其實就是各個節點之間的同步呀
    replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);
}
複製代碼

這裏的 register 方法仍是調用到 AbstractInstanceRegistry 類中的註冊方法,核心主要都在這裏。

首先我提一嘴,Lease 其中是租約對象,其中裝配了實例信息這東西(源碼裏面是泛型 holder)。

你能夠簡單理解爲在租房合同中裏面記錄着房屋的基本信息。
複製代碼
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
    try {
        read.lock();
        // 這玩意不就有點像註冊中心麼
        // 跟spring的IOC有點類似呀
        // 這個registry 是一個 併發hashMap 裏面存儲了許多實例信息
        // 而後 EurekaServer 經過 註冊的實例的應用名去獲取這個map中獲取
        // 若是獲取到了 說明已經存在了 只須要把它取出來作更新就行
        // 不然則新建一個 並作更新
        Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName());
        REGISTER.increment(isReplication);
        if (gMap == null) {
           。。。省略代碼 建立新的gmap
        }
        .......省略一大堆代碼 簡單理解也就是更新
        // 建立租約
        Lease<InstanceInfo> lease = new Lease<InstanceInfo>(registrant, leaseDuration);
        if (existingLease != null) {
            lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
        }
        // 這也是更新、、
        gMap.put(registrant.getId(), lease);
        // 添加到註冊隊列
        synchronized (recentRegisteredQueue) {
            recentRegisteredQueue.add(new Pair<Long, String>(
                    System.currentTimeMillis(),
                    registrant.getAppName() + "(" + registrant.getId() + ")"));
        }
        // 進行已覆蓋狀態的初始狀態轉移
        // 後面涉及到覆蓋狀態了 不用管先
        。。。省略一大堆代碼
    } finally {
        read.unlock();
    }
}
複製代碼

嗯,我省略了不少代碼,若是你感興趣能夠去閱讀源碼並深刻了解原理,這裏我只作簡單分析。

感受和 Spring 中的 IOC 註冊很像,就是註冊到容器中去,這裏只不過換了個名而已,也就是將實例信息封裝成合約而後註冊到統一的 Map 中(註冊中心)。

總結

計算機世界有些東西若是能把它聯想到現實世界的例子那麼就真的很好理解了,好比熔斷,負載均衡等等,甚至能夠這麼說,計算機不少東西的解決方案都是現實世界給的靈感,不知道我舉的房產中介的例子是否能讓 Eureka 變得通俗易懂,不過也感謝你們閱讀。

由於最近在忙其餘的事情,寫文章的時間比較少,後面會加緊ヾ(◍°∇°◍)ノ゙。

相關文章
相關標籤/搜索