朱晔和你聊Spring系列S1E8:凑活着用的Spring Cloud(含一个实际业务贯穿所有组件的完整例子) (上)...


本文会以一个简单而完整的业务来阐述Spring CloudFinchley.RELEASE版本常用组件的使用。如下图所示,本文会覆盖的组件有:

1.      Spring Cloud Netflix Zuul网关服务器

2.      Spring Cloud Netflix Eureka发现服务器

3.      Spring Cloud Netflix Turbine断路器监控

4.      Spring Cloud Sleuth + Zipkin服务调用监控

5.      Sping Cloud Stream + RabbitMQ做异步消息

6.      Spring Data JPA做数据访问

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

1.      Spring Cloud - Finchley.RELEASE

2.      Spring Data - Lovelace-RELEASE

3.      Spring Cloud Stream - Fishtown.M3

4.      Spring Boot - 2.0.5.RELEASE

各项组件详细使用请参见官网,Spring组件版本变化差异较大,网上代码复制粘贴不一定能够适用,最最好的资料来源只有官网+阅读源代码,直接给出地址方便你阅读本文的时候阅读官网的文档:

1.      全链路监控:http://cloud.spring.io/spring-cloud-static/spring-cloud-sleuth/2.0.1.RELEASE/single/spring-cloud-sleuth.html

2.      服务发现、网关、断路器:http://cloud.spring.io/spring-cloud-static/spring-cloud-netflix/2.0.1.RELEASE/single/spring-cloud-netflix.html

3.      服务调用:http://cloud.spring.io/spring-cloud-static/spring-cloud-openfeign/2.0.1.RELEASE/single/spring-cloud-openfeign.html

4.      异步消息:https://docs.spring.io/spring-cloud-stream/docs/Fishtown.M3/reference/htmlsingle/

5.      数据访问:https://docs.spring.io/spring-data/jpa/docs/2.1.0.RELEASE/reference/html/

如下贴出所有基础组件(除数据库)和业务组件的架构图,箭头代表调用关系(实现是业务服务调用、虚线是基础服务调用),蓝色框代表基础组件(服务器)。

朱晔和你聊Spring系列S1E8:凑活着用的Spring Cloud(含一个实际业务贯穿所有组件的完整例子) (上)..._第1张图片


这套架构中有关微服务以及消息队列的设计理念,请参考我之前的《朱晔的互联网架构实战心得》系列文章。下面,我们开始此次Spring Cloud之旅,Spring Cloud内容太多,本文分上下两节,并且不会介绍太多理论性的东西,这些知识点可以介绍一本书,本文更多的意义是给出一个可行可用的实际的示例代码供你参考。

 

业务背景

 

本文我们会做一个相对实际的例子,来演示互联网金融业务募集项目和放款的过程。三个表的表结构如下:

1.      project表存放了所有可募集的项目,包含项目名称、总的募集金额、剩余可以募集的金额、募集原因等等

2.      user表存放了所有的用户,包括借款人和投资人,包含用户的可用余额和冻结余额

3.      invest表存放了投资人投资的信息,包含投资哪个project,投资了多少钱、借款人是谁

CREATETABLE `invest` (

  `id` bigint(20) unsigned NOT NULLAUTO_INCREMENT,

  `project_id` bigint(20) unsigned NOT NULL,

  `project_name` varchar(50) CHARACTER SETutf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,

  `investor_id` bigint(20) unsigned NOT NULL,

  `investor_name` varchar(50) CHARACTER SETutf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,

  `borrower_id` bigint(20) unsigned NOT NULL,

  `borrower_name` varchar(50) CHARACTER SETutf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,

  `amount` decimal(10,2) unsigned NOT NULL,

  `status` tinyint(4) NOT NULL,

  `created_at` datetime NOT NULL DEFAULTCURRENT_TIMESTAMP,

  `updated_at` datetime NOT NULL DEFAULTCURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

  PRIMARY KEY (`id`)

)

CREATETABLE `project` (

  `id` bigint(20) unsigned NOT NULLAUTO_INCREMENT,

  `name` varchar(50) CHARACTER SET utf8mb4COLLATE utf8mb4_0900_ai_ci NOT NULL,

  `reason` varchar(50) CHARACTER SET utf8mb4COLLATE utf8mb4_0900_ai_ci NOT NULL,

  `borrower_id` bigint(20) unsigned NOT NULL,

  `total_amount` decimal(10,0) unsigned NOTNULL,

  `remain_amount` decimal(10,0) unsigned NOTNULL,

  `status` tinyint(3) unsigned NOT NULL COMMENT'1-募集中2-募集完成 3-已放款',

  `created_at` datetime NOT NULL DEFAULTCURRENT_TIMESTAMP,

  `updated_at` datetime NOT NULL DEFAULTCURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

  PRIMARY KEY (`id`) USING BTREE

)

CREATETABLE `user` (

  `id` bigint(20) unsigned NOT NULLAUTO_INCREMENT,

  `name` varchar(50) NOT NULL,

  `available_balance` decimal(10,2) unsignedNOT NULL,

  `frozen_balance` decimal(10,2) unsigned NOTNULL,

  `created_at` datetime NOT NULL DEFAULTCURRENT_TIMESTAMP,

  `updated_at` datetime NOT NULL DEFAULTCURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

  PRIMARY KEY (`id`) USING BTREE

)

我们会搭建四个业务服务,其中三个是被其它服务同步调用的服务,一个是监听MQ异步处理消息的服务:

1.      project service:用于处理project表做项目相关的查询和操作

2.      user service:用于操作user表做用户相关的查询和操作

3.      invest service:用于操作invest表做投资相关的查询和操作

4.      project listener:监听MQ中有关项目变化的消息,异步处理项目的放款业务

整个业务流程其实就是初始化投资人、借款人和项目->项目投资(一个项目可以有多个投资人进行多笔投资)->项目全部募集完毕后把所有投资的钱放款给借款人的过程:

1.      数据库中有id=1和2的user为投资人1和2,初始可用余额10000,冻结余额0

2.      数据库中有id=3的user为借款人1,初始可用余额0,冻结余额0

3.      数据库中有id=1的project为一个可以投资的项目,投资额度为1000元,状态为1募集中

4.      初始情况下数据库中的invest表没记录

5.      用户1通过investservice下单进行投资,每次投资100元投资5次,完成后invest表是5条记录,然后用户1的可用余额为9500,冻结余额为500,项目1的剩余可以投资额度为500元(在整个过程中invest service会调用project service和user service查询项目和用户的信息,以及更新项目和用户的资金)

6.      用户2也是类似重复投资5次,完成后invest表应该是10条记录,然后用户2的可用余额为9500,冻结余额为500,项目1的剩余可以投资额度为0元

7.      此时,project service把project项目状态改为2代表募集完成,然后发送一条消息到MQ服务器

8.      project listener收到这条消息后进行异步的放款处理,调用user service逐一根据10比投资订单的信息,把所有投资人冻结的钱转移到借款人,完成后投资人1和2可用余额为9500,冻结余额为0,借款人1可用余额为1000,冻结余额为0,随后把项目状态改为3放款完成

除了业务服务还有三个基础服务(Ererka+Zuul+Turbine,Zipkin服务不在项目内,我们直接通过jar包启动),整个项目结构如下:

朱晔和你聊Spring系列S1E8:凑活着用的Spring Cloud(含一个实际业务贯穿所有组件的完整例子) (上)..._第2张图片


整个业务包含了同步服务调用和异步消息处理,业务简单而有代表性。但是在这里我们并没有演示SpringCloud Config的使用,之前也提到过,国内开源的几个配置中心比Cloud Config功能强大太多太多,目前Cloud Config实用性不好,在这里就不纳入演示了。

下面我们来逐一实现每一个组件和服务。

 

基础设施搭建

 

我们先来新建一个父模块的pom:

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

    <
groupId>me.josephzhugroupId>
    <
artifactId>springcloud101artifactId>
    <
packaging>pompackaging>
    <
version>1.0-SNAPSHOTversion>
    <
modules>
        <
module>springcloud101-investservice-apimodule>
        <
module>springcloud101-investservice-servermodule>
        <
module>springcloud101-userservice-apimodule>
        <
module>springcloud101-userservice-servermodule>
        <
module>springcloud101-projectservice-apimodule>
        <
module>springcloud101-projectservice-servermodule>
        <
module>springcloud101-eureka-servermodule>
        <
module>springcloud101-zuul-servermodule>
        <
module>springcloud101-turbine-servermodule>
        <
module>springcloud101-projectservice-listenermodule>
    modules>

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

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

    <
dependencies>
        <
dependency>
            <
groupId>org.projectlombokgroupId>
            <
artifactId>lombokartifactId>
            <
optional>trueoptional>
        dependency>
    dependencies>

    <
dependencyManagement>
        <
dependencies>
            <
dependency>
                <
groupId>org.springframework.cloudgroupId>
                <
artifactId>spring-cloud-dependenciesartifactId>
                <
version>Finchley.RELEASEversion>
                <
type>pomtype>
                <
scope>importscope>
            dependency>
            <
dependency>
                <
groupId>org.springframework.datagroupId>
                <
artifactId>spring-data-releasetrainartifactId>
                <
version>Lovelace-RELEASEversion>
                <
scope>importscope>
                <
type>pomtype>
            dependency>
            <
dependency>
                <
groupId>org.springframework.cloudgroupId>
                <
artifactId>spring-cloud-stream-dependenciesartifactId>
                <
version>Fishtown.M3version>
                <
type>pomtype>
                <
scope>importscope>
            dependency>
        dependencies>
    dependencyManagement>


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

    <
repositories>
        <
repository>
            <
id>spring-milestonesid>
            <
name>SpringMilestonesname>
            <
url>https://repo.spring.io/libs-milestoneurl>
            <
snapshots>
                <
enabled>falseenabled>
            snapshots>
        repository>
    repositories>

project>

 

Eureka

 

第一个要搭建的服务就是用于服务注册的Eureka服务器:

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

    <
artifactId>spring101-eureka-serverartifactId>

    <
dependencies>
        <
dependency>
            <
groupId>org.springframework.cloudgroupId>
            <
artifactId>spring-cloud-starter-netflix-eureka-serverartifactId>
        dependency>
    dependencies>

project>

在resources文件夹下创建一个配置文件application.yml(对于Spring Cloud项目由于配置实在是太多,为了模块感层次感强一点,这里我们使用yml格式):

server:
  port:
8865

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

spring:
  application:
    name:
eurka-server

在这里,为了简单期间,我们搭建的是一个Standalone的注册服务(这里,我们注意到Eureka有一个自我保护的开关,默认开启,自我保护的意思是短时间大批节点和Eureka断开的话,这个一般是网络问题,自我保护会开启防止节点注销,在之后的测试过程中因为我们会经常重启调试服务,所以如果遇到节点不注销的问题可以暂时关闭这个功能),分配了8865端口(我们约定,基础组件分配的端口以88开头),随后建立一个主程序文件:

package me.josephzhu.springcloud101.eurekaserver;

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

@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {

   
public static void main(String[]args) {
        SpringApplication.run(EurekaServerApplication.
class, args );
    }
}

对于搭建Spring Cloud的一些基础组件的服务,往往就是三步,加依赖,加配置,加注解开关即可。

 

Zuul

 

Zuul是一个代理网关,具有路由和过滤两大功能。并且直接能和Eureka注册服务以及Sleuth链路监控整合,非常方便。在这里,我们会同时演示两个功能,我们会进行路由配置,使网关做一个反向代理,我们也会自定义一个前置过滤器做安全拦截。

首先,新建一个模块:

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

    <
artifactId>springcloud101-zuul-serverartifactId>

    <
dependencies>
        <
dependency>
            <
groupId>org.springframework.cloudgroupId>
            <
artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
        dependency>
        <
dependency>
            <
groupId>org.springframework.cloudgroupId>
            <
artifactId>spring-cloud-starter-netflix-zuulartifactId>
        dependency>
        <
dependency>
            <
groupId>org.springframework.bootgroupId>
            <
artifactId>spring-boot-starter-actuatorartifactId>
        dependency>
        <
dependency>
            <
groupId>org.springframework.cloudgroupId>
            <
artifactId>spring-cloud-starter-sleuthartifactId>
        dependency>
        <
dependency>
            <
groupId>org.springframework.cloudgroupId>
            <
artifactId>spring-cloud-starter-zipkinartifactId>
        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 StringfilterType() {
       
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 (Exceptione){}

           
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.0http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <
parent>
        <
artifactId>springcloud101artifactId>
        <
groupId>me.josephzhugroupId>
        <
version>1.0-SNAPSHOTversion>
    parent>
    <
modelVersion>4.0.0modelVersion>

    <
artifactId>springcloud101-turbine-serverartifactId>

    <
dependencies>
        <
dependency>
            <
groupId>org.springframework.cloudgroupId>
            <
artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
        dependency>
        <
dependency>
            <
groupId>org.springframework.bootgroupId>
            <
artifactId>spring-boot-starter-actuatorartifactId>
        dependency>
        <
dependency>
            <
groupId>org.springframework.cloudgroupId>
            <
artifactId>spring-cloud-starter-netflix-hystrixartifactId>
        dependency>
        <
dependency>
            <
groupId>org.springframework.cloudgroupId>
            <
artifactId>spring-cloud-starter-netflix-hystrix-dashboardartifactId>
        dependency>
        <
dependency>
            <
groupId>org.springframework.cloudgroupId>
            <
artifactId>spring-cloud-starter-netflix-turbineartifactId>
        dependency>
    dependencies>

project>

第二步配置:

server:
  port:
8867

spring:
  application:
    name:
turbineserver

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

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

 
endpoint:
    health:
      show-details:
always

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

Turbine服务我们使用8867端口,这里重点看一下turbine下面的配置项:

1.      instanceUrlSuffix配置了默认情况下每一个实例监控数据流的拉取地址

2.      app-config配置了所有需要监控的应用程序

我们来看一下文首的架构图,这里的Turbine其实是从各个配置的服务读取监控流来汇总监控数据的,并不是像Zipkin这种由服务主动上报数据的方式。当然,我们还可以通过Turbine Stream的功能让客户端主动上报数据(通过消息队列),这里就不详细展开阐述了。下面是第三步:

package me.josephzhu.springcloud101.turbine.server;

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

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

   
public static void main(String[]args) {
        SpringApplication.run(TurbineServerApplication.
class, args );
    }
}

之后会展示使用截图。

 

Zipkin

 

Zipkin用于收集分布式追踪信息(同时扮演了服务端以及查看后台的角色),搭建方式请参见官网https://github.com/openzipkin/zipkin,最简单的方式是去https://dl.bintray.com/openzipkin/maven/io/zipkin/java/zipkin-server/直接下载jar包运行即可,在生产环境强烈建议配置后端存储为ES或Mysql等等,这里我们用于演示不进行任何其它配置了。我们直接启动即可,默认运行在9411端口:

朱晔和你聊Spring系列S1E8:凑活着用的Spring Cloud(含一个实际业务贯穿所有组件的完整例子) (上)..._第3张图片

之后我们展示全链路监控的截图。

 

用户服务搭建

 

我们先来新建一个被依赖最多的业务服务,每一个服务分两个项目,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.0http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <
parent>
        <
artifactId>springcloud101artifactId>
        <
groupId>me.josephzhugroupId>
        <
version>1.0-SNAPSHOTversion>
    parent>
    <
modelVersion>4.0.0modelVersion>

    <
artifactId>springcloud101-userservice-apiartifactId>

    <
dependencies>
        <
dependency>
            <
groupId>org.springframework.cloudgroupId>
            <
artifactId>spring-cloud-starter-openfeignartifactId>
        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.0http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <
parent>
        <
artifactId>springcloud101artifactId>
        <
groupId>me.josephzhugroupId>
        <
version>1.0-SNAPSHOTversion>
    parent>
    <
modelVersion>4.0.0modelVersion>

    <
artifactId>springcloud101-userservice-serverartifactId>

    <
dependencies>
        <
dependency>
            <
groupId>org.springframework.cloudgroupId>
            <
artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
        dependency>
        <
dependency>
            <
groupId>org.springframework.bootgroupId>
            <
artifactId>spring-boot-starter-webartifactId>
        dependency>
        <
dependency>
            <
groupId>org.springframework.cloudgroupId>
            <
artifactId>spring-cloud-starter-openfeignartifactId>
        dependency>
        <
dependency>
            <
groupId>org.springframework.bootgroupId>
            <
artifactId>spring-boot-starter-actuatorartifactId>
        dependency>
        <
dependency>
            <
groupId>org.springframework.cloudgroupId>
            <
artifactId>spring-cloud-starter-sleuthartifactId>
        dependency>
        <
dependency>
            <
groupId>org.springframework.cloudgroupId>
            <
artifactId>spring-cloud-starter-zipkinartifactId>
        dependency>
        <
dependency>
            <
groupId>org.springframework.bootgroupId>
            <
artifactId>spring-boot-starter-data-jpaartifactId>
        dependency>
        <
dependency>
            <
groupId>mysqlgroupId>
            <
artifactId>mysql-connector-javaartifactId>
        dependency>
        <
dependency>
            <
groupId>com.github.gavlyukovskiygroupId>
            <
artifactId>p6spy-spring-boot-starterartifactId>
            <
version>1.4.3version>
        dependency>
        <
dependency>
            <
groupId>org.springframework.cloudgroupId>
            <
artifactId>spring-cloud-starter-netflix-hystrixartifactId>
        dependency>
        <
dependency>
            <
groupId>org.redissongroupId>
            <
artifactId>redisson-spring-boot-starterartifactId>
            <
version>3.8.2version>
        dependency>

        <
dependency>
            <
groupId>me.josephzhugroupId>
            <
artifactId>springcloud101-userservice-apiartifactId>
            <
version>1.0-SNAPSHOTversion>
        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 {
}

服务实现如下:

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 UserServiceControllerimplements 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 BigDecimalconsumeMoney(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 BigDecimallendpayMoney(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的配置。


==本文太长只能分成上中下发了==

你可能感兴趣的:(朱晔和你聊Spring系列S1E8:凑活着用的Spring Cloud(含一个实际业务贯穿所有组件的完整例子) (上)...)