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

本文会以一个简单而完整的业务来阐述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响应状态码
50000+
5万行代码练就真实本领
17年
创办于2008年老牌培训机构
1000+
合作企业
98%
就业率

联系我们

电话咨询

0532-85025005

扫码添加微信