SpringCloud
- 微服务
- 单体框架
- 微服务框架
- SpringCloud
- 微服务拆分
- 微服务差分原则
- 拆分商品服务
- 拆分购物车服务
- 拆分用户服务
- 拆分交易服务
- 拆分支付服务
- 服务调用
- RestTemplate
- 远程调用
- 微服务拆分总结
- 服务治理
- 注册中心
- Nacos注册中心
- 服务注册
- 服务发现
- OpenFeign实现远程调用
- 快速入门
- 引入依赖
- 启用OpenFeign
- 编写OpenFeign客户端
- 底层连接池改造
- 最佳实现--新建api模块
- 日志输出
- 总结
- 网关
- 快速配置
- 路由属性
- 路由过滤器
- 网关登录校验
- 自定义过滤器
- 实现登录校验
- 网关传递用户
- OpenFeign传递用户
- 配置管理
- 配置共享
- 配置热更新
- 动态路由
- 服务保护
- 雪崩问题
- 服务保护方案
- 请求限流
- 线程隔离
- 失败处理
- failback
- 服务熔断
- Sentinel
- 分布式事务
- Seata框架
微服务
单体框架
将所有功能集中在一个显目中开发,打成一个包部署。
优点:
框架简单
部署成本低
缺点:
团队协作成本高
系统发布效率低
系统可以性差
微服务框架
服务化,把单体框架中的功能模块拆分成为多个独立项目。
SpringCloud
java领域最全面的微服务组件的集合
微服务拆分
微服务差分原则
拆分商品服务
拆分购物车服务
拆分用户服务
拆分交易服务
拆分支付服务
服务调用
在拆分的时候,我们发现一个问题:就是购物车业务中需要查询商品信息,但商品信息查询的逻辑全部迁移到了item-service服务,导致我们无法查询。
最终结果就是查询到的购物车数据不完整,因此要想解决这个问题,我们就必须改造其中的代码,把原本本地方法调用,改造成跨微服务的远程调用。
因此,现在查询购物车列表的流程变成了这样:
思考:
假如我们在cart-service中能模拟浏览器,发送http请求到item-service,是不是就实现了跨微服务的远程调用了呢?
那么:我们该如何用Java代码发送Http的请求呢?
RestTemplate
Spring给我们提供了一个RestTemplate的API,可以方便的实现Http请求的发送。
远程调用
先将RestTemplate注册为一个Bean:
接下来,我们修改cart-service中的com.hmall.cart.service.impl.CartServiceImpl的handleCartItems方法,发送http请求到item-service:
package com.hmall.cart.service.impl;import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmall.cart.domain.dto.CartFormDTO;
import com.hmall.cart.domain.dto.ItemDTO;
import com.hmall.cart.domain.po.Cart;
import com.hmall.cart.domain.vo.CartVO;
import com.hmall.cart.mapper.CartMapper;
import com.hmall.cart.service.ICartService;
import com.hmall.common.exception.BizIllegalException;
import com.hmall.common.utils.BeanUtils;
import com.hmall.common.utils.CollUtils;
import com.hmall.common.utils.UserContext;
import lombok.RequiredArgsConstructor;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;/*** <p>* 订单详情表 服务实现类* </p>** @author 虎哥* @since 2023-05-05*/
@Service
@RequiredArgsConstructor
public class CartServiceImpl extends ServiceImpl<CartMapper, Cart> implements ICartService {// private final IItemService itemService;private final RestTemplate restTemplate;@Overridepublic void addItem2Cart(CartFormDTO cartFormDTO) {// 1.获取登录用户Long userId = UserContext.getUser();// 2.判断是否已经存在if (checkItemExists(cartFormDTO.getItemId(), userId)) {// 2.1.存在,则更新数量baseMapper.updateNum(cartFormDTO.getItemId(), userId);return;}// 2.2.不存在,判断是否超过购物车数量checkCartsFull(userId);// 3.新增购物车条目// 3.1.转换POCart cart = BeanUtils.copyBean(cartFormDTO, Cart.class);// 3.2.保存当前用户cart.setUserId(userId);// 3.3.保存到数据库save(cart);}@Overridepublic List<CartVO> queryMyCarts() {// 1.查询我的购物车列表List<Cart> carts = lambdaQuery().eq(Cart::getUserId, 1L /*TODO UserContext.getUser()*/).list();if (CollUtils.isEmpty(carts)) {return CollUtils.emptyList();}// 2.转换VOList<CartVO> vos = BeanUtils.copyList(carts, CartVO.class);// 3.处理VO中的商品信息handleCartItems(vos);// 4.返回return vos;}private void handleCartItems(List<CartVO> vos) {// TODO 1.获取商品idSet<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());// 2.查询商品// List<ItemDTO> items = itemService.queryItemByIds(itemIds);// 2.1.利用RestTemplate发起http请求,得到http的响应ResponseEntity<List<ItemDTO>> response = restTemplate.exchange("http://localhost:8081/items?ids={ids}",HttpMethod.GET,null,new ParameterizedTypeReference<List<ItemDTO>>() {},Map.of("ids", CollUtil.join(itemIds, ",")));// 2.2.解析响应if(!response.getStatusCode().is2xxSuccessful()){// 查询失败,直接结束return;}List<ItemDTO> items = response.getBody();if (CollUtils.isEmpty(items)) {return;}// 3.转为 id 到 item的mapMap<Long, ItemDTO> itemMap = items.stream().collect(Collectors.toMap(ItemDTO::getId, Function.identity()));// 4.写入vofor (CartVO v : vos) {ItemDTO item = itemMap.get(v.getItemId());if (item == null) {continue;}v.setNewPrice(item.getPrice());v.setStatus(item.getStatus());v.setStock(item.getStock());}}@Overridepublic void removeByItemIds(Collection<Long> itemIds) {// 1.构建删除条件,userId和itemIdQueryWrapper<Cart> queryWrapper = new QueryWrapper<Cart>();queryWrapper.lambda().eq(Cart::getUserId, UserContext.getUser()).in(Cart::getItemId, itemIds);// 2.删除remove(queryWrapper);}private void checkCartsFull(Long userId) {int count = lambdaQuery().eq(Cart::getUserId, userId).count();if (count >= 10) {throw new BizIllegalException(StrUtil.format("用户购物车课程不能超过{}", 10));}}private boolean checkItemExists(Long itemId, Long userId) {int count = lambdaQuery().eq(Cart::getUserId, userId).eq(Cart::getItemId, itemId).count();return count > 0;}
}
微服务拆分总结
服务治理
注册中心
Nacos注册中心
将资料中的SQL文件导入到你Docker中的MySQL容器中
之后执行docker命令:
docker run -d \
--name nacos \
--env-file ./nacos/custom.env \
-p 8848:8848 \
-p 9848:9848 \
-p 9849:9849 \
--restart=always \
nacos/nacos-server:v2.1.0-slim
http://192.168.61.130:8848/nacos/
服务注册
把item-service注册到Nacos,步骤如下:
- 引入依赖
<!--nacos 服务注册发现-->
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
- 配置Nacos地址
spring:application:name: item-servicecloud:nacos:server-addr: 192.168.61.130:8848 # nacos地址
- 重启
服务发现
服务的消费者要去nacos订阅服务,这个过程就是服务发现,步骤如下:
- 引入依赖
<!--nacos 服务注册发现-->
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
- 配置Nacos地址
spring:cloud:nacos:server-addr: 192.168.150.101:8848
- 发现并调用服务(这个太麻烦了,之后用OpenFeign)
服务发现需要用到一个工具,DiscoveryClient,SpringCloud已经帮我们自动装配,我们可以直接注入使用:
private final DiscoveryClient discoveryClient;
接下来,我们就可以对原来的远程调用做修改了,之前调用时我们需要写死服务提供者的IP和端口:
但现在不需要了,我们通过DiscoveryClient发现服务实例列表,然后通过负载均衡算法,选择一个实例去调用:
OpenFeign实现远程调用
是一个声明式的http客户端,作用就是基于SpringMVC的常用注解,帮我们优雅的实现http请求的发送
现在用的最新负载均衡:引入loadbalancer
快速入门
引入依赖
在cart-service服务的pom.xml中引入OpenFeign的依赖和loadBalancer依赖:
org.springframework.cloud spring-cloud-starter-openfeign org.springframework.cloud spring-cloud-starter-loadbalancer启用OpenFeign
接下来,我们在cart-service的CartApplication启动类上添加注解,启动OpenFeign功能:
编写OpenFeign客户端
在cart-service中,定义一个新的接口,编写Feign客户端:
其中代码如下:
package com.hmall.cart.client;import com.hmall.cart.domain.dto.ItemDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;import java.util.List;@FeignClient("item-service")
public interface ItemClient {@GetMapping("/items")List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
}
- @FeignClient(“item-service”) :声明服务名称
- @GetMapping :声明请求方式
- @GetMapping(“/items”) :声明请求路径
- @RequestParam(“ids”) Collection ids :声明请求参数
- List :返回值类型
OpenFeign就可以利用动态代理帮我们实现这个方法,并且向http://item-service/items发送一个GET请求,携带ids为请求参数,并自动将返回值处理为List。
我们只需要直接调用这个方法,即可实现远程调用了。
底层连接池改造
最佳实现–新建api模块
日志输出
总结
网关
网络的关口,负责请求的路由、转发、身份验证
快速配置
新建hm-gateway模板
pox.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"><parent><artifactId>hmall</artifactId><groupId>com.heima</groupId><version>1.0.0</version></parent><modelVersion>4.0.0</modelVersion><artifactId>hm-gateway</artifactId><properties><maven.compiler.source>11</maven.compiler.source><maven.compiler.target>11</maven.compiler.target></properties><dependencies><!--common--><dependency><groupId>com.heima</groupId><artifactId>hm-common</artifactId><version>1.0.0</version></dependency><!--网关--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId></dependency><!--nacos discovery--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><!--负载均衡--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId></dependency></dependencies><build><finalName>${project.artifactId}</finalName><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>
启动类
package com.hmall.gateway;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
public class GatewayApplication {public static void main(String[] args) {SpringApplication.run(GatewayApplication.class, args);}
}
配置路由规则application.yaml
server:port: 8080
spring:application:name: gatewaycloud:nacos:server-addr: 192.168.61.130:8848gateway:routes:- id: item # 路由规则id,自定义,唯一uri: lb://item-service # 路由的目标服务,lb代表负载均衡,会从注册中心拉取服务列表predicates: # 路由断言,判断当前请求是否符合当前规则,符合则路由到目标服务- Path=/items/**,/search/** # 这里是以请求路径作为判断规则- id: carturi: lb://cart-servicepredicates:- Path=/carts/**- id: useruri: lb://user-servicepredicates:- Path=/users/**,/addresses/**- id: tradeuri: lb://trade-servicepredicates:- Path=/orders/**- id: payuri: lb://pay-servicepredicates:- Path=/pay-orders/**
四个属性含义如下:
- id:路由的唯一标示
- predicates:路由断言,其实就是匹配条件
- filters:路由过滤条件
- uri:路由目标地址,lb://代表负载均衡,从注册中心获取目标微服务的实例列表,并且负载均衡选择一个访问。
路由属性
路由过滤器
网关登录校验
ThreadLocal的使用,线程之间共享数据,所以不能使用
自定义过滤器
实现登录校验
网关传递用户
步骤一:在网关登录校验过滤器中,把获取到的用户信息写入请求头
// 5.如果有效,传递用户信息String userInfo = userId.toString();ServerWebExchange swe = exchange.mutate().request(builder -> builder.header("user-info", userInfo)).build();
步骤二:在hm-common中编写SpringMVC拦截器,获取登录用户
package com.hmall.common.interceptors;import cn.hutool.core.util.StrUtil;
import com.hmall.common.utils.UserContext;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;public class UserInfoInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1.获取请求头中的用户信息String userInfo = request.getHeader("user-info");// 2.判断是否为空if (StrUtil.isNotBlank(userInfo)) {// 不为空,保存到ThreadLocalUserContext.setUser(Long.valueOf(userInfo));}// 3.放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 移除用户UserContext.removeUser();}
}
package com.hmall.common.config;import com.hmall.common.interceptors.UserInfoInterceptor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
@ConditionalOnClass(DispatcherServlet.class) //有springMVC,配置才有效
public class MvcConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new UserInfoInterceptor());}
}
OpenFeign传递用户
有些业务是比较复杂的,请求到达微服务后还需要调用其它多个微服务。
由于微服务获取用户信息是通过拦截器在请求头中读取,因此要想实现微服务之间的用户信息传递,就必须在微服务发起调用时把用户信息存入请求头。
微服务之间调用是基于OpenFeign来实现的,Feign中提供的一个拦截器接口:feign.RequestInterceptor
package com.hmall.api.config;import com.hmall.common.utils.UserContext;
import feign.Logger;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Bean;public class DefaultFeignConfig {@Beanpublic Logger.Level feignLoggerLevel(){return Logger.Level.FULL;}@Beanpublic RequestInterceptor userInfoRequestInterceptor(){return new RequestInterceptor() {@Overridepublic void apply(RequestTemplate template) {// 获取登录用户Long userId = UserContext.getUser();if(userId == null) {// 如果为空则直接跳过return;}// 如果不为空则放入请求头中,传递给下游微服务template.header("user-info", userId.toString());}};}
}
配置管理
配置共享
我们可以把微服务共享的配置抽取到Nacos中统一管理,这样就不需要每个微服务都重复配置了。
- 在Nacos中添加共享配置
- 微服务拉取配置
配置热更新
当修改配置文件中的配置时,微服务无需启动即可生效
<!--nacos配置管理--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId></dependency><!--读取bootstrap文件--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-bootstrap</artifactId></dependency>
spring:application:name: cart-service # 服务名称profiles:active: devcloud:nacos:server-addr: 192.168.61.131 # nacos地址config:file-extension: yaml # 文件后缀名shared-configs: # 共享配置- dataId: shared-jdbc.yaml # 共享mybatis配置- dataId: shared-log.yaml # 共享日志配置- dataId: shared-swagger.yaml # 共享日志配置
server:port: 8084
feign:okhttp:enabled: true # 开启OKHttp连接池支持
hm:db:database: hm-cartswagger:title: 购物车服务接口文档package: com.hmall.cart.controller
package com.hmall.cart.config;import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;@Data
@Component
@ConfigurationProperties(prefix = "hm.cart")
public class CartProperties {private Integer maxItems;}
private void checkCartsFull(Long userId) {int count = lambdaQuery().eq(Cart::getUserId, userId).count();if (count >= cartProperties.getMaxItems()) {throw new BizIllegalException(StrUtil.format("用户购物车商品数量不能超过{}", cartProperties.getMaxItems()));}}
动态路由
网关的路由配置全部是在项目启动时由org.springframework.cloud.gateway.route.CompositeRouteDefinitionLocator在项目启动的时候加载,并且一经加载就会缓存到内存中的路由表内(一个Map),不会改变。也不会监听路由变更,所以,我们无法利用上节课学习的配置热更新来实现路由更新。
因此,我们必须监听Nacos的配置变更,然后手动把最新的路由更新到路由表中。这里有两个难点:
- 如何监听Nacos配置变更?
- 如何把路由信息更新到路由表?
服务保护
雪崩问题
微服务调用某个服务故障,引起整个链路中所有微服务都不可以
服务保护方案
请求限流
限制访问接口的并发量,避免因服务激增出现故障
线程隔离
限定每个业务能使用的线程数量而将业务故障隔离, 避免故障扩散
比如,查询购物车的时候需要查询商品,为了避免因商品服务出现故障导致购物车服务级联失败,我们可以把购物车业务中查询商品的部分隔离起来,限制可用的线程资源。这样,即便商品服务出现故障,最多导致查询购物车业务故障,并且可用的线程资源也被限定在一定范围,不会导致整个购物车服务崩溃。
所以,我们要对查询商品的FeignClient接口做线程隔离。
失败处理
定义fallback逻辑,让业务失败时不再抛出异常,而是走fallback逻辑
failback
FeiClient的Fallback配置,FallbackFactory,可以对远程调用的异常做处理,通常都会选择这种
http://192.168.61.133:8848/nacos/
服务熔断
由熔断器统计请求的异常比例,超出阈值则熔断该业务,拦截该接口请求
状态机包括三个状态:
- closed:关闭状态,断路器放行所有请求,并开始统计异常比例、慢请求比例。超过阈值则切换到open状态
- open:打开状态,服务调用被熔断,访问被熔断服务的请求会被拒绝,快速失败,直接走降级逻辑。Open状态持续一段时间后会进入half-open状态
- half-open:半开状态,放行一次请求,根据执行结果来判断接下来的操作。
- 请求成功:则切换到closed状态
- 请求失败:则切换到open状态
Sentinel
Sentinel 的使用可以分为两个部分:
- 核心库(Jar包):不依赖任何框架/库,能够运行于 Java 8 及以上的版本的运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。在项目中引入依赖即可实现服务限流、隔离、熔断等功能。
- 控制台(Dashboard):Dashboard 主要负责管理推送规则、监控、管理机器信息等。
分布式事务
在分布式系统中,如果一个业务需要多个服务合作完成,而且每一个服务都有事务,多个事务必须同时成功或失败
分支事务:每个服务的事务
全局事务:整个业务