开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

SQL 生成器,避免重复编码工作 Sql-Generate

发表于 2021-11-11

Sql-Generate

项目介绍

sql-generate 是一个基于java语言开发的项目,它是一个 SQL 生成类的项目,用于某些文件资源映射成对应的 SQL,提高开发效率

背景

在工作当中,会遇到上线前需要初始化数据或上线后线上数据不正确、数据需要清洗等,对于复杂的场景需要写脚本来支持,对于简单的场景往往是写 sql 人工执行,数据的来源往往是某个文件,通过文件解析成对应的 sql,时间一长,每次发生这种事情就会产生不必要的工作量,所以本项目把解析、生产对应的 sql,抽象成对应工具库,提高开发效率

Features

  • 支持excel、csv、txt(普通文件) 多种文件解析格式
  • 支持文件解析配置、内容过滤、内容转换
  • 支持数据格式化配置
  • 支持console、file等多种视图展示
  • 解析器、视图展示支持可扩展

架构

image-1.png

集成方式

Maven:

1
2
3
4
5
xml复制代码<dependency>
<groupId>com.github.rrsunhome</groupId>
<artifactId>excelsql-generate</artifactId>
<version>2.0.2</version>
</dependency>

Gradle:

1
arduino复制代码implementation 'com.github.rrsunhome:excelsql-generate:2.0.2'

快速开始

加载普通文件资源

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码    Resource resource = new ClassPathResource("order.txt");

TextParserConfig parserConfig = new TextParserConfig("\t");
parserConfig.setRowRange(0,20);
SqlFormatConfig sqlFormatConfig = new SqlFormatConfig("insert into table(a,b,c) values({0},{1},{2});")
.setString(0, 0)
.setString(1, 1)
.setInt(2, 2);
SqlGenerator csvSqlGenerator = new DefaultSqlGenerator(resource,
parserConfig, sqlFormatConfig);
ResultSet resultSet = csvSqlGenerator.execute();
resultSet.outputView();

加载 Excel 资源

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码      Resource resource = new ClassPathResource("order-v1.xlsx");

ExcelParserConfig parserConfig = new ExcelParserConfig();
String sql = "insert into table(a,b,c) values({0},{1},{2});";
SqlFormatConfig sqlFormatConfig = new SqlFormatConfig(sql)
.setString(0, 1)
.setString(1, 0)
.setInt(2, 2);

SqlGenerator csvSqlGenerator = new DefaultSqlGenerator(resource, parserConfig, sqlFormatConfig);
ResultSet resultSet = csvSqlGenerator.execute();
resultSet.outputView();

加载 Csv 资源

1
2
3
4
5
6
7
8
9
10
java复制代码        Resource resource = new ClassPathResource("order.csv");
ExcelParserConfig parserConfig = new ExcelParserConfig();
parserConfig.setSheetIndex(0);
SqlFormatConfig sqlFormatConfig = new SqlFormatConfig("insert into table(a,b,c) values({0},{1},{2});")
.setString(0, 1)
.setString(1, 0)
.setInt(2, 2);
SqlGenerator csvSqlGenerator = new DefaultSqlGenerator(resource, parserConfig, sqlFormatConfig);
ResultSet resultSet = csvSqlGenerator.execute();
resultSet.outputView();

属性配置

基本解析配置

BaseParserConfig

  • titleRowIndex 默认值: 0 标题行下标
  • rowRange 默认值: 全部数据 行数据范围
  • cellName 默认值: 空 列标题
  • cellNum 默认值: 空 列下标
  • CellFilter 默认值: TrueCellFilter 当前列是否过滤,又一列不符合当前行则跳过
  • CellConverter 默认值: ObjectToStringCellConverter 列内容转换器

文本解析配置

TextParserConfig

  • delimiter 默认值: \t 数据分隔符

Excel 解析配置

ExcelParserConfig

  • sheetIndex 默认值: 0 sheet 下标
  • sheetName 默认值: 空 sheet 名称

Csv 解析配置

CsvParserConfig

  • 无特殊配置

配置演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码        CsvParserConfig parserConfig = new CsvParserConfig();
parserConfig.setTitleRowIndex(0);
parserConfig.setRowRange(1, 30);
parserConfig.addCellMapping(CellMapping.builder()
.cellNum(1)
.cellFilter(cellValue -> true)
.cellConverter(Object::toString)
.build());
parserConfig.addCellMapping(CellMapping.builder()
.cellNum(2)
.cellFilter("21"::equals)
.cellConverter(Object::toString)
.build());
parserConfig.addCellMapping(CellMapping.builder()
.cellNum(0)
.build());

SQL 格式化配置

SqlFormatConfig

  • sql 默认值: 空 sql 模版
  • parameterIndexMapping 默认值:空 参数下标与文件 cell 下标映射
  • Formatter 默认值: com.github.rrsunhome.excelsql.format.MessageFormatter sql 格式化
    • com.github.rrsunhome.excelsql.format.MessageFormatter
    • com.github.rrsunhome.excelsql.format.StringFormatter

配置演示

1
2
3
4
java复制代码        SqlFormatConfig sqlFormatConfig = new SqlFormatConfig("insert into table(a,b,c) values({0},{1},{2});", new MessageFormatter())
.setString(0, 1)
.setString(1, 0)
.setInt(2, 2);

项目地址

github.com/rrsunhome/e…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

redis 持久化对比(RDB与AOF) RDB AOF 参

发表于 2021-11-11

redis两种持久化方式:

  • RDB(Redis Database):将内存中的数据作为一份快照保存到硬盘中
  • AOF(Append Only File):每次执行命令都会都会尝试触发刷盘操作,实时性好,数据丢的少,但有性能损耗

RDB

概念

将redis的内存数据,保存一份到二进制文件中

触发方式

  • 主动save:整个redis server会阻塞,直至完成
  • bgsave:通过异步流程完成快照备份
  • 自动触发:根据规则设置触发时机,m秒内发生n次修改(save m n)

bgsave流程

  1. fork出一个子进程
  2. 子进程和主进程共享内存(写时复制),子进程直接读取数据,并写到二进制文件中
  3. 替换原有的RDB

写时复制(cow)

父、子进程共享一块内存数据,当父进程要修改数据时,子进程才会将数据copy到自己的物理空间中

通过这种方式,这样能避免数据的大量copy,减少内存消耗

总结

优点

  • 机制简单:简单的快照原理
  • 写入损耗低:与aof不同,不用在每次写入后,还需要额外的写入aof_buf中
  • 文件小:备份数据时压缩后的二进制文件,空间占用小
  • 恢复速度快:基于二进制直接恢复

缺点

  • 丢失数据:备份间隔宕机时,会丢失数据
  • 备份时性能吃紧:虽然在一般的写入时无需额外的性能开销,但是在备份时会造成大量的io,消耗性能

AOF

概念

  1. redis每执行一次写命令,将数据追加到aof_buf中
  2. 根据appendfysnc设定的刷盘时机,将aof_buf中数据刷到磁盘上

刷新磁盘的时机(appendfysnc)

  • always:每次都会落盘,性能极差,但不会丢数据
  • no:不主动刷盘,交给操作系统刷盘,性能最好,但是丢失数据风险较大
  • everysec:每秒定时刷一次,折衷方案

文件重写

aof文件需要定期重写,以免文件无限膨胀,减少故障时恢复的时间

通过直接读取redis,构造aof文件,可以有效的减少存储空间,主要表现在:

  1. 跳过过期、无效的key
  2. 将若干命令压缩成一条
  3. 有效的减少重复命令

重写的伪代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码f:=createFile(path)
# 遍历 db
for db range redis.DB():
# 遍历 key
for key range db:
# 跳过过期 key
if key.isExpired():
continue
# 压缩 key
f.write(compressCmd(key))
# 设置超时时间
if key.hasExpireTime():
f.write(expiredCmd(key))
f.close()

压缩的方案主要讲若干条对同一个key的重复、散落的命令,压缩成一条命令,for example:

Lpush queue r1

Lpush queue r2

Lpush queue r2

可以被压缩成一条

Lpush queue r1 r2

子进程的后台重写

由于需要遍历线上的redis,直接同步处理的话,会阻塞主进程,从而拒绝访问,相当于停机copy数据(但是不用考虑重写过程中的写入问题,因为新的写入被阻塞了)

为了解决这一问题,诞生了基于子进程的后台重写:子进程通过使用主进程的数据副本,实现copy数据

在这种情况下,主进程仍在服务,无可避免会有新的写入产生。为了保证一致性,在文件重写时,会开辟一块新的buffer,用于存储重写过程中发生的变动,如下图所示:

1.png

在完成重写后,会进行收尾工作:

  1. 将aof文件重写buffer中的数据写到刚刚重写后的aof文件中
  2. 用重写后的aof文件替换现有的aof文件

注意为了保证数据一致性,这两个操作会阻塞主进程

恢复

相对简单,就是创建一个本地的伪客户端,读取aof文件,一次写入server的redis即可

总结

优点

  • 容错性较好:跳过某条失败的命令后,可以继续恢复
  • 易读:每条命令都是一条写入语句,可读性强
  • 支持后台重写:避免文件无限膨胀

缺点

  • 恢复速度较慢:一条一条插入语句,执行耗时
  • 占用空间大:是完整的命令,所用空间当然也多

参考

详解Redis持久化(RDB和AOF)

RDF和AOF的区别

Redis之AOF重写及其实现原理

linux 同步IO: sync、fsync与fdatasync

写时复制技术(详解版)

Linux写时拷贝技术(copy-on-write)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

一文带你玩转“泛型“

发表于 2021-11-11

「这是我参与11月更文挑战的第11天,活动详情查看:2021最后一次更文挑战」。


  温馨提示: 本文大约2419字,阅读完大概需要2-3分钟,希望您能耐心看完,倘若你对该知识点已经比较熟悉,你可以直接通过目录跳转到你感兴趣的地方,希望阅读本文能够对您有所帮助,如果阅读过程中有什么好的建议、看法,欢迎在文章下方留言或者私信我,您的意见对我非常宝贵,再次感谢你阅读本文。

一: 泛型知识点

泛型知识点

二: 泛型(generics)是什么

  泛型是JDK1.5出现的一种新特性,它是一种语法糖,主要用来解决对象类型不确定的问题。

  延申知识:语法糖(Syntactic Sugar),也称糖衣语法,指在计算机语言中添加的某种语法,这种语法对语言本身功能来说没有什么影响,只是为了方便程序员的开发,提高开发效率。说白了,语法糖就是对现有语法的一个封装。

  常见的语法糖:

  1. 泛型与类型擦除
  2. 自动装箱与拆箱,变长参数
  3. 增强for循环
  4. 内部类与枚举类

  泛型可以用在类、接口、方法中,分别称为泛型类、泛型接口、泛型方法。

  一:泛型类(具有一个或者多个类型变量的类)

1
2
java复制代码// K,V表示泛型,编译的时候不知道具体的类型,实例化的时候需要指定具体的类型
public genericClass<K,V> xxxx

  二:泛型接口(具有一个或者多个类型变量的接口)

1
2
java复制代码// K,V表示泛型,编译的时候不知道具体的类型,实现接口的时候需要指定
public interface genericInterface<K> xxx

  三:泛型方法(具有一个或者多个类型变量的方法)

1
2
java复制代码//  K,V表示泛型,编译的时候不知道具体的类型,调用方法的时候需要指定具体的类型
public <T> void genericMethod(T,V)

  四:泛型方法为什么需要在返回值类型前添加泛型类型,不然就报错?

  答:这是java声明泛型方法的固定格式,在方法的返回值声明之前的位置,定义该方法所拥有的泛型标识符,个数可以是多个。

三: 使用泛型有什么好处

  在JDK1.5以前,如果我们不知道对应的类型,可以先使用Object类型来占位,但是后面存在的问题: 需要强制转换,可能存在类型转换错误。

  1、安全性: 在编译期会进行类型检查,类型不对会报错,并且泛型的强制类型转换是自动和隐式的,避免了强制类型转换时可能出现的类型转换(ClassCastException)错误。

  2、提高代码的重用性: 泛型的强制转换都是自动和隐式的。

  3、增强可读性。

四: 如何使用泛型

  (一) 泛型的目的是为了解决当对象的类型不确定时,参数类型如何定义的问题,所以,当参数类型没有确定的时候,可以使用泛型的通配符进行占位。

  (二) 泛型中常见的通配符号: T、K、V、?等,他们并不是一定的,只是编码中的一个约定俗称的东西,我们可以使用A-Z随便的字母进行替换,但是,使用约定俗称的通配符号可以提高可读性,它们表达的具体含义如下:

  1. “?” 通常表示不确定的Java类型
  2. “T” 表示某个具体的Java类型
  3. “K” 代表java键值中的Key
  4. “V” 代表java键值中的Value
  5. “E” 代表Element(某个元素)

  (三) 常见的通配符类型

  1、无边界通配符: <?> 表示没什么限制,无界通配符则表明在使用泛型,如果不指定,则不能添加任何值。

  2、上边界限定通配符,如 <?extendsE>; extends关键字表示这个泛型中的参数必须是 E 或者 E 的子类。

  3、下边界通配符,如 <? super E>; super关键字表示这个泛型中的参数必须是所指定的类型E,或者是此类型的父类型,直至 Object。

五: 泛型的底层实现

  (一) 泛型是在编译期进行类型校验,如果类型校验不通过,则会编译报错,它底层是通过编译器进行实现的。

  (二) 类型擦除: 泛型校验只在编译阶段,在编译生成的字节码中都不包含泛型中的类型参数,这就是泛型擦除,例子如下:

1
2
3
4
java复制代码List<Apple> list1 = new ArrayList<>();
List<Fruit> list2 = new ArrayList<>();
// 结果是true
System.out.println(list1.getClass() == list2.getClass());

六: 泛型的局限

  1. 在指定泛型类型时,不能使用基本类型,只能指定它们的包装类型,如: Person”<”double”>”,因为泛型本质是obejct类型,是引用类型,而不是原始类型(原始类型也就是我们常说的八种基本类型,他们都是存储值在栈中,而引用类型存储的是对象的地址)。
  2. 不能够直接实例化类型变量,如:new T(…),newT[…] 或 T.class。

七: 常见的泛型面试题

(一) Java中的泛型是什么 ?

  答: 泛型是JDK1.5的新特性,它只是一个语法糖,用于解决类、接口、方法、属性对象类型不确定的问题。

(二)使用泛型的好处是什么?

  1、在编译期间会进行类型检查,添加了安全性(安全性)

  2、强制类型转换都是自动和隐式的,提高了代码的重用效率(简洁性)

  3、增加了可读性

(三)Java的泛型是如何工作的 ?

  答: 它是通过编译器实现的,在编译期间进行类型检查

(四)什么是类型擦除 ?

  泛型只存在编译的时候,在运行时会转换成具体的原始类型,即在运行的时候会”擦除”类型的概念。

  泛型只在编译阶段,在编译生成的字节码中都不包含泛型中的类型参数>

(五)什么是泛型中的限定通配符和非限定通配符 ?

  限定通配符对类型进行了限制;泛型类型必须用限定内的类型来进行初始化,否则会导致编译错误。另一方面表示了非限定通配符,因为可以用任意类型来替代。

  限定通配符: 又分为上边界通配符<? extends E>、下边界通配符<? super E>

  1. 上边界通配符<? extends E> 表示: 泛型参数必须是E类型或者E的子类
  2. 下边界通配符<? super E> 表示: 泛型参数必须是E或者E的父类,直至Object类型

  非限定通配符: 类型为“<‘T’>”,可以用任意类型来替代,它表示没有任何的限制,泛型参数可以是任意符合条件的类型。

(六)List<’T’>和List <?>之间有什么区别 ?

  泛型的使用场景有以下两种:

  1. 声明一个泛型类或者泛型方法
  2. 使用泛型类或者泛型方法

  <’T’> : 使用场景是第一种即: 声明泛型类、方法、接口,使用类型参数的目的是解决对象类型不确定的情况。如:

1
java复制代码public class ArrayList<T> extends AbstractList<T>{}

  <’?‘>: 主要是用于第二种: 使用泛型类或者泛型方法(不推荐使用,因为这样使用会存在很多莫名奇妙的问题),如:

1
java复制代码List<?> list = new ArrayList<Integer>();

(六):你了解泛型通配符与上下界吗?
  泛型通配符主要分为: 限定通配符(分为上边界通配符<? extends E>、下边界通配符<? super E>)和非限定通配符,具体作用如下:

  1. 边界通配符<? extends E> 表示: 泛型参数必须是E类型或者E的子类【这个只能用于方法参数,或者变量中修饰,不能修饰接口或类】
  2. 下边界通配符<? super E> 表示: 泛型参数必须是E或者E的父类,直至Object类型【这个只能用于方法参数,或者变量中修饰,不能修饰接口或类】
  3. 非限定通配符: 使用一个单独的T(或者A-Z任意字母)表示,它表示没有任何的限制,泛型参数可以是任意符合条件的类型。

八:总结

  相信看到这里,你对泛型的会有了更深的认识,学习一个知识,只有知道这个知识的原理,才不会感觉一知半解。

  感谢你阅读本文,如果你觉得文章哪里存在错误,欢迎私信或者在下方留言指出。如果你觉得本文对你有一些帮助,可以给我一个点赞和关注,让我有更多动力给大家带来更多的文章,谢谢。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SpringBoot集成Swagger(三)apis()接口

发表于 2021-11-11

「这是我参与11月更文挑战的第9天,活动详情查看:2021最后一次更文挑战」


相关文章

Java随笔记:Java随笔记


前言

  • 小伙伴们,前面一直在讲文档信息的配置,大家有木有想过,为啥我们的Swagger上面连basic-error-controller都会被扫描出来?
  • 下面我们一起来看看是为啥

select()配置扫描接口

  • 前面说过,Docket是链式调用的。
  • 使用select()必须build()
  • 先举个例子再来讲里面的东西吧。
+ 
1
2
3
4
5
6
7
8
scss复制代码    @Bean
   public Docket docket(){
       return new Docket(DocumentationType.SWAGGER_2)
              .apiInfo(apiInfo())
              .select()
              .apis(RequestHandlerSelectors.none())
              .build();
  }
  • 启动看一看
+ ![image-20211110214357478.png](https://gitee.com/songjianzaina/juejin_p9/raw/master/img/0c9bfab3bb9cfd58c7e88209d9aeeef2e576aa14762b54eeb76954585e7d6461)
+ 啊呦!啥都木有了。
  • 这时候我们可以看看,这个apis()里的参数RequestHandlerSelectors里面的是啥?
+ ![image-20211110214606291.png](https://gitee.com/songjianzaina/juejin_p9/raw/master/img/9858b5a6d0578e568dba9264e63256c2f8ff377ab13cedf589420943be987c1b)
+ 重点方法已框出。


    - 当我们设置为 `RequestHandlerSelectors.none()`时,不扫描任何接口。
    - 当我们设置为 `RequestHandlerSelectors.any()`时,扫描所有接口。
    - 当我们设置为 `RequestHandlerSelectors.basePackage("需要扫描的包路径")`时


        * 
1
2
3
4
5
6
7
8
scss复制代码    @Bean
   public Docket docket(){
       return new Docket(DocumentationType.SWAGGER_2)
              .apiInfo(apiInfo())
              .select()
              .apis(RequestHandlerSelectors.basePackage("com.dayu.dyswagger.dycontroller"))
              .build();
  }
* ![image-20211110215044916.png](https://gitee.com/songjianzaina/juejin_p9/raw/master/img/62f59f56955f87a4f5abd2bb16119b5ba149ec88396430b3fc060edb23410280) - 这样我们就可以指定扫描,哪些接口需要展示,哪些接口不需要展示。 - 这时候可能有的同学要问了:那假如我需要匹配多个包下的`controller`该怎么办嘞? * 要么使用`path`来,要么使用`withClassAnnotation`。path下面再讲。这里我们讲解下如何使用withClassAnnotation * * 很明显可以看出,这是指定`注解`的。 * 那我们可以先想象一下:设定所有加了 `@RestController`注解的类都能被扫描到?或者我们自己写个注解,加到我们需要被扫描的Controller上不也是可以的嘛?下面演示一波~ - 扫描指定注解: *
1
2
3
4
5
6
7
8
scss复制代码    @Bean
  public Docket docket(){
      return new Docket(DocumentationType.SWAGGER_2)
               .apiInfo(apiInfo())
               .select()
               .apis(RequestHandlerSelectors.withClassAnnotation(RestController.class))
               .build();
   }
* ![image-20211110220124922.png](https://gitee.com/songjianzaina/juejin_p9/raw/master/img/1095669b712a16067175cae1b9cb03ce32a71462c9998ceaae6ec82cd9d745fd) * ![image-20211110220140501.png](https://gitee.com/songjianzaina/juejin_p9/raw/master/img/aba2cc0370035cb2ba6394f0dbd1348fa92a52d71b81b1caae2d0a6b1b6a3400) * 成功!!但是这种方式也不是特别灵活对吧?因为这些原有注解就是我们常用的,不可能避免某一个类不加。 - 自定义注解 *
1
2
3
4
5
6
7
8
9
10
11
less复制代码/**
* @program: dyswagger
* @description: Swagger扫描注解
* @author: DingYongJun
* @create: 2021-11-10 22:03
*/
@Retention(value = RetentionPolicy.RUNTIME)
@Target(value = ElementType.TYPE)
@Inherited
public @interface StartSwaggerScan {
}
* 需要被扫描到的类上加上 `@StartSwaggerScan`即可 + ![image-20211110220556345.png](https://gitee.com/songjianzaina/juejin_p9/raw/master/img/0275bb19780dde8b3e64d9418e25406a9c545daa0f44f1e5e9ccc3631794abe2) *
1
2
3
4
5
6
7
8
scss复制代码    @Bean
   public Docket docket(){
       return new Docket(DocumentationType.SWAGGER_2)
              .apiInfo(apiInfo())
              .select()
              .apis(RequestHandlerSelectors.withClassAnnotation(StartSwaggerScan.class))
              .build();
  }
* 运行看结果 + ![image-20211110220623739.png](https://gitee.com/songjianzaina/juejin_p9/raw/master/img/b639ec7511f38f19374af23b2f6e77e65fd57fcf4e6f0168851ba1489b9d02bb) * 完美符合要求! - 这种方式相对来说比较灵活!想怎么玩就怎么玩!哈哈哈! + 总结一下: - any() // 扫描所有,项目中的所有接口都会被扫描到 - none() // 不扫描接口 - 通过方法上的注解扫描,如withMethodAnnotation(GetMapping.class)只扫描get请求 - 通过类上的注解扫描,如.withClassAnnotation(Controller.class)只扫描有controller注解的类中的接口 - basePackage(final String basePackage) // 根据包路径扫描接口

路漫漫其修远兮,吾必将上下求索~

如果你认为i博主写的不错!写作不易,请点赞、关注、评论给博主一个鼓励吧~hahah

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

双十一基于交易域的策略模式示例介绍

发表于 2021-11-11

「这是我参与11月更文挑战的第11天,活动详情查看:2021最后一次更文挑战」

前言

  XDM,今天是双十一,大家都剁手了吗?感觉今年各大电商平台的优惠力度不大,往年的11月11日这天,各大电商平台都会实时公布平台的最新交易数据,今年小编在网上,暂未查询到相关平台的双十一成交额的最新数据信息。今年没有公布最新的数据,可能是等双十一结束之后,各大平台公布双十一的销售额的时候会有惊喜吧。

  相信做过电商的都了解不同的客户下单,可能相同的商品,在不同的贸易方式下,会有不同的商品总价信息,大家都是采用if…else 去判断贸易方式和价格计算的算法呢,还是采用设计模式呢。本次针对这种情况,采用策略设计模式来设计。

策略模式

  策略模式是指有一定行动内容的相对稳定的策略名称,这种设计模式属于行为型模式。策略模式主要解决在有多种算法相似的情况下,使用 if…else 所带来的复杂和难以维护,使用策略模式可以将不同的算法和业务独立封装起来,可以在项目运行中独立动态调用不同的算法和逻辑。

优点:

  • 扩展性良好,利于维护
  • 避免使用多重条件判断,减少 if…else
  • 可以自由动态切换不同的业务逻辑
  • 缺点*
  • 会有很多实现的策略类
  • 策略类需要暴露出来

案例理解

  前段时间在开发交易域中台时,有个需求是根据不同的贸易方式,计算订单商品的总价信息。下面就以这个示例进行分析,首先了解到有N种贸易方式,在示例中举出四种,分别为贸易方式A、贸易方式B、贸易方式C、贸易方式D。

  • 贸易方式A ,这种贸易方式一般指用户自提的方式,所以无快递邮寄费用和关税等费用。采用的商品总价计算逻辑是:(商品的单价*数量)-减去优惠。
  • 贸易方式B,是指一般的快递邮寄的方式,有快递运费,无其他费用。则这种贸易方式的商品总价计算方式为:(商品的单价*数量)-优惠的总金额+快递邮寄费用。
  • 贸易方式C ,需要从国外进口,可能有两段快递费用,一段境外的快递运费,一段为境内的快递费用。在进口商品中,需要报关,产生关税费用。 所以计算商品总价格为:(商品单价*数量)-优惠金额+快递一段费用+快递二段费用
  • 贸易方式D,需要从国外进口,可能有两段快递费用,一段境外的快递运费,一段为境内的快递费用。在进口商品中,需要报关,产生关税费用。所以计算商品总价格为:(商品单价*数量)-优惠金额+快递一段费用+快递二段费用。

商品基础信息

  商品基础信息包含:商品编码、商品单价、商品数量、优惠金额、税费金额、第一段运费金额、第二段运费金额,这是商品总价计算的基础业务参数,本文将以此来进行价格的计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
js复制代码@Data
@ApiModel("商品信息")
public class Goods {

private String skuCode;

private BigDecimal price;

private int num;

private BigDecimal discount;

private BigDecimal taxes;

private BigDecimal firstFreight;

private BigDecimal secondFreight;

}

策略接口

  定义一个策略接口,使所有的策略都实现这个接口。

1
2
3
4
js复制代码public interface TradeTypeStrategy {

BigDecimal getGoodsTotalPrice(Goods goods);
}

贸易类型对象

  定义一个贸易类型的对象,维护一个策略接口对象的引用。

1
2
3
4
5
6
7
8
9
10
11
12
js复制代码public class TradeType {

private TradeTypeStrategy tradeTypeStrategy;

public TradeType(TradeTypeStrategy tradeTypeStrategy){
this.tradeTypeStrategy = tradeTypeStrategy;
}

public BigDecimal executeStrategy(Goods goods){
return tradeTypeStrategy.getGoodsTotalPrice(goods);
}
}

编写实现

  根据业务的实际需要,编写多种实现类,实现类中进行业务逻辑和算法的开发。一遍执行不同的策略,调用不同的业务逻辑和算法。示例进行了贸易方式A、贸易方式B、贸易方式C、贸易方式D的实现,简单代码如下:

贸易方式A 总价计算逻辑是:(商品的单价*数量)-减去优惠。

1
2
3
4
5
6
7
8
9
10
11
12
js复制代码public class TradeTypeStrategyA implements TradeTypeStrategy {

/**
*
*
* */
@Override
public BigDecimal getGoodsTotalPrice(Goods goods) {
BigDecimal goodsTotalPrice = goods.getPrice().multiply(BigDecimal.valueOf(goods.getNum())).subtract(goods.getDiscount());
return goodsTotalPrice;
}
}

贸易方式B 总价计算方式为:(商品的单价*数量)-优惠的总金额+快递邮寄费用

1
2
3
4
5
6
7
8
js复制代码public class TradeTypeStrategyB implements TradeTypeStrategy {

@Override
public BigDecimal getGoodsTotalPrice(Goods goods) {
BigDecimal goodsTotalPrice = goods.getPrice().multiply(BigDecimal.valueOf(goods.getNum()))
.subtract(goods.getDiscount()).add(goods.getFirstFreight());
return goodsTotalPrice;
}

贸易方式C 总价格为:(商品单价*数量)-优惠金额+快递一段费用+快递二段费用

1
2
3
4
5
6
7
8
9
js复制代码public class TradeTypeStrategyC implements TradeTypeStrategy {

@Override
public BigDecimal getGoodsTotalPrice(Goods goods) {
BigDecimal goodsTotalPrice = goods.getPrice().multiply(BigDecimal.valueOf(goods.getNum()))
.subtract(goods.getDiscount()).add(goods.getFirstFreight()).add(goods.getTaxes());
return goodsTotalPrice;
}
}

贸易方式D 总价格为:(商品单价*数量)-优惠金额+快递一段费用+快递二段费用

1
2
3
4
5
6
7
8
9
js复制代码public class TradeTypeStrategyD implements TradeTypeStrategy {

@Override
public BigDecimal getGoodsTotalPrice(Goods goods) {
BigDecimal goodsTotalPrice = goods.getPrice().multiply(BigDecimal.valueOf(goods.getNum()))
.subtract(goods.getDiscount()).add(goods.getFirstFreight()).add(goods.getSecondFreight()).add(goods.getTaxes());
return goodsTotalPrice;
}
}

运行测试

  编写测试类,本测试使用的是同一个商品信息,当用户在不同的贸易方式下下单之后,可以看到输出的订单商品的总价是不同的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
js复制代码public class MainTrade {
public static void main(String[] args) {
Goods goods = new Goods();
goods.setSkuCode("G888888");
goods.setPrice(BigDecimal.valueOf(100));
goods.setFirstFreight(BigDecimal.valueOf(20));
goods.setNum(3);
goods.setSecondFreight(BigDecimal.valueOf(8));
goods.setDiscount(BigDecimal.TEN);
goods.setTaxes(BigDecimal.valueOf(32));
TradeType tradeTypeA = new TradeType(new TradeTypeStrategyA());
System.out.println("贸易方式A的总价是 = " + tradeTypeA.executeStrategy(goods));
TradeType tradeTypeB = new TradeType(new TradeTypeStrategyB());
System.out.println("贸易方式B的总价是 = " + tradeTypeB.executeStrategy(goods));
TradeType tradeTypeC = new TradeType(new TradeTypeStrategyC());
System.out.println("贸易方式C的总价是 = " + tradeTypeC.executeStrategy(goods));
TradeType tradeTypeD = new TradeType(new TradeTypeStrategyD());
System.out.println("贸易方式D的总价是 = " + tradeTypeD.executeStrategy(goods));
}
}

  如下图,可以看到在不同的贸易方式下,输出的订单商品总价是不同的。示例信息仅供参考,在计算总价信息是有更复杂的业务逻辑和

图片.png

结语

  好了,以上就是策略模式的使用介绍,感谢您的阅读,希望您喜欢,如对您有帮助,欢迎点赞收藏。如有不足之处,欢迎评论指正。下次见。

  作者介绍:【小阿杰】一个爱鼓捣的程序猿,JAVA开发者和爱好者。公众号【Java全栈架构师】维护者,欢迎关注阅读交流。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

深入浅出,一文吃透mysql索引 索引是什么 B+树索引 M

发表于 2021-11-11

这是我参与11月更文挑战的第11天,活动详情查看:2021最后一次更文挑战

索引是什么

索引是为了提高数据查询效率的数据结构,类似于书的目录一样,可以根据目录而快速找到相关内容。

MySQL 8.0 版本中,InnoDB 存储引擎支持的索引有 B+ 树索引、全文索引、R 树索引,其中,B+ 树索引使用最为广泛。

B+树索引

每一个索引在 InnoDB 里面对应一棵 B+ 树。
B+树索引的特点 :基于磁盘的平衡树,树非常矮,一般为 3~4 层,所以访问效率非常高,从千万或上亿数据里查询一条数据,只用 3、4 次 I/O。

假设我们有如下表,ID是主键,字段 k 上有索引:

主键索引和非主键索引的示意图如下:

其中R代表一整行的值。

主键索引和非主键索引的区别是:

  • 主键索引的叶子节点存放的是整行数据;
  • 非主键索引的叶子节点存放的是主键的值;
  • 非主键索引也被称为二级索引,而主键索引也被称为聚簇索引。

1、如果查询语句是

select * from table where ID = 100,以主键查询的方式,只需要搜索 ID 这棵 B+ 树。

2、如果查询语句是

select * from table where k = 1,以非主键的查询方式,则需要先搜索 k 索引树,得到 ID=100,再到 ID 索引树搜索一次,这个过程也被称为回表。

MySQL 中 B+ 树索引的管理

  1. 命令 EXPLAIN 查看是否使用索引。
  2. 查询表 mysql.innodb_index_stats 查看每个索引的大致情况。
字段 释义
database_name 数据库名
table_name 表名
index_name 索引名
last_update 统计信息最后一次更新时间
stat_name 统计信息名
stat_value 统计信息的值
sample_size 采样大小
stat_description 类型说明
  1. 查询表 sys.schema_unused_indexes 查看有哪些索引一直未被使用过,可以被废弃。
  • MySQL5.7 及以上的版本sys模式下
  • schema_redundant_indexes 和 schema_unused_indexes 两个视图

MySQL 存储数据和索引对象分析

索引组织表

数据的存储分为堆表和索引组织表,目前大部分数据库都支持索引组织表的存储方式。

  1. 堆表

如上图,堆表中的数据和索引是分开存储的,索引有序而数据是无序的,索引的叶子节点存的是数据在堆表中的地址。堆表中数据发生变更,其位置也会变,导致索引中的地址都需要更新,所以很影响性能。

  1. 索引组织表

数据根据主键排序存放在索引中,主键索引又叫聚集索引。在索引组织表中,数据即索引,索引即数据。InnoDB 存储引擎就是这样的数据组织方式。

二级索引

除了主键索引外,其他的索引都称之为二级索引,或非聚集索引,同样也是一颗 B+ 树索引,它和主键索引不同的是叶子节点存放的是索引键值、主键值。

当通过使用二级索引来查询数据时,通过二级索引先找到主键值,再通过主键索引进行查询数据,这种二级索引通过主键索引进行再一次查询”的操作叫作回表。

与堆表相比,这种索引组织表这样的二级索引,若有数据发生变更时,其他索引无须进行维护,除非记录的主键发生了修改,所以性能优势会非常明显。

覆盖索引

上面提到了,二级索引的叶子节点存放的是索引键值、主键值,

例如我们有如下表:

1
2
3
4
5
6
sql复制代码create table user (
id int primary key,
name varchar(20),
sex varchar(5),
index(name)
)engine=innodb;
  1. 索引覆盖
1
sql复制代码select id,name from user where name='ls';

能够命中name索引,索引叶子节点存储了主键id,通过name的索引树即可获取id和name,无需回表,符合索引覆盖,效率较高。

  1. 回表
1
sql复制代码select id,name,sex from user where name='ls';

能够命中name索引,索引叶子节点存储了主键id,但sex字段必须回表查询才能获取到,不符合索引覆盖,需要再次通过id值扫码聚集索引获取sex字段,效率会降低。

索引调优

函数索引

从 MySQL 5.7 版本开始,MySQL 开始支持创建函数索引 (即索引键是一个函数表达式)。 函数索引有两大用处:

  1. 优化业务 SQL 性能:

假如我们有一个注册日期字段 register_date,并对其创建了索引,现在有如下条件查询 where DATE_FORMAT(register_date,'%Y-%m') = '2021-10',那么能不能命中索引呢?

答案是不能,索引只对 register_date 的数据排序,并没有对 DATE_FORMAT(register_date) 排序,因此不能使用到此索引。

我们可以使用函数索引解决这个问题, 创建一个DATE_FORMAT(register_date) 的索引。

1
2
3
sql复制代码ALTER TABLE Testtable
ADD INDEX
idx_func_register_date((DATE_FORMAT(register_date,'%Y-%m')));
  1. 配合虚拟列(Generated Column)。

例如有如下表:

1
2
3
4
5
6
7
sql复制代码CREATE TABLE User (
userId BIGINT,
userInfo JSON,
mobile VARCHAR(255) AS (userInfo->>"$.mobile"),
PRIMARY KEY(userId),
UNIQUE KEY idx_mobile(mobile)
);

mobile 列就是一个虚拟列,由后面的函数表达式计算而成,本身这个列不占用任何的存储空间,而索引 idx_mobile 实质是一个函数索引。这样做的好处是在写 SQL 时可以直接使用这个虚拟列,而不用写冗长的函数:

1
2
3
4
5
6
7
sql复制代码-- 不用虚拟列
SELECT * FROM User
WHERE userInfo->>"$.mobile" = '15088888888'

-- 使用虚拟列
SELECT * FROM User
WHERE mobile = '15088888888'

最左前缀原则

B+ 树这种索引结构,可以利用索引的“最左前缀”,来定位记录。

例如我们有字段 a 和 b,都为高频字段,为了减少回表,我们可以建立联合索引 (a,b),这时不需要单独在 a 上建立索引了。

但是如果查询条件里面只有 b 的语句,是无法使用 (a,b) 这个联合索引的,这时候你不得不维护另外一个索引, 如果 a 字段比 b 字段大可以创建 (a,b)、(b) 这两个索引,反之创建 (b,a)、(a) 这两个索引。

普通索引与唯一索引的选择

先说结论:业务代码已经保证不会写入重复数据”的情况下,建议尽量选择普通索引。

查询时:

  • 普通索引,查找到满足条件的第一个记录后,还需要查找下一个记录,直到碰到第一个不满足条件的记录。
  • 唯一索引,由于索引定义了唯一性,查找到第一个满足条件的记录后,就会停止继续检索。

上面的不同之处在性能差距上微乎其微。因为对于数据的读取不仅仅将需要读取的某一条数据从磁盘上读取出来,Innodb的数据是按照页为单位来进行读写的,每页的默认大小为16KB,所以对于普通索引来说,只是多做一次“查找和判断下一条记录”的操作,只需要一次指针寻找和一次计算,操作成本对于现在的 CPU 来说可以忽略不计。

更新时:

  • 普通索引,则是将更新记录在 change buffer,语句执行就结束了。
  • 唯一索引,需要将数据页读入内存,判断到没有冲突,插入这个值,语句执行结束。

唯一索引的更新不能使用 change buffer,普通索引可以使用到

什么是 change buffer?

  1. 当对数据页进行更新时,如果数据页在内存中则直接更新,如果不在 Innodb 会将更新操作记录在 change buffer 中,免去了去磁盘中读取数据页的过程,下次查询的时候,再将数据页读入内存,结合 change buffer 记录来返回数据,同时进行 merge 操作(将 change buffer 中的操作应用到原数据页)。
  2. change buffer 在内存中有拷贝,也会被写入到磁盘上,它是可以持久化的数据的。

对于唯一索引,更新时需要将数据页读取到内存中来判断是否违反了唯一性约束,数据页既然都已经读到内存中了,自然也就不需要 change buffer了;而普通索引,则是将更新记录在 change buffer。由于磁盘IO成本较高,不如使用 change buffer 对性能更加友好。

组合索引

组合索引(Compound Index)是指由多个列所组合而成的 B+树索引。

  1. 例如:

对组合索引(a,b),因为其对列 a、b 做了排序,所以此索引可以优化的的 SQL 有:

1
2
3
4
sql复制代码WHERE a = ?
WHERE a = ? AND b = ?
WHERE b = ? AND a = ?
WHERE a = ? ORDER BY b DESC

索引(a,b)排序不能得出(b,a)排序,所以下面 SQL 不能被优化:

1
2
sql复制代码WHERE b = ?
WHERE b = ? ORDER BY a DESC
  1. 使用 组合索引 进行 索引覆盖

若查询的字段在二级索引的叶子节点中,则可直接返回结果,无需回表。这种通过组合索引避免回表的优化技术也称为索引覆盖(Covering Index)。

利用组合索引包含多个列的特性,可以实现索引覆盖技术,提升 SQL 的查询性能。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SpringBoot基础之全局异常处理

发表于 2021-11-11

这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战

前言

当前端向后端请求数据的时候,正常情况下,接口会返回我们理想的格式.但是在后端程序出现问题的情况下,返回的默认的数据不一定是我们想要的类型,所有当程序出现异常时,我们需要有一个统一的全局异常处理器,将异常数据处理成,我们需要的格式,并返回前台

需要处理异常的位置

(一) 通过controller抛出的异常

该异常是业务运行时导致的异常,在controller发生,或者能向上抛出到controller的异常,这种异常通过自定义异常类并注解@ControllerAdvice或者@RestControllerAdvice即可达到目的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
java复制代码@RestControllerAdvice
//@ControllerAdvice
@Slf4j
public class AllExceptionHandler{


@ExceptionHandler(value =CustomException.class)
public R customExceptionHandle(Exception e){
//这个地方应该去创建多个Exception类然后分类去捕获
log.error("自定义异常: {}",e);
return R.fail(e.getMessage());
}

@ExceptionHandler(value = org.springframework.validation.BindException.class)
public R bindExceptionHandle(BindException ex){
StringBuffer defaultMessage = new StringBuffer();
List<ObjectError> list = ex.getBindingResult().getAllErrors();
if(list!=null && list.size()>0){
try {
int i=0;
for (ObjectError error : list) {
if(i>0){
defaultMessage.append(",");
}
defaultMessage.append(error.getDefaultMessage());
i++;
}
} catch (Exception e) {
e.printStackTrace();
}
}
String s = defaultMessage.toString();
log.warn("用户数据参数异常: {}",s);
return R.fail(s);

}

@ExceptionHandler(value = Exception.class)
public R exceptionHandle(Exception e){
log.error("未知的异常{},{}",e.getMessage(),e);
return R.fail("系统异常",e.getMessage());
}

public AllExceptionHandler() {
super();
}

使用注意

  1. @RestControllerAdvice注解,可以用于定义@ExceptionHandler、@InitBinder、@ModelAttribute,并应用到所有@RequestMapping中
  2. @RestControllerAdvice继承了@ControllerAdvice功能的子类,将返回信息写入到Response的Body中(因为也注解了@ResponseBody),因此如果是json交互的前后单分离项目直接用@RestControllerAdvice的更好.
  3. @RestControllerAdvice和@ControllerAdvice注解包含一些属性:
属性 解释
@ControllerAdvice(“org.zdc.controllers”) 捕获指定包中的控制器的异常
@ControllerAdvice(annotations = RestController.class) 捕获带有注解@RestController的控制器的异常
@ControllerAdvice(assignableTypes = {AbstractController.class}) 捕获有带有指定签名的控制器异常
4. 如果不生效,则需要注意观察是否被filter,Aspect等拦截,或者异常不是在扫描的包下抛出的.
5. 可以定义多个@ExceptionHandler, 当异常出现的时候,程序会从上到下依次进行匹配,匹配成功之后就会终止继续匹配,因此最后一个@ExceptionHandler应该定义为Exception.class
6. @ExceptionHandler定义的原则: 除了最后一个Exception.class外, 其他的异常则需要按需实现.比如:CustomException.class是业务异常,返回的错误信息,只包含e.getMessage()即可,而@Validated参数验证异常BindException.class,则需要解析异常愿意,好让用户明白具体是哪个参数异常导致的

(二) 拦截404或者服务器错误等未进入controller的异常

当请求了一个未定义的请求或解析请求报错的时候,程序会抛出错误,并把错误转发到/error接口上,然后该返回错误的数据,但是返回的数据格式可能不是我们想要的,所以需要实现一个默认的controller并实现该/error方法.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
typescript复制代码@RestController
@Slf4j
public class OtherExceptionHandler implements ErrorController {
@Override
public String getErrorPath() {
return "/error";
}

@RequestMapping("/error")
public R otherError(HttpServletRequest request, HttpServletResponse response) {

//将所有的错误请求都处理成200 具体根据实际项目配置
response.setStatus(200);

Integer statusCode = (Integer)request.getAttribute("javax.servlet.error.status_code");
String errorMsg = "出现错误";

if (statusCode == null) {
errorMsg = "服务器内部错误";
}

//转发过来的自定义CustomException异常
if(statusCode==5001 && request.getAttribute("javax.servlet.error.exception") instanceof CustomException){
log.warn("Filter异常:{}",((Exception)request.getAttribute("javax.servlet.error.exception")));
return R.fail(((Exception)request.getAttribute("javax.servlet.error.exception")).getMessage());
}

try {
return R.fail(errorMsg, statusCode, HttpStatus.valueOf(statusCode));
} catch (Exception ex) {
errorMsg = "服务器内部错误:" + statusCode;
}
log.error("服务器错误,错误码:{}",statusCode);
return R.fail(errorMsg);
}
}

看到这里你应该看到了转发过来的自定义CustomException异常,实际上我们可以手动将异常转发到这里(比如自定义filter中), 使用方法请文章全局搜索手动转发到/error接口

(三) filter等自定义代码中异常

在自定义的filter中,业务判断不符的时候需要中断操作抛出异常、
在自定义的Aspect中,代理方法报错需要抛出异常,等等,一次我们需要注意处理这里的异常

filter中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
vbscript复制代码@Component
public class ZdcFilter implements Filter {

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {


if(false){
//中断 抛出异常

//第一种 手动转发到/error接口
request.setAttribute("javax.servlet.error.status_code",5001);
request.setAttribute("javax.servlet.error.status_msg","通过转发/error设置返回数据");
request.getRequestDispatcher("/error").forward(request, response);
return;

//第二种 抛出异常,自动转到/error接口,http状态码500
throw new CustomException("通过MyFilter,抛异常返回值");

//第三种 直接写到response中返回
response.setContentType("application/json; charset=utf-8");
response.getWriter().print("通过MyFilter直接接调用response返回数据");
response.getWriter().flush();
response.getWriter().close();
return;

}else{
//正常通过
chain.doFilter(request,response);
}

}
}

当然在这里我们基本上都时使用第三种方式直接通过response写回去.

1
2
3
4
arduino复制代码    作者:ZOUZDC
链接:https://juejin.cn/post/7028963866063306760
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SpringCloud升级之路20200x版-32 改

发表于 2021-11-11

这是我参与11月更文挑战的第11天,活动详情查看:2021最后一次更文挑战

本系列代码地址:github.com/JoJoTec/spr…

在前面一节,我们梳理了实现 Feign 断路器以及线程隔离的思路,这一节,我们先不看如何源码实现(因为源码中会包含负载均衡算法的改进部分),先来讨论下如何优化目前的负载均衡算法。

之前的负载均衡算法

  1. 获取服务实例列表,将实例列表按照 ip 端口排序,如果不排序即使 position 是下一个可能也代表的是之前已经调用过的实例
  2. 根据请求中的 traceId,从本地缓存中以 traceId 为 key 获取一个初始值为随机数的原子变量 position,这样防止所有请求都从第一个实例开始调用,之后第二个、第三个这样。
  3. position 原子加一,之后对实例个数取余,返回对应下标的实例进行调用

其中请求包含 traceId 是来自于我们使用了 spring-cloud-sleuth 链路追踪,基于这种机制我们能保证请求不会重试到之前已经调用过的实例。源码是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
kotlin复制代码//一定必须是实现ReactorServiceInstanceLoadBalancer
//而不是ReactorLoadBalancer<ServiceInstance>
//因为注册的时候是ReactorServiceInstanceLoadBalancer
@Log4j2
public class RoundRobinWithRequestSeparatedPositionLoadBalancer implements ReactorServiceInstanceLoadBalancer {
private final ServiceInstanceListSupplier serviceInstanceListSupplier;
//每次请求算上重试不会超过1分钟
//对于超过1分钟的,这种请求肯定比较重,不应该重试
private final LoadingCache<Long, AtomicInteger> positionCache = Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.MINUTES)
//随机初始值,防止每次都是从第一个开始调用
.build(k -> new AtomicInteger(ThreadLocalRandom.current().nextInt(0, 1000)));
private final String serviceId;
private final Tracer tracer;


public RoundRobinWithRequestSeparatedPositionLoadBalancer(ServiceInstanceListSupplier serviceInstanceListSupplier, String serviceId, Tracer tracer) {
this.serviceInstanceListSupplier = serviceInstanceListSupplier;
this.serviceId = serviceId;
this.tracer = tracer;
}

//每次重试,其实都会调用这个 choose 方法重新获取一个实例
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
return serviceInstanceListSupplier.get().next().map(serviceInstances -> getInstanceResponse(serviceInstances));
}

private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> serviceInstances) {
if (serviceInstances.isEmpty()) {
log.warn("No servers available for service: " + this.serviceId);
return new EmptyResponse();
}
return getInstanceResponseByRoundRobin(serviceInstances);
}

private Response<ServiceInstance> getInstanceResponseByRoundRobin(List<ServiceInstance> serviceInstances) {
if (serviceInstances.isEmpty()) {
log.warn("No servers available for service: " + this.serviceId);
return new EmptyResponse();
}
//为了解决原始算法不同调用并发可能导致一个请求重试相同的实例
//从 sleuth 的 Tracer 中获取当前请求的上下文
Span currentSpan = tracer.currentSpan();
//如果上下文不存在,则可能不是前端用户请求,而是其他某些机制触发,我们就创建一个新的上下文
if (currentSpan == null) {
currentSpan = tracer.newTrace();
}
//从请求上下文中获取请求的 traceId,用来唯一标识一个请求
long l = currentSpan.context().traceId();
AtomicInteger seed = positionCache.get(l);
int s = seed.getAndIncrement();
int pos = s % serviceInstances.size();
log.info("position {}, seed: {}, instances count: {}", pos, s, serviceInstances.size());
return new DefaultResponse(serviceInstances.stream()
//实例返回列表顺序可能不同,为了保持一致,先排序再取
.sorted(Comparator.comparing(ServiceInstance::getInstanceId))
.collect(Collectors.toList()).get(pos));
}
}

但是在这次请求突增很多的时候,这种负载均衡算法还是给我们带来了问题。

首先,本次突增,我们并没有采取扩容,导致本次的性能压力对于压力的均衡分布非常敏感。举个例子是,假设微服务 A 有 9 个实例,在业务高峰点来的时候,最理想的情况是保证无论何时这 9 个负载压力都完全均衡,但是由于我们使用了初始值为随机数的原子变量 position,虽然从一天的总量上来看,负责均衡压力肯定是均衡,但是在某一小段时间内,很可能压力全都跑到了某几个实例上,导致这几个实例被压垮,熔断,然后又都跑到了另外的几个实例上,又被压垮,熔断,如此恶性循环。

然后,我们部署采用的是 k8s 部署,同一个虚拟机上面可能会跑很多微服务的 pod。在某些情况下,同一个微服务的多个 pod 可能会跑到同一个虚拟机 Node 上,这个可以从pod 的 ip 网段上看出来:例如某个微服务有如下 7 个实例:10.238.13.12:8181,10.238.13.24:8181,10.238.15.12:8181,10.238.17.12:8181,10.238.20.220:8181,10.238.21.31:8181,10.238.21.121:8181,那么 10.238.13.12:8181 与 10.238.13.24:8181 很可能在同一个 Node 上,10.238.21.31:8181 和 10.238.21.121:8181 很可能在同一个 Node 上。我们重试,需要优先重试与之前重试过的实例尽量不在同一个 Node 上的实例,因为同一个 Node 上的实例只要有一个有问题或者压力过大,其他的基本上也有问题或者压力过大。

最后,如果调用某个实例一直失败,那么这个实例的调用优先级需要排在其他正常的实例后面。这个对于减少快速刷新发布(一下子启动很多实例之后停掉多个老实例,实例个数大于重试次数配置)对于用户的影响,以及某个可用区突然发生异常导致多个实例下线对用户的影响,以及业务压力已经过去,压力变小后,需要关掉不再需要的实例,导致大量实例发生迁移的时候对用户的影响,有很大的作用。

针对以上问题的优化方案

我们针对上面三个问题,提出了一种优化后的解决方案:

  1. 针对每次请求,记录:
  2. 本次请求已经调用过哪些实例 -> 请求调用过的实例缓存
  3. 调用的实例,当前有多少请求在处理中 -> 实例运行请求数
  4. 调用的实例,最近请求错误率 -> 实例请求错误率
  5. 随机将实例列表打乱,防止在以上三个指标都相同时,总是将请求发给同一个实例。
  6. 按照 当前请求没有调用过靠前 -> 错误率越小越靠前 的顺序排序 -> 实例运行请求数越小越靠前
  7. 取排好序之后的列表第一个实例作为本次负载均衡的实例

具体实现是:以下的代码来自于:github.com/JoJoTec/spr…

我们使用了依赖:

1
2
3
4
xml复制代码<dependency>
<groupId>io.dropwizard.metrics</groupId>
<artifactId>metrics-core</artifactId>
</dependency>

记录实例数据的缓存类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
java复制代码@Log4j2
public class ServiceInstanceMetrics {
private static final String CALLING = "-Calling";
private static final String FAILED = "-Failed";

private MetricRegistry metricRegistry;

ServiceInstanceMetrics() {
}

public ServiceInstanceMetrics(MetricRegistry metricRegistry) {
this.metricRegistry = metricRegistry;
}

/**
* 记录调用实例
* @param serviceInstance
*/
public void recordServiceInstanceCall(ServiceInstance serviceInstance) {
String key = serviceInstance.getHost() + ":" + serviceInstance.getPort();
metricRegistry.counter(key + CALLING).inc();
}
/**
* 记录调用实例结束
* @param serviceInstance
* @param isSuccess 是否成功
*/
public void recordServiceInstanceCalled(ServiceInstance serviceInstance, boolean isSuccess) {
String key = serviceInstance.getHost() + ":" + serviceInstance.getPort();
metricRegistry.counter(key + CALLING).dec();
if (!isSuccess) {
//不成功则记录失败
metricRegistry.meter(key + FAILED).mark();
}
}

/**
* 获取正在运行的调用次数
* @param serviceInstance
* @return
*/
public long getCalling(ServiceInstance serviceInstance) {
String key = serviceInstance.getHost() + ":" + serviceInstance.getPort();
long count = metricRegistry.counter(key + CALLING).getCount();
log.debug("ServiceInstanceMetrics-getCalling: {} -> {}", key, count);
return count;
}

/**
* 获取最近一分钟调用失败次数分钟速率,其实是滑动平均数
* @param serviceInstance
* @return
*/
public double getFailedInRecentOneMin(ServiceInstance serviceInstance) {
String key = serviceInstance.getHost() + ":" + serviceInstance.getPort();
double rate = metricRegistry.meter(key + FAILED).getOneMinuteRate();
log.debug("ServiceInstanceMetrics-getFailedInRecentOneMin: {} -> {}", key, rate);
return rate;
}
}

负载均衡核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
kotlin复制代码private final LoadingCache<Long, Set<String>> calledIpPrefixes = Caffeine.newBuilder()
.expireAfterAccess(3, TimeUnit.MINUTES)
.build(k -> Sets.newConcurrentHashSet());
private final String serviceId;
private final Tracer tracer;
private final ServiceInstanceMetrics serviceInstanceMetrics;

//每次重试,其实都会调用这个 choose 方法重新获取一个实例
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
Span span = tracer.currentSpan();
return serviceInstanceListSupplier.get().next()
.map(serviceInstances -> {
//保持 span 和调用 choose 的 span 一样
try (Tracer.SpanInScope cleared = tracer.withSpanInScope(span)) {
return getInstanceResponse(serviceInstances);
}
});
}


private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> serviceInstances) {
if (serviceInstances.isEmpty()) {
log.warn("No servers available for service: " + this.serviceId);
return new EmptyResponse();
}
//读取 spring-cloud-sleuth 的对于当前请求的链路追踪上下文,获取对应的 traceId
Span currentSpan = tracer.currentSpan();
if (currentSpan == null) {
currentSpan = tracer.newTrace();
}
long l = currentSpan.context().traceId();
return getInstanceResponseByRoundRobin(l, serviceInstances);
}

@VisibleForTesting
public Response<ServiceInstance> getInstanceResponseByRoundRobin(long traceId, List<ServiceInstance> serviceInstances) {
//首先随机打乱列表中实例的顺序
Collections.shuffle(serviceInstances);
//需要先将所有参数缓存起来,否则 comparator 会调用多次,并且可能在排序过程中参数发生改变(针对实例的请求统计数据一直在并发改变)
Map<ServiceInstance, Integer> used = Maps.newHashMap();
Map<ServiceInstance, Long> callings = Maps.newHashMap();
Map<ServiceInstance, Double> failedInRecentOneMin = Maps.newHashMap();
serviceInstances = serviceInstances.stream().sorted(
Comparator
//之前已经调用过的网段,这里排后面
.<ServiceInstance>comparingInt(serviceInstance -> {
return used.computeIfAbsent(serviceInstance, k -> {
return calledIpPrefixes.get(traceId).stream().anyMatch(prefix -> {
return serviceInstance.getHost().contains(prefix);
}) ? 1 : 0;
});
})
//当前错误率最少的
.thenComparingDouble(serviceInstance -> {
return failedInRecentOneMin.computeIfAbsent(serviceInstance, k -> {
double value = serviceInstanceMetrics.getFailedInRecentOneMin(serviceInstance);
//由于使用的是移动平均值(EMA),需要忽略过小的差异(保留两位小数,不是四舍五入,而是直接舍弃)
return ((int) (value * 100)) / 100.0;
});
})
//当前负载请求最少的
.thenComparingLong(serviceInstance -> {
return callings.computeIfAbsent(serviceInstance, k ->
serviceInstanceMetrics.getCalling(serviceInstance)
);
})
).collect(Collectors.toList());
if (serviceInstances.isEmpty()) {
log.warn("No servers available for service: " + this.serviceId);
return new EmptyResponse();
}
ServiceInstance serviceInstance = serviceInstances.get(0);
//记录本次返回的网段
calledIpPrefixes.get(traceId).add(serviceInstance.getHost().substring(0, serviceInstance.getHost().lastIndexOf(".")));
//目前记录这个只为了兼容之前的单元测试(调用次数测试)
positionCache.get(traceId).getAndIncrement();
return new DefaultResponse(serviceInstance);
}

对于记录实例数据的缓存何时更新,是在 FeignClient 粘合重试,断路以及线程隔离的代码中的,这个我们下一节就会看到。

一些组内关于方案设计的取舍 Q&A

1. 为何没有使用所有微服务共享的缓存来保存调用数据,来让这些数据更加准确?

共享缓存的可选方案包括将这些数据记录放入 Redis,或者是 Apache Ignite 这样的内存网格中。但是有两个问题:

  1. 如果数据记录放入 Redis 这样的额外存储,如果 Redis 不可用会导致所有的负载均衡都无法执行。如果放入 Apache Ignite,如果对应的节点下线,那么对应的负载均衡也无法执行。这些都是不能接受的。
  2. 假设微服务 A 需要调用微服务 B,可能 A 的某个实例调用 B 的某个实例有问题,但是 A 的其他实例调用 B 的这个实例却没有问题,例如当某个可用区与另一个可用区网络拥塞的时候。如果用同一个缓存 Key 记录 A 所有的实例调用 B 这个实例的数据,显然是不准确的。

每个微服务使用本地缓存,记录自己调用其他实例的数据,在我们这里看来,不仅是更容易实现,也是更准确的做法。

2. 采用 EMA 的方式而不是请求窗口的方式统计最近错误率

采用请求窗口的方式统计,肯定是最准确的,例如我们统计最近一分钟的错误率,就将最近一分钟的请求缓存起来,读取的时候,将缓存起来的请求数据加在一起取平均数即可。但是这种方式在请求突增的时候,可能会占用很多很多内存来缓存这些请求。同时计算错误率的时候,随着缓存请求数的增多也会消耗更大量的 CPU 进行计算。这样做很不值得。

EMA 这种滑动平均值的计算方式,常见于各种性能监控统计场景,例如 JVM 中 TLAB 大小的动态计算,G1 GC Region 大小的伸缩以及其他很多 JVM 需要动态得出合适值的地方,都用这种计算方式。他不用将请求缓存起来,而是直接用最新值乘以一个比例之后加上老值乘以 (1 - 这个比例),这个比例一般高于 0.5,表示 EMA 和当前最新值更加相关。

但是 EMA 也带来另一个问题,我们会发现随着程序运行小数点位数会非常多,会看到类似于如下的值:0.00000000123, 0.120000001, 0.120000003, 为了忽略过于细致差异的影响(其实这些影响也来自于很久之前的错误请求),我们只保留两位小数进行排序。

微信搜索“我的编程喵”关注公众号,每日一刷,轻松提升技术,斩获各种offer

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Laravel-admin 重写源码 自定义排序回调 Las

发表于 2021-11-11

「这是我参与11月更文挑战的第11天,活动详情查看:2021最后一次更文挑战」

laravel-admin 是比较流程的Laravel管理后台框架

背景

有几个业务相关的配置信息需要管理后台灵活配置,且返回的数据要进行排序

为了保证业务接口的请求速度,我们把这些配置信息接口做了缓存

在管理后台进行form表单提交的时候清空缓存,保证数据及时更新(比如删除、修改之后要刷新缓存)

测试阶段发现一个问题,laravel-admin 的 sortable 扩展和框架本身的form表单提交没有关系,执行排序的时候没有回调函数,导致排序操作后无法主动清除缓存。

实现原理分析

我们可以在 form() 函数中 调用 $form->saved(function () { HobbyInfo::flushCache(); }); 进行相关操作

但是拖拽排序保存是不会触发这个函数的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
php复制代码<?php

namespace App\Admin\Controllers;

.
.
.

class HobbyInfoController extends AdminController
{
/**
* Title for current resource.
*
* @var string
*/
protected $title = '用户爱好-一级分类';

/**
* Make a grid builder.
*
* @return Grid
*/
protected function grid()
{
$grid = new Grid(new HobbyInfo());

$grid->sortable();
.
.
.

return $grid;
}

/**
* Make a show builder.
*
* @param mixed $id
* @return Show
*/
protected function detail($id)
{
$show = new Show(HobbyInfo::findOrFail($id));
return $show;
}

/**
* Make a form builder.
*
* @return Form
*/
protected function form()
{
$form = new Form(new HobbyInfo());

.
.
.

//清空缓存
$form->saved(function () {
HobbyInfo::flushCache();
});
return $form;
}
}

定位问题

  1. 点击【保存排序】按钮时查看网络请求,发现了一个不是我定义的路由 _grid-sortable_
    image.png
  2. 在项目中搜索这个路由,发现是扩展中的一个路由

image.png

  1. 查看这个扩展相关的源码,发现拖拽排序是不会执行我们写的 form 表单提交相关方法的,源码内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
php复制代码<?php

namespace Encore\Admin\GridSortable\Controllers;

use Exception;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;

class GridSortableController extends Controller
{
public function sort(Request $request)
{
$sorts = $request->get('_sort');

$sorts = collect($sorts)
->pluck('key')
->combine(
collect($sorts)->pluck('sort')->sort()
);

$status = true;
$message = trans('admin.save_succeeded');
$modelClass = $request->get('_model');

try {
/** @var \Illuminate\Database\Eloquent\Collection $models */
$models = $modelClass::find($sorts->keys());

foreach ($models as $model) {

$column = data_get($model->sortable, 'order_column_name', 'order_column');

$model->{$column} = $sorts->get($model->getKey());
$model->save();
}
} catch (Exception $exception) {
$status = false;
$message = $exception->getMessage();
}

return response()->json(compact('status', 'message'));
}
}

解决问题

  1. 因为我们有好几个配置模块,需要找到一种通用的配置方式,经过再三考虑,决定修改扩展的源码
  • 自定义回调函数 afterSort ,意为在排序之后执行
  • 通过阅读源码我们不难发现,我们是能够获得model对象的
  • 我们做下兼容判断,如果model中有 afterSort 的话,我们就执行
  • 这样,我们就可以在需要执行排序的model中,自定义afterSort方法,执行我们的操作,比如刷新缓存。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
php复制代码<?php

namespace Encore\Admin\GridSortable\Controllers;

use Exception;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;

class GridSortableController extends Controller
{
public function sort(Request $request)
{
$sorts = $request->get('_sort');

$sorts = collect($sorts)
->pluck('key')
->combine(
collect($sorts)->pluck('sort')->sort()
);

$status = true;
$message = trans('admin.save_succeeded');
$modelClass = $request->get('_model');

try {
/** @var \Illuminate\Database\Eloquent\Collection $models */
$models = $modelClass::find($sorts->keys());

foreach ($models as $model) {

$column = data_get($model->sortable, 'order_column_name', 'order_column');

$model->{$column} = $sorts->get($model->getKey());
$model->save();
}
//自定义回调
if (method_exists($model, 'afterSort')) {
$model->afterSort();
}
} catch (Exception $exception) {
$status = false;
$message = $exception->getMessage();
}

return response()->json(compact('status', 'message'));
}
}
  1. 我们在自己的model中,比如我的兴趣爱好model,自定义 afterSort 函数就可以了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
php复制代码<?php

namespace App\Model;

use App\Model\Cache\CacheKey;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Spatie\EloquentSortable\Sortable;
use Spatie\EloquentSortable\SortableTrait;

class HobbyInfo extends CustomModel implements Sortable
{
protected $table = 'tbl_hobby_info';
protected $connection = 'footprint';

protected $primaryKey = 'id';
public $incrementing = true;


use SortableTrait;

public $sortable = [
'order_column_name' => 'sort',
'sort_when_creating' => true,
];

//改了源码 添加了自定义回调
public static function afterSort()
{
self::flushCache();
}

//清空缓存
public static function flushCache()
{
$cacheKey = CacheKey::getCacheKey(CacheKey::TYPE_USER_SETTING_HOBBY_ALL);
Cache::forget($cacheKey);
Log::info('清空Hobby缓存');
}
}
  1. 这样我们就通过修改源码,实现了自定义回调,在其他model中也可以通过这种方法触发排序后的操作。

image.png

Last but not least

技术交流群请到 这里来。 或者添加我的微信 wangzhongyang0601 ,一起学习。

感谢大家的点赞、评论、关注,谢谢大佬们的支持,感谢。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Redis慢查询及订阅模式 慢查询 发布订阅模式

发表于 2021-11-11

「这是我参与11月更文挑战的第11天,活动详情查看:2021最后一次更文挑战」。

慢查询

慢查询日志就是系统在命令执行时每条命令的执行时间,当超过阀值,就将这条命令记录下来

Redis命令执行流程

image.png

  1. 发送命令
  2. 命令排队
  3. 命令执行
  4. 返回结果

其中命令执行才是慢查询统计的时间

慢查询两个配置参数

  • slowlog-log-slower-than:预设阀值,单位是毫秒,假如执行一条“很慢”的命令,执行时间超过阀值就会被记录下来
  • slowlog-max-len:设置慢查询日志最多存储多少条

发布订阅模式

Redis提供了发布订阅功能,可以用于消息的传输,Redis的发布订阅机制包括三个部分,发布者,订阅者和Channel。

image.png

发布订阅功能

  • 发送消息采用publish命令

image.png

  • 订阅某个频道采用subscribe命令订阅

image.png

  • 模式匹配:同时订阅多个频道,命令是PSUBSCRIBE

image.png

Redis过期时间处理

  1. 主动处理

定时处理,在设置过期时间的时候创建一个定时器,当过期时间到的时候立马执行删除操作,这个操作是即时的,不管在这个时间段内有多少过期Key,也不管服务器运行情况,都会被删除,对CPU不是很友好。

定期删除,定期删除是设置一个时间间隔每个时间段都会检测是否有过期键,如果有就执行删除,

  1. 被动处理

当已经过期的key再次被访问时,才会对key是否过期进行判断,如果已经过期,则进行删除,并返回NIL.这种处理方式对CPU是友好的,不会对其他过期key上占用CPU,但对内存不友好,一个key已经过期,但是在它被操作前都不会被删除,仍然占用内存空间,如果有大量过期Key没有被再次操作,则会浪费大量内存空间。

3.RDB与AOF对过期键的处理

如果在执行save或者bgsave命令创建一个RDB时,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的RDB文件中。

当过期键被惰性删除或者定期删除之后,程序会向AOF文件追加(append)一条DEL命令,来显式地记录该键已被删除。

在执行AOF重写的过程中,程序会对数据库中的键进行检查,已过期的键不会被保存到重写后的AOF文件中。

内存回收

noeviction:默认策略,不会删除任何数据,拒绝所有写入操作并返回客户端错误信息,此时Redis只响应读操作。

volatitle-rlu:根据LRU算法删除设置了超时属性的键,知道腾出足够空间为止。如果没有可删除的键对象,回退到noeviction策略。

allkeys-lru:根据LRU算法删除键,不管数据有没有设置超时属性,直到腾出足够空间为止。

allkeys-random:随机删除所有键,知道腾出足够空间为止。

volatitle-random:随机删除过期键,知道腾出足够空间为止。

volatitle-ttl:根据键值对象的ttl属性,删除最近将要过期数据。如果没有,回退到noeviction策略

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…367368369…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%