1. 程式人生 > 程式設計 >深入淺出 Spring Cloud 之 Eureka

深入淺出 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,34); font-weight: bold;">

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.

這是 Netflix 官方的說明,如果英文不是很熟練的可以對照著下面的翻譯看。

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

簡而言之, Eureka 就是 Netflix

服務發現框架

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

服務提供者: 就是提供一些自己能夠執行的一些服務給外界。

服務消費者: 就是需要使用一些服務的“使用者”。

服務中介: 其實就是服務提供者和服務消費者之間的“橋樑”,服務提供者可以把自己註冊到服務中介那裡,而服務消費者如需要消費一些服務(使用一些功能)就可以在服務中介中尋找註冊在服務中介的服務提供者。

可以充當服務發現的元件有很多:ZookeeperConsulEureka 等。

Eureka 某些基礎概念

  • 服務註冊 Register:當 Eureka 客戶端向 Eureka Server 註冊時,它提供自身的元資料,比如IP地址、埠,執行狀況指示符URL,主頁等。

  • 服務續約 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. 編寫呼叫提供者程式碼的邏輯並設定啟動類
EurekaConsumerApplication {
    // 這個註解告訴spring cloud 建立一個支援 Ribbon 的 RestTemplate
    @LoadBalanced
    @Bean
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }

    @Autowired
    RestTemplate restTemplate;

    "/consumer/{id}")
    consumerTest"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();
    }

    (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")
interface FeginService {
    // 這裡的路徑也要和提供者相同 引數也需要一樣
    @GetMapping("/{id}")
    Map "id") int id);
}
複製程式碼

建立 `Controller` 實現類

FeignController {
    // 呼叫服務
    @Autowired
    private FeginService feginService;

    "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   # 開啟對響應的壓縮
複製程式碼

配置啟動類

@EnableFeignClients   // 這裡需要使用 @EnableFeignClients 來啟用 Feign客戶端
EurekaConsumerApplication {
    複製程式碼

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 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(
                "heartbeat",
                        heartbeatExecutor,
                        renewalIntervalInSecs,117); word-wrap: inherit !important; word-break: inherit !important;" class="hljs-keyword">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 這個類中的方法。

start(int initialDelayMs) {
    // CAS無鎖
    if (started.compareAndSet(falsetrue)) {
        // 初始化需要40秒
        instanceInfo.setIsDirty();  // for initial register
        Future next = scheduler.schedule(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(複製程式碼
boolean register() throws Throwable {
    logger.info(PREFIX + "{}: registering service...", appPathIdentifier);
    EurekaHttpResponse<Void> httpResponse;
    // 深入裡面會呼叫到 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: {}", httpResponse.getStatusCode());
    }
    return httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode();
}
複製程式碼

大致整理了一下上面的流程,如果不深入其實很簡單。

註冊流程
註冊流程

當然,上面我說到註冊方法其實就是通過 RestHttp 最終呼叫 Eureka Server 暴露出來的註冊介面,那麼這個註冊介面在哪呢?

就在 ApplicationResource 中的 addInstance()

// 這裡是暴露了介面
@POST
@Consumes({"application/json",112); word-wrap: inherit !important; word-break: inherit !important;" class="hljs-string">"application/xml"})
public Response addInstance(InstanceInfo info,
                            @HeaderParam(PeerEurekaNode.HEADER_REPLICATION)
 String isReplication) {
    // 做一些日誌和校驗
    .....省略程式碼
    // 到註冊中心去註冊
    registry.register(info,112); word-wrap: inherit !important; word-break: inherit !important;" class="hljs-string">"true".equals(isReplication));
    // 返回 204 即無內容的成功
    return Response.status(204).build();  // 204 to be backwards compatible
}
複製程式碼

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

@Override
final InstanceInfo info,117); word-wrap: inherit !important; word-break: inherit !important;" class="hljs-keyword">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, isReplication);
}
複製程式碼

這裡的 register 方法還是呼叫到 AbstractInstanceRegistry 類中的註冊方法,核心主要都在這裡。

首先我提一嘴,Lease 其中是租約物件,其中裝配了例項資訊這東西(原始碼裡面是泛型 holder)。

你可以簡單理解為在租房合同中裡面記錄著房屋的基本資訊。
複製程式碼
(InstanceInfo registrant,117); word-wrap: inherit !important; word-break: inherit !important;" class="hljs-keyword">int leaseDuration,117); word-wrap: inherit !important; word-break: inherit !important;" class="hljs-keyword">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();
    }
}
複製程式碼

嗯,我省略了很多程式碼,如果你感興趣可以去閱讀原始碼並深入瞭解原理,這裡我只做簡單分析。

感覺和 IOC 註冊很像,就是註冊到容器中去,這裡只不過換了個名而已,也就是將例項資訊封裝成合約然後註冊到統一的 Map 中(註冊中心)。

總結

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

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