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

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


  • 首页

  • 归档

  • 搜索

详解 Spring 中 Bean 的作用域与生命周期

发表于 2021-07-19

摘要:在利用 Spring 进行 IOC 配置时,关于 bean 的配置和使用一直都是比较重要的一部分,同时如何合理的使用和创建 bean 对象,也是小伙伴们在学习和使用 Spring 时需要注意的部分,所以这一篇文章我就来和大家讲一下有关 Spring 中 bean 的作用域和其生命周期。

本文分享自华为云社区《详解Spring中Bean的作用域与生命周期》,原文作者:灰小猿。

在利用 Spring 进行 IOC 配置时,关于 bean 的配置和使用一直都是比较重要的一部分,同时如何合理的使用和创建 bean 对象,也是小伙伴们在学习和使用 Spring 时需要注意的部分,所以这一篇文章我就来和大家讲一下有关 Spring 中 bean 的作用域和其生命周期。

一、Bean 的作用域

首先我们来讲一下有关于 bean 的作用域

一般情况下,我们书写在 IOC 容器中的配置信息,会在我们的 IOC 容器运行时被创建,这就导致我们通过 IOC 容器获取到 bean 对象的时候,往往都是获取到了单实例的 Bean 对象

这样就意味着无论我们使用多少个 getBean()方法,获取到的同一个 JavaBean 都是同一个对象,这就是单实例 Bean,整个项目都会共享这一个 bean 对象。

在 Spring 中,可以在元素的 scope 属性里设置 bean 的作用域,以决定这个 bean 是单实例的还是多实例的。Scope 属性有四个参数,具体的使用可以看下图:

详解 Spring 中 Bean 的作用域与生命周期

1、单实例 Bean 声明

默认情况下,Spring 只为每个在 IOC 容器里声明的 bean 创建唯一一个实例,整个 IOC 容器范围内都能共享该实例:所有后续的 getBean()调用和 bean 引用都将返回这个唯一的 bean 实例。该作用域被称为 singleton,它是所有 bean 的默认作用域。也就是单实例。

为了验证这一说法,我们在 IOC 中创建一个单实例的 bean,并且获取该 bean 对象进行对比:

1
2
3
4
5
xml复制代码<!-- singleton单实例bean
1、在容器创建时被创建
2、只有一个实例
-->
<bean id="book02" class="com.spring.beans.Book" scope="singleton"></bean>

测试获取到的单实例 bean 是否是同一个:

1
2
3
4
5
6
7
ini复制代码@Test
public void test09() {
// 单实例创建时创建的两个bean相等
Book book03 = (Book)iocContext3.getBean("book02");
Book book04 = (Book)iocContext3.getBean("book02");
System.out.println(book03==book04);
}

得到的结果是 true

2、多实例 Bean 声明

而既然存在单实例,那么就一定存在多实例。我们可以为 bean 对象的 scope 属性设置 prototype 参数,以表示该实例是多实例的,同时获取 IOC 容器中的多实例 bean,再将获取到的多实例 bean 进行对比

1
2
3
4
5
6
xml复制代码<!-- prototype多实例bean
1、在容器创建时不会被创建,
2、只有在被调用的时候才会被创建
3、可以存在多个实例
-->
<bean id="book01" class="com.spring.beans.Book" scope="prototype"></bean>

测试获取到的多实例 bean 是否是同一个:

1
2
3
4
5
6
7
ini复制代码@Test
public void test09() {
// 多实例创建时,创建的两个bean对象不相等
Book book01 = (Book)iocContext3.getBean("book01");
Book book02 = (Book)iocContext3.getBean("book01");
System.out.println(book01==book02);
}

得到的结果是 false

这就说明了,通过多实例创建的 bean 对象是各不相同的。

在这里需要注意:

同时关于单实例和多实例 bean 的创建也有不同,当 bean 的作用域为单例时,Spring 会在 IOC 容器对象创建时就创建 bean 的对象实例。而当 bean 的作用域为 prototype 时,IOC 容器在获取 bean 的实例时创建 bean 的实例对象。

二、Bean 的生命周期

1、bean 的初始和销毁

其实我们在 IOC 中创建的每一个 bean 对象都是有其特定的生命周期的,在 Spring 的 IOC 容器中可以管理 bean 的生命周期,Spring 允许在 bean 生命周期内特定的时间点执行指定的任务。如在 bean 初始化时执行的方法和 bean 被销毁时执行的方法。

Spring IOC 容器对 bean 的生命周期进行管理的过程可以分为六步:

  1. 通过构造器或工厂方法创建 bean 实例
  1. 为 bean 的属性设置值和对其他 bean 的引用
  1. 调用 bean 的初始化方法
  1. bean 可以正常使用
  1. 当容器关闭时,调用 bean 的销毁方法

那么关于 bean 的初始和销毁时执行的方法又该如何声明呢?

首先我们应该在 bean 类内部添加初始和销毁时执行的方法。如下面这个 javabean:

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
typescript复制代码package com.spring.beans;


public class Book {
private String bookName;
private String author;
/**
* 初始化方法
* */
public void myInit() {
System.out.println("book bean被创建");
}

/**
* 销毁时方法
* */
public void myDestory() {
System.out.println("book bean被销毁");
}

public String getBookName() {
return bookName;
}
public void setBookName(String bookName) {
this.bookName = bookName;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
@Override
public String toString() {
return "Book [bookName=" + bookName + ", author=" + author + "]";
}
}

这时我们在配置 bean 时,可以通过 init-method 和 destroy-method 属性为 bean 指定初始化和销毁方法

1
2
3
4
5
xml复制代码<!-- 设置bean的生命周期
destory-method:结束调用的方法
init-method:起始时调用的方法
-->
<bean id="book01" class="com.spring.beans.Book" destroy-method="myDestory" init-method="myInit"></bean>

这样当我们在通过 IOC 容器创建和销毁 bean 对象时就会执行相应的方法

但是这里还是有一点需要注意:

我们上面说了,单实例的 bean 和多实例的 bean 的创建时间是不同的,那么他们的初始方法和销毁方法的执行时间就稍稍有不同。

单实例下 bean 的生命周期

容器启动——>初始化方法——>(容器关闭)销毁方法

多实例下 bean 的生命周期

容器启动——>调用 bean——>初始化方法——>容器关闭(销毁方法不执行)

2、bean 的后置处理器

什么是 bean 的后置处理器?bean 后置处理器允许在调用初始化方法前后对 bean 进行额外的处理

bean 后置处理器对 IOC 容器里的所有 bean 实例逐一处理,而非单一实例。

其典型应用是:检查 bean 属性的正确性或根据特定的标准更改 bean 的属性。

bean 后置处理器使用时需要实现接口:

org.springframework.beans.factory.config.BeanPostProcessor。

在初始化方法被调用前后,Spring 将把每个 bean 实例分别传递给上述接口的以下两个方法:

postProcessBeforeInitialization(Object, String)调用前

postProcessAfterInitialization(Object, String)调用后

如下是一个实现在该接口的后置处理器:

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
typescript复制代码package com.spring.beans;


import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;


/**
* 测试bean的后置处理器
* 在这里要注意一点是为了出现bean和beanName,而不是arg0、arg1,需要绑定相应的源码jar包
* */
public class MyBeanPostProcessor implements BeanPostProcessor{


/**
* postProcessBeforeInitialization
* 初始化方法执行前执行
* Object bean
* String beanName xml容器中定义的bean名称
* */
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
// TODO Auto-generated method stub
System.out.println("【"+ beanName+"】初始化方法执行前...");
return bean;
}


/**
* postProcessAfterInitialization
* 初始化方法执行后执行
* Object bean
* String beanName xml容器中定义的bean名称
* */
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
// TODO Auto-generated method stub
System.out.println("【"+ beanName+"】初始化方法执行后...");
return bean;
}


}

将该后置处理器加入到 IOC 容器中:

1
2
xml复制代码<!-- 测试bean的后置处理器 -->
<bean id="beanPostProcessor" class="com.spring.beans.MyBeanPostProcessor"></bean>

由于现在我们的 bean 对象是单实例的,所以容器运行时就会直接创建 bean 对象,同时也会执行该 bean 的后置处理器方法和初始化方法,在容器被销毁时又会执行销毁方法。我们测试如下:

1
2
3
4
5
6
7
8
9
markdown复制代码//*************************bean生命周期*****************
// 由于ApplicationContext是一个顶层接口,里面没有销毁方法close,所以需要使用它的子接口进行接收
ConfigurableApplicationContext iocContext01 = new ClassPathXmlApplicationContext("ioc1.xml");

@Test
public void test01() {
iocContext01.getBean("book01");
iocContext01.close();
}

运行结果:

详解 Spring 中 Bean 的作用域与生命周期

详解 Spring 中 Bean 的作用域与生命周期

总结一下后置处理器的执行过程:

  1. 通过构造器或工厂方法创建 bean 实例
  1. 为 bean 的属性设置值和对其他 bean 的引用
  1. 将 bean 实例传递给 bean 后置处理器的**

postProcessBeforeInitialization()**方法

  1. 调用 bean 的初始化方法
  1. 将 bean 实例传递给 bean 后置处理器的**

postProcessAfterInitialization()**方法

  1. bean 可以使用了
  1. 当容器关闭时调用 bean 的销毁方法

所以添加 bean 后置处理器后 bean 的生命周期为:

容器启动——后置处理器的 before…——>初始化方法——>后置处理器的 after…———>(容器关闭)销毁方法

点击关注,第一时间了解华为云新鲜技术~

本文转载自: 掘金

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

如何第一时间收到接口报错?不用测试妹子再质疑你是不是接口挂了

发表于 2021-07-19

啥样的后端程序员是好程序员?能机器做的事绝不自己做,哈哈。

场景复现

  • 客户端:后端接口报错了,我解析数据失败,你看看为啥?
  • 服务端:好,我查查log。你把请求参数给我打印出来。
  • 客户端:我咋打印?
  • 服务端:….我还是自己查log吧

image.png

以上这种场景在开发中是不是时有发生?是不是很难顶?有啥好办法让debug更智能一点吗?

分析

  • 不管哪个语言做服务端开发,一定有异常处理和日志。
  • 找到一个三方平台,当捕获到异常或者有新的打印日志时回调,推送错误日志给我们。
  • 经过一番调研之后,发现钉钉的机器人是个好工作

说干就干,刷文档,写实现。

后端实现以PHP的Laravel为例,其他语言也可以借鉴思路。

image.png

修改日志配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
php复制代码<?php

use Monolog\Handler\NullHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogUdpHandler;

return [

'default' => env('LOG_CHANNEL', 'stack'),

'channels' => [
'stack' => [
'driver' => 'stack',
//测试环境除了使用daily保存每天日志到logs/laravel.log,还使用’dingding‘channel
'channels' => env("APP_ENV") == 'test' ? ['daily', 'dingding'] : ['daily'],
'ignore_exceptions' => false,
],
//配置钉钉 驱动选择 monolog
'dingding' => [
'driver' => 'monolog',
'level' => 'error',
'handler' => \App\Handler\DingdingLogHandler::class, //自定义handler
],

'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => 'debug',
'days' => 14,
],

.
.
.
],

];

上面不重要的代码使用3个竖向排列的.省略显示。

自定义Handler

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
php复制代码<?php
namespace App\Handler;

use App\Library\CurlRequest;
use App\Library\Utility;
use Monolog\Logger;
use Monolog\Handler;

class DingdingLogHandler extends Handler\AbstractProcessingHandler
{
private $apiKey;
private $channel;

public function __construct(
$level = Logger::DEBUG,
bool $bubble = true
) {
parent::__construct($level, $bubble);


}

protected function write(array $record): void
{
$this->send($record['formatted']);
}


protected function send(string $message): void
{
$microSecond = Utility::getMicroSecond();
$key = "xxxx";
$hashString = hash_hmac("sha256", $microSecond ."\n" . $key, $key, true);
$sign = urlencode(base64_encode($hashString));

CurlRequest::post("https://oapi.dingtalk.com/robot/send?access_token=xxxxx&timestamp=".$microSecond."&sign=".$sign,
[
"msgtype" => "text",
"at" => [
"atMobiles" => [
"xxxx",
"xxxx"
]
],
"text" => [
"content" => $message
]
]);
}
}

部署上线的效果

image.png

再也不用爬日志啦!

测试妹子再找我说客户端报错数据解析错误,我也能马上硬气的回答:”应该是客户端解析问题,服务端没收到报错。“

此处放一个机智的表情

image.png

参考文档

  • 日志相关参考文档 laravel中文文档 日志篇
  • 钉钉相关参考文档 钉钉机器人文档

欢迎大家评论点赞关注。
掘金啥时候来个一键三连的功能呀

本文转载自: 掘金

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

Gokins10 来啦~

发表于 2021-07-19

Gokins: More Power

写在前面

作为Gokins开发者的一员,真开心见证Gokins从0.2版本到1.0的蜕变.从1.0版本开始,我们会开始适应更丰富的场景,朝着云原生CI/CD工具的方向发展.感谢大家长久以来的关注和支持,希望以后能够继续支持我们,这也是我们维护开源项目的一大动力.

Gokins

Gokins一款由Go语言和Vue编写的款轻量级、能够持续集成和持续交付的工具.

  • 持续集成和持续交付

作为一个可扩展的自动化服务器,Gokins 可以用作简单的 CI 服务器,或者变成任何项目的持续交付中心

  • 简易安装

Gokins 是一个基于 Go 的独立程序,可以立即运行,包含 Windows、Mac OS X 和其他类 Unix 操作系统。

  • 安全

绝不收集任何用户、服务器信息,是一个独立安全的服务

Gokins 官网

地址 : gokins.cn

可在官网上获取最新的Gokins动态

Gokins Demo

gokins.cn:8030

1
2
makefile复制代码用户名: guest
密码: 123456

Quick Start

It is super easy to get started with your first project.

Step 1: 环境准备

  • Mysql
  • Docker(非必要)

Step 2: 下载

  • Linux下载:bin.gokins.cn/gokins-linu…
  • Mac下载:bin.gokins.cn/gokins-darw…

我们推荐使用docker或者直接下载release的方式安装Gokins`

Step 3: 启动服务

1
bash复制代码./gokins

Step 3: 安装Gokins

访问 http://localhost:8030进入到Gokins安装页面

按页面上的提示填入信息

默认管理员账号密码

username :gokins

pwd: 123456

Step 4: 新建流水线

  • 进入到流水线页面

  • 点击新建流水线

填入流水线基本信息

  • 流水线配置
1
2
3
4
5
6
7
8
9
10
11
12
13
yaml复制代码version: 1.0
vars:
stages:
- stage:
displayName: build
name: build
steps:
- step: shell@sh
displayName: test-build
name: build
env:
commands:
- echo Hello World

关于流水线配置的YML更多信息请访问 YML文档

  • 运行流水线

这里可以选择输入仓库分支或者commitSha,如果不填则为默认分支

  • 查看运行结果

本文转载自: 掘金

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

nacos、ribbon和feign的简明教程 nacos简

发表于 2021-07-19

nacos简明教程

为什么需要nacos?

在微服务架构中,微服务之间经常要相互通信和调用,而且一个服务往往存在多个实例来降低负荷或保证高可用。我们假定A服务要调用B服务,最简单的方式把B服务的地址和端口保存在A服务的配置文件中。然后通过http请求去完成B服务的调用。但是B服务可能有好多个实例,而且可能会随着业务的需求随时的扩展或者停用掉一些实例,这个时候B服务的地址和端口可能会经常发生改变。如果记录在配置文件就多有不便。而且在众多的B服务中,可能有一些服务会出现各种问题坏掉,我们可能还需要写一个心跳检测,看看是不是所有的服务都正常运行,及时地剔除掉那些不能用的服务。如果完备稳定的实现这些功能,是一个不小的工作量。还好凡是有困难的地方总有前人造轮子。而Nacos就是来解决这样问题的轮子。

在这里插入图片描述

如图所示,通过简单的配置和注解,所有的微服务都把自己信息登记到Nacos server中去。在需要调用的时候,通过登记到Nacos server的名字就可以完成微服务间的调用。比如有以前通过访问 http://12.3.3.5:8090/service 来访问微服务的,变成了http://provider/service 的方式来访问,把服务与端口地址解耦。

如何使用Nacos

Nacos server的启动

Nacos使用非常的简单。从Nacos官网下载release包,linux\mac下面执行sh startup.sh -m standalone,windows下面执行startup.cmd -m standalone 然后就可以完成Nacosserver的启动。

在微服务中使用Nacos做服务注册和发现

通过maven架包使用Nacos发现服务

1
2
3
4
5
xml复制代码<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2.2.3.RELEASE</version>
</dependency>

在配置文件中简单配置

1
2
3
4
5
6
7
yml复制代码spring:
application:
name: provider #这个很重要,是注册到Nacos中调用的服务的名称
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 #配置Nacos的服务地址

在启动类上增加注解@EnableDiscoveryClient

1
2
3
4
5
6
7
8
9
java复制代码@SpringBootApplication
@EnableDiscoveryClient
public class ProviderApplication {

public static void main(String[] args) {
SpringApplication.run(ProviderApplication.class, args);
}

}

通过简单的几步就可以完成了把微服务注册到了Nacos Server。怎么样很简单吧。当然Nacos除了做服务注册和发现外,还可以做配置中心,使用方法大同小异。更多丰富的操作参考官方文档 Nacos官方网站

服务的调用

如果要通过http://provider/service的方式去调用微服务,还需要构造http请求,请求回来的结果还要做json解析等等一系列繁杂的工作。而Ribbon就用来解决这个问题的。在springcloud.alibaba的nacos发现服务的Maven包中,已经包含了ribbon.我们通过简单的几行代码,就可以完成微服务的调用。

假定在provider服务中有这么一段代码,我们要调用

1
2
3
4
5
6
java复制代码//例子来自Nacos官网
@RequestMapping(value = "/echo/{string}",method = RequestMethod.GET)
public String echo(@PathVariable String string)
{
return "Hello Nacos Discover" + string;
}

我们只需要实例化一个RestTemplate

1
2
3
4
5
java复制代码@Bean
public RestTemplate restTemplate()
{
return new RestTemplate();
}

然后就可以再想要调用的地方来通过下面的代码来非常简单地调用。

1
java复制代码String result = restTemplate.getForObject("http://provider/echo/"+str,String.class);

负载均衡的问题

前面讲到,在微服务环境中常常同一个服务会有N多实例,我们不希望所有的调用都跑到一个实例上去,这个时候就需要用到负载均衡。我们只需要在启动来加上 @LoadBalanced 注解。在配置文件的spring.application.name相同的应用会被认为是同一个微服务,然后转发可以通过ribbon内置的策略路由到不同的provider中去。

如果我们期望有的provider的优先级比别的优先级高一些,可以再provider的配置文件中调节不同的权重。

1
2
3
4
5
yml复制代码spring:
cloud:
nacos:
discovery:
weight: 1 #配置权重

使用Feign

通过上面的方法,已经把微服务之间的相互调用变得非常的简单了。但是还不够,Feign可以让调用更加简单。

引用maven包

1
2
3
4
5
xml复制代码<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>

然后我们针对要调用的provider定义一个接口,接口的方法为要调用的方法名,参数和调用参数同名。@FeignClient 注解中name为微服务的名称。复杂一些的方法调用可能需要在接口中配合@RequestMapping指定具体的路由规则,然后就可以通过该接口直接调用微服务方法,是不是更加清晰简单呢?

1
2
3
4
5
java复制代码@FeignClient(name = "provider")
@Service
public interface TestService {
String echo(String serviceName);
}
1
2
3
4
5
6
7
8
9
10
11
java复制代码public class TestController {
private final RestTemplate restTemplate;
@Autowired
private TestService testService;

@GetMapping("/echo2/{str}")
public String echo2(@PathVariable String str)
{
return testService.echo(str);
}
}

本文转载自: 掘金

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

小白都能理解的TCP三次握手四次挥手

发表于 2021-07-19

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

前言

TCP在学习网络知识的时候是经常的被问到知识点,也是程序员必学的知识点,今天小杨用最直白的表述带大家来认识认识,喜欢的朋友记得点点关注哈。

何为TCP

上点官方的话:是一种面向连接(连接导向)的、可靠的、 基于IP的传输层协议。啥意思咧,说白了就是实现客户端与服务器之间的通讯的一种协议,是可靠的,可以传送大量数据的,一个TCP连接必须要经过三次“握手”才能建立起来,经过四次“挥手”断开

三次握手

那么三次握手是咋样的呢,一幅张三上门找李四恰饭的图为你介绍:

三次连接.jpg

通过上面图,我们知道大概知道了啥意思了,结合一点术语的介绍下:

  • 第一次握手:客户端发起请求,将SYN的数据包发送到服务端,请求建立请求
  • 第二次握手:服务端收到客户端的请求后,将(SYN/ACK)的数据包发送到客户端,表示收到请求,待确认
  • 第三次握手:客户端发送(ACK的数据包)请求到服务端,表示确定建立连接

为啥需要三次呢

你想想作为一个张三上门去找李四恰饭,哪一步可以省呢?一问一答,最后肯定是要给人家回复嘛,不然人家还以为你开玩笑呢,你说对吧

四次挥手

那么四次挥手是咋样的呢,张三和李四继续为你上映:
四次连接.jpg

同过上图,我们也知道是啥意思啦,张三一顿胡吃海塞的操作后,就想溜了,结合一点术语的介绍啦:

  • 首先:张三一顿操作吃饱后,就发了一个(FIN数据包)想着提桶跑路
  • 第二:李四收到了信息,发现还有一道拿手的压轴菜还没上,想让他品尝品尝,就发了一个(ACK的数据包),让他再等等
  • 第三:一会菜上了,张三也吃了,心满意足了,发了一个(FIN数据包)告诉张三,今天的菜都吃完了,饭局结束了,要走可以走了啦
  • 第四:张三收到消息后,回了一个(ACK的数据包),表示收到了,然后撒腿就跑

为啥需要四次呢

上面场景来看,张三想走的请求发出后,并不代表对方也都处理完了,于是就先礼貌的回复一下,表示知道你发出请求了,等李四这边安排都好了之后,在主动的告诉张三,张三得知后,也离开了。

欢迎下方交流讨论。如果本篇博客有任何错误,请批评指教,不胜感激 !

共同进步,学习分享

觉得写的还不错的就点个赞,加个关注呗!持续更新 !!! 点关注,不迷路,小杨带你上高速

已经为大家整理好了几百本各类技术电子书和学习资料、最新的面试题,注公众号【写代码的小杨】回复【资料】无套路领取

本文转载自: 掘金

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

SpringBoot技术实践-全局接口(Controller

发表于 2021-07-19

一、返回值格式统一

1.1 返回值介绍

  1. 在使用controller对外提供服务的时候,很多时候都需要统一返回值格式,例如
1
2
3
4
5
6
7
8
9
json复制代码{
"status": true,
"message": null,
"code": "200",
"data": {
"name": "json",
"desc": "json返回值"
}
}
  1. 如果不使用全局统一返回,就需要写一个工具类,然后controller返回对应的对象
1
2
3
4
5
6
7
java复制代码@Data
public class ResponseData {
private boolean status;
private String message;
private String code;
private Object data;
}
1
2
3
4
5
java复制代码@RequestMapping("/foo")
public ResponseData foo() {
// 或者使用工具类返回,根据业务设置值
return new ResponseData();
}
  1. 除了上述方法,可以对返回值进行统一处理,不需要对所有controller都使用一个返回值,controller只需要返回原始值,处理器会对返回值进行封装
  2. 同时也可以添加自定义注解,此注解用于忽略返回值封装,按照controller原始值返回

1.2 基础类功能

  1. org.springframework.web.method.support.HandlerMethodReturnValueHandler
    • 使用不同策略处理从调用处理程序方法的返回值
    • 策略处理顶层接口,自定义返回值格式需要实现此接口
    • supportsReturnType:设置支持返回值类型
    • handleReturnValue:处理返回值基础参数

在这里插入图片描述

  1. org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter
    • 请求映射处理适配,包含了参数、返回值处理器等信息
    • HandlerMethodReturnValueHandlerComposite内部维护了HandlerMethodReturnValueHandler列表

在这里插入图片描述)在这里插入图片描述

  1. 可以自定义注解,用于类或者方法级别忽略返回值封装
1
2
3
4
5
6
java复制代码@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface IgnoreResponseWrapper {

}
  1. org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor
    • 属于HandlerMethodReturnValueHandler子类
    • 主要功能是对请求和响应体的做处理的方法
    • 所有属于RequestResponseBodyMethodProcessor的子类都需要替换为自定义返回值处理
  2. 实现原理就是,在bean初始化的时候,获取到所有处理器数组,然后将所有是RequestResponseBodyMethodProcessor处理器子类对返回值处理的过程替换为自定义处理器处理
  3. 这样当调用对应返回值处理器时,将会使用到自定义的返回值处理器,也就是所有返回值都会按照规定的进行处理

1.3 基础实现

  1. 创建普通springboot项目,项目创建在此不做说明
  2. 创建类实现HandlerMethodReturnValueHandler接口,主要用于实现自定义返回值内容,不需要注入容器
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
java复制代码import com.codecoord.unifyreturn.annotation.IgnoreResponseWrapper;
import com.codecoord.unifyreturn.domain.ResponseBase;
import org.springframework.core.MethodParameter;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;

public class ResponseBodyWrapHandler implements HandlerMethodReturnValueHandler {
private final HandlerMethodReturnValueHandler delegate;

public ResponseBodyWrapHandler(HandlerMethodReturnValueHandler delegate) {
this.delegate = delegate;
}

@Override
public boolean supportsReturnType(MethodParameter returnType) {
return delegate.supportsReturnType(returnType);
}

@Override
public void handleReturnValue(Object returnValue,
MethodParameter returnType,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest) throws Exception {
// 如果类或者方法含有不包装注解则忽略包装
IgnoreResponseWrapper wrapper = returnType.getDeclaringClass().getAnnotation(IgnoreResponseWrapper.class);
if (wrapper != null) {
delegate.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
return;
}
wrapper = returnType.getMethodAnnotation(IgnoreResponseWrapper.class);
if (wrapper != null) {
delegate.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
return;
}

// 自定义返回格式
ResponseBase responseBase = new ResponseBase();
responseBase.setStatus(true);
responseBase.setCode("200");
responseBase.setData(returnValue);
delegate.handleReturnValue(responseBase, returnType, mavContainer, webRequest);
}
}
  1. 创建类实现InitializingBean,在初始化时调用,需要注入到容器中,否则Spring无法管理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
java复制代码import java.util.ArrayList;
import java.util.List;

@Component
public class ResponseBodyWrapFactoryBean implements InitializingBean {
private final RequestMappingHandlerAdapter adapter;

@Autowired
public ResponseBodyWrapFactoryBean(RequestMappingHandlerAdapter adapter) {
this.adapter = adapter;
}

@Override
public void afterPropertiesSet() throws Exception {
List<HandlerMethodReturnValueHandler> returnValueHandlers = adapter.getReturnValueHandlers();
if (returnValueHandlers.size() > 0) {
// 将内置的返回值处理器进行替换
List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>(returnValueHandlers);
decorateHandlers(handlers);
adapter.setReturnValueHandlers(handlers);
}
}

/**
* 将所有RequestResponseBodyMethodProcessor返回值处理器替换为自定义的返回值处理器
*
* @author tianxincode@163.com
* @since 2020/10/12
*/
private void decorateHandlers(List<HandlerMethodReturnValueHandler> handlers) {
for (HandlerMethodReturnValueHandler handler : handlers) {
if (handler instanceof RequestResponseBodyMethodProcessor) {
// 替换为自定义返回值处理器
ResponseBodyWrapHandler decorator = new ResponseBodyWrapHandler(handler);
int index = handlers.indexOf(handler);
handlers.set(index, decorator);
break;
}
}
}
}
  1. 创建controller信息,例如此处map不需要封装,按照原来格式响应
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
java复制代码@RestController
@RequestMapping("/unify")
public class UnifyReturnValueController {

@RequestMapping("string")
public String stringHandler(){
return "接收成功了";
}

@RequestMapping("/json")
public JSONObject jsonHandler(){
JSONObject object = new JSONObject();
object.put("name", "json");
object.put("desc", "json返回值");
return object;
}

@RequestMapping("/map")
@IgnoreResponseWrapper
public Map<String, Object> mapHandler(){
Map<String, Object> map = new HashMap<>(10);
map.put("name", "map");
map.put("desc", "map返回值");
return map;
}

@RequestMapping("/list")
public List<Object> listHandler(){
List<Object> data = new ArrayList<>();
data.add(100);
data.add(95);
data.add(99);
return data;
}
}

1.4 测试信息

  1. 测试json(有封装)
1
2
3
4
5
6
7
8
9
json复制代码{
"status": true,
"message": null,
"code": "200",
"data": {
"name": "json",
"desc": "json返回值"
}
}
  1. 测试map(无封装)
1
2
3
4
json复制代码{
"name": "map",
"desc": "map返回值"
}
  1. 别的方法测试一样

二、附录说明

  1. 项目结构参考红框部分,别的忽略
    在这里插入图片描述
  2. 除了对返回值进行全局统一,也可以对异常进行全局处理和按照统一格式返回

本文转载自: 掘金

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

你觉得我的这段Java代码还有优化的空间吗?

发表于 2021-07-19

上周,因为要测试一个方法的在并发场景下的结果是不是符合预期,我写了一段单元测试的代码。写完之后截了个图发了一个朋友圈,很多人表示短短的几行代码,涉及到好几个知识点。

还有人给出了一些优化的建议。那么,这是怎样的一段代码呢?涉及到哪些知识,又有哪些可以优化的点呢?

让我们来看一下。

背景

先说一下背景,也就是要知道我们单元测试要测的这个方法具体是什么样的功能。我们要测试的服务是AssetService,被测试的方法是update方法。

update方法主要做两件事,第一个是更新Asset、第二个是插入一条AssetStream。

更新Asset方法中,主要是更新数据库中的Asset的信息,这里为了防止并发,使用了乐观锁。

插入AssetStream方法中,主要是插入一条AssetStream的流水信息,为了防止并发,这里在数据库中增加了唯一性约束。

为了保证数据一致性,我们通过本地事务将这两个操作包在同一个事务中。

以下是主要的代码,当然,这个方法中还会有一些前置的幂等性校验、参数合法性校验等,这里就都省略了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typescript复制代码@Service
public class AssetServiceImpl implements AssetService {

@Autowired
private TransactionTemplate transactionTemplate;

@Override
public String update(Asset asset) {
//参数检查、幂等校验、从数据库取出最新asset等。
return transactionTemplate.execute(status -> {
updateAsset(asset);
return insertAssetStream(asset);
});
}
}

因为这个方法可能会在并发场景中执行,所以该方法通过事务+乐观锁+唯一性约束做了并发控制。关于这部分的细节就不多讲了,大家感兴趣的话后面我再展开关于如何防并发的内容。

单测

因为上面这个方法是可能在并发场景中被调用的,所以需要在单测中模拟并发场景,于是,我就写了以下的单元测试的代码:

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

private static ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
.setNameFormat("demo-pool-%d").build();

private static ExecutorService pool = new ThreadPoolExecutor(5, 100,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(128), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());

@Autowired
AssetService assetService;

@Test
public void test_updateConcurrent() {
Asset asset = getAsset();
//参数的准备
//...

//并发场景模拟
CountDownLatch countDownLatch = new CountDownLatch(10);
AtomicInteger failedCount =new AtomicInteger();
//并发批量修改,只有一条可以修改成功
for (int i = 0; i < 10; i++) {
pool.execute(() -> {
try {
String streamNo = assetService.update(asset);
} catch (Exception e) {
System.out.println("Error : " + e);
failedCount.getAndIncrement();
} finally {
countDownLatch.countDown();
}
});
}

try {
//主线程等子线程都执行完之后查询最新的资产
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}

Assert.assertEquals(failedCount.intValue(), 9);

// 从数据库中反查出最新的Asset
// 再对关键字段做注意校验
}
}

以上,就是我做了简化之后的单元测试的部分代码。因为要测并发场景,所以这里面涉及到了很多并发相关的知识。

很多人之前和我说,并发相关的知识自己了解的很多,但是好像没什么机会写并发的代码。其实,单元测试就是个很好的机会。

我们来看看上面的代码涉及到哪些知识点?

知识点

以上这段单元测试的代码中涉及到几个知识点,我这里简单说一下。

线程池

这里面因为要模拟并发的场景,所以需要用到多线程, 所以我这里使用了线程池,而且我没有直接用Java提供的Executors类创建线程池。

而是使用guava提供的ThreadFactoryBuilder来创建线程池,使用这种方式创建线程时,不仅可以避免OOM的问题,还可以自定义线程名称,更加方便的出错的时候溯源。(关于线程池创建的OOM问题)

CountDownLatch

因为我的单元测试代码中,希望在所有的子线程都执行之后,主线程再去检查执行结果。

所以,如何使主线程阻塞,直到所有子线程执行完呢?这里面用到了一个同步辅助类CountDownLatch。

用给定的计数初始化 CountDownLatch。由于调用了 countDown() 方法,所以在当前计数到达零之前,await 方法会一直受阻塞。(多线程中CountDownLatch的用法)

AtomicInteger

因为我在单测代码中,创建了10个线程,但是我需要保证只有一个线程可以执行成功。所以,我需要对失败的次数做统计。

那么,如何在并发场景中做计数统计呢,这里用到了AtomicInteger,这是一个原子操作类,可以提供线程安全的操作方法。

异常处理

因为我们模拟了多个线程并发执行,那么就一定会存在部分线程执行失败的情况。

因为方法底层没有对异常进行捕获。所以需要在单测代码中进行异常的捕获。

1
2
3
4
5
6
7
8
ini复制代码    try {
String streamNo = assetService.update(asset);
} catch (Exception e) {
System.out.println("Error : " + e);
failedCount.increment();
} finally {
countDownLatch.countDown();
}

这段代码中,try、catch、finall都用上了,而且位置是不能调换的。失败次数的统计一定要放到catch中,countDownLatch的countDown也一定要放到finally中。

Assert

这个相信大家都比较熟悉,这就是JUnit中提供的断言工具类,在单元测试时可以用做断言。这就不详细介绍了。

优化点

以上代码涉及到了很多知识点,但是,难道就没有什么优化点了吗?

首先说一下,其实单元测试的代码对性能、稳定性之类的要求并不高,所谓的优化点,也并不是必要的。这里只是说讨论下,如果真的是要做到精益求精,还有什么点可以优化呢?

使用LongAdder代替AtomicInteger

我的朋友圈的网友@zkx 提出,可以使用LongAdder代替AtomicInteger。

java.util.concurrency.atomic.LongAdder是Java8新增的一个类,提供了原子累计值的方法。而且在其Javadoc中也明确指出其性能要优于AtomicLong。

首先它有一个基础的值base,在发生竞争的情况下,会有一个Cell数组用于将不同线程的操作离散到不同的节点上去(会根据需要扩容,最大为CPU核数,即最大同时执行线程数),sum()会将所有Cell数组中的value和base累加作为返回值。

核心的思想就是将AtomicLong一个value的更新压力分散到多个value中去,从而降低更新热点。所以在激烈的锁竞争场景下,LongAdder性能更好。

增加并发竞争

朋友圈网友 Cafebabe 和 @普渡众生的面瘫青年 都提到同一个优化点,那就是如何增加并发竞争。

这个问题其实我在发朋友圈之前就有想到过,心中早已经有了答案,只不过有两位朋友能够几乎同时提到这一点还是很不错的。

我们来说说问题是什么。

我们为了提升并发,使用线程池创建了多个线程,想让多个线程并发执行被测试的方法。

但是,我们是在for循环中依次执行的,那么理论上这10次update方法的调用是顺序执行的。

当然,因为有CPU时间片的存在,这10个线程会争抢CPU,真正执行的过程中还是会发生并发冲突的。

但是,为了稳妥起见,我们还是需要尽量模拟出多个线程同时发起方法调用的。

优化的方法也比较简单,那就是在每一个update方法被调用之前都wait一下,直到所有的子线程都创建成功了,再开始一起执行。

这就还可以用都到我们前面讲过的CountDownLatch。

所以,最终优化后的单测代码如下:

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
scss复制代码//主线程根据此CountDownLatch阻塞
CountDownLatch mainThreadHolder = new CountDownLatch(10);

//并发的多个子线程根据此CountDownLatch阻塞
CountDownLatch multiThreadHolder = new CountDownLatch(1);

//失败次数计数器
LongAdder failedCount = new LongAdder();

//并发批量修改,只有一条可以修改成功
for (int i = 0; i < 10; i++) {
pool.execute(() -> {
try {
//子线程等待,等待主线程通知后统一执行
multiThreadHolder.await();
//调用被测试的方法
String streamNo = assetService.update(asset);
} catch (Exception e) {
//异常发生时,对失败计数器+1
System.out.println("Error : " + e);
failedCount.increment();
} finally {
//主线程的阻塞器奇数-1
mainThreadHolder.countDown();
}
});
}

//通知所有子线程可以执行方法调用了
multiThreadHolder.countDown();

try {
//主线程等子线程都执行完之后查询最新的资产池计划
mainThreadHolder.await();
} catch (InterruptedException e) {
e.printStackTrace();
}

//断言,保证失败9次,则成功一次
Assert.assertEquals(failedCount.intValue(), 9);

// 从数据库中反查出最新的Asset
// 再对关键字段做注意校验

以上,就是关于我的一次单元测试的代码所涉及到的知识点,以及目前所能想到的相关的优化点。

最后,想问一下,对于这部分代码,你觉得还有什么可以优化的地方吗?

关于作者:Hollis,一个对Coding有着独特追求的人,阿里巴巴技术专家,《程序员的三门课》联合作者,《Java工程师成神之路》系列文章作者。

关注公众号【Hollis】,后台回复”成神导图”可以咯领取Java工程师进阶思维导图。

本文转载自: 掘金

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

办公室小姐姐相册打不开了,我只用Python写了20行代码

发表于 2021-07-19

请带上以下文字及链接:本文正在参加「Python主题月」,详情查看 活动链接

大家好,我是Lex 喜欢欺负超人那个Lex

擅长领域:python开发、网络安全、Windows域控Exchange架构

今日重点:女神加密相册打不开了,我用20行代码,帮她打开

一、事情是这样的

今早上班,公司女神小姐姐说,她去年去三亚旅游的照片打不开了

好奇问了一下才知道。

原来是,她把照片压缩了,而且还加了密码。

但是不记得了,只记得是一串6位数字。

话说照片压缩率也不高,而且还加密,难道是有什么可爱的小照片

)​

但是作为一个正(ba)直(gua)的技术人员

我跟她说:“这事交给我,python写个脚本,帮你破解掉~~”

)​

二、首先回顾一下女神的操作流程

对相册进行压缩的时候,添加了password。

LIke This ↓

)​

三、需要打开相册

打开的时候,提示这样的,需要输入设置的密码。

)​

四、python脚本化处理

1、基本思路

首先如果想要python命令行来打开小姐姐相册,那么首先要找到尝试打开的命令行,即解压缩时使用的命令行。然后我们使用python脚本写嵌套循环,不断的对zip文件进行尝试解压,然后找回真实的密码。

2、解压命令

首先压缩文件是zip格式的,我们使用万能的7z命令来进行解压。

为什么不用unzip命令呢?(因为我试过了,unzip无法循环)

)​

3、解压命令参数分析

1
2
3
4
5
6
yaml复制代码#7Z详细参数,下面只截取几个关键参数
PS C:\Users\lex> 7z7-Zip 21.01 alpha (x64) : Copyright (c) 1999-2021
Igor Pavlov : 2021-03-09
Usage: 7z <command> [<switches>...] <archive_name> [<file_names>...] [@listfile]
<Commands> a : Add files to archive
#加入压缩 d : Delete files from archive e : Extract files from archive (without using directory names) t : Test integrity of archive #尝试密码,不解压...<Switches> -o{Directory} : set Output directory -p{Password} : set Password #设置密码参数

4、整理7z解压命令

命令太简单,感觉都有点配不上我的才华和思路

)​

1
2
3
4
python复制代码7z -p 123456 t 三亚相册.zip
# t:尝试打开,类似后台运行
# -p:尝试的密码
# 最后是要解压的文件

5、关门!上python脚本

根据小姐姐的需求,密码是6位纯数字,那就帮我节省了好大一段时间

只对6位纯数字进行尝试就可以了。

三分钟就把脚本搞出来了

五、找女神去…

面对着一筹莫展的女神,我运行起了脚本,不到5秒,相册成功打开了。

效果gif ↓

)​

故事结尾

打开之后,女神看我的眼神都变了。

)​

本文转载自: 掘金

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

【redis前传】为什么set底层hashtable+int

发表于 2021-07-19

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

redis的整数集是什么?当我们想set集合中添加整数时内部又是什么结构?整数集默认是多少范围?超出了范围的数据是如何存储的?删除最长元素后会不会发生降级的变化?
今天,我们就来对整数集一探究竟

为什么整数集升级后不能在进行降级操作 | intset位升级频率

往期回顾

【redis前传】redis字典快速映射+hash釜底抽薪 | 单线程不影响后台工作之渐进式rehash

【redis正传】redis淘汰+过期双向保证高可用 | 单线程如何做到快速响应

【redis前传】 redis五大天王值list基本数据如何成长 | 由内之外深入学习

【redis前传】自己手写一个LRU策略 | 抓住时间的尾巴

基于redis实现分布式锁

【redis前传】zset如何解决内部链表查找效率低下|跳表构建

前言

整数集合相信有的同学没有听说过,因为redis对外提供的只有封装的五大对象!而我们本系列主旨是学习redis内部结构。内部结构是redis五大结构重要支撑!

前面我们分别从redis内部结构分析了redis的List、Hash、Zset三种数据结构了。今天我们再来分析set数据结构内部是如何存储的

基本结构

  • 在src/t_set.c中我们发现这样一段代码

image-20210706105819274

  • 由此我们可知在set中是由两种数据结构构成的: hashtable+intset 。关于redis内部其他的结构我专门在【redis专栏中有介绍】。hashtable不是我们今天的主角,我们今天先分析intset俗称整数集合。

image-20210706110151754

  • 从上图中我们可以看出,我构造了两个set集合分别为【commonset】、【cs】。两个集合前者存储字符串、后者专门存储数字。
  • 我们在通过object encoding key 来查看下两个集合的底层数据结构,发现一个是hashtable 一个是intset 。这也验证了我们上面对set基本结构的描述。
  • 在redis中对外提供五大类型实际上都是redis的一个抽象对象叫做redisobject。在内部映射了我们redis内部的数据结构

image-20210706111432308

  • 针对commonset和cs两个集合在内部数据结构大概可以这么理解

image-20210706112349968

何时使用intset

  • 你可以单纯的认为只要是数字就会使用intset结构来存储,我恐怕要给你当头一棒了。实际上并不是这样
  • 需要同时满足以下两个条件:

image.png

intset

image-20210706133736601

  • 图中表示的很清楚了,在intset中的encoding有三种取值分别代表contents保存数据类型。这里有人可能会有疑问了contents的类型不就是int8_t吗?为什么还需要encoding呢?这里通过源码跟踪内部的确跟int8_t没啥关系。而且数据的默认类型就是int16_t 。关于length这里无需太多解释,记住一点表示contents元素的个数并非表示contents数组的长度!
  • 了解intset的同学都知道在encoding三种取值范围中涉及了升级的操作!在讲升级之前我们先来了解下C、C++中int的取值范围是如何定义的
  • int8_t的取值范围是【-128,127】 。 类似于java中byte占1个字节也就是8位。他的取值范围是

−27∼27−1即−128∼127-2^{7} \sim 2^{7}-1 \
即 \
-128 \sim 127−27∼27−1即−128∼127
image-20210706135925132

添加元素

1
2
3
4
5
复制代码sadd juejin -123
sadd juejin -6
sadd juejin 12
sadd juejin 56
sadd juejin 321
  • juejin这个key内部就是intset 。

image-20210706162521929

  • 上面我们添加了5个元素且这五个元素的长度都在16之内!所以当前的intset的encoding=INTSET_ENC_INT16。-123在contents中占前16位。
  • 所以当前五个元素占contents的长度是16*5=80 ;
  • 注意set在存储int类型数据时,内部是按照从小到大的顺序存储的。

类型变动

image-20210706164922957

  • 上面的问题不知道你有没有考虑过,或者说有没有遇到过!intset默认是int16位,正如我们上面添加的五个元素。加入此时我们添加第6个元素是65535(32位)。那么此时16位的长度就不够存储了这个时候intset会怎么做!
  • 另外当我们添加第6个元素后又将65535删除了之后,结构和添加之前是否一样!下面我们带着这两个问题来一探究竟!!!

升级

  • 首先我们针对第一问题来看看。原来五个元素都是16位就可以满足了,这个时候添加的65535是32位长度的。那么是不是可以直接追加32位分配给65535呢?
  • 答案是肯定不行,首先直接追加无法保证数组元素的大小顺序!其次如果前五个分别是16位,第6个是32位那么在intset结构中没有多余的字段来进行标记。也就是说在解析的时候就无法判断应该解析16位还是32位了.
  • redis为了方便解析所以在有高长度加入时会将整个contents进行升级。意思就是将整个contents先进行扩容,然后在重新填充数据

image-20210706171505334

加入65535

  • 首先根据length可以确定扩容后元素个数为6 , 每个占位32,所以contents长度为32*6=192 。 此时前80位内容保持不变

image-20210706171605386

旧数据移位

  • 开辟了足够的空间后,我们就可以对旧数据进行移位了这里我们从原数组的末尾开始移动,在移动之前需要明确在新数组中的排序位置。
  • 此时我们首先将321进行比对确定在新数组中他的排名是第五名,那么他将占用新contents中128~159区间。

image-20210706172455270

  • 最终前5 个元素就会被移动好 。

image-20210706172652958

  • 最后将新加入的元素填充进去。当发生升级时肯定是因为新元素的长度大于原有长度了。那么他的值一定会是在新数组的两端。负数在最左侧,正数在最右侧

image-20210706172836896

降级

  • 接下来就是第二个问题当新加入的65535又被删除了redis该怎么办,这个时候元素长度实际16位就可以满足了,但是此时encoding却是32位的。按照我的看法应该在实现降级!
  • 但是遗憾的是redis并没有,那么请思考为什么没有?如果让你实现你将如何实现

为什么不实现降级

  • 当加入元素超过当前长度我们很容易就知道此时需要进行升级操作,但是当我们删除一个数据时我们如何判断是否需要降级却很困难,我们需要重新遍历一遍剩下的元素是否小于当前长度,实现复杂度O(N) 。这就是为什么不进行降级原因之一
  • 你可能会说重新遍历一遍很快的反正在内存中,那么你有没有想过如果降级之后又遇到升级情况,这样来回的升级降级就降低了我们程序的性能了。我们知道升级是必须的所以这里降级redis采取的是忽略的策略

小结

image-20210707135328472

参考资料:内存升级优化内存降级

本文转载自: 掘金

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

Kafka的Reactor模式--支撑10万请求数 Kafk

发表于 2021-07-18

本文为博主自学笔记整理,内容来源于互联网,如有侵权,请联系删除。

个人笔记:www.dbses.cn/technotes

关于如何处理请求,我们很容易想到的方案有两个。

  • 顺序处理请求

伪代码大概是这个样子:

1
2
3
4
java复制代码while (true) {
Request request = accept(connection);
handle(request);
}

这种方式的吞吐量太差,每个请求都必须等待前一个请求处理完毕才能得到处理。适用于请求发送非常不频繁的系统。

  • 异步处理请求

伪代码大概是这个样子:

1
2
3
4
5
java复制代码while (true) {
Request = request = accept(connection);
Thread thread = new Thread(() -> handle(request););
thread.start();
}

这个方法的好处是,它是完全异步的,每个请求的处理都不会阻塞下一个请求。

缺陷是开销极大,在某些场景下甚至会压垮整个服务。这个方法也只适用于请求发送频率很低的业务场景。

Kafka 处理请求的方式

Kafka 使用的是 Reactor 模式。

简单来说,Reactor 模式是事件驱动架构的一种实现方式,特别适合应用于处理多个客户端并发向服务器端发送请求的场景。Reactor 模式的架构图如下:

image-20210716233202622

根据上图,Reactor 模式主要特点是分发。

Reactor 有个请求分发线程 Dispatcher,也就是图中的 Acceptor,它会将不同的请求下发到多个工作线程中处理。

Acceptor 线程只是用于请求分发,不涉及具体的逻辑处理,非常轻量级,因此有很高的吞吐量表现。而这些工作线程可以根据实际业务处理需要任意增减,从而动态调节系统负载能力。

Kafka 的处理请求模型类似:

image-20210716233715438

SocketServer 组件:它也有对应的 Acceptor 线程和一个网络线程池。

Acceptor 线程:采用轮询的方式将入站请求公平地发到所有网络线程中。

网络线程池:处理 Acceptor 线程分发的工作任务。

Kafka 提供了 Broker 端参数 num.network.threads,用于调整该网络线程池的线程数。其默认值是 3,表示每台 Broker 启动时会创建 3 个网络线程,专门处理客户端发送的请求。

Kafka 网络线程处理请求的具体过程

客户端发来的请求会被 Broker 端的 Acceptor 线程分发到任意一个网络线程中,Kafka 在这个环节又做了一层异步线程池的处理,我们一起来看一看下面这张图。

image-20210716234619473

主要步骤如下:

  1. 当网络线程拿到请求后,会将请求放入到一个共享请求队列中。
  2. Broker 端有个 IO 线程池,负责从该队列中取出请求,执行真正的处理。

Broker 端参数 num.io.threads 控制了这个线程池中的线程数。 目前该参数默认值是 8,表示每台 Broker 启动后自动创建 8 个 IO 线程处理请求。
3. 处理请求。如果是 PRODUCE 生产请求,则将消息写入到底层的磁盘日志中;如果是 FETCH 请求,则从磁盘或页缓存中读取消息。
4. IO 线程处理完请求后,会将生成的响应发送到网络线程池的响应队列中,然后由对应的网络线程负责将 Response 返还给客户端。

为什么网络线程不直接处理?即为什么要有 2、3 步骤?

请求队列与响应队列的差别

请求队列是所有网络线程共享的,而响应队列则是每个网络线程专属的。

这么设计的原因就在于,Dispatcher 只是用于请求分发而不负责响应回传,因此只能让每个网络线程自己发送 Response 给客户端,所以这些 Response 也就没必要放在一个公共的地方。

Purgatory 组件

图中还有一个叫 Purgatory 的组件,这是 Kafka 中著名的“炼狱”组件。它是用来缓存延时请求(Delayed Request)的。

所谓延时请求,就是那些一时未满足条件不能立刻处理的请求。比如设置了 acks=all 的 PRODUCE 请求,一旦设置了 acks=all,那么该请求就必须等待 ISR 中所有副本都接收了消息后才能返回,此时处理该请求的 IO 线程就必须等待其他 Broker 的写入结果。当请求不能立刻处理时,它就会暂存在 Purgatory 中。稍后一旦满足了完成条件,IO 线程会继续处理该请求,并将 Response 放入对应网络线程的响应队列中。

本文转载自: 掘金

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

1…601602603…956

开发者博客

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