SpringCloud微服务入门
目前企业中最流行的系统开发架构是什么?这个问题的答案无疑就是微服务。现在企业中开启一个新的项目,绝大部分都是选用微服务技术作为系统开发的架构。那么什么是微服务?如何使用微服务?
认识微服务
单体架构
我们平时做的项目就是单体架构的项目。单体架构就是将业务的所有功能集中在一个项目中开发,打成一个包部署。这种架构模式的优点室是架构简单,部署成本低。但是缺点是耦合度比较高。
随着项目的功能越来越多,应用复杂度越来越大,缺点也很明显,体现在以下几点:
- 系统维护困难:随着功能复杂度增加,整个项目变得臃肿,功能模块之间的边界会越来越模糊,牵一发而动全身。
- 应用可靠性较低:所有功能模块耦合在一起,如果任意一个模块出现了bug,可能会导致整个应用崩溃。
- 系统扩展受限:单体系统遇到性能瓶颈问题,只能整体横向扩展,增加服务实例,进行负载均衡分担压力,影响范围大,无法根据业务模块的需要进行单个模块的伸缩。
- 开发效率降低:每次发布都是整个系统进行发布,代码量越来越多,发布时间延长,不利于系统的更新迭代和敏捷开发。
- 技术选型单一:所有功能模块都必须使用用同一种技术,这样模块之间才能互相调用,不利于技术的升级扩展
那么如何解决以上的问题呢?
举个例子,一个饭店刚开始的时候规模较小,客户较少,那么洗菜、切菜、做菜交给一个厨师就可以了,随着客户越来越多,那么一个厨师肯定是忙不过来的,那么怎么办呢?饭店可以多雇几个人手,一个人洗菜、一个人切菜,一个人做菜,这样,上菜的效率就会提升。
因此,对于单体系统也是一样,我们可以将单体应用按照业务功能进行拆分,每个功能模块都是一个单体应用,可以独立开发,单独部署。这种架构模式我们也称之为分布式架构。
分布式架构
分布式架构的优点就是可以降低服务之间的耦合,有利于服务的升级扩展。
- 功能模块边界清晰
- 如果其中一个服务出现了问题,不会影响其他服务的运行,稳定性增强
- 如果系统要扩展一个功能(库存功能),只需要再创建一个单体项目就可以,不会影响其他项目
- 如果某个服务的用户访问量较大,只需要对该服务进行集群就可以了,节约了服务器资源
- 每个服务都可以使用不同的技术,部署互不影响,方便服务的迭代更新
分布式架构在发展过程中,有很多实现方案,目前最火的就是微服务,下面我们介绍下微服务。
微服务架构
微服务是一种经过良好架构设计的分布式架构方案,具有如下特征
:
•单一职责:每个服务按照业务进行更细粒度的划分;
•面向服务:对外提供服务接口,服务之间通过轻量级机制(通常是HTTP资源的API)进行通信;
•技术独立:可以使用不同语言,不同的技术进行开发;
•数据独立:拥有独立的数据库,可以使用不同的数据存储技术;
•部署独立:服务可独立部署,独立扩展,服务之间互相不影响
通过上面的介绍,通过微服务可以实现一个可扩展、可伸缩、高可用的系统,但是微服务说起来容易,真正落地实施还是会有很多的问题。
•服务地址如何维护?
•服务之间如何实现远程调用?
•服务健康状态如何感知?
微服务实现技术
全球的互联网公司都在积极尝试自己的微服务落地技术。在国内最知名的就是SpringCloud和阿里巴巴的Dubbo。
无论是哪一种实现技术,都是为了实现微服务能够落地。因此包含的组件和实现的功能基本是一致的。
首先,你需要按照业务功能进行服务的拆分,每个服务都需要对外提供接口,不同的技术可能实现接口的方式不同,无论是哪一种技术,服务的调用都是错综复杂的,一定需要我们去维护,但是依靠人来维护肯定不行,所以在微服务中往往会有一个注册中心,它可以去维护每个服务的地址信息,并且去监控这些服务的状态。
另外,将来微服务越来越多,如果服务的配置需要修改,手动的修改配置显然不现实,因此,在微服务里面还需要有一个配置中心,可以统一管理微服务集群的配置,微服务可以监控到配置中心的变化,从而实现服务的热更新。
将来,微服务要部署上线,用户也需要访问,如此之多的微服务,用户怎么知道该访问哪一个呢,因此,微服务中还需要有一个统一的服务网关作为入口,用户可以去访问网关,网关把请求路由到我们的微服务群,在路由的过程中可以去做负载均衡来分发请求,同时,在服务之间调用的时候,还需要做好服务的容错处理,避免因为某个服务故障造成级联失败和雪崩,可以通过服务之间的隔离、降级等措施来保护我们的微服务。
通过上面的分析,微服务实现过程中需要包含远程调用、注册中心、配置中心、服务网关和服务保护这些功能。这些功能都需要技术来进行实现。SpringCloud和Dubbo在实现的过程中是有差异的。
下面我们来对比一下每一种实现技术在微服务实现上的差异。这里我们对比三种技术:Dubbo、SpringCloud和SpringCloud Alibaba。
- Dubbo早在2012年左右就已经开源出来了,是阿里巴巴公司开源的。那个时候微服务技术在国内还不太流行。所以,我们的Dubbo并不是严格意义上的一个微服务技术,那个时候它的核心是服务远程调用和服务的注册与发现。注册中心也不是Dubbo自己实现的,而是依赖于Redis和zookeeper,而这些技术并不是专业做注册中心的,Redis是做缓存的,zookeeper是做集群管理的,不具有完善的注册中心功能。而服务远程调用才是Dubbo的核心,Dubbo基于TCP协议专门定义了一套标准,就是Dubbo协议,所以基于Dubbo的远程调用,必须基于Dubbo协议实现对应的接口。而配置中心和服务网关在Dubbo中是没有实现的,提供了dubbo-admin进行基本的服务监控,统计服务调用时间和QPS这些数据。所以,Dubbo实现微服务功能非常单一,不完善。
- 到了2015-2017年这段时间,是微服务技术井喷的时候,各种各样的微服务层出不穷,但是没有一个一统江湖的,直到SpringCloud的出现,SpringCloud并不是发明了什么技术,而是整合,它把全球各个公司的开源的微服务技术都给整合起来了,形成了一套完整的微服务技术实现方案。在SpringCloud中,提供了Eureka和Consul两种注册中心,用于实现服务的注册与发现。提供了基于HTTP协议的远程调用组件Feign,减少了学习的成本,我们之前的控制器就是基于HTTP协议的,所以我们通过定义控制器就可以定义我们的接口。同时,在SpringCloud中提供了SpringCloudConfig作为配置中心,SpringCloudGateway和Zuul作为服务网关以及提供Hystrix进行服务保护,提供了服务隔离、熔断和降级的手段。
- 从整体来看,Dubbo和SprngCloud存在比较大的差距,阿里巴巴也认识到了这个问题,因此近几年不断的奋起直追。逐渐实现了自己的各种微服务的组件,形成了一套技术栈,起了名字叫SpringCloudAlibaba,从这个名字上可以看出,这套技术首先是SpringCloud中的一部分,实现了SpringCloud的标准接口的,因此用起来和SpringCloud没什么差别,从列表中可以看到,SpringCloudAlibaba可以兼容Eureka、Feign、SpringCloudConfig,SpringCloudGateway等等这些组件。除此以外,阿里巴巴的最终目的是将Dubbo也整合进来,所以,也实现了自己的注册中心和配置中心Nacos,Nacos的强大之处在于既支持Dubbo的服务调用,也支持Feign组件。因此SpringBootAlibaba同时兼容了Dubbo和SpringCloud两种架构。所以在国内,这套技术越来越火热。
Spring Cloud微服务框架
SpringCloud介绍
SpringCloud是目前国内使用最广泛的微服务框架。官网地址:Spring Cloud。SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验
案例
根据订单id查询订单功能
需求:根据订单id查询订单的同时,把订单所属的用户信息一起返回
sql:
/*Navicat Premium Data TransferSource Server : localSource Server Type : MySQLSource Server Version : 50622Source Host : localhost:3306Source Schema : heimaTarget Server Type : MySQLTarget Server Version : 50622File Encoding : 65001Date: 01/04/2021 14:57:18
*/SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;-- ----------------------------
-- Table structure for tb_order
-- ----------------------------
DROP TABLE IF EXISTS `tb_order`;
CREATE TABLE `tb_order` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '订单id',`user_id` bigint(20) NOT NULL COMMENT '用户id',`name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '商品名称',`price` bigint(20) NOT NULL COMMENT '商品价格',`num` int(10) NULL DEFAULT 0 COMMENT '商品数量',PRIMARY KEY (`id`) USING BTREE,UNIQUE INDEX `username`(`name`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 109 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;-- ----------------------------
-- Records of tb_order
-- ----------------------------
INSERT INTO `tb_order` VALUES (101, 1, 'Apple 苹果 iPhone 12 ', 699900, 1);
INSERT INTO `tb_order` VALUES (102, 2, '雅迪 yadea 新国标电动车', 209900, 1);
INSERT INTO `tb_order` VALUES (103, 3, '骆驼(CAMEL)休闲运动鞋女', 43900, 1);
INSERT INTO `tb_order` VALUES (104, 4, '小米10 双模5G 骁龙865', 359900, 1);
INSERT INTO `tb_order` VALUES (105, 5, 'OPPO Reno3 Pro 双模5G 视频双防抖', 299900, 1);
INSERT INTO `tb_order` VALUES (106, 6, '美的(Midea) 新能效 冷静星II ', 544900, 1);
INSERT INTO `tb_order` VALUES (107, 2, '西昊/SIHOO 人体工学电脑椅子', 79900, 1);
INSERT INTO `tb_order` VALUES (108, 3, '梵班(FAMDBANN)休闲男鞋', 31900, 1);SET FOREIGN_KEY_CHECKS = 1;
/*Navicat Premium Data TransferSource Server : localSource Server Type : MySQLSource Server Version : 50622Source Host : localhost:3306Source Schema : heimaTarget Server Type : MySQLTarget Server Version : 50622File Encoding : 65001Date: 01/04/2021 14:57:18
*/SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;-- ----------------------------
-- Table structure for tb_user
-- ----------------------------
DROP TABLE IF EXISTS `tb_user`;
CREATE TABLE `tb_user` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`username` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '收件人',`address` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '地址',PRIMARY KEY (`id`) USING BTREE,UNIQUE INDEX `username`(`username`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 109 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;-- ----------------------------
-- Records of tb_user
-- ----------------------------
INSERT INTO `tb_user` VALUES (1, '柳岩', '湖南省衡阳市');
INSERT INTO `tb_user` VALUES (2, '文二狗', '陕西省西安市');
INSERT INTO `tb_user` VALUES (3, '华沉鱼', '湖北省十堰市');
INSERT INTO `tb_user` VALUES (4, '张必沉', '天津市');
INSERT INTO `tb_user` VALUES (5, '郑爽爽', '辽宁省沈阳市大东区');
INSERT INTO `tb_user` VALUES (6, '范兵兵', '山东省青岛市');SET FOREIGN_KEY_CHECKS = 1;
1)注册RestTemplate
RestTemplate是Spring提供的用于发送HTTP请求的客户端工具
package com.flowerfog.orderservice;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;@SpringBootApplication
public class OrderServiceApplication {public static void main(String[] args) {SpringApplication.run(OrderServiceApplication.class, args);}@Beanpublic RestTemplate restTemplate(){return new RestTemplate();}}
2)服务远程调用RestTemplate
package com.flowerfog.orderservice.controller;import com.flowerfog.orderservice.entity.Order;
import com.flowerfog.orderservice.entity.User;
import com.flowerfog.orderservice.service.OrderService;
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;
import org.springframework.web.client.RestTemplate;@RestController
public class OrderController {@Autowiredprivate OrderService orderService;@Autowiredprivate RestTemplate restTemplate;@GetMapping("/order/{orderId}")public Order queryOrderByUserId(@PathVariable("orderId") Long orderId) {// 根据id查询订单并返回Order order=orderService.getById(orderId);//根据用户id从用户服务中获取用户信息String url="http://localhost:8081/user/"+order.getUserId();User user = restTemplate.getForObject(url, User.class);order.setUser(user);return order;}
}
在以上案例中,order-service是服务消费者,user-service是服务提供者。通过RestTemplate实现了服务的远程调用,但是,目前这种调用方式还是存在较大问题的。
问题:请求的http地址中包含ip地址和端口号,这种称之为硬编码,这种写法在不同的环境下(开发、测试、生产)都需要修改代码,显然是不方便不合理的,另外,如果用户服务使用了集群,那么会有多台服务器提供服务,服务消费者调用哪一个服务呢?或者服务提供者的健康状态如何感知呢?这些都是在服务调用过程中会遇到的问题?这个时候,Eureka注册中心就可以登场了
Eureka注册中心
Eureka的简单入门我之前就发过了,这里简单带过。
Eureka的工作原理
在Eureka架构中,微服务角色有两类:
Eureka使用案例
【代码实践】使用Eureka解决微服务案例中存在的远程调用的问题。
- 搭建EurekaServer
- 将user-service、order-service都注册到eureka
- 在order-service中完成服务拉取,然后通过负载均衡挑选一个服务,实现远程调用
搭建EurekaServer
-
创建项目,引入依赖
- 配置dependencyManagement节点引入spring-cloud-dependencies,该以来管理着所有SpringCloud组件的版本号
- 引入spring-cloud-starter-netflix-eureka-server的依赖(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.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.5.0</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.flowerfog</groupId><artifactId>EurekaServer</artifactId><version>0.0.1-SNAPSHOT</version><name>EurekaServer</name><description>EurekaServer</description><url/><licenses><license/></licenses><developers><developer/></developers><scm><connection/><developerConnection/><tag/><url/></scm><properties><java.version>17</java.version></properties><dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>2024.0.1</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-server</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><configuration><annotationProcessorPaths><path><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></path></annotationProcessorPaths></configuration></plugin><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes></configuration></plugin></plugins></build></project>
编写启动类,添加@EnableEurekaServer注解
package com.flowerfog.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);}}
修改application.yml文件,编写下面的配置:
#服务端口号
server:port: 10086
#配置服务名称
spring:application:name: eurekaserver
#注册到Eureka注册中心
eureka:client:service-url:defaultZone: http://127.0.0.1:10086/eureka/
将user-service、order-service都注册到eureka
在user-service项目和order-service项目中
- 配置dependencyManagement节点引入spring-cloud-dependencies,该以来管理着所有SpringCloud组件的版本号
<dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>2025.0.1</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement>
- 引入spring-cloud-starter-netflix-eureka-client的依赖(Eureka客户端)
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
在user-service项目的application.yml文件,编写下面的配置:
spring:application:name: userservice
eureka:client:service-url:defaultZone: http://127.0.0.1:10086/eureka/
在order-service项目的application.yml文件,编写下面的配置:
spring:application:name: orderservice
eureka:client:service-url:defaultZone: http://127.0.0.1:10086/eureka/
另外,我们可以将user-service多次启动, 模拟多实例部署,但为了避免端口冲突,需要修改端口设置:
在order-service中完成服务拉取,然后通过负载均衡挑选一个服务,实现远程调用
//根据用户id从用户服务中获取用户信息String url="http://userservice/user/"+order.getUserId();User user = restTemplate.getForObject(url, User.class);
在order-service项目的启动类OrderServiceApplication中的RestTemplate添加负载均衡注解:
@Bean@LoadBalancedpublic RestTemplate restTemplate(){return new RestTemplate();}