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

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


  • 首页

  • 归档

  • 搜索

requests的使用(一)

发表于 2021-11-25

响应代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
python复制代码import requests
r = requests.get("http://www.baidu.com")

print(type(r)) # <class 'requests.models.Response'>
print(r.status_code) # 200
print(r.url) # http://www.baidu.com/
print(r.headers) #{'Cache-Control': 'private, no-cache, no-store, proxy-revalidate, no-transform', 'Connection': 'keep-alive', 'Content-Encoding': 'gzip', 'Content-Type': 'text/html', 'Date': 'Thu, 25 Nov 2021 08:56:15 GMT', 'Last-Modified': 'Mon, 23 Jan 2017 13:27:52 GMT', 'Pragma': 'no-cache', 'Server': 'bfe/1.0.8.18', 'Set-Cookie': 'BDORZ=27315; max-age=86400; domain=.baidu.com; path=/', 'Transfer-Encoding': 'chunked'}
print(r.cookies) # <RequestsCookieJar[<Cookie BDORZ=27315 for .baidu.com/>]>
print(r.content) # 网页内容
print(type(r.content)) # <class 'bytes'>
print(r.text) # 网页内容
print(type(r.text)) # <class 'str'>
print(r.history) # []
print(r.encoding) # ISO-8859-1
print(r.is_redirect) # 是否重定向 False
print(r.links) # 子链接 {}

get方式的代码

1
2
3
4
5
6
7
8
9
10
11
12
python复制代码import requests

data = {"name":"waws","age":18}
headers = {"User-Agent": "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0);"}
url = "http://httpbin.org/get"
req = requests.get(url,params=data,headers=headers)
# 标准解析格式
print(req.content.decode("utf8","ignore"))

# json解析获得字典
print(req.json()) # {'args': {'age': '18', 'name': 'waws'}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Host': 'httpbin.org', 'User-Agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0);', 'X-Amzn-Trace-Id': 'Root=1-619f5084-5a1b4b6a2c4332e12ee045ab'}, 'origin': 'xxx.xxx.xxx.xx', 'url': 'http://httpbin.org/get?name=waws&age=18'
print(type(req.json())) # <class 'dict'>

上面标准化后的数据的样子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
json复制代码{
"args": {
"age": "18",
"name": "waws"
},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Host": "httpbin.org",
"User-Agent": "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0);",
"X-Amzn-Trace-Id": "Root=1-619f5084-5a1b4b6a2c4332e12ee045ab"
},
"origin": "xxx.x.xxx.xxx",
"url": "http://httpbin.org/get?name=waws&age=18"
}

post方式的代码

注意post方式中传递的参数是data,而get 是params

1
2
3
4
5
6
python复制代码import requests
data = {"user":"admin","pwd":"admin"}
headers = {"User-Agent": "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0);"}
url = "https://httpbin.org/post"
req = requests.post(url,data=data,headers=headers)
print(req.content.decode("utf8","ignore"))

标准化后的数据的样子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
json复制代码{
"args": {},
"data": "",
"files": {},
"form": {
"pwd": "admin",
"user": "admin"
},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Content-Length": "20",
"Content-Type": "application/x-www-form-urlencoded",
"Host": "httpbin.org",
"User-Agent": "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0);",
"X-Amzn-Trace-Id": "Root=1-619f5125-26e1e1e526026c547bb269ed"
},
"json": null,
"origin": "xxx.xxx.xxx.xx",
"url": "https://httpbin.org/post"
}

本文转载自: 掘金

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

Java并发机制底层实现原理之final

发表于 2021-11-25

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

一、final基础使用

1.1、修饰类

当某个类定义为final时,即这个类是不能有子类,final类中的所有方法都隐式为final,无法覆盖它们,所以在final类中给任何方法添加关键字是没有任何意义的。

1
2
arduino复制代码final类型的类如何拓展?
设计模式中最重要的两种关系,一种是继承/实现;另外一种是组合关系。所以当遇到不能继承的(final修饰的类),考虑用组合。
1.2、修饰方法
  • private方法是隐式的final
  • final方法是可以被重载的

private final:类中所有private方法都隐式地指定为final的,由于无法取用private方法,所以也就不能覆盖它,对private方法增添final关键字,这样做并没有什么好处。

​

final方法可以被重载:父类的final方法是不能够被子类重写的,那么final方法可以被重载?可以的

​

1.3、修饰参数

Java允许在参数列表中以声明的方式将参数指明为final,这意味着无法在方法中更改参数引用所指向的对象,这个特性主要用来向匿名内部类传递数据。

​

1.4、修饰变量

所有的final修饰的字段都是编译期常量吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
arduino复制代码public class Test {
    //编译期常量
    final int i = 1;
    final static int J = 1;
    final int[] a = {1,2,3,4};
    //非编译期常量
    Random r = new Random();
    final int k = r.nextInt();

    public static void main(String[] args) {

    }
}

k的值由随机数对象决定,所以不是所有的final修饰的字段都是编译期常量,只是k的值在被始化后无法被更改。

1.5、static final

一个既是static又是final的字段只占据一段不能改变的存储空间,它必须在定义的时候进行赋值,否则编译器将不予通过。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码import java.util.Random;
public class Test {
    static Random r = new Random();
    final int k = r.nextInt(10);
    static final int k2 = r.nextInt(10); 
    public static void main(String[] args) {
        Test t1 = new Test();
        System.out.println("k="+t1.k+" k2="+t1.k2);
        Test t2 = new Test();
        System.out.println("k="+t2.k+" k2="+t2.k2);
    }
}

执行结果输出:

1
2
ini复制代码k=2 k2=7
k=8 k2=7

static关键字所修饰的字段并不属于一个对象,而是属于这个类的,也可以理解为static final所修饰的字段仅占据内存的一个一份空间,一旦被初始化之后便不会被更改。

二、final域重排序规则

2.1、final域为基本类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
csharp复制代码public class FinalDemo {
    private int a;  //普通域
    private final int b; //final域
    private static FinalDemo finalDemo;

    public FinalDemo() {
        a = 1; // 1. 写普通域
        b = 2; // 2. 写final域
    }

    public static void writer() {
        finalDemo = new FinalDemo();
    }

    public static void reader() {
        FinalDemo demo = finalDemo; // 3.读对象引用
        int a = demo.a;    //4.读普通域
        int b = demo.b;    //5.读final域
    }
}

假设线程A在执行writer()方法,线程B执行reader()方法。

​

写final域重排序规则:

​

写final域的重排序规则禁止对final域的写重排序到构造函数之外,这个规则的实现主要包含两个方面:

1、JMM禁止编译器把final域的写重排序到构造函数之外;

2、编译器会在final域写之后,构造函数return之前,插入一个storestore屏障。这个屏障可以禁止处理器把final域的写重排序到构造函数之外。

​

分析writer方法:

1、构造了一个FinalDemo对象;

2、把这个对象赋值给成员变量finalDemo.

​

image.png

image.png

由于a、b之间没有数据依赖性,普通域(普通变量)a可能会被重排序到构造函数之外,线程B就有可能读到的是普通变量a初始化之前的值(0),这样就可能出现错误。而final域变量b,根据重排序规则,会禁止final修饰的变量b重排序到构造函数之外,从而b能够正确赋值,线程B就能够读到final变量初始化后的值。

因此,写final域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了,而普通域不具有这个保障。

​

读final域重排序规则:

​

读final域重排序规则为:在一个线程中,初次读对象引用和初次读该对象包含final域,JMM会禁止这两个操作的重排序,处理器会在读final域操作的前面插入一个LoadLoad屏障,实际上,读对象的引用和读该对象的final域存在间接依赖性,一般处理器不会重排序这两个操作。但是有一些处理器会重排序。因此,这条禁止重排序规则就是针对这些处理器而设定的。

​

read()方法主要包含三个操作:

1、初次读引用变量finalDemo;

2、初次读引用变量finalDemo的普通域a;

3、初次读引用变量finalDemo的final域b;

​

image.png

image.png

读对象的普通域被重排序到了读对象引用的前面就会出现线程B还未读到对象引用就在读取该对象的普通域变量,这显示是错误的操作。而final域的读操作就限定了在读final域变量前已经读到了该对象的引用,从而就可以避免这种情况。

​

读final域的重排序规则可以确保:在读一个对象的final域之前,一定会先读这个包含这个final域的对象的引用。

​

2.2、final域为引用类型

对final修饰的对象的成员域写操作:

​

针对引用数据类型,final域写针对编译器和处理器重排序增加了这样的约束:在构造函数内对一个final修饰的对象的成员域的写入,与随后在构造函数之外把这个被构造的对象的引用赋给一个引用变量,这两个操作是不能被重排序的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
csharp复制代码public class FinalReferenceDemo {
    final int[] arrays;
    private FinalReferenceDemo finalReferenceDemo;

    public FinalReferenceDemo() {
        arrays = new int[1];  //1
        arrays[0] = 1;        //2
    }

    public void writerOne() {
        finalReferenceDemo = new FinalReferenceDemo(); //3
    }

    public void writerTwo() {
        arrays[0] = 2;  //4
    }

    public void reader() {
        if (finalReferenceDemo != null) {  //5
            int temp = finalReferenceDemo.arrays[0];  //6
        }
    }
}

假如线程A执行writerOne方法,执行完后线程B执行writerTwo方法,然后线程C执行reader方法\

image.png

image.png

由于对final域的写禁止重排序到构造方法外,因此1和3不能被重排序。由于一个final域的引用对象的成员域写入不能与随后将这个被构造出来的对象赋给引用变量重排序,因此2和3不能重排序

​

对final修饰的对象的成员域读操作

​

JMM可以确保线程C至少能看到写线程A对final引用的对象的成员域的写入,即能看下arrays[0] = 1,而写线程B对数组元素的写入可能看到可能看不到。JMM不保证线程B的写入线程C可见,线程B 和线程C之间存在数据竞争,此时的结果是不可预知。如果可见的,可使用锁或者volatile.

本文转载自: 掘金

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

冒泡排序升级之【鸡尾酒排序】

发表于 2021-11-25

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

欢迎关注公众号OpenCoder,来和我做朋友吧~❤😘😁🐱‍🐉👀

一.鸡尾酒排序概念

鸡尾酒排序,是冒泡排序算法的一种升级。冒泡排序的每个元素都可以像气泡一样,根据自身大小,一点点的向着数组的某侧移动。算法每一轮都是从左到右来比较元素,进行单向的位置交换的。而鸡尾酒排序是在此基础上元素比较和交换过程变成了双向的。固鸡尾酒排序又称双向冒泡排序、鸡尾酒搅拌排序、搅拌排序、涟漪排序、来回排序或快乐小时排序, 是冒泡排序的一种变形。

鸡尾酒排序最糟或是平均所花费的次数都是O(n²),但如果序列在一开始已经大部分排序过的话,会接近O(n)。

二. 逻辑推演

问题分析:

现有一个数组,里面的数据为: [2,3,4,5,6,7,8,1],我们以此数据来分析:

image-20211013110818725

冒泡排序过程如下:

image-20211013111356725

image-20211013162036391

结果分析:

元素 2、3、4、5、6、7、8已经是有序的了,只需要将元素1的放到正确的位置就可以了,却还是进行了7轮排序,这也太不方便了。要是能直接将1的位置进行调整,数列就有序了。鸡尾酒排序正是要解决这个问题的。

优化思路:

鸡尾酒详细过程:

第一轮(和冒泡排序一样,8和1交换)

image-20211013162754692

第二轮:此时开始不一样了,我们反过来从右往左比较进行交换。

image-20211013163352045

image-20211013163912858

第三轮:虽然实际上已经有序,但是流程并没有结束。

在鸡尾酒排序的第三轮,需要重新从左向右比较进行交换。

1和2比较,位置不变;2和3比较,位置不变;3和4比较,位置不变…7和8比较位置不变。没有元素进行交换,证明当前有序,排序结束。

小结:

本来需要7轮的排序场景,用3轮就解决了,鸡尾酒排序就是这样巧妙的算法。

而鸡尾酒排序的思路,排序过程就像钟摆一样,第一轮从左往右比较,第二轮从右往左比较,第三轮再从左往右比较…

三.鸡尾酒排序算法

代码:

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复制代码    public static void sort(int[] array) {
       //循环计数
       int count = 0;
       //数据交换临时变量
       int tmp = 0;
       for (int i = 0; i < array.length / 2; i++) {
           System.out.println("第" + (++count) + "次循环");
           //每轮的初始值都是true,有序标记:true代表有序
           boolean isSorted = true;
           //奇数轮,从左向右比较和交换
           for (int j = i; j < array.length - i - 1; j++) {
​
               if (array[j] > array[j + 1]) {
                   tmp = array[j];
                   array[j] = array[j + 1];
                   array[j + 1] = tmp;
                   //发生交换,不是有序,标记改成false
                   isSorted = false;
              }
          }
           //是否有序
           if (isSorted) {
               break;
          }
           //在偶数轮之前,将isSorted重新标记为true
           isSorted = true;
           System.out.println("第" + (++count) + "次循环");
           //偶数轮,方向从右向左比较和交换
           for (int j = array.length - i - 1; j > i; j--) {
               if (array[j] < array[j - 1]) {
                   tmp = array[j];
                   array[j] = array[j - 1];
                   array[j - 1] = tmp;
                   isSorted = false;
              }
          }
           //是否有序
           if (isSorted) {
               break;
          }
      }
  }

结果输出:

1
2
3
4
java复制代码第1次循环
第2次循环
第3次循环
[1, 2, 3, 4, 5, 6, 7, 8]

总结:

这段代码是鸡尾酒排序的原始实现。代码外部的大循环控制所有排序回合,大循环内部包含两个小循环,第1个小循环从左往右比较并交换元素,第2个小循环从右往左比较并交换元素。

鸡尾酒的优势是,在大部分元素已经有序的情况下,减少排序的回合数;而缺点也很明显,就是代码量几乎翻了一倍。

欢迎关注公众号OpenCoder,来和我做朋友吧~❤😘😁🐱‍🐉👀

本文转载自: 掘金

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

刷了点力扣,来看看Arrayssort的原理吧

发表于 2021-11-25

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

hi ,大家好,我是三天打鱼,两天晒网的小六六

前言

文本已收录至我的GitHub仓库,欢迎Star:github.com/bin39232820…

种一棵树最好的时间是十年前,其次是现在

最近小六六业余时间都会刷点算法,至于原因当然是小六六太菜了,哈哈,是真的菜,不过刷了一些题后,发现自己的脑子不那么傻了,看来刷力扣还是有助于思维的提升,建议大家业余的时候刷刷,当然算法思维对于我们写的代码的性能也是有帮助的呢?刷力扣的过程中,经常会用到Arrays.sort这个方法,今天小六六就给大家分享分享这个方法,看看Java的JDK是怎么去做排序的

image.png

常见的排序算法

要了解Arrays.sort的底层原理,我们先来看看我们耳熟能详的排序算法吧,这边只是大概的提提这些算法,我们一般在开发的过程中,会碰到以下的排序算法

算法一:插入排序

插入排序示意图

g7pk0bpfgb.gif

插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

算法步骤:

1)将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。

2)从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)

算法二:选择排序

选择排序示意图

c3kk4nrkcz.gif

选择排序
(Selection sort)也是一种简单直观的排序算法。

算法步骤:

1)首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置

2)再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。

3)重复第二步,直到所有元素均排序完毕。

算法三:冒泡排序

冒泡排序示意图

j6bz6vne4q.gif

冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

算法步骤:

1)比较相邻的元素。如果第一个比第二个大,就交换他们两个。

2)对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。

3)针对所有的元素重复以上的步骤,除了最后一个。

4)持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

算法四:归并排序

归并排序示意图

s79zjg4atb.gif

归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。

算法步骤:

  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置
  3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
  4. 重复步骤3直到某一指针达到序列尾
  5. 将另一序列剩下的所有元素直接复制到合并序列尾

算法五:快速排序

快速排序示意图

mwh79s0eqx.gif

快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要Ο(n log n)次比较。在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。

快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。

算法步骤:

1 从数列中挑出一个元素,称为 “基准”(pivot),

2 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。

3 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会退出,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。

当然还有其他的几种排序算法,大家也去了解下,但是我们常用的就上面5种了

来看看Arrays.sort和Collections.sort实现原理解析

  1. 事实上Collections.sort方法底层就是调用的array.sort方法,而且不论是Collections.sort或者是Arrays.sort方法,
  2. 跟踪下源代码吧,首先我们写个demo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ini复制代码
Arrays.sort(nums1);
Arrays.sort(nums2);
int length1 = nums1.length, length2 = nums2.length;
int[] intersection = new int[Math.min(length1, length2)];
int index1 = 0, index2 = 0, index = 0;
while (index1 < length1 && index2 < length2) {
if (nums1[index1] < nums2[index2]) {
index1++;
} else if (nums1[index1] > nums2[index2]) {
index2++;
} else {
intersection[index] = nums1[index1];
index1++;
index2++;
index++;
}
}
return Arrays.copyOfRange(intersection, 0, index);
  1. 点进去看看
    上面的英文注释解释:
    将指定的数组按升序数字排序。
    实现说明:排序算法是由Vladimir Yaroslavskiy, Jon Bentley和Joshua Bloch设计的双枢轴快速排序。该算法在许多数据集上提供了O(n log(n))的性能,这导致其他快速排序的性能下降到二次的性能,而且通常比传统的(单轴)快速排序实现更快。

image.png

  • 快速排序使用的是分治思想,将原问题分成若干个子问题进行递归解决。选择一个元素作为轴(pivot),通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比轴元素小,另外一部分的所有数据都比轴元素大,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
  • 双轴快排(DualPivotQuicksort),顾名思义有两个轴元素pivot1,pivot2,且pivot ≤pivot2,将序列分成三段:x < pivot1、pivot1 ≤ x ≤ pivot2、x >pivot2,然后分别对三段进行递归。这个算法通常会比传统的快排效率更高,也因此被作为Arrays.java中给基本类型的数据排序的具体实现。
  1. 接着看我们重要的sort方法
  • 判断数组的长度是否大于286,大于则使用归并排序(merge sort),否则执行下一步
1
2
3
4
5
6
7
kotlin复制代码// Use Quicksort on small arrays
if (right - left < QUICKSORT_THRESHOLD)
{
//QUICKSORT_THRESHOLD = 286
sort(a, left, right, true);
return;
}
  • 小过这个阀值的进入Quicksort (快速排序),其实并不全是,点进去sort(a, left, right, true);方法:
1
2
3
4
5
6
scss复制代码// Use insertion sort on tiny arrays
if (length < INSERTION_SORT_THRESHOLD)
{
if (leftmost)
{
......
  • 点进去后我们看到第二个阀值INSERTION_SORT_THRESHOLD(47),如果元素少于47这个阀值,就用插入排序,往下看确实如此:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
css复制代码/*
* Traditional (without sentinel) insertion sort,
* optimized for server VM, is used in case of
* the leftmost part.
*/
for (int i = left, j = i; i < right; j = ++i)
{
int ai = a[i + 1];
while (ai < a[j])
{
a[j + 1] = a[j];
if (j-- == left)
{
break;
}
}
a[j + 1] = ai;
  • 这是少于阀值QUICKSORT_THRESHOLD(286)的两种情况,至于大于286的,它会进入归并排序(Merge Sort),但在此之前,它有个小动作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
css复制代码
// Check if the array is nearly sorted
for (int k = left; k < right; run[count] = k) { if (a[k] < a[k + 1]) { // ascending
while (++k <= right && a[k - 1] <= a[k]);
} else if (a[k] > a[k + 1]) { // descending
while (++k <= right && a[k - 1] >= a[k]); for (int lo = run[count] - 1, hi = k; ++lo < --hi; ) { int t = a[lo]; a[lo] = a[hi]; a[hi] = t;
}
} else { // equal
for (int m = MAX_RUN_LENGTH; ++k <= right && a[k - 1] == a[k]; ) { if (--m == 0) {
sort(a, left, right, true); return;
}
}
} /*
* The array is not highly structured,
* use Quicksort instead of merge sort.
*/
if (++count == MAX_RUN_COUNT) {
sort(a, left, right, true); return;
}
}

总结

我们来看看Arrays.sort整一个流程图吧

image.png

O(nlogn)只代表增长量级,同一个量级前面的常数也可以不一样,不同数量下面的实际运算时间也可以不一样。

数量非常小的情况下(就像上面说到的,少于47的),插入排序等可能会比快速排序更快。 所以数组少于47的会进入插入排序。

快排数据越无序越快(加入随机化后基本不会退化),平均常数最小,不需要额外空间,不稳定排序。

归排速度稳定,常数比快排略大,需要额外空间,稳定排序。

所以大于或等于47或少于286会进入快排,而在大于或等于286后,会有个小动作:“// Check if the array is nearly sorted”。这里第一个作用是先梳理一下数据方便后续的双枢轴归并排序,第二个作用就是即便大于286,但在降序组太多的时候(被判断为没有结构的数据,The array is not highly structured,use Quicksort instead of merge sort.),要转回快速排序。

参考

  • 排序算法详解

本文转载自: 掘金

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

Redis 源码简洁剖析 02 - SDS 字符串 C 语言

发表于 2021-11-25

C 语言的字符串函数

C 语言 string 函数,在 C 语言中可以使用 char* 字符数组实现字符串,C 语言标准库 string.h 中也定义了多种字符串操作函数。

字符串使用广泛,需要满足:

  • 高效的字符串操作,比如追加、拷贝、比较、获取长度
  • 能保存任意的二进制数据,比如图片
  • 尽可能省内存

为什么 Redis 不直接使用 C 语言的字符串?

  • C 语言 char* 以 ‘\0’标识字符串的结束,则中间含有’\0’的字符串无法被正确表示;也正因为此,没有办法保存图像等二进制数据。
  • C 语言 char* 获取字符串长度的时间复杂度是 O(N);追加字符串的时间复杂度也是 O(N),同时可能由于可用空间不足,无法追加。

下面代码展示了 C 语言中 ‘\0’ 结束字符对字符串的影响。下图展示了一个值为 “Redis” 的 C 字符串:

1
2
3
4
5
6
7
8
9
c复制代码#include "stdio.h"
#include "string.h"

int main(void) {
char *a = "red\0is";
char *b = "redis\0";
printf("%lu\n", strlen(a));
printf("%lu\n", strlen(b));
}

输出结果是 3 和 5。

SDS 定义

SDS(简单动态字符串) 是 simple dynamic string 的简称,Redis 使用 SDS 作为字符串的数据结构。Redis 中所有的键(key)底层都是 SDS 实现的。

比如:

1
2
shell复制代码redis> SET msg "hello world"
OK
1
2
bash复制代码redis> RPUSH fruits "apple" "banana" "cherry"
(integer) 3

Redis sds 源码主要在 sds.h 和 sds.c 中。其中可以发现 Redis 给 char* 起了别名:

1
c复制代码typedef char *sds;

SDS 内部结构

SDS 结构中有一个元数据 flags,表示的是 SDS 类型(最低 3 位)。事实上,SDS 一共设计了 5 种类型,分别是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64。这 5 种类型的主要区别就在于,它们数据结构中的字符数组现有长度 len 和分配空间长度 alloc,这两个元数据的数据类型不同。

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
c复制代码/* Note: sdshdr5 is never used, we just access the flags byte directly.
* However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* used */
uint8_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len; /* used */
uint16_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len; /* used */
uint32_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /* used */
uint64_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
c复制代码static inline size_t sdslen(const sds s) {
unsigned char flags = s[-1];
switch(flags&SDS_TYPE_MASK) {
case SDS_TYPE_5:
return SDS_TYPE_5_LEN(flags);
case SDS_TYPE_8:
return SDS_HDR(8,s)->len;
case SDS_TYPE_16:
return SDS_HDR(16,s)->len;
case SDS_TYPE_32:
return SDS_HDR(32,s)->len;
case SDS_TYPE_64:
return SDS_HDR(64,s)->len;
}
return 0;
}

获取剩余容量:sdsavail 函数,总容量 alloc - 已使用长度 len,时间复杂度是 O(1)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
c复制代码static inline size_t sdsavail(const sds s) {
unsigned char flags = s[-1];
switch(flags&SDS_TYPE_MASK) {
case SDS_TYPE_5: {
return 0;
}
case SDS_TYPE_8: {
SDS_HDR_VAR(8,s);
return sh->alloc - sh->len;
}
case SDS_TYPE_16: {
SDS_HDR_VAR(16,s);
return sh->alloc - sh->len;
}
case SDS_TYPE_32: {
SDS_HDR_VAR(32,s);
return sh->alloc - sh->len;
}
case SDS_TYPE_64: {
SDS_HDR_VAR(64,s);
return sh->alloc - sh->len;
}
}
return 0;
}

SDS 的主要操作 API

基础方法有:

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
c复制代码sds sdsnewlen(const void *init, size_t initlen);
sds sdstrynewlen(const void *init, size_t initlen);
sds sdsnew(const char *init);
sds sdsempty(void);
sds sdsdup(const sds s);
void sdsfree(sds s);
sds sdsgrowzero(sds s, size_t len);
sds sdscatlen(sds s, const void *t, size_t len);
sds sdscat(sds s, const char *t);
sds sdscatsds(sds s, const sds t);
sds sdscpylen(sds s, const char *t, size_t len);
sds sdscpy(sds s, const char *t);

sds sdscatvprintf(sds s, const char *fmt, va_list ap);
#ifdef __GNUC__
sds sdscatprintf(sds s, const char *fmt, ...)
__attribute__((format(printf, 2, 3)));
#else
sds sdscatprintf(sds s, const char *fmt, ...);
#endif

sds sdscatfmt(sds s, char const *fmt, ...);
sds sdstrim(sds s, const char *cset);
void sdssubstr(sds s, size_t start, size_t len);
void sdsrange(sds s, ssize_t start, ssize_t end);
void sdsupdatelen(sds s);
void sdsclear(sds s);
int sdscmp(const sds s1, const sds s2);
sds *sdssplitlen(const char *s, ssize_t len, const char *sep, int seplen, int *count);
void sdsfreesplitres(sds *tokens, int count);
void sdstolower(sds s);
void sdstoupper(sds s);
sds sdsfromlonglong(long long value);
sds sdscatrepr(sds s, const char *p, size_t len);
sds *sdssplitargs(const char *line, int *argc);
sds sdsmapchars(sds s, const char *from, const char *to, size_t setlen);
sds sdsjoin(char **argv, int argc, char *sep);
sds sdsjoinsds(sds *argv, int argc, const char *sep, size_t seplen);

/* Callback for sdstemplate. The function gets called by sdstemplate
* every time a variable needs to be expanded. The variable name is
* provided as variable, and the callback is expected to return a
* substitution value. Returning a NULL indicates an error.
*/
typedef sds (*sdstemplate_callback_t)(const sds variable, void *arg);
sds sdstemplate(const char *template, sdstemplate_callback_t cb_func, void *cb_arg);

/* Low level functions exposed to the user API */
sds sdsMakeRoomFor(sds s, size_t addlen);
void sdsIncrLen(sds s, ssize_t incr);
sds sdsRemoveFreeSpace(sds s);
size_t sdsAllocSize(sds s);
void *sdsAllocPtr(sds s);

/* Export the allocator used by SDS to the program using SDS.
* Sometimes the program SDS is linked to, may use a different set of
* allocators, but may want to allocate or free things that SDS will
* respectively free or allocate. */
void *sds_malloc(size_t size);
void *sds_realloc(void *ptr, size_t size);
void sds_free(void *ptr);

字符串初始化

整体和 Java 的 StringBuilder 很像了 O_o

1
2
3
4
5
c复制代码/* Create a new sds string starting from a null terminated C string. */
sds sdsnew(const char *init) {
size_t initlen = (init == NULL) ? 0 : strlen(init);
return sdsnewlen(init, initlen);
}

首先是判断输入的 init 字符串的长度,接着调用 sdsnewlen 分配内存空间并赋值。

1
2
3
c复制代码sds sdsnewlen(const void *init, size_t initlen) {
return _sdsnewlen(init, initlen, 0);
}

核心函数_sdsnewlen 如下,主要就是先确保空间是否足够、分配空间,然后再调用 memcpy 将 *init 复制到对应的内存空间。

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
c复制代码/* Create a new sds string with the content specified by the 'init' pointer
* and 'initlen'.
* If NULL is used for 'init' the string is initialized with zero bytes.
* If SDS_NOINIT is used, the buffer is left uninitialized;
*
* The string is always null-termined (all the sds strings are, always) so
* even if you create an sds string with:
*
* mystring = sdsnewlen("abc",3);
*
* You can print the string with printf() as there is an implicit \0 at the
* end of the string. However the string is binary safe and can contain
* \0 characters in the middle, as the length is stored in the sds header. */
sds _sdsnewlen(const void *init, size_t initlen, int trymalloc) {
void *sh;
sds s;
char type = sdsReqType(initlen);
/* Empty strings are usually created in order to append. Use type 8
* since type 5 is not good at this. */
if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
int hdrlen = sdsHdrSize(type);
unsigned char *fp; /* flags pointer. */
size_t usable;

assert(initlen + hdrlen + 1 > initlen); /* Catch size_t overflow */
sh = trymalloc?
s_trymalloc_usable(hdrlen+initlen+1, &usable) :
s_malloc_usable(hdrlen+initlen+1, &usable);
if (sh == NULL) return NULL;
if (init==SDS_NOINIT)
init = NULL;
else if (!init)
memset(sh, 0, hdrlen+initlen+1);
s = (char*)sh+hdrlen;
fp = ((unsigned char*)s)-1;
usable = usable-hdrlen-1;
if (usable > sdsTypeMaxSize(type))
usable = sdsTypeMaxSize(type);
switch(type) {
case SDS_TYPE_5: {
*fp = type | (initlen << SDS_TYPE_BITS);
break;
}
case SDS_TYPE_8: {
SDS_HDR_VAR(8,s);
sh->len = initlen;
sh->alloc = usable;
*fp = type;
break;
}
case SDS_TYPE_16: {
SDS_HDR_VAR(16,s);
sh->len = initlen;
sh->alloc = usable;
*fp = type;
break;
}
case SDS_TYPE_32: {
SDS_HDR_VAR(32,s);
sh->len = initlen;
sh->alloc = usable;
*fp = type;
break;
}
case SDS_TYPE_64: {
SDS_HDR_VAR(64,s);
sh->len = initlen;
sh->alloc = usable;
*fp = type;
break;
}
}
if (initlen && init)
memcpy(s, init, initlen);
s[initlen] = '\0';
return s;
}

Redis 源码简洁剖析系列

最简洁的 Redis 源码剖析系列文章

Java 编程思想-最全思维导图-GitHub 下载链接,需要的小伙伴可以自取~

原创不易,希望大家转载时请先联系我,并标注原文链接。

本文转载自: 掘金

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

Redis 源码简洁剖析 01 - 环境配置 fork Re

发表于 2021-11-25

fork Redis 源码

在 GitHub 上找到并 fork Redis 源码 github.com/redis/redis,然后在本地 clone 自己 fork 出来的源码项目。这样更方便我们在学习源码的过程中,增加注释、调试等。

IDE 工具

本人的技术栈是 Java,JetBrains 的重度用户,所以 IDE 也选用 JetBrains 的 CLion。官网地址是:www.jetbrains.com/clion/。

初探

使用 Statistic 插件查看项目的整体情况。

看到 C 文件总共有 296 个文件,有效代码行数 12.4w 行。整体代码并不算多,抓住主流程框架学习之。

编译

拿到源码先切换到 6.2 分支,整体编译一下。首先执行 make clean,接着执行 make,成功~

Redis 源码简洁剖析系列

最简洁的 Redis 源码剖析系列文章

Java 编程思想-最全思维导图-GitHub 下载链接,需要的小伙伴可以自取~

原创不易,希望大家转载时请先联系我,并标注原文链接。

本文转载自: 掘金

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

Redis 缓存过期处理与内存淘汰机制

发表于 2021-11-25

已过期的key如何处理?

设置了expire的key缓存过期了,但是服务器的内存还是会被占用,这是因为redis所基于的两种删除策略

redis有两种策略:

  1. (主动)定时删除
    • 定时随机的检查过期的key,如果过期则清理删除。(每秒检查次数在redis.conf中的hz配置)
  2. (被动)惰性删除
    • 当客户端请求一个已经过期的key的时候,那么redis会检查这个key是否过期,如果过期了,则删除,然后返回一个nil。这种策略对cpu比较友好,不会有太多的损耗,但是内存占用会比较高。

所以,虽然key过期了,但是只要没有被redis清理,那么其实内存还是会被占用着的。

那么如果内存被Redis缓存占用慢了咋办?

内存占满了,可以使用硬盘,来保存,但是没意义,因为硬盘没有内存快,会影响redis性能。

所以,当内存占用满了以后,redis提供了一套缓存淘汰机制:MEMORY MANAGEMENT

maxmemory:当内存已使用率到达,则开始清理缓存

1
2
3
4
5
6
markdown复制代码* noeviction:旧缓存永不过期,新缓存设置不了,返回错误
* allkeys-lru:清除最少用的旧缓存,然后保存新的缓存(推荐使用)
* allkeys-random:在所有的缓存中随机删除(不推荐)
* volatile-lru:在那些设置了expire过期时间的缓存中,清除最少用的旧缓存,然后保存新的缓存
* volatile-random:在那些设置了expire过期时间的缓存中,随机删除缓存
* volatile-ttl:在那些设置了expire过期时间的缓存中,删除即将过期的`

本文转载自: 掘金

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

SpringBoot常用注解 Configuratio

发表于 2021-11-25

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

前言

SpringBoot 中简化了大量的配置文件,取而代之的是利用注解完成之前通过配置文件完成的工作。操作上便捷了很多,但是也隐藏了一些内部实现细节,在使用的时候不能盲目,应该了解在以往 Spring 项目中是如何配置的,这样可以加深我们对 SpringBoot 的理解

概述

类上加这个注解就说明这个类是一个配置类

  • Spring 项目中添加配置类

在传统的 Spring 项目中,我们想要引入一个配置类,通常是定义一个 bean.xml ,然后通过 bean 注解给容器中添加组件,注入到 Spring 容器中。

1
2
3
4
5
6
7
8
9
10
11
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="config01" class="com.example.helloworlddemo.config.User">
<property name="name" value="zhangsan"/>
<property name="age" value="18"/>
</bean>

</beans>
  • SpringBoot 项目中添加配置类

在 SpringBoot 项目中,我们只需要创建一个配置类,然后在这个类上直接加上 @Configration 注解,这个时候,这个类就等同于之前创建的 bean.xml,之前是针对 xml 操作,这里我们直接操作配置类即可。通过 @bean 注解添加组件。当项目启动的时候就会自动检测到该类,然后将这个类加载注入到容器中,省略了我们之前手动配置注入,扫描配置文件的步骤。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@Configuration // 告诉SpringBoot 这是一个配置类 = bean.xml
public class MyConfig {

/**
* @Bean 等同于 <bean></bean> 标签。
* 方法名 作为组件id
* 返回值类型 组件类型
* 返回值 组件在容器中的实例对象,单例的
*/
@Bean
public User config01() {
return new User("张三", "18");
}
}
  • 验证是否注入成功

我们可以将容器中所有的组件名称打印出来

1
2
3
4
5
6
7
8
9
java复制代码public static void main(String[] args) {
// 返回IOC容器对象
ConfigurableApplicationContext run = SpringApplication.run(HelloworldDemoApplication.class, args);
// 查看容器内的所有组件
String[] beanDefinitionNames = run.getBeanDefinitionNames();
for (String name : beanDefinitionNames) {
System.out.println(name);
}
}

可以看到,我们刚刚自己定义的已经注入到容器中了
image.png

并且这个组件实例是单例的,无论获取多少次都是一样的。

1
2
3
4
5
java复制代码    // 获取组件
User config01 = run.getBean("config01", User.class);
User config02 = run.getBean("config01", User.class);
System.out.println(config01);
System.out.println(config02);

image.png

SpringBoot2.x中的变化

相比于 1.0 版本,2.0版本 @Configuration 注解添加了新的属性 proxyBeanMethods()。

image.png

这个属性的作用是,设置配置组件中获取组件实例对象的方式。简单理解为单例和多例的区别。默认为 true 单例的。

  • @Configuration(proxyBeanMethods = true)

当我们设置为 true 的时候,我们获取到的配置对象(MyConfig)其实是一个代理对象(com.example.helloworlddemo.config.MyConfig$$EnhancerBySpringCGLIB$$97588a67@632aa1a3),每次我们调用这个对象去获取组件实例的时候,会先从容器中检查,有就拿容器内的,没有就新建一个。这样做的目的是为了保证容器中必然存在我们需要的实例对象,解决组件依赖的问题。想想如果一个组件依赖另一个组件, A 组件依赖 B 组件 ,我们获取 A 的时候容器内没有 B 那么这个时候就会出现空指针的问题。总之一句话,true 的时候,SpringBoot 每次都会检查容器中是否存在我们需要的实例对象,保证实例存在。

  • @Configuration(proxyBeanMethods = false)

当我们设置为 false 的时候,返回的就是原始对象(com.example.helloworlddemo.config.MyConfig@c1a4620),这个时候每次我们调用获取实例方法都会返回一个新的实例对象,并且 SpringBoot 会跳过实例检查的步骤,所以这种方式下项目启动加载速度会更快,所以当我们的配置类仅仅是为了返回一个组件实例的时候,可以设置为 false 提高项目加载速度。

总结

  1. 配置类里面使用 @Bean 标注在方法上给容器注册组件,默认也是单实例的
  2. 配置类本身也是组件
  3. proxyBeanMethods:代理 bean 的方法
    • Full 模式 :(proxyBeanMethods = true)
      这种模式下,保证每个 @Bean 方法,被重复调用返回的组件都是单实例的,直接从容器中拿,没有就新建。
    • Lite 模式:(proxyBeanMethods = false)
      这种模式下:每个 @Bean 方法 每次调用返回的实例对象都是新创建的
    • 组件存在依赖必须使用Full模式默认。其他默认是否Lite模式*

本文转载自: 掘金

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

MySQL学习-日志系统 redo log 和 bin lo

发表于 2021-11-25

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

作者:汤圆

个人博客:javalover.cc

前言

redo log 和 bin log 这两个日志系统,都是用来存储更新操作的,不过他们存储的方式不同;

redo log 主要存储做了哪些修改,比如把name=”javalover”改成 name=”admin”;

而bin log 不仅存储做了哪些修改,还存储修改的原始逻辑,比如update t set name=admin where id=1,会同时存储这条语句和name=”admin”最新值。

下面我们分别介绍下两者的工作流程和区别

目录

  1. redo log
  2. bin log
  3. change buffer
  4. 日志的执行流程
  5. redo log 和 bin log 的区别

正文

1. redo log

redo log 重做日志,它属于存储引擎层,是InnoDB特有的的,MyISAM引擎不支持;

空间大小:

redo log 占用的空间大小是固定的,比如我们给redo log分配了4个文件,每个文件占用1G,那么redo log的固定大小就是4G;

当redo log 写满时,会执行合并操作,就是将redo log中的记录合并到数据库磁盘中;

这里也顺带引出了一个WAL的概念;

WAL机制:

全称为 write ahead logging,大致意思就是先写日志,再写磁盘,目的就是提高性能;

因为如果每一次的更新操作都要写进磁盘,那么就需要先把数据页从磁盘中取出来,然后更新,最后写到磁盘中;

这样一来,磁盘的随机IO成本会很高;

所以通过WAL机制,先把更新操作记录到redo log中,然后在系统空闲时,再合并到磁盘中,此时的合并是顺序写入的,磁盘的IO成本很低;

write_point 和 check_point:

write_point 写入点 和 check_point 擦除点,可以理解为两个指针,write_point负责指向待写入记录的位置,而check_point负责指向待擦除的位置;

可以把这两个点想象成铅笔和橡皮擦;

刚开始的时候,这两个指针都指的是redo log的起点,如下所示:

image-20211123164303658

当记录了1条数据后,write_point就会往前移动1次,而check_point不动:

image-20211123164729273

当记录写满后,无法继续写入,check_point就会往前移动,把记录合并到数据库中,然后清除掉该部分记录;

crash-safe:

这个crash-safe,指的是当MySQL服务异常重启时,之前提交的记录也不会丢失;

这个crash-safe是基于redo log实现的,而redo log又是InnoDB引擎特有的,所以很多时候我们都推荐用InnoDB引擎,因为更加安全;

2. bin log

bin log 归档日志,属于Service层的东西,跟存储引擎无关;

也就是说:bin log不仅支持InnoDB引擎,还支持MyISAM引擎;

既然有了redo log为啥还要有bin log呢?

其实是先有的bin log,后有的redo log;

刚开始的时候,MySQL的存储引擎只有MyISAM引擎,而MyISAM引擎只支持bin log,也就是归档日志,它并不具备crash-safe的能力;

后来有了InnoDB引擎,它支持redo log 重做日志,相应的也就有了crash-safe能力;

看起来好像bin log没啥用了,那为啥MySQL不直接取消bin log呢?

因为有的MySQL还是用的MyISAM引擎,取消掉bin log的话,那他们连最基本的日志记录都没有了;

3. change buffer

上一篇我们有介绍change buffer,在更新操作时,会把更新记录存储到change buffer;

那change buffer和redo log有什么关系呢?

首先说下相同的地方:其实他俩的目的是一致的,都是为了减少磁盘的IO操作;

其次说下不同的地方:

  1. 更新操作:更新操作会先把更新记录写到change_buffer,然后再同步到redo log;
  2. 读取操作:如果在change buffer中读到了数据,那么就需要先将changge buffer中的脏页刷到磁盘中再读取;如果在change buffer中没有读到对应的数据,那么就会继续去redo log中查找;
  3. 磁盘IO:change buffer侧重于减少磁盘随机读的次数,而redo log侧重于减少磁盘随机写的次数;因为如果没有change buffer,那么每次的更新操作都会去读取磁盘而且是随机读,结果就是效率很低;

其实总结下来就是,不管读还是写,都是先去change buffer中操作,然后根据情况再看要不要去redo log中操作;

4. 日志的执行流程

这里我们以下面的语句为例子进行分析:这里假设系统用到了两个日志redo log和bin log

1
sql复制代码update t set age=10 where id=1;
  1. 执行器先去存储引擎中取出id=1的这行数据:如果这行数据所在的数据页在内存中,就直接返回;如果不在内存中,则需要先从磁盘读取对应的数据页,然后返回;
  2. 执行器拿到引擎返回的数据后,将对应的age设为10,并更新到内存中的数据页中,同时把操作记录保存到redo log中,此时redo log处于prepare状态;
  3. 接下来将操作记录存储到bin log中;
  4. 最后执行器提交事务,此时redo log变为commit状态,到此更新就完成了;

这里面涉及到一个两阶段提交的概念;

两阶段提交:

就是对于redo log来说,不是一次就写入完成的,而是分两次;

第一次写入记录时设置为prepare状态,然后等待着bin log日志的写入;

等到bin log写入成功后,事务才会提交,此时redo log被引擎设置为commit状态;

这样做的目的就是保证事务的一致性,即redo log和bin log都拥有一致的更新记录;

5. redo log 和 bin log的区别

上面基本都有涵盖到,这里用表格来说明一下,比较清晰:

redo log bin log
存储引擎 InnoDB InnoDB, MyISAM
MySQL架构层 存储引擎层 Service服务层
存储方式 物理日志,存储做了哪些修改 逻辑日志,存储修改的逻辑语句以及结果
空间占用 大小固定,写满后需清空部分记录 可追加写入

总结

redo log属于InnoDB引擎特有,支持crash-safe,也就是MySQL服务异常重启后,之前提交的数据记录还能找见;

bin log属于Service层,所有引擎都支持,大小不固定,可追加写入;

change buffer 和 redo log相辅相成:更新数据会先写入到change buffer,再写入redo log;查询数据先去change buffer找,找不到再去redo log找

本文转载自: 掘金

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

Mybatis的简单增删改查

发表于 2021-11-25

Mybatis的简单增删改查

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

刚开始学习Mybatis可以先看下官方文档,MyBatis是支持定制化SQL、存储过程以及高级映射的优秀的持久层框架。MyBatis避免了几乎所有的JDBC代码和手工设置参数以及抽取结果集。MyBatis使用简单的XML或注解来配置和映射基本体,将接口和Java的POJOs(Plain Old Java Objects,普通的Java对象)映射成数据库中的记录。

下面进入正题:
工具:Navicat premium 、IntelliJ IDEA

简单的目录结构

image.png

1.创建mysql数据库

image.png
创建一个firend_mq数据库,建立一张表为 users ,并插入一些数据

2.新建一个maven项目,并导入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
xml复制代码 <dependencies>
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
<!--添加mybatis依赖-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>
<!--jdbc驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<!--junit测试类-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>

3.在resources文件夹下新建mybatis-config.xml,编写mybaits的核心配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
xml复制代码<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<!-- configuration核心配置文件-->
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/firend_mq?useSSL=false&amp;useUnicode=&amp;characterEncodeing=UTF-8"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<!-- 注册mapper-->
<mappers>
<mapper resource="dao/UserMapper.xml"/>
</mappers>
</configuration>

注意点:resource绑定mapper,需要使用路径,使用”/“
连接mysql数据库的时候可能会出现时区的问题,可以看这篇博客
IntelliJ IDEA连接Mysql数据库和出现的问题(最详细)

4.编写mybatis工具类

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

private static SqlSessionFactory sqlSessionFactory;

static {
try {//使用mybaatis第一步获取SqlSessionFactory对象
String resource="mybatis-config.xml";
InputStream inputStream= Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
} catch (IOException e) {
e.printStackTrace();
}
}
//有了 SqlSessionFactory,顾名思义,我们可以从中获得 SqlSession 的实例。SqlSession
// 提供了在数据库执行 SQL 命令所需的所有方法。你可以通过 SqlSession 实例来直接执行已映射的 SQL 语句

public static SqlSession getSqlSession(){
SqlSession sqlSession = sqlSessionFactory.openSession();
return sqlSession;
}
}

5.编写mybatis实体类

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
java复制代码package pojo;

public class User {
private int id;
private String username;
private String password;

public User(int id, String username, String password) {
this.id = id;
this.username = username;
this.password = password;
}

public User() {
}

public int getId() {
return id;
}

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

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
}

前期的准备工作已完毕,开始编写代码

6.编写Dao层的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public interface UserDao {
//查询所有用户
List<User> getUserList();

//查询指定id用户
User getUserById(int id);

//添加一个用户
int addUser(User user);

//修改用户
int updateUser(User user);

//删除一个用户
int deleteUser(int id);
}

7.编写接口实现类
接口实现类由原来的UserDaoImpl转变为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
xml复制代码<!--namespace=绑定一个对应的Dao/Mapper接口-->
<mapper namespace="dao.UserDao">
<!-- select查询语句 -->
<select id="getUserList" resultType="pojo.User">
select * from firend_mq.users
</select>
<!-- 根据id查询-->
<select id="getUserById" parameterType="int" resultType="pojo.User">
select * from firend_mq.users where id = #{id}
</select>

<!-- 添加一个用户 对象中的属性,可以直接取出去-->
<insert id="addUser" parameterType="pojo.User">
insert into firend_mq.users (id,username,password) value (#{id},#{username},#{password})
</insert>

<update id="updateUser" parameterType="pojo.User">
update firend_mq.users set username=#{username},password=#{password} where id=#{id};
</update>

<!-- 删除一个用户-->
<delete id="deleteUser" parameterType="int">
delete from firend_mq.users where id=#{id}
</delete>
</mapper>

注意点:

  • Mapper配置文件一定要在mybatis-config.xml里面注册
  • 标签就是增删改查的类型,在标签里面书写原来的sql语句,使用#{}进行传值
  • namespace=绑定一个对应的Dao/Mapper接口,不要绑定错误的接口
  • id就是接口里面的方法名,要一致
  • parameterType是方法的参数类型,resultType是方法的返回值类型要对应接口。

8.编写测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
java复制代码public class UserDaoText {
//查询所有用户
@Test
public void getUserList(){
//第一步:获得sqlSession对象
SqlSession sqlSession = Mybatisutil.getSqlSession();
//方式一:getMapper
UserDao mapper = sqlSession.getMapper(UserDao.class);
List<User> userList = mapper.getUserList();
for (User user : userList) {
System.out.println(user);
}
sqlSession.close();
}
// 查询指定id用户
@Test
public void getUserById(){
SqlSession sqlSession = Mybatisutil.getSqlSession();
UserDao mapper = sqlSession.getMapper(UserDao.class);
User user = mapper.getUserById(2);
System.out.println(user);
sqlSession.close();
}
//所有的增删改都要提交事务
//添加一个用户
@Test
public void addUser(){
SqlSession sqlSession = Mybatisutil.getSqlSession();
UserDao mapper = sqlSession.getMapper(UserDao.class);
mapper.addUser(new User(4,"张三","10086"));
//提交事务
sqlSession.commit();
sqlSession.close();
}
@Test
public void updateUser(){
SqlSession sqlSession = Mybatisutil.getSqlSession();
UserDao mapper = sqlSession.getMapper(UserDao.class);
mapper.updateUser(new User(4,"哈哈","123"));
sqlSession.commit();
sqlSession.close();
}
@Test
public void deleteUser(){
SqlSession sqlSession = Mybatisutil.getSqlSession();
UserDao mapper = sqlSession.getMapper(UserDao.class);
mapper.deleteUser(4);
sqlSession.commit();
sqlSession.close();
}

}

这样一个简单Mybatis的增删改查就写完了,细节都在代码中由注释。

本文转载自: 掘金

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

1…194195196…956

开发者博客

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