谷粒商城笔记

目录

谷粒商城笔记

1、搭建环境

  • 下载 VM VirtureBox 并安装
  • 开启虚拟化:开机时按 F2,然后左键选择 Configuration 找到 虚拟化之类的英文(可百度)
  • 下载并安装Vagrant,重启,注意要和 VMBox版本匹配,全部下最新版即可
  • 用命令行 vagrant,看是否安装成功
  • 初始化 centos: vagrant init centos/7
  • 启动虚拟环境: vagrant up 下载慢的话参考:https://www.cnblogs.com/misscai/p/12632545.html
  • 修改网卡:查看本机ip,找到vagrant的 Vagrantfile中的 config.vm.network "private_network", ip: "192.168.56.10"(与本机的 VirtualBox Host-Only Network 中的前4段相同
  • 安装docker(https://www.jianshu.com/p/2dae7b13ce2f)
  • 启动docker:sudo systemtl start docker
  • 设置开机自启: sudo systemctl enable docker
  • 镜像地址:https://82m9ar63.mirror.aliyuncs.com

微服务环境搭建....

2、开发

1)、数据组装成父子关系

  • 将查询出来的list可以用Stream API来进行过滤
    // 2.1)、使用Stream的API过滤出父级菜单
    List<CategoryEntity> entityList = entities.stream()
            .filter(categoryEntity -> categoryEntity.getParentCid() == 0)
            .collect(Collectors.toList());
    
  • 给实体类中添加一个children属性,方便我们进行组装
    /**
     *  分类的子分类
     *  这个属性是数据表中没有的字段 ,
     *  所以需要加上 @TableField(exist = false) 注解
     */
    @TableField(exist = false)
    private List<CategoryEntity> children;
    
  • @Override
    public List<CategoryEntity> listWithTree() {
        // 1、查询出所有菜单分类数据
        List<CategoryEntity> entities = baseMapper.selectList(null);
        // 2、 将其组装成父子关系
        // 2.1)、使用Stream的API过滤
       List<CategoryEntity> entityList = entities.stream()
                // 将父级菜单过滤出来
                .filter(categoryEntity -> categoryEntity.getParentCid() == 0)
                // 映射出父级菜单的子菜单
                .map(menu -> {
                    menu.setChildren(getChildren(menu, entities));
                    return menu;
                })
                // 将菜单排序
                .sorted((menu1, menu2) -> {
                    return (menu1.getSort() == null ? 0 : menu1.getSort())
                        - (menu2.getSort() == 	null ? 0 : menu2.getSort());
                })
                // 将排序好的菜单收集为list
                .collect(Collectors.toList());
        return entityList;
    }
    
    /**
     *
     * @param root : 当前分类元素
     * @param all : 所有的分类
     * @return
     */
    private List<CategoryEntity> getChildren(CategoryEntity root, List<CategoryEntity> all) {
        List<CategoryEntity> children = all.stream()
                // 过滤出同级分类
                .filter(categoryEntity -> {
                    return Objects.equals(categoryEntity.getParentCid(), root.getCatId());
                })
                // 使用递归映射出所有分类的子菜单
                .map(categoryEntity -> {
                    categoryEntity.setChildren(getChildren(categoryEntity, all));
                    return categoryEntity;
                })
                // 菜单排序  getSort() 的返回值可能为null
                .sorted((menu1, menu2) -> {
                    return (menu1.getSort() == null ? 0 : menu1.getSort()) 
                        - (menu2.getSort() == null ? 0 : menu2.getSort());
                })
                .collect(Collectors.toList());
    return children;
    }
    

2)、解决跨域问题

见·SpringCloud-Alibaba组件使用·中的Gateway

3)、MybatisPlus

一、主键配置生成策略

  1. 在配置文件中添加 id-type: auto
  2. 在实体类的主键属性上添加注解: @TableId

二、逻辑删除

mybatisplus提供了逻辑删除(即将数据库的某个状态字段改变,查询时带上状态字段,从而达到删除的效果)

  1. 在mybatisPlus的配置中增加全局逻辑删除配置:

    logic-delete-value: 1
    logic-not-delete-value: 0

  2. mybatisPlus版本 > 3.1 不需要配置类,直接在实体类的逻辑字段属性上添加注解 : @TableLogic,若某张表的逻辑删除与全局的不一致,则: @TableLogic(value = "1",delval = "0"),在注解后面加上对应的即可

    mybatis-plus:
      mapper-locations: classpath*:/mapper/**/*.xml # mapper文件扫描
      global-config:
        db-config:
          id-type: auto # 主键生成策略:自增
          logic-delete-value: 1 # 逻辑已删除 默认1
          logic-not-delete-value: 0 # 逻辑未删除 默认 0
      configuration:
        log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #打印sql
    

三、字段的自动填充

  • 在实体类的字段是使用注解:

    • @TableField(fill = FieldFill.INSERT)
      private Date createTime;
      
      @TableField(fill = FieldFill.INSERT_UPDATE)
      private Date updateTime;
      
  • 创建一个类,实现 MetaObjectHandler 接口

    • **
       * @ClassName MyMetaObjectHandler
       * @Description: 自动填充日期
       **/
      @Component
      public class MyMetaObjectHandler implements MetaObjectHandler {
      
          @Override
          public void insertFill(MetaObject metaObject) {
              this.setFieldValByName("createTime",new Date(),metaObject);
              this.setFieldValByName("updateTime",new Date(),metaObject);
          }
      
          @Override
          public void updateFill(MetaObject metaObject) {
              this.setFieldValByName("updateTime",new Date(),metaObject);
          }
      }
      

4)、文件上传

image-2020051817560000

一、使用阿里云的存储

见 SpringCloud-Alibaba组件使用

3)、JSR303数据校验

一)、使用

  1. 给Bean的属性添加校验注解,注解包:javax.validation.constraints,并添加相对于的提示

    public class BrandEntity implements Serializable {
       private static final long serialVersionUID = 1L;
    
       /**
        * 品牌id
        */
       @TableId
       private Long brandId;
       /**
        * 品牌名
        */
       @NotBlank(message = "品牌名必须提交")
       private String name;
       /**
        * 品牌logo地址
        */
       @URL(message = "logo必须是一个合法的url地址")
       private String logo;
       /**
        * 介绍
        */
       private String descript;
       /**
        * 显示状态[0-不显示;1-显示]
        */
       private Integer showStatus;
       /**
        * 检索首字母
        * 必须为 a-z|A-Z 的字母
        */
       @NotEmpty
       @Pattern(regexp = "/^[a-zA-Z]$/",message = "检索首字母必须是一个首字母")
       private String firstLetter;
       /**
        * 排序
        * 最小为 0 的数组
        */
       @NotNull
       @Min(value = 0,message = "排序必须大于等于0")
       private Integer sort;
    
    }
    
  2. 在controller中,在方法参数中添加 @Valid 注解,开启校验功能,(并添加BindingResult bindingResult,就能获取到校验结果,可以进行封装,可以集中到异常处理中去)

    // 可以省去错误判断等,直接考虑成功逻辑,错误逻辑都到异常处理中去
    public R save(@Valid @RequestBody BrandEntity brand, BindingResult result) {
        Map<String, String> map = new HashMap<>();
        if (result.hasErrors()) {
            // 1、获取校验结果
            result.getFieldErrors().forEach((item)->{
                // 错误消息
                String message = item.getDefaultMessage();
                // 获取错误的名字
                String field = item.getField();
                map.put(field,message);
            });
            return R.error(400,"提交的数据不合法").put("data",map);
        } else {
            brandService.save(brand);
        }
    
        return R.ok();
    }
    
  3. 如果请求错误了,状态码为 400 则说明输入的字段不符合

二)、自定义分组校验

比如当我们新增和修改需要不同的校验规则时,这是我们需要分组校验
  1. 指定不同的空接口:例如 AddGroup、UpdateGroup 空接口就行

  2. 给字段加上校验注解,带上 goups属性:什么情况进行那种校验,如果没有指定分组,只会在@Valid 下进行校验

    @NotNull(message = "修改必须指定品牌id",groups = {UpdateGroup.class}) 
    @NotNull(message = "新增不能指定品牌id",groups = {AddGroup.class})
    @TableId
    private Long brandId;
    
    @NotBlank(message = "品牌名必须提交",groups = {UpdateGroup.class,AddGroup.class})
    private String name;
    
  3. ​ 在controller的方法字段前中加入@Validated(xxx.class),指定那种接口下进行校验

三)、创建自定义校验注解

当我们需要自定义的校验规则,如,status状态只能时我们指定的值:0或1时,就需要它
  1. 创建自定义校验注解

    • 编写校验器注解

      package com.plusjun.common.valid;
      
      import javax.validation.Constraint;
      import javax.validation.Payload;
      import java.lang.annotation.Documented;
      import java.lang.annotation.Retention;
      import java.lang.annotation.Target;
      
      import static java.lang.annotation.ElementType.*;
      import static java.lang.annotation.RetentionPolicy.RUNTIME;
      
      @Documented
      // 指定校验规则,可以指定多个不同的校验器,适配不同类型的校验
      @Constraint(validatedBy = { ListValueConstraintValidator.class}) 
      @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
      @Retention(RUNTIME)
      public @interface ListValue {
          String message() default "{com.plusjun.common.valid.ListValue.message}";
      
          Class<?>[] groups() default { };
      
          Class<? extends Payload>[] payload() default { };
      
          // 自定义规则
          int[] vals() default {};
      }
      
  2. 编写自定义校验器

    • 编写校验器规则类

      /**
       * @ClassName ListValueConstraintValidator
       * @Description: 指定校验属性的类型
       *  实现 ConstraintValidator,泛型为:注解,指定的类型
       **/
      public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {
          private Set<Integer> set = new HashSet<>();
      
          /**
           *  初始化方法
           * @param constraintAnnotation
           */
          @Override
          public void initialize(ListValue constraintAnnotation) {
              int[] vals = constraintAnnotation.vals();
              for(int val : vals) {
                  set.add(val);
              }
          }
      
          /**
           *  判断是否校验成功
           * @param value :需要校验的值
           * @param context
           * @return
           */
          @Override
          public boolean isValid(Integer value, ConstraintValidatorContext context) {
              return set.contains(value);
          }
      }
      
    • 编写校验器提示信息配置文件,文件名为:ValidationMessages.properties

      # 注解全类名.message 
      com.plusjun.common.valid.ListValue.message=必须提交指定的值
      
  3. 在属性字段上加上自定义的校验器和自定义校验注解

    /**
     * 显示状态[0-不显示;1-显示]
     * 自定义的校验注解和规则
     */
    @ListValue(vals = {0,1},groups = {AddGroup.class})
    private Integer showStatus;
    

4)、统一异常处理

  1. 创建异常枚举类

    public enum BizCodeEnum {
        VAILD_EXCEPTION(10001,"参数格式校验失败"),
        UNKONW_EXCEPTION(10000,"系统未知异常");
        private int code;
        private String msg;
    
        BizCodeEnum(int code, String msg) {
            this.code = code;
            this.msg = msg;
        }
    
        public int getCode() {
            return code;
        }
    
        public String getMsg() {
            return msg;
        }
    }
    
  2. 创建类,标注: @RestControllerAdvice(basePackages = "com.plusjun.gulimall.product.controller")

    @Slf4j
    @RestControllerAdvice(basePackages = "com.plusjun.gulimall.product.controller")
    public class GuliamllExceptionAdvice {
    
        @ExceptionHandler(value = MethodArgumentNotValidException.class)
        public R handleVaildException(MethodArgumentNotValidException e) {
            Map<String,String> errorMap = new HashMap<>();
            log.error("数据校验出现问题:{},异常类型:{}", e.getMessage(), e.getClass());
            BindingResult bindingResult = e.getBindingResult();
            bindingResult.getFieldErrors().forEach((item) -> {
                errorMap.put(item.getField(),item.getDefaultMessage());
            });
            return R.error(BizCodeEnum.VAILD_EXCEPTION.getCode(),BizCodeEnum.VAILD_EXCEPTION.getMsg()).put("data",errorMap);
        }
    
        @ExceptionHandler(value = Exception.class)
        public R handleException(){
            return R.error(BizCodeEnum.UNKONW_EXCEPTION.getCode(),BizCodeEnum.UNKONW_EXCEPTION.getMsg());
        }
    }
    
错误码和错误信息定义类
1.错误码定义规则为5为数字
2.前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用001系统未知异常
3.维护错误码后需要维护错误描述,将他们定义为枚举形式
    错误码列表
    10:通用
    	001:参数格式校验
    11:商品
    12:订单
    13:购物车
    14:物流

5)、Object划分

一、Po( persistant object)--持久对象

po就是对应数据库中某个表中的一条记录,多个记录可以用PO的集合。PO含任何对数据库的操作。

二、DO( Domain Object)--领域对象

就是从现实世界中抽象出来的有形或无形的业务实体。

三、TO( Transfer Object)--数据传输对象

不同的应用程序之间传输的对象

四、DTO( Data Transfer object)--数据传输对象

这个概念来源于」2EE的设计模式,原来的目的是为了EB的分布式应用提供粗粒度的数据实体,以减少分布式调用的次数,从而提高分布式调用的性能和降低网络负载,但在这里,泛指用于展示层与服务层之间的数据传输对象。

五、VO(value object)--值对象

通常用于业务层之间的数据传递,和pO一样也是仅仅包含数据而已。但应是抽象出的业务对象,可以和表对应,也可以不,这根据业务的需要。用neW关键字创建,由GC回收的。
View Object 视图对象:接收页面传递来的数据,封装对象;将业务处理完成的对象,封装成页面要用的数据

六、BO(business object)--业务对象

从业务模型的角度看,见UML元件领域模型中的领域对象。封装业务逻辑的java对象,通过调用DAO方法,结合pOo进行业务操作。 business object:业务对象主要作用是把业务逻辑封装为一个对象。这个对象可以包括一个或多个其它的对象。比如一个简历,有教育经历、工作经历、社会关系等等。我们可以把教育经历对应一个po,工作经

历对应一个po,社会关系对应一个po。建立一个对应简历的Bo对象处理简历,每个Bo包含这些po。这样处理业务逻辑时,我们就可以针对Bo去处理。

七、PoJo( plain ordinary java object)--简单无规则java对象

传统意义的java对象。就是说在一些 Object/ Relation Mapping工具中,能够做到维护数据库表记录的 persisent object完全是一个符合 Java bean规范的纯lva对家,没有增加别的属性和方法。我的理解就是最基本的 java Bean,只有属性字段及 setter和 getter方法!。
POJO是DO/DTO/BO/VO的统称。

八、DAO( data access object)数据访问对象

是一个sun的一个标准j2e设计模式,这个模式中有个接囗就是DAO,它负持久层的操作。为业务层提供接口。此对象用于访问数据库。通常和PO结合使用,DAO中包含了各种数据库的操作方法。通过它的方法,结合PO对数据库进行相关的操作。夹在业务逻辑与效据库资源中间。配合VO,提供数据库的CRUD操作

6)、统一时间格式化

在springboot项目中,如果想指定时间输出的格式化,只需要在配置文件中加上:

spring:
 jackson:
  date-format: yyyy-MM-dd HH:mm:ss

7)、thymeleaf

  1. 导入依赖
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    
  2. 页面和相关的静态资源分别放入 resources中的 templatesstatic文件夹下
  3. application.yml
    spring:
        thymeleaf:
            cache: false #关 闭缓存
            #prefix: # 默认为 classpath:/templates/
            #suffix: .html # 默认为 .html
    
  4. 重新编译,当外面的 out目录下有我们的页面则可以访问
  5. 在页面中添加名称空间
    <html xmlns:th="http://www.thymeleaf.org">
    
  6. 使用thymeleaf标签

8)、正向代理与反向代理

image-20200530140137653

  • 让nginx帮我们进行反向代理:所有来源于 gulimall.com 的请求,都转到商品服务

  • nginx.conf介绍

    image-20200530145330114

  • 使用阿里云中的nginx反向代理

    • 用utools的内网穿透插件进行内网穿透

    • 拷贝 nginx配置文件中的 conf.d/default.conf 文件 gulimall.conf,并修改

      image-20200603210644671

    • 修改 nginx.conf 文件

      image-20200603171548921

    • 在网关的配置文件中添加配置

      # 需要放在最后面
      - id: guliamll_host_route
        uri: lb://gulimall-product
        predicates:
          - Host=**.plusjun.top
      
    • 在nginx的 gulimall.conf中的location中加入 proxy_set_header Host $host;

      image-20200603203822603

9)、压力测试

压力测试考察当前软硬件环境下系统所能承受的最大负荷并帮助找出系统瓶颈所在。压测都是为了系统在线上的处理能力和稳定性维持在一个标准范围内,做到心中有数。

使用压力测试,我们有希望找到很多种用其他测试方法更难发现的错误。有两种错误类型是:内存泄漏,并发与同步。

有效的压力测试系统将应用以下这些关键条件:重复,并发,量级,随机变化。

  1. 性能指标
    • 响应时间(Response Time:RT)

      • 响应时间指用户从客户端发起一个请求开始,到客户端接收到从服务器端返回的响应结束,整个过程所耗费的时间。
    • HPS(Hits Per Second):每秒点击次数,单位是次/秒。

    • TPS(Transaction per Second):系统每秒处理交易数,单位是笔/秒。

    • QPS(Query per Second):系统每秒处理查询次数,单位是次/秒。

      • 对于互联网业务中,如果某些业务有且仅有一个请求连接,那么TPS=QPS=HPS,一般情况下用TPS来衡量整个业务流程,用QPS来衡量接口查询次数,用HPS来表示对服务器单击请求。
    • 无论TPS、QPS、HPS,此指标是衡量系统处理能力非常重要的指标,越大越好,根据经
      验,一般情况下:

      • 金融行业:1000TPS~50000TPS,不包括互联网化的活动
      • 保险行业:100TPS~100000TPS,不包括互联网化的活动
      • 制造行业:10TPS~5000TPS
      • 互联网电子商务:10000TPS~1000000TPS
      • 互联网中型网站:1000TPS~50000TPS
      • 互联网小型网站:500TPS~10000TPS
    • 最大响应时间(Max Response Time)指用户发出请求或者指令到系统做出反应(响应)的最大时间。

    • 最少响应时间(Mininum ResponseTime)指用户发出请求或者指令到系统做出反应(响应)的最少时间。

    • 90%响应时间(90%Response Time)是指所有用户的响应时间进行排序,第90%的响应时间。

    • 从外部看,性能测试主要关注如下三个指标

      • 吞吐量:每秒钟系统能够处理的请求数、任务数
      • 响应时间:服务处理一个请求或一个任务的耗时。
      • 错误率:一批请求中结果出错的请求所占比例
  2. JMeter工具
    • jmeter下载

    • 解压之后找到 bin目录下的 jmeter.bat 执行

    • 使用

      1. 添加线程组

        image-20200604151641218

      2. 添加http请求

        image-20200604151952334

      3. 查看测试结果

        • 察看结果树
        • 汇总报告
        • 聚合报告

        image-20200604152115779

      4. 简单分析

        • 影响性能考虑包括;

          数据库、应用程序、中间件(tomcat、Nginx..)、网络和操作系统

        • 首先考虑我们自己的应用属于:CUPU密集型还是 IO密集型
  3. JMeter Address Already in use 错误解决

    windows本身提供的端口访问机制的问题。
    Windows提供给TCP/P链接的端口为1024-5000,并且要四分钟来循环回收他们。就导致我们在短时间内跑大量的请求时将端口占满了。

    1. cmd中,用regedit命令打开注册表
    2. 在HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters下,
      1. 右击parameters,添加一个新的DWORD(32位),名字为MaxUserPort
      2. 然后双击MaxUserPort,输入数值数据为65534,基数选择十进制(如果是分布式运行的话,控制机器和负载机器都需要这样操作哦)
      3. TCPTimedWaitDelay:30
    3. 修改配置完毕之后记得重启机器才会生效
      https://support.microsoft.com/zh-cn/help/196271/when-you-try-to-connect-from-tcp-ports-greater-than-5000-you-receive-t

10)、性能监控

1、JVM内存模型

image-20200604160322613

  • 程序计数器 Program Counter Register

    • 记录的是正在执行的虚拟机字节码指令的地址
    • 此内存区域是唯一一个在JAVA虚拟机规范中没有指定任何 OOM的区域
  • 虚拟机栈 VM Stack

    • 描述的是JAVA方法执行的内存模型,每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法接口等信息
    • 局部变量表存储了编译期可知的各种基本数据类型、对象引用
    • 线程请求的栈深度不够会报 StackOverflowError异常
    • 栈动态扩展的容量不够会报OutOfMemoryError异常
    • 虚拟机栈是线程隔离的,即每个线程都有自己独立的虚拟机栈
  • 本地方法:Native Stack

    • 本地方法栈类似于虚拟机栈,只不过本地方法栈使用的是本地方法
  • 堆:Heap

    • 几乎所有的对象实例都在堆上分配内存

    image-20200604161058208

2、堆

所有的对象实例以及数组都要在堆上分配。堆是垃圾收集器管理的主要区域,也被称为 “GC堆”;也是我们优化组多考虑的地方

  • 新生代
    • Eden 空间
    • From Survior 空间
    • To Survior 空间

3、jconsole与jvisualvm

Jdk的两个小工具jconsole、jvisualvm(升级版的jconsole,推荐使用);通过命令行启动,可监控本地和远程应用。远程应用需要配置

启动 cmd->jvisualvm 或者 jconsole

  1. jvisualvm能做什么

    监控内存泄漏,跟踪垃圾回收,执行时内存、cpu分析,线程分析...

    image-20200604162424323

    • 运行:正在运行的
    • 休眠:sleep
    • 等待:wait
    • 驻留:线程池里面的空闲线程
    • 监视:阻塞的线程,等待所
  2. 安装插件方便查看gc
    • cmd启动 jvisualvm
    • 工具->插件->可用插件->检查...
    • 如果503
    • 安装 visual GC插件,重启
  3. 监控docker中容器的cpu等状态
    • 使用 docker stats

4、监控指标

  1. 中间件指标
  2. 数据库指标
压测内容 压测线程数 吞吐量/s 90%响应时间 99%响应时间
Nginx 50 7100 4 118
Gateway 50 11012 4 9
简单服务 50 30000 2 5
首页一级菜单渲染 50 99(db,thymeleaf) 693 1328
首页一级菜单渲染(开缓存) 50 98 690 1398
首页一级菜单渲染
(开缓存,优化DB,关日志)
50
三级分类数据获取 50 50(db) 64000 65000
首页全量数据获取 50
Nginx+Gateway 50
Gateway+简单服务 50 6100 14 35
全链路 50 1100 39 61

结论:

  • 中间件越多,性能损失越多
  • 业务
    • DB
    • 模板渲染速度
    • 静态资源

5、JVM分析&调优

  • 优化业务逻辑
  • 优化数据库
  • 动静分离

11)、Nginx动静分离

image-20200604220315225

  • 以后将所有项目的静态资源都应该放在nginx里面
  • 规则: /static/** 中的所有请求都由nginx直接返回
  1. 将项目中的所有static下的文件放在nginx的index中的 static下

  2. html中的所有路径加上 /static

  3. 在nginx的/conf/conf.d/gulimall.conf中添加以下配置:将 /static下的请求都转向对应的位置

    image-20200604222206385

12)、缓存

1、缓存的使用

为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问。而db承担数据落盘工作。

哪些数据适合放入缓存呢?
  • 即时 性、数据不一致性要求不高的
  • 访问量大且更新频率不高的数据(读多,写少)
  • 例如:

    • 电商类应用,商品分类,商品列表等适合缓存并加一个失效时间(根据数据更新频率来定),后台如果发布一个商品,买家需要5分钟才能看到新的商品一般还是可以接受的。

      image-20200605142951930

    • if(cache.get(key) == null){
          // .....查询
          cache.put(key,value);
          return ;
      } else{
          return cache.get(key);
      }
      

2、redis

  1. 导入pom

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

    spring.redis.host=xxx
    
  3. config包中加入自己封装的 RedisTemplate

    @EnableConfigurationProperties(CacheProperties.class)
    @Configuration
    public class RedisConfig {
    
        // 这是我给大家写好的一个固定模板,大家在企业中,拿去就可以直接使用!
        // 自己定义了一个 RedisTemplate
        @Bean
        @SuppressWarnings("all")
        public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
            // 我们为了自己开发方便,一般直接使用 <String, Object>
            RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
            template.setConnectionFactory(factory);
    
            // Json序列化配置
            Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
            // 带上了全类名,会有bug
            /*ObjectMapper om = new ObjectMapper();
            om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
            om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
            jackson2JsonRedisSerializer.setObjectMapper(om);*/
            // String 的序列化
            StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
    
            // key采用String的序列化方式
            template.setKeySerializer(stringRedisSerializer);
            // hash的key也采用String的序列化方式
            template.setHashKeySerializer(stringRedisSerializer);
            // value序列化方式采用jackson
            template.setValueSerializer(jackson2JsonRedisSerializer);
            // hash的value序列化方式采用jackson
            template.setHashValueSerializer(jackson2JsonRedisSerializer);
            template.afterPropertiesSet();
    
            return template;
        }
    
       @Bean
        public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
            RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
            config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
            config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
            CacheProperties.Redis redisProperties = cacheProperties.getRedis();
            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;
    
    }
    
  4. 加入自己封装的redis操作

3、使用redis会出现的问题

  1. 产生堆外内存溢出:OutOfDirectMemoryError

    springBoot2.0之后,默认使用lettuce作为操作redis的客户端。它使用netty进行网络通信

    lettuce的bug导致nettry堆外内存溢出,如果nettry没有指定堆外内存,默认使用JVM的内存的大小作为堆外内存,可以通过 -Dio.netty.maxDirectMemory进行设置

    解决:

    • 不能只是调大netty的堆外内存

    • 升级lettuce客户端

    • 切换jedis(先在reis原来中排除lettuce,再加入jedis依赖)

  2. 高并发下缓存失效问题---缓存穿透
    • 缓存穿透

      指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义

    • 风险

      利用不存在的数据进行攻击,数据库瞬时压力增大,最终导致崩溃

    • 解决

      null结果缓存,并加入短暂过期时间

  3. 高并发下缓存失效问题---缓存雪崩

    • 缓存雪崩

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

    • 解决

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

  4. 高并发下缓存失效问题---缓存击穿

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

      加锁:大量并发只让一个去查,其他人等待,查到以后释放锁,其他人获取到锁,先查缓存,就会有数据,不用去db

4、缓存一致性问题

  1. 双写模式

    image-20200606164829540

  2. 失效模式

    image-20200606165040044

  3. 解决方案
    • ·无论是双写模式还是失效模式,都会导致缓存的不一致问题。即多个实例同时更新会出事。怎么办?
      ·1、如果是用户纬度数据(订单数据、用户数据),这种并发几率非常小,不用考虑这个问题,缓存数据加上过期时间,每隔一段时间触发读的主动更新即可

      ·2、如果是菜单,商品介绍等基础数据,也可以去使用canal订阅binlog的方式。
      ·3、缓存数据+过期时间也足够解决大部分业务对于缓存的要求。
      ·4、通过加锁保证并发读写,写写的时候按顺序排好队。读读无所谓。所以适合使用读写锁。(业务不关心脏数据,允许临时脏数据可忽略);

      ·总结:
      ·我们能放入缓存的数据本就不应该是实时性、一致性要求超高的。所以缓存数据的时候加上过期时间,保证每天拿到当前最新数据即可。
      ·我们不应该过度设计,增加系统的复杂性
      ·遇到实时性、一致性要求高的数据,就应该查数据库,即使慢点。

    • 使用Canal

      image-20200606170057738

13)、分布式锁

分布式情况下,只有使用分布式锁,才能锁住所有请求,本地锁只能锁住自己的那个服务---解决击穿问题
  1. 分布式锁演进--基本原理

    我们可以同时去一个地方“占坑”,如果占到,就执行逻辑。否则就必须等待,知道释放锁

    “占坑”可以去redis,可以去数据库,可以去如何大家能访问的地方

    等待可以用 自旋

    image-20200605200026492

  2. 分布式锁演进--阶段1

    image-20200605213613005

  3. 分布式锁演进--阶段2
    • 在阶段1的基础上,给lock锁设置过期时间(分步进行),会有特别情况,导致过期时间没有设置上(设置时间前断电)

    • 解决:在设置锁时带上过期时间: setIfAbsent(key,value,time,timeUtil )

  4. 分布式锁演进--阶段3

    image-20200605214456112

  5. 分布式锁演进--阶段4

    image-20200605215615055

    • Lua脚本

      String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
      // Integer返回值的类型,第二个参数为删除的key(可以时一个数组),第三个参数时key对应的value
      // 成功返回 1
      redisTemplate.execute(new DefaultRedisScript<Integer>(script,Integer.class),
                         Arrays.asList("lock"),value);
      

14)、基于redis的分布式锁--redisson

1、整合Redisson与使用

  1. 导入redisson的依赖

    <dependency>
        <groupId>org.redisson</groupId>
        <artifactId>redisson</artifactId>
        <version>3.12.0</version>
    </dependency>
    <!-- 找不到MeterBinder类时,导入-->
    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-core</artifactId>
        <version>1.0.7</version>
    </dependency>
    
  2. 配置redisson

    • 使用程序化配置

      • 创建配置类

        @Configuration
        public class MyRedissonConfig {
            /**
             * 所有堆redis的使用,都通过RedissionClient
             * @return
             * @throws IOException
             */
            @Bean(destroyMethod = "shutdown")
            RedissonClient redisson() throws IOException {
                Config config = new Config();
                // 单节点模式
                config.useSingleServer().setAddress("redis://ip:port");
                // 集群模式
                //config.useClusterServers().addNodeAddress();
                return Redisson.create(config);
            }
        }
        
  3. Redisson的可重入锁(和JUC中的Lock锁使用一致)
    • 能给锁自动续期,如果业务时间超长,运行期间自动会给锁续上新的30s。不用担心业务时间长,锁自动过期被删

    • 加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s后自动删除

    • // RLock lock = redissonClient.getLock(10,"my-lock");
      RLock lock = redissonClient.getLock("my-lock");
      lock.lock();// 默认锁30s
      try {
          // 业务代码
      } catch (Exception e) {
          e.printStackTrace();
      } finally {
          // 即使我们手动没有解锁,他还是会给我们解锁,所以不会有死锁问题
          lock.unlock();
      }
      
  4. Redisson中的读写锁
    RReadWriteLock lock = redissonClient.getReadWriteLock("my-lock");
    // RLock wLock = lock.writeLock();
    RLock rLock = lock.readLock();
    
    • 读锁:共享锁
    • 写锁:排他锁(互斥锁
    • 写 + 读 :等待写锁释放,再加读锁
    • 写 + 写 : 等待前一个写锁释放,再加写锁
    • 读 + 写:等待读锁释放,再加写锁
    • 读 + 读 : 无需等待,相当于无锁,只会在redis中记录好所有读锁。他们都会同时加锁成功
  5. 信号量Semaphore

    可以做

    1. 普通信号量

      RSemaphore semaphore1 = redissonClient.getSemaphore("my-semaphore");
      // semaphore1.tryAcquire(); // 尝试获取获取不到返回false
      semaphore1.acquire();
      
      RSemaphore semaphore2 = redissonClient.getSemaphore("my-semaphore");
      semaphore2.release();
      
    2. 带过期时间的信号量

      RPermitExpirableSemaphore semaphore = redisson.getPermitExpirableSemaphore("mySemaphore");
      String permitId = semaphore.acquire();
      // 获取一个信号,有效期只有2秒钟。
      String permitId = semaphore.acquire(2, TimeUnit.SECONDS);
      // ...
      semaphore.release(permitId);
      
  6. 闭锁CountDownLatch
    // 等待班级中的人全部走完最后锁门
    RCountDownLatch latch = redisson.getCountDownLatch("door");
    latch.trySetCount(50);
    latch.await();// 等待上面设置的数减少为0
    
    // 在其他线程或其他JVM里
    RCountDownLatch latch = redisson.getCountDownLatch("door");
    latch.countDown();
    

15)、Spring Cache

1、简介

  • Spring从3.1开始定义了org.springframework.cache.Cache和org.springframework.cache.CacheManager接口来统一不同的缓存技术;并支持使用JCache(JSR-107)注解简化我们开发;
  • Cache接口为缓存的组件规范定义,包含缓存的各种操作集合;
    Cache接口下Spring提供了各种xxxCache的实现;如RedisCache,EhCacheCache,ConcurrentMapCache等;
  • 每次调用需要缓存功能的方法时,Spring会检查检查指定参数的指定的目标方法是否已经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。
  • 使用Spring缓存抽象时我们需要关注以下两点;
    • 1、确定方法需要被缓存以及他们的缓存策略
    • 2、从缓存中读取之前缓存存储的数据

2、基础概念

  1. 缓存管理器

    • 整合SpringCache简化redis缓存开发

      1. 导入依赖:spring-boot-starter-cache,redis依赖

      2. 配置yaml:

        spring:
          cache:
            redis:
              time-to-live: 60000 #过期时间60000ms
              #key-prefix: CACHE_ # 如果指定了就用我们指定的前缀。没有就默认使用缓存的名字作为前缀
              use-key-prefix: true #使用前缀
              cache-null-values: true #是否缓存空值。防止缓存穿透
        
      3. 在启动类上开启缓存功能: @EnableCaching

3、注解

  1. @Cacheable:触发将数据保存到缓存的操作

    @Cacheable(value={"xxxx","xxxx" },key="'abc'")

    • value:每一个需要缓存的数据我们都来指定要放到哪个名字的缓存【缓存的分区(按照业务类型分)】
    • key:指定生成的缓存使用的key--key属性指定,接受一个SpEL: #root.method.name
    • 在配置文件中指定缓存的存活时间--spring.cache.redis.time-to-live=60000 #60s
    • 将数据保存为json格式--使用自定义缓存管理器(见redis的config)
  2. @CacheEvict:触发将数据从缓存中删除的操作

    @CacheEvict(value = "category",key = "'getLevel1Categorys'")

    当更新时,缓存被删除(失效模式)

  3. @CachePut:不影响方法执行更新缓存
  4. @Caching:组合以上多个操作

    组合2个删除操作

    @Caching(evict = {
         @CacheEvict(value = "category",key = "'getLevel1Categorys'"),
         @CacheEvict(value = "category",key = "'getCatelog2VO'")
    })
    

    指定删除某个分区中的所有缓存

    @CacheEvict(value = "category",allEntries = true)
    
  5. @CacheConfig:在类级别共享缓存的系统配置

4、表达式语法

5、Spring-Cache的不足与解决

  1. 读模式

    • 缓存穿透:查询一个null数据-->解决:缓存空数据:cache-null-values: true
    • 缓存击穿:大量并发进来同时查询一个正好过期的数据-->解决:加锁,默认没加锁,在@Cacheable中加入:syn=true
    • 缓存雪崩:大量key同时过期-->解决:加上过期时间:time-to-live: 60000
  2. 写模式:(缓存与数据库一致)

    • 读写加锁
    • 引入Canal,感知到MySQL的更新去更新数据库
    • 读多写多,直接去数据库查询
  3. 总结:

    • 常规数据(读多写少,及时性、一致性要求不高的数据);完全可以使用Spring-Cache负责缓存读写
    • 要求及时性高的就需要加分布式读写锁、引入Canal等等

16)、CompletableFuture--异步编排

业务场景:查询商品详情页的逻辑比较复杂,有些数据还需要远程调用,必然是需要花费更多的时间

  1. 获取sku的基本信息 花费0.5
  2. 获取sku的图片信息 花费0.5s
  3. 获取sku的促销信息 花费1s
  4. 获取spu的所有销售属性 花费1s
  5. 获取规格参数及组下的规格参数 1.5s
  6. spu详情 1s

假如商品详情页的每个查询,需要如下标注的时间才能完成
那么,用户需要6.5s后才能看到商品详情页的内容。很显然是不能接受的。
如果有多个线程同时完成这6步操作,也许只需要1.5s即可完成响应。

1、创建异步对象
  • CompletableFuture 提供了 4个静态的方法来创建一个异步操作

    public static CompletableFuture<Void> runAsync(Runnable runnable)
    public static CompletableFuture<Void> runAsync(Runnable runnable,Executor executor)
    public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
    public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier,Executor executor)
    
    • 例子

      • System.out.println("main...start");
                /********没有返回值**********/
                CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
                    System.out.println("当前线程:" + Thread.currentThread().getName());
                    int i = 10 / 2;
                    System.out.println("运行结果" + i);
                }, executor);
                /*********有返回值*********/
                CompletableFuture<Integer> supplyAsync = CompletableFuture.supplyAsync(() -> {
                    return 10 / 2;
                }, executor).whenComplete((res,exception)->{
                    System.out.println("结果是:" + res + ",异常是:" + exception);
                });
        
                System.out.println(supplyAsync.get());
        
                System.out.println("main...stop");
        
  • 传入第一个参数为 Runnable接口(函数式接口,直接使用 lambda表达式)

  • 传入第二个为 自定义线程池,否则就是用默认的线程池

2、计算完成时回调方法
  • whenComplete方法

    public CompletableFuture<T> whenComplete(BiConsumer<? super T, ? super Throwable> action)
    public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T, ? super Throwable> action)    
    public CompletableFuture<T> whenCompleteAsync(
            BiConsumer<? super T, ? super Throwable> action, Executor executor)
    public CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn)
    

    whenComplete可以处理正常和异常的计算结果,exceptionally处理异常情况。
    whenComplete和whenCompleteAsync的区别:

    • whenComplete:是执行当前任务的线程执行继续执行whenComplete的任务。
    • whenCompleteAsync:是执行把whenCompleteAsync这个任务继续提交给线程池来进行执行。
    方法不以Async结尾,意味着Action使用相同的线程执行,而Async可能会使用其他线程执行(如果是使用相同的线程池,也可能会被同一个线程选中执行)

    exceptionally:当异常出现时可以修改感知异常,并且返回异常信息

    • CompletableFuture<Integer> supplyAsync = CompletableFuture.supplyAsync(() -> {
                  return 10 / 2;
              }, executor).whenComplete((res,exception)->{
                  System.out.println("结果是:" + res + ",异常是:" + exception);
              }).exceptionally(t->{
                  // 可以感知异常,出现异常后返回自定义数据
                  return 10;
              });
      
3、handle方法
  • <U> ConnectionFuture<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> var1);
    <U> ConnectionFuture<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> var1, Executor e);
    
    handle既可以感知异常并给定默认返回结果,又可以处理正常返回结果
    • CompletableFuture<Integer> supplyAsync = CompletableFuture.supplyAsync(() -> {
          return 10 / 5;
      }, executor).handleAsync((res,exception) -> {
          // 如果有结果(没有出现异常0,则将结果 *2 返回
          if(res != null) {
              return res *2;
          }
          // 有异常,返回0
          return 0;
      });
      
4、线程串行化方法
  • <U> ConnectionFuture<U> thenApply(Function<? super T, ? extends U> var1);
    <U> ConnectionFuture<U> thenApplyAsync(Function<? super T, ? extends U> var1);
    <U> ConnectionFuture<U> thenApplyAsync(Function<? super T, ? extends U> var1, Executor var2);
    
    ConnectionFuture<Void> thenAccept(Consumer<? super T> var1);
    ConnectionFuture<Void> thenAcceptAsync(Consumer<? super T> var1);
    ConnectionFuture<Void> thenAcceptAsync(Consumer<? super T> var1, Executor var2);
    
    ConnectionFuture<Void> thenRun(Runnable var1);
    ConnectionFuture<Void> thenRunAsync(Runnable var1);
    ConnectionFuture<Void> thenRunAsync(Runnable var1, Executor var2);
    

    thenApply方法:当一个线程依赖另个线程时,获取上一个任务返回的结果,并返回当前任务的返回值
    thenAccept方法:消费处理结果。接收任务的处理结果,并消费处理,无返回结果
    thenRun方法:不能获取上一步的执行结果,只要上面的任务执行完成,就开始执行thenRun,只是处理完任务后,执行thenRun的后续操作
    带有Async默认是异步执行的。同之前。
    以上都要前置任务成功完成。

5、两任务组合 - 都要完成
  • <U, V> ConnectionFuture<V> thenCombine(CompletionStage<? extends U> var1, BiFunction<? super T, ? 		super U, ? extends V> var2);
    
    <U, V> ConnectionFuture<V> thenCombineAsync(CompletionStage<? extends U> var1, BiFunction<? super T, 	? super U, ? extends V> var2);
    
    <U, V> ConnectionFuture<V> thenCombineAsync(CompletionStage<? extends U> var1, BiFunction<? super T, 	? super U, ? extends V> var2, Executor var3);
    
    
    <U> ConnectionFuture<Void> thenAcceptBoth(CompletionStage<? extends U> var1, BiConsumer<? super T, ? 	super U> var2);
    <U> ConnectionFuture<Void> thenAcceptBothAsync(CompletionStage<? extends U> var1, BiConsumer<? super 	T, ? super U> var2);
    <U> ConnectionFuture<Void> thenAcceptBothAsync(CompletionStage<? extends U> var1, BiConsumer<? super 	T, ? super U> var2, Executor var3);
    
    ConnectionFuture<Void> runAfterBoth(CompletionStage<?> var1, Runnable var2);
    ConnectionFuture<Void> runAfterBothAsync(CompletionStage<?> var1, Runnable var2);
    ConnectionFuture<Void> runAfterBothAsync(CompletionStage<?> var1, Runnable var2, Executor var3);
    
    <U> ConnectionFuture<Void> thenAcceptBoth(CompletionStage<? extends U> var1, BiConsumer<? super T, ? 	super U> var2);
    <U> ConnectionFuture<Void> thenAcceptBothAsync(CompletionStage<? extends U> var1, BiConsumer<? super 	T, ? super U> var2);
    <U> ConnectionFuture<Void> thenAcceptBothAsync(CompletionStage<? extends U> var1, BiConsumer<? super 	T, ? super U> var2, Executor var3);
    
    ConnectionFuture<Void> runAfterBoth(CompletionStage<?> var1, Runnable var2);
    ConnectionFuture<Void> runAfterBothAsync(CompletionStage<?> var1, Runnable var2);
    ConnectionFuture<Void> runAfterBothAsync(CompletionStage<?> var1, Runnable var2, Executor var3);
    
  • 两个任务必须都完成,触发该任务

    thenCombine:组合两个future,获取两个future的返回结果,并返回当前任务的返回值
    thenAcceptBoth:组合两个future,获取两个future任务的返回结果,然后处理任务,没有返回值。
    runAfterBoth:组合两个future,不需要获取future的结果,只需两个future处理完任务后,处理该任务。
6、两任务组合 - 一个完成
  • <U> ConnectionFuture<U> applyToEither(CompletionStage<? extends T> var1, Function<? super T, U> var2);
    <U> ConnectionFuture<U> applyToEitherAsync(CompletionStage<? extends T> var1, Function<? super T, U> var2);
    <U> ConnectionFuture<U> applyToEitherAsync(CompletionStage<? extends T> var1, Function<? super T, U> var2, Executor var3);
    
    ConnectionFuture<Void> acceptEither(CompletionStage<? extends T> var1, Consumer<? super T> var2);
    ConnectionFuture<Void> acceptEitherAsync(CompletionStage<? extends T> var1, Consumer<? super T> var2);
    ConnectionFuture<Void> acceptEitherAsync(CompletionStage<? extends T> var1, Consumer<? super T> var2, Executor var3);
    
    ConnectionFuture<Void> runAfterEither(CompletionStage<?> var1, Runnable var2);
    ConnectionFuture<Void> runAfterEitherAsync(CompletionStage<?> var1, Runnable var2);
    ConnectionFuture<Void> runAfterEitherAsync(CompletionStage<?> var1, Runnable var2, Executor var3);
    
  • 当两个任务中,任意一个future任务完成的时候,执行任务。

    applyToEither:两个任务有一个执行完成,获取它的返回值,处理任务并有新的返回值。
    acceptEither:两个任务有一个执行完成,获取它的返回值,处理任务,没有新的返回值。
    runAfterEither:两个任务有一个执行完成,不需要获取future的结果,处理任务,也没有返回值。
7、多任务组合
  • public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs)
        
    public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs)
    
  • allOf:等待所有任务完成
    anyOf:只要有一个任务完成

17)、与配置文件绑定的配置类

  1. 创建 xxxConfigProperties类,并标注

    @ConfigurationProperties(prefix = "gulimall.thread") //配置文件中以 "gulimall.thread" 开头
    @Component // 将其加入容器
    
  2. 添加提示pom依赖(可不加)

    • <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-configuration-processor</artifactId>
          <optional>true</optional>
      </dependency>
      
  3. 示例

    • properties

      @ConfigurationProperties(prefix = "gulimall.thread")
      @Component
      @Data
      public class ThreadPoolProperties {
          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.getMaxSize(), TimeUnit.SECONDS,
                      new LinkedBlockingQueue<>(100000),
                      Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
          }
      
      }
      
    • yaml

      gulimall:
        thread:
          core-size: 20
          max-size: 200
          keep-alive-time: 10
      

18)、社交登录

就像我们平常看到的,通过QQ登录类似的社交登录

1、OAuth2.0

  • OAuth:OAuth(开放授权)是一个开放标准,允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方网站或分享他们数据的所有内容。
  • OAuth2.0:对于用户相关的OpenAPI(例如获取用户信息,动态同步,照片,日志,分享等),为了保护用户数据的安全和隐私,第三方网站访问用户数据前都需要显式的向用户征求授权。
  • 官方版流程
    • image-20200621190543145

2、微博授权登录

  1. 开通微博授权登录
    • 搜索微博开放平台,找到微连接下的网站接入,填写相关信息后,填入应用名称
    • 在 微博开放平台的 我的应用中的 应用信息->高级信息,填写 授权和取消授权回调页
  2. 按照文档,编写相关的流程 https://open.weibo.com/wiki/授权机制说明

19)、Session共享

1、session原理和问题

image-20200624154831189

存在的问题

1、不能跨不同域名共享

2、同一个服务,复制多份,session不能同步

2、解决方案(了解)

1)、session复制(集群少可使用)

image-20200624155339524

·优点
·web-server(Tomcat)原生支持,只需要修改配置文件

·缺点
·session同步需要数据传输,占用大量网络带宽,降低了服务器群的业务处理能力
·任意一台web-server保存的数据都是所有web-server的session总和,受到内存限制无法水平扩展更多的web-server
·大型分布式集群情况下,由于所有web-server都全量保存数据,所以此方案不可取。

2)、客户端存储(弃用)

image-20200624155428467

·优点
·服务器不需存储session,用户保存自己的session信息到cookie中。节省服务端资源

·缺点
·都是缺点,这只是一种思路。
·具体如下:
·每次http请求,携带用户在cookie中的完整信息,浪费网络带宽
·session数据放在cookie中,cookie有长度限制4K,不能保存大量信息
·session数据放在cookie中,存在泄漏、奠改、窃取等安全隐患
· 这种方式不会使用。

3)、hash一致性

image-20200624155935987

·优点:
·只需要改nginx配置,不需要修改应用代码
·负载均衡,只要hash属性的值分布是均匀的,多台web-server的负载是均衡的可以支持web-server水平扩展(session 同步法是不行的,受内存限制)

·缺点
·session还是存在web-server中的,所以web-server重启可能导致部分session丢失,影响业务,如部分用户需要重新登 录
·如果web-server水平扩展,rehash后session重新分布也会有一部分用户路由不到正确的session
·但是以上缺点问题也不是很大,因为session本来都是有有效期的。所以这两种反向代理的方式可以使用

4)、统一存储

image-20200624160130386

·优点:
·没有安全隐患
·可以水平扩展,数据库/缓存水平切分即可
·web-server重启或者扩容都不会有session丢失

·不足
·增加了一次网络调用,并且需要修改应用代码;如将所有的getSession方法替换为从Redis查数据的方式。redis获取数 据比内存慢很多
·上面缺点可以用SpringSession完美解决

3、SpringSession(掌握)

1)、不同服务,子域session共享

image-20200624163628435

2)、整合SpringSession
  1. pom.xml

    <dependency>
        <groupId>org.springframework.session</groupId>
        <artifactId>spring-session-data-redis</artifactId>
    </dependency>
    
  2. applicatiom.yaml

    spring:
    	session: 
    		store-type: redis # 存储类型 redis
        	timeout: 30m # 过期时间 30min
    # 还需要配置redis
    
  3. 开启SpringSession功能

    在主启动类上添加: @EnableRedisHttpSession 注解来开启
3)、使用SpringSession
  1. 将值设置到session域中

    HttpSession session.setAttribute(k,v);

  2. 放大作用域和序列化

    @Configuration
    public class SessionConfig {
        /**
         * 自定义cookie
         * @return
         */
        @Bean
        public CookieSerializer cookieSerializer(){
            DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
            // 自定义cookie
            // 1、设置作用域
            cookieSerializer.setDomainName(".gulimall.com");
            // 2、设置cookie名字
            cookieSerializer.setCookieName("GULISESSION");
    
            return cookieSerializer;
        }
    
        /**
         * 自定义序列化机制
         */
        @Bean
        public RedisSerializer<Object> springSessionDefaultRedisSerializer(){
            return new GenericJackson2JsonRedisSerializer();
        }
    
    }
    
  3. 在需要取session的服务内放置一份config,或者直接放到common中

4、SpringSession核心原理

  1. @EnableRedisHttpSession 导入 RedisHttpSessionConfiguration配置
    1. 给用其中添加了一个组件:RedisOperationsSessionRepository =》redis操作session=》session的增删改查封装类
    2. SessionRepositoryFilter ==》Filter: session存储过滤器;每个请求过来都必须经过过滤器
      1. 创建的时候,就自动从容器中获取到了 SessionRepository
      2. 原始的 request,response分别被包装成 SessionRepositoryRequestWrapper,SessionRepositoryResponseWrapper
      3. 以后获取session时使用:request.getSession
      4. wrapperRequest.getSession(); ==> SessionRepository中获取的

20)、单点登录(SSO)

1、核心流程

三个系统即使域名不一样,想办法给三个系统同步一个用户的票据

  1. 中央认证服务器:ssoserver.com
  2. 其他系统,想要登录去 ssoserver.com 登录,登录成功再跳转回来
  3. 只要有一个登录,其他的都不用登录
  4. 全系统统一一个 sso-sessionid;所有系统可能都不相同
  • sso-server

    @PostMapping("/doLogin")
    public String login(UserVO userVO,
                        HttpServletResponse response) {
        if (!StringUtils.isEmpty(userVO.getUsername()) && !StringUtils.isEmpty(userVO.getPassword())) {
            String uuid = UUID.randomUUID().toString().replace("-", "");
            // 将唯一id添加到redis
            stringRedisTemplate.opsForValue().set(uuid, userVO.getUsername(),10, TimeUnit.MINUTES);
            // 向浏览器中添加一个自定义cookie
            Cookie cookie = new Cookie("sso_token", uuid);
            response.addCookie(cookie);
            // 登录成功就跳回之前页面
            return "redirect:" + userVO.getUrl() + "?token=" + uuid;
        }
        return "login";
    }
    
    @GetMapping("/login.html")
    public String tologinPage(@RequestParam("redirect_url") String url, Model model,
                              @CookieValue(value = "sso_token", required = false) String sso_token) {
        // 第一次登录时没有携带 sso_token,不会执行下面逻辑
        // 只有登录过才携带了sso_token
        if (!StringUtils.isEmpty(sso_token)) {
            return "redirect:" + url + "?token=" + sso_token;
        }
        model.addAttribute("url", url);
    
        // 登录成功就跳回之前页面
        return "login";
    }
    
  • client

    // 在配置文件中配置服务器地址
    @Value("${sso.server.url}")
    private String ssoServerUrl;
    
    /**
    * @param model
    * @param session
    * @param token   : 从ssoServer中跳回来的请求中携带了 token
    * @return
    */
    @GetMapping("/employees")
    public String employees(Model model, HttpSession session,
                            @RequestParam(required = false, value = "token") String token) {
        // 第一次访问时没有 token
        if (!StringUtils.isEmpty(token)) {
            RestTemplate restTemplate = new RestTemplate();
            // 从服务器通过 token获取信息
            ResponseEntity<String> entity = restTemplate.getForEntity(ssoServerUrl+"/info?token="+token, String.class);
            String name = entity.getBody();
            session.setAttribute("loginUser", name);
        }
        Object loginUser = session.getAttribute("loginUser");
        if (loginUser == null) {
            // 没登陆就去登录页
            // 带上自己的需要调回的页面
            return "redirect:" + ssoServerUrl + "?redirect_url=http://client1.com:8081/employees";
        }
        List<String> employees = new ArrayList<>();
        employees.add("张三");
        employees.add("李四");
        model.addAttribute("emps", employees);
        return "list";
    }
    

21)、购物车

1、购物车需求

1)、需求描述

-用户可以在登录状态下将商品添加到购物车【用户购物车/在线购物车】
-放入数据库
-mongodb
-放入redis(采用),登录后会将临时购物车的数据全部合并过来,并且清空临时购物车
-用户可以在未登录状态下将商品添加到购物车【游客购物车/离线购物车】
-放入localstorage
-cookie
-WebSQL
-放入redis(采用)
-用户可以使用购物车一起结算下单
-用户可以查询自己的购物车
-用户可以在购物车中修改购买商品的数量
-用户可以在购物车中删除商品
-选中不选中商品
-在购物车中展示商品优惠信息
-提示购物车商品价格变化

2)、数据结构
  • 每一个购物项信息,都是一个对象,基本字段如下:

    {
        skuId: 12121,
        check: true,
        title: "xxx",
        defaultImage: "xxx",
        price: 12121,
        count: 1,
        totalPrice: 12121,
        skuSaleVO: {...}
    }
    
  • 购物车中不止一条数据,所有最终对象时数组

    {
        {...},{...},{...}
    }
    
  • 在redis中使用 hash来保存整个购物车

    Map<String k1,Map<String k2,CartItemInfo>>
    k1: 标识每一个用户的购物车
    k2: 购物项的商品id
    
3)、流程

2、临时购物车

3、登录购物车

22)、ThreadLocal

在购物系统中,使用到了ThreadLocal 拦截器的使用

拦截器 放行前,使用 ThreadLocal保存一个 UserInfoTO实例,然后在 Controller中,可以通过 CartInterceptor.threadLocal.get();拿到拦截器中保存的实例。在这个线程中,都可以通过这种方法取到前面所放置的东西

  • 编写拦截器

    public class CartInterceptor implements HandlerInterceptor {
    
        public static ThreadLocal<UserInfoTO> threadLocal = new ThreadLocal<>();
        /**
         * 执行目标方法之前拦截
         */
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            /**
             * 业务代码。。。。
             */
            // 调用目标方法之前,将用户信息放入threadLocal中
            threadLocal.set(userInfoTO);
            return true;
        }
    
        /**
         * 业务执行之后,将临时用户放入cookie
         */
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    		// 通过 threadLocal获取
            UserInfoTO userInfoTO = threadLocal.get();
            // 如果时临时用户,则设置cookie
            if(!userInfoTO.isTempUser()) {
                Cookie cookie = new Cookie(CartConstant.TEMP_USER_COOKIE_NAME, userInfoTO.getUserKey());
                cookie.setDomain("gulimall.com");
                cookie.setMaxAge(CartConstant.TEMP_USER_COOKIE_TIMEOUT);
                response.addCookie(cookie);
            }
    
        }
    }
    
  • 注册拦截器

    @Configuration
    public class GulimallWebConfig implements WebMvcConfigurer {
    
        /**
         * 注册拦截器
         * @param registry
         */
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new CartInterceptor());
        }
    }
    

23)、MQ之RabbitMQ

1、MQ概述

1.大多应用中,可通过消息服务中间件来提升系统异步通信、扩展解精能力

2.消息服务中两个重要概念:
消息代理(message broker)目的地(destination)
当消息发送者发送消息以后,将由消息代理接管,消息代理保证消息传递到指定目的地。

3.消息队列主要有两种形式的目的地
1.队列(queue):点对点消息通信(point-to-point)
2.主题(topic):发布(publish)/订阅(subscribe)消息通信

4.点对点式:
-- 消息发送者发送消息,消息代理将其放入一个队列中,消息接收者从队列中获取消息内容,消息读取后被移出队列
-- 消息只有唯一的发送者和接受者,但并不是说只能有一个接收者

5.发布订阅式:
-- 发送者(发布者)发送消息到主题,多个接收者(订阅者)监听(订阅)这个主题,那么就会在消息到达时同时收到消息

6.JMS(Java Message Service)JAVA消息服务:
-- 基于JVM消息代理的规范。ActiveMQ、HornetMQ是JMS实现

7.AMQP(Advanced Message Queuing Protocol)
-- 高级消息队列协议,也是一个消息代理的规范,兼容JMS
-- RabbitMQ是AMQP的实现

8.Spring支持
-- spring-jms提供了对JMS的支持
-- spring-rabbit提供了对AMQP的支持
-- 需要ConnectionFactory的实现来连接消息代理
-- 提供JmsTemplate、RabbitTemplate来发送消息
-- @JmsListener(JMS)、@RabbitListener(AMQP)注解在方法上监听消息代理发布的消息
-- @EnableJms、@EnableRabbit开启支持

9.Spring Boot自动配置
-- JmsAutoConfiguration
-- RabbitAutoConfiguration

10.市面的MQ产品
-- ActiveMQ、 RabbitMQ、 RocketMQ、 Kafka

JMS(Java Message Service) AMQP(Advanced Message Queueing Protocol)
定义 Java API 网络线极协议
跨语言
跨平台
Model 提供两种消息模型:
1)、Peer-2-Peer
2)、Pub/sub
提供了五种消息模型:
(1)、direct exchange
(2)、fanout exchange
(3)、topic change
(4)、headers exchange
(5)、system exchange
本质来讲,后四种和JMS的pub/sub模型没有太大差别,仅是在路由机制上做了更详细的划分
支持消息类型 多种消息类型:
TextMessage
MapMessage
BytesMessage
StreamMessage
ObjectMessage
Message(只有消息头和属性)
byte[]
当实际应用时,有复杂的消息,可以将消息序列化后发送
综合评价 JMS 定义了JAVA API层面的标准;
在java体系中,多个client可以通过JMS进行交互,不需要应用修改代码,但是其对平台的支持较差;
AMQP定义了wire-level层的协议标准:天然具有跨平台、跨语言特性。

2、RabbitMQ概念

RabbitMQ简介:RabbitMQ是一个由erlang开发的AMQP(Advanved Message Queue Protocol)的开源实现。

核心概念

Message

消息,消息是不具名的,它由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。

Publisher

消息的生产者,也是一个向交换器发布消息的客户端应用程序。

Exchange

交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。
Exchange有4种类型:direct(默认),fanout,topic,和headers,不同类型的Exchange转发消息的策略有所区别

Queue

消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。

Binding

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

Connection

网络连接,比如一个TCP连接。

Channel

信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内的虚车接,AMQP命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁TCP都是非常昂贵的开销,所以引入了信道的概念,以复用一条TCP连接。

Consumer

消息的消费者,表示一个从消息队列中取得消息的客户端应用程序。

Virtual Host

虚拟主机,表示一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。每个vhost本质上就是一个mini版的RabbitMQ服务器,拥有自己的队列、交换器、绑定和权限机制。vhost是AMQP概念的基础,必须在连接时指定,RabbitMQ默认的vhost是 /

Broker

表示消息队列服务器实体

image-20200701174839733

3、docker安装启动RabbitMQ

  • docker run -d --name rabbitmq -p 5671:5671 -p 5672:5672 -p 4369:4396 -p 25672:25672 -p 15671:15671 -p 15672:15672 rabbitmq:management
    
    • 4369,25672(Erlang发现&集群端口)
      5672,5671(AMQP端口)
      15672(web管理后台端口)
      61613,61614(STOMP协议端口)
      1883,8883(MQTT协议端口)
      https://www.rabbitmq.com/networking.html

  • 启动后访问: ip:15672;账号密码:guest

4、RabbitMQ运行机制

AMQP中的消息路由
·AMQP中消息的路由过程和Java开发者熟悉的JMS存在一些差别,AMQP中增加了Exchange和Binding的角色生产者把消息发布到
Exchange 上,消息最终到达队列并被消费者接收,而Binding 决定交换器的消息应该发送到那个队列。

image-20200701181420929

  • Exchange类型

    Exchange分发消息时根据类型的不同分发策略有区别,目前共四种类型:direct、fanout、topic、headers。headers匹配AMQP消息的header而不是路由键,headers交换器和direct交换器完全一致,但性能差很多,目前几乎用不到了,所以直接着另外三种类型:

    • image-20200701181704895

    • image-20200701181831391

    • image-20200701181844127

5、SB整合使用RabbitMQ

一、整合
  1. pom.xml

    • <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-amqp</artifactId>
      </dependency>
      
  2. application.yaml

    • spring: 
      	rabbitmq:
      		host: 101.200.53.195  #远程地址
      		port: 5672 # 高级消息协议端口
      		virtual-host: / #虚拟主机地址
      		# 开启发送端确认
              publisher-confirm-type: CORRELATED
              # 开启发送端消息抵达队列的确认
              publisher-returns: true
              # 只要抵达队列,以异步发送有限回调我们这个returenconfirm
              template:
                mandatory: true
              # 开启手动确认接受消息
              listener:
                simple:
                  acknowledge-mode: manual
      
  3. 再启动类上添加 注解: @EnableRabbit

  4. 一些配置类

    • @Configuration
      public class MyRabbitConfig {
          /**
           *  配置使用Json的消息转换器
           * @return
           */
          @Bean
          public MessageConverter messageConverter(){
              return new Jackson2JsonMessageConverter();
          }
          
          /*其他配置在下方:消息确认机制中*/
      }
      
二、使用
  1. 如何创建 Exchange、Queue、Binding

    @Resource
    AmqpAdmin amqpAdmin;

    • Exchange

      • // 交换机名称,是否持久化,是否自动删除
        DirectExchange directExchange = new DirectExchange("hello-java-direct-exchange",true,false);
        amqpAdmin.declareExchange(directExchange);
        
    • Queue

      • // 指定队列名称,是否持久化,是否是排他队列,是否自动删除
        Queue queue = new Queue("hello-java-queue",true,false,false);
        amqpAdmin.declareQueue(queue);
        
      • 排他队列:Queue只能被一条链接连上

    • Bingding

      • /**
         * String destination: 目的地
         * Binding.DestinationType destinationType:目的地类型
         * String exchange:交换机
         * String routingKey: 路由键
         * @Nullable Map<String, Object> arguments 自定义参数
         * 将 exchange 指定的交换机和 destination 目的地进行绑定,
         * 使用 routeKey作为路由键
         */
        Binding binding = new Binding("hello-java-queue",
                                      Binding.DestinationType.QUEUE,
                                      "hello-java-direct-exchange",
                                      "hello",
                                      null);
        amqpAdmin.declareBinding(binding);
        
  2. 如何收发消息

    @Resource
    RabbitTemplate rabbitTemplate;

    • 发送消息

      • // msg 可以是String 也可以是 对象,但对象需要实现序列化接口
        // 可以配置消息转换策略将消息转换成Json 见上方配置类
        rabbitTemplate.convertAndSend(exchange,routeKey,msg);
        
    • 监听接收消息

      • @RabbitListener:queues:声明要监听的队列,标注再方法/类上(监听哪些队列
        @RabbitHandler:只能标注在方法上(重载区分不同的消息

        接收到的消息类型:class org.springframework.amqp.core.Message

        标注了@RabbitListener注解的方法的参数可以写:
        1、Message message: 原生的消息详细消息。头+ 体
        2、MyEntity content:发送的实体类类型 ==> 接收的就是发送时的类型数据
        3、com.rabbitmq.client.Channel channel :传输数据的通道

        Queue可以有很多人同时监听,只要收到消息,队列就删除消息,而且只能一个收到消息
        场景:
        1、订单服务启动多个时,同一个消息,只能有一个客户端收到
        2、只有一个消息完全处理完,方法运行结束,才可以接受下一条消息

      • @Service
        @RabbitListener(queues = {"hello-java-queue"})
        class xxx{
            
            @RabbitHandler
            public void recieveMsg(Message message,
                                   MemberPrice memberPrice){
                System.out.println( memberPrice);
            }
        
            @RabbitHandler
            public void recieveMsg2(Object o) {
                System.out.println(o);
            }    
        }
        

6、RabbitMQ消息确认机制--可靠抵达

    • 保证消息不丢失,可靠抵达,可以使用事务消息,性能下降250倍,为此引入确认机制
    • publisher confirmCallback确认模式
    • publisher returnCallback 未投递到 queue 退回模式
    • consumer ack机制

    image-20200702165003278

1)、发送方
  • 可靠抵达-ConfirmCallback:成功后表示Publish投递到了Broker
    • spring.rabbitmq.publisher-confirms=true(过期使用下面的)

    • spring.rabbitmqpublisher-confirm-type=CORRELATED

    • 在创建connectionFactory的时候设置PublisherConfirms(true)选项,开启confirmcallback。

    • CorrelationData:用来表示当前消息唯一性。

    • 消息只要被broker接收到就会执行confirmCallback,如果是cluster模式,需要所有broker接收到才会调用confirmCallback。

    • 被broker接收到只能表示message已经到达服务器,并不能保证消息一定会被投递到目标queue里。所以需要用到接下来的returnCallback。

    可靠抵达-ReturnCallback
    • spring.rabbitmq.publisher-returns=true
    • spring.rabbitmq.template.mandatory=true
    • confrim模式只能保证消息到达broker,不能保证消息准确投递到目标queue里。在有些业务场景下,我们需要保证消息一定要投递到目标queue里,此时就需要用到return退回模式。
    • 这样如果未能投递到目标queue里将调用returnCallback,可以记录下详细到投递数据,定期的巡检或者自动纠错都需要这些数据。
  • 配置类中

    • /**
       * 定制ReabbitTemplate--发送端确认:
       * @PostConstruct: 在所在类的对象创建完之后执行
       *  1:服务器收到消息就回调
       *      1)、配置yaml:spring.rabbitmqpublisher-confirm-type=CORRELATED
       *      2)、设置确认回调
       *  2:消息正确抵达队列进行回调
       *     1)、yaml:spring.rabbitmq.publisher-returns=true
       *               spring.rabbitmq.template.mandatory=true
       *     2)设置回调
       *
       * 定制ReabbitTemplate--消费端确认:(保证每个消息被正确消费,此时才可以broker删除这个消息)。
       *  1、默认是自动确认的,只要消息接收到,客户端会自动确认,服务端就会移除这个消息
       *      问题:我们收到很多消息,自动回复给服务器ACK,只有一个消息处理成功就宕机了,然后剩下的消息都丢失了
       * 		解决:开启消费者手动确认。只要我们没有明确告诉MQ,消息被签收,没有ACK,就一直时unacked,
       *            即使服务器宕机,消息也不会都是,会重新变为Ready,下一次新的Consumer连接就发给他
       *  2、签收消息:
       *      Long deliveryTag = message.getMessageProperties().getDeliveryTag();
       *      deliveryTag在channel中的消息序号(自增的)
       *      在消费消息后签收: channel.basicAck(deliveryTag,false);//false:非批量模式
       *      拒收: channel.basicNack(deliveryTag,false,false);// 业务失败
       *				//退货 requeue=false 丢弃 requeue=true 发回服务器,服务器重新入队。
       *               //Long deliveryTag,boolean multiple,boolean requeue
       */
      @PostConstruct
      public void initRabbitTemplate() {
          // 设置确认回调
          rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
              /**
               * 只要消息抵达服务器就执行回调
               * @param correlationData:当前消息的唯一关联数据(消息的唯一id)
               * @param ack: 消息是否成功收到 (只要消息抵达 Broker就为true)
               * @param cause: 失败的原因
               */
              @Override
              public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                  System.out.println("confirm..." +correlationData + "===" + ack + "===" + cause);
              }
          });
      
          // 设置消息抵达队列的确认回调
          rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
              /**
               * 只要消息没有投递给指定的队列,就触发这个失败回调
               * @param message 投递失败的消息详细信息
               * @param replyCode 回复的状态码
               * @param replyText 回复的文本内容
               * @param exchange 当时这个消息发送给哪个交换机
               * @param routingKey 当时这个消息用哪个路由键
               */
              @Override
              public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
                  System.out.println("message["+message+"]==replyCode[" + replyCode +
                          "] == replyText["+replyText+ "] == exchange["+ exchange + "] == routingKey[" + routingKey +"]");
              }
          });
      }
      
2)、消费方
  • 可靠抵达-Ack消息确认机制
    • spring.rabbitmq.listener.simple.acknowledge-mode=manual #开启手动确认接受消息

    • 消费者获取到消息,成功处理,可以回复Ack给Broker

    • basic.ack用于肯定确认;broker将移除此消息

    • basic.nack用于否定确认;可以指定broker是否丢弃此消息,可以批量

    • basic.reject用于否定确认;同上,但不能批量

    • 默认自动Ack,消息被消费者收到,就会从broker的queue中移除

    • queue无消费者,消息依然会被存储,直到消费者消费

    • 消费者收到消息,默认会自动ack。但是如果无法确定此消息是否被处理完成,或者成功处理。我们可以开启手动ack模式

    • 消息处理成功,ack(),接受下一个消息,此消息broker就会移除

    • 消息处理失败,nack()/reject(),重新发送给其他人进行处理,或者容错处理后ack

    • 消息一直没有调用ack/nack方法,broker认为此消息正在被处理,不会投递给别人,此时客户端断开,消息不会被broker移除,会投递给别人

    • long deliveryTag = message.getMessageProperties().getDeliveryTag();
      try {
          if(deliveryTag % 2 == 0) {
              // 签收货物,非批量模式
              channel.basicAck(deliveryTag,false);
          } else {
              // 拒收
              //退货 requeue=false 丢弃 requeue=true 发回服务器,服务器重新入队。
              //Long deliveryTag,boolean multiple,boolean requeue
              channel.basicNack(deliveryTag,false,false);
          }
      } catch (IOException e) {
          e.printStackTrace();
      }
      

7、保证消息可靠性

1)、消息丢失
  • 消息发送出去,由于网络问题没有抵达服务器
    • 做好容错方法(try-catch),发送消息可能会网络失败,失败后要有重试机制,可记录到数据库,采用定期扫描重发的方式

      • gulimall_oms> CREATE TABLE `mg_message`
        (
          `message_id`  char(32) primary key ,
          `content` text COMMENT 'JSON',
          `to_exchane` varchar(255) DEFAULT NULL,
          `routing_key` varchar(255) DEFAULT NULL,
          `class_type` varchar(255) DEFAULT NULL,
          `message_status` int(1) DEFAULT '0' COMMENT '0-新建 1-已发送 2错误抵达 3-已抵达',
          `create_time` datetime DEFAULT NULL,
          ` update_time` datetime DEFAULT NULL
        ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4
        
  • 做好日志记录,每个消息状态是否都被服务器收到都应该记录
  • 做好定期重发,如果消息没有发送成功,定期去数据库扫描未成功的消息进行重发
  • -消息抵达Broker,Broker要将消息写入磁盘(持久化)才算成功。此时Broker尚未持久化完成,岩机。
    • publisher也必须加入确认回调机制,确认成功的消息,修改数据库消息状态。
  • 自动ACK的状态下。消费者收到消息,但没来得及消息然后岩机
    • 一定开启手动ACK,消费成功才移除,失败或者没来得及处理就noAck并重新入队
2)、消息重复
  • 消息消费成功,事务已经提交,ack时,机器宕机。导致没有ack成功,Broker的消息重新由unack变为ready,并发送给其他消费者
  • 消息消费失败,由于重试机制,自动又将消息发送出去
  • 成功消费,ack时宕机,消息由unack变为ready,Broker又重新发送
    • 消费者的业务消费接口应该设计为幂等性的。比如扣库存有工作单的状态标志
    • 使用防重表(redis/mysql),发送消息每一个都有业务的唯一标识,处理过就不用处理
    • rabbitMQ的每一个消息都有redelivered字段,可以获取是否是被重新投递过来的,而不是第一次投递过来的
3)、消息积压
  • 消费者宕机积压
  • 消费者消费能力不足积压
  • 发送者发送流量太大
    • 上线更多的消费者,进行正常消费
    • 上线专门的队列消费服务,将消息先批量取出来,记录数据库,离线慢慢处理

8、RabbitMQ延时队列

实现定时任务

场景:
比如未付款订单,超过一定时间后,系统自动取消订单并释放占有物品。

常用解决方案:

​ spring的 schedule 定时任务轮询数据库

缺点:

​ 消耗系统内存、增加了数据库的压力、存在较大的时间误差

解决:

​ rabbitmq的消息TTL和死信Exchange结合

1)、 消息TTL(Time To Live)
    • 消息的TTL就是消息的存活时间
    • RabbitMQ可以对队列和消息分别设置TTL。
    • 对队列设置就是队列没有消费者连着的保留时间,也可以对每一个单独的消息做单独的设置。超过了这个时间,我们认为这个消息就死了,称之为死信
    • 如果队列设置了,消息也设置了,那么会取小的。所以一个消息如果被路由到不同的队列中,这个消息死亡的时间有可能不一样(不同的队列设置)。这里单讲单个消息的TTL,因为它才是实现延迟任务的关键。可以通过设置消息的expiration字段或者x-message-ttl属性来设置时间,两者是一样的效果。
2)、Dead Letter Exchange(DLX)
    • 一个消息在满足如下条件下,会进死信路由,记住这里是路由而不是队列,一个路由可以对应很多队列。(什么是死信)
    • 一个消息被Consumer拒收了,并且reject方法的参数里requeue是false。也就是说不会被再次放在队列里,被其他消费者使用。(basic.reject/basic.nack)requeue=false
    • 上面的消息的TTL到了,消息过期了。
    • 队列的长度限制满了。排在前面的消息会被丢弃或者扔到死信路由上
    • Dead Letter Exchange其实就是一种普通的exchange,和创建其他exchange没有两样。只是在某一个设置Dead Letter Exchange的队列中有消息过期了,会自动触发消息的转发,发送到Dead Letter Exchange中去。
    • 我们既可以控制消息在一段时间后变成死信,又可以控制变成死信的消息被路由到某一个指定的交换机,结合二者,其实就可以实现一个延时队列
    • 手动ack&异常消息统一放在一个队列处理建议的两种方式
    • catch异常后,手动发送到指定队列,然后使用channel给rabbitmq确认消息已消费
    • 给Queue绑定死信队列,使用nack(requque为false)确认消息消费失败
3)、延时队列的实现
  1. 设置消息队列过期时间实现延时队列(推荐)

    image-20200708142447527

  2. 设置消息过期时间实现延时队列

    image-20200708142415925

9、使用延时队列

  1. 图示

    image-20200708143945888

  2. 代码

    @Configuration
    public class MyMQConfig {
    
        @Resource
        AmqpAdmin amqpAdmin;
    
        /**
         * 在sb中,只需将 Binding、Queue、Exchange放到容器中,并且有在监听某个队列
         * 就会自动创建(但如果MQ中有,就不覆盖)
         */
        @RabbitListener(queues = "order.release.order.queue")
        public void handle(Message message){
    		// 这个方法只是用来使得sb创建下面定义的队列等
        }
    
        @Bean
        public Queue orderDelayQueue() {
            Map<String, Object> arguments = new HashMap<>();
            arguments.put("x-dead-letter-exchange", "order-event-exchange");
            arguments.put("x-dead-letter-routing-key", "order-release-order");
            arguments.put("x-message-ttl", 60000);
            Queue queue = new Queue("order.delay.queue", true, false, false, arguments);
            return queue;
        }
    
        @Bean
        public Queue orderReleaseOrderQueue() {
            Queue queue = new Queue("order.release.order.queue", true, false, false);
            return queue;
        }
    
        @Bean
        public Exchange orderEventExchange() {
            TopicExchange topicExchange = new TopicExchange("order-event-exchange", true, false);
            return topicExchange;
        }
    
        @Bean
        public Binding orderCreateOrderBinding() {
    
            Binding binding = new Binding("order.delay.queue",
                    Binding.DestinationType.QUEUE,
                    "order-event-exchange",
                    "order.create.order",
                    null);
            return binding;
        }
    
        @Bean
        public Binding orderReleaseOrderBinding() {
    
            Binding binding = new Binding("order.release.order.queue",
                    Binding.DestinationType.QUEUE,
                    "order-event-exchange",
                    "order.release.order",
                    null);
            return binding;
        }
    }
    

24)、订单业务

1、订单中心

电商系统涉及到3流,分别时信息流,资金流,物流,而订单系统作为中枢将三者有机的集合起来。
订单模块是电商系统的枢纽,在订单这个环节上需求获取多个模块的数据和信息,同时对这些信息进行加工处理后流向下个环节,这一系列就构成了订单的信息流通。

一)、订单构成

image-20200703161008882

  1. 用户信息

    用户信息包括用户账号、用户等级、用户的收货地址、收货人、收货人电话等组成,用户账户需要绑定手机号码,但是用户绑定的手机号码不一定是收货信息上的电话。用户可以添加多个收货信息,用户等级信息可以用来和促销系统进行匹配,获取商品折扣,同时用户等级还可以获取积分的奖励等

  2. 订单基础信息

    订单基础信息是订单流转的核心,其包括订单类型、父/子订单、订单编号、订单状态、订单流转的时间等。
    (1)订单类型包括实体商品订单和虚拟订单商品等,这个根据商城商品和服务类型进行区分
    (2)同时订单都需要做父子订单处理,之前在初创公司一直只有一个订单,没有做父子订单处理后期需要进行拆单的时候就比较麻烦,尤其是多商户商场,和不同仓库商品的时候,
    父子订单就是为后期做拆单准备的。
    (3)订单编号不多说了,需要强调的一点是父子订单都需要有订单编号,需要完善的时候可以对订单编号的每个字段进行统一定义和诠释。
    (4)订单状态记录订单每次流转过程,后面会对订单状态进行单独的说明。
    (5)订单流转时间需要记录下单时间,支付时间,发货时间,结束时间/关闭时间等等

  3. 商品信息

    商品信息从商品库中获取商品的SKU信息、图片、名称、属性规格、商品单价、商户信息等,从用户下单行为记录的用户下单数量,商品合计价格等。

  4. 优惠信息

    优惠信息记录用户参与的优惠活动,包括优惠促销活动,比如满减、满赠、秒杀等,用户使用的优惠券信息,优惠券满足条件的优惠券需要默认展示出来,具体方式已在之前的优惠券篇章做过详细介绍,另外还虚拟币抵扣信息等进行记录。

    为什么把优惠信息单独拿出来而不放在支付信息里面呢?
    因为优惠信息只是记录用户使用的条目,而支付信息需要加入数据进行计算,所以做为区分。

  5. 支付信息

    (1)支付流水单号,这个流水单号是在唤起网关支付后支付通道返回给电商业务平台的支付流水号,财务通过订单号和流水单号与支付通道进行对账使用。
    (2)支付方式用户使用的支付方式,比如微信支付、支付宝支付、钱包支付、快捷支付等。支付方式有时候可能有两个——余额支付+第三方支付。
    (3)商品总金额,每个商品加总后的金额;运费,物流产生的费用;优惠总金额,包括促销活动的优惠金额,优惠券优惠金额,虚拟积分或者虚拟币抵扣的金额,会员折扣的金额等之和;实付金额,用户实际需要付款的金额。
    用户实付金额=商品总金额+运费-优惠总金额

  6. 物流信息

    物流信息包括配送方式,物流公司,物流单号,物流状态,物流状态可以通过第三方接口来获取和向用户展示物流每个状态节点。

二)、订单状态
  1. 待付款

    用户提交订单后,订单进行预下单,目前主流电商网站都会唤起支付,便于用户快速完成支付,需要注意的是待付款状态下可以对库存进行锁定,锁定库存需要配置支付超时时间,超时后将自动取消订单,订单变更关闭状态。

  2. 已付款/待发货

    用户完成订单支付,订单系统需要记录支付时间,支付流水单号便于对账,订单下放到WMS系统,仓库进行调拨,配货,分拣,出库等操作。

  3. 待收货/已发货

    仓储将商品出库后,订单进入物流环节,订单系统需要同步物流信息,便于用户实时知悉物品物流状态

  4. 已完成

    用户确认收货后,订单交易完成。后续支付侧进行结算,如果订单存在问题进入售后状态

  5. 已取消

    付款之前取消订单。包括超时未付款或用户商户取消订单都会产生这种订单状态。

  6. 售货中

    用户在付款后申请退款,或商家发货后用户申请退换货。

    售后也同样存在各种状态,当发起售后申请后生成售后订单,售后订单状态为待审核,等待商家审核,商家审核通过后订单状态变更为待退货,等待用户将商品寄回,商家收货后订单状态更新为待退款状态,退款到用户原账户后订单状态更新为售后成功。

2、订单流程

订单流程是指从订单产生到完成整个流转的过程,从而行程了一套标准流程规则。而不同的产品类型或业务类型在系统中的流程会千差万别,比如上面提到的线上实物订单和虚拟订单的流程,线上实物订单与020订单等,所以需要根据不同的类型进行构建订单流程。不管类型如何订单都包括正向流程和逆向流程,对应的场景就是购买商品和退换货流程,正向流程就是一个正常的网购步骤:订单生成->支付订单->卖家发货->确认收货->交易成功。而每个步骤的背后,订单是如何在多系统之间交互流转的,可概括如下图

  1. 订单创建与支付

    (1)、订单创建前需要预览订单,选择收货信息等
    (2)、订单创建需要锁定库存,库存有才可创建,否则不能创建
    (3)、订单创建后超时未支付需要解锁库存
    (4)、支付成功后,需要进行拆单,根据商品打包方式,所在仓库,物流等进行拆单
    (5)、支付的每笔流水都需要记录,以待查账
    (6)、订单创建,支付成功等状态都需要给MQ发送消息,方便其他系统感知订阅

  2. 逆向流程

    (1)、修改订单,用户没有提交订单,可以对订单一些信息进行修改,比如配送信息,优惠信息,及其他一些订单可修改范围的内容,此时只需对数据进行变更即可。
    (2)、订单取消,用户主动取消订单和用户超时未支付,两种情况下订单都会取消订单,而超时情况是系统自动关闭订单,所以在订单支付的响应机制上面要做支付的限时处理,尤其是在前面说的下单减库存的情形下面,可以保证快速的释放库存。另外需要需要处理的是促销优惠中使用的优惠券,权益等视平台规则,进行相应补回给用户。
    (3)、退款,在待发货订单状态下取消订单时,分为缺货退款和用户申请退款。如果是全部退款则订单更新为关闭状态,若只是做部分退款则订单仍需进行进行,同时生成一条退款的售后订单,走退款流程。退款金额需原路返回用户的账户。
    (4)、发货后的退款,发生在仓储货物配送,在配送过程中商品遗失,用户拒收,用户收货后对商品不满意,这样情况下用户发起退款的售后诉求后,需要商户进行退款的审核,双方达成一致后,系统更新退款状态,对订单进行退款操作,金额原路返回用户的账户,同时关闭原订单数据。仅退款情况下暂不考虑仓库系统变化。如果发生双方协调不一致情况下,可以申请平台客服介入。在退款订单商户不处理的情况下,系统需要做限期判断,比如5天商户不处理,退款单自动变更同意退款。

3、幂等性处理

4、订单业务

25、接口幂等性

一、什么是幂等性

接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用;比如说支付场景,用户购买了商品支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也变成了两条....,这就没有保证接口的幂等性。

二、哪些情况需要防止

  • 用户多次点击按钮
  • 用户页面回退再次提交
  • 微服务互相调用,由于网络问题,导致请求失败。feign触发重试机制
  • 其他业务情况

三、什么情况下需要幂等

以SQL为例,有些操作是天然幂等的。
SELECT*FROM table WHERid=?,无论执行多少次都不会改变状态,是天然的幂等。
UPDATE tab1 SET col1=1 WHERE col2=2,无论执行成功多少次状态都是一致的,也是幂等操作。
delete from user where userid=1,多次操作,结果一样,具备幂等性
insert into user(userid,name)values(1,'a')如userid为唯一主键,即重复操作上面的业务,只会插入一条用户数据,具备幂等性。

UPDATE tab1 SET col1=col1+1 WHERE col2=2,每次执行的结果都会发生变化,不是幂等的。
insert into user(userid,name)values(1,a)如userid不是主键,可以重复,那上面业务多次操作,数据都会新增多条,不具备幂等性

四、幂等性解决方案

1、token机制

1、服务端提供了发送token的接口。我们在分析业务的时候,哪些业务是存在幂等问题的,就必须在执行业务前,先去获取token,服务器会把token保存到redis中。
2、然后调用业务接口请求时,把token携带过去,一般放在请求头部
3、服务器判断token是否存在redis中,存在表示第一次请求,然后删除token,继续执行业务。
4、如果判断token不存在redis中,就表示是重复操作,直接返回重复标记给client,这样就保证了业务代码,不被重复执行。

危险性:
1、先删除token还是后删除token;
(1)先删除可能导致,业务确实没有执行,重试还带上之前token,由于防重设计导致,请求还是不能执行。
(2)后删除可能导致,业务处理成功,但是服务闪断,出现超时,没有删除token,别人继续重试,导致业务被执行两边
(3)我们最好设计为先删除token,如果业务调用失败,就重新获取token再次请求。

解决:Token获取、比较和删除必须是原子性
(1)redis.get(token)、token.equals、redis.del(token)如果这两个操作不是原子,可能导致,高并发下,都get到同样的数据,判断都成功,继续业务并发执行
(2)可以在redis使用lua脚本完成这个操作
if redis.call('get',KEYS[1])ARGV[1]then return redis.call('del',KEYS[1])else return 0 end

2、各种所机制
1、数据库悲观锁

select * from xxxx where id = 1 for update ;
悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,需要根据实际情况选用。另外要注意的是,id字段一定是主键或者唯一索引,不然可能造成锁表的结果,处理起来会非常麻烦。

2、数据库乐观锁

这种方法适合在更新的场景中,
update t goods set count = count -1, version = version + 1 where good_id=2 and version =1
根据version版本,也就是在操作库存前先获取当前商品的version版本号,然后操作的时候带上此version号。我们梳理下,我们第一次操作库存时,得到version为1,调用库存服务version变成了2;但返回给订单服务出现了问题,订单服务又一次发起调用库存服务,当订单服务传如的version还是1,再执行上面的sql语句时,就不会执行;因为version已经变为2了,where条件就不成立。这样就保证了不管调用几次,只会真正的处理一次。乐观锁主要使用于处理读多写少的问题

3、业务层分布式锁

如果多个机器可能在同一时间同时处理相同的数据,比如多台机器定时任务都拿到了相同数据处理,我们就可以加分布式锁,锁定此数据,处理完成后释放锁。获取到锁的必须先判断这个数据是否被处理过。

4、各种唯一约束机制
1)、数据库唯一约束

插入数据,应该按照唯一索引进行插入,比如订单号,相同的订单就不可能有两条记录插入。我们在数据库层面防止重复。
这个机制是利用了数据库的主键唯一约束的特性,解决了在insert场景时飛等问题。但主键的要求不是自增的主键,这样就需要业务生成全局唯一的主键。如果是分库分表场景下,路由规则要保证相同请求下,落地在同一个数据库和同一表中,要不然数据库主键约束就不起效果了,因为是不同的数据库和表主键不相关。

2)、redis set防重

很多数据需要处理,只能被处理一次,比如我们可以计算数据的MD5将其放入redis的set,每次处理数据,先看这个MD5是否已经存在,存在就不处理。

4、防重表

使用订单号orderNo做为去重表的唯一索引,把唯一索引插入去重表,再进行业务操作,且他们在同一个事务中。这个保证了重复请求时,因为去重表有唯一约束,导致请求失败,避免了帚等问题。这里要注意的是,去重表和业务表应该在同一库中,这样就保证了在同一个事务,即使业务操作失败了,也会把去重表的数据回滚。这个很好的保证了数据一致性。

5、全局请求唯一id

调用接口时,生成一个唯一id,redis将数据保存到集合中(去重),存在即处理过。可以使用nginx设置每一个请求的唯一id;
proxy_set_header X-Request-ld $request_id;

26、事务

事务保证:
1、订单服务异常,库存锁定不运行,全部回滚,撤销操作
2、库存服务事务自治,锁定失败全部回滚,订单感受到,继续回滚
3、库存服务锁定成功了,但是网络原因返回数据途中问题?
4、库存服务锁定成功了,库存服务下面的逻辑发生故障,订单回滚了,怎么处理?

  • 利用消息队列实现最终一致
  • 库存服务锁定成功后发给消息队列消息(当前库存工作单),过段时间自动解锁,解锁时先查询订单的支付状态。解锁成功修改库存工作单详情项状态为已解锁

image-20200707133947963

一、本地事务

1、事务的基本性质

数据库事务的几个特性:原子性(Atomicity)、一致性(Consistency)、隔离性或独立性(Isolation)和持久性(Durabilily),简称就是ACID;

  • 原子性:一系列的操作整体不可拆分,要么同时成功,要么同时失败
  • 一致性:数据在事务的前后,业务整体一致。
  • 转账。A:1000;B:1000;转200事务成功;A:800B:1200
  • 隔离性:事务之间互相隔离。
  • 持久性:一旦事务成功,数据一定会落盘在数据库。

再单体应用中我们多个业务操作使用同一条链接操作不同的数据库,一旦有异常,我们可以很容易的整体回滚

image-20200707135311721

  • Business:我们具体的业务代码
  • Storage:库存业务代码;扣库存
  • Order:订单业务代码;保存订单
  • Account:账号业务代码;减账户余额

比如买东西业务,扣库存,下订单,账户扣款,是一个整体;必须同时成功或者失败
一个事务开始,代表以下的所有操作都在同一个连接里面;

2、事务的隔离级别

隔离级别依次提高,越高,并发能力越低

  • READ UNCOMMITTED(读未提交)
    • 该隔离级别的事务会读到其它未提交事务的数据,此现象也称之为脏读。
  • READCOMMITTED(读已提交)(Oracle、SQL Server 默认
    • 一个事务可以读取另一个已提交的事务,多次读取会造成不一样的结果,此现象称为不可重复读问题,Oracle和SQLServer的默认隔离级别。
  • REPEATABLEREAD(可重复读)(MySql 默认
    • 该隔离级别是MVSOI默认的隔离级别,在同一个事务里,selert的结果是事开始时时间点的状态,因此,同样的select操作读到的结果会是一致的,但是,会有幻读现象。MySOL的lnnoDB引擎可以通过next-keylocks机制(参考下文"行锁的算法"一节)来避免幻读。
  • SERIALIZABLE(序列化)
    • 在该隔离级别下事务都是串行顺序执行的,MySQL数据库的InnoDB引擎会给读操作隐式加一把读共享锁,从而避免了脏读、不可重读复读和幻读问题。
3、事务的传播行为
  1. PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,并且设置的所有属性都无效(全部用当前事务的)。该设置是最常用的设置。
  2. PROPAGATION_SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。
  3. PROPAGATION_MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。
  4. PROPAGATION_REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务。(常用)
  5. PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
  6. PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
  7. PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。
4、SpringBoot事务关键点
  1. 事务的自动配置

    在方法上标注: @Transaction

  2. 事务的坑

    本地事务失效:

    • 同一个对象内事务方法互调默认失效,原因 绕过了代理对象,事务使用代理对象来控制的

    解决:使用代理对象来调用事务方法

    1. 引入 aop-starter模块(也就是引入aspactj)

    2. 开启aspectj动态代理功能:@EnableAspectJAutoProxy(exposeProxy = true)

    3. 用代理对象本类互调

      TargetObject t= (TargetObject)AopContext.currentProxy(); 使用这个对象调用本类方法

二、分布式事务

1、为什么会有分布式事务

分布式系统经常出现的异常
机器岩机、网络异常、消息丢失、消息乱序、数据错误、不可靠的TCP、存储数据丢失...

image-20200707141915369

分布式事务是企业集成中的一个技术难点,也是每一个分布式系统架构中都会涉及到的一个东西,特别是在微服务架构中,几乎可以说是无法避免。

2、CAP定理和BASE理论
  1. CAP定理

    CAP原则又称CAP定理,指的是在一个分布式系统中

    • 一致性(Consistency):
    • 在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本)
    • 可用性(Availability)
    • 在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性)
    • 分区容错性(Partition tolerance)
    • 大多数分布式系统都分布在多个子网络。每个子网络就叫做一个区(partition)。分区容错的意思是,区间通信可能失败。比如,一台服务器放在中国,另一台服务器放在美国,这就是两个区,它们之间可能无法通信。

    CAP原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾,一致性和可用性二选一

    image-20200707142754025

    一般来说,分区容错无法避免,因此可以认为CAP的P总是成立。CAP定理告诉我们,剩下的C和A无法同时做到

    raft算法来保持一致性 http://thesecretlivesofdata.com/raft/

  2. 面临的问题

    对于多数大型互联网应用的场景,主机众多、部署分散,而且现在的集群规模越来越大,所以节点故障、网络故障是常态,而且要保证服务可用性达到99.99999%(N个9),即保证P和A,舍弃C。

  3. BASE理论

    是对CAP理论的延伸,思想是即使无法做到强一致性(CAP的一致性就是强一致性),但可以采用适当的采取弱一致性,即最终一致性

    BASE是指

    • 基本可用(Basically Available)
    • 基本可用是指分布式系统在出现故障的时候,允许损失部分可用性(例如响应时间、功能上的可用性),允许损失部分可用性。需要注意的是,基本可用绝不等价于系统不可用。
      • 响应时间上的损失:正常情况下搜索引擎需要在0.5秒之内返回给用户相应的查询结果,但由于出现故障(比如系统部分机房发生断电或断网故障),查询结果的响应时间增加到了1~2秒。
      • 功能上的损失:购物网站在购物高峰(如双十一)时,为了保护系统的稳定性,部分消费者可能会被引导到一个降级页面。
    • 软状态(Soft State)
    • 软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据会有多个副本,允许不同副本同步的延时就是软状态的体现。mysql replication的异步复制也是一种体现。
    • 最终一致性(Eventual Consistency)
    • 最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。
  4. 强一致性、弱一致性

    从客户端角度,多进程并发访问时,更新过的数据在不同进程如何获取的不同策略,决定了不同的一致性。

    • 对于关系型数据库,要求更新过的数据能被后续的访问都能看到,这是强一致性
    • 如果能容忍后续的部分或者全部访问不到,则是弱一致性
    • 如果经过一段时间后要求能访问到更新后的数据,则是最终一致性
3、分布式事务几种方案
  1. 2PC模式(不使用)

    数据库支持的2PC【2 phase commit二阶提交】,又叫做XATransactions。
    MySQL从5.5版本开始支持,SQL Server 2005开始支持,Oracle7开始支持。
    其中,XA是一个两阶段提交协议,该协议分为以下两个阶段:

    • 第一阶段:事务协调器要求每个涉及到事务的数据库预提交(precommit)此操作,并反映是否可以提交.
    • 第二阶段:事务协调器要求每个数据库提交数据。

    其中,如果有任何一个数据库否决此次提交,那么所有数据库都会被要求回滚它们在此事务
    中的那部分信息。

    image-20200707151722288

    • XA协议比较简单,而且一旦商业数据库实现了XA协议,使用分布式事务的成本也比较低。
    • XA性能不理想,特别是在交易下单链路,往往并发量很高,XA无法满足高并发场景
    • XA目前在商业数据库支持的比较理想,在mysql数据库中支持的不太理想,mysql的XA实现,没有记录prepare阶段日志,主备切换回导致主库与备库数据不一致。
    • 许多nosql也没有支持XA,这让XA的应用场景变得非常狭险。
    • 也有3PC,引入了超时机制(无论协调者还是参与者,在向对方发送请求后,若长时间未收到回应则做出相应处理)
  2. 柔性事务-TCC事务补偿型方案

    刚性事务:遵循ACID原则,强一致性。
    柔性事务:遵循BASE理论,最终一致性;I
    与刚性事务不同,柔性事务允许一定时间内,不同节点的数据不一致,但要求最终一致。

    image-20200707152433494

    • 一阶段prepare行为:调用自定义的prepare逻辑。

    • 二阶段commit行为:调用自定义的commit逻辑。

    • 三阶段rollback行为:调用自定义的rollback逻辑。

      所谓TCC模式,是指支持把自定义的分支事务纳入到全局事务的管理中。

  3. 柔性事务-最大努力通知型方案

    按规律进行通知,不保证数据一定能通知成功,但会提供可查询操作接口进行核对。这种方案主要用在与第三方系统通讯时,比如:调用微信或支付宝支付后的支付结果通知。这种方案也是结合MQ进行实现,例如:通过MQ发送http请求,设置最大通知次数。达到通知次数后即不再通知。

    案例:银行通知、商户通知等(各大交易业务平台间的商户通知:多次通知、查询校对、对账文件),支付宝的支付成功异步回调

  4. 柔性事务-可靠消息+最终一致性方案(异步确保型)
    最终目标:防止消息丢失

    实现:业务处理服务在业务事务提交之前,向实时消息服务请求发送消息,实时消息服务只记录消息数据,而不是真正的发送。业务处理服务在业务事务提交之后,向实时消息服务确认发送。只有在得到确认发送指令后,实时消息服务才会真正发送。

27、支付宝支付

1、加入“蚂蚁金融开放平台”

2、下载支付宝官方demo,进行配置和测试

  • 保证项目是utf-8

3、配置使用沙箱进行测试

4、公钥、私钥、加密、签名

1、公钥和私钥
公钥和私钥是一个相对概念

它们的公私性是相对于生成者来说的。一对密钥生成后,保存在生成者手里的就是私钥,生成者发布出去大家用的就是公钥

2、加密和数字签名
1)、加密
  • 对称加密

    • 不安全
    • image-20200710141032372

  • 非对称加密

    • 安全:金融领域需要的是安全!
    • image-20200710140831109

5、支付宝支付流程

6、整合阿里支付

  • pom.xml:

    • <dependency>
          <groupId>com.alipay.sdk</groupId>
          <artifactId>alipay-sdk-java</artifactId>
          <version>4.10.49.ALL</version>
      </dependency>
      
  • application.properties

    • alipay.app_id=商户id
      alipay.notify_url=异步回调地址
      alipay.return_url=返回地址
      alipay.sign_type=RSA2
      alipay.charset=utf-8
      alipay.gatewayUrl=https://openapi.alipaydev.com/gateway.do
      alipay.timeout_express=1m
      
  • 支付模板

    • @ConfigurationProperties(prefix = "alipay")
      @Component
      @Data
      public class AlipayTemplate {
      
      //在支付宝创建的应用的id
      private String app_id = "2088102181151134";
      
      // 商户私钥,您的PKCS8格式RSA2私钥
      private String merchant_private_key = "商户私钥";
      // 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥。
      private String alipay_public_key = "支付宝公钥";
      // 服务器[异步通知]页面路径  需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
      // 支付宝会悄悄的给我们发送一个请求,告诉我们支付成功的信息
      private String notify_url;
      
      // 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
      //同步通知,支付成功,一般跳转到成功页
      private String return_url;
      // 订单超时时间
      String timeout_express;
      
      // 签名方式
      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 + "\","
                  + "\"timeout_express\":\"" + timeout_express + "\","
                  + "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}");
      
          String result = alipayClient.pageExecute(alipayRequest).getBody();
      
          //会收到支付宝的响应,响应的是一个页面,只要浏览器显示这个页面,就会自动来到支付宝的收银台页面
          System.out.println("支付宝的响应:" + result);
          return result;
      }
      }
      
    • 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
      }
      
    • @Data
      public class PayVo {
          private String out_trade_no; // 商户订单号 必填
          private String subject; // 订单名称 必填
          private String total_amount;  // 付款金额 必填
          private String body; // 商品描述 可空
      }
      
  • 接收支付宝返回的数据并验证

    • @RestController
      @Slf4j
      public class OrderPayedListener {
          @Resource
          AlipayTemplate alipayTemplate;
          @PostMapping("/payed/notify")
          public String handleAliPayed(PayAsyncVo vo, HttpServletRequest request) throws AlipayApiException {
              // 验签,防止恶意伪造
              // 只要收到支付宝的异步通知,返回 success 支付宝便不再通知
              // 获取支付宝POST过来反馈信息
              // 验证签名
              Map<String, String> params = new HashMap<>();
              Map<String, String[]> requestParams = request.getParameterMap();
              for (String name : requestParams.keySet()) {
                  String[] values = requestParams.get(name);
                  String valueStr = "";
                  for (int i = 0; i < values.length; i++) {
                      valueStr = (i == values.length - 1) ? valueStr + values[i]
                              : valueStr + values[i] + ",";
                  }
                  //乱码解决,这段代码在出现乱码时使用
                  // valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");
                  params.put(name, valueStr);
              }
              boolean signVerified = AlipaySignature.rsaCheckV1(params, alipayTemplate.getAlipay_public_key(),
                      alipayTemplate.getCharset(), alipayTemplate.getSign_type());
              if (signVerified) {
                  log.info("签名验证成功");
                  /**
                   * 成功后处理业务
                   * String res = orderService.handlePayResult(vo);
                   */
                  // 业务成功就返回 success
                  return res;
              } else {
                  log.worn("签名验证失败");
                  return "error";
              }
          }
      }
      

28、内网穿透

1、介绍

  • image-20200710151436726

  • image-20200710151753468

  • 内网穿透功能可以允许我们使用外网的网址来访问主机;
    正常的外网需要访问我们项目的流程是:
    1、买服务器并且有公网固定IP
    2、买域名映射到服务器的IP
    3、域名需要进行备案和审核

2、使用场景

  1. 开发测试(微信、支付宝)
  2. 智慧互联
  3. 远程控制
  4. 私有云

3、内网穿透的几个软件

  1. natapp:https://natapp.cn/
  2. 续断:www.zhexi.tech
  3. 花生壳:https://www.oray.com/

4、nignx配置

  • image-20200710173523506
  • image-20200710173758932

29、秒杀

1、秒杀业务

秒杀具有瞬间高并发的特点,针对这一特点,必须要做限流+异步+缓存(页面静态化)+独立部署。

限流方式:
1.前端限流,一些高并发的网站直接在前端页面开始限流,例如:小米的验证码设计
2.nginx限流,直接负载部分请求到错误的静态页面:令牌算法漏斗算法
3.网关限流,限流的过滤器
4.代码中使用分布式信号量
5.rabbitmq限流(能者多劳:chanel.basicQos(1)),保证发挥所有服务器的性能。

2、高并发系统关注的问题

  1. 服务单一职责 + 独立部署
    • 秒杀服务即使自己扛不住压力,挂掉。不要影响别人
  2. 秒杀链接加密
    • 防止恶意攻击,模拟秒杀请求,1000次/s攻击。防止链接暴露,自己工作人员,提前秒杀商品。
  3. 库存预热 + 快速扣减
    • 秒杀读多写少,无需每次实时校验库存,我们库存预热,放到redis中,信号量控制进来秒杀的请求
  4. 动静分离
    • nginx做好动静分离。保证秒杀和商品详情页的动态请求才打到后端的服务集群。使用CDN网络,分担本集群压力
  5. 恶意请求拦截
    • 识别非攻击请求进行拦截(网关层)
  6. 流量错峰
    • 使用各种手段,将流量分担到更大快读的时间点,如:验证码,加入购物车
  7. 限流&熔断&降级
    • 前端限流+后端限流
    • 限制次数,限制总量,快速失败降级运行,熔断隔离防止雪崩
  8. 队列消峰
    • 1W个商品,每个1000秒杀。双十一所有秒杀成功的请求,进入队列,慢慢船舰订单、扣减库存即可

2、秒杀流程

3、限流

30、定时任务

一、cron

1、cron表达式(可百度生成)
  • 语法: 秒 分 时 日 月 周 年(Spring不支持)
    • image-20200711161230723
  • 特殊字符

    • : 枚举
      • (cron="7,9,23 * * * * ?"): 任意时刻的 7,9, 23秒启动这个任务
    • - :范围
      • cron="7-20 * * * * ?"):任意时刻的 7-20s之间,没秒启动一次
    • ***** :任意
      • 指定任意的位置时刻都可以
    • /:步长
      • (cron="7/5 * * * * ?"):第 7 秒启动,每 5秒一次
      • (cron="*/5 * * * * ?"):任意秒启动,每 5秒一次
    • ?:(出现在日和周几的位置):为了防止日和周冲突,在周和日上如果要写通配符使用?
      • (cron="**1?"):每月的1号,而且必须是周二然后启动这个任务;
    • L:(出现在日和周的位置)”
      • last:最后一个
      • (cron="* * * ? * 3L"):每月的最后一个周二
    • W:
      • Work Day:工作日
      • (cron="* * * W * ?"):每个月的工作日触发
      • (cron="* * * LW * ?"):每个月的最后一个工作日触发
    • #:第几个
      • (cron="* * * ? *5#2"):每个月的第2个周4
2、cron示例
  • 百度可生成
3、SB整合cron
  • 定时任务不应该阻塞,所以在定时任务类上标注 @EnableAsync开启异步任务功能 方法上: @Async
  • 开启定时任务: 类上:@EnableScheduling 方法上 : @Scheduled(cron = " * * * * ? ")*
  • 定时任务的自动配置类 : TaskSchedulingAutoConfiguration
  • 异步的任务的自动配置类: TaskExecutionAutoConfiguration 属性绑定在: TaskExecutionProperties

二、分布式定时任务

1、分布式下的问题
  • 多台服务器通知执行上架

    使用分布式锁解决

  • image-20200712174541305

posted @ 2020-07-14 17:43  qiuqiup  阅读(638)  评论(0)    收藏  举报