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

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


  • 首页

  • 归档

  • 搜索

10篇文章带你手摸手封装gin框架(2)- Viper配置管

发表于 2021-06-07

前言

这是我参与更文挑战的第2天,大家好,我是作曲家种太阳

上一篇,我们学到了基本的目录设计和基本技术选型,也带大家做了一个gin的helloworld的程序

这篇会带你使用viper处理yaml配置文件并集成到gin框架中来~

  1. 介绍

Viper是适用于Go应用程序的完整配置解决方案。它被设计用于在应用程序中工作,并且可以处理所有类型的配置需求和格式。它支持以下特性:

  1. • 设置默认值
  2. • 从JSON、TOML、YAML、HCL、envfile和Java properties格式的配置文件读取配置信息
  3. • 实时监控和重新读取配置文件(可选)
  4. • 从环境变量中读取
  5. • 从远程配置系统(etcd或Consul)读取并监控配置变化
  6. • 从命令行参数读取配置
  7. • 从buffer读取配置
  8. • 显式配置值

Viper官方地址

  1. 安装

1
shell复制代码go get github.com/spf13/viper
  1. 需求分析

思考下,一个服务需要哪些配置呢?

  1. 存放日志目录路径(logAddress)
  2. 服务名称(name)
  3. 服务端口号(port)
  4. mysql的端口号(port)和地址(host)
    ps:我们暂且先写这么多,后续还需要配置的我们慢慢加

3.数据流向图解

在写代码之前,我们得搞懂到settings-dev.yaml到viper识别,然后在到各个文件中,是怎么个过程
一张图,你就明白了~

image.png

3.编写settings-dev.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
yaml复制代码# settings-dev.yaml
name: "go-gin"
port: 8022
logsAddress: "./logs/"

mysql:
name: "root"
host: "120.XX.XX.XX"
port: 3306
password: "XXXXXXXX"
dbName: "test"

redis:
host: "120.XX.XX.XX"
port: 6379

4.定义配置的对应的struct

在config/config中编写几个结构体

viper会吧yaml的数据给对应的结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
go复制代码package config

type ServerConfig struct {
Name string `mapstructure:"name"`
Port int `mapstructure:"port"`
Mysqlinfo MysqlConfig `mapstructure:"mysql"`
RedisInfo RedisConfig `mapstructure:"redis"`
LogsAddress string `mapstructure:"logsAddress"`
}

type MysqlConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Name string `mapstructure:"name"`
Password string `mapstructure:"password"`
DBName string `mapstructure:"dbName"`
}

type RedisConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
}

ps: 一定要注意 settings-dev.yaml每个字段名称和结构体的tag一一对应!!

5.编写global数据

global的数据主要是把viper的解析出来的数据存储,这样每个go文件都已引用glbal中的配置数据.

在 global/globalVar.go中编写

1
2
3
4
5
6
7
8
9
go复制代码package global
import (
"go.uber.org/zap"
"go_gin/config"
)

var (
Settings config.ServerConfig
)

6.使用Viper处理yaml配置

在 initialize/config中,编写

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
go复制代码package initialize

import (
"github.com/fatih/color"
"github.com/spf13/viper"
"go_gin/config"
"go_gin/global"
)

func InitConfig() {
// 实例化viper
v := viper.New()
//文件的路径如何设置
v.SetConfigFile("./settings-dev.yaml")
if err := v.ReadInConfig(); err != nil {
panic(err)
}
serverConfig := config.ServerConfig{}
//给serverConfig初始值
if err := v.Unmarshal(&serverConfig); err != nil {
panic(err)
}
// 传递给全局变量
global.Settings = serverConfig
color.Blue("11111111", global.Settings.LogsAddress)
}

7.使用配置变量

在 main.go中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
go复制代码package main

import (
"fmt"
"github.com/fatih/color"
"go.uber.org/zap"
"go_gin/global"
"go_gin/initialize"
"go_gin/utils"
)

func main() {
//1.初始化yaml配置
initialize.InitConfig()

r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run(fmt.Sprintf(":%d", global.Settings.Port))
}

最后-验证结果环节

启动 main.go
当你发现你在settings-dev.yaml配置端口号,在控制台打印,说明你就成功了~

image.png

思考

如何区分线上环境和开发环境的配置呢?

本文转载自: 掘金

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

Linux 基础网络设置及搭建 DHCP 服务

发表于 2021-06-07

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


查看网络配置

1.使用ifconfig命令查看网络接口地址

主机的网络接口卡通常被称为“网络接口”。在Linux系统中,使用ifconfig命令可以查看网络接口的地址配置信息。不带任何选项和参数时,将显示当前主机中已启用的网络接口信息。

图片

eth0:第一块以太网卡的名称。“eth0”中的“eth”是“ethernet”的缩写,表示网卡类型为以太网,数字“0”表示第1块网卡。由于大多数主机中只有一块物理网卡,因此“eth0”代表系统中唯一的网络接口。如果有多个物理网卡,则第2块网卡表示为“eth1”,第3块网卡表示为“eth2”。

lo:“回环”网络接口,是“loopback”的缩写,它并不代表真正的网络接口,而是一个虚拟的网络接口,ip地址默认是127.0.0.1.回环地址通常仅用于对本机的网络测试。

当只需要查看其中某一个网络接口的信息时,可以使用网络接口的名称作为ifconfig命令的参数(不论该网络接口是否处于激活状态)。

图片

从上述命令显示的结果中,可以获知eth0网卡的一些基本信息。

  • “HWaddr”:表示网络接口的MAC地址。网络接口的MAC地址通常不能更改,是网卡在生产时确定的全球唯一的硬件地址。
  • “inet addr”:表示网络接口的IP地址。
  • “Bcast”:表示网络接口所在网络的广播地址。
  • “Mask”:表示网络接口的子网掩码。
  • “TX”和“RX”:表示该网络接口发送和接收的数据包个数,流量等。

2.使用hostname命令查看主机名称(不用添加任何选项或参数)

在Linux系统中,一部分网络服务都会通过主机名来识别主机,如果主机名配置不当,可能会导致程序功能出现故障。

1
2
3
4
csharp复制代码[root@localhost-181117 ~]# hostname
localhost-181117
[root@localhost-181117 ~]# hostname localhost-18111716
[root@localhost-181117 ~]# bash //更改主机名时,bash重新启量临时生效

3.使用route命令查看路由表条目

Linux系统中的路由表决定着从本机向其他主机、其他网络发送数据的去向,是排除网络故障的关键信息。直接执行route命令可以查看当前主机中的路由信息。在输出结果中,Destination列对应目标网段的地址,Gateway列对应下一跳路由器的地址,Iface列对应发送数据的网络接口。

1
2
3
4
5
6
csharp复制代码[root@localhost-181117 ~]# route
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
192.168.231.0 * 255.255.255.0 U 1 0 0 eth0
default bogon 0.0.0.0 UG 0 0 0 eth0
[root@localhost-181117 ~]#

当目标网段为“Default”时,表示此行是默认网关记录;当下一跳为“*”时,表示目标网段是与本机直接相连的。结合“-n”选项,可以将路由记录中的地址显示为数字形式。

1
2
3
4
5
sql复制代码[root@localhost-181117 ~]# route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
192.168.231.0 0.0.0.0 255.255.255.0 U 1 0 0 eth0
0.0.0.0 192.168.231.2 0.0.0.0 UG 0 0 0 eth0

4.使用netstat命令查看网络连接情况

通过netstat命令可以查看当前系统的网络连接状态、路由表、接口统计等信息,是了解网络状态及排除网络服务故障的有效工具。

  • -a:显示当前主机中国所有活动的网络连接信息。
  • -n:以数字的形式显示相关的主机地址、端口等信息。
  • -r:显示路由表信息。
  • -l:显示处于监听状态的网络连接及端口信息。
  • -t:查看TCP协议相关的信息。
  • -u:查看UDP协议相关的信息。
  • -p:显示与网络连接相关联的进程号、进程名称信息。

通常使用“-anpt”组合选项,以数字形式显示当前系统中所有的TCP连接信息,同事显示对应的进程信息。结合管道使用“grep”命令,过滤出所需要的特定记录。

1
2
3
4
bash复制代码[root@localhost-181117 ~]# netstat -anpt | grep ":22"
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 1367/sshd
tcp 0 64 192.168.231.131:22 192.168.231.1:60782 ESTABLISHED 33595/sshd
tcp 0 0 :::22 :::* LISTEN 1367/sshd

测试网络连接

1.使用ping命令测试网络连通性

使用ping命令可以向目的主机持续地发送测试数据包,并显示结果,直接按Ctrl+C组合键中止测试,并显示最终的统计结果。

图片

执行ping命令,若不能获得从目标主机发回的数据包,则表示在本机到目标主机之间存在网络连通性故障。

图片

当网络中存在影响通信过程稳定性的因素时,使用ping命令可能会频繁看到“Request timeout”的结果,表示与目标主机间的连接超时。

2.使用traceroute命令跟踪数据包的路由途径

使用traceroute命令可以测试从当前主机到目的主机之间经过了哪些网络结点,并显示各中间结点的连接状态。对于无法响应的结点,连接状态将显示为“*”。

在网络测试与排错过程中,通常会先使用ping命令测试与目的主机的网络连接,如果发现网络连接有故障,则使用traceroute命令跟踪查看是在哪个中间结点存在故障。

3.使用nslookup命令测试DNS域名解析

nslookup命令时用来测试域名解析的专用工具,使用时只要指定要解析的目标域名作为参数即可。

图片

使用网络配置命令

  • 临时配置:通过命令行直接修改当前正在使用的网络地址,修改后立即可以生效。
  • 固定配置:通过配置文件来存放固定的各种网络地址,需要重启network服务或重启主机后才会生效。

1.修改网卡的IP地址、子网掩码格式

ifconfig 网络接口名称 IP地址 [ netmask 子网掩码 ]

ifconfig 网络接口名称 IP地址 [ /子网掩码长度 ]

2.禁用、激活网络接口

需要临时禁用或者重新激活指定的网络接口时,需要结合“down”、“up”开关选项。网络接口被禁用以后,将无法使用该网络接口与其他主机进行连接。

3.添加、删除到指定网段的路由记录

通过“route add”操作可以添加路由记录,结合“-net”选项指定目录网段的地址,结合“gw”选项指定下一跳路由器的IP地址。

通过“route del”操作可以删除路由记录,只要结合“-net”选项指定对应路由记录中目标网段的地址即可。

4.添加、删除默认网关记录

添加、删除默认网关记录时,与添加、删除静态路由记录的命令格式类似,但指定目标网段时只需简单实用“default”表示。无须使用“-net”选项指明网段地址。

修改网络配置文件

1.网络接口配置文件

网络接口的配置文件默认位于目录“/etc/sysconfig/network-scripts/”中,文件名格式为“ifcfg-XXX”,其中“XXX”是网络接口的名称。

1
2
3
4
5
6
7
8
9
10
11
ini复制代码[root@localhost-181117 ~]# ls /etc/sysconfig/network-scripts/ifcfg-*
/etc/sysconfig/network-scripts/ifcfg-eth0
/etc/sysconfig/network-scripts/ifcfg-lo
[root@localhost-181117 ~]
[root@localhost-181117 ~]# vim /etc/sysconfig/network-scripts/ifcfg-eth0
DEVICE=eth0
ONBOOT=yes
BOOTPROTO=static
IPADDR=192.168.231.131
NETMASK=255.255.255.0
GATEWAY=192.168.231.1

上述各配置项的含义及作用

  • DEVICE:设置网络接口的名称。
  • ONBOOT:设置网络接口是否在Linux系统启动时激活。
  • BOOTPROTO:设置网络接口的配置方式,值为“static”时表示使用静态指定的IP地址,为“DHCP”时表示通过DHCP的方式动态获取地址。
  • IPADDR:设置网络接口的IP地址。
  • NETMASK:设置网络接口的子网掩码。
  • GATEWAY:设置网络接口的默认网关地址。

2.启用、禁用网络接口设置

在RHEL系统中,当修改了网络接口的配置文件以后,若要使新的配置生效,可以重新启动network服务或者重启主机。默认情况下,重启network服务将会关闭所有的网络接口,然后在根据配置文件重新启用所有的网络接口。

如果只是想禁用、启用某一个网络接口,可分别使用两个接口控制脚本ifdown、ifup。

3.主机名称配置文件

若要修改Linux系统的主机名,可以修改配置文件/etc/sysconfig/network。在此文件中,“HOSTNAME”行用于设置主机名,而“NETWORKING”行用于设置IPv4网络的默认启用状态。

1
2
3
4
ini复制代码[root@localhost-181117 ~]# vi /etc/sysconfig/network
NETWORKING=yes
NETWORKING=yes
HOSTNAME=localhost-181117

4.域名解析配置文件

指定为本机提供DNS解析的服务器地址

/etc/resolv.conf文件中记录了本机默认使用的DNS服务器的地址信息,对该文件所做的修改将会立刻生效。Linux系统中最多可以指定3个不同的DNS服务器地址,优先使用第1个DNS服务器。

1
2
3
4
5
csharp复制代码[root@localhost-181117 ~]# vi /etc/resolv.conf
# Generated by NetworkManager
domain localdomain
search localdomain
nameserver 192.168.231.2

本地主机映射文件

/etc/hosts文件中记录着一份主机名与IP地址的映射关系表,一般用来保存经常需要访问的主机的信息。当访问一个未知的域名时,先查找该文件中是否有相应的映射记录,如果找不到在去向DNS服务器查询。

1
2
3
4
csharp复制代码[root@localhost-181117 ~]# cat /etc/hosts
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
[root@localhost-181117 ~]#

搭建DHCP服务器

DHCP的典型应用模式:负责其中分配各种网络地址参数(主要包括IP地址、子网掩码、广播地址、默认网关地址、DNS服务器地址)将网卡配置为自动获取地址,即可与DHCP服务器进行通信,完成自动配置过程。

1. 挂载光盘

1
2
3
csharp复制代码[root@localhost-181117 ~]# mount /dev/cdrom /mnt
mount: block device /dev/sr0 is write-protected, mounting read-only
[root@localhost-181117 ~]#

2.安装DHCP服务,进入到挂载点下,安装软件包

1
2
3
4
5
6
ruby复制代码[root@localhost-181117 ~]# cd /mnt/Packages/
[root@localhost-181117 Packages]# rpm -ivh dhcp-4.1.1-38.P1.el6.centos.x86_64.rpm
warning: dhcp-4.1.1-38.P1.el6.centos.x86_64.rpm: Header V3 RSA/SHA1 Signature, key ID c105b9de: NOKEY
Preparing... ########################################### [100%]
1:dhcp ########################################### [100%]
[root@localhost-181117 Packages]#

3.dhcp.conf为主配置文件,在配置文件中没有事先配置好的部分,需要我们手动配置,但是在配置文件中给出了样例配置文件的路径。

1
2
3
4
5
6
7
8
9
10
ini复制代码[root@localhost-181117 Packages]# cd /etc/dhcp/
[root@localhost-181117 dhcp]# ls
dhclient.d dhcpd6.conf dhcpd.conf
[root@localhost-181117 dhcp]# vi dhcpd.conf

#
# DHCP Server Configuration file.
# see /usr/share/doc/dhcp*/dhcpd.conf.sample
# see 'man 5 dhcpd.conf'
#

4.复制样例文件覆盖原文件。

1
2
typescript复制代码[root@localhost-181117 dhcp]# cp /usr/share/doc/dhcp*/dhcpd.conf.sample /etc/dhcp/dhcpd.conf
cp:是否覆盖"/etc/dhcp/dhcpd.conf"? y

5.配置dhcp.conf文件,确定subnet网段声明。

1
2
3
4
5
6
7
ini复制代码[root@localhost-181117 dhcp]# vi dhcpd.conf

subnet 192.168.231.0 netmask 255.255.255.0 {

range 192.168.231.128 192.168.231.254;
option routers 192.168.231.1;
}

6.将DHCP服务器的虚拟机和win7客户端的虚拟通道调节到一个模式下。

图片

7.确定host主机声明(host声明用于设置单个主机的网络属性)用过host关键字指定需要使用保留地址的客户机名称,并使用“hardware ethernet”参数指定该主机的MAC地址,使用“fixed-address”参数指定保留给该主机的IP地址。启动服务或者监听下端口是否正常。(需要关闭、重启dhcp服务时,只要将操作命令中的“restart”改为“start”或者“stop”即可。如果dhcp服务启动失败,可以检查日志文件/var/log/messages末尾的错误提示信息,并根据提示进行排错。)

1
2
3
4
5
6
7
8
9
10
perl复制代码[root@localhost-181117 dhcp]# vi dhcpd.conf
host prtsvr {
hardware ethernet 00:0C:29:75:91:EC;
fixed-address 192.168.231.132;
}
:wq
[root@localhost-181117 dhcp]# service dhcpd restart
关闭 dhcpd: [确定]
正在启动 dhcpd: [确定]
[root@localhost-181117 dhcp]#

8.测试并验证windows客户端是否获取到IP地址。

图片

/etc/dhcp/dhcp.conf文件的配置构成

在主配置文件dhcp.conf中,可以使用声明、参数、选项这三种类型的配置。

  • 声明:用来描述dhcp服务器中对网络布局的划分,是网络设置的逻辑范围。常见的声明是subnet、host,其中subnet声明用来约束一个网段,host声明用来约束一台特定的主机。
  • 参数:由配置关键字和对应的值组成,总是以分好“:”结束,一般位于指定的声明范围之内,用来设置所在范围的运行特性。
  • 选项:由“option”引导,后面跟具体的配置关键字和对应的值,也是以分号“:”结束,用于指定分配给客户机的各种地址参数。

确定dhcp服务的全局配置

为了使配置文件的结构更加清晰,全局配置通常会放在配置文件dhcp.conf的开头部分,可以是配置参数,也可以是配置选项。

  • ddns-update-style,动态DNS更新模式。用来设置与DHCP服务相关联的DNS数据动态更新模式。在实际的DHCP应用中很少用到该参数,将值设为“none”。
  • default-lease-time,默认租约时间。单位为秒,表示客户端可以从DHCP服务器租用某个IP地址的默认时间。
  • max-lease-time,最大租约时间,单位为秒,表示允许DHCP客户端请求的最大租约时间,当客户端未请求明确的租约时间时,服务器将采用默认租约时间。
  • option domain-name,默认搜索区域。为客户机指定解析主机名时的默认搜索域,改配置选项将体现在客户机的/etc/resolv.conf配置文件中。
  • option domain-name-servers,DNS服务器地址,为客户机指定解析域名时使用的DNS服务器地址,该配置选项同样将体现在客户机的/etc/resolv.conf配置文件中。需设置多个DNS服务器地址时,应以逗号进行分隔。

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

我们下期再见!

本文转载自: 掘金

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

JVM参数Xms Xmx PermSize MaxPermS

发表于 2021-06-06

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

常见的JVM参数配置机器含义

1
2
3
4
5
6
java复制代码-vmargs -Xms128M -Xmx512M -XX:PermSize=64M -XX:MaxPermSize=128M
-vmargs 说明后面是VM的参数,所以后面的其实都是JVM的参数了
-Xms128m JVM初始分配的堆内存
-Xmx512m JVM最大允许分配的堆内存,按需分配
-XX:PermSize=64M JVM初始分配的非堆内存
-XX:MaxPermSize=128M JVM最大允许分配的非堆内存,按需分配

我们首先了解一下JVM内存管理的机制,然后再解释每个参数代表的含义。

堆(Heap)和非堆(Non-heap)内存

按照官方的说法:“Java 虚拟机具有一个堆,堆是运行时数据区域,所有类实例和数组的内存均从此处分配。堆是在 Java 虚拟机启动时创建的。”“在JVM中堆之外的内存称为非堆内存(Non-heap memory)”。
可以看出JVM主要管理两种类型的内存:堆和非堆。简单来说堆就是Java代码可及的内存,是留给开发人员使用的;非堆就是JVM留给自己用的,所以方法区、JVM内部处理或优化所需的内存(如JIT编译后的代码缓存)、每个类结构(如运行时常数池、字段和方法数据)以及方法和构造方法的代码都在非堆内存中。

堆内存分配

JVM初始分配的堆内存由-Xms指定,默认是物理内存的1/64;JVM最大分配的堆内存由-Xmx指定,默认是物理内存的1/4。默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;
空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制。因此服务器一般设置-Xms、-Xmx 相等以避免在每次GC 后调整堆的大小。
说明:如果-Xmx 不指定或者指定偏小,应用可能会导致java.lang.OutOfMemory错误,此错误来自JVM,不是Throwable的,无法用try…catch捕捉。

非堆内存分配

JVM使用-XX:PermSize设置非堆内存初始值,默认是物理内存的1/64;由XX:MaxPermSize设置最大非堆内存的大小,默认是物理内存的1/4。(还有一说:MaxPermSize缺省值和-server -client选项相关,-server选项下默认MaxPermSize为64m,-client选项下默认MaxPermSize为32m。这个我没有实验。)
上面错误信息中的PermGen space的全称是Permanent Generation space,是指内存的永久保存区域。还没有弄明白PermGen space是属于非堆内存,还是就是非堆内存,但至少是属于了。
XX:MaxPermSize设置过小会导致java.lang.OutOfMemoryError: PermGen space 就是内存益出。说说为什么会内存益出:

  • 这一部分内存用于存放Class和Meta的信息,Class在被 Load的时候被放入PermGen space区域,它和存放Instance的Heap区域不同。
  • GC(Garbage Collection)不会在主程序运行期对PermGen space进行清理,所以如果你的APP会LOAD很多CLASS 的话,就很可能出现PermGen space错误。

这种错误常见在web服务器对JSP进行pre compile的时候。

JVM内存限制(最大值)

首先JVM内存限制于实际的最大物理内存,假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。简单的说就32位处理器虽然可控内存空间有4GB,但是具体的操作系统会给一个限制,
这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统下为2G-3G),而64bit以上的处理器就不会有限制了。

为什么有的机器我将-Xmx和-XX:MaxPermSize都设置为512M之后Eclipse可以启动,而有些机器无法启动?
通过上面对JVM内存管理的介绍我们已经了解到JVM内存包含两种:堆内存和非堆内存,另外JVM最大内存首先取决于实际的物理内存和操作系统。所以说设置VM参数导致程序无法启动主要有以下几种原因:

  • 参数中-Xms的值大于-Xmx,或者-XX:PermSize的值大于-XX:MaxPermSize;
  • -Xmx的值和-XX:MaxPermSize的总和超过了JVM内存的最大限制,比如当前操作系统最大内存限制,或者实际的物理内存等等。说到实际物理内存这里需要说明一点的是,如果你的内存是1024MB,但实际系统中用到的并不可能是1024MB,因为有一部分被硬件占用了。

如果你有一个双核的CPU,也许可以尝试这个参数:

-XX:+UseParallelGC

让GC可以更快的执行。(只是JDK 5里对GC新增加的参数)

本文转载自: 掘金

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

Redis的持久化机制—RDB与AOF|Java 开发实战

发表于 2021-06-06

Redis之所以那么快的一个重要原因就是Redis是一个高性能的key-value内存数据库,所有的数据都保存在内存中,为了保证在宕机或者重启之后内存数据不丢失,Redis提供了持久化机制,将内存中的数据库状态保存到磁盘,并且在启动的时候可以快速进行恢复。

RDB持久化机制

RDB是Redis提供的一个保存内存数据库状态快照的一个功能,防止内存中的数据丢失。RDB持久化功能生成的RDB文件是一个经过压缩的二进制文件,可以通过这个文件在Redis启动的时候恢复Redis的内存状态到保存RDB文件时。

因为RDB文件中保存的是二进制文件,并且每隔一段时间就保存一次,相对而言,RDB文件的体积和执行速度较快,Redis在重启时可以通过RDB文件快速恢复内存中的数据,不过RDB文件的缺点就是数据可能不全。RDB快照是某个时间点的一次全量数据备份

RDB文件的写入

SAVE或者BGSAVE命令均可以生成RDB文件

  • SAVE创建RDB文件

SAVE命令会阻塞Redis的服务器进程,直到创建RDB文件为止,在服务器进程阻塞期间,服务器不能处理任何请求

  • BGSAVE创建RDB文件

BGSAVE命令会fork一个子进程,然后通过子进程来生成RDB文件,Redis主进程可以继续接收客户端请求。

BGSAVE的主进程阻塞时间只有fork阶段的那一下,相对于save,阻塞时间很短

+ **COW机制**


因为在子进程创建RDB文件的过程中,会将文件中的所有数据写到文件,主进程在继续处理客户端请求时,如果对内存中的数据更新,那么子进程和父进程是共享内存数据,就会导致有数据的问题。


所以,COW(copy on write)机制,指的是修改共享资源时,将共享资源copy一份,加锁后修改,再将原容器的引用指向新的容器,这样每个进程之间操作的内存就不是同一个,不会产生数据问题。


但是缺点就是,COW会将内存copy一份副本,这时内存中的数据就会有两份一样的内存数据,相当于内存减半,需要有足够的内存才可以实现COW机制。
+ **Redis的COW机制**


    1. Redis创建子进程以后,根本不进行数据的copy,主进程与子线程是共享数据的。主进程继续对外提供读写服务,子进程执行写入RDB服务
    2. 虽然不copy数据,但是kernel会把主进程中的所有内存页的权限都设为read-only,主进程和子进程访问数据的指针都指向同一内存地址
    3. 主进程发生写操作时,因为权限已经设置为read-only了,所以会触发页异常中断(page-fault)。**在中断处理中,需要被写入的内存页会复制一份,复制出来的旧数据交给子进程使用,然后主进程对原有的数据页进行写入**。
> 因为数据都是通过页为单位进行存储的,所以可以再主进程操作数据时,对需要操作的那一页数据进行单独的复制,在保存数据的正确性的前提下,还可以减少内存的浪费


Redis的COW机制也是经过优化,防止内存的浪费以及防止内存数据较大,无法有足够的内存进行copy。
  • 自动保存策略

Redis的配置文件中可以配置Redis的RDB文件的自动生成策略:

1
2
3
复制代码save 900 1
save 300 10
save 60 10000

说明:

+ 当满足900s内有1次修改时,就会执行`BGSAVE`
+ 当满足300s内有10次修改时,就会执行`BGSAVE`
+ 当满足60s内有10000次修改时,就会执行`BGSAVE`

AOF持久化机制

AOF持久化机制和RDB持久化机制不同,RDB是通过定时保存内存中的快照到磁盘中,而AOF文件中保存的是每一次Redis执行的写命令,通过记录写命令来保存数据库的状态。

redis-AOF持久化

AOF文件写入
  • AOF_BUF缓冲区

现代操作系统中,当调用write函数将输入写入到文件时,操作系统通常都会将写入的数据暂时保存到一个内存缓冲区中,等到缓冲区的空间被填满或者强制调用函数刷新缓冲区时,操作系统才会将缓冲区中的数据写入到磁盘中,因为每次写入磁盘的操作是较慢的。

Redis服务器在执行完一条命令之后,会将命令追加到aof_buf的缓冲区的末尾,然后通过配置判断当前是否需要将aof_buf缓冲区中的数据刷新到磁盘(即AOF文件中),可以通过配置来保证是每次执行命令都需要将该命令刷新到磁盘还是可以多次执行之后一起刷新到磁盘,可以在数据的安全性和高效率之间在不同的场景选择合适的方案。

  • AOF持久化机制

aof_buf的刷新行为是通过配置appendfsync控制,一共有3中选择可以进行配置

+ **always**


服务器在每次执行完一条命令之后,都需要将aof\_buf缓冲区中的数据刷新到AOF文件中,所以always的效率是最慢的一个,不过安全性是最高的,最多只会丢失一个命令数据
+ **everysec**


服务器在每隔一秒就将aof\_buf缓冲区中数据刷新到AOF文件,即将文件缓冲区的数据也同步到文件中
+ **no**


在每次执行写命令时,不会主动将aof\_buf缓冲区中的数据刷新到AOF文件中,什么时候刷新是操作系统决定。其中`always`是最安全的一种策略,不过频繁刷新到磁盘会影响redis的性能,`everysec`是一种相对安全以及保证效率的策略,最多丢失1s的数据,大部分的场景应该都是可以接收,`no`在不要求数据的可靠性的情况下可以使用。
AOF文件的加载和还原

在AOF文件中,保存的都是一条条的写命令,所以再重新还原Redis的数据库状态时,只需要重新执行AOF文件中的所有命令即可。

不过Redis在执行AOF文件中的命令之前,会先创建一个伪客户端,因为Redis的命令只能在客户端上下文中执行,每次读取一条命令循环执行,直到执行完毕。

Redis在重新启动恢复内存数据时,会优先选择使用AOF文件来恢复数据,因为AOF文件的数据相对较全。不过AOF文件中记录的是所有的写命令,那么时间较长的话,AOF文件的体积是非常大的,在重启恢复数据库状态的时候,就会比较慢,导致Redis不能快速提供服务。

AOF重写机制

因为AOF文件体积较大的问题,影响到了Redis服务器的数据库状态的恢复以及磁盘文件的存放,Redis提供了重写机制来解决AOF文件较大的问题。

AOF重写在Redis中的实现和RDB有些类似,因为重写需要大量的操作,可能会阻塞主进程处理请求,所以采用了后台重写的方式。

简单讲,AOF重写就是将当前数据库中的键值重新写入到一个新的AOF文件中,然后覆盖之前的AOF文件。

  • AOF重写步骤
+ 创建子进程进行AOF的重写,主进行继续处理命令请求
+ 子进程从数据库中读取每个键现在的值,然后将多个命令合并为一个命令,使用一条命令完成之前多条命令的操作,并且由部分数据已经过期或者已经删除,AOF文件中已经不需要记录这部分数据,同样的,命令也不需要再记录到AOF文件中。



> 例如:多条`SADD`命令可以合并为一条`SADD key1 value1 key2 value2 ...`命令,AOF文件中只需要记录一条命令
+ 在AOF重写过程中,如果有需要对当前的数据库数据进行变更的命令,那么Redis就会通过一个**AOF重写缓冲区**来记录当前的操作指令



> 所以在AOF重写期间,如果客户端执行一条写命令,那么首先会执行这个命令,然后将这条命令写入到**AOF缓冲区**,主要是为了将命令同步到现有的AOF文件,然后将命令写入到**AOF重写缓存区**,向新的AOF文件同步数据
+ 子进程完成AOF重写之后,会通知主进程重写完成,主进程接收到完成的信号,会将AOF重写缓冲区的数据写入到新的AOF文件,这个时候新的AOF文件和数据库的状态保持一致。
+ 对新的AOF文件重名名,原子的覆盖当前的AOF文件,完成新旧AOF文件的替换,后续的命令同步会写入到新的AOF文件

执行AOF重写的命令为:BGREWRITEAOF

  • Redis 4.0的AOF持久化机制

AOF不再记录全部的写命令,而是增量的写命令,当RDB开始持久化时,这个时候AOF会开始增量的记录当前写操作的写命令,当RDB持久化完成之后,就不会再记录写命令。只记录RDB持久化开始到持久化结束的这一段时间的增量日志,这部分日志相对会较小。

在Redis恢复的时候,也可以快速的恢复内存中的数据,保证效率和数据的安全性。

Redis4.0的混合持久化机制

Redis 3.x中,持久化机制是只能选择AOF或者RDB中的一种,Redis 4.0在RDB持久化和AOF持久化中并没有做选择,而是全都要!

开启混合持久化

4.0版本的混合持久化默认关闭的,通过aof-use-rdb-preamble配置参数控制,yes则表示开启,no表示禁用,默认是禁用的,可通过config set修改。

混合持久化过程

混合持久化同样也是通过bgrewriteaof完成的,不同的是当开启混合持久化时,fork出的子进程先将共享的内存副本全量的以RDB方式写入aof文件,然后在将重写缓冲区的增量命令以AOF方式写入到文件,写入完成后通知主进程更新统计信息,并将新的含有RDB格式和AOF格式的AOF文件替换旧的的AOF文件。简单的说:新的AOF文件前半段是RDB格式的全量数据后半段是AOF格式的增量数据,如下图:

混合持久化过程

两种持久化机制对比

img

可以看到两种持久化机制各有优缺点:

  • 仅使用RDB快照方式恢复数据,由于快照时间粒度较大,时回丢失大量数据。
  • 仅使用AOF重放方式恢复数据,日志性能相对 rdb 来说要慢。在 Redis 实例很大的情况下,启动需要花费很长的时间。
混合持久化机制

将 rdb 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是自持久化开始到持久化结束的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小。相当于:

  • 大量数据使用粗粒度(时间上)的rdb快照方式,性能高,恢复时间快。
  • 增量数据使用细粒度(时间上)的AOF日志方式,尽量保证数据的不丢失。

在 Redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。

当然,解决了部分问题,同时Redis4.0的混合持久化也有对应的缺点:兼容性较差,aof文件中是混合存储RDB和AOF格式,在低版本无法识别这种日志格式的文件

小结

  • AOF持久化机制通过保存写命令来记录数据库的状态
  • RDB持久化机制通过定期保存数据库的快照
  • AOF持久化的策略有不同的选择,默认是每秒保存一次,当保存写命令的时候不是直接保存到AOF文件,而是先保存到AOF缓冲区中,然后再将AOF缓冲区中的数据刷到磁盘文件(AOF文件)
  • Redis提供了AOF重写机制来解决AOF文件较大的问题,在AOF重写时,会有AOF重写缓冲区来保证重写期间的数据一致性
  • AOF重写的实质就是读取当前数据库中键值对来实现命令的合并
  • Redis 4.0的混合持久化机制,更好的保证了Redis恢复内存数据库的效率,在数据安全性和加载的效率性保证了平衡性。

微信公众号指尖上的代码,欢迎关注~ 一起学习 一起进步

原创不易, 点个赞再走呗~ 欢迎关注,给你带来更精彩的文章!

你的点赞和关注是写文章最大的动力~

本文转载自: 掘金

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

聊聊 Redis 过期键的删除策略 前言 Redis实际使用

发表于 2021-06-06

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

文章首发于公众号:蘑菇睡不着,欢迎来看看

前言

Redis 中都是键值对的存储形式,键都是字符串类型的,而值有很多种类型,如 string、list、hash、set、sorted set等类型。当设置键值对时我们还应该为其设置过期时间,通过 expire 以及 pexpire 命令;还可以通过 setnx 命令设置。那么,当设置过期时间之后,到底是怎么将过期的键值对删除的那?想知道答案的话,我们就一起看看 Redis 的过期键删除策略。

在说删除策略之前有个点带大家先了解下,那就是如果确定一个键是否过期,这里我总结了下:

  1. 检查这个键是否在过期字典中,如果存在,那么取出这个键的过期时间。(过期字典存储的是每个键的过期时间,字典中 key 是 键, value 是 long 类型的过期时间)
  2. 拿到过期时间之后,和当前 UNIX 时间戳比较,如果大于,则键过期。

以上就是判断一个键是否过期的方法。接下来说说当键过期了怎么去删除。

目前来说有三种删除策略:

  • 定时删除:在设置键的过期时间时,创建一个定时器,当到达键过期时间时通过定时器去删除键。
  • 惰性删除:惰性删除并不是当到达过期时间时去删除,而是每次获取键时,会判断是否过期,如果过期则删除,并返回空;没过期,就返回键值。
  • 定期删除:每隔一段时间,就对数据库中的键进行检查,如果过期则删除。至于要删除多少什么时候删除,则是通过具体程序决定。

下面来详细介绍每一种删除策略。

定时删除

定时删除策略

优点是:对内存友好。因为通过定时器,当一个键到达过期时间时就会立马被删除,直接就释放了内存。

缺点是:对 CPU 不友好。因为如果过期键比较多,那么删除这些过期键会占用相当一部分CPU时间,如果CPU时间非常紧张的话,还将CPU时间用在删除和当前任务无关的过期键上,会对服务器的响应时间以及吞吐量造成影响。

因此,通过 定时删除 策略来时间过期键的删除不太现实。

惰性删除

惰性删除策略优点:对 CPU 时间友好。程序只会在取出键时才会判断是否删除,并且只作用到当前键上,其他过期键不会花费 CPU 时间去处理。

惰性删除策略缺点:对内存不友好。因为只有键被使用时才会去检查是否删除,如果有大量的键一直不被使用,那么这些键就算过期了也不会被删除,会一直占用着内存。这种可以理解为是一种内存泄漏——大量无用的数据一直占用着内存,并且不会被删除。

定期删除

相比较定时删除对CPU的不友好,惰性删除的对内存不友好。定期删除采用了一种折中的方式:

  • 定期删除策略每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。
  • 并且,通过定期删除过期键,有效的减少了过期键带来的内存浪费。

但删除的时长和频率比较难定义,因为:

  • 如果频率太高或者时长太长,那么会占用大量的CPU时长。
  • 如果过短又会出现内存浪费的情况。

因此。如果采用定期删除策略的话需要通过具体的业务场景来定义时长和频率。

Redis实际使用的是惰性删除+定期删除的策略。

通过这两种方式可以很好的利用CPU时间以及避免内存浪费的情况。接下来讲讲惰性删除以及定期删除的实现。

惰性删除策略的实现

惰性删除策略由 expireIfNeeded 函数实现,所有读写数据库的 Redis 命令在执行之前都会调用 exipreIfNeeded 函数对输入键进行检查。

  • 如果键过期,会将键删除并返回空。
  • 如果键没有过期,则不做操作。

定期删除策略的实现

定期删除策略由 activeExpireCucle 函数实现,被调用时,它在规定的时间内,分多次遍历服务器中的各个数据库,从数据库的 expires 字典中随机检查一部分键的过期时间,并删除其中的过期键。

  • 函数每次运行时,都是从一定数量的数据库键中随机取一定数量的键进行检查,并删除其中的过期键。
  • 有一个全局变量 current_db 会记录当前 activeExpireCycle 函数检查的进度,并且下一次 函数执行时,接着上一次的进度进行处理。如,当前 activeExpireCycle 函数执行到了 10, 讲 current_db = 10;然后下一次函数执行时,从 current_db 取到 10 继续执行。
  • 当所有的数据库键都被检查完时, current_db = 0。

AOF、RDB 和复制功能对过期键的处理

生成 RDB 文件

在执行 SAVE 命令或 BGSAVE 命令创建一个新的 RDB 文件时,程序会对数据库中的键进行检查,已过期的键不会被保存到新的 RDB 文件中。

如:redis中包含 r1、r2、r3 三个键,并且 r1 已经过期,那么程序只会讲 r2 和 r3 保存到 RDB 文件中。

因此,过期键不会对新的 RDB 文件造成影响。

载入 RDB 文件

在启动 redis 服务器时,如果服务器开启了 RDB 功能,那么服务器将对 RDB 文件进行载入;

  • 如果服务器以主服务器模式运行,那么在载入 RDB 文件时,过期的键会被过滤掉,不会被载入到redis数据库中。
  • 如果以从服务器模式运行,那么无论键是否过期都会被载入到数据库中。但,因为主从服务器在进行数据同步时,从服务器就会被清空,所以,一般来说,过期键对从服务器也不会造成影响。

AOF 文件写入

当服务器开启 AOF 的运行模式时,如果某个键过期了,但没有被惰性或定期删除,那么 AOF 不会理会。如果被惰性或定期删除了, AOF 会在文件末尾追加一条 DEL 命令,来显示地记录该键已被删除。

AOF 重写

当 AOF 重写时,过期的键不会被载入到 redis 数据库中。

复制

当服务器在 复制 模式下时,从服务器的过期键删除动作都是由主服务器来进行的。

  • 主服务器在删除一个过期键之后,会显示地向所有从服务器发送一个 DEL 命令,告知从服务器删除这个过期键。
  • 从服务器在执行客户端发送的读命令时,即使碰到过期的键也不会删除,而是继续的正常操作。
  • 从服务器只有在接到主服务器发来的 DEL 命令之后,才会删除过期键。

**评论区有小伙伴提问:既然从服务器不会主动去删除过期键,那么如果查询从服务器的过期键怎么办?

这个问题问的很好,我写文章的时候也确实落下了这一点,接下来我就引用 Redis 官网上的一段话来解释这个问题。**

redis复制.jpg

简单翻译一下:

1. 从服务器不会去过期key,它会等待 master 去过期key,当 master 过期 key (或由于 LRU 算法驱逐),它会生成一个 DEL 命令发送给所有的从服务器。

2. 但是,由于这是 master 驱动的 key 过期行为,master 无法及时的提供 DEL 命令,导致一些从服务器有时内存中存在逻辑上已经过期的 key,为了处理这个问题,slave 使用它的逻辑时钟以报告只有在不违反数据集的一致性的读取操作(从主机的新命令到达)中才存在 key(这块有点拗口,大概意思就是通过逻辑时钟记录一下本该过期的或等待 master DEL 命令 的key)。通过这种方式,从节点避免了返回一个已经过期的键。在实际经验中,一个通过从节点缓存去扩容的HTML页面缓存将可以避免没有按时过期的问题。

最后

Redis 的过期键删除策略是 惰性删除 + 定期删除,这也既可以合理的控制 CPU 使用 还可以 减少内存的浪费。

关于更多 Java 知识以及刷题分享,可以来公众号 蘑菇睡不着 看看,大家一起学习。

你越主动就会越主动,我们下期见~

本文转载自: 掘金

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

10篇带你手摸手封装gin框架(1)-开篇与目录设计 前言

发表于 2021-06-06

前言

这是我参与更文挑战的第1天,
大家好,我是作曲家种太阳

gin和koa一样,都是小而美的框架,自由端很高,缺点是不能拿来即用,需要做一些二次封装,才能运用到真实的业务场景去.

这个系列文章我会认真的带着大家一步一步的封装好高质量的gin框架,并且每个文章最后都测试环节,检验效果.

类似的gin,koa,falsk的框架有很多,其实是都是一通百通,架构设计和规范还有后端领域相关生态都是类似的,相信你认真学完本专栏的文章,会有一举反三的能力.

遇到关于本系列文章的各种问题,都可以留言,我看到后会及时回答讨论~

  • 我相信最好的学习方式就是输出~
  • 下是整个专栏的文章链接:
  • 文章已经写好,从6.6号开始保证每日一更~

10篇带你手摸手封装gin框架(1)-开篇与架构设计

10篇带你手摸手封装gin框架(2)-Viper配置管理

10篇带你手摸手封装gin框架(3)-Zap日志管理

10篇带你手摸手封装gin框架(4)-Validator字段校验

10篇带你手摸手封装gin框架(5)-responese统一封装

10篇带你手摸手封装gin框架(6)-gorm使用和用户列表接口开发

10篇带你手摸手封装gin框架(7)- redis接入与图形验证码

10篇带你手摸手封装gin框架(8)- jwt验证与登录接口完善

10篇带你手摸手封装gin框架(9)-常用中间件开发

10篇带你手摸手封装gin框架(10)- Minio文件管理与上传头像接口开发

不定期随缘更新:

10篇带你手摸手封装gin框架- Docker部署gin项目

10篇带你手摸手封装gin框架- websocket接入

说明的是,本系列侧重于对gin框架二次封装,接口开发是为了顺带串通整个流程,所以接口实现并一定不规范

项目源码地址:github.com/pyh996/gin_…

建议参考文章和源码一起食用更佳

你所需要具备的能力:

  1. debug能力
  2. 会go语言基础,基本软件安装
  3. 了解mysql与sql语句

1.技术选型

非标准库:

所用技术 概要
gin go中最流行的web框架
zap 日志管理器
viper 配置管理器
gorm go中最流行的orm框架
mysql 数据库
jwt 身份认证
minio 静态资源服务器
redis 数据库
validator 字段校验器
color 终端彩色显示

2.目录结构设计

image.png
从上到下目录结构为:

文件 概要
config 配置文件对应的结构体定义
controller 业务层
dao 操作数据库,给业务controller提供数据
forms 字段验证的struct
global 定义全局变量
initialize 服务初始化
logs 日志存储
middlewares 中间件
models 数据库字段定义
Response 统一封装response
static 资源文件夹
router 路由
setting-dev.yaml 配置文件
main.go 服务启动文件

到这里相信你对框架有了基本的了解,每个文件都在自己的事情,一个清晰的目录更有利于程序的开发

请手动新建下这几个文件,接下来我们开始实操环节

3.编写第一个gin的程序

(1).安装gin

1
shell复制代码 go get -u github.com/gin-gonic/gin

(2).在main.go中编写

1
2
3
4
5
6
7
8
9
10
11
go复制代码package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run() // 监听并在 0.0.0.0:8080 上启动服务
}

最后–测试环节

我们刚才写了一个gin的最简单的程序,打开

http://127.0.0.1:8080/ping

返回结果是
image.png

返回结果一致说明,您做对了,接着学习下一篇吧~

如果这系列的文章对你有有用,请点赞和留言吧~

本文转载自: 掘金

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

MySQL索引的测试 (千万级数据) 以及特点总结|周末学习

发表于 2021-06-06

本文已参与 周末学习计划,点击查看详情

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

通过存储过程插入百万,千万数据,来对比使用索引和没使用索引的区别(普通索引)

创建表

可以看到这里创建的索引类型都是 BTREE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sql复制代码-- ----------------------------
-- Table structure for mall
-- ----------------------------
DROP TABLE IF EXISTS `mall`;
CREATE TABLE `mall` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`categoryId` int(11) NOT NULL,
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`price` decimal(10, 2) NOT NULL,
`type` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`desc` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`img` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

image-20210606182510055

百万级数据

在这里我们使用存储过程直接往表里插入一百万条数据

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
sql复制代码-- ------------ MYSQL8.0.17 插入百万数据
-- 获取数据库版本
SELECT VERSION();

-- ROUND( ) 四舍五入 第二个参数表示保留两位小数 ; RAND() 返回 0-1的小数
SELECT ROUND(RAND()*1000,2) as 'test_name';

-- ---------------------------------创建生成随机字符串函数【START】------------------------------------------------------------------
-- 修改分隔符 避免被MySQL 解析
DELIMITER $$
-- 如果存在就删除
DROP FUNCTION IF EXISTS rand_str;
-- 创建函数名 rand_str 参数为返回的长度
create FUNCTION rand_str(strlen SMALLINT )
-- 返回值
RETURNS VARCHAR(255)

BEGIN
-- 声明的字符串
DECLARE randStr VARCHAR(255) DEFAULT 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890';
-- 声明 i 循环变量
DECLARE i SMALLINT DEFAULT 0;
-- 声明返回变量
DECLARE resultStr VARCHAR(255) DEFAULT '';
WHILE i<strlen DO
SET resultStr=CONCAT(SUBSTR(randStr,FLOOR(RAND()*LENGTH(randStr))+1,1),resultStr);
SET i=i+1;
END WHILE;
RETURN resultStr;
END $$
DELIMITER ;

-- ------------------------------------创建生成随机字符串函数【END】---------------------------------------------------------------


-- 创建函数报错,可参考 # https://www.cnblogs.com/kerrycode/p/7641835.html
show variables like 'log_bin';
show variables like '%log_bin_trust_function_creators%';
set global log_bin_trust_function_creators=1;

-- 调用随机字符串函数
select rand_str(FLOOR(RAND()*20));

-- 创建存储过程 插入1 000 000 数据
DROP PROCEDURE IF EXISTS `add_mall`;

DELIMITER $$
CREATE PROCEDURE `add_mall` ( IN n INT )
BEGIN
DECLARE i INT UNSIGNED DEFAULT 0;
WHILE
i < n DO
INSERT INTO mall ( categoryId, `name`, price, type, `desc`, `img` )
VALUES
( 1, 'test', ROUND( RAND()* 1000, 2 ), rand_str ( FLOOR( RAND()* 20 )), 'test', 'test.jpg' ),
( 1, 'test', ROUND( RAND()* 1000, 2 ), rand_str ( FLOOR( RAND()* 20 )), 'test', 'test.jpg' ),
( 1, 'test', ROUND( RAND()* 1000, 2 ), rand_str ( FLOOR( RAND()* 20 )), 'test', 'test.jpg' ),
( 1, 'test', ROUND( RAND()* 1000, 2 ), rand_str ( FLOOR( RAND()* 20 )), 'test', 'test.jpg' ),
( 1, 'test', ROUND( RAND()* 1000, 2 ), rand_str ( FLOOR( RAND()* 20 )), 'test', 'test.jpg' ),
( 1, 'test', ROUND( RAND()* 1000, 2 ), rand_str ( FLOOR( RAND()* 20 )), 'test', 'test.jpg' ),
( 1, 'test', ROUND( RAND()* 1000, 2 ), rand_str ( FLOOR( RAND()* 20 )), 'test', 'test.jpg' ),
( 1, 'test', ROUND( RAND()* 1000, 2 ), rand_str ( FLOOR( RAND()* 20 )), 'test', 'test.jpg' ),
( 1, 'test', ROUND( RAND()* 1000, 2 ), rand_str ( FLOOR( RAND()* 20 )), 'test', 'test.jpg' ),
( 1, 'test', ROUND( RAND()* 1000, 2 ), rand_str ( FLOOR( RAND()* 20 )), 'test', 'test.jpg' );
SET i = i + 1;
END WHILE;
END $$
DELIMITER;

-- 调用存储过程 100w 829.876s
CALL add_mall(100000);

-- 如果插入数据报错,可能需要调整该值大小
show VARIABLES LIKE '%max_allowd_packet%';

mysql8插入百万数据

索引

先看看表里现在有多少条数据

20200822215700

不使用索引

1
2
sql复制代码-- 查询时不使用缓存
SELECT SQL_NO_CACHE * FROM mall WHERE type ='book';

20200822220030

使用索引

1
2
3
4
5
6
sql复制代码-- 添加索引
ALTER TABLE mall ADD INDEX idx_book(type);
-- 删除索引
DROP INDEX idx_book ON mall;

SELECT SQL_NO_CACHE * FROM mall WHERE type ='book';

可以看到在使用索引之后 这个查询简直是飞快,直接变成 1ms ,对比之前 656ms 的速度 👀

20200822222619

千万级数据

想要更快地插入可以修改引擎为MyISAM,使用jdbc等去批量插入,比如一次插入 5000 甚至更多就可以了。 在使用 innodb 时,可以将 autocommit 关闭,插入完数据再去建立索引(后知后觉🙃)。
下图是改用 MYISAM 后插入 100万 数据使用的时间。

20200823104800

1
2
sql复制代码-- 调用上面的存储过程再插入900w条数据。  这里用了两个多小时 。。  
CALL add_mall(900000);

通过SELECT count(*) FROM mall;看到现在表里有1200万条数据

20200823121400

先简单介绍下 MySQL8 新特性的隐藏索引,一般创建索引比较耗时的(在数据量大的情况下),现在有了这个隐藏索引,我们测试起来就更方便了,实际应用中还可以避免误删索引。

1
2
3
4
5
6
sql复制代码-- mysql8新特性之隐藏索引
alter TABLE mall ALTER INDEX idx_book invisible;
-- 显示索引
alter TABLE mall ALTER INDEX idx_book visible;
-- 简单测试SQL
SELECT SQL_NO_CACHE name,type,price,`desc`,img FROM mall WHERE type = 'book'

接下来我们试试这个MYISAM引擎下的查询耗时情况:

MYISAM

隐藏索引:

20200823123219

显示索引:

20200823123352

🛫

起飞!✔

Innodb下:

隐藏索引:

20200823135504

显示索引:

20200823135059

================ 简单测试结束 😄===================

可以看到使用索引和不使用索引的速度区别是非常大的!

索引的类型

  • 主键索引
  • 普通索引
  • 唯一索引
  • 组合索引
  • 全文索引
  • 空间索引

可以发现索引的类型是很多的,而且和这个存储引擎有关

下面介绍几个常见的存储引擎的索引特点😄

InnoDB 存储引擎的索引特点

Index Class Index Type Stores NULL VALUES Permits Multiple NULL Values IS NULL Scan Type IS NOT NULL Scan Type
Primary key BTREE No No N/A N/A
Unique BTREE Yes Yes Index Index
Key BTREE Yes Yes Index Index
FULLTEXT N/A Yes Yes Table Table

MyISAM 存储引擎的索引特点

Index Class Index Type Stores NULL VALUES Permits Multiple NULL Values IS NULL Scan Type IS NOT NULL Scan Type
Primary key BTREE No No N/A N/A
Unique BTREE Yes Yes Index Index
Key BTREE Yes Yes Index Index
FULLTEXT N/A Yes Yes Table Table
SPATIAL N/A No No N/A N/A

Memory 存储引擎的索引特点

Index Class Index Type Stores NULL VALUES Permits Multiple NULL Values IS NULL Scan Type IS NOT NULL Scan Type
Primary key BTREE No No N/A N/A
Unique BTREE Yes Yes Index Index
Key BTREE Yes Yes Index Index
Primary key HASH No No N/A N/A
Unique HASH Yes Yes Index Index
Key HASH Yes Yes Index Index

欢迎关注,交个朋友呀!! ( •̀ ω •́ )y

嘿嘿,我是4ye 咱们下期…… 很快再见!😄

如果你觉得本篇文章对你有所帮助的话,那拜托再点点赞支持一下呀😝

让我们开始这一场意外的相遇吧!~

欢迎留言!谢谢支持!ヾ(≧▽≦*)o 冲冲冲!!

本文转载自: 掘金

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

Java 函数式编程

发表于 2021-06-06

函数式编程

什么是函数式编程?
  • 一种编程范式
  • 函数作为第一对象
  • 注重描述而不是执行步骤
  • 关心函数之间的关系
  • 不可变
Lambda 表达式

以下的代码展示了一个普通的函数和一个 Lambda 表达式的不同之处

  • 首先是一个 Lambda 表达式的接口 XXFunction ,表达了这个是一个 Lambda 表达式
  • 然后使用变量 fn 来表示这个函数的对象
  • 紧接着在等号后面使用 () 表达了这个函数的参数
  • 使用 -> {} 来表达这个函数到底是做什么
1
2
3
4
5
6
7
8
9
java复制代码// 普通函数
public static void fn(T param1,R param2){
// ······
}

// Lambda
XXFunction fn = (T param1,R param2) -> {
// ······
}
Lambda 表达式语法糖

如果每一个 Lambda 表达式都需要完整的写出以上4个部分,未免比较冗余。

因此 JDK 提供了3个语法糖来简化 Lambda 表达式,以下四种写法展示了 Lambda 的三种语法糖

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码// 完整表达式 =>
public Function<Integer, Integer> function = (Integer a) -> {
return a * a;
};
// 单行可省略大括号 =>
public Function<Integer, Integer> function1 = (Integer a) -> (a * a);

// 参数类型可推导 =>
public Function<Integer, Integer> function2 = a -> {
return a * a;
};
// 单参数可省略小括号
public Supplier<Integer> function3 = () -> 1;
自定义函数式接口

以下的代码展示了怎么自定义一个函数式接口,主要是由 3 个部分组成

1
2
3
4
5
6
7
8
9
java复制代码// 非必须,若加上则编译期会提供校验
@FunctionalInterface

// 必须声明为 interface
public interface LambdaExample<T> {

// 单个非默认/静态实现方法
public abstract T apply();
}
内置常用函数式接口

为了简化开发,JDK 已经提前声明了一些常用的 Lambda 表达式,如下所示

输入 返回值 Class 备注
T R Function<T,R> 一个输入和输出,通常是对一个值操作返回一个值
void T Supplier< T > 只有返回值,通常作为生产者
T void Consumer< T > 只有输入值,通常作为消费者
void void Runnable 即无输入也无输出,单纯的执行
T Boolean Predicate< T > 通常用于对输入值进行判断,Function的特殊形式
T T UnaryOperate< T >

如果我们有以上场景,则直接使用 JDK 提供的内置接口即可

方法引用

如果定义了一个 Lambda 函数,需要怎么才能把一个普通的方法引用到 Lambda 表达式上呢?

如下,先定义一个 Entity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public class Entity {
String msg = "";

public Entity() {

}

public Entity getEntity() {
return new Entity();
}

public static Entity getInstance() {
return new Entity();
}

}

JDK 提供三种方式来引用一个方法,如下所示

1
2
3
4
5
6
7
8
9
java复制代码// 引用构造方法
LambdaExample<Entity> example = Entity::new;

// 引用静态方法
LambdaExample<Entity> example1 = Entity::getInstance;

// 指定实例的方法
Entity entity = new Entity();
LambdaExample<Entity> example2 = entity::getEntity;

通过上面的方法,就可以把一个 Lambda 表达式和一个普通方法绑定了

函数式接口转换

由于 Java 是强类型,在某些场合下,并不要求函数签名完全一致,可以进行转换,例如:

  • 忽略输入:Function <- Supplier
  • 忽略返回:Consumer <- Function
  • 忽略输入和返回: Runnable <- Supplier
Stream

作为函数式编程的最常用的地方,在已经拥有 List 的情况下,为什么还要引入 Stream 呢?

  • Stream 可以是无限的
  • Stream 可以并行处理
  • Stream 可以延迟处理

如何创建一个 Stream ?

  • 静态数据 Stream.of()
  • 容器 collection.stream()
  • 动态 Stream.iterate() & Stream.generate()
  • 其他 API Files.lines()
Stream 基本操作

stream 操作分为两类,分别是中间操作和结束操作,他们在整个 Stream 操作中的关系图如下

Source => Intermediate Operation => Intermediate Operation => …… => Intermediate Operation => Terminal Operation => Result

中间操作( Intermediate Operation )

  • filter
  • distinct
  • skip
  • limit
  • map/flatMap
  • sorted

结束操作( Terminal Operation )

  • count/sum
  • collect/reduce
  • forEach
  • anyMatch/allMatch/noneMath
函数式编程三板斧
  • filter(Predicate predicate)
  • map(Function mapper)
  • reduce(U identity, BinaryOperator acc)

其中,三者的作用通过下图形象的表示出来

图1.png

其中,map 和 filter 比较容易理解:map 是一种映射关系,比如将水果映射成水果块;filter 是过滤,通过条件选出符合要求的;而 reduce 较为抽象,是一种将元素混合累积的概念,比如上图将各种水果切块混成我们想要的沙拉,如下节所示

reduce 理解

选取 Stream 中 reduce 函数 T reduce(T identity, BinaryOperator<T> accumulator); 。可以看到主要是有两个参数:第一个是初始值,第二个是累加累积函数,这个函数其实可以和换种写法更好理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码// reduce 函数
T reduce(T identity, BinaryOperator<T> accumulator);
// 其中accumulator可以看做如下动作
BinaryOperator<T> accumulator = (acc, curr) -> {
// do some
retuen newAcc;
}

//整个上面两个动作可以用如下代码等价替换
R acc = identity;
for ( T curr: datas){
// apply = do some
acc = accumulator.apply(acc,curr);
}
return acc;
reduce 例子

通过几个例子,可以更深入的了解 reduce 的作用

  • reduce 求和

求和使用到了上节所示的 T reduce(T identity, BinaryOperator<T> accumulator);

1
2
3
4
5
6
7
java复制代码public static int sum(Collection<Integer> list) {
return list.stream().reduce(0, (acc, curr) -> acc + curr);
}
public static void main(String[] args) {
List<Integer> list = Arrays.asList(2,3,4,7,5);
System.out.println(sum(list));
}

其中因为是求和,第一个参数初始值直接传入0即可,第二个累加累积函数直接传入两个 int 相加即可

  • reduce 实现 map

为了实现 map ,使用了 如下的需要传入3个参数的reduce函数

1
2
java复制代码//第一个参数初始值,第二个参数累积累积函数,第三个函数累积累积后怎么与初始值进行合并
<U> U reduce(U identity,BiFunction<U, ? super T, U> accumulator,BinaryOperator<U> combiner);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码public static <T, R> List<R> map(List<T> list, Function<T, R> mapFn) {
// 首先声明一个映射后的返回 List
List<R> ret = new ArrayList<>();
// 此时的初始值可以传入 ret List,我们需要对每个元素进行操作添加到该list中
return list.stream().reduce(ret, (acc, curr) -> {
R newValue = mapFn.apply(curr);
acc.add(newValue);
return acc;
},
(list1, list2) -> {
list1.addAll(list2);
return list1;
});
}
public static void main(String[] args) {
List<Integer> list = Arrays.asList(2,3,4,7,5);
List<Integer> list2 = map(list1, i->i*2);
System.out.println(list2);

}

此时想用 reduce 实现对一个 List 的各元素乘以2的映射动作。首先是需要给 map() 传入两个参数,一个是需要进行映射的 list ,第二个是进行 map 的函数。在reduce操作中传入了三个参数,第一个是初始值,就是我们需要进行reduce操作的函数,在整个例子中传入 i->i*2 ,第三个函数就是我们加完一个后,需要对两个list进行合并,这里就使用allAll就行

Stream.collect()

collect是汇合Stream元素的操作,在已经拥有了reduce函数,为什么还需要collect函数?

  • reduce操作不可变数据
  • collect操作可变数据

他们的区别如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码// reduce
R acc = identity;
for ( T curr: datas){
acc = accumulator.apply(acc,curr);
}
return acc;

// collect
R container = supplier.get();
for ( T curr: datas){
accumulator.accept(container,curr);
}
return container;

collect函数提供了两种参数类型

  • collect(Supplier, Accumulator, Combiner)
  • collect(Collector)

其中 Collector 是 JDK 针对于例如List/Map等常用场景提供了一个覆盖了Supplier,Accumulator,Combiner的集合,所以重点还是要落在解析collect(Supplier, Accumulator, Combiner) 上

通过一个图充分展现 collect 三个参数的作用

collect

从图中可以清楚的看到Collector的要素:

  • Supplier:累积数据构造函数,通过get方法获得累积结果的容器
  • Accumulator: 累积函数,通过accept对结果进行累积
  • Combiner: 合并函数,并行处理场合下用
  • Finisher: 对累积数据做最终转换,例如对最后的结果进行加1的操作
  • *Characteristics: 特征(并发/无序/无finisher)
Collectors API
  • toList/to(Concurrent)Map/toSet/toCollection
  • counting/averagingXX/joining/summingXX
  • groupBy/partitioningBy
  • mapping/reducing

其中 toXXX 是针对容器的常用场景,已经封装好一系列函数,counting等也较为容易理解,重点关注groupBy

Collectors.groupingBy

groupBy有三种用法:

  • groupingBy(Function) – 单纯分key存放成Map,默认使用HashMap
  • groupingBy(Function, Collector) - 分key后,对每个key的元素进行后续collect操作,其中Collector还可以继续进行groupingBy操作,进行无限往下分类
  • groupingBy(Function, Suppiler, Collector) - 同上,允许自定义Map创建
Collectors.groupingBy例子

定义一个Programmer的实体类和元组类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码public class Programmer {
private String name;
private int level;
private int salary;
private int output;//from 1-10
private String language;
}
public class Turple<L,R>{
private final L l;
private final R r;

public Turple(L l, R r) {
this.l = l;
this.r = r;
}
@Override
public String toString() {
return "(" + l + ", " + r + " )";
}
}

通过groupingBy操作,先按编程语言,再按编程等级分层,然后返回一个元组<平均工资,程序员列表>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Java复制代码public static Map<String, Map<Integer, Turple<Integer, List<Programmer>>>> classify(List<Programmer> programmers) {
return programmers.stream().collect(
// 首先把程序员语言语言分类
Collectors.groupingBy(Programmer::getLanguage,
// 再这里已经通过语言分好的类后,可以继续使用groupingBy对同一语言的不同等级程序员进行分类
Collectors.groupingBy(Programmer::getLevel,
// 这里collectingAndThen主要接受两个参数,一个是Collector,第二个是Collector完成后进行一个附加操作,这里就是把同一等级的程序员的工资进行一个平均
Collectors.collectingAndThen(
Collectors.toList(),
// 此处的list就已经是同一语言同一等级的程序员list,再对该list进行求工资平均值操作
list -> new Turple(list.stream().collect(Collectors.averagingInt(Programmer::getSalary)), list)
)
)
));
}
Optional

optional也是函数式编程中常用的,平时使用可能只是简单的判断有没有为空等操作,实际上它跟Stream一样也是函数式编程重要的组成部分

  • Stream表达的是函数式编程中,一系列元素的处理
  • Optional表达的是函数编程中,元素有和无的处理
Optional API
  • orElse(T) => if (x!= null) return x; else return T;
  • orElseGet(fn) => if (x!=null) return x else return fn();
  • ifPresent(fn) => if (x!= null) fn();
Optional.map()

optional和stream一样,同为函数式编程的组成部分,都有map操作,通过optional.map(),我们可以很精妙的避免null对我们的操作影响

例如这样一个数据结构:学校->年级->班级->小组->学生,如果要获取一个学生,则需要进行下列的一系列操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public 获取学生(){
if(学校!=null){
年级=学校.get();
if(年级!=null){
班级=年级.get();
if(班级!=null){
小组=班级.get();
if(小组!=null){
学生=小组.get();
return 学生
}
}
}
}
}

而使用Optional,则为如下调用方式

1
2
3
4
5
6
7
8
java复制代码public 获取学生(){
return Optional.ofNullable(学校)
.map(学校::get)
.map(年级::get)
.map(班级::get)
.map(小组::get)
.orElse(null)
}

相比起来,就会优雅很多,这里体现了map的一个运行机制
optional.png

Functor & Monad

为什么 Stream 和 Optional 都有类似的map操作?这里涉及到Stream 和 Optional都属于函数式编程中基本的模型,其他的模型如下:

  • Optional:null Or T
  • Stream:0…n
  • Either:A or B ( JDK 未实现)
  • Promise: ( JDK 未实现)
  • IO:IO operation

为什么这些模型都有类似的操作,这就属于 Functor & Monad 相关的知识了

本文转载自: 掘金

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

对Spring PostConstruct注解的一点新认识

发表于 2021-06-06

无论是Spring还是SpringBoot开发中,PostConstruct注解的使用频率还是比较高的,通常用于Bean初始化完成的一些动作。

在项目代码中,会将配置从配置中心中读取,然后初始化到指定的Bean中。其他需要动态获取配置的地方,直接依赖注入这个Bean即可。
示例代码如下:

ApplicationConfig

动态配置所在的类,主要是属性。

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

/**
* client host
*/
private String host;

/**
* client port
*/
private String port;

public ApplicationConfig() {
log.info("ApplicationConfig constructor execute");
}

@PostConstruct
public void init() {
log.info("ApplicationConfig postConstructor execute");
}
}
ApplicationConfigLoadService

从远程配置中心中获取配置信息,主要依赖PostConstruct方法。

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

@Resource
private ApplicationConfig applicationConfig;

public ApplicationConfigLoadService() {
log.info("ApplicationConfigLoadService constructor execute");
}

@PostConstruct
public void load() {
log.info("ApplicationConfigLoadService postConstruct execute");
// 可以是从数据库,或者远程的配置中心中读取配置
String host = "127.0.0.1";
String port = "8080";
applicationConfig.setHost(host);
applicationConfig.setPort(port);
}
}
ApplicationClientFactory

使用ApplicationConfig,基于配置信息,在类初始化完成后,做一些动作。

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

@Resource
private ApplicationConfig applicationConfig;

public ApplicationClientFactory() {
log.info("ApplicationClientFactory constructor execute");
}

@PostConstruct
public void init() {
log.info("ApplicationClientFactory postConstruct execute, host:{}, port:{}",
applicationConfig.getHost(), applicationConfig.getPort());
}
}

备注:

  1. 主要的类中,提供了一个无参的构造方法,以及一个使用了@PostConstructor注解的初始化方法,主要用于看一下执行顺序。
  2. 代码说明
    1. ApplicationConfigLoadService 的初始化方法中加载配置
    2. ApplicationConfig的setter方法完成配置初始化
    3. ApplicationClientFactory依赖ApplicationConfig中的属性完成一些初始化工作。

将上述代码执行一下,并查看日志。

1
2
3
4
5
6
properties复制代码2021-06-06 15:38:28.591  INFO 2790 --- [           main] c.y.m.c.ApplicationClientFactory         : ApplicationClientFactory constructor execute
2021-06-06 15:38:28.598 INFO 2790 --- [ main] c.y.m.configuration.ApplicationConfig : ApplicationConfig constructor execute
2021-06-06 15:38:28.599 INFO 2790 --- [ main] c.y.m.configuration.ApplicationConfig : ApplicationConfig postConstructor execute
2021-06-06 15:38:28.599 INFO 2790 --- [ main] c.y.m.c.ApplicationClientFactory : ApplicationClientFactory postConstruct execute, host:null, port:null
2021-06-06 15:38:28.602 INFO 2790 --- [ main] c.y.m.c.ApplicationConfigLoadService : ApplicationConfigLoadService constructor execute
2021-06-06 15:38:28.603 INFO 2790 --- [ main] c.y.m.c.ApplicationConfigLoadService : ApplicationConfigLoadService postConstruct execut

可以看到ApplicationClientFactory的构造方法先被执行,然后由于依赖ApplicationConfig类,所以ApplicationConfig的构造方法和标识了PostConstruct注解的方法被执行,然后才会执行ApplicationClientFactory自己的postConstruct方法。

但是从日志中可以看出,此时由于ApplicationConfigLoadService还没被加载,所以读取到的配置都是空的。

尝试的解决方案

方案1:是可以采用DependsOn指定Bean的加载顺序。

修改代码如下:

value即为依赖Bean的名称。

1
2
3
4
java复制代码@DependsOn(value = {"applicationConfigLoadService"})
@Component
@Slf4j
public class ApplicationClientFactory
1
2
3
4
5
6
> vbnet复制代码Beans on which the current bean depends. Any beans specified are guaranteed to be
> created by the container before this bean. Used infrequently in cases where a bean
> does not explicitly depend on another through properties or constructor arguments,
> but rather depends on the side effects of another bean's initialization.
>
>

从JDK文档可以看出,DependsOn注解主要的使用场景是当前Bean没有显示通过属性或者构造参数依赖另外一个Bean,但是却要依赖另外一个Bean的一些初始化动作。

在上述代码示例中,通过添加DependsOn注解,可以解决问题。

1
2
3
4
5
6
properties复制代码2021-06-06 16:36:59.944  INFO 3688 --- [           main] c.y.m.c.ApplicationConfigLoadService     : ApplicationConfigLoadService constructor execute
2021-06-06 16:36:59.948 INFO 3688 --- [ main] c.y.m.configuration.ApplicationConfig : ApplicationConfig constructor execute
2021-06-06 16:36:59.949 INFO 3688 --- [ main] c.y.m.configuration.ApplicationConfig : ApplicationConfig postConstructor execute
2021-06-06 16:36:59.949 INFO 3688 --- [ main] c.y.m.c.ApplicationConfigLoadService : ApplicationConfigLoadService postConstruct execute
2021-06-06 16:36:59.950 INFO 3688 --- [ main] c.y.m.c.ApplicationClientFactory : ApplicationClientFactory constructor execute
2021-06-06 16:36:59.951 INFO 3688 --- [ main] c.y.m.c.ApplicationClientFactory : ApplicationClientFactory postConstruct execute, host:127.0.0.1, port:8080

方案2: 显示通过@Resource或者@Autowired注入待依赖的Bean

在DependsOn的JDK代码中也可以看到,通过显示依赖可以解决问题。通过签名日志可以看出,当显示依赖注入某个Bean时,被注入Bean会依次执行对应的构造函数以及@PostConstructor注解的初始化方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码public class ApplicationClientFactory {

@Resource
private ApplicationConfig applicationConfig;

// 显示依赖
@Resource
private ApplicationConfigLoadService applicationConfigLoadService;

public ApplicationClientFactory() {
log.info("ApplicationClientFactory constructor execute");
}

@PostConstruct
public void init() {
log.info("ApplicationClientFactory postConstruct execute, host:{}, port:{}",
applicationConfig.getHost(), applicationConfig.getPort());
}
}

执行结果

1
2
3
4
5
6
properties复制代码2021-06-06 16:08:17.458  INFO 3286 --- [           main] c.y.m.c.ApplicationClientFactory         : ApplicationClientFactory constructor execute
2021-06-06 16:08:17.464 INFO 3286 --- [ main] c.y.m.configuration.ApplicationConfig : ApplicationConfig constructor execute
2021-06-06 16:08:17.465 INFO 3286 --- [ main] c.y.m.configuration.ApplicationConfig : ApplicationConfig postConstructor execute
2021-06-06 16:08:17.466 INFO 3286 --- [ main] c.y.m.c.ApplicationConfigLoadService : ApplicationConfigLoadService constructor execute
2021-06-06 16:08:17.467 INFO 3286 --- [ main] c.y.m.c.ApplicationConfigLoadService : ApplicationConfigLoadService postConstruct execute
2021-06-06 16:08:17.467 INFO 3286 --- [ main] c.y.m.c.ApplicationClientFactory : ApplicationClientFactory postConstruct execute, host:127.0.0.1, port:8080

此时可以看到在ApplicationClientFactory的postConstruc中,依赖的ApplicationConfig是有对应属性值的。

但是,此处会存在一个风险问题,由于applicationConfigLoadService这个变量在当前类中并未实际使用,仅仅是为了依赖其postConstruct方法。对于后续维护的同学,很有可能无意将其移除。

本文转载自: 掘金

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

异步编程利器:CompletableFuture详解 |Ja

发表于 2021-06-06

前言

文正在参加「Java主题月 - Java 开发实战」,详情查看 (活动链接)

我们异步执行一个任务时,一般是用线程池Executor去创建。如果不需要有返回值,
任务实现Runnable接口;如果需要有返回值,任务实现Callable接口,调用Executor的submit方法,再使用Future获取即可。如果多个线程存在依赖组合的话,我们怎么处理呢?可使用同步组件CountDownLatch、CyclicBarrier等,但是比较麻烦。其实有简单的方法,就是用CompeletableFuture。最近刚好使用CompeletableFuture优化了项目中的代码,所以跟大家一起学习CompletableFuture。

image.png

  • 公众号:捡田螺的小男孩
  • github地址

一个例子回顾 Future

因为CompletableFuture实现了Future接口,我们先来回顾Future吧。

Future是Java5新加的一个接口,它提供了一种异步并行计算的功能。如果主线程需要执行一个很耗时的计算任务,我们就可以通过future把这个任务放到异步线程中执行。主线程继续处理其他任务,处理完成后,再通过Future获取计算结果。

来看个简单例子吧,假设我们有两个任务服务,一个查询用户基本信息,一个是查询用户勋章信息。如下,

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

public UserInfo getUserInfo(Long userId) throws InterruptedException {
Thread.sleep(300);//模拟调用耗时
return new UserInfo("666", "捡田螺的小男孩", 27); //一般是查数据库,或者远程调用返回的
}
}

public class MedalService {

public MedalInfo getMedalInfo(long userId) throws InterruptedException {
Thread.sleep(500); //模拟调用耗时
return new MedalInfo("666", "守护勋章");
}
}

接下来,我们来演示下,在主线程中是如何使用Future来进行异步调用的。

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

public static void main(String[] args) throws ExecutionException, InterruptedException {

ExecutorService executorService = Executors.newFixedThreadPool(10);

UserInfoService userInfoService = new UserInfoService();
MedalService medalService = new MedalService();
long userId =666L;
long startTime = System.currentTimeMillis();

//调用用户服务获取用户基本信息
FutureTask<UserInfo> userInfoFutureTask = new FutureTask<>(new Callable<UserInfo>() {
@Override
public UserInfo call() throws Exception {
return userInfoService.getUserInfo(userId);
}
});
executorService.submit(userInfoFutureTask);

Thread.sleep(300); //模拟主线程其它操作耗时

FutureTask<MedalInfo> medalInfoFutureTask = new FutureTask<>(new Callable<MedalInfo>() {
@Override
public MedalInfo call() throws Exception {
return medalService.getMedalInfo(userId);
}
});
executorService.submit(medalInfoFutureTask);

UserInfo userInfo = userInfoFutureTask.get();//获取个人信息结果
MedalInfo medalInfo = medalInfoFutureTask.get();//获取勋章信息结果

System.out.println("总共用时" + (System.currentTimeMillis() - startTime) + "ms");
}
}

运行结果:

1
复制代码总共用时806ms

如果我们不使用Future进行并行异步调用,而是在主线程串行进行的话,耗时大约为300+500+300 = 1100 ms。可以发现,future+线程池异步配合,提高了程序的执行效率。

但是Future对于结果的获取,不是很友好,只能通过阻塞或者轮询的方式得到任务的结果。

  • Future.get() 就是阻塞调用,在线程获取结果之前get方法会一直阻塞。
  • Future提供了一个isDone方法,可以在程序中轮询这个方法查询执行结果。

阻塞的方式和异步编程的设计理念相违背,而轮询的方式会耗费无谓的CPU资源。因此,JDK8设计出CompletableFuture。CompletableFuture提供了一种观察者模式类似的机制,可以让任务执行完成后通知监听的一方。

一个例子走进CompletableFuture

我们还是基于以上Future的例子,改用CompletableFuture 来实现

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

public static void main(String[] args) throws InterruptedException, ExecutionException, TimeoutException {

UserInfoService userInfoService = new UserInfoService();
MedalService medalService = new MedalService();
long userId =666L;
long startTime = System.currentTimeMillis();

//调用用户服务获取用户基本信息
CompletableFuture<UserInfo> completableUserInfoFuture = CompletableFuture.supplyAsync(() -> userInfoService.getUserInfo(userId));

Thread.sleep(300); //模拟主线程其它操作耗时

CompletableFuture<MedalInfo> completableMedalInfoFuture = CompletableFuture.supplyAsync(() -> medalService.getMedalInfo(userId));

UserInfo userInfo = completableUserInfoFuture.get(2,TimeUnit.SECONDS);//获取个人信息结果
MedalInfo medalInfo = completableMedalInfoFuture.get();//获取勋章信息结果
System.out.println("总共用时" + (System.currentTimeMillis() - startTime) + "ms");

}
}

可以发现,使用CompletableFuture,代码简洁了很多。CompletableFuture的supplyAsync方法,提供了异步执行的功能,线程池也不用单独创建了。实际上,它CompletableFuture使用了默认线程池是ForkJoinPool.commonPool。

CompletableFuture提供了几十种方法,辅助我们的异步任务场景。这些方法包括创建异步任务、任务异步回调、多个任务组合处理等方面。我们一起来学习吧

CompletableFuture使用场景

image.png

创建异步任务

CompletableFuture创建异步任务,一般有supplyAsync和runAsync两个方法

image.png

  • supplyAsync执行CompletableFuture任务,支持返回值
  • runAsync执行CompletableFuture任务,没有返回值。

supplyAsync方法

1
2
3
4
swift复制代码//使用默认内置线程池ForkJoinPool.commonPool(),根据supplier构建执行任务
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
//自定义线程,根据supplier构建执行任务
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)

runAsync方法

1
2
3
4
java复制代码//使用默认内置线程池ForkJoinPool.commonPool(),根据runnable构建执行任务
public static CompletableFuture<Void> runAsync(Runnable runnable)
//自定义线程,根据runnable构建执行任务
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)

实例代码如下:

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

public static void main(String[] args) {
//可以自定义线程池
ExecutorService executor = Executors.newCachedThreadPool();
//runAsync的使用
CompletableFuture<Void> runFuture = CompletableFuture.runAsync(() -> System.out.println("run,关注公众号:捡田螺的小男孩"), executor);
//supplyAsync的使用
CompletableFuture<String> supplyFuture = CompletableFuture.supplyAsync(() -> {
System.out.print("supply,关注公众号:捡田螺的小男孩");
return "捡田螺的小男孩"; }, executor);
//runAsync的future没有返回值,输出null
System.out.println(runFuture.join());
//supplyAsync的future,有返回值
System.out.println(supplyFuture.join());
executor.shutdown(); // 线程池需要关闭
}
}
//输出
run,关注公众号:捡田螺的小男孩
null
supply,关注公众号:捡田螺的小男孩捡田螺的小男孩

任务异步回调

image.png

1. thenRun/thenRunAsync

1
2
arduino复制代码public CompletableFuture<Void> thenRun(Runnable action);
public CompletableFuture<Void> thenRunAsync(Runnable action);

CompletableFuture的thenRun方法,通俗点讲就是,做完第一个任务后,再做第二个任务。某个任务执行完成后,执行回调方法;但是前后两个任务没有参数传递,第二个任务也没有返回值

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

public static void main(String[] args) throws ExecutionException, InterruptedException {

CompletableFuture<String> orgFuture = CompletableFuture.supplyAsync(
()->{
System.out.println("先执行第一个CompletableFuture方法任务");
return "捡田螺的小男孩";
}
);

CompletableFuture thenRunFuture = orgFuture.thenRun(() -> {
System.out.println("接着执行第二个任务");
});

System.out.println(thenRunFuture.get());
}
}
//输出
先执行第一个CompletableFuture方法任务
接着执行第二个任务
null

thenRun 和thenRunAsync有什么区别呢?可以看下源码哈:

1
2
3
4
5
6
7
8
9
10
java复制代码   private static final Executor asyncPool = useCommonPool ?
ForkJoinPool.commonPool() : new ThreadPerTaskExecutor();

public CompletableFuture<Void> thenRun(Runnable action) {
return uniRunStage(null, action);
}

public CompletableFuture<Void> thenRunAsync(Runnable action) {
return uniRunStage(asyncPool, action);
}

如果你执行第一个任务的时候,传入了一个自定义线程池:

  • 调用thenRun方法执行第二个任务时,则第二个任务和第一个任务是共用同一个线程池。
  • 调用thenRunAsync执行第二个任务时,则第一个任务使用的是你自己传入的线程池,第二个任务使用的是ForkJoin线程池

TIPS: 后面介绍的thenAccept和thenAcceptAsync,thenApply和thenApplyAsync等,它们之间的区别也是这个哈。

2.thenAccept/thenAcceptAsync

CompletableFuture的thenAccept方法表示,第一个任务执行完成后,执行第二个回调方法任务,会将该任务的执行结果,作为入参,传递到回调方法中,但是回调方法是没有返回值的。

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

public static void main(String[] args) throws ExecutionException, InterruptedException {

CompletableFuture<String> orgFuture = CompletableFuture.supplyAsync(
()->{
System.out.println("原始CompletableFuture方法任务");
return "捡田螺的小男孩";
}
);

CompletableFuture thenAcceptFuture = orgFuture.thenAccept((a) -> {
if ("捡田螺的小男孩".equals(a)) {
System.out.println("关注了");
}

System.out.println("先考虑考虑");
});

System.out.println(thenAcceptFuture.get());
}
}

3. thenApply/thenApplyAsync

CompletableFuture的thenApply方法表示,第一个任务执行完成后,执行第二个回调方法任务,会将该任务的执行结果,作为入参,传递到回调方法中,并且回调方法是有返回值的。

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
csharp复制代码public class FutureThenApplyTest {

public static void main(String[] args) throws ExecutionException, InterruptedException {

CompletableFuture<String> orgFuture = CompletableFuture.supplyAsync(
()->{
System.out.println("原始CompletableFuture方法任务");
return "捡田螺的小男孩";
}
);

CompletableFuture<String> thenApplyFuture = orgFuture.thenApply((a) -> {
if ("捡田螺的小男孩".equals(a)) {
return "关注了";
}

return "先考虑考虑";
});

System.out.println(thenApplyFuture.get());
}
}
//输出
原始CompletableFuture方法任务
关注了

4. exceptionally

CompletableFuture的exceptionally方法表示,某个任务执行异常时,执行的回调方法;并且有抛出异常作为参数,传递到回调方法。

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
php复制代码public class FutureExceptionTest {

public static void main(String[] args) throws ExecutionException, InterruptedException {

CompletableFuture<String> orgFuture = CompletableFuture.supplyAsync(
()->{
System.out.println("当前线程名称:" + Thread.currentThread().getName());
throw new RuntimeException();
}
);

CompletableFuture<String> exceptionFuture = orgFuture.exceptionally((e) -> {
e.printStackTrace();
return "你的程序异常啦";
});

System.out.println(exceptionFuture.get());
}
}
//输出
当前线程名称:ForkJoinPool.commonPool-worker-1
java.util.concurrent.CompletionException: java.lang.RuntimeException
at java.util.concurrent.CompletableFuture.encodeThrowable(CompletableFuture.java:273)
at java.util.concurrent.CompletableFuture.completeThrowable(CompletableFuture.java:280)
at java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1592)
at java.util.concurrent.CompletableFuture$AsyncSupply.exec(CompletableFuture.java:1582)
at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)
Caused by: java.lang.RuntimeException
at cn.eovie.future.FutureWhenTest.lambda$main$0(FutureWhenTest.java:13)
at java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1590)
... 5 more
你的程序异常啦

5. whenComplete方法

CompletableFuture的whenComplete方法表示,某个任务执行完成后,执行的回调方法,无返回值;并且whenComplete方法返回的CompletableFuture的result是上个任务的结果。

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
csharp复制代码public class FutureWhenTest {

public static void main(String[] args) throws ExecutionException, InterruptedException {

CompletableFuture<String> orgFuture = CompletableFuture.supplyAsync(
()->{
System.out.println("当前线程名称:" + Thread.currentThread().getName());
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "捡田螺的小男孩";
}
);

CompletableFuture<String> rstFuture = orgFuture.whenComplete((a, throwable) -> {
System.out.println("当前线程名称:" + Thread.currentThread().getName());
System.out.println("上个任务执行完啦,还把" + a + "传过来");
if ("捡田螺的小男孩".equals(a)) {
System.out.println("666");
}
System.out.println("233333");
});

System.out.println(rstFuture.get());
}
}
//输出
当前线程名称:ForkJoinPool.commonPool-worker-1
当前线程名称:ForkJoinPool.commonPool-worker-1
上个任务执行完啦,还把捡田螺的小男孩传过来
666
233333
捡田螺的小男孩

6. handle方法

CompletableFuture的handle方法表示,某个任务执行完成后,执行回调方法,并且是有返回值的;并且handle方法返回的CompletableFuture的result是回调方法执行的结果。

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
csharp复制代码public class FutureHandlerTest {

public static void main(String[] args) throws ExecutionException, InterruptedException {

CompletableFuture<String> orgFuture = CompletableFuture.supplyAsync(
()->{
System.out.println("当前线程名称:" + Thread.currentThread().getName());
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "捡田螺的小男孩";
}
);

CompletableFuture<String> rstFuture = orgFuture.handle((a, throwable) -> {

System.out.println("上个任务执行完啦,还把" + a + "传过来");
if ("捡田螺的小男孩".equals(a)) {
System.out.println("666");
return "关注了";
}
System.out.println("233333");
return null;
});

System.out.println(rstFuture.get());
}
}
//输出
当前线程名称:ForkJoinPool.commonPool-worker-1
上个任务执行完啦,还把捡田螺的小男孩传过来
666
关注了

多个任务组合处理

image.png

AND组合关系

image.png

thenCombine / thenAcceptBoth / runAfterBoth都表示:将两个CompletableFuture组合起来,只有这两个都正常执行完了,才会执行某个任务。

区别在于:

  • thenCombine:会将两个任务的执行结果作为方法入参,传递到指定方法中,且有返回值
  • thenAcceptBoth: 会将两个任务的执行结果作为方法入参,传递到指定方法中,且无返回值
  • runAfterBoth 不会把执行结果当做方法入参,且没有返回值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
arduino复制代码public class ThenCombineTest {

public static void main(String[] args) throws InterruptedException, ExecutionException, TimeoutException {

CompletableFuture<String> first = CompletableFuture.completedFuture("第一个异步任务");
ExecutorService executor = Executors.newFixedThreadPool(10);
CompletableFuture<String> future = CompletableFuture
//第二个异步任务
.supplyAsync(() -> "第二个异步任务", executor)
// (w, s) -> System.out.println(s) 是第三个任务
.thenCombineAsync(first, (s, w) -> {
System.out.println(w);
System.out.println(s);
return "两个异步任务的组合";
}, executor);
System.out.println(future.join());
executor.shutdown();

}
}
//输出
第一个异步任务
第二个异步任务
两个异步任务的组合

OR 组合的关系

image.png

applyToEither / acceptEither / runAfterEither 都表示:将两个CompletableFuture组合起来,只要其中一个执行完了,就会执行某个任务。

区别在于:

  • applyToEither:会将已经执行完成的任务,作为方法入参,传递到指定方法中,且有返回值
  • acceptEither: 会将已经执行完成的任务,作为方法入参,传递到指定方法中,且无返回值
  • runAfterEither: 不会把执行结果当做方法入参,且没有返回值。
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
kotlin复制代码public class AcceptEitherTest {
public static void main(String[] args) {
//第一个异步任务,休眠2秒,保证它执行晚点
CompletableFuture<String> first = CompletableFuture.supplyAsync(()->{
try{

Thread.sleep(2000L);
System.out.println("执行完第一个异步任务");}
catch (Exception e){
return "第一个任务异常";
}
return "第一个异步任务";
});
ExecutorService executor = Executors.newSingleThreadExecutor();
CompletableFuture<Void> future = CompletableFuture
//第二个异步任务
.supplyAsync(() -> {
System.out.println("执行完第二个任务");
return "第二个任务";}
, executor)
//第三个任务
.acceptEitherAsync(first, System.out::println, executor);

executor.shutdown();
}
}
//输出
执行完第二个任务
第二个任务

AllOf

所有任务都执行完成后,才执行 allOf返回的CompletableFuture。如果任意一个任务异常,allOf的CompletableFuture,执行get方法,会抛出异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
kotlin复制代码public class allOfFutureTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {

CompletableFuture<Void> a = CompletableFuture.runAsync(()->{
System.out.println("我执行完了");
});
CompletableFuture<Void> b = CompletableFuture.runAsync(() -> {
System.out.println("我也执行完了");
});
CompletableFuture<Void> allOfFuture = CompletableFuture.allOf(a, b).whenComplete((m,k)->{
System.out.println("finish");
});
}
}
//输出
我执行完了
我也执行完了
finish

AnyOf

任意一个任务执行完,就执行anyOf返回的CompletableFuture。如果执行的任务异常,anyOf的CompletableFuture,执行get方法,会抛出异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
csharp复制代码public class AnyOfFutureTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {

CompletableFuture<Void> a = CompletableFuture.runAsync(()->{
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我执行完了");
});
CompletableFuture<Void> b = CompletableFuture.runAsync(() -> {
System.out.println("我也执行完了");
});
CompletableFuture<Object> anyOfFuture = CompletableFuture.anyOf(a, b).whenComplete((m,k)->{
System.out.println("finish");
// return "捡田螺的小男孩";
});
anyOfFuture.join();
}
}
//输出
我也执行完了
finish

thenCompose

thenCompose方法会在某个任务执行完成后,将该任务的执行结果,作为方法入参,去执行指定的方法。该方法会返回一个新的CompletableFuture实例

  • 如果该CompletableFuture实例的result不为null,则返回一个基于该result新的CompletableFuture实例;
  • 如果该CompletableFuture实例为null,然后就执行这个新任务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
arduino复制代码public class ThenComposeTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {

CompletableFuture<String> f = CompletableFuture.completedFuture("第一个任务");
//第二个异步任务
ExecutorService executor = Executors.newSingleThreadExecutor();
CompletableFuture<String> future = CompletableFuture
.supplyAsync(() -> "第二个任务", executor)
.thenComposeAsync(data -> {
System.out.println(data); return f; //使用第一个任务作为返回
}, executor);
System.out.println(future.join());
executor.shutdown();

}
}
//输出
第二个任务
第一个任务

CompletableFuture使用有哪些注意点

CompletableFuture 使我们的异步编程更加便利的、代码更加优雅的同时,我们也要关注下它,使用的一些注意点。

image.png

1. Future需要获取返回值,才能获取异常信息

1
2
3
4
5
6
7
8
9
10
11
ini复制代码ExecutorService executorService = new ThreadPoolExecutor(5, 10, 5L,
TimeUnit.SECONDS, new ArrayBlockingQueue<>(10));
CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> {
int a = 0;
int b = 666;
int c = b / a;
return true;
},executorService).thenAccept(System.out::println);

//如果不加 get()方法这一行,看不到异常信息
//future.get();

Future需要获取返回值,才能获取到异常信息。如果不加 get()/join()方法,看不到异常信息。小伙伴们使用的时候,注意一下哈,考虑是否加try…catch…或者使用exceptionally方法。

2. CompletableFuture的get()方法是阻塞的。

CompletableFuture的get()方法是阻塞的,如果使用它来获取异步调用的返回值,需要添加超时时间~

1
2
3
4
csharp复制代码//反例
CompletableFuture.get();
//正例
CompletableFuture.get(5, TimeUnit.SECONDS);

3. 默认线程池的注意点

CompletableFuture代码中又使用了默认的线程池,处理的线程个数是电脑CPU核数-1。在大量请求过来的时候,处理逻辑复杂的话,响应会很慢。一般建议使用自定义线程池,优化线程池配置参数。

4. 自定义线程池时,注意饱和策略

CompletableFuture的get()方法是阻塞的,我们一般建议使用future.get(3, TimeUnit.SECONDS)。并且一般建议使用自定义线程池。

但是如果线程池拒绝策略是DiscardPolicy或者DiscardOldestPolicy,当线程池饱和时,会直接丢弃任务,不会抛弃异常。因此建议,CompletableFuture线程池策略最好使用AbortPolicy,然后耗时的异步线程,做好线程池隔离哈。

参考与感谢

  • Java8 CompletableFuture 用法全解
  • 详解 java CompletableFuture
  • 基础篇:异步编程不会?我教你啊!
  • CompletableFuture get方法一直阻塞或抛出TimeoutException
  • 编程老司机带你玩转 CompletableFuture 异步编程
  • 解决CompletableFuture异常阻塞

本文转载自: 掘金

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

1…650651652…956

开发者博客

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