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

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


  • 首页

  • 归档

  • 搜索

Value竟然能玩出这么多花样,涨知识了

发表于 2021-10-11

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

前言

对于从事java开发工作的小伙伴来说,spring框架肯定再熟悉不过了。spring给开发者提供了非常丰富的api,满足我们日常的工作需求。

如果想要创建bean实例,可以使用@Controller、@Service、@Repository、@Component等注解。

如果想要依赖注入某个对象,可以使用@Autowired和@Resource注解。

如果想要开启事务,可以使用@Transactional注解。

如果想要动态读取配置文件中的某个系统属性,可以使用@Value注解。

等等,还有很多。。。

前面几种常用的注解,在我以往的文章中已经介绍过了,在这里就不过多讲解了。

今天咱们重点聊聊@Value注解,因为它是一个非常有用,但极其容易被忽视的注解,绝大多数人可能只用过它的一部分功能,这是一件非常遗憾的事情。

所以今天有必要和大家一起,重新认识一下@Value。

最近无意间获得一份BAT大厂大佬写的刷题笔记,一下子打通了我的任督二脉,越来越觉得算法没有想象中那么难了。

BAT大佬写的刷题笔记,让我offer拿到手软

  1. 由一个例子开始

假如在UserService类中,需要注入系统属性到userName变量中。通常情况下,我们会写出如下的代码:

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Service
public class UserService {

@Value("${susan.test.userName}")
private String userName;

public String test() {
System.out.println(userName);
return userName;
}
}

通过@Value注解指定系统属性的名称susan.test.userName,该名称需要使用${}包起来。

这样spring就会自动的帮我们把对应的系统属性值,注入到userName变量中。

不过,上面功能的重点是要在applicationContext.properties文件(简称:配置文件)中配置同名的系统属性:

1
2
java复制代码#张三
susan.test.userName=\u5f20\u4e09

那么,名称真的必须完全相同吗?

  1. 关于属性名

这时候,有些朋友可能会说:
在@ConfigurationProperties配置类中,定义的参数名可以跟配置文件中的系统属性名不同。

比如,在配置类MyConfig类中定义的参数名是userName:

1
2
3
4
5
6
java复制代码@Configuration
@ConfigurationProperties(prefix = "susan.test")
@Data
public class MyConfig {
private String userName;
}

而配置文件中配置的系统属性名是:

1
java复制代码susan.test.user-name=\u5f20\u4e09

类中用的userName,而配置文件中用的user-name,不一样。但测试之后,发现该功能能够正常运行。

配置文件中的系统属性名用 驼峰标识 或 小写字母加中划线的组合,spring都能找到配置类中的属性名userName进行赋值。

由此可见,配置文件中的系统属性名,可以跟配置类中的属性名不一样。不过,有个前提,前缀susan.test必须相同。

那么,@Value注解中定义的系统属性名也可以不一样吗?

答案:不能。如果不一样,启动项目时会直接报错。


此外,如果只在@Value注解中指定了系统属性名,但实际在配置文件中没有配置它,也会报跟上面一样的错。

所以,@Value注解中指定的系统属性名,必须跟配置文件中的相同。

  1. 乱码问题

不知道细心的小伙伴们有没有发现,我配置的属性值:张三,其实是转义过的。

1
java复制代码susan.test.userName=\u5f20\u4e09

为什么要做这个转义?

假如在配置文件中配置中文的张三:

1
java复制代码susan.test.userName=张三

最后获取数据时,你会发现userName竟然出现了乱码:

å¼ ä¸‰

what?

为什么会出现乱码?

答:在springboot的CharacterReader类中,默认的编码格式是ISO-8859-1,该类负责.properties文件中系统属性的读取。如果系统属性包含中文字符,就会出现乱码。


那么,如何解决乱码问题呢?

目前主要有如下三种方案:

  1. 手动将ISO-8859-1格式的属性值,转换成UTF-8格式。
  2. 设置encoding参数,不过这个只对@PropertySource注解有用。
  3. 将中文字符用unicode编码转义。

显然@Value不支持encoding参数,所以方案2不行。

假如使用方案1,具体实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Service
public class UserService {

@Value(value = "${susan.test.userName}")
private String userName;

public String test() {
String userName1 = new String(userName.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
System.out.println();
return userName1;
}
}

确实可以解决乱码问题。

但如果项目中包含大量中文系统属性值,每次都需要加这样一段特殊转换代码。出现大量重复代码,有没有觉得有点恶心?

反转我被恶心到了。

那么,如何解决代码重复问题呢?

答:将属性值的中文内容转换成unicode。

类似于这样的:

1
java复制代码susan.test.userName=\u5f20\u4e09

这种方式同样能解决乱码问题,不会出现恶心的重复代码。但需要做一点额外的转换工作,不过这个转换非常容易,因为有现成的在线转换工具。

推荐使用这个工具转换:www.jsons.cn/unicode/

在这里顺便告诉你一个小秘密:如果你使用的是.yml或.yaml格式的配置文件,并不会出现中文乱码问题。

这又是为什么?

因为.yml或.yaml格式的配置文件,最终会使用UnicodeReader类进行解析,它的init方法中,首先读取BOM文件头信息,如果头信息中有UTF8、UTF16BE、UTF16LE,就采用对应的编码,如果没有,则采用默认UTF8编码。

需要注意的是:乱码问题一般出现在本地环境,因为本地直接读取的.properties配置文件。在dev、test、生产等环境,如果从zookeeper、apollo、nacos等配置中心中获取系统参数值,走的是另外的逻辑,并不会出现乱码问题。

4.默认值

有时候,默认值是我们非常头疼的问题。

为什么这样说呢?

因为很多时候使用java的默认值,并不能满足我们的日常工作需求。

比如有这样一个需求:如果配置了系统属性,userName就用配置的属性值。如果没有配置,则userName用默认值susan。

有些朋友可能认为可以这样做:

1
2
java复制代码@Value(value = "${susan.test.userName}")
private String userName = "susan";

在定义参数时直接给个默认值,但如果仔细想想这招是行不通的的。因为设置userName默认值的时机,比@Value注解依赖注入属性值要早,也就是说userName初始化好了默认值,后面还是会被覆盖。

那么,到底该如何设置默认值呢?

答:使用:。

例如:

1
2
java复制代码@Value(value = "${susan.test.userName:susan}")
private String userName;

在需要设置默认值的系统属性名后,加:符号。紧接着,在:右边设置默认值。

建议大家平时在使用@Value时,尽量都设置一个默认值。如果不需要默认值,宁可设置一个空。比如:

1
2
java复制代码@Value(value = "${susan.test.userName:}")
private String userName;

为什么这么说?

假如有这种场景:在business层中包含了UserService类,business层被api服务和job服务都引用了。但UserService类中@Value的userName只在api服务中有用,在job服务中根本用不到该属性。

对于job服务来说,如果不在.properties文件中配置同名的系统属性,则服务启动时就会报错。

这个坑,我之前踩过多次。所以,建议大家,使用@Value注解时,最好给参数设置一个默认值,以防止出现类似的问题。

  1. static变量

前面我们已经见识过,如何使用@Value注解,给类的成员变量注入系统属性值。

那么,问题来了,静态变量可以自动注入系统属性值不?

我们一起看看,假如将上面的userName定义成static的:

1
2
java复制代码@Value("${susan.test.userName}")
private static String userName;

程序可以正常启动,但是获取到userName的值却是null。

由此可见,被static修饰的变量通过@Value会注入失败。

作为好奇宝宝的你,此时肯定想问:如何才能给静态变量注入系统属性值呢?

答:这就需要使用如下的骚代码了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@Service
public class UserService {

private static String userName;

@Value("${susan.test.userName}")
public void setUserName(String userName) {
UserService.userName = userName;
}

public String test() {
return userName;
}
}

提供一个静态参数的setter方法,在该方法上使用@Value注入属性值,并且同时在该方法中给静态变量赋值。

有些细心的朋友可能会发现,@Value注解在这里竟然使用在setUserName方法上了,也就是对应的setter方法,而不是在变量上。

有趣,有趣,这种用法有点高端喔。

不过,通常情况下,我们一般会在pojo实体类上,使用lombok的@Data、@Setter、@Getter等注解,在编译时动态增加setter或getter方法,所以@Value用在方法上的场景其实不多。

6.变量类型

上面的内容,都是用的字符串类型的变量进行举例的。其实,@Value注解还支持其他多种类型的系统属性值的注入。

6.1 基本类型

众所周知,在Java中的基本数据类型有4类8种,然我们一起回顾一下:

  • 整型:byte、short、int、long
  • 浮点型:float、double
  • 布尔型:boolean
  • 字符型:char

相对应地提供了8种包装类:

  • 整型:Byte、Short、Integer、Long
  • 浮点型:Float、Double
  • 布尔型:Boolean
  • 字符型:Character

@Value注解对这8中基本类型和相应的包装类,有非常良好的支持,例如:

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
java复制代码@Value("${susan.test.a:1}")
private byte a;

@Value("${susan.test.b:100}")
private short b;

@Value("${susan.test.c:3000}")
private int c;

@Value("${susan.test.d:4000000}")
private long d;

@Value("${susan.test.e:5.2}")
private float e;

@Value("${susan.test.f:6.1}")
private double f;

@Value("${susan.test.g:false}")
private boolean g;

@Value("${susan.test.h:h}")
private char h;

@Value("${susan.test.a:1}")
private byte a1;

@Value("${susan.test.b:100}")
private Short b1;

@Value("${susan.test.c:3000}")
private Integer c1;

@Value("${susan.test.d:4000000}")
private Long d1;

@Value("${susan.test.e:5.2}")
private Float e1;

@Value("${susan.test.f:6.1}")
private Double f1;

@Value("${susan.test.g:false}")
private Boolean g1;

@Value("${susan.test.h:h}")
private Character h1;

有了这些常用的数据类型,我们在定义变量类型时,可以非常愉快的玩耍了,不用做额外的转换。

6.2 数组

但只用上面的基本类型是不够的,特别是很多需要批量处理数据的场景中。这时候可以使用数组,它在日常开发中使用的频率很高。

我们在定义数组时可以这样写:

1
2
java复制代码@Value("${susan.test.array:1,2,3,4,5}")
private int[] array;

spring默认使用逗号分隔参数值。

如果用空格分隔,例如:

1
2
kotlin复制代码@Value("${susan.test.array:1 2 3 4 5}")
private int[] array;

spring会自动把空格去掉,导致数据中只有一个值:12345,注意千万别搞错了。

顺便说一下,定义数组的时候,里面还是有挺多门道的。比如上面列子中,我的数据是:1,2,3,4,5。

如果我们把数组定义成:short、int、long、char、string类型,spring是可以正常注入属性值的。

但如果把数组定义成:float、double类型,启动项目时就会直接报错。


小伙伴们,下巴惊掉了没?

按理说,1,2,3,4,5用float、double是能够表示的呀,为什么会报错?

如果使用int的包装类,比如:

1
2
java复制代码@Value("${susan.test.array:1,2,3,4,5}")
private Integer[] array;

启动项目时同样会报上面的异常。

此外,定义数组时一定要注意属性值的类型,必须完全一致才可以,如果出现下面这种情况:

1
2
java复制代码@Value("${susan.test.array:1.0,abc,3,4,5}")
private int[] array;

属性值中包含了1.0和abc,显然都无法将该字符串转换成int。

6.3 集合类

有了基本类型和数组,的确让我们更加方便了。但对数据的处理,只用数组这一种数据结构是远远不够的,下面给大家介绍一下其他的常用数据结构。

6.3.1 List

List是数组的变种,它的长度是可变的,而数组的长度是固定的。

我们看看List是如何注入属性值的:

1
2
java复制代码@Value("${susan.test.list}")
private List<String> list;

最关键的是看配置文件:

1
2
3
4
java复制代码susan.test.list[0]=10
susan.test.list[1]=11
susan.test.list[2]=12
susan.test.list[3]=13

当你满怀希望的启动项目,准备使用这个功能的时候,却发现竟然报错了。


what?

看来@Value不支持这种直接的List注入。

那么,如何解决这个问题呢?

有人说用@ConfigurationProperties。

需要定义一个MyConfig类:

1
2
3
4
5
6
java复制代码@Configuration
@ConfigurationProperties(prefix = "susan.test")
@Data
public class MyConfig {
private List<String> list;
}

然后在调用的地方这样写:

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Service
public class UserService {

@Autowired
private MyConfig myConfig;

public String test() {
System.out.println(myConfig.getList());
return null;
}
}

这种方法确实能够完成List注入。但是,只能说明@ConfigurationProperties注解的强大,跟@Value有半毛钱的关系?

答:没有。

那么,问题来了,用@Value如何实现这个功能呢?

答:使用spring的EL表达式。

List的定义改成:

1
2
java复制代码@Value("#{'${susan.test.list}'.split(',')}")
private List<String> list;

使用#号加大括号的EL表达式。

然后配置文件改成:

1
java复制代码susan.test.list=10,11,12,13

跟定义数组时的配置文件一样。

6.3.2 Set

Set也是一种保存数据的集合,它比较特殊,里面保存的数据不会重复。

我们可以这样定义Set:

1
2
java复制代码@Value("#{'${susan.test.set}'.split(',')}")
private Set<String> set;

配置文件是这样的:

1
java复制代码susan.test.set=10,11,12,13

Set跟List的用法极为相似。

但为了证明本节的独特之处,我打算说点新鲜的内容。

如何给List或者Set设置默认值空呢?

有些朋友可能会说:这还不简单,直接在@Value的$表达式后面加个:号不就行了。

具体代码如下:

1
2
java复制代码@Value("#{'${susan.test.set:}'.split(',')}")
private Set<String> set;

结果却跟想象中不太一样:

Set集合怎么不是空的,而是包含了一个空字符串的集合?

好吧,那我在:号后加null,总可以了吧?

Set集合也不是空的,而是包含了一个”null”字符串的集合。

这也不行,那也不行,该如何是好?

答:使用EL表达式的empty方法。

具体代码如下:

1
2
java复制代码@Value("#{'${susan.test.set:}'.empty ? null : '${susan.test.set:}'.split(',')}")
private Set<String> set;

运行之后,结果对了:

其实List也有类似的问题,也能使用该方法解决问题。

在这里温馨的提醒一下,该判断的表达式比较复杂,自己手写非常容易写错,建议复制粘贴之后根据实际需求改改。

6.3.3 Map

还有一种比较常用的集合是map,它支持key/value键值对的形式保存数据,并且不会出现相同key的数据。

我们可以这样定义Map:

1
2
java复制代码@Value("#{${susan.test.map}}")
private Map<String, String> map;

配置文件是这样的:

1
java复制代码susan.test.map={"name":"苏三", "age":"18"}

这种用法跟上面稍微有一点区别。

设置默认值的代码如下:

1
2
java复制代码@Value("#{'${susan.test.map:}'.empty ? null : '${susan.test.map:}'}")
private Map<String, String> map;

7 EL高端玩法

前面我们已经见识过spring EL表达式的用法了,在设置空的默认值时特别有用。

其实,empty方法只是它很普通的用法,还有更高端的用法,不信我们一起看看。

7.1 注入bean

以前我们注入bean,一般都是用的@Autowired或者@Resource注解。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码@Service
public class RoleService {
public String getRoleName() {
return "管理员";
}
}

@Service
public class UserService {

@Autowired
private RoleService roleService;

public String test() {
System.out.println(roleService.getRoleName());
return null;
}
}

但我要告诉你的是@Value注解也可以注入bean,它是这么做的:

1
2
java复制代码@Value("#{roleService}")
private RoleService roleService;

通过这种方式,可以注入id为roleService的bean。

7.2 bean的变量和方法

通过EL表达式,@Value注解已经可以注入bean了。既然能够拿到bean实例,接下来,可以再进一步。

在RoleService类中定义了:成员变量、常量、方法、静态方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Service
public class RoleService {
public static final int DEFAULT_AGE = 18;
public int id = 1000;

public String getRoleName() {
return "管理员";
}

public static int getParentId() {
return 2000;
}
}

在调用的地方这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码@Service
public class UserService {

@Value("#{roleService.DEFAULT_AGE}")
private int myAge;

@Value("#{roleService.id}")
private int id;

@Value("#{roleService.getRoleName()}")
private String myRoleName;

@Value("#{roleService.getParentId()}")
private String myParentId;

public String test() {
System.out.println(myAge);
System.out.println(id);
System.out.println(myRoleName);
System.out.println(myParentId);
return null;
}
}

在UserService类中通过@Value可以注入:成员变量、常量、方法、静态方法获取到的值,到相应的成员变量中。

一下子有没有豁然开朗的感觉,有了这些,我们可以通过@Value注解,实现更多的功能了,不仅仅限于注入系统属性。

7.3 静态类

前面的内容都是基于bean的,但有时我们需要调用静态类,比如:Math、xxxUtil等静态工具类的方法,该怎么办呢?

答:用T加括号。

示例1:

1
2
java复制代码@Value("#{T(java.io.File).separator}")
private String path;

可以注入系统的路径分隔符到path中。

示例2:

1
2
java复制代码@Value("#{T(java.lang.Math).random()}")
private double randomValue;

可以注入一个随机数到randomValue中。

7.4 逻辑运算

通过上面介绍的内容,我们可以获取到绝大多数类的变量和方法的值了。但有了这些值,还不够,我们能不能在EL表达式中加点逻辑?

拼接字符串:

1
2
java复制代码@Value("#{roleService.roleName + '' + roleService.DEFAULT_AGE}")
private String value;

逻辑判断:

1
2
java复制代码@Value("#{roleService.DEFAULT_AGE > 16 and roleService.roleName.equals('苏三')}")
private String operation;

三目运算:

1
2
java复制代码@Value("#{roleService.DEFAULT_AGE > 16 ? roleService.roleName: '苏三' }")
private String realRoleName;

还有很多很多功能,我就不一一列举了。

EL表达式实在太强大了,对这方面如果感兴趣的小伙伴可以找我私聊。

最近无意间获得一份BAT大厂大佬写的刷题笔记,一下子打通了我的任督二脉,越来越觉得算法没有想象中那么难了。

BAT大佬写的刷题笔记,让我offer拿到手软

8 ${}和#{}的区别

上面巴拉巴拉说了这么多@Value的牛逼用法,归根揭底就是${}和#{}的用法。

下面重点说说${}和#{}的区别,这可能是很多小伙伴比较关心的话题。

8.1 ${}

主要用于获取配置文件中的系统属性值。

例如:

1
2
java复制代码@Value(value = "${susan.test.userName:susan}")
private String userName;

通过:可以设置默认值。如果在配置文件中找不到susan.test.userName的配置,则注入时用默认值。

如果在配置文件中找不到susan.test.userName的配置,也没有设置默认值,则启动项目时会报错。

8.2 #{}

主要用于通过spring的EL表达式,获取bean的属性,或者调用bean的某个方法。还有调用类的静态常量和静态方法。

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Value("#{roleService.DEFAULT_AGE}")
private int myAge;

@Value("#{roleService.id}")
private int id;

@Value("#{roleService.getRoleName()}")
private String myRoleName;

@Value("#{T(java.lang.Math).random()}")
private double randomValue;

如果是调用类的静态方法,则需要加T(包名 + 方法名称)。

例如:T(java.lang.Math)。

好了,今天的内容就介绍到这里,希望对你会有所帮助。随便剧透一下,后面的文章会继续介绍:

  1. @Value的原理
  2. @Value动态刷新属性值的原因
  3. @ConfigurationProperties注解的用法,它也非常强大。

好不好奇?赶紧关注一波呀。

「欢迎在评论区讨论,掘金官方将在掘力星计划活动结束后,在评论区抽送100份掘金周边,抽奖详情见活动文章」

本文转载自: 掘金

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

为什么阿里强制 boolean 类型变量不能使用 is 开头

发表于 2021-10-11

背景

平时工作中大家经常使用到boolean以及Boolean类型的数据,前者是基本数据类型,后者是包装类,为什么不推荐使用isXXX来命名呢?到底是用基本类型的数据好呢还是用包装类好呢?

例子

1.其他非boolean类型

1
2
3
4
typescript复制代码 private String isHot;
public String getIsHot() {
return isHot;
}

2.boolean类型

1
2
3
4
typescript复制代码 private boolean isHot;
public boolean isHot() {
return isHot;
}

3.包装类型

1
2
3
4
typescript复制代码 private Boolean isHot;
public Boolean getHot() {
return isHot;
}

4.不以is开头

1
2
3
4
typescript复制代码  private boolean hot;
public boolean isHot() {
return hot;
}

5.包装类型

1
2
3
4
typescript复制代码 private Boolean hot;
public Boolean getHot() {
return hot;
}

其实阿里巴巴发布的java开发手册中就写明了,强制规定,布尔类型的数据,无论是boolean还是Boolean都不准使用isXXX来命名
在这里插入图片描述

  • 对于非boolean类型的参数,getter和setter方法命名的规范是以get和set开头
  • 对于boolean类型的参数,setter方法是以set开头,但是getter方法命名的规范是以is开头
  • 包装类自动生成的getter和setter方法的名称都是getXXX()和setXXX()

1.其实javaBeans规范中对这些均有相应的规定,基本数据类型的属性,其getter和setter方法是getXXX()和setXXX,但是对于基本数据中布尔类型的数据,又有一套规定,其getter和setter方法是isXXX()和setXXX。但是包装类型都是以get开头

2.这种方式在某些时候是可以正常运行的,但是在一些rpc框架里面,当反向解析读取到isSuccess()方法的时候,rpc框架会“以为”其对应的属性值是success,而实际上其对应的属性值是isSuccess,导致属性值获取不到,从而抛出异常。

总结

1、boolean类型的属性值不建议设置为is开头,否则会引起rpc框架的序列化异常。

2、如果强行将IDE自动生成的isSuccess()方法修改成getSuccess(),也能获取到Success属性值,若两者并存,则之后通过getSuccess()方法获取Success属性值。

工作中使用基本类型的数据好还是包装类好

咱们举个例子,一个计算盈利的系统,其盈利比例有正有负,若使用了基本类型bouble定义了数据,当RPC调用时,若出现了问题,本来应该返回错误的,但是由于使用了基本类型,返回了0.0,系统会认为没有任何问题,今年收支平衡,而不会发现其实是出现了错误。

若使用了包装数据类型Double,当RPC调用失败时,会返回null,这样直接就能看到出现问题了,而不会因为默认值的问题影响判断。

其实阿里java开发手册中对于这个也有强制规定:
在这里插入图片描述
因此,这里建议大家POJO中使用包装数据类型,局部变量使用基本数据类型。

来源:blog.csdn.net/belongtocode/article/details/100635246

本文转载自: 掘金

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

MySql基础知识总结(SQL优化篇)

发表于 2021-10-11

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

本篇是MySQL知识体系总结系列的第二篇,该篇的主要内容是通过explain逐步分析sql,并通过修改sql语句与建立索引的方式对sql语句进行调优,也可以通过查看日志的方式,了解sql的执行情况,还介绍了MySQL数据库的行锁和表锁。

一、explain返回列简介

1、type常用关键字

system > const > eq_ref > ref > range > index > all。

  1. system:表仅有一行,基本用不到;
  2. const:表最多一行数据配合,主键查询时触发较多;
  3. eq_ref:对于每个来自于前面的表的行组合,从该表中读取一行。这可能是最好的联接类型,除了const类型;
  4. ref:对于每个来自于前面的表的行组合,所有有匹配索引值的行将从这张表中读取;
  5. range:只检索给定范围的行,使用一个索引来选择行。当使用=、<>、>、>=、<、<=、IS NULL、<=>、BETWEEN或者IN操作符,用常量比较关键字列时,可以使用range;
  6. index:该联接类型与ALL相同,除了只有索引树被扫描。这通常比ALL快,因为索引文件通常比数据文件小;
  7. all:全表扫描;
    实际sql优化中,最后达到ref或range级别。

2、Extra常用关键字

Using index:只从索引树中获取信息,而不需要回表查询;

Using where:WHERE子句用于限制哪一个行匹配下一个表或发送到客户。除非你专门从表中索取或检查所有行,如果Extra值不为Using where并且表联接类型为ALL或index,查询可能会有一些错误。需要回表查询。

Using temporary:mysql常建一个临时表来容纳结果,典型情况如查询包含可以按不同情况列出列的GROUP BY和ORDER BY子句时;

二、触发索引代码实例

1、建表语句 + 联合索引

1
2
3
4
5
6
7
8
9
10
11
12
13
sql复制代码CREATE TABLE `student` (
`id` int(10) NOT NULL,
`name` varchar(20) NOT NULL,
`age` int(10) NOT NULL,
`sex` int(11) DEFAULT NULL,
`address` varchar(100) DEFAULT NULL,
`phone` varchar(100) DEFAULT NULL,
`create_time` timestamp NULL DEFAULT NULL,
`update_time` timestamp NULL DEFAULT NULL,
`deleted` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `student_union_index` (`name`,`age`,`sex`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

2、使用主键查询

1.png

3、使用联合索引查询

2.png

4、联合索引,但与索引顺序不一致

3.png

备注:因为mysql优化器的缘故,与索引顺序不一致,也会触发索引,但实际项目中尽量顺序一致。

5、联合索引,但其中一个条件是 >

5.png

6、联合索引,order by

6.png

where和order by一起使用时,不要跨索引列使用。

三、单表sql优化

1、删除student表中的联合索引。

7.png

2、添加索引

1
sql复制代码alter table student add index student_union_index(name,age,sex);

8.png

优化一点,但效果不是很好,因为type是index类型,extra中依然存在using where。

3、更改索引顺序

因为sql的编写过程

1
vbnet复制代码select distinct ... from ... join ... on ... where ... group by ... having ... order by ... limit ...

解析过程

1
vbnet复制代码from ... on ... join ... where ... group by ... having ... select distinct ... order by ... limit ...

因此我怀疑是联合索引建的顺序问题,导致触发索引的效果不好。are you sure?试一下就知道了。

1
sql复制代码alter table student add index student_union_index2(age,sex,name);

删除旧的不用的索引:

1
csharp复制代码drop index student_union_index on student

索引改名

1
css复制代码ALTER TABLE student RENAME INDEX student_union_index2 TO student_union_index

更改索引顺序之后,发现type级别发生了变化,由index变为了range。
range:只检索给定范围的行,使用一个索引来选择行。

10.png

备注:in会导致索引失效,所以触发using where,进而导致回表查询。

4、去掉in

11.png

ref:对于每个来自于前面的表的行组合,所有有匹配索引值的行将从这张表中读取;

index 提升为ref了,优化到此结束。

5、小结

  1. 保持索引的定义和使用顺序一致性;
  2. 索引需要逐步优化,不要总想着一口吃成胖子;
  3. 将含in的范围查询,放到where条件的最后,防止索引失效;

四、双表sql优化

1、建表语句

1
2
3
4
5
6
7
8
9
10
11
12
13
sql复制代码CREATE TABLE `student` (
`id` int(10) NOT NULL,
`name` varchar(20) NOT NULL,
`age` int(10) NOT NULL,
`sex` int(11) DEFAULT NULL,
`address` varchar(100) DEFAULT NULL,
`phone` varchar(100) DEFAULT NULL,
`create_time` timestamp NULL DEFAULT NULL,
`update_time` timestamp NULL DEFAULT NULL,
`deleted` int(11) DEFAULT NULL,
`teacher_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
1
2
3
4
5
sql复制代码CREATE TABLE `teacher` (
`id` int(11) DEFAULT NULL,
`name` varchar(100) DEFAULT NULL,
`course` varchar(100) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

2、左连接查询

1
csharp复制代码explain select s.name,t.name from student s left join teacher t on s.teacher_id = t.id where t.course = '数学'

12.png

上一篇介绍过,联合查询时,小表驱动大表。小表也称为驱动表。其实就相当于双重for循环,小表就是外循环,第二张表(大表)就是内循环。

虽然最终的循环结果都是一样的,都是循环一样的次数,但是对于双重循环来说,一般建议将数据量小的循环放外层,数据量大的放内层,这是编程语言的优化原则。

再次代码测试:

student数据:四条

13.png

teacher数据:三条

14.png

按照理论分析,teacher应该为驱动表。

111.png

sql语句应该改为:

1
csharp复制代码explain select teacher.name,student.name from teacher left join student on teacher.id = student.id  where teacher.course = '数学'

优化一般是需要索引的,那么此时,索引应该怎么加呢?往哪个表上加索引?

索引的基本理念是:索引要建在经常使用的字段上。

由on teacher.id = student.id可知,teacher表的id字段使用较为频繁。

left join on,一般给左表加索引;因为是驱动表嘛。

15.png

1
2
sql复制代码alter table teacher add index teacher_index(id);
alter table teacher add index teacher_course(course);

16.png

备注:如果extra中出现using join buffer,表明mysql底层觉得sql写的太差了,mysql加了个缓存,进行优化了。

3、小结

  1. 小表驱动大表
  2. 索引建立在经常查询的字段上
  3. sql优化,是一种概率层面的优化,是否实际使用了我们的优化,需要通过explain推测。

五、避免索引失效的一些原则
1、复合索引,不要跨列或无序使用(最佳左前缀);

2、符合索引,尽量使用全索引匹配;

3、不要在索引上进行任何操作,例如对索引进行(计算、函数、类型转换),索引失效;

4、复合索引不能使用不等于(!=或<>)或 is null(is not null),否则索引失效;

5、尽量使用覆盖索引(using index);

6、like尽量以常量开头,不要以%开头,否则索引失效;如果必须使用%name%进行查询,可以使用覆盖索引挽救,不用回表查询时可以触发索引;

7、尽量不要使用类型转换,否则索引失效;

8、尽量不要使用or,否则索引失效;

六、一些其他的优化方法

1、exist和in

1
csharp复制代码select name,age from student exist/in (子查询);

如果主查询的数据集大,则使用in;

如果子查询的数据集大,则使用exist;

2、order by 优化

using filesort有两种算法:双路排序、双路排序(根据IO的次数)

MySQL4.1之前,默认使用双路排序;双路:扫描两次磁盘(①从磁盘读取排序字段,对排序字段进行排序;②获取其它字段)。

MySQL4.1之后,默认使用单路排序;单路:只读取一次(全部字段),在buffer中进行排序。但单路排序会有一定的隐患(不一定真的是只有一次IO,有可能多次IO)。

注意:单路排序会比双路排序占用更多的buffer。

单路排序时,如果数据量较大,可以调大buffer的容量大小。

1
ini复制代码set max_length_for_sort_data = 1024;单位是字节byte。

如果max_length_for_sort_data值太低,MySQL底层会自动将单路切换到双路。

太低指的是列的总大小超过了max_length_for_sort_data定义的字节数。

提高order by查询的策略:

  1. 选择使用单路或双路,调整buffer的容量大小;
  2. 避免select * from student;(① MySQL底层需要对*进行翻译,消耗性能;② *永远不会触发索引覆盖 using index);
  3. 符合索引不要跨列使用,避免using filesort;
  4. 保证全部的排序字段,排序的一致性(都是升序或降序);

七、sql顺序 -> 慢日志查询

慢查询日志就是MySQL提供的一种日志记录,用于记录MySQL响应时间超过阈值的SQL语句(long_query_time,默认10秒) ;

慢日志默认是关闭的,开发调优时打开,最终部署时关闭。

1、慢查询日志

(1)检查是否开启了慢查询日志:

show variables like '%slow_query_log%'

17.png

(2)临时开启:

1
ini复制代码set global slow_query_log = 1;

(3)重启MySQL:

service mysql restart;

(4)永久开启:

/etc/my.cnf中追加配置:

放到[mysqld]下:

1
2
ini复制代码    slow_query_log=1
slow_query_log_file=/var/lib/mysql/localhost-slow.log

2、阈值

(1)查看默认阈值:

1
sql复制代码show variables like '%long_query_time%'

(2)临时修改默认阈值:

1
ini复制代码set global long_query_time = 5;

(3)永久修改默认阈值:

/etc/my.cnf中追加配置:

放到[mysqld]下:

long_query_time = 5;

(4)MySQL中的sleep:

select sleep(5);

(5)查看执行时间超过阈值的sql:

show global status like '%slow_queries%';

八、慢查询日志 –> mysqldumpslow工具

1、mysqldumpslow工具

慢查询的sql被记录在日志中,可以通过日志查看具体的慢sql。

cat /var/lib/mysql/localhost-slow.log

通过mysqldumpslow工具查看慢sql,可以通过一些过滤条件,快速查出需要定位的慢sql。

mysqldumpslow --help

参数简要介绍:

s:排序方式

r:逆序

l:锁定时间

g:正则匹配模式

2、查询不同条件下的慢sql

(1)返回记录最多的3个SQL

mysqldumpslow -s r -t 3 /var/lib/mysql/localhost-slow.log

(2)获取访问次数最多的3个SQL

mysqldumpslow -s c -t 3 /var/lib/mysql/localhost-slow.log

(3)按时间排序,前10条包含left join查询语句的SQL

mysqldumpslow -s t -t 10 -g "left join" /var/lib/mysql/localhost-slow.log

九、分析海量数据

1、show profiles

打开此功能:set profiling = on;

show profiles会记录所有profileing打来之后,全部SQL查询语句所花费的时间。

缺点是不够精确,确定不了是执行哪部分所消耗的时间,比如CPU、IO。

2、精确分析,sql诊断

show profile all for query 上一步查询到的query_id。

3、全局查询日志

show variables like ‘%general_log%’

开启全局日志:

set global general_log = 1;

set global log_output = table;

十、锁机制详解

1、操作分类

读写:对同一个数据,多个读操作可以同时进行,互不干扰。

写锁:如果当前写操作没有完毕,则无法进行其它的读写操作。

2、操作范围

表锁:一次性对一张表整体加锁。

如MyISAM存储引擎使用表锁,开销小、加锁快、无死锁;但锁的范围大,容易发生冲突、并发度低。

行锁:一次性对一条数据加锁。

如InnoDB存储引擎使用的就是行锁,开销大、加锁慢、容易出现死锁;锁的范围较小,不易发生锁冲突,并发度高(很小概率发生高并发问题:脏读、幻读、不可重复读)

lock table 表1 read/write,表2 read/write,…

查看加锁的表:

show open tables;

3、加读锁,代码实例

1
2
3
4
5
6
7
sql复制代码会话0:
lock table student read;
select * from student; --查,可以
delete from student where id = 1;--增删改,不可以

select * from user; --查,不可以
delete from user where id = 1;--增删改,不可以

如果某一个会话对A表加了read锁,则该会话可以对A表进行读操作、不能进行写操作。即如果给A表加了读锁,则当前会话只能对A表进行读操作,其它表都不能操作

1
2
3
4
5
6
7
sql复制代码会话1:
select * from student; --查,可以
delete from student where id = 1;--增删改,会“等待”会话0将锁释放

会话1:
select * from user; --查,可以
delete from user where id = 1;--增删改,可以

会话0给A表加了锁,其它会话的操作①可以对其它表进行读写操作②对A表:读可以,写需要等待释放锁。

4、加写锁

会话0: lock table student write;

当前会话可以对加了写锁的表,可以进行任何增删改查操作;但是不能操作其它表;

其它会话:

对会话0中对加写锁的表,可以进行增删改查的前提是:等待会话0释放写锁。

5、MyISAM表级锁的锁模式

MyISAM在执行查询语句前,会自动给涉及的所有表加读锁,在执行增删改前,会自动给涉及的表加写锁。

所以对MyISAM表进行操作,会有如下情况发生:

(1)对MyISAM表的读操作(加读锁),不会阻塞其它会话(进程)对同一表的读请求。但会阻塞对同一表的写操作。只有当读锁释放后,才会执行其它进程的写操作。

(2)对MyISAM表的写操作(加写锁),会阻塞其它会话(进程)对同一表的读和写操作,只有当写锁释放后,才会执行其它进程的读写操作。

6、MyISAM分析表锁定

查看哪些表加了锁:

show open tables;1代表被加了锁

分析表锁定的严重程度:

show status like 'table%';

18.png

Table_locks_immediate:可能获取到的锁数

Table_locks_waited:需要等待的表锁数(该值越大,说明存在越大的锁竞争)

一般建议:Table_locks_immediate/Table_locks_waited > 5000,建议采用InnoDB引擎,否则采用MyISAM引擎。

7、InnoDB分析表锁定

为了研究行锁,暂时将自动commit关闭,set autocommit = 0;

show status like ‘%innodb_row_lock%’

19.png

Innodb_row_lock_current_waits:当前正在等待锁的数量
Innodb_row_lock_time:等待总时长。从系统启动到现在一共等待的时间
Innodb_row_lock_time_avg:平均等待时长。从系统启动到现在一共等待的时间
Innodb_row_lock_time_max:最大等待时长。从系统启动到现在一共等待的时间
Innodb_row_lock_waits:等待次数。从系统启动到现在一共等待的时间

8、加行锁代码实例

(1)查询student

select id,name,age from student

20.png

(2)更新student

update student set age = 18 where id = 1

21.png

(3)加行锁

通过select id,name,age from student for update;给查询加行锁。

22.png

依旧修改成功,原因是MySQL默认是自动提交的,因此需要暂时将自动commit关闭

set autocommit = 0;

23.png

9、行锁的注意事项

(1)如果没有索引,行锁自动转为表锁。

(2)行锁只能通过事务解锁。

(3)InnoDB默认采用行锁

优点:并发能力强,性能高,效率高

缺点:比表锁性能损耗大

高并发用InnoDb,否则用MyISAM。

🍅 简介:Java领域优质创作者🏆、Java架构师奋斗者💪

🍅 有兴趣的可以加小编微信,一起学习一起进步guo_rui_

🍅 迎欢点赞 👍 收藏 ⭐留言 📝

「欢迎在评论区讨论,掘金官方将在掘力星计划活动结束后,在评论区抽送100份掘金周边,抽奖详情见活动文章」。

本文转载自: 掘金

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

gitbook的入坑之路 1 问题:安装 gitbook

发表于 2021-10-11

你好,我是悦创。

安装 gitbook 教程很多,我这里就不详细展开了,可以点击这个链接查看:www.aiyc.top/1947.html 如果链接失效,可以留言。

这里主要说一下我安装 gitbook 中所遇到的坑。

  1. 问题:安装 gitbook 出现

1
cmd复制代码TypeError: cb.apply is not a function

解决办法:nodejs 降级

安装 gitbook 的一些问题 gitbook init 和 if (cb) cb.apply(this, arguments),cb.apply is not a function

一,使用 gitbook init 时,卡在了 Installing GitBook 3.2.3 这一步

解决办法:

  1. 翻墙
  2. 使用淘宝镜像下载:
  3. npm下载路径,检查是不是淘宝镜像:
1
2
cmd复制代码npm config get registry
npm config set registry https://registry.npm.taobao.org

切换成淘宝镜像

再检查是不是淘宝镜像:

1
cmd复制代码npm config get registry

再安装:

1
cmd复制代码gitbook init

之前是一直卡在这里,我打了三篇代码没好!!设置之后,打了一局,回头一看,就出来了!

但是报错了!!!但这又是另一个悲伤的故事。。。

二,if (cb) cb.apply(this, arguments),cb.apply is not a function

产生了如下的报错:
在这里插入图片描述

产生这个报错的原因在于,nodejs 的版本不对,不支持这个 gitbook.

有两个解决办法:

一,切换 nodejs 的版本:

切换成 nodejs 的 v10.21.0 版本就会成功。

当然啦,在这里,我又接触到了新的知识!因为 nodejs 的版本很多,所以,就有 nodejs 的版本控制工具,可以方便地切换版本!

这是这个方法的博客地址,www.aiyc.top/1946.html

二,第二个方法呢,就更方便且不要脸了,就是把报错的代码注释掉!
直接打开报错的文件:

C:\Users\Administrator\AppData\Roaming\npm\node_modules\gitbook-cli\node_modules\npm\node_modules\graceful-fs\polyfills.js

错误的位置在代码的第287行,就是这个死乞白赖的函数!!!

1
2
3
4
5
6
7
8
9
10
11
12
13
cmd复制代码function statFix (orig) {
if (!orig) return orig
// Older versions of Node erroneously returned signed integers for
// uid + gid.
return function (target, cb) {
return orig.call(fs, target, function (er, stats) {
if (!stats) return cb.apply(this, arguments)
if (stats.uid < 0) stats.uid += 0x100000000
if (stats.gid < 0) stats.gid += 0x100000000
if (cb) cb.apply(this, arguments)
})
}
}

这个函数的作用是用来修复 node.js 的一些 bug ,但是我就为了学个 gitbook ,没必要难为我自己!

所以,我就找到这个函数的调用:

在这里插入图片描述
在这里插入图片描述

就成这样子啦!嘿嘿~

在这里插入图片描述

  1. 问题:使用 gitbook 编译后公式显示为源码

解决办法:安装 mathjax 插件

  1. 关于 mathjax 突然不能用了

warning: 对于这个问题我并没有弄清楚原理,稀里糊涂就解决了,大家谨慎观看

刚一开始我想在 gitbook 中使用 mathjax 写数学公式,但是按照网上的步骤

首先要有 node.js 环境

根目录创建 book.json 文件

内容为 {plugins: [“mathjax”];}

然后根目录执行 gitbook install./

那么我出现的问题是下载不下来,也许是真的需要多等一会,但是我是个急性子,直接 Google,发现一篇文章:www.aiyc.top/1979.html

gitbook 官方已不再维护插件,mathjax 由于关闭了 cdn 而导致 gitbook 的 mathjax 的官方镜像出问题了。
因此在这里写了一个插件 gitbook-plugin-mathjax-pro

  • npm install mathjax@2.7.7
  • 接着在 book.json 中引入:
1
2
3
cmd复制代码{
"plugins": ["mathjax-pro"]
}
  • 最后安装:gitbook install ./

虽然这次成功了,但是当我对第二个 book 使用同样的方式时,下载成功了,但是生成 book 的时候却报错了:

Error with plugin “mathjax-pro”: Cannot find module ‘mathjax/unpacked/MathJax

继续查,这次上百度,找到了这篇文章:zhuanlan.zhihu.com/p/125577482

在生成 pdf 或者生成网页时,mathjax 会报错,一般出现在新安装 mathjax 或者更新 mathjax 后,解决办法为,为 mathjax 降级,安装 2.7.6版本
npm install mathjax@2.7.6

然后我就稀里糊涂地直接在根目录下执行 npm install mathjax@2.7.6

然后继续 gitbook serve

markdown 里的内容是这样的:

1
2
3
4
markdown复制代码## 3. 子查询的分类
+ **IN / NOT IN** 子查询;
+ $$\theta -Some / \theta-All$$ 子查询;
+ **EXISTS / NOT EXISTS** 子查询;

结果很完美:

在这里插入图片描述

  1. 问题:安装 mathjax 失败

1
cmd复制代码PluginError: Error with plugin "mathjax-pro": Cannot find module 'mathjax/unpacked/MathJax'

解决办法:先安装mathjax@2.7.6

同上!

  1. 问题:安装报错

1
cmd复制代码npm WARN saveError ENOENT: no such file or directory, open 'C:\Users\username\package.json'

解决办法:先执行命名 npm init

npm WARN saveError ENOENT: no such file or directory 解决

安装完成 node.js 后使用 npm 安装 vue 报错如下:

1
2
3
4
5
6
7
8
9
10
cmd复制代码C:\Users\lxz>npm uninstall vueWcsp
npm WARN saveError ENOENT: no such file or directory, open 'C:\Users\lxz\package.json'
npm WARN enoent ENOENT: no such file or directory, open 'C:\Users\lxz\package.json'
npm WARN lxz No description
npm WARN lxz No repository field.
npm WARN lxz No README data
npm WARN lxz No license field.


up to date in 0.765s

根据错误提示,是系统没有 ‘package.json’ 这个文件导致。这个文件的作用就是管理你本地安装的 npm 包,一个 package.json 文件可以做如下事情:

展示项目所依赖的 npm 包

允许你指定一个包的版本[范围]

让你建立起稳定,意味着你可以更好的与其他开发者共享

此刻我们需要执行命令:

1
cmd复制代码npm init

创建 package.json 文件,系统会提示相关配置,也可以使用命令:

1
cmd复制代码npm init -y

直接创建 package.json 文件,这样创建好处是必填项已经帮你填好,执行完命令后可以看到用户路径下多了一个 package.json 文件。

关于 gitbook 我更多文章:

  1. www.chengweiyang.cn/gitbook/ind…
  2. note.heifahaizei.com/book/
  3. juejin.cn/post/693122…
  4. chrisniael.gitbooks.io/gitbook-doc…
  5. yangjh.oschina.io/gitbook/faq… 有测验功能教程
  6. learn-gitbook.gitbook.io/gitbook/
  7. allen5183.gitbooks.io/gitbook/con… 搭配 5

AI悦创·V:Jiabcdefh

公众号:AI悦创

本文转载自: 掘金

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

【Terraform】Terraform快速了解和安装方法

发表于 2021-10-11

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

本文同时参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

Terraform 作为一个云计算领域IaC工具的一个举足轻重的产品,它是HashiCorp公司的核心产品,这家公司的核心产品一共有四个,包括Nomad、Consul、Vault、Terraform,为人熟知的是Consul。Terraform负责在不同的云平台之上创建出一致的基础设施,并维护管理其整个生命周期的状态。

HashiCorp的产品线主要有Nomad、Consul、Valut以及Terraform,另外还有Vagrant以及Packer两个开源工具,2020年还推出了Boundary以及Waypoint两个新产品。

其官网是这样介绍它的:Terraform是一种开源基础设施即代码软件工具,可提供一致的 CLI 工作流来管理数百个云服务。总体而言,Terraform 是一个安全的,可扩展的,有扎实的理论基础,也有渐进式工程实践的资源编排工具。

Terraform 的关键特性:基础设施即代码、多云编排、执行计划与过程分离、统一的资源状态管理,是我们在新一代资源编排系统实践中的重要保障。

工欲善其事必先利其器,为了体验这个产品,首先我们先来安装它。

对于Ubuntu用户:

1
2
3
csharp复制代码curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add -
sudo apt-add-repository -y "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main"
sudo apt-get update && sudo apt-get install -y terraform

对于CentOS用户(我是基于此安装的,没毛病):

1
2
3
arduino复制代码sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo
sudo yum -y install terraform

对于Mac用户:

1
2
bash复制代码brew tap hashicorp/tap
brew install hashicorp/tap/terraform

对于Windows用户,官方推荐的包管理器是choco,可以去chocolatey.org/ 下载安装好chocolatey后,以管理员身份启动powershell,然后:

1
复制代码choco install terraform

如果只想纯手动安装,那么可以前往Terraform官网下载对应操作系统的可执行文件(Terraform是用go编写的,只有一个可执行文件),解压缩到指定的位置后,配置一下环境变量的PATH,使其包含Terraform所在的目录即可。

安装成功以后,我们验证一下是否安装成功。

图片.png

Terraform早期仅支持使用HCL(Hashicorp Configuration Language)语法的.tf文件,近些年来也开始支持JSON。HashiCorp甚至修改了他们的json解析器,使得他们的json可以支持注释,但HCL相比起JSON来说有着更好的可读性。

这些因为团队的喜好和需求有差异性,我使用JSON是因为用其他代码来生成相应的JSON格式的Terraform代码(比如自研的GUI工具,通过拖拽的方式定义基础设施,继而生成相关代码)是相对契合当前的技术栈,并且假使你当初使用的HCL,也是可以通过Terraform提供的工具转换成json语法格式。

Terraform与以往诸如Ansible等配置管理工具比较大的不同在于,它是根据代码计算出的目标状态与当前状态的差异来计算变更计划的,有兴趣的读者可以在执行terraform apply以后,直接再执行一次terraform apply,看看会发生什么,就能明白他们之间的差异。

本文转载自: 掘金

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

闲扯Maven项目代码组织形式

发表于 2021-10-11

@[toc]
因为最近有小伙伴问到了,所以我想和大家随便扯扯 Maven 项目中代码的组织形式这个问题。

其实也不是啥大问题,但是如果不懂的话,就像雾里看花,始终不能看的明明白白,懂了就像一层窗户纸,捅破就好了。

所以我们就简单扯几句。

  1. 代码组织形式

首先来说说代码组织形式。

一般来说,就两种比较常见的形式:

  • 平铺
  • 父子结构

这两种形式松哥在不同的项目中都有遇到过,所以我们就不说孰优孰劣,单纯来说这两种方案。

1.1 平铺

平铺的代码类似下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
css复制代码├── parent
│ ├── pom.xml
│ └── src
│ ├── main
│ │ ├── java
│ │ └── resources
│ └── test
│ └── java
├── vhr-dao
│ ├── pom.xml
│ ├── src
│ │ ├── main
│ │ │ ├── java
│ │ │ └── resources
│ │ └── test
│ │ └── java
└── vhr-service
├── pom.xml
├── src
│ ├── main
│ │ ├── java
│ │ └── resources
│ └── test
│ └── java

如下图:

可以看到,在这种结构下,parent 父工程和各个子工程从代码组织形式上来看都是平级的,都处于同一个目录下。

不过仔细查看 pom.xml 文件,还是能够清晰的看到这三个 module 的父子关系的:

parent:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.javaboy</groupId>
<artifactId>parent</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>../vhr-dao</module>
<module>../vhr-service</module>
</modules>

</project>

可以看到,在指定 module 时,由于 vhr-dao 和 vhr-service 和 parent 的 pom.xml 不在同一个目录下,所以这里使用了相对路径,相对路径的参考依据是 parent 的 pom.xml 文件位置。

vhr-dao:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>parent</artifactId>
<groupId>org.javaboy</groupId>
<version>1.0-SNAPSHOT</version>
<relativePath>../parent/pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>vhr-dao</artifactId>


</project>

可以看到,relativePath 节点中,通过相对路径指定了 parent 的 pom.xml 文件位置,这个相对路径的参考依据是子模块的 pom.xml 文件。

vhr-service:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>parent</artifactId>
<groupId>org.javaboy</groupId>
<version>1.0-SNAPSHOT</version>
<relativePath>../parent/pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>vhr-service</artifactId>


</project>

这个和 vhr-dao 的差不多,不赘述。

1.2 父子结构

父子结构则类似于下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
css复制代码├── maven_parent
│ ├── pom.xml
│ ├── vhr-dao
│ │ ├── pom.xml
│ │ └── src
│ │ ├── main
│ │ │ ├── java
│ │ │ └── resources
│ │ └── test
│ │ └── java
│ └── vhr-service
│ ├── pom.xml
│ └── src
│ ├── main
│ │ ├── java
│ │ └── resources
│ └── test
│ └── java

如下图:

这种父子结构的看起来就非常的层次分明了,parent 和各个 module 一眼就能看出来,我们从 GitHub 上下载的很多开源项目如 Shiro,都是这种结构。

不过文件夹的层级并不能说明任何问题,关键还是要看 pom.xml 中的定义,接下来我们就来看看 parent 的 pom.xml 和各个子模块的 pom.xml 有何异同。

maven_parent:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.javaboy</groupId>
<artifactId>maven_parent</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>vhr-dao</module>
<module>vhr-service</module>
</modules>


</project>

和前面不同的是,这里声明 modules 不需要相对路径了(其实还是相对路径,只是不需要 ../ 了),因为各个子模块和 parent 的 pom.xml 文件处于同一目录下。

vhr-dao:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>maven_parent</artifactId>
<groupId>org.javaboy</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>vhr-dao</artifactId>


</project>

这里也不需要通过 relativePath 节点去指定 parent 的 pom.xml 文件位置了,因为 parent 的 pom.xml 和各个子模块处于同一目录下。

vhr-service:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>maven_parent</artifactId>
<groupId>org.javaboy</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>vhr-service</artifactId>


</project>
  1. 打包问题

2.1 继承

有的时候,单纯只是想通过 parent 来统一管理不同的项目的依赖,并非一个聚合项目。

这个时候只需要去掉 parent 的 pom.xml 中的 modules 节点及其中的内容即可,这样就不是聚合工程了,各个子模块也可以独立打包。

2.2 聚合

当然很多情况我们是聚合工程。

聚合工程的话,一般松哥是建议大家从 parent 处统一进行打包:

这样可以确保打包到的是最新的代码。

当然还有另外一种操作流程:

  1. 首先将 parent 安装到本地仓库。
  2. 然后分别将 model、dao 以及 service 等模块安装到本地仓库。
  3. 最后 web 模块就可以独立打包了。

如果使用这种操作流程,需要注意一点,就是每个模块代码更新之后,要及时安装到本地仓库,否则当 web 模块独立打包时,用到的其他模块就不是最新的代码。

  1. 小结

好啦,几个 Maven 中的小问题,窗户纸捅破了就豁然开朗啦~

本文转载自: 掘金

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

MySQL高可用,就这么完美???

发表于 2021-10-11

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

MySQL以其容易学习和高可用,被开发人员青睐。它的几乎所有的高可用架构,都直接依赖于 binlog。MySQL 能够成为现下最流行的开源数据库,binlog 功不可没。MySQL是怎样实现高可用的?这种高可用足够完美吗?

主备同步流程

流程

主库为A,备库为B,其同步流程如下图所示,这张图也很好的阐明一条更新语句,在master会执行哪些动作:

图片

备库 B 跟主库 A 之间维持了一个长连接。主库 A 内部有一个线程,专门用于服务备库 B 的这个长连接。一个事务日志同步的完整过程是这样的:

  1. 在备库 B 上通过 change master 命令,设置主库 A 的 IP、端口、用户名、密码,以及要从哪个位置开始请求 binlog,这个位置包含文件名和日志偏移量。
  2. 在备库 B 上执行 start slave 命令,这时候备库会启动两个线程,就是图中的 io_thread和 sql_thread。其中 io_thread 负责与主库建立连接。
  3. 主库 A 校验完用户名、密码后,开始按照备库 B 传过来的位置,从本地读取 binlog,发给 B。
  4. 备库 B 拿到 binlog 后,写到本地文件,称为中转日志(relay log)。
  5. sql_thread 读取中转日志,解析出日志里的命令,并执行。

同步位置

主备切换后,从库需要从新的主库同步数据。即上面流程第一步,需要指定从哪个位置开始请求binlog。主要有两种方案:

基于位点

MySQL5.6之前,使用change master命令更换主库。

1
2
3
4
5
6
7
8
ini复制代码CHANGE MASTER TO 

MASTER_HOST=$host_name
MASTER_PORT=$port
MASTER_USER=$user_name
MASTER_PASSWORD=$password
MASTER_LOG_FILE=$master_log_name
MASTER_LOG_POS=$master_log_pos

操作流程如下:

  1. 等待新主库 A’把中转日志(relay log)全部同步完成;
  2. 在 A’上执行 show master status 命令,得到当前 A’上最新的 File 和 Position;
  3. 取原主库 A 故障的时刻 T;
  4. 用 mysqlbinlog 工具解析 A’的 File,得到 T 时刻的位点。

基于GTID

基于位点的方案太过繁琐,MySQL 5.6 版本引入了 GTID,无需人工计算位点。

GTID 的全称是 Global Transaction Identifier,也就是全局事务 ID,是一个事务在提交的时候生成的,是这个事务的唯一标识。每个 MySQL 实例都维护了一个 GTID 集合,用来对应“这个实例执行过的所有事务”。

1
2
3
4
5
6
ini复制代码CHANGE MASTER TO 
MASTER_HOST=$host_name
MASTER_PORT=$port
MASTER_USER=$user_name
MASTER_PASSWORD=$password
master_auto_position=1

master_auto_position=1 就表示这个主备关系使用的是 GTID 协议。

在实例 B 上执行 start slave 命令,取 binlog 的逻辑如下所示,其中set_a和set_b为执行过的事务的 GTID 集合:

  1. 实例 B 指定主库 A’,基于主备协议建立连接。
  2. 实例 B 把 set_b 发给主库 A’。
  3. 实例 A’算出 set_a 与 set_b 的差集,也就是所有存在于 set_a,但是不存在于 set_b的 GTID 的集合,判断 A’本地是否包含了这个差集需要的所有 binlog 事务。

a. 如果不包含,表示 A’已经把实例 B 需要的 binlog 给删掉了,直接返回错误;

b. 如果确认全部包含,A’从自己的 binlog 文件里面,找出第一个不在 set_b 的事务,发给 B;
4. 之后就从这个事务开始,往后读文件,按顺序取 binlog 发给 B 去执行。

基于GTID的操作,可以认为是系统自行计算出对应位点。

循环问题

参数 log_slave_updates 设置为 on,表示备库执行 relay log 后生成 binlog。在主主复制+主从复制情况下,有时会发现主从没有同步,很可能是因为有的主库没有将log_slave_updates设置为on。

既然消费relay log会生成新的binlog,那双master情况下为何没有产生节点间循环复制情况?

主要是因为MySQL 在 binlog 中记录了这个命令第一次执行时所在实例的server id。

  1. 规定两个库的 server id 必须不同,如果相同,则它们之间不能设定为主备关系;
  2. 一个备库接到 binlog 并在重放的过程中,生成与原 binlog 的 server id 相同的新的binlog;
  3. 每个库在收到从自己的主库发过来的日志后,先判断 server id,如果跟自己的相同,表示这个日志是自己生成的,就直接丢弃这个日志。

高可用(HA)

现在大家都用MySQL,主要是因其高可用。高可用原因有两个,一个是主备一致,一个是主备切换。这两者缺一不可。

  1. 正常情况下,只要主库执行更新生成的所有 binlog,都可以传到备库并被正确地执行,备库就能达到跟主库一致的状态,这就是最终一致性。
  2. 主库出现问题,可以将备库作为主库,继续提供服务。

MySQL 高可用系统的基础,就是主备切换逻辑,但主备切换又很依赖主备延迟。

原因很容易理解,如果备库同步没有完成,此时将备库更改为主库,会产生数据丢失、数据不一致问题。

同步延迟

根据上面提到的主备同步流程,我们能够看出与数据同步有关的时间点主要包括以下三个:

  1. 主库 A 执行完成一个事务,写入 binlog,我们把这个时刻记为 T1;
  2. 之后传给备库 B,我们把备库 B 接收完这个 binlog 的时刻记为 T2;
  3. 备库 B 执行完成这个事务,我们把这个时刻记为 T3。

所谓主备延迟,就是同一个事务,在备库执行完成的时间和主库执行完成的时间之间的差值,也就是 T3-T1。所以主库和备库之间,必然会有延迟。

延迟时间可通过在备库上执行 show slave status 命令查看,它的返回结果里会显示seconds_behind_master,用于表示当前备库延迟了多少秒。DB监控上显示的时间,就是seconds_behind_master。

图片

延迟原因

主备延迟必然会有,但不应该延迟太长。主备延迟长,一般有如下几个原因:

  1. 备库所在机器的性能要比主库所在的机器性能差
  • 备库的机器配置本身就比主库差
  • 主库多机器部署,备库单机部署
  1. 备库的压力大
  • 备库上的查询耗费了大量的 CPU 资源
  1. 主库执行大事务或大表 DDL
  • 语句在主库执行多久,便会导致从库延迟多久
  1. 备库的并行复制能力
  • 备库使用单线程复制还是多线程复制
  • 从MySQL5.7.22开始,可以通过 binlog-transaction dependency-tracking 参数的 COMMIT_ORDER、WRITESET 和 WRITE_SESSION,选择使用哪种并行复制策略

主备切换

下面是双Master之间主备切换流程。一般说双M是指AB之间设置为互为主备,不过任何时刻只有一个节点在接受更新。

主备切换主要有两种策略,可靠性优先和可用性优先策略。

图片

可靠性优先

从状态 1 到状态 2 切换的详细过程是这样的:

  1. 判断备库 B 现在的 seconds_behind_master,如果小于某个值(比如 5 秒)继续下一步,否则持续重试;
  2. 把主库 A 改成只读状态,即把 readonly 设置为 true;
  3. 判断备库 B 的 seconds_behind_master 的值,直到这个值变成 0 为止;
  4. 把备库 B 改成可读写状态,也就是把 readonly 设置为 false;
  5. 把业务请求切到备库 B。

可靠性优先的好处是,主备的数据完全一致后再进行切换,不会引起系统问题。但缺点是系统有段时间不可用。

可用性优先

从状态 1 到状态 2 切换的详细过程是这样的:

  1. 把备库 B 改成可读写状态,也就是把 readonly 设置为 false;
  2. 把业务请求切到备库 B;
  3. 把主库 A 改成只读状态,即把 readonly 设置为 true;

可用性优先的好处是,系统几乎就没有不可用时间,坏处是系统可能出现数据不一致情况。

之所以出现数据不一致,是因为备库B接收业务请求的同时,还会继续消费未完成的binlog日志,新的请求和老的请求之间可能存在冲突。将binlog设置为row能够更加及时的发现这种问题,减少问题的加剧。

异常情况

假设,主库 A 和备库 B 间的主备延迟是 30 分钟,这时候主库 A 掉电了,HA 系统要切换B 作为主库。这时候切和不切都会有问题。

  • 切:有些数据在备库无法查到,而且会产生数据不一致问题
  • 不切:数据库不可使用

这也是为什么说MySQL 高可用系统的可用性,是依赖于主备延迟的。延迟的时间越小,在主库故障的时候,服务恢复需要的时间就越短,可用性就越高。所以无论是对DBA还是对研发人员而言,需要重点关注同步延迟。

问题

主从同步虽然给MySQL带来了高可用,但因为必然存在延迟问题,所以会导致更新完主库后,立即查从库,此时从库并没有更新后的数据。这个问题无法避免,但可以想办法优化,需要大家在付出和收益间做好权衡。

强制走主库方案

  • 查找操作不查从库,查主库

sleep 方案

  • 客户端更新成功后,过一小会再做查找操作

判断主备无延迟方案

  • 每次从库执行查询请求前,先判断seconds_behind_master 是否已经等于 0。如果还不等于 0 ,那就必须等到这个参数变为0 才能执行查询请求。
  • 判断位点:读到的主库的最新位点与备库执行的最新位点做比较,相等即可读
  • 判断GTID:备库收到的所有日志的 GTID 集合与备库所有已经执行完成的 GTID 集合是否一致,一致即可读

配合 semi-sync 方案

  • 一主一备下,半同步复制能确保主备全都收到了更新

等主库位点方案

  • 从库上执行select master_pos_wait(file, pos[, timeout]), 返回值是 >=0 的正整数则查询,其中参数 file 和 pos 指的是主库上的文件名和位置

等 GTID 方案

  • 从库上执行select wait_for_executed_gtid_set(gtid_set, 1),如果返回值是 0,则在这个从库执行查询语句,其中gtid_set是主库事务更新完成后,从返回包直接获取到事务的 GTID

总结

其实MySQL主从同步和开发人员相关性不大,但了解其中的不完美,对于发生异常时进行问题追查是很有帮助的。而且,能够扩展出许多新的玩法,如业务消费binlog日志,实现很多功能。

资料

  1. 主从同步设置的重要参数log_slave_updates
  2. MySQL45讲
  3. MySQL DDL–ghost工具学习

最后

大家如果喜欢我的文章,可以关注我的公众号(程序员麻辣烫)

我的个人博客为:shidawuhen.github.io/

往期文章回顾:

  1. 设计模式
  2. 招聘
  3. 思考
  4. 存储
  5. 算法系列
  6. 读书笔记
  7. 小工具
  8. 架构
  9. 网络
  10. Go语言

本文转载自: 掘金

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

windows中python3使用multiprocessi

发表于 2021-10-11

第一部分:在__main__中声明新进程

例如:

1
2
3
4
5
6
7
8
python复制代码from multiprocessing import Pool

def f(x):
return x*x
pool = Pool(processes=4)
r=pool.map(f, range(100)) 
pool.close() 
pool.join()

在spyder里运行直接没反应;在shell窗口里,直接报错,如下:

1
2
3
4
5
6
7
8
9
10
11
python复制代码Process SpawnPoolWorker-15:
Traceback (most recent call last):
File "C:\Anaconda3\lib\multiprocessing\process.py", line 254, in _bootstr
self.run()
File "C:\Anaconda3\lib\multiprocessing\process.py", line 93, in run
self._target(*self._args, **self._kwargs)
File "C:\Anaconda3\lib\multiprocessing\pool.py", line 108, in worker
task = get()
File "C:\Anaconda3\lib\multiprocessing\queues.py", line 357, in get
return ForkingPickler.loads(res)
AttributeError: Can't get attribute 'f' on <module '__main__' (built-in)>

解决:

Windows下面的multiprocessing跟Linux下面略有不同,

  • Linux下面基于fork,fork之后所有的本地变量都复制一份,因此可以使用任意的全局变量
  • 在Windows下面,多进程是通过启动新进程完成的,所有的全局变量都是重新初始化的,在运行过程中动态生成、修改过的全局变量是不能使用的。

multiprocessing内部使用pickling传递map的参数到不同的进程,当传递一个函数或类时,pickling将函数或者类用所在模块+函数/类名的方式表示,如果对端的Python进程无法在对应的模块中找到相应的函数或者类,就会出错。

当你在Interactive Console当中创建函数的时候,这个函数是动态添加到__main__模块中的,在重新启动的新进程当中不存在,所以会出错。

当不在Console中,而是在独立Python文件中运行时,你会遇到另一个问题:由于你下面调用multiprocessing的代码没有保护,在新进程加载这个模块的时候会重新执行这段代码,创建出新的multiprocessing池,无限调用下去。

解决这个问题的方法是永远把实际执行功能的代码加入到带保护的区域中:if __name__ == '__mian__':

第二部分:补充知识: multiprocessing Pool的异常处理问题

multiprocessing.Pool开发多进程程序时,在某个子进程执行函数使用了mysql-python连接数据库,

由于程序设计问题,没有捕获到所有异常,导致某个异常错误直接抛到Pool中,导致整个Pool挂了,其异常错误如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
python复制代码Exception in thread Thread-3:
Traceback (most recent call last):
 File "/usr/lib64/python2.7/threading.py", line 812, in __bootstrap_inner
 self.run()
 File "/usr/lib64/python2.7/threading.py", line 765, in run
 self.__target(*self.__args, **self.__kwargs)
 File "/usr/lib64/python2.7/multiprocessing/pool.py", line 376, in _handle_results
 task = get()
 File "/usr/lib/python2.7/site-packages/mysql/connector/errors.py", line 194, in __init__
 'msg': self.msg.encode('utf8') if PY2 else self.msg
AttributeError: ("'int' object has no attribute 'encode'", <class 'mysql.connector.errors.Error'>, 
(2055, "2055: Lost Connection to MySQL '192.169.36.189:3306', system error: timed out", None))

本文档基于以上问题对multiprocessing.Pool以及python-mysql-connector的源码实现进行分析,以定位具体的错误原因。解决方法其实很简单,不要让异常抛到Pool里就行。

问题产生场景

python 版本centos7.3自带的2.7.5版本,或者最新的python-2.7.14

mysql-connector库,版本是2.0及以上,可到官网下载最新版:mysql-connector

问题发生的code其实可以简化为如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
python复制代码from multiprocessing import Pool, log_to_stderr
import logging
import mysql.connector

# open multiprocessing lib log
log_to_stderr(level=logging.DEBUG)

def func():
  raise mysql.connector.Error("demo test", 100)

if __name__ == "__main__":
p = Pool(3)
res = p.apply_async(func)
res.get()

所以解决问题很简单,在func里加个try-except就可以了。但是如果你好奇为什么为出现AttributeError的异常,那么可以继续往下看。

Multiprocessing.Pool的实现

通过查看源码,大致上multiprocess.Pool的实现如下图所示:

怎么解决windows中python3使用multiprocessing.Pool时出现的问题

当我们执行以下语句时,主进程会创建三个子线程:_handle_workers、_handle_results、_handle_tasks;同时会创建Pool(n)个数的worker子进程。主进程与各个worker子进程间的通信使用内部定义的Queue,其实就是Pipe管道通信,如上图的_taskqueue、_inqueue和_outqueue。

1
2
3
ini复制代码p = Pool(3)
res = p.apply_async(func)
res.get()

这三个子线程的作用是:

  1. handle_workers线程管理worker进程,使进程池维持Pool(n)个worker进程数;
  2. handle_tasks线程将用户的任务(包括job_id, 处理函数func等信息)传递到_inqueue中,子进程们竞争获取任务,然后运行相关函数,将结果放在_outqueue中,然后继续监听tasksqueue的任务列表。其实就是典型的生产消费问题。
  3. handle_results线程监听_outQqueue的内容,有就拿到,通过字典_cache找到对应的job,将结果存储在*Result对象中,释放该job的信号量,表明job执行完毕。此后,就可以通过*Result.get()函数获取执行结果。

当我们调用p.apply_async 或者p.map时,其实就是创建了AsyncResult或者MapResult对象,然后将task放到_taskqueue中;调用*Result.get()方法等待task被worker子进程执行完成,获取执行结果。

在知道了multprocess.Pool的实现逻辑后,现在我们来探索下,当func将异常抛出时,Pool的worker是怎么处理的。下面的代码是pool.worker工作子进程的核心执行函数的简化版。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
python复制代码def worker(inqueue, outqueue, initializer=None, initargs=(), maxtasks=None):
 ...
 while xxx:
  try:
   task = get()
  except:
   ...

  job, i, func, args, kwds = task
  try:
   result = (True, func(*args, **kwds))
  except Exception, e:
   result = (False, e)
  ...
  try:
   put((job, i, result))
  except Exception, e:
   ...

从代码中可以看到,在执行func时,如果func抛出异常,那么worker会将异常对象直接放入到_outqueue中,然后等待下一个task。也就是说,worker是可以处理异常的。

那么接下来看看_handle_result线程是怎么处理worker发过来的结果的。如下所示:

1
2
3
4
5
6
7
8
python复制代码@staticmethod
def _handle_results(outqueue, get, cache):
 while 1:
  try:
   task = get()
  except (IOError, EOFError):
   return
  ...

上述代码为_handle_result的主要处理逻辑,可以看到,它只对 IOError, EOFError进行了处理,也就是说,如果在get()时发生了其它异常错误,将导致_handle_result这个线程直接退出(而事实上的确如此)。既然_handle_result退出了,那么就没有动作来触发_cache中*Result对象释放信号量,则用户的执行流程就一直处于wait状态。这样,用户主进程就会一直卡在get()中,导致主流程执行不下去。

我们通过打开multiprocessing库的日志(log_to_stderr(level=logging.DEBUG)),然后修改multiprocessing.Pool中_handel_result的代码,加上一个except Exception,然后运行文章一开始的的异常代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
python复制代码# multiprocessing : pool.py
#
class Pool(object):
 @staticmethod
 def _handle_results(outqueue, get, cache):
  while 1:
   try:
    task = get()
   except (IOError, EOFError):
    return
   except Exception:
    debug("handle_result not catch Exceptions.")
    return
  ...

控制台如果输出”handle_result not catch Exceptions.”,表明_handle_results没有catch到所有的异常。而实际上,真的是由于task = get()这句话抛异常了。

那么,_outqueue.get()方法做了什么。深入查看源码,发现get()方法其实就是os.pipe的read/write方法,但是做了一些处理吧。其内部实现大致如下:

1
2
3
4
5
6
python复制代码def Pipe(duplex=True):
 ...
 fd1, fd2 = os.pipe()
 c1 = _multiprocessing.Connection(fd1, writable=False) # get
 c2 = _multiprocessing.Connection(fd2, readable=False) # put
 return c1, c2

_multiprocessing.Connection内部使用了C的实现,就不再深入了,否则会就越来越复杂了。它内部应该使用了pickle库,在put时将对象实例pickle(也就是序列化吧),然后在get时将实例unpikcle,重新生成实例对象。具体可查看python官方文档关于pickle的介绍(包括object可pickle的条件以及在unpickle时调用的方法等)。不管如何,就是实例在get,即unpickle的过程出错了。

‘msg’: self.msg.encode(‘utf8’) if PY2 else self.msg

AttributeError: ‘int’ object has no attribute ‘encode’

从上述错误日志中可以看到,表明在重构时msg参数传入了int类型变量。就是说在unpickle阶段,Mysql Error重新实例化时执行了__init__()方法,但是传参错误了。为了验证这一现象,我将MySql Error的__init__()进行简化,最终确认到self.args的赋值上,即Exception及其子类在unpickle时会调用__init__()方法,并将self.args作为参数列表传递给__init__()。

通过以下代码可以简单的验证问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
python复制代码import os
from multiprocessing import Pipe

class DemoError(Exception):

 def __init__(msg, errno):
  print "msg: %s, errno: %s" % (msg, errno)
  self.args = ("aa", "bb")

def func():
 raise DemoError("demo test", 100)

r, w = Pipe(duplex=False)
try:
 result = (True, func(1))
except Exception, e:
 result = (False, e)

print "send result"
w.send(result)
print "get result"
res = r.recv()
print "finished."

日志会在recv调用时打印 msg: aa, errno: bb,表明recv异常类Exception时会将self.args作为参数传入init()函数中。而Mysql的Error类重写self.args变量,而且顺序不对,导致msg在执行编码时出错。MySql Error的实现简化如下:

1
2
3
4
5
6
7
8
9
10
11
python复制代码class Error(Exception):
 def __init__(self, msg=None, errno=None, values=None, sqlstate=None):
  super(Error, self).__init__()
  ...
  if self.msg and self.errno != -1:
   fields = {
    'errno': self.errno,
    'msg': self.msg.encode('utf-8') if PY2 else self.msg
   }
  ...
  self.args = (self.errno, self._full_msg, self.sqlstate)

可以看到,mysql Error中的self.args与__init__(msg, errno, values, sqlstate)的顺序不一,因此self.args第一个参数errno传给了msg,导致AttributeError。至于self.args是什么,简单查了下,是Exception类中定义的,一般用__str__或者__repr__方法的输出,python官方文档不建议overwrite。

总结

好吧,说了这么多,通过问题的追踪,我们也基本上了解清楚multiprocessing.Pool库的实现了。事实上,也很难说是谁的bug,是两者共同作用下出现的。不管如何,希望在用到multiprocessing库时,特别与Pipe相关时,谨慎点使用,最好的不要让异常跑到multiprocess中处理,应该在func中将所有的异常处理掉,如果有自己定于的异常类,请最好保证self.args的顺序与__init__()的顺序一致。同时,网上好像也听说使用multprocessing和subprocess库出现问题,或许也是这个异常抛出的问题,毕竟suprocessError定义与Exception好像有些区别。

本文转载自: 掘金

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

管理系统开发技(10) 使用mq完成打印二维码功能

发表于 2021-10-11

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

前言

由于前面使用前端页面来进行打印后,效果并不理想,在加载 522 张图片的时候需要 10 秒左右的预览时间。因此,将这打印步骤放入后端完成。再选购打印机后,但该打印机只能连接内网打印,而我们的服务是部署在公网中的,如何让内网的数据消费到公网的数据呢?这时候就需要一个中间件,它帮我们做到这两块内容的消息传送。

本次中间件选用的是 Kafka,它具备超一流的读写性能。在面对大数据量消息传输的时候具备很好的高吞吐率。而我们后续的应用会使用到 kafka ,所以这边我们首先来使用 Kafka 来完成这一功能。而为什么不采用 redis。区别如下:

  • 1、存储方式不同:redis 是存储是内存的,而 Kafka 是存储在硬盘的,redis 的数据可能会丢失,而 Kafka 相对会安全很多,同时,硬盘比内存的成本会小很多。
  • 2、订阅机制不同:Kafka 是专业的消息队列,除了主题之外,不仅可以消费已经消费的数据,还可以进行分区消费和分消费者组。

一、设计概述

1.1 打印时序图

就是两个服务之间的服务调用关系。

613aeff3dfe6d.png

1.2 运维子系统打印业务

打印功能相对比二维码的生成比较简单,只需找到所有的设备,然后将这些设备发送给中间件即可。

分为三种打印情况:

  • 1、单独打印
  • 2、按设备类型批量打印
  • 3、按设备类型组装成目录批量打印

先从数据库中获取动态分区,然后发送给指定的 主题和分区即可。

1
less复制代码kafkaTemplate.send(new ProducerRecord(PRODUCE_TOPIC,devicePrint.getPartition(),null,mapper.writeValueAsString(messageVO)));

然后打印服务只需要对打印失败的 retry 次数 +1 即,接下来就是打印业务的处理了。

image-20211011143243494

二、开发流程

后端

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
80
81
scss复制代码 @KafkaListener(id = "consumer2", topicPartitions = { @org.springframework.kafka.annotation.TopicPartition(topic = "print_consumer_consumerPrintInfo", partitions = { "0" }) },groupId="subStation")
  public void consumer2(String str) {
      try {
          DeviceKafkaMessageVO messageVO = mapper.readValue(str, DeviceKafkaMessageVO.class);
          // 消费失败次数三次以内继续消费
          if(0==messageVO.getRetry()){
              socketApi.sendMsg(SocketDTO
                      .builder()
                      .password(configComponent.getPassword())
                      .topic(ElectricityStationConst.DEVICE_BATCH_PRINT + messageVO.getSysId())
                      .data(messageVO)
                      .build()
              );
              DeviceInfo deviceInfo = new DeviceInfo();
              deviceInfo.setId(messageVO.getId()).setIsPrint(0);
              redisSdk.lLeftAdd(configComponent.getPrintListSuccess(),new ObjectMapper().writeValueAsString(deviceInfo));
          }
          else if(messageVO.getRetry()==3){
              socketApi.sendMsg(SocketDTO
                      .builder()
                      .password(configComponent.getPassword())
                      .topic(ElectricityStationConst.DEVICE_BATCH_PRINT + messageVO.getSysId())
                      .data(messageVO)
                      .build()
              );
              DeviceInfo deviceInfo = new DeviceInfo();
              deviceInfo.setId(messageVO.getId()).setIsPrint(1);
              redisSdk.lLeftAdd(configComponent.getPrintListError(),new ObjectMapper().writeValueAsString(deviceInfo));
              System.out.println("值"+redisSdk.lLen(configComponent.getPrintListError()));
          }
          else{
              kafkaTemplate.send(new ProducerRecord(DeviceServiceImpl.PRODUCE_TOPIC,0,null,mapper.writeValueAsString(messageVO)));
          }
          if(redisSdk.lLen(configComponent.getPrintListError())+redisSdk.lLen(configComponent.getPrintListSuccess())>=messageVO.getTotal()){
              int sus = (int) redisSdk.lLen(configComponent.getPrintListSuccess());
              int errs = (int) redisSdk.lLen(configComponent.getPrintListError());
              // 插入消息通知
              if(errs>0){
                  MessageAccept messageAccept = new MessageAccept();
                  messageAccept.setContent("部分设备打印失败!总执行数:"+(sus+errs)+",失败数:"+(errs));
                  messageAccept.setTitle(SmConst.SM_MESAGE_TYPE_CONST_200.getName());
                  messageAccept.setSysId(41040020001L);
                  messageAccept.setType(SmConst.SM_MESAGE_TYPE_CONST_200.getStatus());
                  messageAccept.setCreateTime(new DateTime());
                  messageAccept.setUpdateTime(new DateTime());
                  messageAccept.setDeleteFlag(0);
                  messageAcceptMapper.insert(messageAccept);
              }
              // 批量更新状态
              while(redisSdk.lLen(configComponent.getPrintListSuccess())>0){
                  ObjectMapper objectMapper = new ObjectMapper();
                  ArrayList<DeviceInfo> successList = new ArrayList<>();
                  List<String> list = redisSdk.pLRightPop(configComponent.getPrintListSuccess(),5000);
                  for (String string : list) {
                      if(StrUtil.isNotEmpty(string)) {
                          DeviceInfo deviceInfo = objectMapper.readValue(string, DeviceInfo.class);
                          successList.add(deviceInfo);
                      }
                  }
                  List<Long> collect = successList.stream().map(e -> e.getId()).collect(Collectors.toList());
                  deviceInfoMapper.updateBatchIsPrintSuccess(collect);
              }
              while(redisSdk.lLen(configComponent.getPrintListError())>0){
                  ObjectMapper objectMapper = new ObjectMapper();
                  ArrayList<DeviceInfo> failList = new ArrayList<>();
                  List<String> list = redisSdk.pLRightPop(configComponent.getPrintListError(),5000);
                  for (String string : list) {
                      if(StrUtil.isNotEmpty(string)) {
                          DeviceInfo deviceInfo = objectMapper.readValue(string, DeviceInfo.class);
                          failList.add(deviceInfo);
                      }
                  }
                  List<Long> collect = failList.stream().map(e -> e.getId()).collect(Collectors.toList());
                  deviceInfoMapper.updateBatchIsPrintFail(collect);
              }
          }
      } catch (Exception e) {
          log.error("消费失败");
          e.printStackTrace();
      }
  }

前端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ini复制代码 //批量打印
  printBatch(row){
    this.$data.printshow=true;
    this.$data.printDialog=true;
    StoreService.setPrintNumber();
    let _this=this;
    console.log(row)
    this.$api.req("/am/device/img/menu/printBatch", {id:row.id},res =>{
      this.$data.loading = false;
    },res=>{
      this.$data.printDialog=false;
      this.$data.printshow=false;
      _this.$data.loading = false;
      _this.$message.error(res.msg);
    })
  },
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
ini复制代码printstomp(){
    var _this=this
    // 监听批量打印
    _this.$bus.$on(TopicConst.DEVICE_BATCH_PRINT+StoreService.getStationId(),data =>{
      var a = JSON.parse(data)
      // 将计数器 +1
      StoreService.incryPrintNumber()
      if(a.retry == 3){
        StoreService.incryPrintErrNumber()
      }
      _this.$data.printshow=true
      _this.$data.printDialog=true
      _this.$data.printvalue=parseInt(Math.round(StoreService.getPrintNumber()/a.total*100))
      if(a.total == StoreService.getPrintNumber()){
        if(StoreService.getPrintErrNumber()>0){
          this.$message.error("打印总数:"+StoreService.getPrintNumber()+",失败个数:"+StoreService.getPrintErrNumber())
          Utils.$emit('dot',true)
        }else {
          this.$message.success("打印完成!")
        }
        setTimeout(function(){
          _this.$data.printshow=false
          _this.$data.printDialog=false
          _this.$data.printvalue=0
        }, 1000);
        StoreService.setPrintNumber(0)
      }
    })
  },

最后,上成品。

693b9ed70312306750f7cc04895d5bb.jpg

本文转载自: 掘金

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

SpringBoot 日志配置和请求TraceId设置

发表于 2021-10-11

摘要:该实例主要是基于slf4j的MDC进行实现tranceId的上下文保存,当然也可以自己定义ThreadLocal进行保存,方式一直,对于跨服务的调用也可以把tranceId放在请求头中,接收方复用该tranceId

基于Filter的实现

拦截所有的请求

1
2
3
4
5
6
7
8
9
10
11
scala复制代码@WebFilter(filterName = "traceIdFilter", urlPatterns = "/*")
@Component
public class TraceFilter extends OncePerRequestFilter {

@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
MDC.put("tranceId", UUID.randomUUID().toString());
filterChain.doFilter(httpServletRequest,httpServletResponse);
}

}

logback.xml配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
xml复制代码<!-- Logback configuration. See http://logback.qos.ch/manual/index.html -->
<configuration>

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%date{HH:mm:ss.SSS} | %X{tranceId} | [%thread] %-5level %logger{36} - %msg%n
</pattern>
</encoder>
</appender>

<logger name="org.springframework.web" level="INFO"/>
<logger name="com.huzhihui" level="INFO"/>
<!-- 指定项目可输出的最低级别日志 -->
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</configuration>

定义控制器

1
2
3
4
5
6
7
8
9
10
11
12
less复制代码@Slf4j
@RestController
public class CommonController {

@RequestMapping(value = "i18n")
public Object i18n(){
log.info("ttts");

log.info("bafgsdaf");
return message;
}
}

image.png

完整应用日志配置

  • application.yml
1
2
3
4
5
6
yaml复制代码spring:
application:
name: env
logging:
file:
path: /var/log/${spring.application.name}
  • logback-spring.xml
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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<configuration>

<include resource="org/springframework/boot/logging/logback/defaults.xml" />
<springProperty name="APPLICATION_NAME" source="spring.application.name" />

<property name="LOG_FILE" value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}}/spring.log}" />
<property name="CONSOLE_LOG_PATTERN" value="[${APPLICATION_NAME}] %clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>
<property name="FILE_LOG_PATTERN" value="[${APPLICATION_NAME}] %d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%t] %-40.40logger{39} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>
<property name="LOG_CHARSET" value="UTF-8"/>

<!-- 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
<charset>${LOG_CHARSET}</charset>
</encoder>
</appender>

<appender name="FILE-LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>${LOG_CHARSET}</charset>
</encoder>
<file>${LOG_FILE}</file>

<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_FILE}.%d{yyyy-MM-dd}.%i</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>20MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>7</maxHistory>
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
</appender>

<!--异步输出到文件-->
<appender name="FILE" class="ch.qos.logback.classic.AsyncAppender">
<!--不丢弃日志 默认的,如果队列的80%已满-->
<discardingThreshold>0</discardingThreshold>
<!--更改默认的队列深度,该值会影响性能,默认256-->
<queueSize>512</queueSize>
<!--添加附加的appender,最多只能添加一个-->
<appender-ref ref="FILE-LOG" />
</appender>

<springProfile name="local">
<root level="INFO">
<appender-ref ref="CONSOLE" />
</root>
</springProfile>

<springProfile name="prod">
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
</root>
</springProfile>

<springProfile name="default">
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
</root>
</springProfile>

<!-- 动态修改日志等级 -->
<jmxConfigurator />

</configuration>

本文转载自: 掘金

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

1…498499500…956

开发者博客

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