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

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


  • 首页

  • 归档

  • 搜索

你真的思考过自己写的代码为啥这么垃圾吗? 1 前言 2 命名

发表于 2021-10-29

1 前言

不一致的代码会给团队的新人造成理解压力,明明大家都是在一个团队做项目:

  • 做同种事情,却有不同种做法
  • 起到类似作用的事物,却有不同名字

大部分程序员对一致性的理解都表现在较宏观方面,比如,数据库访问是叫 DAO还是 Mapper、Repository?
在一个团队内,这应该有统一标准,但编码层面,要求往往就没那么细致。所以,我们经常看到代码出现各种不一致写法。

2 命名不一

看一段代码:

也就是目前的分发渠道,看着没啥问题。但我作为新人就疑惑:

  • WEBSITE 和 KINDLE_ONLY 分别表示什么?
    WEBSITE 表示作品只会在我们自己的网站发布,KINDLE_ONLY 表示这部作品只会在 Kindle 的电子书商店里上架
  • 所以都表示只在一个渠道发布?
    是啊!
  • 既然二者都有只在一个平台上架发布的含义,为什么不都叫 XXX 或 XXX_ONLY?
    好像也是哦……

所以问题就是这里 WEBSITE 和 KINDLE_ONLY 名字不一致。

表示类似含义的代码应该名字一致。比如,很多团队里把业务写到服务层,各种服务的命名也都叫 XXXService。
一旦出现不一致名字,通常都表示不同含义。比如,对于那些非业务入口的业务组件,它们的名字就会不一样,会更符合其具体业务行为,像BookSender ,它表示将作品发送到翻译引擎。

一般枚举值表示的含义应该都有一致业务含义,一旦出现不同,就需要确定不同点到底在哪里,这也就是疑惑的原因。

显然,这段代码的作者给这两个枚举值命名时,只分别考虑了它应该起什么名字,却忽略了这个枚举值在整体所扮角色。

修正代码统一形式:

方案不一致

当一个系统向另外一个系统发送请求时,需要带一个时间戳过去,这里就是把这个时间戳按照一定格式转成了字符串类型,主要就是传输用,便于另外的系统进行识别,也方便在开发过程中进行调试。

这段代码本身的实现是没有问题的。它甚至考虑到了 SimpleDateFormat 这个类本身存在的多线程问题,所以,它每次去创建了一个新的 SimpleDateFormat 对象。

那我为什么还说它是有问题的呢?因为这种写法是 Java 8 之前的写法,而我们用的 Java 版本是 Java 8 之后的。

这个项目里,我们的要求是使用新的日期时间解决方案,而这里的 SimpleDateFormat 和 Date 是旧解决方案的一部分。所以,虽然这段代码本身的实现是没有问题的,然而,放在项目整体中,这却是一个坏味道,因为它没有和其它的部分保持一致。

后来使用新的解决方案:

之所以会这样,因为一个项目中,应对同一个问题出现了多个解决方案,如果没有统一约定,项目成员会根据自己写代码时的感觉随机选择方案,导致方案不一致。

为什么一个项目中会出现多个解决方案?

  • 时间
    时间消逝,技术发展,人们会主动意识到原方案的问题,就会提出新方案,像这里 Java 日期时间的解决方案,就是 JDK 本身随时间演化造成的。有的项目时间比较长,也会出现类似问题。
  • 自身原因引入
    比如,在代码中引入做同一件事情类似的程序库。比如判断字符串是否为空或空串,就有 Guava 和 Apache Commons Lang,都能做同样事情,所以,程序员也会根据自己的熟悉程度选择其中之一来用,造成代码不一致。

这两个程序库是很多程序库的基础,经常因为引入了其它程序库,相应的依赖就出现在我们的代码中。所以,我们必须约定,哪种做法是我们在项目中的标准做法,以防出现各自为战的现象。比如,在我的团队中,我们就选择 Guava 作为基础库,因为相对来说,它的风格更现代,所以,团队就约定类似的操作都以 Guava 为准。

代码不一致

在翻译引擎中创建作品的代码:

  • 首先,根据要处理的作品 ID,获取其中已审核通过的作品
  • 然后,发送一个 HTTP 请求在翻译引擎中创建出这个作品

有什么问题?
这段代码的不一致,这些代码不是一个层次的代码!

首先是获取审核通过的作品,这是一个业务动作,接下来的三行其实是在做一件事,也就是发送创建作品的请求,这三行代码:

  • 创建请求的参数
  • 根据参数创建请求
  • 最后把请求发送出去

三行代码合起来完成了一个发送创建作品请求这么一件事,而这件事才是一个完整的业务动作。

所以,这个函数里的代码并不在一个层次上,有的是业务动作,有的是业务动作的细节。理解到这,把这些业务细节的代码提取到一个函数:

结果上看,原来的函数(createBook)里都是业务动作,而提取出来的函数(createRemoteBook)则都是业务动作的细节,各自语句都在一个层次。

分清代码处于不同层次,基本功还是分离关注点!

一旦分解出不同关注点,还可进一步调整代码的结构。
像前面拆分出来的这个方法,我们已经知道它的作用是发出一个请求去创建作品,本质上并不属于这个业务类的一部分。
所以,还可通过引入一个新模型,将这个部分调整出去:

一说到分层,大多数人想到的只是模型的分层,很少有人会想到在函数的语句中也要分层。各种层次的代码混在一起,许多问题也就随之而来了,最典型莫过长函数。

我们在做的依然是模型分层,只不过,这次的出发点是函数的语句。“分离关注点,越小越好”的意义所在。观察代码的粒度足够小,很多问题自然就会暴露出来。

程序员开始写测试时,有一个典型的问题:如何测试一个私有方法。有人建议用一些特殊能力(比如反射)去测试。
但推荐不要测私有方法。
你之所以想测试私有方法,本质还是分离关注点没做好,把不同层次的代码混淆。前面这段代码,如果要测试前面那个 createRemoteBook 方法还是有一定难度的,但调整之后,引入了 TranslationEngine 这个类,这个方法就变成了一个公开方法,就可以按照一个公开方法去测试了,问题迎刃而解。

很多程序员纠结的技术问题,其实是一个软件设计问题,不要通过奇技淫巧去解决一个本来不应该被解决的问题。

总结

代码中的不一致常常是把不同层次的代码写在了一起,最典型的就是把业务层面的代码和实现细节的代码混在一起。解决这种问题的方式,就是通过提取方法,把不同层次的代码放到不同的函数里,而这一切的前提还是是分离关注点,这个代码问题的背后还是设计问题。

请务必保持代码一致性。

本文转载自: 掘金

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

让Java8的Stream更简单

发表于 2021-10-29

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

相信有很多刚刚入坑程序员的小伙伴被一些代码搞的很头疼,这些代码让我们既感觉到很熟悉,又很陌生的感觉。我们很多刚入行的朋友更习惯于使用for循环或是迭代器去解决一些遍历的问题,但公司里很多老油子喜欢使用Java8新特性Stream流去做,这样可以用更短的代码实现需求,但是对于不熟悉的新手来说,可读性差一些。

  1. 为什么有经验的老手更倾向于使用Stream

  • 性能优势,(大数据量)相较于迭代器,速度更快
  • 支持串行与并行处理,并行处理更能充分利用CPU的资源
  • Stream 是一种计算数据的流,它本身不会存储数据
  • 支持函数式编程
  • 代码优雅,让代码更高效,干净,简洁
  1. Stream 的使用方式

三步操作:

  • 创建Stream
  • 中间操作
  • 终止操作
  1. Stream 的创建

Stream 的 创建都会依赖于数据源,通常是容器或者数组
Stream 流的创建大致分为4中,最为常用的就是通过集合创建

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
java复制代码import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public class CreateStreamDemo {

public static void main(String[] args) {
// 1 通过集合创建Stream也是用的最多的一种形式
List<String> strList = new ArrayList<>();
strList.add("a");
strList.add("b");
strList.add("c");
// 创建串行操作流
Stream<String> stream = strList.stream();
// 创建并行操作流
Stream<String> parallelStream = strList.parallelStream();
// 2 通过数组创建Stream
int[] arr = new int[]{1,2,3};
IntStream intStream = Arrays.stream(arr);
// 3 通过Stream.of
Stream<Integer> integerStream = Stream.of(1,2,3);
Stream<String> stringStream = Stream.of("a","b","c");
// 4 无限流
// 每隔五个数取一个
Stream.iterate(0, t -> t + 5).forEach(System.out::println); // 迭代
Stream.generate(Math::random).forEach(System.out::println); // 生成
}
}
  1. Stream 中间操作

Stream 中间操作,我们最为常用的就是过滤,去重,排序
本章包含我们开发最常用的对对象的去重,和更据对象中的对个属性组合排序

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
java复制代码import com.zhj.java8.bean.Student;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.TreeSet;
import java.util.stream.Stream;

import static java.util.stream.Collectors.collectingAndThen;
import static java.util.stream.Collectors.toCollection;


public class MiddleStreamDemo {

public static void main(String[] args) {

List<Student> students = new ArrayList<>();
students.add(new Student(1,"小华",23,1));
students.add(new Student(1,"小华",23,2));
students.add(new Student(2,"小米",20,2));
students.add(new Student(3,"小果",30,3));
students.add(new Student(4,"小维",18,2));

// 过滤
students.stream().filter(stu -> stu.getAge() > 20).forEach(System.out::println);

// 去重
// 对对象去重是根据引用去重,内容重复并不会去重,除非重写equals和hashCode方法
System.out.println("----------去重----------");
System.out.println("去重1----------");
students.stream().distinct().forEach(System.out::println);
// 对集合中对象某些属性去重,不重写equals和hashCode方法,只能借助其他数据结构来辅助去重
// 单个属性可以stu -> stu.getId()
// 多个属性可以stu -> stu.getId() + ";" + stu.getName()
System.out.println("去重2----------");
ArrayList<Student> distinctList = students.stream().collect(
collectingAndThen(toCollection(() -> new TreeSet<>(Comparator.comparing(stu -> stu.getId() + ";" + stu.getName()))), ArrayList::new)
);
distinctList.stream().forEach(System.out::println);

// 排序 支持定义排序方式
// sorted 默认使用 自然序排序, 其中的元素必须实现Comparable 接口
System.out.println("----------排序----------");
System.out.println("排序1----------");
students.stream().sorted().forEach(System.out::println);
// sorted(Comparator<? super T> comparator) :我们可以使用lambada 来创建一个Comparator 实例。可以按照升序或着降序来排序元素。
System.out.println("排序2----------");
students.stream()
.sorted(Comparator.comparing(Student::getAge,Comparator.reverseOrder())) // ,Comparator.reverseOrder() 逆序
.forEach(System.out::println);
// 创建比较器,通过对比较器内容的定义实现对多个属性进行排序,类似sql中连续的orderBy
System.out.println("排序3----------");
students.stream().sorted(
(s1,s2) -> {
if (s1.getAge() == s2.getAge()) {
return s1.getSex().compareTo(s2.getSex());
} else {
return -s1.getAge().compareTo(s2.getAge());
}
}
).forEach(System.out::println);
System.out.println("排序4----------");
Comparator<Student> studentComparator = (s1,s2) -> {
Integer age1 = s1.getAge();
Integer age2 = s2.getAge();
if (age1 != age2) return age1 - age2;
Integer sex1 = s1.getSex();
Integer sex2 = s2.getSex();
if (sex1 != sex2) return sex2 - sex1;
return 0;
};
students.stream().sorted(studentComparator).forEach(System.out::println);

// 截取 截取前三个元素
System.out.println("----------截取----------");
students.stream().limit(3).forEach(System.out::println);

// 跳过 跳过前3个元素
System.out.println("----------跳过----------");
students.stream().skip(3).forEach(System.out::println);

// 映射
System.out.println("----------映射----------");
System.out.println("映射Map----------");
// map接收Lambda,将元素转换其他形式,或者是提取信息,并将其映射成一个新的元素
Stream<Stream<Student>> streamStream1 = students.stream().map(str -> filterStudent(str));
streamStream1.forEach(sm -> sm.forEach(System.out::println));
System.out.println("映射flatMap----------");
// map接收Lambda,将流中的每一个元素转换成另一个流,然后把所有流连成一个流 扁平化映射
Stream<Student> studentStream2 = students.stream().flatMap(str -> filterStudent(str));
studentStream2.forEach(System.out::println);

// 消费
System.out.println("----------消费----------");
students.stream().peek(stu -> stu.setAge(100)).forEach(System.out::println);
}

public static Stream<Student> filterStudent(Student student) {
student = new Student();
return Stream.of(student);
}
}

Student

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
java复制代码public class Student implements Comparable<Student> {

private Integer id;
private String name;
private Integer age;
private Integer sex;

public Student() {
}

public Student(Integer id, String name, Integer age, Integer sex) {
this.id = id;
this.name = name;
this.age = age;
this.sex = sex;
}

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}

public Integer getSex() {
return sex;
}

public void setSex(Integer sex) {
this.sex = sex;
}

@Override
public String toString() {
return "Student{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", age='" + age + '\'' +
", sex=" + sex +
'}';
}

@Override
public int compareTo(Student o) {
return this.getAge() - o.getAge();
}
}
  1. Stream 终止操作

Stream 的终止操作,最常用的就是讲处理过的数据收集到新的容器中,同时可以实现向Sql聚合函数,分组的一些效果

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
java复制代码package com.zhj.java8.stream;

import com.zhj.java8.bean.Student;

import java.util.*;
import java.util.stream.Collectors;

public class TerminationStreamDemo {

public static void main(String[] args) {
List<Student> students = new ArrayList<>();
students.add(new Student(1,"小华",23,1));
students.add(new Student(2,"小米",20,2));
students.add(new Student(3,"小果",30,3));
students.add(new Student(4,"小维",18,2));
students.add(new Student(5,"小华",23,2));
System.out.println("--------------------匹配聚合操作--------------------");
// allMatch:接收一个 Predicate 函数,当流中每个元素都符合该断言时才返回true,否则返回false
boolean allMatch = students.stream().allMatch(stu -> stu.getAge() > 10);
System.out.println("全部符合大于10岁条件:" + allMatch);
// noneMatch:接收一个 Predicate 函数,当流中每个元素都不符合该断言时才返回true,否则返回false
boolean noneMatch = students.stream().noneMatch(stu -> stu.getAge() > 10);
System.out.println("全部不符合大于10岁条件:" + noneMatch);
// anyMatch:接收一个 Predicate 函数,只要流中有一个元素满足该断言则返回true,否则返回false
boolean anyMatch = students.stream().anyMatch(stu -> stu.getAge() > 20);
System.out.println("含有任意符合大于20岁条件:" + anyMatch);

// findFirst:返回流中第一个元素
Student findFirst = students.stream().findFirst().get();
System.out.println("第一个学生:" + findFirst);
// findAny:返回流中的任意元素
Student findAny = students.stream().findAny().get();
System.out.println("任意一个学生:" + findAny);

// count:返回流中元素的总个数
long count = students.stream().count();
System.out.println("学生总数:" + count);
// max:返回流中元素最大值
Student max = students.stream().max(Student::compareTo).get();
System.out.println("年龄最大学生:" + max);
// max:返回流中元素最大值
Student min = students.stream().min(Student::compareTo).get();
System.out.println("年龄最小学生:" + min);

System.out.println("--------------------规约操作--------------------");
System.out.println("学生年龄总和:" + students.stream().map(Student::getAge).reduce(Integer::sum));
System.out.println("学生年龄最大:" + students.stream().map(Student::getAge).reduce(Integer::max));

System.out.println("--------------------收集操作--------------------");
List<Student> list = students.stream().collect(Collectors.toList());
Set<Student> set = students.stream().collect(Collectors.toSet());
Map<Integer, String> map = students.stream().collect(Collectors.toMap(Student::getId, Student::getName));
String joinName = students.stream().map(Student::getName).collect(Collectors.joining(",", "(", ")"));
// 总数
students.stream().collect(Collectors.counting());
// 最大年龄
students.stream().map(Student::getAge).collect(Collectors.maxBy(Integer::compare)).get();
// 年龄和
students.stream().collect(Collectors.summingInt(Student::getAge));
// 平均年龄
students.stream().collect(Collectors.averagingDouble(Student::getAge));

// 信息合集
DoubleSummaryStatistics statistics = students.stream().collect(Collectors.summarizingDouble(Student::getAge));
System.out.println("count:" + statistics.getCount() + ",max:" + statistics.getMax() + ",sum:" + statistics.getSum() + ",average:" + statistics.getAverage());

// 分组
Map<Integer, List<Student>> collect = students.stream().collect(Collectors.groupingBy(Student::getSex));
System.out.println(collect);
//多重分组,先根据性别分再根据年龄分
Map<Integer, Map<Integer, List<Student>>> typeAgeMap = list.stream().collect(Collectors.groupingBy(Student::getSex, Collectors.groupingBy(Student::getAge)));

//分区
//分成两部分,一部分大于20岁,一部分小于等于20岁
Map<Boolean, List<Student>> partMap = list.stream().collect(Collectors.partitioningBy(v -> v.getAge() > 20));

//规约
Integer allAge = list.stream().map(Student::getAge).collect(Collectors.reducing(Integer::sum)).get();
System.out.println(allAge);
}
}
  1. Stream 特性

  • 中间操作惰性执行

多个中间操作的话,不会多次循环,多个转换操作只会在终止操作的时候融合起来,一次循环完成。

  • 内部迭代
  • 找到符合条件的数据后边的迭代不会进行
  • 流的末端操作只有一次

异常:stream has already been operated upon or closed

意思是流已经被关闭了,这是因为当我们使用末端操作之后,流就被关闭了,无法再次被调用,如果我们想重复调用,只能重新打开一个新的流。

本文转载自: 掘金

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

netty系列之 netty实现http2中的流控制 简介

发表于 2021-10-29

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

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

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

简介

HTTP2相对于http1.1来说一个重要的提升就是流控制flowcontrol。为什么会有流控制呢?这是因为不管是哪种协议,客户端和服务器端在接收数据的时候都有一个缓冲区来临时存储暂时处理不了的数据,但是缓冲区的大小是有限制的,所以有可能会出现缓冲区溢出的情况,比如客户端向服务器端上传一个大的图片,就有可能导致服务器端的缓冲区溢出,从而导致一些额外的数据包丢失。

为了避免缓冲区溢出,各个HTTP协议都提供了一定的解决办法。

在HTTP1.1中,流量的控制依赖的是底层TCP协议,在客户端和服务器端建立连接的时候,会使用系统默认的设置来建立缓冲区。在数据进行通信的时候,会告诉对方它的接收窗口的大小,这个接收窗口就是缓冲区中剩余的可用空间。如果接收窗口大小为零,则说明接收方缓冲区已满,则发送方将不再发送数据,直到客户端清除其内部缓冲区,然后请求恢复数据传输。

HTTP2通过客户端和服务器端的应用中进行缓冲区大小消息的传输,通过在应用层层面控制数据流,所以各个应用端可以自行控制流量的大小,从而实现更高的连接效率。

本文将会介绍netty对http2流控制的支持。

http2中的流控制

在简介中我们也提到了,传统的HTTP1.1使用的是系统底层的流量控制机制,具体来说就是TCP的流控制。但是TCP的流控制在HTTP2中就不够用了。因为HTTP2使用的是多路复用的机制,一个TCP连接可以有多个http2连接。所以对http2来说TCP本身的流控制机制太粗糙了,不够精细。

所以在HTTP2中,实现了更加精细的流控制机制,它允许客户端和服务器实现其自己的数据流和连接级流控制。

具体的流程是这样的,当客户端和服务器端建立连接之后,会发送Http2SettingsFrame,这个settings frame中包含了SETTINGS_INITIAL_WINDOW_SIZE,这个是发送端的窗口大小,用于 Stream 级别流控。流控制窗口的默认值设为65,535字节,但是接收方可以对其进行修改,最大值为2^31-1 字节。

建立好初始windows size之后,对于接收方来说,每次发送方发送data frame就会减少window的的大小,而接收方每次发送WINDOW_UPDATE frame时候就会增加window的大小,从达到动态控制的目的。

netty对http2流控制的封装

Http2FlowController

从上面的介绍我们知道,http2对流控制是通过两个方面来实施的,第一个方面就是初始化的Http2SettingsFrame,通过设置SETTINGS_INITIAL_WINDOW_SIZE来控制初始window的大小。第二个方面就是在后续的WINDOW_UPDATE frame中对window的大小进行动态增减。

对于netty来说,这一切都是封装在Http2FlowController类中的。Http2FlowController是一个抽象类,它有两个实现,分别是Http2LocalFlowController和Http2RemoteFlowController。他们分别表示对inbound flow of DATA 和 outbound flow of DATA的处理。

Http2FlowController中主要有5个方法,分别是:

  • set channelHandlerContext:绑定flowcontrol到ChannelHandlerContext上。
  • set initialWindowSize:初始化window size,等同于设置SETTINGS_INITIAL_WINDOW_SIZE。
  • get initialWindowSize: 返回初始化window size。
  • windowSize: 获取当前的windowSize。
  • incrementWindowSize: 增加flow control window的大小。

接下来我们看下他的两个实现类,有什么不一样的地方。

Http2LocalFlowController

LocalFlowController用来对远程节点发过来的DATA frames做flow control。它有5个主要的方法。

  • set frameWriter: 用来设置发送WINDOW_UPDATE frames的frame writer。
  • receiveFlowControlledFrame: 接收inbound DATA frame,并且对其进行flow control。
  • consumeBytes: 表示应用已经消费了一定数目的bytes,可以接受更多从远程节点发过来的数据。flow control可以发送 WINDOW_UPDATE frame来重置window大小。
  • unconsumedBytes: 接收到,但是未消费的bytes。
  • initialWindowSize: 给定stream的初始window大小。

Http2RemoteFlowController

remoteFlowController用来处理发送给远程节点的outbound DATA frames。它提供了8个方法:

  • get channelHandlerContext: 获取当前flow control的context.
  • addFlowControlled: 将flow control payload添加到发送到远程节点的queue中。
  • hasFlowControlled: 判断当前stream是否有 FlowControlled frames在queue中。
  • writePendingBytes: 将流量控制器中的所有待处理数据写入流量控制限制。
  • listener: 给 flow-controller添加listener。
  • isWritable: 确定流是否有剩余字节可用于流控制窗口。
  • channelWritabilityChanged: context的writable状态是否变化。
  • updateDependencyTree: 更新stream之间的依赖关系,因为stream是可以有父子结构的。

流控制的使用

flowControl相关的类主要被用在Http2Connection,Http2ConnectionDecoder,Http2ConnectionEncoder中,在建立http2连接的时候起到相应的作用。

总结

flowControl是http2中的一个比较底层的概念,大家在深入了解netty的http2实现中应该会遇到。

本文已收录于 www.flydean.com/29-netty-fl…

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!

本文转载自: 掘金

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

你是使用什么工具调试 golang 程序的?

发表于 2021-10-29

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

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

写过 C/C++ 的都是到,调试程序的时候通常使用 gdb 工具来进行调试,用起来可爽了,那么 gdb 是否也适合 golang 程序的调试的

我个人到是通常使用 dlv 来进行 golang 程序的调试,分享一波

dlv 是什么,全称 Delve

Delve 可以让你通过控制程序的执行来与程序进行交互,他可以计算变量,并提供线程 / goroutine 状态、CPU 寄存器状态等信息

Delve 的目标是为调试 Go 程序提供一个简单强大的调试功能

尝试看一下 dlv 的 help 信息

image-20211029105238415

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
shell复制代码Usage:
dlv [command]

Available Commands:
attach Attach to running process and begin debugging.
connect Connect to a headless debug server.
core Examine a core dump.
dap [EXPERIMENTAL] Starts a headless TCP server communicating via Debug Adaptor Protocol (DAP).
debug Compile and begin debugging main package in current directory, or the package specified.
exec Execute a precompiled binary, and begin a debug session.
help Help about any command
run Deprecated command. Use 'debug' instead.
test Compile test binary and begin debugging program.
trace Compile and begin tracing program.
version Prints version.

通过 help 我们可以看到可以使用这些命令来调试我们的程序,根据不同的应用场景

例如,

我们直接编译并调试的时候就可以使用 dlv debug

调试一个正在运行的程序,就可以使用 dlv attach

调试一个编译好的二进制文件,可以使用 dlv exec

其他的使用方式也类似,看上述的英文大概就知道啥意思了

开始调试小程序

简单写一个小程序来应用一下这个调试工具

1
2
3
4
5
6
7
8
9
10
11
go复制代码const NUM = 10

func main() {
arr := make([]int, NUM)

for i := 1; i < NUM; i++ {
arr[i] = i + i
fmt.Printf("arr[%d] == %d\n", i, arr[i])
}
fmt.Println("program over")
}

1、使用 dlv debug main.go 开始调试

1
2
3
shell复制代码>dlv debug main.go
Type 'help' for list of commands.
(dlv)

2、dlv 里面使用 help 查看一下可以使用哪些命令

这些命令对应的解释相对还是比较清楚的,我们可以来用一下

3、使用 break 打给 main 函数一个端点,或者是用 b

1
2
shell复制代码(dlv) b main.main
Breakpoint 1 set at 0xef84ea for main.main() d:/mycode/my_new_first/dlvtest/main.go:7

给 main 函数打 1 断点,断点号是 1

4、continue 继续执行代码,直到运行到 main 的中断

1
2
3
4
5
6
7
8
9
10
11
12
13
shell复制代码(dlv) c
> main.main() d:/mycode/my_new_first/dlvtest/main.go:7 (hits goroutine(1):1 total:1) (PC: 0xef84ea)
2:
3: import "fmt"
4:
5: const NUM = 10
6:
=> 7: func main() {
8: arr := make([]int, NUM)
9:
10: for i := 1; i < NUM; i++ {
11: arr[i] = i + i
12: fmt.Printf("arr[%d] == %d\n", i, arr[i])

5、再 打一个断点,加上具体的条件

  • b 文件:行数
  • condition 中断号 具体的条件
1
2
3
shell复制代码(dlv) b main.go:12
Breakpoint 2 set at 0xef85a4 for main.main() d:/mycode/my_new_first/dlvtest/main.go:12
(dlv) condition 2 i==7

6、continue 继续执行代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
shell复制代码(dlv) c
arr[1] == 2
arr[2] == 4
arr[3] == 6
arr[4] == 8
arr[5] == 10
arr[6] == 12
> main.main() d:/mycode/my_new_first/dlvtest/main.go:12 (hits goroutine(1):1 total:1) (PC: 0xef85a4)
7: func main() {
8: arr := make([]int, NUM)
9:
10: for i := 1; i < NUM; i++ {
11: arr[i] = i + i
=> 12: fmt.Printf("arr[%d] == %d\n", i, arr[i])
13: }
14: fmt.Println("program over")
15: }

7、locals 查看本地变量信息 , p/print 打印变量信息

1
2
3
4
5
6
7
8
9
shell复制代码(dlv) locals
arr = []int len: 10, cap: 10, [...]
i = 7
(dlv) args
(no args)
(dlv) p i
7
(dlv) p arr
[]int len: 10, cap: 10, [0,2,4,6,8,10,12,14,0,0]
  • 使用 p 查看多个变量的信息,打印出具体的值

8、bp/breakpoints 查看中断列表 , clear 清空中断

  • 查看中断列表
  • 清空其中一个中断
  • 再查看中断列表看看效果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
shell复制代码(dlv) bp
Breakpoint runtime-fatal-throw (enabled) at 0xe6ca00 for runtime.throw() c:/program files/go/src/runtime/panic.go:1107 (0)
Breakpoint unrecovered-panic (enabled) at 0xe6cc80 for runtime.fatalpanic() c:/program files/go/src/runtime/panic.go:1190 (0)
print runtime.curg._panic.arg
Breakpoint 1 (enabled) at 0xef84ea for main.main() d:/mycode/my_new_first/dlvtest/main.go:7 (1)
Breakpoint 2 (enabled) at 0xef85a4 for main.main() d:/mycode/my_new_first/dlvtest/main.go:12 (1)
cond i == 7
(dlv) clear 2
Breakpoint 2 cleared at 0xef85a4 for main.main() d:/mycode/my_new_first/dlvtest/main.go:12
(dlv) bp
Breakpoint runtime-fatal-throw (enabled) at 0xe6ca00 for runtime.throw() c:/program files/go/src/runtime/panic.go:1107 (0)
Breakpoint unrecovered-panic (enabled) at 0xe6cc80 for runtime.fatalpanic() c:/program files/go/src/runtime/panic.go:1190 (0)
print runtime.curg._panic.arg
Breakpoint 1 (enabled) at 0xef84ea for main.main() d:/mycode/my_new_first/dlvtest/main.go:7 (1)

9、ls 查看当前代码运行的位置 ,next 执行源码的下一行

1
2
3
4
5
6
7
8
9
10
11
shell复制代码(dlv) ls
> main.main() d:/mycode/my_new_first/dlvtest/main.go:12 (hits total:0) (PC: 0xef85a4)
7: func main() {
8: arr := make([]int, NUM)
9:
10: for i := 1; i < NUM; i++ {
11: arr[i] = i + i
=> 12: fmt.Printf("arr[%d] == %d\n", i, arr[i])
13: }
14: fmt.Println("program over")
15: }
1
2
3
4
5
6
7
8
9
10
11
shell复制代码(dlv) next
> main.main() d:/mycode/my_new_first/dlvtest/main.go:12 (hits total:0) (PC: 0xef85a4)
7: func main() {
8: arr := make([]int, NUM)
9:
10: for i := 1; i < NUM; i++ {
11: arr[i] = i + i
12: fmt.Printf("arr[%d] == %d\n", i, arr[i])
=> 13: }
14: fmt.Println("program over")
15: }

通过箭头我们就可以看出来 ,没有毛病

10、bt/stack 打印堆栈信息

1
2
3
4
5
6
7
shell复制代码(dlv) bt
0 0x0000000000ef85a4 in main.main
at d:/mycode/my_new_first/dlvtest/main.go:12
1 0x0000000000e6f2f6 in runtime.main
at c:/program files/go/src/runtime/proc.go:225
2 0x0000000000e9f3a1 in runtime.goexit
at c:/program files/go/src/runtime/asm_amd64.s:1371

查看堆栈信息,可以直接看到汇编里面的具体信息

11、goroutines 查看程序中的协程列表,以及其对应的代码行数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
shell复制代码(dlv) goroutines
* Goroutine 1 - User: d:/mycode/my_new_first/dlvtest/main.go:12 main.main (0xef85a4) (thread 26592)
Goroutine 2 - User: c:/program files/go/src/runtime/proc.go:337 runtime.gopark (0xe6f6f6) [force gc (idle)]
Goroutine 3 - User: c:/program files/go/src/runtime/proc.go:337 runtime.gopark (0xe6f6f6) [GC sweep wait]
Goroutine 4 - User: c:/program files/go/src/runtime/proc.go:337 runtime.gopark (0xe6f6f6) [GC scavenge wait]
Goroutine 5 - User: c:/program files/go/src/runtime/proc.go:337 runtime.gopark (0xe6f6f6) [finalizer wait]
[5 goroutines]
(dlv) goroutine
Thread 26592 at d:/mycode/my_new_first/dlvtest/main.go:12
Goroutine 1:
Runtime: d:/mycode/my_new_first/dlvtest/main.go:12 main.main (0xef85a4)
User: d:/mycode/my_new_first/dlvtest/main.go:12 main.main (0xef85a4)
Go: c:/program files/go/src/runtime/asm_amd64.s:226 runtime.rt0_go (0xe9d3cc)
Start: c:/program files/go/src/runtime/proc.go:115 runtime.main (0xe6f0e0)

goroutine 执行的时候默认是查看当前协程的信息,上面打印可以知道,总共有 5 个协程,当前打印的协程信息是第 1 个

12、goroutine 显示当前当前协程的具体信息和切换协程

主动切换到 第 2 个协程,并查看当前协程的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
shell复制代码(dlv) goroutine 2
Switched from 1 to 2 (thread 26592)
(dlv) goroutine
Thread 26592 at d:/mycode/my_new_first/dlvtest/main.go:12
Goroutine 2:
Runtime: c:/program files/go/src/runtime/proc.go:337 runtime.gopark (0xe6f6f6)
User: c:/program files/go/src/runtime/proc.go:337 runtime.gopark (0xe6f6f6)
Go: c:/program files/go/src/runtime/proc.go:264 runtime.init.6 (0xe6f47c)
Start: c:/program files/go/src/runtime/proc.go:267 runtime.forcegchelper (0xe6f4a0)
(dlv) goroutines
Goroutine 1 - User: d:/mycode/my_new_first/dlvtest/main.go:12 main.main (0xef85a4) (thread 26592)
* Goroutine 2 - User: c:/program files/go/src/runtime/proc.go:337 runtime.gopark (0xe6f6f6) [force gc (idle)]
Goroutine 3 - User: c:/program files/go/src/runtime/proc.go:337 runtime.gopark (0xe6f6f6) [GC sweep wait]
Goroutine 4 - User: c:/program files/go/src/runtime/proc.go:337 runtime.gopark (0xe6f6f6) [GC scavenge wait]
Goroutine 5 - User: c:/program files/go/src/runtime/proc.go:337 runtime.gopark (0xe6f6f6) [finalizer wait]
[5 goroutines]

工具需要用起来才有意义

好了,本次就到这里

技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。

我是小魔童哪吒,欢迎点赞关注收藏,下次见~

本文转载自: 掘金

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

从零开始学MYSQL - MYSQL安装 从零开始学MYSQ

发表于 2021-10-29

从零开始学MYSQL - MYSQL安装

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

前言

​ 这个专栏也可以认为是学习笔记,由于之前的专栏学习的是网络上的培训机构教程,学习完成之后发现虽然讲到一些有一些深入的东西,但是讲的都不是特别深,所以从这一节开始将会从零开始来全盘了解MYSQL,这里找了一本书《从根上理解Mysql》,个人也十分推荐读者去看看这边书,不仅有新特性对接讲解,也有很多的干货,同时讲的也十分好,作为支持个人后面也买了一本实体书(虽然基本都是拿pdf看的)。

思维导图(持续更新)

www.mubucm.com/doc/7DDOY0C…

图片地址:p3-juejin.byteimg.com/tos-cn-i-k3…

参考资料:

  1. 英文mysql5.7官方文档:dev.mysql.com/doc/refman/…
  2. 中文对应翻译网站(机翻):www.docs4dev.com/docs/zh/mys…

概述

  1. 认识mysql的客户端和服务端是怎么一回事
  2. 了解安装mysql的注意事项,以及回顾mysql个人
  3. 简要介绍关于mysql启动的常见四个命令以及具体的作用
    1. mysqld
    2. mysqld_safe
    3. mysql.server
    4. mysqld_multi

认识客户端和服务端

​ 由于是Mysql的专栏,这里就不牵扯什么TCP/IP,什么网络传输协议了,总之我们只需要了解mysql是分为客户端和服务端的,通常我们访问页面或者浏览数据就是一次数据库的访问过程(当然现在多数东西都静态化了),所以连接的这一方被称为客户端而接受请求的这一方面被称为服务端.

mysql的基本任务

通常我们使用MYSQL基本都是干这些事情:

  1. 连接数据库。
  2. 查询数据库的数据,客户端发送请求给服务端,服务端根据命令找到数据回送给客户端。
  3. 和数据库断开连接。

mysql实例

​ 说完了上面的废话之后,我们来说下mysql实例,实例也在操作系统的层面叫做进程,而进程可以看做是处理器,内存,IO设备的抽象,我们不需要知道这个进程底层是如何传输数据存储数据的,我们只需要了解他需要一个端口,并且每一个实例都有一个 进程ID的东西,在数据库实例运行的时候系统会分配一个进程ID给它并且保证唯一,而每一个进程都有自己的名字,这个名称是安装的时候由程序员自己设置的,但是如果没有分配则会使用MYSQL自己默认设置的名称。

我们启动的 MySQL 服务器进程的默认名称为 mysqld , 而我们常用的 MySQL 客户端进程的默认名称为 mysql 。

从这个名称我们也可以推测出为什么我们启动一个服务通常会使用Mysqld,而我们连接数据库通常使用mysql。

  • 每一个文件就是对于IO设备的抽象
  • 虚拟内存是对内存和IO设备的抽象
  • 进程:则是对处理器,虚拟内存和IO设备的抽象

安装Mysql的注意事项

​ 安装Mysql十分简单但是实际上如果全手动安装细节还是比较多的,通常情况下我们自己使用直接用EXE程序或者直接使用BIN包等,但很多时候对于Linux的软件很多人都会推荐使用 源码安装,源码安装的好处不仅仅是缩小体积,经过不少的实验证明源码的安装方式效率会有所提升,所以正式环境下 尽可能使用源码安装,最后需要注意的一点是:Linux下使用RPM包会有单独的服务器和客户端RPM包,需要分别安装。

安装目录位置的区别

​ 下面是具体的Mysql安装目录,当然下面这里只做参考,个人mac电脑使用的是brew install mysql加上m1的的芯片安装的,适配性未知,所以为了保证笔记的可靠,这里用回了windows系统来进行实际测试和演练,下面是不同的操作系统在mysql的安装目录存储位置存在细微的不同,但是一定要十分清楚mysql真实的安装位置,这对于自己捣鼓各种命令以及设置参数很重要。

1
2
3
4
java复制代码macOS 操作系统上的安装目录:
/usr/local/mysql/
Windows 操作系统上的安装目录:
C:\Program Files\MySQL\MySQL Server 5.7

Mysql安装

windows安装过程

​ 安装过程就不演示了,网上的教程一抓一大把,为了稳妥起见这里个人使用的mysql版本是5.7的版本,同时使用了默认exe程序安装,如果你使用了mysql-installxx.exe安装,有的时候会出现下面的命令:

1
arduino复制代码'mysql' 不是内部或外部命令,也不是可运行的程序

​ 看到这个提示之后,第一反应是进入power shell的管理员模式:

1
2
3
4
5
6
7
8
yaml复制代码PS C:\Windows\system32> mysql -uroot -p
mysql : 无法将“mysql”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写,如果包括路径,请确保路径正
确,然后再试一次。
所在位置 行:1 字符: 1
+ mysql -uroot -p
+ ~~~~~
+ CategoryInfo : ObjectNotFound: (mysql:String) [], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException

​ 发现还是报错,然后我跑去服务看了下mysql是否有启动,发现mysql又是启动的,这里有点奇怪

​ 然后这里找了下网络上的解决办法,其实加个环境变量就行了,然后使用power shell直接安装即可,最后我们照常输入命令就可以发现mysql正常安装完毕了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
vbnet复制代码PS C:\Windows\system32> mysql -uroot -pxxxxxx
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 3
Server version: 5.7.35-log MySQL Community Server (GPL)

Copyright (c) 2000, 2021, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>

关于绝对路径和相对路径启动问题:

绝对路径:如果你的系统环境变量里面访问不到你的应用程序命令,这时候就需要进入到相关的目录执行命令,比如上面我没有配置环境变量就需要进入到C:\Program Files\MySQL\MySQL Server 5.7\bin目录下进行操作,也可以正常使用mysql,但是每次这样弄很麻烦,所以基本是个正常人都会使用环境变量,如果你不知道环境变量是什么,额。。。。请自行百度

相对路径:配置完环境变量之后,我们敲命令会根据系统环境变量配置的 先后顺序找到我们的命令并且执行,但是这点在mysql有点特别,后续会讲到如果多个系统参数配置会默认使用 最后读到的配置参数为准。

​

macos安装过程

​ Mac本子个人也是24分期才敢碰的神物,我相信用的人也不多,所以这里直接放个帖子:

​ www.cnblogs.com/nickchen121…

Linux安装过程

​ 由于个人使用云服务器搭建mysql比较多,这里提供了一个阿里云rpm包的安装方式,版本是centeros7,centeros6同样可以使用,不过需要修改部分命令。

​ juejin.cn/post/689525…

Mysql启动:

​ 多数情况我们使用mysql.sever启动即可,因为它会间接的调用其他的几个命令,而mysqld_muti这个命令更建议自己实战的时候进行配置的学习使用,更加事半功倍。

mysqld

​ Mysqld:代表的是mysql的服务器程序,运行就可以启动一个服务器的进程,但是不常用。

​ 个人在实践之后使用了mysqld命令之后,发现运行的结果如下,起初比较莫名其妙的问题,但是看日志不难发现问题,其实就是 目录不存在并且mysql又没法给你创建目录,只要使用everything找到对应点文件即可(mac为什么没有这么好用的软件,哎)。

​ 你可以通过找到下面的my.ini文件并且修改里面关于datadir的路径即可。

​ 通过打开这个文件发现配置路径里面有一个/Data,然后发现目录里面没有这路径:

1
2
ini复制代码# Path to the database root
datadir=C:/ProgramData/MySQL/MySQL Server 5.7/Data

​ 下面是上面描述的日志的运行结果,感兴趣的可以自己试一试,也可能遇不到我这种问题

1
2
3
4
5
6
7
8
9
10
11
12
vbnet复制代码PS C:\Windows\system32> mysqld -datadir=D:\soft\mysqltest
mysqld: Can't change dir to 'C:\Program Files\MySQL\MySQL Server 5.7\data\' (Errcode: 2 - No such file or directory)
2021-10-28T14:20:14.063607Z 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details).
2021-10-28T14:20:14.063669Z 0 [Note] --secure-file-priv is set to NULL. Operations related to importing and exporting data are disabled
2021-10-28T14:20:14.064027Z 0 [Note] C:\Program Files\MySQL\MySQL Server 5.7\bin\mysqld.exe (mysqld 5.7.35) starting as process 3296 ...
2021-10-28T14:20:14.066088Z 0 [Warning] Can't create test file C:\Program Files\MySQL\MySQL Server 5.7\data\DESKTOP-L8AD9HM.lower-test
2021-10-28T14:20:14.066416Z 0 [Warning] Can't create test file C:\Program Files\MySQL\MySQL Server 5.7\data\DESKTOP-L8AD9HM.lower-test
2021-10-28T14:20:14.067090Z 0 [ERROR] failed to set datadir to C:\Program Files\MySQL\MySQL Server 5.7\data\
2021-10-28T14:20:14.067408Z 0 [ERROR] Aborting

2021-10-28T14:20:14.067619Z 0 [Note] Binlog end
2021-10-28T14:20:14.067904Z 0 [Note] C:\Program Files\MySQL\MySQL Server 5.7\bin\mysqld.exe: Shutdown complete

​ 最后你可以运行mysqld启动一个服务器进程并且在对应的目录下面构建了对应的文件。

答案的灵感来自于下面的部分:

What I did (Windows 10) for a new installation:

  1. Start cmd in admin mode (run as administrator by hitting windows key, typing cmd, right clicking on it and selecting “Run as Administrator”
  2. Change into “MySQL Server X.Y” directory (for me the full path is C:\Program Files\MySQL\MySQL Server 5.7”)
  3. using notepad create a my.ini with a mysqld section that points at your data directory
1
2
3
4
> sql复制代码[mysqld]
> datadir="X:\Your Directory Path and Name"
>
>
  1. created the directory identified in my.ini above.
  2. change into bin Directory under server directory and execute: mysqld --initialize
  3. Once complete, started the service and it came up fine.

mysqld_safe

​ mysqld_safe 是一个启动脚本,在间接的调用mysqld ,同时监控进程,使用 mysqld_safe 启动服务器程序时,会通过监控把出错的内容和出错的信息重定向到一某个文件里面产生出错日志,这样可以方便我们找出发生错误的原因。

​ 但是个人实践之后找不到,其实原因是windows没有这个命令的,关于更多mysqld_safe命令的解释可以看看mysql的官方网站:dev.mysql.com/doc/refman/…

如果阅读英文有困难,这里有一个中文的翻译网站:www.docs4dev.com/docs/zh/mys…

对于一些Linux 平台,使用 RPM 或 Debian 软件包安装的 MySQL 包括对 ManagementMySQL 服务器启动和关闭的系统支持。在这些平台上可能被认为没有必要所有没有安装mysql.server和mysqld_safe。

mysql.server

​ 这个文件同样也是一个启动脚本,也是最常用的脚本,实际上这个命令可以看做是一个链接,也就是一个“快捷方式”,实际指向的路径为: ../support-files/mysql.server,另外这个命令会间接的调用mysqld_safe,我们使用下面的命令就可以直接启动服务:

1
sql复制代码mysql.server start

如果操作系统在安装之后没有构建相应的链接文件,可能需要自己手动构建一个链接文件,另外,linux服务器需要注意权限的问题,因为有时候没有root权限可能需要对于对应的目录配置用户组,下马是关于官网的介绍

如果从源分发版或使用不自动安装mysql.server的二进制分发版格式安装 MySQL,则 可以手动安装脚本。它可以 support-files在 MySQL 安装目录下的目录或 MySQL 源代码树中找到。将脚本复制到/etc/init.d名为mysql的目录并使其可执行:

1
2
3
4
> shell复制代码shell> cp mysql.server /etc/init.d/mysql
> shell> chmod +x /etc/init.d/mysql
>
>

​ 最后启动和关闭mysql可以使用如下的方式(linux系统):

1
2
MYSQL复制代码mysql.server start
mysql.server stop

​ 如果是windows系统,使用上面的命令会报错,所以我们使用下面的命令即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mysql复制代码PS C:\Windows\system32> mysql.server start
mysql.server : 无法将“mysql.server”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写,如果包括路径
,请确保路径正确,然后再试一次。
所在位置 行:1 字符: 1
+ mysql.server start
+ ~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (mysql.server:String) [], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException

PS C:\Windows\system32> net start mysql
服务名无效。

请键入 NET HELPMSG 2185 以获得更多的帮助。

PS C:\Windows\system32> net start mysql57
请求的服务已经启动。

mysqld_multi

​ 有的时候我们可能会想要在一台的机器上使用多个服务器的进程,这个命令的作用是对于每一个服务器进程进行启动或者停止监控,但是由于这个命令较为复杂,个人还是建议使用上面的官方稳定链接进行具体的细节了解。

如果阅读英文有困难,这里有一个中文的翻译网站:www.docs4dev.com/docs/zh/mys…

window&服务启动

​ 这个简单了解一下即可,window端的mysql基本是为了照顾windows的用户才出现的,真正能施展拳脚的地方还是linux,当然有些公司确实会使用window作为服务器。。。。。所以还是过一下,下面是安装一个windows的服务的命令:

1
css复制代码"完整的可执行文件路径" --install [-manual] [服务名]

其中的 -manual 可以省略,区别在于加上会关闭 自动启动改为手动启动

​ 最后下面是个人的mysqld服务安装命令,请读者根据自己的系统环境自行安装即可。

1
arduino复制代码C:\Program Files\MySQL\MySQL Server 5.7\bin\mysqld --install

​ 安装之后使用net start mysql和net stop mysql命令即可启动或者关闭。

Mysql连接

​ 这里只有一个需要注意一下的点那就是对于命令格式来说,如果使用-u、-p等参数的时候使用一个短划线,但是如果使用–username、–password等要使用双划线的形式。

总结

​ 本节内容非常简单,介绍了关于mysql的安装过程的踩坑和四个常见的启动命令,其实我们重点只需要掌握一个命令即可,同时对于部分命令更加建议自己使用的时候边学边记录可以更好的消化和吸收。

​ 以上就是笔者边学习边踩坑的记录,最后发现最好的教程还是官方文档,另外遇到问题也不要慌,先在自己脑海中大胆的猜测问题点,进行验证之后反复重试,踩坑多了之后自然会熟悉。

写在最后

​ 本文算是对于专栏的重新编写,后续会对之前的学习内容做一个复盘和总结。

本文转载自: 掘金

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

YAPI 从0搭建API文档管理工具 背景 YAPI简介 依

发表于 2021-10-29

背景

最近在找一款API文档管理工具,之前有用过Swagger、API Manager、Confluence,现在用的还是Confluence。

我个人一直不喜欢用Swagger,感觉“代码即文档”,让代码里的文档无处不在,已经对代码造成了一定的入侵了。API Manager就是一个纯API文档管理的工具了。Confluence是万能的,也是最简单的,支持各种插件在线安装,可以有各种布局,支持MD文档,也支持表格、代码块等。

最近看到一篇文章在说YAPI,就准备搭建一个试试效果如何。

YAPI简介

YAPI是去哪儿网开源的一款API管理工具,理念如下:

YApi让接口开发更简单高效,让接口的管理更具可读性、可维护性,让团队协作更合理。

特性:

基于 Json5 和 Mockjs 定义接口返回数据的结构和文档,效率提升多倍

扁平化权限设计,即保证了大型企业级项目的管理,又保证了易用性

类似 postman 的接口调试

自动化测试, 支持对 Response 断言

MockServer 除支持普通的随机 mock 外,还增加了 Mock 期望功能,根据设置的请求过滤规则,返回期望数据

支持 postman, har, swagger 数据导入

免费开源,内网部署,信息再也不怕泄露了

选择YAPI试试手的原因是因为我看到了它支持MockServer,这样前端开发同学就不用等待后端同学了。主要是我也懒得搭建一套mock服务,有这样一款工具何乐而不为呢?所以今天就找了一台服务器安装了一下。考虑排版问题,就以图片形式放出来了。

依赖环境

  • 系统版本:Linux CentOS 7.4
  • nodeJS
  • MongoDB
  • Git

安装步骤

nodeJs安装

nodeJS长期支持版本官网下载地址:

nodejs.org/dist/v10.16…

YAPI:从0搭建API文档管理工具

nodeJS安装命令

nodeJS安装完毕。

MongoDB安装

YAPI:从0搭建API文档管理工具

MongoDB安装命令

Git安装

1
2
arduino复制代码#yum安装,这个最简单了
yum -y install git

YAPI安装

YAPI安装,GitHub上已经有比较详细的文档了,地址:

github.com/YMFE/yapi,这…

可视化部署:

YAPI:从0搭建API文档管理工具

YAPI可视化部署

yapi安装完毕,访问http://127.0.0.1:9090进行可视化配置一步一步往下走即可。

命令行部署(推荐方式):

YAPI:从0搭建API文档管理工具

命令行部署(推荐)

yapi安装完毕,访问http://127.0.0.1:{config.json中配置的port}即可访问。

后台运行YAPI

node需要安装pm2模块,使用pm2模块后台运行yapi:

YAPI:从0搭建API文档管理工具

pm2运行yapi

运行成功页面:

YAPI:从0搭建API文档管理工具

yapi运行成功页面

至此,YAPI就安装完毕了,简单实用一下还是不错的,因为是国产的,整体操作风格还是比较习惯的。在YAPI这里接口更改还有记录哦~

YAPI:从0搭建API文档管理工具

YAPI控制台

后面再慢慢体验这个里面的一些高级功能吧,比如MockServer、接口测试等功能。

大家还有什么更好用的API管理工具?你觉得一款优秀的API管理工具应该都有哪些必须的功能?欢迎推荐和讨论!

本文转载自: 掘金

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

自己写的springboot第一个博客项目(四)Spring

发表于 2021-10-29

前言

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

此文章在有一定的SpringSecurity基础的看,包括Springboot+SpringSecurity+JWT+Redis进行整合。

记得把之前拦截器注释掉,它已经是过去时了。说实话刚开始学确实摸不懂头脑,但是学完自己写出来就感到无比的喜悦,这可能就是自己选择这么方向的原因吧。加油兄弟们。

项目更改地方

SpringSecurity
  1. pom.xml
1
2
3
4
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
  1. SecurityConfig
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
82
83
84
85
86
87
88
89
90
91
92
java复制代码@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {


@Autowired
private UserDetailsService userDetailsService;

@Autowired
private DataSource datasource;

@Autowired
private JwtAuthTokenFilter jwtAuthTokenFilter;

//授权
@Override
protected void configure(HttpSecurity http) throws Exception {

http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);

http.authorizeRequests()
.antMatchers(HttpMethod.POST,"/login").permitAll()
.antMatchers(
HttpMethod.GET,
"/*.html"
).permitAll()
.antMatchers("/toLogin").permitAll()
.antMatchers("/blog/blogs").hasRole("admin")
.antMatchers("/blog/{id}").hasRole("vip1")
.anyRequest().authenticated();
// .antMatchers("/blogs").hasIpAddress("0:0:0:0:0:0:0:1");

http.formLogin()
//自定义登录页面
.loginProcessingUrl("/login")
.loginPage("/toLogin")
.successForwardUrl("/blogs")
//自定义跳转
// .successHandler(new MyAuthenticationSuccessHandler("/"))
.failureForwardUrl("/toError");
// .failureHandler(new MyAuthenticationFailureHandler("/error"));
//登出
http.logout()
.logoutSuccessUrl("/toLogin");
//禁用跨站csrf攻击防御,否则无法登陆成功
http.csrf().disable();
//登出功能
httpSecurity.logout().logoutUrl("/logout");
// http.rememberMe()
// //失效时间
// .tokenValiditySeconds(60)
// //自定义登录逻辑
// .userDetailsService(userDetailsService)
// //持久层对象
// .tokenRepository(persistentTokenRepository);

// 添加JWT filter, 在每次http请求前进行拦截
http.addFilterBefore(jwtAuthTokenFilter, UsernamePasswordAuthenticationFilter.class);

//异常处理
http.exceptionHandling()
.accessDeniedHandler(new MyAccessDeniedHandler());
}

// @Bean
// public PersistentTokenRepository getPersistentTokenRepository(){
// JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
// jdbcTokenRepository.setDataSource(datasource);
// //自动建表,第一次启动需要,第二次需要注释
//// jdbcTokenRepository.setCreateTableOnStartup(true);
// return jdbcTokenRepository;
// }

//认证
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
//调用DetailsService完成用户身份验证 设置密码加密方式
auth.userDetailsService(userDetailsService).passwordEncoder(getBCryptPasswordEncoder());
}
// 在通过数据库验证登录的方式中不需要配置此种密码加密方式, 因为已经在JWT配置中指定
@Bean
public BCryptPasswordEncoder getBCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
//将AuthenticationManager注入spring中,要不然可调用不到
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}

}
  1. 增加一张表role和user和role多对多表

role 主键 id 和一个 varchar 的name ,必须加ROLE_

role.jpg

user_role 主键id和两个外键分别对应user和role表

userrole.jpg
4. 实体类user修改

继承Security的UserDetails接口。如果不用的话会报错,框架就不认咱们的实体类还是默认框架自己的实体类。实现接口中的方法注意点默认的四个false改为true

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
java复制代码@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("m_user")
public class User implements UserDetails {

public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
this.username = username;
this.password = password;
this.authorities = authorities;
}

@TableId(value = "id", type = IdType.AUTO)
private Long id;
@NotBlank(message = "名字不能为空")
private String username;
@NotBlank(message = "头像不能为空")
private String avatar;
@Email
private String email;
@NotBlank(message = "密码不能为空")
private String password;

private Integer status;
@JsonFormat(pattern="yyyy-MM-dd")
private Date created;
@JsonFormat(pattern="yyyy-MM-dd")
private Date lastLogin;
//增加权限字段
private Collection<? extends GrantedAuthority> authorities;

private static final long serialVersionUID = 1L;
//这里默认为null
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}

@Override
public boolean isAccountNonExpired() {
return true;
}

@Override
public boolean isAccountNonLocked() {
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}

@Override
public boolean isEnabled() {
return true;
}
}
  1. 自定义登录逻辑UserDetailsServiceImpl继承UserDetailsService接口

这里逻辑非常简单,就是从数据库查数据。通过username判断用户(password在框架中封装,由于我的数据库的数据没有进行加密,所以我查出来数据进行了加密),对象为空就可以抛出UsernameNotFoundException异常,这是肯定要有的。

查询权限表SELECT * from m_role where id in (select roleid FROM m_userrole where userid = #{userid})查到名下所有权限,拿出路径用,号分割拼成字符串,然后用AuthorityUtils.commaSeparatedStringToAuthorityList()进行分割存入user对象中。把当前user存入线程以便后续用到,返回user对象此处user必须继承过UserDetails的对象。

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
java复制代码@Service
public class UserDetailsServiceImpl implements UserDetailsService {

@Autowired
private UserService userService;

@Autowired
private RoleService roleService;

@Autowired
private RedisUtil reidsUtil;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

System.out.println("**********UserDetailsServiceImpl********");
User user = userService.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("登录用户:" + username + "不存在");
}
//查询用户所有权限
List<Role> roles = roleService.getAllByUserId(user.getId());
String authorities = "";
int index = 0;
//拿出来所有权限名字 用,分割
for (Role role : roles) {
index++;
authorities += role.getName();
authorities += ",";
}
System.out.println("roles-------" + authorities);
//将数据库的roles解析为UserDetails的权限集
//AuthorityUtils.commaSeparatedStringToAuthorityList将逗号分隔的字符集转成权限对象列表
user.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(authorities));
//将当前用户存到user线程
UserTheadLocal.put(user);
System.out.println(user);
System.out.println(user.getAuthorities());
return user;
}
}
  1. 登录loginController

只是一个简单的api调用方法即可没有多余判断语句。重点在于之前我这里有注解@RequestBody,不知道为什么我加入了security之后总是报错null,有懂得同学谢谢解答一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码@RestController
public class LoginController {

@Autowired
private UserService userService;


@ApiOperation(value = "登录")
@PostMapping("/login")
public Result login(LoginDto loginDto){
System.out.println("login+++++++++++++++++");
System.out.println(loginDto.getUsername()+"******"+loginDto.getPassword());
String token = userService.login(loginDto.getUsername(), loginDto.getPassword());
return Result.success(token);
}

}

UserService中login实现 。首先要问自己如果不用框架应该怎么写呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码       @Override
public String login(String username, String password){
System.out.println("-**************login");
//用户验证
Authentication authentication = null;
try {
// 进行身份验证,
authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(username, password));
} catch (Exception e) {
throw new RuntimeException("用户名密码错误");
}

User user = (User) authentication.getPrincipal();
// 生成token
String token = JwtUtils.createToken(user);
System.out.println("token+++++++"+token);
redisUtil.set("token"+token,user.getUsername(),600);
System.out.println("loca++++"+ UserTheadLocal.get());
return token;
}

我们会直接从数据库里去查找此人是否存在就可以判定。而现在我们需要去实现框架验证登录逻辑获取user对象UsernamePasswordAuthenticationToken通过用户生成authentication,然后authenticationManager.authenticate()对特定的authentication进行认证功能,认证成功后的Authentication就变成授信凭据,并触发认证成功的事件。认证失败的就抛出异常触发认证失败的事件。其中authenticationManager还会调用我们自定义的UserDetailsServiceImpl进行判定。看到这是不是脑袋里有思路了,我建议多打几个断点去跟这断点走这样理解更深。
7. Filter

UsernamePasswordAuthenticationToken判断用户是否经过认证,SecurityContextHolder.getContext().setAuthentication()授权用户,将返回Authentication 对象赋予给当前的 SecurityContext。

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
java复制代码@Component
public class JwtAuthTokenFilter extends OncePerRequestFilter {

@Autowired
private UserDetailsServiceImpl userDetailsService;

@Autowired
private RedisUtil redisUtil;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
System.out.println("*********filter*******");
String requestURI = request.getRequestURI();
System.out.println("*******-----"+requestURI+"*****-++++++");
String token = request.getHeader("Authorization");
System.out.println(token);

if (!StringUtils.isEmpty(token)) {
String username = (String) redisUtil.get("token" + token);
User user = UserTheadLocal.get();
if (username != user.getUsername() && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
//交给security;管理,在之后过滤器就不会拦截进行二次授权
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());

//设置用户身份授权
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}

filterChain.doFilter(request, response);
}
}
测试
  • 测试登录
    security+login.jpg
  • 每次都会经过这个自定义登录方法
    每次都调用userImpl.jpg
  • 和之前jwt一样获取资源还是header加token
    jwtblog1success.jpg
  • 权限不足,如果要用Authorization记得在配置文件改一下token.header=Authorization

权限不足.jpg

总结

通过代码可以发现我代码里有很多的System.out.println("-**************login");虽然已经删除了很多了。这是我在代码出错或者要看到那个方法执行以及方法中某个对象或者变量值都是用这个方法进行判断,不知道好不好但是多起来就难受了
运行.jpg
我看看到很多都是用log所以下一步整合日志框架。

其实并不难,就看自己下不下劲了。我看连写将近一周时间把他搞完,至少看了三个完整视频,五篇以上相关文章,而且还没有看源码,中间出现了很多错误不知道为什么很多网上都没有相关问题。重点还是在于登录逻辑,这只是框架的第二种密码登录模式还是比较简单的。

欢迎大家指导批评

本文转载自: 掘金

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

不得不说的乐观锁和悲观锁 概念 并发控制机制 乐观锁实现机制

发表于 2021-10-29

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

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

概念

乐观锁与悲观锁是一种广义上的概念,其实是对线程同步的不同角度看法。在Java和数据库中都有此概念对应的实际应用。

悲观锁:对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。

悲观.PNG

乐观锁:乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据(具体方法可以使用版本号机制和CAS算法)。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作:重试或者报异常。

乐观.PNG

并发控制机制

当应用中可能出现并发的情况时,就需要保证在并发情况下数据的准确性,以此确保当前用户和其他用户一起操作时,所得到的结果和他单独操作时的结果是一样的。这就叫做并发控制。并发控制的目的是保证一个用户的工作不会对另一个用户的工作产生不合理的影响。即数据出现脏读、幻读和不可重复读等现象。

常说的并发控制,一般都和数据库管理系统(DBMS)有关。在DBMS中并发控制的任务,是确保多个事务同时增删改查同一数据时,不破坏事务的隔离性、一致性和数据库的统一性。实现并发控制的主要手段就是乐观并发控制和悲观并发控制两种。

乐观锁实现机制

版本号方法

版本号控制:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数。当数据被修改时,version值会+1。当线程A要更新数据时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的 version值与当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

比如:数据库中用户表中有一个version字段,当前值为0;而当前帐户余额字段(money)为100 。

线程A此时将其读出(version=0),并从其帐户余额中扣除50(100-50)。
在线程A操作的过程中,线程B也读入此用户信息(version=0),并从其帐户余额中扣除30(100-30)。
线程A完成了修改工作,将数据版本号加一(version=1),连同帐户扣除后余额(money=50),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录version更新为1。
线程B完成了操作,也将版本号加一(version=1)试图向数据库提交数据(money=70),但此时比对数据库记录版本时发现,线程B提交的数据版本号为1 ,数据库记录当前版本也为1,不满足 “ 提交版本必须大于记录当前版本才能执行更新 “ 的乐观锁策略,因此,线程B的提交被驳回。这样,就避免了操作员B用基于version=0的旧数据修改的结果覆盖线程A的操作结果的可能。

CAS算法

CAS即compare and swap(比较与交换),是一种有名的无锁算法。即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)

CAS中涉及三个要素:

需要读写的内存值V

进行比较的值A

拟写入的新值B

当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

JAVA对CAS的支持:在JDK1.5中新添加 java.util.concurrent (J.U.C) 就是建立在CAS之上的。对于 synchronized这种阻塞算法,CAS是非阻塞算法的一种实现。所以J.U.C在性能上有了很大的提升。更详细的介绍请参考上一篇文章:听说你想看CAS原理

悲观锁实现机制

ReentrantLock

可重入锁就是悲观锁的一种。同步状态标识:对外显示锁资源的占有状态。同步队列:存放获取锁失败的线程。等待队列:用于实现多条件唤醒。Node节点:队列的每个节点,线程封装体。cas修改同步状态标识,获取锁失败加入同步队列阻塞,释放锁时唤醒同步队列第一个节点线程。
加锁过程:调用tryAcquire()修改标识state,成功返回true执行,失败加入队列等待。加入队列后判断节点是否为signal状态,是就直接阻塞挂起当前线程。如果不是则判断是否为cancel状态,是则往前遍历删除队列中所有cancel状态节点。如果节点为0或者propagate状态则将其修改为signal状态。阻塞被唤醒后如果为head则获取锁,成功返回true,失败则继续阻塞。
解锁过程:调用tryRelease()释放锁修改标识state,成功则返回true,失败返回false。释放锁成功后唤醒同步队列后继阻塞的线程节点,被唤醒的节点会自动替换当前节点成为head节点。更详细的内容可参考之前的文章:谈谈可重入锁ReentrantLock

synchronized

synchronized和ReentrantLock都是可重入锁,可重入锁的一个优点是可一定程度避免死锁

1
2
3
4
5
6
7
8
9
10
csharp复制代码public class Widget {
public synchronized void doSomething() {
System.out.println("方法1执行...");
doOthers();
}

public synchronized void doOthers() {
System.out.println("方法2执行...");
}
}

示例中的两个方法都是被内置锁synchronized修饰的,doSomething()方法中调用doOthers()方法。因为内置锁是可重入的,所以同一个线程在调用doOthers()时可以直接获得当前对象的锁,进入doOthers()进行操作。如果是一个不可重入锁,那么当前线程在调用doOthers()之前需要将执行doSomething()时获取当前对象的锁释放掉,实际上该对象锁已被当前线程所持有,且无法释放。所以此时会出现死锁。

悲观锁的问题

ReentrantLock:

需要引入相关的Class;

要在finally模块释放锁;

synchronized可以放在方法的定义里面, 而reentrantlock只能放在块里面;

synchronized:

锁的释放情况少,只在程序正常执行完成和抛出异常时释放锁;

试图获得锁是不能设置超时;

不能中断一个正在试图获得锁的线程;

无法知道是否成功获取到锁;

如何选择

悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
csharp复制代码//悲观锁
public synchronized void testMethod() {
// 操作同步资源
}
// ReentrantLock
private ReentrantLock lock = new ReentrantLock(); // 需要保证多个线程使用的是同一个锁
public void modifyPublicResources() {
lock.lock();
// 操作同步资源
lock.unlock();
}

//乐观锁
private AtomicInteger atomicInteger = new AtomicInteger(); // 需要保证多个线程使用的是同一个AtomicInteger
atomicInteger.incrementAndGet(); //执行自增1

通过示例可以发现悲观锁基本都是在显式的锁定之后再操作同步资源,而乐观锁则直接去操作同步资源。

在乐观锁与悲观锁的选择上面,主要看下两者的区别以及适用场景就可以了:

乐观锁并未真正加锁,效率高。一旦锁的粒度掌握不好,更新失败的概率就会比较高,容易发生业务失败。

悲观锁依赖数据库锁,效率低。更新失败的概率比较低。

在互联网项目追求高并发低时延的当下,悲观锁已经越来越少的被使用到生产环境中了,尤其是并发量比较大的业务场景。

本文转载自: 掘金

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

贪心算法 1 leetcode455 分发饼干 2 le

发表于 2021-10-29

image.png
- 贪心算法的局限性
image.png
- 具备贪心思想的问题
image.png

  1. leetcode455 分发饼干

1.1 解题思路

image.png
image.png

1.2 代码实现🔴

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码class Solution {
public int findContentChildren(int[] g, int[] s) { // 饼干是s 小孩胃口值是g
Arrays.sort(g);
Arrays.sort(s);
int i = 0;
int j = 0;
while (i < s.length && j < g.length) {
if (s[i] >= g[j]) {
j++;
}
i++;
}
return j;
}
}
  1. leetcode322 零钱兑换

2.1 解题思路

本题贪心 + 回溯不能提升性能,最优解为动态规划,后续再写。
image.png
image.png

  1. leetcode45 跳跃游戏II

3.1 解题思路

image.png

3.2 代码实现1:DFS(会超时)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码class Solution {
private int minSteps = Integer.MAX_VALUE;

public int jump(int[] nums) {
dfs(nums, 0, new ArrayList<>());
return minSteps == Integer.MAX_VALUE ? 0 : minSteps;
}

private void dfs(int[] nums, int jumpedIndex, List<Integer> path) {
if (jumpedIndex == nums.length - 1) {
minSteps = Math.min(minSteps, path.size());
return;
}
for (int i = 1; i <= nums[jumpedIndex]; i++) {
if (jumpedIndex + i >= nums.length)
continue;
path.add(i);
// jumpedIndex + i,表示跳到下一步所在的位置
dfs(nums, jumpedIndex + i, path);
path.remove(path.size() - 1);
}
}
}

3.2 代码实现2:BFS(会超时)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码class Solution {
public int jump(int[] nums) {
if (nums.length == 1)
return 0;
Queue<Integer> queue = new ArrayDeque<>();
queue.offer(0);
int level = 0;
while (!queue.isEmpty()) {
int size = queue.size();
for (int i = 0; i < size; i++) {
int jumpedIndex = queue.poll();
if (jumpedIndex == nums.length - 1)
return level;
for (int j = 1; j <= nums[jumpedIndex]; j++) {
queue.offer(jumpedIndex + j);
}
}
level++;
}
return 0;
}
}

3.3 代码实现3:贪心

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码class Solution {
// 贪心策略:每步都选择能跳到的最远距离
public int jump(int[] nums) {
if (nums.length == 1)
return 0;
int steps = 0;
int start = 0, end = 0;
while (end < nums.length - 1) { // 走到最后一个位置的时候就不用走了
int maxPos = 0;
// 每次从 [start, end] 中都选择能跳到的最远距离
for (int i = start; i <= end; i++) {
maxPos = Math.max(maxPos,i + nums[i]);
}
start = end + 1;
end = maxPos;
steps++;
}
return steps;
}
}

3.4 代码实现4:🔴贪心(只遍历一次)

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码class Solution {
// 贪心策略:每步都选择能跳到的最远距离
public int jump(int[] nums) {
if (nums.length == 1)
return 0;
int steps = 0;
int maxPos = 0, end = 0;
for (int i = 0; i < nums.length - 1; i++) {
// 仅遍历一次,计算每一个位置并求得最大位置
maxPos = Math.max(maxPos, i + nums[i]);
if (i == end) {
steps++;
end = maxPos;
}
}
return steps;
}
}
  1. leetcode55 跳跃游戏

4.1 解题思路

image.png
image.png

4.2 代码实现🔴

1
2
3
4
5
6
7
8
9
10
11
java复制代码class Solution {
public boolean canJump(int[] nums) {
int maxPos = 0;
for (int i = 0; i < nums.length; i++) {
if (maxPos < i)
return false;
maxPos = Math.max(maxPos, i + nums[i]);
}
return true;
}
}
  1. leetcode1578 避免重复字母的最小删除成本

4.1 解题思路

image.png
image.png

4.2 代码实现🔴

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码class Solution {
public int minCost(String s, int[] cost) {
int res = 0;
int len = s.length();
int i = 0;
while (i < len) {
char c = s.charAt(i);
int maxCost = 0;
int sumCost = 0;
while (i < len && s.charAt(i) == c) {
maxCost = Math.max(maxCost, cost[i]);
sumCost += cost[i];
i++;
}
res += sumCost - maxCost;
}
return res;
}
}
  1. leetcode402 移掉 K 位数字

5.1 解题思路

image.png
image.png

5.2 代码实现1:贪心,时间复杂度O(k * n)

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
java复制代码class Solution {
public String removeKdigits(String num, int k) {
StringBuilder sb = new StringBuilder(num);
for (int i = 0; i < k; i++) {
boolean hasDeleted = false;
for (int j = 1; j < sb.length(); j++) {
// 如果前面的数字大,则删除
if (sb.charAt(j) < sb.charAt(j - 1)) {
sb.deleteCharAt(j - 1);
hasDeleted = true;
break;
}
}
// 说明序列是递增的,那么删除最后一个字符
if (!hasDeleted)
sb.deleteCharAt(sb.length() - 1);
}

// 删除前面是0的字符
int len = sb.length();
while (len != 0) {
if (sb.charAt(0) > '0')
break;
sb.deleteCharAt(0);
len = sb.length();
}
return sb.length() == 0 ? "0" : sb.toString();
}
}

5.3 代码实现2:🔴贪心 + 单调栈 (时间复杂度O(k + n),空间复杂度O(n))

image.png

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
java复制代码class Solution {
public String removeKdigits(String num, int k) {
Deque<Character> deque = new ArrayDeque<>();
for (int i = 0; i < num.length(); i++) {
char c = num.charAt(i);
while (!deque.isEmpty() && k > 0 && deque.peek() > c) {
deque.pop();
k--;
}
deque.push(c);
}
for (int i = 0; i < k; i++) {
deque.pop();
}

StringBuilder sb = new StringBuilder();
boolean isFirst = true;
while (!deque.isEmpty()) {
char c = deque.pollLast();
if (c == '0' && isFirst)
continue;
sb.append(c);
isFirst = false;
}
return sb.length() == 0 ? "0" : sb.toString();
}
}

6 leetcode409 最长回文串

6.1 解题思路

1. 可以将出现次数为偶数个的字符分居两侧构成回文串

2. 只能在回文串的中间添加一个出现次数为奇数个的字符

6.2 代码实现🔴

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码class Solution {
public int longestPalindrome(String s) {
int[] counter = new int[128];
int ans = 0;
for (char c : s.toCharArray()) {
counter[c]++;
}
for (int v : counter) {
ans += v / 2 * 2;
// 中间只能有一个出现奇数次的字符
if (v % 2 == 1 && ans % 2 == 0) {
ans++;
}
}
return ans;
}
}
  1. leetcode680 验证回文字符串Ⅱ

7.1 解题思路

1. 贪心策略:只有在开头和结尾两个字符不相等的时候,才去尝试删除开头或者结尾任一个字符

7.2 代码实现:🔴贪心

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
java复制代码class Solution {
// 贪心策略: 只有在开头和结尾两个字符不相等的时候,才去尝试删除开头或者结尾任一个字符
// 时间复杂度O(n)
// 空间复杂度O(1)
public boolean validPalindrome(String s) {
int left = 0;
int right = s.length() - 1;
while (left < right) {
if (s.charAt(left) == s.charAt(right)) {
left++;
right--;
} else {
// 要么删除left指向的字符,要么删除right指向的字符
// 然后再判断剩余的字符是否是回文
return validPalindrome(s, left + 1, right) ||
validPalindrome(s, left, right - 1);
}
}
return true;
}

private boolean validPalindrome(String s, int left, int right) {
while (left < right) {
if (s.charAt(left) == s.charAt(right)) {
left++;
right--;
} else {
return false;
}
}
return true;
}
}
  1. leetcode316 去除重复字母

8.1 解题思路

1. 记录字符在字符串s中的最后索引,用于判断当前字符后面是否还有将要弹出的字符

2. 维护一个单调栈

3. 使用一个boolean数组记录是否已经存在于栈中

image.png

8.2 代码实现:🔴贪心 + 单调栈

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
java复制代码class Solution {
public String removeDuplicateLetters(String s) {
// 1. 计算字符在字符串s中的最后索引
int[] lastIndex = new int[26];
for (int i = 0; i < s.length(); i++) {
lastIndex[s.charAt(i) - 'a'] = i;
}

// 2. 维护单调栈
Deque<Character> stack = new ArrayDeque<>();
// 用于记录字符是否已经存在于栈中
boolean[] exits = new boolean[26];

for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (exits[c - 'a'])
continue;

// 条件:
// (1). 当前字符小于栈顶字符
// (2). 栈顶字符在当前字符的后面还会出现
while (!stack.isEmpty() && stack.peek() > c
&& lastIndex[stack.peek() - 'a'] > i) {
char top = stack.pop();
exits[top - 'a'] = false;
}

stack.push(c);
exits[c - 'a'] = true;
}

// 3. 将栈中字符拼接成结果
StringBuilder res = new StringBuilder();
while (!stack.isEmpty()) {
res.append(stack.pollLast());
}
return res.toString();
}
}
  1. leetcode1047 删除字符串中的所有相邻重复项

9.1 解题思路

1. 使用栈
image.png
2. 使用快慢指针,0-slow表示不需要删除的字符,fast指针用来遍历整个字符串

image.png
image.png

9.2 代码实现1:🔴使用栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码class Solution {
public String removeDuplicates(String s) {
Deque<Character> stack = new ArrayDeque<>();
for (char c : s.toCharArray()) {
if (!stack.isEmpty() && stack.peek() == c) {
stack.pop();
} else {
stack.push(c);
}
}
StringBuilder sb = new StringBuilder();
while (!stack.isEmpty()){
sb.append(stack.pollLast());
}
return sb.toString();
}
}

9.3 代码实现2:🔴使用快慢指针(空间复杂度降为O(1))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码class Solution {
public String removeDuplicates(String s) {
char[] chars = s.toCharArray();
int slow = -1;
int fast = 0;
while (fast < s.length()) {
if (slow >= 0 && chars[fast] == chars[slow]) {
slow--;
} else {
slow++;
chars[slow] = chars[fast];
}
fast++;
}
return new String(chars, 0, slow + 1);
}
}
  1. leetcode1209 删除字符串中的所有相邻重复项II

10.1 解题思路

1. 使用栈
image.png
image.png
2. 使用快慢指针
image.png

10.2 代码实现1:🔴使用栈(时间复杂度:O(N),空间复杂度:O(N))

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
java复制代码class Solution {
class Pair {
char ch;
int count;

public Pair(char ch, int count) {
this.ch = ch;
this.count = count;
}
}

public String removeDuplicates(String s, int k) {
Deque<Pair> stack = new ArrayDeque<>();
for (int i = 0; i < s.length(); i++) {
if (stack.isEmpty() || s.charAt(i) != stack.peek().ch) {
stack.push(new Pair(s.charAt(i), 1));
} else {
stack.peek().count++;
if (stack.peek().count == k) {
stack.pop();
}
}
}
StringBuilder sb = new StringBuilder();
while (!stack.isEmpty()) {
Pair p = stack.pollLast();
for (int i = 0; i < p.count; i++) {
sb.append(p.ch);
}
}
return sb.toString();
}
}

10.3 代码实现2:🔴使用快慢指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码class Solution {
public String removeDuplicates(String s, int k) {
char[] chars = s.toCharArray();
Deque<Integer> count = new ArrayDeque<>();
int slow = 0;
for (int fast = 0; fast < chars.length; slow++, fast++) {
if (slow != fast) {
chars[slow] = chars[fast];
}
if (slow == 0 || chars[slow] != chars[slow - 1]) {
count.push(1);
} else {
int incremented = count.pop() + 1;
if (incremented == k) {
slow -= k;
} else {
count.push(incremented);
}
}
}
return new String(chars, 0, slow);
}
}
  1. leetcode976 三角形的最大周长

11.1 解题思路

不失一般性,我们假设三角形的边长a,b,c满足a≤b≤c,那么这三条边组成面积不为零的三角形的充分必要条件为a+b>c。

11.2 代码实现🔴

1
2
3
4
5
6
7
8
9
10
11
java复制代码class Solution {
public int largestPerimeter(int[] nums) {
Arrays.sort(nums);
for (int i = nums.length - 1; i >= 2; i--) {
if (nums[i - 2] + nums[i - 1] > nums[i]) {
return nums[i - 2] + nums[i - 1] + nums[i];
}
}
return 0;
}
}
  1. leetcode674 最长连续递增序列

12.1 解题思路

image.png
image.png

12.2 代码实现(贪心 + 双指针)🔴

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码class Solution {
public int findLengthOfLCIS(int[] nums) {
int res = 1;
for (int start = 0, end = 1; end < nums.length; end++) {
if (nums[end] <= nums[end - 1]) {
start = end;
continue;
}
res = Math.max(res, end - start + 1);
}
return res;
}
}
  1. leetcode738 单调递增的数字

13.1 解题思路

1. 先遍历找到第一个非递增的序列
image.png

2. 回退check直至其为递增序列
image.png

3. 将最后改变高位之后的数字全部换成9

image.png

13.2 代码实现🔴

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码class Solution {
public int monotoneIncreasingDigits(int n) {
char[] strN = String.valueOf(n).toCharArray();
int i = 1;
// 1. 找到第一个递减的位
while (i < strN.length && strN[i - 1] <= strN[i])
i++;
if (i < strN.length) {
// 2. 不断将前一个数字 -1,直到前一个数字小于等于后一个数字
while (i > 0 && strN[i - 1] > strN[i]) {
strN[i - 1] -= 1;
i--;
}
// 3. 将 i 后面的数字都设置为9
i++;
while (i < strN.length) {
strN[i++] = '9';
}
return Integer.parseInt(new String(strN));
} else {
return n;
}
}
}
  1. leetcode134 加油站

14.1 解题思路

总结:如果x到不了y + 1(但能到y),那么从x到y的任一点出发都不可能到达y+1。因为从其中任一点出发的话,相当于从0开始加油,而如果从x出发到该点则不一定是从0开始加油,可能还有剩余的油。既然不从0开始都到不了y+1,那么从0开始就更不可能到达y+1了。

image.png
image.png

14.2 代码实现🔴

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码class Solution {
public int canCompleteCircuit(int[] gas, int[] cost) {
int totalGas = 0;
int currGas = 0;
int startStation = 0;
for (int i = 0; i < gas.length; i++) {
totalGas += gas[i] - cost[i];
currGas += gas[i] - cost[i];
if (currGas < 0) {
startStation = i + 1;
currGas = 0;
}
}
return totalGas >= 0 ? startStation : -1;
}
}
  1. leetcode767 重构字符串

15.1 解题思路

image.png
image.png

15.2 代码实现🔴

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
java复制代码class Solution {
public String reorganizeString(String s) {
// 1. 统计每个字符出现的次数
char[] chars = s.toCharArray();
int n = chars.length;
int[] count = new int[26];
for (char c : chars) {
count[c - 'a']++;
if (count[c - 'a'] > (n + 1) / 2)
return "";
}
// 2. 拿到出现次数最多的字符
int maxCountIndex = 0;
for (int i = 0; i < 26; i++) {
if (count[i] > count[maxCountIndex])
maxCountIndex = i;
}
// 3. 先将出现次数最多的字符放在偶数索引上
char[] res = new char[n];
int index = 0;
while (count[maxCountIndex] > 0) {
res[index] = (char) (maxCountIndex + 'a');
index += 2;
count[maxCountIndex]--;
}
// 4. 将其他的字符放在其他的位置
for (int i = 0; i < 26; i++) {
while (count[i] > 0) {
if (index >= n) {
index = 1;
}
res[index] = (char) (i + 'a');
index += 2;
count[i]--;
}
}
return new String(res);
}
}
  1. leetcode621 任务调度器

16.1 解题思路

1. 完成所有任务所需要的最短时间 = 待命的任务数 + 所有的任务数

image.png
2. 次数最多的任务数只出现了一个的情况
image.png
3. 次数最多的任务数出现了多个的情况
image.png
4. 出现次数最多的任务数大于冷却时间

image.png

16.2 代码实现🔴

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码class Solution {
public int leastInterval(char[] tasks, int n) {
int[] counter = new int[26];
int maxAppearCount = 0; // 出现次数最多的任务 最大出现次数
int maxTaskCount = 0; // 出现次数最多的任务 最大任务数量
for (char task : tasks) {
counter[task - 'A']++;
if (counter[task - 'A'] == maxAppearCount) {
maxTaskCount++;
} else if (counter[task - 'A'] > maxAppearCount) {
maxAppearCount = counter[task - 'A'];
maxTaskCount = 1;
}
}

int partCount = maxAppearCount - 1; // 空余部分个数
int partLength = n - (maxTaskCount - 1); // 空余部分的长度
int emptySlots = partCount * partLength; // 空闲槽位数
int availableTasks = tasks.length - maxAppearCount * maxTaskCount; // 剩余可使用任务数
int idles = Math.max(0, emptySlots - availableTasks); // 待命任务数
return tasks.length + idles; // 完成所有任务所需要的最短时间 = 待命的任务数 + 所有的任务数
}
}
  1. leetcode670 最大交换

17.1 解题思路

贪心策略:拿高位后面比高位大的值进行交换,而且越大越好

17.2 代码实现1:暴力

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码class Solution {
public int maximumSwap(int num) {
char[] chars = String.valueOf(num).toCharArray();
int max = num;
for (int i = 0; i < chars.length; i++) {
for (int j = i + 1; j < chars.length; j++) {
swap(chars, i, j);
max = Math.max(max, Integer.parseInt(new String(chars)));
swap(chars, i, j);
}
}
return max;
}

private void swap(char[] chars, int i, int j) {
char temp = chars[i];
chars[i] = chars[j];
chars[j] = temp;
}
}

17.3 代码实现2:🔴贪心

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
java复制代码class Solution {
public int maximumSwap(int num) {
char[] chars = String.valueOf(num).toCharArray();

// 记录每个数字出现的最后一次出现的下标
int[] last = new int[10];
for (int i = 0; i < chars.length; i++) {
last[chars[i] - '0'] = i;
}

// 从高位向低位扫描,找到当前位置右边的最大的数字并交换
for (int i = 0; i < chars.length; i++) {
// 找最大,所以倒着找
for (int d = 9; d > chars[i] - '0'; d--) {
if (last[d] > i) {
char temp = chars[i];
chars[i] = chars[last[d]];
chars[last[d]] = temp;
// 只允许交换一次,因此直接返回
return Integer.parseInt(new String(chars));
}
}
}
return num;
}
}
  1. leetcode861 翻转矩阵后的得分

18.1 解题思路

image.png
image.png

18.2 代码实现🔴

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
java复制代码class Solution {
public int matrixScore(int[][] grid) {
int rows = grid.length;
int cols = grid[0].length;
// 使得每一行都从 1 开头
for (int row = 0; row < rows; row++) {
if (grid[row][0] == 0) {
// 行翻转
for (int col = 0; col < cols; col++) {
grid[row][col] ^= 1;
}
}
}
int res = 0;
// 1 的数量越多,得到的数值越大
for (int col = 0; col < cols; col++) {
int count = 0;
// 统计第 col 列有多少个1
for (int row = 0; row < rows; row++) {
count += grid[row][col];
}
int maxOneCount = Math.max(count, rows - count);
res += maxOneCount * (1 << (cols - col - 1));
}
return res;
}
}
  1. leetcode1029 两地调度

19.1 解题思路

最优方案:选出price_A - price_B最小的N个人,让他们飞往A市,其余的飞往B市

19.2 代码实现:🔴贪心

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码class Solution {
public int twoCitySchedCost(int[][] costs) {
Arrays.sort(costs, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
return (o1[0] - o1[1]) - (o2[0] - o2[1]);
}
});

int n = costs.length / 2;
int total = 0;
for (int i = 0; i < n; i++) {
total += costs[i][0] + costs[i + n][1];
}
return total;
}
}
  1. leetcode330 按要求补齐数组

20.1 解题思路

1. 贪心思想:每次加入nums的数都要在满足条件的前提下尽可能的大以覆盖更大的范围。
2. 对于正整数x,如果区间[1, x - 1]内的所有数字都已经被覆盖,且x在数组中,则区间[1, 2x - 1]内的数字也都被覆盖。
image.png
image.png

20.2 代码实现🔴

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码class Solution {
public int minPatches(int[] nums, int n) {
int res = 0;
// 贪心的保证 [1, x - 1] 这个区间中所有数字会被覆盖
long x = 1;
int index = 0;
while (x <= n) {
if (index < nums.length && nums[index] <= x) {
// 因为根据贪心思想,我们总保证区间小于x的所有值会被覆盖掉
// [1, x + x - 1] [1, x + nums[index] - 1]
x += nums[index];
index++;
} else {
res++; // 把 x 放入数组中
// 对于正整数 x,如果区间 [1, x - 1] 内的所有数字都已经被覆盖。
// 且 x 在数组中,则区间 [1, 2x - 1] 内的所有数字也都被覆盖
x *= 2;
}
}
return res;
}
}

本文转载自: 掘金

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

快速了解XML

发表于 2021-10-29

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

  1. XML 定义

可扩展标记语言,标准通用标记语言的子集,简称XML。是一种用于标记电子文件使其具有结构性的标记语言。

  1. XML 展示

如下是一个xml的标记展示,XML 是不作为的XML 被设计用来结构化、存储以及传输信息,所以我们可以自由标记,只有我们赋予它什么意义他就有什么意义。

xml 用来简化数据共享、简化数据传输、简化平台的变更等功能。

1
2
3
4
5
6
7
8
xml复制代码<xml>
<tag>标签</tag>
<parent>
<son>儿子</son>
<daughter>女儿</daughter>
</parent>
<famliy>xml大家庭</famliy>
</xml>
  1. XML 文档实例

XML 文档必须包含根元素。该元素是所有其他元素的父元素。

XML 文档中的元素形成了一棵文档树。这棵树从根部开始,并扩展到树的最底端。

encoding="utf-8" 用于指定该xml文档编码。

1
2
3
4
5
6
7
8
9
xml复制代码<?xml version="1.0" encoding="utf-8"?>
<xml>
<tag>标签</tag>
<parent>
<son>儿子</son>
<daughter>女儿</daughter>
</parent>
<famliy>xml大家庭</famliy>
</xml>
  1. XML 其它特性

  • xml 省略关闭标签是非法的。所有元素都必须有关闭标签
    <son>儿子</son>
  • 元素可包含其他元素、文本或者两者的混合物。元素也可以拥有属性,熟悉提供元素的额外信息。
1
2
3
4
5
xml复制代码<parent>
父母拥有
<son sex="属性:男">儿子</son>
<daughter sex="属性:女">女儿</daughter>
</parent>
  1. XML 验证

拥有正确语法的 XML 被称为“形式良好”的 XML。

通过 DTD 验证的 XML 是“合法”的 XML。

W3C 支持一种基于 XML 的 DTD 代替者,它名为 XML Schema,所以DTD的规范基本被淘汰了,现在我们主流使用的都是XML Schema,也就是说XML Schema是一种可以描述XML文档结构的定义,如果你的xml文件遵循某一个XML Schema就可以通过再xml文件中制定遵循的XML Schema,XML Schema 语言也可作为 XSD(XML Schema Definition)来引用。

1
2
3
4
5
6
7
8
9
10
11
12
xml复制代码<?xml version="1.0"?>
<xml
xmlns="http://www.springsun.com"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springsun.com xml.xsd">
<tag>标签</tag>
<parent>
<son>儿子</son>
<daughter>女儿</daughter>
</parent>
<famliy>xml大家庭</famliy>
</xml>

其中xmlns是默认命名空间, xmlns:xsi 是一个行业默认标准,使用这个定义XMLSchema实例,xsi:schemaLocation指向XSD可访问的位置。

  1. XML 命名空间

在 XML 中,元素名称是我们自由制定的,当两个不同的文档使用相同的元素名时,就会发生命名冲突,所以为了区分规定为标签可以加上前缀。

1
2
3
4
5
6
7
8
xml复制代码 <s:xml>
<s:tag>标签</s:tag>
<s:parent>
<s:son>儿子</s:son>
<s:daughter>女儿</s:daughter>
</s:parent>
<s:famliy>xml大家庭</s:famliy>
</s:xml>

而命名空间就是为了添加这个前缀,在根节点使用 xmlns 来规定前缀与某个特定标识对应以让它有意义。

1
2
3
4
5
6
7
8
xml复制代码 <s:xml xmlns:s="http://www.springsun.com">
<s:tag>标签</s:tag>
<s:parent>
<s:son>儿子</s:son>
<s:daughter>女儿</s:daughter>
</s:parent>
<s:famliy>xml大家庭</s:famliy>
</s:xml>

www.springsun.com 用于标示命名空间的地址。其惟一的作用是赋予命名空间一个惟一的名称,只是行业都是用某一个网页链接去标识。
为什么我们平时看到的例子里面没有加像xmlns:s这样的前缀标识,因为不加 :s 则标识默认命名空间,后面所有元素不加前缀的都在该命名空间下。

7.0 XSLT 显示 XML

使用 XSLT 显示 XML:使用 XSLT 在浏览器显示 XML 文件之前,先把它转换为 HTML,这样就可以按自定义样式显示xml数据到网页上。

1
2
3
4
5
6
7
8
9
10
11
12
13
xml复制代码<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="xml.xsl"?>
<xml
xmlns="http://www.springsun.com"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springsun.com xml.xsd">
<tag>标签</tag>
<parent>
<son>儿子</son>
<daughter>女儿</daughter>
</parent>
<famliy>xml大家庭</famliy>
</xml>

<?xml-stylesheet type="text/xsl" href="xml.xsl"?> 这个标记用于将xml按照xml.xsl的样式转换到html,如果浏览器支持xsl的话直接就可以显示出来(基本浏览器都支持), xsl可以去查看专门的介绍。

以上这些就可以对xml做一个快速入门了解,还有些其它知识用的时候再去检索下就行了,比如 CDATA 、XML DOM、XPath等。

本文转载自: 掘金

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

1…456457458…956

开发者博客

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