本文会以一个简单而完整的业务来阐述Spring Cloud Finchley.RELEASE版本常用组件的使用。如下图所示,本文会覆盖的组件有:
Spring Cloud Netflix Zuul网关服务器
Spring Cloud Netflix Eureka发现服务器
Spring Cloud Netflix Turbine断路器监控
Spring Cloud Sleuth + Zipkin服务调用监控
Sping Cloud Stream + RabbitMQ做异步消息
Spring Data JPA做数据访问
本文的例子使用的依赖版本是:
Spring Cloud - Finchley.RELEASE
Spring Data - Lovelace-RELEASE
Spring Cloud Stream - Fishtown.M3
Spring Boot - 2.0.5.RELEASE
各项组件详细使用请参见官网,Spring组件版本变化差异较大,网上代码复制粘贴不一定能够适用,最最好的资料来源只有官网+阅读源代码,直接给出地址方便你阅读本文的时候阅读官网的文档:
全链路监控:http://cloud.spring.io/spring-cloud-static/spring-cloud-sleuth/2.0.1.RELEASE/single/spring-cloud-sleuth.html
服务发现、网关、断路器:http://cloud.spring.io/spring-cloud-static/spring-cloud-netflix/2.0.1.RELEASE/single/spring-cloud-netflix.html
服务调用:http://cloud.spring.io/spring-cloud-static/spring-cloud-openfeign/2.0.1.RELEASE/single/spring-cloud-openfeign.html
异步消息:https://docs.spring.io/spring-cloud-stream/docs/Fishtown.M3/reference/htmlsingle/
数据访问:https://docs.spring.io/spring-data/jpa/docs/2.1.0.RELEASE/reference/html/
如下贴出所有基础组件(除数据库)和业务组件的架构图,箭头代表调用关系(实现是业务服务调用、虚线是基础服务调用),蓝色框代表基础组件(服务器)
这套架构中有关微服务以及消息队列的设计理念,请参考我之前的《朱晔的互联网架构实战心得》系列文章。下面,我们开始此次Spring Cloud之旅,Spring Cloud内容太多,本文分上下两节,并且不会介绍太多理论性的东西,这些知识点可以介绍一本书,本文更多的意义是给出一个可行可用的实际的示例代码供你参考。
业务背景
本文我们会做一个相对实际的例子,来演示互联网金融业务募集项目和放款的过程。三个表的表结构如下:
project表存放了所有可募集的项目,包含项目名称、总的募集金额、剩余可以募集的金额、募集原因等等
user表存放了所有的用户,包括借款人和投资人,包含用户的可用余额和冻结余额
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异步处理消息的服务:
project service:用于处理project表做项目相关的查询和操作
user service:用于操作user表做用户相关的查询和操作
invest service:用于操作invest表做投资相关的查询和操作
project listener:监听MQ中有关项目变化的消息,异步处理项目的放款业务
整个业务流程其实就是初始化投资人、借款人和项目->项目投资(一个项目可以有多个投资人进行多笔投资)->项目全部募集完毕后把所有投资的钱放款给借款人的过程:
数据库中有id=1和2的user为投资人1和2,初始可用余额10000,冻结余额0
数据库中有id=3的user为借款人1,初始可用余额0,冻结余额0
数据库中有id=1的project为一个可以投资的项目,投资额度为1000元,状态为1募集中
初始情况下数据库中的invest表没记录
用户1通过invest service下单进行投资,每次投资100元投资5次,完成后invest表是5条记录,然后用户1的可用余额为9500,冻结余额为500,项目1的剩余可以投资额度为500元(在整个过程中invest service会调用project service和user service查询项目和用户的信息,以及更新项目和用户的资金)
用户2也是类似重复投资5次,完成后invest表应该是10条记录,然后用户2的可用余额为9500,冻结余额为500,项目1的剩余可以投资额度为0元
此时,project service把project项目状态改为2代表募集完成,然后发送一条消息到MQ服务器
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:
4.0.0
me.josephzhu
springcloud101
pom
1.0-SNAPSHOT
springcloud101-investservice-api
springcloud101-investservice-server
springcloud101-userservice-api
springcloud101-userservice-server
springcloud101-projectservice-api
springcloud101-projectservice-server
springcloud101-eureka-server
springcloud101-zuul-server
springcloud101-turbine-server
springcloud101-projectservice-listener
org.springframework.boot
spring-boot-starter-parent
2.0.5.RELEASE
UTF-8
UTF-8
1.8
org.projectlombok
lombok
true
org.springframework.cloud
spring-cloud-dependencies
Finchley.RELEASE
pom
import
org.springframework.data
spring-data-releasetrain
Lovelace-RELEASE
import
pom
org.springframework.cloud
spring-cloud-stream-dependencies
Fishtown.M3
pom
import
org.springframework.boot
spring-boot-maven-plugin
spring-milestones
Spring Milestones
https://repo.spring.io/libs-milestone
false
Eureka
第一个要搭建的服务就是用于服务注册的Eureka服务器:
springcloud101
me.josephzhu
1.0-SNAPSHOT
4.0.0
spring101-eureka-server
org.springframework.cloud
spring-cloud-starter-netflix-eureka-server
在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链路监控整合,非常方便。在这里,我们会同时演示两个功能,我们会进行路由配置,使网关做一个反向代理,我们也会自定义一个前置过滤器做安全拦截。
首先,新建一个模块:
springcloud101
me.josephzhu
1.0-SNAPSHOT
4.0.0
springcloud101-zuul-server
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.cloud
spring-cloud-starter-netflix-zuul
org.springframework.boot
spring-boot-starter-actuator
org.springframework.cloud
spring-cloud-starter-sleuth
org.springframework.cloud
spring-cloud-starter-zipkin
随后加一个配置文件:
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端口,这里重点看一下路由的配置:
我们通过path来批量访问请求的路径,转发到指定的serviceId
我们延长了传输和连接的超时时间,以便调试时不超时
对于其它的配置,之后会进行解释,下面我们通过编程实现一个前置过滤:
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响应状态码