admin 管理员组文章数量: 1184232
阶段笔记
Day01
1.请你简单介绍下软件开发中系统架构的演变?
单一应用—>垂直拆分—>分布式服务—>服务治理(SOA)—>微服务架构
2.远程调用的方式有几种? 他们的区别如何?如何选择?
远程调用有两种方式:
RPC(Remote Produce Call)即远程过程调用;Http:一种基于TCP、规定了数据传输格式的网络传输协议;两种方式的区别:
RPC并没有规定数据传输格式,这个格式可以任意指定,不同的RPC协议,数据格式不一定相同;Http中定义了资源定位的路径,RPC中并不需要;RPC需要满足像调用本地服务一样调用远程服务,也就是对调用过程在API层面进行封装。Http协议没有这样的要求,因此请求、响应等细节需要我们自己去实现。优点:
RPC方式更加透明,对用户更方便。HTTP方式更灵活,没有规定API和语言,跨语言、跨平台;缺点:
RPC方式需要在API层面进行封装,限制了开发的语言环境;如何选择:
- 如果对效率要求更高,并且开发过程使用统一的技术栈,用
RPC;- 如果需要更加灵活,跨语言、跨平台,用
HTTP。
3.SpringCloud是什么?
Spring Cloud是一系列框架的有序集合。他利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、熔断器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。
4.简述SpringCloud和SpringBoot的关系?
Spring Boot是Spring的一套快速配置脚手架,可以基于Spring Boot快速可开发单个微服务,Spring Cloud是一个基于Spring Boot实现的云应用开发工具;Spring Boot专注于快速方便开发单个微服务个体,Spring Cloud关注全局的微服务治理框架,它将Spring Boot开发的一个个微服务整合并管理起来,为各个服务之间提供服务发现、负载均衡、断路器、路由、配置管理、微代理、消息总线、全局锁、分布式会话等集成服务;Spring Boot可以离开Spring Cloud独立使用开发项目,但是Spring Cloud离不开Spring Boot,属于依赖关系。
5.SpringCloudNetflix分布式解决方案中主要框架有哪些?
Spring Cloud Netflix分布式解决方案中主要框架有:
- 服务发现——
Netflix Eureka;- 服务调用——
Netflix Feign;- 熔断器——
Netflix Hystrix;- 服务网关——
Netflix Zuul;- 分布式配置——
Spring Cloud Config;- 消息总线——
Spring Cloud Bus。
6.请简单介绍下springCloudAlibaba?
Spring Cloud Alibaba也是一套微服务解决方案,包含开发分布式应用微服务的必需组件,方便开发者通过Spring Cloud编程模型轻松使用这些组件来开发分布式应用服务,是阿里巴巴开源中间件与Spring Cloud体系的融合。
7.阿里开源组件有哪些?功能是什么?
阿里开源组件及功能如下:
Nacos:一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台;
Sentinel:把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性;
RocketMQ:开源的分布式消息系统,基于高可用分布式集群技术,提供低延时的、高可靠的消息发布与订阅服务;
Dubbo:一款高性能 Java RPC 框架;
Seata:一个易于使用的高性能微服务分布式事务解决方案;
Arthas:开源的Java动态追踪工具,基于字节码增强技术,功能非常强大。
8.什么是nacos? 为什么要使用nacos? nacos可以干什么?
什么是
Nacos:
Nacos是阿里巴巴推出来的一个新开源项目,这是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。为什么要使用
Nacos:
Nacos更加强大,它可以与Spring, Spring Boot、Spring Cloud集成,并能代替Spring Cloud Eureka、Spring Cloud Config。通过Nacos Server和spring-cloud-starter-alibaba-nacos-config实现配置的动态变更。通过Nacos Server和spring-cloud-starter-alibaba-nacos-discovery实现服务的注册与发现。
Nacos可以干什么:
- 服务发现和服务健康监测;
- 动态配置付服务;
- 动态DNS服务;
- 服务及其元数据管理
9.简单说说如何下载安装启动nacos?
下载:通过源码和发行包两种方式来获取
Nacos。可以从https://github/alibaba/nacos/releases下载nacos-server-$version.zip包。
启动:
- linux系统:将
Nacos压缩包解压后,进入Nacos目录下的bin目录,然后输入./startup.sh -m standalone启动。- window系统:将
Nacos压缩包解压后,同时按住win+R,然后输入cmd打开命令行窗口,键入Nacos的路径,然后进入bin目录,输入startup.cmd -m standalone启动。
10.如何发布服务到nacos?需要哪些配置?
首先创建两个
Spring Boot工程生产者nacos-provider和消费者nacos-consumer,然后将生产者和消费者注册到nacos注册中心,主要分三步:1、添加依赖 2、在application.yml中配置nacos的服务名及服务地址;3、通过Spring Cloud原生注解开启服务注册发现功能;需要的配置有:
添加依赖:
spring-cloud-starter-alibaba-nacos-discovery及springCloud<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache/POM/4.0.0" xmlns:xsi="http://www.w3/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache/POM/4.0.0 https://maven.apache/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.9.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.offcn</groupId> <artifactId>nacos-provider</artifactId> <version>1.0</version> <name>nacos-provider</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <!--++++++++++++++++++++++++++++++++++++++++++++++++--> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies> <!-- SpringCloud的依赖 --> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Hoxton.SR2</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2.1.0.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <!--++++++++++++++++++++++++++++++++++++++++++++++++--> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>在
application.yml中配置nacos服务地址和应用名spring: application: name: nacos-provider cloud: nacos: discovery: server-addr: 127.0.0.1:8848在引导类中添加
@EnableDiscoveryClient注解
11.消费者如何通过Feign远程调用服务?
首先在项目中引入
feign依赖<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>在消费者服务启动类上添加
@EnableFeignClients注解:创建
feign接口并编写如下内容@FeignClient("nacos-provider") public interface Feign { @RequestMapping("hello") public String hello(); }最后在
Controller中使用feignClient@RestController public class ConsumerController { @Autowired private Feign feign; @GetMapping("hi") public String hi() { return this.feign.hello(); } }
12.简述nacos配置中心可以做什么?
在系统开发过程中,开发者通常会将一些需要变更的参数、变量等从代码中分离出来独立管理,以独立的配置文件的形式存在。目的是让静态的系统工件或者交付物(如 WAR,JAR 包等)更好地和实际的物理运行环境进行适配。配置管理一般包含在系统部署的过程中,由系统管理员或者运维人员完成。配置变更是调整系统运行时的行为的有效手段。
nacos配置中心可以做系统配置的集中管理(编辑、存储、分发)、动态更新不重启、回滚配置(变更管理、历史版本管理、变更审计)等所有与配置相关的活动。命名空间切换环境:在实际开发中,通常有多套不同的环境(默认只有
public),那么这个时候可以根据指定的环境来创建不同的namespce,例如,开发、测试和生产三个不同的环境,那么使用一套nacos集群可以分别建以下三个不同的namespace。以此来实现多环境的隔离。分组区分环境:在实际开发中,除了不同的环境外。不同的微服务或者业务功能,可能有不同的
redis及mysql数据库。区分不同的环境我们使用名称空间(namespace),区分不同的微服务或功能,使用分组(group)。也可以反过来使用,名称空间和分组只是为了更好的区分配置,提供的两个维度而已。
Day02
1.请简单叙述下SpringCloudGateway是什么?有什么功能?
SpringCloudGateway是API网关,是介于客户端和服务器端之间的中间层,所有的外部请求都会先经过API网关这一层。也就是说,API的实现方面更多的考虑业务逻辑,而安全、性能、监控可以交由 API 网关来做,这样既提高业务灵活性又不缺安全性。功能:
- 负载均衡
- 熔断降级
- 统一鉴权
- 请求过滤
- 路径重写
- 限流保护
2.如何快速搭建一个网关Gateway?需要进行哪些配置?
使用
SpringBoot创建网关module,选择Gateway引入依赖
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache/POM/4.0.0" xmlns:xsi="http://www.w3/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache/POM/4.0.0 https://maven.apache/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.9.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.offcn</groupId> <artifactId>gateway-demo</artifactId> <version>1.0</version> <name>gateway-demo</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <!--++++++++++++++++++++++++++++++++++++++++++++++++--> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Hoxton.SR2</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <!--++++++++++++++++++++++++++++++++++++++++++++++++--> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>编写启动类(添加
@EnableDiscoveryClient)@EnableDiscoveryClient public class CloudGatewayApplication { public static void main(String[] args) { SpringApplication.run(CloudGatewayApplication.class, args); } }编写路由规则
server: port: 8888 spring: application: name: gateway cloud: nacos: discovery: server-addr: localhost:8848 gateway: routes: # 下面是配置的例子可以参考 - id: cms-route uri: lb://u-context predicates: - Path=/api/context/** filters: - RewritePath=/api/(?<segment>.*),/$\{segment}
3.网关中路由有几部分组成?分别代表什么意义?如何配置路由?
路由有四部分组成,分别为:
- id——路由名
- uri——接收到请求发送给哪个路径
- predicates——断言 逻辑判断,比如判断请求路径是否符合某个要求
- filters——过滤器 可以在发送下游请求之前或之后修改请求和响应。
# 路由配置:spring: cloud: gateway: routes: - id: nacos-consumer uri: http://127.0.0.1:18080 predicates: - Path=/java/hi filters: - StripPrefix=1
4. springCloudGateway 包含哪些断言工厂?请简述下请求路径断言的配置方式?
包含的断言工厂有:请求时间、请求Cookie、请求Header、请求Host、请求Method、请求Path、请求查询参数、请求远程地址;
# 请求路径断言的配置方式:spring:cloud: gateway: routes: - id: host_route uri: http://example predicates: - Path=/foo/{segment},/bar/{segment}
5. SpringCloudGateway包含哪些过滤器工厂?请简述下 如何配置PrefixPath和RewritePath过滤器工厂?
包含的过滤器工厂有:头部过滤器、请求参数过滤器、路径过滤器、请求(响应)体过滤器、状态过滤器、会话过滤器、重定向过滤器、重试过滤器、限流过滤器、熔断器过滤器
# StripPrefix和RewritePath过滤器工厂配置:spring:cloud: gateway: routes: - id: nacos-consumer uri: lb://nacos-consumer predicates: - Path=/alibaba/hi filters: - StripPrefix=1 - id: nacos-provider uri: lb://nacos-provider predicates: - Path=/java/api/** filters: - RewritePath=/java/api/(?<segment>.*),/api/$\{segment}
6.在服务集群情况下SpringCloudGateway如何配置路由的uri?简述下使用Java代码配置路由?
在服务集群情况下
SpringCloudGateway配置路由的uri:
把网关服务注册到
nacos,引入nacos的相关依赖:<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!-- SpringCloud的依赖 --> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Hoxton.SR2</version> <type>pom</type> <scope>import</scope> </dependency> <!-- 在依赖管理中加入springCloud-alibaba组件的依赖 --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2.1.0.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>配置
nacos服务地址及服务名spring: application: name: gateway cloud: nacos: discovery: server-addr: localhost:8848把网关注入到
nacos@EnableDiscoveryClient修改配置,通过服务名路由
语法:
lb://服务名
lb:LoadBalance,代表负载均衡的方式 服务名取决于
nacos的服务列表中的服务名 如:
-id: nacos-consumer uri: lb://nacos-consumer使用Java代码配置路由代码如下:
@Configurationpublic class RouteLocatorConfig { @Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder){ return builder.routes() .route(r -> r.path("/api/hello/**") .filters(f -> f.stripPrefix(1)) .uri("lb://nacos-provider")) .route(r -> r.path("/ujiuye/hello/**") .filters(f -> f.rewritePath("/ujiuye/(?<segment>.*)", "/${segment}")) .uri("lb://nacos-provider")) .build(); }}
7.如何配置全局网关?
创建全局过滤器:package com.offcn.filter;import com.fasterxml.jackson.core.JsonProcessingException;import com.fasterxml.jackson.databind.ObjectMapper;import com.google.common.collect.Maps;import org.springframework.cloud.gateway.filter.GatewayFilterChain;import org.springframework.cloud.gateway.filter.GlobalFilter;import org.springframework.core.Ordered;import org.springframework.core.io.buffer.DataBuffer;import org.springframework.http.HttpStatus;import org.springframework.http.server.reactive.ServerHttpResponse;import org.springframework.stereotype.Component;import org.springframework.web.server.ServerWebExchange;import reactor.core.publisher.Mono;import java.util.Map;@Componentpublic class AuthFilter implements GlobalFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String token = exchange.getRequest().getQueryParams().getFirst("token"); if (token == null || token.isEmpty()) { ServerHttpResponse response = exchange.getResponse(); // 封装错误信息 Map<String, Object> responseData = Maps.newHashMap(); responseData.put("code", 401); responseData.put("message", "非法请求"); responseData.put("cause", "Token is empty"); try { // 将信息转换为 JSON 如下使用的内置的jackson实现的转换 ObjectMapper objectMapper = new ObjectMapper(); byte[] data = objectMapper.writeValueAsBytes(responseData); // 输出错误信息到页面 DataBuffer buffer = response.bufferFactory().wrap(data); response.setStatusCode(HttpStatus.UNAUTHORIZED); response.getHeaders().add("Content-Type", "application/json;charset=UTF-8"); return response.writeWith(Mono.just(buffer)); } catch (JsonProcessingException e) { e.printStackTrace(); } } return chain.filter(exchange); } @Override public int getOrder() { return 1;//过滤器的顺序,越小,越先执行 }}
8.简述Sentinel是什么? 谈谈它的发展历史? 以及包括那两部分?
Sentinel是分布式系统的流量防卫兵。随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。发展历史:
2012 年,
Sentinel诞生,主要功能为入口流量控制。 2013-2017 年,
Sentinel在阿里巴巴集团内部迅速发展,成为基础技术模块,覆盖了所有的核心场景。Sentinel也因此积累了大量的流量归整场景以及生产实践。 2018 年,
Sentinel开源,并持续演进。 2019 年,
Sentinel朝着多语言扩展的方向不断探索,推出 C++ 原生版本,同时针对Service Mesh场景也推 出了Envoy集群流量控制支持,以解决Service Mesh架构下多语言限流的问题。 2020 年,推出
Sentinel Go版本,继续朝着云原生方向演进。包括核心库(Java客户端)和控制台(Dashboard)。
9.简述下Sentinel基本概念以及作用?
基本概念:
资源:是
Sentinel的关键概念。它可以是 Java 应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码。在接下来的文档中,我们都会用资源来描述代码块。只要通过SentinelAPI 定义的代码,就是资源,能够被Sentinel保护起来。大部分情况下,可以使用方法签名,URL,甚至服务名称作为资源名来标示资源。 规则:围绕资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规则。所有规则可以动态 实时调整。
作用:
- 流量控制
- 熔断降级
- 系统负载保护
10. 什么是流量控制? sentinel 流量控制有那几个角度? 简述如何进行配置?
流量控制在网络传输中是一个常用的概念,它用于调整网络包发送数据。然而,从系统稳定性角度考虑,在处理请求的速度上,也有非常多的讲究。任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制。
流量控制有以下几个角度:
- 资源的调用关系,例如资源的调用链路,资源和资源之间的关系;
- 运行指标,例如 QPS(每秒查询率)、线程数等;
- 控制的效果,例如直接限流(快速失败)、冷启动(Warm Up)、匀速排队(排队等待)等。
配置:
QPS流量控制:
直接拒绝:簇点链路–流控
阈值类型为QPS,单机阈值为2,点击新增按钮
冷启动:单机阈值为10,预热时间为5,点击保存
匀速排队:
1、单机阈值为1,选择排队等待,超时时间为20000,点击保存;
2、在
postman中,新建一个collection(这里collection名称是sentinel),并把一个请求添加到该collection: 1、选择Collections
2、选择sentinel
3、选择Save
4、选择Save As
Request name设置为http://localhost:18080/hi,点击Save to sentinel 3、请求添加成功后,点击
run按钮,配置每隔100ms发送一次请求,一共发送20个请求,点击“run sentinel”按钮;
查看控制台,效果如下:可以看到基本每隔1s打印一次
关联限流:
资源名为/hi2,阈值类型为QPS,单机阈值为2,流控模式为关联,关联资源为/hi;
资源名为/hi,阈值类型为QPS,单机阈值为2,流控模式为直接,流控效果为快速失败。
给消费者添加一个controller方法:
@GetMapping("hi2") public String hi2(){ return this.providerClient.hello() + "hi2"; } postman配置如下:每个400ms发送一次请求,一共发送50个。每秒钟超过了2次
链路限流:
资源名为/hi2,阈值类型为QPS,单机阈值为2,流控模式为链路,入口资源为/Entrance1,流控效果为快速失败。
线程数限流:
资源名为/hi,阈值类型为线程数,单机阈值为1。
改造controller中的hi方法:@GetMapping("hi") public String hi(){ try{ Thread.sleep(1000); }catch (InterruptedException e){ e.printStackTrace(); } return this.providerClient.hello(); }
postmain配置:Iterations为50,Delay为10
11. 什么是熔断降级? sentinel进行容量降级包含哪些指标? 简述如何进行配置?
Sentinel熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联错误。当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出DegradeException)。限流降级指标有三个:
- 平均响应时间(RT)
- 异常比例
- 异常数
配置:
平均响应时间:
资源名为/hi,熔断策略为慢调用比例,最大RT为100,比例阈值为0.1,熔断时长为10,最小请求数为5;
代码中仍然睡了1s
@GetMapping("hi")public String hi(){ try{ Thread.sleep(1000); }catch (InterruptedException e){ e.printStackTrace(); } return this.providerClient.hello();}
postmain配置:Iterations为50,Delay为10异常比例:
当资源的每秒请求量 >= 5,且每秒异常总数占通过量的比值超过阈值(DegradeRule中的 count)之后,资源 进入降级状态,即在接下的时间窗口(DegradeRule中的timeWindow,以 s 为单位)之内,对这个方法的调用 都会自动地返回。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。异常数:当资源近 1 分钟的异常数目超过阈值之后会进行熔断。
12. 如何实现Sentinel规则的持久化?
引入依赖
<dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> </dependency>添加配置:
spring: cloud: sentinel: transport: dashboard: 192.168.188.138:8080 port: 8719 datasource: consumer: nacos: server-addr: 192.168.188.138:8848 dataId: ${spring.application.name}-sentinel-rules groupId: SENTINEL_GROUP data-type: json rule_type: flownacos中创建流控规则
Data ID中写dataId
Group中写groupId
配置格式选JSON
配置内容:[ { "resource": "/hi", "limitApp": "default", "grade": 1, "count": 2, "strategy": 0, "controlBehavior": 0, "clusterMode": false }]
13. Sleuth是什么? 如何使用Sleuth实现服务请求的分布式链路追踪?简述其配置?
Spring Cloud Sleuth为springCloud实现了一个分布式链路追踪解决方案,大量借鉴了Dapper、Zipkin和HTrace等链路追踪技术。对于大多数用户而言,Sleuth应该是不可见的,并且您与外部系统的所有交互都应自动进行检测。您可以简单地在日志中捕获数据,也可以将其发送到远程收集器服务。使用Sleuth实现服务请求的分布式链路追踪步骤和配置:
引入sleuth的依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zipkin</artifactId></dependency>
zipkin的启动器包含了sleuth的依赖。配置
zipkin的相关信息spring: zipkin: base-url: http://192.168.188.138:9411 discovery-client-enabled: false sender: type: web重启
consumer/provider服务后,访问消费者:http://localhost:18080/hi。
Day03
1.简单介绍下优学题库? 用到了那些技术栈(前端 、后台)?
优学题库是一个以微服务为架构的在线考试题库项目,前后端分离,其中包括实体分类管理、考试题目管理、在线答题等众多功能模块,是一套完整的互联网应用系统。
后端:
SpringBoot+SpringCloud GateWay+MybatisPlus+Nacos前端:
Vue+ElementUI+Node.js
2.简述下优学题库的项目结构?
该项目使用前后端分离技术,前端由
admin-vue,小程序客户端两部分组成,所有的请求都必须先通过网关GateWay,然后网关将请求分发给后端的其他微服务模块,有u-context,u-member,u-question,renrenfast这些微服务,Nacos作为这些微服务的注册中心,Sentinel用于限流,Seata用于分布式事务控制,SchedulerX用于分布式任务调度,其中还有一些功能采用第三方服务,比如短信,支付等功能。项目中还使用了一些其他框架,比如Redis(缓存),ElasticSearch(搜索)等等
3.优学题库中用到了哪些数据库? 这个数据库分别对象哪些模块?
uxue_admin:管理后台数据库 ————renrenfast-fast
uxue_cms:广告内容服务数据库 ————u-context
uxue_qms:题目服务数据库 ————u-question
uxue_ums:会员服务数据库 ————u-member
4.如何在git创建一个代码仓库? 如何下载到本地?本地修改后如何同步到git代码仓库?
登录
gitee账号,点击右上角"+"号选择新建仓库,仓库名称为uxue,状态为私有,语言为java,选择"使用Readme文件初始化这个仓库",点击创建。点击进入到
gitee的uxue项目,复制克隆里HTTPS的网址,然后在本地创建一个空文件夹为uxue,打开这个文件夹路径的命令窗口,输入命令git clone 复制的内容,执行后就好了。在
uxue文件夹中,添加或创建一个pom.xml文件(将其变为Maven工程),然后使用IDEA打开,打开IDEA下方的Terminal窗口,输入命令git add .(添加)git commit -m "备注"(提交到本地仓库),git push origin master(推送到远程仓库)。
5.简述下你是如何搭建项目的管理后台的?
微服务:创建
SpringBoot项目,导入依赖,修改yml配置文件,在启动类上添加注解@EnableDiscoveryClient(开启服务发现),在uxue的pom.xml文件中对模块进行聚合(u-context)。后台后端:使用
renren-fasthttps://gitee/renrenio/renren-fast.git
后台前端:使用
renren-fast-vuehttps://gitee/renrenio/renren-fast-vue.git
6.简述如何使用人人开源的代码生成器,生成代码?
下载人人开源的代码生成器框架
git clone https://gitee/renrenio/renren-generator.git导入生成器代码到
uxue项目修改
application.yml,把数据库及其连接信息改成广告的数据库:uxue_cms修改
generator.properties:修改
controller模板文件src/main/resources/template/Controller.java.vm暂时删除引入的包,后面再引入//import org.apache.shiro.authz.annotation.RequiresPermissions;注释掉方法中
RequiresPermissions注解,后面再引入//@RequiresPermissions("${moduleName}:${pathName}:list")启动逆向工程
RenrenApplication:启动成功,监听端口号为80。浏览器访问,点击
renren-fast,出现生成界面,可以看到数据表选中全部表,点击生成代码按钮,即可生成一个压缩包,被下载下来。
7.如何对 广告、用户、问题等模块进行配置,遇到公共的依赖等如何处理?
- 创建一个
Maven模块(u-common),将公共的依赖添加导pom.xml文件中- 在
u-common中创建utils包和xss包utils包存放一些从renren-fast模块中复制过来的文件:Constans.java、PageUtils.java、Query.java、R.java、RRException.javaxss包存放一些从renren-fast模块中复制过来的文件:HTMLFilter.java、SQLFilter.java- 对
u-common进行clear(清理)和install(安装到本地仓库)- 当其他微服务需要公共的依赖时,只需导入
u-common这个依赖即可
8.如何发布各微服务到nacos上,需要进行哪些配置?
添加依赖
spring-cloud-starter-alibaba-nacos-discovery修改
yml文件spring: cloud: nacos: discovery: server-addr: localhost:8848启动类添加注解
@EnableDiscoveryClient
9.如何搭建网关,如何配置相关模块的路由,如何发布网关微服务到nacos上?
(1)创建一个
SpringBoot工程,名为cloud-gateway;(2)添加依赖
spring-cloud-starter-gateway;(3)修改
yml文件,配置网关路由规则spring.cloud.gateway.routes: - id: cms-route uri: lb://u-context predicates: - Path=/context/** filters: - RewritePath=/(?<segment>.*),/$\{segment}
Day04
1.如何启动人人开源管理前端,要从开发工具、环境配置、相关命令方面去介绍?
- 安装
nodejs 10.14.2- 安装
vscode 1.34.0版本- 保存工作区
- 进入
VSCODE终端npm -v查看npm版本- 配置配置淘宝镜像
cnpm npm install -g cnpm --registry=https://registry.npm.taobaonpm install npm@6.14.5 -g#安装指定版本的npm- 安装
node_modules依赖包cnpm install- 编译打包运行前端项目
npm run dev
2.如何配置前端使用api网关请求后端api接口?
修改配置文件对接后端
api地址 文件:renren-fast-vue\static\config\index.js
window.SITE_CONFIG['baseUrl'] = 'http://localhost:8080/renren-fast';
替换为
window.SITE_CONFIG['baseUrl'] = 'http://localhost:8888/api'; // 网关地址配置
cloud-gateway转发到renren-fast的路由routes: - id: renrenfast-route # 人人后台服务路由 uri: lb://renren-fast predicates: - Path=/api/** filters: - RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}
3.什么是跨域? 如何我们项目中如何解决跨域的?
什么是跨域
跨域资源共享(CORS) 是一种机制,它使用额外的 HTTP 头来告诉浏览器 让运行在一个origin(domain) 上的Web应用被准许访问来自不同源服务器上的指定的资源。当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,资源会发起一个跨域 HTTP 请求。如何解决跨域:
添加响应头,配置当次请求允许跨域
在cloud-gateway模块添加解决跨域的配置文件GatewayCorsConfiguration.java@Configurationpublic class GatewayCorsConfiguration { @Bean public CorsWebFilter corsWebFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration corsConfiguration = new CorsConfiguration(); // 配置跨域 corsConfiguration.addAllowedHeader("*"); // 允许所有请求头跨域 corsConfiguration.addAllowedMethod("*"); // 允许所有请求方法跨域 corsConfiguration.addAllowedOrigin("*"); // 允许所有请求来源跨域 corsConfiguration.setAllowCredentials(true); //允许携带cookie跨域,否则跨域请求会丢失cookie信息 source.registerCorsConfiguration("/**", corsConfiguration); return new CorsWebFilter(source); }}注释
renren-fast跨域配置 重启各个服务及网关,测试即可成功登录
4.如何开发配置题目分类服务目录和菜单?如何显示新增、批量删除按钮的?
添加题目分类服务目录:选择管理界面–系统管理–菜单管理,点击 新增按钮
添加题目菜单:选择管理界面–系统管理–菜单管理,点击 新增按钮
拷贝
\main\resources\src\views\modules\question目录到前端目录renren-fast-vue\src\views\modules修改
colud-gateway网关配置,配置转发题目微服务
qms-route的路由配置一定要配置在renrenfast-route之前,拥有加载顺序显示新增、批量删除按钮:
打开前端工程
renren-fast-vue找到配置文件src\utils\index.js暂时不判断权限,全部返回true
5.什么是逻辑删除? 如果需要查看程序调用数据是发送的sql语句等情况应该如何配置?
逻辑删除:就是不直接把数据记录从数据库删除,而是采用设置删除标记来标识数据记录被删除
查看
sql语句配置:logging:level: com.offcn.question: debug
6.如何实现模糊查询功能?如何实现分页功能?
实现模糊查询功能:
修改实现类TypeServiceImpl的方法queryPage@Overridepublic PageUtils queryPage(Map<String, Object> params) { //1、获取查询关键字 String key= (String) params.get("key"); //2、创建查询条件对象 QueryWrapper<TypeEntity> queryWrapper = new QueryWrapper<>(); //3、设置查询条件 if(!StringUtils.isEmpty(key)){ queryWrapper.eq("id",key).or().like("type",key); } IPage<TypeEntity> page = this.page( new Query<TypeEntity>().getPage(params), queryWrapper ); return new PageUtils(page);}配置分页
@Configuration@EnableTransactionManagementpublic class MyBatisConfig { //引入分页插件 @Bean public PaginationInterceptor paginationInterceptor() { PaginationInterceptor paginationInterceptor = new PaginationInterceptor(); // 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认false paginationInterceptor.setOverflow(true); // 设置最大单页限制数量,默认 500 条,-1 不受限制 paginationInterceptor.setLimit(1000); return paginationInterceptor; }}
7.如何实现题目类型下拉菜单数据的查询?
获取根据分类获取题库列表数据接口
接口:
TypeService增加获取全部分类方法实现类:
TypeServiceImpl实现编写获取题库分类控制器
修改
TypeController,增加获取全部分类接口前端代码编写
修改
question-add-or-update.vue修改数据结构,定义题目类型下拉菜单数据
修改方法,当用户点击打开新增、或者修改页面的时候,调用后台接口获取全部题目类型数据
点击新增、或者修改,可以看到题目类型下拉菜单成功显示
8.新增题目 如何处理是否显示、是否删除?
修改页面
question-add-or-update.vue<el-radio v-model="dataForm.enable" label="0">不显示</el-radio> <el-radio v-model="dataForm.enable" label="1">显示</el-radio>修改数据结构,定义是否显示、删除标记的初始化值
enable: '1'
delFlag: '0'dataForm: { id: 0, title: '', answer: '', level: '', displayOrder: '', subTitle: '', type: '', enable: '1', delFlag: '0', createTime: '', updateTime: ''},当选中数据进行修改的时候,是否显示和删除没有回显。进行如下操作使其回显
关键点:data.question.enable + ''
data.question.delFlag + ''if (data && data.code === 0) { this.dataForm.title = data.question.title this.dataForm.answer = data.question.answer this.dataForm.level = data.question.level this.dataForm.displayOrder = data.question.displayOrder this.dataForm.subTitle = data.question.subTitle this.dataForm.type = data.question.type this.dataForm.enable = data.question.enable + '' this.dataForm.delFlag = data.question.delFlag + '' this.dataForm.createTime = data.question.createTime this.dataForm.updateTime = data.question.updateTime}
9.如何快速设置是否显示?
修改页面
question.vue添加更新状态方法methods: { // 更新题目是否显示 updateQuestionStatus (data) { console.log(data) // 析构函数提取data中的id和enable数据 let {id, enable} = data this.$http({ url: this.$http.adornUrl('/question/question/update'), method: 'post', data: this.$http.adornData({id, enable}, false) }).then(({ data }) => { this.$message({ type: 'success', message: '状态更新成功' }) }) }},修改页面
question.vue<el-table-column prop="enable" header-align="center" align="center" label="是否显示"> <template slot-scope="scope"> <el-switch v-model="scope.row.enable" :active-value=1 :inactive-value=0 active-color="#13ce66" inactive-color="#ff4949" @change="updateQuestionStatus(scope.row)"> </el-switch> </template></el-table-column>
10.简述下如何实现题目的导入、导出功能?
后端:
修改项目
u-question的接口QuestionService增加导入、导出接口定义//导入 public Map importExcel(MultipartFile file);//导出 public Workbook exportExcel();修改实现类
QuestionServiceImpl实现导入、导出方法修改控制器
QuestionController实现导入、导出方法@PostMapping("/upload")前端:
在
renren-fast-vue前端工程/src/views/modules/question/目录下新增视图文件question-import.vue修改题目维护页面
question.vueimport ImportExcel from './question-import'
声明允许显示导入弹窗变量importVisible: false
把导入页面注册到当前页面组件:ImportExcel
添加导入导出点击触发方法:
在页面模板区域,添加 导入、导出按钮:<el-button type="primary"@click="importExcelHandle()"> 导入</el-button>在模板中注册导入窗口:
<ImportExcel v-if="importVisible" ref="importExcel" @refreshDataList="getDataList">
11.如何实现题库分类数量统计分析?
后端:
打开java模块
u-question,编辑接口QuestionService新增统计分类题目数量方法public List<Map<String, Object>> countTypeQuestion();编辑接口实现类
QuestionServiceImpl,编写统计分类题目数量实现方法public List<Map<String, Object>> countTypeQuestion() { //SELECT COUNT(*) AS num,TYPE FROM qms_question WHERE 1=1 GROUP BY TYPE QueryWrapper<QuestionEntity> queryWrapper = new QueryWrapper<QuestionEntity>().select("TYPE,COUNT(TYPE) AS num").groupBy("type"); List<Map<String, Object>> mapList = questionDao.selectMaps(queryWrapper); return mapList;}编辑
QuestionController,新增统计分类题目数量方法@RequestMapping("/countTypeQuestion")前端:
1.前端项目renren-fast-vue/src/views/modules/question目录 新增统计注册用户数量页面echarts.vue
2.配置用户注册统计菜单:菜单管理——题目管理——querstion/echarts
Day05
1.简述什么OSS云存储? 如何开发阿里云存储?如何测试文件上传?
什么是oss云存储:
阿里云对象存储OSS(Object Storage Service)是阿里云提供的海量、安全、低成本、高持久的云存储服务。我们需要将上传的文件进行存储,传统的文件上传到本机已经不适用于分布式系统,自己搭建服务器有复杂性和维护成本,所以我们可以使用市面上成熟的文件存储服务,如阿里云OSS对象存储。如何开发:
2.1 登陆
2.2 创建
Bucket并新建文件夹2.3 申请
accesskey
- 创建用户
- 添加权限
AliyunOSSFullAccess- 点击用户列表,点击有用户,创建
AccessKey获得AccessKey ID和AccessKey Secret文件文件上传测试:
3.1 在模块中引入SDK依赖
3.2 测试类中编写文件上传测试方法
- 创建三个字符串变量,
endpoint、accessKeyId、accessKeySecret- 创建
ossClient实例调用build方法,传入三个字符串- 创建要上传文件的输入流
- 调用
ossClient实例的putObject方法,传入参数- 关闭
ossClient- 测试完成
2.分别简述通过应用服务器中转上传和直接javascript客户端上传到oss上传原因?
通过应用服务器中转上传
用户数据需先上传到应用服务器,之后再上传到
OSS直接
javaScript客户端上传到oss
- 用户发送上传
Policy(策略)请求到应用服务器。- 应用服务器返回上传
Policy和签名给用户。- 用户直接上传数据到
OSS
3.如何解决直接javascript客户端上传到oss的时候的安全隐患?
采用
JavaScript客户端直接签名时,AccessKeyID和AcessKeySecret会暴露在前端页面,因此存在严重的安全隐患。因此,OSS提供了服务端签名后直传的方案来解决安全隐患。
4.如何实现图片上传到oss云存储?如何在题目分类新增页面增加上传控件实现图片上传?
图片上传
oss云存储:
- 创建三个字符串变量,
endpoint、accessKeyId、accessKeySecret- 创建
ossClient实例调用build方法,传入三个字符串- 创建要上传图片的输入流
- 调用
ossClient实例的putObject方法,传入参数- 关闭
ossClient- 上传完成
页面增加上传控件实现图片上传:
在前端工程
renren-fast-vue目录src/views/modules/common目录下 创建上传页面singleUpload.vue在前端工程
renren-fast-vue目录src/views/modules/question目录下 修改页面type-add-or-update.vue引入上传控件
在
export default上面引入:import SingleUpload from "../common/singleUpload" // 引入单文件上传组件注册上传组件:
export default { components:{ SingleUpload }}修改分类logo输入框,改成上传按钮
5.简述下如何实现广告内容的前端维护管理?
- 将人人代码生成器生成的前端页面导入,拷贝
\main\resources\src\views\modules\context目录到前端目录renren-fast-vue\src\views\modules- 添加广告内容管理目录(一级菜单)
- 添加广告轮播图管理菜单(二级菜单)
- 添加广告资讯管理菜单(二级菜单)
- 配置网关转发,找到
cloud-gateway,修改配置文件application.yml- 修改广告轮播图广告配图上传
- 修改广告资讯广告配图上传
6.简述下如何实现用户管理前端维护管理?
拷贝前端代码到 前端工程
renren-fast-vue拷贝
\main\resources\src\views\modules\member目录到前端目录renren-fast-vue\src\views\modules添加用户管理目录(一级菜单)
添加用户列表管理菜单(二级菜单)
添加用户充值记录管理菜单(二级菜单)
配置网关转发,找到
cloud-gateway,修改配置文件application.yml
7.如何开发微信小程序登录及验证调用接口?请简述用户登录认证的流程?
开发微信小程序登录及验证调用接口
在
common模块中引入JWT依赖库并导入JWT工具类JWTUtil引入
redis依赖在用户模块
controller中编写login和refreshtoken方法配置网关路由转发
配置网关过滤器,拦截微信客户端请求验证token
修改
pom.xml引入common依赖,排除不需要的mybatis-plus-boot-starter依赖编写网关过滤器类
配置网关过滤器
@Configurationpublic class GatewayCorsConfiguration { @Bean public JwtCheckGatewayFilterFactory jwtCheckGatewayFilterFactory(){ return new JwtCheckGatewayFilterFactory(); }}修改网关配置,加入过滤器
filters: - RewritePath=/(?<segment>.*),/$\{segment}用户登陆认证流程
- 微信小程序使用账号密码登陆
- 用户管理后台使用账号和密码查询认证,验证成功,生成令牌,存入
redis,并返回前端- 账号执行其操作时会先经过网关,路由过滤器验证令牌是否合法,合法继续操作,不合法返回错误信息。
8.如何开发微信小程序分类、及题库、广告轮播图读取调用接口?
获取题库分类接口
修改网关配置文件,增加weixin-question-route路由- id: weixin-question-route # 提供微信客户端调用的,题库微服务路由 uri: lb://u-question predicates: - Path=/question/** filters: - RewritePath=/(?<segment>.*),/$\{segment} - JwtCheck获取广告轮播图数据接口
修改网关配置文件,增加weixin-context-route路由- id: weixin-context-route # 提供微信客户端调用的,用户微服务路由 uri: lb://u-context predicates: - Path=/context/** filters: - RewritePath=/(?<segment>.*),/$\{segment} - JwtCheck
9.简述如何编写接口文档?
接口文档应该包含以下信息:
- 基本信息
- Path:请求路径
- Method:请求方式
- 接口描述:接口的详细描述
- Request
- Headers:请求头
- Query:参数
- Body:请求体
- Response
Day06
1.什么是微信小程序? 微信小程序和移动应用相比有哪些区别?以及如何选择?
小程序是一种不需要下载安装即可以使用的应用,他实现了应用“触手可及”的梦想,用户扫一扫或者搜一下即可打开应用。也体现了“用完即走”的理念,用户不用担心是否安装太多应用的问题。应用将无处不在随时可用,但又无需安装卸载。
小程序和移动应用的区别
- 下载:App 从应用商店(如 App Store)里下载;小程序 通过微信(扫描二维码、搜索)直接获得;
- 安装:App 安装在手机内存中,就像自己买了辆车放在车库里随时开;小程序不需要安装,就像免费用嘀嘀打车,召之即来用完拜拜;
- 占用空间:App 会一直存在手机中占用空间,太多的 App 可能会导致内存不足;小程序因为不需要安装,占用内存空间忽略不计;
- 广告推送:App 会隔三差五给用户推送广告,太多未读提示会逼死强迫症;小程序不允许主动给用户发送广告,仅能回复模版消息;
- 机会:App市场已经饱和,几乎所有的领域都已经被覆盖;小程序是一片蓝海,在新的使用场景下有很多瓜分蛋糕的好机会;
- 开发:App 需要适配市场上很多款的主流手机,开发成本大;小程序 一次开发就可以自动适配所有手机;
- 发布:App 需要向十几个应用商店提交审核,且每个应用商店要求的资料都不一样,非常繁琐;小程序 只需要提交到微信公众平台审核 ;
- 用户群:App 面向所有智能手机用户,截止2015年约19亿台;小程序面向所有微信用户,约8亿人 ;
- 开发周期:一款完善的双平台 App 平均的开发周期约3个月;小程序 平均开发周期约2周,仅为App的六分之一;
- 功能:App 可以实现完整功能;小程序仅限微信提供的接口功能;
如何选择:
- 使用频率高而且还很重要不适合用微信小程序,应该用原生的app开发
- 使用频率低而且很重要应该用微信小程序开发
- 使用频率高但是不重要应该用小程序为入口导向原生app
- 使用频率不高也不重要优选小程序开发
2.微信小程序用到的技术点有哪些?常用的api包括哪些内容?
技术点:
- 并不是
HTML5/CSS3技术实现- 抛弃了臃肿的
WebView- 采用了
JavaScriptCore动态解析- 大量借鉴
React.js+ReactNative.js思想常用
api:
- 视图容器:视图(View)、滚动视图、
Swiper- 基础内容:图标、文本、进度条
- 表单组件:按钮、表单等等
- 操作反馈导航
- 媒体组建:音频、图片、视频。
- 地图画布文件操作能力
- 网络:上传下载能力、
WebSocket- 数据:数据缓存能力
- 位置:获取位置、查看位置
- 设备:网络状态、系统信息、重力感应、罗盘
- 界面:设置导航条、导航、动画、绘图等等
- 开放接口:登录,包括签名加密,用户信息、微信支付、模板消息
3.如何开始开发一个微信小程序? 需要那些准备步骤?简单介绍下微信小程序的目录结构?
- 注册微信小程序账号
- 下载微信小程序开发工具,下载完成以后直接安装即可使用,使用时需要用微信扫描二维码
- 打开工具后选择小程序开发,然后点击添加项目,点击 + 号,新建小程序,点击新建按钮,即可新建一个小程序项目
小程序包含一个描述整体程序的 app 和多个描述各自页面的 page。一个小程序主体部分主要由
app.js、app.json、app.wxss(不是必须的)组成,必须放在项目的根目录下。
4.一个微信小程序页面部分包含那四个文件?如何快速生成某个页面的文件?
一个小程序页面由四个文件组成,分别是
js、wxml、json、wxss,其中js和wxml是必需的,json和wxss不是必需。js用于页面逻辑,wxml用于页面结构,json用于页面配置,wxss用于页面样式。新建页面时,在pages项中添加新建页面配置项,将会自动创建页面。小程序页面文件夹名与文件名相同。
5.app.json 文件有什么功能? 可以进行哪些配置? app.json中window配置和某个页面中的xxx.json配置有什么区别?
app.json文件用来对微信小程序进行全局配置,决定页面文件的路径、窗口表现、设置网络超时时间、设置多 tab 等
app.json可以进行如下配置:
pages:设置页面路径window:设置默认页面的窗口表现tabBar:设置底部tab的表现netWorkTimeout:设置网络超时时间debug:设置是否开启debug模式除了全局的
app.json配置外,还可以用.json文件对小程序项目中的每一个页面进行配置,但只能设置本页面的window配置项的内容,页面.json文件中的window配置值将覆盖app.json中的配置值。
6.什么是tabBar?请简述下常见的属性以及意义?
小程序可以是多标签页切换的应用,需要通过
tarBar配置项来指定标签页的表现,及标签页切换时所显示的对应页面。常见属性:
color:tab上的文字默认颜色selectedColor:tab上的文字选中时的颜色backgroundColor:tab的背景色borderStyle:tabber上边框的颜色,仅支持black/whitelist:tab的列表,最少2个最多5个tabposition:可选值bottom、top其中list接收一个数组,数组中的每个项都是一个对象,属性如下:
pagePath:页面路径,必须在pages中先定义text:tab上按钮文字iconPath:图片路径,icon大小限制40kb,建议尺寸81px*81px,当position为top时,此参数无效,不支持网络图片selectedIconPath:选中时的图片路径,icon大小限制40kb,建议尺寸81px*81px,当position为top时,此参数无效
7.什么是tabBar?请简述下常见的属性以及意义?
数据是通过
{{}}来绑定的,在index.js中定义数据通过对
button设置点击事件,动态的更改绑定数据的内容
- 给
button设置点击事件<button type="primary" bindtap="btnClick">primary</button>- 在
index.js中实现点击事件btnClick:function(){this.setData({text:"修改后的内容"})}
8.如何使用条件标签和循环标签? 如何使用模板?
条件标签:
定义if标签
index.wxml:<view wx:if="{{show}}">{{textif}}</view>
index.js:Page({ data:{ text:"这里是内容", textif:"if判断", show:true },当
show的值为true显示,为fasle不显示;修改index.js增加根据button的点击事件控制show的结果,动态切换数据,这样可以通过点击button来控制显示或者不显示textif的数据循环标签:
创建数组数据
index.js:data:{ text:"这里是内容", textif:"if判断", show:false, news: ['aaa','bbb','ccc','ddd'] }循环遍历显示数据
index.wxml:<view wx:for="{{news}}">这是循环的内容</view>如果需要显示for中的内容,通过:
wx:for-item获取遍历的各个节点元素,通过index获取索引,也可以根据点击事件动态改变new的数据模板的使用:
通过
include引入
在
pages下新建template文件,然后在template中创建header.wxml:<text>这是头布局</text>然后在
index.wxml中引入<include src="../template/header.wxml"/>通过
import引入
在
template下创建footer.wxml文件<template name="footer">这是底部布局</template>在
index.wxml中引入<import src="../template/footer.wxml"/><template is="footer"/>
9.什么是事件,常见的时间类别有哪些? 如何防止时间的冒泡?
什么是事件:
- 事件是视图层到逻辑层的通讯方式。
- 事件可以将用户的行为反馈到逻辑层进行处理。
- 事件可以绑定在组件上,当达到触发事件,就会执行逻辑层中对应的事件处理函数。
- 事件对象可以携带额外信息,如id, dataset, touches。
常见的类别有哪些:
- 点击事件
tap- 长按事件
longtap- 触摸事件:
touchstart:开始触摸;touchmove:手指触摸后移动;touchcansce:取消触摸;touchend:触摸结束。如何防止事件冒泡:
bind事件绑定不会阻止冒泡事件向上冒泡,catch事件绑定可以阻止冒泡事件向上冒泡
10.如何在微信小程序中配置多个页面数据,如何实现页面的跳转? 如何设置页面的标题?
多页面:
编辑项目文件
app.json,新增一个页面test1:{"pages":[ "pages/index/index", "pages/test1/test1", "pages/logs/logs"], 保存文件,自动在
pages目录下生成test1目录,下面创建了对应的页面和js样式的相关文件页面的跳转:
在首页加入跳转连接
<view class="btn-area"> <navigator url="/pages/test1/test1" hover-class="navigator-hover">跳转test页面</navigator> </view> 点击 跳转
test页面,即可跳转到test页面页面的标题:
找到所在页面的目录,找到对应的
test1.json{"navigationBarTitleText": "详情页" }
navigationBarTitleText属性值既是页面标题
11.微信小程序如何设置底部导航栏?
图标准备阿里图标库 http://www.iconfont/collections/show/29 在这个网站上下载一些自己要用到的图标,比如人员头像,
home主页等一些常用的图标,直接点击下载保存到本地,修改一下命名。也可以使用UI准备好的图标。回到项目里,新建一个
images文件夹,将刚刚下载好的图标放在文件夹底下备用,将上述起好名字的图标保存到小程序项目目录中新创建的images文件夹中添加底部导航栏配置文件找到项目根目录中的配置文件
app.json加入如下配置信息"tabBar": { "color": "#a9b7b7", "selectedColor": "#11cd6e", "borderStyle": "white", "list": [ { "selectedIconPath": "src/pages/images/11.png", "iconPath": "src/pages/images/10.png", "pagePath": "src/pages/index/index", "text": "首页" }, { "selectedIconPath": "src/pages/images/21.png", "iconPath": "src/pages/images/20.png", "pagePath": "src/pages/log/log", "text": "日志" }, { "selectedIconPath": "src/pages/images/31.png", "iconPath": "src/pages/images/30.png", "pagePath": "src/pages/test1/test1", "text": "测试" } ] },注意添加了
tabBar原理的页面链接跳转,需要增加设置tabBar属性,不加tabBar原理的不需要加
12.微信小程序页面如何跳转,如何在跳转的时候传递数据?如何使用全局变量共享参数?
navigator跳转时传参在
wxml页面跳转时候,可以在跳转地址后,传递参数:<view class="btn-area"> <navigator url="/pages/test2/test2?id=1001&name=张三" hover-class="navigator-hover">跳转test页面</navigator> </view>在
js代码里navigator跳转时修改index.jstoTest2: function(){ wx.navigateTo({ url: '../test2/test2?id=1001&name=张三', }) }修改
index.wxml<view> <button bindtap="toTest2">点击跳转</button></view>对应
test.js接收参数onLoad: function (options) { var id=options.id var name=options.name console.log('id:'+id+" name:"+name) },在
js代码里redirectTo跳转时修改index.wxml<view> <button bindtap="toTest22">点击跳转2</button></view>修改
index.jstoTest22: function(){ wx.redirectTo({ url: '../test2/test2?id=1001&name=张三', }) }带有
tagbar的页面跳转需要用到switchTab(object)来实现修改
index.wxml<view> <button bindtap="toLog">点击跳转tagbar查看日志</button></view>修改
index.jstoLog: function(){ wx.switchTab({ url: '../logs/logs', }) },使用全局变量共享参数:
修改
app.jsApp({ globalData: { userInfo: null, id: null } }) 在
js里面可以给全局变量赋值:修改index.js在onLoad方法中增加给全局变量赋值代码:var app = getApp();app.globalData.id = 2 修改
test2.js获取全局变量的值var app = getApp();//获取全局变量console.log("全局变量id:"+app.globalData.id)
13.微信小程序如何发出网络请求? 有哪些注意实现?
wx.request({url: 'http://192.168.1.137:80/app/guanggao',method: 'POST',data: { type: "1"},header: { 'Accept': 'application/json'},success: function (res) { that.setData({ images: res.data.data.guanggao })}fail:function(err){ console.log(err)}})上面的代码会发送一个http post请求,其中的参数也比较容易理解。
url:服务器的url地址data:请求的参数可以采用String data:”xxx=xxx&xxx=xxx”的形式或者Object data:{“userId”:1}的形式header: 设置请求的headermethod:http的方法,默认为GET请求success:接口成功的回调fail:接口失败的回调注意首先配置本地微信开发工具,不校验合法域名
Day07
1.优学题库微信小程序如何创建并初始化?
创建步骤分为:
- 新建项目优学题库,并且命名,选择对应存储目录
- 点测试号生成
APPID- 开发模式选择小程序,语言选中
JavaScript
2.优学题库微信小程序主要包括几个页面,每个页面的功能是什么?
页面主要包括:account,register,index,type,item,info
- 登录页面:账号登录
- 注册页面: 账号注册
- 用户信息:获取用户微信账号
- 题库分类:获取题库分类
- 具体分类实体列表页:显示选定分类的试题列表
- 题目详情页面:显示题目详情信息
3.在页面中如何获得应用程序的实例对象,进而获得全局初始化参数?
在
app.js文件中定义全局初始化参数globalData,其中包括存储的使用信息,是否捕获到全局变量,配置指向后端的网关地址。在页面的使用中,例如
account页面,需要在account.js中先获取应用实例//获取应用实例const app = getApp()Page({ ....再设置全局属性
//设置全局属性 app.globalData.userInfo = res.userInfo app.globalData.hasUserInfo = true然后将全局变量设置为本地变量
this.setData({ userInfo: res.userInfo, hasUserInfo: true })
4.小程序跳转到注册页面的时候如何获取当前微信的用户信息?
在
pages\register\register.js中定义获取应用实例,然后修改生命周期函数——监听页面加载onLoad增加获取微信用户信息方法,通过全局变量获取用户信息,然后把全局变量设置为本地变量
5. 如何判断用户登录?在用户登录的过程中当用户登录或未登录的时候,页面显示如何处理?
判断用户是否成功的标志在于
tabBar是否隐藏,通过生命周期函数——监听页面显示来判断登录页面,如果没有登录或者登录失败。没有token,则隐藏标签栏,登录成功,有token就显示标签栏,存储token到本地存储,类似于cookie,并且显示登录成功提示,跳转到题库分类页面。
6.当多个页面共享数据显示的时候,如何进行数据处理请举例说明?
利用
url传值进行数据分享,比如编写从分类跳转到题目列表页面中,在type.js文件,通过给url传递对应的id值去寻找题目分类里对应的题目列表,同时在type.wxml文件中也绑定了跳转方法//点击分类,跳转到题目列表页getItem:function(e){ var id = e.currentTarget.dataset.id console.log("key : "+id); wx.navigateTo({ url: '../item/item?id='+id, }) },<view bindtap="getItem" data-id="{{item.id}}" class="action text-bold text-black text-lg"> <image src="{{item.logoUrl}}" class="img"></image> <text class="cu-title"></text> {{item.type}}</view>利用全局变量进行数据处理,在修改页面上的注册按钮绑定获取信息方法
wx.getUserProfile({ desc: '用于完善会员资料', // 声明获取用户个人信息后的用途,后续会展示在弹窗中,请谨慎填写 success: (res) => { console.log(res) //设置全局属性 app.globalData.userInfo = res.userInfo app.globalData.hasUserInfo = true //将全局变量设置为本地变量 this.setData({ userInfo: res.userInfo, hasUserInfo: true }) //跳转到注册页码 this.register() } }) },利用缓存原理,通过获取缓存中的数据,实现共享,比如调用
getStorageSync获取token。onShow: function () { //生命周期函数 -- 监听页面显示 //判断如果登录token不存在就影藏tabBar console.log('token:--'+ wx.getStorageSync('token')) if(!wx.getStorageSync("token")){//没有登录隐藏标签栏,登录成功显示标签栏 console.log('ssss'); wx.hideTabBar({ animation: false, }) } },
7.在题目分类过程中如何向后台发送查询请求,如何进行响应结果的处理?请写出相关代码
在
type.js文件中先获取应用实例,然后编写获取分类数据方法,通过发送的请求中使用的url向后台进行数据的查询,并且后台返回的数据在存储在赋给本地变量。//读取全部分类数据loadAllType:function() { let baseUrl = app.globalData.baseUrl; var t = this; wx.request({ url: baseUrl+'/question/type/findall', method:'GET', header:{'Authorization':wx.getStorageSync('token')}, success:function(res){ console.log(res.data); t.setData({ exams:res.data.data }) console.log(res.data); } })},
8.根据题目分类的编号查询题目列表,或者根据题目id查询题目详情中,请问页面如何将编号和id传递到js响应时间方法的代码中的?
先修改
type.js://点击分类,跳转到题目列表页 getItem:function(e){ var id = e.currentTarget.dataset.id console.log("key : "+id); wx.navigateTo({ url: '../item/item?id='+id, }) },对应的
type.wxml调用跳转方法:<view bindtap="getItem" data-id="{{item.id}}" class="action text-bold text-black text-lg"> <image src="{{item.logoUrl}}" class="img"></image> <text class="cu-title"></text> {{item.type}}</view>
9.如何实现微信真机测试?为什么?
- 内网穿透:需要独立的IP地址可以回调接口,哲西云浏览器客户端配置隧道,映射网关的
8888端口- 修改微信小程序对接地址,在
app.js的globalData中的baseUrl修改- 启动真机调试
Day08
1.请你简单介绍下软件开发中系统架构的演变?
单一应用—>垂直拆分—>分布式服务—>服务治理(SOA)—>微服务架构
- 集中式架构(单一应用):当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。
- 垂直拆分:当访问量逐渐增大,单一应用无法满足需求,此时为了应对更高的并发和业务需求,我们根据业务功能对系统进行拆分。
- 分布式服务:当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。
- 服务治理(SOA):当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)是关键。
- 微服务与微服务架构:微服务是一种架构模式或者一种架构风格,提倡将单一应用程序划分成一组小的服务独立部署,服务之间相互配合、相互协调,每个服务运行于自己的进程中。
2.远程调用的方式有几种? 他们的区别如何?如何选择?
RPC:Remote Produce Call远程过程调用,类似的还有RMI(Remote Method Invocation,远程方法调用)。自定义数据格式,基于原生TCP通信,速度快,效率高。早期的webservice,现在热门的dubbo,都是RPC的典型
Http:http其实是一种网络传输协议,基于TCP,规定了数据传输的格式。现在客户端浏览器与服务端通信基本都是采用Http协议。也可以用来进行远程服务调用。缺点是消息封装臃肿。现在热门的Rest风格,就可以通过http协议来实现。
http和rpc差别:
RPC并没有规定数据传输格式,这个格式可以任意指定,不同的RPC协议,数据格式不一定相同。Http中定义了资源定位的路径,RPC中并不需要- 最重要的一点:
RPC需要满足像调用本地服务一样调用远程服务,也就是对调用过程在API层面进行封装。Http协议没有这样的要求,因此请求、响应等细节需要我们自己去实现。优点:
RPC方式更加透明,对用户更方便。Http方式更灵活,没有规定API和语言,跨语言、跨平台缺点:
RPC方式需要在API层面进行封装,限制了开发的语言环境
- 速度来看,
RPC要比http更快,虽然底层都是TCP,但是http协议的信息往往比较臃肿,不过可以采用gzip压缩。- 难度来看,
RPC实现较为复杂,http相对比较简单- 灵活性来看,
http更胜一筹,因为它不关心实现细节,跨平台、跨语言。微服务,更加强调的是独立、自治、灵活。而
RPC方式的限制较多,因此微服务框架中,一般都会采用基于Http的Rest风格服务。
3.SpringCloud是什么?
Spring Cloud是一系列框架的有序集合。它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、熔断器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。Spring Cloud并没有重复制造轮子,它只是将目前各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。
4.简述Spring Cloud和Spring Boot的关系?
Spring Boot是Spring的一套快速配置脚手架,可以基于Spring Boot快速开发单个微服务,Spring Cloud是一个基于Spring Boot实现的云应用开发工具;
Spring Boot可以离开Spring Cloud独立使用开发项目,但是Spring Cloud离不开Spring Boot,属于依赖的关系。
5.SpringCloudNetflix分布式解决方案中主要框架有哪些?
服务发现————Netflix Eureka
服务调用————Netflix Feign
熔断器—————Netflix Hystrix
服务网关————Netflix Zuul
分布式配置———Spring Cloud Config
消息总线 ————Spring Cloud Bus
6.Eureka是什么? 包括哪些部分?功能分别是什么?
Eureka是Spring Cloud Netflix微服务套件中的一部分,它基于Netflix Eureka做了二次封装, 主要负责完成微服务架构中的服务治理功能。
Eureka包含两个组件:Eureka Server和Eureka Client。
Eureka Server提供服务注册服务。
Eureka Client是一个java客户端,用来简化与Eureka Server的交互、客户端同时也就是一个内置的、使用轮询(round-robin)负载算法的负载均衡器。
7.如何创建一个Eureka服务端? 需要那些配置?
1.创建EurekaServer01模块(Maven工程即可)
2.导入pom文件
<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency></dependencies>3.编写配置文件
#内置的tomcat服务启动监听端口号server:port: 8888\#应用名称spring:application: name: EurekaServer01\#EurekaServer配置eureka:instance: hostname: localhostserver: \#关闭自我保护模式(缺省为打开) enable-self-preservation: false \#扫描失效服务的间隔时间(缺省为60*1000ms) eviction-interval-timer-in-ms: 1000client: register-with-eureka: false #此EurekaServer不在注册到其他的注册中心 fetch-registry: false #不在从其他中心中心拉取服务器信息 service-url: defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka #注册中心访问地址4.编写启动类
@SpringBootApplication@EnableEurekaServerpublic class EurekaServer01Start { public static void main(String[] args) { SpringApplication.run(EurekaServer01Start.class, args); }}5.启动测试结果
然后在浏览器地址栏输入 http://localhost:8888/ 运行
Eureka客户端
8.如何创建一个微服务集成Eureka客户端?
1.创建HelloProvider01模块
2.编写pom文件
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>com.offcn</groupId> <artifactId>HelloInterface01</artifactId> <version>1.0</version> </dependency></dependencies>3.编写yml文件
spring:application: name: HelloProvider01server:port: 9001eureka:client: service-url: defaultZone: http://localhost:8888/eureka4.编写接口的实现类
package com.offcn.service.impl;import com.offcn.service.HelloService;import org.springframework.stereotype.Service;@Servicepublic class HelloServiceImpl implements HelloService { @Override public String sayHello() { return "hello Eureka!"; }}5.编写HelloController实现类
package com.offcn.controller;import com.offcn.service.HelloService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;@RestControllerpublic class HelloController { @Autowired private HelloService helloService; @GetMapping("/hello") public String sayHello(){ return helloService.sayHello(); }}6.编写启动类
package com.offcn;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;@SpringBootApplication@EnableDiscoveryClientpublic class HelloProvider01Start { public static void main(String[] args) { SpringApplication.run(HelloProvider01Start.class,args); }}7.启动服务
9.服务的消费者如何远程调用服务生产者提供的服务?(DiscoverClient、RestTemplate)
1.创建HelloConsumer01模块
2.导入pom依赖
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>com.offcn</groupId> <artifactId>HelloInterface01</artifactId> <version>1.0</version> </dependency> </dependencies>3.yml配置文件
spring:application: name: helloConsumer01server:port: 9002eureka:client: service-url: defaultZone: http://localhost:8888/eureka4.编写启动类
@SpringBootApplication@EnableDiscoveryClientpublic class HelloConsumer01Start { public static void main(String[] args) { SpringApplication.run(HelloConsumer01Start.class,args); }}5.编写配置类(也可以在启动类中进行配置)实例化远程调用模板类
@Bean public RestTemplate restTemplate(){ return new RestTemplate(); }6.编写实现类
package com.offcn.service.impl;import com.offcn.service.HelloService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.cloud.client.ServiceInstance;import org.springframework.cloud.client.discovery.DiscoveryClient;import org.springframework.http.HttpStatus;import org.springframework.http.ResponseEntity;import org.springframework.stereotype.Service;import org.springframework.web.client.RestTemplate;import java.util.List;@Servicepublic class HelloServiceImpl implements HelloService { @Autowired private DiscoveryClient discoveryClient; //是一个服务查询工具,可以连接到EurekaServer,根据服务名称去查询服务信息,注意别导错包了 @Autowired private RestTemplate restTemplate; //调用rest风格接口工具类 //从EurekaServer获取对应服务的地址和端口 public String getServerInfo(){ List<ServiceInstance> instanceList = discoveryClient.getInstances("HELLOPROVIDER01"); if(instanceList!=null&&instanceList.size()>0){ ServiceInstance serviceInstance = instanceList.get(0); //获取对应服务的主机地址 String host = serviceInstance.getHost(); //获取对应服务端口号 int port = serviceInstance.getPort(); return "http://"+host+":"+port; } return null; } @Override public String sayHello(){ ResponseEntity<String> responseEntity = restTemplate.getForEntity(getServerInfo() + "/hello", String.class); String body = responseEntity.getBody(); System.out.println("调用远程服务返回值:"+body); return body; }}7.controller调用代码
package com.offcn.controller;import com.offcn.service.HelloService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestControllerpublic class HelloControllerConsumer { @Autowired private HelloService helloService; @RequestMapping("/testHello") public String sayHello(){ return helloService.sayHello(); }}8.启动类
package com.offcn;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;import org.springframework.context.annotation.Bean;import org.springframework.web.client.RestTemplate;@SpringBootApplication@EnableDiscoveryClientpublic class HelloConsumer01Start { public static void main(String[] args) { SpringApplication.run(HelloConsumer01Start.class,args); } @Bean public RestTemplate getResTemplate(){ return new RestTemplate(); }}9.启动服务
10.如何实现Eureka的高可用性?
1.创建第二个eureka
2.修改eureka1的yml文件配置
#内置的tomcat服务启动监听端口号server:port: 10086\#应用名称spring:application: name: EurekaServer\#EurekaServer配置eureka:client: service-url: defaultZone: http://localhost:10087/eureka #指向高可用另外一台Eureka服务器3.编写eureka2的yml文件
#内置的tomcat服务启动监听端口号server:port: 10087\#应用名称spring:application: name: EurekaServer\#EurekaServer配置eureka:client: service-url: defaultZone: http://localhost:10086/eureka #指向高可用另外一台Eureka服务器4.启动两个eureka
启动项目EurekaServer1、EurekaServer2
5.修改提供者和消费者的注册中心地址
#配置EurekaServer注册中心服务器地址eureka:client: service-url: defaultZone: http://localhost:10086/eureka,http://localhost:10087/eureka此时配置文件同时指向两个配置中心
11.如何在调用服务的时候实现负载均衡(LoadBlance、 Ribbon)?
基于LoadBalance服务调用1.创建两个提供者模块启动两个工程查看注册中心2.创建消费者测试负载均衡
新建模块UserWeb(参考HelloConsumer01),引入依赖如下
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>com.offcn</groupId> <artifactId>UserInterface</artifactId> <version>1.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> </dependencies>编写yml文件
server: port: 9001spring: thymeleaf: prefix: classpath:/templates/ #在构建URL时预先查看名称的前缀 suffix: .html #构建URL时附加到查看名称的后缀 cache: false application: name: UserWebeureka: client: service-url: defaultZone: http://localhost:10086/eureka,http://localhost:10087/eureka远程调用业务类代码
@Servicepublic class UserServiceImpl implements UserService { //远程服务调用客户端 @Autowired RestTemplate restTemplate; //支持负载均衡的调用客户端 @Autowired LoadBalancerClient loadBalancerClient; /*** * 通过客户端负载均衡器获取生产者服务器基础地址 * @return */ public String getServerUrl() { //通过客户端调用服务均衡器查找服务 ServiceInstance inst = loadBalancerClient.choose("USERPROVIDER"); //获取服务提供者服务器ip、端口号 String ip = inst.getHost(); int port = inst.getPort(); //拼接调用地址 String url="http://"+ip+":"+port; return url; } @Override public Map<String,Object> findAll() { String url=getServerUrl(); Map<String,Object> map = restTemplate.getForObject(url + "/user/", Map.class); return map; } }controller代码
package com.offcn.controller;import com.offcn.service.UserService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Controller;import org.springframework.ui.Model;import org.springframework.web.bind.annotation.GetMapping;import java.util.Map;/** * @Author zdy * @Date 2021/1/28 16:37 * @Description :ujiuye java */@Controllerpublic class UserController { @Autowired private UserService userService; //查询全部用户数据,显示列表 @GetMapping("/") public String findAll(Model model){ Map<String, Object> map = userService.findAll(); model.addAttribute("page",map.get("list")); //获取服务提供者信息 model.addAttribute("version",map.get("version")); System.out.println("version:"+map.get("version")); return "user/list"; }}创建list.html页面代码
测试效果访问消费者工程UserWeb02地址:http://localhost:9001多次刷新后可以看见 控制台在 provider1和provider2之间切换
b.基于Ribbon的远程调用
创建新模块UserWeb02
复制工程UserWeb的内容到UserWeb02
修改pom.xml引入Ribbon依赖包
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId></dependency>修改UserWeb02工程中配置文件application.yml
#服务器端口号server: port: 9002spring: thymeleaf: prefix: classpath:/templates/ #在构建URL时预先查看名称的前缀 suffix: .html #构建URL时附加到查看名称的后缀 cache: false application: name: UserWeb02修改项目启动类AppStartApplication
在
RestTemplate定义上增加注解@LoadBalanced,即可开启Ribbon修改类UserServiceImpl
@Servicepublic class UserServiceImpl implements UserService { @Autowired private RestTemplate restTemplate; //rest接口调用工具//编写查询服务方法 public String getServerUrl(){ return "http://USERPROVIDER"; } @Override public Map<String,Object> findAll() { String url=getServerUrl(); Map<String,Object> map = restTemplate.getForObject(url + "/user/", Map.class); return map; } } ```6. **测试效果**
12.什么是服务调用的重试机制?为什么要使用重试机制, 如何实现重试机制?
重试机制:当一次服务调用失败后,不会立即抛出一次,而是再次重试另一个服务。
如果一个服务停掉,因为服务剔除的延迟,消费者并不会立即得到最新的服务列表,此时再次访问你会得到错误提示。重试机制可以避免出现这种异常,正常调用服务。
实现重试机制:
修改配置文件application.yml
spring: #开启Spring Cloud的重试功能 cloud: loadbalancer: retry: enabled: trueUSERPROVIDER: ribbon: #配置指定服务的负载均衡策略 NFLoadBalancerRuleClassName: comflix.loadbalancer.RoundRobinRule #Ribbon的连接超时时间 ConnectTimeout: 250 #Ribbon的数据读取超时时间 ReadTimeout: 1000 # 是否对所有操作都进行重试 OkToRetryOnAllOperations: true # 切换实例的重试次数 MaxAutoRetriesNextServer: 1 # 对当前实例的重试次数 MaxAutoRetries: 1配置RestTemplate的连接超时时间
注意:在配置文件中添加了,此处无需再次加。
@Bean @LoadBalanced public RestTemplate restTemplate(){ //设置连接,连接读取超时时间,如上在配置文件中添加了,此处无需再次加。 HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(); factory.setReadTimeout(1000); factory.setConnectTimeout(250); return new RestTemplate(factory); }因为实际调用还是采用的RestTemplate ,只不过负载均衡采用了Ribbon
修改pom文件引入依赖
<dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId> <version>1.2.4.RELEASE</version> </dependency>4.测试效果
Day09
1.什么是Feign?Feign有哪些特点?
Feign是一个声明式的web服务客户端,它使编写web服务客户端变得更加容易。创建一个接口并添加一个Fegin的注解@FeignClient,就可以通过该接口调用生产者提供的服务。Spring Cloud对Feign进行了增强,使得Feign支持了Spring MVC注解有以下两个特点:
Feign采用的是接口加注解的声明式服务调用;Fegin整合Ribbon及Eureka,支持负载均衡
2.如何使用Feign远程实现服务的调用?
创建新模块UserWeb03,修改pom.xml引入Feign依赖包
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId></dependency>修改应用启动类
启动类增加注解
@EnableFeignClients对配置文件application.yml 进行修改
#服务器端口号server: port: 9003spring: thymeleaf: cache: false application: name: UserWeb03在com.offcn.service中添加接口UserService
@FeignClient(value = "USERPROVIDER")public interface UserService { //查询全部 @GetMapping("/user/") public Map<String,Object> findAll();}删除实现类UserServiceImpl,以及依赖pom.xml移除对接口UserInterface的依赖
UserController直接调用接口UserService,余则不变
@Controllerpublic class UserController{ @Autowired UserService userService;}访问测试:http://localhost:9003/
3.如何开启日志功能?如何日志级别包括哪些?都有什么意义?
修改application.yml设置对应包的日志级别
#设置消费者指定包日志级别logging.level.offcn=debug编写配置类,定义日志级别
@Configurationpublic class FeignConfig { @Bean public Logger.Level getFeignlogger(){ return Logger.Level.FULL; }}
修改接口UserService中的注解:
@FeignClient,指定Feign日志配置文件启动测试,访问地址http://localhost:9003/
Feign支持4种级别及意义:
NONE:不记录任何日志信息,这是默认值。
BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。
4.什么是熔断器Hystrix?介绍下熔断器的工作原理?
Hystrix是Netflix的针对微服务分布式系统的熔断保护中间件,是一个有关延迟和失败容错的开源库包,用来设计隔离访问远程服务、第三方库等,防止出现级联式失败。主页:https://github/Netflix/Hystrix/
工作原理:熔断器机制的原理很简单,像家里的电路保险丝,如果电路发生短路能立刻熔断电路,避免发生灾难。在分布式系统中应用这一模式之后,服务调用方可以自己进行判断某些服务反应慢或者存在大量超时的情况时,能够主动熔断,防止整体系统被拖垮。
5.熔断器如何和Ribbon进行整合实现熔断功能?如何进行优化?
pom.xml增加Hystrix依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency>修改应用启动类。
增加注解
@EnableCircuitBreaker允许Hystrix熔断器生效修改业务实现类UserServiceImpl
@HystrixCommand(fallbackMethod="findAllCallBack"):声明一个失败回滚处理函数
findAllCallBack,当findAll执行超时(默认是1000毫秒),就会执行findAllCallBack函数,返回错误提示。为了方便查看熔断的触发时机,我们记录请求访问时间修改服务提供者让服务随机休眠一段时间,以触发熔断
//随机睡 0-1500毫秒 try { Thread.sleep(new Random().nextInt(1500)); } catch (InterruptedException e) { e.printStackTrace(); }启动应用,访问地址:http://localhost:9002/
优化Ribbon使用Hystrix:
设定消费者UserWeb02熔断超时时间,修改application.yml
#设定Hystrix熔断超时时间hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 6000改服务提供者,让服务随机休眠一段时间(1-6000毫秒)
6.熔断器如何和Feign进行整合实现熔断功能?
修改application.yml
feign: hystrix: enabled: true#总连接超时时间=(切换服务实例次数+1)*(每个实例重试次数+1)*连接超时时间 USERPROVIDER: #服务名称 ribbon: #配置指定服务的负载均衡策略 NFLoadBalancerRuleClassName: comflix.loadbalancer.RoundRobinRule # Ribbon的连接超时时间 ConnectTimeout: 250 # Ribbon的数据读取超时时间 ReadTimeout: 250 # 是否对所有操作都进行重试 OkToRetryOnAllOperations: true # 切换实例的重试次数 MaxAutoRetriesNextServer: 1 # 对当前实例的重试次数 MaxAutoRetries: 1#设定Hystrix熔断超时时间 ,理论上熔断时间应该大于总连接超时时间 hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 6000导入熔断所需依赖包
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId></dependency>编写Feign声明接口UserService的实现类UserServiceImpl
@Servicepublic class UserServiceImpl implements UserService {@Override public Map<String, Object> findAll() { Map<String,Object> map=new HashMap<>(); map.put("list",new ArrayList<>()); map.put("version","调用远程服务失败,熔断被触发!"); return map; }}
在Feign调用接口UserService中,使用注解
@FeignClient声明熔断调用实现类 ```java @FeignClient(value=“USERPROVIDER”, configuration=FeignConfig.class, fallback=UserServiceImpl.class)重启动项目,访问http://localhost:9003/
7.什么是熔断器监控服务器?如果要看到熔断的动态效果应该如何实现?
断路器是根据一段时间窗内的请求情况来判断并操作断路器的打开和关闭状态的。而这些请求情况的指标信息都是HystrixCommand和HystrixObservableCommand实例在执行过程中记录的重要度量信息,它们除了Hystrix断路器实现中使用之外,对于系统运维也有非常大的帮助。这些指标信息会以“滚动时间窗”与“桶”结合的方式进行汇总,并在内存中驻留一段时间,以供内部或外部进行查询使用,Hystrix Dashboard就是这些指标内容的消费者之一。
修改项目pom.xml增加Hystrix监控所需依赖包
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId> </dependency>在服务实例的主类中已经使用@EnableCircuitBreaker或@EnableHystrix注解,开启断路器功能,同时增加监控路径访问地址定义/hystrix.stream可以访问
重启应用,访问地址:http://localhost:9003/hystrix.stream
使用Hystrix Dashboard对Hystrix监控数据进行图形化监控
访问http://localhost:9003/hystrix,在出现的
Hystrix Dashboard的首页中输入http://localhost:9003/hystrix.stream,点击“Monitor Stream”按钮
8.什么是docker?docker的主要功能是什么?docker的常用概念有哪些?
Docker是一个开源的容器引擎,它可以帮助我们更快地交付应用。Docker可将应用程序和基础设施层隔离,并且能将基础设施当作程序一样进行管理。使用Docker,可更快地打包、测试以及部署应用程序,并可减少从编写到部署运行代码的周期。Docker官方网站:https://www.docker/常用概念:
Docker(企业版)
Docker EE由公司支持,可在经过认证的操作系统和云提供商中使用,并可运行来自Docker Store的、经过认证的容器和插件。Docker(免费版)
Docker CE是免费的Docker产品的新名称,Docker CE包含了完整的Docker平台,非常适合开发人员和运维团队构建容器APP
9.如何安装docker并且配置镜像加速器?
docker的安装:
上传 docker-ce-18.06.2.ce-3.el7.x86_64.rpm 到opt
执行命令:
yum install -y docker-ce-18.06.2.ce-3.el7.x86_64.rpm
注意:确保linux服务器处于联网状态。
配置镜像加速器:
新建编辑:
/etc/docker/daemon.json{ "registry-mirrors": ["https://ksc53x4t.mirror.aliyuncs"]}
10.操作镜像的常用指令有哪些?意义是什么?
指令搜索:
docker search 镜像名称下载镜像:
docker pull 镜像名称:查询最近的版本
docker pull 镜像名称:版本标签
看本地镜像:
docker images删除镜像:
docker rmi 镜像名:版本号:通过镜像名:版本号删除
docker rmi IMAGE ID:通过IMAGE ID删除
11.操作容器的常用指令有哪些?意义是什么?
创建一个容器:
docker run列出容器:
docker ps,
- 列出当前全部容器 :
docker ps -a- 列出当前容器的所有编号 :
docker ps -a -q停止容器:
docker stop启动容器:
docker start 容器名称或容器编号强制停止容器:
docker kill 容器名称或容器编号重启容器:
docker restart 容器名称或者容器编号退出容器:
exit
12.请列出如何创建Mysql容器,请说明其中的注意要点?
- 第一种创建容器MySQL容器的方式:
docker run -d --name=offcn_mysql -p 33306:3306 -e MYSQL_ROOT_PASSWORD=123456 -e MYSQL_DATABASE=scwdb mysql:5.7注意:因为offcn_mysql容器中不能使用vi命令,所以我们将文件拷贝到宿主机后修改后在拷贝回去
第二种创建容器MySQL容器的方式:
创新容器同时创建表的时候,解决中文问题(使用挂载的方式,将刚刚解决乱码的mysqldf 挂载下容器中相应的位置)
docker run -d --name=offcn_mysql -p 33306:3306 -v /root/mysqldf:/etc/mysql/mysql.conf.d/mysqldf -e MYSQL_ROOT_PASSWORD=123456 -eMYSQL_DATABASE=scwdb mysql:5.7第三种创建容器MySQL容器的方式:
docker run -d --name=offcn_mysql -p 33306:3306 -v /root/mysqldf:/etc/mysql/mysql.conf.d/mysqldf -v /root/scwdb.sql://docker-entrypoint-initdb.d/mysql.sql -e MYSQL_ROOT_PASSWORD=123456 -eMYSQL_DATABASE=scwdb mysql:5.7
13.如何自定义镜像
创建一个目录:
mkdir /usr/local/dockerjdk上传
jdk-8u131-linux-x64.tar.gz到/usr/local/dockerjdk/创建一个
Dockerfile文件 并编辑保存vi Dockerfile内容如下FROM centos:centos7MAINTAINER ujiuyeRUN mkdir /usr/local/javaADD jdk-8u131-linux-x64.tar.gz /usr/local/java/ENV JAVA_HOME /usr/local/java/jdk1.8.0_131ENV CLASSPATH $JAVA_HOME/libENV PATH $JAVA_HOME/bin:$PATH创建镜像 为
jdk1.8:docker build -t='jdk1.8' /usr/local/dockerjdk/查看已经创建的镜像:
docker images使用自定义的镜像创建一个容器:
docker run -di --name=java1 jdk1.8查看创建的容器:
docker ps验证自定义容器jdk环境是否正常:
docker exec -it java1 /bin/bash
java -version
Day10
1.请你简单介绍一下电商行业? 技术有什么特点? 常见的电商的模式有哪些?
电子商务,简称电商,是指在互联网、内部网和增值网上以电子交易方式进行交易活动和相关服务活动,使传统商业活动各环节的电子化、网络化。近年来,中国的电子商务快速发展,交易额连创新高,电子商务在各领域的应用不断拓展和深化、相关服务业蓬勃发展、支撑体系不断健全完善、创新的动力和能力不断增强。
电子商务的技术有以下特点:
- 技术新
- 技术范围广
- 分布式
- 高并发、集群、负载均衡、高可用
- 海量数据
- 业务复杂
- 系统安全
常见的电商模式有:
- B2B——企业对企业
- C2C——个人对个人
- B2C——企业对个人
- C2B——个人对企业
- O2O——线上到线下
- F2C——工厂到个人
- B2B2C——企业到企业到个人
2.请简单介绍下东易买项目?如包括哪些子系统,每个系统包括哪些模块?
东易买网上商城是一个综合性的B2B2C平台,类似京东和天猫商城。网站采用商家入驻的模式,商家入驻平台提交申请,有平台进行资质审核,审核通过后,商家拥有独立的管理后台录入商品信息。商品经过平台审核后即可发布。用户可以在线购买商品、加入购物车、下单、秒杀商品,可以评论已购买商品,客户可以在后台处理退款操作。
东易买网上商城主要分为网站前台、运营商后台、商家管理后台三个子系统。
网站前台:网站首页、商家首页、商品详细页、搜索页、会员中心、订单与支付相关页面、秒杀频道等模块。
运营商后台:商家审核、品牌管理、规格管理、模板管理、商品分类管理、商品审核、广告类型管理、广告管理、订单查询、商家结算等。
商家管理后台:对商品的管理以及订单查询统计、资金结算等功能。
3.简单介绍下东易买项目功能架构以及技术架构?
项目主要采用了前后端分离的开发模式。
前端主要负责用户的商品管理、商品搜索、用户模块、购物车系统、订单系统、秒杀系统的网页界面开发。
后端主要开发以下功能:
- 商品模块:品牌管理、规格管理、模块管理、分类管理、SPU管理、SKU管理
- 用户模块:用户注册、用户登录、短信验证、邮件验证、常用收件人信息管理
- 广告模块:广告分类管理、广告管理、广告显示排序
- 搜索模块:关键词搜索、商品分类统计搜索、搜索条件筛选、搜索排序、价格区间筛选
- 购物车模块:未登录数据储存、已登录数据储存、总费用合计
- 订单模块:订单生成、库存更新、订单状态管理、支付宝沙箱支付
- 秒杀模块:秒杀商品管理、秒杀下单、秒杀支付、秒杀免超卖
技术架构如下:
- 前端:vue.js
- 网关:Nginx zull
- 基础功能:商品管理、分类管理、模板管理、品牌管理、购物车管理、订单管理、权限管理、秒杀管理
- 服务层:安全认证Spring Security JWT、接口文档Swagger2、单元测试Junit、Spring Cloud、搜索框架Elasticsearch、消息中间件RabbitMQ、序列化工具Jackson、RESTful api Spring MVC、RPC Spring Cloud OpenFeign、分布式作业调度Spring Task、降级,熔断Hystrix、分布式锁Redisson、本地缓存Cookie、客户端负载均衡Ribbon、阿里云市场短信api
- 数据层:连接池DruidDataSource、持久化Mybatis、分布式ID、分库分表、主从同步、数据缓存Redis、文件储存FastDFS
- 基础设施:注册中心erueka、配置中心、数据库Mysql、数据缓存 分布式锁 网关限流Redis、fastDFS服务器
- 开发工具:IDEA
- 版本管理:Git
- CI/CD:Maven
- 集成第三方:阿里短信、邮件服务、文件服务
- 系统部署:Docker
4.简述东易买项目的结构,以及如何搭建东易买项目?
项目结构:
- dongyimai-gateway 网关模块
- dongyimai-service 微服务模块
- dongyimai-service_api JavaBean、Feign、以及Hystrix配置
- dongyimai-transaction-fesca 分布式事务模块
- dongyimai-web web服务工程
搭建项目:
首先搭建父工程dongyimai-parent,引入依赖
创建dongyimai-gateway、dongyimai-service、dongyimai-service-api、dongyimai-web工程,直接创建Maven模块,工程全部为pom工程,并将所有工程的src文件删除,pom.xml中打pom包
<packaging>pom</packaging>创建dongyimai-eureka项目模块,集成父类依赖,引入关键依赖:
创建配置文件application.yml:server: port: 8761spring: application: name: dym-eurekaeureka: instance: hostname: 127.0.0.1 client: register-with-eureka: false #是否将自己注册到eureka中 fetch-registry: false #是否从eureka中获取信息 service-url: defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka server: enable-self-preservation: false # 关闭自我保护 eviction-interval-timer-in-ms: 5000 # 每隔5秒进行一次服务列表清理配置启动类,加入
@EnableEurekaServer注解搭建公共子模块dongyimai-common,引入依赖
公共子模块引入依赖后,其他微服务引入dongyimai-common后也自动引入了这些依赖
创建entity包,在entity包下创建返回状态码实体类:
/** * 返回码 */public class StatusCode { public static final int OK = 20000;//成功 public static final int ERROR = 20001;//失败 public static final int LOGINERROR = 20002;//用户名或密码错误 public static final int ACCESSERROR = 20003;//权限不足 public static final int REMOTEERROR = 20004;//远程调用失败 public static final int REPERROR = 20005;//重复操作 public static final int NOTFOUNDERROR = 20006;//没有对应的抢购数据}entity包下建立类Result用于微服务返回结果给前端:
/** * 返回结果实体类 */public class Result<T> { private boolean flag;//是否成功 private Integer code;//返回码 private String message;//返回消息 private T data;//返回数据 public Result(boolean flag, Integer code, String message, T data) { this.flag = flag; this.code = code; this.message = message; this.data = data; } public Result(boolean flag, Integer code, String message) { this.flag = flag; this.code = code; this.message = message; } public Result() { this.flag = true; this.code = StatusCode.OK; this.message = "操作成功!"; }// and getter and setter.....}在entity包下建立类用于承载分页的数据结果:
/** * 分页结果类 */public class PageResult<T> { private Long total;//总记录数 private List<T> rows;//记录 public PageResult(Long total, List<T> rows) { this.total = total; this.rows = rows; } public PageResult() { } //getter and setter ......}创建utils工具类包,导入工具类
创建公共模块dongyimai-common-db,引入依赖
这个公共模块是连接数据库的公共微服务模块,所有需要连接数据库的微服务都继承自此工程。
创建dongyimai-service-api子模块dongyimai-sellergoods-service-api,利用代码生成器code-template自动生成文件代码,将Pojo文件中的类导入到工程中,并引入依赖
在dongyimai-service中创建dongyimai-sellergoods-service,引入依赖
创建配置文件application.yml:server: port: 9001spring: application: name: dym-sellergoods datasource: druid: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/dongyimaidb?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8 username: root password: 123456 db-type: com.alibaba.druid.pool.DruidDataSource min-idle: 5 # 最小连接池数量 max-active: 20 # 最大连接池数量 max-wait: 60000 # 获取连接时最大等待时间eureka: client: service-url: defaultZone: http://localhost:8761/eureka instance: lease-renewal-interval-in-seconds: 5 # 每隔5秒发送一次心跳 lease-expiration-duration-in-seconds: 10 # 10秒不发送就过期 prefer-ip-address: true ip-address: 127.0.0.1 instance-id: ${spring.application.name}:${server.port}feign: hystrix: enabled: truemybatis-plus: configuration: map-underscore-to-camel-case: true #开启驼峰式编写规范 type-aliases-package: com.czl.sellergoods.pojo\# 配置sql打印日志logging: level: com: czl: debug创建启动类SellerGoodsApplication,加入
@EnableDiscoveryClient和@MapperScan("com.offcn.sellergoods.dao")注解
5.品牌管理如何实现按条件分页查询?
sellergoods.service.impl.BrandServiceImpl下brand品牌对象构建条件查询对象QueryWrapper
private QueryWrapper<Brand> createQueryWrapper(Brand brand){ QueryWrapper<Brand> queryWrapper = new QueryWrapper<>(); if(brand!=null){ // if(brand.getId()!=null){ queryWrapper.eq("id",brand.getId()); } // 品牌名称 if(!StringUtils.isEmpty(brand.getName())){ queryWrapper.like("name",brand.getName()); } // 品牌首字母 if(!StringUtils.isEmpty(brand.getFirstChar())){ queryWrapper.eq("first_char",brand.getFirstChar()); } // 品牌图像 if(!StringUtils.isEmpty(brand.getImage())){ queryWrapper.eq("image",brand.getImage()); } } return queryWrapper; }sellergoods.config包下新增MyBatisPlus分页插件,PageConfig.java类
@Bean public MybatisPlusInterceptor mybatisPlusInterceptor(){ MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(); paginationInnerInterceptor.setDbType(DbType.MYSQL); //设置请求的页面大于最大页后操作,true调回到首页,false继续请求 默认false paginationInnerInterceptor.setOverflow(true); //设置最大单页限制数量,默认 500 条, -1 不受限制 paginationInnerInterceptor.setMaxLimit(500L); interceptor.addInnerInterceptor(paginationInnerInterceptor); return interceptor; }创建sellergoods.service.BrandService,增加多条件分页查询方法
PageResult<Brand> findPage(Brand brand, int page, int size);创建sellergoods.service.impl.BrandServiceImpl,添加多条件分页查询方法代码
@Override public PageResult<Brand> findPage(Brand brand, int page, int size){ Page<Brand> mypage = new Page<>(page, size); QueryWrapper<Brand> queryWrapper = this.createQueryWrapper(brand); IPage<Brand> iPage = this.page(mypage, queryWrapper); return new PageResult<Brand>(iPage.getTotal(),iPage.getRecords()); }新增控制层方法BrandController
@PostMapping(value = "/search/{page}/{size}" )public Result<PageResult> findPage(@RequestBody(required = false) Brand brand, @PathVariable int page, @PathVariable int size){ //执行搜索 PageResult<Brand> pageResult = brandService.findPage(brand, page, size); return new Result(true,StatusCode.OK,"查询成功",pageResult);}
6.什么是FastDFS? 简述下FastDFS角色和工作原理?
FastDFS是用c语言编写的一款开源的分布式文件系统,它对文件进行管理,功能包括:文件存储、文件同步、文件访问(文件上传、文件下载)等,解决了大容量存储和负载均衡的问题。特别适合以文件为载体的在线服务,如相册网站、视频网站等等。
FastDFS架构包括Tracker server和Storage server。客户端请求Tracker server进行文件上传、下载,通过Tracker server调度最终由Storage server完成文件上传和下载。
Tracker server:追踪服务器,作用是负载均衡和调度,通过Tracker server在文件上传时可以根据一些策略找到Storage server提供文件上传服务。Storage server:存储服务器,作用是文件存储,客户端上传的文件最终存储在 Storage 服务器上,Storage server没有实现自己的文件系统而是利用操作系统的文件系统来管理文件。
7.FastDFS如何实现文件的上传和下载?
上传:
Storage Server定时向Tracker Server上传状态信息,监控用户请求- 有客户端上传或下载连接请求时,
Tracker Server会安排查询可用Storage Server提供上传或下载服务- 提供服务后向客户端返回
storage的ip和端口等信息- 根据返回信息找到对应
storage后上传文件,上传后生成文件唯一标识file_id并将上传内容写入磁盘- 向客服端返回
file_id,储存文件信息下载:
1-3步与上传相同,根据file_id在storage内查找对应文件,返回file_content文件信息并下载
8.FastDFS 上传文件后 返回的文件索引信息 包括哪些内容请一一解释?
客户端上传文件后存储服务器将文件 ID 返回给客户端,此文件 ID 用于以后访问该文件的索引信息。文件索引信息包括:组名,虚拟磁盘路径,数据两级目录,文件名。
- 组名:文件上传后所在的 storage 组名称,在文件上传成功后有storage 服务器返回,需要客户端自行保存。
- 虚拟磁盘路径:storage 配置的虚拟路径,与磁盘选项store_path*对应。如果配置了
store_path0 则是 M00,如果配置了 store_path1 则是 M01,以此类推。- 数据两级目录:storage 服务器在每个虚拟磁盘路径下创建的两级目录,用于存储数据文件。
- 文件名:与文件上传时不同。是由存储服务器根据特定信息生成,文件名包含:源存储服务器 IP 地址、文件创建时间戳、文件大小、随机数和文件拓展名等信息。
Day11
1.请简述规格管理中相关表有什么特点?
tb_specification规格表,主要包含两个字段
主键id 和 规格名称spec_name
tb_specification_option规格选项表,主要包含四个字段主键
id和 规格选项名称option_name,spec_id规格id 和orders排序其中规格表中的主键
id,对应着规格选项表的spec_id,两张表是一对多的关系
2.如何实现规格的添加与修改?如何测试?
规格的新增:
在dongyimai-sellergoods-service-api 建立com.offcn.sellergoods.group包,包下建立SpecEntity类
代码:@ApiModel(description = "规格复合实体类",value = "SpecEntity")public class SpecEntity implements Serializable { @ApiModelProperty(value = "规格对象",required = false) private Specification specification; @ApiModelProperty(value = "规格选项对象",required = false) private List<SpecificationOption> specificationOptionList; public Specification getSpecification() { return specification; } public void setSpecification(Specification specification) { this.specification = specification; } public List<SpecificationOption> getSpecificationOptionList() { return specificationOptionList; } public void setSpecificationOptionList(List<SpecificationOption> specificationOptionList) { this.specificationOptionList = specificationOptionList; }}修改业务层方法
/**** 新增Specification* @param specEntity*/void add(SpecEntity specEntity);修改com.offcn.sellergoods.service.impl.SpecificationServiceImpl【需要在其实现类中引入specificationOptionMapper】
/** * 增加Specification * @param specEntity */ @Override public void add(SpecEntity specEntity){ //1.保存规格名称 this.save(specEntity.getSpecification()); //2.得到规格名称ID if (null != specEntity.getSpecificationOptionList() && specEntity.getSpecificationOptionList().size() > 0) { for (SpecificationOption specificationOption : specEntity.getSpecificationOptionList()) { //3.向规格选项中设置规格ID specificationOption.setSpecId(specEntity.getSpecification().getId()); //4.保存规格选项 specificationOptionMapper.insert(specificationOption); } } }修改控制层SpecificationController新增方法
/*** * 新增Specification数据 * @param specEntity * @return */ @ApiOperation(value = "Specification添加",notes = "添加Specification方法详情",tags = {"SpecificationController"}) @PostMapping public Result add(@RequestBody @ApiParam(name = "SpecEntity复合实体",value = "传入JSON数据",required = true) SpecEntity specEntity){ //调用SpecificationService实现添加Specification specificationService.add(specEntity); return new Result(true,StatusCode.OK,"添加成功"); }在postman软件中进行测试,输入http://localhost:9001/specification
输入测试的json数据:{ "specification":{ "specName":"颜色11" }, "specificationOptionList":[ { "optionName":"白色", "orders":1 }, { "optionName":"红色", "orders":2 } ]}修改规格:
修改com.offcn.sellergoods.service.SpecificationService,修改规格方法
/*** * 修改Specification数据 * @param specEntity */ void update(SpecEntity specEntity);修改com.offcn.sellergoods.service.impl.SpecificationServiceImpl,修改品牌方法【修改实现类方法】
/** * 修改Specification * @param specEntity */ @Override public void update(SpecEntity specEntity){ //1.修改规格名称对象 this.updateById(specEntity.getSpecification()); //2.根据ID删除规格选项集合 QueryWrapper<SpecificationOption> queryWrapper = new QueryWrapper<SpecificationOption>(); queryWrapper.eq("spec_id", specEntity.getSpecification().getId()); //执行删除 specificationOptionMapper.delete(queryWrapper); //3.重新插入规格选项 if (!CollectionUtils.isEmpty(specEntity.getSpecificationOptionList())) { for (SpecificationOption specificationOption : specEntity.getSpecificationOptionList()) { //先设置规格名称的ID specificationOption.setSpecId(specEntity.getSpecification().getId()); specificationOptionMapper.insert(specificationOption); } } }修改控制层controller方法【SpecificationController】
/*** * 修改Specification数据 * @param specEntity * @param id * @return */ @ApiOperation(value = "Specification根据ID修改",notes = "根据ID修改Specification方法详情",tags = {"SpecificationController"}) @ApiImplicitParam(paramType = "path", name = "id", value = "主键ID", required = true, dataType = "Long") @PutMapping(value="/{id}") public Result update(@RequestBody @ApiParam(name = "Specification对象",value = "传入JSON数据",required = false) SpecEntity specEntity,@PathVariable Long id){ //设置主键值 specEntity.getSpecification().setId(id); specificationService.update(specEntity); return new Result(true,StatusCode.OK,"修改成功"); }在postman软件中进行测试,输入http://localhost:9001/specification/36
测试的json数据为:
{ "specification":{ "specName":"颜色22" }, "specificationOptionList":[ { "optionName":"白色1", "orders":1 }, { "optionName":"红色1", "orders":2 } ]}
3.模板管理中表结构有什么特点?保存了哪些信息?
模板管理中的表为:
tb_type_template模板表
表中主要有以下五个字段:
- 主键:
id- 模板名称:
name- 关联规格:
spec_ids主要保存了规格的 json 数据- 关联品牌:
brand_ids主要保存了品牌的 json 数据- 扩展属性:
custom_attribute_items保存了扩展数据的 json 数据主要作用:
是用来关联品牌和规格的
定义扩展属性
4.如何实现模板添加中品牌和规格下拉列表后台数据的开发?
品牌下拉列表的开发步骤:
修改dao层 ,在BrandMapper中添加查询
代码:@Select("select id,name as text from tb_brand")public List<Map> selectOptions();修改com.offcn.sellergoods.service.BrandService,增加方法定义【需要在实现类中注入brandMapper】
代码:/** * 查询品牌下拉列表 * @return*/public List<Map> selectOptions();修改controller层【BrandController.java】
代码:@ApiOperation(value = "查询品牌下拉列表",notes = "查询品牌下拉列表",tags = {"BrandController"})@GetMapping("/selectOptions")public ResponseEntity<List<Map>> selectOptions(){ return ResponseEntity.ok(brandService.selectOptions());}测试
规格下拉列表的开发步骤【原理同上】:
修改dao层 ,在SpecificationMapper中添加查询
代码:@Select("select id,spec_name as text from tb_specification") public List<Map> selectOptions();修改com.offcn.sellergoods.service.SpecificationService,增加方法定义【需要在实现类中注入specificationMapper】
代码:/** * 查询规格下拉列表 * * @return */ @Override public List<Map> selectOptions() { return specificationMapper.selectOptions(); }修改controller层代码【主要修改SpecificationController.java】
代码如下:@ApiOperation(value = "查询规格下拉列表",notes = "查询规格下拉列表",tags = {"SpecificationController"})@GetMapping("/selectOptions")public List<Map> selectOptions() { return specificationService.selectOptions();}测试
5.请简述商品分类表有哪些特点?如何实现查询下级分类的后台业务?
商品分类表是一张自关联表
包含四个字段:
- id:主键
- parent_id:上级ID
- name:分类名称
- type_id:类型模板ID
当parent_id为0时候,查询出来的分类数据都是一级分类
当parent_id为一级分类的id的时候,查询出来的数据是该一级分类下的二级分类
当parent_id为二级分类的id的时候,查询出来的数据是该二级分类下的三级分类
查询下级分类的后台业务的步骤:
修改com.offcn.sellergoods.service.ItemCatService接口
代码:/** * 根据父级ID查询分类列表 * @param parentId * @return */public List<ItemCat> findByParentId(Long parentId);修改com.offcn.sellergoods.service.impl.ItemCatServiceImpl的实现类方法
代码:/** * 根据父级ID查询分类列表 * * @param parentId * @return */ public List<ItemCat> findByParentId(Long parentId) { ItemCat itemCat = new ItemCat(); itemCat.setParentId(parentId); QueryWrapper<ItemCat> queryWrapper = this.createQueryWrapper(itemCat); return this.list(queryWrapper); }修改控制层方法:【ItemCatController.java】
代码:@ApiOperation(value = "根据父级ID查询ItemCat",notes = "根据父级ID查询ItemCat",tags = {"ItemCatController"})@GetMapping("/findByParentId/{parentId}")public Result<List<ItemCat>> findByParentId(@PathVariable Long parentId) { List<ItemCat> list = itemCatService.findByParentId(parentId); return new Result<List<ItemCat>>(true, StatusCode.OK,"查询成功",list) ;}测试
6.什么是SPU和SKU? 请举例说明他们有什么区别?
- SPU=Standard Product Unit (标准产品单位)
- 概念:SPU 是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。
- 通俗来说:同款商品的公共属性抽取
- 举例:Iphone12就是一个SPU,与商家,与颜色、款式、套餐都无关
- SKU = stock keeping unit( 库存量单位)
- SKU 即库存进出计量的单位, 可以是以件、盒、托盘等为单位。
- SKU是物理上不可分割的最小存货单位。在使用时要根据不同业态,不同管理模式来处理,是某个库存单位的商品独有属性
- 举例:Iphone12 黑色 128G 全网通 就是一个 SKU
7.东易买中关于商品的三张表有哪些特点?相互之间有哪些关系?
关于商品三张表分别是
Tb_good商品表,Tb_goods_desc商品扩展信息表,Tb_item SKU表
Tb_goods商品表和Tb_goods_desc商品扩展信息表共用主键Tb_goods中的主键id和tb_item的goods_id对应Tb_goods和tb_goods_desc两张表是一对一的Tb_goods和tb_item两张表是一对多的关系
8.商品添加中页面中sku列表中item的数量与选中的规格有什么关系?
商品添加页面中显示的
sku列表数量就是我们勾选的中的规格来显示的。
9.如何实现商品添加功能?如何实现商品修改功能?
商品添加功能的实现:
需要先查询分类级联数据【实现商品增加之前,需要先选择对应的分类,选择分类的时候,首选选择一级分类,然后根据选中的分类,将选中的分类作为查询的父ID,再查询对应的子分类集合】
Dao层代码实现:public interface ItemCatMapper extends BaseMapper<ItemCat> {}Service代码实现:
/** * 根据父级ID查询分类列表 * @param parentId * @return */ public List<ItemCat> findByParentId(Long parentId);controller层代码实现:
@ApiOperation(value = "根据父级ID查询ItemCat",notes = "根据父级ID查询ItemCat",tags = {"ItemCatController"})@GetMapping("/findByParentId")public Result<List<ItemCat>> findByParentId(Long parentId) { List<ItemCat> list = itemCatService.findByParentId(parentId); return new Result<List<ItemCat>>(true, StatusCode.OK,"查询成功",list) ;}测试:http://localhost:9001/ItemCat/findByParentId/1
查询分类下品牌数据【根据用户选择的三级分类中的typeId(即是模板ID),根据模板ID到
tb_type_template表中查询模板信息中的品牌】Dao层代码实现:
public interface TypeTemplateMapper extends BaseMapper<TypeTemplate> {}service层接口代码实现:
/*** 根据ID查询TypeTemplate* @param id* @return*/TypeTemplate findById(Long id);service层实现类代码实现:
/*** 根据ID查询TypeTemplate* @param id* @return*/@Overridepublic TypeTemplate findById(Long id){ return this.getById(id);}Controller层代码实现:
/**** 根据ID查询TypeTemplate数据* @param id* @return*/@ApiOperation(value = "TypeTemplate根据ID查询",notes = "根据ID查询TypeTemplate方法详情",tags = {"TypeTemplateController"})@ApiImplicitParam(paramType = "path", name = "id", value = "主键ID", required = true, dataType = "Long")@GetMapping("/{id}")public Result<TypeTemplate> findById(@PathVariable Long id){//调用TypeTemplateService实现根据主键查询TypeTemplate TypeTemplate typeTemplate = typeTemplateService.findById(id); return new Result<TypeTemplate>(true,StatusCode.OK,"查询成功",typeTemplate);}规格查询【用户选择三级分类后,需要根据所选三级分类对应的模板ID查询对应的规格以及规格选项】:
Service层接口代码代码:
/** * 根据模板ID查询规格列表 * @param typeId * @return */public List<Map> findSpecList(Long typeId);Service层实现类代码实现:
@Autowiredprivate SpecificationOptionMapper specificationOptionMapper;/** * 根据模板ID查询规格列表 * * @param typeId * @return */public List<Map> findSpecList(Long typeId) { //1.根据模板ID查询模板对象 TypeTemplate typeTemplate = this.findById(typeId); //2.将规格名称JSON结构的字符串转换成JSON对象 [{"id":27,"text":"网络"},{"id":32,"text":"机身内存"}] List<Map> specList = JSON.parseArray(typeTemplate.getSpecIds(), Map.class); if (!CollectionUtils.isEmpty(specList)) { for (Map map : specList) { Long specId = new Long((Integer) map.get("id")); //Map集合中取得数值类型的值默认是整型 //3.根据规格ID查询规格选项集合 QueryWrapper<SpecificationOption> queryWrapper = new QueryWrapper(); queryWrapper.eq("spec_id", specId); List<SpecificationOption> specificationOptionList = specificationOptionMapper.selectList(queryWrapper); //4.重新将规格选项集合设置回JSON对象中 {"id":27,"text":"网络","options":[{},{},{}]} map.put("options", specificationOptionList); } } return specList;}Controller层代码实现【TypeTemplateController】
代码实现:@ApiOperation(value = "查询规格及规格选项信息",notes = "查询规格及规格选项信息",tags = {"TypeTemplateController"})@ApiImplicitParam(paramType = "path", name = "id", value = "主键ID", required = true, dataType = "Long") @GetMapping("/findSpecList/{id}") public Result<List<Map>> findSpecList(@PathVariable Long id){ List<Map> list =typeTemplateService.findSpecList(id); return new Result<List<Map>>(true, StatusCode.OK,"查询成功",list) ; }SPU+SKU保存信息【保存商品数据的时候,需要保存Spu和Sku,一个Spu对应多个Sku,先构建一个Goods对象,将Spu和List组合到一起,前端将2者数据提交过来,再实现添加操作】
pojo改造【组合实体类,com.offcn.sellergoods.group.GoodsEntity,】
public class GoodsEntity implements Serializable { private Goods goods; //商品信息 SPU private GoodsDesc goodsDesc; //商品扩展信息 private List<Item> itemList; //商品详情 SKU //..get..set..toString}扩展信息GoodsDesc是使用SPU(Goods)的主键,所以需要修改扩展信息的Pojo(GoodsDesc)的ID生成方式为input类型
service 接口层代码实现:
/*** * 新增Goods * @param goodsEntity */void add(GoodsEntity goodsEntity);service 实现类代码实现【增加启用规格判断】:
/** * 增加Goods * @param goodsEntity */ @Override public void add(GoodsEntity goodsEntity){ goodsEntity.getGoods().setAuditStatus("0"); //审核状态 未审核 //1.保存SPU 商品信息对象 goodsMapper.insert(goodsEntity.getGoods()); //2.获取商品信息对象主键ID ,向商品扩展信息对象中设置主键 goodsEntity.getGoodsDesc().setGoodsId(goodsEntity.getGoods().getId()); //3.保存商品扩展信息 goodsDescMapper.insert(goodsEntity.getGoodsDesc()); //4.保存SKU 商品详情信息 if ("1".equals(goodsEntity.getGoods().getIsEnableSpec())) { //5.保存SKU 商品详情信息 if (!CollectionUtils.isEmpty(goodsEntity.getItemList())) { for (Item item : goodsEntity.getItemList()) { String title = goodsEntity.getGoods().getGoodsName(); //设置SKU的名称 商品名+规格选项 Map<String, String> specMap = JSON.parseObject(item.getSpec(), Map.class); //取得SKU的规格选项,并做JSON类型转换 for (String key : specMap.keySet()) { title += specMap.get(key) + " "; } item.setTitle(title); //SKU名称 this.setItemValue(goodsEntity,item); //保存SKU信息 itemMapper.insert(item); } } } else { //不启用规格 SKU信息为默认值 Item item = new Item(); item.setTitle(goodsEntity.getGoods().getGoodsName()); //商品名称 item.setPrice(goodsEntity.getGoods().getPrice()); //默认使用SPU的价格 item.setNum(9999); item.setStatus("1"); //是否启用 item.setIsDefault("1"); //是否默认 item.setSpec("{}"); //没有选择规格,则放置空JSON结构 this.setItemValue(goodsEntity, item); itemMapper.insert(item); } } private void setItemValue(GoodsEntity goodsEntity, Item item) { item.setCategoryId(goodsEntity.getGoods().getCategory3Id()); //商品分类 三级 item.setCreateTime(new Date()); //创建时间 item.setUpdateTime(new Date()); //更新时间 item.setGoodsId(goodsEntity.getGoods().getId()); //SPU ID item.setSellerId(goodsEntity.getGoods().getSellerId()); //商家ID //查询分类对象 ItemCat itemCat = itemCatMapper.selectById(goodsEntity.getGoods().getCategory3Id()); item.setCategory(itemCat.getName()); //分类名称 //查询品牌对象 Brand tbBrand = brandMapper.selectById(goodsEntity.getGoods().getBrandId()); item.setBrand(tbBrand.getName()); //品牌名称 List<Map> imageList = JSON.parseArray(goodsEntity.getGoodsDesc().getItemImages(), Map.class); if (imageList.size() > 0) { item.setImage((String) imageList.get(0).get("url")); //商品图片 } }controller层代码实现:
/*** * 新增Goods数据 * @param goodsEntity * @return */ @ApiOperation(value = "Goods添加",notes = "添加Goods方法详情",tags = {"GoodsController"}) @PostMapping public Result add(@RequestBody @ApiParam(name = "Goods复合实体对象",value = "传入JSON数据",required = true) GoodsEntity goodsEntity){ //调用GoodsService实现添加Goods复合实体对象 goodsService.add(goodsEntity); return new Result(true,StatusCode.OK,"添加成功"); }保存修改:
接口层【GoodsService】:
/*** * 修改Goods数据 * @param goodsEntity */void update(GoodsEntity goodsEntity);实现类【GoodsServiceImpl】
提取公共代码://保存SKU信息 private void saveItemList(GoodsEntity goodsEntity){ if ("1".equals(goodsEntity.getGoods().getIsEnableSpec())) { //5.保存SKU 商品详情信息 if (!CollectionUtils.isEmpty(goodsEntity.getItemList())) { for (Item item : goodsEntity.getItemList()) { String title = goodsEntity.getGoods().getGoodsName(); //设置SKU的名称 商品名+规格选项 Map<String, String> specMap = JSON.parseObject(item.getSpec(), Map.class); //取得SKU的规格选项,并做JSON类型转换 for (String key : specMap.keySet()) { title += specMap.get(key) + " "; } item.setTitle(title); //SKU名称 this.setItemValue(goodsEntity,item); //保存SKU信息 itemMapper.insert(item); } } } else { //不启用规格 SKU信息为默认值 Item item = new Item(); item.setTitle(goodsEntity.getGoods().getGoodsName()); //商品名称 item.setPrice(goodsEntity.getGoods().getPrice()); //默认使用SPU的价格 item.setNum(9999); item.setStatus("1"); //是否启用 item.setIsDefault("1"); //是否默认 item.setSpec("{}"); //没有选择规格,则放置空JSON结构 this.setItemValue(goodsEntity, item); itemMapper.insert(item); } }在add方法中调用
/** * 增加Goods * @param goodsEntity */ @Override public void add(GoodsEntity goodsEntity){ goodsEntity.getGoods().setAuditStatus("0"); //审核状态 未审核 //1.保存SPU 商品信息对象 goodsMapper.insert(goodsEntity.getGoods()); //2.获取商品信息对象主键ID ,向商品扩展信息对象中设置主键 goodsEntity.getGoodsDesc().setGoodsId(goodsEntity.getGoods().getId()); //3.保存商品扩展信息 goodsDescMapper.insert(goodsEntity.getGoodsDesc()); //4.保存SKU 商品详情信息 saveItemList(goodsEntity); }
10.如何进行商品审核?
修改dongyimai-sellergoods-service`工程的com.offcn.sellergoods.service.GoodsService接口,添加审核方法
/*** * 商品审核 * @param goodsId */void audit(Long goodsId);增加实现类方法,修改com.offcn.sellergoods.service.impl.GoodsServiceImpl类,添加audit方法
/*** * 商品审核 * @param goodsId */@Overridepublic void audit(Long goodsId) { //查询商品 Goods goods = goodsMapper.selectById(goodsId); //判断商品是否已经删除 "1" 已经逻辑删除 Null 未被删除 if("1".equals(goods.getIsDelete())){ throw new RuntimeException("该商品已经删除!"); } //实现上架和审核 goods.setAuditStatus("1"); //审核通过 goods.setIsMarketable("1"); //上架 goodsMapper.updateById(goods);}修改控制层方法,修改com.offcn.sellergoods.controller.GoodsController,新增audit方法
/** * 审核 * @param id * @return */@PutMapping("/audit/{id}")public Result audit(@PathVariable Long id){ goodsService.audit(id); return new Result(true,StatusCode.OK,"审核成功");}修改方法:
/** * 修改Goods * @param goodsEntity */@Overridepublic void update(GoodsEntity goodsEntity){ //将审核状态重新设置为 未审核 goodsEntity.getGoods().setAuditStatus("0"); //1.修改SPU的信息 goodsMapper.updateById(goodsEntity.getGoods()); //2.修改商品扩展信息 goodsDescMapper.updateById(goodsEntity.getGoodsDesc()); //3.先根据商品ID删除SKU信息 QueryWrapper<Item> queryWrapper = new QueryWrapper(); queryWrapper.eq("goods_id", goodsEntity.getGoods().getId()); itemMapper.delete(queryWrapper); //4.重新添加SKU信息 this.saveItemList(goodsEntity);}修改控制层的修改方法【GoodsController】:
/*** * 修改Goods数据 * @param goodsEntity * @param id * @return */ @ApiOperation(value = "Goods根据ID修改",notes = "根据ID修改Goods方法详情",tags = {"GoodsController"}) @ApiImplicitParam(paramType = "path", name = "id", value = "主键ID", required = true, dataType = "Long") @PutMapping(value="/{id}") public Result update(@RequestBody @ApiParam(name = "Goods复合实体对象",value = "传入JSON数据",required = false) GoodsEntity goodsEntity, @PathVariable Long id){ //设置goods主键值 goodsEntity.getGoods().setId(id); //设置goodsDesc主键值 goodsEntity.getGoodsDesc().setGoodsId(id); //调用GoodsService实现修改Goods goodsService.update(goodsEntity); return new Result(true,StatusCode.OK,"修改成功"); }
11.什么是事务?事务由哪些特点?事务的隔离级别有哪些?如何进行注解式事务管理?
事务的概念:是数据库操作的最小工作单元,是作为单个逻辑工作单元执行的一系列操作;这些操作作为一个整体一起向系统提交,要么都执行、要么都不执行;事务是一组不可再分割的操作集合(工作逻辑单元)
事务的四个属性:
原子性(atomicity):事务是一个完整的操作。事务的各步操作是不可分的;要么都执行、要么都不执行。一致性(consistency):当事务完成时,数据必须处于一致状态。隔离性(isolation):对数据进行修改的所有并发事务是彼此隔离的,这表明事务必须是独立的,它不应以任何方式依赖于或影响其他事务持久性(durability):事务完成后,它对数据库的修改被永久保存,事务日志能保持事务的永久性。隔离级别:
ReadUncommitted(读未提交)Read Committed(读提交)Repeatable Read(可以重复读)Serializable(序列化)开启注解事务管理的步骤:
- 在项目pom.xml文件中引入
spring-boot-starter-jdbc的依赖,此依赖提供事务的支持。- 在类上或者方法上加上
@Transactional
- 如果标注在类上,这个类的所有公共方法,都支持事务;
- 如果类和方法上都有,则方法上的注解相关配置,覆盖类上的注解
Day12
1.网站首页广告设计到几张表?如何实现广告展示过程中高并发的架构设计?
tb_content_category广告分类表tb_content广告表
- 首先访问
nginx,我们可以采用缓存的方式,先从nginx本地缓存中获取,获取到直接响应- 如果没有获取到,再次访问
redis,我们可以从redis中获取数据,如果有 则返回,并缓存到nginx中- 如果没有获取到,再次访问
mysql,我们从mysql中获取数据,再将数据存储到redis中,返回。
2.什么是Lua? lua有哪些特征? 在哪些场景下可以使用Lua?
Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放,其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。
特性:
- 支持面向过程编程和函数式编程;
- 自动内存管理;只提供了一种通用类型的表(table),用它可以实现数组,哈希表,集合,对象;
- 语言内置模式匹配;闭包(closure);函数也可以看做一个值;提供多线程(协同进程,并非操作系统所支持的线程)支持;
- 通过闭包和table可以很方便地支持面向对象编程所需要的一些关键机制,比如数据抽象,虚函数,继承和重载等。
3.如何安装配置lua?lua的编程方式有几种?介绍下lua的基本语法?
安装步骤,在linux系统中执行下面的命令
curl -R -O http://www.lua/ftp/lua-5.3.5.tar.gz
tar zxvf lua-5.3.5.tar.gz -C /usr/local
mv lua-5.3.5 lua
cd lua
make linux test
此时需要安装lua相关依赖库的支持,执行如下命令即可:
yum install gcc libtermcap-devel ncurses-devel libevent-devel readline-devel -y
重新编译lua
make linux test
安装lua
make install
删除老版本lua,关联新版本lua
cd /usr/bin
rm -rf lua luac
ln -s /usr/local/bin/lua /usr/bin/lua
ln -s /usr/local/bin/luac /usr/bin/luac
此时再执行lua测试看lua是否安装成功
lua有交互式编程和脚本式编程。
交互式编程就是直接输入语法,就能执行。
脚本式编程需要编写脚本,然后再执行命令 执行脚本才可以
4.什么是OpenRestry?如何安装openResty?操作nginx的常见命令有哪些?
OpenResty 简单理解, 就是相当于封装了nginx,并且集成了LUA脚本,开发人员只需要简单的为其提供模块就可以实现相关的逻辑,而不再像之前,还需要在nginx中自己编写lua的脚本,再进行调用了。
启动openresty
cd /usr/local/openresty/nginx/sbin
./nginx停止openresty
./nginx -s stop重启openresty
./nginx -s reload
5.如何实现广告的缓存的载入与读取?
实现思路——查询数据放入redis中
实现思路:
定义请求:用于查询数据库中的数据更新到redis中。
连接mysql ,按照广告分类ID读取广告列表,转换为json字符串。
连接redis,将广告列表json字符串存入redis
实现思路-从redis中获取数据
定义请求,用户根据广告分类的ID 获取广告的列表。通过lua脚本直接从redis中获取数据即可。
加入openresty本地缓存
先查询openresty本地缓存
如果没有
再查询redis中的数据,
如果没有
再查询mysql中的数据,但凡有数据 则返回即可。
6.如何实现网站首页前台工程广告轮播图展示?
创建静态web工程
拷贝网站首页静态资源
把用到的vue\axios类库拷贝过来
网站首页开发
编辑index.html
- 引入vue、axios类库到当前页面
- 在页面添加vue识别div
- 编写vue代码,当页面加载的时候,从服务器端获取广告轮播图数据
- 修改index.html循环显示轮播图
部署页面,上传首页相关文件到nginx目录:/usr/local/openresty/nginx/html
7.canal是什么?canal的工作原理有哪些?
canal可以用来监控数据库数据的变化,从而获得新增数据,或者修改的数据,是一个用来同步增量数据的工具。
原理相对比较简单:
- canal把自己伪装成MySQL slave,模拟MySQL slave的交互协议向MySQL Mater发送 dump协议
- MySQL mater收到canal发送过来的dump请求,开始推送binary log给canal(也就是canal)
- canal解析binary log,再发送到存储目的地,比如MySQL,Kafka,Elastic Search等等
8.如何实现Canal监听mysql数据库?请简述下实现过程?
先使用docker 创建mysql容器
- 连接到mysql容器中,并修改/etc/mysql/mysql.conf.d/mysqldf 需要开启主 从模式,开启binlog模式。
执行如下命令,编辑mysql配置文件
docker exec -it mysql /bin/bash
cd /etc/mysql/mysql.conf.d
vi mysqldf
修改mysqldf配置文件,添加如下配置:
log-bin=/var/lib/mysql/mysql-bin
server-id=12345- 创建账号 用于测试使用
使用root账号创建用户并授予权限
create user canal@’%’ IDENTIFIED by ‘canal’;
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT,SUPER ON . TO ‘canal’@’%’;
FLUSH PRIVILEGES;- 重启mysql容器
docker restart mysql
9.canal微服务如何搭建?
- 安装辅助jar包
- canal微服务工程搭建
在dongyimai-service下创建dongyimai-canal-service工程,并引入相关配置。
进行application.yml配置
防火墙对11111端口号释放,还有就是 host port 的值是否正确- 监听创建
创建一个CanalDataEventListener类,实现对表增删改操作的监听- 启动类创建
- 测试
启动canal微服务,然后修改任意数据库的表数据
10.什么是swagger?
Swagger 是一个用于生成、描述和调用 RESTful 接口的 Web 服务。通俗的来讲,Swagger 就是将项目中所有(想要暴露的)接口展现在页面上,并且可以进行接口调用和测试的服务。
11.如何实现广告同步?
每次执行广告操作的时候,会记录操作日志到,然后将操作日志发送给canal,canal将操作记录发送给canal微服务,canal微服务根据修改的分类ID调用content微服务查询分类对应的所有广告,canal微服务再将所有广告存入到Redis缓存。
构建工程
在dongyimai-service中搭建dongyimai-content-service微服务,对应的dao、service、controller、pojo、feign由代码生成器生成。生成代码、拷贝到工程
dongyimai-content-service-api模块中拷贝生成的pojo实体类、feign调用接口
dongyimai-content-service模块中拷贝生成的dao、service、controller代码创建配置文件
在模块dongyimai-content-service下 src/main/resources目录下创建配置文件:application.yml编写swagger文档配置类、MyBatisPlus分页插件配置类
编写主启动类
启动测试 访问Swagger测试接口
Day13
1.elasticsearch是什么? 为什么要使用elasticsearch?基本概念有哪些都是什么意思?
- elasticsearch简介
- ElasticSearch是一个基于Lucene的搜索服务器
- 是一个分布式、高扩展、高实时的搜索与数据分析引擎
- 基于RESTful web接口
- Elasticsearch是用Java语言开发的,并作为Apache许可条款下的开放源码发布,是一种流行的企业级搜索引擎
- 官网:https://www.elastic.co/
- elasticsearch功能
- 搜索:海量数据的查询
- 日志数据分析
- 实时数据分析
- elasticsearch基本概念:
索引(index)
ElasticSearch存储数据的地方,可以理解成关系型数据库中的数据库概念。
映射(mapping)
mapping定义了每个字段的类型、字段所使用的分词器等。相当于关系型数据库中的表结构。
文档(document)
Elasticsearch中的最小数据单元,常以json格式显示。一个document相当于关系型数据库中的一行数据。
倒排索引
一个倒排索引由文档中所有不重复词的列表构成,对于其中每个词,对应一个包含它的文档id列表。
类型(type)
一种type就像一类表。如用户表、角色表等。在Elasticsearch7.X默认type为_doc
- ES 5.x中一个index可以有多种type。
- ES 6.x中一个index只能有一种type。
- ES 7.x以后,将逐步移除type这个概念,现在的操作已经不再使用,默认_doc
2.使用elasticsearch要注意和springBoot版本配对吗? 如何配对?
需要进行版本配对
配对步骤:
- 查看项目中自己的springboot版本
- 查看Spring date Elasticsearch的版本
- 根据上面的两个参数对应版本兼容表格,进行Elasticsearch服务端进行适配。
- 如果以上感觉到麻烦可以,直接查看引入的版本,这样更精确快捷。
- 下载Elasticsearch镜像
3.如何安装elasticsearch并配置?包括如何配置中文分词器?常见分词有哪几种?
一. 安装elasticsearch并配置
1.1 docker镜像下载
docker pull elasticsearch:7.6.2
1.2如果虚拟机中没有可以上传 资料/elasticsearch762.tar到虚拟机中,使用命令
docker load -i /root/elasticsearch762.tar
2.1 将镜像文件还原为镜像
docker run -di --name=dym_elasticsearch -p 9200:9200 -p 9300:9300 -e ES_JAVA_POTS="-Xms256m -Xmx256m" -e “discovery.type=single-node” elasticsearch:7.6.2
9200端口(Web管理平台端口) 9300(服务默认端口)3.1 检查es是否安装完成
curl http://localhost:9200
3.2 或者在浏览器输入地址访问:
http://192.168.188.128:9200/二.配置中文分词器
安装ik分词器
IK分词器下载地址https://github/medcl/elasticsearch-analysis-ik/releases
将资源/elasticsearch-analysis-ik-7.6.2.zip 上传到
将ik分词器上传到服务器上,然后解压,并改名字为ik, (观察解析是外面是否有包,如无包,解压是指定一个报名如ik)
此时需要安装一个 zip压缩文件的解压工具unzip
yum install unzip -y
unzip elasticsearch-analysis-ik-7.6.2.zip -d /usr/local/ik
cd /usr/local
mv elasticsearch ik
将ik目录拷贝到docker容器的plugins目录下
docker cp ./ik dym_elasticsearch:/usr/share/elasticsearch/plugins
重启:
docker restart dym_elasticsearch
三.IK分词器有两种分词模式:ik_max_word和ik_smart模式。
4.什么是Kibana? 如何安装配置Kibana?如何使用kibana?
一. kibana简介
Kibana是一个开源的分析和可视化平台,设计用于和Elasticsearch一起工作。
你用Kibana来搜索,查看,并和存储在Elasticsearch索引中的数据进行交互。
可以轻松地执行高级数据分析,并且以各种图标、表格和地图的形式可视化数据。
Kibana使得理解大量数据变得很容易。它简单的、基于浏览器的界面使你能够快速创建和共享动态仪表板,实时显示Elasticsearch查询的变化。二.安装配置Kibana
(1)镜像下载
docker pull kibana:7.6.2可以将资源镜像文件上传到虚拟机中.
docker load -i /root/kibana762.tar(2)安装kibana容器
执行如下命令,开始安装kibana容器
docker run -it -d --link dym_elasticsearch:elasticsearch --name kibana --restart=always -p 5601:5601 kibana:7.6.2restart=always:每次服务都会重启,也就是开启启动
5601:5601:端口号(3)访问测试
访问http://192.168.188.128:5601三.使用kibana
要使用Kibana,您必须至少配置一个索引(但是要求Elasticsearch中要有数据)。索引用于标识Elasticsearch索引以运行搜索和分析。它们还用于配置字段等。
- 指定一个索引模式来匹配一个或多个你的Elasticsearch索引。当你指定了你的索引模式以后,任何匹配到的索引都将被展示出来。
- 点击“Next Step”以选择你想要用来执行基于时间比较的包含timestamp字段的索引。如果你的索引没有基于时间的数据,那么选择“I don’t want to use the Time Filter”选项。
- 点击“Create index pattern”按钮来添加索引模式。第一个索引模式自动配置为默认的索引默认,以后当你有多个索引模式的时候,你就可以选择将哪一个设为默认。
5. DSL常见查询语句有哪些?
(1)查询所有数据
GET /user/_search(2)根据ID查询
GET /user/_doc/2(3)Sort排序
GET /user/_search
{
“query”:{
“match_all”: {}
},
“sort”:{
“age”:{
“order”:“desc”
}
}
}(4)分页
GET /user/_search
{
“query”:{
“match_all”: {}
},
“sort”:{
“age”:{
“order”:“desc”
}
},
“from”: 0,
“size”: 2
}
6.如何使用商品数据导入到elasticsearch索引库?
我们可以使用Spring Data ElasticSearch对elasticSearch进行操作,将elasticSearch的客户端API进行封装 。
官方网站:http://projects.spring.io/spring-data-elasticsearch/
(1)API工程搭建,在pom文件中导入坐标<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId></dependency>(2)编写application.yml配置
server:port: 9005spring:application:name: searchelasticsearch:rest:uris: 192.168.188.128:9200 #此处配置elasticsearch的访问地址eureka:client:service-url:defaultZone: http://127.0.0.1:8761/eurekainstance:prefer-ip-address: truefeign:hystrix:enabled: true#超时配置ribbon:ReadTimeout: 300000hystrix:command:default:execution: isolation: thread: timeoutInMilliseconds: 10000(3)编写启动类,并创建对应的包,dao、service、controller
@SpringBootApplication(exclude={DataSourceAutoConfiguration.class})@EnableEurekaClientpublic class SearchApplication {public static void main(String[] args) { /** * Springboot整合Elasticsearch 在项目启动前设置一下的属性,防止报错 * 解决netty冲突后初始化client时还会抛出异常 * availableProcessors is already set to [12], rejecting [12] ***/ System.setProperty("es.setty.runtime.available.processors", "false"); SpringApplication.run(SearchApplication.class,args); }}数据导入流程如下:
- 请求search服务,调用数据导入地址
- 根据注册中心中的注册的goods服务的地址,使用Feign方式查询所有已经审核的Sku
- 使用SpringData Es将查询到的Sku集合导入到ES中
7.如何进行关键字查询?
编写service接口
Map search(Map<String, String> searchMap);编写接口实现类
@Autowiredprivate ElasticsearchRestTemplate esRestTemplateRest;public Map search(Map<String, String> searchMap) {//1.获取关键字的值String keywords = searchMap.get("keywords");if (StringUtils.isEmpty(keywords)) { keywords = "华为";//赋值给keywords一个默认的值}//2.创建查询对象 的构建对象NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();//3.设置查询的条件//使用:QueryBuilders.matchQuery("title", keywords) ,搜索华为 ---> 华 为 二字可以拆分查询,//使用:QueryBuilders.matchPhraseQuery("title", keywords) 华为二字不拆分查询nativeSearchQueryBuilder.withQuery(QueryBuilders.matchQuery("title", keywords));//4.构建查询对象NativeSearchQuery query = nativeSearchQueryBuilder.build();//5. 执行搜索,获取封装响应数据结果的SearchHits集合SearchHits<SkuInfo> searchHits = esRestTemplateRest.search(query, SkuInfo.class);//对搜索searchHits集合进行分页封装SearchPage<SkuInfo> skuPage = SearchHitSupport.searchPageFor(searchHits, query.getPageable());//遍历取出查询的商品信息 List<SkuInfo> skuList=new ArrayList<>();for (SearchHit<SkuInfo> searchHit :skuPage.getContent()) { // 获取搜索到的数据 SkuInfo content = (SkuInfo) searchHit.getContent(); SkuInfo skuInfo = new SkuInfo(); BeanUtils.copyProperties(content, skuInfo); skuList.add(skuInfo);}//6.返回结果Map resultMap = new HashMap<>();resultMap.put("rows", skuPage.getContent());//获取所需SkuInfo集合数据内容resultMap.put("total", skuInfo);//总记录数resultMap.put("totalPages", skuPage.getTotalPages());//总页数return resultMap;}控制层调用service层方法
/**搜索@param searchMap@return*/@PostMappingpublic Map search(@RequestBody(required = false) Map searchMap){return skuService.search(searchMap);}
8.如何在后台查询商品所在分类数据?
(1)修改search微服务的com.offcn.search.service.impl.SkuServiceImpl类
public Map search(Map<String, String> searchMap) { //1.获取关键字的值 String keywords = searchMap.get("keywords"); if (StringUtils.isEmpty(keywords)) { keywords = "华为";//赋值给keywords一个默认的值 } //2.创建查询对象 的构建对象 NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder(); //3.设置查询的条件 //设置分组条件 按照商品分类进行分组 nativeSearchQueryBuilder.addAggregation( // AggregationBuilders聚合条件构造器 // terms("skuCategorygroup"):给列取别名 // field("category"):字段名称 // size 指定查询结果的数量 默认是10个 AggregationBuilders.terms("skuCategorygroup") .field("category") .size(50)); //使用:QueryBuilders.matchQuery("title", keywords) ,搜索华为 ---> 华 为 二字可以拆分查询, //使用:QueryBuilders.matchPhraseQuery("title", keywords) 华为二字不拆分查询 nativeSearchQueryBuilder.withQuery(QueryBuilders.matchQuery("title", keywords)); //4.构建查询对象 NativeSearchQuery query = nativeSearchQueryBuilder.build(); //5. 执行搜索,获取封装响应数据结果的SearchHits集合 SearchHits<SkuInfo> searchHits = esRestTemplateRest.search(query, SkuInfo.class); //获取分组结果 Terms terms = searchHits.getAggregations().get("skuCategorygroup"); // 获取分类名称集合 List<String> categoryList = new ArrayList<>(); if (terms != null) { for (Terms.Bucket bucket : terms.getBuckets()) { String keyAsString = bucket.getKeyAsString();// 分组的值(分类名称) categoryList.add(keyAsString); } } //对搜索searchHits集合进行分页封装 SearchPage<SkuInfo> skuPage = SearchHitSupport.searchPageFor(searchHits, query.getPageable()); //遍历取出查询的商品信息 List<SkuInfo> skuList=new ArrayList<>(); for (SearchHit<SkuInfo> searchHit :skuPage.getContent()) { // 获取搜索到的数据 SkuInfo content = (SkuInfo) searchHit.getContent(); SkuInfo skuInfo = new SkuInfo(); BeanUtils.copyProperties(content, skuInfo); skuList.add(skuInfo); } //6.返回结果 Map resultMap = new HashMap<>(); resultMap.put("categoryList", categoryList); resultMap.put("rows", skuList);//获取所需SkuInfo集合数据内容 resultMap.put("total", skuPage.getTotalElements());//总记录数 resultMap.put("totalPages", skuPage.getTotalPages());//总页数 return resultMap;}
Day14
1.如何实现品牌数据查询?
思路:品牌数据的查询可以使用分类统计的方式进行分组实现,在执行搜索后根据品牌名字分组查看品牌数据
实现:
修改search微服务的SkuServiceImpl实现类,添加品牌分组搜索代码如下:1)设置品牌的分组条件nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("skuBrandgroup").field("brand").size(50));2)获取品牌的分组结果和名称集合nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("skuBrandgroup").field("brand").size(50));3)返回结果resultMap.put("brandList", brandList);4)新增获取品牌列表数据的方法private List<String> getStringsBrandList(Terms termsBrand) { List<String> brandList = new ArrayList<>(); if (termsBrand != null) { for (Terms.Bucket bucket : termsBrand.getBuckets()) { String keyAsString = bucket.getKeyAsString(); brandList.add(keyAsString); } } return brandList; }
2.如何实现规格数据查询? 为什么要添加.keyword?
- 思路:
1)与品牌数据的查询略有不同,规格选项存在多个,所以需要先搜索规格名称,再按照名称分组搜索规格选项。
2)获取到的规格数据存在重复,可将所有规格数据转换成Map集合去重
- 实现:
1)设置规格的分组条件
nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms(“skuSpecgroup”).field(“spec.keyword”).size(100));
其中.keyword便于聚合搜索:term 查询的时候,由于是精准匹配,所以查询的关键字在es上的类型,必须是keyword而不能是text。如果不加.keyword可能会出错:Elasticsearch报错:exception [type=search_phase_execution_exception, reason=all shards failed]
2)获取规格分组结果
Terms termsBrand = searchHits.getAggregations().get(“skuBrandgroup”);
Map<String, Set> specMap=getStringSetMap(termsSpec);
resultMap.put(“specMap”, specMap);
3)新增获取规格列表数据方法private Map<String, Set<String>> getStringSetMap(Terms termsSpec) { Map<String, Set<String>> specMap = new HashMap<String, Set<String>>(); Set<String> specList = new HashSet<>(); if (termsSpec != null) { for (Terms.Bucket bucket : termsSpec.getBuckets()) { specList.add(bucket.getKeyAsString()); } } for (String specjson : specList) { Map<String, String> map = JSON.parseObject(specjson, Map.class); for (Map.Entry<String, String> entry : map.entrySet()) {// String key = entry.getKey(); //规格名字 String value = entry.getValue(); //规格选项值 //获取当前规格名字对应的规格数据 Set<String> specValues = specMap.get(key); if (specValues == null) { specValues = new HashSet<String>(); } //将当前规格加入到集合中 specValues.add(value); //将数据存入到specMap中 specMap.put(key, specValues); } } return specMap; }
3.如何查询条件的刷选如 分类、品牌 、规格、价格区间?
分类和品牌筛选:
思路:两者是明确的,可在search方法里创建多条件组合查询对象,设置品牌和分类的查询条件。
实现代码如下://创建多条件组合查询对象 BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); //设置品牌查询条件 if (!StringUtils.isEmpty(searchMap.get("brand"))) { boolQueryBuilder.filter(QueryBuilders.termQuery("brand", searchMap.get("brand"))); } //设置分类查询条件 if (!StringUtils.isEmpty(searchMap.get("category"))) { boolQueryBuilder.filter(QueryBuilders.termQuery("category", searchMap.get("category"))); } //关联过滤查询对象到查询器 nativeSearchQueryBuilder.withFilter(boolQueryBuilder);规格筛选
思路:规格搜索需要向后台发送规格名称和规格值,根据这一要求封装数据,依靠前缀spec区分规格数据,故找数据也按照如下格式来找:spechMap.规格名字.keyword
实现:在SkuServiceImpl的search方法增加规格查询操作,代码如下:if (searchMap != null) { for (String key : searchMap.keySet()) {//{ brand:"",category:"",spec_网络:"电信4G"} if (key.startsWith("spec_")) { //截取规格的名称 boolQueryBuilder.filter(QueryBuilders.termQuery("specMap." + key.substring(5) + ".keyword", searchMap.get(key))); } }}价格区间筛选
在SkuServiceImpl的search方法增加价格区间查询操作,代码如下:String price = searchMap.get("price");if (!StringUtils.isEmpty(price)) { String[] split = price.split("-"); if (!split[1].equalsIgnoreCase("*")) { boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").from(split[0], true).to(split[1], true)); } else { boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(split[0])); }}
4.如何实现搜索分页
思路:分页使用PageRequest.of( pageNo- 1, pageSize);实现,第1个参数表示第N页,从0开始,第2个参数表示每页显示多少条
代码实现:
//1.构建过滤查询 nativeSearchQueryBuilder.withFilter(boolQueryBuilder); //2.构建分页查询 Integer pageNum = 1; if (!StringUtils.isEmpty(searchMap.get("pageNum"))) { try { pageNum = Integer.valueOf(searchMap.get("pageNum")); } catch (NumberFormatException e) { e.printStackTrace(); pageNum=1; } } Integer pageSize = 3; nativeSearchQueryBuilder.withPageable(PageRequest.of(pageNum - 1, pageSize)); //3.构建查询对象 NativeSearchQuery query = nativeSearchQueryBuilder.build();
5.如何实现搜索排序
思路:排序总共有价格、评价、新品、销量四个排序,但后台只需要知晓排序域名称和排序方式即可实现。
实现:前端页面传递要排序的字段(field)和要排序的类型(ASC,DESC),后台接收
String sortRule = searchMap.get("sortRule");String sortField = searchMap.get("sortField");if (!StringUtils.isEmpty(sortRule) && !StringUtils.isEmpty(sortField)) { nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort(sortField).order(sortRule.equals("DESC") ? SortOrder.DESC : SortOrder.ASC));}
6.如何实现高亮显示
含义:指根据商品关键字搜索商品时,显示的页面对关键字给定了特殊样式
思路:
1)指定高亮域,也就是设置哪个域需要高亮显示。设置高亮域的时候,需要指定前缀和后缀,也就是关键词用什么html标签包裹,再给该标签样式
2)高亮搜索实现
3)将非高亮数据替换成高亮数据
- 实现
1)添加光亮显示的域nativeSearchQueryBuilder.withHighlightFields(new HighlightBuilder.Field("title"));nativeSearchQueryBuilder.withHighlightBuilder(newHighlightBuilder().preTags("<em style=\"color:red\">").postTags("</em>"));nativeSearchQueryBuilder.withQuery(QueryBuilders.multiMatchQuery(keywords,"title","brand","category"));2)在search方法遍历查询商品信息的for循环中,添加处理高亮数据的内容
for (SearchHit<SkuInfo> searchHit :skuPage.getContent()) { SkuInfo content = (SkuInfo) searchHit.getContent(); SkuInfo skuInfo = new SkuInfo(); BeanUtils.copyProperties(content, skuInfo); // 处理高亮 Map<String, List<String>> highlightFields = searchHit.getHighlightFields(); for (Map.Entry<String, List<String>> stringHighlightFieldEntry : highlightFields.entrySet()) { String key = stringHighlightFieldEntry.getKey(); if (StringUtils.equals(key, "title")) { List<String> fragments = stringHighlightFieldEntry.getValue(); StringBuilder sb = new StringBuilder(); for (String fragment : fragments) { sb.append(fragment.toString()); } skuInfo.setTitle(sb.toString()); } } skuList.add(skuInfo); }
Day15
1.简述Thymeleaf是什么? 有哪些功能?
- thymeleaf是一个XML/XHTML/HTML5模板引擎,可用于Web与非Web环境中的应用开发
- 提供可被浏览器正确显示的、格式良好的模板,可以处理一下六种模板:XML,有效的XML,XHTML,有效的XHTML,HTML 5,旧版HTML5静态建模
2.Springboot如何整合thymeleaf? 简述实现步骤,说说Thymeleaf基本语法?
创建一个sprinboot项目
添加spring web的起步依赖,添加thymeleaf的起步依赖
<dependencies> <!--web起步依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--thymeleaf配置--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> </dependencies>编写html 使用thymleaf的语法获取变量对应后台传递的值4. 编写controller 设置变量的值到model中th:action:定义后台控制器路径,类似
<form>标签的action属性th:each:对象遍历,功能类似jstl中的<c:forEach>标签Map输出:后台向Map集合添加键值,页面从Map集合中取出数组输出:后台向数组添加值,页面从数组中取出Date输出:后台向Date对象添加值,页面从Date对象取出th:if条件:条件判断,满足相应条件才可显示Javascript语法||:字符拼接
3.搜索页面如何渲染?搜索关键数据如何回显, 商品分类 商品品牌 商品规格,商品价格如何显示?
- 用户搜索——>搜索业务工程——>搜索微服务——>搜索业务工程——>搜索页面渲染
- 回显:在前端传值到后台时,后端通过Map集合接收,然后将该Map集合封装到Model对象中,前端直接直接从Map中将数据取出,然后在页面输出即可
4.如何收集页面的查询条件? 如关键字 分类 品牌 规格 价格 排序? 如何实现移除搜索条件?
在标签内通过thymeleaf语法"${#maps.containsKey(searchMap,‘要传入值的名称’)"将查询条件封装到Map集合中
关键字:
<a th:text="${key}"></a>分类:
<div class="type-wrap" th:if="${#maps.containsKey(result,'categoryList') }"></div>品牌:
<div class="type-wrap logo" th:if="${#maps.containsKey(result,'brandList') }"></div>规格:
`<liclass=“tag"th:each=“spec,specStat: s e a r c h M a p " t h : i f = " {searchMap}"th:if=" searchMap"th:if="{#strings.startsWith(spec.key,‘spec_’)}”>
手机屏幕尺寸
:
5.5寸<aclass=“sui-iconicon-tb-close”
th:href=”@{${#strings.replace(url,spec.key+’=’+spec.value,’’)}}">- `
价格:
<div class="type-wrap" th:unless="${#maps.containsKey(searchMap,'price')}"></div>排序:
`
ath:href="@{${url}(sortField=price,sortRule=ASC)}"价格↑
5.后台如何针对 页面收集过来的数据进行搜索查询?
@Override publicMapsearch(Map<String,String>searchMap){ //获取关键字的值 Stringkeywords=searchMap.get("keywords"); if(StringUtils.isEmpty(keywords)){ keywords="华为";//赋值给keywords一个默认的值 } //创建查询对象的构建对象 NativeSearchQueryBuildernativeSearchQueryBuilder=newNativeSearchQueryBuilder(); //设置查询的条件 nativeSearchQueryBuilder.withQuery(QueryBuilders.matchQuery("title",keywords)); //设置高亮条件 nativeSearchQueryBuilder.withHighlightFields(newHighlightBuilder.Field("title")); //添加高亮样式的前缀和后缀 nativeSearchQueryBuilder.withHighlightBuilder(newHighlightBuilder().preTags("<emstyle='color:red'>").postTags("</em>")); //设置分组条件按照商品分类进行分组 nativeSearchQueryBuilder.addAggregation( //AggregationBuilder聚合条件构造器 //terms("skuCategorygroup"):给列取别名 //field("category"):字段名称 //size指定查询结果的数量默认是10个 AggregationBuilders.terms("skuCategorygroup").field("category").size(50) ); nativeSearchQueryBuilder.addAggregation( AggregationBuilders.terms("skuBrandgroup").field("brand").size(50) ); nativeSearchQueryBuilder.addAggregation( AggregationBuilders.terms("skuSpecgroup").field("spec.keyword").size(100) ); //设置主关键字查询,修改为多字段的搜索条件 //nativeSearchQueryBuilder.withQuery(QueryBuilders.matchQuery("title",keywords)); nativeSearchQueryBuilder.withQuery(QueryBuilders.multiMatchQuery(keywords,"title","brand","category")); //=========开始过滤查询 //创建多条件组合查询对象 BoolQueryBuilderboolQueryBuilder=QueryBuilders.boolQuery(); //设置品牌查询条件 if(!StringUtils.isEmpty(searchMap.get("brand"))){ boolQueryBuilder.filter(QueryBuilders.termQuery("brand",searchMap.get("brand"))); } //设置分类查询条件 if(!StringUtils.isEmpty(searchMap.get("category"))){ boolQueryBuilder.filter(QueryBuilders.termQuery("category",searchMap.get("category"))); } //设置规格过滤查询条件 if(searchMap!=null){ for(Stringkey:searchMap.keySet()){ if(key.startsWith("spec_")){ //截取规格的名称 boolQueryBuilder.filter(QueryBuilders.termQuery( "specMap."+key.substring(5)+".keyword",searchMap.get(key))); } } } //价格过滤查询 Stringprice=searchMap.get("price"); if(!StringUtils.isEmpty(price)){ String[]split=price.split("-"); if(!split[1].equalsIgnoreCase("*")){ boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").from(split[0],true) .to(split[1],true)); }else{ boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(split[0])); } } //关联过滤查询对象到查询器 nativeSearchQueryBuilder.withFilter(boolQueryBuilder); //构建分页查询 IntegerpageNum=1; if(!StringUtils.isEmpty(searchMap.get("pageNum"))){ try{ pageNum=Integer.valueOf(searchMap.get("pageNum")); }catch(NumberFormatExceptione){ e.printStackTrace(); pageNum=1; } } IntegerpageSize=10; nativeSearchQueryBuilder.withPageable(PageRequest.of(pageNum-1,pageSize)); //构建排序查询 StringsortRule=searchMap.get("sortRule"); StringsortField=searchMap.get("sortField"); if(!StringUtils.isEmpty(sortRule)&&!StringUtils.isEmpty(sortField)){ nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort(sortField).order(sortRule.equals("DESC")?SortOrder.DESC:SortOrder.ASC)); } //=========过滤查询结束 //构建查询对象 NativeSearchQueryquery=nativeSearchQueryBuilder.build(); //执行搜索,获取封装相应数据结果的SearchHits集合 SearchHits<SkuInfo>searchHits=elasticsearchRestTemplate.search(query,SkuInfo.class); //获取分组结果 TermstermsCategory=searchHits.getAggregations().get("skuCategorygroup");//商品分类 TermstermsBrand=searchHits.getAggregations().get("skuBrandgroup");//商品品牌 TermstermsSpec=searchHits.getAggregations().get("skuSpecgroup");//商品规格 List<String>categoryList=getStringList(termsCategory); List<String>brandList=getStringList(termsBrand); Map<String,Set<String>>specMap=getStringSetMap(termsSpec); //对搜索searchHits集合进行分页封装 SearchPage<SkuInfo>skuInfoSearchPage=SearchHitSupport.searchPageFor(searchHits,query.getPageable()); //遍历取出查询的商品信息 ArrayList<SkuInfo>list=newArrayList<>(); for(SearchHit<SkuInfo>searchHit:skuInfoSearchPage.getContent()){ //获取搜索到的数据 SkuInfocontent=searchHit.getContent(); SkuInfoskuInfo=newSkuInfo(); BeanUtils.copyProperties(content,skuInfo); //处理高亮 Map<String,List<String>>highlightFields=searchHit.getHighlightFields(); for(Map.Entry<String,List<String>>stringListEntry:highlightFields.entrySet()){ Stringkey=stringListEntry.getKey(); if(StringUtils.equals(key,"title")){ List<String>fragments=stringListEntry.getValue(); StringBuildersb=newStringBuilder(); for(Stringfragment:fragments){ sb.append(fragment); } skuInfo.setTitle(sb.toString()); } } list.add(skuInfo); } //返回结果 MapresultMap=newHashMap<>(); resultMap.put("rows",list);//获取所需skuinfo集合数据内容 resultMap.put("total",skuInfoSearchPage.getTotalElements());//总记录数 resultMap.put("totalPages",skuInfoSearchPage.getTotalPages());//总页数 resultMap.put("categoryList",categoryList); resultMap.put("brandList",brandList); resultMap.put("specMap",specMap);//获取所需的skuinfo集合数据内容 //分页数据保存 //设置当前分页 resultMap.put("pageNum",pageNum); resultMap.put("pageSize",10); returnresultMap; }
6.什么是网页静态化? 为什么要使用网页静态化技术?
- 网页静态化:就是将大规模且相对变化不太频繁的数据转换成单独的静态页面,用户浏览时,服务器直接将该页面发送给用户,可以通过使用Nginx这样的高性能的web服务器来部署
- 减轻减轻数据库的访问压力,提高并发能力,利于SEO
7.减轻数据库的压力有哪些技术?适用于什么场景?
网页静态化技术和缓存技术
网页静态化比较适合大规模且相对变化不太频繁的数据
缓存比较适合小规模的数据
8.如何实现商品详情页? 搭建项目 需求分析 相关逻辑代码?
(1)在dongyimai-web下创建一个名称为dongyimai-item-web的模块,该微服务只用于生成商品静态页 (2)dongyimai-item-web中添加起步依赖: <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache/POM/4.0.0" xmlns:xsi="http://www.w3/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache/POM/4.0.0 http://maven.apache/xsd/maven-4.0.0.xsd"> <parent> <artifactId>dongyimai-web</artifactId> <groupId>com.offcn</groupId> <version>1.0</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>dongyimai-item-web</artifactId> <dependencies> <dependency> <groupId>com.offcn</groupId> <artifactId>dongyimai-sellergoods-serivce-api</artifactId> <version>1.0</version> <exclusions> <exclusion> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> </dependencies> </project> (3)修改application.yml的配置 server: port: 9102 eureka: client: service-url: defaultZone: http://127.0.0.1:8761/eureka instance: prefer-ip-address: true feign: hystrix: enabled: true spring: thymeleaf: cache: false application: name: item-web main: allow-bean-definition-overriding: true # 生成静态页的位置 pagepath: C:\\items (4)创建系统启动类 @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) @EnableEurekaClient @EnableFeignClients(basePackages = "com.offcn.sellergoods.feign") public class ItemApplication { public static void main(String[] args) { SpringApplication.run(ItemApplication.class,args); } } (5)Feign创建: 一会儿需要查询SPU和SKU以及Category,所以我们需要先创建Feign,修改dongyimai-sellergoods-service-api,添加ItemCatFeign,并在ItemCatFeign中添加根据ID查询分类数据,代码如下: @FeignClient(name="dym-sellergoods") @RequestMapping("/itemCat") public interface ItemCatFeign { /** * 获取分类的对象信息 * @param id * @return */ @GetMapping("/{id}") public Result<ItemCat> findById(@PathVariable(name = "id") Integer id); } (6)在dongyimai-sellergoods-service-api,添加GoodsFeign,并添加根据SpuID查询Spu信息,代码如下: @FeignClient(name="dym-sellergoods") @RequestMapping("/goods") public interface GoodsFeign { /*** * 根据ID查询Spu数据 * @param id * @return */ @GetMapping("/{id}") Result<GoodsEntity> findById(@PathVariable Long id); } (7)创建service 在项目dongyimai-item-web中创建接口 com.offcn.item.service.PageService 接口:PageService public interface PageService { /** * 根据商品的ID 生成静态页 * @param spuId */ public void createPageHtml(Long spuId) ; } 在项目dongyimai-item-web中创建实现类 com.offcn.item.service.impl.PageServiceImpl实现类,代码如下: @Service public class PageServiceImpl implements PageService { @Autowired private GoodsFeign goodsFeign; @Autowired private ItemCatFeign itemCatFeign; @Autowired private TemplateEngine templateEngine; //生成静态文件路径 @Value("${pagepath}") private String pagepath; /** * 构建数据模型 * @param spuId * @return */ private Map<String,Object> buildDataModel(Long spuId){ //构建数据模型 Map<String, Object> dataMap = new HashMap<>(); //获取SPU 和SKU列表 Result<GoodsEntity> result = goodsFeign.findById(spuId); GoodsEntity goodsEntity = result.getData(); //1.加载SPU数据 Goods goods = goodsEntity.getGoods(); //2.加载商品扩展数据 GoodsDesc goodsDesc = goodsEntity.getGoodsDesc(); //3.加载SKU数据 List<Item> itemList = goodsEntity.getItemList(); dataMap.put("goods", goods); dataMap.put("goodsDesc", goodsDesc); dataMap.put("specificationList", JSON.parseArray(goodsDesc.getSpecificationItems(),Map.class)); dataMap.put("imageList",JSON.parseArray(goodsDesc.getItemImages(),Map.class)); dataMap.put("itemList",itemList); //4.加载分类数据 dataMap.put("category1",itemCatFeign.findById(goods.getCategory1Id().intValue()).getData()); dataMap.put("category2",itemCatFeign.findById(goods.getCategory2Id().intValue()).getData()); dataMap.put("category3",itemCatFeign.findById(goods.getCategory3Id().intValue()).getData()); return dataMap; } /*** * 生成静态页 * @param spuId */ @Override public void createPageHtml(Long spuId) { // 1.上下文 Context context = new Context(); Map<String, Object> dataModel = buildDataModel(spuId); context.setVariables(dataModel); // 2.准备文件 File dir = new File(pagepath); if (!dir.exists()) { dir.mkdirs(); } File dest = new File(dir, spuId + ".html"); // 3.生成页面 try (PrintWriter writer = new PrintWriter(dest, "UTF-8")) { templateEngine.process("item", context, writer); } catch (Exception e) { e.printStackTrace(); } } } 创建Controller @RestController @RequestMapping("/page") public class PageController { @Autowired private PageService pageService; /** * 生成静态页面 * @param id * @return */ @RequestMapping("/createHtml/{id}") public Result createHtml(@PathVariable(name="id") Long id){ pageService.createPageHtml(id); return new Result(true, StatusCode.OK,"ok"); } } 拷贝模板文件 资料/网站前台/item.html 填充模板: 首先将模板 将资料/网站前台/item.html 拷贝到 resources/templates/下 添加thymeleaf的头 <html xmlns:th="http://www.thymeleaf"> <!--引入vue的js文件cdn(内容分发网络)--> <script src="https://cdn.jsdelivr/npm/vue"></script> 修改item.html,填充三个分类数据传入,代码如下: <div class="crumb-wrap"> <ul class="sui-breadcrumb"> <li> <a href="#" th:text="${category1.name}"></a> </li> <li> <a href="#" th:text="${category2.name}"></a> </li> <li> <a href="#" th:text="${category3.name}"></a> </li> </ul> </div> 修改item.html,将商品图片信息输出,在真实工作中需要做空判断,代码如下: <div class="fl preview-wrap"> <!--放大镜效果--> <div class="zoom"> <!--默认第一个预览--> <div id="preview" class="spec-preview"> <span class="jqzoom"><img th:jqimg="${imageList[0].url}" th:src="${imageList[0].url}" width="400px"/></span> </div> <!--下方的缩略图--> <div class="spec-scroll"> <a class="prev"><</a> <!--左右按钮--> <div class="items"> <ul> <li th:each="img:${imageList}"><img th:src="${img.url}" th:bimg="${img.url}" onmousemove="preview(this)"/></li> </ul> </div> <a class="next">></a> </div> </div> </div> 规格输出,代码如下: <div id="specification" class="summary-wrap clearfix"> <!--循环MAP--> <dl th:each="spec,specStat:${specificationList}"> <dt> <div class="fl title"> <i th:text="${spec.attributeName}"></i> </div> </dt> <dd th:each="arrValue:${spec.attributeValue}"> <a href="javascript:;" th:v-bind:class="|{selected:sel('${spec.attributeName}','${arrValue}')}|" th:@click="|selectSpecification('${spec.attributeName}','${arrValue}')|"> <i th:text="${arrValue}"></i> <span title="点击取消选择"> </span> </a> </dd> </dl> </div> 扩展属性,代码如下: <div class="tab-content tab-wraped"> <div id="one" class="tab-pane active"> <ul class="goods-intro unstyled"> </ul> <div class="intro-detail" th:utext="${goodsDesc.introduction}"> </div> </div> <div id="two" class="tab-pane" th:utext="${goodsDesc.packageList}"> <p>规格与包装</p> </div> <div id="three" class="tab-pane" th:utext="${goodsDesc.saleService}"> <p>售后保障</p> </div> <div id="four" class="tab-pane"> <p>商品评价</p> </div> <div id="five" class="tab-pane"> <p>手机社区</p> </div> </div> 默认SKU显示,代码如下: <script th:inline="javascript"> var item = new Vue({ el: '#itemArray', data: { skuList: [[${itemList}]], sku: {}, spec: {} }, created: function () { this.sku = JSON.parse(JSON.stringify(this.skuList[0])); this.spec = JSON.parse(this.skuList[0].spec); } }) </script> 页面显示默认的Sku信息: <div class="fr itemInfo-wrap" id="itemArray"> <div class="sku-name"> <h4>{{sku.title}}</h4> </div> <div class="news"><span th:text="${goods.caption}"></span></div> <div class="summary"> <div class="summary-wrap"> <div class="fl title"> <i>价 格</i> </div> <div class="fl price"> <i>¥</i> <em>{{sku.price}}</em> <span>降价通知</span> </div> <div class="fr remark"> <i>累计评价</i><em>612188</em> </div> ....... </div> </div> <!--product-detail--> 记录选中的Sku methods: { selectSpecification: function (specName, specValue) { //选中的spec信息 this.$set(this.spec, specName, specValue); //循环匹配 for (var i = 0; i < this.skuList.length; i++) { //匹配规格是否相同,如果相同,则表明选中的是该SKU if (this.matchObject(JSON.parse(this.skuList[i].spec), this.spec)) { this.sku = this.skuList[i]; return; } } //如果上面执行完毕,没有找到SKU,则提示下架操作 this.sku = {'id':0,'title':'提示:该商品已经下架','price':0}; }, matchObject: function (map1, map2) { for (var k in map1) { if (map1[k] != map2[k]) { return false; } } for (var k in map2) { if (map2[k] != map1[k]) { return false; } } return true; } } 添加规格点击事件 <ddth:each="arrValue:${spec.attributeValue}"> <ahref="javascript:;" th:v-bind:class="|{selected:sel('${spec.attributeName}','${arrValue}')}|" th:@click="|selectSpecification('${spec.attributeName}','${arrValue}')|"> <ith:text="${arrValue}"></i> <spantitle="点击取消选择"> </span> </a> </dd> 样式切换: sel: function (name, value) { if (this.spec == undefined) { return false; } if (this.spec[name] == value) { return true; } else { return false; } }启动启动类
9.如何在数据库中商品数据发生变化立即生成该商品的静态商品详情页?
使用canal 动态监控数据库中商品数据的变化,canal监听到商品数据发生变化,就会调用静态页微服务并传递spuId, 接下来静态页微服务就生成静态页
Day16
1.什么是消息队列?实现消息队列的有那两种主流方式?他们的区别是什么?1.什么是消息队列?实现消息队列的有那两种主流方式?他们的区别是什么?
消息队列,即MQ,Message Queue
消息队列是典型的:生产者、消费者模型。生产者不断向消息队列中生产消息,消费者不断的从队列中获取消息。因为消息的生产和消费都是异步的,而且只关心消息的发送和接收,没有业务逻辑的侵入,这样就实现了生产者和消费者的解耦。
MQ是消息通信的模型,并不是具体实现。现在实现MQ的有两种主流方式:AMQP、JMS。
两者间的区别和联系:
- JMS是定义了统一的接口,来对消息操作进行统一;AMQP是通过规定协议来统一数据交互的格式
- JMS限定了必须使用Java语言;AMQP只是协议,不规定实现方式,因此是跨语言的。
- JMS规定了两种消息模型;而AMQP的消息模型更加丰富
2.常见的MQ产品有哪些?分别有哪些特点?
ActiveMQ:基于JMS
RabbitMQ:基于AMQP协议,erlang语言开发,稳定性好
RocketMQ:基于JMS,阿里巴巴产品,目前交由Apache基金会
Kafka:分布式消息系统,高吞吐量
3.简单介绍下RabbitMQ?RabbitMQ有哪几种消息模型?
RabbitMQ概述:
RabbitMQ是一个开源的,在AMQP基础上完整的,可复用的企业消息系统
支持主流的操作系统,linux、windows、macox等
多种开发语言支持,java,python、Ruby、.NET等RabbitMQ的五种消息模型
简单工作模型
工作模型
订阅模式-----Fanout(广播)
订阅模式 ---- Direct(路由)
订阅模式 ---- topic(通配符)
4.如何下载安装配置RabbitMQ容器?
docker镜像下载:
docker pull rabbitmq:management
注意点:如果docker pull rabbitmq 后面不带management,启动rabbitmq后是无法打开管理界面的,所以我们要下载带management插件的rabbitmq.创建rabbitmq容器
docker run -di -p 5672:5672 -p 15672:15672 --name rabbitmq rabbitmq:management访问管理界面:
访问管理界面的地址就是 http://【自己的虚拟机IP地址】:15672,可以使用默认的账户登录,用户名和密码都guest
5.SpringBoot如何整合AMQP?
新建一个springboot工程,在其建立过程中,在【Dependencies】选项中选择Web下的Spring Web,在【Messaging】选项中选择Spring for RabbitMQ
导入依赖
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.amqp</groupId> <artifactId>spring-rabbit-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>修改application.yml配置文件:
spring: rabbitmq: host: 192.168.111.131 port: 5672 username: guest password: guest创建配置类:
package com.offcn.config;import org.springframework.amqp.core.Queue;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configurationpublic class SimpleQueueConfig { /** * 定义简单队列名 */ private String simpleQueue = "spring.test.queue"; @Bean public Queue simpleQueue() { return new Queue(simpleQueue); }}发送端代码:
package com.offcn.controller;import org.springframework.amqp.rabbit.core.RabbitTemplate;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestControllerpublic class SendController { @Autowired private RabbitTemplate rabbitTemplate; @RequestMapping("/simple") public void send1() { for(int i=0;i<5;i++) { String message="简单队列消息"+i; rabbitTemplate.convertAndSend("spring.test.queue", message); } }}接收端代码:
package com.offcn.listener;import org.springframework.amqp.rabbit.annotation.RabbitListener;import org.springframework.stereotype.Component;@Componentpublic class ReceiveListener { @RabbitListener(queues= "spring.test.queue") public void recieve1(String message) { System.out.println(message); }}测试:
访问http://localhost:8080/simple
6.简单介绍下简单队列模式,work消息模型、发布订阅模式、路由模式、主题模式各有什么特点和区别?
简单队列模式:
只包含一个生产者以及一个消费者,生产者Producer将消息发送到队列中,消费者Consumer从该队列接收消息。(单生产单消费)work消息模型:
多个消费者绑定到同一个队列上,一条消息只能被一个消费者进行消费。工作队列有轮训分发和公平分发两种模式
轮训分发:
特点:两个消费者处理消息的效率不同,但是最后接收到的消息还是一样多
公平分发模式:
特点:消费者如果处理消息的时间较短(效率较高),那么它处理的消息会比较多一些【能者多劳】发布订阅模式:
生产者将消息发送到交换器,然后交换器绑定到多个队列,监听该队列的所有消费者消费消息。
特点:
一个生产者,多个消费者;
每一个消费者都有自己的消息队列,分别绑定到不同的队列上;
生产者没有把消息发送到队列,而是发送到交换器exchange上;
每个队列都需要绑定到交换机上;
生产者生产的消息先经过交换机然后到达队列,一个消息可以被多个消费者消费;
如果消息发送到没有队列绑定的交换器时,消息将会丢失,因为交换器没有存储消息的能力,只有队列才有存储消息的能力;
路由模式(Routing)
生产者将消息发送到direct交换器,它会把消息路由到那些binding key与routing key完全匹配的Queue中,这样就能实现消费者有选择性地去消费消息主题模式:
类似于正则表达式匹配的一种模式。主要使用#、*进行匹配
7.如何发送短信?
- 创建 dongyimai-sms-service (WAR工程)并且引入依赖
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.15</version> </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.2.1</version> </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpcore</artifactId> <version>4.2.1</version> </dependency> <dependency> <groupId>commons-lang</groupId> <artifactId>commons-lang</artifactId> <version>2.6</version> </dependency> <dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-util</artifactId> <version>9.3.7.v20160115</version> </dependency> </dependencies>
- 创建配置文件application.yml
server: port: 9006 spring: application: name: sms rabbitmq: host: 192.168.188.128 port: 5672 username: guest password: guestsms: appcode: b6972XXXXXXXfc0cdb62c48 #使用自己的AppCode tpl_id: TP1711063 # 默认模板创建配置类:
package com.offcn.config;import org.springframework.amqp.core.Queue;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configurationpublic class SimpleQueueConfig { /** * 定义简单队列名 */ private String simpleQueue = "dongyimai.sms.queue"; @Bean public Queue simpleQueue() { return new Queue(simpleQueue); }}引入短信工具类SmsUtil.java
package com.offcn.util;import org.apache.http.HttpResponse;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Component;import java.util.HashMap;import java.util.Map;@Componentpublic class SmsUtil { @Value("${sms.appcode}") private String appcode; @Value("${sms.tpl_id}") private String tpl_id; private String host = "http://dingxin.market.alicloudapi"; private String path = "/dx/sendSms"; private String method = "POST"; public HttpResponse sendSms(String mobile, String code) { Map<String, String> headers = new HashMap<String, String>(); //最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105 headers.put("Authorization", "APPCODE " + appcode); Map<String, String> querys = new HashMap<String, String>(); querys.put("mobile", mobile); querys.put("param", "code:" + code); querys.put("tpl_id", tpl_id); Map<String, String> bodys = new HashMap<String, String>(); try { HttpResponse response = HttpUtils.doPost(host, path, method, headers, querys, bodys); System.out.println(response.toString()); //获取response的body System.out.println(EntityUtils.toString(response.getEntity())); return response; } catch (Exception e) { e.printStackTrace(); return null; } }}编写消息监听类:SmsListener.java
package com.offcn.listener;import com.offcn.util.SmsUtil;import org.springframework.amqp.rabbit.annotation.RabbitListener;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import java.util.Map;@Componentpublic class SmsListener { @Autowired private SmsUtil smsUtil; @RabbitListener(queues = "dongyimai.sms.queue") public void getMessage(Map<String,String> map) throws Exception { if (map == null) { return; } String mobile = map.get("mobile"); String code = map.get("code"); // 发送短信 smsUtil.sendSms(mobile,code); }}编写测试短信发送类 SendController.java
@RestControllerpublic class SendController { @Autowired private RabbitTemplate rabbitTemplate; @RequestMapping("/") public void send() { Map<String, String> map = new HashMap<>(); map.put("mobile", "【填写自己的手机号】"); map.put("code", "9999"); rabbitTemplate.convertAndSend("dongyimai.sms.queue", map); }}编写启动类:
package com.offcn;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplicationpublic class SmsApplication { public static void main(String[] args) { SpringApplication.run(SmsApplication.class,args); }}测试:
访问http://localhost:9006/
8.用户注册的时候如何实现通过消息发送短信进行用户手机的短信验证?说说具体实现思路
实现思路:
点击页面上的”获取短信验证码”连接,向后端传递手机号。后端随机生成6位数字作为短信验证码,将其保存在redis中(手机号作为KEY),并发送到短信网关。
用户注册时,后端根据手机号查询redis中的验证码与用户填写的验证码是否相同,如果不同则提示用户不能注册。
实现代码:
生成验证码:/** * 生成短信验证码 * @return */ public void createSmsCode(String phone);接口实现类:
@Autowired private RedisTemplate redisTemplate; /** * 生成短信验证码 * * @param phone * @return */ @Override public void createSmsCode(String phone) { //生成6位随机数 String code = (long) (Math.random()*1000000)+""; System.out.println("验证码:"+code); //存入缓存 redisTemplate.boundHashOps("smscode").put(phone, code); //发送到RabbitMQ System.out.println("发送的手机验证码为:"+code); Map<String, String> map = new HashMap<>(); map.put("mobile",phone); map.put("code",code); rabbitTemplate.convertAndSend("dongyimai.sms.queue",map); }dongyimai-common 添加工具类PhoneFormatCheckUtils.java(资源\工具类),用于验证手机号
修改dongyimai-user-service的UserController.java
/** * 发送短信验证码 * @param phone * @return */ @GetMapping("/sendCode") public Result sendCode(String phone){//判断手机号格式if(!PhoneFormatCheckUtils.isPhoneLegal(phone)){ return new Result(false, StatusCode.ERROR,"手机号格式不正确");}try { userService.createSmsCode(phone);//生成验证码 return new Result(true,StatusCode.OK, "验证码发送成功");} catch (Exception e) { e.printStackTrace(); return new Result(false,StatusCode.ERROR, "验证码发送失败");} }用户注册判断验证码:
修改dongyimai-user-service的UserService.java,增加方法/** * 判断短信验证码是否存在 * @param phone * @return */ public boolean checkSmsCode(String phone,String code);修改dongyimai-user-service的 UserServiceImpl.java
/** * 判断验证码是否正确 */ public boolean checkSmsCode(String phone,String code){ //得到缓存中存储的验证码 String sysCode = (String) redisTemplate.boundHashOps("smscode").get(phone); if(sysCode==null){ return false; } if(!sysCode.equals(code)){ return false; } return true; }修改dongyimai-user-serivce的UserController.java
/** * 增加 * @param user * @return */ @PostMapping("/add") public Result add(@RequestBody User user,String smscode){ boolean checkSmsCode = userService.checkSmsCode(user.getPhone(), smscode); if(checkSmsCode==false){ return new Result(false,StatusCode.ERROR ,"验证码输入错误!"); } try { userService.add(user); return new Result(true,StatusCode.OK, "增加成功"); } catch (Exception e) { e.printStackTrace(); return new Result(false,StatusCode.ERROR, "增加失败"); } }测试
Day17
1.什么是微服务网关?使用微服务网关可以又哪些优点?
可以解决哪些问题?微服务网关就是一个系统,通过暴露该微服务网关系统,方便我们进行相关的鉴权,安全控制,日志统一处理,易于监控的相关功能。
优点如下:
安全,只有网关系统对外进行暴露,微服务可以隐藏在内网,通过防火墙保护。
易于监控。可以在网关收集监控数据并将其推送到外部系统进行分析。
易于认证。可以在网关上进行认证,然后再将请求转发到后端的微服务,而无须在每个微服务中进行认证。
减少了客户端与各个微服务之间的交互次数
易于统一授权。
可以解决以下问题:
客户端会多次请求不同的微服务,增加了客户端的复杂性
存在跨域请求,在一定场景下处理相对复杂
认证复杂,每个服务都需要独立认证
难以重构,随着项目的迭代,可能需要重新划分微服务。例如,可能将多个服务合并成一个或者将一个服务拆分成多个。如果客户端直接与微服务通信,那么重构将会很难实施
某些微服务可能使用了防火墙/浏览器不友好的协议,直接访问会有一定的困难
2.可以实现微服务网关的技术有哪些?
- nginx Nginx (tengine x)是一个高性能的[HTTP]和[反向代理]web服务器,同时也提供了IMAP(交互邮件访问协议)/POP3(邮局协议版本3)/SMTP(简单邮件传输协议)服务。
- zuul ,Zuul 是 Netflix 出品的一个基于 JVM 路由和服务端的负载均衡器。
- spring-cloud-gateway,是spring 出品的基于spring 的网关项目,集成断路器,路径重写,性能比Zuul好。
3.如何搭建一个微服务网关?如何对网关进行跨域配置?
1)在dongyimai-gateway工程中,创建 dongyimai-gateway-web工程,引入如下依赖:
<!--网关依赖--><dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency></dependencies>2)添加启动类
@SpringBootApplication@EnableEurekaClientpublic class GatewayWebApplication {public static void main(String[] args){ SpringApplication.run(GatewayWebApplication.class,args);}}3)编写application.yml配置文件
spring: application: name: gateway-webserver: port: 8001eureka: client: service-url: defaultZone: http://localhost:8761/eureka instance: prefer-ip-address: true#开启网关的端点监控management: endpoint: gateway: enabled: true web: exposure: include: true4)对网关进行跨域配置
spring:cloud: gateway: #网关 globalcors: #全局跨域 cors-configurations: #跨域配置 '[/**]': #匹配所有请求 allowedOrigins: "*" #跨域处理允许所有的域 allowedMethods: #支持的方法 - GET - POST - PUT - DELETE
4.网关如何进行过滤配置?以及限流配置?
过滤配置:
1)host路由
比如用户请求cloud.ujiuye的时候,可以将请求路由给http://localhost:9001服务处理,需要在配置文件spring.cloud.gateway下添加如下代码routes: - id: dongyimai_goods_route uri: http://localhost:9001 predicates: - Host=cloud.ujiuye****注意:**此时要想让cloud.ujiuye访问本地计算机,要配置
C:\Windows\System32\drivers\etc\hosts文件,映射配置: 127.0.0.1 cloud.ujiuye。通过SwitchHosts来进行修改。
2)路径匹配过滤配置
根据请求路径实现对应的路由过滤操作,例如请求中以/brand/路径开始的请求,都直接交给http://localhost:9001服务处理,添加如下配置:routes: - id: dongyimai_goods_route uri: http://localhost:9001 predicates: - Path=/brand**3)prefixpath过滤配置
用户每次请求路径的时候,可以给真实请求加一个统一前缀,例如请求http://localhost:8001的时候我们让它请求真实地址http://localhost:8001/brand,添加如下配置:routes: - id: dongyimai_goods_route uri: http://localhost:9001 predicates: #- Host=cloud.ujiuye** - Path=/** filters: - PrefixPath=/brand4)stripprefix过滤配置
请求路径是/api/brand,而真实路径是/brand,这时候我们需要去掉/api才是真实路径,配置如下:routes: - id: dongyimai_goods_route uri: http://localhost:9001 predicates: #- Host=cloud.ujiuye** - Path=/api/brand filters: #- PrefixPath=/brand - StripPrefix=15)LoadBalancerClient 路由过滤器(客户端负载均衡)
当并发量较大的时候,我们需要根据服务的名称判断来做负载均衡操作,可以使用LoadBalancerClientFilter来实现负载均衡调用。修改application.yml配置文件,代码如下:routes: - id: dongyimai_goods_route #uri: http://localhost:9001 uri: lb://DYM-SELLERGOODS predicates: #- Host=cloud.ujiuye** - Path=/** filters: #- PrefixPath=/brand - StripPrefix=1限流配置:
1)在dongyimai-gateway的pom.xml中引入redis的依赖<!--redis--><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis-reactive</artifactId> <version>2.1.3.RELEASE</version></dependency>2)在Applicatioin引导类中添加如下代码
/**** IP限流 *@return */ @Bean(name="ipKeyResolver") public KeyResolver userKeyResolver(){ return new KeyResol ver(){ @Override public Mono<String> resolve(ServerWebExchange exchange){ //获取远程客户端IP String hostName = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress(); System.out.println("hostName:"+hostName); return Mono.just(hostName); } };}3)修改application.yml中配置
spring: cloud: gateway: globalcors: cors-configurations: '[/**]': # 匹配所有请求 allowedOrigins: "*" #跨域处理 允许所有的域 allowedMethods: # 支持的方法 - GET - POST - PUT - DELETE routes: - id: dongyimai_goods_route uri: lb://DYM-SELLERGOODS predicates: # - Host=cloud.ujiuye** - Path=/api/album/**,/api/brand/** filters: - StripPrefix=1 - name: RequestRateLimiter #请求数限流 名字不能随便写 ,使用默认的facatory args: key-resolver: "#{@ipKeyResolver}" redis-rate-limiter.replenishRate: 1 redis-rate-limiter.burstCapacity: 1 application: name: gateway-web #Redis配置 redis: host: 192.168.188.129 port: 6379server: port: 8001eureka: client: service-url: defaultZone: http://localhost:8761/eureka instance: lease-renewal-interval-in-seconds: 5 # 每隔5秒发送一次心跳 lease-expiration-duration-in-seconds: 10 # 10秒不发送就过期 prefer-ip-address: true ip-address: 127.0.0.1 instance-id: ${spring.application.name}:${server.port}management: endpoint: gateway: enabled: true web: exposure: include: true
5.什么是JWT? JWT令牌包括哪些组成部分?如何生成一个令牌? 如何解析一个令牌
JWT:
JWT是用于微服务之间传递用户信息的一段加密字符串,该字段串是一个json格式,每个微服务可以根据该JSON字符串识别用户身份信息,也就是该JSON字符串中包含(封装)用户身份信息。
JWT由三部分组成:分别是头部、载荷与签名。
如何生成令牌:
1)在dongyimai-parent项目中的pom.xml中添加依赖:
<!--鉴权--><dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version></dependency>2)在dongyimai-common的/test/java下创建测试类,并设置测试方法
public class JwtTest {/***创建Jwt令牌*/ @Test public void testCreateJwt(){ JwtBuilder builder= Jwts.builder() .setId("888")//设置唯一编号 .setSubject("小白")//设置主题可以是JSON数据 .setIssuedAt(new Date())//设置签发日期 .signWith(SignatureAlgorithm.HS256,"ujiuye");//设置签名使用HS256算法,并设置SecretKey(字符串) //构建并返回一个字符串 System.out.println( builderpact()); }}如何解析一个令牌:
编写如下测试方法:/***解析Jwt令牌数据*/@Testpublic void testParseJwt(){ String compactJwt="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIwNjIyODd9.RBLpZ79USMplQyfJCZFD2muHV_KLks7M1ZsjTu6Aez4"; Claims claims = Jwts.parser(). setSigningKey("ujiuye"). parseClaimsJws(compactJwt). getBody(); System.out.println(claims);}
6.请说说微服务中实现鉴权的业务逻辑?以及实现的技术流程?如何实现会话的保持?
业务逻辑:
用户通过访问微服务网关调用微服务,同时携带头文件信息
在微服务网关这里进行拦截,拦截后获取用户要访问的路径
识别用户访问的路径是否需要登录,如果需要,识别用户的身份是否能访问该路径[这里可以基于数据库设计一套权限
如果需要权限访问,用户已经登录,则放行
如果需要权限访问,且用户未登录,则提示用户需要登录
用户通过网关访问用户微服务,进行登录验证
验证通过后,用户微服务会颁发一个令牌给网关,网关会将用户信息封装到头文件中,并响应用户
用户下次访问,携带头文件中的令牌信息即可识别是否登录
实现的技术流程:
(1)生成令牌工具类在dongyimai-common中创建类utils.JwtUtil,主要辅助生成Jwt令牌信息,代码如下:
public class JwtUtil { //有效期为 public static final Long JWT_TTL = 3600000L;//60 *60 *1000 一个小时 //Jwt令牌信息 public static final String JWT_KEY = "ujiuye"; /** * 创建令牌 * @param id 设置唯一编号 * @param subject 设置主题可以是JSON数据 载荷 * @param ttlMillis 有效期 过期时间 * @return */ public static String createJWT(String id, String subject, Long ttlMillis){ //指定算法 SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; //当前系统时间 long nowMillis = System.currentTimeMillis(); //令牌签发时间 Date now = new Date(nowMillis); //如果令牌有效期为null,则默认设置有效期1小时 if(ttlMillis==null){ ttlMillis=JwtUtil.JWT_TTL; } //令牌过期时间设置 long expMillis = nowMillis + ttlMillis; Date expDate = new Date(expMillis); //生成秘钥 SecretKey secretKey = generalKey(); //封装Jwt令牌信息 JwtBuilder builder = Jwts.builder() .setId(id)//唯一的ID .setSubject(subject)//主题可以是JSON数据 .setIssuer("admin")//签发者 .setIssuedAt(now)//签发时间 .signWith(signatureAlgorithm, secretKey)//签名算法以及密匙 .setExpiration(expDate); //设置过期时间 return builderpact(); } /** *生成加密 secretKey *@return */ public static SecretKey generalKey(){ byte[] encodedKey = Base64.getEncoder().encode(JwtUtil.JWT_KEY.getBytes()); SecretKey key = new SecretKeySpec(encodedKey,0, encodedKey.length,"AES"); return key; } /** *解析令牌数据 *@param jwt *@return *@throws Exception */ public static Claims parseJWT(String jwt) throws Exception { SecretKey secretKey = generalKey(); return Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(jwt) .getBody(); }}(2)用户登录成功则签发TOKEN,修改登录的方法:
/****用户登录*/@RequestMapping(value = "/login")public Result login(String username,String password){ //查询用户信息 User user = userService.findByUsername(username); if(user!=null && BCrypt.checkpw(password,user.getPassword())){ //设置令牌信息 Map<String,Object> info = new HashMap<String,Object>(); info.put("role","USER"); info.put("success","SUCCESS"); info.put("username",username); //生成令牌 String jwt = JwtUtil.createJWT(UUID.randomUUID().toString(), JSON.toJSONString(info),null); return new Result(true,StatusCode.OK,"登录成功!",jwt); } return new Result(false,StatusCode.LOGINERROR,"账号或者密码错误!");}(3)自定义全局过滤器,创建过滤器类
@Componentpublic class AuthorizeFilter implements GlobalFilter, Ordered { //令牌头名字 private static final String AUTHORIZE_TOKEN = "Authorization"; /*** *全局过滤器 *@param exchange *@param chain *@return */ @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain){ //获取Request、Response对象 ServerHttpRequest request = exchange.getRequest(); ServerHttpResponse response = exchange.getResponse(); //获取请求的URI String path = request.getURI().getPath(); //如果是登录、goods等开放的微服务[这里的goods部分开放],则直接放行,这里不做完整演示,完整演示需要设计一套权限系统 if (path.startsWith("/api/user/login")|| path.startsWith("/api/brand/search/")){ //放行 Mono<Void> filter = chain.filter(exchange); return filter; } //获取头文件中的令牌信息 String tokent = request.getHeaders().getFirst(AUTHORIZE_TOKEN); //如果头文件中没有,则从请求参数中获取 if (StringUtils.isEmpty(tokent)){ tokent = request.getQueryParams().getFirst(AUTHORIZE_TOKEN); } //如果为空,则输出错误代码 if (StringUtils.isEmpty(tokent)){ //设置方法不允许被访问,405错误代码 response.setStatusCode(HttpStatus.METHOD_NOT_ALLOWED); return response.setComplete(); } //解析令牌数据 try { Claims claims = JwtUtil.parseJWT(tokent); } catch (Exception e){ e.printStackTrace(); //解析失败,响应401错误 response.setStatusCode(HttpStatus.UNAUTHORIZED); return response.setComplete(); } //放行 return chain.filter(exchange); } /*** *过滤器执行顺序 *@return */ @Override public int getOrder(){ return 0; }}会话保持
用户每次请求的时候,我们都需要获取令牌数据,方法有多重,可以在每次提交的时候,将数据提交到头文件中,也可以将数据存储到Cookie中,每次从Cookie中校验数据,还可以每次将令牌数据以参数的方式提交到网关
Day18
1.什么认证?什么是授权?
身份认证:
用户身份认证即用户去访问系统资源时系统要求验证用户的身份信息,身份合法方可继续访问。常见的用户身份认证表现形式有:用户名密码登录,指纹打卡等方式。说通俗点,就相当于校验用户账号密码是否正确。用户授权:
用户认证通过后去访问系统的资源,系统会判断用户是否拥有访问资源的权限,只允许访问有权限的系统资源,没有权限的资源将无法访问,这个过程叫用户授权。
2.什么是单点登录?实现单点登陆的思路?单点登录特点是什么?有哪些技术可以实现单点登录?
单点登录:
单点登录(SSO)的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。实现单点登录的思路:
分布式系统要实现单点登录,通常将认证系统独立抽取出来,并且将用户身份信息存储在单独的存储介质,比如: MySQL、Redis,考虑性能要求,通常存储在Redis中单点登录特点:
1、认证系统为独立的系统。
2、各子系统通过Http或其它协议与认证系统通信,完成用户认证。
3、用户身份信息存储在Redis集群。可以实现单点登录的技术有:
1、Apache Shiro.
2、CAS
3、Spring security CAS
3.什么是第三方登录,第三方登录有哪些优点?第三方认证实现思路如何?
所谓的第三方登录,是说基于用户在第三方平台上已有的账号和密码来快速完成己方应用的登录或者注册的功能。而这里的第三方平台,一般是已经拥有大量用户的平台,国外的比如Facebook,Twitter等,国内的比如微博、微信、QQ等。
第三方登录的优点:
- 相比于本地注册,第三方登录一般来说比较方便、快捷,能够显著降低用户的注册和登录成本,方便用户实现快捷登录或注册。
- 不用费尽心思地应付本地注册对账户名和密码的各种限制,如果不考虑昵称的重复性要求,几乎可以直接一个账号走遍天下,再也不用在大脑或者什么地方记住N多不同的网站或App的账号和密码,整个世界一下子清静了。
- 在第一次绑定成功之后,之后用户便可以实现一键登录,使得后续的登录操作比起应用内的登录来容易了很多。
- 对于某些喜欢社交,并希望将更多自己的生活内容展示给朋友的人来说,第三方登录可以实现把用户在应用内的活动同步到第三方平台上,省去了用户手动发布动态的麻烦。但对于某些比较注重个人隐私的用户来说,则会有一些担忧,所以印哥所说的这个优点是有前提的。
- 因为降低了用户的注册或登录成本,从而减少由于本地注册的繁琐性而带来的隐形用户流失,最终提高注册转化率。
- 对于某些应用来说,使用第三方登录完全可以满足自己的需要,因此不必要设计和开发一套自己的账户体系。
- 通过授权,可以通过在第三方平台上分享用户在应用内的活动在第三方平台上宣传自己,从而增加产品知名度。
- 通过授权,可以获得该用户在第三方平台上的好友或粉丝等社交信息,从而后续可以针对用户的社交关系网进行有目的性的营销宣传,为产品的市场推广提供另一种渠道。
第三方认证实现思路:
当需要访问第三方系统的资源时需要首先通过第三方系统的认证(例如:微信认证),由第三方系统对用户认证通过,并授权资源的访问权限。
4.什么是Oauth2认证? Oauth2的认证流程是什么?我们在东易买项目使用Oauth2实现哪些目标?
Oauth2认证:
OAuth(开放授权)是一个开放标准,允许用户授权第三方移动应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享他们数据的所有内容,OAuth2.0是OAuth协议的延续版本。Oauth2的认证流程:
- 客户端请求第三方授权
- 资源拥有者同意给客户端授权
- 客户端获取到授权码,请求认证服务器申请令牌 此过程用户看不到,客户端应用程序请求认证服务器,请求携带授权码。
- 认证服务器向客户端响应令牌 认证服务器验证了客户端请求的授权码,如果合法则给客户端颁发令牌,令牌是客户端访问资源的通行证。 此交互过程用户看不到,当客户端拿到令牌后,用户在优就业官网看到已经登录成功。
- 客户端请求资源服务器的资源 客户端携带令牌访问资源服务器的资源。
- 资源服务器返回受保护资源 资源服务器校验令牌的合法性,如果合法则向用户响应资源信息内容。 注意:资源服务器和认证服务器可以是 一个服务也可以分开的服务,如果是分开的服务, 资源服务器通常要请求认证 服务器来校验令牌的合法性。
在东易买项目使用Oauth2实现如下目标:
- 东易买访问第三方系统的资源
- 外部系统访问东易买商城的资源
- 东易买前端(客户端) 访问东易买微服务的资源。
- 东易买微服务之间访问资源,例如:微服务A访问微服务B的资源,B访问A的资源。
5.简单介绍下Spring Security Oauth2认证解决方案?
用户请求认证服务完成认证。
认证服务下发用户身份令牌,拥有身份令牌表示身份合法。
用户携带令牌请求资源服务,请求资源服务必先经过网关。
网关校验用户身份令牌的合法,不合法表示用户没有登录,如果合法则放行继续访问。
资源服务获取令牌,根据令牌完成授权。
资源服务完成授权则响应资源信息。
6.Oauth2的授权模式有哪些? 其中授权码授权如何实现? 账号密码授权如何实现?
授权码模式(Authorization Code)
隐式授权模式(Implicit)
密码模式(Resource Owner Password Credentials)
客户端模式(Client Credentials)
授权码授权实现流程:
客户端请求第三方授权
用户(资源拥有者)同意给客户端授权
客户端获取到授权码,请求认证服务器申请令牌
认证服务器向客户端响应令牌
客户端请求资源服务器的资源,资源服务校验令牌合法性,完成授权
资源服务器返回受保护资源
账号密码授权实现流程:
- 认证
密码模式(Resource Owner Password Credentials)与授权码模式的区别是申请令牌不再使用授权码,而是直接 通过用户名和密码即 可申请令牌。
测试如下:
Post请求:http://localhost:9100/oauth/token
参数:
grant_type:密码模式授权填写password
username:账号
password:密码
并且此链接需要使用 http Basic认证。- 校验令牌
Spring Security Oauth2提供校验令牌的端点,如下:
Get: http://localhost:9100/oauth/check_token?token=
参数:
token:令牌- 刷新令牌
刷新令牌是当令牌快过期时重新生成一个令牌,它于授权码授权和密码授权生成令牌不同,刷新令牌不需要授权码 也不需要账号和密码,只需 要一个刷新令牌、客户端id和客户端密码。
测试如下:
Post:http://localhost:9100/oauth/token
参数:
grant_type: 固定为 refresh_token
refresh_token:刷新令牌(注意不是access_token,而是refresh_token)
7.资源服务器授权的流程是什么? 传统授权流程与公钥和私钥的授权流程相比有哪些异同?
资源服务器授权的流程:
传统授权流程:
客户端先去授权服务器申请令牌,申请令牌后,携带令牌访问资源服务器,资源服务器访问授权服务校验令牌的合法性,授权服务会返回校验结果,如果校验成功会返回用户信息给资源服务器,资源服务器如果接收到的校验结果通过了,则返回资源给客户端。公钥私钥授权流程:
- 客户端请求认证服务申请令牌
- 认证服务生成令牌认证服务采用非对称加密算法,使用私钥生成令牌。
- 客户端携带令牌访问资源服务,客户端在Http header 中添加: Authorization:Bearer 令牌。
- 资源服务器接收到令牌,使用公钥校验令牌的合法性。
- 令牌有效,资源服务向客户端响应资源信息
传统授权流程与公钥和私钥的授权流程异同点:
- 相同点:客户端都要去授权服务器申请令牌,并携带令牌访问资源服务器
- 不同点:传统的授权模式性能低下,每次都需要请求授权服务校验令牌合法性,而公钥私钥授权模式通过加密解密算法性能更高效
8.公钥私钥的原理时什么?如何生成公钥? 如何生成私钥?
公钥私钥的原理:
张三有两把钥匙,一把是公钥,另一把是私钥。
张三把公钥送给他的朋友们----李四、王五、赵六----每人一把。
李四要给张三写一封保密的信。她写完后用张三的公钥加密,就可以达到保密的效果。
张三收信后,用私钥解密,就看到了信件内容。这里要强调的是,只要张三的私钥不泄露,这封信就是安全的,即使落在别人手里,也无法 解密。
张三给李四回信,决定采用"数字签名"。他写完后先用Hash函数,生成信件的摘要(digest)。然后张三使用私钥,对这个摘要加密,生 成”数字签名”(signature),张三将这个签名,附在信件下面,一起发给李四。
李四收信后,取下数字签名,用张三的公钥解密,得到信件的摘要。由此证明,这封信确实是张三发出的。李四再对信件本身使用Hash函 数,将得到的结果,与上一步得到的摘要进行对比。如果两者一致,就证明这封信未被修改过。
生成公钥和私钥:
生成密钥证书
创建一个文件夹,在该文件夹下执行如下命令行:
keytool -genkeypair -alias dongyimai -keyalg RSA -keypass dongyimai -keystore dongyimai.jks -storepass dongyimai
并输入个人信息查询证书信息
keytool -list -keystore dongyimai.jks删除别名(不要去删除别名了)
9.简述认证开发的执行流程?并且叙述出认证流程的代码执行过程?
执行流程:
用户登录,请求认证服务
认证服务认证通过,生成jwt令牌,将jwt令牌及相关信息写入cookie
用户访问资源页面,带着cookie到网关
网关从cookie获取token,如果存在token,则校验token合法性,如果不合法则拒绝访问,否则放行
用户退出,请求认证服务,删除cookie中的token
代码执行过程:
- 工具封装
在dongyimai-user-oauth工程中添加如下工具对象,方便操作令牌信息。
创建com.offcn.oauth.util.AuthToken类,存储用户令牌数据,代码如下:public class AuthToken implements Serializable{ //令牌信息 String accessToken; //刷新token(refresh_token) String refreshToken; //jwt短令牌 String jti; //...get...set}创建com.offcn.oauth.util.UserJwt类,封装SpringSecurity中User信息以及用户自身基本信息,代码如下:
import org.springframework.security.core.GrantedAuthority;import org.springframework.security.core.userdetails.User;import java.util.Collection;public class UserJwt extends User { private String id; //用户ID private String name; //用户名字 public UserJwt(String username, String password, Collection<? extends GrantedAuthority> authorities) { super(username, password, authorities); } //...get...set}
- 业务层
用户从页面输入账号密码,到认证服务的Controller层,Controller层调用Service层,Service层调用OAuth2.0的认证地址,进行密码授权认证操作,如果账号密码正确了,就返回令牌信息给Service层,Service将令牌信息给Controller层,Controller层将数据存入到Cookie中,再响应用户。创建com.offcn.oauth.service.AuthService接口,并添加授权认证方法:
public interface AuthService { /*** * 授权认证方法 */ AuthToken login(String username, String password, String clientId, String clientSecret);}创建com.offcn.oauth.service.impl.AuthServiceImpl实现类,实现获取令牌数据,这里认证获取令牌采用的是密码授权模式,用的是RestTemplate向OAuth服务发起认证请求,代码如下:
@Servicepublic class AuthServiceImpl implements AuthService { @Autowired private LoadBalancerClient loadBalancerClient; @Autowired private RestTemplate restTemplate; /*** * 授权认证方法 * @param username * @param password * @param clientId * @param clientSecret * @return */ @Override public AuthToken login(String username, String password, String clientId, String clientSecret) { //申请令牌 AuthToken authToken = applyToken(username,password,clientId, clientSecret); if(authToken == null){ throw new RuntimeException("申请令牌失败"); } return authToken; } /**** * 认证方法 * @param username:用户登录名字 * @param password:用户密码 * @param clientId:配置文件中的客户端ID * @param clientSecret:配置文件中的秘钥 * @return */ private AuthToken applyToken(String username, String password, String clientId, String clientSecret) { //选中认证服务的地址 ServiceInstance serviceInstance = loadBalancerClient.choose("user-auth"); if (serviceInstance == null) { throw new RuntimeException("找不到对应的服务"); } //获取令牌的url String path = serviceInstance.getUri().toString() + "/oauth/token"; //定义body MultiValueMap<String, String> formData = new LinkedMultiValueMap<>(); //授权方式 formData.add("grant_type", "password"); //账号 formData.add("username", username); //密码 formData.add("password", password); //定义头 MultiValueMap<String, String> header = new LinkedMultiValueMap<>(); header.add("Authorization", httpbasic(clientId, clientSecret)); //指定 restTemplate当遇到400或401响应时候也不要抛出异常,也要正常返回值 restTemplate.setErrorHandler(new DefaultResponseErrorHandler() { @Override public void handleError(ClientHttpResponse response) throws IOException { //当响应的值为400或401时候也要正常响应,不要抛出异常 if (response.getRawStatusCode() != 400 && response.getRawStatusCode() != 401) { super.handleError(response); } } }); Map map = null; try { //http请求spring security的申请令牌接口 ResponseEntity<Map> mapResponseEntity = restTemplate.exchange(path, HttpMethod.POST,new HttpEntity<MultiValueMap<String, String>>(formData, header), Map.class); //获取响应数据 map = mapResponseEntity.getBody(); } catch (RestClientException e) { throw new RuntimeException(e); } if(map == null || map.get("access_token") == null || map.get("refresh_token") == null || map.get("jti") == null) { //jti是jwt令牌的唯一标识作为用户身份令牌 throw new RuntimeException("创建令牌失败!"); } //将响应数据封装成AuthToken对象 AuthToken authToken = new AuthToken(); //访问令牌(jwt) String accessToken = (String) map.get("access_token"); //刷新令牌(jwt) String refreshToken = (String) map.get("refresh_token"); //jti,作为用户的身份标识 String jwtToken= (String) map.get("jti"); authToken.setJti(jwtToken); authToken.setAccessToken(accessToken); authToken.setRefreshToken(refreshToken); return authToken; } /*** * base64编码 * @param clientId * @param clientSecret * @return */ private String httpbasic(String clientId,String clientSecret){ //将客户端id和客户端密码拼接,按“客户端id:客户端密码” String string = clientId+":"+clientSecret; //进行base64编码 byte[] encode = Base64Utils.encode(string.getBytes()); return "Basic "+new String(encode); }}
- 控制层
创建控制层com.offcn.oauth.controller.AuthController,编写用户登录授权方法,代码如下:@RestController@RequestMapping(value = "/user")public class AuthController { //客户端ID @Value("${auth.clientId}") private String clientId; //秘钥 @Value("${auth.clientSecret}") private String clientSecret; @Autowired AuthService authService; @PostMapping("/login") public Result login(String username, String password, HttpServletRequest request,HttpServletResponse response) { if(StringUtils.isEmpty(username)){ throw new RuntimeException("用户名不允许为空"); } if(StringUtils.isEmpty(password)){ throw new RuntimeException("密码不允许为空"); } //申请令牌 AuthToken authToken = authService.login(username,password,clientId,clientSecret); //用户身份令牌 String access_token = authToken.getAccessToken(); //将令牌存储到cookie CookieUtil.setCookie(request,response,"Authorization",access_token); return new Result(true, StatusCode.OK,"登录成功!"); }}
Day19
1.资源服务器如何进行授权配置?说说其中的业务流程和配置步骤?
(1)配置公钥 认证服务生成令牌采用非对称加密算法,认证服务采用私钥加密生成令牌,对外向资源服务提供公钥,资源服务使 用公钥 来校验令牌的合法性。 将公钥拷贝到 public.key文件中,将此文件拷贝到每一个需要的资源服务工程的classpath下 ,例如:用户微服务.
(2)添加依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId></dependency>(3)配置每个系统的Http请求路径安全控制策略以及读取公钥信息识别令牌
@Configuration@EnableResourceServer@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)//激活方法上的PreAuthorize注解public class ResourceServerConfig extends ResourceServerConfigurerAdapter { //公钥 private static final String PUBLIC_KEY = "public.key"; /*** * 定义JwtTokenStore * @param jwtAccessTokenConverter * @return */ @Bean public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) { return new JwtTokenStore(jwtAccessTokenConverter); } /*** * 定义JJwtAccessTokenConverter * @return */ @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setVerifierKey(getPubKey()); return converter; } /** * 获取非对称加密公钥 Key * @return 公钥 Key */ private String getPubKey() { Resource resource = new ClassPathResource(PUBLIC_KEY); try { InputStreamReader inputStreamReader = new InputStreamReader(resource.getInputStream()); BufferedReader br = new BufferedReader(inputStreamReader); return br.lines().collect(Collectors.joining("\n")); } catch (IOException ioe) { return null; } } /*** * Http安全配置,对每个到达系统的http请求链接进行校验 * @param http * @throws Exception */ @Override public void configure(HttpSecurity http) throws Exception { //所有请求必须认证通过 http.authorizeRequests() //下边的路径放行 .antMatchers( "/user/add"). //配置地址放行 permitAll() .anyRequest(). authenticated(); //其他地址需要认证授权 }}
2.OAuth如何对接到微服务?SpringSecurity如何进行权限控制?
用户登录成功后,会将令牌信息存入到cookie中(一般建议存入到头文件中)
用户携带Cookie中的令牌访问微服务网关
微服务网关先获取头文件中的令牌信息,如果Header中没有Authorization令牌信息,则从参数中找,参数中如果没有,则取Cookie中找Authorization,最后将令牌信息封装到Header中,并调用其他微服务
其他微服务会获取头文件中的Authorization令牌信息,然后匹配令牌数据是否能使用公钥解密,如果解密成功说明用户已登录,解密失败,说明用户未登录
3.OAuth客户端数据以及用户数据如何实现动态数据加载?请写出关键代码和配置?
(1) 在
com.yue.oauth.config.dongyimai-user-oauth操作①建库建表–记录客户端相关信息
创建数据库dongyimai_user,表名oauth_client_details,表字段client_id:客户端id resource_ids:资源id(暂时不用) client_secret:客户端秘钥 scope:范围 access_token_validity:访问token的有效期(秒) refresh_token_validity:刷新token的有效期(秒) authorized_grant_type:授权型:authorization_code,password,refresh_token,client_credentials②添加yml文件的数据源
datasource: druid: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/dongyimai_user?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true&serverTimezone=GMT%2B8 username: root password: 123456③修改客户端信息配置
/*** * 客户端信息配置 * @param clients * @throws Exception */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.jdbc(dataSource).clients(clientDetails()); }(2) 操作
com.yue.oauth.config.UserDetailsServiceImpl#对loadUserByUsername方法进行修改#将下面代码//静态方式return new User(username,new BCryptPasswordEncoder().encode(clientSecret), AuthorityUtilsmaSeparatedStringToAuthorityList(""));#修改为//数据库查找方式return new User(username,clientSecret,AuthorityUtilsmaSeparatedStringToAuthorityList(""));(3) 修改
com.offcn.user.feign.UserFeign@FeignClient(name="user")@RequestMapping("/user")public interface UserFeign { /*** * 根据username查询用户信息 * @param username * @return */ @GetMapping("/load/{username}") Result<User> findByUsername(@PathVariable String username);}(4) 修改
UserController@GetMapping("/load/{username}")public Result<User> findByUsername(@PathVariable String username){ //调用UserService实现根据主键查询User User user = userService.findByUsername(username); return new Result<User>(true,StatusCode.OK,"查询成功",user);}(5) 放行查询用户方法,在
dongyimai-user-service中放行/user/load/**方法(6)
oauth调用查询用户信息①
oauth引入对user-api的依赖<!--依赖用户api--><dependency> <groupId>com.yue</groupId> <artifactId>dongyimai-user-service-api</artifactId> <version>1.0-SNAPSHOT</version></dependency>②在启动类添加注解,开启feign
@EnableFeignClients(basePackages = {"com.offcn.user.feign"})③修改
oauth的com.offcn.oauth.config.UserDetailsServiceImpl的loadUserByUsername方法,调用UserFeign查询用户信息Result<com.yue.user.pojo.User> result = userFeign.findByUsername(username);String permissions="salesman,accountant,user,admin";UserJwt userDetails = new UserJwt(username,result.getData().getPassword(), AuthorityUtils.commaSeparatedStringToAuthorityList(permissions));
4.简述下购物车的数据结构如何?
用户username (1个) ----> 购物车 (n个) == List<Cart>购物车 (1个): String sellerId;//商家ID (1个) String sellerName;//商家名称 (1个) List<OrderItem> orderItemList;//购物车明细 (1个) === OrderItem (n个)
5.分析下如何实现购物车的业务实现流程?
- 创建
OrderItem的feign接口,在订单的业务层引入;- 实现
public List<Cart> addGoodsToCartList(List<Cart> cartList, Long ItemId, Integer num)1.根据商家ID判断购物车列表中该商家的购物车是否为空, -空:创建购物车对象,并且根据ItemId查询出对应的商品,赋值给购物车对象的OrderItemList; -不为空: --判断购物车里的OrderItem是否为空: ---为空:创建OrderItem对象,添加数据后赋值给购物车对象; ---不为空:取出里面的OrderItem对象,添加数据后返回;
6.购物车中如何获得当前登录的用户信息,即如何实现用户的身份识别?
简单说:当用户通过登录成功时,会将用户的一些数据封装到令牌中,并且将令牌存放到Cookie中,当再次通过网关访问其他受保护的资源时,网关中的
AuthorizeFilter过滤器会获得Cookie中的令牌,并将令牌存放到请求头上,以此通过请求头来实现微服务之间令牌的传递,而购物车所在的微服务通过请求头获得令牌,然后利用公钥进行解密就可以得到用户的信息了。
Day20
1.登录页如何配置? 如何让静态资源可以访问?
- 准备工作
将资料/页面/前端登录相关的静态资源导入到dongyimai-user-oauth中
- 引入
thymeleaf<!--修改dongyimai-user-oauth,引入thymeleaf模板引擎--><!--thymeleaf--><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId></dependency>
- 登录配置
//修改dongyimai-user-oauth,编写一个控制器`com.offcn.oauth.controller.LoginRedirect`,实现登录页跳转@Controller@RequestMapping(value = "/oauth")public class LoginRedirect { /*** * 跳转到登录页面 * @return */ @GetMapping(value = "/login") public String login(){ return "login"; }}
- 登录页配置
//针对静态资源和登录页面,我们需要实现忽略安全配置,并且要指定登录页面。修改`com.offcn.oauth.config.WebSecurityConfig`的2个`configure`方法 第一个方法 /*** * 忽略安全拦截的URL * @param web * @throws Exception */ @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers( "/user/login", "/user/logout", "/oauth/login", "/login", "/login.html", "/favicon.ico", "/css/**", "/data/**", "/fonts/**", "/img/**", "/js/**", "/plugins/**"); }第二个方法 //登录配置 http.formLogin().loginPage("/oauth/login") //自定义登录地址 .loginProcessingUrl("/user/login");//登录处理地址()登录页面静态资源的处理
- 引入thymeleaf命名空间
<!DOCTYPE html><html xmlns:th="http://www.thymeleaf">
修改login.html中资源css js img等的路径
<link rel="stylesheet" type="text/css" href="css/webbase.css" th:href="@{/css/webbase.css}"/><li><img src="img/sina.png" th:src="@{/img/sina.png}" /> <script type="text/javascript" src="js/plugins/jquery/jquery.min.js" th:src="@{/js/plugins/jquery/jquery.min.js}"></script>
2.如何实现登录页的登录后页面的跳转?
- 修改网关的头文件,让用户每次未登录的时候,都跳转到登录页面。并且给登录页传递一个原始资源访问地址
修改dongyimai-gateway-web的`com.offcn.filter.AuthorizeFilter` //用户登陆地址 private static final String USER_LOGIN_URL = "http://localhost:9100/oauth/login"; //如果为空,跳转登陆页面 return needAuthorization(USER_LOGIN_URL,exchange,uri); /** * 响应设置 * @param url * @param exchange * @return */ public Mono<Void> needAuthorization(String url, ServerWebExchange exchange,String from) { ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.SEE_OTHER); response.getHeaders().set("Location",url+"?from="+from); return exchange.getResponse().setComplete(); }
接收资源地址返回登录页
修改dongyimai-user-oauth中 com.offcn.oauth.controlle.LoginRedirect ,在登录方法接收请求参数from
@Controller
@RequestMapping(value = “/oauth”)
public class LoginRedirect {/***
- 跳转到登录页面
- @return
*/
@GetMapping(value = “/login”)
public String login(@RequestParam(value=“from”,required = false,defaultValue = “”)
String from, Model model){
//存储from
model.addAttribute(“from”,from);
return “login”;
}}
- 修改登录表单
<form id="formLogin"class="sui-form" action="/user/login" method="post" > name="username" <input type="hidden" name="from" th:value="${param.from}"> οnclick="document.getElementById('formLogin').onsubmit()"
- 完成登录认证返回资源页
//修改dongyimai-user-oauth中 com.offcn.oauth.controller.AuthController中登录的方法,接收用户访问的资源地址,以及登录账号和密码,登录成功后进行页面跳转,并将生成的临牌以消息头的形式响应给客户端浏览器 //将令牌存储到cookie saveCookie(access_token,from); return new Result(true, StatusCode.OK,"登录成功!:"+from); /*** * 将令牌存储到cookie * @param token */ private void saveCookie(String token, String from){ HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder .getRequestAttributes()).getResponse(); CookieUtil.addCookie(response,cookieDomain,"/","Authorization",token,cookieMaxAge,false); response.setHeader("Refresh","3;URL="+from);//3秒后刷新页面 访问新的地址 from
3.订单结算中如何确定登录用户,查询用户的地址列表?
实现用户收件地址查询
修改dongyimai-user-service微服务,需改com.offcn.user.service.AddressService接口,添加根据用户名字查询用户收件地址信息 /** * 根据用户查询地址 * @param userId * @return */ public List<Address> findListByUserId(String userId );业务层接口实现类
修改dongyimai-user-service微服务,修改com.offcn.user.service.impl.AddressServiceImpl类,添加根据用户查询用户收件地址信息实现方法
/** * 根据用户查询地址 * * @param userId * @return */@Overridepublic List<Address> findListByUserId(String userId) { QueryWrapper<Address> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("user_id",userId); //根据构建的条件查询数据 return this.list(queryWrapper);}控制层
修改dongyimai-user-service微服务,修改com.offcn.user.controller.AddressController,添加根据用户名查询用户收件信息方法
@Autowiredprivate TokenDecode tokenDecode;/**** * 用户收件地址 */@GetMapping(value = "/user/list")public Result<List<Address>> findListByUserId(){ //获取用户登录信息 Map<String, String> userMap = tokenDecode.getUserInfo(); String userId = userMap.get("username"); //查询用户收件地址 List<Address> addressList = addressService.findListByUserId(userId); return new Result(true, StatusCode.OK,"查询成功!",addressList);}修改启动类UserApplication中加入TokenDecode
@Bean public TokenDecode getTokenDecode(){ return new TokenDecode(); }查询用户的地址列表
地址列表其实就是购物车列表,直接查询之前的购物车列表即可
4.如何实现生成订单?
下单的时候,先添加订单往tb_order表中增加数据,再添加订单明细,往tb_order_item表中增加数据。
- 分布式ID生成器
在`com.offcn.OrderApplication`中创建IdWorker@Beanpublic IdWorker idWorker(){ return new IdWorker(1,1);}修改Pojo下的Id修改dongyimai-order-service微服务,修改com.offcn.order.service.impl.OrderServiceImpl
@Autowired private RedisTemplate redisTemplate; @Autowired private IdWorker idWorker; @Autowired private OrderItemMapper orderItemMapper; @Autowired private OrderMapper orderMapper; ………… /** * 增加Order * @param order */ @Override public void add(Order order){ // 得到购物车数据 List<Cart> cartList = (List<Cart>) redisTemplate.boundHashOps("cartList").get(order.getUserId()); for (Cart cart : cartList) { long orderId = idWorker.nextId(); System.out.println("sellerId:" + cart.getSellerId()); System.out.println("orderId:" + orderId); Order tborder = new Order();// 新创建订单对象 tborder.setOrderId(orderId);// 订单ID tborder.setUserId(order.getUserId());// 用户名 tborder.setPaymentType(order.getPaymentType());// 支付类型 tborder.setStatus("1");// 状态:未付款 tborder.setCreateTime(new Date());// 订单创建日期 tborder.setUpdateTime(new Date());// 订单更新日期 tborder.setReceiverAreaName(order.getReceiverAreaName());// 地址 tborder.setReceiverMobile(order.getReceiverMobile());// 手机号 tborder.setReceiver(order.getReceiver());// 收货人 tborder.setSourceType(order.getSourceType());// 订单来源 tborder.setSellerId(cart.getSellerId());// 商家ID // 循环购物车明细 double money = 0; for (OrderItem orderItem : cart.getOrderItemList()) { orderItem.setId(idWorker.nextId()); orderItem.setOrderId(orderId);// 订单ID orderItem.setSellerId(cart.getSellerId()); money += Double.parseDouble(orderItem.getTotalFee());// 金额累加 System.out.println("orderItem.getId():"+orderItem.getId()); //保存订单明细到数据库中 orderItemMapper.insert(orderItem); } tborder.setPayment(money+""); orderMapper.insert(tborder); } redisTemplate.boundHashOps("cartList").delete(order.getUserId()); }
5.如何实现递减库存?
1.添加依赖
工程dongyimai-sellergoods-service中后面需要查询购物车数据,所以需要引入订单的api,在pom.xml中添加如下依赖:<!--order api 依赖--><dependency> <groupId>com.offcn</groupId> <artifactId>dongyimai-order-service-api</artifactId> <version>1.0</version></dependency>2.修改dongyimai-sellergoods–service的application.yml配置
#hystrix 配置hystrix:command: default: execution: isolation: thread: timeoutInMilliseconds: 10000 strategy: SEMAPHORE #使用Seamphore,你创建了多少线程,实际就会有多少线程进行执行,只是可同时执行的线程数量会受到限制3.在启动类中添加MyFeignInterceptor
/*** * 创建拦截器Bean对象 * @return */ @Bean public FeignInterceptor feignInterceptor(){ return new FeignInterceptor(); }4.修改dongyimai-sellergoods-service微服务的
com.offcn.sellergoods.dao.ItemMapper接口,增加库存递减方法/** * 递减库存 * @param orderItem * @return */ @Update("UPDATE tb_item SET num=num-#{num} WHERE id=#{itemId} AND num>=#{num}") int decrCount(OrderItem orderItem);5.修改dongyimai-sellergoods-service微服务的
com.offcn.sellergoods.service.ItemService接口,添加如下方法/*** * 库存递减 * @param username */void decrCount(String username);6.修改dongyimai-sellergoods-service微服务的
com.offcn.sellergoods.service.impl.ItemServiceImpl实现类,添加一个实现方法,代码如下:@Autowired private ItemMapper itemMapper; @Autowired private RedisTemplate redisTemplate;/*** * 库存递减 * @param username */@Overridepublic void decrCount(String username) { //获取购物车数据 List<Cart> cartList = (List<Cart>) redisTemplate.boundHashOps("cartList").get(username); for (Cart cart : cartList) { //循环递减 for (OrderItem orderItem : cart.getOrderItemList()) { //递减库存 int count = itemMapper.decrCount(orderItem); if(count<=0){ throw new RuntimeException("库存不足,递减失败!"); } } }}7.修改dongyimai-sellergoods-service的
com.offcn.sellergoods.controller.ItemController类,添加库存递减方法,代码如下:/*** * 库存递减 * @param username * @return */@PostMapping(value = "/decr/count")public Result decrCount(String username){ //库存递减 itemService.decrCount(username); return new Result(true,StatusCode.OK,"库存递减成功!");}8.创建feign
同时在dongyimai-sellergoods-service-api工程添加
com.offcn.sellergoods.feign.SkuFeign的实现/*** * 库存递减 * @param username * @return */@PostMapping(value = "/decr/count")Result decrCount(@RequestParam(value = "username") String username);9.修改dongyimai-order-service微服务的com.offcn.order.service.impl.OrderServiceImpl类的add方法,增加库存递减的调用。
@Autowiredprivate ItemFeign itemFeign;//减少库存 调用goods 微服务的 feign 减少库存itemFeign.decrCount(order.getUserId());
6.如何实现添加积分?
1.修改dongyimai-user-service微服务的
com.offcn.user.dao.UserMapper接口,增加用户积分方法/*** * 增加用户积分 * @param username * @param points * @return */@Update("UPDATE tb_user SET points=points+#{points} WHERE username=#{username}")int addUserPoints(@Param("username") String username, @Param("points") Integer points);2.修改dongyimai-user-service微服务的
com.offcn.user.service.UserService接口/*** * 添加用户积分 * @param username * @param points * @return */int addUserPoints(String username,Integer points);3.修改dongyimai-user-service微服务的
com.offcn.user.service.impl.UserServiceImpl,增加添加积分方法实现@Autowiredprivate UserMapper userMapper;/*** * 添加用户积分 * @param username * @param points * @return */@Overridepublic int addUserPoints(String username, Integer points) { return userMapper.addUserPoints(username,points);}4.修改dongyimai-user-service微服务的
com.offcn.user.controller.UserController,添加增加用户积分方法@Autowiredprivate TokenDecode tokenDecode;/*** * 增加用户积分 * @param points:要添加的积分 */@GetMapping(value = "/points/add")public Result addPoints(Integer points){ //获取用户名 Map<String, String> userMap = tokenDecode.getUserInfo(); String username = userMap.get("username"); //添加积分 userService.addUserPoints(username,points); return new Result(true,StatusCode.OK,"添加积分成功!");}5.修改dongyimai-user-service-api工程,修改
com.offcn.user.feign.UserFeign,添加增加用户积分方法/*** * 添加用户积分 * @param points * @return */@GetMapping(value = "/points/add")Result addPoints(@RequestParam(value = "points")Integer points);6.修改dongyimai-order-service,添加dongyimai-user-service-api的依赖
<!--user api 依赖--><dependency> <groupId>com.offcn</groupId> <artifactId>dongyimai-user-service-api</artifactId> <version>1.0</version></dependency>7.在增加订单的时候,同时添加用户积分,修改dongyimai-order-service微服务的
com.offcn.order.service.impl.OrderServiceImpl下单方法,增加调用添加积分方法//增加积分,调用用户微服务的userFeign 增加积分userFeign.addPoints(10);8.修改dongyimai-order-service的启动类
com.offcn.OrderApplication,添加feign的包路径
Day21
1.什么是二维码?二维码有哪些优势? 二维码的容错级别有几种?如何生成二维码?
- 二维码:
二维码又称QR Code,QR全称Quick Response,是一个近几年来移动设备上超流行的一种编码方式,它比传统的Bar Code条形码能存更多的信息,也能表示更多的数据类型。- 优势:
信息容量大, 可以容纳多达1850个大写字母或2710个数字或500多个汉字
应用范围广, 支持文字,声音,图片,指纹等等…
容错能力强, 即使图片出现部分破损也能使用
成本低, 容易制作- 容错级别:
L级(低) 7%的码字可以被恢复。
M级(中) 的码字的15%可以被恢复。
Q级(四分)的码字的25%可以被恢复。
H级(高) 的码字的30%可以被恢复。- 如何生成:
qrious是一款基于HTML5 Canvas的纯JS二维码生成插件。通过qrious.js可以快速生成各种二维码,你可以控制二维码的尺寸颜色,还可以将生成的二维码进行Base64编码。使用html页面即可生成二维码
2.二维码的容错级别与二维码保存的信息之间的关系?
- L级(低) 7%的码字可以被恢复。
- M级(中) 的码字的15%可以被恢复。
- Q级(四分)的码字的25%可以被恢复。
- H级(高) 的码字的30%可以被恢复。
3.如果开通使用支付宝进行支付,说说使用的步骤?
- 进入支付宝官网,选择我是开发者,使用支付宝扫码登陆,选择自研开发者
- 登陆开发者界面,进入研发中心,进入沙箱环境
- 下载秘钥生成工具,生成公钥
查看RSA2秘钥,选择更换应用公钥,复制刚生成的应用公钥并保存
4.如何生成支付的页面?
- 实现思路:
商户系统通过AlipayClient调用支付宝预下单接口alipay.trade.precreate,获得该订单二维码图片地址。构建参数发送给预下单接口 ,返回的信息中有支付url,根据url生成二维码,显示的订单号和金额也在返回的信息中。- 创建支付微服务
- 配置文件中配置支付宝支付信息(支付宝网关、appId、用户私钥、格式、字符编码、阿里公钥、signType)
- 在配置类中使用这些参数创建一个支付客户端,注入spring容器
5. 如何预下单?
- 创建预下单请求对象AlipayTradePrecreateRequest
- 将方法中传入的参数total_fee金额转换下单金额按照元
- 调用request的setBizContent方法设置业务参数
- 调用阿里支付客户端的execute方法,传入参数request,得到AlipayTradePrecreateResponse对象
- 判断response中的code是否等于10000,是的话往map中封装参数qrcode、out_trade_no、total_fee,不等于的话打印调用预下单接口失败
- 最后将封装了参数的map返回
6.如何查看交易的状态?
- 实现思路
我们通过AlipayClient实现对交易查询接口(alipay.trade.query)的调用。- 业务流程
- 编写查询支付状态接口
- 创建一个map用来封装参数
- 创建AlipayTradeQueryRequest对象
- 调用request的setBizContent方法设置业务参数(out_trade_no、trade_no)
- 调用阿里支付客户端的execute方法,传入参数request,得到AlipayTradeQueryResponse对象
- 判断respose中的code是否为10000,是的话向map中封装参数out_trade_no,tradestatus,trade_no,后两个参数从respose中获取,第一个参数由方法传入
- 控制层中死循环
- 调用查询支付状态接口方法,得到map
- 如果map为空,返回支付出错,结束本次循环
- 如果map中的交易状态为TRADE_SUCCESS,返回交易成功,结束循环
- 如果map中的交易状态为TRADE_CLOSED,返回交易关闭,结束循环
- 如果map中的交易状态为TRADE_FINISHED,返回交易结束,结束循环
- 如果不符合这些条件的话,也就是用户一直未支付,就一直循环
- 每次开始下次循环前线程休眠三秒
- 这样的话有一个问题,如果用户到了二维码页面一直未支付,或是关掉了支付页面,我们的代码会一直循环调用支付宝接口,这样会对程序造成很大的压力。所以我们要加一个时间限制或是循环次数限制,当超过时间或次数时,跳出循环。在循环开始前定义一个变量x为0,每次循环结束x自增,当x达到规定次数时,返回二维码超时,结束循环。
7.说说下单成功后日志生成的业务流程?
- 实现思路:
- 在用户下订单时,判断如果为支付宝支付,就向支付日志表添加一条记录,信息包括支付总金额、订单ID(多个)、用户ID 、下单时间等信息,支付状态为0(未支付)
- 生成的支付日志对象放入redis中,以用户ID作为key,这样在生成支付二维码时就可以从redis中提取支付日志对象中的金额和订单号。
- 业务实现:
- 拷贝逆向工程中的相关日志的pojo、Feign到dongyimai-order-service-api,拷贝 dao、service、controller到dongyimai-order-service中。
- 修改订单接口实现类的add添加订单方法
- 注入PayLogMapper
- 创建一个orderIdList存放订单id,定义总金额为0
- 在每次循环遍历cartList中,向orderIdList中添加orderId,并且给总金额累加money
- cartList循环遍历结束后,判断order中的支付类型是否为1,如果是,证明是支付宝支付,创建PayLog对象,并设置参数,参数设置完后调用payLogMapper的insert方法,传入payLog对象,往数据库添加支付日志,并放入缓存,userId对应paylog
- 注意:将paylog实体类的支付订单号主键类型改为INPUT
8.说说支付成功修改日志的业务流程?
- 在订单接口中创建修改订单状态的方法,并在实现类编写具体业务
- 调用payLogMapper的selectbyId方法,传入out_trade_no,得到payLog对象
- 设置payLog对象的支付时间为现在,设置交易状态为1,设置交易号,调用updateById方法,传入payLog对象
- 清除redis缓存的paylog
- 在orderFeign中提供修改订单状态方法的接口
- 修改orderController中的queryPayStatus方法,支付成功后调用orderFeign的updateOrderStatus方法,传入map中的out_trade_no和trade_no
Day22
1.你做过秒杀吗? 请你简单介绍下秒杀的业务是如何实现的?
秒杀商品通常有两种限制:库存限制、时间限制
秒杀的需求:
- 录入秒杀商品数据,主要包括:商品标题、原价、秒杀价、商品图片、介绍、秒杀时段等信息
- 秒杀频道首页列出秒杀商品(进行中的)点击秒杀商品图片跳转到秒杀商品详细页。
- 商品详细页显示秒杀商品信息,点击立即抢购实现秒杀下单,下单时扣减库存。当库存为0或不在活动期范围内时无法秒杀。
- 秒杀下单成功,直接跳转到支付页面(扫码),支付成功,跳转到成功页,填写收货地址、电话、收件人等信息,完成订单。
- 当用户秒杀下单5分钟内未支付,取消预订单,调用支付的关闭订单接口,恢复库存。
秒杀技术实现核心思想是运用缓存减少数据库瞬间的访问压力!读取商品详细信息时运用缓存,当用户点击抢购时减少缓存中的库存数量,当库存数为0时或活动期结束时,同步到数据库。 产生的秒杀预订单也不会立刻写到数据库中,而是先写到缓存,当用户付款成功后再写入数据库。
2.秒杀列表中中时间列表 和商品列表是如何实现?
秒杀时间菜单,这个菜单是从后台获取的。可以先求出当前时间的凌晨,然后每2个小时后作为下一个抢购的开始时间,这样可以分出12个抢购时间段,而现实的菜单只需要计算出当前时间在哪个时间段范围,该时间段范围就属于正在秒杀的时间段,而后面即将开始的秒杀时间段的计算也就出来了,可以在当前时间段基础之上+2小时、+4小时、+6小时、+8小时。
关于时间菜单的运算,在给出的DateUtil包里已经实现,代码如下:/*** * 获取时间菜单 在当前时间的基础上的 +2小时、+4小时、+6小时、+8小时 共5个时间段 * @return */public static List<Date> getDateMenus(){ //定义一个List<Date>集合,存储所有时间段 List<Date> dates = getDates(12); //判断当前时间属于哪个时间范围 Date now = new Date(); for (Date cdate : dates) { //开始时间<=当前时间<开始时间+2小时 if(cdate.getTime()<=now.getTime() && now.getTime()<addDateHour(cdate,2).getTime()){ now = cdate; break; } } //当前需要显示的时间菜单 List<Date> dateMenus = new ArrayList<Date>(); for (int i = 0; i <5 ; i++) { dateMenus.add(addDateHour(now,i*2)); } return dateMenus;}/*** * 指定时间往后N个时间间隔 * @param hours * @return */public static List<Date> getDates(int hours) { List<Date> dates = new ArrayList<Date>(); //循环12次 Date date = toDayStartHour(new Date()); //凌晨 for (int i = 0; i <hours ; i++) { //每次递增2小时,将每次递增的时间存入到List<Date>集合中 dates.add(addDateHour(date,i*2)); } return dates;}/** * 时间转换为yyyy-MM-dd HH:mm:ss格式 * @param */ public static String date2StrFull(Date date){ SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return simpleDateFormat.format(date); }查询秒杀商品导入Reids我们可以写个定时任务,查询从当前时间开始,往后延续4个时间菜单间隔,也就是一共只查询5个时间段抢购商品数据,并压入缓存,实现代码如下:
修改SeckillGoodsPushTask的loadGoodsPushRedis方法,代码如下:/**** * 每30秒执行一次 */ @Scheduled(cron = "0/30 * * * * ?") public void loadGoodsPushRedis(){ //获得时间段集合 List<Date> dateMenus = DateUtil.getDateMenus(); //循环时间段集合 for (Date startTime : dateMenus) { //提取开始时间,转换为年月日时格式的字符串 String extName = DateUtil.date2Str(startTime); //创建查询条件对象 QueryWrapper<SeckillGoods> queryWrapper = new QueryWrapper<>(); //设置查询条件 1)商品必须审核通过 status=1 queryWrapper.eq("status","1"); //2)库存大于0 queryWrapper.gt("stock_count",0); //3)开始时间<=活动开始时间(数据库) queryWrapper.ge("start_time",DateUtil.date2StrFull(startTime)); //4)活动结束时间<开始时间+2小时 queryWrapper.lt("end_time",DateUtil.date2StrFull(DateUtil.addDateHour(startTime,2))); //5)读取redis中存在的当天的秒杀商品 Set keys = redisTemplate.boundHashOps("SeckillGoods_"+extName).keys(); //判断keys不为空,就设置排除条件 if(keys!=null&&keys.size()>0){ queryWrapper.notIn("id",keys); } //查询符合条件的数据库 List<SeckillGoods> seckillGoodsList = seckillGoodsMapper.selectList(queryWrapper); //遍历查询到数据集合,存储数据到redis for (SeckillGoods seckillGoods : seckillGoodsList) { redisTemplate.boundHashOps("SeckillGoods_"+extName).put(seckillGoods.getId(),seckillGoods); //设置超时时间2小时 redisTemplate.expireAt("SeckillGoods_"+extName,DateUtil.addDateHour(startTime,2)); } } }秒杀频道首页,显示正在秒杀的和未开始秒杀的商品(已经开始或者还没开始,未结束的秒杀商品)
3.在springBoot中如何实现定时任务?
- 在定时任务类的指定方法上加上@Scheduled开启定时任务
- 定时任务表达式:使用cron属性来配置定时任务执行时间
4.简单介绍下cron时间表达式的组成部分,都代表什么意思? 特殊符号有什么意义?请举例说明
序号 说明 是否必填 允许填写的值 允许的通配符 1 秒 是 0-59 , - * / 2 分 是 0-59 , - * / 3 小时 是 0-23 , - * / 4 日 是 1-31 , - * ? / L W 5 月 是 1-12或JAN-DEC , - * / 6 周 是 1-7或SUN-SAT , - * ? / L W 7 年 否 empty 或1970-2099 , - * / 通配符说明:
* 表示所有值. 例如:在分的字段上设置 "*",表示每一分钟都会触发。? 表示不指定值。使用的场景为不需要关心当前设置这个字段的值。例如:要在每月的10号触发一个操作,但不关心是周几,所以需要周位置的那个字段设置为"?" 具体设置为 0 0 0 10 * ?- 表示区间。例如 在小时上设置 "10-12",表示 10,11,12点都会触发。, 表示指定多个值,例如在周字段上设置 "MON,WED,FRI" 表示周一,周三和周五触发 12,14,19/ 用于递增触发。如在秒上面设置"5/15" 表示从5秒开始,每增15秒触发(5,20,35,50)。 在月字段上设置'1/3'所示每月1号开始,每隔三天触发一次。L 表示最后的意思。在日字段设置上,表示当月的最后一天(依据当前月份,如果是二月还会依据是否是润年[leap]), 在周字段上表示星期六,相当于"7"或"SAT"。如果在"L"前加上数字,则表示该数据的最后一个。例如在周字段上设置"6L"这样的格式,则表示“本月最后一个星期五"W 表示离指定日期的最近那个工作日(周一至周五). 例如在日字段上设置"15W",表示离每月15号最近的那个工作日触发。如果15号正好是周六,则找最近的周五(14号)触发, 如果15号是周未,则找最近的下周一(16号)触发.如果15号正好在工作日(周一至周五),则就在该天触发。如果指定格式为 "1W",它则表示每月1号往后最近的工作日触发。如果1号正是周六,则将在3号下周一触发。(注,"W"前只能设置具体的数字,不允许区间"-").# 序号(表示每月的第几个周几),例如在周字段上设置"6#3"表示在每月的第三个周六.注意如果指定"#5",正好第五周没有周六,则不会触发该配置(用在母亲节和父亲节再合适不过了) ;常用表达式:
0 0 10,14,16 * * ? 每天上午10点,下午2点,4点 0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时 0 0 12 ? * WED 表示每个星期三中午12点 "0 0 12 * * ?" 每天中午12点触发 "0 15 10 ? * *" 每天上午10:15触发 "0 15 10 * * ?" 每天上午10:15触发 "0 15 10 * * ? *" 每天上午10:15触发 "0 15 10 * * ? 2005" 2005年的每天上午10:15触发 "0 * 14 * * ?" 在每天下午2点到下午2:59期间的每1分钟触发 "0 0/5 14 * * ?" 在每天下午2点到下午2:55期间的每5分钟触发 "0 0/5 14,18 * * ?" 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发 "0 0-5 14 * * ?" 在每天下午2点到下午2:05期间的每1分钟触发 "0 10,44 14 ? 3 WED" 每年三月的星期三的下午2:10和2:44触发 "0 15 10 ? * MON-FRI" 周一至周五的上午10:15触发 "0 15 10 15 * ?" 每月15日上午10:15触发 "0 15 10 L * ?" 每月最后一日的上午10:15触发 "0 15 10 ? * 6L" 每月的最后一个星期五上午10:15触发 "0 15 10 ? * 6L 2002-2005" 2002年至2005年的每月的最后一个星期五上午10:15触发 "0 15 10 ? * 6#3" 每月的第三个星期五上午10:15触发
5.如何实现多线程抢单?请说说其中的业务流程?如何异步实现?
下订单这里,我们一般采用多线程下单,但多线程中我们又需要保证用户抢单的公平性,也就是先抢先下单。我们可以这样实现,用户进入秒杀抢单,如果用户符合抢单资格,只需要记录用户抢单数据,存入队列,多线程从队列中进行消费即可,存入队列采用左压,多线程下单采用右取的方式。
异步实现:
要想使用Spring的异步操作,需要先开启异步操作,在启动类SeckillApplication用@EnableAsync注解开启,然后在对应的异步方法上添加注解@Async即可,代码如下:@Componentpublic class MultiThreadingCreateOrder { /*** * 多线程下单操作 */ @Async public void createOrder(){ try { System.out.println("准备执行...."); Thread.sleep(20000); System.out.println("开始执行...."); } catch (InterruptedException e) { e.printStackTrace(); } }}上面createOrder方法进行了休眠阻塞操作,我们在下单的方法调用createOrder方法,如果下单的方法没有阻塞,继续执行,说明属于异步操作,如果阻塞了,说明没有执行异步操作。
6.如何查询订单的状态?
用户下单异步操作,并不能确定下单是否成功,所以我们需要做一个页面判断,每过1秒钟查询一次下单状态,多线程下单的时候,需要修改抢单状态,支付的时候,清理抢单状态。
用户每次点击抢购的时候,如果排队成功,则将用户抢购状态存储到Redis中,多线程抢单的时候,如果抢单成功,则更新抢单状态。
Day23
1.如何防止秒杀重复排队?
用户每次抢单的时候,一旦排队,我们设置一个自增值,让该值的初始值为1,每次进入抢单的时候,对它进行递增,如果值>1,则表明已经排队,不允许重复排队,如果重复排队,则对外抛出异常,并抛出异常信息100表示已经正在排队
- 对于后台排队记录,修改SeckilOrderServiceImpl的add方法,新增递增值判断是否排队中
//递增,判断是否排队Long userQueueCount = redisTemplate.boundHashOps("UserQueueCount").increment(username, 1);if(userQueueCount>1){ //100:表示有重复抢单 throw new RuntimeException(String.valueOf(StatusCode.REPERROR));}
2.如何避免秒杀中并发超卖问题?
解决超卖问题,可以利用Redis队列实现,给每件商品创建一个独立的商品个数队列,例如:A商品有2个,A商品的ID为1001,则可以创建一个队列,key=SeckillGoodsCountList_1001,往该队列中塞2次该商品ID。
每次给用户下单的时候,先从队列中取数据,如果能取到数据,则表明有库存,如果取不到,则表明没有库存,这样就可以防止超卖问题产生了。
在我们对Redis进行操作的时候,很多时候,都是先将数据查询出来,在内存中修改,然后存入到Redis,在并发场景,会出现数据错乱问题,为了控制数量准确,我们单独将商品数量整一个自增键,自增键是线程安全的,所以不担心并发场景的问题
对于代码的实现部分,先修改SeckillGoodsPushTask,添加一个pushIds方法,用于将指定商品ID放入到指定的数组中,代码如下:
/*** * 将商品ID存入到数组中 * @param len:长度 * @param id :值 * @return */public Long[] pushIds(int len,Long id){ Long[] ids = new Long[len]; for (int i = 0; i <ids.length ; i++) { ids[i]=id; } return ids;}修改SeckillGoodsPushTask的loadGoodsPushRedis方法,添加队列操作,代码如下:
//商品数据队列存储,防止高并发超卖Long[] ids = pushIds(seckillGoods.getStockCount(), seckillGoods.getId());redisTemplate.boundListOps("SeckillGoodsCountList_"+seckillGoods.getId()).leftPushAll(ids);//自增计数器redisTemplate.boundHashOps("SeckillGoodsCount").increment(seckillGoods.getId(), seckillGoods.getStockCount());超卖控制:修改多线程下单方法,分别修改数量控制,以及售罄后用户抢单排队信息的清理,修改代码如下图:
/*** * 多线程下单操作 */@Asyncpublic void createOrder(){ //从队列中获取排队信息 SeckillStatus seckillStatus = (SeckillStatus)redisTemplate.boundListOps("SeckillOrderQueue").rightPop(); try { //从队列中获取一个商品 Object sgood = redisTemplate.boundListOps("SeckillGoodsCountList_" + seckillStatus.getGoodsId()).rightPop(); if(sgood==null){ //清理当前用户的排队信息 clearQueue(seckillStatus); return; } //时间区间 String time=seckillStatus.getTime(); //用户登录名 String username=seckillStatus.getUsername(); //用户抢购商品 Long id=seckillStatus.getGoodsId(); //获取商品数据 SeckillGoods goods = (SeckillGoods) redisTemplate.boundHashOps("SeckillGoods_" + time).get(id); //如果有库存,则创建秒杀商品订单 SeckillOrder seckillOrder = new SeckillOrder(); seckillOrder.setId(idWorker.nextId()); seckillOrder.setSeckillId(id); seckillOrder.setMoney(goods.getCostPrice()); seckillOrder.setUserId(username); seckillOrder.setCreateTime(new Date()); seckillOrder.setStatus("0"); //将秒杀订单存入到Redis中 redisTemplate.boundHashOps("SeckillOrder").put(username,seckillOrder); //商品库存-1 Long surplusCount = redisTemplate.boundHashOps("SeckillGoodsCount").increment(id, -1);//商品数量递减 goods.setStockCount(surplusCount.intValue()); //根据计数器统计 //判断当前商品是否还有库存 if(surplusCount<=0){ //并且将商品数据同步到MySQL中 seckillGoodsMapper.updateById(goods); //如果没有库存,则清空Redis缓存中该商品 redisTemplate.boundHashOps("SeckillGoods_" + time).delete(id); }else{ //如果有库存,则直数据重置到Reids中 redisTemplate.boundHashOps("SeckillGoods_" + time).put(id,goods); } //抢单成功,更新抢单状态,排队->等待支付 seckillStatus.setStatus(2); seckillStatus.setOrderId(seckillOrder.getId()); seckillStatus.setMoney(Float.parseFloat(seckillOrder.getMoney())); redisTemplate.boundHashOps("UserQueueStatus").put(username,seckillStatus); } catch (Exception e) { e.printStackTrace(); }}/*** * 清理用户排队信息 * @param seckillStatus */public void clearQueue(SeckillStatus seckillStatus){ //清理排队标示 redisTemplate.boundHashOps("UserQueueCount").delete(seckillStatus.getUsername()); //清理抢单标示 redisTemplate.boundHashOps("UserQueueStatus").delete(seckillStatus.getUsername());}
3.简述下秒杀订单支付的业务流程?
- 用户抢单,经过秒杀系统实现抢单,下单后会将向MQ发送一个延时队列消息,包含抢单信息,延时半小时后才能监听到
- 秒杀系统同时启用延时消息监听,一旦监听到订单抢单信息,判断Redis缓存中是否存在订单信息,如果存在,则回滚
- 秒杀系统还启动支付回调信息监听,如果支付完成,则将订单持久化到MySQL,如果没完成,清理排队信息回滚库存
- 每次秒杀下单后调用支付系统,创建二维码,如果用户支付成功了,支付宝平台会将支付信息发送给支付系统指定的回调地址,支付系统收到信息后,将信息发送给MQ,第3个步骤就可以监听到消息了。
4.如何实现支付宝支付状态的监听?
支付回调这一块代码已经实现了,但之前实现的是订单信息的回调数据发送给MQ,指定了对应的队列,不过现在需要实现的是秒杀信息发送给指定队列,所以之前的代码那块需要动态指定队列。
支付回调队列指定:
1.创建支付二维码需要指定队列
2.回调地址回调的时候,获取支付二维码指定的队列,将支付信息发送到指定队列中并且在支付宝统一下单的API中,有一个附加参数,我们可以在创建二维码的时候,指定该参数,该参数用于指定回调支付信息的对应队列,每次回调的时候,会获取该参数,然后将回调信息发送到该参数对应的队列去。
body:附加数据,String(127),在查询API和支付通知中原样返回,可作为自定义参数使用。
在预下单时传递队列信息,修改支付微服务的SeckillPayServiceImpl的createNative方法,创建二维码的时候,需要将下面几个参数传递过去:
username:用户名,可以根据用户名查询用户排队信息
out_trade_no:商户订单号,下单必须
total_fee:支付金额,支付必须
queue:队列名字,回调的时候,可以知道将支付信息发送到哪个队列
routingkey:路由的名称
exchange:交换机名称修改主启动类,eckillPayApplication,添加对应队列以及对应交换机绑定
5.支付失败的时候如何实现删除订单和回滚数据?
修改SeckillOrderService,创建一个关闭订单方法,代码如下:
/*** * 关闭订单,回滚库存 */void closeOrder(String username);修改SeckillOrderServiceImpl,创建一个关闭订单实现方法,代码如下:
/*** * 关闭订单,回滚库存 * @param username */@Overridepublic void closeOrder(String username) { //将消息转换成SeckillStatus SeckillStatus seckillStatus = (SeckillStatus) redisTemplate.boundHashOps("UserQueueStatus").get(username); //获取Redis中订单信息 SeckillOrder seckillOrder = (SeckillOrder) redisTemplate.boundHashOps("SeckillOrder").get(username); //如果Redis中有订单信息,说明用户未支付 if(seckillStatus!=null && seckillOrder!=null){ //删除订单 redisTemplate.boundHashOps("SeckillOrder").delete(username); //回滚库存 //1)从Redis中获取该商品 SeckillGoods seckillGoods = (SeckillGoods) redisTemplate.boundHashOps("SeckillGoods_"+seckillStatus.getTime()).get(seckillStatus.getGoodsId()); //2)如果Redis中没有,则从数据库中加载 if(seckillGoods==null){ seckillGoods = seckillGoodsMapper.selectById(seckillStatus.getGoodsId()); } //3)数量+1 (递增数量+1,队列数量+1) Long surplusCount = redisTemplate.boundHashOps("SeckillGoodsCount").increment(seckillStatus.getGoodsId(), 1); seckillGoods.setStockCount(surplusCount.intValue()); redisTemplate.boundListOps("SeckillGoodsCountList_" + seckillStatus.getGoodsId()).leftPush(seckillStatus.getGoodsId()); //4)数据同步到Redis中 redisTemplate.boundHashOps("SeckillGoods_"+seckillStatus.getTime()).put(seckillStatus.getGoodsId(),seckillGoods); //清理排队标示 redisTemplate.boundHashOps("UserQueueCount").delete(seckillStatus.getUsername()); //清理抢单标示 redisTemplate.boundHashOps("UserQueueStatus").delete(seckillStatus.getUsername()); }}调用删除订单,修改SeckillOrderPayMessageListener,在用户支付失败后调用关闭订单方法,代码如下:
//支付失败,删除订单seckillOrderService.closeOrder(resultMap.get("username"));消息监听器中完成代码,做了微调:
@Component@RabbitListener(queues = "${mq.pay.queue.seckillorder}")public class SeckillOrderPayMessageListener { @Autowired private SeckillOrderService seckillOrderService; /** * 监听消费消息 * @param message */ @RabbitHandler public void consumeMessage(@Payload String message){ System.out.println(message); //将消息转换成Map对象 Map<String,String> resultMap = JSON.parseObject(message, Map.class); System.out.println("监听到的消息:"+resultMap); //获取交易状态 String trade_status = resultMap.get("trade_status"); String body= resultMap.get("body"); Map<String, String> bodyMap = new HashMap<>(); if(resultMap.get("body")!=null) { String[] splits = body.split("&"); for (String split : splits) { String[] vs = split.split("="); bodyMap.put(vs[0], vs[1]); } } System.out.println("bodyMap:"+bodyMap.get("username")); //判断交易状态 if(trade_status!=null&& trade_status.equalsIgnoreCase("TRADE_SUCCESS")){ seckillOrderService.updatePayStatus(resultMap.get("out_trade_no"),resultMap.get("trade_no"),bodyMap.get("username")); }else if(trade_status!=null&& trade_status.equalsIgnoreCase("WAIT_BUYER_PAY")){ System.out.println("正在等待用户支付。。。。。。"); }else if(trade_status!=null&& trade_status.equalsIgnoreCase("TRADE_FINISHED")) { System.out.println("支付交易完成。。。。。代表支付完成。"); seckillOrderService.updatePayStatus(resultMap.get("out_trade_no"), resultMap.get("trade_no"), bodyMap.get("username")); }else if(trade_status!=null&& trade_status.equalsIgnoreCase("TRADE_CLOSED")) { System.out.println("支付交易关闭。。。。。代表支付失败。"); //没有支付成功, 删除订单。 seckillOrderService.closeOrder(bodyMap.get("username")); } }}
6.如何实现RabbitMQ演示消息队列,比如支付关闭的时候如何实现订单的回滚库存?
配置延时队列:
–在dongyimai-seckill-service中修改application.yml文件中引入队列信息配置,如下:#位置支付交换机和队列mq:pay: exchange: order: exchange.order seckillorder: exchange.seckillorder queue: order: queue.order seckillorder: queue.seckillorder seckillordertimer: queue.seckillordertimer seckillordertimerdelay: queue.seckillordertimerdelay routing: orderkey: queue.order seckillorderkey: queue.seckillorder seckillordertimerkey: queue.seckillordertimer配置队列与交换机,在SeckillApplication中添加如下方法:
/** * 到期数据队列 * @return */ @Bean public Queue seckillOrderTimerQueue() { return new Queue(env.getProperty("mq.pay.queue.seckillordertimer"), true); } /** * 超时数据队列 * @return */ @Bean public Queue delaySeckillOrderTimerQueue() { return QueueBuilder.durable(env.getProperty("mq.pay.queue.seckillordertimerdelay")) .withArgument("x-dead-letter-exchange", env.getProperty("mq.pay.exchange.order")) // 消息超时进入死信队列,绑定死信队列交换机 .withArgument("x-dead-letter-routing-key", env.getProperty("mq.pay.routing.seckillordertimerkey")) // 绑定指定的routing-key .build(); } /*** * 交换机与队列绑定l * @param * @param directExchange * @return */ @Bean public Binding basicBinding(Queue seckillOrderTimerQueue, DirectExchange directExchange) { return BindingBuilder.bind(seckillOrderTimerQueue) .to(directExchange) .with(env.getProperty("mq.pay.routing.seckillordertimerkey")); }发送延时信息:
– 修改MultiThreadingCreateOrder,添加如下方法:
/*** * 发送延时消息到RabbitMQ中 * @param seckillStatus */public void sendTimerMessage(SeckillStatus seckillStatus){ rabbitTemplate.convertAndSend(env.getProperty("mq.pay.queue.seckillordertimerdelay"), (Object) JSON.toJSONString(seckillStatus), new MessagePostProcessor() { @Override public Message postProcessMessage(Message message) throws AmqpException { message.getMessageProperties().setExpiration("10000"); return message; } });}–在createOrder方法中调用上面方法,如下代码:
//发送延时消息到MQ中sendTimerMessage(seckillStatus); //用于测试创建项目模块dongyimai-seckillpay-service-api,提供Feign调用支付服务关闭预下单接口,并且注意修改配置文件application.yml设置feign调用超时时间
库存回滚,创建SeckillOrderDelayMessageListener实现监听消息,并回滚库存,代码如下:
package com.offcn.seckill.consumer;import com.alibaba.fastjson.JSON;import com.offcn.entity.Result;import com.offcn.seckill.entity.SeckillStatus;import com.offcn.seckillpay.feign.PayFeign;import com.offcn.seckill.pojo.SeckillOrder;import com.offcn.seckill.service.SeckillOrderService;import org.springframework.amqp.rabbit.annotation.RabbitHandler;import org.springframework.amqp.rabbit.annotation.RabbitListener;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.messaging.handler.annotation.Payload;import org.springframework.stereotype.Component;import java.util.Map;@Component@RabbitListener(queues = "${mq.pay.queue.seckillordertimer}")public class SeckillOrderDelayMessageListener { @Autowired private RedisTemplate redisTemplate; @Autowired private SeckillOrderService seckillOrderService; @Autowired private PayFeign payFeign; /*** * 读取消息 * 判断Redis中是否存在对应的订单 * 如果存在,则关闭支付,再关闭订单 * @param message */ @RabbitHandler public void consumeMessage(@Payload String message){ //读取消息 SeckillStatus seckillStatus = JSON.parseObject(message,SeckillStatus.class); //获取Redis中订单信息 String username = seckillStatus.getUsername(); SeckillOrder seckillOrder = (SeckillOrder) redisTemplate.boundHashOps("SeckillOrder").get(username); //如果Redis中有订单信息,说明用户未支付 if(seckillOrder!=null){ System.out.println("准备回滚---"+seckillStatus); //关闭支付 Result closeResult = payFeign.closePay(seckillStatus.getOrderId()); Map<String,String> closeMap = (Map<String, String>) closeResult.getData(); if(closeMap!=null && closeMap.get("code").equalsIgnoreCase("10000")){ //关闭订单 seckillOrderService.closeOrder(username); } } }}
Day24
1.什么是事务? 事务有哪些特性? 事务的隔离级别有哪些?
数据库事务:指数据库执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成
事务拥有以下四个特性:
原子性(Atomicity)
原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚。
一致性(Consistency)
一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。
拿转账来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。
隔离性(Isolation)
隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
即要达到这么一种效果:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。
持久性(Durability)
持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。
事务的隔离级别有:Read Uncommitted (读未提交内容),Read Committed (读已提交内容),Repeatable read (可重读),Serializable (可串行化)。
2.什么是本地事务? 数据库如何实现原子性和持久性?
倘若将一个单一的服务操作作为一个事务,那么整个服务操作只能涉及一个单一的数据库资源,这类基于单个服务单一数据库资源访问的事务,被称为本地事务(Local Transaction)
原子性和持久性都要通过undo和redo日志来实现
3.简单介绍下undo日志和redo日志?
(1)Redo日志
redo日志是为了保障数据库的持久性的,是一种物理日志。事务要对数据库中的数据进行修改,会先将数据从磁盘读取到内存(Buffer Pool)中,此时修改后的结果在内存中没有持久化之前,数据库节点突然下电会使内存中的数据全部丢失,那么这个已经提交了的事务对数据库中所做的更改也就跟着丢失了,这就违背了持久性。
解决方案就是在事务提交时,将事务对数据库做的更改记录到redo日志中,并将redo日志持久化到磁盘,这样即使之后系统崩溃了,重启之后只要按照上述内容所记录的步骤重新更新一下数据页,那么该事务对数据库中所做的修改又可以被恢复出来,也就意味着满足持久性的要求。
(2)Undo日志
undo日志是为了保障数据库的原子性和一致性,是一种逻辑日志。undo log主要记录的是数据的逻辑变化,为了在发生错误时回滚之前的操作,需要将之前的操作都记录下来,然后在发生错误时才可以回滚。
4.什么是分布式事务?
分布式事务指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上,且属于不同的应用,分布式事务需要保证这些操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。
5.CAP定理是什么?能否共存?注册中心分为几类? 代表是什么?
CAP 定理,又被叫作布鲁尔定理。
**C (一致性):**对某个指定的客户端来说,读操作能返回最新的写操作。
对于数据分布在不同节点上的数据来说,如果在某个节点更新了数据,那么在其他节点如果都能读取到这个最新的数据,那么就称为强一致,如果有某个节点没有读取到,那就是分布式不一致。
**A (可用性):**非故障的节点在合理的时间内返回合理的响应(不是错误和超时的响应)。可用性的两个关键一个是合理的时间,一个是合理的响应。
合理的时间指的是请求不能无限被阻塞,应该在合理的时间给出返回。合理的响应指的是系统应该明确返回结果并且结果是正确的,这里的正确指的是比如应该返回 50,而不是返回 40。
**P (分区容错性):**当出现网络分区后,系统能够继续工作。打个比方,这里集群有多台机器,有台机器网络出现了问题,但是这个集群仍然可以正常工作。
三者不能共存。
注册中心主要分为两类,一类是CP类注册中心,另一类是AP类注册中心。
CP类注册中心更强调一致性,1比如 eureka ;而AP类注册中心更强调可用性,比如zookeeper 。
6.简述下常见的分布式事务解决方案有哪些?如何解决分布式事务的?
解决方案:
1.XA两段提交(低效率)-如: XA JTA分布式事务解决方案
(1)投票阶段(voting phase):协调者将通知事务参与者准备提交或取消事务,然后进入表决过程。参与者将告知协调者自己的决策:同意(事务参与者本地事务执行成功,但未提交)或取消(本地事务执行故障);
(2)提交阶段(commit phase):收到参与者的通知后,协调者再向参与者发出通知,根据反馈情况决定各参与者是否要提交还是回滚;2.TCC三段提交(2段,高效率[不推荐(补偿代码)])
其核心在于将业务分为两个操作步骤完成。不依赖 RM 对分布式事务的支持,而是通过对业务逻辑的分解来实现分布式事务。
例如: A要向 B 转账,思路大概是:
我们有一个本地方法,里面依次调用
1、首先在 Try 阶段,要先调用远程接口把 B和 A的钱给冻结起来。
2、在 Confirm 阶段,执行远程调用的转账的操作,转账成功进行解冻。
3、如果第2步执行成功,那么转账成功,如果第二步执行失败,则调用远程冻结接口对应的解冻方法 (Cancel)。3.本地消息(MQ+Table)
基本思路就是:
消息生产方,需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过MQ发送到消息的消费方。如果消息发送失败,会进行重试发送。
消息消费方,需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行。如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。
生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。
这种方案遵循BASE理论,采用的是最终一致性,笔者认为是这几种方案里面比较适合实际业务场景的,即不会出现像2PC那样复杂的实现(当调用链很长的时候,2PC的可用性是非常低的),也不会像TCC那样可能出现确认或者回滚不了的情况。
4.事务消息(RocketMQ[alibaba])
以阿里的 RocketMQ 中间件为例,其思路大致为:
第一阶段Prepared消息,会拿到消息的地址。
第二阶段执行本地事务,第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。5.Seata(alibaba)
seata中有两种分布式事务实现方案,AT及TCC
- AT模式主要关注多 DB 访问的数据一致性,当然也包括多服务下的多 DB 数据访问一致性问题
- TCC 模式主要关注业务拆分,在按照业务横向扩展资源时,解决微服务间调用的一致性问题
7.如何在分布式系统事务中使用seata解决分布式事务问题?说说思路和使用步骤?
Seata AT模式是基于XA事务演进而来的一个分布式事务中间件,XA是一个基于数据库实现的分布式事务协议,本质上和两阶段提交一样,需要数据库支持,Mysql5.6以上版本支持XA协议,其他数据库如Oracle,DB2也实现了XA接口
第一阶段
Seata 的 JDBC 数据源代理通过对业务 SQL 的解析,把业务数据在更新前后的数据镜像组织成回滚日志,利用 本地事务 的 ACID 特性,将业务数据的更新和回滚日志的写入在同一个 本地事务 中提交。
第二阶段
如果决议是全局提交,此时分支事务此时已经完成提交,不需要同步协调处理(只需要异步清理回滚日志),Phase2 可以非常快速地完成.
如果决议是全局回滚,RM 收到协调器发来的回滚请求,通过 XID 和 Branch ID 找到相应的回滚日志记录,通过回滚记录生成反向的更新 SQL 并执行,以完成分支的回滚
本文标签: 阶段性
版权声明:本文标题:阶段性回顾 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.roclinux.cn/b/1729823062a1369960.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论