网站建设的工作内容,优化设计官网,个人做金融网站能赚钱吗,公司网站建设优点SpringCloud 微服务单体框架微服务框架SpringCloud微服务拆分微服务差分原则拆分商品服务拆分购物车服务拆分用户服务拆分交易服务拆分支付服务服务调用RestTemplate远程调用 微服务拆分总结 服务治理注册中心Nacos注册中心服务注册服务发现 OpenFeign实现远程调用快速入门引入… 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 ServiceImplCartMapper, 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 ListCartVO queryMyCarts() {// 1.查询我的购物车列表ListCart carts lambdaQuery().eq(Cart::getUserId, 1L /*TODO UserContext.getUser()*/).list();if (CollUtils.isEmpty(carts)) {return CollUtils.emptyList();}// 2.转换VOListCartVO vos BeanUtils.copyList(carts, CartVO.class);// 3.处理VO中的商品信息handleCartItems(vos);// 4.返回return vos;}private void handleCartItems(ListCartVO vos) {// TODO 1.获取商品idSetLong itemIds vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());// 2.查询商品// ListItemDTO items itemService.queryItemByIds(itemIds);// 2.1.利用RestTemplate发起http请求得到http的响应ResponseEntityListItemDTO response restTemplate.exchange(http://localhost:8081/items?ids{ids},HttpMethod.GET,null,new ParameterizedTypeReferenceListItemDTO() {},Map.of(ids, CollUtil.join(itemIds, ,)));// 2.2.解析响应if(!response.getStatusCode().is2xxSuccessful()){// 查询失败直接结束return;}ListItemDTO items response.getBody();if (CollUtils.isEmpty(items)) {return;}// 3.转为 id 到 item的mapMapLong, 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(CollectionLong itemIds) {// 1.构建删除条件userId和itemIdQueryWrapperCart queryWrapper new QueryWrapperCart();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 \
--restartalways \
nacos/nacos-server:v2.1.0-slimhttp://192.168.61.130:8848/nacos/
服务注册
把item-service注册到Nacos步骤如下
引入依赖
!--nacos 服务注册发现--
dependencygroupIdcom.alibaba.cloud/groupIdartifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactId
/dependency配置Nacos地址
spring:application:name: item-servicecloud:nacos:server-addr: 192.168.61.130:8848 # nacos地址重启
服务发现
服务的消费者要去nacos订阅服务这个过程就是服务发现步骤如下
引入依赖
!--nacos 服务注册发现--
dependencygroupIdcom.alibaba.cloud/groupIdartifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactId
/dependency配置Nacos地址
spring:cloud:nacos:server-addr: 192.168.150.101:8848发现并调用服务(这个太麻烦了之后用OpenFeign)
服务发现需要用到一个工具DiscoveryClientSpringCloud已经帮我们自动装配我们可以直接注入使用 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)ListItemDTO queryItemByIds(RequestParam(ids) CollectionLong 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 version1.0 encodingUTF-8?
project xmlnshttp://maven.apache.org/POM/4.0.0xmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsdparentartifactIdhmall/artifactIdgroupIdcom.heima/groupIdversion1.0.0/version/parentmodelVersion4.0.0/modelVersionartifactIdhm-gateway/artifactIdpropertiesmaven.compiler.source11/maven.compiler.sourcemaven.compiler.target11/maven.compiler.target/propertiesdependencies!--common--dependencygroupIdcom.heima/groupIdartifactIdhm-common/artifactIdversion1.0.0/version/dependency!--网关--dependencygroupIdorg.springframework.cloud/groupIdartifactIdspring-cloud-starter-gateway/artifactId/dependency!--nacos discovery--dependencygroupIdcom.alibaba.cloud/groupIdartifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactId/dependency!--负载均衡--dependencygroupIdorg.springframework.cloud/groupIdartifactIdspring-cloud-starter-loadbalancer/artifactId/dependency/dependenciesbuildfinalName${project.artifactId}/finalNamepluginsplugingroupIdorg.springframework.boot/groupIdartifactIdspring-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配置管理--dependencygroupIdcom.alibaba.cloud/groupIdartifactIdspring-cloud-starter-alibaba-nacos-config/artifactId/dependency!--读取bootstrap文件--dependencygroupIdorg.springframework.cloud/groupIdartifactIdspring-cloud-starter-bootstrap/artifactId/dependencyspring: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.controllerpackage 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 等框架也有较好的支持。在项目中引入依赖即可实现服务限流、隔离、熔断等功能。控制台DashboardDashboard 主要负责管理推送规则、监控、管理机器信息等。
分布式事务
在分布式系统中如果一个业务需要多个服务合作完成而且每一个服务都有事务多个事务必须同时成功或失败
分支事务每个服务的事务 全局事务整个业务
Seata框架