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


本文會以一個簡單而完整的業務來闡述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. 全鏈路監控:http://cloud.spring.io/spring-cloud-static/spring-cloud-sleuth/2.0.1.RELEASE/single/spring-cloud-sleuth.html
  2. 服務發現、網關、斷路器:http://cloud.spring.io/spring-cloud-static/spring-cloud-netflix/2.0.1.RELEASE/single/spring-cloud-netflix.html
  3. 服務調用:http://cloud.spring.io/spring-cloud-static/spring-cloud-openfeign/2.0.1.RELEASE/single/spring-cloud-openfeign.html
  4. 異步消息:https://docs.spring.io/spring-cloud-stream/docs/Fishtown.M3/reference/htmlsingle/
  5. 數據訪問:https://docs.spring.io/spring-data/jpa/docs/2.1.0.RELEASE/reference/html/

如下貼出所有基礎組件(除數據庫)和業務組件的架構圖,箭頭代表調用關系(實現是業務服務調用、虛線是基礎服務調用),藍色框代表基礎組件(服務器)

這套架構中有關微服務以及消息隊列的設計理念,請參考我之前的《朱曄的互聯網架構實戰心得》系列文章。下面,我們開始此次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 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端口:


之后我們展示全鏈路監控的截圖。

用戶服務搭建

我們先來新建一個被依賴最多的業務服務,每一個服務分兩個項目,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注冊中心:

這里可以看到所有服務已經注冊在線:

  1. 8866的Zuul
  2. 8867的Tubine
  3. 8761的用戶服務
  4. 8762的項目服務
  5. 8763的投資服務

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

訪問http://localhost:8762/getProject?id=2可以測試項目服務:

我們來初始化一下數據庫,默認有一個項目信息:

還有兩個投資人和一個借款人:


現在來通過網關訪問http://localhost:8866/invest/createInvest投資服務(使用網關進行路由,我們配置的是匹配invest/**這個path路由到投資服務,直接訪問服務的時候無需提供invest前綴)使用投資人1做一次投資:

在沒有提供token的時候會出現錯誤,加上token后訪問成功:

可以看到投資后投資人凍結賬戶為100,項目剩余金額為900,多了一條投資記錄:

我們使用投資人1測試5次投資,使用投資人2測試5次投資,測試后可以看到項目狀態變為了3放款完成:

數據庫中有10條投資記錄:

兩個投資人的凍結余額都為0,可用余額分別少了500,借款人可用余額多了1000,說明放款成功了?:

同時可以在ProjectListner的日志中看到收到消息的日志:

我們可以訪問http://localhost:15672打開RabbitMQ都是管理台看一下我們那條消息的情況:

可以看到在隊列中的確有一條消息先收到然后不久后(大概是6秒后)得到了ack處理完畢。隊列綁定到了zhuye這個交換機上:

至此,我們已經演示了Zuul、Eureka和Stream,現在我們來看一下斷路器功能。
我們首先訪問http://localhost:8867/hystrix:

然后輸入http://localhost:8867/turbine.stream(Turbine聚合監控數據流)進入監控面板:

多訪問幾次投資服務接口可以看到每一個服務方法的斷路器情況以及三套服務斷路器線程池的情況,我們接下去關閉用戶服務,再多訪問幾次投資服務接口,可以看到getUser斷路器打開(getUser方法有個紅點):

同時在投資服務日志中可以看到斷路器走了Fallback的用戶服務:

最后,我們訪問Zipkin來看一下服務鏈路監控的威力,訪問http://localhost:9411/zipkin/然后點擊按照最近排序可以看到有一條很長的鏈路:

點進去看看:

整個鏈路覆蓋:

  1. 網關:
  2. 斷路器以及同步服務調用
  3. 消息發送和接受的異步處理

    整個過程一清二楚,只是這里沒有Redis和數據庫訪問的信息,我們可以通過定義擴展實現,這里不展開闡述。還可以點擊Zipkin的依賴鏈接分析服務之間的依賴關系:

    點擊每一個服務可以查看明細:

    還記得我們引用了p6spy嗎,我們來看一下投資服務的日志:

    方括號中的幾個數據分別是appname,traceId,spanId,exportable(是否發送到zipkin)。
    隨便復制一個traceId,粘貼到zipkin即可查看這個SQL的完整鏈路:

    演示到此結束。

總結

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

  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,這里沒有演示

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM