个人项目学习笔记

Maven工程聚合

pom: 表示该工程是一个聚合工程

jar 和war 可不写

1. 实体类的设计

所有的实体类都不需要设置id属性,将id,创建时间,修改时间和逻辑删除抽离出来,所有实体类都继承它

@Data
public class BaseEntity implements Serializable {

    @ApiModelProperty(value = "id")
    @TableId(type = IdType.AUTO)
    private Long id;

    @ApiModelProperty(value = "创建时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @TableField("create_time")
    private Date createTime;

    @ApiModelProperty(value = "更新时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @TableField("update_time")
    private Date updateTime;

    @ApiModelProperty(value = "逻辑删除(1:已删除,0:未删除)")
    @JsonIgnore
    @TableLogic
    @TableField("is_deleted")
    private Integer isDeleted;
}

商品分类

spu为荣耀70 , 根据销售属性来确定 sku 也就是具体的某一款 (比如需要什么颜色,版本,内存)

image-20220923151645597

1.4 基本信息—spu与 sku

SKU=Stock Keeping Unit(库存量单位)。即库存进出计量的基本单元,可以是以件,盒,托盘等为单位。SKU这是对于大型连锁超市DC(配送中心)物流管理的一个必要的方法。现在已经被引申为产品统一编号的简称,每种产品均对应有唯一的SKU**。

SPU(Standard Product Unit):标准化产品单元。是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。

首先通过检索搜索出来的商品列表中,每个商品都是一个sku。每个sku都有自己独立的库存数。也就是说每一个商品详情展示都是一个sku。

如上图,一般的电商系统你点击进去以后,都能看到这个商品关联了其他好几个类似的商品,而且这些商品很多的信息都是共用的,比如商品图片,海报、销售属性等。

那么系统是靠什么把这些sku识别为一组的呢,那是这些sku都有一个公用的spu信息。而它们公共的信息,都放在spu信息下。

所以,sku与spu的结构如下:

图中有两个图片信息表,其中spu_image表示整个spu相关下的所有图片信息,而sku_image表示这个spu下的某个sku使用的图片。sku_image中的图片是从spu_image中选取的。

每组sku的图片都是在spu下面,单击某个sku就会从spu中获取该sku图片并且展示

但是由于一个spu下的所有sku的海报都是一样,所以只存一份spu_poster就可以了。

根据spu制作sku

spu:表示一组商品的最小聚合信息

sku:表示库存量单位

image-20220923151710959

2.构建springboot项目

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4LYDQDt6-1663918453440)(C:\Users\caituxinchu\AppData\Roaming\Typora\typora-user-images\image-20220907152240086.png)]

类加载扫描

@Configuration @Component 注解都需要被扫描 使用@ComponentScan({“com.tanyang.common”}) 来扫描路径

@Configuration 包含@Component

@Configuration类中可以使用@MapperScan(“扫描路径”) 可以扫描mapper

image-20220923151351375

@EnableDiscoveryClient 将服务注册并且将注册了的服务全部拉取下来

只是想从注册中心拉取服务,我们只需要引导类上的注解改成@EnableDiscoveryClient(autoRegister = false)。

@GetMapping("/getCategory2/{category1Id}")
public Result getCategory2(@PathVariable(value = "category1Id") Long id){   //变量和路径变量不一致可以这样写
    
}

swagger

<!--swagger-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
        </dependency>

        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
        </dependency>

import com.google.common.base.Predicates;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.service.Parameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

import java.util.ArrayList;
import java.util.List;

/**
 * Swagger2配置信息
 */
@Configuration
@EnableSwagger2
public class Swagger2Config {

    @Bean
    public Docket webApiConfig(){

        //添加head参数start
        List<Parameter> pars = new ArrayList<>();
        ParameterBuilder tokenPar = new ParameterBuilder();
        tokenPar.name("userId")
                .description("用户ID")
                .defaultValue("1")
                .modelRef(new ModelRef("string"))
                .parameterType("header")
                .required(false)
                .build();
        pars.add(tokenPar.build());

        ParameterBuilder tmpPar = new ParameterBuilder();
                tmpPar.name("userTempId")
                .description("临时用户ID")
                .defaultValue("1")
                .modelRef(new ModelRef("string"))
                .parameterType("header")
                .required(false)
                .build();
        pars.add(tmpPar.build());
        //添加head参数end

        return new Docket(DocumentationType.SWAGGER_2)
                .groupName("webApi谭阳")
                .apiInfo(webApiInfo())
                .select()
                //过滤掉admin路径下的所有页面
                .paths(Predicates.and(PathSelectors.regex("/api/.*")))
                //过滤掉所有error或error.*页面
                //.paths(Predicates.not(PathSelectors.regex("/error.*")))
                .build()
                .globalOperationParameters(pars);

    }

    @Bean
    public Docket adminApiConfig(){

        return new Docket(DocumentationType.SWAGGER_2)
                .groupName("adminApi")
                .apiInfo(adminApiInfo())
                .select()
                //只显示admin路径下的页面
                .paths(Predicates.and(PathSelectors.regex("/admin/.*")))
                .build();

    }

    private ApiInfo webApiInfo(){

        return new ApiInfoBuilder()
                .title("网站-API文档")
                .description("本文档描述了网站微服务接口定义")
                .version("1.0")
                .contact(new Contact("Helen", "https://blog.csdn.net/qq_50629351?spm=1000.2115.3001.5343", "2115653270@qq.com"))
                .build();
    }

    private ApiInfo adminApiInfo(){

        return new ApiInfoBuilder()
                .title("后台管理系统-API文档")
                .description("本文档描述了后台管理系统微服务接口定义")
                .version("1.0")
                .contact(new Contact("Helen", "https://blog.csdn.net/qq_50629351?spm=1000.2115.3001.5343", "2115653270@qq.com"))
                .build();
    }


}

3.Mapper配置与使用

如果只有一个参数 可以直接写@Param ,但是有多个参数则必须指定value = “” ,这个就是xml文件的变量

  List<BaseAttrInfo> selectAttrInfoList(@Param(value = "category1Id") Long category1Id,
                                          @Param(value = "category2Id") Long category2Id,
                                          @Param(value = "category3Id") Long category3Id);
mybatis-plus:
  mapper-locations: classpath:mapper/*Mapper.xml       指定到类路径下取名为*Mapper 比如userMapper
mapper.xml的使用
<where>标签中 能够自动判断 当第一个语句成立并且前面有or会自动去除 
   比如: 这个第一个<if>中的 or会去除
 <where>
           <if test="category1Id !=null and category1Id !=0">
             or  (bai.category_level=1 and bai.category_id=#{category1Id})
           </if>
           <if test="category2Id !=null and category2Id !=0">
               or  (bai.category_level=1 and bai.category_id=#{category2Id})
           </if>
           <if test="category3Id !=null and category3Id !=0">
               or  (bai.category_level=1 and bai.category_id=#{category3Id})
           </if>

       </where>
结果集映射:
column: 返回数据字段名 也就是数据库字段,但是当给sql字段取别名的时候返回的就是别名
property: 实体属性名
autoMapper = "true"  自动映射  如果column和property相同的字段则无需指明 会自动映射
<!--   结果集处理-->
<resultMap id="baseAttrInfoMap" type="com.atguigu.gmall.model.product.BaseAttrInfo" autoMapping="true">
    <id column="id" property="id"></id>

<!--    property 实体类属性名
        ofType 实体类属性对应的类型
-->
    <collection property="attrValueList" ofType="com.atguigu.gmall.model.product.BaseAttrValue" autoMapping="true">
        <id column="attr_value_id" property="id"></id>
    </collection>

</resultMap>

异常

一般分两类,运行时异常和检查时异常。

checked 异常就是经常遇到的IO异常,以及SQL异常等等,对于这种异常,编译器强制要求我们去try/catch。

而对于runtime exception,我们可以不处理,比如:我们从来没有人去处理过NullPointerException异常,它就是运行时异常,这还是个最常见的异常之一。

@Transactional: 
    默认的配置方式:只能对运行时异常回滚。
 对于IOException 和SQLExceoption 这种检查时异常无法回滚
 所以我们需要指定回滚异常为Exception
 @Transactional(rollbackFor = Exception.class)

添加修改整合为一个接口

image-20220923151828793

 /**
     * 新增和修改品台属性 具体逻辑根据attr_id判断 存在就是修改 不存在就是新增
     * 在Mp中新增 主键自增会自动封装在实体类中
     * @param baseAttrInfo
     */
    @Override
    public void saveAttrInfo(BaseAttrInfo baseAttrInfo) {

//      判断当前操作是保存还是修改
        if (baseAttrInfo.getId() != null){
//         修改平台属性
            baseAttrInfoMapper.updateById(baseAttrInfo);

           //修改:通过先删除{baseAttrValue},再新增的方式!
           //删除条件: baseAttrValue.attrId = baseAttrInfo.id
            QueryWrapper<BaseAttrValue> baseAttrValueQueryWrapper = new QueryWrapper<>();
            baseAttrValueQueryWrapper.eq("attr_id",baseAttrInfo.getId());
            baseAttrValueMapper.delete(baseAttrValueQueryWrapper);
        }else{
//          保存平台属性
            baseAttrInfoMapper.insert(baseAttrInfo);
        }

        //操作平台属性
        List<BaseAttrValue> attrValueList = baseAttrInfo.getAttrValueList();

        if (!CollectionUtils.isEmpty(attrValueList)){
             for (BaseAttrValue baseAttrValue : attrValueList){
                 // 设置平台属性id
                 baseAttrValue.setAttrId(baseAttrInfo.getId());
                 baseAttrValueMapper.insert(baseAttrValue);
             }
        }
    }

mysql数据库编码

#当使用 show variables like '%char%';查看数据库编码的时候:
  可以发现 character_set_server 为 latin1
  这时候我们就需要在虚拟机的mysq

进入 mysql进行修改配置文件

echo "character-set-server=utf8" >> /etc/mysql/mysql.conf.d/mysqld.cnf
[root@localhost ~]# docker exec -it mysql /bin/bash
root@5804a01caf31:/# ls
bin   dev                         entrypoint.sh  home  lib64  mnt  proc  run   srv  tmp  var
boot  docker-entrypoint-initdb.d  etc            lib   media  opt  root  sbin  sys  usr
root@5804a01caf31:/# 
root@5804a01caf31:/# echo "character-set-server=utf8" >> /etc/mysql/mysql.conf.d/mysqld.cnf
root@5804a01caf31:/# exit
exit
[root@localhost ~]# docker restart mysql
mysql

跨域

2.4.4.2 解决跨域问题的方案

目前比较常用的跨域解决方案有3种:

- Jsonp

最早的解决方案,利用script标签可以跨域的原理实现。

https://www.w3cschool.cn/json/json-jsonp.html

限制:

- 需要服务的支持

- 只能发起GET请求

- nginx反向代理

思路是:利用nginx把跨域反向代理为不跨域,支持各种请求方式

缺点:需要在nginx进行额外配置,语义不清晰

- CORS

规范化的跨域请求解决方案,安全可靠。

优势:

- 在服务端进行控制是否允许跨域,可自定义规则

- 支持各种请求方式

缺点:

- 会产生额外的请求

image-20220923151857996

image-20220923151909976

在gateway网关模块配置全局跨域的配置:

配置该配置之后其他Controller层不需要再写@CrossOrigin

package com.tanyang.gmall.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;

@Configuration
public class CorsConfig {


    @Bean
    public CorsWebFilter corsWebFilter(){
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        // 设置允许跨域的网络地址
        corsConfiguration.addAllowedOrigin("*");
        // 设置跨域是否允许携带cookie
        corsConfiguration.setAllowCredentials(true);
        // 跨域的请求方式设置为任意
        corsConfiguration.addAllowedMethod("*");
        // 允许携带的头信息也为任意
        corsConfiguration.addAllowedHeader("*");


        //创建配置对象
        UrlBasedCorsConfigurationSource configurationSource = new UrlBasedCorsConfigurationSource();

        //设置配置 {过滤哪些请求}

        configurationSource.registerCorsConfiguration("/**",corsConfiguration);


        return new CorsWebFilter(configurationSource);
    }

}

配置文件迁移到nacos

当微服务模块过多配置文件散落在各个模块不好管理,可以迁移到Nacos进行管理

  1. 删除原有的nacos

    1. docker stop nacos

    2. docker rm nacos

    3. docker run -d  \
      -e MODE=standalone  \
      -e PREFER_HOST_MODE=hostname  \
      -e SPRING_DATASOURCE_PLATFORM=mysql  \
      -e MYSQL_SERVICE_HOST=192.168.176.100 \ 
      -e MYSQL_SERVICE_PORT=3306  \
      -e MYSQL_SERVICE_USER=root  \
      -e MYSQL_SERVICE_PASSWORD=root  \
      -e MYSQL_SERVICE_DB_NAME=nacos  \
      -p 8848:8848  \
      --name nacos  \
      --restart=always  \
      nacos/nacos-server:1.4.1
      
      
    4. 在数据库新建配置的数据库 可见上面代码 为nacos, 导入sql脚本

    5. 重新访问nacos

    6. 查看nacos 日期 docker log nacos 如果有端口号8848说明启动成功

    7. 访问 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wMyZnQZj-1663918453444)(C:\Users\caituxinchu\AppData\Roaming\Typora\typora-user-images\image-20220909155129557.png)]

千万不要写行尾注释

spring.application.name=server-gateway
spring.profiles.active=dev
#    #注册中心的地址 注释不能写在后面 会被识别为 整体的字符串
spring.cloud.nacos.discovery.server-addr=ip地址:8848
# 配置中心的地址
spring.cloud.nacos.config.server-addr=ip地址:8848
spring.cloud.nacos.config.prefix=${spring.application.name}
spring.cloud.nacos.config.file-extension=yaml

image-20220923152208436

配置文件的优先级

父容器 bootstrap.yml/properties

子容器 application.yml/properties

bootstrap.yml/properties 父容器的作用:

​ 1. 配置一些引导地址: 例如 配置中心的地址

image-20220923152144497

image-20220909160649982

Spu相关业务

image-20220923152243009

content-type

无需自己设置content-type: 浏览器会根据数据类型使用正确的content-type

Headers:

参数名称参数值是否必须示例备注
Content-Typeapplication/json

问题描述

有这么一个接口,postman(或其他API测试工具)能够跑通,但是从浏览器的前端代码里发出去就错了,服务器报500,那么很有可能遇到了这个问题:content-type的设置和服务器期望的不一样.

现在后端通常会希望获取到json格式的数据,所以通常请求的headers里要设置content-type:application/json

headers: {
“Content-Type”: “application/json”,
}

一般来说这么做是没有问题的,但是就怕忘了设置header,又多此一举的改变了body中数据的类型:

fetch(‘url’, {
method: ‘POST’,
body: JSON.stringify({
xxx:xxx,
}),
})

就是这么多此一举,

后端希望得到json, 于是我自己将post数据的body变成了一个json字符串,同时我又忘记设置了header的content-type,会出现什么呢?从开发者工具看到请求的header里的content-type变成了text/plain,所以服务器拿不到数据json,和期望的数据不同,自然就很有可能跑挂了,报500的可能性就很大了.

其实浏览器是很智能的.
如果没有设置header的conten-type, 在body中传递了对象

fetch(‘url’, {
method: ‘POST’,
body: {
xxx:xxx,
},
})

浏览器会自动将content-type改成application/json,

如果是用了formdata

const body = new FormData()
body.append(‘xxx’, ‘xxx’)
fetch(‘url’, {
method: ‘POST’,
body,
})

浏览器也可以很好的识别出请求的content-type,

所以如果可以的话, 尽量不要自己设置这个字段了,用正确的数据类型去告诉浏览器要用什么content-type就可以了

Mp的底层使用

可以把baseMapper当任意实体Mapper使用,因为他们是同一个类型

image-20220923152442064

分类品牌中间表的业务

  1. 根据三级分类查询品牌

image-20220923152452311

  1. 删除 只需要删除分配和品牌的中间表即可

  2. 添加品牌

image-20220923152500430

service层的this.saveBatch()

//这里不是使用baseCategoryTradeMarkMapper 而是是用BaseCateogryTradeMarkServiceImpl {因为继承了mp中的saceBatch方法}
this.saveBatch(baseCategoryTrademarkList);

商品关系数据表

image-20220923152516414

四、spu的保存功能中的图片上传

4.1 MinIo介绍

MinIO 是一个基于Apache License v2.0开源协议的对象存储服务。它兼容亚马逊S3云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等,而一个对象文件可以是任意大小,从几kb到最大5T不等。

MinIO是一个非常轻量的服务,可以很简单的和其他应用的结合,类似 NodeJS, Redis 或者 MySQL。

官方文档:http://docs.minio.org.cn/docs 旧一点

https://docs.min.io/ 新

文件上传

image-20220923152530745

@RestController
@RequestMapping("admin/product")
public class FileUploadController {

    //  获取文件上传对应的地址
    @Value("${minio.endpointUrl}")
    public String endpointUrl;
    @Value("${minio.accessKey}")
    public String accessKey;
    @Value("${minio.secreKey}")
    public String secreKey;
    @Value("${minio.bucketName}")
    public String bucketName;


    /***
     * 文件上传
     * /fileUpload
     * @param file
     */
    @PostMapping(value = "/fileUpload")
    public Result fileUpload(MultipartFile file){

        // 文件上传的路径
        String url = "";

        try {
            // 使用MinIO服务的URL,端口,Access key和Secret key创建一个MinioClient对象
            MinioClient minioClient = MinioClient.builder().endpoint(endpointUrl).credentials(accessKey,secreKey).build();

            // 检查存储桶是否已经存在
            boolean isExist = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
            if(isExist) {
                System.out.println("Bucket gmall already exists.");
            } else {
                // 创建一个名为asiatrip的存储桶,用于存储照片的zip文件。
                minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
            }

            // 定义文件上传的名称 被上传文件的名称 名称不能重复!
            String fileName = System.currentTimeMillis()+ UUID.randomUUID().toString();

            // 使用putObject上传一个文件到存储桶中。
            minioClient.putObject(PutObjectArgs.builder().bucket(bucketName).object(fileName).stream(file.getInputStream(),
                    file.getSize(),-1).contentType(file.getContentType()).build());

            // 文件上传后的路径 为Ip :端口号 /gmall/***
            url =  endpointUrl   + "/"+  bucketName + "/"+ fileName;

            System.out.println(url + " is successfully uploaded to" + endpointUrl+bucketName);
        } catch(Exception e) {
            e.printStackTrace();
        }

        return Result.ok(url);
    }
}

保存spuInfo

/*
spuInfo;
spuImage;
spuSaleAttr;
spuSaleAttrValue;
spuPoster
*/

image-20220923152551938

Sku数据表结构

image-20220923152604057

保存SkuInfo

image-20220923152637006

由图可见,关联着四张表

商品上下架

git

image-20220923152701896

image-20220923152708403

  1. 命令行 ssh-keygen 回车回车 重置公钥 并且会在C盘生成一个公钥文件
  2. 进入gitee设置中 创建一个新的公钥
  3. image-20220923152716356

Thymeleaf

image-20220923151007357

有哪些对象可以将数据带到视图

  1. Model model

  2. HttpServletRequest request

  3. HttpSession session

    request.setAttribute("name","柳岩");
    
拼接的规则

引号嵌套: 双引号嵌套单引号

​ 单引号嵌套双引号

引入了Mysql 如果不添加响应配置文件则会无法启动程序 因为springboot会根据依赖自动配置数据源 ,那么我们可以在启动类手动排除数据源

只有排除mysql

<!--mysql-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

三、商品详情业务需求分析

image-20220923152750154

image-20220923151126536

3.1 详情渲染功能介绍

商品详情所需构建的数据如下:

1,Sku基本信息

2,Sku图片信息

3,Sku分类信息

4,Sku销售属性相关信息

5,Sku价格信息(平台可以单独修改价格,sku后续会放入缓存,为了回显最新价格,所以单独获取)

6,展示商品的海报

7,获取skuId 对应的商品规格参数

3.2 详情模块规划

模块规划思路:

1,service-item微服务模块封装详情页面所需数据接口;

2,service-item通过feign client调用其他微服务数据接口进行数据汇总;

3,pc端前台页面通过web-all调用service-item数据接口渲染页面;

4,service-item可以为pc端、H5、安卓与ios等前端应用提供数据接口,web-all为pc端页面渲染形式

5,service-item获取商品信息需要调用service-product服务sku信息等;

6,由于service各微服务可能会相互调用,调用方式都是通过feign client调用,所以我们把feign client api接口单独封装出来,需要时直接引用feign client api模块接口即可,即需创建service-client父模块,管理各service微服务feign client api接口。

  1. 根据三级分类Id查询一级 二级 三级的 id 和name


####创建视图
create view base_category_view2 as
select 
  c3.id as id,
	c1.id as category1_id , c1.name as category1_name,
	c2.id as category1_id, c2.name as category2_name,
	c3.id as category3_id , c3.name as category3_name
	
from
   base_category1 as c1 
	 INNER JOIN base_category2  as c2 on c1.id = c2.category1_id
   INNER JOIN base_category3 as c3 on c2.id = c3.category2_id

####查询视图
select * from base_category_view where id = 1
  1. 查询spu销售属性 和 选中的sku属性

image-20220923152821052

根据销售属性 销售属性值 sku销售属性值表 查找选中属性的组 和未选中的组

image-20220923152956997

### 将关联查询到的销售属性与sku_sale_attr_value进行查询的结果 与

## 将第一步的查询结果与 sku_sale_attr_value查询  依据是skuId 和spu_sale_attr_value关联
select ssa.id,
				ssa.spu_id,
				ssa.base_sale_attr_id,
				ssa.sale_attr_name,
				ssav.id  sale_attr_value_id,
			 ssav.sale_attr_value_name,
			 if( skav.sku_id is null,0 ,1) is_checked
			
			 
  from
	spu_sale_attr ssa  INNER JOIN spu_sale_attr_value ssav  
	  on
     ssa. spu_id  = ssav.spu_id and ssa.base_sale_attr_id = ssav.base_sale_attr_id
		 left join sku_sale_attr_value skav
		 on skav.sale_attr_value_id = ssav.id and skav.sku_id =  24
		 where  ssa.spu_id = 11
  order by ssa.base_sale_attr_id , ssav.id

根据spuid获取销售属性id和skuid的对应关系

image-20220923153009816

sql解析:

我们查询sku销售属性表中的sku_id = 26的也就是 获取sku为26的这套sku属性,那么有两个sale_attr_value_id对应这个sku_id = 26的记录 ,而sale_attr_value_id 则关联表 spu_sale_attr_value这张表 ,而spu_sale_attr_value这张表记录id 为3735 3736 里面的销售属性值有基于base_sale_attr的表 有颜色和版本 并且有销售属性值字段 里面有具体颜色【幻夜星河】 或者 具体版本【6G+64G】,而skuId则在skuInfo表中,skuId为26的某个具体的skuInfo信息

##### 在分组的前提下 使用对每一组指定的数据进行拼接
##### GROUP_CONCAT:将gourp by 产生的同一个分组中的值拼接起来,返回一个字符串结果
##### group_concat(【distinct】 要连接的字段,【order by】要排序的字段,【separator】分隔符)
### 因为需要颜色在上面 所以拼接的时候需要根据base_sale_attr id进行升序排序 颜色在上 版本在下

select
group_concat(skav.sale_attr_value_id order by ssav.base_sale_attr_id asc  SEPARATOR '|' ) as value_ids, sku_id

   from 
	  sku_sale_attr_value as skav
		INNER JOIN 
	 spu_sale_attr_value as ssav 
	 on skav.sale_attr_value_id  = ssav.id
	 where skav.spu_id = 11
	 group by
	 skav.sku_id

image-20220923153047441

<!--   将map的属性值进行自动映射-->
  <resultMap id="getSkuValueIdsMap" type="map" autoMapping="true">

  </resultMap>
    <select id="selectgetSkuValueIdsMap" resultMap="getSkuValueIdsMap">
        select
            group_concat(skav.sale_attr_value_id order by ssav.base_sale_attr_id asc  SEPARATOR '|' ) as value_ids, sku_id

        from
            sku_sale_attr_value as skav
                INNER JOIN
            spu_sale_attr_value as ssav
            on skav.sale_attr_value_id  = ssav.id
        where skav.spu_id = #{spuId}
        group by
            skav.sku_id
    </select>

OpenFeign的使用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Dq9pcoQH-1663918453456)(C:\Users\caituxinchu\AppData\Roaming\Typora\typora-user-images\image-20220915164835916.png)]

注:同一个@FeignClient里,fallback 和 fallbackFactory 不能同时使用。

负载均衡器SpringCloudLoadBalancer

由于 Netflix 对于 Ribbon 的维护已经暂停,所以 Spring Cloud 对于负载均衡建议使用由其自己定义的 Spring Cloud LoadBalancer。对于Spring Cloud LoadBalancer 的使用非常简单。
1、关闭Ribbon的负载均衡器

spring:
  application:
    name: consumer01-depart
  cloud:
    loadbalancer:

# 关闭Ribbon的负载均衡器

​      ribbon:
​        enabled: false

2、pom中添加LoadBalancer依赖

<!--spring cloud loadbalancer 依赖--> 
        <dependency> 
            <groupId>org.springframework.cloud</groupId> 
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
            <version>2.2.3.RELEASE</version>
        </dependency>

Feign 源码解析

FeignClient的配置

默认的配置类为FeignClientsConfiguration,这个类在spring-cloud-netflix-core的jar包下,打开这个类,可以发现它是一个配置类,注入了很多的相关配置的bean,包括feignRetryer、FeignLoggerFactory、FormattingConversionService等,其中还包括了Decoder、Encoder、Contract,如果这三个bean在没有注入的情况下,会自动注入默认的配置。

Decoder feignDecoder: ResponseEntityDecoder(这是对SpringDecoder的封装)
Encoder feignEncoder: SpringEncoder
Logger feignLogger: Slf4jLogger
Contract feignContract: SpringMvcContract
Feign.Builder feignBuilder: HystrixFeign.Builder

代码如下:

@Configuration
public class FeignClientsConfiguration {

...//省略代码

@Bean
    @ConditionalOnMissingBean
    public Decoder feignDecoder() {
        return new ResponseEntityDecoder(new SpringDecoder(this.messageConverters));
    }

    @Bean
    @ConditionalOnMissingBean
    public Encoder feignEncoder() {
        return new SpringEncoder(this.messageConverters);
    }

    @Bean
    @ConditionalOnMissingBean
    public Contract feignContract(ConversionService feignConversionService) {
        return new SpringMvcContract(this.parameterProcessors, feignConversionService);
    }

...//省略代码
}

重写配置:

你可以重写FeignClientsConfiguration中的bean,从而达到自定义配置的目的,比如FeignClientsConfiguration的默认重试次数为Retryer.NEVER_RETRY,即不重试,那么希望做到重写,写个配置文件,注入feignRetryer的bean,代码如下:

@Configuration
public class FeignConfig {

    @Bean
    public Retryer feignRetryer() {
        return new Retryer.Default(100, SECONDS.toMillis(1), 5);
    }
}

在上述代码更改了该FeignClient的重试次数,重试间隔为100ms,最大重试时间为1s,重试次数为5次。

Feign的工作原理

feign是一个伪客户端,即它不做任何的请求处理。Feign通过处理注解生成request,从而实现简化HTTP API开发的目的,即开发人员可以使用注解的方式定制request api模板,在发送http request请求之前,feign通过处理注解的方式替换掉request模板中的参数,这种实现方式显得更为直接、可理解。

通过包扫描注入FeignClient的bean,该源码在FeignClientsRegistrar类:
首先在启动配置上检查是否有@EnableFeignClients注解,如果有该注解,则开启包扫描,扫描被@FeignClient注解接口。代码如下:

private void registerDefaultConfiguration(AnnotationMetadata metadata,
            BeanDefinitionRegistry registry) {
        Map<String, Object> defaultAttrs = metadata
                .getAnnotationAttributes(EnableFeignClients.class.getName(), true);

        if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) {
            String name;
            if (metadata.hasEnclosingClass()) {
                name = "default." + metadata.getEnclosingClassName();
            }
            else {
                name = "default." + metadata.getClassName();
            }
            registerClientConfiguration(registry, name,
                    defaultAttrs.get("defaultConfiguration"));
        }
    }

<!--   插件 打可执行jar包的插件-->
    <build>
        <finalName>web-all</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

将实体类解析为JSON字符串

后端将pojo对象解析为json字符串 因为需要传递一个字符串类型

//  将这个map 转换为页面需要的Json 对象
String valueJson = JSON.toJSONString(skuValueIdsMap);
result.put("valuesSkuJson", valueJson);

前端 将字符串解析为Pojo对象

//  JSON.parse() : Json 字符串转换为对象! {"115|117":"44","114|117":"45"}
var valuesSkuJson = JSON.parse(this.valuesSkuJson);

Redis

Redis配置

作为公共模块引入

image-20220916172154730

  1. 首先引入坐标依赖

    <!-- redis -->
    <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
     
    <!-- spring2.X集成redis所需common-pool2-->
    <dependency>
       <groupId>org.apache.commons</groupId>
       <artifactId>commons-pool2</artifactId>
       <version>2.6.0</version>
       </dependency>
    
    
    
    1. 设置redisTemplate的配置类

      /**
       * Redis配置类
       */
      @Configuration
      @EnableCaching
      public class RedisConfig {
          // 声明模板
          /*
          ref = 表示引用
          value = 具体的值
          <bean class="org.springframework.data.redis.core.RedisTemplate" >
              <property name="defaultSerializer" ref = "">
          </bean>
           */
          @Bean
          public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
              RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
              //  设置redis的连接池工厂。
              redisTemplate.setConnectionFactory(redisConnectionFactory);
              //  设置序列化的。
              Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
              ObjectMapper objectMapper = new ObjectMapper();
              objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
              objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
              jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
              //  将Redis 中 string ,hash 数据类型,自动序列化!
              redisTemplate.setKeySerializer(new StringRedisSerializer());
              redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
              //  设置数据类型是Hash 的 序列化!
              redisTemplate.setHashKeySerializer(new StringRedisSerializer());
              redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
      
              redisTemplate.afterPropertiesSet();
              return redisTemplate;
          }
      
      }
      

Redis开发的相关规则

开始开发先说明redis key的命名规范,由于Redis不像数据库表那样有结构,其所有的数据全靠key进行索引,所以redis数据的可读性,全依靠key。

企业中最常用的方式就是:object:id:field

​ 比如:sku:1314:info

​ user:1092:info

:表示根据windows的 /一个意思

image-20220916181621751

1.4 缓存常见问题

缓存最常见的3个问题: 面试

\1. 缓存穿透

\2. 缓存雪崩

\3. 缓存击穿

缓存穿透: 是指查询一个不存在的数据,由于缓存无法命中,将去查询数据库,但是数据库也无此记录,并且出于容错考虑,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。

解决:空结果也进行缓存,但它的过期时间会很短,最长不超过五分钟。

​ 比如请求一个4 我们 存储null值 4 null 30s

但是恶意攻击的时候:使用随机数随机ID访问导致宕机,我们可以使用布隆过滤器

【布隆过滤器】:将所有的id存储在布隆过滤器中 ,当请求的id不在布隆过滤器中直接返回 结束请求

缓存雪崩:是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。

解决:原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

缓存击穿: 是指对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:如果这个key在大量请求同时进来之前正好失效,那么所有对这个key的数据查询都落到db,我们称为缓存击穿。

与缓存雪崩的区别:

\1. 击穿是一个热点key失效

\2. 雪崩是很多key集体失效

解决:锁

1568162613917

本地锁的局限

下图清晰的说明了本地锁的局限性,会造成线程安全问题。

image-20220916201943013

本地锁只能锁住同一个工程的资源 在分布式系统里面有很大的局限性。 单纯的Java Api 并不提供分布式锁的能力,为了解决这个问题就需要一种跨jvm的互斥机制来控制共享资源的访问。

2.2 分布式锁实现的解决方案

随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!

分布式锁主流的实现方案:

  1. 基于数据库实现分布式锁

  2. 基于缓存(Redis等)

  3. 基于Zookeeper (强一致性,单个结点不能用其他的结点也无法使用,可用性就低了)

每一种分布式锁解决方案都有各自的优缺点:

  1. 性能:redis最高

  2. 可靠性:zookeeper最高

这里,我们就基于redis实现分布式锁。

2.3 使用redis实现分布式锁

1568170959121

\1. 多个客户端同时获取锁(setnx)

\2. 获取成功,才能够执行操作,执行业务逻辑{从db获取数据,放入缓存},执行完成释放锁(del)

\3. 其他客户端等待重试 ,把锁放开,让其他用户进行争抢锁。

代码模拟:

 @Override
    public void testLock() {
        // 1. 获取锁
        //2. 操作
        // 3. 释放锁

//      设置的锁如果不存在
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "111");

          if (lock){
              String value   =  stringRedisTemplate.opsForValue().get("num");
              if (StringUtils.isEmpty(value)){
                  this.stringRedisTemplate.opsForValue().set("num",String.valueOf(0));
                  return;
              }
              Integer num = Integer.valueOf(value);
              num = num +1;
              this.stringRedisTemplate.opsForValue().set("num",String.valueOf(num));
              
              //        操作完释放锁
              stringRedisTemplate.delete("lock");

          }else{
              try {
                  Thread.sleep(100);
                  this.testLock();
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
    }
出现问题1:

当获取锁lock后 出现异常,可能会导致锁无法释放。

一开始的解决思路: 可以获取锁后给锁设置一个过期时间,这样即便出现异常也能自动释放。

代码如下:

   @Override
    public void testLock() {
        // 1. 获取锁
        //2. 操作
        // 3. 释放锁

//      设置的锁如果不存在
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "111");

          if (lock){
              //在这里给获取到的锁设置一个生命周期
               stringRedisTemplate.expire("lock",7, TimeUnit.SECONDS);
              String value   =  stringRedisTemplate.opsForValue().get("num");
              if (StringUtils.isEmpty(value)){
                  this.stringRedisTemplate.opsForValue().set("num",String.valueOf(0));
                  return;
              }
              Integer num = Integer.valueOf(value);
              num = num +1;
              this.stringRedisTemplate.opsForValue().set("num",String.valueOf(num));
              
              //        操作完释放锁
              stringRedisTemplate.delete("lock");

          }else{
              try {
                  Thread.sleep(100);
                  this.testLock();
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
    }
出现问题2:

当前先获取锁,再设置过期时间不是原子操作,当获取锁后发生异常,同样会无法设置过期时间。

保证原子性操作代码如下:

  @Override
    public void testLock() {
        // 1. 获取锁
        //2. 操作
        // 3. 释放锁

//      设置的锁如果不存在
//        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "111");
        //在获取锁的时候设置生命周期,这样保证了原则子操作
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "111",7,TimeUnit.SECONDS);

          if (lock){
              //在这里给获取到的锁设置一个生命周期
               stringRedisTemplate.expire("lock",7, TimeUnit.SECONDS);
              String value   =  stringRedisTemplate.opsForValue().get("num");
              if (StringUtils.isEmpty(value)){
                  this.stringRedisTemplate.opsForValue().set("num",String.valueOf(0));
                  return;
              }
              Integer num = Integer.valueOf(value);
              num = num +1;
              this.stringRedisTemplate.opsForValue().set("num",String.valueOf(num));
              
              //        操作完释放锁
             stringRedisTemplate.delete("lock");

          }else{
              try {
                  Thread.sleep(100);
                  this.testLock();
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
    }
出现问题3

当设置的锁的生存时间如果比业务逻辑执行的时间少,在业务还没执行结束就过期了,那么其他的线程就能够获取该锁,进行业务操作,这样也会导致线程不安全。还有可能使得某个锁释放的是其他线程的锁,比如第二个线程还没执行完业务,锁的声明周期过期了,释放了本锁,这时候第三个线程获取该锁,第二个线程执行完业务也会释放锁,此时释放的锁就是第三个线程的了,那么就会造成锁的混乱释放。

image-20220916211025724

解决问题3的思路:因为过期导致的释放锁混乱的情况,我们可以使用UUID防止误删的方法

代码如下:

 @Override
    public void testLock() {
        // 1. 获取锁
        //2. 操作
        // 3. 释放锁

//      设置的锁如果不存在
//        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "111");
        //在获取锁的时候设置生命周期,这样保证了原则子操作
         String uuid = UUID.randomUUID().toString().replaceAll("-","");
         //使用uuid作为唯一标识进行上锁
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid,7,TimeUnit.SECONDS);

          if (lock){
              //在这里给获取到的锁设置一个生命周期
               stringRedisTemplate.expire("lock",7, TimeUnit.SECONDS);
              String value   =  stringRedisTemplate.opsForValue().get("num");
              if (StringUtils.isEmpty(value)){
                  this.stringRedisTemplate.opsForValue().set("num",String.valueOf(0));
                  return;
              }
              Integer num = Integer.valueOf(value);
              num = num +1;
              this.stringRedisTemplate.opsForValue().set("num",String.valueOf(num));

               if (uuid.equals(stringRedisTemplate.opsForValue().get("lock"))){
                   //        使用uuid校验是否误删
                   stringRedisTemplate.delete("lock");
               }
          }else{
              try {
                  Thread.sleep(100);
                  this.testLock();
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
    }
出现问题4

当在线程A业务执行完后,进入了uuid判断并且判断为当前锁,这时候lock锁过期了,那么当前lock锁就被释放了,这时候另外一个线程B能够获取到当前的锁,并且进行执行业务lock锁不释放,然后回到A的判断中,这时候A将释放锁,并且已经早已判断过uuid是当前线程的,但是释放的锁却是B线程的lock锁,这依然也会造成误删除的问题。

比如:

image-20220916221955758

问题:删除操作缺乏原子性。

场景:

\1. index1执行删除时,查询到的lock值确实和uuid相等

img

\2. index1执行删除前,lock刚好过期时间已到,被redis自动释放

在redis中没有了锁。

img

\3. index2获取了lock,index2线程获取到了cpu的资源,开始执行方法

\4. index1执行删除,此时会把index2的lock删除

index1 因为已经在方法中了,所以不需要重新上锁。index1有执行的权限。index1已经比较完成了,这个时候,开始执行

img

删除的index2的锁!

问题4的解决方案

引入lua脚本: 链接SET — Redis 命令参考 (redisfans.com)

可以通过以下修改,让这个锁实现更健壮:

  • 不使用固定的字符串作为键的值,而是设置一个不可猜测(non-guessable)的长随机字符串,作为口令串(token)。
  • 不使用 DEL 命令来释放锁,而是发送一个 Lua 脚本,这个脚本只在客户端传入的值和键的口令串相匹配时,才对键进行删除。

这两个改动可以防止持有过期锁的客户端误删现有锁的情况出现。

以下是一个简单的解锁脚本示例:

if redis.call("get",KEYS[1]) == ARGV[1]
then
    return redis.call("del",KEYS[1])
else
    return 0
end
 @Override
    public void testLock() {
        // 1. 获取锁
        //2. 操作
        // 3. 释放锁
        //在获取锁的时候设置生命周期,这样保证了原则子操作
         String uuid = UUID.randomUUID().toString().replaceAll("-","");
         //使用uuid作为唯一标识进行上锁
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid,7,TimeUnit.SECONDS);

          if (lock){
              //在这里给获取到的锁设置一个生命周期
               stringRedisTemplate.expire("lock",7, TimeUnit.SECONDS);
              String value   =  stringRedisTemplate.opsForValue().get("num");
              if (StringUtils.isEmpty(value)){
                  this.stringRedisTemplate.opsForValue().set("num",String.valueOf(0));
                  return;
              }
              Integer num = Integer.valueOf(value);
              num = num +1;
              this.stringRedisTemplate.opsForValue().set("num",String.valueOf(num));


              // 2. 释放锁 del  每个结点用户端输入的参数
              String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
// 设置lua脚本返回的数据类型
              DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
// 设置lua脚本返回类型为Long
              redisScript.setResultType(Long.class);
              redisScript.setScriptText(script);
              //执行
              redisTemplate.execute(redisScript, Arrays.asList("lock"),uuid);

              
          }else{
              try {
                  Thread.sleep(100);
                  this.testLock();
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
    }

为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:

- 互斥性。在任意时刻,只有一个客户端能持有锁。

- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。

- 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

- 加锁和解锁必须具有原子性

redis集群状态下的问题:

\1. 客户端A从master获取到锁

\2. 在master将锁同步到slave之前,master宕掉了。

\3. slave节点被晋级为master节点

\4. 客户端B取得了同一个资源被客户端A已经获取到的另外一个锁。

安全失效!

如图: A还未同步到slave之前宕机了,此时B又能够向集群获取该锁,造成安全问题。

image-20220917010151203

解决方案:了解即可!

img

Redisson

Redisson提供的分布式锁是支持锁自动续期的,也就是说,如果线程仍旧没有执行完,那么redisson会自动给redis中的目标key延长超时时间,这在Redisson中称之为 Watch Dog 机制。

如果拿到分布式锁的节点宕机,且这个锁正好处于锁住的状态时,会出现锁死的状态,为了避免这种情况的发生,锁都会设置一个过期时间。这样也存在一个问题,假如一个线程拿到了锁设置了30s超时,在30s后这个线程还没有执行完毕,锁超时释放了,就会导致问题,Redisson给出了自己的答案,就是 watch dog 自动延期机制。

Redisson提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期,也就是说,如果一个拿到锁的线程一直没有完成逻辑,那么看门狗会帮助线程不断的延长锁超时时间,锁不会因为超时而被释放。

默认情况下,看门狗的续期时间是30s,也可以通过修改Config.lockWatchdogTimeout来另行指定。另外Redisson 还提供了可以指定leaseTime参数的加锁方法来指定加锁的时间。超过这个时间后锁便自动解开了,不会延长锁的有效期

同时 redisson 还有公平锁、读写锁的实现。

使用样例如下,附有方法的详细机制释义

private void redissonDoc() throws InterruptedException {
    //1. 普通的可重入锁
    RLock lock = redissonClient.getLock("generalLock");

    // 拿锁失败时会不停的重试
    // 具有Watch Dog 自动延期机制 默认续30s 每隔30/3=10 秒续到30s
    lock.lock();

    // 尝试拿锁10s后停止重试,返回false
    // 具有Watch Dog 自动延期机制 默认续30s
    boolean res1 = lock.tryLock(10, TimeUnit.SECONDS);

    // 拿锁失败时会不停的重试
    // 没有Watch Dog ,10s后自动释放
    lock.lock(10, TimeUnit.SECONDS);

    // 尝试拿锁100s后停止重试,返回false
    // 没有Watch Dog ,10s后自动释放
    boolean res2 = lock.tryLock(100, 10, TimeUnit.SECONDS);

    //2. 公平锁 保证 Redisson 客户端线程将以其请求的顺序获得锁
    RLock fairLock = redissonClient.getFairLock("fairLock");

    //3. 读写锁 没错与JDK中ReentrantLock的读写锁效果一样
    RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("readWriteLock");
    readWriteLock.readLock().lock();
    readWriteLock.writeLock().lock();
}

使用场景代码
@Override
    public void testLock() {

//     创建锁:
        String skuId = "20";
        String lockKey = "lock:" + skuId;

//     锁的是每个商品  普通的可重入锁
        RLock lock = redissonClient.getLock(lockKey);

//       开始加锁 拿锁失败会一直尝试拿锁 具有Watch Dog 自动延迟机制 默认续30s, 每隔30/3=10s 后会自动续约到30s
//        lock.lock();
        try {
            //尝试拿锁20s后会停止重试 但有Watch Dog 机制 默认续约30s 会自动自旋
            boolean lockRes = lock.tryLock(20, TimeUnit.SECONDS);
            if (lockRes){
                //      业务逻辑代码
//       获取数据
                String value   =  stringRedisTemplate.opsForValue().get("num");
                if (StringUtils.isEmpty(value)){
                    return;
                }

//       对redis值++
                int num = Integer.valueOf(value);
                redisTemplate.opsForValue().set("num",String.valueOf(++num));
                // 执行完业务可能还没到默认的30s 这时候需要释放锁
                lock.unlock();
            }else{
                //如果没有拿到锁 就自己进入自旋
                Thread.sleep(30);
                this.testLock();
            }
        } catch (InterruptedException e) {
            // 如果释放锁因为异常没有执行,就需要捕获异常来释放锁
            lock.unlock();
            e.printStackTrace();
        }

    }

启动Redisson的看门机制

如果你想让Redisson启动看门狗机制,你就不能自己在获取锁的时候,定义超时释放锁的时间,无论,你是通过lock() (void lock(long leaseTime, TimeUnit unit);)

还是通过tryLock获取锁,只要在参数中,不传入releastime或者传入的releastime为-1,就会开启看门狗机制,
就是这两个方法不要用:

boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException
和 void lock(long leaseTime, TimeUnit unit);,

因为它俩都传release

但是,你传的leaseTime是-1,也是会开启看门狗机制的

默认情况下,看门狗的续期时间是30s,也可以通过修改Config.lockWatchdogTimeout来另行指定。

设置获取锁超时时间的原因

当一个线程A在获取redis分布式锁的时候,没有设置超时时间,如果在释放锁的时候,出现了异常,那么锁就会常驻redis服务中,当另外一个线程B获取锁的时候,无论你是通过自定义的redis分布式锁setnx,还是通过Redisson实现的分布式锁的方式

**if (redis.call(‘exists’, KEYS[1]) == 0) **,在获取锁之前,其实都有一个逻辑判断:如果该锁已经存在,就是key已经存在,就不往redis中写了,也就是获取锁失败
那么线程B就永远不会获取到锁,自然就一直阻塞在获取锁的代码处,发生死锁
如果有了超时时间,异常发生了,超时的话,redis服务器自己就把key删除了,也就是锁释放了
这也就避免了并发下的死锁问题

注意:
  1. watch dog 在当前节点存活时每10s给分布式锁的key续期 30s;
  2. watch dog 机制启动,且代码中没有释放锁操作时,watch dog 会不断的给锁续期;
  3. 如果程序释放锁操作时因为异常没有被执行,那么锁无法被释放,所以释放锁操作一定要放到 finally {} 中;

如果释放锁操作本身异常了,watch dog 还会不停的续期吗?不会,因为无论释放锁操作是否成功,EXPIRATION_RENEWAL_MAP中的目标 ExpirationEntry 对象已经被移除了,watch dog 通过判断后就不会继续给锁续期了。

因为无论在释放锁的时候,是否出现异常,都会执行释放锁的回调函数,把看门狗停了

总结:
  1. watch dog 机制启动,且代码中没有释放锁操作时,watch dog 会不断的给锁续期;
  2. 如果程序释放锁操作时因为异常没有被执行,那么锁无法被释放,所以释放锁操作一定要放到 finally {} 中;
  3. 要使 watchLog机制生效 。只要不传leaseTime即可
  4. watchdog的延时时间 可以由 lockWatchdogTimeout指定默认延时时间,但是不要设置太小。如100
  5. watchdog 会每 lockWatchdogTimeout/3时间,去延时。
  6. watchdog 通过 类似netty的 Future功能来实现异步延时
  7. watchdog 最终还是通过 lua脚本来进行延时

2.4.3 读写锁(ReadWriteLock)

基于Redis的Redisson分布式可重入读写锁RReadWriteLock Java对象实现了java.util.concurrent.locks.ReadWriteLock接口。其中读锁和写锁都继承了RLock接口。

分布式可重入读写锁允许同时有多个读锁和一个写锁处于加锁状态。

有四种情况:

  1. 读 读
  2. 读 写
  3. 写 读
  4. 写 写

当有写事务都会锁住 其他线程无法获取读锁 或者写锁

测试代码:

@Override
public void writeLock() {
    // 获取读写锁 需要对同一个锁进行操作
    RReadWriteLock readwriteLock = redissonClient.getReadWriteLock("readwriteLock");
    // 获取写锁
    RLock rLock = readwriteLock.writeLock();
    // 锁10s 不会续期
    rLock.lock(10,TimeUnit.SECONDS);
    // 写入 msg数据
    this.redisTemplate.opsForValue().set("msg",UUID.randomUUID().toString());
}

@Override
public void readLock() {
    // 获取读写锁 需要对同一个锁进行操作
    RReadWriteLock readwriteLock = redissonClient.getReadWriteLock("readwriteLock");
    // 获取读锁
    RLock rLock = readwriteLock.readLock();
    // 加锁 锁10s 不会续期
    rLock.lock(10,TimeUnit.SECONDS);
    // 写入 msg数据
    this.redisTemplate.opsForValue().get("msg");
}

使用redssion实现分布式锁
 private SkuInfo getSkuInfoByRedisson(Long skuId) {

         SkuInfo skuInfo = null;
        try {
            String skuKey = RedisConst.SKUKEY_PREFIX+skuId + RedisConst.SKUKEY_SUFFIX;

            // 获取缓存数据
            skuInfo = (SkuInfo)redisTemplate.opsForValue().get(skuKey);

            //如果skuInfo为空 则表示缓存中没有此数据
            if (skuInfo == null){

                // 缓存中没有数据 就从数据库中获取数据 再将数据缓存在redis中
                //操作数据库 使用redisson 添加锁的方式

                //定义锁的key
                String lockKey = RedisConst.SKUKEY_PREFIX + skuId + RedisConst.SKULOCK_SUFFIX;

                // 请求获取锁
                RLock lock = redissonClient.getLock(lockKey);

                // 加锁 看门狗续期30s 这里使用waitTime 和 realseTime 都为1s
                boolean tryLock = lock.tryLock(RedisConst.SKULOCK_EXPIRE_PX1,RedisConst.SKULOCK_EXPIRE_PX2,TimeUnit.SECONDS);

                if (tryLock){
                    try {
                        //如果获取锁了  查询数据库
                        skuInfo = getSkuInfoDB(skuId);
                        // 如果数据库中也没有
                        if (skuInfo == null){
                            // 为了避免缓存穿透 应该将查询的数据放入缓存中
                            SkuInfo skuInfoNullValue = new SkuInfo();
                            // 将skuInfo为空的存入redis 存入一个短期的(不重要数据) 存储知识以防缓存穿透
                            redisTemplate.opsForValue().set(skuKey,skuInfoNullValue,RedisConst.SKUKEY_TEMPORARY_TIMEOUT,TimeUnit.SECONDS);
                            return skuInfoNullValue;
                        }
                        // 否则在数据库中查询到了skuInfo 现在将它续期
                        redisTemplate.opsForValue().set(skuKey,skuInfo,RedisConst.SKUKEY_TIMEOUT,TimeUnit.SECONDS);
                        return skuInfo;
                    } catch (Exception e) {
                        e.printStackTrace();
                    }finally {
                        // 加锁后的逻辑操作产生异常需要手动解锁
                        lock.unlock();
                    }
                }else{
                    //否者枷锁失败 进入自选 它能够自选 但是最好自己手动自选
                    Thread.sleep(1000);
                    return this.getSkuInfoByRedisson(skuId);
                }
            }else{
                // 如果缓存中有数据就直接返回
                return skuInfo;
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 防止缓存宕机 ,如果缓存宕机这里就查询数据库来兜底
        return this.getSkuInfoDB(skuId);
    }

AOP

image-20220920182522553

代理:只在原有的代码上面做增强

静态代理 (简单的注入组合) 动态代理 (jdk cglib)

jdk :实现接口 需要实现被代理类的接口 有接口才能被代理

cglib: 可以使用继承类的方式来增强

自定义注解

image-20220920202953962

/**
 * @Target 表示注解使用的位置 类 ,方法 ,属性 构造方法 参数
 * @Retention: 表示注解的生命周期
 *             SOURCE:只存在类文件源码中 ,如果编译后变成class文件后不存在了
 *             CLASS: 会存在到类文件(字节码文件)中
 *             RUNTIME: 也会存在运行时
 *@Inherited: 继承 表示被GmallCache修饰的类的子类会继承GmallCache这个注解
 *
 * @Documented 可以被生成javaDoc文档
 */

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface GmallCache {

    //缓存的前缀
    String prefix() default "";

    //缓存的后缀
    String suffix() default "";
}

切面

@Component
@Aspect   //定义切面

/**
 * 例如在 public SkuInfo getSkuInfo(Long skuId) 上面添加注解@GmallCache(prefix = "sku:")
 *   那么我们可以根据joinPoint连接点 获取
 *                1. 可以获取添加@GmallCache的方法 getSkuInfo
 *                2. 可以获取注解
 *                3. 可以获取注解的属性 prefix suffix等
 *                4. 可以获取方法的参数 Long skuId
 */
public class GmallCacheAspect {
    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private RedissonClient redissonClient;

    @Around("@annotation(com.tanyang.gmall.common.cache.GmallCache)")
    public Object CacheGmallAspect(ProceedingJoinPoint joinPoint) throws Throwable {
        // 1. 创建返回的对象
        Object object = new Object();
        // 2. 获取添加了注解的方法
        // MethodSignatrue接口里面有 Class getReturnType(); 获取返回值     Method getMethod();获取方法
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        // 获取注解
        GmallCache gmallCache = signature.getMethod().getAnnotation(GmallCache.class);
        // 获取缓存的前缀
        String prefix = gmallCache.prefix();
        //获取缓存的后缀
        String suffix = gmallCache.suffix();
        //根据连接点获取方法传入的参数
        Object[] args = joinPoint.getArgs();
        //组合获取数据的key
        String key = prefix + Arrays.asList(args).toString() + suffix;

        //从缓存中获取数据
        object = cacheHit(key,signature);

        try {
            if (object == null){
                //缓存中没有数据 需要从数据库查询
                // 1.定义锁的key
                String lockKey = prefix + ":lock";

                //准备上锁 使用Redisson
                RLock lock = redissonClient.getLock(lockKey);
                // 上锁锁
                boolean tryLock = lock.tryLock(RedisConst.SKULOCK_EXPIRE_PX1, RedisConst.SKULOCK_EXPIRE_PX2, TimeUnit.SECONDS);
                //判断是否获取成功
                if (tryLock){
                    //成功获取锁
                    // 执行切入方法的方法体 比如SkuInfoDB 就是查询数据库
                    // 使用连接点执行方法体 并且传入被切入方法的参数来执行该方法体
                    try {
                        object = joinPoint.proceed(args);

                        //判断是否从Mysql中查询到了数据
                        if (object == null){
                            //执行了方法体查询数据库 表示数据库中也没有数据
                            //创建空值
//                             object = new Object();
                            Class returnType = signature.getReturnType();
                            //创建对象 使用多台 向上转型
                            object  = returnType.newInstance();

                            // 将空值也存储redis中 避免缓存穿透  存储以字符串的形式进行存储
                            redisTemplate.opsForValue().set(key,JSON.toJSONString(object),RedisConst.SKUKEY_TEMPORARY_TIMEOUT,TimeUnit.SECONDS);
                           //返回object
                            return object;
                        }else{
                            //从数据库查询的数据不为空 就进行续约
                            redisTemplate.opsForValue().set(key,JSON.toJSONString(object),RedisConst.SKUKEY_TEMPORARY_TIMEOUT,TimeUnit.SECONDS);
                            //返回object
                            return object;
                        }
                    } catch (Throwable throwable) {
                        throwable.printStackTrace();
                    } finally {
                        // 如果业务出现异常了 那么就需要手动解锁
                        lock.unlock();
                    }

                }else{
                    //睡眠 重新自旋
                    Thread.sleep(1000);
                    this.CacheGmallAspect(joinPoint);
                }
            }else{
                //从缓存中获取到了数据
                return object;
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 查询缓存出现异常 使用查询数据库来兜底
        return joinPoint.proceed(args);

    }


    /**
     * 从缓存中获取数据
     * @param key
     * @param signature
     * @return
     */
    private Object cacheHit(String key, MethodSignature signature) {
        // 获取数据 存储的时候 转换成Json字符串
        String strJson =(String) redisTemplate.opsForValue().get(key);
        if (!StringUtils.isEmpty(strJson)){
            //获取当前对象的返回值类型
            Class returnType = signature.getReturnType();
            //将字符串转成方法指定的类型
            return JSON.parseObject(strJson,returnType);
        }
        return strJson;

    }
}

注意

最好使用 父类 父类 = new 子类(); 这样向上转型了 避免向下转型时候报转换异常

Class returnType = signature.getReturnType();
//创建对象 使用多台 向上转型
object  = returnType.newInstance();


  // 将空值也存储redis中 避免缓存穿透  存储以字符串的形式进行存储    JSON.toJSONString(object)
redisTemplate.opsForValue().set(key,JSON.toJSONString(object),RedisConst.SKUKEY_TEMPORARY_TIMEOUT,TimeUnit.SECONDS);

    //将字符串转成方法指定的类型
            return JSON.parseObject(strJson,returnType);

布隆过滤器

布隆过滤器可以用于检索一个元素是否在一个集合。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。存储的数据不是0就是1,默认是0。主要用于判断一个元素是否在一个集合中,0代表不存在某个数据,1代表存在某个数据。

通过K个哈希函数计算该数据,返回K个计算出的hash值

这些K个hash值映射到对应的K个二进制的数组下标

将K个下标对应的二进制数据改成1。

例如,第一个哈希函数返回x,第二个第三个哈希函数返回y与z,那么: X、Y、Z对应的二进制改成1。

image-20220921154735331

查询过程

布隆过滤器主要作用就是查询一个数据,在不在这个二进制的集合中,查询过程如下:

1、通过K个哈希函数计算该数据,对应计算出的K个hash值

2、通过hash值找到对应的二进制的数组下标

3、判断:如果存在一处位置的二进制数据是0,那么该数据不存在。如果都是1,该数据存在集合中。

5.1.4 布隆过滤器的优缺点

优点

\1. 由于存储的是二进制数据,所以占用的空间很小

\2. 它的插入和查询速度是非常快的,时间复杂度是O(K),空间复杂度:O (M)。

K: 是哈希函数的个数

M: 是二进制位的个数

\3. 保密性很好,因为本身不存储任何原始数据,只有二进制数据

缺点

\1. 添加数据是通过计算数据的hash值,那么很有可能存在这种情况:两个不同的数据计算得到相同的hash值。

img

例如图中的“张三”和“张三丰”,假如最终算出hash值相同,那么他们会将同一个下标的二进制数据改为1。

这个时候,你就不知道下标为1的二进制,到底是代表“张三”还是“张三丰”。

由此得出如下缺点:

一、存在误判

假如上面的图没有存 “张三”,只存了 “张三丰”,那么用"张三"来查询的时候,会判断"张三"存在集合中。

因为“张三”和“张三丰”的hash值是相同的,通过相同的hash值,找到的二进制数据也是一样的,都是1。

误判率:

​ 受三个因素影响: 二进制位的个数m, 哈希函数的个数k, 数据规模n (添加到布隆过滤器中的函数)

img

已知误判率p, 数据规模n, 求二进制的个数m,哈希函数的个数k {m,k 程序会自动计算 ,你只需要告诉我数据规模,误判率就可以了}

img

ln: 自然对数是以常数e为底数对数,记作lnN(N>0)。在物理学,生物学等自然科学中有重要的意义,一般表示方法为lnx。数学中也常见以logx表示自然对数。

二、删除困难

还是用上面的举例,因为“张三”和“张三丰”的hash值相同,对应的数组下标也是一样的。

如果你想去删除“张三”,将下标为1里的二进制数据,由1改成了0。

那么你是不是连“张三丰”都一起删了呀。

初始化

  1. 数据的数量

  2. 误判率

  3. 在启动类实现CommandLineRunner 并且实现方法来添加布隆过滤器数据的数量和误判率

@Slf4j
public class ServiceProductApplication implements CommandLineRunner {
  
   /**
     * 初始化布隆过滤器
     * @param args
     * @throws Exception
     */
    @Override
    public void run(String... args) throws Exception {
       // 获取布隆过滤器
        RBloomFilter<Object> bloomFilter = redissonClient.getBloomFilter(RedisConst.SKU_BLOOM_FILTER);
       // 初始化布隆过滤器 计算元素的数量 比如预计有多少个sku
        bloomFilter.tryInit(10001,0.001);
    }
  
}


  1. 在插入数据后将插入记录的主键添加到布隆过滤器中
     //告诉布隆过滤器 存储是否存在的标记到布隆过滤器
        RBloomFilter<Object> bloomFilter = redissonClient.getBloomFilter(RedisConst.SKU_BLOOM_FILTER);
         // 将存储skuInfo的主键存储到布隆过滤器
          bloomFilter.add(skuInfo.getId());
  1. 在查询数据的时候 在布隆过滤器中过滤

    //在布隆过滤器中判断数据id是否存在
    RBloomFilter<Object> bloomFilter = redissonClient.getBloomFilter(RedisConst.SKU_BLOOM_FILTER);
    if (!bloomFilter.contains(skuId)){
        //如果 布隆过滤器中没有存储该值 就直接返回
        return result;
    }
    

CompletableFuture异步编排

可通过回调的方式处理计算结果

1.2 创建异步对象

CompletableFuture 提供了四个静态方法来创建一个异步操作。

img

没有指定Executor的方法会使用ForkJoinPool.commonPool() 作为它的线程池执行异步代码。如果指定线程池,则 。

  • runAsync方法不支持返回值。

- supplyAsync可以支持返回值。

1.3 计算完成时回调方法

当CompletableFuture的计算结果完成,或者抛出异常的时候,可以执行特定的Action。主要是下面的方法:

img

whenComplete可以处理正常或异常的计算结果,exceptionally处理异常情况。BiConsumer<? super T,? super Throwable>可以定义处理业务

whenComplete 和 whenCompleteAsync 的区别:

whenComplete:是执行当前任务的线程执行继续执行 whenComplete 的任务。

whenCompleteAsync:是执行把 whenCompleteAsync 这个任务继续提交给线程池来进行执行。

方法不以Async结尾,意味着Action使用相同的线程执行,而Async可能会使用其他线程执行(如果是使用相同的线程池,也可能会被同一个线程选中执行)

// 创建对象
        CompletableFuture<String> integerCompletableFuture = CompletableFuture.supplyAsync(new Supplier<String>() {
            @Override
            public String get() {
                int a = 1/0;
                return "404";
            }
        }).whenComplete(new BiConsumer<String, Throwable>() {
            /**
             *
             * @param s 异步对象执行后的返回结果
             * @param throwable   异常对象
             */
            @Override
            public void accept(String s, Throwable throwable) {
              System.out.println("whenComplete:"+ s);
              System.out.println("whenComplete:" + throwable);
            }
        }).exceptionally(new Function<Throwable, String>() {
            /**
             *  只处理异常的结果
             * @param throwable
             * @return
             */
            @Override
            public String apply(Throwable throwable) {
                return null;
            }
        }).whenCompleteAsync(new BiConsumer<String, Throwable>() {

            /**
             *   和异步对象可能不使用同一个线程 可能由线程池重新分配其他线程
             * @param s   返回值
             * @param throwable  异常对象
             */
            @Override
            public void accept(String s, Throwable throwable) {

            }
        });
        String s = integerCompletableFuture.get();


    }

1.4 线程串行化与并行化方法

thenApply 方法:当一个线程依赖另一个线程时,获取上一个任务返回的结果,并返回当前任务的返回值。

img

thenAccept方法:消费处理结果。接收任务的处理结果,并消费处理,无返回结果。

img

thenRun方法:只要上面的任务执行完成,就开始执行thenRun,只是处理完任务后,执行 thenRun的后续操作

img

带有Async默认是异步执行的。这里所谓的异步指的是不在当前线程内执行。

Function<? super T,? extends U>

T:上一个任务返回结果的类型

U:当前任务的返回值类型

image-20220922134232405

首页

首页流程分析

image-20220922162218918

1. 封装数据的步骤

image-20220922165336602

JSONObject

JSONObject底层使用了一个map对象作为属性 ,所以可以直接将JSONObject作为一个map使用

使用JSONObject的前提是导入alibaba的fastjson

image-20220922170227264

image-20220922173143319

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值