# hmall **Repository Path**: duyus/hmall ## Basic Information - **Project Name**: hmall - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-04-18 - **Last Updated**: 2025-04-18 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ### 黑马商城 参考文章:https://b11et3un53m.feishu.cn/wiki/UMgpwmmQKisWBIkaABbcwAPonVf #### Mybstis (1) 使用 QueryWrapper ```java QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("user_id", userId); // 字段名是字符串 Long count = cartMapper.selectCount(queryWrapper); ``` (2) 使用 LambdaQueryWrapper ```java LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>(); lambdaQueryWrapper.eq(Cart::getUserId, userId); // 字段名通过方法引用 Long count = cartMapper.selectCount(lambdaQueryWrapper); ``` #### 跨微服务的远程调用(RPC,即Remote Produce Call) ![img.png](z-images/img1.png) ##### RestTemplate实现远程调用 ![img.png](z-images/img2.png) 1. 调用者需要定义配置类 ```java // 先将RestTemplate注册为一个Bean: @Configuration public class RemoteCallConfig { @Bean public RestTemplate restTemplate() { return new RestTemplate(); } } ``` 2. 修改调用者逻辑代码 实现远程调用服务 ![img.png](z-images/img3.png) 3. 具体步骤 - 注册RestTemplate到Spring容器 - 调用RestTemplate的API发送请求,常见方法有: - getForObject:发送Get请求并返回指定类型对象 - PostForObject:发送Post请求并返回指定类型对象 - put:发送PUT请求 - delete:发送Delete请求 - exchange:发送任意类型请求,返回ResponseEntity ##### 服务注册和发现 **利用Nacos实现了服务的治理,利用RestTemplate实现了服务的远程调用** ![img.png](z-images/img4.png) 1. 流程如下: - 服务启动时就会注册自己的服务信息(服务名、IP、端口)到注册中心 - 调用者可以从注册中心订阅想要的服务,获取服务对应的实例列表(1个服务可能多实例部署) - 调用者自己对实例列表负载均衡,挑选一个实例 - 调用者向该实例发起远程调用 2. 当服务提供者的实例宕机或者启动新实例时,调用者如何得知呢? - 服务提供者会定期向注册中心发送请求,报告自己的健康状态(心跳请求) - 当注册中心长时间收不到提供者的心跳时,会认为该实例宕机,将其从服务的实例列表中剔除 - 当服务有新实例启动时,会发送注册服务请求,其信息会被记录在注册中心的服务实例列表 - 当注册中心服务列表变更时,会主动通知微服务,更新本地服务列表 ###### Nacos注册中心 1. 服务注册(在提供服务的模块添加 --item-service) - 引入依赖 - 配置文件配置Nacos地址 2. 服务发现(在消费服务的模块添加 --cart-service ) - 引入依赖 - 配置Nacos地址 - 使用负载均衡算法调用服务 - 随机 - 轮询 - Ip的Hash - 最近最少访问 3. 调用服务 ![img.png](z-images/img5.png) ##### OpenFeign 远程掉用 **利用Nacos实现了服务的治理,利用OpenFeign实现了服务的远程调用** 1. 引入依赖 2. 启动组件Feign - 在消费服务的一方启动类启动组件功能 - ``` @EnableFeignClient``` 3. 编写组件Feign - 在 api模块 编写Feign客户端 ```java @FeignClient("item-service") // 声明服务名称 public interface ItemClient { @GetMapping("/items") // 声明请求路径 List queryItemByIds(@RequestParam("ids") Collection ids); //具体调用方法的方法声明 } ``` 4. 使用Client ![img.png](z-images/img6.png) feign替我们完成了服务拉取、负载均衡、发送http请求的所有工作,是不是看起来优雅多了。 而且,这里我们不再需要RestTemplate了,还省去了RestTemplate的注册。 5. 连接池 - **连接池指的是一组预先创建的HTTP 连接,这些连接可以被重复使用,而不是每次请求都创建一个新的连接。** - Feign底层发起http请求,依赖于其它的框架。其底层支持的http客户端实现包括: - HttpURLConnection:默认实现,不支持连接池 - Apache HttpClient :支持连接池 - OKHttp:支持连接池 6. 日志配置 - OpenFeign只会在FeignClient所在包的日志级别为DEBUG时,才会输出日志。而且其日志级别有4级: - NONE:不记录任何日志信息,这是默认值。 - BASIC:仅记录请求的方法,URL以及响应状态码和执行时间 - HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息 - FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。 - Feign默认的日志级别就是NONE,所以默认我们看不到请求日志。 #### 网关 由于每个微服务都有不同的地址或端口,入口不同,相信大家在与前端联调的时候发现了一些问题: - 请求不同数据时要访问不同的入口,需要维护多个入口地址,麻烦 - 前端无法调用nacos,无法实时更新服务列表 单体架构时我们只需要完成一次用户登录、身份校验,就可以在所有业务中获取到用户信息。而微服务拆分后,每个微服务都独立部署,这就存在一些问题: - 每个微服务都需要编写登录校验、用户信息获取的功能吗? - 当微服务之间调用时,该如何传递用户信息? ![img.png](z-images/img7.png) ##### 第一章:网关路由,解决前端请求入口的问题 - 创建网关微服务 - 创建新的模块,不需要添加任何代码 - 引入SpringCloudGateway、NacosDiscovery依赖 ```yaml server: port: 8080 spring: application: name: gateway cloud: nacos: server-addr: 192.168.150.101:8848 gateway: routes: - id: item # 路由规则id,自定义,唯一 uri: lb://item-service # 路由的目标服务,lb代表负载均衡,会从注册中心拉取服务列表 predicates: # 路由断言,判断当前请求是否符合当前规则,符合则路由到目标服务 - Path=/items/**,/search/** # 这里是以请求路径作为判断规则 - id: cart uri: lb://cart-service predicates: - Path=/carts/** - id: user uri: lb://user-service predicates: - Path=/users/**,/addresses/** - id: trade uri: lb://trade-service predicates: - Path=/orders/** - id: pay uri: lb://pay-service predicates: - Path=/pay-orders/** ``` - 编写启动类 - 配置网关路由 - 四个属性含义如下: - id:路由的唯一标示 - predicates:路由断言,其实就是匹配条件 - filters:路由过滤条件,后面讲 - uri:路由目标地址,lb://代表负载均衡,从注册中心获取目标微服务的实例列表,并且负载均衡选择一个访问。 ##### 第二章:网关鉴权,解决统一登录校验和用户信息获取的问题 - 网关是所有登录微服务的入口,从网关开始鉴权 - ![img.png](z-images/img8.png) - 网关过滤器 - ![img.png](z-images/img9.png) 最终请求转发是有一个名为NettyRoutingFilter的过滤器来执行的,而且这个过滤器是整个过滤器链中顺序最靠后的一个。如果我们能够定义一个过滤器,在其中实现登录校验逻辑,并且将过滤器执行顺序定义到NettyRoutingFilter之前,这就符合我们的需求了! - GatewayFilter:路由过滤器,作用范围比较灵活,可以是任意指定的路由Route. - GlobalFilter:全局过滤器,作用范围是所有路由,不可配置。 - 自定义GatewayFilter(可以设置动态参数) ```java @Component public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactory { @Override public GatewayFilter apply(Object config) { return new GatewayFilter() { @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 获取请求 ServerHttpRequest request = exchange.getRequest(); // 编写过滤器逻辑 System.out.println("过滤器执行了"); // 放行 return chain.filter(exchange); } }; } } ``` ```yaml spring: cloud: gateway: default-filters: - PrintAny # 此处直接以自定义的GatewayFilterFactory类名称前缀类声明过滤器 ``` ###### 注意:该类的名称一定要以GatewayFilterFactory为后缀! ![img.png](z-images/img10.png) - 自定义GlobalFilter (无法设置动态参数) ```java @Component public class PrintAnyGlobalFilter implements GlobalFilter, Ordered { @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 编写过滤器逻辑 System.out.println("未登录,无法访问"); // 放行 // return chain.filter(exchange); // 拦截 ServerHttpResponse response = exchange.getResponse(); response.setRawStatusCode(401); return response.setComplete(); } @Override public int getOrder() { // 过滤器执行顺序,值越小,优先级越高 return 0; } } ``` - 存在的问题 - 如何在网关转发之前做登录校验 - 网关如何将用户信息传递给微服务 - 如何在微服务之间传递用户信息 ###### 登录校验的实现(getwawy模块下) 所需的文件具体作用如下: - AuthProperties:配置登录校验需要拦截的路径,因为不是所有的路径都需要登录才能访问 - JwtProperties:定义与JWT工具有关的属性,比如秘钥文件位置 - SecurityConfig:工具的自动装配 - JwtTool:JWT工具,其中包含了校验和解析token的功能 - hmall.jks:秘钥文件 定义过滤器 ```java @Component @RequiredArgsConstructor @EnableConfigurationProperties(AuthProperties.class) public class AuthGlobalFilter implements GlobalFilter, Ordered { private final JwtTool jwtTool; private final AuthProperties authProperties; private final AntPathMatcher antPathMatcher = new AntPathMatcher(); @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 1.获取Request ServerHttpRequest request = exchange.getRequest(); // 2.判断是否不需要拦截 if(isExclude(request.getPath().toString())){ // 无需拦截,直接放行 return chain.filter(exchange); } // 3.获取请求头中的token String token = null; List headers = request.getHeaders().get("authorization"); if (!CollUtils.isEmpty(headers)) { token = headers.get(0); } // 4.校验并解析token Long userId = null; try { userId = jwtTool.parseToken(token); } catch (UnauthorizedException e) { // 如果无效,拦截 ServerHttpResponse response = exchange.getResponse(); response.setRawStatusCode(401); return response.setComplete(); } // 5.如果有效,传递用户信息 保存到请求头 String userInfo = userId.toString(); // 由于 ServerWebExchange 是不可变的,Spring 提供了 mutate() 方法来创建一个可变的副本。通过这个副本,你可以对请求或响应的内容进行修改。 ServerWebExchange serverWebExchange = exchange.mutate() // 创建当前对象的可变副本 .request(builder -> builder.header("user-info",userInfo)) .build(); System.out.println("userId = " + userId); System.out.println("userInfo = " + userInfo); // 6.放行 return chain.filter(serverWebExchange); } private boolean isExclude(String antPath) { for (String pathPattern : authProperties.getExcludePaths()) { if(antPathMatcher.match(pathPattern, antPath)){ return true; } } return false; } @Override public int getOrder() { return 0; } } ``` ###### 拦截器获取用户 (common 模块下) - 编写拦截器 ```java public class UserInfoInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1.获取请求头中的用户信息 String userInfo = request.getHeader("user-info"); // 2.判断是否为空 if (StrUtil.isNotBlank(userInfo)) { // 不为空,保存到ThreadLocal UserContext.setUser(Long.valueOf(userInfo)); } // 3.放行 return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 移除用户 UserContext.removeUser(); } } ``` - 注册拦截器 ```java /** * gateway不是基于springMvc的,所以该MvcConfig不应该生效。 * 通过使用@ConditionalOnClass(DispatcherServlet.class), * 表示仅对包含了springMvc的核心类(DispatcherServlet)的微服务生效 */ @Configuration @ConditionalOnClass(DispatcherServlet.class) // 解决方案是 让配置类生效有限制,就是仅仅包含了MVC的核心组件生效 public class MvcConfig implements WebMvcConfigurer { @Override // 原本是要设置生效的路径 但是默认就是全部拦截 因此不需要设置 public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new UserInfoInterceptor()); } } ``` `注意!!common中的 mvc maven 限定了scope 为 provided,不会依赖传递到网关` Spring Cloud Gateway 基于 Spring WebFlux,不依赖于 Spring MVC。 使用 @ConditionalOnClass 可以确保仅在特定类(如 DispatcherServlet)存在于类路径上时才加载某些配置。 正确管理依赖范围(如 provided)可以避免不必要的传递依赖,确保网关服务不会因缺少或多余的依赖而出现问题。 ###### OpenFeign传递用户 (api模块) 目前通过拦截器、过滤器来实现由网关实现登录校验、并将将用户信息传递到微服务,但是微服务之间互相的调用还未实现,如下 (过滤器是网关的 拦截器是微服务的 先走过滤器 后走拦截器) ![img.png](z-images/img11.png) 要想实现微服务之间的用户信息传递,就必须在微服务发起调用时把用户信息存入请求头。 微服务之间调用是基于OpenFeign来实现的,并不是我们自己发送的请求 这里要借助Feign中提供的一个拦截器接口:feign.RequestInterceptor,让每一个由OpenFeign发起的请求自动携带登录用户信息呢 在com.hmall.api.config.DefaultFeignConfig中添加一个Bean: ```java @Bean public RequestInterceptor userInfoRequestInterceptor(){ return new RequestInterceptor() { @Override public void apply(RequestTemplate template) { // 获取登录用户 Long userId = UserContext.getUser(); if(userId == null) { // 如果为空则直接跳过 return; } // 如果不为空则放入请求头中,传递给下游微服务 template.header("user-info", userId.toString()); } }; } ``` ##### 第三章:统一配置管理,解决微服务的配置文件重复和配置热更新问题(Nacos) - 在Nacos中添加共享配置 - 微服务拉取配置,实现配置热更新 接下来,我们要在微服务拉取共享配置。将拉取到的共享配置与本地的application.yaml配置合并,完成项目上下文的初始化。 不过,需要注意的是,读取Nacos配置是SpringCloud上下文(ApplicationContext)初始化时处理的,发生在项目的引导阶段。然后才会初始化SpringBoot上下文,去读取application.yaml。 也就是说引导阶段,application.yaml文件尚未读取,根本不知道nacos 地址,该如何去加载nacos中的配置文件呢? SpringCloud在初始化上下文的时候会先读取一个名为bootstrap.yaml(或者bootstrap.properties)的文件,如果我们将nacos地址配置到bootstrap.yaml中,那么在项目引导阶段就可以读取nacos中的配置了。 ```java // 使用该注解实现热更新 @ConfigurationProperties(prefix = "hm.cart") public class CartProperties { private Integer maxAmount; } ``` ![img.png](z-images/img12.png) ##### 扩展:动态路由 **是一个难点 建议去b站多看** 这里核心的步骤有2步: - 创建ConfigService,目的是连接到Nacos - 添加配置监听器,编写配置变更的通知处理逻辑 #### 服务保护和分布式事务 - 业务的健壮性:从业务角度来说,为了提升用户体验,即便是商品查询失败,购物车列表也应该正确展示出来,哪怕是不包含最新的商品信息。 - 级联失败问题(雪崩):商品服务业务并发较高,占用过多Tomcat连接。可能会导致商品服务的所有接口响应时间增加,延迟变高,甚至是长时间阻塞直至查询失败。 此时查询购物车业务需要查询并等待商品查询结果,从而导致查询购物车列表业务的响应时间也变长,甚至也阻塞直至无法访问。 - 事务问题:比如昨天讲到过的下单业务,下单的过程中需要调用多个微服务,必须确保所有操作的同时成功或失败: - 商品服务:扣减库存 - 订单服务:保存订单 - 购物车服务:清理购物车 ##### 微服务保护 ###### Sentinel - 请求限流 - 线程隔离 - 服务熔断 这些方案或多或少都会导致服务的体验上略有下降,比如请求限流,降低了并发上限;线程隔离,降低了可用资源数量;服务熔断,降低了服务的完整度,部分服务变的不可用或弱可用。因此这些方案都属于服务降级的方案。但通过这些方案,服务的健壮性得到了提升。 Sentinel 的使用可以分为两个部分: - 核心库(Jar包):不依赖任何框架/库,能够运行于 Java 8 及以上的版本的运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。在项目中引入依赖即可实现服务限流、隔离、熔断等功能。 - 控制台(Dashboard):Dashboard 主要负责管理推送规则、监控、管理机器信息等。 1. 请求限流 ![img.png](z-images/img13.png) ![img.png](z-images/img16.png) 这样就把查询购物车列表这个簇点资源的流量限制在了每秒6个,也就是最大QPS为6. 2. 线程隔离 当一个业务接口响应时间长,而且并发高时,就可能耗尽服务器的线程资源,导致服务内的其它接口受到影响。所以我们必须把这种影响降低,或者缩减影响的范围。线程隔离正是解决这个问题的好办法 ![img.png](z-images/img14.png) 对查询商品的FeignClient接口做线程隔离。 ```yaml feign: sentinel: enabled: true # 开启feign对sentinel的支持 ``` 3. 服务熔断 - 编写服务降级逻辑:就是服务调用失败后的处理逻辑,根据业务场景,可以抛出异常,也可以返回友好提示或默认数据。 - 异常统计和熔断:统计服务提供方的异常比例,当比例过高表明该接口会影响到其它服务,应该拒绝调用该接口,而是直接走降级逻辑。 ![img.png](z-images/img15.png) 触发限流或熔断后的请求不一定要直接报错,也可以返回一些默认数据或者友好提示,用户体验会更好。 给FeignClient编写失败后的降级逻辑有两种方式: - 方式一:FallbackClass,无法对远程调用的异常做处理 - 方式二:FallbackFactory,可以对远程调用的异常做处理,我们一般选择这种方式。 ###### Seata 其实分布式事务产生的一个重要原因,就是参与事务的多个分支事务互相无感知,不知道彼此的执行状态。因此解决分布式事务的思想非常简单: 就是找一个统一的事务协调者,与多个分支事务通信,检测每个分支事务的执行状态,保证全局事务下的每一个分支事务同时成功或失败即可。大多数的分布式事务框架都是基于这个理论来实现的。 Seata的事务管理中有三个重要的角色: - TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚。 - TM (Transaction Manager) - 事务管理器:定义全局事务的范围、开始全局事务、提交或回滚全局事务。 - RM (Resource Manager) - 资源管理器:管理分支事务,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。 ![img.png](z-images/img17.png) 其中,TM和RM可以理解为Seata的客户端部分,引入到参与事务的微服务依赖中即可。将来TM和RM就会协助微服务,实现本地分支事务与TC之间交互,实现事务的提交或回滚。 1. 部署TC服务 - 数据库表 - 配置文件 - dockr部署 - 要确保nacos、mysql都在hm-net网络中。如果某个容器不再hm-net网络,可以参考下面的命令将某容器加入指定网络: - `` docker network connect [网络名] [容器名] `` 2. 微服务集成Seata - 引入依赖 - 改造配置 主要用nacos来配置共享 方便微服务调用 - 添加 undo_log数据库表 - 将其上的@Transactional注解改为Seata提供的@GlobalTransactional