Spring Cloud Alibaba
Spring Cloud 是什么
[百度百科]Spring Cloud是一系列框架的有序集合。它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用 Spring Boot的开发风格做到一键启动和部署。Spring Cloud并没有重复制造轮子,它只是将目前各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者提供一套简单易懂、易部署和易维护的分布式系统开发工具包。
Spring Cloud为开发人员提供了工具来快速构建分布式系统中的一些常见模式(例如配置管理、服务发现、断路器、智能路由、微代理、控制总线、一次性令牌、全局锁、领导选举、分布式会话、集群状态)。分布式系统的协调产生了模板模式,使用Spring Cloud开发人员可以快速建立实现这些模式的服务和应用程序。它们在任何分布式环境中都能很好地工作,包括开发者自己的笔记本电脑、裸机数据中心以及云计算等托管平台。
Spring Cloud 解决什么问题
Spring Cloud 规范及实现意图解决的问题是微服务架构实施过程中存在的一些问题,比如微服务架构中的服务注册发现问题、网络问题(比如熔断场景)、统一认证安全授权问题、负载均衡问题、链路追踪等问题。
Spring Cloud 架构
Spring Cloud是一个微服务相关规范,这个规范意图为搭建微服务架构提供一站式服务,采用组件(框架)化机制定义一系列组件,各类组件针对性的处理微服务中的特定问题,这些组件共同来构成Spring Cloud微服务技术栈。
Spring Cloud 核心组件
Spring Cloud 的生态技术圈中,按照发展可以分为第一代Spring Cloud组件和第二代Spring Cloud组件。具体的组件对比如下:
组件 | 第一代Spring Cloud(Netflix) | 第二代Spring Cloud(Alibaba) |
---|---|---|
注册中心 | Netflix Eureka | Alibaba Nacos |
客户端负载均衡 | Netflix Ribbon | Alibaba Dubbo LB、Spring Cloud Loadbalaner |
熔断器 | Netflix Hystrix | Alibaba Sentinel |
网关 | Netflix Zuul | Spring Cloud Gateway |
配置中心 | Spring Cloud Config | Alibaba Nacos、携程 Apollo |
服务调用 | Netflix Feign | Alibaba Dubbo RPC |
消息驱动 | Spring Cloud Stream | |
链路追踪 | Spring Cloud Sleuth/Zipkin | |
事务 | Alibaba seata |
组件协同工作流程
Spring Cloud 和 Dubbo 对比
Dubbo是阿里巴巴公司开源的一个高性能优秀的服务框架,基于RPC调用,对于目前使用率较高的Spring Cloud Netflix来说,它是基于HTTP的,所以效率上没有Dubbo高,但问题在于Dubbo体系的组件不全,不能够提供一站式解决⽅案,比如服务注册与发现需要借助于Zookeeper等实现,SpringCloud Netflix则是真正的提供了一站式服务化解决方案,且有Spring家族背景。
Spring Cloud 和 Spring Boot 的关系
Spring Cloud 只是利用了Spring Boot 的特点,让我们能够快速的实现微服务组件开发,否则不使用Spring Boot的话,我们在使用Spring Cloud时,每个组件的相关Jar包都需要我们导入配置以及需要考虑兼容性等各种情况。所以Spring Boot是我们快速把Spring Cloud微服务技术应用起来。
案例准备
首先我们按照普通的方式模拟一个微服务之间的调用过程,后面会慢慢的加入Spring Cloud的组件改造该案例。通过这样的方式让大家了解微服务如何替代我们原始的开发,以及如何解决我们的痛点问题。
拉勾App有这样一个功能:“面试直通车”,当求职用户开启了面试直通车之后,会根据企业客户的招聘岗位需求进行双向匹配。其中有一个操作是:为企业用户开启一个定时任务,根据企业录用的用人条件,每日匹配一定数量的应聘者“投递”到企业的资源池中去,那么系统在将匹配到的应聘者投递到资源池的时候需要先检查:此时应聘者默认简历的状态(公开/隐藏),如果此时默认简历的状态已经被应聘者设置为“隐藏”,那么不再执行“投递”操作。 “自动投递功能”在“自动投递微服务”中,“简历状态查询功能”在“简历微服务”中,那么就涉及到“自动投递微服务”调用“简历微服务”查询简历。在这种场景下,“自动投递微服务”就是一个服务消费者,“简历微服务”就是一个服务提供者。
数据库SQL
/*
Navicat Premium Data Transfer
Source Server : localhost
Source Server Type : MySQL
Source Server Version : 50717
Source Host : 127.0.0.1:3306
Source Schema : lagou
Target Server Type : MySQL
Target Server Version : 50717
File Encoding : 65001
Date: 19/04/2020 17:49:15
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for r_resume
-- ----------------------------
DROP TABLE IF EXISTS `r_resume`;
CREATE TABLE `r_resume` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`sex` varchar(10) DEFAULT NULL COMMENT '性别',
`birthday` varchar(30) DEFAULT NULL COMMENT '出生日期',
`work_year` varchar(100) DEFAULT NULL COMMENT '工作年限',
`phone` varchar(20) DEFAULT NULL COMMENT '手机号码',
`email` varchar(100) DEFAULT NULL COMMENT '邮箱',
`status` varchar(80) DEFAULT NULL COMMENT '目前状态',
`resumeName` varchar(500) DEFAULT NULL COMMENT '简历名称',
`name` varchar(40) DEFAULT NULL,
`createTime` datetime DEFAULT NULL COMMENT '创建日期',
`headPic` varchar(100) DEFAULT NULL COMMENT '头像',
`isDel` int(2) DEFAULT NULL COMMENT '是否删除 默认值0-未删除 1-已删除',
`updateTime` datetime DEFAULT NULL COMMENT '简历更新时间',
`userId` int(11) DEFAULT NULL COMMENT '用户ID',
`isDefault` int(2) DEFAULT NULL COMMENT '是否为默认简历 0-默认 1-非默认',
`highestEducation` varchar(20) DEFAULT '' COMMENT '最高学历',
`deliverNearByConfirm` int(2) DEFAULT '0' COMMENT '投递附件简历确认 0-需要确认 1-不需要确认',
`refuseCount` int(11) NOT NULL DEFAULT '0' COMMENT '简历被拒绝次数',
`markCanInterviewCount` int(11) NOT NULL DEFAULT '0' COMMENT '被标记为可面试次数',
`haveNoticeInterCount` int(11) NOT NULL DEFAULT '0' COMMENT '已通知面试次数',
`oneWord` varchar(100) DEFAULT '' COMMENT '一句话介绍自己',
`liveCity` varchar(100) DEFAULT '' COMMENT '居住城市',
`resumeScore` int(3) DEFAULT NULL COMMENT '简历得分',
`userIdentity` int(1) DEFAULT '0' COMMENT '用户身份1-学生 2-工人',
`isOpenResume` int(1) DEFAULT '3' COMMENT '人才搜索-开放简历 0-关闭,1-打开,2-简历未达到投放标准被动关闭 3-从未设置过开放简历',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2195388 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of r_resume
-- ----------------------------
BEGIN;
INSERT INTO `r_resume` VALUES (2195320, '女', '1990', '2年', '199999999', 'test@testtest01.com', '我目前已离职,可快速到岗', '稻壳儿的简历', 'wps', '2015-04-24 13:40:14', 'images/myresume/default_headpic.png', 0, '2015-04-24 13:40:14', 1545132, 1, '本科', 0, 0, 0, 0, '', '广州', 15, 0, 3);
INSERT INTO `r_resume` VALUES (2195321, '女', '1990', '2年', '199999999', 'test@testtest01.com', '我目前已离职,可快速到岗', '稻壳儿的简历', 'wps', '2015-04-24 14:17:54', 'images/myresume/default_headpic.png', 0, '2015-04-24 14:20:35', 1545133, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
INSERT INTO `r_resume` VALUES (2195322, '女', '1990', '2年', '199999999', 'test@testtest01.com', '我目前已离职,可快速到岗', '稻壳儿的简历', 'wps', '2015-04-24 14:42:45', 'images/myresume/default_headpic.png', 0, '2015-04-24 14:43:34', 1545135, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
INSERT INTO `r_resume` VALUES (2195323, '女', '1990', '2年', '199999999', 'test@testtest01.com', '我目前已离职,可快速到岗', '稻壳儿的简历', 'wps', '2015-04-24 14:48:19', 'images/myresume/default_headpic.png', 0, '2015-04-24 14:50:34', 1545136, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
INSERT INTO `r_resume` VALUES (2195331, '女', '1990', '2年', '199999999', 'test@testtest01.com', '我目前已离职,可快速到岗', '稻壳儿的简历', 'wps', '2015-04-24 18:43:35', 'images/myresume/default_headpic.png', 0, '2015-04-24 18:44:08', 1545145, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
INSERT INTO `r_resume` VALUES (2195333, '女', '1990', '2年', '199999999', 'test@testtest01.com', '我目前已离职,可快速到岗', '稻壳儿的简历', 'wps', '2015-04-24 19:01:13', 'images/myresume/default_headpic.png', 0, '2015-04-24 19:01:14', 1545148, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
INSERT INTO `r_resume` VALUES (2195336, '女', '1990', '2年', '199999999', 'test@testtest01.com', '我目前已离职,可快速到岗', '稻壳儿的简历', 'wps', '2015-04-27 14:13:02', 'images/myresume/default_headpic.png', 0, '2015-04-27 14:13:02', 1545155, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
INSERT INTO `r_resume` VALUES (2195337, '女', '1990', '2年', '199999999', 'test@testtest01.com', '我目前已离职,可快速到岗', '稻壳儿的简历', 'wps', '2015-04-27 14:36:55', 'images/myresume/default_headpic.png', 0, '2015-04-27 14:36:55', 1545158, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
INSERT INTO `r_resume` VALUES (2195369, '女', '1990', '10年以上', '199999999', 'test@testtest01.com', '我目前已离职,可快速到岗', '稻壳儿', 'wps', '2015-05-15 18:08:19', 'images/myresume/default_headpic.png', 0, '2015-05-15 18:08:19', 1545346, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
INSERT INTO `r_resume` VALUES (2195374, '女', '1990', '1年', '199999999', 'test@testtest01.com', '我目前正在职,正考虑换个新环境', '稻壳儿', 'wps', '2015-06-04 17:53:37', 'images/myresume/default_headpic.png', 0, '2015-06-04 17:53:39', 1545523, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
INSERT INTO `r_resume` VALUES (2195375, '女', '1990', '1年', '199999999', 'test@testtest01.com', '我目前正在职,正考虑换个新环境', '稻壳儿', 'wps', '2015-06-04 18:11:06', 'images/myresume/default_headpic.png', 0, '2015-06-04 18:11:07', 1545524, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
INSERT INTO `r_resume` VALUES (2195376, '女', '1990', '1年', '199999999', 'test@testtest01.com', '我目前正在职,正考虑换个新环境', '稻壳儿', 'wps', '2015-06-04 18:12:19', 'images/myresume/default_headpic.png', 0, '2015-06-04 18:12:19', 1545525, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
INSERT INTO `r_resume` VALUES (2195377, '女', '1990', '1年', '199999999', 'test@testtest01.com', '我目前正在职,正考虑换个新环境', '稻壳儿', 'wps', '2015-06-04 18:13:28', 'images/myresume/default_headpic.png', 0, '2015-06-04 18:13:28', 1545526, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
INSERT INTO `r_resume` VALUES (2195378, '女', '1990', '1年', '199999999', 'test@testtest01.com', '我目前正在职,正考虑换个新环境', '稻壳儿', 'wps', '2015-06-04 18:15:16', 'images/myresume/default_headpic.png', 0, '2015-06-04 18:15:16', 1545527, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
INSERT INTO `r_resume` VALUES (2195379, '女', '1990', '1年', '199999999', 'test@testtest01.com', '我目前正在职,正考虑换个新环境', '稻壳儿', 'wps', '2015-06-04 18:23:06', 'images/myresume/default_headpic.png', 0, '2015-06-04 18:23:06', 1545528, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
INSERT INTO `r_resume` VALUES (2195380, '女', '1990', '1年', '199999999', 'test@testtest01.com', '我目前正在职,正考虑换个新环境', '稻壳儿', 'wps', '2015-06-04 18:23:38', 'images/myresume/default_headpic.png', 0, '2015-06-04 18:23:39', 1545529, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
INSERT INTO `r_resume` VALUES (2195381, '女', '1990', '1年', '199999999', 'test@testtest01.com', '我目前正在职,正考虑换个新环境', '稻壳儿', 'wps', '2015-06-04 18:27:33', 'images/myresume/default_headpic.png', 0, '2015-06-04 18:27:33', 1545530, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
INSERT INTO `r_resume` VALUES (2195382, '女', '1990', '1年', '199999999', 'test@testtest01.com', '我目前正在职,正考虑换个新环境', '稻壳儿', 'wps', '2015-06-04 18:31:36', 'images/myresume/default_headpic.png', 0, '2015-06-04 18:31:39', 1545531, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
INSERT INTO `r_resume` VALUES (2195383, '女', '1990', '1年', '199999999', 'test@testtest01.com', '我目前正在职,正考虑换个新环境', '稻壳儿', 'wps', '2015-06-04 18:36:48', 'images/myresume/default_headpic.png', 0, '2015-06-04 18:36:48', 1545532, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
INSERT INTO `r_resume` VALUES (2195384, '女', '1990', '1年', '199999999', 'test@testtest01.com', '我目前正在职,正考虑换个新环境', '稻壳儿', 'wps', '2015-06-04 19:15:15', 'images/myresume/default_headpic.png', 0, '2015-06-04 19:15:16', 1545533, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
INSERT INTO `r_resume` VALUES (2195385, '女', '1990', '1年', '199999999', 'test@testtest01.com', '我目前正在职,正考虑换个新环境', '稻壳儿', 'wps', '2015-06-04 19:28:53', 'images/myresume/default_headpic.png', 0, '2015-06-04 19:28:53', 1545534, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
INSERT INTO `r_resume` VALUES (2195386, '女', '1990', '1年', '199999999', 'test@testtest01.com', '我目前正在职,正考虑换个新环境', '稻壳儿', 'wps', '2015-06-04 19:46:42', 'images/myresume/default_headpic.png', 0, '2015-06-04 19:46:45', 1545535, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
INSERT INTO `r_resume` VALUES (2195387, '女', '1990', '1年', '199999999', 'test@testtest01.com', '我目前正在职,正考虑换个新环境', '稻壳儿', 'wps', '2015-06-04 19:48:16', 'images/myresume/default_headpic.png', 0, '2015-06-04 19:48:16', 1545536, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3);
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;
工程环境
父工程lagou-parent
-
pom.xml
<?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>org.example</groupId> <artifactId>spring-cloud-netflix</artifactId> <version>1.0-SNAPSHOT</version> <!--父工程打包方式为pom--> <packaging>pom</packaging> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.6.RELEASE</version> </parent> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Greenwich.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <!--web依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--⽇志依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </dependency> <!--测试依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!--lombok⼯具--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.4</version> <scope>provided</scope> </dependency> <!-- Actuator可以帮助你监控和管理Spring Boot应⽤--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!--热部署--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> </dependency> <!--引⼊Jaxb,开始--> <dependency> <groupId>com.sun.xml.bind</groupId> <artifactId>jaxb-core</artifactId> <version>2.2.11</version> </dependency> <dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> </dependency> <dependency> <groupId>com.sun.xml.bind</groupId> <artifactId>jaxb-impl</artifactId> <version>2.2.11</version> </dependency> <dependency> <groupId>org.glassfish.jaxb</groupId> <artifactId>jaxb-runtime</artifactId> <version>2.2.10-b140310.1920</version> </dependency> <dependency> <groupId>javax.activation</groupId> <artifactId>activation</artifactId> <version>1.1.1</version> </dependency> <!--引⼊Jaxb,结束--> </dependencies> <build> <plugins> <!--编译插件--> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>11</source> <target>11</target> <encoding>utf-8</encoding> </configuration> </plugin> <!--打包插件--> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
简历微服务
-
pom.xml
<dependencies> <!--Spring Data Jpa--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> common模块坐标 </dependency> </dependencies>
-
实体类,放到common模块
package org.example.pojo; import lombok.Data; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; @Data @Entity @Table(name="r_resume") public class Resume { @Id private Long id; private String sex; private String birthday; @Column(name = "work_year") private String workYear; private String phone; private String email; private String status; private String resumeName; private String name; private String createTime; private String headPic; private Integer isDel; private String updateTime; private Long userId; private Integer isDefault; private String highestEducation; private Integer deliverNearByConfirm; private Integer refuseCount; private Integer markCanInterviewCount; private Integer haveNoticeInterCount; private String oneWord; private String liveCity; private Integer resumeScore; private Integer userIdentity; private Integer isOpenResume; }
-
Dao层接口
package org.example.dao; import org.example.pojo.Resume; import org.springframework.data.jpa.repository.JpaRepository; public interface ResumeDao extends JpaRepository<Resume, Long> { }
-
Service层接口和实现类
package org.example.service; import org.example.pojo.Resume; public interface ResumeService { Resume findDefaultResumeByUserId(Long userId); }
package org.example.service.impl; import org.example.pojo.Resume; import org.example.dao.ResumeDao; import org.example.service.ResumeService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Example; import org.springframework.stereotype.Service; @Service public class ResumeServiceImpl implements ResumeService { @Autowired private ResumeDao resumeDao; @Override public Resume findDefaultResumeByUserId(Long userId) { Resume resume = new Resume(); resume.setId(userId); Example<Resume> resumeExample = Example.of(resume); return resumeDao.findOne(resumeExample).get(); } }
-
Controller控制层
package org.example.controller; import org.example.service.ResumeService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/resume") public class ResumeController { @Autowired private ResumeService resumeService; @RequestMapping("/openstate/{userId}") public Integer findDefaultResumeState(@PathVariable Long userId) { return resumeService.findDefaultResumeByUserId(userId).getIsOpenResume(); } }
-
Spring Boot 启动类
package org.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @SpringBootApplication @EntityScan("org.example.pojo") public class ResumeApplication8080 { public static void main(String[] args) { SpringApplication.run(ResumeApplication8080.class, args); } }
-
yml配置文件
server: port: 8080 spring: application: name: service-resume # 配置应用的名称 datasource: # 配置数据库信息 serverTimezone=UTC避免时区错误 url: jdbc:mysql://localhost:3306/lagou_spring_cloud?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC driver-class-name: com.mysql.jdbc.Driver username: root password: root jpa: # 配置jpa信息 database: mysql # 数据库类型 show-sql: true # 是否显示执行的sql hibernate: # jpa底层是使用hibernate作为实现 naming: physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl # 避免将驼峰命名转换为下划线命名
自动投递微服务
-
yml配置文件
server: port: 8090
-
Controller控制层
package org.example.controller; import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import java.util.List; /** * @Author zhang yong jun * @time 2020/8/25 15:51 */ @RestController @RequestMapping("/autodeliver") public class AutodeliverController { @Autowired private RestTemplate restTemplate; // spring 对restful编程风格的支持 // URL常见的参数模式:http://localhost:8090/autodeliver/checkState?userId=12356 // 将参数做为请求路径的写法:http://localhost:8090/autodeliver/checkState/12356 @RequestMapping("/checkState/{userId}") public Integer findResumeOpenState(@PathVariable(value = "userId") Long userId) { // http方式调用 Integer forObject = restTemplate.getForObject("http://localhost:8080/resume/openstate/" + userId, Integer.class); System.out.println("[autodeliver]微服务调用[resume]微服务的[openstate]方法返回结果:" + forObject); return forObject; } }
-
Spring Boot 启动类
@SpringBootApplication public class AutodeliverApplication8090 { public static void main(String[] args) { SpringApplication.run(AutodeliverApplication8090.class, args); } }
测试
- 启动自动投递微服务和简历微服务
- 通过浏览器访问自动投递微服务的findResumeOpenState方法,可以发现通过http调用了简历微服务
问题分析
- 接口调用问题,自动投递微服务通过http方式硬编码了请求简历微服务的地址,不方便维护
- 简历微服务目前只有一个服务,但是如果是集群,那么如何分配请求成为难题,负载均衡难以处理
- 自动投递微服务不清楚简历微服务的状态,服务是否可用
- 如果调用简历微服务出现异常,如何处理
- RestTemplate请求方式如何优化
- 各个微服务之间如何统一认证
- 多个服务之间的功能配置如何管理,如何动态修改立即生效
问题解决
- 服务管理:自动注册、发现与监控服务状态
- 服务负载均衡
- 熔断
- 远程过程调用
- 网关统一拦截、路由、认证
- 配置中心
对于以上问题,Spring Cloud都提供了对应的解决方案,下面我们慢慢改造这个案例,逐步引入Spring Cloud组件。
第二代Spring Cloud核心组件
说明:由于篇幅有限,这里的案例改造我们只演示Nacos服务注册中心和Dubbo远程调用组件。更多的技术知识学习可以加入拉勾Java高薪训练营
Nacos服务注册
什么是Nacos
Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。
Nacos地图
Nacos生态图
基本架构
-
服务 (Service)
服务是指一个或一组软件功能(例如特定信息的检索或一组操作的执行),其目的是不同的客户端可以为不同的目的重用(例如通过跨进程的网络调用)。Nacos 支持主流的服务生态,如 Kubernetes Service、gRPC|Dubbo RPC Service 或者 Spring Cloud RESTful Service.
-
服务注册中心 (Service Registry)
服务注册中心,它是服务,其实例及元数据的数据库。服务实例在启动时注册到服务注册表,并在关闭时注销。服务和路由器的客户端查询服务注册表以查找服务的可用实例。服务注册中心可能会调用服务实例的健康检查 API 来验证它是否能够处理请求。
-
服务元数据 (Service Metadata)
服务元数据是指包括服务端点(endpoints)、服务标签、服务版本号、服务实例权重、路由规则、安全策略等描述服务的数据
-
服务提供方 (Service Provider)
是指提供可复用和可调用服务的应用方
-
服务消费方 (Service Consumer)
是指会发起对某个服务调用的应用方
-
配置 (Configuration)
在系统开发过程中通常会将一些需要变更的参数、变量等从代码中分离出来独立管理,以独立的配置文件的形式存在。目的是让静态的系统工件或者交付物(如 WAR,JAR 包等)更好地和实际的物理运行环境进行适配。配置管理一般包含在系统部署的过程中,由系统管理员或者运维人员完成这个步骤。配置变更是调整系统运行时的行为的有效手段之一。
-
配置管理 (Configuration Management)
在数据中心中,系统中所有配置的编辑、存储、分发、变更管理、历史版本管理、变更审计等所有与配置相关的活动统称为配置管理。
-
名字服务 (Naming Service)
提供分布式系统中所有对象(Object)、实体(Entity)的“名字”到关联的元数据之间的映射管理服务,例如 ServiceName -> Endpoints Info, Distributed Lock Name -> Lock Owner/Status Info, DNS Domain Name -> IP List, 服务发现和 DNS 就是名字服务的2大场景。
-
配置服务 (Configuration Service)
在服务或者应用运行过程中,提供动态配置或者元数据以及配置管理的服务提供者。
逻辑架构
- 服务管理:实现服务CRUD,域名CRUD,服务健康状态检查,服务权重管理等功能
- 配置管理:实现配置管CRUD,版本管理,灰度管理,监听管理,推送轨迹,聚合数据等功能
- 元数据管理:提供元数据CURD 和打标能力
- 插件机制:实现三个模块可分可合能力,实现扩展点SPI机制
- 事件机制:实现异步化事件通知,sdk数据变化异步通知等逻辑
- 日志模块:管理日志分类,日志级别,日志可移植性(尤其避免冲突),日志格式,异常码+帮助文档
- 回调机制:sdk通知数据,通过统一的模式回调用户处理。接口和数据结构需要具备可扩展性
- 寻址模式:解决ip,域名,nameserver、广播等多种寻址模式,需要可扩展
- 推送通道:解决server与存储、server间、server与sdk间推送性能问题
- 容量管理:管理每个租户,分组下的容量,防止存储被写爆,影响服务可用性
- 流量管理:按照租户,分组等多个维度对请求频率,长链接个数,报文大小,请求流控进行控制
- 缓存机制:容灾目录,本地缓存,server缓存机制。容灾目录使用需要工具
- 启动模式:按照单机模式,配置模式,服务模式,dns模式,或者all模式,启动不同的程序+UI
- 一致性协议:解决不同数据,不同一致性要求情况下,不同一致性机制
- 存储模块:解决数据持久化、非持久化存储,解决数据分片问题
- Nameserver:解决namespace到clusterid的路由问题,解决用户环境与nacos物理环境映射问题
- CMDB:解决元数据存储,与三方cmdb系统对接问题,解决应用,人,资源关系
- Metrics:暴露标准metrics数据,方便与三方监控系统打通
- Trace:暴露标准trace,方便与SLA系统打通,日志白平化,推送轨迹等能力,并且可以和计量计费系统打通
- 接入管理:相当于阿里云开通服务,分配身份、容量、权限过程
- 用户管理:解决用户管理,登录,sso等问题
- 权限管理:解决身份识别,访问控制,角色管理等问题
- 审计系统:扩展接口方便与不同公司审计系统打通
- 通知系统:核心数据变更,或者操作,方便通过SMS系统打通,通知到对应人数据变更
- OpenAPI:暴露标准Rest风格HTTP接口,简单易用,方便多语言集成
- Console:易用控制台,做服务管理、配置管理等操作
- SDK:多语言sdk
- Agent:dns-f类似模式,或者与mesh等方案集成
- CLI:命令行对产品进行轻量化管理,像git一样好用
Nacos功能特性
-
服务发现和服务健康监测
Nacos 支持基于 DNS 和基于 RPC 的服务发现。服务提供者使用 原生SDK、OpenAPI、或一个独立的Agent TODO注册 Service 后,服务消费者可以使用DNS TODO 或HTTP&API查找和发现服务。
Nacos 提供对服务的实时的健康检查,阻止向不健康的主机或服务实例发送请求。Nacos 支持传输层 (PING 或 TCP)和应用层 (如 HTTP、MySQL、用户自定义)的健康检查。 对于复杂的云环境和网络拓扑环境中(如 VPC、边缘网络等)服务的健康检查,Nacos 提供了 agent 上报模式和服务端主动检测2种健康检查模式。Nacos 还提供了统一的健康检查仪表盘,帮助您根据健康状态管理服务的可用性及流量。
-
动态配置服务
动态配置服务可以让您以中心化、外部化和动态化的方式管理所有环境的应用配置和服务配置。
动态配置消除了配置变更时重新部署应用和服务的需要,让配置管理变得更加高效和敏捷。
配置中心化管理让实现无状态服务变得更简单,让服务按需弹性扩展变得更容易。
Nacos 提供了一个简洁易用的UI帮助您管理所有的服务和应用的配置。Nacos 还提供包括配置版本跟踪、金丝雀发布、一键回滚配置以及客户端配置更新状态跟踪在内的一系列开箱即用的配置管理特性,帮助您更安全地在生产环境中管理配置变更和降低配置变更带来的风险。
-
动态 DNS 服务
动态 DNS 服务支持权重路由,让您更容易地实现中间层负载均衡、更灵活的路由策略、流量控制以及数据中心内网的简单DNS解析服务。动态DNS服务还能让您更容易地实现以 DNS 协议为基础的服务发现,以帮助您消除耦合到厂商私有服务发现 API 上的风险。
-
服务及其元数据管理
Nacos 能让您从微服务平台建设的视角管理数据中心的所有服务及元数据,包括管理服务的描述、生命周期、服务的静态依赖分析、服务的健康状态、服务的流量管理、路由及安全策略、服务的 SLA 以及最首要的 metrics 统计数据。
Nacos单例服务部署
# 下载Nacos的tar.gz安装包并解压
# linux启动
sh startup.sh -m shandalone
# windows启动
cmd startup.cmd
# 访问地址
http://localhost:8848/nacos
http://localhost:8848/nacos/#/login
# 账号密码
nacos/nacos
引入Nacos服务注册中心
简历微服务改造
-
在parent中引入SCA依赖
<dependencyManagement> <dependencies> <!--SCA --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2.1.0.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
-
简历微服务引入nacos依赖
<dependencies> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> </dependencies>
-
application.yml修改,添加nacos配置
server: port: 8084 # 应用端口,resumer使用8080-8089区间的端口 spring: application: name: service-resume # 配置应用的名称 datasource: # 配置数据库信息 serverTimezone=UTC避免时区错误 url: jdbc:mysql://localhost:3306/lagou_spring_cloud?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC driver-class-name: com.mysql.jdbc.Driver username: root password: root jpa: # 配置jpa信息 database: mysql # 数据库类型 show-sql: true # 是否显示执行的sql hibernate: # jpa底层是使用hibernate作为实现 naming: physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl # 避免将驼峰命名转换为下划线命名 cloud: # nacos注册中心配置 nacos: discovery: server-addr: 127.0.0.1:8848
操作启动简历微服务,打开nacos控制台,可以看到简历微服务的注册信息
挑战一下
- 自己尝试一下将自动投递微服务改造并注册到Nacos注册中心
引入Dubbo RPC调用
-
提起Dubbo服务接口工程,lagou-service-dubbo-api
// 由于简历微服务和自动投递微服务需要通一个接口,所以抽取为公共模块 public interface ResumeService { Integer findDefaultResumeByUserId(Long userId); }
简历微服务改造
-
pom.xml引入spring cloud和dubbo依赖
<!--功能接口:根据用户id查询该用户默认简历的公开状态--> <dependencies> <dependency> <groupId>com.lagou.edu</groupId> <artifactId>lagou-service-common</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!--spring cloud+dubbo 依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-dubbo</artifactId> </dependency> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-apache-dubbo-adapter</artifactId> </dependency> <!--dubbo 服务接口依赖--> <dependency> <groupId>com.lagou.edu</groupId> <artifactId>lagou-service-dubbo-api</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies>
-
ResumeService接口实现类
import com.lagou.edu.dao.ResumeDao; import com.lagou.edu.pojo.Resume; import com.lagou.edu.service.ResumeService; import org.apache.dubbo.config.annotation.Service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Example; @Service // 这个是dubbo的注解 public class ResumeServiceImpl implements ResumeService { @Autowired private ResumeDao resumeDao; @Override public Integer findDefaultResumeByUserId(Long userId) { Resume resume = new Resume(); resume.setUserId(userId); // 查询默认简历 resume.setIsDefault(1); Example<Resume> example = Example.of(resume); return resumeDao.findOne(example).get().getIsOpenResume(); } }
-
application.yml
server: port: 8084 spring: application: name: lagou-service-resume datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/lagou?useUnicode=true&characterEncoding=utf8 username: root password: 123456 jpa: database: MySQL show-sql: true hibernate: naming: physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl #避免将驼峰命名转换为下划线命名 # nacos配置 cloud: nacos: discovery: # 集群中各节点信息都配置在这里(域名-VIP-绑定映射到各个实例的地址信息) server-addr: 127.0.0.1:8848 main: allow-bean-definition-overriding: true management: endpoints: web: exposure: include: "*" dubbo: scan: # dubbo 服务扫描基准包 base-packages: com.lagou.edu.service.impl protocol: # dubbo 协议 name: dubbo # dubbo 协议端口( -1 表示自增端口,从 20880 开始) port: -1 host: 127.0.0.1 registry: # 挂载到 Spring Cloud 的注册中心 address: spring-cloud://localhost
自动投递微服务改造
-
pom.xml引入spring cloud和dubbo依赖
<!--功能接口:根据用户id查询该用户默认简历的公开状态--> <dependencies> <dependency> <groupId>com.lagou.edu</groupId> <artifactId>lagou-service-common</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!--spring cloud+dubbo 依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-dubbo</artifactId> </dependency> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-apache-dubbo-adapter</artifactId> </dependency> <!--dubbo 服务接口依赖--> <dependency> <groupId>com.lagou.edu</groupId> <artifactId>lagou-service-dubbo-api</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies>
-
改造Controller
package com.lagou.edu.controller; import com.alibaba.csp.sentinel.annotation.SentinelResource; import com.alibaba.csp.sentinel.slots.block.BlockException; import com.lagou.edu.config.SentinelHandlersClass; import com.lagou.edu.service.ResumeService; import org.apache.dubbo.config.annotation.Reference; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/autodeliver") public class AutodeliverController { @Reference private ResumeService resumeService; @GetMapping("/checkState/{userId}") @SentinelResource(value = "findResumeOpenState",blockHandlerClass = SentinelHandlersClass.class, blockHandler = "handleException",fallbackClass = SentinelHandlersClass.class,fallback = "handleError") public Integer findResumeOpenState(@PathVariable Long userId) { return resumeService.findDefaultResumeByUserId(userId); } }
操作启动自动投递微服务,通过浏览器发起调用,看是否可以正常调用简历微服务
s.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/autodeliver")
public class AutodeliverController {
@Reference
private ResumeService resumeService;
@GetMapping("/checkState/{userId}")
@SentinelResource(value = "findResumeOpenState",blockHandlerClass = SentinelHandlersClass.class,
blockHandler = "handleException",fallbackClass = SentinelHandlersClass.class,fallback = "handleError")
public Integer findResumeOpenState(@PathVariable Long userId) {
return resumeService.findDefaultResumeByUserId(userId);
}
}
**操作**启动自动投递微服务,通过浏览器发起调用,看是否可以正常调用简历微服务