朱晔和你聊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。

相关文章
相关标签/搜索