项目整体架构

Docker

虚拟化容器计数,Docker基于镜像,可以秒级启动各种容器,每一种容器都是一个完整的运行环境,容器之间相互隔离;

安装docker

安装前卸载原有的docker

yum remove docker \
                   docker-client \
                   docker-client-latest \
                   docker-common \
                   docker-latest \
                   docker-latest-logrotate \
                   docker-logrotate \
                   docker-engine

 安装yum-utils

 yum install -y yum-utils

 设置阿里云镜像仓库地址

 yum-config-manager \
  --add-repo \
   http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

 安装docker相关引擎

yum makecache fase 
yum install docker-ce docker-ce-cli containerd.io
#启动docker
systemctl  start  docker
#查看docker种正在运行的容器
docker ps
#查看本机的docker镜像
docker  images
#使用阿里云镜像加速
mkdir -p /etc/docker

tee /etc/docker/daemon.json <<-'EOF'
{
  "registry-mirrors": ["https://kskdqwg1.mirror.aliyuncs.com"]
}
EOF

 systemctl daemon-reload
 systemctl restart docker

安装Mysql

docker pull mysql:5.7

docker run -p 3306:3306 --name mysql \
-v /mydata/mysql/log:/var/log/mysql \
-v /mydata/mysql/data:/var/lib/mysql \
-v /mydata/mysql/conf:/etc/mysql \
-e MYSQL_ROOT_PASSWORD=root \
-d mysql:5.7

#重启mysql
docker restart mysql

安装Redis

#创建目录结构
mkdir -p /mydata/redis/conf
touch /mydata/redis/conf/redis.conf
#安装redis
docker pull redis
#启动redis
docker run -p 6379:6379 --name redis \
-v /mydata/redis/data:/data \
-v /mydata/redis/conf/redis.conf:/etc/redis/redis.conf \
-d redis redis-server /etc/redis/redis.conf

#运行redis
docker exec -it redis redis-cli
#设置redis持久化
cd /mydata/redis/conf
vi redis.conf
#修改如下属性
appendonly yes

安装es

# 将docker里的目录挂载到linux的/mydata目录中
# 修改/mydata就可以改掉docker里的
mkdir -p /mydata/elasticsearch/config
mkdir -p /mydata/elasticsearch/data
 
# es可以被远程任何机器访问
echo "http.host: 0.0.0.0" >> /mydata/elasticsearch/config/elasticsearch.yml
 
# 递归更改权限,es需要访问
chmod -R 777 /mydata/elasticsearch/

# 9200是用户交互端口 9300是集群心跳端口
# -e指定是单阶段运行
# -e指定占用的内存大小,生产时可以设置32G
docker run --name elasticsearch -p 9200:9200 -p 9300:9300 \
-e  "discovery.type=single-node" \
-e ES_JAVA_OPTS="-Xms64m -Xmx512m" \
-v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /mydata/elasticsearch/data:/usr/share/elasticsearch/data \
-v  /mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
-d elasticsearch:7.4.2 
 
 
# 设置开机启动elasticsearch
docker update elasticsearch --restart=always

es的可视化工具kibana

# kibana指定了了ES交互端口9200  # 5600位kibana主页端口
docker run --name kibana -e ELASTICSEARCH_HOSTS=http://192.168.116.128:9200 -p 5601:5601 -d kibana:7.4.2
 
 
# 设置开机启动kibana
docker update kibana  --restart=always

 ik分词器

#首先需要下架wget命令
yum install wget
#下载分词器
wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.4.2/elasticsearch-analysis-ik-7.4.2.zip

#解压文件
unzip elasticsearch-analysis-ik-7.4.2.zip -d ik
#移动到目标文件夹
mv ik plugins/
#修改权限
chmod -R 777 plugins/ik
#重启容器
docker restart elasticsearch
#删除安装包
rm -rf elasticsearch-analysis-ik-7.4.2.zip

安装Nginx 

docker run -p80:80 --name nginx -d nginx:1.10  

复制nginx 

删除拷贝用的nginx

 

移动文件夹

 

创建html和logs文件夹

 

启动nginx 

#启动nginx
docker run -p 80:80 --name nginx \
 -v /mydata/nginx/html:/usr/share/nginx/html \
 -v /mydata/nginx/logs:/var/log/nginx \
 -v /mydata/nginx/conf/:/etc/nginx \
 -d nginx:1.10

#设置开机自启
docker update nginx --restart=always

docker安装RabbitMQ 

docker run -d --name rabbitmq -p 5671:5671 -p 5672:5672 -p 4369:4369 -p  25672:25672 -p 15671:15671 -p 15672:15672 rabbitmq:management

开机自启 它的账号密码默认为guest

docker update rabbitmq --restart=always

SpringCloud Alibaba

 导入的依赖管理:

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>2.2.6.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

Nacos注册中心,配置中心

注册中心

导入注册中心环境

1.导入依赖: (导入依赖后需要下载一个nacos的压缩包,startup脚本开启nacos)

<!-- nacos-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

 2.在服务的配置文件中配置nacos注册中心的地址

spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848

3. 开启服务发现功能,在springBoot项目主类上添加@EnableDiscoveryClient注解

4. Nacos注册中心端口号为localhost:8848/nacos; 账号密码都为nacos

5. 在配置文件中配置当前服务的名字(至此已经将当前服务配置到nacos注册中心了)

spring:
  application:
    name: gulimall-coupon

配置中心

导入配置中心环境

1.导入依赖

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>

2.作为配置中心的时候,编写配置文件需要在bootstrap配置文件中编写

#此文件会优先于application.properties来加载
#配置中心的信息
spring.application.name=gulimall-coupon
spring.cloud.nacos.config.server-addr=127.0.0.1:8848

3.进入Nacos后点击配置列表后的+增加配置,配置的名字默认为项目名.properties,在这里我们添加需要的配置:

 4.在代码中我们使用@Value即可操作配置中心里的值,联合注解@RefreshScope这样就可以实现项目在发布上线以后,不修改代码重启项目也能做到一些值的更新

@RestController
@RequestMapping("coupon/coupon")
@RefreshScope//用于刷新的时候动态更改配置中心发布的内容
public class CouponController {
    @Value("${coupon.user.name}")
    private String name;
    @Value("${coupon.user.age}")
    private String age;
    @RequestMapping("/test")
    public R test(){
        return R.ok().put("name",name).put("age",age);
    }
}

命名空间: 用于不同环境的配置区分隔离,比如开发环境和生产环境的资源隔离

1.新建的配置默认都属于public命名空间,我们可以新建多个命名空间来进行环境隔离

 2.在配置文件中通过如下属性和对应的uuid来启用命名空间

配置集:所有配置的集合

配置集ID:类似于文件名,也就是新建配置时输入的Data ID

配置分组:默认所有的配置集都属于Default_Group组

同样的通过如下属性来读入组内容,0 1 2索引对应相应的配置文件,如果搜寻不到nacos里的分组,就会搜索本地的文件加载本地的配置

spring.application.name=gulimall-coupon
 
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
# 可以选择对应的命名空间 # 写上对应环境的命名空间ID
spring.cloud.nacos.config.namespace=b176a68a-6800-4648-833b-be10be8bab00
# 更改配置分组
spring.cloud.nacos.config.group=dev
 
spring.cloud.nacos.config.extension-configs[0].data-id=datasource.yml
spring.cloud.nacos.config.extension-configs[0].group=dev
spring.cloud.nacos.config.extension-configs[0].refresh=true
 
spring.cloud.nacos.config.extension-configs[1].data-id=mybatis.yml
spring.cloud.nacos.config.extension-configs[1].group=dev
spring.cloud.nacos.config.extension-configs[1].refresh=true
 
spring.cloud.nacos.config.extension-configs[2].data-id=other.yml
spring.cloud.nacos.config.extension-configs[2].group=dev
spring.cloud.nacos.config.extension-configs[2].refresh=true

Feign远程调用

它是一个声明式的HTTP客户端,提供了HTTP请求的模板,通过编写简单的接口和插入注解就可以定义好HTTP请求的参数,格式,地址等信息,Feign整合了Ribbon(负载均衡)和Hystrix(服务熔断),可以让我们不再需要显示的使用这两个组件;

1. 引入依赖

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

2.开启feign功能,在SpringBoot主类上使用@EnableFeignClient注解,可以在该注解的属性package中指定接口的位置

3.声明式远程接口,使用@FeignClient("这里填的是需要调用的远程服务名")注解,声明一个远程接口,注意远程接口的路径要写全

@FeignClient("gulimall-coupon")//告诉springcloud这里需要调用远程服务
public interface CouponFeignService {
    //远程接口,如果以后这个方法被调用,那么就会去调用coupon里的对应方法
    @RequestMapping("coupon/coupon/member/list")
    public R memberCouponEntity();
}

Gateway网关

网关作为流量的入口常用功能包括路由转发,权限校验,限流控制;它有三个核心概念:

路由:一个路由由一个标识的id,一个目标的URI地址,一个断言的集合和一个过滤器的集合构成;只要断言为真,路由就能到指定服务

断言:判断路由到哪个服务的判断条件

过滤器:在请求前和请求后都可以通过过滤器对请求进行修改

网关环境导入

1.网关作为一个单独的模块,也需要把自己注册到配置中心和注册中心(配置方法如上)

2.网关的xml依赖配置如下:

<dependencies>
        <dependency>
            <groupId>com.wuyimin.gulimall</groupId>
            <artifactId>gulimall-common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <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>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

3.由于在gulimall-common模块中我们配置了数据源相关的操作,所以我们要排除此操作

//开启服务的注册发现(配置注册中心地址)
@EnableDiscoveryClient
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class GulimallGatewayApplication {

    public static void main(String[] args) {
        SpringApplication.run(GulimallGatewayApplication.class, args);
    }

}

4.yml配置路由

spring:
  cloud:
    gateway:
      routes:
        #优先级比下面的哪个路由要高所以要放在上面,不然会被截断
        - id: admin_route
          #lb表示负载均衡
          uri: lb://renren-fast
          #规定前端项目必须带有一个api前缀
          #原来验证码的uri ...localhost:88/api/captcha.jpg
          #应该改成的uri ...localhost:88/renren-fast/captcha.jpg
          predicates:
            - Path=/api/**
          filters:
            - RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}
        - id: member_route
          uri: lb://gulimall-member
          predicates:
            - Path=/api/member/**
          filters:
            #api前缀去掉剩下的全体保留
            - RewritePath=/api/(?<segment>.*),/$\{segment}

跨域配置

跨域指的是浏览器不能执行其他网站的脚本,它是由用浏览器的同源策略造成的,是浏览器对js施加的安全限制

跨域请求的实现是通过预检请求实现的,先发送一个OPTIONS探路,收到响应允许跨域后再发送真实的请求,在网关统一配置跨域:

package com.wuyimin.gulimall.gateway.config;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
 
 
 
/**
 * @author wuyimin
 * @create 2021-08-04 20:36
 * @description 跨域的配置
 */
@Configuration
public class MyCorsConfiguration {
    @Bean
    public CorsWebFilter corsWebFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
 
        CorsConfiguration corsConfiguration = new CorsConfiguration();
 
        // 配置跨域
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        corsConfiguration.addAllowedOrigin("*");
        corsConfiguration.setAllowCredentials(true);
 
        source.registerCorsConfiguration("/**", corsConfiguration);
        return new CorsWebFilter(source);
    }
}

Oss云端准备

创建一个子模块专门用于与Oss之间的数据传输:勾选两个基础模块SpringWeb和OpengFeign

1.依赖导入(配置注册中心,配置中心)

 <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alicloud-oss</artifactId>
            <version>2.2.0.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>com.wuyimin.gulimall</groupId>
            <artifactId>gulimall-common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>

2.通过OssClient来测试上传

@SpringBootTest
class GulimallThirdPartyApplicationTests {
    @Autowired
    OSSClient ossClient;
 
    @Test
    void contextLoads() throws FileNotFoundException {
        // 上传文件流。
        InputStream inputStream = new FileInputStream("C:\\Users\\56548\\Desktop\\1.jpg");
        // 上传
        ossClient.putObject("gulimall-wuyimin", "1.jpg", inputStream);
 
        // 关闭OSSClient。
        ossClient.shutdown();
        System.out.println("上传成功.");
    }
}

3.Oss服务直传

/**
 * @author wuyimin
 * @create 2021-08-06 14:34
 * @description 签名信息
 */
@RestController
public class OssController {
    @Autowired
    OSS ossClient;
 
    @Value("${spring.cloud.alicloud.oss.endpoint}")
    private String endpoint;
 
    private String bucket="gulimall-wuyimin";
    @Value("${spring.cloud.alicloud.access-key}")
    private String accessId;
    @RequestMapping("/oss/policy")
    public R policy(){
        // https://gulimall-hello.oss-cn-beijing.aliyuncs.com/hahaha.jpg  host的格式为 bucketname.endpoint
        String host = "https://" + bucket + "." + endpoint;
        // callbackUrl为 上传回调服务器的URL,请将下面的IP和Port配置为您自己的真实信息。
        // String callbackUrl = "http://88.88.88.88:8888";
        String format = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
        // 用户上传文件时指定的前缀。
        String dir = format + "/";
        Map<String, String> respMap = null;
        try {
            long expireTime = 30;
            long expireEndTime = System.currentTimeMillis() + expireTime * 1000;
            Date expiration = new Date(expireEndTime);
            PolicyConditions policyConds = new PolicyConditions();
            policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);
            policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);
 
            String postPolicy = ossClient.generatePostPolicy(expiration, policyConds);
            byte[] binaryData = postPolicy.getBytes(StandardCharsets.UTF_8);
            String encodedPolicy = BinaryUtil.toBase64String(binaryData);
            String postSignature = ossClient.calculatePostSignature(postPolicy);
 
            respMap = new LinkedHashMap<String, String>();
            respMap.put("accessid", accessId);
            respMap.put("policy", encodedPolicy);
            respMap.put("signature", postSignature);
            respMap.put("dir", dir);
            respMap.put("host", host);
            respMap.put("expire", String.valueOf(expireEndTime / 1000));
            // respMap.put("expire", formatISO8601Date(expiration));
 
        } catch (Exception e) {
            // Assert.fail(e.getMessage());
            System.out.println(e.getMessage());
        }
 
        return R.ok().put("data", respMap);
    }
}

4.Oss的跨域需要在Nacos的命名空间中进行修改,在来源中填入*

JSR303检验注解

1.依赖导入

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

2.在相关的字段上方添加对应注解比如名字不能为空(空字符串也不行),就在name上添加一个@NotBlank注解,该注解的message属性可以写入提示信息;@NotEmpty可以为空串;@URL表示必须是一个url地址,@Pattern可以自定义一个正则表达式,@Min表示大于等于

3.在需要校验的方法参数前加入@Valid注解表示这是一个需要校验的地方,后面紧跟一个BindingResult对象参数可以获得一些校验信息

/**
     * 保存
     */
    @RequestMapping("/save")
    public R save(@Valid @RequestBody BrandEntity brand, BindingResult result){//告诉springMVC这个字段需要校验,后面紧跟一个参数可以获取错误信息
        if(result.hasErrors()){
            Map<String ,String> map=new HashMap<>();
            result.getFieldErrors().forEach((item)->{
                String defaultMessage = item.getDefaultMessage();//获取信息
                String field=item.getField();//获取错误的名字
                map.put(field,defaultMessage);
            });
            return R.error(400,"提交的数据不合法").put("data",map);
        }else{
            brandService.save(brand);
            return R.ok();
        }
    }

集中异常处理

@RestControllerAdvice注解可以捕获全局异常

@WxceptionHandler可以对特定的异常进行处理

@Slf4j//记录日志
//restController+ControllerAdvice
@RestControllerAdvice(basePackages = "com.wuyimin.gulimall.product.controller")
public class GulimallExceptionControllerAdvice {
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public R handleValidException(MethodArgumentNotValidException e){
        log.error("数据校验出现问题:{},异常类型:{}",e.getMessage(),e.getClass());
        BindingResult bindingResult = e.getBindingResult();//之前的BindingResult属性
        Map<String,String> map=new HashMap<>();
        bindingResult.getFieldErrors().forEach(item->{
            map.put(item.getField(),item.getDefaultMessage());
        });
        return R.error(BizCodeEnum.VALID_EXCEPTION.getCode(),BizCodeEnum.VALID_EXCEPTION.getMsg()).put("data",map);
    }
 
    //其他任何异常都默认返回error
    @ExceptionHandler(value=Throwable.class)
    public R handleException(Throwable throwable){
        return R.error(BizCodeEnum.UNKNOWN_EXCEPTION.getCode(),BizCodeEnum.UNKNOWN_EXCEPTION.getMsg());
    }
}

为了规范每个错误可以创建异常枚举类,对每种错误进行处理

public enum BizCodeEnum {
        /**
         * 系统未知异常
         */
        UNKNOWN_EXCEPTION(10000, "系统未知异常"),
        /**
         * 参数校验错误
         */
        VALID_EXCEPTION(10001, "参数格式校验失败");
        private final int code;
        private final String msg;
        BizCodeEnum(int code, String msg) {
            this.code = code;
            this.msg = msg;
        }
        public int getCode() {
            return code;
        }
        public String getMsg() {
            return msg;
        }
    }

自定义校验注解

1.引入依赖

<!-- https://mvnrepository.com/artifact/javax.validation/validation-api -->
<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
</dependency>

2.自定义注解

@Documented
@Constraint(validatedBy = {ListValueConstraintValidator.class})//关联自定义的校验器
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
public @interface ListValue {
    String message() default "{com.wuyimin.common.valid.ListValue.message}";//提示信息从ValidationMessages.Properties里拿到,也可以直接定义
 
    Class<?>[] groups() default {};
 
    Class<? extends Payload>[] payload() default {};
 
    int[] vals() default {};
}

3.实现校验器

public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {
    private Set<Integer> set=new HashSet<>();
    //初始化方法 把val值拿到存入集合
    @Override
    public void initialize(ListValue constraintAnnotation) {
        int[] vals=constraintAnnotation.vals();
        for (int val:vals){
            set.add(val);
        }
    }
    //判断是否校验成功
    /**
     *
     * @param integer 需要校验的值
     * @param constraintValidatorContext
     * @return
     */
    @Override
    public boolean isValid(Integer integer, ConstraintValidatorContext constraintValidatorContext) {
        return set.contains(integer);//包含就返回true,不包含就返回false
    }
}

引入分页插件

@Configuration
@EnableTransactionManagement//开启事务功能
@MapperScan("com.wuyimin.gulimall.product.dao")
 
public class MybatisConfig {
    //引入分页插件
    @Bean
    public PaginationInterceptor paginationInterceptor(){
        PaginationInterceptor paginationInterceptor=new PaginationInterceptor();
        paginationInterceptor.setOverflow(true);//请求页面大于最后页面 false为默认-请求到空数据 true--跳到第一页
        paginationInterceptor.setLimit(1000);//每页最大受限1000条 -1不受限制
        return paginationInterceptor;
    }
}

Vo值对象

也可以叫做View Object:视图对象,之前我们在数据库表实体类上添加了很多注解,比如@JsonInclude,@TableFiled这样的操作其实是不规范的,正确的应该使用vo对象;它的作用就是用于接收页面传递来的数据,封装对象或者将业务处理完的对象,封装成页面需要使用的工具

vo对象在编程中我们使用new关键字来创建,让gc来回收,在实际操作的时候

    @Override
    @Transactional//事务原子性
    public void saveAttr(AttrVo attr) {
        AttrEntity attrEntity = new AttrEntity();//这是一个po持久对象用于保存数据库信息
        BeanUtils.copyProperties(attr,attrEntity);//使用BeanUtils拷贝属性,两者属性名必须一一对应
        this.save(attrEntity);//保存基本数据
        //保存关联关系
        AttrAttrgroupRelationEntity entity = new AttrAttrgroupRelationEntity();
        entity.setAttrGroupId(attr.getAttrGroupId());
        entity.setAttrId(attrEntity.getAttrId());
        relationService.save(entity);//最好是注入service
    }

数据库时间格式化

增加配置文件spring.jackson.date-formate属性 yyyy-MM-dd HH:mm:ss

Es添加分词文件

1.首先要保证安装了nginx,在nginx/html/下创建es文件夹,添加一个fenci.txt分词文件,里面直接写入需要填写的分词

2.修改elasticsearch/plugins/ik/config/IkAnalyzer.cfg.xml,添加自己分词文件所在的地址

3.修改完之后需要重启es 

ES整合SpringBoot

1.依赖导入

<dependency>
   <groupId>org.elasticsearch.client</groupId>
   <artifactId>elasticsearch-rest-high-level-client</artifactId>
   <version>7.4.2</version>
 </dependency>
<dependency>
    <groupId>com.wuyimin.gulimall</groupId>
    <artifactId>gulimall-common</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

2.排除数据源操作

3. 这里要注意的是springboot和es有版本对应关系,并且springboot有内置的es版本号,如果在这里导入了两个不同的版本的es会导致错误

 4.es的配置文件,这里主要是防止一个用于增删改查的client

@Configuration
public class ESConfig {
    public static final RequestOptions COMMON_OPTIONS;
    //默认规则
    static {
        RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();
 
        COMMON_OPTIONS = builder.build();
    }
 
    @Bean
    public RestHighLevelClient esRestClient() {
 
        RestClientBuilder builder = null;
        // 可以指定多个es
        builder = RestClient.builder(new HttpHost("192.168.116.128", 9200, "http"));
 
        RestHighLevelClient client = new RestHighLevelClient(builder);
        return client;
    }
}

5.测试es

对应复杂检索的构造条件

@SpringBootTest
class GulimallSearchApplicationTests {
 
    @Autowired
    private RestHighLevelClient client;
    @Test
    void contextLoads() throws IOException {
        //测试存储数据
        IndexRequest users = new IndexRequest("users");//索引名为user
        users.id("1");//id全部都是字符串的形式
        users.source("userName","张三","age",18,"gender","男");//第一种方案
        User user = new User();
        user.setAge(10);
        user.setGender("女");
        user.setUserName("小小吴");
        String s = JSON.toJSONString(user);//需要导入FastJson
        users.source(s, XContentType.JSON);//同时也需要传入数据的类型
        //调用es执行保存操作
        IndexResponse index = client.index(users, ESConfig.COMMON_OPTIONS);
        //提取响应数据
        System.out.println(index);
    }
    @Test
    void test() throws IOException {
        //1.创建检索请求
        SearchRequest searchRequest=new SearchRequest();
        //2.指定索引
        searchRequest.indices("bank");
        //3.DSL,检索条件
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        //构造年龄值分布
        searchSourceBuilder.query(QueryBuilders.matchQuery("address","mill"));//address值必须为mill
        TermsAggregationBuilder ageAgg = AggregationBuilders.terms("ageAgg").field("age").size(10);//名字为ageAgg,年龄进行聚合分组,只显示10种可能
        searchSourceBuilder.aggregation(ageAgg);
        //同级聚合,计算平均薪资
        AvgAggregationBuilder balanceAvg = AggregationBuilders.avg("balanceAvg").field("balance");//求平均值
        searchSourceBuilder.aggregation(balanceAvg);
        System.out.println(searchSourceBuilder.toString());
 
        searchRequest.source(searchSourceBuilder);
        //4.执行检索
        SearchResponse searchResponse = client.search(searchRequest, ESConfig.COMMON_OPTIONS);
        //5.分析结果
        //获取所有查到的数据
        SearchHit[] hits=searchResponse.getHits().getHits();//获得我们里面的hits
        for(SearchHit searchHit:hits){
            String string = searchHit.getSourceAsString();
            Account account = JSON.parseObject(string, Account.class);
            System.out.println(account);
        }
        //获取分析信息
        Aggregations aggregations = searchResponse.getAggregations();
        Terms agg = aggregations.get("ageAgg");
        for (Terms.Bucket bucket : agg.getBuckets()) {
            String key = bucket.getKeyAsString();
            System.out.println("年龄: "+key+"==>"+bucket.getDocCount());//key为xx的人有xx个
        }
        Avg balanceAvg1 = aggregations.get("balanceAvg");
        System.out.println("平均薪资:"+balanceAvg1.getValue());
    }
    @Data
    class User{
        private String userName;
        private String gender;
        private Integer age;
    }  //必须是static才能被fastjson parse
    @Data
    @ToString
    static class Account{
        private int account_number;
 
        private int balance;
 
        private String firstname;
 
        private String lastname;
 
        private int age;
 
        private String gender;
 
        private String address;
 
        private String employer;
 
        private String email;
 
        private String city;
 
        private String state;
    }
 
}

关于哈希表的bug(面试问到遇到什么问题装b用)

bug出现的原因:人人开源的返回对象R是继承于HashMap的,由于我希望在消费者远程调用生产者方法的时候直接拿到一些数据,在R类里设置一个私有属性对象,我修改了R中如下代码

public class R<T> extends HashMap<String, Object> {
   private static final long serialVersionUID = 1L;
   private T data;
   public T getData(){return data;}
   pubblic void setData(T data){this.data=data;}
}

在debug的时候发现,本应该set进R里的私有属性的数据竟然没有显示

 

这是因为jackson对于HashMap有特殊的处理方式,会将该类直接向上转型为map并且导致私有属性的消失,所以后续使用FastJson以序列化反序列化的方式传递对象

public class R extends HashMap<String, Object> {
    private static final long serialVersionUID = 1L;
    private R setData(Object o){
        put("data",o);
 
        return this;
    }
    //利用fastJson进行逆转
    public<T> T getData(TypeReference<T> typeReference){
        Object data=get("data");
        String s = JSON.toJSONString(data);
        T t=JSON.parseObject(s,typeReference);
        return t;
    }

生产者提供资源的代码:

    @PostMapping("/hasstock")
    public R getSkuHasStock(@RequestBody List<Long> skuIds){
        List<SkuHasStockVo> skuHasStockVos=wareSkuService.getSkuHasStock(skuIds);
        return R.ok().setData(skuHasStockVos);
    }

消费者消费资源的代码,注意typeReference这个类的构造器受保护的特性

 try{
            R r=wareFeignService.getSkuHasStock(skuIds);
            TypeReference<List<SkuHasStockVo>> typeReference = new TypeReference<List<SkuHasStockVo>>() {};//构造器受保护我们拿不到,只能生成一个匿名类对象
 
            //根据skuid和bool值组合成了一个map
            List<SkuHasStockVo> data = r.getData(typeReference);
            data.stream().collect(Collectors.toMap(SkuHasStockVo::getSkuId, item -> item.getHasStock()));
        }catch (Exception e){
            log.error("库存服务查询异常:原因{}",e);
        }

Nginx域名访问环境搭建

关于请求头Host:

一个IP地址可以对应多个域名,比如假设我有这么几个域名www.qiniu.com,www.taobao.com和www.jd.com然后在域名提供商最终都和我的虚拟机服务器IP 111.111.111.111关联起来,那么我通过任何一个域名去访问最终解析到的都是IP 111.111.111.111

虚拟机111.111.111.111上面其实是可以放很很多网站的我们可以把www.qiniu.com,www.taobao.com和www.jd.com这些网站都假设那台虚拟机上面,但是这样会有一个问题,我们每次访问这些域名其实都是解析到服务器IP 111.111.111.111,我怎么来区分每次根据域名显示出不同的网站的内容呢,其实这就要用到请求头中Host的概念了,每个Host可以看做是我在服务器111.111.111.111上面的一个站点,每次我用那些域名访问的时候都是会解析同一个虚拟机没错,但是我通过不同的Host可以区分出我是访问这个虚拟机上的哪个站点

反向代理:屏蔽服务器信息,负载均衡访问

本处需要实现的逻辑:本机浏览器请求xxx.com,通过配置hosts文件之后,相当于域名解析DNS服务得到ip 192.168.116.128(默认是80端口)

1.(用户==>nginx)首先修改了host文件中域名访问地址,现在我们访问gulimall.com实际访问的是我们的虚拟机

2.(Nginx==>网关)修改nginx/conf/nginx.conf,在upstream块中配置网关服务为nginx的上游服务器(88端口)(这里可以配置多个服务器,后可以跟weight属性决定负载均衡权重)

注意到最后一行我们include了目标文件夹下所有的conf后缀的文件,我们将server块的内容配置在此,他们都会被识别到此文件

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #上游服务器的名字叫gulimall 服务器位于175.10.107.1:88
    upstream gulimall{
     server 175.10.107.1:88;
	}
    include /etc/nginx/conf.d/*.conf;
}

修改nginx/conf/conf.d/gulimall.conf

server_name:设置该server块解析的主机地址

listen表示监听的端口为80 也就是所有来源于192.168.116.128的信息它都可以获取

proxy_pass 表示把这个请求转交给谁

proxy_set_header设置的请求头是传递给后端服务器的

由于nginx的转发会丢失请求的Host信息,所以这里要添加一个host头

server {
    listen       80;
    server_name gulimall.com;


    location /static {
        root   /usr/share/nginx/html;#静态资源位置.../nginx/html/static
    }

    location /payed/ {
        proxy_pass http://gulimall;#代理转发到网关,之前配置的上游服务器的名字叫gulimall
        proxy_set_header Host order.gulimall.com;#
    }
    location / {
        proxy_pass http://gulimall;
        proxy_set_header Host $host;#$host表示代理服务器本身ip
    }
}

3.(网关==>具体服务),这个先放在网关的最下面,因为网关的请求处理顺序是根据配置文件的顺序决定的,而当前的请求范围太宽泛了,会覆盖掉下面的具体请求;-Host表示任意**.gulimall.com为host的请求

    - id: gulimall_host_route
          uri: lb://gulimall-product
          predicates:
           - Host=**.gulimall.com

JMeter压力测试

JMeter错误解决

进行压测的时候会占用端口,所以要先修改windows文件,预留够足够的端口

 新建两个dword值,表示预留端口65534个,每30s回收一个端口

 修改完之后需要重启计算机

JMeter的基本使用

 添加一个线程组:

 线程属性参数:如图参数表示200个线程,在10秒内全部启动完成,每个线程发送50个请求 

 添加线程组下的请求属性:

 设置请求路径属性等

 查看压测结果:

查看结果树:可以查看线程是否结束,失败等

查看汇总报告:核心参数:吞吐量

查看聚合报告:

 Jvisualvm工具的使用

通过jvisualvm命令直接打开可使用该工具,线程有五个运行模式

其中驻留表示线程池空闲的线程,监视表示正在阻塞,等待锁的线程,通过安装Visual GC工具来查看GC情况,如下是一个正常健康状态的GC

Redis整合

 1.导入依赖

 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

2.yml文件中指明redis的host地址

3.引入StringRedisTemplate就可以进行对数据库的操作了

 4.Redis自带的分布式锁 setIfAbsent总是不能达到我们预期的效果;原因如下:

  • 问题一:解锁失败会导致死锁--》设置一个过期时间
  • 问题二:过期时间之前如果继续闪断怎么办,依然会导致死锁-》使用原子操作
  • 问题三:如果业务超时了,我们的锁早就过期了,我们的线程就会去删除其他线程的锁,所以更多的线程能拿到这一把锁--》占锁的时候指定key为lock,value为uuid,如果是自己的锁才删除
  • 问题四:依旧会删掉别人的锁,因为如果在判断了value为uuid后业务超时,在delete之前别的线程抢到了锁,后面delete依然会删别人的锁--》获取值对比和删除锁也必须是一个原子操作--Lua脚本解锁
  • 问题五:redis集群环境下,无论如何都不ok===>解决方案是引入redisson锁

5.引入Redisson锁依赖

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.4</version>
</dependency>

6.放入一个bean在配置文件中

    // redission通过redissonClient对象使用 // 如果是多个redis集群,可以配置
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redisson() {
        Config config = new Config();
        // 创建单节点模式的配置
        config.useSingleServer().setAddress("redis://192.168.116.128:6379");
        return Redisson.create(config);
    }

7.测试加锁代码;

假设解锁代码块没有运行,redisson会不会死锁?==>不会,redisson有一个看门狗,管理锁的自动续期,如果业务超长,看门狗自动续期30s,加锁的业务只要运行完成,就不会给当前的锁续期,即使不手动解锁,锁默认在30s之后自动删除;看门狗的原理是通过定时任务,重新给锁设置过期的时间;

redisson锁还提供了读写锁,信号量,countDownLatch等对标juc包下的内容

@ResponseBody
    @GetMapping("/hello")
    public String hello(){
        //获取一把锁,只要锁的名字一样就是同一把锁
        RLock myLock = redissonClient.getLock("myLock");
        myLock.lock();//加锁,阻塞式的等待
        try{
            System.out.println("业务代码"+Thread.currentThread().getId());
            Thread.sleep(3000);
        }catch (Exception e){

        }finally {
            System.out.println("释放锁"+Thread.currentThread().getId());
            myLock.unlock();//解锁
        }
        return "hello";
    }

整合SpringCache

 1.依赖

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>

2.编写配置(还可以配置key-prefix属性来指定缓存key的前缀,如果不指定就会把缓存的名字作为前缀)

spring:
  redis:
    host: 192.168.116.128
  cache:
    #指定缓存类型为redis
    type: redis
    redis:
      # 指定redis中的过期时间为1h
      time-to-live: 3600000

3.注解

  • @Cacheable 触发将数据保存到缓存的操作
  • @CacheEvict 触发将数据从缓存删除的操作
  • @CachePut 不影响方法执行更新缓存
  • @Caching 组合以上多个操作
  • @CacheConfig 在类级别共享缓存的相同配置
  • @EnableCaching 开启缓存功能,放在主类上
 //每一个需要缓存的数据我们都来指定要放到哪个名字的缓存(缓存分区--按照业务类型分,可以是数组)
    @Cacheable(value = "category",key = "#root.methodName") //现在只需要加上缓存注解就行了,如果缓存中有连方法都不会被调用,指定key属性可以指定缓存的key值,支持spel表达式
    @Override
    public List<CategoryEntity> getLevel1Categorys() {
        List<CategoryEntity> categoryEntities = this.list(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
        return categoryEntities;
    }

在数据库中缓存如图:

默认是JDK序列化;如果需要配置Json序列化的话需要加入配置文件

//默认使用jdk进行序列化(可读性差),默认ttl为-1永不过期,自定义序列化方式需要编写配置类
@Configuration
public class MyCacheConfig {
    @Bean
    public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {

        CacheProperties.Redis redisProperties = cacheProperties.getRedis();
        org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration
                .defaultCacheConfig();
        //指定缓存序列化方式为json
        config = config.serializeValuesWith(
                RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
        //设置配置文件中的各项配置,如过期时间
        if (redisProperties.getTimeToLive() != null) {
            config = config.entryTtl(redisProperties.getTimeToLive());
        }

        if (redisProperties.getKeyPrefix() != null) {
            config = config.prefixKeysWith(redisProperties.getKeyPrefix());
        }
        if (!redisProperties.isCacheNullValues()) {
            config = config.disableCachingNullValues();
        }
        if (!redisProperties.isUseKeyPrefix()) {
            config = config.disableKeyPrefix();
        }
        return config;
    }
}

在指定方法上使用@CaheEvict注解,指定缓存名和key名可以对缓存进行删除,比如每当我们对Category进行更新的时候,之前的缓存就没用了,此时可以如此使用注解

@CacheEvict(value = "category",key = "'getLevel1Categorys'")//表达式是普通字符串必须加上单引号
    @Transactional
    @Override
    public void updateCascade(CategoryEntity category) {
        this.updateById(category);
        categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());
    }

注解的一些其他使用方式

   //Caching组合注解    
@Caching(evict = {
            @CacheEvict(value = "category",key = "'getLevel1Categorys'"),
            @CacheEvict(value = "category",key = "'getCatelogJson'")
    })
    // 删除分区的所有数据
@CacheEvict(value="category",allEntries=true)

    //指定同步
@Cacheable(value = {"category"},key = "#root.methodName",sync = true)

常规数据(读多写少,即时性,一致性要求不高的数据,完全可以使用Spring-Cache)

ES总结

ElasticSearch是一个分布式,高性能、高可用、可伸缩、RESTful 风格的搜索和数据分析引擎;

为什么搜索商品的时候要使用ES而不是sql的like来做模糊查询?

我们假设一个场景:我们要买苹果吃,咱们想买天水特产的花牛苹果,然后在搜索框输入天水花牛苹果,这时候咱们希望搜索到所有的售卖天水花牛苹果的商家,但是如果咱们技术上根据这个天水花牛苹果使用sql的like模糊查询,是不能匹配到诸如天水特产花牛苹果,天水正宗,果园直送精品花牛苹果这类的不连续的店铺的。所以sql的like进行模糊查询来搜索商品还真不香!

基本概念:

ESMySql
字段
文档一行数据
类型(已废弃)
索引数据库

 其他概念:

节点:一个节点就是一个ES实例,类型分为以下几种:

  • Master Eligible节点:每个节点启动后的默认节点模式,此几点可以参加选主流程,并且成为Master节点;每个节点都保存了集群的状态,但是只有Master节点才可以修改集群的状态;
  • Data节点:可以保存数据的节点,主要负责保存分片数据有利于数据扩展
  • Coordinating节点:负责接受哭护短请求,将请求发送到合适的节点,最终把结果汇聚在一起;

分片:Es里的索引可能存储大量数据,这些数据可能会超出单个节点的硬件限制,为了解决这个问题,ES提供了将索引细分为多个碎片的功能,这就是分片

分片的注意事项:

  • 通过分片计数,我们可以水平差分数据量,同时它还支持跨碎片(可能在多个节点上)分布和并行操作,从而提高性能
  • ES可以完全自动管理分片的分配和文档的聚合来完成搜索请求,并且对用户完全透明
  • 主分片数在创建索引时指定,后续只能通过Reindex修改

副本分片:为了实现高可用,遇到问题时实现分片的故障转移机制,ES允许将索引分片的一个或者多个复制成所谓的副本分片

注意事项:

  • 当分片或者节点发生故障的时候提供高可用性;副本分片永远都不会分配到复制它的原始或者主分片所在的节点上
  • 可以提高扩展搜索量和吞吐量,因为ES允许在所有副本上并行执行搜索
  • 默认情况下ES中每个索引都分配五个主分片,并且每个主分片分配1个副本分片;主分片在创建时指定吗,不能修改,副本分片可以修改

基础使用

创建一个空索引:名字为ropledata,分片数为2,副本分片为0

PUT /ropledata
{
  "settings": { 
    "number_of_shards": "2", 
    "number_of_replicas": "0"
  } 
}

修改副本分片数:

PUT ropledata/_settings 
{ 
  "number_of_replicas" : "2" 
}

删除索引:

DELETE /ropledata

插入数据:插入数据的时候可以指定id,如果不指定,ES会自动生成,如下代码创建了一个101的文档

//指定id 
POST /ropledata/_doc/101 
{
  "id":1,
  "name":"111",
  "page":"https://www.baidu.com",
  "say":"123456" 
}

修改数据

ES的文档不可以修改,但是支持覆盖,对他做修改本质上时对他覆盖,它的修改分为全局更新和局部更新

全局:每次全局更新以后,它的_version版本都会+1

PUT /ropledata/_doc/101
{ 
  "id":1,
  "name":"222",
  "page":"https://www.qq.com",
  "say":"11111" 
}

局部更新:除了第一次执行,后续不管执行了多少次,_version都不会再发生变化,局部更新效率比全局更新更好

POST /ropledata/_update/101 
{
  "doc":
  {
    "say":"奥力给"
  } 
}

查询数据:原文链接

 对应的JavaAPI

异步编排

在渲染商品详情页的时候使用了异步编排API CompletableFutrue,其中自己编写了一个ThreadPoolExecutor,其中的ThreadPoolConfigProperties是自己配置的一个类,该类的参数使用@ConfigurationProperties注解将配置的具体信息放在了yml文件中

#配置线程池
gulimall:
  thread:
    core-size: 20
    max-size: 200
    keep-alive-time: 10
@ConfigurationProperties(prefix = "gulimall.thread")//使用此注解可以在配置文件中修改线程池属性
@Component
@Data
public class ThreadPoolConfigProperties {
    private Integer coreSize;
    private Integer maxSize;
    private Integer keepAliveTime;
}
@Configuration
public class MyThreadConfig {
//核心线程数,最大线程数,存活时间,时间单位,阻塞队列,线程工厂,拒绝策略
    @Bean
    public ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties pool){
        return new ThreadPoolExecutor(pool.getCoreSize(),pool.getMaxSize(),pool.getKeepAliveTime(), TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(100000), Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
    }
}

 方法逻辑:

获取销售属性组合的方法: group_concat用于组连接,mysql还规定,Group By的字段必须要查询,distinct用于去重

 <resultMap id="SkuItemSaleAttrVo" type="com.wuyimin.gulimall.product.vo.SkuItemSaleAttrsVo">
        <result column="attr_id" property="attrId"></result>
        <result column="attr_name" property="attrName"></result>
        <collection property="attrValues" ofType="com.wuyimin.gulimall.product.vo.AttrValueWithSkuIdVo">
            <result column="attr_value" property="attrValue"></result>
            <result column="sku_ids" property="skuIds"></result>
        </collection>
  </resultMap>

##分析当前spu有多少个sku,所有sku涉及的属性组合
    <select id="getSaleAttrsBySpuId" resultType="com.wuyimin.gulimall.product.vo.SkuItemSaleAttrsVo">
        SELECT 
        ssav.`attr_id` attr_id,
        ssav.`attr_name` attr_name,
        GROUP_CONCAT(DISTINCT ssav.`attr_value`) attr_values
        FROM `pms_sku_info` info 
        LEFT JOIN `pms_sku_sale_attr_value` ssav ON ssav.`sku_id`=info.`sku_id`
        WHERE info.`spu_id`=#{spuId}
        GROUP BY ssav.`attr_id`,ssav.`attr_name`
    </select>

其查询效果如图:

  //运行的顺序 1 2 6同时 345要在1运行完之后运行
    @Override
    public SkuItemVo item(Long skuId) throws ExecutionException, InterruptedException {
        SkuItemVo skuItemVo = new SkuItemVo();
        //第一个异步任务的结果别人还要用,所以就使用supply
        CompletableFuture<SkuInfoEntity> futureInfo = CompletableFuture.supplyAsync(() -> {
            //1.查询当前sku的基本信息 sku_info表
            SkuInfoEntity skuInfoEntity = getById(skuId);
            skuItemVo.setInfo(skuInfoEntity);
            return skuInfoEntity;
        }, executor);
        //需要接受结果
        CompletableFuture<Void> futureSaleAttrs = futureInfo.thenAcceptAsync(res -> {
            //3.获取spu的销售属性组合
            List<SkuItemSaleAttrsVo> skuItemSaleAttrsVos = skuSaleAttrValueService.getSaleAttrsBySpuId(res.getSpuId());
            skuItemVo.setSaleAttrsVos(skuItemSaleAttrsVos);
        }, executor);

        CompletableFuture<Void> futureInfoDesc = futureInfo.thenAcceptAsync(res -> {
            //4.获取spu的介绍
            SpuInfoDescEntity spuInfoDescEntity = spuInfoDescService.getById(res.getSpuId());
            skuItemVo.setDesp(spuInfoDescEntity);
        }, executor);

        CompletableFuture<Void> futureItemAttrGroups = futureInfo.thenAcceptAsync(res -> {
            //5.获取spu的规格参数
            List<SpuItemAttrGroup> spuItemAttrGroups = attrGroupService.getAttrGroupWithAttrsBySpuId(res.getSpuId(), res.getCatalogId());
            skuItemVo.setGroupAttrs(spuItemAttrGroups);
        }, executor);
        //没有什么返回结果
        CompletableFuture<Void> futureImage = CompletableFuture.runAsync(() -> {
            //2.sku的图片信息 pms_sku_image
            List<SkuImagesEntity> skuImagesEntities = imagesService.list(new QueryWrapper<SkuImagesEntity>().eq("sku_Id", skuId));
            skuItemVo.setImages(skuImagesEntities);
        }, executor);
        //等到任务全部做完,可以不用写info,因为image完了info肯定完了
        //6.查询到当前商品是否参加秒杀活动
        CompletableFuture<Void> futureSeckill = CompletableFuture.runAsync(() -> {
            R seckillSkuInfo = seckillFeignService.getSeckillSkuInfo(skuId);
            if (seckillSkuInfo.getCode() == 0) {
                SeckillSkuRedisTo data = seckillSkuInfo.getData(new TypeReference<SeckillSkuRedisTo>() {
                });
                skuItemVo.setSeckillSkuRedisTo(data);
            }
        }, executor);


        CompletableFuture.allOf(futureImage, futureInfoDesc, futureItemAttrGroups, futureSaleAttrs,futureSeckill).get();
        return skuItemVo;
    }

注册功能

阿里云短信验证码

正常来说需要使用@ConfigurationProperties配置到配置文件里,这里直接抽取不做配置,实际上也就是使用特定的参数发送post请求

@Component
@Data
public class SmsComponent {
    private String host;
    private String path;
    private String templateId="908e94ccf08b4476ba6c876d13f084ad";
    private String smsSignId="2e65b1bb3d054466b82f0c9d125465e2";
    private String appCode="78442b1006ae490da40cedda6826c7b5";
    public void sendSmsCode(String phone,String code){
        String host = "https://gyytz.market.alicloudapi.com";
        String path = "/sms/smsSend";
        String method = "POST";
        String appcode = appCode;
        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", phone);
        querys.put("param", "**code**:"+code+"**minute**:5");
        querys.put("smsSignId", smsSignId);
        querys.put("templateId", templateId);
        Map<String, String> bodys = new HashMap<String, String>();
        try {
            /**
             * HttpUtils请从
             * https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/src/main/java/com/aliyun/api/gateway/demo/util/HttpUtils.java
             * 下载
             */
            HttpResponse response = HttpUtils.doPost(host, path, method, headers, querys, bodys);
            System.out.println(response.toString());
            //获取response的body
            //System.out.println(EntityUtils.toString(response.getEntity()));
        } catch (Exception e) {
            e.printStackTrace();
        }
    } 
}

 接口防刷

虽然设定了规定时间间隔才能发送验证码,但是其实只要每次刷新网页就可以重新发送验证码了,这里使用redis来实现接口防刷功能

@Slf4j
@Controller
public class LoginController {
    @Autowired
    ThirdPartyFeignService thirdPartyFeignService;
    @Autowired
    StringRedisTemplate redisTemplate;
    @ResponseBody
    @GetMapping("/sms/sendcode")
    public R sendCode(@RequestParam("phone") String phone){
        //TODO 接口防刷
        String redisCode = redisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CAHE_PREFIX + phone);
        if(!StringUtils.isEmpty(redisCode)){
            long l=Long.parseLong(redisCode.split("_")[1]);//拿到时间
            if(System.currentTimeMillis()-l<60000){
                //60秒内不能再发
                return R.error(BizCodeEnum.SMS_CODE_EXCEPTION.getCode(),BizCodeEnum.SMS_CODE_EXCEPTION.getMsg());
            }
        }
        String code = UUID.randomUUID().toString().substring(0, 5)+"_"+System.currentTimeMillis();//加上系统时间
        //验证码的再次校验,存入redis key-手机号 value-code
        redisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CAHE_PREFIX+phone,code,10, TimeUnit.MINUTES);
        try {
            thirdPartyFeignService.sendCode(phone,code);//第三方服务
        } catch (Exception e) {
            log.warn("远程调用不知名错误 [无需解决]");
        }
        return R.ok();
    }
}

接口校验

引入依赖

  <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

添加校验注解

@Data
public class UserRegistVo {
    @NotEmpty(message = "用户名必须提交")
    @Length(min = 6,max = 18,message = "长度必须在6-18")
    private String userName;
    @NotEmpty(message = "密码必须提交")
    @Length(min = 6,max = 18,message = "长度必须在6-18")
    private String password;
    //第一个数组必须是1,第二个数字在3-9剩下9个数字在0-9,一共11位
    @NotEmpty(message = "手机号必须填写")
    @Pattern(regexp = "^[1]([3-9])[0-9]{9}$",message = "手机号格式不正确")
    private String phone;
    @NotEmpty(message = "验证码必须填写")
    private String code;
}

配置路径映射和redisSession

视图映射可以做到收到指定的请求返回指定的视图,但是要注意的一点是它只支持get请求而不支持post请求

@Configuration
public class MyWebConfig implements WebMvcConfigurer {
    //视图映射
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        //registry.addViewController("/login.html").setViewName("login");
        registry.addViewController("/reg.html").setViewName("reg");
    }
}

注册功能逻辑

注册成功之后,需要重定向到登录页;失败后继续留在注册页

@PostMapping("/regist")
    public String register(@Valid UserRegistVo vo, BindingResult result, RedirectAttributes attributes){//第三个参数是专门用来重定向携带数据的
        //注册成功会到登录页
        //1.判断校验是否通过
        Map<String, String> errors = new HashMap<>();
        if (result.hasErrors()){
            //1.1 如果校验不通过,则封装校验结果
            result.getFieldErrors().forEach(item->{
                // 获取错误的属性名和错误信息
                errors.put(item.getField(), item.getDefaultMessage());
                //1.2 将错误信息封装到session中
                attributes.addFlashAttribute("errors", errors);
            });
            //校验出错,重定向到注册页
            return "redirect:http://auth.gulimall.com/reg.html";//防止刷新的时候表单重复提交,采用重定向
        }else{
            //真正的注册
            //1.校验验证码
            String code=vo.getCode();
            String redisCode = redisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CAHE_PREFIX + vo.getPhone());
            if(!StringUtils.isEmpty(redisCode)){
                if(code.equals(redisCode.split("_")[0])){
                    //删除验证码
                    redisTemplate.delete(AuthServerConstant.SMS_CODE_CAHE_PREFIX + vo.getPhone());
                    //验证码通过,调用远程接口进行服务注册
                    R r = memberFeignService.regist(vo);
                    if(r.getCode()==0){
                        //成功
                        return "redirect:http://auth.gulimall.com/login.html";
                    }else{
                        //调用失败,返回注册页并显示错误信息
                        String msg = (String) r.get("msg");
                        errors.put("msg", msg);
                        attributes.addFlashAttribute("errors", errors);
                        log.error("远程调用会员服务抛出异常");
                        return "redirect:http://auth.gulimall.com/reg.html";
                    }
                }else{
                    //验证码没有匹配
                    errors.put("code","验证码错误");
                    attributes.addFlashAttribute("errors",errors);
                    return "redirect:http://auth.gulimall.com/reg.html";
                }
            }else{
                //没有验证码
                errors.put("code","验证码错误");
                attributes.addFlashAttribute("errors",errors);
                return "redirect:http://auth.gulimall.com/reg.html";
            }
        }
    }

远程调用的Member服务regist方法

自定义异常类

public class PhoneExistException extends RuntimeException {
    public PhoneExistException() {
        super("手机号已经存在");
    }
}

检查手机和用户名是否正确的函数

@Override
    public void checkPhone(String phone) throws PhoneExistException {
        Integer count = baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("mobile", phone));
        if (count > 0) {
            throw new PhoneExistException();
        }
    }
 
    @Override
    public void checkUserName(String userName) throws UsernameExistException {
        Integer count = baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("username", userName));
        if (count > 0) {
            throw new UsernameExistException();
        }
    }

regist方法,其中密码使用了MD5盐值加密

 @Override
    public void regist(MemberRegisterVo vo) {
        MemberEntity memberEntity = new MemberEntity();
        MemberLevelEntity memberLevelEntity = memberLevelDao.getDefaultLevel();
        memberEntity.setLevelId(memberLevelEntity.getId());
        checkPhone(vo.getPhone());
        checkUserName(vo.getUserName());
        memberEntity.setMobile(vo.getPhone());
        memberEntity.setUsername(vo.getUserName());
        //设置密码(密码需要进行加密存储)
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String encode = passwordEncoder.encode(vo.getPassword());
        memberEntity.setPassword(encode);
        //其他的默认信息。。
        //保存
        baseMapper.insert(memberEntity);
    }

调用它的Controller,接受自定义的异常并进行处理

 @PostMapping("/regist")
    public R regist(@RequestBody MemberRegisterVo vo){//远程服务必须要获得json对象
        try{
            memberService.regist(vo);
        }catch (PhoneExistException e){
           return R.error(BizCodeEnum.PHONE_EXIST_EXCEPTION.getCode(),BizCodeEnum.PHONE_EXIST_EXCEPTION.getMsg());
        }catch (UsernameExistException e){
            return R.error(BizCodeEnum.USER_EXIST_EXCEPTION.getCode(),BizCodeEnum.USER_EXIST_EXCEPTION.getMsg());
        }
        return R.ok();
    }

登录功能

普通登录

member服务里的登录逻辑

    @Override
    public MemberEntity login(MemberLoginVo memberLoginVo) {
        String loginacct = memberLoginVo.getLoginacct();
        String password = memberLoginVo.getPassword();
        //去数据库查询 loginacct有可能是用户名也有可能是手机号
        MemberEntity memberEntity = baseMapper.selectOne(new QueryWrapper<MemberEntity>().eq("mobile", loginacct)
                .or().eq("username", loginacct));
        //如果实体类不存在就登录失败
        if(memberEntity==null){
            return null;
        }else{
            String passwordDB = memberEntity.getPassword();
            //密码匹配
            BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
            boolean matches = bCryptPasswordEncoder.matches(password, passwordDB);
            if(matches){
                return memberEntity;
            }
            return null;
        }
    }

认证服务中远程调用该接口:

   @PostMapping("/login")
    public String login(UserLoginVo vo,RedirectAttributes redirectAttributes){//页面提交过来的数据是key-value,不能用requestBody接受
        R login = memberFeignService.login(vo);
        Map<String,String> errors=new HashMap<>();
        if(login.getCode()==0){
            return "redirect:http://gulimall.com";
        }else{
            //登录失败
            errors.put("msg",login.getData("msg",new TypeReference<String>(){}));
            redirectAttributes.addFlashAttribute("errors",errors);
            return "redirect:http://auth.gulimall.com/login.html";
        }
    }

Gitee社交登录

社交登录流程:

 社交登录远程接口

    //具有登录和注册合并逻辑
    @Override
    public MemberEntity login(SocialUserVo socialUserVo) throws Exception {
        long uid = socialUserVo.getUid();
        //判断当前社交用户是否已经登录过系统
        MemberEntity user = baseMapper.selectOne(new QueryWrapper<MemberEntity>().eq("social_uid", uid));
        if(user!=null){
            //这个用户已经注册了
            user.setAccessToken(socialUserVo.getAccess_token());
            long expires_in = socialUserVo.getExpires_in();
            user.setExpiresIn(String.valueOf(expires_in));
            baseMapper.updateById(user);
            return user;
        }else{
         //需要注册一个用户
            MemberEntity newMember = new MemberEntity();
            //查询当前社交用户的性别,信息,这里我设置没给,就直接设置id就行了
            newMember.setAccessToken(socialUserVo.getAccess_token());
            newMember.setExpiresIn(String.valueOf(socialUserVo.getExpires_in()));
            newMember.setSocialUid(String.valueOf(socialUserVo.getUid()));
            baseMapper.insert(newMember);
            return newMember;
        }
    }

社交登录具体流程

@Slf4j
@Controller
public class OAuth2Controller {
    @Autowired
    MemberFeignService memberFeignService;
    @GetMapping("/oauth2.0/gitee/success")
    public String gitee(@RequestParam("code") String code, HttpSession session) throws Exception {
        //根据code换取accessToken
        HashMap<String, String> map = new HashMap<>();
        HashMap<String, String> header = new HashMap<>();
        HashMap<String, String> query = new HashMap<>();
        map.put("client_id","8351b1529803f1bca29176b023f2c431c48ffe8cd8398165d7bc26baeb6c6f74");
        map.put("client_secret","001a283d5c21b12ffd74cbb63c9968e318abbf07611fbb6b2bb1efa60c959fb0");
        map.put("grant_type","authorization_code");
        map.put("redirect_uri","http://auth.gulimall.com/oauth2.0/gitee/success");
        map.put("code",code);
        HttpResponse response = HttpUtils.doPost("https://gitee.com", "/oauth/token", "post", header, query, map);
        if(response.getStatusLine().getStatusCode()==200){
            //获取到了token
            //这里得到的就是access_token": "48e3a360bab1a289c882b81a2aa75633",
            //  "token_type": "bearer",
            //  "expires_in": 86400,
            //  "refresh_token": "29cc443c3eb07d366850034475862004ba6b71d590a0dd0cb7ffcb85c82df7fa",
            //  "scope": "user_info",
            //  "created_at": 1629684682---》我猜这个就是uuid
            //  的json字符串
            String string = EntityUtils.toString(response.getEntity());
            SocialUserVo socialUserVo = JSON.parseObject(string, SocialUserVo.class);
            //giee比微博多了一个步骤是需要我们提交一个get请求来获得这个用户的唯一id
            //https://gitee.com/api/v5/user?access_token=0a7c505a421334e51bc77cad97860bf8
            HashMap<String, String> getHeader = new HashMap<>();
            HashMap<String, String> getQuery = new HashMap<>();
            HttpResponse get = HttpUtils.doGet("https://gitee.com", "/api/v5/user?access_token="+socialUserVo.getAccess_token(), "get", getHeader, getQuery);
            String s = EntityUtils.toString(get.getEntity());
            //拿到的字符串"id":8442725,....
            String id = s.split(",")[0].split(":")[1];
            long l = Long.parseLong(id);
            socialUserVo.setUid(l);
            //当前的用户如果是第一次进网站,那么就需要注册进来(自动注册)
            //社交用户关联自己系统的会员
            R r = memberFeignService.oauthLogin(socialUserVo);
            if(r.getCode()==0){
                MemberRespVo data = r.getData("data", new TypeReference<MemberRespVo>() {
                });
                log.info("登录成功 用户: {}",data.toString());
                //把值传入session中带给前端
                session.setAttribute("loginUser",data);
                //远程调用成功
                //成功就跳回首页
                return "redirect:http://gulimall.com";
            }else{
                return "redirect:http://auth.gulimall.com/login.html";
            }
        }else{
            return "redirect:http://auth.gulimall.com/login.html";
        }
    }
}

分布式Session-保存用户的登录信息

使用redisSession方案

1.导入依赖

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2.修改配置

spring: #配置nacos
  session:
    store-type: redis
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848

  application:
    name: gulimall-auth-server
  redis:
    host: 192.168.116.128
server:
  port: 20000
  servlet:
    session:
      timeout: 30m

3.在主函数上添加@EnableRedisHttpSession注解

4.Vo想要作为数据被存到redis里必须实现Serializable接口

5.由于Cookie的作用域不够,所以要提升Cookie的作用域

@Configuration
public class RedisSessionConfig {
    @Bean // redis的json序列化
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }

    @Bean // cookie
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        serializer.setCookieName("GULISESSIONID"); // cookie的键
        serializer.setDomainName("gulimall.com"); // 扩大session作用域,也就是cookie的有效域
        return serializer;
    }
}

6.由于redis缓存的数据product模块需要取出,并且auth模块也需要存入,因此to必须放在公共模块里,如果仅仅是复制两份会导致包名不一致,序列化拿不出来

SpringSession的原理:在SessionRepositoryFilter中有这样一个方法doFilterInternal ,它将我们原生的请求和响应拿过来进行包装,将包装后的请求和响应应用到我们的整个执行链,这一段代码也就是springSession的核心原理

 7.自此当我们往session里面存储数据的时候,redis里面也会有相对应的数据

            if(r.getCode()==0){
                MemberRespVo data = r.getData("data", new TypeReference<MemberRespVo>() {
                });
                log.info("登录成功 用户: {}",data.toString());
                //把值传入session中带给前端
                session.setAttribute("loginUser",data);
                //远程调用成功
                //成功就跳回首页
                return "redirect:http://gulimall.com";
            }

ThreadLocal和拦截器

添加拦截器,定义拦截域

@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {//和网络有关的都要实现这个接口
    //添加拦截器的配置
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new CartInterceptor()).addPathPatterns("/**");//拦截所有请求
    }
}

类需要继承HandlerInterceptor接口

PreHandler

重写其preHandler方法,在触发请求前触发段代码;

PostHandler

 重写postHandler方法,在请求完成后,运行该段代码

 //@Component//在配置中创建了就不用放在容器中了
public class CartInterceptor implements HandlerInterceptor {
    //其实是一个Map<Thread,T>
    public static ThreadLocal<UserInfoVo> threadLocal=new ThreadLocal<>();
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HttpSession session = request.getSession();//获得session
        MemberRespVo memberRespVo = (MemberRespVo) session.getAttribute(AuthServerConstant.LOGIN_USER);//获得用户数据
        UserInfoVo userInfoVo = new UserInfoVo();
        if(memberRespVo!=null){
            //用户登录了
            userInfoVo.setUserId(memberRespVo.getId());
        }
        //离线购物车内容,通过cookie的key-value来区别用户,来到这里的用户是已经获得了cookie的临时用户
        Cookie[] cookies = request.getCookies();
        if (cookies!=null&&cookies.length>0) {
            for (Cookie cookie : cookies) {
                if(cookie.getName().equals(CartConstant.TEMP_USER_COOKIE_NAME)){
                    userInfoVo.setUserKey(cookie.getValue());
                    userInfoVo.setHasCookie(true);
                    break;
                }
            }
        }
        //如果是第一次登录的临时用户
        if(StringUtils.isEmpty(userInfoVo.getUserKey())){
            String uuid= UUID.randomUUID().toString();
            userInfoVo.setUserKey(uuid);
        }
        //在目标方法执行之前
        threadLocal.set(userInfoVo);
        // 还有一个登录后应该删除临时购物车的逻辑没有实现
        return true;
    }
    //业务执行之后:分配一个临时用户让浏览器保存
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        //不加判断就是一直延长过期时间
        UserInfoVo userInfoVo = threadLocal.get();
        //不是临时用户就代表以前没有注册过--第一次登录的临时用户或者已经登录的用户
        if(!userInfoVo.isHasCookie()){
            Cookie cookie = new Cookie(CartConstant.TEMP_USER_COOKIE_NAME, userInfoVo.getUserKey());
            cookie.setDomain("gulimall.com");//整个作用域都有效
            cookie.setMaxAge(CartConstant.TEMP_USER_COOKIE_TIMEOUT);
            response.addCookie(cookie);
        }
        //用完了threadLocal记得要清空,防止内存泄露,key为若引用,value为强引用,如果GC会导致key被收走了,value还在
        //线程池里面的线程,线程都是复用的,那么之前的线程实例处理完之后,出于复用的目的线程依然存活,所以,ThreadLocal设定的value值被持有,导致内存泄露。
        threadLocal.remove();
    }
}

拦截未登录的用户

package com.wuyimin.gulimall.order.interceptor;
 
/**
 * @ Author wuyimin
 * @ Date 2021/8/26-10:52
 * @ Description 拦截未登录用户
 */
@Component //放入容器中
public class LoginUserInterceptor implements HandlerInterceptor {
    public static ThreadLocal<MemberRespVo> loginUser=new ThreadLocal<>();//方便其他请求拿到
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HttpSession session = request.getSession();//获取session
        MemberRespVo attribute = (MemberRespVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
        if(attribute!=null){
            //已经登录
            loginUser.set(attribute);
            return true;
        }else{
            //给前端用户的提示
            request.getSession().setAttribute("msg","请先进行登录");
            //未登录
            response.sendRedirect("http://auth.gulimall.com/login.html");//重定向到登录页
            return false;
        }
    }
}

添加购物车接口幂等性设计

添加购物车请求/addToCart?skuId=1&num=1,当我们刷新这个请求的时候,页面会不断的增加商品的数量

 //添加完数据之后直接重定向到另一个请求,这样就不会造成刷新这个页面重复添加数据,因为刷新的是下一个请求
    @GetMapping("/addToCart")
    public String addToCart(@RequestParam("skuId") Long skuId, @RequestParam("num") Integer num, RedirectAttributes redirectAttributes) throws ExecutionException, InterruptedException {
        CartItem cartItem = cartService.addToCart(skuId, num);
        redirectAttributes.addAttribute("skuId", skuId);//给下面的请求传参数,model无法传参
        return "redirect:http://cart.gulimall.com/addToCartSuccess.html";//重定向要写完整域名
    }
 
    @GetMapping("/addToCartSuccess.html")
    public String addToCartSuccessPage(@RequestParam("skuId") Long skuId, Model model) {
        //重定向到成功页面,再次查询购物车数据即可
        CartItem cartItem = cartService.getCartItem(skuId);
        model.addAttribute("item", cartItem);
        return "success";
    }

RabbitMQ

为什么使用MQ?MQ的应用场景

在项目中,可将一些无需即时返回且耗时的操作提取出来,进行异步处理,而这种异步处理的方式大大的节省了服务器的请求响应时间,从而提高系统吞吐量

1、任务异步处理

将不需要同步处理的并且耗时长的操作由消息队列通知消息接收方进行异步处理。提高了应用程序的响应时间。

2、应用程序解耦合

MQ相当于一个中介,生产方通过MQ与消费方交互,它将应用程序进行解耦合。

比如订单系统要远程调用库存系统、支付系统、物流系统,这样会耦合,修改参数时候麻烦。

使用消息队列后,订单系统给消息MQ发送一条消息就算成功了

3、削峰填谷

如订单系统,在下单的时候就会往数据库写数据。但是数据库只能支撑每秒1000左右的并发写入,并发量再高就容易宕机。低峰期的时候并发也就100多个,但是在高峰期时候,并发量会突然激增到5000以上,这个时候数据库肯定卡死了。使用消息队列将数据保存起来, 然后系统就可以按照自己的消费能力来消费,比如每秒1000个数据,这样慢慢写入数据库,这样就不会卡死数据库了。

RabbitMQ基本介绍

消息(Message):由消息头和消息体组成,消息体不透明,消息头有一系列可选属性包括路由键(routing-key),优先权(priority)等

生产者(Publisher):向交换器发布消息的客户端应用程序

消费者(Consumer):从消息队列中取得消息的客户端应用程序

交换器(Exchange):用来接受生产者发送的消息并且将这些消息路由给服务器中的队列,它有四种类型direct(默认),fanout,topic,headers(已舍弃)

  • direct:精确匹配routingkey,将消息发送到绑定到交换机的指定队列中
  • fanout:忽略routingkey,将消息发送给绑定交换机的所有队列中
  • topic:模糊匹配routingkey,将消息发送到绑定到交换机的指定队列中,#匹配0个或者多个单词,*号匹配一个单词

消息队列(Queue):用来保存消息直到发送给消费者,它是消息的容器,也是消息的终点,一个消息可以投入多个队列,入队的消息一直会储存在队列中,直到有消费者连接将其取走

绑定关系(Binding):用于消息队列和交换器之间的关联,一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定关系构成的路由表,Exchange和Queue的绑定可以是多对多的关系

信道(Channel):多路复用连接中一条独立的双向数据流通道;

SpringBoot整合rabbitMQ

1.导入依赖

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

2.主类上开启注解

@EnableDiscoveryClient
@SpringBootApplication
@EnableRabbit//开启RabbitMQ
public class GulimallOrderApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(GulimallOrderApplication.class, args);
    }
 
}

3.配置文件

 4.Json序列化配置

@Configuration
public class MyRabbitConfig {
    @Bean
    public MessageConverter messageConverter(){
        return new Jackson2JsonMessageConverter();//转成json
    }
}

两个关键api的使用--AmpqAdmin和RabbitTemplate

所有创建的队列,交换机,绑定关系等需要在ampqAdmin中注册才能使用

@Slf4j
@SpringBootTest
class GulimallOrderApplicationTests {
    @Autowired
    AmqpAdmin amqpAdmin;//增删改查交换机,队列,绑定关系
    @Autowired
    RabbitTemplate rabbitTemplate;//收发消息
    //创建交换机
    @Test
    void createExchange() {
        //名字-是否持久化-是否自动删除
        DirectExchange directExchange = new DirectExchange("hello-java-exchange",true,false);
        amqpAdmin.declareExchange(directExchange);
        log.info("交换机创建完成");
    }
    //创建队列
    @Test
    void createQueue() {
        //名字-是否持久化-是否排它(只能被一个连接使用)-自动删除
        Queue queue = new Queue("hello-java-queue",true,false,false
        );
        amqpAdmin.declareQueue(queue);
        log.info("队列创建成功");
    }
    //创建绑定关系
    @Test
    void createBindingRelation() {
        //目的地(队列)-目的地类型-交换机-路由键-绑定的参数
        Binding binding = new Binding("hello-java-queue", Binding.DestinationType.QUEUE,"hello-java-exchange","hello.java",null);
        amqpAdmin.declareBinding(binding);
        log.info("绑定创建成功");
    }
    //如果需要使用json序列化机制需要在配置类里进行配置
    //发送消息
    @Test
    void sendMessage() {
        //经过测试对象不能是内部类
        OrderEntity orderEntity=new OrderEntity();
        orderEntity.setId(1L);
        orderEntity.setCreateTime(new Date());
         //交换机-路由键-消息内容(也可以发送对象-》需要序列化对象)-消息的唯一ID
        rabbitTemplate.convertAndSend("hello-java-exchange","hello.java",orderEntity,new CorrelationData(UUID.randomUUID().toString()));//转换并且发送
    }
}

两个注解的配合接受消息--@RabbitListener和@RabbitHandler

一个消息队列中可能有多个消息对象,我们可以在类上使用@RabbitListener注解表明需要监视哪个队列,然后再具体方法上使用@RabbitHandler注解配合上不同的接受参数,来区别对待不同的对象 

//可以放在类位置和方法位置
@RabbitListener(queues = {"hello-java-queue"})//声明需要监听的队列 使用此注解该类需要在spring容器里注册
@Service("mqMessageService")
public class MqMessageServiceImpl extends ServiceImpl<MqMessageDao, MqMessageEntity> implements MqMessageService {
    @RabbitHandler//只能放在方法上
    //channel是传输数据的通道
    // Queue可以有很多人来监听,同一个消息只能有一个收到,只有一个消息的方法处理完了,才能继续处理下一条消息
    public void receiveMessage(Message message, OrderEntity orderEntity, Channel channel){//可以直接获取消息体的对象
        byte[] body=message.getBody();//消息体 我们发送的对象
        MessageProperties messageProperties = message.getMessageProperties();//消息头
        System.out.println("收到消息:"+message+"==>Type:"+message.getClass()+"内容:"+orderEntity);
    }
    @RabbitHandler//一个队列可能有多个消息对象--可以区分不同的消息
    public void receiveMessage2(Message message, OrderSettingEntity orderSettingEntity, Channel channel){//可以直接获取消息体的对象
        byte[] body=message.getBody();//消息体 我们发送的对象
        MessageProperties messageProperties = message.getMessageProperties();//消息头
        System.out.println("收到消息:"+message+"==>Type:"+message.getClass()+"内容:"+orderSettingEntity);
    }
 
}

可靠投递

需要在配置文件中开启手动模式:

保证消息不丢失,可靠抵达,但是因此性能会大幅度下降,因此引入了确认机制;可靠消息的流程如下:

生产者丢消息给服务器(p-b)--》服务器交给交换机----》交换机交给queue(e->q)-》queue交还给消费者(q->c),期间三个位置需要我们注意消息的丢失

publisher: confirmCallback 确认模式  p->b:消息只要被broker接收到就会执行,如果是集群(cluster)模式需要所有的broker接收到才会调用
publisher: returnCallback 未投递到queue退回模式 e->q: queue收到就会执行,可以进行异步回调
consumer: ack机制  q->c: 消费者收到消息默认会自动ack

配置文件:

spring:
  rabbitmq:
    host: 192.168.116.128
    port: 5672 #高级协议端口
    virtual-host: /
    publisher-confirm-type: correlated #开启发送端的确认
    publisher-returns: true #开启发送端抵达队列的确认
    template:
      mandatory: true #只要抵达队列,以异步发送优先回调
    listener:
      simple:
        acknowledge-mode: manual #手动ack消息

confirmCallback确认回调配置和returnCallback配置;

@Configuration
public class MyRabbitConfig {
    @Bean
    public MessageConverter messageConverter(){
        return new Jackson2JsonMessageConverter();//转成json
    }
    @Autowired
    RabbitTemplate rabbitTemplate;
    //确认回调配置
    @PostConstruct//在MyRabbitConfig对象创建完之后,才会执行这个方法
    public void initRabbitTemplate(){
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            /**
             *
             * @param correlationData 当前消息的唯一关联数据(消息的唯一id)
             * @param b 消息是否成功收到 只要消息抵达broker就等于true
             * @param s 失败的原因
             */
            @Override
            public void confirm(CorrelationData correlationData, boolean b, String s) {
                //服务器收到了
                System.out.println("confirm...correlationData["+correlationData+"]==>ack["+b+"]==>cause["+s+"]");
            }
        });
        //设置消息抵达队列的确认回调
        rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
            /**
             *
             * @param message 投递失败的消息详细信息
             * @param i 回复的状态码 312对应No_Route 没有路由信息
             * @param s 回复的文本内容
             * @param s1 当时这个消息发送给那个交换机
             * @param s2 当时这个消息用的哪个路由键
             */
            @Override
            public void returnedMessage(Message message, int i, String s, String s1, String s2) {
                //报错误了,修改数据库当前状态-》错误状态
                System.out.println("message:"+message+",code:"+i+",content:"+s+",exchange:"+s1+",route-key:"+s2);
            }
        });
    }

}

手动签收消息,只要不手动确认消息,消息就会一直放在队列中;通过channel的basicAck等方法可以签收或者不签收消息

 @RabbitHandler//只能放在方法上
    //channel是传输数据的通道
    // Queue可以有很多人来监听,同一个消息只能有一个收到,只有一个消息的方法处理完了,才能继续处理下一条消息
    public void receiveMessage(Message message, OrderEntity orderEntity, Channel channel){//可以直接获取消息体的对象
        byte[] body=message.getBody();//消息体 我们发送的对象
        MessageProperties messageProperties = message.getMessageProperties();//消息头
        System.out.println("收到消息:"+message+"==>Type:"+message.getClass()+"内容:"+orderEntity);
        //消费者处理完消息
        long deliveryTag = message.getMessageProperties().getDeliveryTag();//这个tag是正在channel里按顺序自增的
        try {
            //传入tag-是否批量签收
            channel.basicAck(deliveryTag,false);//签收
            //channel.basicNack(deliveryTag,false,true);//拒签 是否批量签收-最后一个参数表示是否重新入队,为false直接丢弃了
            //channel.basicReject(deliveryTag,false);//拒签 没有批量参数 最后一个参数表示是否重新入队,为false直接丢弃了
        }catch (Exception e){
            //网络中断
        }
    }

延时队列实现定时任务

基本概念:

消息的TTL:消息的存活时间

RabbitMQ可以分别对队列和消息设置TTL:

  • 对队列设置就是队列没有消费者连着的保留时间,也可以对每一个单独的消息做设置,超过了这个时间,我们就认为消息死了,称之为死信
  • 如果队列设置了,消息也设置了,会取小的时间,所以一个消息如果被路由到不同的队列中,这个消息的死亡时间有可能不一样(因为队列的TTL不一致)。这里单讲单个消息的TTL,因为它才是实现延迟任务的关键,可以通过设置消息的expiration字段或者x-message-ttl属性来设置时间,两者都是一样的效果

以下情况消息会进入死信路由:

  • 一个消息被Consumer拒收了,并且reject方法的参数里requeue是false。也就是说不会被再次放在队列里,被其他消费者使用
  • 上面消息的TTL到了,消息过期了
  • 队列的长度被限制满了,排在前面的消息被丢弃或者扔到死信路由上

我们既可以控制消息在一段时间后变成死信,又可以控制变成死信的消息被路由指定到某一个交换机,结合二者,就可以实现一个延时队列,总结来说我们就是要使用死掉的消息来完成延时队列

一般设定队列的TTL来完成延时队列功能,如果设置消息延时的话,假如有三个消息分别是5分钟,1分钟,2分钟,由于RMQ的惰性检查机制,他检查第一个消息发现是五分钟后过期,服务器就会五分钟之后再过来取走消息,导致后面两个短时间的消息都要五分钟后才能取出来;

测试流程:

订单模块创建相应的交换机,绑定关系和队列\

其中的死信路由是order-event-exchange,死信路由键是order.release.order这两者都是在延迟队列orderDelayQueue中设置的;

package com.wuyimin.gulimall.order.config;
/**
 * @ Author wuyimin
 * @ Date 2021/8/29-15:59
 * @ Description
 */
@Configuration
public class MyMQConfig {
    @RabbitListener(queues = "order.release.order.queue")//消费者
    public void listener(OrderEntity orderEntity, Channel channel, Message message) throws IOException {
        System.out.println("收到过期的订单信息:准备关闭订单"+orderEntity);
        //手动签收消息(拿到原生消息,选择不批量告诉)
        channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
    }
    //创建绑定关系,队列和交换机的便捷方式
    @Bean
    public Queue orderDelayQueue(){
        HashMap<String, Object> map = new HashMap<>();
        map.put("x-dead-letter-exchange","order-event-exchange");//死信路由
        map.put("x-dead-letter-routing-key","order.release.order");//死信的路由键
        map.put("x-message-ttl",60000);//消息过期时间一分钟
        //队列名字,是否持久化,是否排他(只能被一个连接使用),是否自动删除
        return new Queue("order.delay.queue",true,false,false,map);
    }
    @Bean
    public Queue orderReleaseOrderQueue(){
        return new Queue("order.release.order.queue",true,false,false);
    }
    @Bean
    public Exchange orderEventExchange(){
        //名字,是否持久化,是否自动删除 Topic交换机可以绑定多个队列
        return new TopicExchange("order-event-exchange",true,false);
    }
    @Bean
    //两个绑定关系
    public Binding orderCreateOrder(){
        return new Binding("order.delay.queue", Binding.DestinationType.QUEUE,
                "order-event-exchange","order.create.order",null);
    }
    @Bean
    public Binding orderReleaseOrder(){
        return new Binding("order.release.order.queue", Binding.DestinationType.QUEUE,
                "order-event-exchange","order.release.order",null);
    }
}

创建测试接口,试着给交换机发送信息模拟生产者

package com.wuyimin.gulimall.order.web;
 
/**
 * @ Author wuyimin
 * @ Date 2021/8/26-9:53
 * @ Description
 */
@Controller
public class HelloController {
    @Autowired
    RabbitTemplate rabbitTemplate;
    @ResponseBody
    @GetMapping("/test/createorder")
    public String createOrderTest(){
        OrderEntity orderEntity=new OrderEntity();
        orderEntity.setOrderSn(UUID.randomUUID().toString());
        orderEntity.setModifyTime(new Date());
        //给mq发送消息
        rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",orderEntity);
        return "ok";
    }
}
  • 消息先进入交换机然后根据路由键order.create.order进入延迟队列因为延迟队列没有消费者,所以所有的消息都会在一分钟后死信
  • 因为我们设置了队列的死信路由和死信路由键,它将会以order.release.order的死信路由重新交还给了交换机发送消息
  • 而此时之前我们的绑定关系order.release.order就起作用了,它将我们的消息路由到真正可以被消费者处理的队列order.release.order.queue
  • 最后经过我们的消费者手动签收了消息,于是完成了打印

分布式事务解决方案-seata

每个微服务要使用seata都需要一个undo_log表,并且数据库的引擎必须使用innodb引擎

1.undo_log表

-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  `ext` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

2.在common模块中导入依赖

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        </dependency>

3.下载seata-server与springboot相对应的版本

4.更改其配置文件,设置才注册中心为nacos

 5.添加全局事务注解

6.每个想要使用分布式事务的微服务都要使用seataSourceProxy来代理自己的数据源

package com.wuyimin.gulimall.order.config;
 
/**
 * @ Author wuyimin
 * @ Date 2021/8/29-11:55
 * @ Description Seata代理数据源配置
 */
@Configuration
public class MySeataConfig {
    @Autowired
    DataSourceProperties dataSourceProperties;
 
    @Bean
    public DataSource dataSource(DataSourceProperties dataSourceProperties) {
 
        HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
        if (StringUtils.hasText(dataSourceProperties.getName())) {
            dataSource.setPoolName(dataSourceProperties.getName());
        }
        return new DataSourceProxy(dataSource);
    }
}

7.在每个微服务中还需要file.config和registry.config两个文件,在file.config里手动配置当前server的信息

service {
  #vgroup->rgroup
  vgroupMapping.gulimall-ware-fescar-service-group = "default"
  #only support single node
  default.grouplist = "127.0.0.1:8091"
  #degrade current not support
  enableDegrade = false
  #disable
  disable = false
  #unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
  max.commit.retry.timeout = "-1"
  max.rollback.retry.timeout = "-1"
}

8.配置yml文件

如此配置之后,在方法上使用@GlobalTransactional注解,远程调用的情况下,当前函数事务失败,远程的事务也会回滚

要注意的是Seata比较适合于非高并发的场景比如后台管理系统里的保存操作,高并发场景建议使用消息队列来实现

Cookie,Session和Token

HTTP 协议是一种无状态协议,即每次服务端接收到客户端的请求时,都是一个全新的请求,服务器并不知道客户端的历史请求记录;Session 和 Cookie 的主要目的就是为了弥补 HTTP 的无状态特性;

何为session

当客户端请求服务端,服务端会为这次请求开辟一块内存空间,这个对象就是Session,它从存储结构为ConcurrentHashMap,Session弥补了HTTP的无状态特性,服务器可以利用Session存储客户端在同一个会话期间的一些操作记录

session如何判断是同一会话

服务器第一次接受到请求的时候,开辟了一块Session的空间(创建了对象),同时生成一个sessionid,通过响应头命令,向客户端发送要求设置Cookie的响应;客户端接收到响应之后,在本机客户端设置了一个Cookie信息,该Cookie的过期时间为浏览器会话结束,接下来每当客户端向同一个网址发送请求的时候,请求头都会带上该Cookie信息,然后,服务器通过读取请求头中的Cookie信息,获取名称为JSESSIONID的值,得到此次请求的sessionid

Session的缺点

Session机制有一个缺点,比如A服务器储存了Session,做了负载均衡之后,加入一段时间内A服务器访问量激增,会转发到B进行访问,但是B服务器没有储存A的Session,会导致Session的失效--解决方案就是redis+Session方案

何为Cookie

它是服务器发送到 Web 浏览器的一小块数据。服务器发送到浏览器的 Cookie,浏览器会进行存储,并与下一个请求一起发送到服务器。通常,它用于判断两个请求是否来自于同一个浏览器,例如用户保持登录状态。它有两种类型会话Cookie和持久性Cookie;

  • 会话Cookie不包含到期日期, 存储在内存中,不会写入到磁盘,当浏览器关闭的时候,此Cookie永远失效
  • 持久性Cookie包含到期日期,如果到达了指定的日期就会从磁盘中删除

Cookie的作用域

Domain和Path标识定义了它的作用域:即它应该发送给哪些URL

Domain标识了哪些主机可以接受Cookie,如果不指定,默认为当前主机(不包含子域名),如果指定了Domain,则一般包含子域名;举个例子如果Domain=abc.org那么Cookie也会被包含在子域111.abc.org中; 设置path=/123那么/123/456也会匹配

JWT和Session Cookies

JWT又称Json Web Token,它和Session一样都可以为网站提供用户的身份认证;因为HTTP是一个无状态的协议,也就意味着每当你访问某个网页,然后单机同一站点上的另一个页面的时候,服务器的内存中将不会记录你之前的操作,因此如果你登录并访问了你有权访问的另一个页面,由于Http不会记录你刚才的登录信息,所以你会被强制再次登录;这两个技术的相同点在于:能够支持你在发送不同请求之间,记录并验证你的登录状态的一种机制;

他们的不同之处在于:

  • JWT具有加密签名,但是Session Cookies没有
  • JWT是无状态的,因为声明被存储在客户端,而不是服务端内存中,,身份验证可以在本地进行,而不是请求必须通过服务器数据进行;这意味着可以对用户进行多次身份认证,而无需与站点进行通信;因此JWT节省了服务器资源相比于Session Cookies具备了更强的可扩展性
  • Session Cookies只能在单个节点的域或者它的子域中有效,如果他们尝试通过第三个节点访问就会被禁止,如果你希望自己的网站和其他站点建立一个安全连接时,它是一个问题;JWT可以解决这个问题,使用JWT能够通过多个节点进行用户认证,也就是跨域认证

禁用Cookie如何使用Session

如果禁用了 Cookies,服务器仍会将 sessionId 以 cookie 的方式发送给浏览器,但是,浏览器不再保存这个cookie (即sessionId) 了;如果想要继续使用 session,需要采用 URL重写 的方式来实现

请求和转发

地址栏

  • 转发:不变,不会显示出转向的地址
  • 重定向:会显示转向之后的地址

请求

  • 重定向:至少提交了两次请求

数据

  • 转发:对request对象的信息不会丢失,因此可以在多个页面交互过程中实现请求数据的共享
  • 重定向:request信息将丢失

原理

  • 转发(服务器行为):是在服务器内部控制权的转移,是由服务器区请求,客户端并不知道是怎样转移的,因此客户端浏览器的地址不会显示出转向的地址。
  • 重定向(浏览器/客户端行为):是服务器告诉了客户端要转向哪个地址,客户端再自己去请求转向的地址,因此会显示转向后的地址,也可以理解浏览器至少进行了两次的访问请求。

Feign远程调用丢失请求头的问题+List传参问题

丢失请求头问题

我们在登录order服务后,利用feign远程调用cart服务的api,但是cart服务给我们的回馈是我们没有登录,产生此问题的原因如下:

 解决方案如下:

package com.wuyimin.gulimall.order.config;
 
 
/**
 * @ Author wuyimin
 * @ Date 2021/8/26-16:55
 * @ Description
 */
@Configuration
public class OrderFeignConfig {
    //这个拦截器方法会在远程调用之前触发
    @Bean
    public RequestInterceptor requestInterceptor(){
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate template) {
                //源码里这个方法就是从threadLocal里拿
                ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                HttpServletRequest request = requestAttributes.getRequest();//这里获取到的是老请求
                //同步请求头数据--同步cookie
                template.header("Cookie",request.getHeader("Cookie"));//Feign创建的新请求
            }
        };
    }
}

在异步编排的操作之后,请求头又丢失了,这是因为RequestContextHolder使用ThreadLocal共享数据,所以在开启异步时获取不到老请求的信息

修改配置类代码,主要是增加非空判断

package com.wuyimin.gulimall.order.config;
 
/**
 * @ Author wuyimin
 * @ Date 2021/8/26-16:55
 * @ Description
 */
@Configuration
public class OrderFeignConfig {
    //这个拦截器方法会在远程调用之前触发
    @Bean
    public RequestInterceptor requestInterceptor(){
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate template) {
                //源码里这个方法就是从threadLocal里拿
                ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                if(requestAttributes!=null){
                    HttpServletRequest request = requestAttributes.getRequest();//这里获取到的是老请求
                    if(request!=null){
                        //同步请求头数据--同步cookie
                        template.header("Cookie",request.getHeader("Cookie"));//Feign创建的新请求
                    }
                }
            }
        };
    }
}

业务逻辑方法,我们先在主线程中吧threadLocal的值取出来,然后 调用子线程的如下方法

RequestContextHolder.setRequestAttributes(requestAttributes)
package com.wuyimin.gulimall.order.service.impl;
 
@Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
        OrderConfirmVo confirmVo = new OrderConfirmVo();
        //从拦截器里获得用户信息
        MemberRespVo loginUser = LoginUserInterceptor.loginUser.get();
        //主线程里先把threadLocal的值取出来,因为下面异步就取不到了
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        CompletableFuture<Void> futureAddress = CompletableFuture.runAsync(() -> {
            //远程查询用户地址信息
            //子线程里拿到父线程的threadLocal里储存的信息
            RequestContextHolder.setRequestAttributes(requestAttributes);
            List<MemberAddressVo> address = memberFeignService.getAddress(loginUser.getId());
            confirmVo.setMemberAddressVos(address);
        }, executor);
        CompletableFuture<Void> futureItems = CompletableFuture.runAsync(() -> {
            //远程查询用户的购物车
            RequestContextHolder.setRequestAttributes(requestAttributes);
            List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItems();
            confirmVo.setItems(currentUserCartItems);
        }, executor);
 
        //查询用户积分
        Integer integration = loginUser.getIntegration();
        confirmVo.setIntegration(integration);
        //其他数据自动计算
        //等待异步任务完成
        CompletableFuture.allOf(futureAddress,futureItems).get();
        return confirmVo;
    }

传参问题

SpringCloud中微服务之间的调用,传递参数时需要加相应的注解。用到的主要是三个注解@RequestBody,@RequestParam(),@PathVariable()

 

Get请求==>使用PathVariable和RequestParam来获取,在使用RequestParam获取单个值的时候一定要标明value的值

@GetMapping("/orders/{id}")
     public String getOrder(@PathVariable(value = "id")Integer id,
                            @RequestParam(value = "name")String name,
                           @RequestParam(value = "price",required = false,defaultValue = "0") Integer price){
        String result = "id:"+id+",name:"+name+",price:"+price;
        return result;
    }

Post请求==>使用RequestParam处理非Json格式的数据使用RequestBody来处理Json格式的数据,标明注解后,json会自动注入到实体类中(@ResponseBody注解将Java对象转成Json格式)

@PostMapping("/order/check")
public String checkOrder(@RequestBody Order order){
         String result = "id:"+order.getId()+",name:"+order.getName()+",price:"+order.getPrice();
         return result;
     }

Feign传递List类型参数

当请求有多个参数,并且其中一个参数是List的时候,靠Feign是传不过去的,但是单个List是可以传的,基本数据类型可以通数组的方式进行传递,实体类型可以通过FastJson转成String后进行传递

//调用方代码
String contracts = JSONObject.toJSONString(contractBOList);
contractDao.contractBatchSetRedis(contracts , 60 * 60);
 
//接收方代码
@PostMapping("/contract/contractBatchSetRedis")
void contractBatchSetRedis(@RequestParam("contractBOList") String contractBOList, @RequestParam("expire") long expire) {
    List<ContractBO> contracts = JSONObject.parseArray(contractBOList, ContractBO.class);
    if (contracts == null || contracts.size() == 0) {
         return;
    }
    //批量set数据
    redisUtil.getRedisTemplate().executePipelined((RedisCallback<String>) connection -> {
        for (ContractBO contract : contracts) {
            connection.setEx((RedisPrefixConst.CONTRACT_PREFIX + contract.getBusinessCode() + RedisPrefixConst.UNDERLINE_SEPARATOR + contract.getContractNo()).getBytes(), expire, JSONObject.toJSONString(contract).getBytes());
        }
        return null;
    });
}
 

接口幂等性处理

确认订单按钮必须是幂等的,在数据库层面首先将订单号设置成唯一约束unique

业务层面使用token机制,只有token核验通过才可以发送请求

token何时删除:

如果在业务结束以后删除,当两个请求速度很快进来了,那么他们都能创建订单

在业务结束前删除:前端带来一个token,如果相同就直接删除令牌,再调用业务逻辑,如果redis里的数据没来得及删除,也不能保证幂等性,所以获取令牌,对比和删除必须是一个原子操作,这里可以使用lua脚本完成这个操作

确认订单的逻辑:这里主要是设置了一个token,并且储存进了redis

package com.wuyimin.gulimall.order.service.impl;
 
    @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
       xxxx....
        //其他数据自动计算
        //防重令牌
        String token= UUID.randomUUID().toString().replace("-","");
        redisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX+loginUser.getId(),token,30, TimeUnit.MINUTES);
        confirmVo.setOrderToken(token);
       xxxxx...
    }
 
}

在提交订单功能中:如果lua脚本执行结果返回1表示删除成功

@Transactional
    @Override
    public SubmitOrderResponseVo submitOrder(OrderSubmitVo orderSubmitVo) {
        threadLocal.set(orderSubmitVo);
        //下单:去创建订单,检验令牌,检验价格,锁定库存
        //1.验证令牌
        String orderToken = orderSubmitVo.getOrderToken();//页面传递过来的值
        MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
        String key = OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId();//redis里存的key
        //lua脚本保证原子性 返回1代表删除成功,0代表删除失败
        String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
        Long res = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(key), orderToken);
        if(res==1){
            //2.验证成功--下单创建订单,检验令牌,检验价格,锁库存
    
        }else{
            //验证失败
            return responseVo;
        }
    }

RabbitMQ的消息问题

消息丢失:

1)发送出去没抵达服务器

1.做好容错方法,(try-catch)包裹消息发送代码段

2.做好日志记录,每个消息状态是否被服务器收到都要记录

3.做好定期重发,如果消息没有发送成功,定期取数据库扫描未成功发送的消息重发

数据库建立表

   public class MqMessageEntity implements Serializable {
        private static final long serialVersionUID = 1L;
        @TableId
        private String messageId;
        /**
         * JSON
         */
        private String content;
        private String toExchange;
        private String classType;
        /**
         * 0-新建 1-已发送 2-错误抵达 3-已抵达
         */
        private Integer messageStatus;
        private Date createTime;
        private Date updateTime;
    }

2)消息抵达Broker,写入磁盘的时候宕机

publisher加入确认回调机制,确认成功的消息,修改数据库消息状态

3)自动ACK状态下,消费者收到消息,单没来的及处理就宕机了

开启手动ACK,消费成功才移除,失败的消息重新入队

消息重复:

成功消费,ack的时候宕机,消息由unack变成ready,Broker又重新发送:

  •     消费者的业务消费接口应该设计为幂等性的
  •     使用防重表,发送消息每一个都有业务的唯一标识,处理过就不用继续处理
  •     MQ每个消息都有receive字段,可以获取消息是否是被重新投递过来的信息

消息挤压:

消费者宕机挤压

消费者消费能力不足

发送者发送流量过大:

    上线更多的消费者,进行正常消费
    上线专门的队列消费服务,将消息先批量取出来,记录数据库,离线慢慢处理

支付宝沙箱支付

简介

支付宝使用的是RSA非对称加密:

非对称加密算法需要两个密钥:公开密钥(publickey)和私有密钥(privatekey)。公开密钥与私有密钥是一对,如果用公开密钥对数据进行加密,只有用对应的私有密钥才能解密;如果用私有密钥对数据进行加密,那么只有用对应的公开密钥才能解密。因为加密和解密使用的是两个不同的密钥,所以这种算法叫做非对称加密算法。

商户端和支付宝端的情况:

整合

导入依赖:

        <dependency>
            <groupId>com.alipay.sdk</groupId>
            <artifactId>alipay-sdk-java</artifactId>
            <version>4.9.28.ALL</version>
        </dependency>

使用支付宝工具类AlipayTemplate

package com.wuyimin.gulimall.order.config;
 
@ConfigurationProperties(prefix = "alipay")
@Component
@Data
public class AlipayTemplate {
 
    //在支付宝创建的应用的id
    private   String app_id = "2016092200568607";
 
    // 商户私钥,您的PKCS8格式RSA2私钥
    private  String merchant_private_key = "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCSgX/nTQ0lD+S8ObaM5LGZ1hiz18GXnNpqPLhJCym4xOpn35FNPHrPkDGEoMKrZ5LJeA4cZulckD8AtpvBCpeyIkrj/i1WVmSg10hVX67MlVets4UecCHZv2hKAN0/iId76kozdqrd7Csp/YgXPquN9Np0NFotggTrmiBANk+vcpTF9SCGrDq/isOoCvClfbvVJjApfLLOel3yECe5K/SZ8puiWILVm1NxEXAqJ8z0ipPZVGrXsT6Bo0pEyCPcEL0SqaC9WT0zdWQzdUknCzZV9W2wKjEXBJG9hqxay5kPaKm9leBatSkDAaDxH/N5g36HRfY7BmklwRZsp17lHinxAgMBAAECggEAfnnfck35WBKFc90a9D0F+Xlzr+ZGEV3uzKIIsb46UXFlrzC5HoVkvEWOCiJCjHiIpvbGr8xED43TZgk/IwLC/JxQLM0kVJGWo6fWoSVOIP2YSLNe620APBvaq3BdkFiMJfSYBB+g2J7mkIR39SE8Nvu3j3QWmYzSNJbE2spINnwTzNBL1OPaB5h3hSjyI07KaUcOjhTBF0EZl83NlBDsxmQvy0NmuOIWAcIXXvGoIbwkA774J3LhwL+VS4W2FpQj4FlxvDlPu24GeNWN7oO66T3Jp9bweO120ObhuKwZQosDGkJq0975zVSJX5QtUWHMM/QDPO8Pk24n2AoPcACQcQKBgQDS6kqD+sK8dDBpkmxYopA1gJJATnur0RHFZJb5webOhnEZnePhB1hhhGvKFcrdY2hcYeQiUZkHMsnWItNUe9E9ccp4++m6KKG0iV/BQda7zx1zMTTZUMvSbO282Q31YnQu7Yz6BSk4f/U5Qbu61AK53Tv1ejSAgQhXt1Pwq8KD7QKBgQCx0pkqW4+53tY2o4iPqFGjKYI2yk5bAH5etmOvW51OZ4Slsq/aUJKBVG6fOpRVKkiXulHhrp5csZH0/C7kaj4Hy7TjgUKSWvwlv7i7jgN0dq/bhVJz82y+N9pENWvy5J0I8Kt67XH+6JDEGWjlV58auifMRSx5mRJNn5pM6qrFlQKBgFyZWm/JV1fv1xVyoLjlXlTvBsbO7kMH/jpgqFwtAk1n/x3VEShJ1kayIbTOjotWSopMvCFJG9tqM+0cyxWLatkELXWifAIsNpqRuYWah1FbZD2fu+kxLNtM0a+YyCUUvZeg2cUnIOraWupxbp9e13eMpvdmWMiWXfhM18CRWEwdAoGAUwT0l076EhgUQJwm1JML0jY94eCfpmLbnNJgRe1qysEPr+B1s2IslA7cOqC5we0kyRmmwsuoibQpZYwbRG7JmRAk2pZtgzDRSbpxv7a0rDoBLmbXMOU0Hraqw2+Bf3v2SMc79/9FWnIvrC4EyBYZZPwGOpsNAZRSdEUQX9qrceUCgYB99OOtFFt1ixzyTCyUj3Fuiw7BsPhdI3nuMSoNTPIDNpzRBp/KFXyv/FNJ2CjTAsX3OR3D6KmEYihqUfrYeb0P5zoybcQLMxbXxK+ec6F2o6U2iqFIq0MKwHUqsb9X3pj4qE0ZHbFgRtIHnL2/QGV5PFJdmIZIBKZcvB8fW6ztDA==";
    // 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥。
    private  String alipay_public_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyQQceVUChTJGtF/a8SXufhSxDTKporieTq9NO7yDZSpDlAX1zVPT/nf0KWAlxq1TYappWMIYtyrOABhJyn6flNP6vuSBiM5lYsepHvYrtRHqlFiJruEkiaCgEZBKL5aCfBHYj0oqgQn9MpNV/PEH4cBYAVaiI4+VX8CBUQfeEGjgN6OkpLULZ3X0JUkmSnVvCNJ1m3PD68IIlbOfEZXJUKCqmZhzprGR5VWswjxA+g87cMwvijL4gdkSy/daG62Bz5vApcmmMkuX1k1fMWP4ajZCASVw8HD+MSLRhd8We9F97gd8CW0TavzbdR+mTS5H4yEgO8F9HRAsbkhV9yu0yQIDAQAB";
    // 服务器[异步通知]页面路径  需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
    // 支付宝会悄悄的给我们发送一个请求,告诉我们支付成功的信息
    private  String notify_url;
 
    // 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
    //同步通知,支付成功,一般跳转到成功页
    private  String return_url;
 
    // 签名方式
    private  String sign_type = "RSA2";
 
    // 字符编码格式
    private  String charset = "utf-8";
 
    // 支付宝网关; https://openapi.alipaydev.com/gateway.do
    private  String gatewayUrl = "https://openapi.alipaydev.com/gateway.do";
 
    public  String pay(PayVo vo) throws AlipayApiException {
 
        //AlipayClient alipayClient = new DefaultAlipayClient(AlipayTemplate.gatewayUrl, AlipayTemplate.app_id, AlipayTemplate.merchant_private_key, "json", AlipayTemplate.charset, AlipayTemplate.alipay_public_key, AlipayTemplate.sign_type);
        //1、根据支付宝的配置生成一个支付客户端
        AlipayClient alipayClient = new DefaultAlipayClient(gatewayUrl,
                app_id, merchant_private_key, "json",
                charset, alipay_public_key, sign_type);
 
        //2、创建一个支付请求 //设置请求参数
        AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
        alipayRequest.setReturnUrl(return_url);
        alipayRequest.setNotifyUrl(notify_url);
 
        //商户订单号,商户网站订单系统中唯一订单号,必填
        String out_trade_no = vo.getOut_trade_no();
        //付款金额,必填
        String total_amount = vo.getTotal_amount();
        //订单名称,必填
        String subject = vo.getSubject();
        //商品描述,可空
        String body = vo.getBody();
 
        alipayRequest.setBizContent("{\"out_trade_no\":\""+ out_trade_no +"\","
                + "\"total_amount\":\""+ total_amount +"\","
                + "\"subject\":\""+ subject +"\","
                + "\"body\":\""+ body +"\","
                + "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}");
 
        String result = alipayClient.pageExecute(alipayRequest).getBody();
 
        //会收到支付宝的响应,响应的是一个页面,只要浏览器显示这个页面,就会自动来到支付宝的收银台页面
        System.out.println("支付宝的响应:"+result);
 
        return result;
 
    }
}

调用此方法,直接通过@GetMapping的produces属性声明返回的是一个页面

package com.wuyimin.gulimall.order.web;
 
/**
 * @ Author wuyimin
 * @ Date 2021/8/30-19:39
 * @ Description
 */
@Controller
public class PayWebController {
    @Autowired
    AlipayTemplate alipayTemplate;
    @Autowired
    OrderService orderService;
    @ResponseBody//返回数据带有json必须要
    @GetMapping(value = "/payOrder",produces = "text/html")
    public String payOrder(@RequestParam("orderSn") String orderSn) throws AlipayApiException {
        PayVo payVo =orderService.getOrderBySn(orderSn);
        //返回的是一个页面 直接把页面交给浏览器就行
        String pay = alipayTemplate.pay(payVo);
        System.out.println(pay);
        return pay;
    }
}

异步通知

  • 订单支付成功后支付宝会回调商户接口,这个时候需要修改订单状态
  • 由于同步跳转可能由于网络问题失败,所以使用异步通知
  • 支付宝使用的是最大努力通知方案,保障数据一致性,隔一段时间会通知商户支付成功,直到返回success

设置回调和通知url,通知的url必须是外网可以正常访问

 在支付宝异步通知后会返回给我们一个map的参数

收到支付宝异步通知******************
key:gmt_create===========>value:2020-10-18 09:13:26
key:charset===========>value:utf-8
key:gmt_payment===========>value:2020-10-18 09:13:34
key:notify_time===========>value:2020-10-18 09:13:35
key:subject===========>value:华为
key:sign===========>value:aqhKWzgzTLE84Scy5d8i3f+t9f7t7IE5tK/s5iHf3SdFQXPnTt6MEVtbr15ZXmITEo015nCbSXaUFJvLiAhWpvkNEd6ysraa+2dMgotuHPIHnIUFwvdk+U4Ez+2A4DBTJgmwtc5Ay8mYLpHLNR9ASuEmkxxK2F3Ov6MO0d+1DOjw9c/CCRRBWR8NHSJePAy/UxMzULLtpMELQ1KUVHLgZC5yym5TYSuRmltYpLHOuoJhJw8vGkh2+4FngvjtS7SBhEhR1GvJCYm1iXRFTNgP9Fmflw+EjxrDafCIA+r69ZqoJJ2Sk1hb4cBsXgNrFXR2Uj4+rQ1Ec74bIjT98f1KpA==
key:buyer_id===========>value:2088622954825223
key:body===========>value:上市年份:2020;内存:64G
key:invoice_amount===========>value:6300.00
key:version===========>value:1.0
key:notify_id===========>value:2020101800222091334025220507700182
key:fund_bill_list===========>value:[{"amount":"6300.00","fundChannel":"ALIPAYACCOUNT"}]
key:notify_type===========>value:trade_status_sync
key:out_trade_no===========>value:12345523123
key:total_amount===========>value:6300.00
key:trade_status===========>value:TRADE_SUCCESS
key:trade_no===========>value:2020101822001425220501264292
key:auth_app_id===========>value:2016102600763190
key:receipt_amount===========>value:6300.00
key:point_amount===========>value:0.00
key:app_id===========>value:2016102600763190
key:buyer_pay_amount===========>value:6300.00
key:sign_type===========>value:RSA2
key:seller_id===========>value:2088102181115314

我们通过一个vo来封装这个参数

package com.wuyimin.gulimall.order.vo;
 
@ToString
@Data
public class PayAsyncVo {
 
    private String gmt_create;
    private String charset;
    private String gmt_payment;
    private String notify_time;
    private String subject;
    private String sign;
    private String buyer_id;//支付者的id
    private String body;//订单的信息
    private String invoice_amount;//支付金额
    private String version;
    private String notify_id;//通知id
    private String fund_bill_list;
    private String notify_type;//通知类型; trade_status_sync
    private String out_trade_no;//订单号
    private String total_amount;//支付的总额
    private String trade_status;//交易状态  TRADE_SUCCESS
    private String trade_no;//流水号
    private String auth_app_id;//
    private String receipt_amount;//商家收到的款
    private String point_amount;//
    private String app_id;//应用id
    private String buyer_pay_amount;//最终支付的金额
    private String sign_type;//签名类型
    private String seller_id;//商家的id
}

让一个controller监听支付成功的信息:其逻辑如下

package com.wuyimin.gulimall.order.listener;
@RestController
public class OrderPayedListener {
    @Autowired
    OrderService orderService;
 
    @Autowired
    AlipayTemplate alipayTemplate;
 
    @PostMapping("/payed/notify")
    public String handleAlipayed(PayAsyncVo vo, HttpServletRequest request) throws AlipayApiException, UnsupportedEncodingException {
        //验签--是不是支付宝返回的数据(别人有可能模拟数据)
        Map<String, String> params = new HashMap<String, String>();
        Map<String, String[]> requestParams = request.getParameterMap();
        for (Iterator<String> iter = requestParams.keySet().iterator(); iter.hasNext(); ) {
            String name = (String) iter.next();
            String[] values = (String[]) requestParams.get(name);
            String valueStr = "";
            for (int i = 0; i < values.length; i++) {
                valueStr = (i == values.length - 1) ? valueStr + values[i]
                        : valueStr + values[i] + ",";
            }
            params.put(name, valueStr);
        }
        boolean signVerified = AlipaySignature.rsaCheckV1(params, alipayTemplate.getAlipay_public_key(), alipayTemplate.getCharset(), alipayTemplate.getSign_type()); //调用SDK验证签名
        if (signVerified) {
            System.out.println("签名验证成功");
            String res = orderService.handlePayResult(vo);
            return res;
        } else {
            return "error";
        }
    }
}

handlePayResult方法:

package com.wuyimin.gulimall.order.service.impl;
@Override
    public String handlePayResult(PayAsyncVo vo) {
        //1.保存交易流水
        PaymentInfoEntity paymentInfoEntity = new PaymentInfoEntity();
        paymentInfoEntity.setAlipayTradeNo(vo.getTrade_no());
        paymentInfoEntity.setOrderSn(vo.getOut_trade_no());
        paymentInfoEntity.setPaymentStatus(vo.getTrade_status());
        paymentInfoEntity.setCallbackContent(vo.getNotify_time());
        paymentInfoService.save(paymentInfoEntity);
        //2.修改订单的状态信息
        if (vo.getTrade_status().equals("TRADE_SUCCESS")||vo.getTrade_status().equals("TRADE_FINISHED")){
            String outTradeNo = vo.getOut_trade_no();
            this.updateOrderStatus(outTradeNo,OrderStatusEnum.PAYED.getCode());
        }
        return "success" ;
    }

收单操作

因为可能出现订单已经过期后,库存已经解锁,但是支付成功后再修改订单的情况,因此需要设置支付有效时间,只能在有效期内完成支付

alipayRequest.setBizContent("{\"out_trade_no\":\""+ out_trade_no +"\","
        + "\"total_amount\":\""+ total_amount +"\","
        + "\"subject\":\""+ subject +"\","
        + "\"body\":\""+ body +"\","
        //设置过期时间为1m
        +"\"timeout_express\":\"1m\","
        + "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}");

定时任务

cron表达式

 
Cron表达式参数分别表示:
 
秒(0~59) 例如0/5表示每5秒
分(0~59)
时(0~23)
日(0~31)的某天,需计算
月(0~11)
周几( 可填1-7 或 SUN/MON/TUE/WED/THU/FRI/SAT)
@Scheduled:除了支持灵活的参数表达式cron之外,还支持简单的延时操作,例如 fixedDelay ,fixedRate 填写相应的毫秒数即可。
// Cron表达式范例:
 
每隔5秒执行一次:*/5 * * * * ?
 
每隔1分钟执行一次:0 */1 * * * ?
 
每天23点执行一次:0 0 23 * * ?
 
每天凌晨1点执行一次:0 0 1 * * ?
 
每月1号凌晨1点执行一次:0 0 1 1 * ?
 
每月最后一天23点执行一次:0 0 23 L * ?
 
每周星期天凌晨1点实行一次:0 0 1 ? * L
 
在26分、29分、33分执行一次:0 26,29,33 * * * ?
 
每天的0点、13点、18点、21点都执行一次:0 0 0,13,18,21 * * ?

整合springBoot

package com.wuyimin.gulimall.seckill.scheduled;
 
/**
 * @ Author wuyimin
 * @ Date 2021/8/31-16:54
 * @ Description
 */
@Slf4j
@Component
@EnableScheduling
public class HelloSchedule {
    //只允许六位,不允许第七位的年份
    @Scheduled(cron = "* * * * * ?")//每秒打印
    public void hello(){
        log.info("hello");
    }
}

定时任务默认阻塞,开发中定时任务不应该被阻塞,可以采用注解@Async注解并且在主类上添加@EnableAsync注解启用异步任务的解决方式

@Slf4j
@Component
@EnableScheduling
@EnableAsync
public class HelloSchedule {
    //只允许六位,不允许第七位的年份
    @Async
    @Scheduled(cron = "* * * * * ?")//每秒打印
    public void hello() throws InterruptedException {
        log.info("hello");
        Thread.sleep(3000);
    }
}

时间和日期的处理

  package com.wuyimin.gulimall.coupon.service.impl;
private String getStartTime(){
        LocalDate now = LocalDate.now();//拿到当前时间
        LocalTime min = LocalTime.MIN;//00:00
        return LocalDateTime.of(now, min).format(DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss"));
    }
    private String getEndTime(){
        LocalDate now = LocalDate.now();//拿到当前时间
        LocalDate plusDays = now.plusDays(2);//加两天
        LocalTime max = LocalTime.MAX;//23:59.9999
        return LocalDateTime.of(plusDays, max).format(DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss"));
    }

使用Redisson分布式信号量

引入依赖

 <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.13.4</version>
        </dependency>

配置分布式锁,后续直接通过redisClient对象操作锁相关的东西

package com.wuyimin.gulimall.seckill.config;
 
/*
 * @ Author wuyimin
 * @ Date 2021/8/17-9:42
 * @ Description 分布式锁配置文件
 */
@Configuration
public class RedissonConfig {
    // redission通过redissonClient对象使用 // 如果是多个redis集群,可以配置
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redisson() {
        Config config = new Config();
        // 创建单节点模式的配置
        config.useSingleServer().setAddress("redis://192.168.116.128:6379");
        return Redisson.create(config);
    }
}

引入Sentinel

整合sentinel

导入依赖

<!-- 可以把他配置到common中-->
<!--sentinel和actuator用于流量监控和统计-->
<dependency><!--流量监控-->
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency><!--流量统计-->
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

查看核心包版本,根据版本下载dashboard控制台,使用如下命令打开控制台

java -jar sentinel-dashboard-1.8.1.jar --server.port=8333

在项目里配置控制台的信息

# 3项sentinel配置
# sentinel控制台地址
spring.cloud.sentinel.transport.dashboard=localhost:8333
spring.cloud.sentinel.transport.port=8719
# 暴露所有监控端点,使得sentinel可以实时监控
management.endpoints.web.exposure.include=*

Sentinel的使用

1.定义资源

2.定义规则

添加流量控制规则:

Field说明默认值
resource资源名
count限流阈值
grade

QPS模式(每秒请求数)或者并发线程数模式

(当调用该api的线程数达到阈值的时候,进行限流)

QPS
limitApp流控针对的调用来源不区分来源
strategy直接,链路,关联直接
controlBehavior直接拒绝/排队等待/慢启动模式直接拒绝
clusterMode是否集群限流(n台机器共同参与总数x的限流,平均下来是x/n)

在请求频率超过预设频率的时候,就会提示Blocked by Sentinel,这时连方法都不会被调用了

  •  直接:api达到限流条件时,直接限流。分为QPS和线程数
  •  关联:当关联的资到阈值时,就限流自己。别人惹事,自己买单。当两个资源之间具有资源争抢或者依赖关系的时候,这两个资源便具有了关联。,举例来说,read_db 和 write_db 这两个资源分别代表数据库读写,我们可以给 read_db 设置限流规则来达到写优先的目的:设置 strategy 为 RuleConstant.STRATEGY_RELATE 同时设置 refResource 为 write_db。这样当写库操作过于频繁时,读数据的请求会被限流。
  •  链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就进行限流)【api级别的针对来源】
  • 快速失败:直接拒绝。当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出FlowException
  •  warm up:若干秒后才能达到阈值。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮
  • 排队等待:让请求以均匀的速度通过
     

3.检测规则是否生效

自定义流控响应--实现BlockExceptionHandler的handler方法

@Component
public class MyConfig implements BlockExceptionHandler {
 
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws Exception {
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setContentType("application/json");
        httpServletResponse.getWriter().write(JSON.toJSONString("服务器流量错误:9999"));
    }
}

熔断降级--服务降级后自动调用熔断方法

开启调用方的sentinel功能

指定远程调用的fallback属性

@FeignClient(value = "gulimall-product",fallback = FallBackTest.class)
public interface TestFeign {
    @GetMapping("/hellogulimall")
    String hello();
}

编写快速返回的方法

@Component//需要加入到容器中,因为是使用对象来调用的
public class FallBackTest implements TestFeign {
 
    @Override
    public String hello() {
        return "远程调用失败了,这是我自定义的熔断保护错误";
    }
}

 可以指定降级规则:其中分为三种策略

RT基于响应时间的降级,一秒内持续进入5个请求他们的平均响应时间如果大于阈值就会触发降级机制,等待到时间窗口结束之后,降级又会被关闭

 自定义受保护的资源

达到默认限流之后会抛出一个异常,可以使用blockHandler属性来调用指定限流回调的方法,两者的返回值必须一样

    @SentinelResource(value="getFeignResource",blockHandler = "blockHandler")
    @GetMapping("/feign")
    public String test2() {
        System.out.println("执行了正常方法");
        String hello = "";
        hello = testFeign.hello();
        return hello;
    }
    //返回值必须一样,参数可以使用BlockException
    public String blockHandler(BlockException e) {
        System.out.println("限流方法被执行");
        return "限流方法被执行";
    }

Sentinel网关流控

导入网关流控依赖

<!-- 引入sentinel网关限流 -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
    <version>2.1.0.RELEASE</version>
</dependency>

可以在网关层面控制所有的请求流量和降级

 也可以自定义网关流控返回

 Sleuth+Zipkin服务链路追踪

span:跨度,基本的工作单元,发送一个远程调度任务就会产生一个span

trace:跟踪,一系列span组成的一个树状结构,请求一个微服务系统的api接口,这个api接口需要调用多个微服务,调用每个微服务都会产生一个新的span,所有由这个请求产生的span组成了这个trace

annotation:标注,用来及时记录一个事件的,一些核心注解用来定义一个请求的开始和结束。这些注解包括以下:

  • cs-Client Sent --客户端发送一个请求,这个注解描述了这个span的开始
  • sr-Server Received --服务端获得请求并准备开始处理它,如果将其sr减去cs时间戳就可以获得网络传输的时间
  • ss-Server Sent 服务端发送响应 -该注解表面请求处理的完成(当请求返回客户端),如果ss减去sr时间戳就可以获得服务器请求的时间
  • cr-Client Received 客户端接受响应-此时span结束,如果cr时间戳减去cs时间戳就可以获得整个请求消耗的时间

整合springBoot

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-sleuth</artifactId>
        </dependency>

docker安装zipkin

docker run -d 9411:9411 openzipkin/zipkin

添加配置

 docker持久化

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐