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

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


  • 首页

  • 归档

  • 搜索

3D立体花朵送女友最合适了 展示 导读 源码和详解

发表于 2021-11-30

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

展示

这是一个动态图哦

.

dba593a6b4dc4312ab5a1b2662951d71.gif

导读

兄弟们可以收藏一下哦!情人节可以送出去,肥学找了几朵python写的花给封装好送给大家。不是多炫酷但是有足够的用心哦。别忘了点赞呀我也就不细说了,来吧展示!

源码和详解

荷花

1
2
3
4
5
6
7
8
9
10
python复制代码def lotus():
fig = plt.figure(figsize=(10,7),facecolor='black',clear=True)
ax = fig.gca(projection='3d')
[x, t] = np.meshgrid(np.array(range(25))/24.0, np.arange(0, 575.5, 0.5)/575 * 17 * np.pi-2*np.pi)
p = (np.pi/2)*np.exp(-t/(8*np.pi))
u = 1-(1-np.mod(3.6*t, 2*np.pi)/np.pi)**4/2
y = 2*(x**2-x)**2*np.sin(p)
r = u*(x*np.sin(p)+y*np.cos(p))
surf = ax.plot_surface(r*np.cos(t), r*np.sin(t), u*(x*np.cos(p)-y*np.sin(p)), rstride=1, cstride=1, cmap=cm.gist_rainbow_r,
linewidth=0, antialiased=True)

玫瑰花

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
python复制代码def rose_flower():
fig = plt.figure(figsize=(10,7),facecolor='black',clear=True)
ax = fig.gca(projection='3d')
# 将相位向后移动了6*pi
[x, t] = np.meshgrid(np.array(range(25)) / 24.0, np.arange(0, 575.5, 0.5) / 575 * 20 * np.pi + 4*np.pi)
p = (np.pi / 2) * np.exp(-t / (8 * np.pi))
# 添加边缘扰动
change = np.sin(15*t)/150
# 将t的参数减少,使花瓣的角度变大
u = 1 - (1 - np.mod(3.3 * t, 2 * np.pi) / np.pi) ** 4 / 2 + change
y = 2 * (x ** 2 - x) ** 2 * np.sin(p)
r = u * (x * np.sin(p) + y * np.cos(p))
h = u * (x * np.cos(p) - y * np.sin(p))
c= cm.get_cmap('Reds')
surf = ax.plot_surface(r * np.cos(t), r * np.sin(t), h, rstride=1, cstride=1,
cmap= c, linewidth=0, antialiased=True)

桃花

1
2
3
4
5
6
7
8
9
10
11
12
13
14
python复制代码
def peach_blossom():
fig = plt.figure(figsize=(10,7),facecolor='black',clear=True)
ax = fig.gca(projection='3d')
[x, t] = np.meshgrid(np.array(range(25)) / 24.0, np.arange(0, 575.5, 0.5) / 575 * 6 * np.pi - 4*np.pi)
p = (np.pi / 2) * np.exp(-t / (8 * np.pi))
change = np.sin(10*t)/20
u = 1 - (1 - np.mod(5.2 * t, 2 * np.pi) / np.pi) ** 4 / 2 + change
y = 2 * (x ** 2 - x) ** 2 * np.sin(p)
r = u * (x * np.sin(p) + y * np.cos(p)) * 1.5
h = u * (x * np.cos(p) - y * np.sin(p))
c= cm.get_cmap('spring_r')
surf = ax.plot_surface(r * np.cos(t), r * np.sin(t), h, rstride=1, cstride=1,
cmap= c, linewidth=0, antialiased=True)

月季

1
2
3
4
5
6
7
8
9
10
11
12
13
python复制代码def monthly_rose():
fig = plt.figure(figsize=(10,7),facecolor='black',clear=True)
ax = fig.gca(projection='3d')
[x, t] = np.meshgrid(np.array(range(25)) / 24.0, np.arange(0, 575.5, 0.5) / 575 * 30 * np.pi - 4 * np.pi)
p = (np.pi / 2) * np.exp(-t / (8 * np.pi))
change = np.sin(20 * t) / 50
u = 1 - (1 - np.mod(3.3 * t, 2 * np.pi) / np.pi) ** 4 / 2 + change
y = 2 * (x ** 2 - x) ** 2 * np.sin(p)
r = u * (x * np.sin(p) + y * np.cos(p)) * 1.5
h = u * (x * np.cos(p) - y * np.sin(p))
c = cm.get_cmap('magma')
surf = ax.plot_surface(r * np.cos(t), r * np.sin(t), h, rstride=1, cstride=1,
cmap=c, linewidth=0, antialiased=True)

我的文章里面有怎么打包pyinstaller的文,教大家怎么做可执行文件如果不会做这个.exe文件的大佬可以跟着学习一下。如果觉得麻烦可以找我获取可执行文件。可以在我的主页找到领取信息回复。

本文转载自: 掘金

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

深入理解Redis 数据结构—双链表

发表于 2021-11-30

在 Redis 数据类型中的列表list,对数据的添加和删除常用的命令有 lpush,rpush,lpop,rpop,其中 l 表示在左侧,r 表示在右侧,可以在左右两侧做添加和删除操作,说明这是一个双向的数据结构,而 list 数据结构正是双向链表,类似 java 中的 LinekdList 链表列表。

链表提供了高效的节点重排能力,以及顺序的节点访问方式,通过修改节点的 pre 和 next 指针来修改链表的数据。

C 语言没有内置链表的数据结构,所以 Redis 构建了自己的链表结构。

链表的数据结构,链表以及链表节点

链表是由链表以及链表节点组成,每个链表节点使用一个 adlist.h/listNode 结构来表示:

1
2
3
4
5
6
7
8
arduino复制代码typedef struct listNode {
//前置节点
struct listNode *prev;
//后置节点
struct listNode *next;
// 节点值
void *value;
} listNode;

多个 listNode 可以通过 prev 和 next 指针组成双链表的,如题所示:
listNode节点

多个 listNode 可以组成链表,但是为了方便管理,使用 adlist.h/list 管理链表,list 结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
c复制代码typedef struct list {
// 列表头结点
listNode *head;
// 列表尾结构
listNode *tail;
// 节点值复制函数
void *(*dup)(void *ptr);
// 节点值释放函数
void (*free)(void *ptr);
// 节点值对比函数
int (*match)(void *ptr, void *key);
// 列表节点数量
unsigned long len;
} list;

list 结构为链表提供了表头指针 head、表尾指针 tail,以及节点数量计算 len。下图展示一个由 list 结构和三个 listNode 节点组成的链表:

Redis 链表实现的特征有如下的总结:

  • 双向:链表节点带有 prev 和 next 指针,可以通过指针获取每一个数据
  • 快速计算链表长度:通过 list 结构中的 len 属性计算 list 的长度,而时间复杂度为O(1)
  • 多态: 链表节点使用 void* 指针保存节点,所以链表支持保存各种不同类型的值

双链表的运用

列表键,发布订阅、慢查询以及监视器等。

总结

  • 本文通过介绍链表的数据结构,链表是由链表和链表节点组成的
  • 链表节点都有一个前置和后置指针,所以 Redis 的链表是一个双向链表
  • 链表可以存储头结点,尾节点,更好的管理自己的节点,len 属性快速算出链表的长度
  • 链表通过 void* 以及不同的类型设定函数,所以链表可以不同的类型的值

如果觉得文章对你有帮助的话,请点个赞吧!

本文转载自: 掘金

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

为什么SOLID原则仍然是现代软件架构的基石

发表于 2021-11-30

概述

最近20年软件设计发生了天翻地覆的变化,但是SOLID原则至今仍然是软件设计的最佳实践。

SOLID 原则对于对于创建高质量软件是久经测试的标题。但是在现代多范式编程(函数式编程等)和云计算兴起的年代,它依然能够坚挺吗?我将通过如下文章解释SOLID代表了什么,为什么它依然适用现代软件,并且分享一些例子来解释。

什么是SOLID

SOLID是Robert C. Martin在2000年提取出来的一系列原则。它被建议去作为面向对象(OO)编程质量的特殊思考方式。总得来讲SOLID在这几个方面:代码如何切分,代码私有和对外暴露,代码之间如何调用提出了建议。我下面将深入研究每个字母(S.O.L.I.D)的原始含义,并且扩展到面向对象编程之外的使用。

有哪些改变?

21世纪早期,是Java和C++称霸的时期,理所当然我的大学很多课程都使用Java语言来当作训练。Java的流行催生出来了一些书籍,课程 和其它资料使得人们从写代码过度到写出好的代码。

因此,软件工业发生了深远的影响。有几个值得注意的点:

  • **动态类型语言(Dynamically-typed languages) ** 比如 Python,Ruby, 尤其JavaScript 变的和Java一样流行—甚至在某些行业和某类公司已经超过了Java
  • 非面向对象范式(Non-object-oriented paradigms) 最值得注意的是函数式编程(FP),在这些新语言中也比较常见。甚至Java本身也引入了lambdas!元编程技术(增加和改变对象的方法和特性)等技术也越来越流行。还有拥有“软面向对象”特征的Go语言,它具有静态类型但是没有继承。所有的这些都表明了现代软件中类和集成没有过去重要。
  • 开源软件(Open-source software) 的扩散。早期,更多的通用软件是闭源(closed-source)人们使用的都是编译后的软件,现在通常人们依赖的软件都是开源的。因此,在编写库是曾经对必不可少的逻辑和数据隐藏不再哪么重要。
  • 微服务和软件即服务(Saas) 爆炸式地出现。与其将应用程序部署为将所有依赖项链接在一起的大型可执行文件,不如部署一个与其他服务(自己的活第三方提供支持的服务)调用的小型服务。

整体来看,SOLID真正关心的许多事情——例如类和接口,数据隐藏性和多态——不再是程序员每天都要处理的事情。

有什么没发生变化?

现在工业界有很多不一样了,但是也还有一些东西未发生改变。包括:

  • 代码是由人类编写和修改的 。代码被编写一次并且被读很多很多次。所以对内部和外部需要很好的代码说明文档,尤其很好的API文档。
  • 代码被组织成模块。在某些语言中,这些是类。在其他情况下,他们可能是单独的源文件。在JavaScript中,它们可能是导出对象。无论如何,总是存在某种方式去隔离和组织代码成为独立,有界的单元。因此,总是需要决定如何最好的将代码组织在一起。
  • 代码可以是内部的或者外部的。一些编写出来的代码是被你自己或者你的团队使用,一些可能会被其它团队甚至其他顾客通过API的方式使用。这也意味着需要某种方式来决定哪些代码是“可见的”哪些是隐藏的。

“现代”SOLID

在接下来的文章中,我将把SOLID中的每一项原则都表述为更一般的描述,并且说明是如何应用在OO,FP,多范式编程中的,并距离说明。在许多情况下,这些原则甚至可以应用在整个服务或者系统中。

需要注意的是我将使用“模块”代指一组代码,可以是类,包,文件等等。

单一职责 (Single responsibility principle)

原始定义: “一个类改变的原因不会超过一个”

如果您编写的类有很多关注点或“更改的原因”,那么这些关注点中任何一个需要更改,您就需要更改相同的代码。这增加了对一个特性功能的修改就会破坏另外一个特性功能的可能性。

一个例子,如下是一个永远不应该应用在生产环境的Franken-class:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码class FrankenClass {
public void savaUserDetail(User user) {
//...
}

public void performOrder(Order order) {
//...
}

public void shipItem(Item item, String address) {
//...
}
}

新的定义: “每个模块应该做一件事,并且做好”。

这个原则和高内聚(high cohesion)话题紧密联系。本质上,您的代码不应该将很多角色或者用途混在一起。

如下是使用JavaScript的同一示例的函数式编程(FP)版本:

1
2
3
4
5
javascript复制代码const saveUserDetails = (user) => {...}
const performOrder = (order) => { ...}
const shipItem = (item, address) => { ... }
export { saveUserDetails, performOrder, shipItem };
import { saveUserDetails, performOrder, shipItem } from "allActions";

这也适用在微服务设计;如果你有一个单独服务来处理所有这三个功能,它就会尝试做太多事情。

开闭原则(Open-closed principle)

原始定义: “软件实体应该对扩展开放,对修改关闭。”

这也是Java语言设计的一部分—你可以创建一个子类来继承一个类,但是不能去修改原始的类。

“对扩展开放”的原因之一是限制了对类作者的依赖——如果你需要改动一个类,你不得不去等待类的原始作者去修改,或者你深入研究这个类后再去修改。更重要的是这个类承担了太多的关注点这将打破单一职责原则。

“对修改关闭”的原因是我们不相信下游的使用者能够完全理解我们所有的“private”私有代码,我们希望保护它免收不熟练的人的修改带来的损害。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码class Notifier {
public void notify(String message) {
//send an e-mail
}
}

class LoggingNotifier extends Notifier {
public void notify(String message) {
super.notify(message);//keep parent behaiver
//also log the message;
}
}

新定义: “应该能够在不重写模块的情况下使用和添加模块”。

这在面向对象领域是免费的(AOP)。在函数式编程代码必须明确定义Hook point以允许修改。这事一个示例,其中不仅仅允许使用前后hooks,而且甚至可以通过将函数传递给您的函数来覆盖其基本行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
javascript复制代码//library code

const saveRecord = (record, save, beforeSave, afterSave) => {
const defaultSave = (record) => {
//default save function;
}

if (beforeSave) beforeSave(record);
if (save) {
save(record);
} else {
defaultSave(record);
}
if(afterSave) afterSave(record);
//calling code
}

//calling code
const customSave = (record) => {...}
saveRecord(myRecord, customSave);

里氏替换原则

原始定义: “如果类型S是T的子类型,那么类型T可以被替换为S类型而不用修改程序的任何所需属性”。

这也是面向对象语言的基础属性。它意味着你可以使用任意子类替换它们的父类。所以你可以对这一个约定充满信心:你可以安全的使用任何 “is a” type T的对象而像T一样去使用。如下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码class Vehicle {
public int getNumberOfWheels() {
return 4;
}
}

class Bicycle extends Vehicle {
public int getNumberOfWheels() {
return 2;
}
}

// calling code
public static int COST_PER_TIRE = 50;

public int tireCost(Vehicle vehicle) {
return COST_PER_TIRE * vehicle.getNumberOfWheels();
}

Bicycle bicycle = new Bicycle();
System.out.println(tireCost(bicycle)); //100

新定义: 你可以使用一个东西代替另一个东西只要它们声明的行为方式一样。

在动态语言中,重要的是如果你的程序“promises”去做某些事情(例如实现一个接口或者函数),你需要遵守你的“promises”不要给客户端不符合“promises”的东西。

许多动态语言使用“duck typing”(不知道什么意思 戳这儿 )去达到这一点。本质上,你的function正式或者非正式的声明它期望其输入以特定方式运行并根据该假设运行。

例如Ruby:

1
2
3
4
ruby复制代码# @param input [#to_s]
def split_lines(input)
input.to_s.split("\n")
end

在这个例子中,这个function并不在乎输入类型——只关心它有一个to_s函数,它的行为方式与所有to_s函数的行为方式相同:即把输入变成字符串。许多动态语言没有办法去强制这种行为,因此这更像一种纪律问题而不是一种形式化技术。

接下来是函数式编程TypeScript例子,在这个例子中高阶函数引入了一个过滤器输入一个数字返回一个boolean值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typescript复制代码const isEven = (x: number) : boolean => x % 2 == 0;
const isOdd = (x: number) : boolean => x % 2 == 1;

const printFiltered = (arr: number[], filterFunc: (int) => boolean) => {
arr.forEach((item) => {
if (filterFunc(item)) {
console.log(item);
}
})
}

const array = [1,2,3,4,5,6];
printFiltered(array, isEven);
printFiltered(array, isOdd);

接口隔离原则(Interface segregation principle)

原始定义: “许多客户端(client-specific)接口要好于一个通用的(general-purpose)接口。”

在面向对象语言中,你可以理解为是为你的类提供了一个“视图”(view)。与其提供给你一个大而全的实现给你的客户端,而是仅使用与该客户端相关的方法在它们之上创建接口,并要求您的客户端使用这些接口。

正如单一职责原则一样,接口隔离原则隔离了系统之间的耦合,并且确保客户端不需要了解它所依赖的无关功能。

如下例子通过SPR测试:

1
2
3
4
5
java复制代码class PrintRequest {
public void createRequest() {}
public void deleteRequest() {}
public void workOnRequest() {}
}

这段代码通常只有一个“原因去更改”——它都与打印请求有关,它们都是同一个域的一部分,并且所有三种方法都可能会更改相同的状态。但是,创建请求的客户端不太可能是处理请求的客户端。将这些接口隔离开会更有意义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码interface PrintRequestModifier {
public void createRequest();
public void deleteRequest();
}

interface PrintRequestWorker {
public void workOnRequest()
}

class PrintRequest implements PrintRequestModifier, PrintRequestWorker {
public void createRequest() {}
public void deleteRequest() {}
public void workOnRequest() {}
}

新的定义: “不要向客户端展示它不需要看到的东西。”

只记录你客户端需要知道的内容。这意味着使用文档生成器只输入“public”function 或路由,而“private”没必要输出。

在微服务时代,你可以使用文档或者真正的隔离增加清晰度。例如,你外部客户可能只能以用户身份登录,但你的内部服务可能获取用户列表或者其他属性。你也可以创建一个单独的“仅限外部”用户服务来调用你的主服务,或者你可以只为隐藏内部路由的外部用户输出特定文档。

依赖倒置原则(Dependency inversion principle)

原始定义: “依赖抽象,而不是依赖具体实现”

在面向对象语言中,这意味着客户端应该尽可能依赖接口而不是具体实现类。这确保了代码应该依赖尽可能小的面积——事实上,客户端不必要依赖所有的代码,只需要依赖一个定义代码应该如何表现的契约。和其它原则一样这降低了修改一处而导致破坏了其它功能的风险。下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码interface Logger {
public void write(String message);
}

class FileLogger implements Logger {
public void write(String message) {
//write to file
}
}

class StandardOutLogger implements Logger {
public void write(String message) {
//write to standard out
}
}

// call
public void doStuff(Logger logger) {
//do stuff
logger.write("some message");
}

如果你正在写的代码需要一个logger, 你不想去限制自己仅仅只是去写入文件中,因为你不在乎。你仅仅只需要调用 write方法而让具体实现去输出。

新的定义: “依赖抽象,而不是具体实现。”

对的,这个例子我将定义保留原样!保持东西依赖抽象依然是重要的,即使现代代码中的抽象机制不像严格的面向对象世界那样强大。

尤其,这和上面讨论的里氏替换原则是一样的。主要的区别是它没有默认实现。因此,该部分中涉及鸭子类型和钩子函数的讨论通用适用于依赖倒置。

你也可以使用抽象对于微服务。比如,你可以将服务之间的直接通信替换为消息总线活队列平台,例如Kafka或者其他消息中间件。这样允许服务将消息发送到单个通用位置,而无需关心哪个特定服务将接收这些消息并执行其任务。

总结

再次重新梳理“现代SOLID”一次:

  • 不要惊讶别人读到你的代码。
  • 不要惊讶别人使用你的代码。
  • 不要让阅读你代码的人感到迷惑。
  • 为你的代码使用合理的边界。
  • 使用正确的耦合级别——尘归尘,土归土。

好的代码就是好的代码——从未改变,SOLID也是,实践是坚实的基础。

原文连接: stackoverflow.blog/2021/11/01/…

本文转载自: 掘金

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

Innodb存储引擎原理(一) MySQL概述 InnoDB

发表于 2021-11-30

MySQL概述

​ 当启动实例时,MySQL数据库会去读取配置文件,根据配置文件的参数来启动数据库实例。如果没有配置文件会按照编译时默认参数设置启动实例。用以下命令可以查看当mysql数据库实例启动时,会在哪些位置查找配置文件。

mysql –help | grep my.cnf

img
​ 可以看到MySQL数据库按/etc/my.cnf /etc/mysql/my.cnf /usr/local/mysql/etc/my.cnf ~/my.cnf的顺序读取配置文件。如果多个配置文件都有同一个参数,MySQL数据库会以读取到最后一个配置文件中的参数为准。

img
​ 从图1-1可以看出MySQL由以下几个部分组成:

  • 连接池组件
  • 管理服务和工具组件
  • SQL接口组件
  • 查询分析器组件
  • 优化器组件
  • 缓冲(Cache)组件
  • 插件式存储引擎
  • 物理文件

​ 从图1-1可以我们还可以发现,MySQL数据库区别于其他的数据库最重要的一个特点就是就是其插件式的表存储引擎。存储引擎是基于表的,而不是基于数据库的。

插件式引擎

MyISAM存储引擎

​ MyISAM不支持事务,表锁设计,支持全文索引,MySQL 5.5.8之前MyISAM是默认的存储引擎(Windows除外)。MyISAM存储引擎的缓冲池(cache)只缓存索引文件,而不缓冲数据文件。

​ MyISAM存储引擎表由MYD和MYI组成,MYD用于存放数据文件,MYI用于存放索引文件。可以使用myisampack工具来进一步压缩数据文件,因为myisampack工具使用哈夫曼编码静态算法来压缩数据,因此使用myisampack压缩的表是只读的,用户也可以使用myisampack来解压数据文件。

​ 在MySQL 5.0版本之前,MyISAM默认支持的表大小为4GB,如果需要支持大于4GB的MyISAM表时,则需要定制MAX_ROWS和AVG_ROW_LENGTH属性。从MySQL 5.0开始,MyISAM默认支持256TB的单表数据,这足够满足一般应用需求。

img

NDB存储引擎

​ NDB是一个集群存储引擎,其结构是share nothing的集群架构,因此能提供更高的可用性。NDB的特点是数据全部放在内存中(从MySQL 5.1开始可以将非索引数据存放在磁盘上),因此主键查找的速度极快,并且通过添加NDB数据存储节点可以线性地提高数据库性能性能,是高可用、高性能的集群系统。

关于NDB存储引擎有一个问题值得注意,那就是NDB存储引擎的连接操作(JOIN)是在MySQL数据库层完成的,而不是在存储引擎层完成的。这意味着复杂的连接操作需要巨大的网络开销,因此查询速度很慢。

Memory存储引擎

​ memory存储引擎(之前称HEAP存储引擎)将表中的数据存放在内存中,如果数据库发生重启或者崩溃,则表中的数据将全部消失。它非常适合用于存储临时数据的临时表以及数据仓库中的纬度表。Memory存储引擎默认使用哈希索引,而不是我们熟知的B+树索引。

​ 虽然memory存储引擎速度非常快,但在使用中还是有一定限制的,比如只支持表锁,并发性能差,并且不支持TEXT和BLOB列类型。最重要的是,存储变长字段(varchar)是按照定长字段(char)的方式进行的,因此会浪费内存。

​ MySQL数据库使用Memory存储引擎作为临时表来存放查询的中间结果集。如果中间结果集大于Memory存储引擎表的容量设置,又或者中间结果集含有TEXT和BLOB列类型字段,则MySQL数据库会将其转换到MyISAM存储引擎表存放到磁盘中。之前提到MyISAM不缓存数据文件,因此这时产生的临时表的性能对查询会有损失。

各存储引擎之间的比较

img
可以用过 SHOW ENGINES 语句来查看当前使用的MySQL数据库所支持的存储引擎,也可以通过 information_schema 架构下的 ENGINES 表,如下所示:

img
InnoDB存储引擎
==========

InnoDB体系架构

img
从图可见InnoDB存储引擎有多个内存块,可以认为这些内存块组成了一个大的内存池,负责如下工作:

  • 维护所有进程/线程需要访问的多个内部数据结构。
  • 缓存磁盘数据,方便快速地读取,同时在对磁盘数据文件的修改之前在这里缓存。
  • 重做日志(redo log)缓存。
  • ……

​ 后台线程主要作用是负责刷新内存池中的数据,保证缓冲池中的内存缓存是最近的数据。此外将已修改的内存文件刷新到磁盘文件,同时保证数据库在发生异常的情况下,InnoDB能恢复到正常运行的状态。

后台线程

​ InnoDB是多线程的模型,因此后台有多个不同的线程,负责处理不同的任务

Master Thread

​ Master Thread是一个非常核心的后台线程,主要负责将缓冲池中的数据异步刷新到磁盘,保证数据的一致性,包括脏页数据的刷新、合并插入缓冲(INSERT BUFFER)、UNDO页的回收等。

IO Thread

​ 在InnoDB存储引擎中大量使用了AIO来处理写IO请求,这样可以极大提高数据库的性能。而IO Thread的工作主要负责这些IO请求的回调(callback)处理。InnoDB 1.0版本以前共有4个IO Thread,分别是read、write、insert buff和log IO Thread。在Linux平台下,IO Thread的数量不能进行调整,但是在Windows平台下可以通过参数innodb_file_io_threads来增大IO Thread。从InnoDB 1.0.x版本开始,read thread和write thread分别增大到4个,并且不能使用innodb_file_io_threads参数,而是分别使用innodb_read_io_threads和innodb_write_io_threads参数进行设置,如:

img

Purge Thread

​ 事务被提交后,其所使用的undolog可能不再需要,因此需要PurgeThread来回收已经使用并分配的undo页。在InnoDB 1.1版本之前,purge操作仅在InnoDB存储引擎的Master Thread中完成。而从InnoDB 1.1版本开始,purge操作可以独立到单独的线程中进行,以此减轻Master Thread的工作,从而提高CPU的使用率以及提升存储引擎的性能。用户可以在MySQL数据库的配置文件中添加如下命令来启用独立的Purge Thread:

[mysqld]

innodb_purge_threads=1

在InnoDB 1.1版本中,即使将innodb_purge_threads设为大于1,InnoDB存储一青年启动时也会将其设为1,并在错误文件中出现如下类似的提示:

img

​ 从InnoDB 1.2版本开始,InnoDB支持多个Purge Thread,这样做的目的是为了进一步加快undo页的回收。同时由于Purge Thread需要离散地读取undo页,这样也能更进一步利用磁盘的随机读取性能。

Page Cleaner Thread

​ Page Cleaner Thread是在InnoDB 1.2.x版本中引入的。其作用是将之前版本中脏页的刷新操作都放入到单独的线程中完成。而其目的是为了减轻原Master Thread的工作以及对于用户查询线程的阻塞,进一步提高InnoDB存储引擎的性能。

内存

缓冲池

​ InnoDB存储引擎是基于磁盘储存的,并将其中的记录按照页的方式进行管理。因此可将其视为基于磁盘的数据库系统。在数据库系统中,由于CPU速度与磁盘速度之间的鸿沟,基于磁盘的数据库系统通常使用缓冲池技术来提高数据库的整体性能。

​ 缓冲池简单来说就是一块内存区域,通过内存的速度来弥补磁盘速度较慢对数据库性能的影响。在数据库中进行读取页的操作,首先将从磁盘读取到的页存放在缓冲池中,这个过程称为将页“FIX”在缓冲池中。下一次再读相同的页时,首先判断该页是否在缓冲池中。若在缓冲池中,称该页在缓冲池中被命中,直接读取该页。否则读取磁盘上的页。

​ 对于数据库中页的修改操作,则首先修改在缓冲池中的页,然后再以一定的频率刷新到磁盘上。这里需要注意的是,页从缓冲池刷新回磁盘的操作并不是每次页发生更新时触发,而是通过一种称为CheckPoint的机制刷新回磁盘。同样,这也是为了提高数据库的整体性能。

​ 对于InnoDB存储引擎而言,其缓冲池的配置通过参数innodb_buffer_pool_size来设置。下面显示一台MySQL数据库服务器,其将InnoDB存储引擎的缓冲池设置为15GB。

show variables like ‘innodb_buffer_pool_size’\G;

img
​ 具体来看,缓冲池中缓存的数据页类型有:索引页、数据页、undo页、插入缓冲(insert buffer)、自适应哈希索引(adaptive hash index)、InnoDB存储的锁信息(lock info)、数据字典信息等。不能简单的认为,缓冲池只是缓存索引页和数据页,它们只是占缓冲池很大的一部分而已。图2-2很好地显示了InnoDB存储引擎中内存的结构情况。

img
​ 从InnoDB 1.0.x版本开始,允许有多个缓冲池实例。每个页根据哈希值平均分配到不同缓冲池实例中。这样做的好处是减少数据库内部的资源竞争,增加数据库的并发处理能力。可以通过参数innodb_buffer_pool_instances来进行配置,改值默认为1。

img
通过命令SHOW ENGINE INNODB STATUS可以观察到每个缓冲池实例对象的运行情况。

LRU List、Free List和Flush List

​ 通常来说,数据库中的缓冲池通过LRU(Last Recent Used,最近最少使用)算法来进行管理的。即最频繁使用的页在LRU列表的前端,而最少使用的页在LRU列表的尾端。当缓冲池不能存放新读取到的页时,将首先释放LRU列表尾端的页。

​ 在InnoDB存储引擎中,缓冲池中页的大小默认为16KB,同样适用LRU算法对缓冲池进行管理。稍有不同的是InnoDB存储引擎对传统的LRU算法做了一些优化。在InnoDB存储引擎中,LRU列表中还加入了midpoint位置。新读取到的页,虽然是最新访问的页,但并不是直接放入到LRU列表的首部,而是放入到LRU列表的midpoint位置。这个算法在InnoDB存储引擎下称为midpoint insertion strategy。在默认配置下,该位置在LRU列表长度的5/8处。midpoint位置可由参数 innodb_old_blocks_pct 控制,如:

show variables like ‘innodb_old_blocks_pct’\G

img
​ 从上面的例子可以看到,参数 innodb_old_blocks_pct 默认值为37.表示新读取的页插入到LRU列表尾端的37%的位置(差不多3/8的位置)。在InnoDB存储引擎中,把midpoint之后的列表称为old列表,之前的列表成为new列表。可以简单地理解为new列表中的页都是最活跃的热点数据。

​ 那为什么不采用朴素的LRU算法,直接将读取的页放入到LRU列表的首部呢?这是因为若直接将读取到的页放入到LRU的首部,那么某些SQL操作可能会使缓冲池中的页被刷新出,从而影响缓冲池的效率。常见的这类操作为索引或数据的扫描操作。这类操作需要访问表中的许多页,甚至是全部的页,而这些页通常来说又仅在这次查询操作中需要,并不是活跃的热点数据。如果页被放入LRU列表的首部,那么非常可能将所需的热点数据页从LRU列表中移除,而在下一次需要读取该页时,InnoDB存储引擎需要再次访问磁盘。

​ 为了解决这个问题,InnoDB存储引擎引入了另一个参数来进一步管理LRU列表,这个参数是innodb_old_blocks_time,用于表示读取到mid位置后需要等待多久才会被加入到LRU列表的热端。刚才当需要执行上述所说的SQL操作时,可以通过下面的方法尽可能使LRU列表中的热点数据不被刷出。

set global innodb_old_blocks_time=1000;

img
如果用户预估自己活跃的热点数据不止63%,那么在执行SQL语句前,可以通过下面的语句来减少热点页可能被刷出的概率。

set global innodb_old_blocks_pct=20;

img
​ LRU列表用来管理已经读取的页,但当数据库刚启动时,LRU列表是空的,即没有任何的页。这时页都放在Free列表中。当需要从缓冲池分页时,首先从Free列表中查找是否有可用的空闲页,若有则将该页从Free列表中删除,放入到LRU列表中。否则,根据LRU算法,淘汰LRU列表末尾的页,将该内存空间分配给新的页。当页从LRU列表的old部分加入到new部分时,称此时发生的操作为page made young,而因为innodb_old_blocks_time的设置而导致页没有从old部分移动到new部分的操作称为page not made young。可以通过命令SHOW ENGINE INNODB STATUS来观察LRU列表以及Free列表的使用情况和运行状态。

show engine innodb status\G;

img
​ 通过命令SHOW ENGINE INNODB STATUS可以看到:当前Buffer pool size共有327679个页,即327679*16K,总共5GB的缓冲池。Free buffers表示当前Free列表中页的数量。Database pages表示LRU列表中页的数量。可能的情况是Free buffers与Database pages的数量之和不等于Buffer pool size。正如图2-2所示的那样,因为缓冲池中的页还可能被分配给自适应哈希索引、Lock信息、Insert Buffer等页,而这部分页不需要LRU算法进行维护,因此不存在LRU列表中。

​ pages made young显示了LRU列表中页移动到前端的次数,因为该服务器在运行阶段没有改变innodb_old_blocks_time的值,因此not young为0。young/s、non-young/s表示每秒这两类操作的次数。这里还有一个重要的观察变量——Buffer pool hit rate,表示缓冲池的命中率,这个例子中为100%,说明缓冲池运行状态非常良好。通常该值不应该小于95%。若发生Buffer pool hit rate的值小于95%这种情况,用户需要观察是否是由于全部扫描引起的LRU列表被污染的问题。

​ 从Innodb 1.2版本开始,还可以通过表INNODB_BUFFER_POOL_STATS来观察缓冲池的运行状态,如:

img
​ 此外,还可以通过表INNODB_BUFFER_PAGE_LRU来观察每个LRU列表中每个页的具体信息,例如通过下面的语句可以看到缓冲池中LRU列表中SPACE为1的表的页类型:

img
img
​ InnoDB存储引擎从1.0.x版本开始支持压缩页的功能,即将原本16KB的页压缩为1KB、2KB、4KB和8KB。而由于页的大小发生了变换,LRU列表也有了些许的改变。对于非16KB的页,是通过unzip_LRU列表进行管理的。通过命令SHOW ENGINE INNODB STATUS可以观察到如下内容:

img
​ 可以看到LRU列表中一共有1539个页,而unzip_LRU列表中有156个页。这里需要注意的是,LRU中的页包含了unzip_LRU列表中的页面。对于压缩页的表,每个表的压缩比率可能各不相同。可能存在有的表页大小为8KB,有的表页大小为2KB的情况。unzip_LRU是怎么从缓冲池中分配内存的呢?

​ 首先,在unzip_LRU列表中对不同压缩页大小的页进行分别管理。其次,通过伙伴算法进行内存的分配。例如对需要从缓冲池中申请页为4KB的大小,其过程如下:

1)检查4KB的unzip_LRU列表,检查是否有可用的空闲页面;

2)若有,则直接使用;

3)否则,检查8KB的unzip_LRU列表;

4)若能够得到空闲页,将页分为2个4KB页,存放到4KB的unzip_LRU列表;

5)若不能的到空闲页,从LRU列表中申请一个16KB的页,将页分为1个8KB的页、2个4Kb的页,分别存放在对应的unzip_LRU列表中。

​ 同样可以通过information_schema架构下的表INNODB_BUFFER_PAGE_LRU来观察unzip_LRU列表中的页,如:

img
​ 在LRU列表中的页被修改后,称该页为脏页(dirty page),即缓冲池中的页和磁盘上的页的数据产生了不一致。这时数据库会通过CHECKPOINT机制将脏页刷新回磁盘,而Flush列表中的页即为脏页列表。需要注意的是,脏页既存在于LRU列表中,也存在于Flush列表中。LRU列表用来管理缓冲池中页的可用性,FLush列表用来管理将页刷新到磁盘,二者互不影响。

​ 同LRU列表一样,Flush列表也可以通过命令SHOW ENGINE INNODB STATUS来查看,前面例子中Modified db pages 24673就显示了脏页的数量。information_schema架构下并没有类似INNODB_BUFFER_PAGE_LRU的表来显示脏页的数量及脏页的类型,但正如前面所述的那样,脏页同样存在于LRU列表中,故用户可以用过元数据表INNODB_BUFFER_PAGE_LRU来查看,唯一不同的是需要加入OLDEST_MODIFICATION大于0的SQL查询条件,如:

img
可以看到当前共有5个脏页及它们对应的表和页的类型。TABLE_NAME为NULL表示该页属于系统表空间。

重做日志缓冲

​ 从图2-2可以看到,InnoDB存储引擎的内存区域出了有缓冲池外,还有重做日志缓冲(redo log buffer)。InnoDB存储引擎首先将重做日志信息先放入到这个缓冲区,然后按一定频率将其刷新到重做日志文件。重做日志缓冲一般不需要设置得很大,因为一般情况下每一秒会将重做日志缓冲刷新到日志文件,因此用户只需要保证每秒产生的事务量在这个缓冲大小之内即可。该值可由配置参数innodb_log_buffer_size控制,默认为8MB:

img

​ 在通常情况下,8MB的重做日志缓冲池足以满足绝大部分的应用,因为重做日志在下列三种情况下会将重做日志缓冲中的内容刷新到外部磁盘的重做日志文件中。

  • Master Thread每一秒将重做日志缓冲刷新到重做日志文件;
  • 每个事务提交时会将重做日志缓冲刷新到重做日志文件;
  • 当重做日志缓冲池剩余空间小于1/2时,重做日志缓冲刷新到重做日志文件。

额外内存池

​ 额外的内存池通常地被DBA忽略,他们认为该值并不十分重要,事实恰恰相反,该值同样十分重要。在InnoDB存储引擎中,对内存的管理通过一种称为内存堆(heap)的方式进行的。在对一些数据结构本身的内存进行分配时,需要从额外的内存池中进行申请,当该区域的内存不够时,会从缓冲池中进行申请。例如,分配了缓冲池(innodb_buffer_pool),但是每个缓冲池中的帧缓冲(frame buffer)还有对应的缓冲控制对象(buffer control block),这些对象记录了一些诸如LRU、锁、等待等信息,而这个对象的内存需要从额外内存池中申请。因此,在申请很大的InnoDB缓冲池时,也应该相应地增加这个值。

Checkpoint技术

​ 前面已经讲到了,缓冲池的设计目的为了协调CPU速度与磁盘速度的鸿沟。因此页的操作首先都是在缓冲池中完成的。如果一条DML语句,如Update或Delete改变了页中的记录,那么此时页是脏的,即缓冲池中的页的版本比磁盘的新。数据库需要将新版本的页从缓冲池刷新到磁盘。

​ 倘若每次一个页发生变化,就将新页的版本刷新到磁盘,那么这个开销是非常大的。若热点数据集中在某几个页中,那么数据库的性能将变得非常差。同时,如果在从缓冲池将页的新版本刷新到磁盘时候发生了宕机,那么数据就不能恢复了。为了避免发生数据丢失的问题,当前事务数据库系统普遍采用了Write Ahead Log策略,即当事务提交时,先写重做日志,再修改页。当由于发生宕机时,完全可以通过重做日志来恢复整个数据库系统中的数据到宕机发生的时刻。但是这需要两个前提条件:

  • 缓冲池可以缓存数据库中所有的数据;
  • 重做日志可以无限增大。

​ 对于第一个前提条件,有经验的用户都知道,当数据库刚开始创建时,表中没有任何数据。缓冲池的确可以缓冲所有的数据库文件。然而随着市场的推广,用户的增加,产品越来越受到关注,使用量也越来越大。这时负责后台存储的数据库的容量必定会不断增大。当前3TB的MySQL数据库并不少见,但是3TB的内存却非常少见。因此第一个假设对生产环境应用中的数据库是很难得到保证的。

​ 再来看第二个前提条件:重做日志可以无限增大。也许是可以的,但是这对成本的要求太高,同时不便于运维。DBA或SA不知道什么时候重做日志是否已经接近于磁盘可使用空间的阈值,并且要让存储设备可动态扩展也是需要一定的技巧和设备支持的。

​ 好的,即使上述两个条件都满足,那么还有一个情况需要考虑:宕机后数据库的恢复时间。当数据库运行了几个月甚至几年,这时发生宕机,重新应用重做日志的时间会非常久,此时恢复的代价也会非常大。

因此Checkpoint(检查点)技术的目的是解决以下几个问题:

  • 缩短数据库的恢复时间;
  • 缓冲池不够用时,将脏页刷新到磁盘;
  • 重做日志不可用时,刷新脏页。

​ 当数据库发生宕机时,数据库不需要重做所有的日志,因为Checkpoint之前的页都已经刷新回磁盘。故数据库只需要对Checkpoint后的重做日志进行恢复。这样就大大缩短了恢复的时间。

​ 此外,当缓冲池不够用时,根据LRU算法会溢出最少使用的页,若此页为脏页,那么需要强制执行Checkponit,将脏页也就是页的新版本刷新回磁盘。

​ 重做日志出现不可用的情况是因为当前事务数据库系统对重做日志的设计都是循环使用的,并不是让其无限增大的,这从成本以及管理上都是比较困难的。重做日志可以被重用的部分是指这些重做日志已经不再需要,即当数据库宕机时,数据库恢复操作不需要这部分的重做日志,因此这部分可以被覆盖重用。若此时重做日志还需要使用,那么必须强制产生Checkpoint,将缓冲池中的页至少刷新到当前重做日志的位置。

​ 对于InnoDB存储引擎而言,其是通过LSN(Log Sequence Number)来标记版本的。而LSN是8个字节的数字,其单位是字节。每个页都有LSN,重做日志中也有LSN,Checkpoint也有LSN。可以通过命令SHOW ENGINE INNODB STATUS来观察:

img
​ 在InnoDB存储引擎中,Checkpoint发生的时间、条件以及脏页的选择等都非常复杂。而Checkpoint所做的事情无外乎将缓冲池中的脏页刷回到磁盘。不同之处在于每次刷新多少页到磁盘,每次从哪里取脏,以及什么时间触发Checkpoint。在InnoDB存储引擎内部,有两种Checkpoint,分别为:

  • Sharp Checkpoint
  • Fuzzy Checkpoint

​ Sharp Checkpoint发生在数据库关闭时将所有的脏页刷新回磁盘,这是默认的工作方式,即参数innodb_fast_shutdown=1。

​ 但是若数据库在运行时也是用Sharp Checkpoint,那么数据库的可用性就会收到很大的影响。故在InnoDB存储引擎内部使用Fuzzy Checkpoint进行页的刷新,即只刷新一部分脏页,而不是刷新所有的脏页回磁盘。

​ 在InnoDB存储引擎中可能发生如下几种情况的Fuzzy Checkpoint:

  • Master Thread Checkpoint
  • FLUSH_LRU_List Checkpoint
  • Async/Sync Flush Checkpoint
  • Dirty Page too much Checkpoint

​ 对于Master Thread中发生的Checkpoint,差不多以每秒或每10秒的速度从缓冲池的脏页列表中刷新一定比例的页回磁盘。这个过程是异步的,即此时InnoDB存储引擎可以进行其他的操作,用户查询线程不会阻塞。

​ FLUSH_LRU_List Checkpoint是因为InnoDB存储引擎需要保证LRU列表中需要差不多100多个空闲页可供使用。在InnoDB 1.1.x版本之前,需要检查LRU列表中是否有足够的可用空间操作发生在用户查询线程中,显然会阻塞用户的查询操作。倘若没有100个可用的空闲页,那么InnoDB存储引擎会将LRU列表尾端的页移除。如果这些页中有脏页,那么需要进行Checkpoint,而这些页是来自LRU列表,因此称为FLUSH_LRU_LIST Checkpoint。

​ 而从MySQL 5.6版本,也就是InnoDB 1.2.x版本开始,这个检查放在了一个单独的Page Cleaner线程中进行,并且用户可以通过参数innodb_lru_scan_depth控制LRU列表中可用页的数量,该值默认为1024,如:

img
​ Async/Sync Flush Checkpoint指的是重做日志文件不可用的情况,这时需要强制将一些页刷新回磁盘,而此时脏页是从脏页列表中选取的。若将已经写入到重做日志的LSN记为redo_lsn,将已经刷新回磁盘最新页的LSN记为checkpoint_lsn,则可定义为:

​ checkpoint_age = redo_lsn - checkpoint_lsn

​ 再定义以下的变量:

​ async_water_mark = 75% * total_redo_log_file_size

​ sync_water_mark = 90% * total_redo_log_file_size

​ 若每个重做日志文件的大小为1GB,并且定义了两个重做日志文件,则重做日志文件的总大小为2GB。那么async_water_mark=1.5G, sync_water_mark=1.8G。则:

  • 当checkpoint_age < async_water_mark时,不需要刷新任何脏页到磁盘;
  • 当async_water_mark < checkpoint_age < sync_water_mark时触发Async Flush,从Flush列表中刷新足够的脏页回磁盘,使得刷新后满足checkpoint_age < async_water_mark。
  • checkpoint_age > sync_water_mark这种情况一般很少发生,除非设置的重做日志文件太小,并且在进行类似LOAD DATA的BULK INSERT操作。此时出发Sync Flush操作,从Flush列表刷新足够的脏页回磁盘,使得刷新后满足checkpoint_age < async_water_mark。

​ 可见,Async/Sync Flush Checkpoint是为了保证重做日志的循环使用的可用性。在InnoDB 1.2.x版本之前,Async Flush Checkpoint会阻塞发现问题的用户查询线程,而Sync Flush Checkpoint会阻塞所有的用户查询线程,并且等待脏页刷新完成。从InnoDB 1.2.x版本开始——也就是MySQL 5.6版本,这部分的刷新操作同样放入到了单独的Page Cleaner Thread中,故不会阻塞用户查询线程。

​ InnoDB版本提供了查看刷新页是从Flush列表中还是从LRU列表中进行Checkpoint和重做日志产生的Async/Sync Flush次数,如:

img
​ 最后一种Checkpoint的情况是Dirty Page too much,即脏页的数量太多,导致InnoDB存储引擎强制进行Checkpoint。其目的总的来说还是为了保证缓冲池中有足够可用的页。其可由参数innodb_max_dirty_pages_pct控制:

img

​ innodb_max_dirty_pages_pct 值为75表示,当缓冲池中脏页的数量占据75%时,强制进行Checkpoint,刷新一部分脏页到磁盘。在InnoDB 1.0.x版本之前,该参数的默认值为90,之后的版本都为75。

参考

《MySQL技术内幕 InnoDB存储引擎 第2版》

本文转载自: 掘金

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

面试官问我HTTP,我真的是

发表于 2021-11-30

本文正在参与 “网络协议必知必会”征文活动

面试官:今天要不来聊聊HTTP吧?

候选者:嗯,HTTP「协议」是客户端和服务器「交互」的一种通迅的格式

候选者:所谓的「协议」实际上就是双方约定好的「格式」,让双方都能看得懂的东西而已

候选者:所谓的交互实际上就是「请求」和「响应」

面试官:那你知道HTTP各个版本之间的区别吗?

候选者:HTTP1.0默认是短连接,每次与服务器交互,都需要新开一个连接

候选者:HTTP1.1版本最主要的是「默认持久连接」。只要客户端服务端没有断开TCP连接,就一直保持连接,可以发送多次HTTP请求。

候选者:其次就是「断点续传」(Chunked transfer-coding)。利用HTTP消息头使用分块传输编码,将实体主体分块进行传输

候选者:HTTP/2不再以文本的方式传输,采用「二进制分帧层」,对头部进行了「压缩」,支持「流控」,最主要就是HTTP/2是支持「多路复用」的(通过单一的TCP连接「并行」发起多个的请求和响应消息)

面试官:嗯,稍微打断下。我知道HTTP1.1版本有个管线化(pipelining)理论,但默认是关闭的。管线化这个跟HTTP/2的「多路复用」是很类似的,它们有什么区别呀?

候选者:HTTP1.1提出的「管线化」只能「串行」(一个响应必须完全返回后,下一个请求才会开始传输)

候选者:HTTP/2多路复用则是利用「分帧」数据流,把HTTP协议分解为「互不依赖」的帧(为每个帧「标序」发送,接收回来的时候按序重组),进而可以「乱序」发送避免「一定程度上」的队首阻塞问题

候选者:但是,无论是HTTP1.1还是HTTP/2,response响应的「处理顺序」总是需要跟request请求顺序保持一致的。假如某个请求的response响应慢了,还是同样会有阻塞的问题。

候选者:这受限于HTTP底层的传输协议是TCP,没办法完全解决「线头阻塞」的问题

面试官:哦,好的。

候选者:HTTP/3 跟前面版本最大的区别就是:HTTP1.x和HTTP/2底层都是TCP,而HTTP/3底层是UDP。使用HTTP/3能够减少RTT「往返时延」(TCP三次握手,TLS握手)

面试官:你了解HTTPS的过程吗?

候选者:嗯啊,HTTPS在我的理解下,就是「安全」的HTTP协议(客户端与服务端的传输链路中进行加密)

候选者:HTTPS首先要解决的是:认证的问题

候选者:客户端是需要确切地知道服务端是不是「真实」,所以在HTTPS中会有一个角色:CA(公信机构)

候选者:服务端在使用HTTPS前,需要去认证的CA机构申请一份「数字证书」。数字证书里包含有证书持有者、证书有效期、「服务器公钥」等信息

候选者:CA机构也有自己的一份公私钥,在发布数字证书之前,会用自己的「私钥」对这份数字证书进行加密

候选者:等到客户端请求服务器的时候,服务端返回证书给客户端。客户端用CA的公钥对证书解密(因为CA是公信机构,会内置到浏览器或操作系统中,所以客户端会有公钥)。这个时候,客户端会判断这个「证书是否可信/有无被篡改」

候选者:私钥加密,公钥解密我们叫做「数字签名」(这种方式可以查看有无被篡改)

候选者:到这里,就解决了「认证」的问题,至少客户端能保证是在跟「真实的服务器」进行通信。

候选者:解决了「认证」的问题之后,就要解决「保密」问题,客户端与服务器的通讯内容在传输中不会泄露给第三方

候选者:客户端从CA拿到数字证书后,就能拿到服务端的公钥

候选者:客户端生成一个Key作为「对称加密」的秘钥,用服务端的「公钥加密」传给服务端

候选者:服务端用自己的「私钥解密」客户端的数据,得到对称加密的秘钥

候选者:之后客户端与服务端就可以使用「对称加密的秘钥」愉快地发送和接收消息

面试官:了解了

关注我的微信公众号【Java3y】来聊点不一样的!

【对线面试官+从零编写Java项目】 持续高强度更新中!求star

原创不易!!求三连!!

本文转载自: 掘金

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

PHP 设计模式十六 观察者模式

发表于 2021-11-30

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

课程背景

  • 最近完成一个项目 对于代码分层有了一丢丢了解 但是架构设计合理性上存在问题
  • 万物看本质基本功硬 底层知识扎实才能写出更优质的代码 才能走得更远
  • 作为努力奔跑的程序员 又一次的去温习 思考 设计模式等基础的php知识
  • 因为掘金粑粑有奖励,所以把学习的笔记整理记录并分享了出来。

正文开始

这里继续上一节中的内容
2 创建观察者统一处理类 \Observer\EquipSaveOb.php

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
php复制代码<?php
namespace Observer;
/**
* 创建抽象类 保证只能被继承 不能被实例化
*/
abstract class EquipSaveOb{

private $observer_arr = []; //保存观察者

/**
* 添加观察者
* 必须是实现了EquipSaveI接口的对象才能添加到观察者中
*/
public function addObServer(\Observer\EquipSaveI $server){
$this->observer_arr[] = $server;
}

/**
* 执行操作
*/
public function executeAll(){
foreach ($this->observer_arr as $key => $value) {
$value->execute();
}
}
}

因为抽象类只能被继承不能被创建的特点,这就保证了,想要使用这个观察者就必须是继承自类EquipSaveOb才可以。

3 我们创建具体执行操作的类,然后继承上面的观察者方法

1
2
3
4
5
6
7
8
9
10
11
12
php复制代码<?php
namespace Observer;

class EquipSave extends EquipSaveOb{

/**
* 执行保存的操作
*/
public function save(){
$this->executeAll();
}
}

这边绑定观察者,跟执行的方法已经编写完成。我们要做的就是给当前操作添加观察者,然后就会依次执行每个观察者中的代码。

4 创建观察者,并执行操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
php复制代码class Message implements \Observer\EquipSaveI{
public function execute(){
echo '设备站内信<br>';
}
}

class Notify implements \Observer\EquipSaveI{
public function execute(){
echo '设备短信通知<br>';
}
}

$res = new \Observer\EquipSave();
$res->addObServer(new Message());
$res->addObServer(new Notify());
$res->save();

添加了两个类,对应两个操作。此时访问url,会看到执行成功的返回结果。

image.png

本文转载自: 掘金

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

高频算法面试题(四十五)- 二叉树的锯齿形层序遍历

发表于 2021-11-30

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

刷算法题,从来不是为了记题,而是练习把实际的问题抽象成具体的数据结构或算法模型,然后利用对应的数据结构或算法模型来进行解题。个人觉得,带着这种思维刷题,不仅能解决面试问题,也能更多的学会在日常工作中思考,如何将实际的场景抽象成相应的算法模型,从而提高代码的质量和性能

二叉树的锯齿形层序遍历

题目来源:LeetCode-103. 二叉树的锯齿形层序遍历

题目描述

给定一个二叉树,返回其节点值的锯齿形层序遍历。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)

示例

示例 1

给定二叉树 [3,9,20,null,null,15,7],

1
2
3
4
5
markdown复制代码    3
/ \
9 20
/ \
15 7

返回锯齿形层序遍历如下:

1
2
3
4
5
csharp复制代码[
[3],
[20,9],
[15,7]
]

解题

解法一:广度优先搜索

思路

本题本质上就是二叉树的层序遍历。因此,不难想到用广度优先搜索的思想,实现过程中需要借助一个队列来记录每一层的结点

与层序遍历不同的是,每一层的结果按照锯齿形打印。所以,我们可以偶数层的数据,从左往右打印,奇数层的数据从右往左打印

这道题和高频算法面试题(十九)- 按之字形顺序打印二叉树这道题几乎一样,里边有图解整个过程

代码

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
go复制代码func zigzagLevelOrder(root *TreeNode) (ans [][]int) {
if root == nil {
return
}
queue := []*TreeNode{root}
for level := 0; len(queue) > 0; level++ {
vals := []int{}
q := queue
queue = nil
for _, node := range q {
vals = append(vals, node.Val)
if node.Left != nil {
queue = append(queue, node.Left)
}
if node.Right != nil {
queue = append(queue, node.Right)
}
}
// 本质上和层序遍历一样,我们只需要把奇数层的元素翻转即可
if level%2 == 1 {
for i, n := 0, len(vals); i < n/2; i++ {
vals[i], vals[n-1-i] = vals[n-1-i], vals[i]
}
}
ans = append(ans, vals)
}
return
}

本文转载自: 掘金

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

SpringBoot 实战:JUnit5+MockMvc+M

发表于 2021-11-30

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

本文被《Spring Boot 实战》专栏收录。

你好,我是看山。

今天聊聊如何在 SpringBoot 中集成 Junit5、MockMvc、Mocktio。Junit5 是在 Java 栈中应用最广的测试框架,Junit4 一度霸榜。

升级到 Junit5 之后,除了增加 Java8 的很多特性,做了很多功能增强,在结构上做了优化调整,拆分了很多不同的模块,可以按需引入,比如:

  • JUnit Platform - 在 JVM 上启动测试框架
  • JUnit Jupiter - 在 JUnit5 中编写测试和扩展
  • JUnit Vintage - 提供运行基于 JUnit3 和 JUnit4 的测试引擎

从 SpringBoot 2.2.0 之后,Junit5 已经成为了默认的 Junit 版本。有了 JUnit Vintage,从 Junit4 迁移到 Junit5 的成本极低。所以本文就直接针对 Junit5 开始了。

版本

先说版本,是为了避免因为版本差异出现各种奇怪的问题:

  • JDK:jdk8(小版本可以忽略)
  • SpringBoot:2.5.2
    • 继承spring-boot-starter-parent
    • 依赖spring-boot-starter-web
    • 依赖spring-boot-starter-test
  • JUnit:5.7.2
  • Mockito:3.9.0
  • hamcrest:2.2

SpringBoot 的好处在于,只要继承spring-boot-starter-parent或引入spring-boot-dependencies,然后添加spring-boot-starter-test依赖即可。定义的 POM 内容如下:

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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.2</version>
    </parent>
    <groupId>cn.howardliu.effective.spring</groupId>
    <artifactId>springboot-junit5-mockito</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot-junit5-mockio</name>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

因为继承了spring-boot-starter-parent,所以我们依赖的spring-boot-starter-test不需要写具体的版本,可以直接集成父级的版本定义。其中,spring-boot-starter-web是用于提供 REST API 的 web 容器,spring-boot-starter-test可以提供各种测试框架的,spring-boot-maven-plugin是将 SpringBoot 应用打包为可执行 jar 的插件。

项目结构

因为是 DEMO 示例,我们实现一个 Echo 接口,能够接收请求参数,并返回加工后的字符串。按照惯例,我们使用万能的Hello, World!。

我们的项目结构如下:

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
css复制代码├── pom.xml
└── src
├── main
│   ├── java
│   │   └── cn
│   │   └── howardliu
│   │   └── effective
│   │   └── spring
│   │   └── springbootjunit5mockio
│   │   ├── SpringbootJunit5MockioApplication.java
│   │   ├── controller
│   │   │   └── EchoController.java
│   │   └── service
│   │   ├── EchoService.java
│   │   └── impl
│   │   └── EchoServiceImpl.java
│   └── resources
│   └── application.yaml
└── test
└── java
└── cn
└── howardliu
└── effective
└── spring
└── springbootjunit5mockio
└── controller
├── EchoControllerMockTest.java
└── EchoControllerNoMockitoTest.java
  • SpringbootJunit5MockioApplication:SpringBoot 应用启动入口
  • EchoController:接口定义
  • EchoService:实现业务逻辑接口
  • EchoServiceImpl:接口实现
  • EchoControllerMockTest:使用 Mock 代理 EchoService 实现
  • EchoControllerNoMockitoTest:直接测试接口实现

EchoServiceImpl

我们看下EchoService的实现,这将是我们 DEMO 的核心实现:

1
2
3
4
5
6
7
java复制代码@Service
public class EchoServiceImpl implements EchoService {
    @Override
    public String echo(String foo) {
        return "Hello, " + foo;
    }
}

EchoControllerNoMockitoTest

我们先使用 Junit5+MockMvc 实现 Controller 接口的普通调用,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码@SpringBootTest(classes = SpringbootJunit5MockioApplication.class)
@AutoConfigureMockMvc
class EchoControllerNoMockitoTest {
    @Autowired
    private MockMvc mockMvc;

    @Test
    void echo() throws Exception {
        final String result = mockMvc.perform(
                MockMvcRequestBuilders.get("/echo/")
                        .param("name", "看山")
        )
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andDo(MockMvcResultHandlers.print())
                .andReturn()
                .getResponse()
                .getContentAsString(StandardCharsets.UTF_8);

        Assertions.assertEquals("Hello, 看山", result);
    }
}

我们通过SpringBootTest注解定义这是一个 SpringBoot 应用的测试用例,然后通过AutoConfigureMockMvc启动测试容器。这样,就可以直接注入MockMvc实例测试 Controller 接口。

这里需要注意一点,网上很多教程会让写@ExtendWith({SpringExtension.class})这样一个注解,其实完全没有必要。通过源码我们可以知道,SpringBootTest注解已经添加了ExtendWith。

EchoControllerMockTest

这个测试用例中,我们通过 Mockito 组件代理EchoService的echo方法,代码如下:

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复制代码@SpringBootTest(classes = SpringbootJunit5MockioApplication.class)
@ExtendWith(MockitoExtension.class)
@AutoConfigureMockMvc
class EchoControllerMockTest {
    @Autowired
    private MockMvc mockMvc;
    @MockBean
    private EchoService echoService;

    @BeforeEach
    void setUp() {
        Mockito.when(echoService.echo(Mockito.any()))
                .thenReturn("看山说:" + System.currentTimeMillis());
    }

    @Test
    void echo() throws Exception {
        final String result = mockMvc.perform(
                MockMvcRequestBuilders.get("/echo/")
                        .param("name", "看山的小屋")
        )
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andDo(MockMvcResultHandlers.print())
                .andReturn()
                .getResponse()
                .getContentAsString(StandardCharsets.UTF_8);

        Assertions.assertTrue(result.startsWith("看山"));
    }
}

在这个示例中,我们需要注意@ExtendWith(MockitoExtension.class)注解,这个注解是用于引入MockBean的,我们通过对echo方法的拦截,使其返回我们定义好的响应结果。这种方式是为了在多系统或者多功能测试时,不需要真正调用接口。

比如,我们需要获取用户手机号,通常在接口中会校验用户有没有登录,我们就可以使用 Mockito 的能力代理登录验证,使结果永远是 true。

文末总结

至此,我们完成了 SpringBoot 集成 Junit5、MockMvc、Mockito 的示例。想要获取源码,只需要关注公众号「看山的小屋」,回复”spring”即可。

很多同学感觉单元测试没有编写的必要,直接使用 Swagger 或者 Postman 之类的工具就能很好的测试接口。确实如此,对于简单的 CRUD 接口,写单元测试的必要性不太高。但是,如果是复杂接口呢?接口参数有很多的组合,响应结果也需要各种验证,如果使用一次性的工具,每次测试组合参数就已经让人崩溃了,而且组合参数不能存留甚至不能在多人间传承,就会浪费很多的人力。

此时,单元测试的效果就会显现。我们只需要编写一次参数组合,放在 csv 之类的文件中,通过单元测试的参数化测试方式,即可多次运行,验证接口的正确性。

或者,当我们感觉系统已经臭味弥漫,对其重构之后,为了验证接口功能不变,也可以直接使用原来的测试用例加以验证。

综上,虽然测试用例编写麻烦,但是妙用无穷。

推荐阅读

  • SpringBoot 实战:一招实现结果的优雅响应
  • SpringBoot 实战:如何优雅的处理异常
  • SpringBoot 实战:通过 BeanPostProcessor 动态注入 ID 生成器
  • SpringBoot 实战:自定义 Filter 优雅获取请求参数和响应结果
  • SpringBoot 实战:优雅的使用枚举参数
  • SpringBoot 实战:优雅的使用枚举参数(原理篇)
  • SpringBoot 实战:在 RequestBody 中优雅的使用枚举参数
  • SpringBoot 实战:在 RequestBody 中优雅的使用枚举参数(原理篇)
  • SpringBoot 实战:JUnit5+MockMvc+Mockito 做好单元测试
  • SpringBoot 实战:加载和读取资源文件内容

你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。欢迎关注公众号「看山的小屋」,发现不一样的世界。

本文转载自: 掘金

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

Spring Cloud Gateway过滤器精确控制异常返

发表于 2021-11-30

欢迎访问我的GitHub

这里分类和汇总了欣宸的全部原创(含配套源码):github.com/zq2599/blog…

本篇概览

  • 前文《Spring Cloud Gateway过滤器精确控制异常返回(分析篇)》咱们阅读源码,了解到Spring Cloud Gateway是如何处理全局异常信息的,学了那么多理论,不免手痒想实战验证学习效果,今天咱们就来写代码,最终目标是改写下图两个红框中的内容:

在这里插入图片描述

  • 为了简单起见,本篇不再新增maven子工程,而是基于前文创建的子工程gateway-change-body,在这里面继续写代码;

源码下载

  • 本篇实战中的完整源码可在GitHub下载到,地址和链接信息如下表所示(github.com/zq2599/blog…%EF%BC%9A)
名称 链接 备注
项目主页 github.com/zq2599/blog… 该项目在GitHub上的主页
git仓库地址(https) github.com/zq2599/blog… 该项目源码的仓库地址,https协议
git仓库地址(ssh) git@github.com:zq2599/blog_demos.git 该项目源码的仓库地址,ssh协议
  • 这个git项目中有多个文件夹,本篇的源码在spring-cloud-tutorials文件夹下,如下图红框所示:

在这里插入图片描述

  • spring-cloud-tutorials文件夹下有多个子工程,本篇的代码是gateway-change-body,如下图红框所示:

在这里插入图片描述

第一种:抛出ResponseStatusException异常

  • 打开gateway-change-body工程的RequestBodyRewrite.java文件,改动如下图红框,如果请求body不含user-id参数就返回Mono.error,入参是ResponseStatusException异常,设置了返回码为400,message为一段中文描述:

在这里插入图片描述

  • 接下来运行nacos、provider-hello工程、gateway-change-body工程
  • 用postman发请求试试,请求和响应的详情如下图:

在这里插入图片描述

  • 从上图可见,返回码为400,和我们设定的一样,但是message却为空,这是怎么回事呢?按照咱们的设定,这里应该显示请求参数必须包含user-id字段,看来咱们遇到一只拦路虎了

小小拦路虎

  • 咱们代码中,抛异常的时候设定message内容如下图红框所示,但运行的时候返回的是空字符串,这是怎么回事呢?

在这里插入图片描述

  • 来看DefaultErrorWebExceptionHandler.isIncludeMessage方法,看下图红框中的那个errorProperties,您会不会恍然大悟:这不就是springboot配置中的erro配置嘛!

在这里插入图片描述

  • 修改工程的配置文件,红框内是新增的配置:

在这里插入图片描述

  • 再用postman试试,如下图,这一次,status、message、exception、trace齐聚一堂,完全符合预期:

在这里插入图片描述

  • 看来第一种方法是可行的:返回ResponseStatusException类型的异常;

第二种:自定义异常,带ResponseStatus注解

  • 接下来试试第二种方法:通ResponseStatus注解
  • 首先新建一个异常类MyGatewayException.java,使用了ResponseStatus,在里面配置返回码和message内容,这次的返回码用的是403:
1
2
3
4
5
6
7
8
java复制代码package com.bolingcavalry.changebody.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(code = HttpStatus.FORBIDDEN, reason = "user-id字段不能为空")
public class MyGatewayException extends Exception {
}
  • 编码完成,重启应用,然后再发一次请求,如下图,返回码和message内容都符合预期:

在这里插入图片描述

  • 至此,两种最简单的方式都完成验证,一般情况下已经满足要求:将错误信息准确传递给调用方

留有瑕疵

  • 聪明的您应该已发现上述两种方案有瑕疵:返回body的格式和字段都是固定的,如果项目中对返回body的内容有严格要求,例如只允许code、message、data三个字段,其余字段一律不能返回,此时又该怎么办呢?
  • 似乎需要一种方法,让咱们可以随心所欲的设置body内容,篇幅所限,这种终极的解决方式就留在下一篇吧,敬请期待,欣宸原创,必不辜负您…

你不孤单,欣宸原创一路相伴

  1. Java系列
  2. Spring系列
  3. Docker系列
  4. kubernetes系列
  5. 数据库+中间件系列
  6. DevOps系列

欢迎关注公众号:程序员欣宸

微信搜索「程序员欣宸」,我是欣宸,期待与您一同畅游Java世界…
github.com/zq2599/blog…

本文转载自: 掘金

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

并行Stream与Spring事务相遇?不是冤家不聚头~

发表于 2021-11-30

今天这篇文章跟大家分享一个实战中的Bug及解决方案和技术延伸。

事情是这样的:运营人员反馈,通过Excel导入数据时,有一部分成功了,有一部分未导入。初步猜测,是事务未生效导致的。

查看代码,发现导入部分已经通过@Transcational注解进行事务控制了,为什么还会出现事务不生效的问题呢?

下面我们就进行具体的案例分析,Let’s go!

事务不生效的代码

这里写一段简单的伪代码来演示展示一下事务不生效的代码:

1
2
3
4
java复制代码  @Transactional(rollbackFor = Exception.class)
public void batchInsert(List<Order> list) {
list.parallelStream().forEach(order -> orderMapper.save(order));
}

逻辑很简单,遍历list,然后批量插入Order数据到数据库。在该方法上使用@Transactional来声明出现异常时进行回滚。

但事实情况是,其中某一条数据执行异常时,事务并没有进行回滚。这到底是为什么呢?

下面一探究竟。

JDK 8 的Stream

上面代码中涉及到了两个知识点:parallelStream和@Transactional,我们先来铺垫一下parallelStream相关知识。

在JDK8 中引入了Stream API的概念和实现,这里的Stream有别于 InputStream 和OutputStream,Stream API 是处理对象流而不是字节流。

比如,我们可以通过如下方式来基于Stream进行实现:

1
2
3
4
ini复制代码List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9); 
numbers.stream().forEach(num->System.out.println(num));
​
输出:1 2 3 4 5 6 7 8 9

代码看起来方便清爽多了。关于Stream的基本处理流程如下:

jdk8 Stream

在这些Stream API中,还提供了一个并行处理的API,也就是parallelStream。它可以将任务拆分子任务,分发给多个处理器同时处理,之后合并。这样做的目的很明显是为了提升处理效率。

并行处理

parallelStream的基本使用方式如下:

1
2
scss复制代码// 并行执行流
list.stream().parallel().filter(e -> e > 10).count()

针对上述代码,对应的流程如下:

parallelStream流程图

而parallelStream会将流划分成多个子流,分散到不同的CPU并行处理,然后合并处理结果。其中,parallelStream默认是基于ForkJoinPool.commonPool()线程池来实现并行处理的。

通常情况下,我们可以认为并行会比串行快,但还是有前提条件的:

  • 处理器核心数量:并行处理核心数越多,处理效率越高;
  • 处理数据量:处理数据量越大优势越明显;

但并行处理也面临着一系列的问题,比如:资源竞争、死锁、线程切换、事务、可见性、线程安全等问题。

@Transactional事务处理

上面了解了parallelStream的基本原理及特性之后,再来看看@Transactional的事务处理特性。

@Transactional是Spring提供的基于注解的一种声明式事务方式,该注解只能运用到public的方法上。

基本原理:当一个方法被@Transactional注解之后,Spring会基于AOP在方法执行之前开启一个事务。当方法执行完毕之后,根据方法是否报错,来决定回滚或提交事务。

在默认代理模式下,只有目标方法由外部方法调用时,才能被Spring的事务拦截器拦截。所以,在同一个类中的两个方法直接调用,不会被Spring的事务拦截器拦截。这是事务不生效的一个场景,但在上述案例中,并不存在这种情况。

Spring在处理事务时,会从连接池中获得一个jdbc connection,将连接绑定到线程上(基于ThreadLocal),那么同一个线程中用到的就是同一个connection了。具体实现在DataSourceTransactionManager#doBegin方法中。

Bug综合分析

在了解了parallelStream和@Transactional的相关知识之后,我们会发现:parallelStream处理时开启了多线程,而@Transactional在处理事务时会(基于ThreadLocal)将连接绑定到当前线程,由于@Transactional绑定管理的是主线程的事务,而parallelStream开启的新的线程与主线程无关。因此,事务也就无效了。

此时,将parallelStream改为普通的stream,事务可正常回滚。这就提示我们,在使用基于@Transactional方式管理事务时,慎重使用多线程处理。

问题拓展

虽然parallelStream带来了更高的性能,但也要区分场景进行使用。即便是在不需要事务管理的情况下,如果parallelStream使用不当,也会造成同一时间对数据库发起大量请求等问题。

因此,在stream与parallelStream之间进行选择时,还要考虑几个问题:

  • 是否需要并行?数据量比较大,处理器核心数比较多的情况下才会有性能提升。
  • 任务之间是否是独立的,是否会引起任何竞态条件?比如:是否共享变量。
  • 执行结果是否取决于任务的调用顺序?并行执行的顺序是不确定的。

小结

本篇文章讲述的Bug虽然简单,但如果不了解parallelStream与@Transactional注解的特性,还是很难排查的。而且也让我们意识到,虽然Spring通过@Transactional将事务管理进行了简化处理,但作为开发者,还是需要深入了解一下它的基本运作原理。不然,在排查bug时,很容易抓瞎。

博主简介:《SpringBoot技术内幕》技术图书作者,酷爱钻研技术,写技术干货文章。

公众号:「程序新视界」,博主的公众号,欢迎关注~

技术交流:请联系博主微信号:zhuan2quan

\

本文转载自: 掘金

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

1…111112113…956

开发者博客

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