今天开始,我和大家一起,从0开始,基于Spring Cloud Alibaba,搭建一套基本的微服务架构的项目。
主要用到下面的知识内容
之所以这么干,我还是考虑到一个定制化和灵活性的问题,在真正的大企业中,其认证授权体系是非常个性化的,如果用了某项技术,往往会制约这种个性化的实现。好吧,我这里主要想吐槽的就是 Spring Security太复杂了,做一个简单的功能,要理解的概念太多了。既然RBAC理论和OAuth2.0协议本身已经非常成熟了,我们根据自己的需要,手撸一个也不是什么大问题
首先,微服务本身,是一种软件设计,软件架构的思想,并不是具体的某一种技术。可千万别把微服务和Spring Cloud给划上等号。
我们先来说说传统的单体应用,也就是在一个项目中,把所有的代码都写在一个应用中。
这么做其实在后续功能不断迭代之后,在软件管理和设计上,是会带来种种问题的。
1、应用整体挂机的概率增强
我们写代码的时候要有一个底线思维,就是要把情况往糟糕的情况下去考虑,然后给出解决方案。
只有这样严格要求自己,我们的代码才会更加健壮。
但即使是这样,只要是人,就会犯错,并且,一个项目中的代码,迭代几轮之后,不知道经了多少人的手。
保不齐就有个小伙伴埋了个雷,一旦这个雷爆发,那么我们整个系统也就挂了。
2、技术受限制
当我们在已有的应用中添加功能代码的时候,是需要受当前应用的限制的。
比如说,我举个极端的例子,这个项目比较老了,使用的是Spring2.5,而现在我们做开发一般至少都用JDK8.
那么很不幸,Spring2.5和JDK8是不兼容的。于是我们就没法使用JDK1.8提供的各种实用的特性。
3、部署复杂
这个部署复杂,是针对代码量急剧增加之后来说的。
随着代码量越来越庞大,不管是启动还是测试,上线部署,都将变得越来越慢。
有时候明明改的是一个很小的功能,但是没办法,我们就是需要整个项目都部署一遍
与单体应用相对的,就是分布式系统。
所谓分布式,就是说我们把一个项目,按照一定的划分原则,将各个功能的实现拆开来,部署的时候分开部署。
应用之间通过约定的方式调用。一般是http协议或者rpc协议(如 gRPC、thrift、Dubbo等)。
当我们按照功能拆开后进行部署和开发,带来的好处是很明显的,至少把上面讲的,单体应用的几个缺点,是都给解决掉了。
但是同时,也引入了新的问题
1、事务怎么办,在单体应用中,一个进程中操作一个数据库,回滚是非常方便的,直接使用数据库的特性就可以实现回滚
2、基础功能重复开发怎么办?即如何复用?各个模块中有些功能可能是公用的,一旦拆开后,怎么复用这些代码?
3、测试困难,原先只要启动一个应用就够了,现在如果一个功能的测试,依赖于多个模块,那么相关模块就都要运行起来
但是,后端开发进入现在这个阶段,我们对大数据量高并发的要求,相对于分布式系统的一点点缺点,其带来的好处是不言而喻的。
所以,现在基本上一家企业如果已经过了市场验证阶段,没理由继续使用单体应用,肯定都是要改造成分布式系统的。
那么什么是微服务呢?
微服务肯定是分布式的。那么微服务和分布式系统又有什么区别呢?
以我自己不成熟的看法来说的话,所谓微服务,除了解决分布式系统领域的问题之外,其特点,重在一个微。
怎么样的一个服务,可以称之为微服务呢?这个才是难点。一个系统中,我们怎么拆分,不管是从业务的角度还是技术的角度,各个应用单元之间如何协作,这个才是微服务设计中最最复杂的部分,技术上的难点,其实互联网上的大厂,基本上都已经有解决方案了。
微服务设计6大原则
既然微服务有这么多优点,单体应用有那么多缺点,我们在实际项目中到底选择使用哪种开发模式呢?
这个其实和公司当前的实际情况是挂钩的,我们做开发的,一定要理清楚一点,技术,是为业务服务的,或者说是为了需求服务的。
千万不要为了技术而技术。如果一家公司,刚起步,人手不足、业务量也不大、技术实力也不够雄厚,这个时候贸贸然跟风上微服务,这不就是自己给自己找事儿做吗?
当公司业务上来了,团队规模也起来了,人员配备足够覆盖微服务本身的规模,能够驾驭了,这个时候,才能够切实感受到微服务对整个系统带来的好处。
Spring Cloud,就是针对微服务的一套实现。
比较著名的所有 Spring Cloud Netflix和Spring Cloud Alibaba
以上讲的都是一些概念性质的东西,具体怎么开发已经详细的理论,我会在后续实战部分,逐个进行讲解。
我们先来对这一整个微服务体系,进行整体设计。
第一张图,是我在网上找的一个关于微服务架构的社交
第二张图,我重新精简了一下,基本上就划分为网关、认证中心、其他业务模块这几种类型。
我们创建了一个项目来进行依赖管理,其他大多数pom关系不大,
不过,对于Spring Cloud、Spring Cloud Alibaba、SpringBoot,这三者之间的版本,是有关联的。
Spring Cloud Alibaba的版本与Spring Boot一致
参见:https://start.spring.io/actuator/info
https://gitee.com/test-qqqq/spring-cloud-alibaba
Spring Cloud的版本与Spring Boot 一致
参考:https://spring.io/projects/spring-cloud/#overview
Spring Cloud Alibaba及其组件(Dubbo、Seata、Sentinel、Nacos),他们之间的版本也是有关联的。
我们在配置这几个组件的版本的时候,最好先去网上查一下,他们是怎么搭配的。
一般这种配置,除非是大版本的更新,否则一旦配置好后,就不要去修改了,避免引起不必要麻烦
多个服务之间在相互调用的时候,是需要建立连接的。
那么他们怎么知道对方部署时的ip和port是多少呢?
这个时候,就要用到注册中心。
注册中心主要是用来服务注册和发现的
参考
git clone https://gitee.com/test-qqqq/nacos-docker.git
我这里就直接把nacos-docker clone到chan项目了,但是,我把nacos-docker加入了git忽略列表,不提交。
之所以将nacos-docker放到chan中,只是为了将所有与项目有关的物料,都放在此。
cd nacos-docker
# 启动单机版,用于开发测试足矣,生产上如果是部署在阿里云环境,可以购买服务,也不需要自己构建
docker-compose -f example/standalone-mysql-5.7.yaml up -d
# 停止
docker-compose -f example/standalone-mysql-5.7.yaml down
不过,这个standalone-mysql-5.7.yaml,里面的镜像速度超有点慢,我把他们都上传到自己的镜像仓库了,然后修改yaml文件如下
version: "2"
services:
nacos:
image: registry.cn-hangzhou.aliyuncs.com/sherry/nacos-server:latest
container_name: nacos-standalone-mysql
env_file:
- ../env/nacos-standlone-mysql.env
volumes:
- ./standalone-logs/:/home/nacos/logs
- ./init.d/custom.properties:/home/nacos/init.d/custom.properties
ports:
- "8848:8848"
- "9555:9555"
depends_on:
- mysql
restart: on-failure
mysql:
container_name: mysql
image: registry.cn-hangzhou.aliyuncs.com/sherry/nacos-mysql:5.7
env_file:
- ../env/mysql.env
volumes:
- ./mysql:/var/lib/mysql
ports:
- "3306:3306"
prometheus:
container_name: prometheus
image: registry.cn-hangzhou.aliyuncs.com/sherry/prometheus:latest
volumes:
- ./prometheus/prometheus-standalone.yaml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"
depends_on:
- nacos
restart: on-failure
grafana:
container_name: grafana
image: registry.cn-hangzhou.aliyuncs.com/sherry/grafana:latest
ports:
- 3000:3000
restart: on-failure
http://localhost:8848/nacos
默认账号密码均为nacos
1、编写一个正常的接口
@RequestMapping("/user")
@Slf4j
@RestController
public class UserController {
@GetMapping(params = "userId")
public R userInfo(@RequestParam String userId) {
return R.ok("返回id为" + userId + "的用户信息");
}
}
2、在api模块中暴露此接口
@FeignClient(name = "chan-user-svc")
public interface UserServiceApi {
/**
* 查询
*
* @param userId
* @return
*/
@GetMapping("/user")
R user(@RequestParam(name = "userId") String userId);
}
1、引入user-api模块
<dependency>
<groupId>com.zhanglngroupId>
<artifactId>chan-user-apiartifactId>
<version>${project.parent.version}version>
dependency>
2、auth模块调用user
3、auth模块中打开Feign支持
package com.zhangln.chan.gateway.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.util.pattern.PathPatternParser;
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedMethod("*");
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
spring:
application:
name: chan-gateway-svc
main:
allow-bean-definition-overriding: true
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
discovery:
locator:
enabled: true
routes:
- id: auth_route
uri: lb://chan-auth-svc
predicates:
- Path=/auth/**
- id: user_route
uri: lb://chan-user-svc
predicates:
- Path=/user/**
/**
* 日志记录
*
* @author sherry
* @description
* @date Create in 2020/8/22
* @modified By:
*/
@Component
@Slf4j
public class LogFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
GatewayLogDto gatewayLogDto = new GatewayLogDto();
gatewayLogDto.setId(UUID.randomUUID().toString().replace("-", ""));
gatewayLogDto.setReqTime(LocalDateTime.now());
log.info("{},日志过滤器--start", gatewayLogDto.getId());
ServerHttpRequest serverHttpRequest = exchange.getRequest();
ServerHttpResponse serverHttpResponse = exchange.getResponse();
gatewayLogDto.setUri(serverHttpRequest.getURI().getRawPath());
HttpHeaders headers = serverHttpRequest.getHeaders();
gatewayLogDto.setHeaderMap(headers);
gatewayLogDto.setHost(headers.getHost().getHostName());
MultiValueMap<String, String> queryParams = serverHttpRequest.getQueryParams();
gatewayLogDto.setParamMap(queryParams);
String method = serverHttpRequest.getMethodValue();
gatewayLogDto.setMethod(method);
String contentType = serverHttpRequest.getHeaders().getFirst("Content-Type");
//向headers中放文件,记得build
ServerHttpRequest host = null;
try {
URI newUri = null;
// 对URL参数进行URL编码
if (!Objects.isNull(queryParams)) {
StringBuilder query = new StringBuilder();
Set<String> keySet = queryParams.keySet();
for (Iterator<String> iterator = keySet.iterator(); iterator.hasNext(); ) {
String k = iterator.