总结
本文将通过Spring Cloud Alibaba和Spring Cloud相关基础组件,搭建一个分布式服务基础框架,使用nacos作为注册中心,gateway作为入口网关,通过JWT标准完成统一认证,使用sentinel进行流量控制与熔断降级,seata进行分布式事务(seata使用AT场景,也实践了RocketMQ事务消息),redisson进行分布式锁。整个项目对相关分布式技术进行覆盖使用,以达到速通和学习的目的。
下一篇:SpringCloud分布式框架&分布式事务&分布式锁
版本选择
这里边由于要使用到Spring Cloud Alibaba相关组件,故涉及到与Spring Cloud、Springboot的版本配套,由于我这边使用的还是JDK8,故选择的是2021.0.5.0相关版本。这里Alibaba的版本一般会在Spring Cloud版本的基础上再添加一个子版本。
版本地址:版本发布说明-阿里云Spring Cloud Alibaba官网
项目搭建
项目结构
springcloud-demo // 父项目
-- gateway // 网关项目
-- open-api // 服务间开放接口
-- services // 服务父项目
-- -- order-service // 订单服务
-- -- product-service // 商品服务
-- -- user-service // 用户服务springcloud-demo父项目pom引入的依赖,这里主要是确定Springboot、SpirngCloud和SpringCloud Alibaba相关版本,保证整个项目的相关版本一致。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.13</version>
</parent>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-cloud.version>2021.0.5</spring-cloud.version>
<spring-cloud.alibaba.version>2021.0.5.0</spring-cloud.alibaba.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud.alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>services父项目引入各服务相关的依赖
<dependencies>
<!-- springboot服务依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- springboot web服务依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- springcloud alibaba nacos 依赖,用于服务的注册与发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Json序列化工具,按需引用 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.47</version>
</dependency>
<!-- lombok 数据对象、日志工具包,按需引用 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- springcloud alibaba seata 依赖,用于分布式事务 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<!-- mybatis plus依赖,用于数据库sql操作 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.11</version>
</dependency>
<!-- druid数据连接池依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.24</version>
</dependency>
<!-- mysql连接依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
</dependencies>open-api项目依赖,这里服务间请求主要是feign客户端,以及负载均衡loadbalancer:
<dependencies>
<!-- lombok依赖,用于数据对象set\get方法,日志,按需引入 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- openfeign依赖,用于远程调用-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- loadbalancer依赖,用于远程调用时的负载均衡策略-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- fastjson依赖,对象序列化,按需依赖>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.47</version>
<scope>compile</scope>
</dependency>
<!-- 工具包,用于hash或是加解密的工具包,按需依赖-->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
</dependencies>gateway项目依赖:
<dependencies>
<!-- springboot-web服务依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- gateway依赖,用于网关业务-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- 负载均衡处理依赖,这里是因为我在gateway里边,使用了webClient进行了远程调用,进而引入, 按需依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- nacos依赖, 注册与发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 工具包,按需依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>compile</scope>
</dependency>
<!-- 工具包,按需依赖-->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.47</version>
<scope>compile</scope>
</dependency>
<!-- 单元测试包,按需依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>服务注册与发现
注册中心
进行nacos下载,下载地址:Release 2.2.0 (Dec 14, 2022) · alibaba/nacos · GitHub,在页面下载压缩包。
下载包之后解压,进入bin目录,在当前目录执行cmd,运行命令“startup.cmd -m standalone”,进行单机下启动nacos。启动成功之后,控制台访问地址http://ip:8848/,账号密码默认都是nacos
服务注册与发现
在services项目下的各个服务模块,在引入nacos的包之后,服务的注册与发现也会非常简单,只需要添加相关注解以及配置项即可。
application.yml配置:
server:
port: 8000 # 启动端口
spring:
application:
name: order-service # 当前服务名
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 # nacos的地址application.class类上添加注解
@SpringBootApplication // 启动注解
@EnableDiscoveryClient // 服务注册与发现注解
// 启用feign客户端,以及需要调用的接口扫描路径
@EnableFeignClients(basePackages = {"cn.lonple.api.product.inter"})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}Feign客户端接口定义
// 添加feign客户端,以及指定要访问的服务名,该服务名为application.yml中注册的spring.application.name
@FeignClient(value = "product-service")
public interface ProductAPI {
@GetMapping("/product/detail/{id}")
ProductInfo getProductDetail(@PathVariable("id") Long id);
}自定义负载均衡算法
在SpringCloud高版本里,针对feign客户端调用接口时的负载均衡,已经弃用了ribbon改为使用LoadBalancer,其中支持的负载均衡算法为随机、轮询,以及由nocas提供的基于IP的权重算法,其中默认为轮询,支持自定义。
- 在application.class上添加负载均衡自定义算法
@LoadBalancerClients({
@LoadBalancerClient(name = "order-service", configuration = LoadBalanceConfig.class)
})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}- 定义LoadBalanceConfig类,初始化负载均衡算法类,这里这个类上边不要添加注解。
public class LoadBalanceConfig {
@Bean
public ReactorLoadBalancer<ServiceInstance> reactorLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty("loadbalancer.client.name");
// 初始化自定义负载均衡算法
return new CustomLoadBalance(
loadBalancerClientFactory.getLazyProvider(name,
ServiceInstanceListSupplier.class), name);
}
}
- 实现自定义负载均衡算法,可以直接参考轮询负载均衡算法类的实现,完全复制过来之后,修改主要的负载逻辑即可。
// 实现ReactorServiceInstanceLoadBalancer接口,完全参考RoundRobinLoadBalancer类的实现
public class CustomLoadBalance implements ReactorServiceInstanceLoadBalancer {
// ...... 省略部分代码
// 负载均衡逻辑实现方法
private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {
if (instances.isEmpty()) {
if (log.isWarnEnabled()) {
log.warn("No servers available for service: " + this.serviceId);
}
return new EmptyResponse();
} else {
// 这里是轮询负载均衡算法逻辑,修改此处进行自定义即可。
int index = ThreadLocalRandom.current().nextInt(instances.size());
ServiceInstance instance = (ServiceInstance) instances.get(index);
return new DefaultResponse(instance);
}
}
}网关
在分布式服务场景下,由于后端服务众多,为了统一对外提供服务而不用暴露所有服务应用,可以使用网关进行代理以及请求转发。在pom文件中引入依赖之后,只需要添加相关注解和配置即可完成。
更多使用可以参考:Spring Cloud Gateway 中文文档
网关配置
- application.class添加注解
@SpringBootApplication // 启动服务
@EnableDiscoveryClient // 服务注册与发现
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}- application.yml添加路由配置
server:
port: 7000 # 网关端口
spring:
application:
name: gateway # 网关注册名称
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 # nacos注册地址,这里都是在默认的命名空间public和分组DEFAULT_GROUP
gateway:
routes:
- id: order # 唯一id
uri: lb://order-service # 需要路由的服务名,lb表示使用负载均衡算法
predicates:
- Path=/order/** # 断言,这里只配置了url匹配路基,即/order开头的请求,全部转发给order-service服务
- id: product
uri: lb://product-service
predicates:
- Path=/product/**
- id: user
uri: lb://user-service
predicates:
- Path=/user/**- 网关启动之后,就可以通过访问网络,完成请求转发。
统一认证
在实际项目中,对每一个接口的请求,都会要求进行访问认证,即单点登录之后,处处访问,这里我们通过JWT协议,在网关侧进行校验。
JWT协议
JSON Web Token即一个Json数据格式的web请求token,基于对token的发放与认证,完成单点登录与服务请求放行。JWT分为三个部分,Header(标头)、Payload(载体)和Signature(签名),其中Header和Payload都为JSON数据格式,Signature为Header、Payload进行base64运算之后,进行Hash的结果。整个组装过程如下:
- Header部分内容,一般存储数据格式JWT和签名的算法。
{
"alg" : "SHA-256",
"type" : "JWT"
}- Payload部分,主要携带一些简单信息,比如当前用户,过期时间等,不可携带隐私信息,因为该部分只是做Base64处理,可以被转换成明文。
{
"createTime" : 1744704728709,
"expireTime": 1744704728709,
"name" : "John Do",
"admin" : true
}- 签名部分,先将Header和Payload进行base64处理,然后使用“.”进行拼接,在加载盐值(需要严格保管盐值,一旦泄露,token就可以被任意伪造了)进行Hash处理,后续以同样的方式进行签名校验。(这里只是我处理的方式,比较简单,可以自行定义更安全的处理方式)
Signature = sha256Hex(base64(Header) + "." + base64(Payload) + "." + salt)- 用户通过账号密码登录成功之后,返回token,完整格式如下。
base64(Header).base64(Payload).Signature网关认证
用户完成登录认证之后,后续的请求就会携带token,这时需要在网关侧进行token认证,如果只是简单的JWT认证,在网关侧即可以编码完成验证(需要维护签名的盐值),我这边考虑是将token的生成和验证,都放在了user-service服务,这样的话网关需要发送http请求到用户中心完成token认证。
- 首先创建一个全局的拦截器,对所有请求进行拦截处理。由于gateway底层使用的是响应式编程,这里请求远程服务使用RestTemplate会报错,需要使用webClient。
@Component
// 这里实现GlobalFilter,所有请求都会拦截
// Ordered 对拦截器的执行顺序进行调整,很重要
public class AuthFilter implements GlobalFilter, Ordered {
@Autowired
private UserService userServiceClient;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
URI uri = request.getURI();
if (uri.getPath().contains("/user/auth")) {
return chain.filter(exchange);
} else {
HttpHeaders headers = request.getHeaders();
String token = headers.getFirst("token");
if (token == null || token.isEmpty()) {
return Result.response(exchange, "请先登录", HttpStatus.UNAUTHORIZED.value());
} else {
// 调用用户中心服务,进行token校验
return userServiceClient.userVerify(token, exchange, chain);
}
}
}
@Override
// 这里很重要,这个全局拦截器需要在gateway自身路由的拦截器之前执行,不然通过webClient进行负载均衡请求时,会拿不到远程服务的实例
public int getOrder() {
return -1;
}
}- 初始化一个WebClient的Builder
@Configuration
public class ClientConfig {
@Bean
@LoadBalanced
public WebClient.Builder loadBalancedWebClientBuilder() {
return WebClient.builder();
}
}- 基于WebClient进行远程请求,这里注意WebClient使用的是响应式编程,在获取结果时不可以使用block方法。
private final WebClient userServiceClient;
public UserService(WebClient.Builder builder) {
userServiceClient = builder.baseUrl("http://user-service").build();
}
public Mono<Void> userVerify(String token, ServerWebExchange exchange, GatewayFilterChain chain) {
return userServiceClient
.get().uri("/user/verify")
.header("token", token)
.retrieve() // 获取响应体
.bodyToMono(new ParameterizedTypeReference<Result<Boolean>>() {
}).doOnSuccess(response -> {
// 记录日志(非阻塞操作)
log.info("权限校验结果: {}", response);
})
.flatMap(response -> {
// 业务逻辑
if (response != null && response.getCode() == 200 && response.getData() != null && response.getData()) {
return chain.filter(exchange);
} else {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
exchange.getResponse().setComplete();
return Result.response(exchange, "校验失败", HttpStatus.UNAUTHORIZED.value());
}
}).onErrorResume(e -> {
// 处理异常(如503、超时等)
return Result.response(exchange, "校验服务请求失败", HttpStatus.INTERNAL_SERVER_ERROR.value());
});
}限流与熔断
sentinel安装
下载sentinel包:Release v1.8.6 · alibaba/Sentinel · GitHub ,直接下jar包,通过java -jar即可运行,启动之后的访问地址http://127.0.0.1:8080,账号密码都为sentinel。
接口限流与熔断
1.引入依赖包
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
<version>2021.0.5.0</version>
</dependency>- application.yml添加sentinel的配置信息。
spring:
cloud:
sentinel:
transport:
dashboard: 127.0.0.1:80803.添加接口资源注释,进行接口注册。
// 添加@SentinelResource注解
@SentinelResource(value = "order-detail", fallback = "orderDetailFallback", blockHandler = "orderDetailBlockHandler")
@GetMapping("/detail/{id}")
public OrderInfo getOrderDetail(@PathVariable("id") Long id) {
log.info("accept request : {} , get order : {}", port, id);
return orderService.getOrderDetail(id);
}
public Result<String> orderDetailFallback(Long id) {
return Result.failed("id: " + id + " fallback");
}
public Result<String> orderDetailBlockHandler(Long id, Throwable e) {
return Result.failed("id: " + id + " block");
}
- 进行一次请求之后,在sentinel控制台可以看到请求接口,针对请求接口可以进行流控和熔断配置。