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

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

  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作數據訪問

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

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

各項組件詳細使用請參見官網,Spring組件版本變化差別較大,網上代碼複製粘貼不必定可以適用,最最好的資料來源只有官網+閱讀源代碼,直接給出地址方便你閱讀本文的時候閱讀官網的文檔:mysql

  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…

以下貼出全部基礎組件(除數據庫)和業務組件的架構圖,箭頭表明調用關係(實現是業務服務調用、虛線是基礎服務調用),藍色框表明基礎組件(服務器) git

這套架構中有關微服務以及消息隊列的設計理念,請參考我以前的《朱曄的互聯網架構實戰心得》系列文章。下面,咱們開始這次Spring Cloud之旅,Spring Cloud內容太多,本文分上下兩節,而且不會介紹太多理論性的東西,這些知識點能夠介紹一本書,本文更多的意義是給出一個可行可用的實際的示例代碼供你參考。

業務背景

本文咱們會作一個相對實際的例子,來演示互聯網金融業務募集項目和放款的過程。三個表的表結構以下:github

  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異步處理消息的服務:web

  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:redis

<?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服務器:spring

<?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格式):sql

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。

相關文章
相關標籤/搜索