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

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


  • 首页

  • 归档

  • 搜索

还在用httpclient请求接口?那你就out了

发表于 2021-07-06

实际项目中免不了要跟其他系统交互,那就会涉及到接口调用,如果是自己本系统的还好,可以通过打jar包引用的方式放到本项目中直接调用。

如果是其他系统的,那就需要通过远程接口调用,而远程接口调用通常使用两种方式调用

  1. 其一是webservice接口,
  2. 其二是rest接口(也就是对方给我们一个http链接地址)

本文就来讲讲如何通过rest接口调用远程服务。

在没有springboot之前,大家会想到使用httpclient去调用,没错,httpclient是一个很好的选择。但是springboot给我们封装了更好的调用,RestTemplate!

先建立如下工程目录结构:

下面逐一的讲关键类列出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码@RestController
@RequestMapping("demo")

public class DemoController {

@GetMapping("getUser")
public void getUser(@RequestParam("name")String name){
//此处需要远程调用第三方服务 获取user对象
}

@GetMapping("saveUser")
public void getUser(){
UserDto userDto = new UserDto();
userDto.setName("resttemplate");
userDto.setAge(17);
userDto.setBirthday(new Date());
//此处需要远程调用第三方服务 将user对象发送给第三方
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码@RestController
@RequestMapping("api")

//此类是模拟第三方提供服务的
public class ApiController {

//模拟获取数据
@GetMapping("get")
public UserDto get(@RequestParam("username")String username){
System.out.println("从数据库中查询username【"+username+"】的数据");
UserDto userDto = new UserDto();
userDto.setName("springboot");
userDto.setAge(18);
userDto.setBirthday(new Date());

return userDto;
}

//模拟接收数据保存到数据库
@GetMapping("save")
public UserDto save(@RequestBody UserDto userDto) throws JsonProcessingException {
System.out.println(new ObjectMapper().writeValueAsString(userDto));
//TODO:保存到数据库
return userDto;
}
}
1
2
3
4
5
6
7
8
java复制代码@Data

public class UserDto {

private String name;
private Integer age;
private Date birthday;
}

启动工程,我们先用postman确保如下两个服务是ok的。

1
2
3
shell复制代码get:localhost:8080/demo/get

post:localhost:8080/demo/save

post:localhost:8080/demo/save

为了模拟简单,我们将第三方提供的服务也部署在本工程,也就是localhost:8080/demo/get和localhost:8080/demo/save。

下面我们需要在ApiController里面使用远程调用去发送请求了,怎么搞呢?之前说过了用RestTemplate。

我们在Application中声明一个RestTemplate

1
2
3
4
5
java复制代码@Bean

public RestTemplate restTemplate(){
return new RestTemplate();
}

然后在DemoController中使用@Resource注入进来

下面我们通过postman调用http://localhost:8080/demo/getUser?name=123看看。

如下是控制台的输出,第一句是ApiController的输出,第二句是上级调用的输出,说明我们已经通过了demoController调用到了另外一个rest服务了

在发送一个post请求:

如下是控制台的输出,第一句也是apiController的输出,第二句是上级调用的输出,同样达到了我们的预期。

实际生产实例中还会涉及到超时,如果第三方响应太慢了,则不再等待,

是的。这里RestTemplate也可以做到,我们不是在Application中声明了一个RestTemplate吗?,只需要将如下代码替换即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@Bean
public SimpleClientHttpRequestFactory httpClientFactory() {
SimpleClientHttpRequestFactory httpRequestFactory = new SimpleClientHttpRequestFactory();
httpRequestFactory.setReadTimeout(1000*5);//读取超时
httpRequestFactory.setConnectTimeout(1000*5);//链接超时

return httpRequestFactory;
}

@Bean
public RestTemplate restTemplate(SimpleClientHttpRequestFactory httpClientFactory) {
RestTemplate restTemplate = new RestTemplate(httpClientFactory);
return restTemplate;
}

更多java原创阅读:javawu.com

本文转载自: 掘金

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

大数据 ETL 处理工具 Kettle 入门实践

发表于 2021-07-06

Kettle 简介

ETL(Extract-Transform-Load 的缩写,即数据抽取、转换、装载的过程),对于数据开发人员来说,我们经常会遇到各种数据的处理,转换,迁移,所以了解并掌握一种 ETL 工具的使用,必不可少,这里我们要学习的 ETL 工具就是 Kettle。

Kettle 是什么

Kettle 是一款国外开源的 ETL 工具,对商业用户也没有限制,纯 Java 编写,可以在 Window、Linux、Unix 上运行,绿色无需安装,数据抽取高效稳定。Kettle 中文名称叫水壶,它允许管理来自不同数据库的数据,把各种数据放到一个壶里,然后以一种指定的格式流出。Kettle 中有两种脚本文件,Transformation 和 Job, Transformation 完成针对数据的基础转换,Job 则完成整个工作流的控制。通过图形界面设计实现做什么业务,并在 Job 下的 start 模块,有一个定时功能,可以每日,每周等方式进行定时。

Kettle 的核心组件

名称 功能
Spoon 通过图形接口,允许你通过图形界面来设计 ETL 转换过程(Transformation)
Pan 运行转换的命令行工具
Kitchen 运行作业的命令行工具
Carte Carte 是一个轻量级别的 Web 容器,用于建立专用、远程的 ETL Server
  • 作业和转换可以在图形界面里执行,但这只适合在开发、测试和调试阶段。在开发完成后,需要部署到生产环境中 Spoon 就很少用到了,Kitchen 和 Pan 命令行工具用于实际的生产环境。
  • 部署生产阶段一般需要通过命令行执行,需要把命令行放到 Shell 脚本中,并定时调度这个脚本。
  • Kitchen 和 Pan 工具是 Kettle 的命令行执行程序,只是在 Kettle 执行引擎上的封装,它们只是解释命令行参数,调用并把这些参数传递给 Kettle 引擎。
  • Kitchen 和 Pan 在概念和用法上都非常相近,这两个命令的参数也基本是一样的。唯一不同的是 Kitchen 用于执行作业,Pan 用于执行转换。

Kettle 概念模型

Kettle 的执行分为两个层次:Job(作业,.kjb 后缀)和 Transformation(转换,.ktr 后缀)

img

简单地说,一个转换就是一个 ETL 的过程,而作业则是多个转换、作业的集合,在作业中可以对转换或作业进行调度、定时任务等。

在实际过程中,写的流程不能很复杂,当数据抽取需要多步骤时,需要分成多个转换,在集成到一个作业里顺序摆放,然后执行即可。

目录文件功能说明

img

img

img

下载及安装

1
2
java复制代码官网各个版本下载地址:https://sourceforge.net/projects/pentaho/files/Data%20Integration/
国内 Kettle 论坛网:https://www.kettle.net.cn/

Kettle 是纯 Java 编程的开源软件,需要安装 JDK,并配置环境变量,解压后直接使用无需安装。

需准备的其他东西:数据库驱动,如将驱动放在 Kettle 根目录的 bin 文件夹下面即可。

打开 Kettle 只需要运行 Spoon.bat (win)/ spoon.sh (Linux / macOS),即可打开 Spoon 图形工具。

启动 Kettle

如下图,执行 ./spoon.sh 命令

image-20210705214148018

欢迎页面

首页

HelloWorld

把数据从 CSV 文件复制到 Excel 文件

CSV 文件到 Excel 文件

CSV 文件输入

CSV 输入控件

将 「CSV 文件输入」拖拽到右侧的工作区,双击进行编辑,浏览选择准备好的测试文件,点击「获取字段」自动获取 CSV 文件中表头信息,输入配置完成,下一步进行输出配置。

编辑 CSV 文件输入

Excel 输出

Excel 输出

将 「Excel 输出」拖拽到右侧的工作区,双击进行编辑,这步比较简单,浏览选择输出目录和设置文件名,完成配置。

输出配置

转换文件

按住 shift + 鼠标左键可以建立连接,保存转换配置

image-20210705223800432

运行转换

image-20210705223644738

查看结果

运行结果

总结

初步了解 Kettle 核心组件及其使用

  • 作业和转换可以在图形界面里执行,但这只适合在开发、测试和调试阶段。在开发完成后,需要部署到生产环境中 Spoon 就很少用到了,Kitchen 和 Pan 命令行工具用于实际的生产环境。
  • 部署生产阶段一般需要通过命令行执行,需要把命令行放到 Shell 脚本中,并定时调度这个脚本。
  • Kitchen 和 Pan 工具是 Kettle 的命令行执行程序,只是在 Kettle 执行引擎上的封装,它们只是解释命令行参数,调用并把这些参数传递给 Kettle 引擎。
  • Kitchen 和 Pan 在概念和用法上都非常相近,这两个命令的参数也基本是一样的。唯一不同的是 Kitchen 用于执行作业,Pan 用于执行转换。

分步操作一个 HelloWrold 过程

helloworld_compress.gif

欢迎关注公众号:HelloTech,获取更多内容

本文转载自: 掘金

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

一致性hash原理 看这一篇就够了

发表于 2021-07-06

​ 在了解一致性哈希算法之前,最好先了解一下缓存中的一个应用场景,了解了这个应用场景之后,再来理解一致性哈希算法,就容易多了,也更能体现出一致性哈希算法的优点,那么,我们先来描述一下这个经典的分布式缓存的应用场景。

1、场景描述

​ 假设,我们有三台缓存服务器,用于缓存图片,我们为这三台缓存服务器编号为0号、1号、2号,现在,有3万张图片需要缓存,我们希望这些图片被均匀的缓存到这3台服务器上,以便它们能够分摊缓存的压力。也就是说,我们希望每台服务器能够缓存1万张左右的图片,那么,我们应该怎样做呢?如果我们没有任何规律的将3万张图片平均的缓存在3台服务器上,可以满足我们的要求吗?可以!但是如果这样做,当我们需要访问某个缓存项时,则需要遍历3台缓存服务器,从3万个缓存项中找到我们需要访问的缓存,遍历的过程效率太低,时间太长,当我们找到需要访问的缓存项时,时长可能是不能被接收的,也就失去了缓存的意义,缓存的目的就是提高速度,改善用户体验,减轻后端服务器压力,如果每次访问一个缓存项都需要遍历所有缓存服务器的所有缓存项,想想就觉得很累,那么,我们该怎么办呢?原始的做法是对缓存项的键进行哈希,将hash后的结果对缓存服务器的数量进行取模操作,通过取模后的结果,决定缓存项将会缓存在哪一台服务器上,这样说可能不太容易理解,我们举例说明,仍然以刚才描述的场景为例,假设我们使用图片名称作为访问图片的key,假设图片名称是不重复的,那么,我们可以使用如下公式,计算出图片应该存放在哪台服务器上。

hash(图片名称)% N

​ 因为图片的名称是不重复的,所以,当我们对同一个图片名称做相同的哈希计算时,得出的结果应该是不变的,如果我们有3台服务器,使用哈希后的结果对3求余,那么余数一定是0、1或者2,没错,正好与我们之前的服务器编号相同,如果求余的结果为0, 我们就把当前图片名称对应的图片缓存在0号服务器上,如果余数为1,就把当前图片名对应的图片缓存在1号服务器上,如果余数为2,同理,那么,当我们访问任意一个图片的时候,只要再次对图片名称进行上述运算,即可得出对应的图片应该存放在哪一台缓存服务器上,我们只要在这一台服务器上查找图片即可,如果图片在对应的服务器上不存在,则证明对应的图片没有被缓存,也不用再去遍历其他缓存服务器了,通过这样的方法,即可将3万张图片随机的分布到3台缓存服务器上了,而且下次访问某张图片时,直接能够判断出该图片应该存在于哪台缓存服务器上,这样就能满足我们的需求了,我们暂时称上述算法为HASH算法或者取模算法,取模算法的过程可以用下图表示。

​ 但是,使用上述HASH算法进行缓存时,会出现一些缺陷,试想一下,如果3台缓存服务器已经不能满足我们的缓存需求,那么我们应该怎么做呢?没错,很简单,多增加两台缓存服务器不就行了,假设,我们增加了一台缓存服务器,那么缓存服务器的数量就由3台变成了4台,此时,如果仍然使用上述方法对同一张图片进行缓存,那么这张图片所在的服务器编号必定与原来3台服务器时所在的服务器编号不同,因为除数由3变为了4,被除数不变的情况下,余数肯定不同,这种情况带来的结果就是当服务器数量变动时,所有缓存的位置都要发生改变,换句话说,当服务器数量发生改变时,所有缓存在一定时间内是失效的,当应用无法从缓存中获取数据时,则会向后端服务器请求数据,同理,假设3台缓存中突然有一台缓存服务器出现了故障,无法进行缓存,那么我们则需要将故障机器移除,但是如果移除了一台缓存服务器,那么缓存服务器数量从3台变为2台,如果想要访问一张图片,这张图片的缓存位置必定会发生改变,以前缓存的图片也会失去缓存的作用与意义,由于大量缓存在同一时间失效,造成了缓存的雪崩,此时前端缓存已经无法起到承担部分压力的作用,后端服务器将会承受巨大的压力,整个系统很有可能被压垮,所以,我们应该想办法不让这种情况发生,但是由于上述HASH算法本身的缘故,使用取模法进行缓存时,这种情况是无法避免的,为了解决这些问题,一致性哈希算法诞生了。

​ 我们来回顾一下使用上述算法会出现的问题。

​ 问题1:当缓存服务器数量发生变化时,会引起缓存的雪崩,可能会引起整体系统压力过大而崩溃(大量缓存同一时间失效)。

​ 问题2:当缓存服务器数量发生变化时,几乎所有缓存的位置都会发生改变,怎样才能尽量减少受影响的缓存呢?

其实,上面两个问题是一个问题,那么,一致性哈希算法能够解决上述问题吗?

我们现在就来了解一下一致性哈希算法。

2、一致性hash算法的基本概念

​ 其实,一致性哈希算法也是使用取模的方法,只是,刚才描述的取模法是对服务器的数量进行取模,而一致性哈希算法是对2^32取模,什么意思呢?我们慢慢聊。

​ 首先,我们把二的三十二次方想象成一个圆,就像钟表一样,钟表的圆可以理解成由60个点组成的圆,而此处我们把这个圆想象成由2^32个点组成的圆,示意图如下:

​ 圆环的正上方的点代表0,0点右侧的第一个点代表1,以此类推,2、3、4、5、6……直到2^32-1,也就是说0点左侧的第一个点代表2^32-1 ,我们把这个由2的32次方个点组成的圆环称为hash环。

​ 那么,一致性哈希算法与上图中的圆环有什么关系呢?我们继续聊,仍然以之前描述的场景为例,假设我们有3台缓存服务器,服务器A、服务器B、服务器C,那么,在生产环境中,这三台服务器肯定有自己的IP地址,我们使用它们各自的IP地址进行哈希计算,使用哈希后的结果对2^32取模,可以使用如下公式示意。

​ hash(服务器A的IP地址) % 2^32

​ 通过上述公式算出的结果一定是一个0到2^32-1之间的一个整数,我们就用算出的这个整数,代表服务器A,既然这个整数肯定处于0到2^32-1之间,那么,上图中的hash环上必定有一个点与这个整数对应,而我们刚才已经说明,使用这个整数代表服务器A,那么,服务器A就可以映射到这个环上,用下图示意

​ 同理,服务器B与服务器C也可以通过相同的方法映射到上图中的hash环中

​ hash(服务器B的IP地址) % 2^32

​ hash(服务器C的IP地址) % 2^32

​ 通过上述方法,可以将服务器B与服务器C映射到上图中的hash环上,示意图如下

​ 假设3台服务器映射到hash环上以后如上图所示(当然,这是理想的情况,我们慢慢聊)。

​ 好了,到目前为止,我们已经把缓存服务器与hash环联系在了一起,我们通过上述方法,把缓存服务器映射到了hash环上,那么使用同样的方法,我们也可以将需要缓存的对象映射到hash环上。

1
bash复制代码	假设,我们需要使用缓存服务器缓存图片,而且我们仍然使用图片的名称作为找到图片的key,那么我们使用如下公式可以将图片映射到上图中的hash环上。

​ hash(图片名称) % 2^32

​ 映射后的示意图如下,下图中的橘黄色圆形表示图片

​ 好了,现在服务器与图片都被映射到了hash环上,那么上图中的这个图片到底应该被缓存到哪一台服务器上呢?上图中的图片将会被缓存到服务器A上,为什么呢?因为从图片的位置开始,沿顺时针方向遇到的第一个服务器就是A服务器,所以,上图中的图片将会被缓存到服务器A上,如下图所示。

​ 没错,一致性哈希算法就是通过这种方法,判断一个对象应该被缓存到哪台服务器上的,将缓存服务器与被缓存对象都映射到hash环上以后,从被缓存对象的位置出发,沿顺时针方向遇到的第一个服务器,就是当前对象将要缓存于的服务器,由于被缓存对象与服务器hash后的值是固定的,所以,在服务器不变的情况下,一张图片必定会被缓存到固定的服务器上,那么,当下次想要访问这张图片时,只要再次使用相同的算法进行计算,即可算出这个图片被缓存在哪个服务器上,直接去对应的服务器查找对应的图片即可。

​ 刚才的示例只使用了一张图片进行演示,假设有四张图片需要缓存,示意图如下

​ 1号、2号图片将会被缓存到服务器A上,3号图片将会被缓存到服务器B上,4号图片将会被缓存到服务器C上。

3、一致性哈希算法的优点

​ 经过上述描述,我想兄弟你应该已经明白了一致性哈希算法的原理了,但是话说回来,一致性哈希算法能够解决之前出现的问题吗,我们说过,如果简单的对服务器数量进行取模,那么当服务器数量发生变化时,会产生缓存的雪崩,从而很有可能导致系统崩溃,那么使用一致性哈希算法,能够避免这个问题吗?我们来模拟一遍,即可得到答案。

​ 假设,服务器B出现了故障,我们现在需要将服务器B移除,那么,我们将上图中的服务器B从hash环上移除即可,移除服务器B以后示意图如下。

​ 在服务器B未移除时,图片3应该被缓存到服务器B中,可是当服务器B移除以后,按照之前描述的一致性哈希算法的规则,图片3应该被缓存到服务器C中,因为从图片3的位置出发,沿顺时针方向遇到的第一个缓存服务器节点就是服务器C,也就是说,如果服务器B出现故障被移除时,图片3的缓存位置会发生改变

​ 但是,图片4仍然会被缓存到服务器C中,图片1与图片2仍然会被缓存到服务器A中,这与服务器B移除之前并没有任何区别,这就是一致性哈希算法的优点,如果使用之前的hash算法,服务器数量发生改变时,所有服务器的所有缓存在同一时间失效了,而使用一致性哈希算法时,服务器的数量如果发生改变,并不是所有缓存都会失效,而是只有部分缓存会失效,前端的缓存仍然能分担整个系统的压力,而不至于所有压力都在同一时间集中到后端服务器上。

​ 这就是一致性哈希算法所体现出的优点。

4、哈希环的偏斜

​ 在介绍一致性哈希的概念时,我们理想化的将3台服务器均匀的映射到了hash环上,如下图所示

​ 在实际的映射中,服务器可能会被映射成如下模样。

​ 聪明如你一定想到了,如果服务器被映射成上图中的模样,那么被缓存的对象很有可能大部分集中缓存在某一台服务器上,如下图所示。

​ 上图中,1号、2号、3号、4号、6号图片均被缓存在了服务器A上,只有5号图片被缓存在了服务器B上,服务器C上甚至没有缓存任何图片,如果出现上图中的情况,A、B、C三台服务器并没有被合理的平均的充分利用,缓存分布的极度不均匀,而且,如果此时服务器A出现故障,那么失效缓存的数量也将达到最大值,在极端情况下,仍然有可能引起系统的崩溃,上图中的情况则被称之为hash环的偏斜,那么,我们应该怎样防止hash环的偏斜呢?一致性hash算法中使用”虚拟节点”解决了这个问题,我们继续聊。

5、虚拟节点

​ 话接上文,由于我们只有3台服务器,当我们把服务器映射到hash环上的时候,很有可能出现hash环偏斜的情况,当hash环偏斜以后,缓存往往会极度不均衡的分布在各服务器上,聪明如你一定已经想到了,如果想要均衡的将缓存分布到3台服务器上,最好能让这3台服务器尽量多的、均匀的出现在hash环上,但是,真实的服务器资源只有3台,我们怎样凭空的让它们多起来呢,没错,就是凭空的让服务器节点多起来,既然没有多余的真正的物理服务器节点,我们就只能将现有的物理节点通过虚拟的方法复制出来,这些由实际节点虚拟复制而来的节点被称为”虚拟节点”。加入虚拟节点以后的hash环如下。

​ “虚拟节点”是”实际节点”(实际的物理服务器)在hash环上的复制品,一个实际节点可以对应多个虚拟节点。

​ 从上图可以看出,A、B、C三台服务器分别虚拟出了一个虚拟节点,当然,如果你需要,也可以虚拟出更多的虚拟节点。引入虚拟节点的概念后,缓存的分布就均衡多了,上图中,1号、3号图片被缓存在服务器A中,5号、4号图片被缓存在服务器B中,6号、2号图片被缓存在服务器C中,如果你还不放心,可以虚拟出更多的虚拟节点,以便减小hash环偏斜所带来的影响,虚拟节点越多,hash环上的节点就越多,缓存被均匀分布的概率就越大。

本文转载自: 掘金

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

服务器被黑,p0级事故现场

发表于 2021-07-06

本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

事情过程

image.png

网站访问不了的事也有发生过,我当时的也没多想,感觉应该是服务挂了,重启一下就可以了。

image.png

花Gie用二指禅熟练的敲着命令,因为是直接部署在tomcat下面的,所以直接进入tomcat的bin目录,执行./startup.sh,服务成功启动,很完美,访问一下主页试试。

abfc6489985db8d0117a34b64e5724a.png

这……,略显尴尬,于是打开日志看了一下,在日志上提示是表不存在。

image.png

linux查看文件命令:

  • cat: 由第一行开始显示文件内容
  • tac 从最后一行开始显示,与cat相反
  • nl 显示的时候带有行号
  • more 一页一页的显示文件内容
  • less: 与 more 类似,但是比 more 更好的是,他可以往前翻页!
  • head: 只看头几行
  • tail: 只看尾巴几行
  • tailf: 显示动态日志,也可以使用
  • tail -200f: 设置加载的行数

当时还是觉得问题不大,猜想应该是数据库出现异常,没有忽略数据库表大小写之类的问题,然后就用本地的navicat打开数据库看了一下。

不看还好,一看眼睛都湿润了,这也太感人了,两个库删的干干净净,就剩下了一张表,表的内容大致意思就是 你的数据库已被我们黑了,需要交钱才能赎回 。

image.png

image.png

数据库恢复之路

就算一万个那啥啥奔腾而过,花Gie也要坚强,不哭,不能慌。常在河边走哪有不湿鞋,这个时候更需要展现出惊人的应对能力,开启数据库恢复之路。

  • 备份库

数据库损坏,首先我们应该考虑的是以前有没有备份,可能有很多小伙伴没有接触过数据库备份,这里也和小伙伴们大致介绍一下通过过设定Crontab来每天备份MySQL数据库的方法。

  1. 新建脚本,每天定时执行
1
2
3
4
5
6
7
8
9
10
11
12
13
ini复制代码#!/bin/bash
# 数据库认证
user=""
password=""
host=""
db_name=""
# 备份路径
backup_path="/home/mysql"
date=$(date +"%d-%b-%Y")
# 设置导出文件的缺省权限
umask 177
# Dump数据库到SQL文件
mysqldump --user=$user --password=$password --host=$host $db_name > $backup_path/$db_name-$date.sql
  1. 定期删除

通过上面的脚本,我们可以每天导出一份数据库的sql备份文件,但是随着时间变化,数据库备份脚本占用较大内存,可以执行脚本删除一些较久远的备份文件。如果备份的数据库文件内容为空,查看是否缺少系统环境信息mysqldump。

1
2
bash复制代码# 删除7天之前的就备份文件
find $backup_path/* -mtime +7 -exec rm {} ;

是不是很简单呢,但是我的库因为是自己博客,以前也没有被黑的经历,完全没有当回事,所以没有数据备份,忍不住流下没有备份习惯的眼泪。

  • binlog

不慌,还有binlog(此时的内心已经开始慌张,默默祈求当时一定开启过binlog)。

首先打开数据库,输入show variables like 'log_bin';查看

image.png

嗯,很好,结果查出来了,和我想的一样,我没有开启binlog。。。

image.png

在MySQL8以前,这个功能是默认关闭的(内心已经接近崩溃),需要手动开启。虽然我的数据库binlog没有开启,这里还是要给其他小伙伴介绍一下binlog怎么用。

  1. 在/etc/my.cn添加如下配置文件(后面会专门写一篇binlog怎么玩的教程)

[mysqld]

server_id=1918

log_bin = mysql-bin

binlog_format = ROW

image.png

  1. 重启mysql

CentOS 6:service mysqld restart

CentOS 7:systemctl restart mysqld

  1. 使用show variables like '%log_bin%'查看是否开启成功

image.png

  • 服务器快照

前面两种方式都不适用,说实话,我有点慌了,真慌了。。。我个人博客辛辛苦苦积累的十万张图片素材,还有一千多篇文章就这么没了,真的挺可惜的。于是我冥思苦想,想起来我可以去找找服务器运营商管理界面的快照,这是我最后一根救命稻草,我小心翼翼的登录网站,点开快照按钮,我甚至不敢睁开眼。

哈哈哈哈哈哈哈…….哈哈哈哈哈哈哈………啊啊啊啊啊啊啊啊啊啊啊啊…………啊啊啊啊啊啊啊啊啊啊啊啊……………啊哈哈啊哈哈哈哈哈哈。

苍天不负有心人,终于经过这么久的努力,我放弃了….,服务器没有快照。我哭了,哭的好大声。

image.png

最后的倔强

从小就受优秀环境熏陶的花Gie是不会向恶势力低头的,绝不,绝不可能。大家看好,就是这个邮箱…

image.png

血的教训

综合我的这次事件,我这里也给小伙伴们一些警醒,也是一些注意项:

  • 密码强化,千万不要root/root,admin/admin,这简直送人头,非常容易破解
  • 不推荐使用默认的端口3306
  • 极其不推荐放开远程访问权限,在my.cnf 配置文件中添加 bind-address =127.0.0.1, 设置仅限本地访问,如果是硬性需求,只允许特定安全网段访问数据库。
  • 开启binlog
  • 使用云主机的安全组功能,限制访问来源和端口。

总结

对于网络安全,这次也算是一个警醒,也算给小伙伴们做了一个示范,如果有大佬还有其他什么办法,可以留言告诉我,和大家一起分享这些抗争的经验.

当然我们需要注意,遇到这种事情千万不要联系对方,因为对方根本就不会备份你的库,交钱就等于打水漂。

点关注,防走丢

以上就是本期全部内容,如有纰漏之处,请留言指教,非常感谢。我是花Gie,有问题大家随时留言讨论 ,我们下期见🦮。

原创不易,你怎忍心白嫖,如果你觉得这篇文章对你有点用的话,感谢老铁为本文点个赞、评论或转发一下,因为这将是我输出更多优质文章的动力,感谢!

本文转载自: 掘金

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

全新一代API网关,带可视化管理,文档贼友好!

发表于 2021-07-06

SpringBoot实战电商项目mall(50k+star)地址:github.com/macrozheng/…

摘要

提到API网关,大家比较熟悉的有Spring Cloud体系中的Gateway和Zuul,这些网关在使用的时候基本都要修改配置文件或自己开发功能。今天给大家介绍一款功能强大的API网关apisix,自带可视化管理功能,多达三十种插件支持,希望对大家有所帮助!

简介

apisix是一款云原生微服务API网关,可以为API提供终极性能、安全性、开源和可扩展的平台。apisix基于Nginx和etcd实现,与传统API网关相比,apisix具有动态路由和插件热加载,特别适合微服务系统下的API管理。

核心概念

我们先来了解下apisix的一些核心概念,对我们接下来的使用会很有帮助!

  • 上游(Upstream):可以理解为虚拟主机,对给定的多个目标服务按照配置规则进行负载均衡。
  • 路由(Route):通过定义一些规则来匹配客户端的请求,然后对匹配的请求执行配置的插件,并把请求转发给指定的上游。
  • 消费者(Consumer):作为API网关,有时需要知道API的消费方具体是谁,通常可以用来做身份认证。
  • 服务(Service): 可以理解为一组路由的抽象。它通常与上游是一一对应的,路由与服务之间,通常是多对一的关系。
  • 插件(Plugin):API网关对请求的增强操作,可以对请求增加限流、认证、黑名单等一系列功能。可以配置在消费者、服务和路由之上。

安装

由于官方提供了Docker Compose部署方案,只需一个脚本即可安装apisix的相关服务,非常方便,这里我们也采用这种方案来部署。

  • 首先下载apisix-docker项目,其实我们只需要使用其中的example目录就行了,下载地址:github.com/apache/apis…

  • 接下来我们把example目录上传到Linux服务器上去,来了解下这个目录里面的东西;
1
2
3
4
5
6
7
8
9
bash复制代码drwxrwxrwx. 2 root root   25 Jun 19 10:12 apisix_conf   # apisix配置文件目录
drwxrwxrwx. 2 root root 71 Jun 24 09:36 apisix_log # apisix日志文件目录
drwxrwxrwx. 2 root root 23 Jun 23 17:10 dashboard_conf # 可视化工具apisix-dashboard配置文件目录
-rwxrwxrwx. 1 root root 1304 Jun 19 10:12 docker-compose-alpine.yml # docker-compose 部署脚本(alpine)版本
-rwxrwxrwx. 1 root root 1453 Jun 19 10:12 docker-compose.yml # docker-compose 部署脚本
drwxrwxrwx. 2 root root 27 Jun 19 10:12 etcd_conf # ectd配置文件目录
drwxrwxrwx. 3 root root 31 Jun 23 17:06 etcd_data # ectd数据目录
drwxrwxrwx. 2 root root 107 Jun 19 10:12 mkcert
drwxrwxrwx. 2 root root 40 Jun 19 10:12 upstream # 两个测试用的Nginx服务配置
  • 从docker-compose.yml中我们可以发现,该脚本不仅启动了apisix、apisix-dashboard、etcd这三个核心服务,还启动了两个测试用的Nginx服务;
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
yaml复制代码version: "3"

services:
# 可视化管理工具apisix-dashboard
apisix-dashboard:
image: apache/apisix-dashboard:2.7
restart: always
volumes:
- ./dashboard_conf/conf.yaml:/usr/local/apisix-dashboard/conf/conf.yaml
ports:
- "9000:9000"
networks:
apisix:

# 网关apisix
apisix:
image: apache/apisix:2.6-alpine
restart: always
volumes:
- ./apisix_log:/usr/local/apisix/logs
- ./apisix_conf/config.yaml:/usr/local/apisix/conf/config.yaml:ro
depends_on:
- etcd
##network_mode: host
ports:
- "9080:9080/tcp"
- "9443:9443/tcp"
networks:
apisix:

# apisix配置数据存储etcd
etcd:
image: bitnami/etcd:3.4.15
user: root
restart: always
volumes:
- ./etcd_data:/bitnami/etcd
environment:
ETCD_ENABLE_V2: "true"
ALLOW_NONE_AUTHENTICATION: "yes"
ETCD_ADVERTISE_CLIENT_URLS: "http://0.0.0.0:2379"
ETCD_LISTEN_CLIENT_URLS: "http://0.0.0.0:2379"
ports:
- "2379:2379/tcp"
networks:
apisix:

# 测试用nginx服务web1,调用返回 hello web1
web1:
image: nginx:1.19.0-alpine
restart: always
volumes:
- ./upstream/web1.conf:/etc/nginx/nginx.conf
ports:
- "9081:80/tcp"
environment:
- NGINX_PORT=80
networks:
apisix:

# 测试用nginx服务web2,调用返回 hello web2
web2:
image: nginx:1.19.0-alpine
restart: always
volumes:
- ./upstream/web2.conf:/etc/nginx/nginx.conf
ports:
- "9082:80/tcp"
environment:
- NGINX_PORT=80
networks:
apisix:

networks:
apisix:
driver: bridge
  • 在docker-compose.yml文件所在目录下,使用如下命令可以一次性启动所有服务;
1
bash复制代码docker-compose -p apisix-docker up -d
  • 启动成功后,使用如下命令可查看所有服务的运行状态;
1
bash复制代码docker-compose -p apisix-docker ps
1
2
3
4
5
6
7
bash复制代码              Name                            Command               State                       Ports                     
--------------------------------------------------------------------------------------------------------------------------
apisix-docker_apisix-dashboard_1 /usr/local/apisix-dashboar ... Up 0.0.0.0:9000->9000/tcp
apisix-docker_apisix_1 sh -c /usr/bin/apisix init ... Up 0.0.0.0:9080->9080/tcp, 0.0.0.0:9443->9443/tcp
apisix-docker_etcd_1 /opt/bitnami/scripts/etcd/ ... Up 0.0.0.0:2379->2379/tcp, 2380/tcp
apisix-docker_web1_1 /docker-entrypoint.sh ngin ... Up 0.0.0.0:9081->80/tcp
apisix-docker_web2_1 /docker-entrypoint.sh ngin ... Up 0.0.0.0:9082->80/tcp
  • 接下来就可以通过可视化工具来管理apisix了,登录账号密码为admin:admin,访问地址:http://192.168.5.78:9000/

  • 登录之后看下界面,还是挺漂亮的,apisix搭建非常简单,基本无坑;

  • 还有两个测试服务,web1访问地址:http://192.168.5.78:9081/

  • 另一个测试服务web2访问地址:http://192.168.5.78:9082/

使用

apisix作为新一代的网关,不仅支持基本的路由功能,还提供了丰富的插件,功能非常强大。

基本使用

我们先来体验下apisix的基本功能,之前已经启动了两个Nginx测试服务web1和web2,接下来我们将通过apisix的路由功能来访问它们。

  • 首先我们需要创建上游(Upstream),上游相当于虚拟主机的概念,可以对真实的服务提供负载均衡功能;

  • 创建web1的上游,设置好名称、负载均衡算法和目标节点信息;

  • 再按照上述方法创建web2的上游,创建完成后上游列表显示如下;

  • 再创建web1的路由(Route),路由可以用于匹配客户端的请求,然后转发到上游;

  • 再选择好路由的上游为web1;

  • 接下来选择需要应用到路由上的插件,apisix的插件非常丰富,多达三十种,作为基本使用,我们暂时不选插件;

  • 再创建web2的路由,创建完成后路由列表显示如下;

  • 接下来我们通过apisix网关访问下web1服务:http://192.168.5.78:9080/web1/

  • 接下来我们通过apisix网关访问下web2服务:http://192.168.5.78:9080/web2/

进阶使用

apisix通过启用插件,可以实现一系列丰富的功能,下面我们来介绍几个实用的功能。

身份认证

使用JWT来进行身份认证是一种非常流行的方式,这种方式在apisix中也是支持的,可以通过启用jwt-auth插件来实现。

  • 首先我们需要创建一个消费者对象(Consumer);

  • 然后在插件配置中启用jwt-auth插件;

  • 启用插件时配置好插件的key和secret;

  • 创建成功后消费者列表时显示如下;

  • 之后再创建一个路由,路由访问路径匹配/auth/*,只需启用jwt-auth插件即可;

  • 访问接口获取生成好的JWT Token,需要添加两个参数,key为JWT插件中配置的key,payload为JWT中存储的自定义负载数据,JWT Token生成地址:http://192.168.5.78:9080/apisix/plugin/jwt/sign

  • 不添加JWT Token访问路由接口,会返回401,接口地址:http://192.168.5.78:9080/auth/

  • 在请求头Authorization中添加JWT Token后即可正常访问;

  • 当然apisix支持的身份认证并不只这一种,还有下面几种。

限流功能

有时候我们需要对网关进行限流操作,比如每个客户端IP在30秒内只能访问2次接口,可以通过启用limit-count插件来实现。

  • 我们在创建路由的时候可以选择配置limit-count插件;

  • 然后对limit-count插件进行配置,根据remote_addr进行限流;

  • 当我们在30秒内第3次调用接口时,apisix会返回503来限制我们的调用。

跨域支持

如果你想让网关支持跨域访问的话,可以通过启用cors插件来实现。

  • 我们在创建路由的时候可以选择配置cors插件;

  • 然后对cors插件进行配置,配置好跨域访问策略;

  • 调用接口测试可以发现接口已经返回了CORS相关的请求头。

总结

体验了一把apisix这个全新一代的API网关,有可视化管理的网关果然不一样,简单易用,功能强大!如果你的微服务是云原生的话,可以试着用它来做网关。

其实apisix并不是个小众框架,很多国内外大厂都在使用了,如果你想知道哪些公司在使用,可以参考下面的连接。

github.com/apache/apis…

参考资料

apisix的官方文档非常友好,支持中文,简直是业界良心!过一遍官方文档基本就能掌握apisix了。

官方文档:apisix.apache.org/zh/docs/api…

项目源码地址

github.com/apache/apis…

本文 GitHub github.com/macrozheng/… 已经收录,欢迎大家Star!

本文转载自: 掘金

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

SpringBoot中使用Bean Validation校验

发表于 2021-07-06

七月第一文

GitHub地址

一、简述

JSR-303 是 JAVA EE 6 中的一项子规范,叫做 Bean Validation。Bean Validation 为 Java Bean 验证定义了相应的元数据模型和 API。用于对Java Bean 中的字段的值进行验证,使得基本的验证逻辑可以从业务代码中脱离出来。

常用的注解:

Constraint 详细信息 作用类型
@Null 被注释的元素必须为 null 引用类型
@NotNull 被注释的元素必须不为 null 引用类型
@AssertTrue 被注释的元素必须为 true boolean
@AssertFalse 被注释的元素必须为 false boolean
@Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 byte、short、int、long及对应的包装类型以及BigDecimal、BigInteger
@Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 byte、short、int、long及对应的包装类型以及BigDecimal、BigInteger
@DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 byte、short、int、long及对应的包装类型以及BigDecimal、BigInteger、String
@DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 byte、short、int、long及对应的包装类型以及BigDecimal、BigInteger、String
@Size(max, min) 被注释的元素的大小必须在指定的范围内 String、Collection、Map和数组
@Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内 byte、short、int、long及各自的包装类型以及BigDecimal、BigInteger、String
@Past 被注释的元素必须是一个过去的日期 java.util.Date,java.util.Calendar
@Future 被注释的元素必须是一个将来的日期 java.util.Date,java.util.Calendar
@Pattern(regex=) 被注释的元素必须符合指定的正则表达式 String
@Valid 被注释的元素需要递归验证 引用对象

还有hibernate Validator新增的注解

Constraint 详细信息 作用类型
@Email 被注释的元素必须是电子邮箱地址 String
@Length(min=下限, max=上限) 被注释的字符串的大小必须在指定的范围内 String
@NotEmpty 被注释的元素的必须非空并且size大于0 String、Collection、Map和数组
@NotBlank 被注释的元素必须不为空且不能全部为’ ‘(空字符串) String
@Range(min=最小值, max=最大值) 被注释的元素必须在合适的范围内 byte、short、int、long及各自的包装类型以及BigDecimal、BigInteger、String

二、开始使用Bean Validation

使用到的依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
xml复制代码    <dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>

假如有一个文章的实体类,用于接收前端的传来的数据。使用了校验注解@Size和@NotBlank来校验文章标题是否为空,且长度是都在允许的范围内;使用了校验注解@Email来校验是否符合邮箱格式;使用校验注解@URL来校验封面是否是一个url。

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
java复制代码@Data
public class Article {
/**
* 文章id
*/
private Long articleId;
/**
* 文章标题
*/
@Size(min = 5, max = 10)
@NotBlank(message = "文章标题不能为空")
private String title;
/**
* 作者
*/
private String author;
/**
* 描述
*/
private String description;
/**
* 内容
*/
private String context;
/**
* 邮箱
*/
@Email
private String email;
/**
* 封面
*/
@URL
private String coverImage;

/**
* 状态
*/
private Integer status;
}

编写controller,一个Valid对应一个BindingResult。

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
java复制代码@RestController
public class TestController {

@PostMapping("test")
public Object test(@Valid @RequestBody Article article, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
// 这里用一个Map来模拟开发中的一个返回对象
HashMap<String, Object> map = new HashMap<>(3);

HashMap<String, String> data = new HashMap<>(16);
bindingResult.getFieldErrors().stream().forEach(item -> {
String message = item.getDefaultMessage();
String field = item.getField();
data.put(field, message);
});

map.put("code", 400);
map.put("message", "参数不合法");
map.put("data", data);

return map;
}else {
// 校验成功,继续业务逻辑
// ....
return article;
}
}
}

测试结果:

三、统一异常处理和分组校验

3.1 使用统一异常处理

如果是按照在上面的示例来写,那么每个方法都要去处理BindingResult这会显得很麻烦和很low,使得代码很冗余。

在Spring Boot中可以使用ControllerAdvicd来做统一异常处理。如下:

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

@ExceptionHandler(MethodArgumentNotValidException.class)
public Object handleValidateException(MethodArgumentNotValidException e) {
BindingResult bindingResult = e.getBindingResult();
// 这里用一个Map来模拟开发中的一个返回对象
HashMap<String, Object> map = new HashMap<>(3);

HashMap<String, String> data = new HashMap<>(16);
bindingResult.getFieldErrors().stream().forEach(item -> {
String message = item.getDefaultMessage();
String field = item.getField();
data.put(field, message);
});

map.put("code", 400);
map.put("message", "参数不合法");
map.put("data", data);

return map;
}
}

3.2 使用分组校验

可能会有这种情况,文章在添加的时候是不能带Id使用数据库自增Id或者自定义的Id,但是在更新的时候就必须带上Id。又例如在更新文章状态的时候参数是有文章的状态改变了,只需要校验文章状态。所以引入分组校验的概念,来解决。

1
2
3
4
5
6
7
8
java复制代码public class ValidateGroup {
// 添加时校验
public static interface AddValidate {
}
// 更新时检验
public static interface UpdateValidate {
}
}

实体类,这些校验注解都有一个groups属性,指定该校验注解在那些分组上生效。

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复制代码@Data
public class Article {
/**
* 文章id
*/
@NotNull(message = "文章更新时articleId不能为空", groups = {ValidateGroup.UpdateValidate.class})
@Null(message = "文章新增时articleId必须为空", groups = {ValidateGroup.AddValidate.class})
private Long articleId;
/**
* 文章标题
*/
@Size(min = 5, max = 10, groups = {ValidateGroup.AddValidate.class, ValidateGroup.UpdateValidate.class})
@NotBlank(message = "文章标题不能为空", groups = {ValidateGroup.AddValidate.class, ValidateGroup.UpdateValidate.class})
private String title;
/**
* 作者
*/
private String author;
/**
* 描述
*/
private String description;
/**
* 内容
*/
private String context;
/**
* 邮箱
*/
@Email
private String email;
/**
* 封面
*/
@URL(groups = {ValidateGroup.AddValidate.class, ValidateGroup.UpdateValidate.class})
private String coverImage;

/**
* 状态
*/
private Integer status;
}

编写controller演示方法,这时候校验注解里面就需要添加校验组了,@Valid是没有这个属性的,需要使用@Validated注解,在value属性中填写要生效的校验分组,该属性是个数组可以填写多个校验分组。

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

@PostMapping("add")
public Object add(@Validated(value = {ValidateGroup.AddValidate.class}) @RequestBody Article article) {
return article;
}

@PostMapping("update")
public Object update(@Validated(value = {ValidateGroup.UpdateValidate.class}) @RequestBody Article article) {
return article;
}

}

使用Postman测试一下:

调用添加接口时,文章id必须为空。

调用更新接口时,必须带上文章id。

如果Bean的一些校验注解上没有指定分组,但是在controller的校验参数上指定了校验分组@Validated,那么这些没有指定分组的属性是不会生效的。

四、自定义校验器

假如文章的状态只有0(审核中)、1(已通过)和2(删除)三种状态,所以前端传过来的参数只能是这三个,传过来是其他的那就提示参数有问题。但是看了一下提供的校验注解并没有我们符合我们需求的注解。所以我们需要自己动手自己实现。看下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@Documented
@Constraint(
// 自定义校验器
validatedBy = {ListValueValidator.class}
)
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE,LOCAL_VARIABLE,TYPE_PARAMETER })
@Retention(RUNTIME)
public @interface ListValue {
// 这些可以属性参考Spring中的实现
String message() default "{com.msr.better.annotation.ListValue.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
int[] list() default {};
}

在使用校验注解时,当我没有指定message属性,它是会有一个默认的。这些默认的提示写在一个ValidationMessages.properties属性文件中,所以在resources文件夹下创建一个ValidationMessages.properties文件,内容如下:

1
properties复制代码com.msr.better.annotation.ListValue.message=必须提交指定值

这个默认的属性文件在hibernate-validator这个依赖里面,有各种语言的版本。

在SpringBoot的配置文件中配置一下,防止ValidationMessages.properties属性文件内容中文乱码

1
2
3
yaml复制代码spring:
messages:
encoding: UTF-8

现在编写自定义校验器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码public class ListValueValidator implements ConstraintValidator<ListValue, Integer> {

// 定义一个Set集合存放文章状态
private Set<Integer> set = new HashSet<>();

@Override
public void initialize(ListValue constraintAnnotation) {
// 获取@ListValue注解上list的属性(0、1、2)的指定的文章状态
int[] list = constraintAnnotation.list();
for (int i : list) {
set.add(i);
}
}

@Override
public boolean isValid(Integer value, ConstraintValidatorContext constraintValidatorContext) {
// 参数value就是传过来的参数,校验一下传过来的参数是否在Set集合里面
return set.contains(value);
}
}

新增一个更新文章状态的校验分组:

1
2
3
4
5
6
7
8
9
10
11
java复制代码public class ValidateGroup {
// 添加时校验
public static interface AddValidate {
}
// 更新时检验
public static interface UpdateValidate {
}
// 更新文章状态时校验
public static interface ArticleStatusValidate {
}
}

使用自定义的校验注解:

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Data
public class Article {
// ...
// 同上省略

/**
* 状态
*/
@ListValue(list = {0, 1, 2}, groups = {ValidateGroup.ArticleStatusValidate.class})
private Integer status;
}

编写一个controller的方法来演示:

1
2
3
4
java复制代码	@PutMapping("update/status")
public Object updateStatus(@Validated(value = {ValidateGroup.ArticleStatusValidate.class}) @RequestBody Article article) {
return article;
}

使用Postman测试:

五、总结

有些参数校验通过提供的注解来实现,可以减少手动编写校验代码,让整个项目更加的整洁。

本文转载自: 掘金

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

Qt项目升级到Qt6吐血经验总结

发表于 2021-07-06

Qt的版本发布越来越频繁,Qt6发布已经有一段时间了,越来越多的人咨询之前的代码是否可以增加对Qt6的支持,包括开源的项目QWidgetDemo(一年时间超过2.6K star),近期百忙之中,对所有项目进行了Qt6的兼容升级,自此所有项目从Qt4兼容到了Qt6,耗时一个多月,总计更新调整代码行数一万多行。虽然更新工作量不是很大,但还是废了九牛二虎之力,毕竟项目数太多,光几十个项目作品中的其中之一自定义控件就188个子项目,为了方便自己也为了总结分享经验,特意整理了从Qt4兼容到Qt6的一些经验要点,只是部分,后期持续更新,也欢迎各位留言补充。

直观总结

  1. 增加了很多轮子,同时原有模块拆分的也更细致,估计为了方便拓展个管理。
  2. 把一些过度封装的东西移除了(比如同样的功能有多个函数),保证了只有一个函数执行该功能。
  3. 把一些Qt5中兼容Qt4的方法废弃了,必须用Qt5中对应的新的函数。
  4. 跟随时代脚步,增加了不少新特性以满足日益增长的客户需求。
  5. 对某些模块和类型及处理进行了革命性的重写,运行效率提高不少。
  6. 有参数类型的变化,比如 long * 到 qintptr * 等,更加适应后续的拓展以及同时对32 64位不同系统的兼容。
  7. 源码中的double数据类型全部换成了qreal,和Qt内部数据类型高度一致和统一。
  8. 我测试的都是QWidget部分,quick部分没有测试,估计quick部分更新可能会更多。
  9. 强烈建议暂时不要用Qt6.0到Qt6.2之间的版本,一些模块还缺失,相对来说BUG也比较多,推荐6.2版本开始正式迁移。

经验总结

  1. 万能方法:安装5.15版本,定位到报错的函数,切换到源码头文件,可以看到对应提示字样 QT_DEPRECATED_X(“Use sizeInBytes”) 和新函数。按照这个提示类修改就没错,一些函数是从Qt5.7 5.9 5.10等版本新增加的,可能你的项目还用的Qt4的方法,但是Qt6以前都兼容这些旧方法,到了Qt6就彻底需要用新方法了。
  2. Qt6对core这个核心类进行了拆分,多出来core5compat,因此你需要在pro增加对应的模块已经代码中引入对应的头文件。
1
2
3
4
5
6
7
8
9
10
11
cpp复制代码//pro文件引入模块
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
greaterThan(QT_MAJOR_VERSION, 5): QT += core5compat

//代码中引入头文件
#if (QT_VERSION >= QT_VERSION_CHECK(5,0,0))
#include <QtWidgets>
#endif
#if (QT_VERSION >= QT_VERSION_CHECK(6,0,0))
#include <QtCore5Compat>
#endif
  1. 默认Qt6开启了高分屏支持,界面会变得很大,甚至字体发虚,很多人会不习惯,因为这种模式如果程序很多坐标计算没有采用devicePixelRatio进行运算的话,100%会出现奇奇怪怪的问题,因为坐标不准确了。要取消这种效果可以设置高分屏缩放因子。
1
2
3
cpp复制代码#if (QT_VERSION >= QT_VERSION_CHECK(6,0,0))
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::Floor);
#endif
  1. 原有的随机数函数提示用QRandomGenerator替代,为了兼容所有qt版本,改动最小的办法是直接用c++中的随机数,比如qsrand函数换成srand,qrand函数换成rand,查看过源代码,其实封装的就是c++中的随机数,很多类似的封装比如qSin封装的sin。
  2. QColor的 light 改成 lighter ,dark 改成 darker,其实 lighter、darker 这两个方法以前一直有。
  3. QFontMetricsF 中的 fm.width 换成 fm.horizontalAdvance ,从5.11开始用新函数。
  4. QPalette调色板枚举值,Foreground = WindowText, Background = Window,其中 Foreground 和 Background 没有了,要用 WindowText 和 Window 替代,以前就有。类似的还有 setTextColor 改成了 setForeground 。
  5. QWheelEvent的 delta() 改成 angleDelta().y(),pos() 改成 position() 。
  6. svg模块拆分出来了svgwidgets,如果用到了该模块则需要在pro增加 QT += svgwidgets 。
  7. qlayout中的 margin() 函数换成 contentsMargins().left(),查看源码得知以前的 margin() 返回的就是 contentsMargins().left(),在四个数值一样的时候,默认四个数值就是一样。类似的还有setMargin移除了,统统用setContentsMargins。
  8. 之前 QChar c = 0xf105 全部要改成强制转换 QChar c = (QChar)0xf105,不再有隐式转换,不然编译报错提示error: conversion from ‘int’ to ‘QChar’ is ambiguous 。
  9. qSort等一些函数用回c++的 std::sort 。
1
2
3
4
5
cpp复制代码#if (QT_VERSION >= QT_VERSION_CHECK(6,0,0))
std::sort(ipv4s.begin(), ipv4s.end());
#else
qSort(ipv4s);
#endif
  1. Qt::WA_NoBackground 改成 Qt::WA_OpaquePaintEvent 。
  2. QMatrix 类废弃了没有了,换成 QTransform ,函数功能基本一致,QTransform 类在Qt4就一直有。
  3. QTime 计时去掉了,需要改成 QElapsedTimer ,QElapsedTimer 类在Qt4就一直有。
  4. QApplication::desktop()废弃了, 换成了 QApplication::primaryScreen()。
1
2
3
4
5
6
7
8
9
cpp复制代码#if (QT_VERSION > QT_VERSION_CHECK(5,0,0))
#include "qscreen.h"
#define deskGeometry qApp->primaryScreen()->geometry()
#define deskGeometry2 qApp->primaryScreen()->availableGeometry()
#else
#include "qdesktopwidget.h"
#define deskGeometry qApp->desktop()->geometry()
#define deskGeometry2 qApp->desktop()->availableGeometry()
#endif
  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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
cpp复制代码//获取当前屏幕索引
int QUIHelper::getScreenIndex()
{
//需要对多个屏幕进行处理
int screenIndex = 0;
#if (QT_VERSION >= QT_VERSION_CHECK(5,0,0))
int screenCount = qApp->screens().count();
#else
int screenCount = qApp->desktop()->screenCount();
#endif

if (screenCount > 1) {
//找到当前鼠标所在屏幕
QPoint pos = QCursor::pos();
for (int i = 0; i < screenCount; ++i) {
#if (QT_VERSION >= QT_VERSION_CHECK(5,0,0))
if (qApp->screens().at(i)->geometry().contains(pos)) {
#else
if (qApp->desktop()->screenGeometry(i).contains(pos)) {
#endif
screenIndex = i;
break;
}
}
}
return screenIndex;
}

//获取当前屏幕尺寸区域
QRect QUIHelper::getScreenRect(bool available)
{
QRect rect;
int screenIndex = QUIHelper::getScreenIndex();
if (available) {
#if (QT_VERSION >= QT_VERSION_CHECK(5,0,0))
rect = qApp->screens().at(screenIndex)->availableGeometry();
#else
rect = qApp->desktop()->availableGeometry(screenIndex);
#endif
} else {
#if (QT_VERSION >= QT_VERSION_CHECK(5,0,0))
rect = qApp->screens().at(screenIndex)->geometry();
#else
rect = qApp->desktop()->screenGeometry(screenIndex);
#endif
}
return rect;
}
  1. QRegExp类移到了core5compat模块,需要主动引入头文件 #include 。
  2. QWheelEvent构造参数和对应的计算方位函数变了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
cpp复制代码//模拟鼠标滚轮
#if (QT_VERSION < QT_VERSION_CHECK(6,0,0))
QWheelEvent wheelEvent(QPoint(0, 0), -scal, Qt::LeftButton, Qt::NoModifier);
#else
QWheelEvent wheelEvent(QPointF(0, 0), QPointF(0, 0), QPoint(0, 0), QPoint(0, -scal), Qt::LeftButton, Qt::NoModifier, Qt::ScrollBegin, false);
#endif
QApplication::sendEvent(widget, &wheelEvent);

//鼠标滚轮直接修改值
QWheelEvent *whellEvent = (QWheelEvent *)event;
//滚动的角度,*8就是鼠标滚动的距离
#if (QT_VERSION < QT_VERSION_CHECK(6,0,0))
int degrees = whellEvent->delta() / 8;
#else
int degrees = whellEvent->angleDelta().x() / 8;
#endif
//滚动的步数,*15就是鼠标滚动的角度
int steps = degrees / 15;
  1. qVariantValue 改成 qvariant_cast ,qVariantSetValue(v, value) 改成了 v.setValue(val)。相当于退回到最原始的方法,查看qVariantValue源码封装的就是qvariant_cast。
  2. QStyleOption的init改成了initFrom。
  3. QVariant::Type 换成了 QMetaType::Type ,本身以前的 QVariant::Type 封装的就是 QMetaType::Type 。
  4. QStyleOptionViewItemV2 V3 V4 之类的全部没有了,暂时可以用 QStyleOptionViewItem 替代。
  5. QFont的 resolve 的一个重载函数换成了 resolveMask。
  6. QSettings的 setIniCodec 方法移除了,默认就是utf8,不需要设置。
  7. qcombobox 的 activated(QString) 和 currentIndexChanged(QString) 信号删除了,用int索引参数的那个,然后自己通过索引获取值。个人觉得这个没必要删除。
  8. qtscript模块彻底没有了,尽管从Qt5时代的后期版本就提示为废弃模块,一致坚持到Qt6才正式废弃,各种json数据解析全部换成qjson类解析。
  9. QByteArray 的 append indexOf lastIndexOf 等众多方法的QString参数重载函数废弃了,要直接传 QByteArray,就在原来参数基础上加上 .toUtf8() 。查看源码也看得到以前的QString参数也是转成.toUtf8()再去比较。
  10. QDateTime的时间转换函数 toTime_t + setTime_t 名字改了,对应改成了 toSecsSinceEpoch + setSecsSinceEpoch ,这两个方法在Qt5.8时候新增加的。
  11. QLabel的 pixmap 函数之前是指针 *pixmap() 现在换成了引用 pixmap()。
  12. QTableWidget的 sortByColumn 方法移除了默认升序的方法,必须要填入第二个参数表示升序还是降序。
  13. qtnetwork中的错误信号error换成了errorOccurred。
  14. XmlPatterns模块木有了,全部用xml模块重新解析。
  15. nativeEvent的参数类型变了。
1
2
3
4
5
cpp复制代码#if (QT_VERSION >= QT_VERSION_CHECK(6,0,0))
bool nativeEvent(const QByteArray &eventType, void *message, qintptr *result);
#else
bool nativeEvent(const QByteArray &eventType, void *message, long *result);
#endif

本文转载自: 掘金

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

Java动态脚本&规则引擎、计算/表达式引擎 1 常见的使

发表于 2021-07-06

这是我参与新手入门的第1篇文章

  1. 常见的使用场景

在日常的Java项目中,我们免不了会遇到这样的需求:

  1. 动态地获取并运行自定义脚本文件,以实现特定的功能
  2. 对数据流执行用户自定义的数据有效性、公式计算、数据处理ETL(如数据截取、拼接)等不同业务逻辑
  3. 对用户输入的代码或脚本文件进行测试、运行,确保其语法正确、功能正常
  4. 处理需要灵活配置且不断变更的动态业务规则
  5. 代码的热更新、热修复

诸如此类的需求若采用硬编码实现,则迭代成本相当高,每次改动都需要进行开发、测试、部署。同时业务规则的频繁变更会导致代码的开发和维护成本大大提高。

  1. Java动态脚本常见的实现方式

2.1 动态编译执行Java代码

2.2 通过Java脚本引擎执行不同类型的脚本

  • JavaScript
  • Groovy
  • Python
  • Lua
  • Ruby
  • Scala

以上脚本语言可根据项目实际需求、对不同脚本语言的掌握情况出发进行选择。

一般的需求可使用上述动态脚本实现,如果遇到业务规则更为复杂、规模更大、对功能和性能有更高要求的需求时,可考虑更为专业的规则引擎和计算/表达式引擎。

3 进阶:规则引擎&计算/表达式引擎

3.1 规则引擎

规则引擎由推理引擎发展而来,是一种嵌入在应用程序中的组件,实现了将业务决策从应用程序代码中分离出来,并使用预定义的语义模块编写业务决策。接受数据输入,解释业务规则,并根据业务规则做出业务决策。

分离商业决策者的商业决策逻辑和应用开发者的技术决策

  • Drools (推荐)
  • OpenRules
  • EasyRules
  • OpenL Tablets
  • RuleBook
  • Esper

3.2 计算/表达式引擎

表达式(布尔组合)、特殊数学公式计算(高精度)、语法分析、脚本二次定制

  • Fel
  • OGNL
  • Mvel
  • SpEL
  • JEXL
  • JSEL
  • Aviator
  • Expression4j
  • QLExpress
  1. 使用动态脚本及上述引擎的优点

  • 逻辑和数据隔离
  • 可扩展性高
  • 可维护性高
  • 知识集中化
  • 提高业务灵活性
  • 业务透明度增强
  • 减少系统频繁迭代升级风险
  • 简化系统架构
  1. 使用动态脚本常见的问题及解决方案

5.1. 安全问题

  • 提供常见的脚本文件供用户选择(系统提供的脚本文件更规范、更安全、性能更好)
  • 设置脚本编写规范及模板
  • 对用户提交的脚本做健康检查(如对Thread、reflect、truncate等关键词进行检查,做提示确认或拒绝通过)
  • 对脚本进行版本管理,可对脚本做回滚处理
  • 对脚本设置审核机制
  • 可根据实际情况考虑是否对用户的脚本执行进行进程或线程隔离

5.2 OOM问题

Java执行动态脚本的过程往往会产生很多对象,而同一个脚本没必要每次执行时都去获取、编译、执行。可设计缓存策略,对编译后的脚本引擎进行缓存,既可避免频繁创建对象引起的内存溢出问题,又可提高性能


  • 如果这篇文章对你有用的话,点个赞再走呗!
  • 如有问题,欢迎留言评论!
  • 欢迎转载,烦请注明出处!

本文转载自: 掘金

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

为什么说datax是目前最好的异构数据源数据交换工具 (一)

发表于 2021-07-05

听说微信搜索《Java鱼仔》会变更强哦!

本文收录于JavaStarter ,里面有我完整的Java系列文章,学习或面试都可以看看哦

(一)什么是Datax

以前我做过一个项目,其中有个需求就是每天定时把sql server中的数据同步到Mysql中,当时写了一段Java的代码来实现,一套Java代码中需要写两个数据源的连接以及两套sql的代码,十分不方便。如果还要实现Oracle、Mysql、SqlServer的互相同步,那代码逻辑就更加复杂。而且通过代码的方式,同步600万条数据要花费2个多小时,性能效率十分低下。

最近在工作中接触到了一个新的工具datax,才意识到数据同步原来还有这么简单的方式。

Datax是阿里巴巴开源的一个异构数据源离线同步工具,DataX 实现了包括 MySQL、Oracle、SqlServer、Postgre、HDFS、Hive、ADS、HBase、TableStore(OTS)、MaxCompute(ODPS)、Hologres、DRDS 等各种异构数据源之间高效的数据同步功能。

简单来讲,datax就是可以把各个数据库之间的数据来回传输同步,并且操作起来只需要配置一下json文件就可以了。

目前Datax开源在github上:github.com/alibaba/Dat…

(二)Datax架构

Datax的架构采用FrameWork+plugin构建,其中:

Reader:数据采集模块,负责采集数据源的数据,将数据发送给Framework

Writer:数据写入模块,负责不断向Framework取数据,并将数据写入到目的端

Framework:Framework用于连接reader和writer,作为两者的数据传输通道,并处理缓冲、流控、并发、数据转换等核心技术问题。

(三)Datax运行原理

Job:单个作业的管理节点,负责数据清理、子任务划分、TaskGroup监控管理

Task:由Job切分出来,是Datax的最小单元,每隔Task负责一部分数据的同步工作

Schedule:将Task组成TaskGroup,单个TaskGroup的并发数量为5.
TaskGroup:负责启动Task

(四)DataX快速入门

datax的推荐系统为:

  • Linux
  • JDK(1.8以上,推荐1.8)
  • Python(推荐Python2.6.X)
  • Apache Maven 3.x (Compile DataX)

我这里就按照推荐系统进行操作。

首先我们将datax下载下来,datax的下载有两种方式,一种是直接下载压缩包,另外一种是下载源码自己手动编译,这里先展示下载压缩包的使用方式:

首先是下载datax的压缩包:datax-opensource.oss-cn-hangzhou.aliyuncs.com/datax.tar.g…

下载下来后上传到linux服务器上,解压:

1
bash复制代码tar -zxvf datax.tar.gz

进入datax的bin目录,运行自检脚本

1
2
bash复制代码cd datax/bin/
python datax.py ../job/job.json

运行结果如果是下面这样说明datax安装成功。

(五)datax控制台数据同步

datax的作用就是实现异构数据库之间的数据传输,并且应用起来还比较简单,只需要配置好对应的json模板,就可以对数据进行传输。

通过下面的命令,就可以拿到datax对应的json模板,比如我现在的reader是控制台数据,writer也是控制台数据:

1
bash复制代码python datax.py -r streamreader -w streamwriter

就拿到了对应的模板:

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
json复制代码{
"job": {
"content": [
{
"reader": {
"name": "streamreader",
"parameter": {
"column": [],
"sliceRecordCount": ""
}
},
"writer": {
"name": "streamwriter",
"parameter": {
"encoding": "",
"print": true
}
}
}
],
"setting": {
"speed": {
"channel": ""
}
}
}
}

我们简单配置一下看看效果,效果就是在控制台输出十遍hello,world,在job目录下新建一个文件叫stream2stream.json

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
json复制代码{
"job": {
"content": [
{
"reader": {
"name": "streamreader",
"parameter": {
"column": [
{
"type":"string",
"value":"hello"
},
{
"type":"string",
"value":"world"
}
],
"sliceRecordCount": "10"
}
},
"writer": {
"name": "streamwriter",
"parameter": {
"encoding": "UTF-8",
"print": true
}
}
}
],
"setting": {
"speed": {
"channel": "1"
}
}
}
}

运行项目:

1
bash复制代码python datax.py ../job/stream2stream.json

查看效果

(六)datax mysql数据同步

因为本地只装了mysql,就直接用mysql演示数据同步,首先还是通过命令拿到基本配置模板:

1
bash复制代码python datax.py -r mysqlreader -w mysqlwriter

简单介绍一下模板:

column:表示reader或者writer中对应的列名

connection:填写连接信息

where:设置连接条件

具体的其他参数可以在官方文档中全部找到更详细说明

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
json复制代码{
"job": {
"content": [
{
"reader": {
"name": "mysqlreader",
"parameter": {
"column": [], #需要同步的列
"connection": [ #连接信息
{
"jdbcUrl": [],
"table": []
}
],
"password": "", #密码
"username": "", #用户名
"where": "" #筛选条件
}
},
"writer": {
"name": "mysqlwriter",
"parameter": {
"column": [], #写入段的列名,与上面需要同步的值的位置保持一致
"connection": [ #连接信息
{
"jdbcUrl": "",
"table": []
}
],
"password": "", #密码
"preSql": [], #执行写入之前做的事情
"session": [], # DataX在获取Mysql连接时,执行session指定的SQL语句,修改当前connection session属性
"username": "", #用户名
"writeMode": "" #控制写入数据到目标表采用 insert into 或者 replace into 或者 ON DUPLICATE KEY UPDATE 语句
}
}
}
],
"setting": {
"speed": {
"channel": ""
}
}
}
}

做个初始工作,在mysql中先建两张表,一张有数据,一张没有数据:

1
2
3
4
5
6
7
8
9
10
11
12
sql复制代码CREATE TABLE `user`(
`id` int(4) not null auto_increment,
`name` varchar(32) not null,
PRIMARY KEY(id)
)
CREATE TABLE `user2`(
`id` int(4) not null auto_increment,
`name` varchar(32) not null,
PRIMARY KEY(id)
)
INSERT INTO `user` VALUES (1,'javayz')
INSERT INTO `user` VALUES (2,'java')

接下来配置mysql2mysql.json

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
json复制代码{
"job": {
"content": [
{
"reader": {
"name": "mysqlreader",
"parameter": {
"column": [
"id",
"name"
],
"connection": [
{
"jdbcUrl": ["jdbc:mysql://10.10.128.120:3306/test"],
"table": ["user"]
}
],
"password": "123456",
"username": "root"
}
},
"writer": {
"name": "mysqlwriter",
"parameter": {
"column": [
"id",
"name"
],
"connection": [
{
"jdbcUrl": "jdbc:mysql://10.10.128.120:3306/test",
"table": ["user2"]
}
],
"password": "123456",
"username": "root"
}
}
}
],
"setting": {
"speed": {
"channel": "1"
}
}
}
}

同样运行脚本:

1
bash复制代码python datax.py ../job/mysql2mysql.json

控制台输出成功之后,查看数据库,可以发现数据已经同步过去了。

(七)总结

上面展示了两种方式的数据同步,除此之外datax还支持大量的数据库,并且使用文档写的十分详细,大家有兴趣可以自己去尝试一下,十分有意思的工具。

我是鱼仔,我们下期再见!

本文转载自: 掘金

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

10分钟理解-TCP流量控制-滑动窗口

发表于 2021-07-05

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」

TCP就像个老司机,除了买票上车(三次握手),还有荷载量(滑动窗口),这小车就在互联网的世界里反复运输,当乘客过多,车车肯定就装不下了,并通知乘客下一次我有空位你在上来。在互联网中TCP也是如此,当网络传输流量过大,就容易导致接受方处理不过来,容易造成请求积压,那么TCP是如何解决的呢?今儿就要探讨一下TCP流量控制手段滑动窗口。
这里就用案例来说明,这样更容易理解。

假设A向B发送数据。在连接建立时,B告诉A:“我的接收窗口rwnd = 400”(这里rwnd表示receiver window)。这表示发送方A的发送窗口不能超过接收方B给出的接收窗口的数值,也就是400字节。这里注意一下,TCP的窗口单位是字节,不是报文段。再设每一个报文段为100字节长,而数据报文段序号的初始值设为1(见图中第一个箭头上面的序号seq = 1)。本文中A代表发送方,B代表消息接收方。

接收方的主机B进行了三次流量控制。第一次把窗口减小到rwnd =300,第二次又减到rwnd = 100,最后减到rwnd = 0,即不允许发送方再发送数据了。应当注意,B向A发送的三个报文段都设置了ACK = 1,只有在ACK = 1时确认号字段(ack)才有意义。

image-20210705195859028.png

总结一下,就是发送端只能发送接收端指定的报文大小,超过了则暂停发送消息。

滑动窗口流程讲完了,但是问题还没完。

1.如果发送方发送报文丢失,例如上图的seq=201,怎么解决?

2.如果B向A发送0窗口(rwnd=0)后,B的接收缓存又有了一些存储空间,于是B又给A发送于是B向A发送了rwnd = 400的报文段,但是该次请求丢失,那么A岂不是一直还是认为B是0窗口,然后一直等待吗?

带着问题再来梳理一下

A给B发送消息时,A的消息丢失,但不影响之后的消息发送,只要A重连后重新发送即可。但注意的是,重新发送的请求不能发送新数据。

当B给A端发送窗口值丢失时,此时TCP的持续计数器就起到了作用,当A端接收到0窗口通知,持续计数器就会启动,若持续计数器设置的超时时间已经到达,A就会给B发送一个0窗口探测报文,此时B就可以确认窗口值,如果窗口仍然是零,那么收到这个报文段的一方就重新设置持续计时器。如果窗口不是零,那么死锁的僵局就可以打破了。

总结第二下,滑动窗口由窗口值来控制传输流量的大小;利用重发机制解决发送端消息丢失;利用持续计数器和探测报文解决接收端发送的窗口值丢失导致的无限等待情况。

用通俗的案例在加深一下理解:我车子的荷载量只有200(TCP中的窗口值),每次载满后禁止乘客上车(发送端被阻塞),当有部分人下车后(TCP接收缓存又有一部分空间),就可以继续允许乘客上车。

读到这里,是不是又多了一个跟面试官吹牛的小知识点。勇敢牛牛向前冲!!

image.png

本文转载自: 掘金

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

1…619620621…956

开发者博客

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