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

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


  • 首页

  • 归档

  • 搜索

Java 开发最容易写的 10 个bug

发表于 2021-10-09

原文链接:10 个让人头疼的 bug

那个谁,今天又写 bug 了,没错,他说的好像就是我。。。。。。

作为 Java 开发,我们在写代码的过程中难免会产生各种奇思妙想的 bug ,有些 bug 就挺让人无奈的,比如说各种空指针异常,在 ArrayList 的迭代中进行删除操作引发异常,数组下标越界异常等。

如果你不小心看到同事的代码出现了我所描述的这些 bug 后,那你就把我这篇文章甩给他!!!你甩给他一篇文章,并让他关注了一波 cxuan,你会收获他在后面像是如获至宝并满眼崇拜大神的目光。

废话不多说,下面进入正题。

错误一:Array 转换成 ArrayList

Array 转换成 ArrayList 还能出错?这是哪个笨。。。。。。

等等,你先别着急说,先来看看是怎么回事。

如果要将数组转换为 ArrayList,我们一般的做法会是这样

1
java复制代码List<String> list = Arrays.asList(arr);

Arrays.asList() 将返回一个 ArrayList,它是 Arrays 中的私有静态类,它不是 java.util.ArrayList 类。如下图所示

image-20211005232205213

Arrays 内部的 ArrayList 只有 set、get、contains 等方法,但是没有能够像是 add 这种能够使其内部结构进行改变的方法,所以 Arrays 内部的 ArrayList 的大小是固定的。

image-20211006094537453

如果要创建一个能够添加元素的 ArrayList ,你可以使用下面这种创建方式:

1
java复制代码ArrayList<String> arrayList = new ArrayList<String>(Arrays.asList(arr));

因为 ArrayList 的构造方法是可以接收一个 Collection 集合的,所以这种创建方式是可行的。

image-20211006094827686

错误二:检查数组是否包含某个值

检查数组中是否包含某个值,部分程序员经常会这么做:

1
2
java复制代码Set<String> set = new HashSet<String>(Arrays.asList(arr));
return set.contains(targetValue);

这段代码虽然没错,但是有额外的性能损耗,正常情况下,不用将其再转换为 set,直接这么做就好了:

1
java复制代码return Arrays.asList(arr).contains(targetValue);

或者使用下面这种方式(穷举法,循环判断)

1
2
3
4
5
java复制代码for(String s: arr){
if(s.equals(targetValue))
return true;
}
return false;

上面第一段代码比第二段更具有可读性。

错误三:在 List 中循环删除元素

这个错误我相信很多小伙伴都知道了,在循环中删除元素是个禁忌,有段时间内我在审查代码的时候就喜欢看团队的其他小伙伴有没有犯这个错误。

image-20211006101709155
说到底,为什么不能这么做(集合内删除元素)呢?且看下面代码

1
2
3
4
5
java复制代码ArrayList<String> list = new ArrayList<String>(Arrays.asList("a", "b", "c", "d"));
for (int i = 0; i < list.size(); i++) {
list.remove(i);
}
System.out.println(list);

这个输出结果你能想到么?是不是蠢蠢欲动想试一波了?

答案其实是 [b,d]

为什么只有两个值?我这不是循环输出的么?

其实,在列表内部,当你使用外部 remove 的时候,一旦 remove 一个元素后,其列表的内部结构会发生改变,一开始集合总容量是 4,remove 一个元素之后就会变为 3,然后再和 i 进行比较判断。。。。。。所以只能输出两个元素。

你可能知道使用迭代器是正确的 remove 元素的方式,你还可能知道 for-each 和 iterator 这种工作方式类似,所以你写下了如下代码

1
2
3
4
5
6
java复制代码ArrayList<String> list = new ArrayList<String>(Arrays.asList("a", "b", "c", "d"));

for (String s : list) {
if (s.equals("a"))
list.remove(s);
}

然后你充满自信的 run xxx.main() 方法,结果。。。。。。ConcurrentModificationException

为啥呢?

那是因为使用 ArrayList 中外部 remove 元素,会造成其内部结构和游标的改变。

在阿里开发规范上,也有不要在 for-each 循环内对元素进行 remove/add 操作的说明。

image-20211006100608623

所以大家要使用 List 进行元素的添加或者删除操作,一定要使用迭代器进行删除。也就是

1
2
3
4
5
6
7
8
9
java复制代码ArrayList<String> list = new ArrayList<String>(Arrays.asList("a", "b", "c", "d"));
Iterator<String> iter = list.iterator();
while (iter.hasNext()) {
String s = iter.next();

if (s.equals("a")) {
iter.remove();
}
}

.next() 必须在 .remove() 之前调用。 在 foreach 循环中,编译器会在删除元素的操作后调用 .next(),导致ConcurrentModificationException。

错误四:Hashtable 和 HashMap

这是一条算法方面的规约:按照算法的约定,Hashtable 是数据结构的名称,但是在 Java 中,数据结构的名称是 HashMap,Hashtable 和 HashMap 的主要区别之一就是 Hashtable 是同步的,所以很多时候你不需要 Hashtable ,而是使用 HashMap。

错误五:使用原始类型的集合

这是一条泛型方面的约束:

在 Java 中,原始类型和无界通配符类型很容易混合在一起。以 Set 为例,Set 是原始类型,而 Set<?> 是无界通配符类型。

比如下面使用原始类型 List 作为参数的代码:

1
2
3
4
5
6
7
8
java复制代码public static void add(List list, Object o){
list.add(o);
}
public static void main(String[] args){
List<String> list = new ArrayList<String>();
add(list, 10);
String s = list.get(0);
}

这段代码会抛出 java.lang.ClassCastException 异常,为啥呢?

image-20211006162921268
使用原始类型集合是比较危险的,因为原始类型会跳过泛型检查而且不安全,Set、Set<?> 和 Set<Object> 存在巨大的差异,而且泛型在使用中很容易造成类型擦除。

大家都知道,Java 的泛型是伪泛型,这是因为 Java 在编译期间,所有的泛型信息都会被擦掉,正确理解泛型概念的首要前提是理解类型擦除。Java 的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程成为类型擦除。

如在代码中定义List<Object>和List<String>等类型,在编译后都会变成List,JVM 看到的只是List,而由泛型附加的类型信息对 JVM 是看不到的。Java 编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法在运行时刻出现的类型转换异常的情况,类型擦除也是 Java 的泛型与 C++ 模板机制实现方式之间的重要区别。

比如下面这段示例

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

public static void main(String[] args) {

ArrayList<String> list1 = new ArrayList<String>();
list1.add("abc");

ArrayList<Integer> list2 = new ArrayList<Integer>();
list2.add(123);

System.out.println(list1.getClass() == list2.getClass());
}

}

在这个例子中,我们定义了两个ArrayList数组,不过一个是ArrayList<String>泛型类型的,只能存储字符串;一个是ArrayList<Integer>泛型类型的,只能存储整数,最后,我们通过list1对象和list2对象的getClass()方法获取他们的类的信息,最后发现结果为true。说明泛型类型String和Integer都被擦除掉了,只剩下原始类型。

所以,最上面那段代码,把 10 添加到 Object 类型中是完全可以的,然而将 Object 类型的 “10” 转换为 String 类型就会抛出类型转换异常。

错误六:访问级别问题

我相信大部分开发在设计 class 或者成员变量的时候,都会简单粗暴的直接声明 public xxx,这是一种糟糕的设计,声明为 public 就很容易赤身裸体,这样对于类或者成员变量来说,都存在一定危险性。

错误七:ArrayList 和 LinkedList

哈哈哈,ArrayList 是我见过程序员使用频次最高的工具类,没有之一。

image-20211006165557687
当开发人员不知道 ArrayList 和 LinkedList 的区别时,他们经常使用 ArrayList(其实实际上,就算知道他们的区别,他们也不用 LinkedList,因为这点性能不值一提),因为看起来 ArrayList 更熟悉。。。。。。

但是实际上,ArrayList 和 LinkedList 存在巨大的性能差异,简而言之,如果添加/删除操作大量且随机访问操作不是很多,则应首选 LinkedList。如果存在大量的访问操作,那么首选 ArrayList,但是 ArrayList 不适合进行大量的添加/删除操作。

错误八:可变和不可变

不可变对象有很多优点,比如简单、安全等。但是不可变对象需要为每个不同的值分配一个单独的对象,对象不具备复用性,如果这类对象过多可能会导致垃圾回收的成本很高。在可变和不可变之间进行选择时需要有一个平衡。

一般来说,可变对象用于避免产生过多的中间对象。 比如你要连接大量字符串。 如果你使用一个不可变的字符串,你会产生很多可以立即进行垃圾回收的对象。 这会浪费 CPU 的时间和精力,使用可变对象是正确的解决方案(例如 StringBuilder)。如下代码所示:

1
2
3
4
java复制代码String result="";
for(String s: arr){
result = result + s;
}

所以,正确选择可变对象还是不可变对象需要慎重抉择。

错误九:构造函数

首先看一段代码,分析为什么会编译不通过?

image-20211006172246303

发生此编译错误是因为未定义默认 Super 的构造函数。 在 Java 中,如果一个类没有定义构造函数,编译器会默认为该类插入一个默认的无参数构造函数。 如果在 Super 类中定义了构造函数,在这种情况下 Super(String s),编译器将不会插入默认的无参数构造函数。 这就是上面 Super 类的情况。

要想解决这个问题,只需要在 Super 中添加一个无参数的构造函数即可。

1
2
3
java复制代码public Super(){
System.out.println("Super");
}

错误十:到底是使用 “” 还是构造函数

考虑下面代码:

1
2
java复制代码String x = "abc";
String y = new String("abc");

上面这两段代码有什么区别吗?

可能下面这段代码会给出你回答

1
2
3
4
5
6
7
8
9
java复制代码String a = "abcd";
String b = "abcd";
System.out.println(a == b); // True
System.out.println(a.equals(b)); // True

String c = new String("abcd");
String d = new String("abcd");
System.out.println(c == d); // False
System.out.println(c.equals(d)); // True

这就是一个典型的内存分配问题。

后记

今天我给你汇总了一下 Java 开发中常见的 10 个错误,虽然比较简单,但是很容易忽视的问题,细节成就完美,看看你还会不会再犯了,如果再犯,嘿嘿嘿。

image-20211005230533419
点赞在看分享朋友圈是基操哦!快来一键三连!!!

本文转载自: 掘金

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

SpringBoot集成itextpdf动态生成pdf并展示

发表于 2021-10-09

背景

接上文SpringBoot集成markdown实现文档管理,对于表格的支持markdown不是特别友好,同时内部文档管理需要增加表格式api接口文档的功能,所以决定采用结合数据库存储与动态生成pdf借助目录结构展示的方式

表结构设计

目录表

1
2
3
4
5
6
7
8
9
10
11
sql复制代码DROP TABLE IF EXISTS `knowledge_interfacecatalog`;
CREATE TABLE `knowledge_interfacecatalog` (
`ID` int(11) NOT NULL AUTO_INCREMENT,
`UnitGuid` varchar(50) DEFAULT NULL,
`AddDate` datetime DEFAULT NULL,
`CataName` varchar(100) DEFAULT NULL,
`ParentCataGuid` varchar(50) DEFAULT NULL,
`SortNum` int(11) DEFAULT NULL,
`DocGuid` varchar(50) DEFAULT NULL,
KEY `ID` (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4;

接口内容表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
sql复制代码DROP TABLE IF EXISTS `knowledge_interfaceinfo`;
CREATE TABLE `knowledge_interfaceinfo` (
`ID` int(11) NOT NULL AUTO_INCREMENT,
`UnitGuid` varchar(50) DEFAULT NULL,
`AddDate` datetime DEFAULT NULL,
`InterfaceName` varchar(100) DEFAULT NULL,
`Description` varchar(500) DEFAULT NULL,
`Remark` varchar(500) DEFAULT NULL,
`ParamJson` varchar(2000) DEFAULT NULL,
`ResponseJson` varchar(2000) DEFAULT NULL,
`InterfaceAddress` varchar(500) DEFAULT NULL,
`SortNum` int(11) DEFAULT NULL,
`CataGuid` varchar(50) DEFAULT NULL,
`DocGuid` varchar(50) DEFAULT NULL,
KEY `ID` (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=24 DEFAULT CHARSET=utf8mb4;

录入界面

PDF生成

引用

1
2
3
4
5
6
7
8
9
10
11
java复制代码        <dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.4.3</version>
</dependency>

<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext-asian</artifactId>
<version>5.2.0</version>
</dependency>

创建PDF

1
2
3
4
5
6
7
java复制代码	Document document = new Document(PageSize.A2);
PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream(filePath));
document.addTitle(doc.getDocName());
document.addAuthor("xxxxx");
document.addCreationDate();
document.addLanguage("中文");
document.open();

设置自定义字体

1
2
3
java复制代码    File fontFile = new File("font/msyh.ttf");
BaseFont bf = BaseFont.createFont(fontFile.getAbsolutePath(), BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
Font fontChinese5 = new Font(bf,14);

添加章节与段落

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码 	//目录的字体
Font cataFont = new Font(bf, 24, Font.NORMAL, BaseColor.BLACK);
// 接口的字体
Font interFont = new Font(bf, 20, Font.NORMAL, BaseColor.BLACK);
for(int i=1;i<=10;i++){
Chapter chapter = new Chapter(new Paragraph("目录", cataFont),i);
for(int j=1;j<=5;j++){
Section section = chapter.addSection(new Paragraph("接口", interFont));
}
document.add(chapter);
}
document.close();

添加表格

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码  PdfPTable table = new PdfPTable(3);//生成一个3列的表格
//表格垂直居中
table.setHorizontalAlignment(Element.ALIGN_CENTER);
table.setTotalWidth(800f);
float[] widths = new float[] { 150f,325f,325f };
table.setWidths(widths);
PdfPCell cell;
cell = new PdfPCell(new Paragraph("接口地址",fontChinese5));
table.addCell(cell);
cell = new PdfPCell(new Paragraph(interfaceInfoDO.getInterfaceAddress(),fontChinese5));
cell.setColspan(2);
table.addCell(cell);

制表符替换

在pdf生成过程中\t制表符无效导致样式变动,需要进行编码替换replace(“\t”,”\u00a0\u00a0\u00a0\u00a0”)

PDF展示

这里选择的是pdf.js进行展示,引用相关文件,地址栏出入file代表url地址即可,因为用于接口文档展示,所以我需要默认进入就是目录模式,所以需要对页面进行相关js处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
javascript复制代码<script type="text/javascript">
var interval = setInterval('loadPdf()', 1000);

function loadPdf() {
if (PDFViewerApplication.pdfDocument == null) {
console.info('Loading...');
} else {
clearInterval(interval);
console.info('Load Success...');
var sidebarToggle = $("#sidebarToggle");
var viewOutline = $("#viewOutline");
if(!sidebarToggle.hasClass("toggled")){
sidebarToggle.click();
}
if(!viewOutline.hasClass("toggled")){
viewOutline.click();
}
}
}
</script>

展示效果

本文转载自: 掘金

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

poi操作word文档docx

发表于 2021-10-09

XWPF 文档(POI API 文档) (apache.org)

依赖

1
2
3
4
5
6
7
8
9
10
11
12
xml复制代码<!-- https://mvnrepository.com/artifact/org.apache.poi/poi -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>4.1.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.poi/poi-ooxml -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>

XWPFDocument类

1. 构造方法

  • XWPFDocument(java.io.InputStream is) 输入流读文件打开
1
java复制代码XWPFDocument document = new XWPFDocument(new FileInputStream(path));
  • XWPFDocument(OPCPackage pkg) 打开文档
1
java复制代码XWPFDocument document = new XWPFDocument(XWPFDocument.openPackage("C:\Users\admin\Desktop\0.docx"));

注意: 两种方式的区别 文件流打开

1
2
arduino复制代码 new XWPFDocument(new FileInputStream(path)) 打开操作word不会修改原模板输出操作时。
new XWPFDocument(XWPFDocument.openPackage("C:\Users\admin\Desktop\0.docx")) 原来的word模板会被修改

2.常用

  • createParagraph() 创建段落
  • createTable() 创建一个空表,其中一行和一列为默认值。也可以指定行列
  • createStyles() 如果文档尚未存在,则创建空样式
  • getParagraphs() 返回持有头或脚文本的段落。
  • insertNewParagraph(org.apache.xmlbeans.XmlCursor cursor) 在光标的位置添加新段落。 游标=光标
  • removeBodyElement(int pos)从word文档body元素阵列列表中删除身体元素(从上到下)
  • write(java.io.OutputStream out) 写文件

实例demo

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
java复制代码    public class Test004 {
public static void main(String[] args) throws IOException, OpenXML4JException, XmlException {

XWPFDocument document = new XWPFDocument(new FileInputStream("C:\Users\admin\Desktop\0.docx"));

XWPFParagraph paragraph = document.createParagraph();
//获取所有段落
List<XWPFParagraph> paragraphs = document.getParagraphs();
for (int i = 0; i < paragraphs.size(); i++) {
XWPFParagraph paragraph1 = paragraphs.get(i);
if (paragraph1.getText().equals("开头")){
//将一个新运行追加到这一段
XWPFRun r1=paragraph.createRun();
r1.addCarriageReturn();
// 获取当前光标位置
XmlCursor cursor = paragraph1.getCTP().newCursor();
cursor.toNextSibling();
// 光标位置插入新段落
XWPFParagraph newParagraph = document.insertNewParagraph(cursor);
XWPFRun run = newParagraph.createRun();
run.getCTR().addNewRPr().addNewHighlight().setVal(STHighlightColor.LIGHT_GRAY);
String s= " ";

String s1 = "插入当前位置!!!!";
StringBuilder stringBuilder = new StringBuilder();
for (int j = 0; j < 64-s1.length(); j++) {
stringBuilder.append(" ");
}
run.setText(s1+stringBuilder);


document.removeBodyElement(document.getPosOfParagraph(paragraph1));
}

System.out.println("死循环"+i);
}


document.write(new FileOutputStream("C:\Users\admin\Desktop\001.docx"));
document.close();

}
}
1
复制代码   效果

2F~RUEVDCQE4HJ}AT_GPO2I.png

本文转载自: 掘金

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

日常工作中如何优化 COUNT(*) 语句 背景 COUNT

发表于 2021-10-09

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

大家好,我是小黑,8、9双月忙翻了,荒废了分享的大事,最近缓和了,加紧补上。

背景

如果大家平时有关注线上系统的数据库慢查询日志,相信一些 COUNT() 语句是最常出现的,今天和大家分享下怎么优化 COUNT() 语句。

COUNT()函数的基本原理

先简单介绍下 COUNT() 的基本原理。

COUNT() 本质上是一个MySQL的内置函数,入参是数据库表的某个字段名,另外还有 COUNT(1) 和 COUNT(*) 两种特殊用法。

如果传的是数据库表的某个字段,COUNT() 的作用就是统计库表中该字段非空的行数。如果是 COUNT(1) 和 COUNT(*),MySQL 则会直接统计数据库表中的行数。

另外,不同的存储引擎在细节上还会不同。

像 MyISAM 引擎,直接记录了数据库表的行数,如果执行 COUNT(1),COUNT() 的话可以很快的得到结果(当然不能带上 WHERE 条件)。而 InnoDB 为了支持事物,并没有存储数据库表的行数,所以执行 COUNT(1)/COUNT(),往往需要全表扫描。

日常工作中,更多是使用 InnoDB 引擎,也正是因为 InnoDB 引擎没有记录数据库表的行数,而且往往也是带上了WHERE条件的,如果数据量一大的话,扫描的行数就大,COUNT(1)/COUNT(*) 语句自然就慢了。

COUNT(*),COUNT(1),COUNT(主键 id),COUNT(col)的性能区别

那这么多 COUNT() 的用法,应该怎么选呢?(下面仅针对 InnoDB 引擎)

  • COUNT(col) 主要是用来统计某一列非 NULL 的行数,会去遍历字段,并判断是否为空
  • COUNT(主键id) 和 COUNT(col) 本质是一样的,也会去遍历字段,但是主键 id 都是 NOT NULL 的,所以判空这一步 MySQL 做了优化
  • COUNT(1) 和 COUNT(*) 是一样的,也会去遍历,但是仅仅只是从存储引擎层取出来,性能要优异些

​

所以,性能上:count(col) < count(主键 id) < count(1) = count(*)

对精度要求不高的场景

那么,该如何优化呢?这个要分场景,如果对精度要求不高的场景。比如百度上显示查询结果条目的场景,用户并不需要知道具体有多少行,知道个大概就行。
image.png
​

这类场景,就可以使用 MySQL 的 explanin 命令。这个命令的作用是统计出sql执行的大概计划,其中的 rows 字段(从右往左数第三个)就是数据库表行数的估算值(MySQL自身会使用这个值选择优化方案)。

从实际情况看,如果写入操作都是有序的,很少做随机的删除操作,这个值的估算还是比较准确的,当然为了保险起见,可以专门起个定时任务,定时对数据库表做:analyze table t,让MySQL重新采样统计基数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sql复制代码-- 使 用explanin 查看 city 表的行数
mysql> EXPLAIN SELECT * FROM city;

+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+

| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra |

+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+

|  1 | SIMPLE      | city  | NULL       | ALL  | NULL          | NULL | NULL    | NULL |  600 |   100.00 | NULL  |

+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+

1 row in set, 1 warning (0.01 sec)

​

对精度要求高的场景

那如果对精度要求高呢?
​

这类场可以基于 redis 实现,如果插入/删除数据就同步修改下 redis,这个方案的缺点是如果事物回滚,redis 的数据和 MySQL 的行数就不一致了,还需要弄个定时任务去兜底同步数据。
​

还可以就用 MySQL 实现,设计一张计数表统计数量。如果插入/删除数据,就在同一个事物内更新计数表,缺点是有幻读的问题。
​

总结

最后总结一下。
​

  1. 性能上,count(col) < count(主键 id) < count(1) = count(*),日常工作中推荐使用 COUNT(*),它是 SQL92 定义的标准统计行数的语法。

​

  1. 如果要优化 COUNT(*),首先要确认场景,精确、简单、快速,三者只能得其二。
    • 要性能,要成本,不要求精度:show table status
    • 要性能,要精度,成本啥的无所谓:设计一个计数的统计机制(要注意下并发情况下的一致性和事物回滚的情况)
    • 要成本,要精读,不要求性能:那就啥都别说了,老老实实用COUNT(*)

感谢阅读,thanks。
image.png

Ref

  1. 极客时间,14 | count(*)这么慢,我该怎么办?

time.geekbang.org/column/arti…

  1. MySQL的COUNT语句,竟然都能被面试官虐的这么惨!?

mp.weixin.qq.com/s/IOHvtel2K…

本文转载自: 掘金

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

Spring Boot中使用PostgreSQL数据库 Po

发表于 2021-10-09

在如今的关系型数据库中,有两个开源产品是你必须知道的。其中一个是MySQL,相信关注我的小伙伴们一定都不陌生,因为之前的Spring Boot关于关系型数据库的所有例子都是对MySQL来介绍的。而今天我们将介绍另外一个开源关系型数据库:PostgreSQL,以及在Spring Boot中如何使用。

PostgreSQL简介

在学习PostgreSQL的时候,我们总是会将其与MySQL放一起来比较:MySQL自称是最流行的开源数据库,而PostgreSQL则标榜自己是最先进的开源数据库,那么有多先进呢?下面就一起认识一下它!

PostgreSQL是一种特性非常齐全的自由软件的对象-关系型数据库管理系统(ORDBMS),是以加州大学计算机系开发的POSTGRES,4.2版本为基础的对象关系型数据库管理系统。POSTGRES的许多领先概念只是在比较迟的时候才出现在商业网站数据库中。PostgreSQL支持大部分的SQL标准并且提供了很多其他现代特性,如复杂查询、外键、触发器、视图、事务完整性、多版本并发控制等。同样,PostgreSQL也可以用许多方法扩展,例如通过增加新的数据类型、函数、操作符、聚集函数、索引方法、过程语言等。另外,因为许可证的灵活,任何人都可以以任何目的免费使用、修改和分发PostgreSQL。

PostgreSQL的优势

既然跟MySQL一样,同为关系型数据库,那么什么时候用MySQL,什么时候用PostgreSQL自然是我们需要去了解的。所以下面简单介绍一下,PostgreSQL相比于MySQL来说,都有哪些优势,如果你有这些需求,那么选择PostgreSQL就优于MySQL,反之则还是选择MySQL更佳:

  • 支持存储一些特殊的数据类型,比如:array、json、jsonb
  • 对地理信息的存储与处理有更好的支持,所以它可以成为一个空间数据库,更好的管理数据测量和几何拓扑分析
  • 可以快速构建REST API,通过PostgREST可以方便的为任何PostgreSQL数据库提供RESTful API的服务
  • 支持树状结构,可以更方便的处理具备此类特性的数据存储
  • 外部数据源支持,可以把MySQL、Oracle、CSV、Hadoop等当成自己数据库中的表来进行查询
  • 对索引的支持更强,PostgreSQL支持 B-树、哈希、R-树和 Gist 索引。而MySQL取决于存储引擎。MyISAM:BTREE,InnoDB:BTREE。
  • 事务隔离更好,MySQL 的事务隔离级别repeatable read并不能阻止常见的并发更新,得加锁才可以,但悲观锁会影响性能,手动实现乐观锁又复杂。而 PostgreSQL 的列里有隐藏的乐观锁 version 字段,默认的 repeatable read 级别就能保证并发更新的正确性,并且又有乐观锁的性能。
  • 时间精度更高,可以精确到秒以下
  • 字符支持更好,MySQL里需要utf8mb4才能显示emoji,PostgreSQL没这个坑
  • 存储方式支持更大的数据量,PostgreSQL主表采用堆表存放,MySQL采用索引组织表,能够支持比MySQL更大的数据量。
  • 序列支持更好,MySQL不支持多个表从同一个序列中取id,而PostgreSQL可以
  • 增加列更简单,MySQL表增加列,基本上是重建表和索引,会花很长时间。PostgreSQL表增加列,只是在数据字典中增加表定义,不会重建表。

这里仅列举了开发者视角关注的一些优势,还有一些其他优势读者可查看这篇文章,获得更详细的解读。

下载与安装

读者可以通过下面的链接获取PostgreSQL各版本的安装程序,这里不对安装过程做详细描述了,根据安装程序的指引相信大家都能完成安装(一路next,设置访问密码和端口即可)。

下载地址:www.enterprisedb.com/downloads/p…

注意:因为14是今天刚发布的版本,为避免Spring Boot的兼容问题,还是选用之前的13.4版本来完成下面的实验。

安装完成后,打开pgAdmin。因为自带了界面化的管理工具,所以如果你用过mysql等任何关系型数据库的话,基本不用怎么学,就可以上手使用了。

PostgreSQL pgAdmin

Spring Boot中如何使用

在安装好了PostgreSQL之后,下面我们尝试一下在Spring Boot中使用PostgreSQL数据库。

第一步:创建一个基础的Spring Boot项目(如果您还不会,可以参考这篇文章:快速入门)

第二步:在pom.xml中引入访问PostgreSQL需要的两个重要依赖:

1
2
3
4
5
6
7
8
9
10
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>

这里postgresql是必须的,spring-boot-starter-data-jpa的还可以替换成其他的数据访问封装框架,比如:MyBatis等,具体根据你使用习惯来替换依赖即可。因为已经是更上层的封装,所以基本使用与之前用MySQL是类似的,所以你也可以参考之前MySQL的文章进行配置,但数据源部分需要根据下面的部分配置。

第三步:在配置文件中为PostgreSQL数据库配置数据源、以及JPA的必要配置。

1
2
3
4
5
6
7
properties复制代码spring.datasource.url=jdbc:postgresql://localhost:5432/test
spring.datasource.username=postgres
spring.datasource.password=123456
spring.datasource.driver-class-name=org.postgresql.Driver

spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.properties.hibernate.hbm2ddl.auto=create

第四步:创建用户信息实体,映射user_info表(最后完成可在pgAdmin中查看)

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

@Id
@GeneratedValue
private Long id;

private String name;
private Integer age;

public UserInfo(String name, Integer age) {
this.name = name;
this.age = age;
}
}

第五步:创建用户信息实体的增删改查

1
2
3
4
5
6
7
8
9
10
java复制代码public interface UserInfoRepository extends JpaRepository<UserInfo, Long> {

UserInfo findByName(String name);

UserInfo findByNameAndAge(String name, Integer age);

@Query("from UserInfo u where u.name=:name")
UserInfo findUser(@Param("name") String name);

}

第六步:创建单元测试,尝试一下增删改查操作。

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
java复制代码@Slf4j
@SpringBootTest
public class ApplicationTests {

@Autowired
private UserInfoRepository userRepository;

@Test
public void test() throws Exception {
// 创建10条记录
userRepository.save(new UserInfo("AAA", 10));
userRepository.save(new UserInfo("BBB", 20));
userRepository.save(new UserInfo("CCC", 30));
userRepository.save(new UserInfo("DDD", 40));
userRepository.save(new UserInfo("EEE", 50));
userRepository.save(new UserInfo("FFF", 60));
userRepository.save(new UserInfo("GGG", 70));
userRepository.save(new UserInfo("HHH", 80));
userRepository.save(new UserInfo("III", 90));
userRepository.save(new UserInfo("JJJ", 100));

// 测试findAll, 查询所有记录
Assertions.assertEquals(10, userRepository.findAll().size());

// 测试findByName, 查询姓名为FFF的User
Assertions.assertEquals(60, userRepository.findByName("FFF").getAge().longValue());

// 测试findUser, 查询姓名为FFF的User
Assertions.assertEquals(60, userRepository.findUser("FFF").getAge().longValue());

// 测试findByNameAndAge, 查询姓名为FFF并且年龄为60的User
Assertions.assertEquals("FFF", userRepository.findByNameAndAge("FFF", 60).getName());

// 测试删除姓名为AAA的User
userRepository.delete(userRepository.findByName("AAA"));

// 测试findAll, 查询所有记录, 验证上面的删除是否成功
Assertions.assertEquals(9, userRepository.findAll().size());

}

}

把单元测试跑起来:

单元测试

一切顺利的话,因为这里用的是create策略,所以表还在,打开pgAdmin,可以看到user_info表自动创建出来了,里面的数据也可以查到,看看跟单元测试的逻辑是否符合。

PostgreSQL pgAdmin

思考一下

如果您之前有读过本系列教程中关于MySQL的10多篇使用案例,再看这篇使用PostgreSQL的案例,是不是感觉差别非常小?其实真正变动的部分主要是两个地方:

  1. 数据库驱动的依赖
  2. 数据源的配置信息

而对于更为上层的数据操作,其实并没有太大的变化,尤其是当使用Spring Data JPA的时候,这就是抽象的魅力所在!你体会到了吗?

好了,今天的学习就到这里!如果您在学习过程中遇到困难?可以加入我们超高质量的Spring技术交流群,参与交流与讨论,更好的学习与进步!更多Spring Boot教程可以点击直达!,欢迎收藏与转发支持!

代码示例

本文的完整工程可以查看下面仓库中2.x目录下的chapter6-4工程:

  • Github:github.com/dyc87112/Sp…
  • Gitee:gitee.com/didispace/S…

如果您觉得本文不错,欢迎Star支持,您的关注是我坚持的动力!

参考资料:

  • baike.baidu.com/item/Postgr…
  • www.biaodianfu.com/mysql-vs-po…

欢迎关注我的公众号:程序猿DD,分享外面看不到的干货与思考!

本文转载自: 掘金

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

还在用http工具类?一行代码解决

发表于 2021-10-09

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

以前的调用方式

最近项目上有个发起请求的需求.就开始找项目里的工具类.
发现项目里的http工具类五花八门 请求代码过长不够优雅. 具体的方法就不贴了 太占地方.
`

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
ini复制代码private static HttpsURLConnection initHttps(String url, String method,
Map<String, String> headers) throws Exception {
TrustManager[] tm = { new MyX509TrustManager() };
System.setProperty("https.protocols", "TLSv1");
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, tm, new java.security.SecureRandom());
// 从上述SSLContext对象中得到SSLSocketFactory对象
SSLSocketFactory ssf = sslContext.getSocketFactory();
URL _url = new URL(url);
HttpsURLConnection http = (HttpsURLConnection) _url.openConnection();
// 设置域名校验
http.setHostnameVerifier(new HttpUtil().new TrustAnyHostnameVerifier());
// 连接超时
http.setConnectTimeout(DEF_CONN_TIMEOUT);
// 读取超时 --服务器响应比较慢,增大时间
http.setReadTimeout(DEF_READ_TIMEOUT);
http.setUseCaches(false);
http.setRequestMethod(method);
http.setRequestProperty("Content-Type",
"application/x-www-form-urlencoded");
http.setRequestProperty(
"User-Agent",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.146 Safari/537.36");
if (null != headers && !headers.isEmpty()) {
for (Entry<String, String> entry : headers.entrySet()) {
http.setRequestProperty(entry.getKey(), entry.getValue());
}
}
http.setSSLSocketFactory(ssf);
http.setDoOutput(true);
http.setDoInput(true);
http.connect();
return http;
}
public static String get(String url, Map<String, String> params,
Map<String, String> headers) throws Exception {
HttpURLConnection http = null;
if (isHttps(url)) {
http = initHttps(initParams(url, params), _GET, headers);
} else {
http = initHttp(initParams(url, params), _GET, headers);
}
InputStream in = http.getInputStream();
BufferedReader read = new BufferedReader(new InputStreamReader(in,
DEFAULT_CHARSET));
String valueString = null;
StringBuffer bufferRes = new StringBuffer();
while ((valueString = read.readLine()) != null) {
bufferRes.append(valueString);
}
in.close();
if (http != null) {
http.disconnect();// 关闭连接
}
return bufferRes.toString();
}
public static String post(String url, String params)
throws Exception {
HttpURLConnection http = null;
if (isHttps(url)) {
http = initHttps(url, _POST, null);
} else {
http = initHttp(url, _POST, null);
}
OutputStream out = http.getOutputStream();
out.write(params.getBytes(DEFAULT_CHARSET));
out.flush();
out.close();

InputStream in = http.getInputStream();
BufferedReader read = new BufferedReader(new InputStreamReader(in,
DEFAULT_CHARSET));
String valueString = null;
StringBuffer bufferRes = new StringBuffer();
while ((valueString = read.readLine()) != null) {
bufferRes.append(valueString);
}
in.close();
if (http != null) {
http.disconnect();// 关闭连接
}
return bufferRes.toString();
}

链式调用

就想着有没有方便开箱就用的请求框架…发现了一款可以链式调用的OkHttps框架很好用.

调用示例

1
2
3
4
scss复制代码List<User> users = http.sync("/users") // 请求数据的链接🔗
.get() // GET请求
.getBody() // 获取响应报文体
.toList(User.class); // 得到目标数据

快速上手

引入maven包

1
2
3
4
5
xml复制代码<dependency>
<groupId>com.ejlchina</groupId>
<artifactId>okhttps</artifactId>
<version>3.1.5</version>
</dependency>

注意:单独使用 OkHttps 需要自定义MsgConvertor,否则无法使用 自动正反序列化 相关功能.

使用

  1. 构建 HTTP
1
ini复制代码HTTP http = HTTP.builder().build();
  1. 同步请求
1
2
3
4
ini复制代码List<User> users = http.sync("/users") 
.get()
.getBody()
.toList(User.class);
  1. 异步请求
1
2
3
4
5
6
scss复制代码http.async("/users")               
.setOnResponse((HttpResult res) -> {
// 得到目标数据
User user = res.getBody().toBean(User.class);
})
.get();
  1. WebSocket
1
2
3
4
5
6
7
8
9
10
11
scss复制代码http.webSocket("/chat") 
.setOnOpen((WebSocket ws, HttpResult res) -> {
ws.send("hello world");
})
.setOnMessage((WebSocket ws,Message msg) -> {
// 从服务器接收消息(自动反序列化)
Chat chat = msg.toBean(Chat.class);
// 相同的消息发送给服务器(自动序列化 Chat 对象)
ws.send(chat);
})
.listen();

一般请求分三步:

第一步、确定请求方式

同步 HTTP(sync)、异步 HTTP(async)或 WebSocket(webSocket)

第二步、构建请求任务

  • addXxxPara - 添加请求参数
  • setOnXxxx - 设置回调函数
  • tag - 添加标签
  • …

第三步、调用请求方法

HTTP 请求方法:

  • get() - GET 请求
  • post() - POST 请求
  • put() - PUT 请求
  • delete() - DELETE 请求
  • …

Websocket 方法:

  • listen() - 启动监听
    具体的介绍就到这里了..有想法的小朋友可以去官方文档查看:
    okhttps.ejlchina-app.com/v3/

本文转载自: 掘金

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

假如面试官让你聊聊Sentinel(哨兵),看完这篇文章足矣

发表于 2021-10-09

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

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

1、简介

主从复制奠定了Redis分布式的基础,但是普通的主从复制并不能达到高可用的状态。在普通的主从复制模式下,如果主服务器宕机,就只能通过运维人员手动切换主服务器,很显然这种方案并不可取。

针对上述情况,Redis官方推出了可抵抗节点故障的高可用方案——Redis Sentinel(哨兵)。Redis Sentinel(哨兵):由一个或多个Sentinel实例组成的Sentinel系统,它可以监视任意多个主从服务器,当监视的主服务器宕机时,自动下线主服务器,并且择优选取从服务器升级为新的主服务器。

​

如下示例:当旧Master下线时长超过用户设定的下线时长上限,Sentinel系统就会对旧Master执行故障转移操作,故障转移操作包含三个步骤:

  1. 在Slave中选择数据最新的作为新的Master
  2. 向其他Slave发送新的复制指令,让其他从服务器成为新的Master的Slave
  3. 继续监视旧Master,如果其上线则将旧Master设置为新Master的Slave

sentinel监视主服务器下线.png

本文基于如下资源清单进行开展:

IP地址 节点角色 端口
192.168.211.104 Redis Master/ Sentinel 6379/26379
192.168.211.105 Redis Slave/ Sentinel 6379/26379
192.168.211.106 Redis Slave/ Sentinel 6379/26379

​

2、Sentinel初始化与网络连接

Sentinel并没有什么特别神奇的地方,它就是一个更加简单的Redis服务器,在Sentinel启动的时候它会加载不同的命令表和配置文件,因此从本质上来讲Sentinel就是一个拥有较少命令和部分特殊功能的Redis服务。当一个Sentinel启动时它需要经历如下步骤:

  1. 初始化Sentinel服务器
  2. 替换普通Redis代码为Sentinel的专用代码
  3. 初始化Sentinel状态
  4. 根据用户给定的Sentinel配置文件,初始化Sentinel监视的主服务器列表
  5. 创建连接主服务器的网络连接
  6. 根据主服务获取从服务器信息,创建连接从服务器的网络连接
  7. 根据发布/订阅获取Sentinel信息,创建Sentinel之间的网络连接

2.1 初始化Sentinel服务器

Sentinel本质上就是一个Redis服务器,因此启动Sentinel需要启动一个Redis服务器,但是Sentinel并不需要读取RDB/AOF文件来还原数据状态。

​

2.2 替换普通Redis代码为Sentinel的专用代码

Sentinel用于较少的Redis命令,大部分命令在Sentinel客户端都不支持,并且Sentinel拥有一些特殊的功能,这些需要Sentinel在启动时将Redis服务器使用的代码替换为Sentinel的专用代码。在此期间Sentinel会载入与普通Redis服务器不同的命令表。

Sentinel不支持SET、DBSIZE等命令;保留支持PING、PSUBSCRIBE、SUBSCRIBE、UNSUBSCRIBE、INFO等指令;这些指令在Sentinel工作中提供了保障。

​

2.3 初始化Sentinel状态

装载Sentinel的特有代码之后,Sentinel会初始化sentinelState结构,该结构用于存储Sentinel相关的状态信息,其中最重要的就是masters字典。

1
2
3
4
5
6
7
8
9
10
11
arduino复制代码struct sentinelState {

    //当前纪元,故障转移使用
    uint64_t current_epoch; 

    // Sentinel监视的主服务器信息 
    // key -> 主服务器名称 
    // value -> 指向sentinelRedisInstance指针
    dict *masters; 
    // ...
} sentinel;

2.4 初始化Sentinel监视的主服务器列表

Sentinel监视的主服务器列表保存在sentinelState的masters字典中,当sentinelState创建之后,开始对Sentinel监视的主服务器列表进行初始化。

  • masters的key是主服务的名字
  • masters的value是一个指向sentinelRedisInstance指针

主服务器的名字由我们sentinel.conf配置文件指定,如下主服务器名字为redis-master(我这里是一主二从的配置):

1
2
3
4
5
6
7
8
bash复制代码daemonize yes
port 26379
protected-mode no
dir "/usr/local/soft/redis-6.2.4/sentinel-tmp"
sentinel monitor redis-master 192.168.211.104 6379 2
sentinel down-after-milliseconds redis-master 30000
sentinel failover-timeout redis-master 180000
sentinel parallel-syncs redis-master 1

sentinelRedisInstance实例保存了Redis服务器的信息(主服务器、从服务器、Sentinel信息都保存在这个实例中)。

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
arduino复制代码typedef struct sentinelRedisInstance {

    // 标识值,标识当前实例的类型和状态。如SRI_MASTER、SRI_SLVAE、SRI_SENTINEL
    int flags;

    // 实例名称 主服务器为用户配置实例名称、从服务器和Sentinel为ip:port
    char *name;

    // 服务器运行ID
    char *runid;

    //配置纪元,故障转移使用
    uint64_t config_epoch; 

    // 实例地址
    sentinelAddr *addr;

    // 实例判断为主观下线的时长 sentinel down-after-milliseconds redis-master 30000
    mstime_t down_after_period; 

    // 实例判断为客观下线所需支持的投票数 sentinel monitor redis-master 192.168.211.104 6379 2
    int quorum;

    // 执行故障转移操作时,可以同时对新的主服务器进行同步的从服务器数量 sentinel parallel-syncs redis-master 1
    int parallel-syncs;

    // 刷新故障迁移状态的最大时限 sentinel failover-timeout redis-master 180000
    mstime_t failover_timeout;

    // ...
} sentinelRedisInstance;

根据上面的一主二从配置将会得到如下实例结构:\

初始实例结构.png

2.5 创建连接主服务器的网络连接

当实例结构初始化完成之后,Sentinel将会开始创建连接Master的网络连接,这一步Sentinel将成为Master的客户端。

Sentinel和Master之间会创建一个命令连接和一个订阅连接:

  • 命令连接用于获取主从信息
  • 订阅连接用于Sentinel之间进行信息广播,每个Sentinel和自己监视的主从服务器之间会订阅sentinel:hello频道(注意Sentinel之间不会创建订阅连接,它们通过订阅sentinel:hello频道来获取其他Sentinel的初始信息)

命令连接和订阅连接.png

Sentinel在创建命令连接完成之后,每隔10秒钟向Master发送一次INFO指令,通过Master的回复信息可以获得两方面的知识:

  • Master本身的信息
  • Master下的Slave信息

主从信息.png

​

2.6 创建连接从服务器的网络连接

根据主服务获取从服务器信息,Sentinel可以创建到Slave的网络连接,Sentinel和Slave之间也会创建命令连接和订阅连接。\

Slave命令连接和订阅连接.png

当Sentinel和Slave之间创建网络连接之后,Sentinel成为了Slave的客户端,Sentinel也会每隔10秒钟通过INFO指令请求Slave获取服务器信息。

到这一步Sentinel获取到了Master和Slave的相关服务器数据。这其中比较重要的信息如下:

  • 服务器ip和port
  • 服务器运行id run id
  • 服务器角色role
  • 服务器连接状态mater_link_status
  • Slave复制偏移量slave_repl_offset(故障转移中选举新的Master需要使用)
  • Slave优先级slave_priority

此时实例结构信息如下所示:\

从服务器信息.png

2.7 创建Sentinel之间的网络连接

此时是不是还有疑问,Sentinel之间是怎么互相发现对方并且相互通信的,这个就和上面Sentinel与自己监视的主从之间订阅sentinel:hello频道有关了。

Sentinel会与自己监视的所有Master和Slave之间订阅sentinel:hello频道,并且Sentinel每隔2秒钟向sentinel:hello频道发送一条消息,消息内容如下:

PUBLISH sentinel:hello “,,,,,,,”

其中s代码Sentinel,m代表Master;ip表示IP地址,port表示端口、runid表示运行id、epoch表示配置纪元。

​

多个Sentinel在配置文件中会配置相同的主服务器ip和端口信息,因此多个Sentinel均会订阅sentinel:hello频道,通过频道接收到的信息就可获取到其他Sentinel的ip和port,其中有如下两点需要注意:

  • 如果获取到的runid与Sentinel自己的runid相同,说明消息是自己发布的,直接丢弃
  • 如果不相同,则说明接收到的消息是其他Sentinel发布的,此时需要根据ip和port去更新或新增Sentinel实例数据

Sentinel之间不会创建订阅连接,它们只会创建命令连接:\

sentinel之间的命令连接.png

此时实例结构信息如下所示:\

sentinel服务器信息.png

​

3、Sentinel工作

Sentinel最主要的工作就是监视Redis服务器,当Master实例超出预设的时限后切换新的Master实例。这其中有很多细节工作,大致分为检测Master是否主观下线、检测Master是否客观下线、选举领头Sentinel、故障转移四个步骤。

​

3.1 检测Master是否主观下线

Sentinel每隔1秒钟,向sentinelRedisInstance实例中的所有Master、Slave、Sentinel发送PING命令,通过其他服务器的回复来判断其是否仍然在线。​

1
erlang复制代码sentinel down-after-milliseconds redis-master 30000

在Sentinel的配置文件中,当Sentinel PING的实例在连续down-after-milliseconds配置的时间内返回无效命令,则当前Sentinel认为其主观下线。Sentinel的配置文件中配置的down-after-milliseconds将会对其sentinelRedisInstance实例中的所有Master、Slave、Sentinel都适应。

无效指令指的是+PONG、-LOADING、-MASTERDOWN之外的其他指令,包括无响应

如果当前Sentinel检测到Master处于主观下线状态,那么它将会修改其sentinelRedisInstance的flags为SRI_S_DOWN\

主观下线状态修改.png

​

3.2 检测Master是否客观下线

当前Sentinel认为其下线只能处于主观下线状态,要想判断当前Master是否客观下线,还需要询问其他Sentinel,并且所有认为Master主观下线或者客观下线的总和需要达到quorum配置的值,当前Sentinel才会将Master标志为客观下线。\

客观下线状态修改.png

当前Sentinel向sentinelRedisInstance实例中的其他Sentinel发送如下命令:

1
xml复制代码SENTINEL is-master-down-by-addr <ip> <port> <current_epoch> <runid>
  • ip:被判断为主观下线的Master的IP地址
  • port:被判断为主观下线的Master的端口
  • current_epoch:当前sentinel的配置纪元
  • runid:当前sentinel的运行id,runid

current_epoch和runid均用于Sentinel的选举,Master下线之后,需要选举一个领头Sentinel来选举一个新的Master,current_epoch和runid在其中发挥着重要作用,这个后续讲解。

​

接收到命令的Sentinel,会根据命令中的参数检查主服务器是否下线,检查完成后会返回如下三个参数:

  • down_state:检查结果1代表已下线、0代表未下线
  • leader_runid:返回*代表判断是否下线,返回runid代表选举领头Sentinel
  • leader_epoch:当leader_runid返回runid时,配置纪元会有值,否则一直返回0
  1. 当Sentinel检测到Master处于主观下线时,询问其他Sentinel时会发送current_epoch和runid,此时current_epoch=0,runid=*
  2. 接收到命令的Sentinel返回其判断Master是否下线时down_state = 1/0,leader_runid = *,leader_epoch=0

询问其他sentinel.png

​

3.3 选举领头Sentinel

down_state返回1,证明接收is-master-down-by-addr命令的Sentinel认为该Master也主观下线了,如果down_state返回1的数量(包括本身)大于等于quorum(配置文件中配置的值),那么Master正式被当前Sentinel标记为客观下线。

此时,Sentinel会再次发送如下指令:

1
xml复制代码SENTINEL is-master-down-by-addr <ip> <port> <current_epoch> <runid>

此时的runid将不再是0,而是Sentinel自己的运行id(runid)的值,表示当前Sentinel希望接收到is-master-down-by-addr命令的其他Sentinel将其设置为领头Sentinel。这个设置是先到先得的,Sentinel先接收到谁的设置请求,就将谁设置为领头Sentinel。

发送命令的Sentinel会根据其他Sentinel回复的结果来判断自己是否被该Sentinel设置为领头Sentinel,如果Sentinel被其他Sentinel设置为领头Sentinel的数量超过半数Sentinel(这个数量在sentinelRedisInstance的sentinel字典中可以获取),那么Sentinel会认为自己已经成为领头Sentinel,并开始后续故障转移工作(由于需要半数,且每个Sentinel只会设置一个领头Sentinel,那么只会出现一个领头Sentinel,如果没有一个达到领头Sentinel的要求,Sentinel将会重新选举直到领头Sentinel产生为止)。

​

3.4 故障转移

故障转移将会交给领头sentinel全权负责,领头sentinel需要做如下事情:

  1. 从原先master的slave中,选择最佳的slave作为新的master
  2. 让其他slave成为新的master的slave
  3. 继续监听旧master,如果其上线,则将其设置为新的master的slave

这其中最难的一步是如果选择最佳的新Master,领头Sentinel会做如下清洗和排序工作:

  1. 判断slave是否有下线的,如果有从slave列表中移除
  2. 删除5秒内未响应sentinel的INFO命令的slave
  3. 删除与下线主服务器断线时间超过down_after_milliseconds * 10 的所有从服务器
  4. 根据slave优先级slave_priority,选择优先级最高的slave作为新master
  5. 如果优先级相同,根据slave复制偏移量slave_repl_offset,选择偏移量最大的slave作为新master
  6. 如果偏移量相同,根据slave服务器运行id run id排序,选择run id最小的slave作为新master

新的Master产生后,领头sentinel会向已下线主服务器的其他从服务器(不包括新Master)发送SLAVEOF ip port命令,使其成为新master的slave。

​

到这里Sentinel的的工作流程就算是结束了,如果新master下线,则循环流程即可!

本文转载自: 掘金

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

用魔法打败魔法——解决Java代码中的魔法值

发表于 2021-10-08

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

  1. 问题

在日常代码编写过程中,在判断条件中经常会使用如下语句:

1
2
3
4
5
java复制代码if(!"1".equals(MapUtils.getString(user, "role"))){
...
}else{
...
}

该语句在程序运行时是正常的,但是却违反了开发规范,如果IDEA中使用了阿里巴巴Java开发规范插件,则此处会有提示:“魔法值【”1”】”、魔法值【”role”】。

image.png

点开规范提醒的详细信息,可以看到:不允许任何魔法值(即未经定义的常量)直接出现在代码中。

image.png

  1. 魔法值

1.1 魔法值概念

魔法值,也叫做魔法数值、魔法数字,通常是指在代码编写时莫名出现的数字,无法直接判断数值代表的含义,必须通过联系代码上下文分析才可以明白,严重降低了代码的可读性。

除数字之外,代码中作为key值的常量字符串也被认为是魔法值,尽管其表示含义比数值较为清晰,但是仍然会产生不规范问题。

1.2 为什么会出现魔法值

在对数据的处理过程中,为了充分利用机器的存储性能,数据库存储时往往使用更加简单的数值来代表复杂的名词含义,如使用1、2、3、4等数值来代表状态信息,0、1来代表false和true等。

由于数据库中存储的是数值数据,因此后端代码在与数据交互时,对数据进行验证同样需要使用数值进行判断,这就造成了后端代码中大量[魔法值]的存在。

这种编写方式不会产生任何执行错误,代码也是简洁的,但是代码含义非常不直观、且review和后期维护成本是巨大的。

1.3 魔法值的潜在危害

除了代码阅读不直观外,魔法值还可能造成以下问题的发生:

  • 数值使用不规范,多处使用不统一,修改时工作量大且容易遗漏
  • 数值使用错误,程序不产生异常,但业务逻辑数据出现问题
  • 常量字符串作为key时拼写错误,key值无对应value,导致数据异常或缓存无法命中
  1. 魔法值解决方法

2.1 静态常量方法

仅在当前类中使用或在方法内部使用的值,可以通过定义静态常量的方式来避免魔法值的出现。
如下使用定义静态常量的方式替换掉代码中的魔法值:

1
2
3
4
5
6
7
java复制代码public static final String ADMIN_ROLE = "1";
public static final String _NAME_ = "name";
if(!ADMIN_ROLE.equals(MapUtils.getString(user, _NAME_))){
...
}else{
...
}

静态常量定义时约定名称的所有字母都要大写,以此表示该标识为不变常量,如此这般使用之后,IDEA中的阿里巴巴规范便不再提示问题。

2.2 枚举类方法

枚举类是另一种更规范的消除魔法值的方法,使用时需要定义一个枚举类,并为类定义属性和构造方法等。枚举类能够约束静态常量的定义规范,提供统一格式的静态常量值,在统一异常等内容红广泛使用。

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

// '状态信息(1-未审核,2-已审核,3-审核通过, 4-审核作废)',
STATUS_INFO_1(1, "未审核"),
STATUS_INFO_2(2, "已审核"),
STATUS_INFO_3(3, "审核通过"),
STATUS_INFO_4(4, "审核作废");

private int code;
private String caption;
ComnCodeEnum(int code, String caption) {
this.code = code;
this.caption = caption;
}

public int code() {
return code;
}
public String caption(){
return caption;
}
}

定义了枚举类信息后,我们便可以在代码中使用枚举值来代替魔法值

1
2
3
4
5
java复制代码if(CommonCodeEnum.STATUS_INFO_1.code().equals(user.getStatus())){
...
}else{
...
}

2.3 消除魔法值的必要性

在使用静态常量或枚举类来替代代码中的魔法值时,我们发现大多数地方的代码并没有简化,反而更加复杂了,而我们原来的使用方式也没有不妥,因此会引发一种思考:大量重复使用的常量抽取定义是应该的,但是仅在类中使用一次或在方法中局部使用的字符串和数值,定义静态常量或枚举类是否有必要呢?

  • 这个问题可以说见仁见智了,代码的规范对于参与人数较多、模块精细化的大项目是十分有必要的,因为每个人都有自己的开发风格,将不同的个人风格杂揉到一个项目中,产生的那叫四不像。
  • 当然,对于个人开发的小项目,只要保证业务逻辑数据的正确性,对于开发规范也没那么重要,但是不得不说遵循规范的开发在后期维护时那叫一个流畅!
  1. 总结

在工作中学习,不断提升自己,通过解决代码中魔法值规范提醒的问题,发现:

  • 开发中要不断提升自己的代码规范意识,项目中一大半的bug都是由于代码不规范造成的
  • 善用各种代码规范工具,如阿里巴巴Java开发规范插件,代码质量扫描工具等,发现并解决问题、学习积累知识

本文转载自: 掘金

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

MyBatis 批量插入数据的 3 种方法!

发表于 2021-10-08

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

批量插入功能是我们日常工作中比较常见的业务功能之一,之前我也写过一篇关于《MyBatis Plus 批量数据插入功能,yyds!》的文章,但评论区的反馈不是很好,主要有两个问题:第一,对 MyBatis Plus(下文简称 MP)的批量插入功能很多人都有误解,认为 MP 也是使用循环单次插入数据的,所以性能并没有提升;第二,对于原生批量插入的方法其实也是有坑的,但鲜有人知。
​

所以综合以上情况,磊哥决定再来一个 MyBatis 批量插入的汇总篇,同时对 3 种实现方法做一个性能测试,以及相应的原理分析。
​

先来简单说一下 3 种批量插入功能分别是:

  1. 循环单次插入;
  2. MP 批量插入功能;
  3. 原生批量插入功能。

准备工作

开始之前我们先来创建数据库和测试数据,执行的 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
sql复制代码-- ----------------------------
-- 创建数据库
-- ----------------------------
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
DROP DATABASE IF EXISTS `testdb`;
CREATE DATABASE `testdb`;
USE `testdb`;

-- ----------------------------
-- 创建 user 表
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
`createtime` datetime NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Dynamic;

-- ----------------------------
-- 添加测试数据
-- ----------------------------
INSERT INTO `user` VALUES (1, '赵云', '123456', '2021-09-10 18:11:16');
INSERT INTO `user` VALUES (2, '张飞', '123456', '2021-09-10 18:11:28');
INSERT INTO `user` VALUES (3, '关羽', '123456', '2021-09-10 18:11:34');
INSERT INTO `user` VALUES (4, '刘备', '123456', '2021-09-10 18:11:41');
INSERT INTO `user` VALUES (5, '曹操', '123456', '2021-09-10 18:12:02');

SET FOREIGN_KEY_CHECKS = 1;

数据库的最终效果如下:
image.png

1.循环单次插入

接下来我们将使用 Spring Boot 项目,批量插入 10W 条数据来分别测试各个方法的执行时间。
​

循环单次插入的(测试)核心代码如下:

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
java复制代码import com.example.demo.model.User;
import com.example.demo.service.impl.UserServiceImpl;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class UserControllerTest {

// 最大循环次数
private static final int MAXCOUNT = 100000;

@Autowired
private UserServiceImpl userService;

/**
* 循环单次插入
*/
@Test
void save() {
long stime = System.currentTimeMillis(); // 统计开始时间
for (int i = 0; i < MAXCOUNT; i++) {
User user = new User();
user.setName("test:" + i);
user.setPassword("123456");
userService.save(user);
}
long etime = System.currentTimeMillis(); // 统计结束时间
System.out.println("执行时间:" + (etime - stime));
}
}

运行以上程序,花费了 88574 毫秒,如下图所示:
image.png

2.MP 批量插入

MP 批量插入功能核心实现类有三个:UserController(控制器)、UserServiceImpl(业务逻辑实现类)、UserMapper(数据库映射类),它们的调用流程如下:
image.png
注意此方法实现需要先添加 MP 框架,打开 pom.xml 文件添加如下内容:

1
2
3
4
5
xml复制代码<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>mybatis-plus-latest-version</version>
</dependency>

注意:mybatis-plus-latest-version 表示 MP 框架的最新版本号,可访问 mvnrepository.com/artifact/co… 查询最新版本号,但在使用的时候记得一定要将上面的 “mybatis-plus-latest-version”替换成换成具体的版本号,如 3.4.3 才能正常的引入框架。

更多 MP 框架的介绍请移步它的官网:baomidou.com/guide/

① 控制器实现

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
java复制代码import com.example.demo.model.User;
import com.example.demo.service.impl.UserServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

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

@RestController
@RequestMapping("/u")
public class UserController {

@Autowired
private UserServiceImpl userService;

/**
* 批量插入(自定义)
*/
@RequestMapping("/mysavebatch")
public boolean mySaveBatch(){
List<User> list = new ArrayList<>();
// 待添加(用户)数据
for (int i = 0; i < 1000; i++) {
User user = new User();
user.setName("test:"+i);
user.setPassword("123456");
list.add(user);
}
return userService.saveBatchCustom(list);
}
}

② 业务逻辑层实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.demo.mapper.UserMapper;
import com.example.demo.model.User;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper,User>
implements UserService {

@Autowired
private UserMapper userMapper;

public boolean saveBatchCustom(List<User> list){
return userMapper.saveBatchCustom(list);
}
}

③ 数据持久层实现

1
2
3
4
5
6
7
8
9
10
11
java复制代码import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.demo.model.User;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface UserMapper extends BaseMapper<User>{

boolean saveBatchCustom(List<User> list);
}

经过以上代码实现,我们就可以使用 MP 来实现数据的批量插入功能了,但本篇除了具体的实现代码之外,我们还要知道每种方法的执行效率,所以接下来我们来编写 MP 的测试代码。
​

MP 性能测试

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
java复制代码import com.example.demo.model.User;
import com.example.demo.service.impl.UserServiceImpl;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

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

@SpringBootTest
class UserControllerTest {

// 最大循环次数
private static final int MAXCOUNT = 100000;

@Autowired
private UserServiceImpl userService;

/**
* MP 批量插入
*/
@Test
void saveBatch() {
long stime = System.currentTimeMillis(); // 统计开始时间
List<User> list = new ArrayList<>();
for (int i = 0; i < MAXCOUNT; i++) {
User user = new User();
user.setName("test:" + i);
user.setPassword("123456");
list.add(user);
}
// MP 批量插入
userService.saveBatch(list);
long etime = System.currentTimeMillis(); // 统计结束时间
System.out.println("执行时间:" + (etime - stime));
}
}

以上程序的执行总共花费了 6088 毫秒,如下图所示:
image.png
从上述结果可知,使用 MP 的批量插入功能(插入数据 10W 条),它的性能比循环单次插入的性能提升了 14.5 倍。

MP 源码分析

从 MP 和循环单次插入的执行时间我们可以看出,使用 MP 并不是像有些朋友认为的那样,还是循环单次执行的,为了更清楚的说明此问题,我们查看了 MP 的源码。
​

MP 的核心实现代码是 saveBatch 方法,此方法的源码如下:
image.png
我们继续跟进 saveBatch 的重载方法:
image.png
从上述源码可以看出,MP 是将要执行的数据分成 N 份,每份 1000 条,每满 1000 条就会执行一次批量插入,所以它的性能要比循环单次插入的性能高很多。
​

那为什么要分批执行,而不是一次执行?别着急,当我们看了第 3 种实现方法之后我们就明白了。

3.原生批量插入

原生批量插入方法是依靠 MyBatis 中的 foreach 标签,将数据拼接成一条原生的 insert 语句一次性执行的,核心实现代码如下。

① 业务逻辑层扩展

在 UserServiceImpl 添加 saveBatchByNative 方法,实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.demo.mapper.UserMapper;
import com.example.demo.model.User;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
implements UserService {

@Autowired
private UserMapper userMapper;

public boolean saveBatchByNative(List<User> list) {
return userMapper.saveBatchByNative(list);
}

}

② 数据持久层扩展

在 UserMapper 添加 saveBatchByNative 方法,实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
java复制代码import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.demo.model.User;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface UserMapper extends BaseMapper<User> {

boolean saveBatchByNative(List<User> list);
}

③ 添加 UserMapper.xml

创建 UserMapper.xml 文件,使用 foreach 标签拼接 SQL,具体实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
java复制代码<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.UserMapper">
<insert id="saveBatchByNative">
INSERT INTO `USER`(`NAME`,`PASSWORD`) VALUES
<foreach collection="list" separator="," item="item">
(#{item.name},#{item.password})
</foreach>
</insert>

</mapper>

经过以上步骤,我们原生的批量插入功能就实现的差不多了,接下来我们使用单元测试来查看一下此方法的执行效率。

原生批量插入性能测试

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
java复制代码import com.example.demo.model.User;
import com.example.demo.service.impl.UserServiceImpl;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

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

@SpringBootTest
class UserControllerTest {

// 最大循环次数
private static final int MAXCOUNT = 100000;

@Autowired
private UserServiceImpl userService;

/**
* 原生自己拼接 SQL,批量插入
*/
@Test
void saveBatchByNative() {
long stime = System.currentTimeMillis(); // 统计开始时间
List<User> list = new ArrayList<>();
for (int i = 0; i < MAXCOUNT; i++) {
User user = new User();
user.setName("test:" + i);
user.setPassword("123456");
list.add(user);
}
// 批量插入
userService.saveBatchByNative(list);
long etime = System.currentTimeMillis(); // 统计结束时间
System.out.println("执行时间:" + (etime - stime));
}
}

然而,当我们运行程序时却发生了以下情况:
image.png
纳尼?程序的执行竟然报错了。
​

缺点分析

从上述报错信息可以看出,当我们使用原生方法将 10W 条数据拼接成一个 SQL 执行时,由于拼接的 SQL 过大(4.56M)从而导致程序执行报错,因为默认情况下 MySQL 可以执行的最大 SQL(大小)为 4M,所以程序就报错了。
​

这就是原生批量插入方法的缺点,也是为什么 MP 需要分批执行的原因,就是为了防止程序在执行时,因为触发了数据库的最大执行 SQL 而导致程序执行报错。

解决方案

当然我们也可以通过设置 MySQL 的最大执行 SQL 来解决报错的问题,设置命令如下:

1
2
sql复制代码-- 设置最大执行 SQL 为 10M
set global max_allowed_packet=10*1024*1024;

如下图所示:
image.png

注意:以上命令需要在 MySQL 连接的客户端中执行。

但以上解决方案仍是治标不治本,因为我们无法预测程序中最大的执行 SQL 到底有多大,那么最普世的方法就是分配执行批量插入的方法了(也就是像 MP 实现的那样)。

当我们将 MySQL 的最大执行 SQL 设置为 10M 之后,运行以上单元测试代码,执行的结果如下:
image.png

总结

本文我们介绍了 MyBatis 批量插入的 3 种方法,其中循环单次插入的性能最低,也是最不可取的;使用 MyBatis 拼接原生 SQL 一次性插入的方法性能最高,但此方法可能会导致程序执行报错(触发了数据库最大执行 SQL 大小的限制),所以综合以上情况,可以考虑使用 MP 的批量插入功能。

关注公众号「Java中文社群」查看更多 MyBatis 和 Spring Boot 的系列文章。

本文转载自: 掘金

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

Windows下安装、使用Pycharm教程,这下全了

发表于 2021-10-08

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

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

我是老表,Python终身学习者,数据分析爱好者,宠猫狂人~

零、为什么写这个

前天电脑出了点问题,所有软件配置全没了,环境变量也出了点问题,于是就把Pycharm卸载了,于是乎,想把整个从下载到安装破解使用的过程记录下来,给需要的读者朋友。

一、下载

浏览器打开网站:www.jetbrains.com/pycharm/
或者直接点击这里进入
建议使用谷歌浏览器,可以直接翻译成中文
[谷歌浏览器效果]
在这里插入图片描述
[其他浏览器效果]
在这里插入图片描述
[下载专业版](现在:初学者推荐直接使用社区版,比较方便,学习不受影响)
在这里插入图片描述
选择好下载的安装包目录,自己记得就好,建议选择桌面,免得搞完不知道放哪了!

二、安装

[双击刚刚下载的安装包,最好的是右键,管理员权限运行]
在这里插入图片描述
[Next]
在这里插入图片描述
[选择安装路径,Next]
在这里插入图片描述
[看着√,Next]
在这里插入图片描述
[Next,Next]
在这里插入图片描述

三、使用方法

3.1 推荐:使用社区版,安装后可以直接使用

3.2 专业版使用方法

3.2.1 推荐:采用学生/教师认证、Github项目认证申请使用专业版
  • 学生/教师认证 后免费使用方法说明
1
url复制代码https://sales.jetbrains.com/hc/zh-cn/articles/207154369-学生授权申请方式
  • 申请地址
1
url复制代码https://www.jetbrains.com/community/education/#students
  • 通过开源项目申请说明及入口
1
url复制代码https://www.jetbrains.com/zh-cn/community/opensource/#support
3.2.2 不推荐:科(po)学(jie)方法使用专业版

需要提前[下载科学补丁文件],网上找一下,直接百度搜索即可。

1.将刚刚下载的科学补丁放置到 pycharm安装目录的\bin目录下(位置可随意,只要配置文件填写相对应的路径)。
在这里插入图片描述
2.在 Pycharm安装目录的\bin目录下找到 pycharm.exe.vmoptions 和 pycharm64.exe.vmoptions ,以文本格式打开并同时在两个文件最后追加 -javaagent:你pycharm的安装路径\bin\JetbrainsCrack-release-enc.jar,注意路径修改成你的pycharm安装路径,然后保存。
在这里插入图片描述
确保上述操作无误
[启动Pycharm,输入下面注册码],注册码网上找一下,比较难找到有用的,所以还是推荐直接。。
在这里插入图片描述
在这里插入图片描述
再次说明,不推荐使用这种方法获取专业版使用,太麻烦,而且不一定有效。

四、使用

[双击桌面Pycharm图标,选择创建一个新项目]
在这里插入图片描述
[创建一个项目的基本配置]
在这里插入图片描述
我们选择建立在一个真实的环境上,点击···,找到自己的环境安装目录
我的是C:\Users\82055\AppData\Local\Programs\Python\Python36
在这里插入图片描述
[点击Create,完成项目创建]
在这里插入图片描述
[创建一个项目代码文件(.py),并输入代码,运行]
点击项目名,右键,New,选择PythonFile
在这里插入图片描述
在这里插入图片描述
[输入运行代码]
在这里插入图片描述
[运行后控制台显示出我们打印的文字]
在这里插入图片描述

!希望对大家有所帮助!我是老表,Python终身学习者,数据分析爱好者,宠猫狂人~

本文转载自: 掘金

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

1…504505506…956

开发者博客

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