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

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


  • 首页

  • 归档

  • 搜索

第七讲:使用云服务器搭建自己的邮件服务

发表于 2021-06-15

  随着移动互联网的普及,邮件服务使用的场景越来越少。但是注册帐号的时候,还是需要邮箱地址。市面上免费的邮箱很多,不过一般需要绑定手机号,如果想在同一个邮箱服务商那注册多个邮箱帐号,需要准备多个手机号,这非常的烦人。接下来我们自己动手搭建邮件服务器吧。

准备工作

  邮件服务器之间采用SMTP协议进行通信,如果我们的云服务器想要接受邮件,需要打开25这个端口,并允许访问25端口。

1.png

  邮件服务使用的特殊的DNS,需要进行配置。这里涉及到了两次映射,为了方便叙述,假设我的邮箱名字是:abc@example.com。第一次映射通过添加MX记录类型,将邮箱@后面的主机映射到域名上,如下图所示,添加一个MX的记录类型。

2.png

第二次映射将上面的域名映射到我们云主机上的IP上,看过之前文章的朋友应该知道如何配置。

安装邮件服务

  我们将使用postfix作为邮件服务器,因为它的特殊设定,需要先检查云主机的hostname,将hostname设置为邮件服务器的主机名。

1
shell复制代码sudo hostnamectl set-hostname example.com

  安装postfix,执行下面的命令,弹窗会让你选择configuration和你的mail name

1
2
shell复制代码sudo apt-get update
sudo apt-get install postfix -y

3.png

4.png
  安装完成之后,检查一下服务是否启动,如果没有启动,可以执行postfix start。

1
shell复制代码sudo ss -lnpt | grep master

  邮件服务器搭建完毕,学习过计算机网络的同学应该知道,smtp只是邮件服务器之间的传输协议,读取邮件还需要pop3或者imap协议。所以如果还要读取邮件,还需要安装其他软件。需要说明的是:如果你的Ubuntu有user1的用户,那么邮件服务器就有user1@example.com的账户。

1
shell复制代码sudo apt-get install mailutils

接收邮件测试

  接下来我们用qq邮箱给刚搭好的邮件服务器发送测试邮件,当在云服务器上执行mail的

5.png

时候,提示收到了新邮件,选择1就显示邮件的内容。

6.png

7.png
  细心的小伙伴可能已经发现了,邮件的内容是base64编码后的,并不能直接读取,这个时候执行以下decode,选择消息,就能进行解码啦

8.png

9.png
  postfix还能发送邮件,但是前提是云服务器25端口出的流量是开启的,可以在云服务器上自己进行测试,看端口25是不是通的。对于postfix发送邮件感兴趣的可以自行搜索资料哦~

1
shell复制代码telnet  mx1.qq.com 25

总结

  有了自建的邮件服务器,再也不用担心要多个手机号啦

本文转载自: 掘金

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

电机控制进阶3——PID串级控制(附全套代码下载) 串级PI

发表于 2021-06-14

前两篇文章,分别介绍了PID速度控制和PID位置控制,分别用来控制电机以期望的速度持续转动以及以期望的位置(圈数)转动,这里的期望值都只有一个,但是,如果想要以期望的速度转动到期望的位置(启动与停止的加减速过程不考虑),该怎么控制呢?那就要将两者结合起来了,即PID的串级控制来控制电机。

串级PID结构图

PID串级控制的典型结构为位置环+速度环+电流环,如下图。

PID串级控制中,最外环是输入是整个控制系统的期望值,外环PID的输出值是内环PID的期望值。

能够使用三环控制的前提是要硬件支持,比如位置环和速度环需要实时的电机转动位置和转动速度作为反馈,这就需要电机需要配有编码器用于测速与测量转动的位置;电流环需要有电流采样电路来实时获取电机的电流作为反馈。

如果没有电流采样电路,可以将电流环去掉,只使用位置环+速度环,系统的期望仍是转动的位置,内环可以调节转动的速度。

另外,如果只是想控制电机转速实现电机调速,可以使用速度环+电流环,系统的期望仍是转动的位置,内环可以调节电机的电流,增强系统转动调节的抗干扰能力。

位置环+速度环实践

由于我的电机没有电流测量电路,所以,本文以位置环+速度环来学习PID串级控制。就是按照下面这个图:

PID参数定义

由于是串级PID控制,每一级的PID都要有自己的参数,本次实验使用位置PID+速度PID,参数定义如下:

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
c复制代码/*定义位置PID与速度PID结构体型的全局变量*/
PID pid_location;
PID pid_speed;

/**
* @brief PID参数初始化
* @note 无
* @retval 无
*/
void PID_param_init()
{
/* 位置相关初始化参数 */
pid_location.target_val = TOTAL_RESOLUTION*10;
pid_location.output_val = 0.0;
pid_location.err = 0.0;
pid_location.err_last = 0.0;
pid_location.integral = 0.0;

pid_location.Kp = 0.05;
pid_location.Ki = 0;
pid_location.Kd = 0;

/* 速度相关初始化参数 */
pid_speed.target_val=10.0;
pid_speed.output_val=0.0;
pid_speed.err=0.0;
pid_speed.err_last=0.0;
pid_speed.integral=0.0;

pid_speed.Kp = 80.0;
pid_speed.Ki = 2.0;
pid_speed.Kd = 100.0;
}

位置PID的实现

这里有两点需要注意:

闭环死区的设定

闭环死区是指执行机构的最小控制量,无法再通过调节来满足控制精度,如果仍然持续调节,系统则会在目标值前后频繁动作,不能稳定下来。

比如某个系统的控制精度是1,但目标值需要是1.5,则无论怎么调节,最终的结果只能控制在 1或 2,始终无法达到预设值。这 1.5L小数点后的范围,就是闭环死区,系统是无法控制的,误差会一直存在,容易发生震荡现象。

对应精度要求不高的系统,可以设定闭环死区,比如将允许的误差范围设为0.5,则最终结果在 1或 2都认为是没有误差,这时将目标值 与实际值之差强制设为 0,认为没有误差,即限定了闭环死区。

积分分离的设定

通过积分分离的方式来实现抗积分饱和,积分饱和是指执行机构达到极限输出能力了,仍无法到达目标值,在很长一段时间内无法消除静差造成的。

例如,PWM输出到了100%,仍达不到期望位置,此时若一直进行误差累加,在一段时间后, PID 的积分项累计了很大的数值,如果这时候到达了目标值或者重新设定了目标值,由于积分由于累计的误差很大,系统并不能立即调整到目标值,可能造成超调或失调的现象。

解决积分饱和的一种方法是使用积分分离,该方法是在累计误差小于某个阈值才使用积分项,累计误差过大则不再继续累计误差,相当于只使用了PD控制器。

控制流程图

带有闭环死区与积分分离的PID控制流程如下图:

完整的位置PID代码如下:

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
c复制代码/**
* @brief 位置PID算法实现
* @param actual_val:实际值
* @note 无
* @retval 通过PID计算后的输出
*/
#define LOC_DEAD_ZONE 60 /*位置环死区*/
#define LOC_INTEGRAL_START_ERR 200 /*积分分离时对应的误差范围*/
#define LOC_INTEGRAL_MAX_VAL 800 /*积分范围限定,防止积分饱和*/
float location_pid_realize(PID *pid, float actual_val)
{
/*计算目标值与实际值的误差*/
pid->err = pid->target_val - actual_val;

/* 设定闭环死区 */
if((pid->err >= -LOC_DEAD_ZONE) && (pid->err <= LOC_DEAD_ZONE))
{
pid->err = 0;
pid->integral = 0;
pid->err_last = 0;
}

/*积分项,积分分离,偏差较大时去掉积分作用*/
if(pid->err > -LOC_INTEGRAL_START_ERR && pid->err < LOC_INTEGRAL_START_ERR)
{
pid->integral += pid->err;
/*积分范围限定,防止积分饱和*/
if(pid->integral > LOC_INTEGRAL_MAX_VAL)
{
pid->integral = LOC_INTEGRAL_MAX_VAL;
}
else if(pid->integral < -LOC_INTEGRAL_MAX_VAL)
{
pid->integral = -LOC_INTEGRAL_MAX_VAL;
}
}

/*PID算法实现*/
pid->output_val = pid->Kp * pid->err +
pid->Ki * pid->integral +
pid->Kd * (pid->err - pid->err_last);

/*误差传递*/
pid->err_last = pid->err;

/*返回当前实际值*/
return pid->output_val;
}

速度PID实现

速度PID的实现代码与位置PID的类似:

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
c复制代码/**
* @brief 速度PID算法实现
* @param actual_val:实际值
* @note 无
* @retval 通过PID计算后的输出
*/
#define SPE_DEAD_ZONE 5.0f /*速度环死区*/
#define SPE_INTEGRAL_START_ERR 100 /*积分分离时对应的误差范围*/
#define SPE_INTEGRAL_MAX_VAL 260 /*积分范围限定,防止积分饱和*/
float speed_pid_realize(PID *pid, float actual_val)
{
/*计算目标值与实际值的误差*/
pid->err = pid->target_val - actual_val;

/* 设定闭环死区 */
if( (pid->err>-SPE_DEAD_ZONE) && (pid->err<SPE_DEAD_ZONE ) )
{
pid->err = 0;
pid->integral = 0;
pid->err_last = 0;
}

/*积分项,积分分离,偏差较大时去掉积分作用*/
if(pid->err > -SPE_INTEGRAL_START_ERR && pid->err < SPE_INTEGRAL_START_ERR)
{
pid->integral += pid->err;
/*积分范围限定,防止积分饱和*/
if(pid->integral > SPE_INTEGRAL_MAX_VAL)
{
pid->integral = SPE_INTEGRAL_MAX_VAL;
}
else if(pid->integral < -SPE_INTEGRAL_MAX_VAL)
{
pid->integral = -SPE_INTEGRAL_MAX_VAL;
}
}

/*PID算法实现*/
pid->output_val = pid->Kp * pid->err +
pid->Ki * pid->integral +
pid->Kd *(pid->err - pid->err_last);

/*误差传递*/
pid->err_last = pid->err;

/*返回当前实际值*/
return pid->output_val;
}

串级控制代码

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
c复制代码//周期定时器的回调函数
void AutoReloadCallback()
{
static uint32_t location_timer = 0; // 位置环周期

static __IO int encoderNow = 0; /*当前时刻总计数值*/
static __IO int encoderLast = 0; /*上一时刻总计数值*/
int encoderDelta = 0; /*当前时刻与上一时刻编码器的变化量*/
float actual_speed = 0; /*实际测得速度*/
int actual_speed_int = 0;

int res_pwm = 0;/*PID计算得到的PWM值*/
static int i=0;

/*【1】读取编码器的值*/
encoderNow = read_encoder() + EncoderOverflowCnt*ENCODER_TIM_PERIOD;/*获取当前的累计值*/
encoderDelta = encoderNow - encoderLast; /*得到变化值*/
encoderLast = encoderNow;/*更新上次的累计值*/

/*【2】位置PID运算,得到PWM控制值*/
if ((location_timer++ % 2) == 0)
{
float control_val = 0; /*当前控制值*/

/*位置PID计算*/
control_val = location_pid_realize(&pid_location, encoderNow);

/*目标速度值限制*/
speed_val_protect(&control_val);

/*设定速度PID的目标值*/
set_pid_target(&pid_speed, control_val);
}

/* 转速(1秒钟转多少圈)=单位时间内的计数值/总分辨率*时间系数, 再乘60变为1分钟转多少圈 */
actual_speed = (float)encoderDelta / TOTAL_RESOLUTION * 10 * 60;

/*【3】速度PID运算,得到PWM控制值*/
actual_speed_int = actual_speed;
res_pwm = pwm_val_protect((int)speed_pid_realize(&pid_speed, actual_speed));

/*【4】PWM控制电机*/
set_motor_rotate(res_pwm);

/*【5】数据上传到上位机显示*/
set_computer_value(SEND_FACT_CMD, CURVES_CH1, &encoderNow, 1); /*给通道1发送实际的电机【位置】值*/
}

PID的计算是通过定时器调用,每10ms一次,从代码中可以看到,内环(速度PID)控制的周期要比外环(位置PID)的周期短,位置PID是每两次循环计算一次,因为内环控制着最终的输出,这个输出对应的就是实际场景中的控制量 (本实验最终控制的是位置),位置是无法突变,是需要时间积累的,所以内环输出尽可能快些。

视频演示

视频中,测试以不同的目标速度到达目标位置,视频后半段测试引入干扰情况下的控制效果:
视频:www.bilibili.com/video/BV1QK…

封面.png

开源代码

本篇以及前面几篇电机与PID的完整程序代码已开源分享:gitee.com/xxpcb/stm32…

gitee.jpg
文章对你有帮助,欢迎转发、点赞支持哦~

本文转载自: 掘金

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

【python实战】python一行代码,实现文件共享服务器

发表于 2021-06-14

一行代码实现文件共享

在一个局域网内,需要共享一个文件夹里内容。

我们可以在任意一台有python环境的电脑上,迅速架起一个http协议的服务,然后将文件夹里的文件内容共享出来。是的仅仅需要一行代码

就是这么简单

把电脑的相关项目文件通过http协议共享出去,让局域网内的其他用户 通过IP地址就可以直接进行访问和下载。

1、需要共享的内容

1
2
3
复制代码IP地址:192.168.0.111

共享文件夹:file

)​

2、python一键共享

共享代码

1
yaml复制代码PS D:\file> python -m http.server 9090

3、共享效果

)​

4、通过http直接访问

在浏览器中直接访问共享的地址+端口,即可访问共享文件夹,还可以下载。

1
arduino复制代码浏览器打开 http://192.168.0.111:9090

)​

参数详解

1
bash复制代码--bind:绑定的IP 和 端口,默认是本机ip,默认端口是8080

1
2
3
4
5
6
7
8
9
10
11
12
less复制代码PS D:\> python -m http.server -h
usage: server.py [-h] [--cgi] [--bind ADDRESS] [port]

positional arguments:
port Specify alternate port [default: 8000]

optional arguments:
-h, --help show this help message and exit
--cgi Run as CGI Server
--bind ADDRESS, -b ADDRESS
Specify alternate bind address [default: all interfaces]
PS D:\>

本文转载自: 掘金

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

迁移失败!试试这个脚本删除 Django 项目下的迁移文件

发表于 2021-06-14

当你在终端上输入如下的命令执行数据迁移的时候:

1
bash复制代码python manage.py makemigrations

它报错了!

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
bash复制代码/usr/lib/python3/dist-packages/requests/__init__.py:80: RequestsDependencyWarning: urllib3 (1.26.4) or chardet (3.0.4) doesn't match a supported version!
RequestsDependencyWarning)
Traceback (most recent call last):
File "manage.py", line 22, in <module>
main()
File "manage.py", line 18, in main
execute_from_command_line(sys.argv)
File "/usr/local/lib/python3.6/dist-packages/django/core/management/__init__.py", line 401, in execute_from_command_line
utility.execute()
File "/usr/local/lib/python3.6/dist-packages/django/core/management/__init__.py", line 395, in execute
self.fetch_command(subcommand).run_from_argv(self.argv)
File "/usr/local/lib/python3.6/dist-packages/django/core/management/base.py", line 330, in run_from_argv
self.execute(*args, **cmd_options)
File "/usr/local/lib/python3.6/dist-packages/django/core/management/base.py", line 371, in execute
output = self.handle(*args, **options)
File "/usr/local/lib/python3.6/dist-packages/django/core/management/base.py", line 85, in wrapped
res = handle_func(*args, **kwargs)
File "/usr/local/lib/python3.6/dist-packages/django/core/management/commands/makemigrations.py", line 87, in handle
loader = MigrationLoader(None, ignore_no_migrations=True)
File "/usr/local/lib/python3.6/dist-packages/django/db/migrations/loader.py", line 53, in __init__
self.build_graph()
File "/usr/local/lib/python3.6/dist-packages/django/db/migrations/loader.py", line 255, in build_graph
self.graph.validate_consistency()
File "/usr/local/lib/python3.6/dist-packages/django/db/migrations/graph.py", line 195, in validate_consistency
[n.raise_error() for n in self.node_map.values() if isinstance(n, DummyNode)]
File "/usr/local/lib/python3.6/dist-packages/django/db/migrations/graph.py", line 195, in <listcomp>
[n.raise_error() for n in self.node_map.values() if isinstance(n, DummyNode)]
File "/usr/local/lib/python3.6/dist-packages/django/db/migrations/graph.py", line 58, in raise_error
raise NodeNotFoundError(self.error_message, self.key, origin=self.origin)
django.db.migrations.exceptions.NodeNotFoundError: Migration comments.0001_initial dependencies reference nonexistent parent node ('tweets', '0002_auto_20210426_0742')

面对红红的一堆的错误,怒火中烧,而 Django 又没有提供删除迁移文件的命令 🤡 ,而手动删除又非常的无趣和浪费时间!
我提供了一个 Python 写的脚本,来删除迁移文件,放到你项目的根目录并执行它吧!

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

for root, dirs, files in os.walk('.'):
if 'migrations' in dirs:
dir = dirs[dirs.index('migrations')]
for root_j, dirs_j, files_j in os.walk(os.path.join(root, dir)):
for file_k in files_j:
if file_k != '__init__.py':
dst_file = os.path.join(root_j, file_k)
print('>>> ', dst_file)
os.remove(dst_file)

整体的思路就是,先通过 os.walk 递归遍历项目根路径下所有的文件夹,找到所有名为 migrations 文件夹,除了 __init__.py 文件,删除里面所有的文件。为什么不能删除 __init__.py 文件呢?因为没有 __init__.py 文件就不是一个 Python 包了,执行迁移命令的时候就不会迁移这个 app 下的 Models

这里在提供一种思路,就是先找到所有的 migrations 文件夹,然后删除它们,在原位置重新创建,记得在文件夹里面创建一个名为 __init__.py 的空文件哦!

为什么要选 Python 脚本来做这件事情呢?当然是 Python 的跨平台特性,这样不管是 UNIX 还是 Windows 都可以使用。

当然上面这段代码非常的丑陋 🤡 ,谁有空可以优化一下后再发我一段吧!

本文转载自: 掘金

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

Spring Session初次体验

发表于 2021-06-14

最近团队一个项目需要使用Session,之前没有在实际项目中使用过Spring-Session,这里记录一下使用的过程。

Session

Http协议是无状态的,这样对于服务端来说,没有办法区分是新的访客还是旧的访客。但是,有些业务场景,需要追踪用户多个请求,此时就需要Session。关于session的百度百科session

Session:在计算机中,尤其是在网络应用中,称为“会话控制”。Session对象存储特定用户会话所需的属性及配置信息。这样,当用户在应用程序的Web页之间跳转时,存储在Session对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。当用户请求来自应用程序的 Web页时,如果该用户还没有会话,则Web服务器将自动创建一个 Session对象。当会话过期或被放弃后,服务器将终止该会话

核心特点:

  1. 服务端存储
  2. 会过期

Session常用解决方案

对于Session的常用解决方案,可以划分为三种。

  • 负载均衡方式

借助负载均衡设备或者模块,将指定的Session始终路由到同一台机器即可,如Nginx。

  • 副本复制方式

利用服务器节点间的副本复制方式,保证集群所有节点拥有的Session数据一致。

  • 集中存储方式

引入第三方存储,将Session数据集中存储到外部存储中,如Redis或者数据库等。

本文介绍的Spring-Session是采用第三种,集中存储的方式。

Spring-Session

核心组成模块

  • Spring Session Core

提供Spring Session核心的功能和API

  • Spring Session Data Redis

提供基于Redis的SessionRepository以及配置

  • Spring Session JDBC

提供基于关系型数据库的SessionRepository以及配置

  • Spring Session Hazelcast

提供基于Hazelcast的SessionRepository以及配置

测试代码

controller提供三个接口,分别对应Session的获取、保存和清理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
java复制代码
@GetMapping("/")
public String process(Model model, HttpSession session) {
@SuppressWarnings("unchecked")
List<String> messages = (List<String>) session.getAttribute("springSession");

if (messages == null) {
messages = new ArrayList<>();
}
model.addAttribute("sessionMessages", messages);

return "sessionTest";
}

@PostMapping("/persistSession")
public String persistMessage(@RequestParam("msg") String msg, HttpServletRequest request) {
@SuppressWarnings("unchecked")
List<String> messages = (List<String>) request.getSession().getAttribute("springSession");
if (messages == null) {
messages = new ArrayList<>();
request.getSession().setAttribute("springSession", messages);
}
messages.add(msg);
request.getSession().setAttribute("springSession", messages);
return "redirect:/";
}

@PostMapping("/destroySession")
public String destroySession(HttpServletRequest request) {
request.getSession().invalidate();
return "redirect:/";
}

sessionTest.html对应页面操作

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
html复制代码<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Spring Boot Session Example</title>
</head>
<body>
<div>
<form th:action="@{/persistSession}" method="post">
<textarea name="msg" cols="40" rows="2"></textarea>
<br> <input type="submit" value="保存" />
</form>
</div>
<div>
<h2>session列表</h2>
<ul th:each="message : ${sessionMessages}">
<li th:text="${message}">message</li>
</ul>
</div>

<div>
<form th:action="@{/destroySession}" method="post">
<input type="submit" value="清空" />
</form>
</div>
</body>
</html>

image-20210614151042118

基于数据库的Spring-Session
  1. 引入maven依赖

使用MySQL存储,所以引入了MySQL。

涉及到SpringBoot JDBC的配置,引入了Spring Boot JDBC Starter。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
xml复制代码<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-core</artifactId>
<version>2.5.0</version>
</dependency>

<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-jdbc</artifactId>
<version>2.5.0</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.18</version>
</dependency>

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

注意:

No session repository could be auto-configured, check your configuration (session store type is ‘jdbc’)

如果存在这个报错,是因为没有引入spring-boot-starter-jdbc,引入即可。
2. 配置application.properties文件

主要包含两部分,数据库的配置以及Spring Session Jdbc配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
properties复制代码# 配置数据源相关内容
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/spring_learn?createDatabaseIfNotExist=true&autoReconnect=true&useSSL=false
spring.datasource.username=xxx
spring.datasource.password=xxx
spring.datasource.initialization-mode=always

# session类型选择jdbc
spring.session.store-type=jdbc
spring.session.jdbc.initialize-schema=always
# 指定表名
#spring.session.jdbc.table-name=SESSIONS
# 超时时间
spring.session.timeout=180s
  1. 数据库存储解析

默认情况下,数据库中会创建2张表。SPRING_SESSION和SPRING_SESSION_ATTRIBUTION。

SPRING_SESSION用于存在session自身的一些属性,如创建时间、过期时间等,详细schema如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
sql复制代码CREATE TABLE `SPRING_SESSION` (
`PRIMARY_ID` char(36) NOT NULL,
`SESSION_ID` char(36) NOT NULL,
`CREATION_TIME` bigint NOT NULL,
`LAST_ACCESS_TIME` bigint NOT NULL,
`MAX_INACTIVE_INTERVAL` int NOT NULL,
`EXPIRY_TIME` bigint NOT NULL,
`PRINCIPAL_NAME` varchar(100) DEFAULT NULL,
PRIMARY KEY (`PRIMARY_ID`),
UNIQUE KEY `SPRING_SESSION_IX1` (`SESSION_ID`),
KEY `SPRING_SESSION_IX2` (`EXPIRY_TIME`),
KEY `SPRING_SESSION_IX3` (`PRINCIPAL_NAME`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC

SPRING_SESSION_ATTRIBUTION用于存储session相关联的属性,schema如下。

1
2
3
4
5
6
7
sql复制代码CREATE TABLE `SPRING_SESSION_ATTRIBUTES` (
`SESSION_PRIMARY_ID` char(36) NOT NULL,
`ATTRIBUTE_NAME` varchar(200) NOT NULL,
`ATTRIBUTE_BYTES` blob NOT NULL,
PRIMARY KEY (`SESSION_PRIMARY_ID`,`ATTRIBUTE_NAME`),
CONSTRAINT `SPRING_SESSION_ATTRIBUTES_FK` FOREIGN KEY (`SESSION_PRIMARY_ID`) REFERENCES `SPRING_SESSION` (`PRIMARY_ID`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC
  1. 测试执行

SPRING_SESSION中的数据

image-20210614151346323

SPRING_SESSION_ATTRIBUTION中的数据。

image-20210614151515300

基于Redis的Spring-Session

几乎同样的步骤

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

<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
  1. application.properties配置
1
2
3
properties复制代码spring.session.store-type=redis
spring.redis.host=127.0.0.1
spring.redis.port=6379
  1. 结果分析

一次请求后,多了三个属性,分析如下。

key 类型 用途 value
spring:session:sessions:expires:${sessionId} string 判断sesssion是否存在 空
spring:session:sessions:${sessionId} hash session相关的属性,包括有效期、创建时间、具体属性等 creationTime/lastAccessedTime/sessionAttr/maxInactiveInterval
spring:session:expirations:1623656160000 set 存储待过期的sessionId列表 key: 过期的时间戳;value: 在这个时间戳将要过期的expire key列表。

在访问时,先通过第一个key,判断session是否存在以及是否过期。如果没有过期,可以通过第二个key获取或者更新对应的session详情。

对于第三个key,实际上Spring-Session-Redis会有特殊的用途,主要是为了Redis的keySpace-notificationhttps://redis.io/topics/notifications。核心目的是为了确保过期的session一定要触发过期事件。关于这方面的解释,可以看一下RedisIndexedSessionRepository中的注释。
4. 订阅Spring-Session的相关事件

有些时候,我们比较关心Session的创建或者销毁事件,做一些特殊的处理或者记录。基于Redis的Spring-Session利用Spring Event将该事件发布出来,我们可以使用EventListener监听做处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码
@Component
@Slf4j
public class AnnotationDrivenEventListener {

@EventListener
public void handleSessionCreated(SessionCreatedEvent sessionCreatedEvent) {
String sessionId = sessionCreatedEvent.getSessionId();
log.info("session id:{} created", sessionId);
}

@EventListener
public void handleSessionDestroyed(SessionDestroyedEvent sessionDestroyedEvent) {
String sessionId = sessionDestroyedEvent.getSessionId();
log.info("session id:{} destroyed", sessionId);
}
}

总结

Spring Session提供了非常便利的,基于关系型数据库或者Redis的Session解决方案。

Redis版访问速度快,基于Redis的过期策略,保证过期数据会被删除,同时支持事件订阅。

数据库版直接基于数据库,无需单独引入其他存储。但是访问速度相对较慢,过期数据需要依赖应用程序自身进行删除。同时没有提供事件订阅能力。

参考文章

[1] www.javainuse.com/spring/spri…

[2] www.javainuse.com/spring/spri…

本文转载自: 掘金

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

Java开源项目推荐

发表于 2021-06-14

推荐几个优秀的开源项目,包含Java项目学习,Java程序员面试等等,帮助你打好基础,提升能力,扩展视野,以及更好地应对面试。

项目学习相关

Java商城

github地址:github.com/macrozheng/…

完整学习文档:www.macrozheng.com/

mall项目是一套电商系统,包括前台商城系统及后台管理系统,基于SpringBoot+MyBatis实现,采用Docker容器化部署。前台商城系统包含首页门户、商品推荐、商品搜索、商品展示、购物车、订单流程、会员中心、客户服务、帮助中心等模块。后台管理系统包含商品管理、订单管理、会员管理、促销管理、运营管理、内容管理、统计报表、财务管理、权限管理、设置等模块。

Java秒杀

github地址:github.com/qiurunze123…

高并发大流量如何进行秒杀架构,作者这部分知识做了一个系统的整理,写了一套系统。

后台管理系统

github地址:github.com/elunez/elad…

项目基于 Spring Boot 2.1.0 、 Jpa、 Spring Security、redis、Vue的前后端分离的后台管理系统,项目采用分模块开发方式, 权限控制采用 RBAC,支持数据字典与数据权限管理,支持一键生成前后端代码,支持动态路由。

微人事

github地址:github.com/lenve/vhr

微人事是一个前后端分离的人力资源管理系统,项目采用SpringBoot+Vue开发。

Java实验室

github地址:github.com/YunaiV/Spri…

一个涵盖六个专栏:Spring Boot 2.X、Spring Cloud、Spring Cloud Alibaba、Dubbo、分布式消息队列、分布式事务的仓库。有非常多的教程和代码demo。

springboot学习项目

github地址:

  • github.com/ZHENFENG13/…
  • github.com/newbee-ltd/…

该仓库中主要是 Spring Boot 的入门学习教程以及一些常用的 Spring Boot 实战项目教程,包括 Spring Boot 使用的各种示例代码,同时也包括一些实战项目的项目源码和效果展示,实战项目包括基本的 web 开发以及目前大家普遍使用的前后端分离实践项目等。

Java教程

github地址:github.com/ZhongFuChen…

从Java基础、JavaWeb基础到常用的框架再到面试题都有完整的教程,几乎涵盖了Java后端必备的知识点。

面试相关

Java面试

github地址:github.com/Snailclimb/…

「Java学习+面试指南」一份涵盖大部分 Java 程序员所需要掌握的核心知识。准备 Java 面试,首选 JavaGuide!

系统设计面试

github地址:github.com/donnemartin…

学习如何设计可扩展的系统将会有助于你成为一个更好的工程师。

系统设计是一个很宽泛的话题。在互联网上,关于系统设计原则的资源也是多如牛毛。

这个仓库就是这些资源的组织收集,它可以帮助你学习如何构建可扩展的系统。

计算机面试

github地址:github.com/CyC2018/CS-…

技术面试必备基础知识、Leetcode、计算机操作系统、计算机网络、系统设计

合集相关

Java开源项目合集

github地址:github.com/CodingDocs/…

Github 上非常棒的 Java 开源项目整理,我前面提到的开源项目很多都在里面有收录

本文转载自: 掘金

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

《蹲坑也能进大厂》多线程系列-Java内存模型精讲

发表于 2021-06-14

这是我参与更文挑战的第 4 天,活动详情查看:更文挑战

作者:JavaGieGie

微信公众号:Java开发零到壹

前言

前面两期我们介绍了多线程的基础知识点,都是一些面试高频问题,没有看和忘记的小伙伴可以回顾一下。

《蹲坑也能进大厂》多线程这几道基础面试题,80%小伙伴第一题就答错

《蹲坑也能进大厂》多线程系列-上下文、死锁、高频面试题

端午节.jpg

本章主要是分析一下大家非常面熟的Java内存模型,用代码的方式介绍重排序、可见性以及线程之间通信等原理,大家看完本篇必定有更加清楚的认识和理解。

狗剩子:花GieGie~,节日快乐啊!这么早就来蹲坑。

我:哟,狗剩子你今天又来加班了,365天无休啊你。

狗剩子:这不今天过节,没有什么好东西送给各位看官,只能肝出来一些干货送给老铁们么。

我:接招吧,狗儿。

正文

我:书接上文,狗剩子你给大伙讲讲什么是volatile?

上来就搞这么刺激的吗,你让咱家想想…

image.png

我:ok,小辣鸡,那我换个问题,你了解过Java内存模型吗?

这个不是三伏天喝冰水,正中下怀么。

Java内存模型(Java Memory Model)简称JMM,首先要知道它是一组规范,是一组多线程访问Java内存的规范。

我们都知道市面上Java虚拟机种类有很多,比如HotSpot VM、J9 VM以及各种实现(Oracle / Sun JDK、OpenJDK),而每一种虚拟机在解释Java代码、并进行重排序时都有自己的一套流程,如果没有JMM规范,那很有可能相同代码在不同JVM解释后,得到的运行结果也是不一致的,这是我们不希望看到的。

我:有点意思,但这种说法还是有点模糊,你再具体说说它都有哪些规范?

讨厌,就知道你会这么问,小伙们提到Java内存模型我们第一时间要想到3个部分,重排序、可见性、原子性。

  • 重排序

先看一段代码,给你几分钟时间,看看这段代码输出有几种结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码private static int x = 0, y = 0;
private static int a = 0, b = 0;

Thread one = new Thread(new Runnable() {
@Override
public void run() {
a = 1;
x = b;
}
});
Thread two = new Thread(new Runnable() {
@Override
public void run() {
b = 1;
y = a;
}
});
two.start();
one.start();
one.join();
two.join();
System.out.println("x = "+x+", y = "+y);

你的答案是不是这三种呢

image.png

如果是的话,那么恭喜你,可以继续和狗哥我一块继续往下研究第四种情况

12.jpg

这里我增加了一个for循环,可以循环打印,直到打印自己想要的结果,小伙伴们自己运行一下。

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
java复制代码private static int x = 0, y = 0;
private static int a = 0, b = 0;

public static void main(String[] args) throws InterruptedException {
int i = 0;
for (; ; ) {
i++;
x = 0;
y = 0;
a = 0;
b = 0;

CountDownLatch latch = new CountDownLatch(3);

Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
latch.countDown();
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
a = 1;
x = b;
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
latch.countDown();
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
b = 1;
y = a;
}
});
thread2.start();
thread1.start();
latch.countDown();
thread1.join();
thread2.join();

String result = "第" + i + "次(" + x + "," + y + ")";
if (x == 0 && y == 0) {
System.out.println(result);
break;
} else {
System.out.println(result);
}
}
}

看看你执行到多少次会出现呢,这里我是执行到将近17万次。

image.png

为什么会出现这种情况呢,那是因为这里发生了重排序,在重排序后,代码的执行顺序变成了:

  • y=2;
  • a=1;
  • x=b;
  • b=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
java复制代码public class Visibility {
int a = 1;
int b = 2;

private void change() {
a = 3;
b = a;
}


private void print() {
System.out.println("b=" + b + ";a=" + a);
}

public static void main(String[] args) {
while (true) {
Visibility visibility = new Visibility();
// 线程1
new Thread(() -> {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
visibility.change();
}).start();
// 线程2
new Thread(() -> {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
visibility.print();
}).start();
}
}
}

这里同样建议停留几分钟,你觉得print()打印结果有几种呢,多思考才能理解更深刻。

  • a=1,b=2 :线程1未执行到change(),此时线程2已执行print()
  • a=3,b=2:线程1执行到change()的a = 3,然后线程2正好执行print()
  • a=3,b=3:线程1执行完change(),然后线程2执行print()

这是大家最容易想到和理解的(如果没有想到,记得去补习一下花Gie的前两篇基础),但是还有一种情况比较特殊:

  • b=3,a=1

是不是没想到啊(手动得意),这里我们假如线程1执行完change()方法后,此时a=3且b=3,但是这时只是线程1自己知道这个结果值,对于线程2来说,他可能只看到了一部分,出现这种情况的原因,是因为线程之间通信是有延时的,而且多个线程之间不会进行实时同步,所以线程2只看到了b的最新值,并没有看到a的改变。

我:你这么说的话,我好像有点明白了,但还不是很清晰。

你可以再说说这个变量是怎么传递的吗,为什么线程2没有接收到a的变化呢?

好的呢,我都依你,我直接上个简单的草图吧。

图中我们分析出以下4个步骤。

  • 每个线程都会从主内存中获取变量,保存在自己的工作内存(线程私有)中,图1是线程1、线程2初始化状态;
  • 图2是线程1执行完change()方法后,先将b=3写回主内存(此时a=3还尚未写回主内存)
  • 线程2从主内存获取最新数据a = 1,b = 3,并写到自己的工作线程
  • 线程2最终打印出a=1,b=3

我:这下子我都看明白了,那你给我总结一下为什么会出现可见性原因吧,万一面试官问我我也好回答。

。。。

造成可见性的原因,主要是因为CPU有多级缓存,而每个线程会将自己需要的数据读取到独占缓存中,在数据修改后也是写入到缓存中,然后等待刷回主内存,这就导致了有些线程读写的值是一个过期的值。

我:有点6,我给你先点个赞,那还要一个原子性呢?

原子性我再后面再进行介绍,因为我们先了解volatile、synchronized之后再了解会更简单(你以为我不会volatile么,斜眼笑)。今天就先到这里吧,写了这么多,大家都懒得看了。

总结

JMM这块只是是非常重要的,熟练掌握以后在排查问题、写需求会更加得心应手,本篇本来想再多介绍一些其他内容,但是再写下去篇幅过长,效果就不是很好,所以先介绍这些,这里花Gie也强烈建议小伙伴们能亲手敲一下,纸上得来终觉浅,动手敲一敲以后写代码才不会虚。

下一章花Gie会继续介绍happens-before、volatile、内存结构进阶等,希望大家持续关注,明天假期结束了,我们继续肝。

点关注,防走丢

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

文章持续更新,可以微信搜一搜「 花哥编程 」第一时间阅读,后续会持续更新Java面试和各类知识点,有兴趣的小伙伴欢迎关注,一起学习,一起哈🐮🥃。

qrcode_for_gh_6c44fed6833c_344.jpg

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

本文转载自: 掘金

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

后端程序员必备:攻克order by

发表于 2021-06-13

前言

日常开发中,我们经常会使用到order by,亲爱的小伙伴,你是否知道order by 的工作原理呢?order by的优化思路是怎样的呢?使用order by有哪些注意的问题呢?本文将跟大家一起来学习,攻克order by~

image.png

  • 微信公众号:捡田螺的小男孩
  • github地址,感谢每一颗star
  • 如果觉得有收获,帮忙点赞,转发下哈,感谢感谢

一个使用order by 的简单例子

假设用一张员工表,表结构如下:

1
2
3
4
5
6
7
8
9
sql复制代码CREATE TABLE `staff` (
`id` BIGINT ( 11 ) AUTO_INCREMENT COMMENT '主键id',
`id_card` VARCHAR ( 20 ) NOT NULL COMMENT '身份证号码',
`name` VARCHAR ( 64 ) NOT NULL COMMENT '姓名',
`age` INT ( 4 ) NOT NULL COMMENT '年龄',
`city` VARCHAR ( 64 ) NOT NULL COMMENT '城市',
PRIMARY KEY ( `id`),
INDEX idx_city ( `city` )
) ENGINE = INNODB COMMENT '员工表';

表数据如下:

image.png

我们现在有这么一个需求:查询前10个,来自深圳员工的姓名、年龄、城市,并且按照年龄小到大排序。对应的 SQL 语句就可以这么写:

1
csharp复制代码select name,age,city from staff where city = '深圳' order by age limit 10;

这条语句的逻辑很清楚,但是它的底层执行流程是怎样的呢?

order by 工作原理

image.png

explain 执行计划

我们先用Explain关键字查看一下执行计划

image.png

  • 执行计划的key这个字段,表示使用到索引idx_city
  • Extra 这个字段的 Using index condition 表示索引条件
  • Extra 这个字段的 Using filesort表示用到排序

我们可以发现,这条SQL使用到了索引,并且也用到排序。那么它是怎么排序的呢?

全字段排序

MySQL 会给每个查询线程分配一块小内存,用于排序的,称为 sort_buffer。什么时候把字段放进去排序呢,其实是通过idx_city索引找到对应的数据,才把数据放进去啦。

我们回顾下索引是怎么找到匹配的数据的,现在先把索引树画出来吧,idx_city索引树如下:

image.png

idx_city索引树,叶子节点存储的是主键id。 还有一棵id主键聚族索引树,我们再画出聚族索引树图吧:

image.png

我们的查询语句是怎么找到匹配数据的呢?先通过idx_city索引树,找到对应的主键id,然后再通过拿到的主键id,搜索id主键索引树,找到对应的行数据。

加上order by之后,整体的执行流程就是:

  1. MySQL 为对应的线程初始化sort_buffer,放入需要查询的name、age、city字段;
  2. 从索引树idx_city, 找到第一个满足 city=’深圳’条件的主键 id,也就是图中的id=9;
  3. 到主键 id 索引树拿到id=9的这一行数据, 取name、age、city三个字段的值,存到sort_buffer;
  4. 从索引树idx_city 拿到下一个记录的主键 id,即图中的id=13;
  5. 重复步骤 3、4 直到city的值不等于深圳为止;
  6. 前面5步已经查找到了所有city为深圳的数据,在 sort_buffer中,将所有数据根据age进行排序;
  7. 按照排序结果取前10行返回给客户端。

执行示意图如下:

image.png

将查询所需的字段全部读取到sort_buffer中,就是全字段排序。这里面,有些小伙伴可能会有个疑问,把查询的所有字段都放到sort_buffer,而sort_buffer是一块内存来的,如果数据量太大,sort_buffer放不下怎么办呢?

磁盘临时文件辅助排序

实际上,sort_buffer的大小是由一个参数控制的:sort_buffer_size。如果要排序的数据小于sort_buffer_size,排序在sort_buffer 内存中完成,如果要排序的数据大于sort_buffer_size,则借助磁盘文件来进行排序

如何确定是否使用了磁盘文件来进行排序呢? 可以使用以下这几个命令

1
2
3
4
5
6
csharp复制代码## 打开optimizer_trace,开启统计
set optimizer_trace = "enabled=on";
## 执行SQL语句
select name,age,city from staff where city = '深圳' order by age limit 10;
## 查询输出的统计信息
select * from information_schema.optimizer_trace

可以从 number_of_tmp_files 中看出,是否使用了临时文件。

image.png

number_of_tmp_files 表示使用来排序的磁盘临时文件数。如果number_of_tmp_files>0,则表示使用了磁盘文件来进行排序。

使用了磁盘临时文件,整个排序过程又是怎样的呢?

  1. 从主键Id索引树,拿到需要的数据,并放到sort_buffer内存块中。当sort_buffer快要满时,就对sort_buffer中的数据排序,排完后,把数据临时放到磁盘一个小文件中。
  2. 继续回到主键 id 索引树取数据,继续放到sort_buffer内存中,排序后,也把这些数据写入到磁盘临时小文件中。
  3. 继续循环,直到取出所有满足条件的数据。最后把磁盘的临时排好序的小文件,合并成一个有序的大文件。

TPS: 借助磁盘临时小文件排序,实际上使用的是归并排序算法。

小伙伴们可能会有个疑问,既然sort_buffer放不下,就需要用到临时磁盘文件,这会影响排序效率。那为什么还要把排序不相关的字段(name,city)放到sort_buffer中呢?只放排序相关的age字段,它不香吗? 可以了解下rowid 排序。

rowid 排序

rowid 排序就是,只把查询SQL需要用于排序的字段和主键id,放到sort_buffer中。那怎么确定走的是全字段排序还是rowid 排序排序呢?

实际上有个参数控制的。这个参数就是max_length_for_sort_data,它表示MySQL用于排序行数据的长度的一个参数,如果单行的长度超过这个值,MySQL 就认为单行太大,就换rowid 排序。我们可以通过命令看下这个参数取值。

1
sql复制代码show variables like 'max_length_for_sort_data';

image.png

max_length_for_sort_data 默认值是1024。因为本文示例中name,age,city长度=64+4+64 =132 < 1024, 所以走的是全字段排序。我们来改下这个参数,改小一点,

1
2
3
4
ini复制代码## 修改排序数据最大单行长度为32
set max_length_for_sort_data = 32;
## 执行查询SQL
select name,age,city from staff where city = '深圳' order by age limit 10;

使用rowid 排序的话,整个SQL执行流程又是怎样的呢?

  1. MySQL 为对应的线程初始化sort_buffer,放入需要排序的age字段,以及主键id;
  2. 从索引树idx_city, 找到第一个满足 city=’深圳’条件的主键 id,也就是图中的id=9;
  3. 到主键 id 索引树拿到id=9的这一行数据, 取age和主键id的值,存到sort_buffer;
  4. 从索引树idx_city 拿到下一个记录的主键 id,即图中的id=13;
  5. 重复步骤 3、4 直到city的值不等于深圳为止;
  6. 前面5步已经查找到了所有city为深圳的数据,在 sort_buffer中,将所有数据根据age进行排序;
  7. 遍历排序结果,取前10行,并按照 id 的值回到原表中,取出city、name 和 age 三个字段返回给客户端。

执行示意图如下:

image.png

对比一下全字段排序的流程,rowid 排序多了一次回表。

什么是回表?拿到主键再回到主键索引查询的过程,就叫做回表

我们通过optimizer_trace,可以看到是否使用了rowid排序的:

1
2
3
4
5
6
csharp复制代码## 打开optimizer_trace,开启统计
set optimizer_trace = "enabled=on";
## 执行SQL语句
select name,age,city from staff where city = '深圳' order by age limit 10;
## 查询输出的统计信息
select * from information_schema.optimizer_trace

image.png

全字段排序与rowid排序对比

  • 全字段排序: sort_buffer内存不够的话,就需要用到磁盘临时文件,造成磁盘访问。
  • rowid排序: sort_buffer可以放更多数据,但是需要再回到原表去取数据,比全字段排序多一次回表。

一般情况下,对于InnoDB存储引擎,会优先使用全字段排序。可以发现 max_length_for_sort_data 参数设置为1024,这个数比较大的。一般情况下,排序字段不会超过这个值,也就是都会走全字段排序。

order by的一些优化思路

我们如何优化order by语句呢?

  • 因为数据是无序的,所以就需要排序。如果数据本身是有序的,那就不用排了。而索引数据本身是有序的,我们通过建立联合索引,优化order by 语句。
  • 我们还可以通过调整max_length_for_sort_data等参数优化;

联合索引优化

再回顾下示例SQL的查询计划

1
csharp复制代码explain select name,age,city from staff where city = '深圳' order by age limit 10;

image.png

我们给查询条件city和排序字段age,加个联合索引idx_city_age。再去查看执行计划

1
2
sql复制代码alter table staff add  index idx_city_age(city,age);
explain select name,age,city from staff where city = '深圳' order by age limit 10;

image.png

可以发现,加上idx_city_age联合索引,就不需要Using filesort排序了。为什么呢?因为索引本身是有序的,我们可以看下idx_city_age联合索引示意图,如下:

image.png

整个SQL执行流程变成酱紫:

  1. 从索引idx_city_age找到满足city=’深圳’ 的主键 id
  2. 到主键 id索引取出整行,拿到 name、city、age 三个字段的值,作为结果集的一部分直接返回
  3. 从索引idx_city_age取下一个记录主键id
  4. 重复步骤 2、3,直到查到第10条记录,或者是不满足city=’深圳’ 条件时循环结束。

流程示意图如下:

image.png

从示意图看来,还是有一次回表操作。针对本次示例,有没有更高效的方案呢?有的,可以使用覆盖索引:

覆盖索引:在查询的数据列里面,不需要回表去查,直接从索引列就能取到想要的结果。换句话说,你SQL用到的索引列数据,覆盖了查询结果的列,就算上覆盖索引了。

我们给city,name,age 组成一个联合索引,即可用到了覆盖索引,这时候SQL执行时,连回表操作都可以省去啦。

调整参数优化

我们还可以通过调整参数,去优化order by的执行。比如可以调整sort_buffer_size的值。因为sort_buffer值太小,数据量大的话,会借助磁盘临时文件排序。如果MySQL服务器配置高的话,可以使用稍微调整大点。

我们还可以调整max_length_for_sort_data的值,这个值太小的话,order by会走rowid排序,会回表,降低查询性能。所以max_length_for_sort_data可以适当大一点。

当然,很多时候,这些MySQL参数值,我们直接采用默认值就可以了。

使用order by 的一些注意点

没有where条件,order by字段需要加索引吗

日常开发过程中,我们可能会遇到没有where条件的order by,那么,这时候order by后面的字段是否需要加索引呢。如有这么一个SQL,create_time是否需要加索引:

1
vbnet复制代码select * from A order by create_time;

无条件查询的话,即使create_time上有索引,也不会使用到。因为MySQL优化器认为走普通二级索引,再去回表成本比全表扫描排序更高。所以选择走全表扫描,然后根据全字段排序或者rowid排序来进行。

如果查询SQL修改一下:

1
vbnet复制代码select * from A order by create_time limit m;
  • 无条件查询,如果m值较小,是可以走索引的.因为MySQL优化器认为,根据索引有序性去回表查数据,然后得到m条数据,就可以终止循环,那么成本比全表扫描小,则选择走二级索引。

分页limit过大时,会导致大量排序怎么办?

假设SQL如下:

1
css复制代码select * from A order by a limit 100000,10
  • 可以记录上一页最后的id,下一页查询时,查询条件带上id,如: where id > 上一页最后id limit 10。
  • 也可以在业务允许的情况下,限制页数。

索引存储顺序与order by不一致,如何优化?

假设有联合索引 idx_age_name, 我们需求修改为这样:查询前10个员工的姓名、年龄,并且按照年龄小到大排序,如果年龄相同,则按姓名降序排。对应的 SQL 语句就可以这么写:

1
sql复制代码select name,age from staff  order by age ,name desc limit 10;

我们看下执行计划,发现使用到Using filesort。

image.png

这是因为,idx_age_name索引树中,age从小到大排序,如果age相同,再按name从小到大排序。而order by 中,是按age从小到大排序,如果age相同,再按name从大到小排序。也就是说,索引存储顺序与order by不一致。

我们怎么优化呢?如果MySQL是8.0版本,支持Descending Indexes,可以这样修改索引:

1
2
3
4
5
6
7
8
9
sql复制代码CREATE TABLE `staff` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`id_card` varchar(20) NOT NULL COMMENT '身份证号码',
`name` varchar(64) NOT NULL COMMENT '姓名',
`age` int(4) NOT NULL COMMENT '年龄',
`city` varchar(64) NOT NULL COMMENT '城市',
PRIMARY KEY (`id`),
KEY `idx_age_name` (`age`,`name` desc) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8 COMMENT='员工表';

使用了in条件多个属性时,SQL执行是否有排序过程

如果我们有联合索引idx_city_name,执行这个SQL的话,是不会走排序过程的,如下:

1
csharp复制代码select * from staff where city in ('深圳') order by age limit 10;

image.png

但是,如果使用in条件,并且有多个条件时,就会有排序过程。

1
csharp复制代码 explain select * from staff where city in ('深圳','上海') order by age limit 10;

image.png

这是因为:in有两个条件,在满足深圳时,age是排好序的,但是把满足上海的age也加进来,就不能保证满足所有的age都是排好序的。因此需要Using filesort。

最后

  • 如果觉得有收获,帮忙点赞,转发下哈,感谢感谢
  • 微信搜索公众号:捡田螺的小男孩,加个好友,进技术交流群

参考与感谢

  • MySQL实战45讲

本文转载自: 掘金

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

Java 并发包之Lock接口 何谓Lock

发表于 2021-06-13

何谓Lock

它是java.util.concurrent.locks包下的一个类

老样子,来通过官方文档来详细了解Lock是什么以及怎么用

image.png

  • Lock提供了比使用synchronized方法和语句块更广泛的锁操作。就是加锁的方式更灵活。它们允许更灵活的结构,可能具有完全不同的属性,并且可能支持多个关联的Condition对象。
  • 通常,lock提供对共享资源的独占访问:一次只有一个线程可以获取lock,所有对共享资源的访问都需要先获取lock。 但是,某些lock可能允许并行访问共享资源,例如ReadWriteLock的ReadLock。
+ > **关于并行访问共享资源,例如,在多线程情况下,有些线程是读操作,有些线程是写操作,对于读操作的共享资源其实可以并行的访问它,而不需要限制一次一个线程**
  • synchronized方法或语句的使用提供对与每个对象关联的隐式monitor lock的访问,但强制所有锁获取和释放以块结构方式发生:当获取多个锁时(一个线程获取多个对象的锁),它们必须以相反的顺序释放,并且所有锁都必须在它们被获取的同一个词法范围内(语句块)释放。

image.png

  • synchronized很方便,但有弊端:锁的获取和释放有顺序且都是自动的且要在同一个作用域内。
  • Lock接口的实现允许在不同范围(作用域)内获取和释放锁,并允许以任何顺序获取和释放多个锁,即消除了synchronized锁的自动释放,变成了手动。
  • 当锁定和解锁发生在不同的作用域时,必须注意确保持有锁时执行的所有代码都受到 try-finally 或 try-catch 的保护,以确保在必要时释放锁。
  • Lock实现通过提供非阻塞的获取锁尝试( tryLock() )、尝试获取可以被中断的锁(lockInterruptibly ,以及尝试获取锁tryLock()来提供比使用synchronized方法和语句更多的功能可以超时获取锁( tryLock(long, TimeUnit) ,即超过规定时间没有获取到锁就继续往下执行,而不会被阻塞)。而synchronized是会导致线程阻塞的。
+ [关于阻塞和非阻塞](https://blog.csdn.net/weixin_37850264/article/details/112793865?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_baidulandingword-1&spm=1001.2101.3001.4242),(**阻塞和非阻塞就是会不会被挂起导致线程无所事事**)

image.png

  • Lock类还可以提供与隐式监视器(monitor,synchronized关键关键字其实是让线程获得监视器,只是在代码层看不见而已)锁完全不同的行为和语义,例如保证排序、不可重入使用或死锁检测。 如果实现提供了这样的专门语义,那么实现必须记录这些语义。
  • 请注意, Lock实例只是普通对象,它们本身可以用作synchronized语句中的目标。 获取Lock实例的监视器锁与调用该实例的任何lock方法没有特定的关系。 建议不要以这种方式使用Lock实例以避免混淆,除非在它们自己的实现中。
  • 除非另有说明,否则为任何参数传递null值都将导致抛出NullPointerException 。
  • 所有Lock实现都必须强制执行与内置监视器锁提供的相同内存同步语义
+ [Java内存模型](https://www.cnblogs.com/Scramblecode/p/11392639.html)
  • 锁获取的三种形式(可中断、不可中断和定时)可能在它们的性能特征、排序保证或其他实现质量方面有所不同。 此外,在给定的Lock类中可能无法中断正在进行的锁获取。 因此,实现不需要为所有三种形式的锁获取定义完全相同的保证或语义,也不需要支持正在进行的锁获取的中断。 需要一个实现来清楚地记录每个锁定方法提供的语义和保证。 它还必须遵守此接口中定义的中断语义,以支持锁定获取的中断:完全或仅在方法入口上。
  • 由于中断通常意味着取消,并且中断检查通常很少,因此实现可以倾向于响应中断而不是正常的方法返回。 即使可以证明中断发生在另一个动作可能已经解除了线程的阻塞之后也是如此。 实现应记录此行为。

接接着瞧一瞧该Lock接口中的方法

void lock();

image.png

  • 获得锁。
  • 如果锁不可用,则当前线程将被禁用以用于线程调度目的并处于休眠状态,直到获得锁。
  • 实现时注意事项: Lock实现可能能够检测到对锁的错误使用,例如会导致死锁的调用,并且在这种情况下可能会抛出(未经检查的)异常。 该Lock实现必须标记异常类型。

void lockInterruptibly() throws InterruptedException;

image.png

  • 除非当前线程被中断,否则获取锁。
+ > **[线程中断](https://blog.csdn.net/xinxiaoyong100440105/article/details/80931705),中断的结果线程是死亡、还是等待新的任务或是继续运行至下一步,就取决于这个程序本身。**
  • 如果lock可用,则获取锁并立即返回。
  • 如果锁不可用,则当前线程将出于线程调度目的而被禁用并处于休眠状态,直到发生以下两种情况之一:
+ 锁被当前线程获取; 或者
+ 其他一些线程中断当前线程,支持中断获取锁。
  • 如果当前线程:
+ 在进入此方法时设置其中断状态; 或者
+ 获取锁时中断,支持中断获取锁,然后抛出InterruptedException并清除当前线程的中断状态。
  • 实现时注意事项:
+ 在某些实现中,中断获取锁的能力可能是不可能的,如果可能的话,这可能是一项**昂贵的操作**。 程序员应该意识到可能是这种情况。 在这种情况下,实现应该记录。
+ 与正常方法返回相比,实现**可以更倾向于响应中断**。
+ Lock实现可能能够检测到对锁的错误使用,例如会导致死锁的调用,并且在这种情况下可能会抛出(未经检查的)异常。 该Lock实现必须记录情况和标记异常类型。
  • 抛出InterruptedException
+ 如果当前线程在**获取锁时被中断**(并且支持锁获取中断)

boolean tryLock();

image.png

  • 仅当调用时锁空闲时才获取锁。
  • 如果可用,则获取锁并立即返回值为true 。 如果锁不可用,则此方法将立即返回false值。
  • 这种方法的典型用法是:
1
2
3
4
5
6
7
8
9
10
java复制代码 Lock lock = ...;
if (lock.tryLock()) {
try {
// manipulate protected state(处于保护状态的代码块)
} finally {
lock.unlock();
}
} else {
// perform alternative actions(替代操作,不让线程阻塞)
}
  • 此用法可确保在获得锁时解锁,如果未获得锁,则不会尝试解锁。
  • 返回值:
    • 如果获得锁则为true ,否则为false

boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

image.png

  • 如果在给定的等待时间内空闲并且当前线程未被中断,则获取锁。

  • 如果锁可用,则此方法立即返回true值。 如果锁不可用,则当前线程将出于线程调度目的而被禁用并处于休眠状态,直到发生以下三种情况之一:

    • 锁被当前线程获取; 或者
    • 其他一些线程中断当前线程,支持中断获取锁(中断获取锁被支持); 或者
    • 经过指定的等待时间
  • 如果获得了锁,则返回值true 。

  • 如果当前线程:

    • 在进入此方法时设置其为中断状态; 或者
    • 获取锁时中断,支持中断获取锁,然后抛出InterruptedException并清除当前线程的中断状态。
  • 如果指定的等待时间过去,则返回值false 。

  • 如果时间小于或等于零,则该方法根本不会等待。

  • 实现时注意事项:

    • 在某些实现中,中断获取锁的能力可能是不可能的,如果可能的话,这可能是一项昂贵的操作。 程序员 应该意识到可能是这种情况。 在这种情况下,实现应该记录。

    • 实现可以倾向于响应中断(对中断请求做出响应)而不是正常方法返回,或报告超时。

    • Lock实现可能能够检测到对锁的错误使用,例如会导致死锁的调用,并且在这种情况下可能会抛出(未经检查的)异常。 该Lock实现必须记录情况和异常类型。
      参数:

      time - 等待获得锁的最长时间

      unit – time参数的时间单位(是枚举值)

      返回值:

      如果获得锁则为true如果在获得锁之前等待时间已过则为false

      抛出:

      InterruptedException - 如果当前线程在获取锁时被中断(并且支持锁获取中断)

void unlock();

image.png

  • 释放锁。
  • 实现时注意事项:
    • Lock实现通常会对哪个线程可以释放锁施加限制(通常只有锁的持有者可以释放它)并且如果违反限制可能会抛出(未经检查的)异常。 该Lock实现必须记录任何限制和异常类型。

Condition newCondition();

image.png

  • 返回绑定到此Lock实例的新Condition实例。

  • 在等待Condition之前,当前线程必须持有锁。 调用Condition.await()将在等待之前自动释放锁,并在等待返回之前重新获取锁。

  • 实现时注意事项

    • Condition实例的确切操作取决于Lock实现,并且必须由该实现记录。
      返回值:

      此Lock实例的新Condition实例

      抛出:

      UnsupportedOperationException – 如果此Lock实现不支持条件

小结

关于Lock和synchronized的主要区别

1.锁的获取方式:前者是通过程序代码的方式由开发者手工获取,后者是通过JVM来获取(无需开发者干预)

2.具体实现方式:前者是通过Java代码的方式来实现,后者是通过JVM底层来实现(无需开发者关注)

3.锁的释放方式:前者务必通过unlock()方法在finally块中手工释放,后者是通过JvM来释放(无需开发者关注)

4.锁的具体类型:前者提供了多种,如公平锁、非公平锁,后者与前者均提供了可重入锁

关于公平锁和非公平锁以及其他一些实现细节将在ReentrantLock的有关章节介绍

本文转载自: 掘金

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

一文吃透Json解析工具类合集(FastJson、Jacks

发表于 2021-06-13

前言

在工作中,少不了Json解析处理,一般我们常用的就是FastJson、Jackson、Gson这三大工具类,他们都有各自的优缺点,本篇文章我们来使用一下他们各自的API,对比一下。

一、各个JSON技术的简介

1.1 FastJson角色

fastjson是阿里巴巴的开源JSON解析库,它可以解析JSON格式的字符串,支持将Java Bean序列化为JSON字符串,也可以从JSON字符串反序列化到JavaBean。

Github官网简介:github.com/alibaba/fas…

1.2 Jackson角色

Jackson被称为“ Java JSON库”或“ Java的最佳JSON解析器”。或简称为“ JSON for Java”。

Github官网简介:github.com/FasterXML/j…

1.3 Gson角色

Gson是一个Java库,可用于将Java对象转换为其JSON表示形式。它也可以用于将JSON字符串转换为等效的Java对象。Gson可以处理任意Java对象,包括您没有源代码的预先存在的对象。

Github官网简介:github.com/google/gson

二、使用步骤

2.1 引入库

1、FastJson依赖

1
2
3
4
5
6
xml复制代码<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.75</version>
</dependency>

Maven中央仓库:mvnrepository.com/artifact/co…

2、Jackson依赖

1
2
3
4
5
6
xml复制代码<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-annotations -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.12.1</version>
</dependency>

Maven中央仓库:mvnrepository.com/artifact/co…

3、Gson依赖

1
2
3
4
5
6
xml复制代码<!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.6</version>
</dependency>

Maven中央仓库:mvnrepository.com/artifact/co…

2.2 将JSON 字符串转换为Java 对象

Person对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码/**
* @author DT开发者
*/
@Data
@EqualsAndHashCode(callSuper = false)
@AllArgsConstructor
@NoArgsConstructor
public class Person implements Serializable {

private Integer id;

private String name;

private Date createTime;
}

1、FastJson版本

1
2
3
4
5
6
7
8
9
10
java复制代码 public static void main(String[] args) {
String json = "{\"createTime\":1621341922450,\"id\":1,\"name\":\"DT\"}";
// 方式1: 将json字符串转为Java对象
Person person = JSON.parseObject(json,Person.class);
System.out.println("java对象->>>"+person);
System.out.println("=========================================================");
// 方式2: 将json字符串转为Java对象
Person newPerson2 = JSONObject.parseObject(json, Person.class);
System.out.println("java对象->>>"+newPerson2);
}
1
2
java复制代码JSON.parseObject(String,Object.class)
JSONObject.parseObject(String, Object.class)

在这里插入图片描述

2、Jackson版本

1
2
3
4
5
6
7
8
9
10
11
java复制代码public static void main(String[] args) {
// 创建Jackson核心对象 ObjectMapper
ObjectMapper objectMapper = new ObjectMapper();
String json = "{\"createTime\":1621341922450,\"id\":1,\"name\":\"DT\"}";
try {
Person person = objectMapper.readValue(json, Person.class);
System.out.println("java对象->>>"+person);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}

在这里插入图片描述

3、Gson版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public static void main(String[] args) {
// gson无法正常将时间戳转化成date
// 使用JSON内存树注册Date类型
final Gson gson = new GsonBuilder()
.registerTypeAdapter(Date.class, new JsonDeserializer<Date>() {
@Override
public Date deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext context) throws JsonParseException {
return new Date(jsonElement.getAsJsonPrimitive().getAsLong());
}
}).create();

String json = "{\"createTime\":1621341922450,\"id\":1,\"name\":\"DT\"}";
Person person = gson.fromJson(json, Person.class);
System.out.println("java对象->>>"+person);
}

在这里插入图片描述

2.3 将Java对象转换为JSON 字符串

1、FastJson版本

1
2
3
4
5
6
7
8
9
10
java复制代码public static void main(String[] args) {
Person person = new Person(1,"DT",new Date());
// 方式1:将对象转为json字符串
String json = JSON.toJSONString(person);
System.out.println("json字符串->>>"+json);
System.out.println("=========================================================");
// 方式2:将对象转为json字符串
String json2 = JSONObject.toJSONString(person);
System.out.println("json字符串->>>"+json2);
}
1
2
java复制代码JSONObject.toJSONString(String)
JSON.toJSONString(String)

在这里插入图片描述

2、Jackson版本

1
2
3
4
5
6
7
8
9
10
11
java复制代码public static void main(String[] args) {
// 创建Jackson核心对象 ObjectMapper
ObjectMapper objectMapper = new ObjectMapper();
Person person = new Person(1,"DT",new Date());
try {
String json = objectMapper.writeValueAsString(person);
System.out.println("json字符串->>>"+json);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}

在这里插入图片描述

3、Gson版本

1
2
3
4
5
6
java复制代码public static void main(String[] args) {
Gson gson = new Gson();
Person person = new Person(1,"DT",new Date());
String json = gson.toJson(person);
System.out.println("json字符串->>>"+json);
}

在这里插入图片描述

2.4 将JSON 字符串数组转为JSON数组

1、FastJson版本

1
2
3
4
5
6
7
8
9
java复制代码public static void main(String[] args) {
List<Person> personList = new ArrayList<>();
personList.add(new Person(1,"DT",new Date()));
personList.add(new Person(2,"DT1",new Date()));
personList.add(new Person(3,"DT2",new Date()));
String json = JSONObject.toJSONString(personList);
JSONArray jsArr = JSONObject.parseArray(json);
System.out.println(jsArr);
}
1
java复制代码JSONObject.parseArray(json)

在这里插入图片描述
遍历循环Json数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码 public static void main(String[] args) {
List<Person> personList = new ArrayList<>();
personList.add(new Person(1,"DT",new Date()));
personList.add(new Person(2,"DT1",new Date()));
personList.add(new Person(3,"DT2",new Date()));
String json = JSONObject.toJSONString(personList);
JSONArray jsArr = JSONObject.parseArray(json);
// 遍历方式1
jsArr.stream().forEach(json1 ->{
System.out.println(json1.toString());
});
// 遍历方式2
for (Object o : jsArr) {
JSONObject obj = (JSONObject) o;
System.out.println("取到id->>>" + obj.get("id"));
// 判断是否存在key
System.out.println("key是否存在->>>"+obj.containsKey("name1"));
// 判断是否存在值
System.out.println("value是否存在->>>"+obj.containsValue(obj.get("id")));
}
}

2、Jackson版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public static void main(String[] args) throws JsonProcessingException {
// 将JSON数组转换为array对象
ObjectMapper mapper = new ObjectMapper();
String json = "[{\"id\":1,\"name\":\"张三\",\"createTime\":1622719597081}, {\"id\":2,\"name\":\"李四\",\"createTime\":1622719597081}]";
// 方式1
Person[] pp1 = mapper.readValue(json, Person[].class);
for (Person person : pp1) {
System.out.println(person);
}
// 方式2
List<Person> ppl2 = Arrays.asList(mapper.readValue(json, Person[].class));
ppl2.stream().forEach(System.out::println);
// 方式3
List<Person> pp3 = mapper.readValue(json, new TypeReference<List<Person>>() {});
pp3.stream().forEach(System.out::println);
}

3、Gson版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码```java
public static void main(String[] args) {
// gson无法正常将时间戳转化成date
// 使用JSON内存树注册Date类型
final Gson gson = new GsonBuilder()
.registerTypeAdapter(Date.class, new JsonDeserializer<Date>() {
@Override
public Date deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext context) throws JsonParseException {
return new Date(jsonElement.getAsJsonPrimitive().getAsLong());
}
}).create();

String json = "[{\"id\":1,\"name\":\"张三\",\"createTime\":1622719597081}, {\"id\":2,\"name\":\"李四\",\"createTime\":1622719597081}]";
Type type = new TypeToken<ArrayList<Person>>(){}.getType();
List<Person> personList = gson.fromJson(json, type);
personList.stream().forEach(System.out::println);
}

List反序列化时必须提供它的Type,通过Gson提供的TypeToken.getType()方法可以定义当前List的Type。

在这里插入图片描述

2.5 将JSON数组转为JSON 字符串

1、FastJson版本

1
2
3
4
5
6
7
8
java复制代码public static void main(String[] args) {
List<Person> personList = new ArrayList<>();
personList.add(new Person(1,"DT",new Date()));
personList.add(new Person(2,"DT1",new Date()));
personList.add(new Person(3,"DT2",new Date()));
String json = JSONObject.toJSONString(personList);
System.out.println("json->>>"+json);
}

2、Jackson版本

1
2
3
4
5
6
7
8
9
java复制代码public static void main(String[] args) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
List<Person> personList = new ArrayList<>();
personList.add(new Person(1,"DT",new Date()));
personList.add(new Person(2,"DT1",new Date()));
personList.add(new Person(3,"DT2",new Date()));
String json = objectMapper.writeValueAsString(personList);
System.out.println("json->>>"+json);
}

3、Gson版本

1
2
3
4
5
6
7
8
9
java复制代码public static void main(String[] args) {
Gson gson = new Gson();
List<Person> personList = new ArrayList<>();
personList.add(new Person(1,"DT",new Date()));
personList.add(new Person(2,"DT1",new Date()));
personList.add(new Person(3,"DT2",new Date()));
String json = gson.toJson(personList);
System.out.println("json->>>"+json);
}

总结

以上就是今天的内容,能坚持看到这里,你一定会有收获,我个人比较喜欢使用FastJson,推挤教程文档:www.runoob.com/w3cnote/fas…

技术群:176251012 喜欢是一种态度,坚持才是态度的体现。

本文转载自: 掘金

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

1…643644645…956

开发者博客

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