朱曄和你聊Spring系列S1E8:湊活著用的Spring Cloud(含一個實際業務貫穿所有元件的完整例子)

powerzhuye發表於2018-10-12

本文會以一個簡單而完整的業務來闡述Spring Cloud Finchley.RELEASE版本常用元件的使用。如下圖所示,本文會覆蓋的元件有:

  1. Spring Cloud Netflix Zuul閘道器伺服器
  2. Spring Cloud Netflix Eureka發現伺服器
  3. Spring Cloud Netflix Turbine斷路器監控
  4. Spring Cloud Sleuth + Zipkin服務呼叫監控
  5. Sping Cloud Stream + RabbitMQ做非同步訊息
  6. Spring Data JPA做資料訪問

本文的例子使用的依賴版本是:

  1. Spring Cloud - Finchley.RELEASE
  2. Spring Data - Lovelace-RELEASE
  3. Spring Cloud Stream - Fishtown.M3
  4. Spring Boot - 2.0.5.RELEASE

各項元件詳細使用請參見官網,Spring元件版本變化差異較大,網上程式碼複製貼上不一定能夠適用,最最好的資料來源只有官網+閱讀原始碼,直接給出地址方便你閱讀本文的時候閱讀官網的文件:

  1. 全鏈路監控:cloud.spring.io/spring-clou…
  2. 服務發現、閘道器、斷路器:cloud.spring.io/spring-clou…
  3. 服務呼叫:cloud.spring.io/spring-clou…
  4. 非同步訊息:docs.spring.io/spring-clou…
  5. 資料訪問:docs.spring.io/spring-data…

如下貼出所有基礎元件(除資料庫)和業務元件的架構圖,箭頭代表呼叫關係(實現是業務服務呼叫、虛線是基礎服務呼叫),藍色框代表基礎元件(伺服器)

朱曄和你聊Spring系列S1E8:湊活著用的Spring Cloud(含一個實際業務貫穿所有元件的完整例子)
這套架構中有關微服務以及訊息佇列的設計理念,請參考我之前的《朱曄的網際網路架構實戰心得》系列文章。下面,我們開始此次Spring Cloud之旅,Spring Cloud內容太多,本文分上下兩節,並且不會介紹太多理論性的東西,這些知識點可以介紹一本書,本文更多的意義是給出一個可行可用的實際的示例程式碼供你參考。

業務背景

本文我們會做一個相對實際的例子,來演示網際網路金融業務募集專案和放款的過程。三個表的表結構如下:

  1. project表存放了所有可募集的專案,包含專案名稱、總的募集金額、剩餘可以募集的金額、募集原因等等
  2. user表存放了所有的使用者,包括借款人和投資人,包含使用者的可用餘額和凍結餘額
  3. invest表存放了投資人投資的資訊,包含投資哪個project,投資了多少錢、借款人是誰
CREATE TABLE `invest` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `project_id` bigint(20) unsigned NOT NULL,
  `project_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `investor_id` bigint(20) unsigned NOT NULL,
  `investor_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `borrower_id` bigint(20) unsigned NOT NULL,
  `borrower_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `amount` decimal(10,2) unsigned NOT NULL,
  `status` tinyint(4) NOT NULL,
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
)
CREATE TABLE `project` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `reason` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `borrower_id` bigint(20) unsigned NOT NULL,
  `total_amount` decimal(10,0) unsigned NOT NULL,
  `remain_amount` decimal(10,0) unsigned NOT NULL,
  `status` tinyint(3) unsigned NOT NULL COMMENT '1-募集中 2-募集完成 3-已放款',
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`) USING BTREE
)
CREATE TABLE `user` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL,
  `available_balance` decimal(10,2) unsigned NOT NULL,
  `frozen_balance` decimal(10,2) unsigned NOT NULL,
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`) USING BTREE
)
複製程式碼

我們會搭建四個業務服務,其中三個是被其它服務同步呼叫的服務,一個是監聽MQ非同步處理訊息的服務:

  1. project service:用於處理project表做專案相關的查詢和操作
  2. user service:用於操作user表做使用者相關的查詢和操作
  3. invest service:用於操作invest表做投資相關的查詢和操作
  4. project listener:監聽MQ中有關專案變化的訊息,非同步處理專案的放款業務 整個業務流程其實就是初始化投資人、借款人和專案->專案投資(一個專案可以有多個投資人進行多筆投資)->專案全部募集完畢後把所有投資的錢放款給借款人的過程:
  5. 資料庫中有id=1和2的user為投資人1和2,初始可用餘額10000,凍結餘額0
  6. 資料庫中有id=3的user為借款人1,初始可用餘額0,凍結餘額0
  7. 資料庫中有id=1的project為一個可以投資的專案,投資額度為1000元,狀態為1募集中
  8. 初始情況下資料庫中的invest表沒記錄
  9. 使用者1通過invest service下單進行投資,每次投資100元投資5次,完成後invest表是5條記錄,然後使用者1的可用餘額為9500,凍結餘額為500,專案1的剩餘可以投資額度為500元(在整個過程中invest service會呼叫project service和user service查詢專案和使用者的資訊,以及更新專案和使用者的資金)
  10. 使用者2也是類似重複投資5次,完成後invest表應該是10條記錄,然後使用者2的可用餘額為9500,凍結餘額為500,專案1的剩餘可以投資額度為0元
  11. 此時,project service把project專案狀態改為2代表募集完成,然後傳送一條訊息到MQ伺服器
  12. project listener收到這條訊息後進行非同步的放款處理,呼叫user service逐一根據10比投資訂單的資訊,把所有投資人凍結的錢轉移到借款人,完成後投資人1和2可用餘額為9500,凍結餘額為0,借款人1可用餘額為1000,凍結餘額為0,隨後把專案狀態改為3放款完成 除了業務服務還有三個基礎服務(Ererka+Zuul+Turbine,Zipkin服務不在專案內,我們直接通過jar包啟動),整個專案結構如下:

朱曄和你聊Spring系列S1E8:湊活著用的Spring Cloud(含一個實際業務貫穿所有元件的完整例子)
整個業務包含了同步服務呼叫和非同步訊息處理,業務簡單而有代表性。但是在這裡我們並沒有演示Spring Cloud Config的使用,之前也提到過,國內開源的幾個配置中心比Cloud Config功能強大太多太多,目前Cloud Config實用性不好,在這裡就不納入演示了。 下面我們來逐一實現每一個元件和服務。

基礎設施搭建

我們先來新建一個父模組的pom:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>me.josephzhu</groupId>
    <artifactId>springcloud101</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>springcloud101-investservice-api</module>
        <module>springcloud101-investservice-server</module>
        <module>springcloud101-userservice-api</module>
        <module>springcloud101-userservice-server</module>
        <module>springcloud101-projectservice-api</module>
        <module>springcloud101-projectservice-server</module>
        <module>springcloud101-eureka-server</module>
        <module>springcloud101-zuul-server</module>
        <module>springcloud101-turbine-server</module>
        <module>springcloud101-projectservice-listener</module>
    </modules>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.5.RELEASE</version>
        <relativePath/>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Finchley.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.data</groupId>
                <artifactId>spring-data-releasetrain</artifactId>
                <version>Lovelace-RELEASE</version>
                <scope>import</scope>
                <type>pom</type>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-stream-dependencies</artifactId>
                <version>Fishtown.M3</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>


    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/libs-milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>

</project>
複製程式碼

Eureka

第一個要搭建的服務就是用於服務註冊的Eureka伺服器:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springcloud101</artifactId>
        <groupId>me.josephzhu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>spring101-eureka-server</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
    </dependencies>

</project>
複製程式碼

在resources資料夾下建立一個配置檔案application.yml(對於Spring Cloud專案由於配置實在是太多,為了模組感層次感強一點,這裡我們使用yml格式):

server:
  port: 8865

eureka:
  instance:
    hostname: localhost
  client:
    registry-fetch-interval-seconds: 5
    registerWithEureka: false
    fetchRegistry: false
    serviceUrl:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
  server:
    enable-self-preservation: true
    eviction-interval-timer-in-ms: 5000

spring:
  application:
    name: eurka-server
複製程式碼

在這裡,為了簡單期間,我們搭建的是一個Standalone的註冊服務(這裡,我們注意到Eureka有一個自我保護的開關,預設開啟,自我保護的意思是短時間大批節點和Eureka斷開的話,這個一般是網路問題,自我保護會開啟防止節點登出,在之後的測試過程中因為我們會經常重啟除錯服務,所以如果遇到節點不登出的問題可以暫時關閉這個功能),分配了8865埠(我們約定,基礎元件分配的埠以88開頭),隨後建立一個主程式檔案:

package me.josephzhu.springcloud101.eurekaserver;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {

    public static void main(String[] args) {
        SpringApplication.run( EurekaServerApplication.class, args );
    }
}
複製程式碼

對於搭建Spring Cloud的一些基礎元件的服務,往往就是三步,加依賴,加配置,加註解開關即可。

Zuul

Zuul是一個代理閘道器,具有路由和過濾兩大功能。並且直接能和Eureka註冊服務以及Sleuth鏈路監控整合,非常方便。在這裡,我們會同時演示兩個功能,我們會進行路由配置,使閘道器做一個反向代理,我們也會自定義一個前置過濾器做安全攔截。 首先,新建一個模組:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springcloud101</artifactId>
        <groupId>me.josephzhu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>springcloud101-zuul-server</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-sleuth</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zipkin</artifactId>
        </dependency>
    </dependencies>
</project>
複製程式碼

隨後加一個配置檔案:

server:
  port: 8866

spring:
  application:
    name: zuulserver
  main:
    allow-bean-definition-overriding: true
  zipkin:
      base-url: http://localhost:9411
  sleuth:
    feign:
      enabled: true
    sampler:
      probability: 1.0

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8865/eureka/
    registry-fetch-interval-seconds: 5

zuul:
  routes:
    invest:
      path: /invest/**
      serviceId: investservice
    user:
      path: /user/**
      serviceId: userservice
    project:
      path: /project/**
      serviceId: projectservice
    host:
      socket-timeout-millis: 60000
      connect-timeout-millis: 60000


management:
  endpoints:
    web:
      exposure:
        include: "*"

  endpoint:
    health:
      show-details: always
複製程式碼

Zuul閘道器我們這裡使用8866埠,這裡重點看一下路由的配置:

  1. 我們通過path來批量訪問請求的路徑,轉發到指定的serviceId
  2. 我們延長了傳輸和連線的超時時間,以便除錯時不超時 對於其它的配置,之後會進行解釋,下面我們通過程式設計實現一個前置過濾:
package me.josephzhu.springcloud101.zuul.server;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_DECORATION_FILTER_ORDER;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;

@Component
public class TokenFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return PRE_DECORATION_FILTER_ORDER - 1;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        String token = request.getParameter("token");
        if(token == null) {
            ctx.setSendZuulResponse(false);
            ctx.setResponseStatusCode(401);
            try {
                ctx.getResponse().setCharacterEncoding("UTF-8");
                ctx.getResponse().getWriter().write("禁止訪問");
            } catch (Exception e){}

            return null;
        }
        return null;
    }
}
複製程式碼

這個前置過濾演示了一個授權校驗的例子,檢查請求是否提供了token引數,如果沒有的話拒絕轉發服務,返回401響應狀態碼和錯誤資訊。 下面實現服務程式:

package me.josephzhu.springcloud101.zuul.server;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

@SpringBootApplication
@EnableZuulProxy
@EnableDiscoveryClient
public class ZuulServerApplication {

    public static void main(String[] args) {
        SpringApplication.run( ZuulServerApplication.class, args );
    }
}
複製程式碼

這裡解釋一下兩個註解:

  1. @EnableZuulProxy vs @EnableZuulServer:@EnableZuulProxy不但可以開啟Zuul伺服器,而且直接啟用更多的一些過濾器實現代理功能,而@EnableZuulServer只是啟動一個空白的Zuul,功能上是@EnableZuulProxy的子集。在這裡我們使用功能更強大的前者。
  2. @EnableDiscoveryClient vs @EnableEurekaClient:@EnableDiscoveryClient啟用的是發現服務的客戶端功能,支援各種註冊中心,@EnableEurekaClient只支援Eureka,功能也是一樣的。在這裡我們使用通用型更強的前者。

Turbine

Turbine用於彙總Hystrix服務斷路器監控流。Spring Cloud還提供了Hystrix的Dashboard,在這裡我們把這兩個功能集合在一個服務中執行。三部曲第一步依賴:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springcloud101</artifactId>
        <groupId>me.josephzhu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>springcloud101-turbine-server</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-turbine</artifactId>
        </dependency>
    </dependencies>

</project>
複製程式碼

第二步配置:

server:
  port: 8867

spring:
  application:
    name: turbineserver

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8865/eureka/

management:
  endpoints:
    web:
      exposure:
        include: "*"

  endpoint:
    health:
      show-details: always

turbine:
  aggregator:
    clusterConfig: default
  clusterNameExpression: "'default'"
  combine-host: true
  instanceUrlSuffix:
    default: actuator/hystrix.stream
  app-config: investservice,userservice,projectservice,projectservice-listener
複製程式碼

Turbine服務我們使用8867埠,這裡重點看一下turbine下面的配置項:

  1. instanceUrlSuffix配置了預設情況下每一個例項監控資料流的拉取地址
  2. app-config配置了所有需要監控的應用程式

我們來看一下文首的架構圖,這裡的Turbine其實是從各個配置的服務讀取監控流來彙總監控資料的,並不是像Zipkin這種由服務主動上報資料的方式。當然,我們還可以通過Turbine Stream的功能讓客戶端主動上報資料(通過訊息佇列),這裡就不詳細展開闡述了。下面是第三步:

package me.josephzhu.springcloud101.turbine.server;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;
import org.springframework.cloud.netflix.turbine.EnableTurbine;

@SpringBootApplication
@EnableDiscoveryClient
@EnableHystrix
@EnableHystrixDashboard
@EnableCircuitBreaker
@EnableTurbine
public class TurbineServerApplication {

    public static void main(String[] args) {
        SpringApplication.run( TurbineServerApplication.class, args );
    }
}
複製程式碼

之後會展示使用截圖。

Zipkin

Zipkin用於收集分散式追蹤資訊(同時扮演了服務端以及檢視後臺的角色),搭建方式請參見官網https://github.com/openzipkin/zipkin ,最簡單的方式是去https://dl.bintray.com/openzipkin/maven/io/zipkin/java/zipkin-server/直接下載jar包執行即可,在生產環境強烈建議配置後端儲存為ES或Mysql等等,這裡我們用於演示不進行任何其它配置了。我們直接啟動即可,預設執行在9411埠:

朱曄和你聊Spring系列S1E8:湊活著用的Spring Cloud(含一個實際業務貫穿所有元件的完整例子)
之後我們展示全鏈路監控的截圖。

使用者服務搭建

我們先來新建一個被依賴最多的業務服務,每一個服務分兩個專案,API定義和實現。Spring Cloud推薦API定義客戶端和服務端分別自己定義,不共享API介面,這樣耦合更低。我覺得網際網路專案注重快速開發,服務多並且往往用於內部呼叫,還是共享介面方式更切實際,在這裡我們演示的是介面共享方式的實踐。首先新建API專案的模組:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springcloud101</artifactId>
        <groupId>me.josephzhu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>springcloud101-userservice-api</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
    </dependencies>

</project>
複製程式碼

API專案不包含任何服務端實現,因此這裡只是引入了feign。在API介面專案中,我們一般定義兩個東西,一是服務介面定義,二是傳輸資料DTO定義。使用者DTO如下:

package me.josephzhu.springcloud101.userservice.api;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;
import java.util.Date;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Long id;
    private String name;
    private BigDecimal availableBalance;
    private BigDecimal frozenBalance;
    private Date createdAt;
}
複製程式碼

對於DTO我建議重新定義一份,不要直接使用資料庫的Entity,前者用於服務之間對外的資料傳輸,後者用於服務內部和資料庫進行互動,不能耦合在一起混為一談,雖然這多了一些轉化工作。 使用者服務如下:

package me.josephzhu.springcloud101.userservice.api;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.math.BigDecimal;

public interface UserService {
    @GetMapping("getUser")
    User getUser(@RequestParam("id") long id) throws Exception;
    @PostMapping("consumeMoney")
    BigDecimal consumeMoney(@RequestParam("investorId") long investorId,
                            @RequestParam("amount") BigDecimal amount) throws Exception;
    @PostMapping("lendpayMoney")
    BigDecimal lendpayMoney(@RequestParam("investorId") long investorId,
                            @RequestParam("borrowerId") long borrowerId,
                            @RequestParam("amount") BigDecimal amount) throws Exception;
}
複製程式碼

這裡定義了三個服務介面,在介紹服務實現的時候再來介紹這三個介面。 API模組是會被服務實現的服務端和其它服務使用的客戶端引用的,本身不具備獨立使用功能,所以也就沒有啟動類。 下面我們實現使用者服務服務端,首先是pom:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springcloud101</artifactId>
        <groupId>me.josephzhu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>springcloud101-userservice-server</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-sleuth</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zipkin</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.github.gavlyukovskiy</groupId>
            <artifactId>p6spy-spring-boot-starter</artifactId>
            <version>1.4.3</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.8.2</version>
        </dependency>

        <dependency>
            <groupId>me.josephzhu</groupId>
            <artifactId>springcloud101-userservice-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>
</project>
複製程式碼

由於我們的服務具有發現、監控、資料訪問、分散式鎖全功能,所以引入的依賴比較多一點:

  1. spring-cloud-starter-netflix-eureka-client用於服務發現和註冊
  2. spring-boot-starter-web用於服務承載(服務本質上是Spring MVC專案)
  3. spring-cloud-starter-openfeign用於宣告方式呼叫其它服務,使用者服務不會呼叫其它服務,但是為了保持所有服務端依賴統一,我們這裡也啟用這個依賴
  4. spring-boot-starter-actuator用於開啟監控和打點等等功能,見此係列文章前面一篇
  5. spring-cloud-starter-sleuth用於全鏈路追蹤基礎功能,開啟後可以在日誌中看到traceId等資訊,之後會演示
  6. spring-cloud-starter-zipkin用於全鏈路追蹤資料提交到zipkin
  7. spring-boot-starter-data-jpa用於資料訪問
  8. p6spy-spring-boot-starter是開源社群某人提供的一個包,用於顯示JDBC的事件,並且可以和全鏈路追蹤整合
  9. spring-cloud-starter-netflix-hystrix用於斷路器功能
  10. redisson-spring-boot-starter用於在專案中方便使用Redisson提供的基於Redis的鎖服務
  11. mysql-connector-java用於訪問mysql資料庫
  12. springcloud101-userservice-api是服務介面依賴

下面我們建立一個配置檔案,這次我們建立的是properties格式(只是為了說明更方便一點,網上有工具可以進行properties和yml的轉換):

  1. server.port=8761:服務的埠,業務服務我們以87開始。
  2. spring.application.name=userservice:服務名稱,以後其它服務都會使用這個名稱來引用到使用者服務
  3. spring.datasource.url=jdbc:mysql://localhost:3306/p2p?useSSL=false:JDBC連線字串
  4. spring.datasource.username=root:mysql帳號
  5. spring.datasource.password=root:mysql密碼
  6. spring.datasource.driver-class-name=com.mysql.jdbc.Driver:mysql驅動
  7. spring.zipkin.base-url=http://localhost:9411:zipkin服務端地址
  8. spring.sleuth.feign.enabled=true:啟用客戶端宣告方式訪問服務整合全鏈路監控
  9. spring.sleuth.sampler.probability=1.0:全鏈路監控抽樣概率100%(預設10%,丟資料太多不方便觀察結果)
  10. spring.jpa.show-sql=true:顯示JPA生成的SQL
  11. spring.jpa.hibernate.use-new-id-generator-mappings=false:禁用Hibernate ID生成對映表
  12. spring.redis.host=localhost:Redis地址
  13. spring.redis.pool=6379:Redis埠
  14. feign.hystrix.enabled=true:啟用宣告方式訪問服務的斷路器功能
  15. eureka.client.serviceUrl.defaultZone=http://localhost:8865/eureka/:註冊中心地址
  16. eureka.client.registry-fetch-interval-seconds=5:客戶端從註冊中心拉取服務資訊的間隔,我們為了測試方便,把這個時間設定了短一點
  17. management.endpoints.web.exposure.include=*:直接暴露actuator所有埠
  18. management.endpoint.health.show-details=always:展開顯示actuator的健康資訊

下面實現服務,首先定義資料庫實體:

package me.josephzhu.springcloud101.userservice.server;

import lombok.Data;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.*;
import java.math.BigDecimal;
import java.util.Date;

@Data
@Entity
@Table(name = "user")
@EntityListeners(AuditingEntityListener.class)
public class UserEntity {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private BigDecimal availableBalance;
    private BigDecimal frozenBalance;
    @CreatedDate
    private Date createdAt;
    @LastModifiedDate
    private Date updatedAt;
}
複製程式碼

沒有什麼特殊的,只是我們使用了@CreatedDate和@LastModifiedDate註解來生成記錄的建立和修改時間。下面是資料訪問資源庫,一鍵實現增刪改查:

package me.josephzhu.springcloud101.userservice.server;

import org.springframework.data.repository.CrudRepository;

public interface UserRepository extends CrudRepository<UserEntity, Long> {
}
複製程式碼

服務實現如下:

package me.josephzhu.springcloud101.userservice.server;

import me.josephzhu.springcloud101.userservice.api.User;
import me.josephzhu.springcloud101.userservice.api.UserService;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController;

import java.math.BigDecimal;

@RestController
public class UserServiceController implements UserService {

    @Autowired
    UserRepository userRepository;
    @Autowired
    RedissonClient redissonClient;

    @Override
    public User getUser(long id) {
        return userRepository.findById(id).map(userEntity ->
                User.builder()
                        .id(userEntity.getId())
                        .availableBalance(userEntity.getAvailableBalance())
                        .frozenBalance(userEntity.getFrozenBalance())
                        .name(userEntity.getName())
                        .createdAt(userEntity.getCreatedAt())
                        .build())
                .orElse(null);
    }

    @Override
    public BigDecimal consumeMoney(long investorId, BigDecimal amount) {
        RLock lock = redissonClient.getLock("User" + investorId);
        lock.lock();
        try {
            UserEntity user = userRepository.findById(investorId).orElse(null);
            if (user != null && user.getAvailableBalance().compareTo(amount)>=0) {
                user.setAvailableBalance(user.getAvailableBalance().subtract(amount));
                user.setFrozenBalance(user.getFrozenBalance().add(amount));
                userRepository.save(user);
                return amount;
            }
            return null;
        } finally {
            lock.unlock();
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public BigDecimal lendpayMoney(long investorId, long borrowerId, BigDecimal amount) throws Exception {
        RLock lock = redissonClient.getLock("User" + investorId);
        lock.lock();
        try {
            UserEntity investor = userRepository.findById(investorId).orElse(null);
            UserEntity borrower = userRepository.findById(borrowerId).orElse(null);

            if (investor != null && borrower != null && investor.getFrozenBalance().compareTo(amount) >= 0) {
                investor.setFrozenBalance(investor.getFrozenBalance().subtract(amount));
                userRepository.save(investor);
                borrower.setAvailableBalance(borrower.getAvailableBalance().add(amount));
                userRepository.save(borrower);
                return amount;
            }
            return null;
        } finally {
            lock.unlock();
        }
    }

}
複製程式碼

這裡實現了三個服務介面:

  1. getUser:根據使用者ID查詢使用者資訊
  2. consumeMoney:在使用者投資的時候需要為使用者扣款,這個時候需要把錢從可用餘額扣走,加入凍結餘額,為了避免併發問題(這還是很重要的一點,否則肯定會遇到BUG),我們引入了Redisson提供的基於Redis的分散式鎖
  3. lendpayMoney:在完成募集進行放款的時候把錢從投資人的凍結餘額轉到借款人的可用餘額,這裡同時啟用了分散式鎖和Spring事務

這裡我們看到由於我們的實現類直接實現了介面(共享Feign介面方式),在實現業務邏輯的時候不需要去考慮引數如何獲取,介面暴露地址等事情。 最後實現主程式:

package me.josephzhu.springcloud101.userservice.server;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@SpringBootApplication
@EnableDiscoveryClient
@EnableJpaAuditing
@EnableHystrix
@EnableCircuitBreaker
@Configuration
public class UserServiceApplication {
    @Bean
    RedissonClient redissonClient() {
        return Redisson.create();
    }
    public static void main(String[] args) {
        SpringApplication.run( UserServiceApplication.class, args );
    }
}
複製程式碼

所有服務我們都一視同仁,開啟服務發現、斷路器、斷路器監控等功能。這裡額外定義了一下Redisson的配置。

專案服務搭建

專案服務和使用者服務比較類似,唯一區別是專案服務會用到外部其它服務(使用者服務)。首先定義專案服務介面模組:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springcloud101</artifactId>
        <groupId>me.josephzhu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>springcloud101-projectservice-api</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
    </dependencies>

</project>
複製程式碼

介面中的DTO:

package me.josephzhu.springcloud101.projectservice.api;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;
import java.util.Date;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Project {
    private Long id;
    private BigDecimal totalAmount;
    private BigDecimal remainAmount;
    private String name;
    private String reason;
    private long borrowerId;
    private String borrowerName;
    private int status;
    private Date createdAt;
}
複製程式碼

以及服務定義:

package me.josephzhu.springcloud101.projectservice.api;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.math.BigDecimal;

public interface ProjectService {
    @GetMapping("getProject")
    Project getProject(@RequestParam("id") long id) throws Exception;
    @PostMapping("gotInvested")
    BigDecimal gotInvested(@RequestParam("id") long id,
                            @RequestParam("amount") BigDecimal amount) throws Exception;
    @PostMapping("lendpay")
    BigDecimal lendpay(@RequestParam("id") long id) throws Exception;
}
複製程式碼

不做過多說明了,直接來實現服務實現模組:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springcloud101</artifactId>
        <groupId>me.josephzhu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>springcloud101-projectservice-server</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-sleuth</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zipkin</artifactId>
        </dependency>
        <dependency>
            <groupId>com.github.gavlyukovskiy</groupId>
            <artifactId>p6spy-spring-boot-starter</artifactId>
            <version>1.4.3</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-stream</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
        </dependency>

        <dependency>
            <groupId>me.josephzhu</groupId>
            <artifactId>springcloud101-projectservice-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>me.josephzhu</groupId>
            <artifactId>springcloud101-userservice-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>
</project>
複製程式碼

依賴和使用者服務基本一致,只有幾個區別:

  1. 引入了Spring Cloud Stream相關依賴,回顧一下文首的架構圖,我們的專案服務在募集完成之後會發出一個MQ訊息,通知訊息關心著來進行專案的後續放款處理,這裡我們的專案服務扮演的是一個MQ訊息傳送者,也就是Spring Cloud Stream中的Source角色。
  2. 除了引入專案服務介面依賴還引入了使用者服務介面依賴,因為專案服務中會呼叫使用者服務。

下面是配置:

server:
  port: 8762

spring:
  application:
    name: projectservice
  cloud:
    stream:
      bindings:
        output:
          destination: zhuye
  datasource:
    url: jdbc:mysql://localhost:3306/p2p?useSSL=false
    username: root
    password: root
    driver-class-name: com.mysql.jdbc.Driver
  zipkin:
    base-url: http://localhost:9411
  sleuth:
    feign:
      enabled: true
    sampler:
      probability: 1.0
  jpa:
    show-sql: true
    hibernate:
      use-new-id-generator-mappings: false
feign:
  hystrix:
    enabled: true

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8865/eureka/
    registry-fetch-interval-seconds: 5


management:
  endpoints:
    web:
      exposure:
        include: "*"
  endpoint:
    health:
      show-details: always
複製程式碼

專案服務的配置直接把使用者服務的配置拿來改一下即可,有幾個需要改的地方:

  1. 對外埠地址
  2. 應用程式名稱
  3. Spring Cloud的配置,這裡定向了繫結的輸出到RabbitMQ名為zhuye的交換機上,這裡不對RabbitMQ做詳細說明了,之後會給出演示的圖

首先實現專案實體類:

package me.josephzhu.springcloud101.projectservice.server;

import lombok.Data;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.*;
import java.math.BigDecimal;
import java.util.Date;

@Data
@Entity
@Table(name = "project")
@EntityListeners(AuditingEntityListener.class)
public class ProjectEntity {
    @Id
    @GeneratedValue
    private Long id;
    private BigDecimal totalAmount;
    private BigDecimal remainAmount;
    private String name;
    private String reason;
    private long borrowerId;
    private int status;
    @CreatedDate
    private Date createdAt;
    @LastModifiedDate
    private Date updatedAt;
}
複製程式碼

然後是資料訪問增刪改查Repository:

package me.josephzhu.springcloud101.projectservice.server;

import org.springframework.data.repository.CrudRepository;

public interface ProjectRepository extends CrudRepository<ProjectEntity, Long> {
}
複製程式碼

然後是依賴的外部使用者服務:

package me.josephzhu.springcloud101.projectservice.server;

import me.josephzhu.springcloud101.userservice.api.User;
import me.josephzhu.springcloud101.userservice.api.UserService;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;

import java.math.BigDecimal;

@FeignClient(value = "userservice",fallback = RemoteUserService.Fallback.class)
public interface RemoteUserService extends UserService {
    @Component
    class Fallback implements RemoteUserService {

        @Override
        public User getUser(long id) throws Exception {
            return null;
        }

        @Override
        public BigDecimal consumeMoney(long id, BigDecimal amount) throws Exception {
            return null;
        }

        @Override
        public BigDecimal lendpayMoney(long investorId, long borrowerId, BigDecimal amount) throws Exception {
            return null;
        }
    }
}
複製程式碼

這裡我們需要宣告@Feign註解根據服務名稱來使用外部的使用者服務,此外,我們還定義了服務熔斷時的Fallback類,實現上我們給出了返回null的空實現。 最關鍵的服務實現如下:

package me.josephzhu.springcloud101.projectservice.server;

import lombok.extern.slf4j.Slf4j;
import me.josephzhu.springcloud101.projectservice.api.Project;
import me.josephzhu.springcloud101.projectservice.api.ProjectService;
import me.josephzhu.springcloud101.userservice.api.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.web.bind.annotation.RestController;

import java.math.BigDecimal;

@RestController
@Slf4j
@EnableBinding(Source.class)
public class ProjectServiceController implements ProjectService {

    @Autowired
    ProjectRepository projectRepository;
    @Autowired
    RemoteUserService remoteUserService;

    @Override
    public Project getProject(long id) throws Exception {
        ProjectEntity projectEntity = projectRepository.findById(id).orElse(null);
        if (projectEntity == null) return null;
        User borrower = remoteUserService.getUser(projectEntity.getBorrowerId());
        if (borrower == null) return null;

        return Project.builder()
                .id(projectEntity.getId())
                .borrowerId(borrower.getId())
                .borrowerName(borrower.getName())
                .name(projectEntity.getName())
                .reason(projectEntity.getReason())
                .status(projectEntity.getStatus())
                .totalAmount(projectEntity.getTotalAmount())
                .remainAmount(projectEntity.getRemainAmount())
                .createdAt(projectEntity.getCreatedAt())
                .build();
    }

    @Override
    public BigDecimal gotInvested(long id, BigDecimal amount) throws Exception {
        ProjectEntity projectEntity = projectRepository.findById(id).orElse(null);
        if (projectEntity != null && projectEntity.getRemainAmount().compareTo(amount)>=0) {
            projectEntity.setRemainAmount(projectEntity.getRemainAmount().subtract(amount));
            projectRepository.save(projectEntity);

            if (projectEntity.getRemainAmount().compareTo(new BigDecimal("0"))==0) {
                User borrower = remoteUserService.getUser(projectEntity.getBorrowerId());
                if (borrower != null) {
                    projectEntity.setStatus(2);
                    projectRepository.save(projectEntity);
                    projectStatusChanged(Project.builder()
                            .id(projectEntity.getId())
                            .borrowerId(borrower.getId())
                            .borrowerName(borrower.getName())
                            .name(projectEntity.getName())
                            .reason(projectEntity.getReason())
                            .status(projectEntity.getStatus())
                            .totalAmount(projectEntity.getTotalAmount())
                            .remainAmount(projectEntity.getRemainAmount())
                            .createdAt(projectEntity.getCreatedAt())
                            .build());
                }
                return amount;
            }
            return amount;
        }
        return null;
    }

    @Override
    public BigDecimal lendpay(long id) throws Exception {
        Thread.sleep(5000);
        ProjectEntity project = projectRepository.findById(id).orElse(null);
        if (project != null) {
            project.setStatus(3);
            projectRepository.save(project);
            return project.getTotalAmount();
        }
        return null;
    }

    @Autowired
    Source source;

    private void projectStatusChanged(Project project){
        if (project.getStatus() == 2)
        try {
            source.output().send(MessageBuilder.withPayload(project).build());
        } catch (Exception ex) {
            log.error("傳送MQ失敗", ex);
        }
    }
}
複製程式碼

三個方法的業務邏輯如下:

  1. getProject用於查詢專案資訊,在實現中我們會呼叫使用者服務來查詢借款人的資訊
  2. gotInvested用於在投資人投資後更新專案的募集餘額,當專案募集餘額為0的時候,我們把專案狀態改為2募集完成,然後傳送MQ訊息通知訊息訂閱者做後續非同步處理
  3. 使用Spring Cloud Stream傳送訊息非常簡單,這裡我們扮演的是Source角色(訊息來源),只要注入Source,然後構造一個Message呼叫source的output方法獲取MessageChannel發出去訊息即可
  4. lendpay用於在放款完成後更新專案狀態為3放款完成

最後定義啟動類:

package me.josephzhu.springcloud101.projectservice.server;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@EnableJpaAuditing
@EnableHystrix
@EnableCircuitBreaker
public class ProjectServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run( ProjectServiceApplication.class, args );
    }
}
複製程式碼

投資服務搭建

投資服務和前兩個服務也是類似的,只不過它更復雜點,會依賴使用者服務和專案服務。首先建立一個服務定義模組:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springcloud101</artifactId>
        <groupId>me.josephzhu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>springcloud101-investservice-api</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
    </dependencies>

</project>
複製程式碼

然後DTO:

package me.josephzhu.springcloud101.investservice.api;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;
import java.util.Date;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Invest {
    private Long id;
    private long investorId;
    private long borrowerId;
    private long projectId;
    private int status;
    private BigDecimal amount;
    private Date createdAt;
    private Date updatedAt;
}
複製程式碼

以及介面定義:

package me.josephzhu.springcloud101.investservice.api;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.math.BigDecimal;
import java.util.List;

public interface InvestService {
    @PostMapping("createInvest")
    Invest createOrder(@RequestParam("userId") long userId,
                     @RequestParam("projectId") long projectId,
                     @RequestParam("amount") BigDecimal amount) throws Exception;
    @GetMapping("getOrders")
    List<Invest> getOrders(@RequestParam("projectId") long projectId) throws Exception;
}
複製程式碼

實現了定義模組後來實現服務模組:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springcloud101</artifactId>
        <groupId>me.josephzhu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>springcloud101-investservice-server</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-sleuth</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zipkin</artifactId>
        </dependency>
        <dependency>
            <groupId>com.github.gavlyukovskiy</groupId>
            <artifactId>p6spy-spring-boot-starter</artifactId>
            <version>1.4.3</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>

        <dependency>
            <groupId>me.josephzhu</groupId>
            <artifactId>springcloud101-investservice-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>me.josephzhu</groupId>
            <artifactId>springcloud101-userservice-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>me.josephzhu</groupId>
            <artifactId>springcloud101-projectservice-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

</project>
複製程式碼

依賴使用和使用者服務基本類似,只是多了幾個外部服務介面的引入。 然後是配置:

server:
  port: 8763

spring:
  application:
    name: investservice
  datasource:
    url: jdbc:mysql://localhost:3306/p2p?useSSL=false
    username: root
    password: root
    driver-class-name: com.mysql.jdbc.Driver
  zipkin:
    base-url: http://localhost:9411
  sleuth:
    feign:
      enabled: true
    sampler:
      probability: 1.0
  jpa:
    show-sql: true
    hibernate:
      use-new-id-generator-mappings: false
feign:
  hystrix:
    enabled: true

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8865/eureka/
    registry-fetch-interval-seconds: 5


management:
  endpoints:
    web:
      exposure:
        include: "*"
  endpoint:
    health:
      show-details: always
複製程式碼

和使用者服務也是類似,只是修改了埠和程式名。 現在來建立資料實體:

package me.josephzhu.springcloud101.investservice.server;

import lombok.Data;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.*;
import java.math.BigDecimal;
import java.util.Date;

@Data
@Entity
@Table(name = "invest")
@EntityListeners(AuditingEntityListener.class)
public class InvestEntity {
    @Id
    @GeneratedValue
    private Long id;
    private long investorId;
    private long borrowerId;
    private long projectId;
    private String investorName;
    private String borrowerName;
    private String projectName;
    private BigDecimal amount;
    private int status;
    @CreatedDate
    private Date createdAt;
    @LastModifiedDate
    private Date updatedAt;
}
複製程式碼

資料訪問Repository:

package me.josephzhu.springcloud101.investservice.server;

import org.springframework.data.repository.CrudRepository;

import java.util.List;

public interface InvestRepository extends CrudRepository<InvestEntity, Long> {
    List<InvestEntity> findByProjectIdAndStatus(long projectId, int status);
}
複製程式碼

具備熔斷Fallback的使用者外部服務客戶端:

package me.josephzhu.springcloud101.investservice.server;

import lombok.extern.slf4j.Slf4j;
import me.josephzhu.springcloud101.userservice.api.User;
import me.josephzhu.springcloud101.userservice.api.UserService;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;

import java.math.BigDecimal;

@FeignClient(value = "userservice", fallback = RemoteUserService.Fallback.class)
public interface RemoteUserService extends UserService {
    @Component
    @Slf4j
    class Fallback implements RemoteUserService {

        @Override
        public User getUser(long id) throws Exception {
            log.warn("getUser fallback");
            return null;
        }

        @Override
        public BigDecimal consumeMoney(long id, BigDecimal amount) throws Exception {
            log.warn("consumeMoney fallback");
            return null;
        }

        @Override
        public BigDecimal lendpayMoney(long investorId, long borrowerId, BigDecimal amount) throws Exception {
            log.warn("lendpayMoney fallback");
            return null;
        }
    }
}
複製程式碼

專案服務訪問客戶端:

package me.josephzhu.springcloud101.investservice.server;

import me.josephzhu.springcloud101.projectservice.api.Project;
import me.josephzhu.springcloud101.projectservice.api.ProjectService;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;

import java.math.BigDecimal;

@FeignClient(value = "projectservice", fallback = RemoteProjectService.Fallback.class)
public interface RemoteProjectService extends ProjectService {
    @Component
    class Fallback implements RemoteProjectService {

        @Override
        public Project getProject(long id) throws Exception {
            return null;
        }

        @Override
        public BigDecimal gotInvested(long id, BigDecimal amount) throws Exception {
            return null;
        }

        @Override
        public BigDecimal lendpay(long id) throws Exception {
            return null;
        }
    }
}
複製程式碼

服務介面實現:

package me.josephzhu.springcloud101.investservice.server;

import lombok.extern.slf4j.Slf4j;
import me.josephzhu.springcloud101.investservice.api.Invest;
import me.josephzhu.springcloud101.investservice.api.InvestService;
import me.josephzhu.springcloud101.projectservice.api.Project;
import me.josephzhu.springcloud101.userservice.api.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController;

import java.math.BigDecimal;
import java.util.List;
import java.util.stream.Collectors;

@RestController
@Slf4j
public class InvestServiceController implements InvestService {
    @Autowired
    InvestRepository investRepository;
    @Autowired
    RemoteUserService remoteUserService;
    @Autowired
    RemoteProjectService remoteProjectService;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Invest createOrder(long userId, long projectId, BigDecimal amount) throws Exception {
        User investor = remoteUserService.getUser(userId);
        if (investor == null) throw new Exception("無效使用者ID");
        if (amount.compareTo(investor.getAvailableBalance()) > 0) throw new Exception("使用者餘額不足");

        Project project = remoteProjectService.getProject(projectId);
        if (project == null) throw new Exception("無效專案ID");
        if (amount.compareTo(project.getRemainAmount()) > 0) throw new Exception("專案餘額不足");
        if (project.getStatus() !=1) throw new Exception("專案不是募集中狀不能投資");

        InvestEntity investEntity = new InvestEntity();
        investEntity.setInvestorId(investor.getId());
        investEntity.setInvestorName(investor.getName());
        investEntity.setAmount(amount);
        investEntity.setBorrowerId(project.getBorrowerId());
        investEntity.setBorrowerName(project.getBorrowerName());
        investEntity.setProjectId(project.getId());
        investEntity.setProjectName(project.getName());
        investEntity.setStatus(1);
        investRepository.save(investEntity);

        if (remoteUserService.consumeMoney(userId, amount) == null) throw new Exception("使用者消費失敗");
        if (remoteProjectService.gotInvested(projectId, amount) == null) throw new Exception("專案投資失敗");

        return Invest.builder()
                .id(investEntity.getId())
                .amount(investEntity.getAmount())
                .borrowerId(investEntity.getBorrowerId())
                .investorId(investEntity.getInvestorId())
                .projectId(investEntity.getProjectId())
                .status(investEntity.getStatus())
                .createdAt(investEntity.getCreatedAt())
                .updatedAt(investEntity.getUpdatedAt())
                .build();
    }

    @Override
    public List<Invest> getOrders(long projectId) throws Exception {
        return investRepository.findByProjectIdAndStatus(projectId,1).stream()
                .map(investEntity -> Invest.builder()
                        .id(investEntity.getId())
                        .amount(investEntity.getAmount())
                        .borrowerId(investEntity.getBorrowerId())
                        .investorId(investEntity.getInvestorId())
                        .projectId(investEntity.getProjectId())
                        .status(investEntity.getStatus())
                        .createdAt(investEntity.getCreatedAt())
                        .updatedAt(investEntity.getUpdatedAt())
                        .build())
                .collect(Collectors.toList());
    }
}
複製程式碼

投資服務定義了兩個介面:

  1. createOrder:先後呼叫外部服務獲取投資人和專案資訊,然後插入投資記錄,然後呼叫使用者服務去更新投資人的凍結賬戶餘額,呼叫專案服務去更新專案餘額。
  2. getOrders:根據專案ID查詢所有狀態為1的投資訂單(在放款操作的時候需要用到)。

啟動類如下:

package me.josephzhu.springcloud101.investservice.server;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.ApplicationContext;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

import java.util.Arrays;
import java.util.stream.Stream;

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@EnableJpaAuditing
@EnableHystrix
@EnableCircuitBreaker
public class InvestServiceApplication implements CommandLineRunner{
    public static void main(String[] args) {
        SpringApplication.run( InvestServiceApplication.class, args );
    }

    @Autowired
    ApplicationContext applicationContext;

    @Override
    public void run(String... args) throws Exception {
        System.out.println("所有註解:");
        Stream.of(applicationContext.getBeanDefinitionNames())
                .map(applicationContext::getBean)
                .map(bean-> Arrays.asList(bean.getClass().getAnnotations()))
                .flatMap(a->a.stream())
                .filter(annotation -> annotation.annotationType().getName().startsWith("org.springframework.cloud"))
                .forEach(System.out::println);
    }
}
複製程式碼

和其它幾個服務一樣沒啥特殊的,只是這裡多了個Runner,這個是我自己玩的,想輸出一下Spring中的Bean上定義的和Spring Cloud相關的註解,和業務沒有關係。

專案監聽服務搭建

最後一個服務是監聽MQ進行處理的專案(訊息)監聽服務。這個服務其實是可以和其它服務進行合併的,但是為了清晰我們還是分開做了一個模組:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springcloud101</artifactId>
        <groupId>me.josephzhu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>springcloud101-projectservice-listener</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-sleuth</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zipkin</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-stream</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.9.7</version>
        </dependency>

        <dependency>
            <groupId>me.josephzhu</groupId>
            <artifactId>springcloud101-userservice-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>me.josephzhu</groupId>
            <artifactId>springcloud101-projectservice-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>me.josephzhu</groupId>
            <artifactId>springcloud101-investservice-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>
</project>
複製程式碼

引入了Stream相關依賴,去掉了資料訪問相關依賴,因為這裡我們只會呼叫外部服務,服務本身不會進行資料訪問。 配置資訊如下:

server:
  port: 8764

spring:
  application:
    name: projectservice-listener
  cloud:
    stream:
      bindings:
        input:
          destination: zhuye
  zipkin:
    base-url: http://localhost:9411
  sleuth:
    feign:
      enabled: true
    sampler:
      probability: 1.0

feign:
  hystrix:
    enabled: true

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8865/eureka/
    registry-fetch-interval-seconds: 5

management:
  endpoints:
    web:
      exposure:
        include: "*"
  endpoint:
    health:
      show-details: always
複製程式碼

唯一值得注意的是,這裡我們定義了Spring Cloud Input繫結到也是之前定義的Output的那個交換機zhuye上面,實現了MQ傳送接受資料連通。 下面我們定義了三個外部服務客戶端(程式碼和其它地方使用的一模一樣。 投資服務:

package me.josephzhu.springcloud101.projectservice.listener;

import me.josephzhu.springcloud101.investservice.api.InvestService;
import org.springframework.cloud.openfeign.FeignClient;

@FeignClient(value = "investservice")
public interface RemoteInvestService extends InvestService {
}
複製程式碼

使用者服務:

package me.josephzhu.springcloud101.projectservice.listener;

import lombok.extern.slf4j.Slf4j;
import me.josephzhu.springcloud101.userservice.api.User;
import me.josephzhu.springcloud101.userservice.api.UserService;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;

import java.math.BigDecimal;

@FeignClient(value = "userservice", fallback = RemoteUserService.Fallback.class)
public interface RemoteUserService extends UserService {
    @Component
    @Slf4j
    class Fallback implements RemoteUserService {

        @Override
        public User getUser(long id) throws Exception {
            log.warn("getUser fallback");
            return null;
        }

        @Override
        public BigDecimal consumeMoney(long id, BigDecimal amount) throws Exception {
            log.warn("consumeMoney fallback");
            return null;
        }

        @Override
        public BigDecimal lendpayMoney(long investorId, long borrowerId, BigDecimal amount) throws Exception {
            log.warn("lendpayMoney fallback");
            return null;
        }
    }
}
複製程式碼

專案服務:

package me.josephzhu.springcloud101.projectservice.listener;

import me.josephzhu.springcloud101.projectservice.api.Project;
import me.josephzhu.springcloud101.projectservice.api.ProjectService;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;

import java.math.BigDecimal;

@FeignClient(value = "projectservice", fallback = RemoteProjectService.Fallback.class)
public interface RemoteProjectService extends ProjectService {
    @Component
    class Fallback implements RemoteProjectService {

        @Override
        public Project getProject(long id) throws Exception {
            return null;
        }

        @Override
        public BigDecimal gotInvested(long id, BigDecimal amount) throws Exception {
            return null;
        }

        @Override
        public BigDecimal lendpay(long id) throws Exception {
            return null;
        }
    }
}
複製程式碼

監聽程式實現如下:

package me.josephzhu.springcloud101.projectservice.listener;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import me.josephzhu.springcloud101.projectservice.api.Project;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.stereotype.Component;

@Component
@EnableBinding(Sink.class)
@Slf4j
public class ProjectServiceListener {
    @Autowired
    RemoteUserService remoteUserService;
    @Autowired
    RemoteProjectService remoteProjectService;
    @Autowired
    RemoteInvestService remoteInvestService;

    static ObjectMapper objectMapper = new ObjectMapper();

    @StreamListener(Sink.INPUT)
    public void handleProject(Project project) {
        try {
            log.info("收到訊息: " + project);
            if (project.getStatus() == 2) {
                remoteInvestService.getOrders(project.getId())
                        .forEach(invest -> {
                            try {
                                remoteUserService.lendpayMoney(invest.getInvestorId(), invest.getBorrowerId(), invest.getAmount());
                            } catch (Exception ex) {
                                try {
                                    log.error("處理放款的時候遇到異常:" + objectMapper.writeValueAsString(invest), ex);
                                } catch (JsonProcessingException e) {

                                }
                            }
                        });
                remoteProjectService.lendpay(project.getId());
            }
        } catch (Exception ex) {
            log.error("處理訊息出現異常",ex);
        }
    }
}
複製程式碼

我們通過@StreamListener方便實現訊息監聽,在收聽到Project訊息(其實最標準的應該為MQ訊息定義一個XXNotification的DTO,比如ProjectStatusChangedNotification,這裡我們偷懶直接使用了Project這個DTO)後:

  1. 判斷專案狀態是不是2募集完成,如果是的話
  2. 首先,呼叫投資服務getOrders介面獲取專案所有投資資訊
  3. 然後,逐一呼叫使用者服務lendpayMoney介面為每一筆投資進行餘額轉移(把投資人凍結的錢解凍,轉給借款人可用餘額)
  4. 最後,呼叫專案服務lendpay介面更新專案狀態為放款完成

這裡可以看到,雖然lendpay介面耗時很久(裡面休眠5秒)但是由於處理是非同步的,不會影響投資訂單這個操作,這是通過MQ進行非同步處理的應用點之一。

演示和測試

激動人心的時刻來了,我們來通過演示看一下我們這套Spring Cloud微服務體系的功能。 先啟動Eureka,然後依次啟動所有的基礎服務,最後依次啟動所有的業務服務。 全部啟動後,訪問一下http://localhost:8865/來檢視Eureka註冊中心:

朱曄和你聊Spring系列S1E8:湊活著用的Spring Cloud(含一個實際業務貫穿所有元件的完整例子)
這裡可以看到所有服務已經註冊線上:

  1. 8866的Zuul
  2. 8867的Tubine
  3. 8761的使用者服務
  4. 8762的專案服務
  5. 8763的投資服務

訪問http://localhost:8761/getUser?id=1可以測試使用者服務:

朱曄和你聊Spring系列S1E8:湊活著用的Spring Cloud(含一個實際業務貫穿所有元件的完整例子)
訪問http://localhost:8762/getProject?id=2可以測試專案服務:
朱曄和你聊Spring系列S1E8:湊活著用的Spring Cloud(含一個實際業務貫穿所有元件的完整例子)
我們來初始化一下資料庫,預設有一個專案資訊:
朱曄和你聊Spring系列S1E8:湊活著用的Spring Cloud(含一個實際業務貫穿所有元件的完整例子)
還有兩個投資人和一個借款人:

朱曄和你聊Spring系列S1E8:湊活著用的Spring Cloud(含一個實際業務貫穿所有元件的完整例子)
現在來通過閘道器訪問http://localhost:8866/invest/createInvest投資服務(使用閘道器進行路由,我們配置的是匹配invest/**這個path路由到投資服務,直接訪問服務的時候無需提供invest字首)使用投資人1做一次投資:
朱曄和你聊Spring系列S1E8:湊活著用的Spring Cloud(含一個實際業務貫穿所有元件的完整例子)
在沒有提供token的時候會出現錯誤,加上token後訪問成功:
朱曄和你聊Spring系列S1E8:湊活著用的Spring Cloud(含一個實際業務貫穿所有元件的完整例子)
可以看到投資後投資人凍結賬戶為100,專案剩餘金額為900,多了一條投資記錄:
朱曄和你聊Spring系列S1E8:湊活著用的Spring Cloud(含一個實際業務貫穿所有元件的完整例子)
我們使用投資人1測試5次投資,使用投資人2測試5次投資,測試後可以看到專案狀態變為了3放款完成:
朱曄和你聊Spring系列S1E8:湊活著用的Spring Cloud(含一個實際業務貫穿所有元件的完整例子)
資料庫中有10條投資記錄:
朱曄和你聊Spring系列S1E8:湊活著用的Spring Cloud(含一個實際業務貫穿所有元件的完整例子)
兩個投資人的凍結餘額都為0,可用餘額分別少了500,借款人可用餘額多了1000,說明放款成功了?:
朱曄和你聊Spring系列S1E8:湊活著用的Spring Cloud(含一個實際業務貫穿所有元件的完整例子)
同時可以在ProjectListner的日誌中看到收到訊息的日誌:
朱曄和你聊Spring系列S1E8:湊活著用的Spring Cloud(含一個實際業務貫穿所有元件的完整例子)
我們可以訪問http://localhost:15672開啟RabbitMQ都是管理臺看一下我們那條訊息的情況:
朱曄和你聊Spring系列S1E8:湊活著用的Spring Cloud(含一個實際業務貫穿所有元件的完整例子)
可以看到在佇列中的確有一條訊息先收到然後不久後(大概是6秒後)得到了ack處理完畢。佇列繫結到了zhuye這個交換機上:
朱曄和你聊Spring系列S1E8:湊活著用的Spring Cloud(含一個實際業務貫穿所有元件的完整例子)
至此,我們已經演示了Zuul、Eureka和Stream,現在我們來看一下斷路器功能。 我們首先訪問http://localhost:8867/hystrix:
朱曄和你聊Spring系列S1E8:湊活著用的Spring Cloud(含一個實際業務貫穿所有元件的完整例子)
然後輸入http://localhost:8867/turbine.stream(Turbine聚合監控資料流)進入監控皮膚:
朱曄和你聊Spring系列S1E8:湊活著用的Spring Cloud(含一個實際業務貫穿所有元件的完整例子)
多訪問幾次投資服務介面可以看到每一個服務方法的斷路器情況以及三套服務斷路器執行緒池的情況,我們接下去關閉使用者服務,再多訪問幾次投資服務介面,可以看到getUser斷路器開啟(getUser方法有個紅點):
朱曄和你聊Spring系列S1E8:湊活著用的Spring Cloud(含一個實際業務貫穿所有元件的完整例子)
同時在投資服務日誌中可以看到斷路器走了Fallback的使用者服務:
朱曄和你聊Spring系列S1E8:湊活著用的Spring Cloud(含一個實際業務貫穿所有元件的完整例子)
最後,我們訪問Zipkin來看一下服務鏈路監控的威力,訪問http://localhost:9411/zipkin/然後點選按照最近排序可以看到有一條很長的鏈路:
朱曄和你聊Spring系列S1E8:湊活著用的Spring Cloud(含一個實際業務貫穿所有元件的完整例子)
點進去看看:
朱曄和你聊Spring系列S1E8:湊活著用的Spring Cloud(含一個實際業務貫穿所有元件的完整例子)
整個鏈路覆蓋:

  1. 閘道器:
    朱曄和你聊Spring系列S1E8:湊活著用的Spring Cloud(含一個實際業務貫穿所有元件的完整例子)
  2. 斷路器以及同步服務呼叫
    朱曄和你聊Spring系列S1E8:湊活著用的Spring Cloud(含一個實際業務貫穿所有元件的完整例子)
  3. 訊息傳送和接受的非同步處理
    朱曄和你聊Spring系列S1E8:湊活著用的Spring Cloud(含一個實際業務貫穿所有元件的完整例子)
    整個過程一清二楚,只是這裡沒有Redis和資料庫訪問的資訊,我們可以通過定義擴充套件實現,這裡不展開闡述。還可以點選Zipkin的依賴連結分析服務之間的依賴關係:
    朱曄和你聊Spring系列S1E8:湊活著用的Spring Cloud(含一個實際業務貫穿所有元件的完整例子)
    點選每一個服務可以檢視明細:
    朱曄和你聊Spring系列S1E8:湊活著用的Spring Cloud(含一個實際業務貫穿所有元件的完整例子)
    還記得我們引用了p6spy嗎,我們來看一下投資服務的日誌:
    朱曄和你聊Spring系列S1E8:湊活著用的Spring Cloud(含一個實際業務貫穿所有元件的完整例子)
    方括號中的幾個資料分別是appname,traceId,spanId,exportable(是否傳送到zipkin)。 隨便複製一個traceId,貼上到zipkin即可檢視這個SQL的完整鏈路:
    朱曄和你聊Spring系列S1E8:湊活著用的Spring Cloud(含一個實際業務貫穿所有元件的完整例子)
    演示到此結束。

總結

這是一篇超長的文章,在本文中我們以一個實際的業務例子介紹演示瞭如下內容:

  1. Eureka服務註冊發現
  2. Feign服務遠端呼叫
  3. Hystrix服務斷路器
  4. Turbine斷路器監控聚合
  5. Stream做非同步處理
  6. Sleuth和Zipkin服務呼叫鏈路監控
  7. Zuul服務閘道器和自定義過濾器
  8. JPA資料訪問和Redisson分散式鎖 雖然我們給出的是一個完整的業務例子,但是我們可以看到投資的時候三大服務是需要做事務處理的,這裡因為是演示Spring Cloud,完全忽略了分散式事務處理,以後有機會會單獨寫文章來討論這個事情。

總結一下我對Spring Cloud的看法:

  1. 發展超快,感覺Spring Cloud總是會先用開源的東西先納入體系然後慢慢推出自己的實現,Feign、Gateway就是這樣的例子
  2. 因為發展快,版本迭代快,所以網上的資料往往五花八門,各種配置不一定適用最新版本,還是看官方文件最好
  3. 但是官方文件有的時候也不全面,這個時候只能自己閱讀相關原始碼
  4. 現在還不夠成熟(可用,但用的不是最舒服,需要用好的話需要做很多定製),功能不是最豐富,屬於湊活能用的階段,照這個速度,1年後我們再看到時候可能就很爽了
  5. 期待Spring Cloud在配置服務、閘道器服務、全鏈路監控、一體化的配置後臺方面繼續加強
  6. 不管怎麼說,如果只需要2小時就可以搭建一套微服務體系,具有服務發現+同步呼叫+非同步呼叫+呼叫監控+熔斷+閘道器的功能,還是很震撼的,小型創業專案用這套架構可以當天就起步專案
  7. 社群還提供了一個Admin專案功能比較豐富,你可以嘗試搭建https://github.com/codecentric/spring-boot-admin,安裝過程請檢視原始碼,啟動後截圖如下:
    朱曄和你聊Spring系列S1E8:湊活著用的Spring Cloud(含一個實際業務貫穿所有元件的完整例子)

朱曄和你聊Spring系列S1E8:湊活著用的Spring Cloud(含一個實際業務貫穿所有元件的完整例子)

希望本文對你有用,完整程式碼見https://github.com/JosephZhu1983/SpringCloud101。

相關文章