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

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


  • 首页

  • 归档

  • 搜索

redis集群搭建(非常详细,适合新手)

发表于 2021-05-05

redis集群搭建

在开始redis集群搭建之前,我们先简单回顾一下redis单机版的搭建过程

1、下载redis压缩包,然后解压压缩文件;

2、进入到解压缩后的redis文件目录(此时可以看到Makefile文件),编译redis源文件;

3、把编译好的redis源文件安装到/usr/local/redis目录下,如果/local目录下没有redis目录,会自动新建redis目录;

4、进入/usr/local/redis/bin目录,直接./redis-server启动redis(此时为前端启动redis);

5、将redis启动方式改为后端启动,具体做法:把解压缩的redis文件下的redis.conf文件复制到/usr/local/redis/bin目录下,然后修改该redis.conf文件->daemonize:no 改为daemonize:yse;

6、在/bin目录下通过./redis-server redis.conf启动redis(此时为后台启动)。

综上redis单机版安装启动完成。

具体详细带图步骤请参考上篇文章

请原谅我的啰嗦,ok,接着咱们回到本次话题—-redis集群搭建!

一、Redis Cluster(Redis集群)简介

  1. redis是一个开源的key value存储系统,受到了广大互联网公司的青睐。redis3.0版本之前只支持单例模式,在3.0版本及以后才支持集群,我这里用的是redis3.0.0版本;
  2. redis集群采用P2P模式,是完全去中心化的,不存在中心节点或者代理节点;
  3. redis集群是没有统一的入口的,客户端(client)连接集群的时候连接集群中的任意节点(node)即可,集群内部的节点是相互通信的(PING-PONG机制),每个节点都是一个redis实例;
  4. 为了实现集群的高可用,即判断节点是否健康(能否正常使用),redis-cluster有这么一个投票容错机制:如果集群中超过半数的节点投票认为某个节点挂了,那么这个节点就挂了(fail)。这是判断节点是否挂了的方法;
  5. 那么如何判断集群是否挂了呢? -> 如果集群中任意一个节点挂了,而且该节点没有从节点(备份节点),那么这个集群就挂了。这是判断集群是否挂了的方法;
  6. 那么为什么任意一个节点挂了(没有从节点)这个集群就挂了呢?-> 因为集群内置了16384个slot(哈希槽),并且把所有的物理节点映射到了这16384[0-16383]个slot上,或者说把这些slot均等的分配给了各个节点。当需要在Redis集群存放一个数据(key-value)时,redis会先对这个key进行crc16算法,然后得到一个结果。再把这个结果对16384进行求余,这个余数会对应[0-16383]其中一个槽,进而决定key-value存储到哪个节点中。所以一旦某个节点挂了,该节点对应的slot就无法使用,那么就会导致集群无法正常工作。

综上所述,每个Redis集群理论上最多可以有16384个节点。

二、集群搭建需要的环境

2.1 Redis集群至少需要3个节点,因为投票容错机制要求超过半数节点认为某个节点挂了该节点才是挂了,所以2个节点无法构成集群。

2.2 要保证集群的高可用,需要每个节点都有从节点,也就是备份节点,所以Redis集群至少需要6台服务器。因为我没有那么多服务器,也启动不了那么多虚拟机,所在这里搭建的是伪分布式集群,即一台服务器虚拟运行6个redis实例,修改端口号为(7001-7006),当然实际生产环境的Redis集群搭建和这里是一样的。

2.3 安装ruby

三、集群搭建具体步骤如下(注意要关闭防火墙)

3.1 在usr/local目录下新建redis-cluster目录,用于存放集群节点

新建Redis集群目录

图片.png
3.2 把redis目录下的bin目录下的所有文件复制到/usr/local/redis-cluster/redis01目录下,不用担心这里没有redis01目录,会自动创建的。操作命令如下(注意当前所在路径):

cp -r redis/bin/ redis-cluster/redis01

图片.png
3.3 删除redis01目录下的快照文件dump.rdb,并且修改该目录下的redis.cnf文件,具体修改两处地方:一是端口号修改为7001,二是开启集群创建模式,打开注释即可。分别如下图所示:
删除dump.rdb文件

图片.png
修改端口号为7001,默认是6379

图片.png
将cluster-enabled yes 的注释打开

图片.png
3.4 将redis-cluster/redis01文件复制5份到redis-cluster目录下(redis02-redis06),创建6个redis实例,模拟Redis集群的6个节点。然后将其余5个文件下的redis.conf里面的端口号分别修改为7002-7006。分别如下图所示:
创建redis02-06目录

图片.png
分别修改redis.conf文件端口号为7002-7006

图片.png
3.5 接着启动所有redis节点,由于一个一个启动太麻烦了,所以在这里创建一个批量启动redis节点的脚本文件,命令为start-all.sh,文件内容如下:

cd redis01

./redis-server redis.conf

cd ..

cd redis02

./redis-server redis.conf

cd ..

cd redis03

./redis-server redis.conf

cd ..

cd redis04

./redis-server redis.conf

cd ..

cd redis05

./redis-server redis.conf

cd ..

cd redis06

./redis-server redis.conf

cd ..

3.6 创建好启动脚本文件之后,需要修改该脚本的权限,使之能够执行,指令如下:

chmod +x start-all.sh

图片.png
3.7 执行start-all.sh脚本,启动6个redis节点

图片.png
3.8 ok,至此6个redis节点启动成功,接下来正式开启搭建集群,以上都是准备条件。大家不要觉得图片多看起来冗长所以觉得麻烦,其实以上步骤也就一句话的事情:创建6个redis实例(6个节点)并启动。
要搭建集群的话,需要使用一个工具(脚本文件),这个工具在redis解压文件的源代码里。因为这个工具是一个ruby脚本文件,所以这个工具的运行需要ruby的运行环境,就相当于java语言的运行需要在jvm上。所以需要安装ruby,指令如下:

yum install ruby

然后需要把ruby相关的包安装到服务器,我这里用的是redis-3.0.0.gem,大家需要注意的是:redis的版本和ruby包的版本最好保持一致。

将Ruby包安装到服务器:需要先下载再安装,如图

图片.png
安装命令如下:

gem install redis-3.0.0.gem

图片.png
3.9 上一步中已经把ruby工具所需要的运行环境和ruby包安装好了,接下来需要把这个ruby脚本工具复制到usr/local/redis-cluster目录下。那么这个ruby脚本工具在哪里呢?之前提到过,在redis解压文件的源代码里,即redis/src目录下的redis-trib.rb文件。

图片.png

图片.png
3.10 将该ruby工具(redis-trib.rb)复制到redis-cluster目录下,指令如下:

cp redis-trib.rb /usr/local/redis-cluster
然后使用该脚本文件搭建集群,指令如下:
./redis-trib.rb create –replicas 1 47.106.219.251:7001 47.106.219.251:7002 47.106.219.251:70
注意:此处大家应该根据自己的服务器ip输入对应的ip地址!

图片.png
中途有个地方需要手动输入yes即可

图片.png
至此,Redi集群搭建成功!大家注意最后一段文字,显示了每个节点所分配的slots(哈希槽),这里总共6个节点,其中3个是从节点,所以3个主节点分别映射了0-5460、5461-10922、10933-16383solts。

3.11 最后连接集群节点,连接任意一个即可:

redis01/redis-cli -p 7001 -c
注意:一定要加上-c,不然节点之间是无法自动跳转的!如下图可以看到,存储的数据(key-value)是均匀分配到不同的节点的:

图片.png

四、结语

呼长舒一口气…终于搭建好了Redis集群。
整个过程其实挺简单,本篇主要正对入门级别的小伙伴,插入了很多图片,所以显得冗长,希望大家多多理解,如果不当之处,还望及时指正

最后,加上两条redis集群基本命令:

1.查看当前集群信息

cluster info
2.查看集群里有多少个节点
cluster nodes

原文:blog.csdn.net/qq_42815754…

Java面试百分百.jpg

本文转载自: 掘金

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

SpringBoot与RabbitMQ整合,发送和接收消息实

发表于 2021-05-04

SpringBoot与RabbitMQ整合入门案例的码云仓库地址:
gitee.com/qinstudy/sp…

1、RabbitMQ 消息中间件

大榜:小汪你怎么了,我看你脸色不太好呀。

小汪:最近团队正在使用消息队列实现业务模块之间的解耦,昨天熬了一晚上,也是云里雾里。哎,都快熬成熊猫眼了。

大榜:哦哦。消息队列的使用,可以对多个模块之间进行解耦,并实现异步通信,降低系统整体的响应时间。你们团队的想法挺好的,那采用的是哪种消息队列呢?

小汪:昨天上网搜索了半天,有ActiveMQ、RabbitMQ、RocketMQ、Kafka这四种,它们之间的区别,我当时还找了一张图,我发过来给你看看,帮我参谋下

消息中间件的对比.png

大榜:上面这四种消息队列的对比,我之前也看过,你们公司100多号人,应该属于中小型公司,使用RabbitMQ作为消息队列应该是很好的选择,而且RabbitMQ社区很活跃,我上次使用RabbitMQ作为消息中间件,发生了消息丢失的现象,最后依靠RabbitMQ社区的力量解决的。

小汪:可以啊,榜哥,教教我。我现在脑子里面只知道有几个概念:生产者、消费者、消息模型啥的

2、RabbitMQ核心概念(类比实际生活场景)

大榜:嗯嗯,你说的很对,RabbitMQ的核心概念就是生产者、消费者、消息模型,而消息模型是由交换机、路由、队列组成。

小汪:你这么一说,我想起来了,昨晚的书里面介绍了交换机、路由的概念,但我还是觉得太抽象了,不太好理解。

大榜:确实不好理解,我刚开始学习时,也是不理解,后来我想到了一个实际生活中寄快递、取快递的场景,正好可以解释交换机、路由、队列。我们就举个栗子,假设生活在武汉的小芹,想寄一个爱心包裹,发送给远在深圳的你(小汪)。

小汪:小芹给我寄爱心包裹,说得我心里暖暖的。哎,但实际情况是我寄包裹给小芹,呜呜呜。

大榜:小汪,面包会有的,牛奶也会有的,只要我们向阳成长。扯远了,我们假设小芹寄爱心包裹给你(小汪)。具体流程应该如下:小芹将包裹给快递员,然后快递员通过某种运输方式将包裹从武汉运到深圳,小汪开开心心地从深圳快递点取走爱心包裹。

类比下,可以得到如下的关系:

小芹:生产者

爱心包裹:消息

快递员:交换机

某种运输方式:路由

武汉到深圳:队列名称

小汪:消费者

总的来说,小芹将包裹给快递员,然后快递员通过某种运输方式将包裹从武汉运到深圳,小汪开开心心地从深圳快递点取走爱心包裹。我们就可以看成:生产者产生消息,然后消息到达交换机,再通过路由,将消息发送至指定的队列中,最后消费者监听该队列中的消息,进行消费处理。


小汪:你这一说,我好像有点懂了。寄快递、取快递这种实际生活场景,可以看成如下的消息传输过程,生产者将消息交给交换机,然后交换机通过路由,将消息发送武汉到深圳的队列,消费者取出该条消息进行处理。RabbitMQ中生产者、消息、交换机、路由、队列、消费者之间的关系,可以画成下面这张图:
RabbitMQ的生产者-消息模型-消费者.png

大榜:嗯嗯,你这理解力可以啊!

小汪:过奖过奖,这都是昨晚熬成熊猫眼的成果,而且刚刚和你讨论后,感觉进一步理解了RabbitMQ的概念。我现在懂了RabbitMQ的生产者、消息模型、消费者的大致原理,但团队要搭建一个RabbitMQ与SpringBoot整合的一个框架,昨晚搭建了半宿,也没干完,我这一时半天也搭不出来了,下午开周例会,等待被老大捶了,呜呜呜。

3、RabbitMQ发送和接收消息实战

大榜:RabbitMQ与SpringBoot整合,不算很难,因为SpringBoot是集成了Spring的,比Spring框架更灵活。

3.1、pom文件中引入RabbitMQ的起步依赖

我们只需要在pom文件中引入RabbitMQ的起步依赖

1
2
3
4
5
6
xml复制代码  <!-- RabbitMQ的起步依赖,和spring-boot整合成一个Jar包了 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
<version>1.5.6.RELEASE</version>
</dependency>

3.2、配置RabbitMQ的host、端口等信息

在application.properties配置文件中,增加host主机地址、port端口号、用户名、密码

1
2
3
4
5
6
7
xml复制代码# RabbitMQ配置
spring.rabbitmq.virtual-host=/
# 配置host、端口号、用户名、密码
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest

接着,在配置文件中声明RabbitMQ的队列、交换机、路由的信息:

1
2
3
4
5
6
7
8
xml复制代码# 设置交换机、路由、队列,使用directExchange消息模型
# 自定义变量,表示本地开发环境
mq.env=local

# 设置direct消息模型中队列、交换机、路由
mq.basic.info.queue.name=${mq.env}.middleware.mq.basic.info.queue.demo1
mq.basic.info.exchange.name=${mq.env}.middleware.mq.basic.info.exchange.demo1
mq.basic.info.routing.key.name=${mq.env}.middleware.mq.basic.info.routing.key.demo1

小汪:榜哥,3.1中在pom文件引入RabbitMQ的依赖,3.2中在配置文件里面设置了RabbitMQ服务器的地址为localhost,用户名、密码都为guest。这个我懂,然后你设置了队列、交换机、路由的名称,再往下应该要把队列、交换机、路由注入到Spring容器中进行管理,是吧?

大榜:是的,思维转得很快嘛。RabbitsMQ在实际项目的应用过程中,如果配置和使用不当,则会出现各种令人头疼的问题,而且面试官经常考察的问题:如何防止消息丢失?如何保证消费不被重复消费?我当时就出现了消息丢失,熬出了好几个熊猫眼才解决消息丢失的问题。

小汪:榜哥,别卖关子了,你要是和盘托出,小弟请你喝冰可乐!

3.3、自定义注入Bean组件

大榜:我当时查阅和学习了RabbitMQ的相关教程和视频,为了保证消息的高可用和确认消费,RabbitMQ官方给我们提供了三条准则。也就是如果我们想要保证消息的高可用和确认消费,需要遵守这3条准则:生产者的发送确认机制;创建队列、交换机消息时设置持久化模式;消费者的确认消费ACK机制。

小汪:榜哥牛逼啊,堪称RabbitMQ实战经验的总结!</font>

大榜:别给我带高帽子,我只是比你早学习RabbitMQ半年。上面3条准则对应的代码,我之前配置好了,可以直接拿来用。项目链接放在码云仓库:
gitee.com/qinstudy/sp…

3.3.1、生产者的发送确认机制

RabbitMQ要求生产者在发送完消息之后进行”发送确认“,当生产者确认成功时即代表消息已经完成发送出去了!其中的代码如下:

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复制代码 /**
* 构建RabbitMQ发送消息的操作组件实例
* 生产者的发送确认机制
*/
@Bean(name = "rabbitMQTemplate")
public RabbitTemplate rabbitTemplate() {
// 生产者确认消息是否发送过去了
connectionFactory.setPublisherConfirms(true);

// 生产者发送消息后,返回反馈消息
connectionFactory.setPublisherReturns(true);

// 构建rabbitTemlate操作模板
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMandatory(true);

// 生产者发送消息后,如果发送成功,则打印“消息发送成功”的日志信息
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
log.info("消息发送成功:correlationData({}),ack({}),cause({})",correlationData,ack,cause);
}
});

// 生产者发送消息后,若发送失败,则输出“消息发送失败”的日志信息
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
log.info("消息丢失:exchange({}),route({}),replyCode({}),replyText({}),message:{}",exchange,routingKey,replyCode,replyText,message);
}
});

return rabbitTemplate;
}

3.3.2、创建队列、交换机、消息,设置持久化模式

第二条准则,就是如何保证RabbitMQ队列中的消息”不丢失“,RabbitMQ官方建议开发者在创建队列、交换机设置持久化参数为true,也就是durable参数的值为true。同时,官方强烈建议开发者在创建消息时设置消息的持久化模式为”持久化“,这样就可以保证RabbitMQ服务器崩溃并执行重启操作后,队列、交换机仍然存在,而且该消息不会丢失。

创建队列、交换机设置持久化参数为true,也就是durable参数的值为true,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码 /**
* 创建direct消息模型:队列、交换机、路由
*/
// 1.1、创建队列
@Bean(name = "basicQueue")
public Queue basicQueue() {
return new Queue(env.getProperty("mq.basic.info.queue.name"), true);
}

// 1.2、创建交换机
@Bean
public DirectExchange basicExchange() {
return new DirectExchange(env.getProperty("mq.basic.info.exchange.name"), true, false);
}

// 1.3、创建绑定关系
@Bean
public Binding basicBinding() {
return BindingBuilder.bind(basicQueue()).to(basicExchange()).with(env.getProperty("mq.basic.info.routing.key.name"));
}

在创建消息时设置消息的持久化模式为”持久化“,代码如下:

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复制代码@Component
@Slf4j
public class BasicPublisher {

@Autowired
private RabbitTemplate rabbitTemplate;

@Autowired
Environment env;

// 发送字符串类型的消息
public void sendMsg(String messageStr) {
if (!Strings.isNullOrEmpty(messageStr)) {
try {
rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
rabbitTemplate.setExchange(env.getProperty("mq.basic.info.exchange.name"));
rabbitTemplate.setRoutingKey(env.getProperty("mq.basic.info.routing.key.name"));

// 2创建队列、交换机、消息 设置持久化模式
// 设置消息的持久化模式
Message message = MessageBuilder.withBody(messageStr.getBytes("utf-8")).
setDeliveryMode(MessageDeliveryMode.PERSISTENT).build();
rabbitTemplate.convertAndSend(message);
log.info("基本消息模型-生产者-发送消息:{}", messageStr);

} catch (UnsupportedEncodingException e) {
log.error("基本消息模型-生产者-发送消息发生异常:{}", messageStr, e.fillInStackTrace());
}
}
}

}

3.3.3、消费者的确认消费ACK机制

消费者的确认消费机制有三种:None、Auto、Manual

None:不进行确认消息,也就是消费者发送任何反馈信息给MQ服务端;

Auto:消费者自动确认消费。消费者处理该消息后,需要发送一个自动的ack反馈信息给MQ服务端,之后该消息从MQ的队列中移除掉。其底层的实现逻辑是由RabbitMQ内置的相关组件实现自动发送确认反馈信息。

Manual:人为手动确认消费机制。消费者处理该消息后,需要手动地“以代码的形式”发送给一个ack反馈信息给MQ服务端。

RabbitmqConfig#listenerContainer(),代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码 /**
* 设置单个消费者
* 消费者的Ack确认机制为AUTO
*/
@Bean(name = "singleListenerContainer")
public SimpleRabbitListenerContainerFactory listenerContainerFactory() {
SimpleRabbitListenerContainerFactory containerFactory = new SimpleRabbitListenerContainerFactory();
containerFactory.setConnectionFactory(connectionFactory);
containerFactory.setMessageConverter(new Jackson2JsonMessageConverter());

// 设置消费者的个数
containerFactory.setConcurrentConsumers(1);
// 设置消费者的最大值
containerFactory.setMaxConcurrentConsumers(1);
// 设置消费者每次拉取的消息数量,即消费者一次拉取几条消息
containerFactory.setPrefetchCount(1);

// 设置确认消息模型为自动确认消费AUTO,目的是防止消息丢失和消息被重复消费
containerFactory.setAcknowledgeMode(AcknowledgeMode.AUTO);
return containerFactory;
}

小汪:哦哦,我懂了。为了保证消息的高可用和确认消费,需要遵守这3条准则。3.3.1是生产者的发送确认机制;3.3.2中的代码是告诉我们创建队列、交换机消息时,要设置持久化模式;3.3.3是消费者的确认消费ACK机制。那RabbitMQ的发送、接收代码该如何实现呢?

大榜:上面我们已经配置了RabbitMQ的队列、交换机、路由,而且为了保证消息的高可用和确认消费,我们遵守3条准则:生产者的发送确认机制;创建队列、交换机消息时设置持久化模式;消费者的确认消费ACK机制。我们解决了大难点,发送、接收代码实现就简单了。

3.4、RabbitMQ发送、接收实战

3.4.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
java复制代码@Component
@Slf4j
public class BasicPublisher {

@Autowired
private RabbitTemplate rabbitTemplate;

@Autowired
Environment env;

// 发送字符串类型的消息
public void sendMsg(String messageStr) {
if (!Strings.isNullOrEmpty(messageStr)) {
try {
rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
rabbitTemplate.setExchange(env.getProperty("mq.basic.info.exchange.name"));
rabbitTemplate.setRoutingKey(env.getProperty("mq.basic.info.routing.key.name"));

// 2创建队列、交换机、消息 设置持久化模式
// 设置消息的持久化模式
Message message = MessageBuilder.withBody(messageStr.getBytes("utf-8")).
setDeliveryMode(MessageDeliveryMode.PERSISTENT).build();
rabbitTemplate.convertAndSend(message);
log.info("基本消息模型-生产者-发送消息:{}", messageStr);

} catch (UnsupportedEncodingException e) {
log.error("基本消息模型-生产者-发送消息发生异常:{}", messageStr, e.fillInStackTrace());
}
}
}

}

3.4.2、创建消费者

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

/**
* 监听并消费队列中的消息
*/
@RabbitListener(queues = "${mq.basic.info.queue.name}", containerFactory = "singleListenerContainer")
public void consumerMsg(@Payload byte[] msg) {
try {
String messageStr = new String(msg, "utf-8");
log.info("基本消息模型-消费者-监听并消费到的消息:{}", messageStr);

} catch (UnsupportedEncodingException e) {

log.error("基本消息模型-消费者-发生异常:", e.fillInStackTrace());
}
}


}

3.4.3、编写单元测试,将一串字符串发送到队列中,消费者监听并处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@Slf4j
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class RabbitMQTest {

@Autowired
private BasicPublisher basicPublisher;

// 测试 基本消息模型,消息内容为 字符串
@Test
public void testBasicMessageModel() {
String msgStr = "~~~这是一串字符串消息~~~~";
basicPublisher.sendMsg(msgStr);
}
}

3.4.4、单元测试的结果分析

对编写的单元测试,结果如下图:
单元测试.png

从图中可以看到,生产者发送了一条消息:”这是一串字符串消息“,放到了消息队列中;消费者监听并处理了该条消息,然后消费者打印了该消息内容。

小汪:懂了,对于我这样的刚刚接触RabbitMQ的来说,都是干货啊。榜哥,我梳理下,看看理解的对不对。首先,我们从团队的业务需要实现解耦说起,引出了消息中间件;接着比较了4种消息队列的区别,根据公司的实际情况选择了RabbitMQ;然后将生产者、消息、交换机、路由、队列、消费者类比于寄快递、取快递这一实际场景(小芹给小汪的寄快递);最后,我们将RabbitMQ与流行的SpringBoot整合起来,遵守3条准则,保证RabbitMQ的高可用和确认消费,并实现了RabbitMQ的发送和接收消息实战。

大榜:是的,看来你已经入门了RabbitMQ,RabbitMQ发送和接收消息实战,该项目链接放在码云仓库,你为团队引入RabbitMQ时可以直接拿来用,链接地址:
gitee.com/qinstudy/sp…

小汪:昨晚看书,RabbitMQ还有死信队列的概念,能解下惑吗?

大榜:到饭点了,要不咱们先吃个午饭,边吃边聊…..

本文转载自: 掘金

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

你真的了解Python中的类class?

发表于 2021-05-04

概述

在Python的类中,有着类属性、实例属性,静态方法、类方法、实例方法的区别。到底有什么不一样呢?接下来我们就一探究竟。

类属性、实例属性

来看下简单的 Student 类的例子

1
2
3
4
5
6
7
8
9
10
python复制代码
class Student(object):

# 类属性
school = '井冈山大学'

def __init__(self, name):

# 实例属性
self.name = name

其中 school 是 Student 类的类属性,name 则是实例属性。

在 ipython 中测试一下如何访问其属性

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
python复制代码In [5]: stu1 = Student('hui')

In [6]: stu2 = Student('wang')

In [7]: stu3 = Student('zack')

In [8]: stu1.name, Student.school
Out[8]: ('hui', '井冈山大学')

In [9]: stu2.name, Student.school
Out[9]: ('wang', '井冈山大学')

In [10]: stu3.name, Student.school
Out[10]: ('zack', '井冈山大学')

# 看看实例对象能不能访问类属性,类对象能不能访问实例属性
In [11]: stu1.name, stu1.school
Out[11]: ('hui', '井冈山大学')

In [12]: Student.name, stu1.school
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-12-b897e001b174> in <module>
----> 1 Student.name, stu1.school

AttributeError: type object 'Student' has no attribute 'name'

经过测试可以发现 实例属性需要通过实例对象来访问,类属性通过类来访问,但在测验中 stu1.school 实例对象也能访问类属性,为什么呢?

其实,实例对象也是间接的通过类对象进行访问的,在每一个实例对象中都有一个 __class__ 的属性,其指向的就是创建实例对象的类对象。stu1.__class__ 的指向就是 Student类对象。然后实例对象访问属性的规则是先访问实例属性,然后再根据实例对象的 __class__ 来访问类属性。如果都没有找到则报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
python复制代码In [15]: dir(stu1)
Out[15]:
['__class__',
'__delattr__',
'__dict__',
'__dir__',

....

'name',
'school']

In [16]: stu1.__class__
Out[16]: __main__.Student

In [17]: stu1.__class__.school
Out[17]: '井冈山大学'

In [18]: id(Student)
Out[18]: 2011692023944

In [19]: id(stu1.__class__)
Out[19]: 2011692023944

可以看出 Student,stu1.__class__ 的 id() 都一样,说明其内存地址都一样。因此实例属性可以通过 __class__ 访问类属性。

存储方式如下图

类对象派生实例对象

由上图可以看出:

  • 类属性在内存中只保存一份
  • 实例属性在每个对象中都要保存一份

还是以上面的例子在 ipython 中对类属性的修改进行测验

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
python复制代码In [24]: class Student(object):
...:
...: # 类属性
...: school = '井冈山大学'
...:
...: def __init__(self, name):
...:
...: # 实例属性
...: self.name = name
...:

In [25]: stu1 = Student('hui')

In [26]: stu2 = Student('jack')

In [27]: stu1.name, stu1.school
Out[27]: ('hui', '井冈山大学')

In [28]: stu2.name, stu2.school
Out[28]: ('jack', '井冈山大学')

# 通过类对象进行修改
In [29]: Student.school = '清华大学'

In [30]: stu2.name, stu2.school
Out[30]: ('jack', '清华大学')

In [31]: stu1.name, stu1.school
Out[31]: ('hui', '清华大学')

# 通过实例对象进行修改
IIn [33]: stu1.school = '北京大学'

In [34]: stu1.name, stu1.school
Out[34]: ('hui', '北京大学')

In [35]: stu2.name, stu2.school
Out[35]: ('jack', '清华大学')

In [36]: Student.school
Out[36]: '清华大学'

In [37]: stu1.__class__.school
Out[37]: '清华大学'

In [39]: id(stu2.school)
Out[39]: 2011720409808

In [40]: id(Student.school)
Out[40]: 2011720409808

In [41]: id(stu1.school)
Out[41]: 2011720494992

# 通过实例对象的__class__属性修改
IIn [42]: stu2.__class__.school = '井冈山大学'

In [43]: stu1.name, stu1.school
Out[43]: ('hui', '北京大学')

In [44]: stu2.name, stu2.school
Out[44]: ('jack', '井冈山大学')

In [45]: Student.school
Out[45]: '井冈山大学'

说明: 实例对象.类属性 = xxx 并没有修改到其类属性,而是在实例对象中创建了一个与类属性同名的实例属性。因此修改类属性,应该使用类对象进行修改。再外界最好不要使用 实例对象.新属性 = xxx,动态创建实例属性。

使用场景

到底是用类属性,还是实例属性?

如果每个实例对象需要具有相同值的属性,那么就使用类属性,用一份既可。

1
2
3
4
5
6
7
8
9
10
11
python复制代码
class Province(object):
# 类属性
country = '中国'

def __init__(self, name):
# 实例属性
self.name = name

p1 = Province('江西省')
p2 = Province('四川省')

实例方法、静态方法和类方法

类中方法包括:实例方法、静态方法和类方法,三种方法在内存中都归属于类,区别在于调用方式不同。

  • 实例方法:由对象调用,至少一个 self 参数;执行实例方法时,自动将调用该方法的对象赋值给 self。
  • 类方法:由类调用,至少一个 cls 参数;执行类方法时,自动将调用该方法的类赋值给 cls。
  • 静态方法:由类调用,无默认参数。
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
python复制代码
class Foo(object):

foo = 'Foo'

def __init__(self, name):
self.name = name

def instance_func(self):
print(self.name)
print(self.foo)
print('实例方法')

@classmethod
def class_func1(cls):
print(cls.foo)
print('类方法1')

@classmethod
def class_func2(cls):
print(cls.name)
print('类方法二')

@staticmethod
def static_func():
print('静态方法')

其中 @classmethod 是装饰器,说明这是类方法,@staticmethod 则说明是静态方法。关于装饰器的内容这里就不在赘述了。

在 ipython 中测验一下各方法

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
python复制代码# 实例对象调用
In [71]: f = Foo('hui')

In [72]: f.instance_func()
hui
Foo
实例方法

In [73]: f.class_func1()
Foo
类方法1

In [74]: f.class_func2()
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-74-7d161e9e60ec> in <module>
----> 1 f.class_func2()

<ipython-input-60-7fc48649a96a> in class_func2(cls)
18 @classmethod
19 def class_func2(cls):
---> 20 print(cls.name)
21 print('类方法二')
22

AttributeError: type object 'Foo' has no attribute 'name'

In [75]: f.static_func()
静态方法

# 类对象自身调用
In [76]: Foo.instance_func()
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-76-883efcb56130> in <module>
----> 1 Foo.instance_func()

TypeError: instance_func() missing 1 required positional argument: 'self'

In [77]: Foo.class_func1()
Foo
类方法1

In [78]: Foo.static_func()
静态方法

可以发现实例对象三种方法都可以调用,但 cls 类对象不能访问实例属性。类对象不能直接调用实例方法,类、静态方法可以。

self与cls的区别

  • self 指的是类实例对象本身(注意:不是类本身)。
  • cls 指的是类对象本身
  • self 可以访问到类属性、实例属性,cls 只能访问类属性。

其中 self, cls 只是代指实例对象和类对象,因此换成其他变量也可以,但是约定成俗(为了和其他编程语言统一,减少理解难度),不要搞另类,大家会不明白的。

使用场景

需要操作类属性的定义成类方法。

需要操作实例属性的定义成实例方法。

既不需要操作类属性,也不需要操作实例属性就定义成静态方法。

公众号

新建文件夹X

大自然用数百亿年创造出我们现实世界,而程序员用几百年创造出一个完全不同的虚拟世界。我们用键盘敲出一砖一瓦,用大脑构建一切。人们把1000视为权威,我们反其道行之,捍卫1024的地位。我们不是键盘侠,我们只是平凡世界中不凡的缔造者 。

本文转载自: 掘金

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

Kotlin 编程

发表于 2021-05-04

⭐️ 本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 和 [BaguTree Pro] 知识星球提问。

在 Android 生态中主要有 C++、Java、Kotlin 三种语言 ,它们的关系不是替换而是互补。其中,C++ 的语境是算法和高性能,Java 的语境是平台无关和内存管理,而 Kotlin 则融合了多种语言中的优秀特性,带来了一种更现代化的编程方式。

本文是 Kotlin 编程与跨平台系列的第 2 篇文章,完整文章目录请移步到文章末尾~


前言

  • 委托(Delegate)是 Kotlin 的一种语言特性,用于更加优雅地实现委托模式;
  • 在这篇文章里,我将总结 Kotlin 委托机制的使用方法 & 原理,如果能帮上忙,请务必点赞加关注,这真的对我非常重要。
  • 本文相关代码可以从 DemoHall·KotlinDelegate 下载查看。

目录


  1. 概述

  • 什么是委托: 一个对象将消息委托给另一个对象来处理。
  • Kotlin 委托解决了什么问题: Kotlin 通过 by 关键字可以更加优雅地实现委托。

  1. Kotlin 委托基础

  • 类委托: 一个类的方法不在该类中定义,而是直接委托给另一个对象来处理。
  • 属性委托: 一个类的属性不在该类中定义,而是直接委托给另一个对象来处理。
  • 局部变量委托: 一个局部变量不在该方法中定义,而是直接委托给另一个对象来处理。

2.1 类委托

Kotlin 类委托的语法格式如下:

1
csharp复制代码class <类名>(b : <基础接口>) : <基础接口> by <基础对象>

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
kotlin复制代码// 基础接口
interface Base {
fun print()
}

// 基础对象
class BaseImpl(val x: Int) : Base {
override fun print() { print(x) }
}

// 被委托类
class Derived(b: Base) : Base by b

fun main(args: Array<String>) {
val b = BaseImpl(10)
Derived(b).print() // 最终调用了 Base#print()
}

基础类和被委托类都实现同一个接口,编译时生成的字节码中,继承自 Base 接口的方法都会委托给基础对象处理。

2.2 属性委托

Kotlin 属性委托的语法格式如下:

1
kotlin复制代码val/var <属性名> : <类型> by <基础对象>

举例:

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
kotlin复制代码class Example {
// 被委托属性
var prop: String by Delegate() // 基础对象
}

// 基础类
class Delegate {
private var _realValue: String = "彭"

operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
println("getValue")
return _realValue
}

operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("setValue")
_realValue = value
}
}

fun main(args: Array<String>) {
val e = Example()
println(e.prop) // 最终调用 Delegate#getValue()
e.prop = "Peng" // 最终调用 Delegate#setValue()
println(e.prop) // 最终调用 Delegate#getValue()
}

输出:
getValue
彭
setValue
getValue
Peng

基础类不需要实现任何接口,但必须提供 getValue() 方法,如果是委托可变属性,还需要提供 setValue()。在每个属性委托的实现的背后,Kotlin 编译器都会生成辅助属性并委托给它。 例如,对于属性 prop,会生成「辅助属性」 prop$delegate。 而 prop 的 getter() 和 setter() 方法只是简单地委托给辅助属性的 getValue() 和 setValue() 处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
kotlin复制代码源码:
class Example {
// 被委托属性
var prop: String by Delegate() // 基础对象
}

--------------------------------------------------------
编译器生成的字节码:
class Example {
private val prop$delegate = Delegate()
// 被委托属性
var prop: String
get() = prop$delegate.getValue(this, this:prop)
set(value : String) = prop$delegate.setValue(this, this:prop, value)
}

注意事项:

  • thisRef —— 必须与属性所有者类型相同或者是它的超类型。
  • property —— 必须是类型 KProperty<*> 或其超类型。
  • value —— 必须和属性同类型或者是它的超类型。

2.3 局部变量委托

局部变量也可以声明委托,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kotlin复制代码fun main(args: Array<String>) {
val lazyValue: String by lazy {
println("Lazy Init Completed!")
"Hello World."
}

if (true/*someCondition*/) {
println(lazyValue) // 首次调用
println(lazyValue) // 后续调用

}
}
输出:
Lazy Init Completed!
Hello World.
Hello World.

  1. Kotlin 委托进阶

3.1 延迟属性委托 lazy

lazy 是一个标准库函数,参数为一个 Lambda 表达式,返回值为一个 Lazy 实例,使用 lazy 可以实现延迟属性委托,在委托对象比较耗资源的场景会非常有用。首次访问属性是,会执行 lazy 函数的 lambda 表达式并将结果记录到「背域」,后续调用 getter() 方法只是直接返回「背域」的值。 例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kotlin复制代码val lazyValue: String by lazy {
println("Lazy Init Completed!")
"Hello World."
}

fun main(args: Array<String>) {
println(lazyValue) // 首次调用
println(lazyValue) // 后续调用
}

输出:
Lazy Init Completed!
Hello World.
Hello World.

3.2 可观察属性 ObservableProperty

使用 Delegates.observable() 可以实现可观察属性,函数接受两个参数:第一个参数为初始值,第二个参数为属性值变化的回调。函数的返回值是 ObservableProperty 可观察属性,它在调用 setValue(…) 是触发回调。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
kotlin复制代码class User {
var name: String by Delegates.observable("初始值") { prop, old, new ->
println("旧值:$old -> 新值:$new")
}
}

fun main(args: Array<String>) {
val user = User()
user.name = "第一次赋值"
user.name = "第二次赋值"
}

输出:
旧值:初始值 -> 新值:第一次赋值
旧值:第一次赋值 -> 新值:第二次赋值

3.3 使用 Map 存储属性值

Map / MutableMap 也可以用来实现属性委托,从而此时字段名是 Key,属性值是 Value。例如;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kotlin复制代码class User(val map: Map<String, Any?>) {
val name: String by map
}

fun main(args: Array<String>) {
val map = mutableMapOf(
"name" to "彭"
)
val user = User(map)
println(user.name)
map["name"] = "peng"
println(user.name)
}
输出:
彭
peng

不过,这里有一个坑:如果 Map 中不存在委托属性名的映射值,在取值的时候会抛异常:Key $key is missing in the map.。源码体现如下:

标准库·MapAccessors.kt

1
2
3
4
5
6
7
8
less复制代码@kotlin.jvm.JvmName("getVar")
@kotlin.internal.InlineOnly
public inline operator fun <V, V1 : V> MutableMap<in String, out @Exact V>.getValue(thisRef: Any?, property: KProperty<*>): V1 = (getOrImplicitDefault(property.name) as V1)

@kotlin.internal.InlineOnly
public inline operator fun <V> MutableMap<in String, in V>.setValue(thisRef: Any?, property: KProperty<*>, value: V) {
this.put(property.name, value)
}

标准库·MapWithDefault.kt

1
2
3
4
5
6
7
8
kotlin复制代码@kotlin.jvm.JvmName("getOrImplicitDefaultNullable")
@PublishedApi
internal fun <K, V> Map<K, V>.getOrImplicitDefault(key: K): V {
if (this is MapWithDefault)
return this.getOrImplicitDefault(key)

return getOrElseNullable(key, { throw NoSuchElementException("Key $key is missing in the map.") })
}

反正我是猜不透 Kotlin 官方为什么要加这个限制,所以项目里我不会直接使用标准库里的实现,而是采用以下自定义实现:

MapAccessors.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kotlin复制代码class MapAccessors(val map: MutableMap<String, Any?>) {

public inline operator fun <V> getValue(thisRef: Any?, property: KProperty<*>): V = @Suppress("UNCHECKED_CAST") (map[property.name] as V)

public inline operator fun <V> setValue(thisRef: Any?, property: KProperty<*>, value: V) {
map[property.name] = value
}
}

// 使用方法(其实用扩展函数语法更简洁,但考虑到编辑器不会帮我们导致自定义实现,所以故意套在 MapAccessors 内):

private val _data = MapAccessors(HashMap<String, Any?>())

private var count: Int? by _data

3.4 ReadOnlyProperty / ReadWriteProperty

实现属性委托或局部委托时,除了定义类 Delegate 外,还可以直接使用 Kotlin 标准库中的两个接口:ReadOnlyProperty / ReadWriteProperty。对于 val 变量使用 ReadOnlyProperty,而 var 变量实现ReadWriteProperty,使用这两个接口可以方便地让 IDE 帮你生成函数签名。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kotlin复制代码val name by object : ReadOnlyProperty<Any?, String> {
override fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "Peng"
}
}

var name by object : ReadWriteProperty<Any?, String> {
override fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "Peng"
}

override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
}
}

  1. 在 Android 中使用 Kotlin 委托

4.1 Kotlin 委托 + Fragment / Activity 传参

我们经常需要在 Activity / Fragment 之间传递参数,类似以下代码:

OrderDetailFragment.kt

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
kotlin复制代码class OrderDetailFragment : Fragment(R.layout.fragment_order_detail) {

private var orderId: Int? = null
private var orderType: Int? = null

companion object {

const val EXTRA_ORDER_ID = "orderId"
const val EXTRA_ORDER_TYPE = "orderType";

fun newInstance(orderId: Int, orderType: Int?) = OrderDetailFragment().apply {
Bundle().apply {
putInt(EXTRA_ORDER_ID, orderId)
if (null != orderType) {
putInt(EXTRA_ORDER_TYPE, orderType)
}
}.also {
arguments = it
}
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

arguments?.let {
orderId = it.getInt(EXTRA_ORDER_ID, 10000)
orderType = it.getInt(EXTRA_ORDER_TYPE, 2)
}
}
}

可以看到我们要为每个参数编写类似的模板代码,还要考虑参数为空的问题,而且 Int 基础类型不能使用 lateinit 关键字,你还不得不声明属性为可空类型,即使你可以确保它不会为空。

有没有办法收敛模板代码呢?这里就符合委托机制的应用场景了,我们可以把参数赋值和获取的代码抽取委托类,然后将 orderId 和 orderType 声明为「委托属性」。例如:

OrderDetailFragment.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
kotlin复制代码class OrderDetailFragment : Fragment(R.layout.fragment_order_detail) {

private lateinit var tvDisplay: TextView

private var orderId: Int by argument()
private var orderType: Int by argument(2)

companion object {
fun newInstance(orderId: Int, orderType: Int) = OrderDetailFragment().apply {
this.orderId = orderId
this.orderType = orderType
}
}

override fun onViewCreated(root: View, savedInstanceState: Bundle?) {
// Try to modify (UnExcepted)
this.orderType = 3
// Display Value
tvDisplay = root.findViewById(R.id.tv_display)
tvDisplay.text = "orderId = $orderId, orderType = $orderType"
}
}

干净清爽!相对于常规的写法,使用属性委托优势很明显:

  • 1、样板代码减少: 不再需要定义 Key 字符串,而是直接使用变量名作为 Key;不再需要编写向 Argument 设置参数和读取参数的代码;
  • 2、非空参数可以声明 val: 可空参数和非空参数区分两种委托,现在非空参数也可以声明为 val 了;
  • 3、清晰地设置可空参数默认值: 声明可空参数时可以顺便声明默认值。

除了 Fragment 传参,Activity 传参也可以使用委托属性。完整代码和演示工程你可以直接下载查看:下载路径,这里只展示部分核心代码如下:

ArgumentDelegate.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
kotlin复制代码fun <T> fragmentArgument() = FragmentArgumentProperty<T>()

class FragmentArgumentProperty<T> : ReadWriteProperty<Fragment, T> {

override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
return thisRef.arguments?.getValue(property.name) as? T
?: throw IllegalStateException("Property ${property.name} could not be read")
}

override fun setValue(thisRef: Fragment, property: KProperty<*>, value: T) {
val arguments = thisRef.arguments ?: Bundle().also { thisRef.arguments = it }
if (arguments.containsKey(property.name)) {
// The Value is not expected to be modified
return
}
arguments[property.name] = value
}
}

4.2 Kotlin 委托 + ViewBinding

ViewBinding 是 Android Gradle Plugin 3.6 中新增的特性,用于更加轻量地实现视图绑定,可以理解为轻量版本的 DataBinding。ViewBinding 的使用方法和实现原理都很好理解,但常规的使用方法存在一些局限性:

  • 1、创建和回收 ViewBinding 对象需要重复编写样板代码,特别是在 Fragment 中使用的案例;
  • 2、binding 属性是可空的,也是可变的,使用起来不方便。

使用 Kotlin 属性委托可以非常优雅地解决这两个问题,优化前后对比:

TestFragment.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
kotlin复制代码class TestFragment : Fragment(R.layout.fragment_test) {

private var _binding: FragmentTestBinding? = null

private val binding get() = _binding!!

override fun onViewCreated(root: View, savedInstanceState: Bundle?) {
_binding = FragmentTestBinding.bind(root)

binding.tvDisplay.text = "Hello World."
}

override fun onDestroyView() {
super.onDestroyView()

_binding = null
}
}

优化后:

TestFragment.kt

1
2
3
4
5
6
7
8
kotlin复制代码class TestFragment : Fragment(R.layout.fragment_test) {

private val binding by viewBinding(FragmentTestBinding::bind)

override fun onViewCreated(root: View, savedInstanceState: Bundle?) {
binding.tvDisplay.text = "Hello World."
}
}

干净清爽!详细分析过程你直接看我的另一篇文章:Android Jetpack 开发套件 #6 ViewBinding 与 Kotlin 委托双剑合璧


  1. 总结

Kotlin 委托的语法关键字是 by,其本质上是面向编译器的语法糖,三种委托(类委托、对象委托和局部变量委托)在编译时都会转化为 “无糖语法”。例如类委托:编译器会实现基础接口的所有方法,并直接委托给基础对象来处理。例如对象委托和局部变量委托:在编译时会生成辅助属性(prop$degelate),而属性 / 变量的 getter() 和 setter() 方法只是简单地委托给辅助属性的 getValue() 和 setValue() 处理。

参考资料

  • Kotlin 委托 —— 菜鸟教程 著
  • Kotlin 实战(第 7 章)—— [俄] DmitryJeme 著

推荐阅读

Kotlin 编程与跨平台系列完整目录如下(2023/07/11 更新):

  • #1 金三银四必备,全面总结 Kotlin 面试知识点
  • #2 委托机制 & 原理 & 应用
  • #3 扩展函数(终于知道为什么 with 用 this,let 用 it)

⭐️ 永远相信美好的事情即将发生,欢迎加入小彭的 Android 交流社群~

本文转载自: 掘金

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

再乱用缓存,cto可就发飙了!

发表于 2021-05-03

原创:小姐姐味道(微信公众号ID:xjjdog),欢迎分享,转载请保留出处。

今天总监很生气,原因是强调了很多年的缓存同步方案,硬是有人不按常理出牌,挑战权威。最终出了问题,这让总监很没面子。

“一群人拿着大老板的账号在哪里瞎JB测,结果把数据搞出问题来了吧”,总监牛鼻子里喷着气,叉着腰说。自从老板的账号有一次参与测试之后,就成了大家心照不宣的终极测试账号。很多人用,硬生生把一个账号的余额操作,给搞成了高并发的。

“老板的账号不就是个测试账号么…”,下面有人小声的嘀咕。

“现实中哪会有账号有这样的密集型操作的…”,又有人小声嘀咕,让总监的脸色越来越沉。

“你们觉得我在开玩笑么?”,总监红着眼说,“我就曾经因为这样的数据不一致问题,吃过一个一级故障。正好,今天就带你们了解一下,为什么会有数据不一致的情况吧”。

我扶着眼镜摇摇晃晃的做到台下,心中暗笑,总监又要把Cache Aside Pattern给科普一遍了。

开源一套以教学为目的系统,欢迎star:github.com/xjjdog/bcma…。它包含ToB复杂业务、互联网高并发业务、缓存应用;DDD、微服务指导。模型驱动、数据驱动。了解大型服务进化路线,编码技巧、学习Linux,性能调优。Docker/k8s助力、监控、日志收集、中间件学习。前端技术、后端实践等。主要技术:SpringBoot+JPA+Mybatis-plus+Antd+Vue3。

  1. 为什么数据会不一致?

数据库的瓶颈是大家有目共睹的,高并发的环境下,很容易I/O锁死。当务之急,就是把常用的数据,给捞到速度更快的存储里去。这个更快的存储,就有可能是分布式的,比如Redis,也有可能是单机的,比如Caffeine。

但一旦加入缓存,就不得不面对一个蛋疼的问题:数据的一致性。

数据不一致的问题,人世间多了去了。进修过Java多线程的同学,肯定会对JMM的模型记忆犹新。一个数值,只要同时在两个地方存储,那就会产生问题。

但缓存系统和数据库,比JMM更加的不可靠。因为分布式组件更加的脆弱,它随时都可能发生问题。

  1. Cache Aside Pattern

怎样保证数据在DB和缓存中的一致性呢?现在一个比较好的最佳实践方案,就是Cache Aside Pattern。

先来看一下数据的读取过程,规则是: 先读cache,再读db 。详细步骤如下:

  1. 每次读取数据,都从cache里读
  2. 如果读到了,则直接返回,称作 cache hit
  3. 如果读不到cache的数据,则从db里面捞一份,称作cache miss
  4. 将读取到的数据,塞入到缓存中,下次读取的时候,就可以直接命中

再来看一下写请求。规则是: 先更新db,再删除缓存 。详细步骤如下:

  1. 将变更写入到数据库中
  2. 删除缓存里对应的数据

说到这里,我看着有几个人皱起了眉头。我知道,肯定会有人不服气,认为自己那一套是对的。比如,为什么是删除缓存,不是更新缓存呢?效率会不会更低?为什么不先删除缓存再更新数据库?

好家伙,他们要向总监发问了。

  1. 为什么是删除缓存,而不是更新缓存?

这个比较好理解。当多个更新操作同时到来的时候,删除动作,产生的结果是确定的;而更新操作,则可能会产生不同的结果。

image.png

如图。两个请求A和B,请求B在请求A之后,数据是最新的。由于缓存的存在,如果在保存的时许发生稍许的偏差,就会造成A的缓存值覆盖了B的值,那么数据库中的记录值,和缓存中的就产生了不一致,直到下一次数据变更。

而使用删除的方式,由于缓存会miss,所以会每次都会从db中获取最新的数据进行填充,与缓存操作的时机关系不大。

image.png

  1. 为什么不先删缓存,再更新数据库?

这个问题是类似的。我们甚至都不需要并发写的场景就能发现问题。

我们上面提到的缓存删除动作,和数据库的更新动作,明显是不在一个事务里的。如果一个请求删除了缓存,同时有另外一个请求到来,此时发现没有相关的缓存项,就从数据库里加载了一份到缓存系统。接下来,数据库的更新操作也完成了,此时数据库的内容和缓存里的内容,就产生了不一致。

image.png

如上图,写请求首先删除了缓存。结果在这个时候,有其他的读请求,将数据库的旧值,读取到数据库中,此时缓存中的数据是0。接下来更新了DB,将数据库记录改为了100。经过这么一哆嗦,数据库和缓存中的数据,就产生了不一致。

大家都恍然大悟的点点头,不少人露出了迷之微笑。

  1. Spring中的缓存注解

使用 SpringBoot 可以很容易地对 Redis 进行操作,Java 的 Redis的客户端,常用的有三个:jedis、redisson 和 lettuce,Spring 默认使用的是 lettuce。

很多人,喜欢使用Spring 抽象的缓存包 spring-cache。

它使用注解,采用 AOP的方式,对 Cache 层进行了抽象,可以在各种堆内缓存框架和分布式框架之间进行切换。这是它的 maven 坐标:

1
2
3
4
xml复制代码<dependency> 
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

使用 spring-cache 有三个步骤:

  1. 在启动类上加入 @EnableCaching 注解;
  2. 使用 CacheManager 初始化要使用的缓存框架,使用 @CacheConfig 注解注入要使用的资源;
  3. 使用 @Cacheable 等注解对资源进行缓存。

而针对缓存操作的注解,有三个:

  1. @Cacheable 表示如果缓存系统里没有这个数值,就将方法的返回值缓存起来;
  2. @CachePut 表示每次执行该方法,都把返回值缓存起来;
  3. @CacheEvict 表示执行方法的时候,清除某些缓存值。

那么问题来了,spring-cache中的@CacheEvict注解,到底是先删缓存,还是后删缓存呢?不弄明白这一点,真的是让人夜不能寐。关键技术嘛,不仅要用的开心,也要用的放心。

缓存的移除,是在CacheAspectSupport中实现的,我们注意到下面的代码。

1
2
3
4
5
6
java复制代码// Process any early evictions
processCacheEvicts(contexts.get(CacheEvictOperation.class), true,
CacheOperationExpressionEvaluator.NO_RESULT);
...
// Process any late evictions
processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);

它有一个前置的清除动作,还有后置的清除动作,是通过一个bool变量boolean beforeInvocation进行设置的。这个值从哪里来的呢?还是得看@CacheEvict注解。

1
2
3
4
5
6
7
8
9
10
java复制代码/**
* Whether the eviction should occur before the method is invoked.
* <p>Setting this attribute to {@code true}, causes the eviction to
* occur irrespective of the method outcome (i.e., whether it threw an
* exception or not).
* <p>Defaults to {@code false}, meaning that the cache eviction operation
* will occur <em>after</em> the advised method is invoked successfully (i.e.,
* only if the invocation did not throw an exception).
*/
boolean beforeInvocation() default false;

很好很好,它的默认值是false,证明删除动作是滞后的,践行的也是Cache Aside Pattern。

  1. 还有其他模式?

我听说,还有Read Through Pattern,Write Through Pattern,Write Behind Caching Pattern等其他常见的缓存同步模式,为什么不用这些呢? 有位同学的屁股一直在椅子上来回挪动,跃跃欲试,逮住机会,他终于发言了。

其实,这些方式使用的也非常广泛,但由于对业务大多数是无感知的,所以很多人都忽略了。换句话说,这几个模式,大多数是在一些中间件,或者比较底层的数据库中实现的,写业务代码可能接触不到这些东西。

比如,Read Through,其实就是让你对读操作感知不到缓存层的存在。通常情况下,你会手动实现缓存的载入,但Read Through可能就有代理层给你捎带着做了。

再比如,Write Through,你不用再考虑数据库和缓存是不是同步了,代理层都给你做了,你只管往里塞数据就行。

Read Through和Write Through是不冲突的,它们可以同时存在,这样业务层的代码里就没有同步这个概念了。爽歪歪。

至于Write Behind Caching,意思就是先落地到缓存,然后有异步线程缓慢的将缓存中的数据落地到DB中。要用这个东西,你得评估一下你的数据是否可以丢失,以及你的缓存容量是否能够经得起业务高峰的考验。现在的操作系统、DB、甚至消息队列如Kafaka等,都会在一定程度上践行这个模式。

但它现在和我们的业务需求没半点关系。

  1. Cache Aside Pattern也有问题

总监上马,一个顶俩,科普了这半天,所有的同学都心服口服。正在大家想要把掌声送给总监的时候,一个不和谐的声音传来了。

我发现了一个天大的问题。 有同学说, 如果数据库更新成功了,但缓存删除失败了,也会造成缓存不一致。

这个问题问的好啊,故障大多数就是由于这些极端情况造成的。这个时候就有意思了,我们要拼概率,毕竟没有100%的安全套。 总监笑了。

方法一:将数据更新和缓存删除动作,放在一个事务里,同进退。

方法二:缓存删除动作失败后,重试一定的次数。如果还是不行,大概率是缓存服务的故障,这时候要记录日志,在缓存服务恢复正常的时候将这些key删除掉

方法三:再多一步操作,先删缓存,再更新数据,再删缓存。这样虽然操作多一些,但也更保险一些。

是不是没有问题了? 总监环顾四周,看到大家都在点头。No no no,依然还有数据不一致的情况。

所有人都一头雾水。

上面那张看起来正确的图,其实是错误的。为什么呢?因为数据在从数据库读到缓存中的操作,并不是原子性的。

image.png

比如上图,当缓存失效(或者被删除)的时候,有一个读请求正好到来。这个读请求,拿到了旧的数据库值,但它由于多方面的原因(比如网络抽风),没有立马写入到缓存中,而是发生了延迟。在它打算写入到缓存的这段时间,发生了很多事情,有另外一个请求,将数据库的值更新为200,并删除了缓存。

直到第二个请求全部完成,第一个请求写入缓存的操作,才真正落地。但其实,这时候数据库和缓存的值,已经不是同步的了。

那么为什么大家在平常的设计中,几乎把这个场景给忽略掉了呢?因为它发生的概率实在太低了。它要求在读取数据的时候,有两个或者多个并发写操作(或者发生了数据失效),这在实际的应用场景中实在是太少了。而且,我们要注意虚线所持续的周期,是一个数据库的更新操作,加上一个cache的删除操作,这个操作一般情况下,也会比缓存的设置持续的时间长,所以进一步降低了概率。

所以,你们知道正确的操作方式了么? 总监问。

知道了!以后我们就用spring-cache的注解去完成工作,再也不在代码中手写一致性逻辑了。

很好很好,如果这么做的话,再发生问题,好像可以把锅甩给spring团队了呢。

作者简介:小姐姐味道 (xjjdog),一个不允许程序员走弯路的公众号。聚焦基础架构和Linux。十年架构,日百亿流量,与你探讨高并发世界,给你不一样的味道。我的个人微信xjjdog0,欢迎添加好友,进一步交流。

本文转载自: 掘金

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

从零开始教你安装Oracle数据库!Oracle 数据库的安

发表于 2021-05-03

1、数据库安装

1.1下载

在这里插入图片描述
根据自己的操作系统位数,到oracle官网下载(以oracle 11g 为例)
之后把两个压缩包解压到同一个文件夹内(需要注意的是,这个文件夹路径名称中最好不要出现中文、空格等不规则字符。)
注意:下载的是Oracle DataBase数据库服务器!!不要下错了,下载成client客户端!!

1.2 安装

打开相应的解压路径,找到安装文件“setup.exe”,双击进行安装,如下图所示:
在这里插入图片描述
取消下图中的“我希望通过My Oracle Support接受安全更新(W)”,点击下一步
在这里插入图片描述
在这里插入图片描述
下面需要注意,如果是笔记本的话选择“桌面类”,服务器就选择“服务器类”
在这里插入图片描述
自定义oracle基目录(安装路径)及密码。
在这里插入图片描述
在这里插入图片描述
先决条件检查。 安装程序会检查软硬件系统是否满足,安装此Oracle版本的最低要求。 直接下一步就OK 了。在这里插入图片描述
概要 安装前的一些相关选择配置信息。 可以保存成文件 或 不保存文件直接点完成即可。
在这里插入图片描述
数据库管理软件文件及dbms文件安装完后,会自动创建安装一个实例数据库默认前面的orcl名称的数据库。
在这里插入图片描述
最后完成oracle安装。

2、配置数据库

2.1 创建表空间

Win+R 输入cmd 进入命令行,输入下面,以sysdba 登录进去:

1
2
3
dos复制代码sqlplus  /nolog
connect / as sysdba
startup

注意:在conn / as sysdba过程中遇到报ORA-01301:insufficient privileges错误
解决办法:出现这种问题的原因,有两个原因,一是在oracle的用户组中没有本机系统;二是操作系统的本地验证不允许。

  • 将当前登录用户添加到ora_dba组中。在这里插入图片描述
  • 在sql.net文件中修改语句为
1
ini复制代码SQLNET.AUTHENTICATION_SERVICE=(NTS)

在这里插入图片描述
创建表空间

1
2
3
4
5
dos复制代码create tablespace TBS_CHOVA_DATA datafile 'E:\Oracle\oradata\TBS_CHOVA_DATA.dbf' size 1000 M autoextend on next 100 maxsize unlimited;

create temporary tablespace TBS_CHOVA_TEMP tempfile 'E:\Oracle\oradata\TBS_CHOVA_TEMP.dbf' size 1000 M autoextend on next 100 maxsize unlimited;

create tablespace TBS_CHOVA_IDX datafile 'E:\Oracle\oradata\TBS_CHOVA_IDX.dbf' size 200 M autoextend on next 100 maxsize unlimited;

注意:文件名前面的路径需要真实存在,没有这个路径的话,需要自己手动创建这个路径

2.2 创建用户

建立用户,分配权限。第一个smis是用户名,第二个smis是密码。

1
2
3
4
5
dos复制代码create user smis identified by smis default tablespace TBS_CHOVA_DATA temporary tablespace TBS_CHOVA_TEMP;

grant connect,resource to smis;

grant dba to smis;

2.3 配置监听

监听器是Oracle基于服务器端的一种网络服务,主要用于监听客户端向数据库服务器端提出的连接请求。既然是基于服务器端的服务,那么它也只存在于数据库服务器端,进行监听器的设置也是在数据库服务器端完成的。

  • 打开oracle程序下的 Net Manager
    在这里插入图片描述
  • 选择监听程序,LISTENER ,监听位置处,添加地址,主机填写自己电脑的IP地址,端口1521
    在这里插入图片描述
  • 选中窗口右侧栏下拉选项中的“数据库服务”,点击添加数据库按钮。在出现的数据库栏中输入全局数据库名。注意这里的全局数据库名与数据 库SID有所区别,全局数据库名实际通过域名来控制在同一网段内数据库全局命名的唯一性,就如Windows下的域名控制器。 Oracle主目录可以不填写,输入SID。
    在这里插入图片描述

2.4 配置本地服务名(Tnsnames)

  • 本地服务名是基于Oracle客户端的网络配置,所以,如果客户端需要连接数据库服务器进行操作,则需要配置该客户端,其依附对象可以是任意一台欲连接数据库服务器进行操作的PC机,也可以是数据库服务器自身。如前面所介绍,可以利用Oracle自带的图形化管理工具Net Manager来完成Oracle客户端的配置。选中服务命名,再点击左上侧“+”按钮,弹出如下图示对话框:
    在这里插入图片描述
  • 输入Net服务名,如myoracle,点击下一步,进入下图示对话框:
    在这里插入图片描述
  • 选中TCP/IP(Internet协议),点击下一步,如下图示:
    在这里插入图片描述
  • 输入主机名与端口号。注意这里的主机名与端口号必须与数据库服务器端监听器配置的主机名和端口号相同。点击下一步,如下图示:
    在这里插入图片描述
  • 选中(Oracle8i或更高版本)服务名,输入服务名。这里的服务名实际上就是数据库服务器端监听器配置中的全局数据库名,前者与后者必须相同。连接类型通常选专用服务器,这要视数据库服务器的配置而定,如果配置的共享数据库服务器,这里的连接类型就要选共享服务器,否则建议选专用服务器(关于专用服务器的介绍请参阅相关文档)。配置好后点击下一步,如下图示:
    在这里插入图片描述
  • 如果数据库服务器端相关服务启动了,可以点击测试按钮进行连接测试。Oracle默认是通过scott/tiger用户进行测试连接,由于scott用户是Oracle自带的示例用户,对于正式的业务数据库或专业测试数据库可能没有配置这个用户,所以需要更改成有效的用户登录才可能测试成功。如果这里测试连接不成功,也不要紧,先点完成按钮结束配置。回到Oracle网络管理器(Oracle Net Manager)主窗口,保存配置,默认即可在Oracle安装目录下找到本地服务名配置文件 (Windows下如D:/oracle/ora92/network/admin/tnsnames.ora,Linux/Unix下$ORACLE_HOME/network/admin/ tnsnames.ora)。配置完成的本地服务名如下图示:
    在这里插入图片描述
  • 树形目录下的服务命名可以通过编辑菜单里的重命名菜单更改成任意合法字符组成的服务名称,注意服务名称前不能有空格字符,否则可能无法连接数据库服务器。

网络配置与访问方式完全解析

三个配置文件 listener.ora 、sqlnet.ora 、tnsnames.ora ,都是放在目录:

1
复制代码ORACLE_HOME\network\admin
  1. sqlnet.ora—– 作用类似于linux 或者其他unix 的nsswitch.conf 文件,通过这个文件来决定怎么样找一个连接中出现的连接字符串。
    例如我们客户端输入
1
dos复制代码sqlplus sys/oracle@orcl

假如我的sqlnet.ora 是下面这个样子

1
2
ini复制代码SQLNET.AUTHENTICATION_SERVICES= (NTS)
NAMES.DIRECTORY_PATH= (TNSNAMES,HOSTNAME)

那么,客户端就会首先在tnsnames.ora 文件中找orcl 的记录. 如果没有相应的记录则尝试把orcl 当作一个主机名,通过网络的途径去解析它的 ip 地址然后去连接这个ip 上GLOBAL_DBNAME=orcl 这个实例,当然我这里orcl 并不是一个主机名。
如果我是这个样子

1
ini复制代码NAMES.DIRECTORY_PATH= (TNSNAMES)

那么客户端就只会从tnsnames.ora 查找orcl 的记录, 括号中还有其他选项,如LDAP 等并不常用。
2. Tnsnames.ora—— 这个文件类似于unix 的hosts 文件,提供的tnsname 到主机名或者ip 的对应,只有当sqlnet.ora 中类似

1
ini复制代码NAMES.DIRECTORY_PATH= (TNSNAMES)

这样,也就是客户端解析连接字符串的顺序中有TNSNAMES 是,才会尝试使用这个文件。
PROTOCOL :客户端与服务器端通讯的协议,一般为TCP ,该内容一般不用改。
HOST:数据库侦听所在的机器的机器名或IP 地址,数据库侦听一般与数据库在同一个机器上,所以当我说数据库侦听所在的机器一般也是指数据库所在的机器。在UNIX 或WINDOWS 下,可以通过在数据库侦听所在的机器的命令提示符下使用hostname 命令得到机器名,或通过ipconfig(for WINDOWS) or ifconfig (for UNIX )命令得到IP 地址。需要注意的是,不管用机器名或IP 地址,在客户端一定要用ping 命令ping 通数据库侦听所在的机器的机器名,否则需要在 hosts 文件中加入数据库侦听所在的机器的机器名的解析。
PORT:数据库侦听正在侦听的端口,可以察看服务器端的listener.ora 文件或在数据库侦听所在的机器的命令提示符下通过lnsrctl status [listener name] 命令察看。此处Port 的值一定要与数据库侦听正在侦听的端口一样。
SERVICE_NAME:在服务器端,用system 用户登陆后,sqlplus> show parameter service_name 命令察看。
ORCL: 对应的本机,SALES 对应的另外一个IP 地址,里边还定义了使用主用服务器还是共享服务器模式进行连接。

连接的时候输入的 TNSNAME

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ini复制代码ORCL =
(DESCRIPTION =
(ADDRESS_LIST =
# 下面是这个TNSNAME 对应的主机,端口,协议
(ADDRESS = (PROTOCOL = TCP)(HOST = 127.0.0.1)(PORT = 1521))
)
(CONNECT_DATA =
# 使用专用服务器模式去连接需要跟服务器的模式匹配,如果没有就根据服务器的模式自动调节
(SERVER = DEDICATED)
# 对应service_name ,SQLPLUS>;show parameter service_name; 进行查看
(SERVICE_NAME = orcl)
)
)
# 下面这个类似
SALES =
(DESCRIPTION =
(ADDRESS_LIST =
(ADDRESS = (PROTOCOL = TCP)(HOST =dg1)(PORT = 1521))
)
(CONNECT_DATA =
(SERVICE_NAME = sales)
)
)

注意:如果数据库服务器用MTS ,客户端程序需要用database link 时最好明确指明客户端用dedicated 直连方式, 不然会遇到很多跟分布式环境有关的ORACLE BUG 。一般情况下数据库服务器用直接的连接会好一些,除非你的实时数据库连接数接近1000 。
3. listener.ora——listener 监听器进程的配置文件
关于listener 进程就不多说了,接受远程对数据库的接入申请并转交给oracle 的服务器进程。所以如果不是使用的远程的连接,并且不需要使用OEM时,listener 进程就不是必需的,同样的如果关闭listener 进程并不会影响已经存在的数据库连接。
Listener.ora文件的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ini复制代码#listener.ora Network Configuration File: 
#E:\oracle\product\10.1.0\Db_2\NETWORK\ADMIN\listener.ora
# Generated by Oracle configuration tools.
# 下面定义LISTENER 进程为哪个实例提供服务 这里是ORCL ,并且它对应的ORACLE_HOME 和GLOBAL_DBNAME 其中GLOBAL_DBNAME 不是必需的除非
# 使用HOSTNAME 做数据库连接
SID_LIST_LISTENER =
(SID_LIST =
(SID_DESC =
(GLOBAL_DBNAME = boway)
(ORACLE_HOME = /u01/app/oracle)
(SID_NAME = ORCL)
)
)
# 监听器的名字,一台数据库可以有不止一个监听器
# 再向下面是监听器监听的协议,ip,端口等,这里使用的tcp1521端口,并且使#用的是主机名
LISTENER =
(DESCRIPTION =
(ADDRESS = (PROTOCOL = TCP)(HOST = dg1)(PORT = 1521))
)

上面的例子是一个最简单的例子,但也是最普遍的。一个listener 进程为一个instance(SID) 提供服务。
监听器的操作命令

1
bash复制代码ORACLE_HOME/bin/lsnrctl start

其他诸如stop,status 等。具体敲完一个lsnrctl 后看帮助。
上面说到的三个文件都可以通过图形的配置工具来完成配置

1
2
bash复制代码$ORACLE_HOME/netca 向导形式的
$ORACLE_HOME/netmgr

本人比较习惯netmgr,
profile 配置的是sqlnet.ora 也就是名称解析的方式
service name 配置的是tnsnames.ora 文件
listeners 配置的是listener.ora 文件,即监听器进程
具体的配置可以尝试一下然后来看一下配置文件。
这样一来总体结构就有了不同的连接方式 。

连接过程

当你输入

1
dos复制代码sqlplus sys/oracle@orcl

1.查询sqlnet.ora 看看名称的解析方式,发现是TNSNAME
2.则查询tnsnames.ora 文件,从里边找orcl 的记录,并且找到主机名,端口和service_name
3. 如果listener 进程没有问题的话,建立与listener 进程的连接。
4 .根据不同的服务器模式如专用服务器模式或者共享服务器模式,listener 采取接下去的动作。默认是专用服务器模式,没有问题的话客户端就连接上了数据库的server process 。
5 .这时候网络连接已经建立,listener 进程的历史使命也就完成了。
##


几种连接方式

简便命名连接:
默认已启用,不需要进行客户机配置,仅支持TC/IP(无SSL),不支持高级连接,如:连接时故障转移,源路由,负载平衡。连接方式如:

1
dos复制代码connect hr/hr@db.us.oracle.com:1521/dba10g

其中db.us.oracle.com为主机名当然也可以用IP代替,1521为连接端口,dba10g为服务名—可通过show parameter service 查看。
本地命名:
需要客户机名称解析文件tnsname.ora,支持所有的Oracle Net协议,支持高级连接选项。连接方式如: connect hr/hr@orcl,其中orcl为数据库实例名
目录命名:
需要加载了Oracle Net名称解析的LDAP:Oracle Internet Directory和Microsoft Active Directory Services。支持所有的Oracle Net协议,支持高级连接选项。连接方式如: connect hr/hr@orcl
外部命名:
使用支持的非Oracle命名服务,包括:网络信息服务(NIS)外部命名,分布式计算环境(DCE)单元目录服务(CDS)

连接用到的几种验证形式

1
dos复制代码sqlplus / as sysdba

这是典型的操作系统认证,不需要listener 进程

1
dos复制代码sqlplus sys/oracle

这种连接方式只能连接本机数据库,同样不需要listener 进程

1
dos复制代码sqlplus sys/oracle@orcl

这种方式需要listener 进程处于可用状态。最普遍的通过网络连接。
以上验证方式使用sys 用户或者其他通过密码文件验证的用户都不需要数据库处于可用状态,操作系统认证也不需要数据库可用,数据库用户认证放是由于采用数据库认证,所以数据库必需处于open 状态。
作为普通用户进行登录

1
2
3
4
5
6
7
dos复制代码[oracle@dg1 admin]$ sqlplus sys/oracle
SQL*Plus: Release 10.2.0.1.0 - Production on Sun Feb 13 16:18:33 2011
Copyright (c) 1982, 2005, Oracle. All rights reserved.
ERROR:
ORA-01034: ORACLE not available
ORA-27101: shared memory realm does not exist
Linux Error: 2: No such file or directory
1
2
sql复制代码Enter user-name: 
initSID.ora 中的Remote_Login_Passwordfile 对身份验证的影响

三个可选值:
NONE
默认值,指示Oracle 系统不使用密码文件,通过操作系统进行身份验证的特权用户拥有SYSORA 和SYSOPER 权限。
EXCLUSIVE
1.表示只有一个数据库实例可以使用密码文件
2.允许将SYSORA 和SYSOPER 权限赋值给SYS 以外的其它用户
SHARED
1.表示可以有多个数据库实例可以使用密码文件
2.不允许将SYSORA 和SYSOPER 权限赋值给SYS 以外的其它用户
所以,如果要以操作系统身份登录,Remote_Login_Passwordfile 应该设置为NONE
关于域名( 主机名) 解析

1
2
3
scss复制代码/etc/hosts (UNIX)
或者
windows\hosts(WIN98) winnt\system32\drivers\etc\hosts (WIN2000)

客户端需要写入数据库服务器IP 地址和主机名的对应关系。

1
2
3
4
dos复制代码127.0.0.1 dg1
192.168.0.35 oracledb oracledb
192.168.0.45 tomcat tomcat
202.84.10.193 bj_db bj_db

有些时候我们配置好第一步后,tnsping 数据库服务器别名显示是成功的,
但是sqlplus username/password@servicename 不通,jdbc thin link 也不通的时候,
一定不要忘了在客户端做这一步,原因可能是DNS 服务器里没有设置这个服务器IP 地址和主机名的对应关系。
如果同时有私有IP 和Internet 上公有IP ,私有IP 写在前面,公有IP 写在后面。
编辑前最好留一个备份,增加一行时也最好用复制粘贴,避免编辑hosts 时空格或者tab 字符错误。
UNIX 下ORACLE 多数据库的环境,OS 客户端需要配置下面两个环境变量

1
2
ini复制代码ORACLE_SID=appdb;export ORACLE_SID
TWO_TASK=appdb;export TWO_TASK

来指定默认的目标数据库


**平时排错可能会用到的:**

1 .lsnrctl status 查看服务器端listener 进程的状态

1
2
3
dos复制代码LSNRCTL>help
LSNRCTL>status
LSNRCTL> services

2 .tnsping 查看客户端sqlnet.ora 和tnsname.ora 文件的配置正确与否,及对应的服务器的listener 进程的状态。

1
2
3
4
5
6
7
8
9
dos复制代码[oracle@dg1 dbs]$ tnsping orcl
TNS Ping Utility for Linux: Version 10.2.0.1.0 - Production on 13-FEB-2011 16:48:06
Copyright (c) 1997, 2005, Oracle. All rights reserved.
Used parameter files:
/u01/app/oracle/network/admin/sqlnet.ora

Used TNSNAMES adapter to resolve the alias
Attempting to contact (DESCRIPTION = (ADDRESS_LIST = (ADDRESS = (PROTOCOL = TCP)(HOST = dg1)(PORT = 1521))) (CONNECT_DATA = (SID = orcl)))
OK (10 msec)

3.查看instance 是否已经启动

1
dos复制代码SQL>select instance_name,host_name,status from v$instance;

查看数据库是打开还是mount 状态。

1
2
3
4
dos复制代码SQL>select open_mode from v$database 
INSTANCE_NAME STATUS
------------------------------ ------------------------------------
orcl OPEN

使用hostname 访问数据库而不是tnsname 的例子
使用tnsname 访问数据库是默认的方式,但是也带来点问题,那就是客户端都是需要配置tnsnames.ora 文件的。如果你的数据库服务器地址发生改变,就需要重新编辑客户端这个文件。通过hostname 访问数据库就没有了这个麻烦。
查看数据库名

1
2
3
4
dos复制代码SQL> select name from v$database;
NAME
---------------------------
ORCL

需要修改服务器端listener.ora

  • 监听器的配置文件listener.ora
  • 使用host naming 则不再需要tnsname.ora 文件做本地解析
  • listener.ora Network Configuration File:
1
makefile复制代码d:\oracle\product\10.1.0\db_1\NETWORK\ADMIN\listener.ora
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ini复制代码# Generated by Oracle configuration tools.
SID_LIST_LISTENER =
(SID_LIST =
(SID_DESC =
# (SID_NAME = PLSExtProc)
(SID_NAME = orcl)
(GLOBAL_DBNAME = ORCL)
(ORACLE_HOME = /u01/app/oracle)
# (PROGRAM = extproc)
)
)

LISTENER =
(DESCRIPTION_LIST =
(DESCRIPTION =
(ADDRESS = (PROTOCOL = IPC)(KEY = EXTPROC))
)
(DESCRIPTION =
(ADDRESS = (PROTOCOL = TCP)(HOST = dg1)(PORT = 1521))
)
)

客户端sqlnet.ora 如果确认不会使用TNSNAME 访问的话,可以去掉 TNSNAMES

1
arduino复制代码sqlnet.ora Network Configuration File: d:\oracle\product\10.1.0\db_1\NETWORK\ADMIN\sqlnet.ora
1
2
3
ini复制代码Generated by Oracle configuration tools.
SQLNET.AUTHENTICATION_SERVICES= (NTS)
NAMES.DIRECTORY_PATH= (HOSTNAME)

Tnsnames.ora 文件不需要配置,删除也无所谓。
下面就是网络和操作系统的配置问题了,怎么样能够解析我的主机名的问题了
可以通过下面的方式连接

1
dos复制代码sqlplus sys/oracle@orcl

这样的话,会连接orcl 这台服务器,并且listener 来确定你所要连接的service_name

2.5 oracle连接问题

要排除客户端与服务器端的连接问题,首先检查客户端配置是否正确(客户端配置必须与数据库服务器端监听配置一致),再根据错误提示解决。下面列出几种常见的连接问题:

1.ORA-12541: TNS: 没有监听器

显而易见,服务器端的监听器没有启动,另外检查客户端IP地址或端口填写是否正确。启动监听器:

1
dos复制代码lsnrctl start

2.ORA-12500: TNS: 监听程序无法启动专用服务器进程

对于Windows而言,没有启动Oracle实例服务。启动实例服务:

1
makefile复制代码C:oradim –startup -sid myoracle

3.ORA-12535: TNS: 操作超时

出现这个问题的原因很多,但主要跟网络有关。解决这个问题,首先检查客户端与服务端的网络是否畅通,如果网络连通,则检查两端的防火墙是否阻挡了连接。

4.ORA-12154: TNS: 无法处理服务名

检查输入的服务名与配置的服务名是否一致。另外注意生成的本地服务名文件(Windows下如

1
2
3
bash复制代码D:oracleora92networkadmin tnsnames.ora
或者
Linux/Unix下/network/admin/tnsnames.ora

里每项服务的首 行服务名称前不能有空格。

5.ORA-12514: TNS: 监听进程不能解析在连接描述符中给出的SERVICE_NAME

打开Net Manager,选中服务名称,检查服务标识栏里的服务名输入是否正确。该服务名必须与服务器端监听器配置的全局数据库名一致。同时检查sqlnet.ora,例如如果想要采用简便连接方式连接就需要在NAMES.DIRECTORY_PATH参数中添加EZCONNECT。

6.ORA-12518 TNS:监听程序无法分发客户机连接

出现该报错有两个原因:在共享模式下是由于调度进程(dispatchers)太少,在独占模式下是由于进程数(proces ses)超过了数据库默认的最大进程数。解决步骤:
1、show parameter process查看数据库允许最大进程数
2、select count(*) from v$session;查看当前系统进程数
如果进程数不够,可通过扩大PGA来增大进程数:

1
2
sql复制代码alter system set workarea_size_policy=auto scope=both;
alter system set pga_aggregate_target=512m scope=both;

3、show parameter dispatchers查看调度进程数量
如果调度进程太少,则可执行:

1
ini复制代码alter system set dispatchers = '(protocol=tcp)(dispatchers=3)(service=oracle10xdb)';

7.Windows 下启动监听服务提示找不到路径

用命令或在服务窗口中启动监听提示找不到路径,或监听服务启动异常。打开注册表,进入

1
sql复制代码HKEY_LOCAL_MACHINE/SYSTEM/Current ControlSet/Services/OracleOraHome92TNSListener

查看ImagePath字符串项是否存在,如果没 有,设定值为D:oracleora92BINTNSLSNR,不同的安装路径设定值做相应的更改。这种方法同样适用于Oracle实例服务,同 上,找到如同

1
sql复制代码HKEY_LOCAL_MACHINE/SYSTEM/Current ControlSet/Services/Oracle ServiceMYORACLE

查看ImagePath字符串项是否存在,如果没有,则新建,设定值为d:oracleora92 binORACLE.EXE MYORACLE。以上是Oracle客户端连接服务器端常见的一些问题,当然不能囊括所有的连接异常。解决问题的关键在于方法与思路,而不是每种问题都有固定的答案。

8.TNS-12537, TNS-12560, TNS-00507 Linux Error: 29: Illegal seek error When Starting the Listener

在linux,Unix底下如果/etc/hosts文件配置不正确会出现如下报错

1
2
3
4
5
6
dos复制代码lsnrctl start
LSNRCTL for HPUX: Version 10.1.0.4.0 - Production on 01-JUL-2005 10:16:59 Copyright (c) 1991, 2004, Oracle. All rights reserved.
Starting /db02/product/10.1/bin/tnslsnr: please wait...
TNS-12537: TNS:connection closed
TNS-12560: TNS:proto adapter error
TNS-00507: Connection closed HPUX Error: 29: Illegal seek

解决方式:
检查该用户是否有/etc/hosts文件的访问权限,检查/etc/hosts文件下是否包含

1
复制代码127.0.0.1 localhost.localdomain localhost

9.ORA-12505, TNS:listener does not currently know of SID given in connect descriptor The Connection descriptor used by the client was:192.168.1.1:1521:bbcd

这个问题一般发生在利用JDBC连接数据库时,这里需要注意,上面的bbcd的位置应该填写sid_name,一般JDBC的配置格式为

1
ruby复制代码jdbc:oracle:thin:@IP/HOSTNAME:PORT:SID例如jdbc:oracle:thin:@145.**.**.**:1521:z***db2

10.Ora-12514:TNS:监听程序当前无法识别链接描述符中请求的服务

该问题是由于缺少监听器的SID_LIST描述项引起的,采用netca进行配置的时候经常会遇到该问题,listener.ora示例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ini复制代码SID_LIST_LISTENER =
(SID_LIST =
(SID_DESC =
(SID_NAME = PLSExtProc)
(ORACLE_HOME = /opt/oracle/product/9.2.0.4)
(PROGRAM = extproc)
)
(SID_DESC =
(GLOBAL_DBNAME = SAMPLE.COM)
(ORACLE_HOME = /opt/oracle/product/9.2.0.4)
(SID_NAME = SAMPLE)
))

LISTENER =
(DESCRIPTION_LIST =
(DESCRIPTION =
(ADDRESS_LIST =
(ADDRESS = (PROTOCOL = TCP)(HOST = tcy.com)(PORT = 1521))
)
(ADDRESS_LIST =
(ADDRESS = (PROTOCOL = IPC)(KEY = EXTPROC))
)))

11.ORA-12528: TNS:listener: all appropriate instances are blocking new connections

ORA-12528:监听中的服务使用了动态服务,系统启动后,数据库没有正常的MOUNT,因此在动态模式下,就会出现这个问题,用静态的就不会有这个问题,因此上面的方法就是把监听设置为静态,或者在tnsnames.ora中追加(UR=A)。
lisnter.ora增加如下内容

1
2
3
4
5
6
7
ini复制代码 (SID_DESC =
(GLOBAL_DBNAME = ammicly)
(ORACLE_HOME = c:\oracle\product\10.1.0\db_1)
(SID_NAME = ammicly)
)
或者在tnsnames.ora增加如下内容:
(UR=A)

12.ORA-01034: ORACLE not available和ORA-27101: shared memory realm does not exist

检查tnsping是否能正常工作,检查lsnrctl status是否正常。检查local_listener参数(pmon只会动态注册port等于1521的监听,否则pmon不能动态注册listener,要想让pmon动态注册listener,需要设置local_listener参数),通过如下方式设置

1
dos复制代码alter system set local_listener='(ADDRESS =(PROTOCOL=TCP)(HOST=10.201.1.134)(PORT=1521)(SID=siebtest))';

13.ORA-12520 TNS:Listener count not find available handler for requested type of server

有以下3种可能
1、检查数据库是否是专用服务器,但是在tnsname.ora配置中设置了连接方式为shared,这种情况下

1
ini复制代码打开tnsname.ora, 把(server = shared) 改成 (server = dedicate);

2、是由于process不够引起的:

1
2
3
sql复制代码select count(*) from v$session;
show parameter processes
show parameter sessions

调大processes参数即可
3、local_listener设置不当,设置方式参见上文。

14.TNS-12542: TNS:address already in use

1
2
dos复制代码TNS-12560: TNS:protocol adapter error
TNS-00512: Address already in use

检查/etc/hosts的配置,是否有多个ip指向同一主机名的情况
参考至:lzysystem.iteye.com/blog/424569
blogold.chinaunix.net/u2/82873/sh…
gggwfn1982.blog.163.com/blog/static…
guolr.iteye.com/blog/549692

blog.sina.com.cn/s/blog_4cd0…
pengxianfeng.i.sohu.com/blog/view/8…
blog.sina.com.cn/s/blog_517c…
luoping.blog.51cto.com/534596/1062…
xiekeli.blogbus.com/logs/936195…
www.linuxidc.com/Linux/2012-…

15. Windows下启动监听服务提示找不到路径

用命令或在服务窗口中启动监听提示找不到路径,或监听服务启动异常。打开注册表,进入

1
sql复制代码HKEY_LOCAL_MACHINE/SYSTEM/Current ControlSet/Services/OracleOraHome92TNSListener

查看ImagePath字符串项是否存在,如果没有,设定值为

1
javascript复制代码D:/oracle/ora92/BIN/TNSLSNR

不同的安装路径设定值做相应的更改。这种方法同样适用于Oracle实例服务,同上,找到如同

1
sql复制代码HKEY_LOCAL_MACHINE/SYSTEM/Current ControlSet/Services/Oracle ServiceMYORACLE

查看ImagePath字符串项是否存在,如果没有,则新建,设定值为

1
bash复制代码d:/oracle/ora92/binORACLE.EXE MYORACLE

以上是Oracle客户端连接服务器端常见的一些问题,当然不能囊括所有的连接异常。解决问题的关键在于方法与思路,而不是每种问题都有固定的答案。

16.ORA-12638: 身份证明检索失败

1
2
rust复制代码开始 -> 程序 -> Oracle -> Configuration and Migration Tools ->
Net Manager→本地→概要文件→Oracle高级安全性→验证→去掉所选方法中的 "NTS"

就可以了.

3.备份还原数据库

3.1 备份数据库

exp导出

ORACLE 11g新特性,当表没有数据时,不分配segment,以节省空间,所以exp导出的时候,不导出这些表。针对这个问题,首先执行下面的sql:

1
sql复制代码select 'alter table '||table_name||' allocate extent;' from user_tables where num_rows=0 or num_rows is null;

复制上面语句生成的结果,在执行这些即可。
在命令行执行下面即可导出数据库

1
dos复制代码exp  rad/rad@orcl  file='E:\rad.dmp'  buffer=40960000

使用导出expdp工具

首先 指定转储文件和日志文件所在的目录directory – lmm_db_bak

1
2
3
4
5
dos复制代码sqlplus  /nolog
connect / as sysdba
startup
create or replace directory lmm_db_bak as 'D:/lmm_db_bak';
grant read,write on directory lmm_db_bak to public;
  • 查看
1
sql复制代码select * from dba_directories;
  • 导出
1
dos复制代码 expdp  rad/rad@orcl  schemas= rad  dumpfile=rad-20170101.dmp directory= lmm_db_bak
  • 导出部分表使用include
1
dos复制代码 expdp  rad/rad@orcl  schemas= rad  dumpfile=rad-20170101.dmp directory= lmm_db_bak include=table:\"like \'CT%\'\"

注:这种方式可以直接导出那些空表

3.2 还原数据库

imp导入

如果是还原完整的oracle数据库,可以先删除当前用户,再创建用户,导入数据库

  • 删除用户
1
sql复制代码drop user rad cascade;
  • 创建用户
1
2
3
sql复制代码create user rad identified by rad default tablespace TBS_LLM_DATA temporary tablespace TBS_LLM_TEMP;
grant connect,resource to rad;
grant dba to rad;

在命令行执行下面即可导入数据库

1
dos复制代码imp rad/rad@orcl  file='E:\rad.dmp'  buffer=40960000 full=y;

导入非完整数据库dmp文件(部分表)

如果自己原本数据库和dmp中存在同样的表,那么导入dmp文件是不会导入已经有的同名表的。需要提前删除自己本来库里的同名表。

使用导入impdb工具

1
dos复制代码impdp  rad/rad@orcl  schemas= rad  directory= lmm_db_bak dumpfile=RAD-20170101.DMP FULL=y;
1
2
3
4
5
6
7
ini复制代码如果想导入的用户已经存在:
1. 导出用户 expdp user1/pass1 directory=dumpdir dumpfile=user1.dmp
2. 导入用户 impdp user2/pass2 directory=dumpdir dumpfile=user1.dmp REMAP_SCHEMA=user1:user2 EXCLUDE=USER full=y;
如果想导入的用户不存在:
1. 导出用户 expdp user1/pass1 directory=dumpdir dumpfile=user1.dmp
2. 导入用户 impdp system/passsystem directory=dumpdir dumpfile=user1.dmp REMAP_SCHEMA=user1:user2 full=y;
3. user2会自动建立,其权限和使用的表空间与user1相同,但此时用user2无法登录,必须修改user2的密码

本文转载自: 掘金

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

心心念念的JVM调优:jmap、jstack、jstat在真

发表于 2021-05-03

本篇大纲

JVM调优策略JVM基本工具介绍JVM基本工具介绍jmapjmapjstackjstackjstatjstatJVM的优化思路JVM的优化思路JVM的优化思路JVM的优化思路JVM调优案例JVM调优案例调优案例调优案例JVM调优策略

  • 第一阶段:JVM基本工具介绍的详细介绍
  • 第二阶段:JVM的优化思路
  • 第三阶段:JVM的真实调优案例

第一阶段:JVM基本工具介绍

jmap—>Java内存映像工具

jmapMemory Map for Java用于生成堆转储快照一般称为heapdump或dump文件。同时它还可以查询finalize执行队列、Java堆和方法区的 详细信息,如空间使用率、当前用的是哪种收集器等。

说简单点就是它能用来查看堆内存信息

jmap命令格式

jmap option vmid

option选项

image.png

jmap使用demo例子

例子:大家在随便创建一个spring-boot项目然后加上web模块启动就可以了,这里不展开详细的步骤

第一步:启动创建的spring-boot项目

image.png

第二步:在终端输入jps命令

jpsJava Virtual Machine Process Status Tool是Java提供的一个显示当前所有Java进程pid的命令

找到对应的Java进程号:49150

image.png

第三步:在终端根据jmap命令格式输入命令查看

第一个命令:jmap -histo 进程号

输入jmap -histo 49150来看下结果:

image.png

发现在终端显示不下,打印的东西太多,所以我们把打印的内容输出到文件中

输入jmap -histo 49150 > ./jmapLog.txt来看下结果:

image.png

我们发现在对应的文件路径下就生成了这么一个jmapLog.txt文件,打开来看下:

image.png

可以看到三块内容最左边的编号可以不管,从右往左看分别是

  1. class name:类名
  2. bytes:字节数
  3. instances:实例数

说白了就是打印每个类的实例数有多少,总共占用了多少字节或者说是内存大小

第二个命令:jmap -heap 进程号

根据上方的表格可知:这个命令是用来显示Java堆的详细信息,如使用哪种垃圾收集器、参数配置、分代状况等等,我们来打印一下看一下

输入jmap -heap 49150来看下结果:

image.png

大家如果和我有一样问题的,可以看下这篇文章,应该就能解决你的问题,没有当然最好:

使用 jmap 打印 Java 堆信息时报错:Can’t attach symbolicator to the process

经过上面文章中的调整,最终输入jhsdb jmap –heap –pid 49150命令后打印:

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
java复制代码$ jhsdb jmap --heap --pid 49150
Attaching to process ID 49150, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 11.0.11+9-LTS-194

using thread-local object allocation.
Garbage-First (G1) GC with 4 thread(s)

Heap Configuration:
MinHeapFreeRatio = 40
MaxHeapFreeRatio = 70
MaxHeapSize = 2147483648 (2048.0MB)
NewSize = 1363144 (1.2999954223632812MB)
MaxNewSize = 1287651328 (1228.0MB)
OldSize = 5452592 (5.1999969482421875MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 1048576 (1.0MB)

Heap Usage:
G1 Heap:
regions = 2048
capacity = 2147483648 (2048.0MB)
used = 14274048 (13.61279296875MB)
free = 2133209600 (2034.38720703125MB)
0.6646871566772461% used
G1 Young Generation:
Eden Space:
regions = 2
capacity = 69206016 (66.0MB)
used = 2097152 (2.0MB)
free = 67108864 (64.0MB)
3.0303030303030303% used
Survivor Space:
regions = 7
capacity = 7340032 (7.0MB)
used = 7340032 (7.0MB)
free = 0 (0.0MB)
100.0% used
G1 Old Generation:
regions = 5
capacity = 57671680 (55.0MB)
used = 4836864 (4.61279296875MB)
free = 52834816 (50.38720703125MB)
8.386896306818182% used
第一块内容:堆的配置信息

image.png

  • MinHeapFreeRatio: 空闲堆空间的最小百分比,计算公式为:HeapFreeRatio =(CurrentFreeHeapSize/CurrentTotalHeapSize) * 100,值的区间为0到100,默认值为 40。如果HeapFreeRatio < MinHeapFreeRatio,则需要进行堆扩容,扩容的时机应该在每次垃圾回收之后。
  • MaxHeapFreeRatio: 空闲堆空间的最大百分比,计算公式为:HeapFreeRatio =(CurrentFreeHeapSize/CurrentTotalHeapSize) * 100,值的区间为0到100,默认值为 70。如果HeapFreeRatio > MaxHeapFreeRatio,则需要进行堆缩容,缩容的时机应该在每次垃圾回收之后。
  • MaxHeapSize: 最大堆内存
  • NewSize: 新生代占用的空间
  • MaxNewSize: 最大新生代可用空间
  • OldSize: 老年代占用空间
  • NewRatio: 新生代占的比例,2表示1:2,占三分之一
  • SurvivorRatio: 两个Survivor区和eden区的比例,8表示2个Survivor:Eden = 2:8,意味着一个Survivor区占年轻代的1/10
  • MetaspaceSize:元空间大小
  • CompressedClassSpaceSize:指针压缩空间大小
  • MaxMetaspaceSize: 最大元空间大小
  • G1HeapRegionSize: G1垃圾收集器中的一个Region大小
第二块内容:正在使用的堆信息

image.png

因为我用的是jdk 11,默认使用是G1的垃圾收集器,所以打印的堆内存结构和jdk 8不同,大家根据自己真实的场景去分析,没必要和我保持一致

  • G1 Heap:总的堆空间,regions就是region的个数,默认2048个,capacity是总的堆的大小,used是已经使用的堆空间,free是没有使用的堆空间,最后是使用比例
  • G1 Young Generation:Young空间部分/年轻代大小,其中又分为Eden区和Survivor区,这里就不再详细介绍每块区域大小,大家直接看图就好
  • G1 Old Generation:Old空间部分/老年代大小
第三个命令:jmap -dump 进程号

image.png

举个例子: 执行一下jmap -dump:format=b,file=jmapDump 49150

image.png

发现就会在对应的目录下生成名称为jmapDump的堆快照信息文件

设置自动下载

我们还可以设置自动下载,当内存溢出的时候自动dump文件

小贴士:内存很大的时候,可能会导不出来

  1. -XX:+HeapDumpOnOutOfMemoryError设置自动导出
  2. -XX:HeapDumpPath=./xxx/xxx/xxx路径
查看jmapDump例子

在test目录下新建测试类就是在死循环中创建对象,并且让这个对象一直被GC Roots引用,不会被回收

image.png

加上 -XX:+HeapDumpOnOutOfMemoryError、-XX:HeapDumpPath 这两个命令后运行,当内存溢出的时候自动dump

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复制代码
// 测试代码

public class DumpTest {

public static void main(String[] args) {
List<Object> list = new ArrayList<>();
int i = 0;
int j = 0;
while (true) {
list.add(new Teacher("teacher" + i++, i++));
new Teacher("teacher" + j--, j--);
}
}

}
class Teacher {
private String name;
private Integer age;
public Teacher(String name, Integer age) {
this.name = name;
this.age = age;
}
}

我们在idea的配置中加上JVM参数,没有的同学按照下方红框框中选择JVM参数那一栏

image.png

-Xms10M -Xmx10M -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/dumpTest.log

-Xms10M -Xmx10M是为了尽早发生OOM,配置好参数之后执行一下,发现产生了OOM

image.png

然后去搜这个dumpTest.log文件,发现也已经自动生成了,那么问题来了,我们怎么分析这份文件呢?

image.png

jvisualvm

jdk自带的这个工具通过导入就可以查看这个文件

小贴士:亲测,jdk11不支持这个工具,由于我电脑的jdk是双版本,所以这里演示的时候是切换到jdk8 然后打开的jvisualvm

我们在终端输入jvisualvm,打开工具后,通过文件导入打开下载下来的dumpTest.log文件

image.png

就可以看到显示的有基本信息、环境、系统属性和堆快照上的线程信息

image.png

我们切换到类的窗口上

image.png

可以知道哪几个类的实例最多,占用的空间大小、比例是多少,我们可以通过这样的方式,很清楚的知道堆中类的实例分布,如果发现某一个类的实例特别多,我们就可以去定位创建这个类实例的地方,进行排查,我们可以通过这个方法找出JVM内存飙升的原因

剩下的jmap命令,剩下的还有三个jmap命令,由于平常不常用,这里就不再演示了

image.png

jstack—>Java堆栈跟踪工具

jstackStack Trace for Java命令用于生成JVM当前时刻的线程快照

线程快照

线程快照就是当前JVM内每一条线程正在执行的方法堆栈的集合,生成线程快照的 目的通常是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间挂起等,都是导致线程长时间停顿的常见原因。

线程出现停顿时通过jstack来查看各个线程的调用堆栈, 就可以获知没有响应的线程到底在后台做些什么事情,或者等待着什么资源。

jstack命令格式

jstack option vmid

image.png

jstack例子

既然是用来定位线程出现长时间停顿的原因,如线程间死锁,那么我们就模拟一个线程的死锁,下面是一个简单的死锁类:

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
java复制代码/**
* 当DeadLock类的对象flag==1时(td1),先锁定o1,睡眠500毫秒
* 而td1在睡眠的时候另一个flag==0的对象(td2)线程启动,先锁定o2,睡眠500毫秒
* td1睡眠结束后需要锁定o2才能继续执行,而此时o2已被td2锁定;
* td2睡眠结束后需要锁定o1才能继续执行,而此时o1已被td1锁定;
* td1、td2相互等待,都需要得到对方锁定的资源才能继续执行,从而死锁。
*/
public class DeadLockTest implements Runnable{

public int flag = 1;

//静态对象是类的所有对象共享的
private static Object o1 = new Object(), o2 = new Object();
@Override
public void run() {
System.out.println("flag=" + flag);
if (flag == 1) {
synchronized (o1) {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println("1");
}
}
}
if (flag == 0) {
synchronized (o2) {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (o1) {
System.out.println("0");
}
}
}
}

public static void main(String[] args) {
DeadLockTest td1 = new DeadLockTest();
DeadLockTest td2 = new DeadLockTest();
td1.flag = 1;
td2.flag = 0;
//td1,td2都处于可执行状态,但JVM线程调度先执行哪个线程是不确定的。
//td2的run()可能在td1的run()之前运行
new Thread(td1).start();
new Thread(td2).start();
}
}

我们运行一下,发现程序一直卡在这里进行不下去

image.png

我们程序不终止,用jstack命令来看下怎么排查,首先通过jps命令找到java进程

image.png

然后用jstack 进程号来看下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
java复制代码2021-04-27 23:42:14
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.171-b11 mixed mode):

"Attach Listener" #15 daemon prio=9 os_prio=31 tid=0x00007fc476153800 nid=0x320b waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"DestroyJavaVM" #14 prio=5 os_prio=31 tid=0x00007fc4761b1800 nid=0x1003 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"Thread-1" #13 prio=5 os_prio=31 tid=0x00007fc475108800 nid=0x3e03 waiting for monitor entry [0x0000700002560000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.project.mall.test.DeadLockTest.run(DeadLockTest.java:39)
- waiting to lock <0x0000000795946928> (a java.lang.Object)
- locked <0x0000000795946938> (a java.lang.Object)
at java.lang.Thread.run(Thread.java:748)

"Thread-0" #12 prio=5 os_prio=31 tid=0x00007fc4768b8800 nid=0x3c03 waiting for monitor entry [0x000070000245d000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.project.mall.test.DeadLockTest.run(DeadLockTest.java:27)
- waiting to lock <0x0000000795946938> (a java.lang.Object)
- locked <0x0000000795946928> (a java.lang.Object)
at java.lang.Thread.run(Thread.java:748)

"Service Thread" #11 daemon prio=9 os_prio=31 tid=0x00007fc476152800 nid=0x4203 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"C1 CompilerThread2" #10 daemon prio=9 os_prio=31 tid=0x00007fc476152000 nid=0x4403 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"C2 CompilerThread1" #9 daemon prio=9 os_prio=31 tid=0x00007fc476151000 nid=0x3903 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"C2 CompilerThread0" #8 daemon prio=9 os_prio=31 tid=0x00007fc47687c800 nid=0x4503 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"JDWP Command Reader" #7 daemon prio=10 os_prio=31 tid=0x00007fc475032000 nid=0x4603 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"JDWP Event Helper Thread" #6 daemon prio=10 os_prio=31 tid=0x00007fc475031000 nid=0x4703 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"JDWP Transport Listener: dt_socket" #5 daemon prio=10 os_prio=31 tid=0x00007fc47582e800 nid=0x4807 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"Signal Dispatcher" #4 daemon prio=9 os_prio=31 tid=0x00007fc47582a000 nid=0x4903 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"Finalizer" #3 daemon prio=8 os_prio=31 tid=0x00007fc475022000 nid=0x5103 in Object.wait() [0x0000700001939000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x0000000795588ed0> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143)
- locked <0x0000000795588ed0> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:164)
at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:212)

"Reference Handler" #2 daemon prio=10 os_prio=31 tid=0x00007fc476033800 nid=0x5203 in Object.wait() [0x0000700001836000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x0000000795586bf8> (a java.lang.ref.Reference$Lock)
at java.lang.Object.wait(Object.java:502)
at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
- locked <0x0000000795586bf8> (a java.lang.ref.Reference$Lock)
at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)

"VM Thread" os_prio=31 tid=0x00007fc476014800 nid=0x2b03 runnable

"GC task thread#0 (ParallelGC)" os_prio=31 tid=0x00007fc47501b800 nid=0x1f07 runnable

"GC task thread#1 (ParallelGC)" os_prio=31 tid=0x00007fc47501c000 nid=0x2103 runnable

"GC task thread#2 (ParallelGC)" os_prio=31 tid=0x00007fc47501c800 nid=0x2303 runnable

"GC task thread#3 (ParallelGC)" os_prio=31 tid=0x00007fc47501d800 nid=0x2a03 runnable

"VM Periodic Task Thread" os_prio=31 tid=0x00007fc475962000 nid=0x3b03 waiting on condition

JNI global references: 1579


Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00007fc4758254a8 (object 0x0000000795946928, a java.lang.Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x00007fc475821408 (object 0x0000000795946938, a java.lang.Object),
which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
at com.project.mall.test.DeadLockTest.run(DeadLockTest.java:39)
- waiting to lock <0x0000000795946928> (a java.lang.Object)
- locked <0x0000000795946938> (a java.lang.Object)
at java.lang.Thread.run(Thread.java:748)
"Thread-0":
at com.project.mall.test.DeadLockTest.run(DeadLockTest.java:27)
- waiting to lock <0x0000000795946938> (a java.lang.Object)
- locked <0x0000000795946928> (a java.lang.Object)
at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.

打印的都是进程中的线程信息,例如下面的这两个线程:

image.png

  • Thread-1:线程名
  • prio:优先级
  • os_prio:操作系统级别的线程优先级
  • tid:线程id
  • nid:线程对应本地线程id
  • java.lang.Thread.State:线程状态

继续往下看,到打印信息的尾端,我们发现jstack帮我们已经发现了一个死锁,说发现了一个Java层面的一个死锁

image.png

不仅如此,jstack还帮我们打印了死锁产生的原因,比如下方,由于Thread-1等待获取0x00007fc4758254a8这把锁,但是这把锁,现在被Thread-0持有,Thread-0等待获取0x00007fc475821408这把锁,但是这把锁,现在被Thread-1持有

image.png

打印了死锁产生的原因之外,还帮我们定位到了java的代码行数:

image.png

jvisualvm检测死锁

我们通过jvisualvm也可以快速的检测死锁,如下图所示

image.png

旁边的线程Dump就是执行我们上面说的jstack 进程id

关于jvisualvm你一定要知道的事

虽然jvisualvm能监控远程,但是一般是不用的,因为如果要监控服务器,那么服务器启动的时候要启动JMX的端口配置

image.png

但是一般生产环境中是不可能把这么重要的端口开放出去的,所以一般不使用jvisualvm监控线上的JVM,所以具体的配置方式就不再展开,但是开发环境、测试环境可以根据需要使用比如压测的时候

jstack找出占用CPU最高的线程

准备:我直接拿开发环境的来模拟演示了,大家看下演示过程

使用top命令找出cpu最高的进程

输入top查看cpu占用率最高的进程

image.png

根据图中显示,占用cpu最高的java进程是18955

使用top -p 进程号命令

使用top -p 进程号命令查看进程的内存使用情况,我们输入top -p 18955来看下

image.png

按H进入进程的详情

按H查看进程内每个线程的内存使用情况

image.png

最左边的PID就是我们的线程ID

根据jstack命令找到线程ID对应的代码

  1. 首先把线程ID转化成16进制的因为jstack信息里打印的线程ID都是16进制的,所以要把24091转换成16进制就是5e1b
  2. 执行jstack 18955|grep -A 10 5e1b,匹配这个线程所在行的后面10行代码,从堆栈中可以找到导致cpu飙升的调用方法
  3. 分析堆栈中打印的代码,如下图所示这个只是例子,别在意里面的内容
1
2
3
4
5
6
7
8
9
10
java复制代码"http-nio-8080-ClientPoller" #43 daemon prio=5 os_prio=31 cpu=24.76ms elapsed=327.74s tid=0x00007fd3f33d0800 nid=0x5e1b runnable  [0x000070000d58d000]
java.lang.Thread.State: RUNNABLE
at sun.nio.ch.KQueue.poll(java.base@11.0.11/Native Method)
at sun.nio.ch.KQueueSelectorImpl.doSelect(java.base@11.0.11/KQueueSelectorImpl.java:122)
at sun.nio.ch.SelectorImpl.lockAndDoSelect(java.base@11.0.11/SelectorImpl.java:124)
- locked <0x00000007843563e8> (a sun.nio.ch.Util$2)
- locked <0x0000000784356290> (a sun.nio.ch.KQueueSelectorImpl)
at sun.nio.ch.SelectorImpl.select(java.base@11.0.11/SelectorImpl.java:136)
at org.apache.tomcat.util.net.NioEndpoint$Poller.run(NioEndpoint.java:816)
at java.lang.Thread.run(java.base@11.0.11/Thread.java:834)

jinfo—>Java配置信息工具

jinfoConfiguration Info for Java的作用是实时查看和调整JVM配置的各项参数

查看JVM参数

jinfo -flags 进程号

image.png

小贴士: jdk8的版本这个命令有问题和上面jmap命令失效的情况的一样,需要切换其他的jdk版本

查看系统参数

jinfo还可以使用-sysprops选项把虚拟机进程的System.getProperties()的内容打印出来,如下图所示,大家只要知道有这个命令就好,用的很少

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
java复制代码VM Flags:
-XX:-BytecodeVerificationLocal -XX:-BytecodeVerificationRemote -XX:CICompilerCount=3 -XX:ConcGCThreads=1 -XX:G1ConcRefinementThreads=4 -XX:G1HeapRegionSize=1048576 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=134217728 -XX:+ManagementServer -XX:MarkStackSize=4194304 -XX:MaxHeapSize=2147483648 -XX:MaxNewSize=1287651328 -XX:MinHeapDeltaBytes=1048576 -XX:NonNMethodCodeHeapSize=6973028 -XX:NonProfiledCodeHeapSize=244685212 -XX:ProfiledCodeHeapSize=0 -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:TieredStopAtLevel=1 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:+UseG1GC
zhouxinzedeMacBook-Pro:~ zhouxinze$ jinfo -sysprops 84587
Java System Properties:
#Thu Apr 29 21:55:19 CST 2021
gopherProxySet=false
awt.toolkit=sun.lwawt.macosx.LWCToolkit
java.specification.version=11
sun.cpu.isalist=
sun.jnu.encoding=UTF-8
java.class.path=/Users/zhouxinze/IdeaProjects/mall/target/classes\:/Users/zhouxinze/.m2/repository/org/springframework/boot/spring-boot-starter-web/2.4.5/spring-boot-starter-web-2.4.5.jar\:/Users/zhouxinze/.m2/repository/org/springframework/boot/spring-boot-starter/2.4.5/spring-boot-starter-2.4.5.jar\:/Users/zhouxinze/.m2/repository/org/springframework/boot/spring-boot/2.4.5/spring-boot-2.4.5.jar\:/Users/zhouxinze/.m2/repository/org/springframework/boot/spring-boot-autoconfigure/2.4.5/spring-boot-autoconfigure-2.4.5.jar\:/Users/zhouxinze/.m2/repository/org/springframework/boot/spring-boot-starter-logging/2.4.5/spring-boot-starter-logging-2.4.5.jar\:/Users/zhouxinze/.m2/repository/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar\:/Users/zhouxinze/.m2/repository/ch/qos/logback/logback-core/1.2.3/logback-core-1.2.3.jar\:/Users/zhouxinze/.m2/repository/org/apache/logging/log4j/log4j-to-slf4j/2.13.3/log4j-to-slf4j-2.13.3.jar\:/Users/zhouxinze/.m2/repository/org/apache/logging/log4j/log4j-api/2.13.3/log4j-api-2.13.3.jar\:/Users/zhouxinze/.m2/repository/org/slf4j/jul-to-slf4j/1.7.30/jul-to-slf4j-1.7.30.jar\:/Users/zhouxinze/.m2/repository/jakarta/annotation/jakarta.annotation-api/1.3.5/jakarta.annotation-api-1.3.5.jar\:/Users/zhouxinze/.m2/repository/org/yaml/snakeyaml/1.27/snakeyaml-1.27.jar\:/Users/zhouxinze/.m2/repository/org/springframework/boot/spring-boot-starter-json/2.4.5/spring-boot-starter-json-2.4.5.jar\:/Users/zhouxinze/.m2/repository/com/fasterxml/jackson/core/jackson-databind/2.11.4/jackson-databind-2.11.4.jar\:/Users/zhouxinze/.m2/repository/com/fasterxml/jackson/core/jackson-annotations/2.11.4/jackson-annotations-2.11.4.jar\:/Users/zhouxinze/.m2/repository/com/fasterxml/jackson/core/jackson-core/2.11.4/jackson-core-2.11.4.jar\:/Users/zhouxinze/.m2/repository/com/fasterxml/jackson/datatype/jackson-datatype-jdk8/2.11.4/jackson-datatype-jdk8-2.11.4.jar\:/Users/zhouxinze/.m2/repository/com/fasterxml/jackson/datatype/jackson-datatype-jsr310/2.11.4/jackson-datatype-jsr310-2.11.4.jar\:/Users/zhouxinze/.m2/repository/com/fasterxml/jackson/module/jackson-module-parameter-names/2.11.4/jackson-module-parameter-names-2.11.4.jar\:/Users/zhouxinze/.m2/repository/org/springframework/boot/spring-boot-starter-tomcat/2.4.5/spring-boot-starter-tomcat-2.4.5.jar\:/Users/zhouxinze/.m2/repository/org/apache/tomcat/embed/tomcat-embed-core/9.0.45/tomcat-embed-core-9.0.45.jar\:/Users/zhouxinze/.m2/repository/org/glassfish/jakarta.el/3.0.3/jakarta.el-3.0.3.jar\:/Users/zhouxinze/.m2/repository/org/apache/tomcat/embed/tomcat-embed-websocket/9.0.45/tomcat-embed-websocket-9.0.45.jar\:/Users/zhouxinze/.m2/repository/org/springframework/spring-web/5.3.6/spring-web-5.3.6.jar\:/Users/zhouxinze/.m2/repository/org/springframework/spring-beans/5.3.6/spring-beans-5.3.6.jar\:/Users/zhouxinze/.m2/repository/org/springframework/spring-webmvc/5.3.6/spring-webmvc-5.3.6.jar\:/Users/zhouxinze/.m2/repository/org/springframework/spring-aop/5.3.6/spring-aop-5.3.6.jar\:/Users/zhouxinze/.m2/repository/org/springframework/spring-context/5.3.6/spring-context-5.3.6.jar\:/Users/zhouxinze/.m2/repository/org/springframework/spring-expression/5.3.6/spring-expression-5.3.6.jar\:/Users/zhouxinze/.m2/repository/org/slf4j/slf4j-api/1.7.30/slf4j-api-1.7.30.jar\:/Users/zhouxinze/.m2/repository/org/springframework/spring-core/5.3.6/spring-core-5.3.6.jar\:/Users/zhouxinze/.m2/repository/org/springframework/spring-jcl/5.3.6/spring-jcl-5.3.6.jar
java.vm.vendor=Oracle Corporation
sun.arch.data.model=64
catalina.useNaming=false
java.vendor.url=https\://openjdk.java.net/
user.timezone=Asia/Shanghai
java.vm.specification.version=11
os.name=Mac OS X
sun.java.launcher=SUN_STANDARD
user.country=CN
sun.boot.library.path=/Library/Java/JavaVirtualMachines/jdk-11.0.11.jdk/Contents/Home/lib
spring.application.admin.enabled=true
sun.java.command=com.mall.MallApplication
com.sun.management.jmxremote=
jdk.debug=release
sun.cpu.endian=little
spring.liveBeansView.mbeanDomain=
user.home=/Users/zhouxinze
user.language=zh
java.specification.vendor=Oracle Corporation
java.version.date=2021-04-20
java.home=/Library/Java/JavaVirtualMachines/jdk-11.0.11.jdk/Contents/Home
file.separator=/
spring.output.ansi.enabled=always
java.vm.compressedOopsMode=Zero based
line.separator=\n
java.specification.name=Java Platform API Specification
java.vm.specification.vendor=Oracle Corporation
FILE_LOG_CHARSET=UTF-8
java.awt.graphicsenv=sun.awt.CGraphicsEnvironment
java.awt.headless=true
user.script=Hans
sun.management.compiler=HotSpot 64-Bit Tiered Compilers
java.runtime.version=11.0.11+9-LTS-194
spring.jmx.enabled=true
path.separator=\:
os.version=10.15.6
java.runtime.name=Java(TM) SE Runtime Environment
file.encoding=UTF-8
spring.beaninfo.ignore=true
java.vm.name=Java HotSpot(TM) 64-Bit Server VM
java.vendor.version=18.9
java.vendor.url.bug=https\://bugreport.java.com/bugreport/
java.io.tmpdir=/var/folders/qz/gl34hk1x5yv5wb0l6tsyq_x80000gp/T/
catalina.home=/private/var/folders/qz/gl34hk1x5yv5wb0l6tsyq_x80000gp/T/tomcat.8080.5214054096261208489
java.version=11.0.11
os.arch=x86_64
java.vm.specification.name=Java Virtual Machine Specification
PID=84587
java.awt.printerjob=sun.lwawt.macosx.CPrinterJob
sun.os.patch.level=unknown
CONSOLE_LOG_CHARSET=UTF-8
catalina.base=/private/var/folders/qz/gl34hk1x5yv5wb0l6tsyq_x80000gp/T/tomcat.8080.5214054096261208489
java.vendor=Oracle Corporation
java.vm.info=mixed mode
java.vm.version=11.0.11+9-LTS-194
java.rmi.server.randomIDs=true
sun.io.unicode.encoding=UnicodeBig
java.class.version=55.0

重点:jstat—>JVM统计信息监视工具

jstatJVM Statistics Monitoring Tool是用于监视JVM各种运行状态信息的命令行工具这个命令很重要很重要。

它可以显示本地或者远程JVM进程中的类加载、内存、垃圾收集、即时编译等运行时数据,在没有GUI图形界面、只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的常用工具。

jstat命令格式

jstat [-命令选项] [vmid] [间隔时间][查询次数]

image.png

命令选项清单

image.png

第一个功能: 垃圾回收统计

jstat -gc 进程号

image.png

  • S0C:当前survivor0区的大小
  • S1C:当前survivor1区的大小
  • S0U:survivor0区的已经使用大小
  • S1U:survivor1区的已经使用大小
  • EC:Eden区的大小
  • EU:Eden区的使用大小
  • OC:老年代的大小
  • MC:元空间的大小
  • MU:元空间的使用大小
  • CCSC:指针压缩空间的大小
  • CCSU:指针压缩空间的使用大小
  • YGC:程序运行以来共发生Minor GC的次数
  • YGCT:Minor GC消耗的时间根据案例可知:4次MinorGC总共花费0.021秒
  • FGC:程序运行以来共发生Full GC的次数
  • FGCT:总共Full GC消耗的时间
  • CGC:G1并发收集Mixed GC次数
  • CGCT:G1并发收集Mixed GC消耗的时间
  • GCT:总共垃圾回收消耗的时候

第二个功能: 堆内存统计

jstat -gccapacity 进程号

image.png

  • NGCMN:年轻代最小容量
  • NGCMX:年轻代最大容量
  • NGC:当前年轻代容量
  • S0C、S1C、EC和上面GC信息中一样
  • OGCMN:老年代最小容量
  • OGCMX:老年代最大容量
  • OGC:当前老年代大小
  • OC:和上面GC信息中一样
  • MCMN:元空间最小容量
  • MCMX:元空间最大容量
  • MC:当前元空间容量
  • CCSMN:最小压缩指针空间大小
  • CCSMX:最大压缩指针空间大小
  • CCSC:当前压缩指针空间大小
  • YGC、FGC、CGC和上面GC信息中一样

依次类推,如果想要查看专门的监视内容就下面这张图,注意点:不同垃圾收集器,打印出来的内容可能会不一样,这里就不一一带着大家去看了

image.png

JVM运行情况预估

我们还记得上面说的这个间隔时间吗,我们可以用这个间隔时间来预估JVM的运行情况

image.png

举个例子:想要每个1s执行一次jstat gc统计,总共统计10次,我们就可以这样执行jstat -gc 85164 1000 10,我们就能得到下方图中的统计信息本地随便启动了个Tomcat一直放在那里,所以没有发生变化,你们在写测试类的时候可以通过不断的new对象,来达到数据的变化监控

image.png

我们可以从这张图中看出哪些东西?

  1. 预测年轻代对象的增长速率
  2. Minor GC的触发频率和平均耗时平均耗时=YGCT/YGC,总的MinorGC耗时时间除MinorGC次数计算而出,得出系统间隔多久Minor GC频率会停顿多久Minor GC耗时
  3. 每次Minor GC之后有多少对象进入老年代,每次Minor GC之后观察EU、S0U、S1U和OU的变化情况,从而推断出每次Minor GC有多少对象进入老年代,再结合Minor GC频率判断老年代对象增长速率
  4. Full GC的触发频率和平均耗时FGCT/FGC

第二阶段: 常见的JVM优化思路

结合对象挪动到老年代的规则主要可以由以下优化思路:

  1. 简单来说就是尽量让每次Minor GC后的存活对象小于Survivor区的50%避免因为动态年龄判断机制而过早进入老年代,尽量都存活在年轻代中,尽量减少Full GC的频率,Full GC对JVM性能的影响很严重这种情况出现在高并发的系统,正常情况下每秒创建的对象都是很少的,但是某一时间段,并发量突然上升,导致新对象创建的过快,很容易因为动态年龄判断机制过早的进入老年代,所以这种情况下,要调整年轻代的大小,让这些对象都尽可能的留在年轻代,因为这些都是朝生夕死的对象
  2. 大对象需要大量连续内存空间的对象 比如字符串、数组要尽早进入老年代,因为年轻代用的是标记-复制算法,大对象在复制的时候会消耗大量的性能,所以要尽早的进入老年代使用-XX:PretenureSizeThreshold设置大小,超过这个大小的对象直接进入老年代,不会进入年轻代,这个参数只在Serial和ParNew两个GC收集器下有用

结合频繁发生Full GC的次数,主要由以下优化思路

  1. 除了正常的因为老年代空间不足而发生Full GC,还有老年代空间担保机制的存在,年轻代在每次Minor GC之前,JVM都会计算下老年代剩余可用空间,如果这个可用空间小于年轻代里现有的所有对象大小之和,就会看一个 -XX:-HandlePromotionFailure JDK 1.8默认设置参数是否设置,这个参数就是一个担保参数,担保一下如果老年代剩余空间小于历史每一次Minor GC后进入老年代的对象的平均值,就会先发生一次Full GC,再执行Minor GC,Minor GC后,老年代不够又会发生Full GC,这样一次完整的Minor GC就是两次Full GC,一次Minor GC
  2. 元空间不够会导致多余的Full GC,导致Full GC次数频繁
  3. 显示调用System.gc造成多余的Full GC,这种一般线上尽量通过 -XX:+DisableExplicitGC参数禁用,加上这个参数,System.gc就没有任何效果

第三阶段: JVM的调优案例

JVM的调优案例:线上发生频繁的Full GC

假设有这么一串JVM参数模拟真实场景下的JVM环境

1
java复制代码-Xms1536M -Xmx1536M -Xmn512M -Xss256K -XX:SurvivorRatio=6 -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly

不清楚这些命令的同学请看下面的指令介绍,知道的可以跳过下面的这一截内容:

  1. -Xms1536M:设置JVM初始内存为1536M。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
  2. -Xmx1536M:设置JVM最大可用内存为1536M
  3. -Xmn512M:设置年轻代大小为512M
  4. -Xss256K:设置每个线程的线程栈大小为256K
  5. -XX:SurvivorRatio=6:设置年轻代中Eden区与Survivor区的大小比值。设置为6,则两个Survivor区与一个Eden区的比值为2:6,一个Survivor区占整个年轻代的1/8
  6. -XX:MetaspaceSize=256M:设置元空间大小为256M
  7. -XX:MaxMetaspaceSize=256M:设置最大元空间大小为256M
  8. -XX:+UseParNewGC:设置年轻代垃圾回收器是ParNew
  9. -XX:+UseConcMarkSweepGC:设置老年代垃圾回收器是CMS
  10. -XX:CMSInitiatingOccupancyFraction=75:设置CMS在对老年代内存使用率达到75%的时候开始GC因为CMS会有浮动垃圾,所以一般都较早启动GC
  11. -XX:+UseCMSInitiatingOccupancyOnly:只用设置的回收阈值上面指定的75%,如果不指定,JVM仅在第一次使用设定值,后续则自动调整,一般和上一个命令组合使用

猜想一:由于动态年龄判断机制导致频繁的发生Full GC

那么由于动态年龄判断机制的原因导致的频繁发生Full GC,应该怎么调优呢,我们先从以下几个方面来看?

  1. 动态年龄判断机制的关键点在于年轻代的空间大小,所以首先就是要把年轻代的空间调大
  2. 如果是并发量大的系统,我们可以调小CMSInitiatingOccupancyFraction设定的值,避免产生Serial Old收集器的情况,但是如果是并发量小的系统,我们可以调大CMSInitiatingOccupancyFraction设定的值,充分利用堆空间

所以,经过调整之后,JVM的参数就如下图代码中所示:把年轻代的空间调大成1024M 这样老年代的空间就是512M 把CMS老年代内存使用阈值调大成90% 充分利用老年代的空间,如果并发量大的同学 有可能需要调低这个值 避免最后因为并发冲突导致使用Serial Old收集器

1
java复制代码-Xms1536M -Xmx1536M -Xmn1024M -Xss256K -XX:SurvivorRatio=6 -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=90 -XX:+UseCMSInitiatingOccupancyOnly

猜想二:由于老年代空间担保机制导致频繁的发生Full GC

如果按照上面设置,把老年代设置小的话,很容易会因为老年代空间担保机制,导致频繁的发生Full GC,老年代空间担保机制的关键点在于每次Minor GC的时候进入老年代对象的平均大小,所以我们要控制每次Minor GC后进入老年代的对象平均大小

判断内存中对象的分布情况

使用jmap -histo 进程号的命令,观察内存中对象的分布情况,观察有没有比较集中的对象,因为如果是并发量高的系统,接口很有可能是集中的,创建的对象也是集中的,所以可以从cpu占用比较高的方法,也就是热点方法、内存占用比较多的对象这两个方面去分析

  • 借助jvisualvm的抽样器寻找热点方法jdk8有 jdk11不支持

image.png

  • 借助jmap -histo 进程号观察占用内存比较多的对象从你工程中的对象入手

image.png

优化方向

  1. 如果是循环创建对象的话,尽量控制循环次数比如每次查询5000条记录,这些记录如果加载到内存就是要创建不少的对象,如果这批对象经过Minor GC,很容易由于老年代空间分配担保机制,发生Full GC,所以要减少查询记录条数,从而减少创建的对象
  2. 最快也是最有效的办法,在预算允许的情况下,增加物理机器的配置,增大整个堆的内存,在条件允许的情况下,也不失为一个好方法

本文总结

好啦,以上就是这篇文章的全部内容,在这篇文章中,我们也经历了下面的几个步骤

  1. 第一阶段:讲述了jmap、jinfo、jstack、jstat各个命令的基本使用,以及有什么作用
  2. 第二阶段:讲述了JVM的优化思路方向,总共可以分为两个方向
  3. 第三阶段:根据线上频繁的发生Full GC这个案例讲述了各个命令是怎么配合使用的

命令有点多,大家可以多多收藏,等到下次遇到问题了,可以直接翻出来使用

絮叨

最后,如果感到文章有哪里困惑的,请第一时间留下评论,如果各位看官觉得小沙弥我有点东西的话 求点赞👍 求关注❤️ 求分享👥 对我来说真的 非常有用!!!

本文转载自: 掘金

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

spring初识--bean的几种注册方式 注册bean的几

发表于 2021-05-03

spring彻底改变了java世界观。spring解决了java 对象管理问题,今天我们来看看spring创建对象的方式有哪些至今还不知道的吧

注册bean的几种方式(IOC)

BeanDefinition

  • 我们查看类图可以看出,BeanDefinitionRegistry下有三个实现类。spring为我们提供了一个默认的BeanDefinition注册工厂DefaultListableFactory 。 为什么说他是默认的不仅仅是因为名字里出现了Default字样。而是在AnnotationConfigApplicationContext中默认使用的就是DefaultListableFactory。在注解式开发汇总AnnotationConfigApplicationContext上下文是经常使用的。他也可以理解成spring容器。

配置类Config

  • 相信大家都是用过springboot。在springboot中首先需要一个启动类。这个启动类上会添加各种各样的注解进行修饰。其实他就是一个配置的入口。相当于spring中的xml配置文件。
  • 为了演示我们这里也需要一个这样的配置类。告诉spring哪些类他需要进行装载解析。
1
2
3
4
java复制代码
@ComponentScan(value = "com.zxhtom.cloud.order.spring")
public class Config {
}
  • Config的作用就是告诉spring去扫描com.zxhtom.cloud.order.spring包下带有spring注解的类及配置类

生成bean

  • 相信大家对@Bean、@Component、@Service这些注解都不陌生。这些都是spring bean的注解。但是这些注解的背后又是什么呢。对!就是本章节的主题BeanDefinition 。 spring的bean的注册都是通过它来完成的。
1
2
3
4
5
6
7
8
9
10
java复制代码//首先获取容器上下文
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
//生成java类对应的BeanDefinitionBuilder
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.genericBeanDefinition(Student.class);
//将BeanDefinition注册到该spring容器上
context.registerBeanDefinition("student",builder.getBeanDefinition());
//尝试获取
Object orderController = context.getBean("student");
System.out.println(orderController);

  • 上面简单几行就完成了springbean的注册。在AnnotationConfigApplicationContext中,我们看到他是继承了GenericApplicationContext这个类的。而这个类中默认的是上面我们提到的DefaultListableFactory
  • 这也是我们在开始放置的一张图。到这里我们知道BeanDefinition是spring注册bean的元素。而BeanDefinitionRegistry是注册真正的工作者。他负责解析BeanDefinition将对应的Java对象转换成spring的bean。这中间还有很多很多的细节处理。这里我们不做展开啦。

FactoryBean

  • FactoryBean是一个接口,在java8中我们只需要实现getObject、getObjectType两个方法。前者是生成对象后者是返回对象Class对象。
  • 下面我们通过FactoryBean来创建一个springbean .
1
2
3
4
5
java复制代码
@Data
public class User {
private String name;
}
  • ①首先我们编写一个普通的Java类User 。不要添加spring注册bean的注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码
public class UserFactoryBean implements FactoryBean<User> {
@Override
public User getObject() throws Exception {
User user = new User();
user.setName("hello");
return user;
}

@Override
public Class<?> getObjectType() {
return User.class;
}
}
  • ②然后在编写一个实现了FactoryBean接口的实现类。在getObject方法中我们构造一个User对象并返回。
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码
public class BeanTest {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.genericBeanDefinition(UserFactoryBean.class);
context.refresh();
context.registerBeanDefinition("user", builder.getBeanDefinition());
System.out.println("获取user:"+context.getBean("user"));
System.out.println("获取userFactoryBean:"+context.getBean("&user"));
}
}
  • ③最后我们构建一个UserFactoryBean对应的BeanDefinition对象。值得注意的是UserFactoryBean的BeanDefinition会向spring注册两个对象到spring容器中。一个是getObject方法中的对象。另外一个就是UserFactoryBean自己本身。
  • 在spring容器中是通过KV形式保存对象信息的,两个对象是不可能对应同一个key的。上面注册进来的名称是user 。
key 对象
user User对象
&user UserFactoryBean对象
  • FactoryBean作用就是将负责的bean生成过程进行代码话。 比如上面是构建一个User对象注册到spring容器。这种需求我们直接在User类上添加@Component并在Springboot启动类上添加扫描路径就可以了。
  • 但是如果User对象中的name在启动是需要将当前时间赋值给name, 这种需求我们就不好通过spring提供的注解配置了。但是通过FactoryBean就可以很好的解决了。因为我们通过代码很容器就获取到时间并进行赋值。
  • FactoryBean在spring中的地位也是很高的。在mybatis框架中如何将Mapper接口注册到spring容器就是利用他的功能。因为spring中是无法注册接口的。mybatis将接口生成代理类注册到spring容器。在执行这些代理类的时候在根据Mapper对象的xml里的sql进行SqlSession执行sql进行数据的解析。

Supplier

  • 在GenericApplicationContext类中我们发现registryBean方法中有个重载的方法需要参数Class、Supplier、BeanDefinitionCustomer。
1
2
3
4
5
6
7
8
java复制代码
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.refresh();
context.registerBean("user", User.class);
User user = (User) context.getBean("user");
System.out.println(user.getName());
}
  • 还是上面的需求,我需要注册是将当前时间赋值给User#name属性。这个时候常规操作是没法满足需求的。这个时候除了上面提到的FactoryBean以外,我们还可以通过Supplier来初始化。
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) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.refresh();
context.registerBean(User.class, new Supplier<User>() {
@Override
public User get() {
User user = new User();
user.setName(new Date().toString());
return user;
}
}, new BeanDefinitionCustomizer() {
@Override
public void customize(BeanDefinition bd) {
bd.setPrimary(true);
System.out.println(bd);
}
});
User user = (User) context.getBean("user");
System.out.println(user.getName());
}
  • 上述代码实现了User对象属性初始化注册。并对BeanDefinition进行加强更新。在Spring中Customizer往往代码封装的意思。BeanDefinitionCustomizer就是对BeanDefinition对象的封装方便对他们进行二次操作。

spring AOP

  • AOP全称Aspect Oriented Programming的缩写。在spring中我们已经习惯了面向切面编程了。切面真的帮助我们实现了很多帮助。他将我们重复性的工作进行抽离,是的我们整体的业务不再线性的编程了。最终我们在开发过程就会变成模块–>aop–>模块

概念

  • 在aop中我们需要了解到几个专有名词
  • Aspect : 申明切面
  • Joint point : 表示连接点。对异常处理
  • Pointcut : 切点。定义拦截点。由点生面
  • Advice : 在切点上进行增强,对执行点进行包装
  • Target : 代理对象的真实对象
  • Weaving: 将Aspect连接起来

spring容器的认识

  • 上面我们演示的spring的两大特性。IOC+AOP。 其中IOC就是通过反射将java对象注册到容器中。那么这个容器到底是什么个东西。这里我们就简单的理解成KV 。 内部其实就是一个Map. key就是java对象在容器中的beanName。value就是java对象本身.

授人以鱼不如授人以渔,Spring的强大相信做过Java开发的都是知道。今天我们开始Spring相关课程的第一话–纵观全局
今后我们也是从这个五个方面进行入手,由于探讨框架本身存在很多未知数,里面的总结也肯定是参考别的文献的。个人总结

Spring结构

结构

Core Container

Spring中的Core Container(核心容器)包含有Core、Beans、Context和Expression Language模块。Core和Beans模块是框架的基础部分,提供IoC(反转控制)和依赖注入特性。这里的基础概念是BeanFactory,它对Factory模式的金典实现来消除对程序性单利模式的需要,并真正地允许你从程序逻辑中分离出依赖关系和配置。

Core

core模块主要包含Spring框架基本的核心工具类,Spring的其他组件都需要使用这个包里的类。
core相当于是底层文件,任何spring的产品都是建立在core上的。

Beans

Beans在Core的基础上进行了功能的扩展它包含访问配置文件,创建和管理bean以及进行控制反转、依赖注入操作相关的所有类。
Beans相当于是功能性的提供。所有的spring项目都用到这个功能。这里引入了Spring重大特性之一的依赖注入(控制反转)

Context

Context 模块构建于 Core 和 Beans 模块基础之上,提供了一种类似于刑DI 注册器的框
架式的对象访问方法 。 Context 模块继承了 Beans 的特性,为 Spring 核心提供了大量
扩展,添加了对国际化(例如资源绑定)、事件传播、资源加载和对 Co ntext 的 透明创
建的支持 。 Context 模块同时也支持 J2EE 的一些特性, 171) :Ji.口 EJB 、几仪和基础的远程
处理 。 ApplicationContext 接口是 Context 模块的关键 。

Expression Language模块提供了一个强大的表达式语言用于在运行时查询和操作对象。

Expression Language 模块提供了强大的表达式语言,用于在运行时查询和操纵对象 。
它是 JSP 2.1 规范中定义的 unifed expression language 的扩展 。 该语言支持设直/获取属
性的值,属性的分配,方法的调用,访问数组上下文(
accessiong the context of arrays )、
容器和索引器、逻辑和算术运算符、命名变量以及从 S prit屯的 IoC 容器中根据名称检
索对象 。 它也支持 list 投影、选择和一般的 list 聚合

Data Access/Integration

Data Access/Integration 层包含而BC 、 ORM 、 OXM 、几础和 Transaction 模块 。

  • JDBC模块提供了一个JDBC抽象层,它可以消除冗长的JDBC编码和解析数据库厂商特有的错误代码。 这个模块包含了Spring对JDBC数据访问进行封装的所有类。
  • ORM模块为流行的对象-关系映射API,如JPA、JDO、Hibernate、Mybatis.提供了一个交互层。利用ORM封装包,可以混合使用所有Springboot提供的特性进行OR映射
    Spring 框架插入了若干个 ORM 框架,从而提供了 ORM 的对象关系工具,其中包括 JDO 、Hibernate 和 iBatisSQL Map 。 所有这些都遵从 Spring 的通用事务和 DAO 异常层次结构
  • OXM 模块提供了一个对 ObjecνXML 映射实现的抽象层, Object/XML 映射实现包括JAXB 、 Castor 、 XMLBeans 、 JiBX 和 XStrearn 。。
  • JMS ( Java Messaging Service )模块主要包含了 一些制造和消 费消息的特性 。
  • Transaction 模块支持编程和声明性的事务 管理,这些事务类必须实现特定的接 口,并且对所有的 POJO 都适用 。

Web

Web上下文模块建立在应用程序上下文模块之上。为基于Web的应用程序提供了上下文。所以Spring框架支持与Jakarta Struts集成。Web模块还简化了处理大部分请求以及讲请求参数绑定到域对象工作,Web层包含了Web,Servlet、Struts、Porlet模块。

  • Web模块:童工基础的面向web的集成特性。多文件上传、使用listeners初始化IOC容器、容器上下文。还包括Spring远程支持中Web的相关部分。
  • Servlet模块: 该模块包含 Spring 的 model-view-controller ( MVC)实现 。 Spring 的 MVC 框架使得模型范围内的代码和 web forms 之间能够清楚地分离开
    来,并与 Spring 框架的其他特性集成在一起
  • Struts模块:该模块提供了对Struts支持,是的类在Springboot容器中能够与一个典型的Struts web 层集成在一起,spring3.0之后已被抛弃
  • Porlet模块: 提供了用于Portlet环境和Web——servlet模块的Mvc的实现

Aop

AOP 模块提供了 一个符合 AOP 联盟标准的面向切面编程的实现,它让你可以定义例如方
法拦截器和切点,从而将逻辑代码分开,降低它们之间的调合性 。 利用 source-level 的元数据
功能,还可以将各种行为信息合并到你的代码中,这有点像 .Net 技术中的 attribute 概念 。
通过配置管理特性, SpringAOP 模块直接将面向切面的编程功能集成到了 Spring 框架中,
所以可以很容易地使 Spring 框架管理的任何对象支持 AOP 。 Spring AOP 模块为基于 Spring 的
应用程序中的对象提供了事务管理服务 。 通过使用 SpringAOP ,不用依赖 EJB 组件,就可以将
声 明性事务管理集成到应用程序中 。
Aspects 模块提供了对 AspectJ 的集成支持 。
。
Instrumentation 模块提供了 class instrumentation 支持和 classloader 实现, 使得可以在特
定的应用服务器上使用 。

Test

Test模块就是在容器内进行单元测试。没啥好说的。

单例池
BeanFactory
ApplicationContext
AnnotationConfigApplicationContext
ClassPathXmlApplicationContext
FileSystemApplicationContext

总结

  • spring是个框架,但是现在spring已经不仅仅是框架了。我们可以把spring理解成一个生态。
  • 在spring基础上已经衍生出脚手架springboot 。 还有在微服务上的springcloud
  • 而在springcloud中有衍生出很多的组件。大多都是借鉴了netflix 。 比如说eureka、 zuul 等 。 其中zuul已被springcloud gateway取代
  • 因为spring的设计优秀已经和mybatis、redis、rabbitmq这些都是完美的整合了。
  • 本章主要是介绍spring . 后面也会继续更新springcloud专题。 因为在cloud中发现对spring的基础要求还是很高的。
  • 这个五一先放放cloud 。 来个spring爽爽 。 cloud五一后再见

nuli.gif

本文转载自: 掘金

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

告诉面试官,我能优化groupBy,而且知道得很深!

发表于 2021-05-02

导读

当我们交友平台在线上运行一段时间后,为了给平台用户在搜索好友时,在搜索结果中推荐并置顶他感兴趣的好友,这时候,我们会对用户的行为做数据分析,根据分析结果给他推荐其感兴趣的好友。

这里,我采用最简单的SQL分析法:对用户过去查看好友的性别和年龄进行统计,按照年龄进行分组得到统计结果。依据该结果,给用户推荐计数最高的某个性别及年龄的好友。

那么,假设我们现在有一张用户浏览好友记录的明细表t_user_view,该表的表结构如下:

1
2
3
4
5
6
7
8
9
10
11
sql复制代码CREATE TABLE `t_user_view` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增id',
`user_id` bigint(20) DEFAULT NULL COMMENT '用户id',
`viewed_user_id` bigint(20) DEFAULT NULL COMMENT '被查看用户id',
`viewed_user_sex` tinyint(1) DEFAULT NULL COMMENT '被查看用户性别',
`viewed_user_age` int(5) DEFAULT NULL COMMENT '被查看用户年龄',
`create_time` datetime(3) DEFAULT CURRENT_TIMESTAMP(3),
`update_time` datetime(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (`id`),
UNIQUE KEY `idx_user_viewed_user` (`user_id`,`viewed_user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

为了方便使用SQL统计,见上面的表结构,我冗余了被查看用户的性别和年龄字段。

我们再来看看这张表里的记录:

image-20210321202431004.png

现在结合上面的表结构和表记录,我以user_id=1的用户为例,分组统计该用户查看的年龄在18 ~ 22之间的女性用户的数量:

1
sql复制代码SELECT viewed_user_age as age, count(*) as num FROM t_user_view WHERE user_id = 1 AND viewed_user_age BETWEEN 18 AND 22 AND viewed_user_sex = 1 GROUP BY viewed_user_age

得到统计结果如下:

image-20210321172324495.png

可见:

  • 该用户查看年龄为18的女性用户数为2
  • 该用户查看年龄为19的女性用户数为1
  • 该用户查看年龄为20的女性用户数为3

所以,user_id=1的用户对年龄为20的女性用户更感兴趣,可以更多推荐20岁的女性用户给他。

如果此时,t_user_view这张表的记录数达到千万规模,想必这条SQL的查询效率会直线下降,为什么呢?有什么办法优化呢?

想要知道原因,不得不先看一下这条SQL执行的过程是怎样的?

Explain

我们先用explain看一下这条SQL:

1
sql复制代码EXPLAIN SELECT viewed_user_age as age, count(*) as num FROM t_user_view WHERE user_id = 1 AND viewed_user_age BETWEEN 18 AND 22 AND viewed_user_sex = 1 GROUP BY viewed_user_age

执行完上面的explain语句,我们得到如下结果:

image-20210321200747868.png

在Extra这一列中出现了三个Using,这3个Using代表了《导读》中的groupBy语句分别经历了3个执行阶段:

  1. Using where:通过搜索可能的idx_user_viewed_user索引树定位到满足部分条件的viewed_user_id,然后,回表继续查找满足其他条件的记录
  2. Using temporary:使用临时表暂存待groupBy分组及统计字段信息
  3. Using filesort:使用sort_buffer对分组字段进行排序

这3个阶段中出现了一个名词:临时表。这个名词我在《MySQL分表时机:100w?300w?500w?都对也都不对!》一文中有讲到,这是MySQL连接线程可以独立访问和处理的内存区域,那么,这个临时表长什么样呢?

下面我就先讲讲这张MySQL的临时表,然后,结合上面提到的3个阶段,详细讲解《导读》中SQL的执行过程。

临时表

我们还是先看看《导读》中的这条包含groupBy语句的SQL,其中包含一个分组字段viewed_user_age和一个统计字段count(*),这两个字段是这条SQL中统计所需的部分,如果我们要做这样一个统计和分组,并把结果固化下来,肯定是需要一个内存或磁盘区域落下第一次统计的结果,然后,以这个结果做下一次的统计,因此,像这种存储中间结果,并以此结果做进一步处理的区域,MySQL叫它临时表。

刚刚提到既可以将中间结果落在内存,也可以将这个结果落在磁盘,因此,在MySQL中就出现了两种临时表:内存临时表和磁盘临时表。

内存临时表

什么是内存临时表?在早期数据量不是很大的时候,以存储分组及统计字段为例,那么,基本上内存就可以完全存放下分组及统计字段对应的所有值,这个存放大小由tmp_table_size参数决定。这时候,这个存放值的内存区域,MySQL就叫它内存临时表。

此时,或许你已经觉得MySQL将中间结果存放在内存临时表,性能已经有了保障,但是,在《MySQL分表时机:100w?300w?500w?都对也都不对!》中,我提到过内存频繁的存取会产生碎片,为此,MySQL设计了一套新的内存分配和释放机制,可以减少甚至避免临时表内存碎片,提升内存临时表的利用率。

此时,你可能会想,在《为什么我调大了sort_buffer_size,并发量一大,查询排序慢成狗?》一文中,我讲了用户态的内存分配器:ptmalloc和tcmalloc,无论是哪个分配器,它的作用就是避免用户进程频繁向Linux内核申请内存空间,造成CPU在用户态和内核态之间频繁切换,从而影响内存存取的效率。用它们就可以解决内存利用率的问题,为什么MySQL还要自己搞一套?

或许MySQL的作者觉得无论哪个内存分配器,它的实现都过于复杂,这些复杂性会影响MySQL对于内存处理的性能,因此,MySQL自身又实现了一套内存分配机制:MEM_ROOT。它的内存处理机制相对比较简单,内存临时表的分配就是采用这样一种方式。

下面,我就以《导读》中的SQL为例,详细讲解一下分组统计是如何使用MEM_ROOT内存分配和释放机制的?

MEM_ROOT

我们先看看MEM_ROOT的结构,MEM_ROOT设计比较简单,主要包含这几部分,如下图:

image-20210322205718291.png

free:一个单向链表,链表中每一个单元叫block,block中存放的是空闲的内存区,每个block包含3个元素:

  • left:block中剩余的内存大小
  • size:block对应内存的大小
  • next:指向下一个block的指针

如上图,free所在的行就是一个free链表,链表中每个箭头相连的部分就是block,block中有left和 size,每个block之间的箭头就是next指针

used:一个单向链表,链表中每一个单元叫block,block中存放已使用的内存区,同样,每个block包含上面3 个元素

min_malloc:控制一个 block 剩余空间还有多少的时候从free链表移除,加入到used链表中

block_size:block对应内存的大小

block_num:MEM_ROOT 管理的block数量

first_block_usage:free链表中第一个block不满足申请空间大小的次数

pre_alloc:当释放整个MEM_ROOT的时候可以通过参数控制,选择保留pre_alloc指向的block

下面我就以《导读》中的分组统计SQL为例,看一下MEM_ROOT是如何分配内存的?

分配

image-20210326002410273.png

  1. 初始化MEM_ROOT,见上图:

min_malloc = 32

block_num = 4

first_block_usage = 0

pre_alloc = 0

block_size = 1000

err_handler = 0

free = 0

used = 0
2. 申请内存,见上图:

由于初始化MEM_ROOT时,free = 0,说明free链表不存在,故向Linux内核申请4个大小为1000/4=250的block,构造一个free链表,如上图,链表中包含4个block ,结合前面free链表结构的说明,每个block中size为250,left也为250
3. 分配内存,见上图:

(1) 遍历free链表,从free链表头部取出第一个block,如上图向下的箭头

(2) 从取出的block中划分220大小的内存区,如上图向右的箭头上面-220,block中的left从250变成30

(3) 将划分的220大小的内存区分配给SQL中的groupby字段viewed_user_age和统计字段count(*),用于后面的统计分组数据收集到该内存区

(4) 由于第(2)步中,分配后的block中的left变成30,30 < 32,即小于第(1)步中初始化的min_malloc,所以,结合上面min_malloc的含义的讲解,该block将插入used链表尾部,如上图底部,由于used链表在第(1)步初始化时为0,所以,该block插入used链表的尾部,即插入头部

释放

下面还是以《导读》中的分组统计为例,我们再来看一下MEM_ROOT是如何释放内存的?

image-20210323233158459.png

如上图,MEM_ROOT释放内存的过程如下:

  1. 遍历used链表中,找到需要释放的block,如上图,block(30,250)为之前已分配给分组统计用的block
  2. 将block(30,250)中的left + 220,即30 + 220 = 250,释放该block已使用的220大小的内存区,得到释放后的block(250,250)
  3. 将block(250,250)插入free链表尾部,如上图曲线箭头部分

通过MEM_ROOT内存分配和释放的讲解,我们发现MEM_ROOT的内存管理方式是在每个Block上连续分配,内部碎片基本在每个Block的尾部,由min_malloc成员变量控制,但是min_malloc的值是在代码中写死的,有点不够灵活。所以,对一个block来说,当left小于min_malloc,从其申请的内存越大,那么block中的left值越小,那么,该block的内存利用率越高,碎片越少,反之,碎片越多。这个写死是MySQL的内存分配的一个缺陷。

磁盘临时表

当分组及统计字段对应的所有值大小超过tmp_table_size决定的值,那么,MySQL将使用磁盘来存储这些值。这个存放值的磁盘区域,MySQL叫它磁盘临时表。

我们都知道磁盘存取的性能一定比内存存取的性能差很多,因为会产生磁盘IO,所以,一旦分组及统计字段不得不写入磁盘,那性能相对是很差的,所以,我们尽量调大参数tmp_table_size,使得组及统计字段可以在内存临时表中处理。

执行过程

无论是使用内存临时表,还是磁盘临时表,临时表对组及统计字段的处理的方式都是一样的。《导读》中我提到想要优化《导读》中的那条SQL,就需要知道SQL执行的原理,所以,下面我就结合上面讲解的临时表的概念,详细讲讲这条SQL的执行过程,见下图:

image-20210326002155314.png

  1. 创建临时表temporary,表里有两个字段viewed_user_age和count(*),主键是viewed_user_age,如上图,倒数第二个框temporary表示临时表,框中包含两个字段viewed_user_age和count(*),框内就是这两个字段对应的值,其中viewed_user_age就是这张临时表的主键
  2. 扫描表辅助索引树idx_user_viewed_user,依次取出叶子节点上的id值,即从索引树叶子节点中取到表的主键id。如上图中的idx_user_viewed_user框就是索引树,框右侧的箭头表示取到表的主键id
  3. 根据主键id到聚簇索引cluster_index的叶子节点中查找记录,即扫描cluster_index叶子节点:

(1) 得到一条记录,然后取到记录中的viewed_user_age字段值。如上图,cluster_index框,框中最右边的一列就是viewed_user_age字段的值

(2) 如果临时表中没有主键为viewed_user_age的行,就插入一条记录 (viewed_user_age, 1)。如上图的temporary框,其左侧箭头表示将cluster_index框中的viewed_user_age字段值写入temporary临时表

(3) 如果临时表中有主键为viewed_user_age的行,就将viewed_user_age这一行的count(*)值加 1。如上图的temporary框
4. 遍历完成后,再根据字段viewed_user_age在sort_buffer中做排序,得到结果集返回给客户端。如上图中的最右边的箭头,表示将temporary框中的viewed_user_age和count(*)的值写入sort_buffer,然后,在sort_buffer中按viewed_user_age字段进行排序

通过《导读》中的SQL的执行过程的讲解,我们发现该过程经历了4个部分:idx_user_viewed_user、cluster_index、temporary和sort_buffer,对比上面explain的结果,其中前2个就对应结果中的Using where,temporary对应的是Using temporary,sort_buffer对应的是Using filesort。

优化方案

此时,我们有什么办法优化这条SQL呢?

既然这条SQL执行需要经历4个部分,那么,我们可不可以去掉最后两部分呢,即去掉temporary和sort_buffer?

答案是可以的,我们只要给SQL中的表t_user_view添加如下索引:

1
sql复制代码ALTER TABLE `t_user_view` ADD INDEX `idx_user_age_sex` (`user_id`, `viewed_user_age`, `viewed_user_sex`);

你可以自己尝试一下哦!用explain康康有什么改变!

小结

本章围绕《导读》中的分组统计SQL,通过explain分析SQL的执行阶段,结合临时表的结构,进一步剖析了SQL的详细执行过程,最后,引出优化方案:新增索引,避免临时表对分组字段的统计,及sort_buffer对分组和统计字段排序。

当然,如果实在无法避免使用临时表,那么,尽量调大tmp_table_size,避免使用磁盘临时表统计分组字段。

思考题

为什么新增了索引idx_user_age_sex可以避免临时表对分组字段的统计,及sort_buffer对分组和统计字段排序?

提示:结合索引查找的原理。

本文转载自: 掘金

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

追溯Python类的鼻祖——元类

发表于 2021-05-02

Python中万物皆对象

Python是一门面向对象的语言,所以Python中数字、字符串、列表、集合、字典、函数、类等都是对象。

利用 type() 来查看Python中的各对象类型

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
python复制代码In [11]: # 数字

In [12]: type(10)
Out[12]: int

In [13]: type(3.1415926)
Out[13]: float

In [14]: # 字符串

In [15]: type('a')
Out[15]: str

In [16]: type("abc")
Out[16]: str

In [17]: # 列表

In [18]: type(list)
Out[18]: type

In [19]: type([])
Out[19]: list

In [20]: # 集合

In [21]: type(set)
Out[21]: type

In [22]: my_set = {1, 2, 3}

In [23]: type(my_set)
Out[23]: set

In [24]: # 字典

In [25]: type(dict)
Out[25]: type

In [26]: my_dict = {'name': 'hui'}

In [27]: type(my_dict)
Out[27]: dict

In [28]: # 函数

In [29]: def func():
...: pass
...:

In [30]: type(func)
Out[30]: function

In [31]: # 类

In [32]: class Foo(object):
...: pass
...:

In [33]: type(Foo)
Out[33]: type

In [34]: f = Foo()

In [35]: type(f)
Out[35]: __main__.Foo

In [36]: # type

In [37]: type(type)
Out[37]: type

可以看出

  • 数字 1 是 int类型 的对象
  • 字符串 abc 是 str类型 的对象
  • 列表、集合、字典是 type类型 的对象,其创建出来的对象才分别属于 list、set、dict 类型
  • 函数 func 是 function类型 的对象
  • 自定义类 Foo 创建出来的对象 f 是 Foo 类型,其类本身 Foo 则是 type类型 的对象。
  • 连 type 本身都是type类型的对象
  1. 类也是对象

类就是拥有相等功能和相同的属性的对象的集合

在大多数编程语言中,类就是一组用来描述如何生成一个对象的代码段。在 Python 中这一点仍然成立:

1
2
3
4
5
6
7
8
python复制代码In [1]: class ObjectCreator(object):
...: pass
...:

In [2]: my_object = ObjectCreator()

In [3]: print(my_object)
<__main__.ObjectCreator object at 0x0000021257B5A248>

但是,Python中的类还远不止如此。类同样也是一种对象。是的,没错,就是对象。只要你 使用关键字 class,Python解释器在执行的时候就会创建一个对象。

下面的代码段:

1
2
3
python复制代码>>> class ObjectCreator(object):
… pass
…

将在内存中创建一个对象,名字就是 ObjectCreator。这个 对象(类对象ObjectCreator)拥有创建对象(实例对象)的能力。但是,它的本质仍然是一个对象,于是乎你可以对它做如下的操作:

  1. 你可以将它赋值给一个变量
  2. 你可以拷贝它
  3. 你可以为它增加属性
  4. 你可以将它作为函数参数进行传递

如下示例:

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
python复制代码In [39]: class ObjectCreator(object):
...: pass
...:

In [40]: print(ObjectCreator)
<class '__main__.ObjectCreator'>

In [41]:# 当作参数传递

In [41]: def out(obj):
...: print(obj)
...:

In [42]: out(ObjectCreator)
<class '__main__.ObjectCreator'>

In [43]: # hasattr 判断一个类是否有某种属性

In [44]: hasattr(ObjectCreator, 'name')
Out[44]: False

In [45]: # 新增类属性

In [46]: ObjectCreator.name = 'hui'

In [47]: hasattr(ObjectCreator, 'name')
Out[47]: True

In [48]: ObjectCreator.name
Out[48]: 'hui'

In [49]: # 将类赋值给变量

In [50]: obj = ObjectCreator

In [51]: obj()
Out[51]: <__main__.ObjectCreator at 0x212596a7248>

In [52]:
  1. 动态地创建类

因为类也是对象,你可以在运行时动态的创建它们,就像其他任何对象一样。首先,你可以在函数中创建类,使用 class 关键字即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
python复制代码def cls_factory(cls_name):
"""
创建类工厂
:param: cls_name 创建类的名称
"""
if cls_name == 'Foo':
class Foo():
pass
return Foo # 返回的是类,不是类的实例

elif cls_name == 'Bar':
class Bar():
pass
return Bar

IPython 测验

1
2
3
4
5
6
7
python复制代码MyClass = cls_factory('Foo')

In [60]: MyClass
Out[60]: __main__.cls_factory.<locals>.Foo # 函数返回的是类,不是类的实例

In [61]: MyClass()
Out[61]: <__main__.cls_factory.<locals>.Foo at 0x21258b1a9c8>

但这还不够动态,因为你仍然需要自己编写整个类的代码。由于类也是对象,所以它们必须是通过什么东西来生成的才对。

当你使用class关键字时,Python解释器自动创建这个对象。但就和Python中的大多数事情一样,Python仍然提供给你手动处理的方法。

  1. 使用 type 创建类

type 还有一种完全不同的功能,动态的创建类。

type可以接受一个类的描述作为参数,然后返回一个类。(要知道,根据传入参数的不同,同一个函数拥有两种完全不同的用法是一件很傻的事情,但这在Python中是为了保持向后兼容性)

type 可以像这样工作:

type(类名, 由父类名称组成的元组(针对继承的情况,可以为空),包含属性的字典(名称和值))

比如下面的代码:

1
2
3
4
5
6
7
8
python复制代码In [63]: class Test:
...: pass
...:

In [64]: Test()
Out[64]: <__main__.Test at 0x21258b34048>

In [65]:

可以手动像这样创建:

1
2
3
4
5
6
python复制代码In [69]:# 使用type定义类

In [69]: Test2 = type('Test2', (), {})

In [70]: Test2()
Out[70]: <__main__.Test2 at 0x21259665808>

我们使用 Test2 作为类名,并且也可以把它当做一个变量来作为类的引用。类和变量是不同的,这里没有任何理由把事情弄的复杂。即 type函数 中第1个实参,也可以叫做其他的名字,这个名字表示类的名字

1
2
3
4
5
6
python复制代码In [71]: UserCls = type('User', (), {})

In [72]: print(UserCls)
<class '__main__.User'>

In [73]:

使用 help 来测试这2个类

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
python复制代码In [74]: # 用 help 查看 Test类

In [75]: help(Test)
Help on class Test in module __main__:

class Test(builtins.object)
| Data descriptors defined here:
|
| __dict__
| dictionary for instance variables (if defined)
|
| __weakref__
| list of weak references to the object (if defined)


In [76]: # 用 help 查看 Test2类

In [77]: help(Test2)
Help on class Test2 in module __main__:

class Test2(builtins.object)
| Data descriptors defined here:
|
| __dict__
| dictionary for instance variables (if defined)
|
| __weakref__
| list of weak references to the object (if defined)


In [78]:
  1. 使用type创建带有属性的类

type 接受一个字典来为类定义属性,因此

1
python复制代码Parent = type('Parent', (), {'name': 'hui'})

可以翻译为:

1
2
python复制代码class Parent(object):
name = 'hui'

并且可以将 Parent 当成一个普通的类一样使用:

1
2
3
4
5
6
7
8
9
10
11
12
python复制代码In [79]: Parent = type('Parent', (), {'name': 'hui'})

In [80]: print(Parent)
<class '__main__.Parent'>

In [81]: Parent.name
Out[81]: 'hui'

In [82]: p = Parent()

In [83]: p.name
Out[83]: 'hui'

当然,你可以继承这个类,代码如下:

1
2
3
4
5
6
7
python复制代码class Child1(Parent):
name = 'jack'
sex = '男'

class Child2(Parent):
name = 'mary'
sex = '女'

就可以写成:

1
2
3
4
5
6
7
8
9
python复制代码 Child1 = type('Child1', (Parent, ), {'name': 'jack', 'sex': '男'})

In [85]: Child2 = type('Child2', (Parent, ), {'name': 'mary', 'sex': '女'})

In [87]: Child1.name, Child1.sex
Out[87]: ('jack', '男')

In [88]: Child2.name, Child2.sex
Out[88]: ('mary', '女')

注意:

  • type 的第2个参数,元组中是父类的名字,而不是字符串
  • 添加的属性是 类属性,并不是实例属性
  1. 使用type创建带有方法的类

最终你会希望为你的类增加方法。只需要定义一个有着恰当签名的函数并将其作为属性赋值就可以了。

添加实例方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
python复制代码In [89]: Parent = type('Parent', (), {'name': 'hui'})

In [90]: # 定义函数

In [91]: def get_name(self):
...: return self.name
...:

In [92]: Child3 = type('Child3', (Parent, ), {'name': 'blob', 'get_name': get_name})

In [93]: c3 = Child3()

In [94]: c3.name
Out[94]: 'blob'

In [95]: c3.get_name()
Out[95]: 'blob'

添加静态方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
python复制代码In [96]: Parent = type('Parent', (), {'name': 'hui'})

In [97]: # 定义静态方法

In [98]: @staticmethod
...: def test_static():
...: print('static method called...')
...:

In [100]: Child4 = type('Child4', (Parent, ), {'name': 'zhangsan', 'test_static': test_static})

In [101]: c4 = Child4()

In [102]: c4.test_static()
static method called...

In [103]: Child4.test_static()
static method called...

添加类方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
python复制代码In [105]: Parent = type('Parent', (), {'name': 'hui'})

In [106]: # 定义类方法

In [107]: @classmethod
...: def test_class(cls):
...: print(cls.name)
...:

In [108]: Child5 = type('Child5', (Parent, ), {'name': 'lisi', 'test_class': test_class})

In [109]: c5 = Child5()

In [110]: c5.test_class()
lisi

In [111]: Child5.test_class()
lisi

你可以看到,在Python中,类也是对象,你可以动态的创建类。这就是当你使用关键字 class 时 Python 在幕后做的事情,就是通过元类来实现的。

较为完整的使用 type 创建类的方式:

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
python复制代码class Animal(object):

def eat(self):
print('吃东西')


def dog_eat(self):
print('喜欢吃骨头')

def cat_eat(self):
print('喜欢吃鱼')


Dog = type('Dog', (Animal, ), {'tyep': '哺乳类', 'eat': dog_eat})

Cat = type('Cat', (Animal, ), {'tyep': '哺乳类', 'eat': cat_eat})

# ipython 测验
In [125]: animal = Animal()

In [126]: dog = Dog()

In [127]: cat = Cat()

In [128]: animal.eat()
吃东西

In [129]: dog.eat()
喜欢吃骨头

In [130]: cat.eat()
喜欢吃鱼
  1. 到底什么是元类(终于到主题了)

元类就是用来创建类的【东西】。你创建类就是为了创建类的实例对象,不是吗?但是我们已经学习到了Python中的类也是对象。

元类就是用来创建这些类(对象)的,元类就是类的类,你可以这样理解为:

1
2
python复制代码MyClass = MetaClass() # 使用元类创建出一个对象,这个对象称为“类”
my_object = MyClass() # 使用“类”来创建出实例对象

你已经看到了type可以让你像这样做:

1
python复制代码MyClass = type('MyClass', (), {})

这是因为函数 type 实际上是一个元类。type 就是 Python在背后用来创建所有类的元类。现在你想知道那为什么 type 会全部采用小写形式而不是 Type 呢?好吧,我猜这是为了和 str 保持一致性,str是用来创建字符串对象的类,而 int 是用来创建整数对象的类。type 就是创建类对象的类。你可以通过检查 __class__ 属性来看到这一点。因此 Python中万物皆对象

现在,对于任何一个 __class__ 的 __class__ 属性又是什么呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
python复制代码In [136]: a = 10

In [137]: b = 'acb'

In [138]: li = [1, 2, 3]

In [139]: a.__class__.__class__
Out[139]: type

In [140]: b.__class__.__class__
Out[140]: type

In [141]: li.__class__.__class__
Out[141]: type

In [142]: li.__class__.__class__.__class__
Out[142]: type

因此,元类就是创建类这种对象的东西。type 就是 Python的内建元类,当然了,你也可以创建自己的元类。

  1. __metaclass__ 属性

你可以在定义一个类的时候为其添加 __metaclass__ 属性。

1
2
3
python复制代码class Foo(object):
__metaclass__ = something…
...省略...

如果你这么做了,Python就会用元类来创建类Foo。小心点,这里面有些技巧。你首先写下 class Foo(object),但是类Foo还没有在内存中创建。Python会在类的定义中寻找 __metaclass__ 属性,如果找到了,Python就会用它来创建类Foo,如果没有找到,就会用内建的 type 来创建这个类。

1
2
python复制代码class Foo(Bar):
pass

Python做了如下的操作:

  1. Foo中有 __metaclass__ 这个属性吗?如果有,Python会通过 __metaclass__ 创建一个名字为Foo的类(对象)
  2. 如果Python没有找到 __metaclass__,它会继续在 Bar(父类) 中寻找 __metaclass__ 属性,并尝试做和前面同样的操作。
  3. 如果Python在任何父类中都找不到 __metaclass__,它就会在模块层次中去寻找 __metaclass__,并尝试做同样的操作。
  4. 如果还是找不到 __metaclass__ ,Python就会用内置的 type 来创建这个类对象。

现在的问题就是,你可以在 __metaclass__ 中放置些什么代码呢?

答案就是:可以创建一个类的东西。那么什么可以用来创建一个类呢?type,或者任何使用到type或者子类化的type都可以。

  1. 自定义元类

元类的主要目的就是为了当创建类时能够自动地改变类。

假想一个很傻的例子,你决定在你的模块里所有的类的属性都应该是大写形式。有好几种方法可以办到,但其中一种就是通过在模块级别设定 __metaclass__。采用这种方法,这个模块中的所有类都会通过这个元类来创建,我们只需要告诉元类把所有的属性都改成大写形式就万事大吉了。

幸运的是,__metaclass__ 实际上可以被任意调用,它并不需要是一个正式的类。所以,我们这里就先以一个简单的函数作为例子开始。

python2中

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
python复制代码# -*- coding:utf-8 -*-
def upper_attr(class_name, class_parents, class_attr):

# class_name 会保存类的名字 Foo
# class_parents 会保存类的父类 object
# class_attr 会以字典的方式保存所有的类属性

# 遍历属性字典,把不是__开头的属性名字变为大写
new_attr = {}
for name, value in class_attr.items():
if not name.startswith("__"):
new_attr[name.upper()] = value

# 调用type来创建一个类
return type(class_name, class_parents, new_attr)

class Foo(object):
__metaclass__ = upper_attr # 设置Foo类的元类为upper_attr
bar = 'bip'

print(hasattr(Foo, 'bar'))
# Flase
print(hasattr(Foo, 'BAR'))
# True

f = Foo()
print(f.BAR)

python3中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
python复制代码# -*- coding:utf-8 -*-
def upper_attr(class_name, class_parents, class_attr):

#遍历属性字典,把不是__开头的属性名字变为大写
new_attr = {}
for name,value in class_attr.items():
if not name.startswith("__"):
new_attr[name.upper()] = value

#调用type来创建一个类
return type(class_name, class_parents, new_attr)

# 再类的继承()中使用metaclass
class Foo(object, metaclass=upper_attr):
bar = 'bip'

print(hasattr(Foo, 'bar'))
# Flase
print(hasattr(Foo, 'BAR'))
# True

f = Foo()
print(f.BAR)

再做一次,这一次用一个真正的 class 来当做元类。

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
python复制代码class UpperAttrMetaClass(type):

def __new__(cls, class_name, class_parents, class_attr):
# 遍历属性字典,把不是__开头的属性名字变为大写
new_attr = {}
for name, value in class_attr.items():
if not name.startswith("__"):
new_attr[name.upper()] = value

# 方法1:通过'type'来做类对象的创建
return type(class_name, class_parents, new_attr)

# 方法2:复用type.__new__方法
# 这就是基本的OOP编程,没什么魔法
# return type.__new__(cls, class_name, class_parents, new_attr)


# python3的用法
class Foo(object, metaclass=UpperAttrMetaClass):
bar = 'bip'

# python2的用法
class Foo(object):
__metaclass__ = UpperAttrMetaClass
bar = 'bip'


print(hasattr(Foo, 'bar'))
# 输出: False
print(hasattr(Foo, 'BAR'))
# 输出: True

f = Foo()
print(f.BAR)
# 输出: 'bip'
1
2
3
4
python复制代码__new__ 是在__init__之前被调用的特殊方法
__new__是用来创建对象并返回之的方法
而__init__只是用来将传入的参数初始化给对象
这里,创建的对象是类,我们希望能够自定义它,所以我们这里改写__new__

就是这样,除此之外,关于元类真的没有别的可说的了。但就元类本身而言,它们其实是很简单的:

  1. 拦截类的创建
  2. 修改类
  3. 返回修改之后的类

究竟为什么要使用元类?

现在回到我们的大主题上来,究竟是为什么你会去使用这样一种容易出错且晦涩的特性?

好吧,一般来说,你根本就用不上它:

“元类就是深度的魔法,99%的用户应该根本不必为此操心。如果你想搞清楚究竟是否需要用到元类,那么你就不需要它。那些实际用到元类的人都非常清楚地知道他们需要做什么,而且根本不需要解释为什么要用元类。” —— Python界的领袖 Tim Peters

源代码

源代码已上传到 Gitee PythonKnowledge: Python知识宝库,欢迎大家来访。

✍ 码字不易,还望各位大侠多多支持❤️。

公众号

新建文件夹X

大自然用数百亿年创造出我们现实世界,而程序员用几百年创造出一个完全不同的虚拟世界。我们用键盘敲出一砖一瓦,用大脑构建一切。人们把1000视为权威,我们反其道行之,捍卫1024的地位。我们不是键盘侠,我们只是平凡世界中不凡的缔造者 。

本文转载自: 掘金

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

1…676677678…956

开发者博客

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