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

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


  • 首页

  • 归档

  • 搜索

分布式任务调度框架XXL-JOB(一):手把手教你使用XXL

发表于 2021-08-17

作为当下最热门的分布式调度框架,xxl-job收到众多开发者的追捧。本文将从实践着手,从0到1搭建一个xxl-job调度平台。

1. 搭建xxl-job

1.1 下载xxl-job源码

码云地址:gitee.com/xuxueli0323…
官网地址:

1.2 执行sql脚本

在doc/db/tables_xxl_job.sql目录下,有xxl-job必备的数据库

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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
sql复制代码#
# XXL-JOB v2.3.0
# Copyright (c) 2015-present, xuxueli.

CREATE database if NOT EXISTS `xxl_job` default character set utf8mb4 collate utf8mb4_unicode_ci;
use `xxl_job`;

SET NAMES utf8mb4;

CREATE TABLE `xxl_job_info` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`job_group` int(11) NOT NULL COMMENT '执行器主键ID',
`job_desc` varchar(255) NOT NULL,
`add_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`author` varchar(64) DEFAULT NULL COMMENT '作者',
`alarm_email` varchar(255) DEFAULT NULL COMMENT '报警邮件',
`schedule_type` varchar(50) NOT NULL DEFAULT 'NONE' COMMENT '调度类型',
`schedule_conf` varchar(128) DEFAULT NULL COMMENT '调度配置,值含义取决于调度类型',
`misfire_strategy` varchar(50) NOT NULL DEFAULT 'DO_NOTHING' COMMENT '调度过期策略',
`executor_route_strategy` varchar(50) DEFAULT NULL COMMENT '执行器路由策略',
`executor_handler` varchar(255) DEFAULT NULL COMMENT '执行器任务handler',
`executor_param` varchar(512) DEFAULT NULL COMMENT '执行器任务参数',
`executor_block_strategy` varchar(50) DEFAULT NULL COMMENT '阻塞处理策略',
`executor_timeout` int(11) NOT NULL DEFAULT '0' COMMENT '任务执行超时时间,单位秒',
`executor_fail_retry_count` int(11) NOT NULL DEFAULT '0' COMMENT '失败重试次数',
`glue_type` varchar(50) NOT NULL COMMENT 'GLUE类型',
`glue_source` mediumtext COMMENT 'GLUE源代码',
`glue_remark` varchar(128) DEFAULT NULL COMMENT 'GLUE备注',
`glue_updatetime` datetime DEFAULT NULL COMMENT 'GLUE更新时间',
`child_jobid` varchar(255) DEFAULT NULL COMMENT '子任务ID,多个逗号分隔',
`trigger_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '调度状态:0-停止,1-运行',
`trigger_last_time` bigint(13) NOT NULL DEFAULT '0' COMMENT '上次调度时间',
`trigger_next_time` bigint(13) NOT NULL DEFAULT '0' COMMENT '下次调度时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `xxl_job_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`job_group` int(11) NOT NULL COMMENT '执行器主键ID',
`job_id` int(11) NOT NULL COMMENT '任务,主键ID',
`executor_address` varchar(255) DEFAULT NULL COMMENT '执行器地址,本次执行的地址',
`executor_handler` varchar(255) DEFAULT NULL COMMENT '执行器任务handler',
`executor_param` varchar(512) DEFAULT NULL COMMENT '执行器任务参数',
`executor_sharding_param` varchar(20) DEFAULT NULL COMMENT '执行器任务分片参数,格式如 1/2',
`executor_fail_retry_count` int(11) NOT NULL DEFAULT '0' COMMENT '失败重试次数',
`trigger_time` datetime DEFAULT NULL COMMENT '调度-时间',
`trigger_code` int(11) NOT NULL COMMENT '调度-结果',
`trigger_msg` text COMMENT '调度-日志',
`handle_time` datetime DEFAULT NULL COMMENT '执行-时间',
`handle_code` int(11) NOT NULL COMMENT '执行-状态',
`handle_msg` text COMMENT '执行-日志',
`alarm_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '告警状态:0-默认、1-无需告警、2-告警成功、3-告警失败',
PRIMARY KEY (`id`),
KEY `I_trigger_time` (`trigger_time`),
KEY `I_handle_code` (`handle_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `xxl_job_log_report` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`trigger_day` datetime DEFAULT NULL COMMENT '调度-时间',
`running_count` int(11) NOT NULL DEFAULT '0' COMMENT '运行中-日志数量',
`suc_count` int(11) NOT NULL DEFAULT '0' COMMENT '执行成功-日志数量',
`fail_count` int(11) NOT NULL DEFAULT '0' COMMENT '执行失败-日志数量',
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `i_trigger_day` (`trigger_day`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `xxl_job_logglue` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`job_id` int(11) NOT NULL COMMENT '任务,主键ID',
`glue_type` varchar(50) DEFAULT NULL COMMENT 'GLUE类型',
`glue_source` mediumtext COMMENT 'GLUE源代码',
`glue_remark` varchar(128) NOT NULL COMMENT 'GLUE备注',
`add_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `xxl_job_registry` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`registry_group` varchar(50) NOT NULL,
`registry_key` varchar(255) NOT NULL,
`registry_value` varchar(255) NOT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `i_g_k_v` (`registry_group`,`registry_key`,`registry_value`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `xxl_job_group` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`app_name` varchar(64) NOT NULL COMMENT '执行器AppName',
`title` varchar(12) NOT NULL COMMENT '执行器名称',
`address_type` tinyint(4) NOT NULL DEFAULT '0' COMMENT '执行器地址类型:0=自动注册、1=手动录入',
`address_list` text COMMENT '执行器地址列表,多地址逗号分隔',
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `xxl_job_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '账号',
`password` varchar(50) NOT NULL COMMENT '密码',
`role` tinyint(4) NOT NULL COMMENT '角色:0-普通用户、1-管理员',
`permission` varchar(255) DEFAULT NULL COMMENT '权限:执行器ID列表,多个逗号分割',
PRIMARY KEY (`id`),
UNIQUE KEY `i_username` (`username`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `xxl_job_lock` (
`lock_name` varchar(50) NOT NULL COMMENT '锁名称',
PRIMARY KEY (`lock_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO `xxl_job_group`(`id`, `app_name`, `title`, `address_type`, `address_list`, `update_time`) VALUES (1, 'xxl-job-executor-sample', '示例执行器', 0, NULL, '2018-11-03 22:21:31' );
INSERT INTO `xxl_job_info`(`id`, `job_group`, `job_desc`, `add_time`, `update_time`, `author`, `alarm_email`, `schedule_type`, `schedule_conf`, `misfire_strategy`, `executor_route_strategy`, `executor_handler`, `executor_param`, `executor_block_strategy`, `executor_timeout`, `executor_fail_retry_count`, `glue_type`, `glue_source`, `glue_remark`, `glue_updatetime`, `child_jobid`) VALUES (1, 1, '测试任务1', '2018-11-03 22:21:31', '2018-11-03 22:21:31', 'XXL', '', 'CRON', '0 0 0 * * ? *', 'DO_NOTHING', 'FIRST', 'demoJobHandler', '', 'SERIAL_EXECUTION', 0, 0, 'BEAN', '', 'GLUE代码初始化', '2018-11-03 22:21:31', '');
INSERT INTO `xxl_job_user`(`id`, `username`, `password`, `role`, `permission`) VALUES (1, 'admin', 'e10adc3949ba59abbe56e057f20f883e', 1, NULL);
INSERT INTO `xxl_job_lock` ( `lock_name`) VALUES ( 'schedule_lock');

commit;

image.png

1.3 修改配置文件

修改配置文件,主要是将数据库连接信息配置成自己的数据库信息,邮箱也配置成自己的邮箱账户信息

xxl-job/xxl-job-admin/src/main/resources/application.properties

image.png

image.png

修改日志配置文件logback.xml,将路径修改成相对路径

1
xml复制代码<property name="log.path" value="./data/applogs/xxl-job/xxl-job-admin.log"/>

并在resources目录下,新增/data/applogs/xxl-job/xxl-job-admin.log文件

image.png

1.4 启动项目

com.xxl.job.admin.XxlJobAdminApplication 启动项目,启动成功如下,默认是8080端口。

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
log复制代码Connected to the target VM, address: '127.0.0.1:53860', transport: 'socket'

. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.4.2)

10:22:57.256 logback [main] INFO c.x.job.admin.XxlJobAdminApplication - Starting XxlJobAdminApplication using Java 1.8.0_211 on zhaowenyiDSC008130.local with PID 83345 (/Users/zhaowenyi/Documents/project/volvo/cms-x/xxl-job/xxl-job-admin/target/classes started by zhaowenyi in /Users/zhaowenyi/Documents/project/volvo/cms-x/xxl-job)
10:22:57.259 logback [main] INFO c.x.job.admin.XxlJobAdminApplication - No active profile set, falling back to default profiles: default
10:23:00.583 logback [main] INFO o.s.b.w.e.tomcat.TomcatWebServer - Tomcat initialized with port(s): 8080 (http)
10:23:00.607 logback [main] INFO o.a.coyote.http11.Http11NioProtocol - Initializing ProtocolHandler ["http-nio-8080"]
10:23:00.609 logback [main] INFO o.a.catalina.core.StandardService - Starting service [Tomcat]
10:23:00.609 logback [main] INFO o.a.catalina.core.StandardEngine - Starting Servlet engine: [Apache Tomcat/9.0.41]
10:23:00.732 logback [main] INFO o.a.c.c.C.[.[.[/xxl-job-admin] - Initializing Spring embedded WebApplicationContext
10:23:00.732 logback [main] INFO o.s.b.w.s.c.ServletWebServerApplicationContext - Root WebApplicationContext: initialization completed in 2577 ms
10:23:01.904 logback [main] INFO c.x.j.a.c.scheduler.XxlJobScheduler - >>>>>>>>> init xxl-job admin success.
10:23:01.951 logback [xxl-job, admin JobLogReportHelper] INFO com.zaxxer.hikari.HikariDataSource - HikariCP - Starting...
10:23:02.133 logback [main] INFO o.s.s.c.ThreadPoolTaskExecutor - Initializing ExecutorService 'applicationTaskExecutor'
10:23:02.350 logback [main] INFO o.s.b.a.w.s.WelcomePageHandlerMapping - Adding welcome page template: index
10:23:03.153 logback [xxl-job, admin JobLogReportHelper] INFO com.zaxxer.hikari.HikariDataSource - HikariCP - Start completed.
10:23:03.257 logback [main] INFO o.s.b.a.e.web.EndpointLinksResolver - Exposing 2 endpoint(s) beneath base path '/actuator'
10:23:03.308 logback [main] INFO o.a.coyote.http11.Http11NioProtocol - Starting ProtocolHandler ["http-nio-8080"]
10:23:03.349 logback [main] INFO o.a.c.c.C.[.[.[/xxl-job-admin] - Initializing Spring DispatcherServlet 'dispatcherServlet'
10:23:03.349 logback [main] INFO o.s.web.servlet.DispatcherServlet - Initializing Servlet 'dispatcherServlet'
10:23:03.352 logback [main] INFO o.s.web.servlet.DispatcherServlet - Completed initialization in 2 ms
10:23:03.353 logback [main] INFO o.s.b.w.e.tomcat.TomcatWebServer - Tomcat started on port(s): 8080 (http) with context path '/xxl-job-admin'
10:23:03.393 logback [main] INFO c.x.job.admin.XxlJobAdminApplication - Started XxlJobAdminApplication in 7.021 seconds (JVM running for 8.851)
10:23:06.002 logback [xxl-job, admin JobScheduleHelper#scheduleThread] INFO c.x.j.a.c.thread.JobScheduleHelper - >>>>>>>>> init xxl-job admin scheduler success.

在浏览器中打开 http://localhost:8080/xxl-job-admin/toLogin ,打开如下界面说明部署成功

账号名:admin 密码:123456

image.png

2. 配置xxl-job执行器

在xxl-job中,每一个项目相当于一个执行器,一个执行器里面可以配置多个定时任务。

2.1 添加依赖

  • 首先在需要引入定时任务的项目中添加依赖
1
2
3
4
5
xml复制代码<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>2.3.0</version>
</dependency>

2.2 修改配置

  • 在yml配置文件中,新增xxl-job相关配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
yml复制代码xxl:
job:
admin:
# 管理台地址
addresses: http://127.0.0.1:8080/xxl-job-admin
accessToken:
executor:
# 当前项目名称
appname: cms-content
address:
ip:
port: 9999
logpath:
logretentiondays: 30

2.3 添加config配置文件

直接添加无需修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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复制代码/**
* xxl-job config
*
* @author xuxueli 2017-04-28
*/
@Configuration
public class XxlJobConfig {
private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);

@Value("${xxl.job.admin.addresses}")
private String adminAddresses;

@Value("${xxl.job.accessToken}")
private String accessToken;

@Value("${xxl.job.executor.appname}")
private String appname;

@Value("${xxl.job.executor.address}")
private String address;

@Value("${xxl.job.executor.ip}")
private String ip;

@Value("${xxl.job.executor.port}")
private int port;

@Value("${xxl.job.executor.logpath}")
private String logPath;

@Value("${xxl.job.executor.logretentiondays}")
private int logRetentionDays;


@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
logger.info(">>>>>>>>>>> xxl-job config init.");
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
xxlJobSpringExecutor.setAppname(appname);
xxlJobSpringExecutor.setAddress(address);
xxlJobSpringExecutor.setIp(ip);
xxlJobSpringExecutor.setPort(port);
xxlJobSpringExecutor.setAccessToken(accessToken);
xxlJobSpringExecutor.setLogPath(logPath);
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);

return xxlJobSpringExecutor;
}

/**
* 针对多网卡、容器内部署等情况,可借助 "spring-cloud-commons" 提供的 "InetUtils" 组件灵活定制注册IP;
*
* 1、引入依赖:
* <dependency>
* <groupId>org.springframework.cloud</groupId>
* <artifactId>spring-cloud-commons</artifactId>
* <version>${version}</version>
* </dependency>
*
* 2、配置文件,或者容器启动变量
* spring.cloud.inetutils.preferred-networks: 'xxx.xxx.xxx.'
*
* 3、获取IP
* String ip_ = inetUtils.findFirstNonLoopbackHostInfo().getIpAddress();
*/


}

2.4 新增执行器

执行器管理-新增执行器

  • AppName 填写应用名
  • 名称 填写描述信息
  • 注册方式 自动注册
  • 机器地址 无需填写,会自定扫描

image.png

3. 配置任务

3.1 新增任务

  • 接下来就新增一个定时任务,我们需要在刚才配置的执行器项目中新增job
1
2
3
4
5
6
7
8
9
10
java复制代码@Component
@Slf4j
public class AutoPublishJob{

@XxlJob("AutoPublishJob")
public ReturnT<String> execute(String s) throws Exception {
log.info("---------------开始执行任务-------------");
return null;
}
}

3.2 配置任务

进入到管理界面,任务管理-新增任务,填写必填信息即可

  • 执行器:选择刚刚新增的执行器
  • 调度类型:Cron表达式
  • JobHandler:xxljob注解中定义的
  • 运行模式:bean模式

image.png

image.png

3.3 启动项目

项目启动后,会注册到任务调度中心,日志如下

1
2
3
4
5
6
7
8
9
10
11
log复制代码2021-08-17 12:01:56.293  INFO 88467 --- [           main] c.s.cms.base.web.config.XxlJobConfig     : >>>>>>>>>>> xxl-job config init.
2021-08-17 12:01:56.372 INFO 88467 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2021-08-17 12:01:56.481 INFO 88467 --- [ main] o.s.s.c.ThreadPoolTaskScheduler : Initializing ExecutorService 'Nacos-Watch-Task-Scheduler'
2021-08-17 12:01:56.617 WARN 88467 --- [ main] iguration$LoadBalancerCaffeineWarnLogger : Spring Cloud LoadBalancer is currently working with the default cache. You can switch to using Caffeine cache, by adding it to the classpath.
2021-08-17 12:01:56.650 INFO 88467 --- [ main] c.xxl.job.core.executor.XxlJobExecutor : >>>>>>>>>>> xxl-job register jobhandler success, name:AutoPublishJob, jobHandler:com.xxl.job.core.handler.impl.MethodJobHandler@59c862af[class com.souche.cms.base.web.job.AutoPublishJob#execute]
2021-08-17 12:01:56.727 WARN 88467 --- [ main] c.xxl.job.core.executor.XxlJobExecutor : >>>>>>>>>>> xxl-job accessToken is empty. To ensure system security, please set the accessToken.
2021-08-17 12:01:56.900 INFO 88467 --- [ main] com.alibaba.nacos.client.naming : initializer namespace from System Property :null
2021-08-17 12:01:56.900 INFO 88467 --- [ main] com.alibaba.nacos.client.naming : initializer namespace from System Environment :null
2021-08-17 12:01:56.901 INFO 88467 --- [ main] com.alibaba.nacos.client.naming : initializer namespace from System Property :null
2021-08-17 12:01:56.993 INFO 88467 --- [ Thread-22] com.xxl.job.core.server.EmbedServer : >>>>>>>>>>> xxl-job remoting server start success, nettype = class com.xxl.job.core.server.EmbedServer, port = 9999
2021-08-17 12:01:57.145 INFO 88467 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8072 (http) with context path ''

3.4 启动任务

image.png

启动成功后,定时任务就会被调用成功

image.png

image.png

4. 总结

至此我们已经成功搭建了xxl-job和定时任务。关于xxl-job的一些工作机制和原理还没有涉及,计划将在后续文章进行深入探讨。

本文转载自: 掘金

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

不懂Ribbon原理的可以进来看看哦,分析SpringBoo

发表于 2021-08-17

在这里插入图片描述  前面详细的给大家介绍了SpringBoot的核心内容,有了这部分的基础支持的话,我们再来分析SpringCloud中的相关组件就很容器了,本文我们来给大家开始介绍Ribbon的相关内容,首先来介绍下Ribbon项目在启动的时候完成了哪些操作。

一、项目案例准备

  首先我们大家案例环境,通过【RestTemplate】来实现服务调用,通过【Ribbon】实现客户端负载均衡操作。一起来进阶提升吧:463257262【QQ总群】

在这里插入图片描述

1.Order服务

  我们的Order服务作为服务提供者。创建SpringBoot项目,并添加相关依赖

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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.9</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.bobo.springcloud</groupId>
<artifactId>spring-cloud-order-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-cloud-order-server</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>


<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR10</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

  然后在属性文件中添加相关的配置

1
2
properties复制代码spring.application.name=spring-cloud-order-service
server.port=8081

  然后创建自定义的Controller 提供对外的服务

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

@Value("${server.port}")
private int port;

@GetMapping("/orders")
public String orders(){
System.out.println("Order 服务端口是:"+port);
return "Order Services ..... ";
}
}

然后我们可以分别启动两个Order服务,端口分别设置为 8081和8082

2.User服务

  User服务作为调用用Order服务的客户端。也是我们要重点介绍【Ribbon】的服务。同样创建一个SpringBoot项目,添加相关的依赖

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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.9.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.bobo.springcloud</groupId>
<artifactId>spring-cloud-user-service2</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-cloud-user-service2</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR10</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

然后在属性文件中配置相关信息

1
2
properties复制代码spring.application.name=spring-cloud-user-service
spring-cloud-order-service.ribbon.listOfServers=localhost:8081,localhost:8082

然后创建自定义的Controller来实现服务的调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码@RestController
public class UserController {
@Autowired
public RestTemplate restTemplate;

@Autowired
LoadBalancerClient loadBalancerClient;

@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}

@GetMapping("/users")
public String users(){
ServiceInstance choose = loadBalancerClient.choose("spring-cloud-order-service");
String url = String.format("http://%s:%s",choose.getHost(),choose.getPort()+"/orders");
//return restTemplate.getForObject(url,String.class);
return restTemplate.getForObject("http://spring-cloud-order-service/orders",String.class);
}
}

然后启动User服务访问,可以看到【Ribbon】默认通过轮询的方式来实现了服务的调用
在这里插入图片描述

二、Ribbon原理分析

  应用比较简单,我们主要是来分析下【Ribbon】的核心原理,先来看看自动装配做了哪些事情。

1.RibbonAutoConfiguration

  Ribbon在系统启动的时候自动装配完成的设置,我们先来看看对应的spring.factories 中的配置信息吧
请添加图片描述
emsp; 所以我们要继续来看【RibbonAutoConfiguration】配置类,我们贴出【RibbonAutoConfiguration】的关键信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码@Configuration
@Conditional({RibbonAutoConfiguration.RibbonClassesConditions.class})
@RibbonClients
@AutoConfigureAfter(
name = {"org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration"}
)
// RibbonAutoConfiguration配置类注入容器后会完成 LoadBalancerAutoConfiguration 和 AsyncLoadBalancerAutoConfiguration 的注入
@AutoConfigureBefore({LoadBalancerAutoConfiguration.class, AsyncLoadBalancerAutoConfiguration.class})
@EnableConfigurationProperties({RibbonEagerLoadProperties.class, ServerIntrospectorProperties.class})
public class RibbonAutoConfiguration {


/**
* 如果IoC容器中不存在 LoadBalancerClient 类型的对象就注入一个
* 具体注入的类型为 RibbonLoadBalancerClient 对象
**/
@Bean
@ConditionalOnMissingBean({LoadBalancerClient.class})
public LoadBalancerClient loadBalancerClient() {
return new RibbonLoadBalancerClient(this.springClientFactory());
}

// 省略其他代码

  通过源码查看我们知道在SpringBoot项目启动的时候完成了【LoadBalancerClient】对象的注入,且具体的类型为【RibbonLoadBalancerClient】,同时还会完成【LoadBalancerAutoConfiguration】这个配置类型的加载。在看【LoadBalancerAutoConfiguration】做了什么事情之前,我们先来搞清楚【@LoadBalanced】注解的作用

2.LoadBalancerAutoConfiguration

1
2
3
4
5
6
7
8
java复制代码@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LoadBalanced {

}

  【@LoadBalanced】本质上就是一个【@Qualifier】注解。作用就是标记,我们通过案例来演示说明。

定义一个简单的【User】类

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

String name;

public User(String name) {
this.name = name;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
'}';
}
}

然后定义一个Java配置类,有两个添加了【@LoadBalanced】注解,有一个没有加。

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

@LoadBalanced
@Bean("user1")
public User user1(){
return new User("user1");
}

@Bean("user2")
public User user2(){
return new User("user2");
}

@LoadBalanced
@Bean("user3")
public User user3(){
return new User("user3");
}


}

然后创建我们的控制器,来测试使用

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

@LoadBalanced
@Autowired
List<User> list = Collections.emptyList();

@GetMapping("/querys")
public String query(){
return list.toString();
}
}

项目结构
请添加图片描述

启动SpringBoot项目后我们看效果
请添加图片描述

搞清楚了【@LoadBalanced】的作用后,我们再来看看【LoadBalancerAutoConfiguration】的配置加载做了什么事情

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

/**
* 1.
* 获取IoC容器中所有的被【@LoadBalanced】注解修饰的RestTemplate对象
* 这些对象保存在了一个集合中
**/
@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();

@Autowired(required = false)
private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList();

/**
* 4.
* 向容器中注入了 SmartInitializingSingleton 对象,并且实现了 SmartInitializingSingleton 接口中声明的
* afterSingletonsInstantiated 方法,在该方法中 通过3 中的 RestTemplateCustomizer中定义的 customize 方法
* 实现了 RestTemplate 对象拦截器的植入
**/
@Bean
public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(
final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) {
return () -> restTemplateCustomizers.ifAvailable(customizers -> {
for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
for (RestTemplateCustomizer customizer : customizers) {
customizer.customize(restTemplate);
}
}
});
}

@Bean
@ConditionalOnMissingBean
public LoadBalancerRequestFactory loadBalancerRequestFactory(
LoadBalancerClient loadBalancerClient) {
return new LoadBalancerRequestFactory(loadBalancerClient, this.transformers);
}

@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
static class LoadBalancerInterceptorConfig {

/**
* 2.
* 创建了一个 LoadBalancerInterceptor 并注入到了容器中
**/
@Bean
public LoadBalancerInterceptor loadBalancerInterceptor(
LoadBalancerClient loadBalancerClient,
LoadBalancerRequestFactory requestFactory) {
return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
}
/**
* 3.
* 创建了一个 RestTemplateCustomizer 并注入到了容器中
* 而且通过内部类的方式定义定义了 RestTemplateCustomizer 接口中的 customize 方法的逻辑
**/
@Bean
@ConditionalOnMissingBean
public RestTemplateCustomizer restTemplateCustomizer(
final LoadBalancerInterceptor loadBalancerInterceptor) {
return restTemplate -> {
// 获取 RestTemplate 中原有的 拦截器
List<ClientHttpRequestInterceptor> list = new ArrayList<>(
restTemplate.getInterceptors());
// 在原有的拦截器的基础上 添加了一个 LoadBalancerInterceptor
list.add(loadBalancerInterceptor);
// 然后将添加有新的 拦截器的集合 设置到了 RestTemplate 对象中
restTemplate.setInterceptors(list);
};
}

}

// 省略其他代码

}

通过对应的备注大家可以搞清楚该配置类的作用是实现了对【RestTemplate】对象(被@LoadBalanced修饰)植入

【LoadBalancerInterceptor】拦截器的功能。

小结Ribbon系统时的操作
请添加图片描述

~好了相信大家应该对于在自动装配时完成了 【RestTemplate】植入拦截器的逻辑应该很清楚了,下篇文章我们详细介绍Ribbon具体是怎么来处理负载均衡逻辑的,敬请期待,欢迎一键三连哦!!!

本文转载自: 掘金

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

用Python图像识别技术打造一个小狗分类器,实现让机器自己

发表于 2021-08-17

这是我参与8月更文挑战的第7天

前言

今日给大家带来的是图像识别技术——小狗分类器
在这里插入图片描述

工具使用

开发环境:win10、python3.6
开发工具:pycharm
工具包 :keras,numpy, PIL

效果展示

训练集的准确率为0.925,但测试集只有0.7
说明过拟合了,可以再增加一些图片,或者使用数据增强,来减少过拟合。

图片

测试了两张图片,全都识别对了!

图片

思路分析

  • 1 准备数据集
  • 2 数据集的预处理
  • 3 搭建卷积神经网络
  • 4 训练
  • 5 预测

1、准备数据集

我们可以通过爬虫技术,把4类图像(京巴、拉布拉多、柯基、泰迪)保存到本地。总共有840张图片做训练集,188张图片做测试集。

2 数据集的预处理

1 统一尺寸为1001003(RGB彩色图像)

1
2
3
4
python复制代码# 统一尺寸的核心代码
img = Image.open(img_path)
new_img = img.resize((100, 100), Image.BILINEAR)
new_img.save(os.path.join('./dog_kinds_after/' + dog_name, jpgfile))

2 由于数据是自己下载的,需要制作标签(label),可提取图像名称的第一个数字作为类别。(重命名图片)

1
2
3
4
5
6
7
python复制代码kind = 0

# 遍历京巴的文件夹
images = os.listdir(images_path)
for name in images:
image_path = images_path + '/'
os.rename(image_path + name, image_path + str(kind) +'_' + name.split('.')[0]+'.jpg')

3 划分数据集

840张图片做训练集,188张图片做测试集。

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复制代码# 只放了训练集的代码,测试集一样操作。
ima_train = os.listdir('./train')

# 图片其实就是一个矩阵(每一个像素都是0-255之间的数)(100*100*3)
# 1.把图片转换为矩阵
def read_train_image(filename):
img = Image.open('./train/' + filename).convert('RGB')
return np.array(img)

x_train = []
# 2.把所有的图片矩阵放在一个列表里 (840, 100, 100, 3)
for i in ima_train:
x_train.append(read_train_image(i))
x_train = np.array(x_train)
# 3.提取kind类别作为标签
y_train = []
for filename in ima_train:
y_train.append(int(filename.split('_')[0]))

# 标签(0/1/2/3)(840,)
y_train = np.array(y_train)

# 我是因为重命名图片为(1/2/3/4),所以都减了1
# 为了能够转化为独热矩阵
y_train = y_train - 1

# 4.把标签转换为独热矩阵
# 将类别信息转换为独热码的形式(独热码有利于神经网络的训练)
y_train = np_utils.to_categorical(y_train)
y_test = np_utils.to_categorical(y_test)
print(y_test)

x_train = x_train.astype('float32')
x_test = x_test.astype('float32')

x_train /= 255
x_test /= 255
print(x_train.shape) # (840, 100, 100, 3)
print(y_train.shape) # (840,)

3 搭建卷积神经网络

Keras是基于TensorFlow的深度学习库,是由纯Python编写而成的高层神经网络API,也仅支持Python开发。

它是为了支持快速实践而对Tensorflow的再次封装,让我们可以不用关注过多的底层细节,能够把想法快速转换为结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
python复制代码# 1.搭建模型(类似于VGG,直接拿来用就行)
model = Sequential()
# 这里搭建的卷积层共有32个卷积核,卷积核大小为3*3,采用relu的激活方式。
# input_shape,字面意思就是输入数据的维度。

#这里使用序贯模型,比较容易理解
#序贯模型就像搭积木一样,将神经网络一层一层往上搭上去

model.add(Conv2D(32, (3, 3), activation='relu', input_shape=(100, 100, 3)))
model.add(Conv2D(32, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))

model.add(Conv2D(64, (3, 3), activation='relu'))
model.add(Conv2D(64, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))
#dropout层可以防止过拟合,每次有25%的数据将被抛弃

model.add(Flatten())
model.add(Dense(256, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(4, activation='softmax'))

4 训练

训练的过程,就是最优解的过程。

图片

对上图来说,就是根据数据集,不断的迭代,找到一条最近似的直线(y = kx + b),把参数k,b保存下来,预测的时候直接加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
python复制代码# 编译模型
sgd = SGD(lr=0.01, decay=1e-6, momentum=0.9, nesterov=True)
model.compile(loss='categorical_crossentropy', optimizer=sgd, metrics=['accuracy'])

# 一共进行32轮
# 也就是说840张图片,每次训练10张,相当于一共训练84次
model.fit(x_train, y_train, batch_size=10, epochs=32)


# 保存权重文件(也就是相当于“房价问题的k和b两个参数”)
model.save_weights('./dog_weights.h5', overwrite=True)
# 评估模型
score = model.evaluate(x_test, y_test, batch_size=10)
print(score)

5 预测

此时k、b(参数)和x(小狗的图像)都是已知的了,求k(类别)就完了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
python复制代码# 1.上传图片
name = input('上传图片的名称(例如:XX.jpg)为:')

# 2.预处理图片(代码省略)

# 3.加载权重文件
model.load_weights('dog_weights.h5')

# 4.预测类别
classes = model.predict_classes(x_test)[0]

target = ['京巴', '拉布拉多', '柯基', '泰迪']
# 3-泰迪 2-柯基 1-拉布拉多 0-京巴

# 5.打印结果
print("识别结果为:" + target[classes])

文章到这里就结束了,感谢你的观看,Python数据分析系列,下个系列分享Python小技巧

为了感谢读者们,我想把我最近收藏的一些编程干货分享给大家,回馈每一个读者,希望能帮到你们。

干货主要有:

① 2000多本Python电子书(主流和经典的书籍应该都有了)

② Python标准库资料(最全中文版)

③ 项目源码(四五十个有趣且经典的练手项目及源码)

④ Python基础入门、爬虫、web开发、大数据分析方面的视频(适合小白学习)

⑤ Python学习路线图(告别不入流的学习)

All done~详见个人简介或者私信获取完整源代码。。

往期回顾

Python实现“假”数据

Python爬虫鲁迅先生《经典语录》

Python爬虫豆瓣热门话题

本文转载自: 掘金

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

Flink 从0-1实现 电商实时数仓 - 日志数据采集

发表于 2021-08-17

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

日志数据采集

流程

image.png

  1. 前端埋点
  2. 通过nginx到日志服务器
  3. 将 Event 打印到日志文件
  4. 将 Event 发送至Kafka

实现

1. 新建日志项目
  1. 新建 springboot 项目 tmall-logger
  2. 添加 Maven 依赖
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
xml复制代码<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
  1. 新建 LoggerController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码@Slf4j
@RestController
public class LoggerController {

@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
@Value("${kafka.topic}")
private String kafkaTopic;

@RequestMapping("/applog")
public String logger(String param) {
//打印日志
log.info(param);
//发送至kafka
kafkaTemplate.send(kafkaTopic, param);
return "success";
}

}
  1. 配置 logback
* 给 LoggerController 单独配置打印到一个文件,每行一个 Event
* 在 `resources` 新建 `logback-spring.xml`
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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<configuration>

<springProperty scope="context" name="LOG_PATH" source="logging.file.path" defaultValue="./logs"/>
<springProperty scope="context" name="LOG_LEVEL" source="logging.level.root" defaultValue="INFO"/>

<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%msg%n</pattern>
</encoder>
</appender>

<appender name="rollingFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/app.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/app.%d{yyyy-MM-dd}.log</fileNamePattern>
</rollingPolicy>
<encoder>
<pattern>%msg%n</pattern>
</encoder>
</appender>


<!-- 将某一个包下日志单独打印日志 -->
<logger name="wang.yeting.tmall.logger.controller.LoggerController"
level="INFO" additivity="false">
<appender-ref ref="rollingFile" />
<appender-ref ref="console" />
</logger>

<root level="error" additivity="false">
<appender-ref ref="console" />
</root>
</configuration>
  1. 修改配置文件
1
2
3
4
5
6
7
8
9
ini复制代码server.port=8081
//kafka
spring.kafka.bootstrap-servers=hd1:9092,hd2:9092,hd3:9092
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer
kafka.topic=ods_base_log
//日志
#logging.file.path=/opt/logs
#logging.level.root=info
2. 打包部署
  1. maven 打包
  2. 上传到服务器
  3. 启动

我这里启动了三台,分别是 hd1:8081   hd2:8081   hd3:8081

3. 配置 Nginx
  1. 在server内部配置
1
2
3
bash复制代码location /applog{
proxy_pass http://logserver.com;
}
  1. 在server外部配置反向代理
1
2
3
4
5
ini复制代码upstream logserver.com{
server hd1:8081 weight=1;
server hd2:8081 weight=2;
server hd3:8081 weight=3;
}
4. 群启 群停 脚本

提前配置好java环境变量

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
shell复制代码#!/bin/bash

APPNAME=tmall-logger-0.0.1-SNAPSHOT.jar
case $1 in
"start")
{
for i in hd1 hd2 hd3
do
echo "启动 logger 服务: $i"
ssh $i  "java -Xms32m -Xmx64m  -jar /opt/module/tmall/$APPNAME >/dev/null 2>&1  &"
done

echo "启动 hd1 nginx"
ssh hd1 "/opt/module/nginx/sbin/nginx"
};;

"stop")
{
echo "停止 hd1 nginx"
sh hd1 "/opt/module/nginx/sbin/nginx -s stop"

for i in hd1 hd2 hd3
do
echo "停止 logger 服务: $i"
ssh $i "ps -ef|grep $APPNAME |grep -v grep|awk '{print \$2}'|xargs kill" >/dev/null 2>&1
done
};;
esac

明日预告:Flink 从0-1实现 电商实时数仓 - 业务数据采集

关注专栏持续更新 👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻

本文转载自: 掘金

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

SpringBoot基于异常处理exception发送邮件消

发表于 2021-08-17

在项目常常会出现一些意料之外的错误,不能及时处理,大家都懂的哈。😁当然现在有很多监控服务,我这点能力是不够写的哈。☺

就有了这么一个小小的思路,用邮件服务来提醒出现异常啦。👩‍💻

(狗头保命)👩‍💻

很喜欢一句话:”八小时内谋生活,八小时外谋发展“
我们:"待别日相见时,都已有所成”😁
封面:
曾经想和女朋友一起去看的生活这么久的城市中的一个小小地方,事实上去是去了,只是一个人去了。
21.8.14


一、前言

SpringBoot异步实现发送邮件服务

1)异常处理概述:

异常处理,是编程语言或计算机硬件里的一种机制,用于处理软件或信息系统中出现的异常状况(即超出程序正常执行流程的某些特殊条件)。通过异常处理,我们可以对用户在程序中的非法输入进行控制和提示,以防程序崩溃。以返回正确的信息给前台。

2)异常处理:

SpringBoot中的异常处理分为局部处理异常和全局处理异常。方式稍稍有些差异。

2.1、局部异常处理:

  • 是在当前类中进行处理,复用性太低,不推荐使用,所以只是简单举个例子哈。
  • @ExceptionHandler 注解处理局部异常

例如:

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

private static final Logger log = LoggerFactory.getLogger(ExceptionController.class);

@RequestMapping("/exceptionMethod")
public String exceptionMethod(Model model) throws Exception {
model.addAttribute("msg", "没有抛出异常");
int num = 1/0;
log.info(String.valueOf(num));
return "home";
}

/**
* 描述:捕获 ExceptionController 中的 ArithmeticException 异常
* @param model 将Model对象注入到方法中
* @param e 将产生异常对象注入到方法中
* @return 指定错误页面
*/
@ExceptionHandler(value = {ArithmeticException.class})
public String arithmeticExceptionHandle(Model model, Exception e) {
model.addAttribute("msg", "@ExceptionHandler" + e.getMessage());
log.info(e.getMessage());
return "error";
}
}

2.2、全局异常处理:

  • 使用 @ControllerAdvice + @ExceptionHandler 注解能够处理全局异常,这种方式推荐使用,可以根据不同的异常对不同的异常进行处理。

这种稍后会在案例中讲解。

全局处理还有一种方式:配置 SimpleMappingExceptionResolver 类处理异常

因为现在使用SpringBoot更多的是使用前后端分离的方式,这种和视图的关联就不怎么合适,所以也归入不推荐的方式中啦。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码@Configuration
public class GlobalException {
@Bean
public SimpleMappingExceptionResolver
getSimpleMappingExceptionResolver(){
SimpleMappingExceptionResolver resolver = new SimpleMappingExceptionResolver();
Properties mappings = new Properties();
/*
* 参数一:异常的类型,注意必须是异常类型的全名
* 参数二:视图名称
*/
mappings.put("java.lang.ArithmeticException", "errors");

//设置异常与视图映射信息的
resolver.setExceptionMappings(mappings);
return resolver;
}
}

二、环境准备

案例:

我这里只是简单模拟了一个最简单的异常来测试哈。就是请求方法出错HttpRequestMethodNotSupportedException,然后发送邮件哈。

项目结构:

在这里插入图片描述

下面来看具体的代码:

2.1、导入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
xml复制代码<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
</dependencies>

2.2、yml配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
yml复制代码server:
port: 8092
spring:
application:
name: springboot-exception-email
mail:
# 配置 SMTP 服务器地址
host: smtp.qq.com
# 发送者邮箱
username:
# 配置密码,注意不是真正的密码,而是刚刚申请到的授权码
password:
# 端口号465或587
port: 587
# 默认的邮件编码为UTF-8
default-encoding: UTF-8
# 配置SSL 加密工厂
properties:
mail:
smtp:
socketFactoryClass: javax.net.ssl.SSLSocketFactory
#表示开启 DEBUG 模式,这样,邮件发送过程的日志会在控制台打印出来,方便排查错误
debug: true

2.3、一些公共的类

ThreadPoolTaskExecutorConfig :线程池配置类

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
java复制代码/**
* 异步线程池ThreadPoolExecutor 配置类
* @author cuberxp
* @since 1.0.0
* Create time 2020/4/2 23:23
*/
@Configuration
@EnableAsync
public class ThreadPoolTaskExecutorConfig {

@Bean
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//设置核心线程数
executor.setCorePoolSize(10);
//设置最大线程数
executor.setMaxPoolSize(20);
//缓冲队列200:用来缓冲执行任务的队列
executor.setQueueCapacity(200);
//线程活路时间 60 秒
executor.setKeepAliveSeconds(60);
//线程池名的前缀:设置好了之后可以方便我们定位处理任务所在的线程池
executor.setThreadNamePrefix("taskExecutor-");
//设置拒绝策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.setWaitForTasksToCompleteOnShutdown(true);
return executor;
}
}

ResponseDto:统一返回给前端的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码/**
* @author crush
*/
@Data
@NoArgsConstructor
public class ResponseDto<T> {
/** * 错误码*/
private Integer code;
/** * 提示信息*/
private String msg;
/** * 具体的内容*/
private T data;

public ResponseDto(Integer code, String msg) {
this.code = code;
this.msg = msg;
this.data = null;
}
public static ResponseDto success(Object object){
ResponseDto result = new ResponseDto();
result.setCode(200);
result.setMsg("操作成功");
result.setData(object);
return result;
}
}

一些基础环境就准备好了,剩下就是最简单的编码啦哈。

2.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
java复制代码/**
* @author crush
* @ControllerAdvice
* @ResponseBody //表示返回的对象,Spring会自动把该对象进行json转化,最后写入到Response中。
*/
@ControllerAdvice
@ResponseBody
@Component
public class GlobalExceptionHandler {

@Autowired
EmailService emailService;
/**
* //表示让Spring捕获到所有抛出的SignException异常,并交由这个被注解的方法处理。
* //表示设置状态码
* @return
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
ResponseDto handleException(HttpRequestMethodNotSupportedException exception){
LogEmail email = new LogEmail()
.setToEmail("951930136@qq.com")
.setSubject("异常报告")
.setContext(exception.getMessage());
emailService.senderEmail(email);
return new ResponseDto(405,exception.getMessage());
}
}

三、业务代码

3.1、entity

1
2
3
4
5
6
7
8
java复制代码@Data
@Accessors(chain = true)
public class LogEmail {
private String toEmail;
private String fromEmail;
private String subject;
private String context;
}

3.2、Service

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
java复制代码public interface EmailService {
/*** 出现异常 发送短信*/
void senderEmail(LogEmail logEmail);
}

@Slf4j
@Service
public class EmailServiceImpl implements EmailService {

@Autowired
private JavaMailSender javaMailSender;

@Value("${spring.mail.username}")
private String fromEmail;

@Async("taskExecutor")
@Override
public void senderEmail(LogEmail logEmail) {
log.info(Thread.currentThread().getName());
//一个复杂的邮件
MimeMessage message = this.javaMailSender.createMimeMessage();
try {
//组装
MimeMessageHelper helper = new MimeMessageHelper(message, true);
//正文
//主题
helper.setSubject(logEmail.getSubject());
//开启html模式
helper.setText("<h1>"+logEmail.getContext()+"</h1>" ,true);
//附件
helper.addAttachment("1.jpg", new File("C:\\Users\\ASUS\\Desktop\\杂七杂八\\杂图\\2.gif"));
helper.setTo(logEmail.getToEmail());
helper.setFrom(fromEmail);
javaMailSender.send(message);
} catch (MessagingException e) {
e.printStackTrace();
}
}
}

3.3、Controller

1
2
3
4
5
6
7
8
java复制代码@RestController
public class DemoController {
@GetMapping("/test")
public ResponseDto test(){![在这里插入图片描述](https://img-blog.csdnimg.cn/d5ba2fdfee864dec834cac791c09ef65.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NTgyMTgxMQ==,size_16,color_FFFFFF,t_70#pic_center)

return ResponseDto.success("我喜欢你!!!");
}
}

业务代码就这些了,不过记得要补充一个主启动类哈,这个我就不贴啦哈。

四、测试

测试特别简单,先看正常的哈。

我们用正确的GET方式发送请求是完全没有问题的,返回也是正确的数据。

在这里插入图片描述

接下来我们用POST方式来请求,看能不能正确的调用邮件方法发送邮件啊😁

在这里插入图片描述
证明我们确实已经抓住了这个异常,并且也成功发送了邮件。

这里只是一个小小的Demo,处理的异常也比较简单,如果真正要去用的话,肯定是不会放在这样的异常上面的,而是一些更加重要的异常上面,细节也会更加的完善。邮件可以一次性提醒很多人,方便应用程序的及时维护。

五、自言自语

在这里插入图片描述

我知道咱们掘金的大佬,讲话又好听,长的又帅,女朋友随便new,给小弟一个赞👍,这肯定的吧。😁

你好,如果你正巧看到这篇文章,并且觉得对你有益的话,就给个赞吧,让我感受一下分享的喜悦吧,蟹蟹。🤗

如若有写的有误的地方,也请大家不啬赐教!!

同样如若有存在疑惑的地方,请留言或私信,定会在第一时间回复你。

持续更新中

本文转载自: 掘金

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

JVM学习日记⭐️HotSpot算法细节实现(上)⭐️ 🔉引

发表于 2021-08-17

“这是我参与8月更文挑战的第17天,活动详情查看:8月更文挑战”

🔉引言

前面说了对象判决和垃圾收集算法,接下来说说HotSpot算法细节的实现,算法算法,效率第一,才能保证虚拟机的高效运行。

根节点枚举

不要怕,根节点枚举,对于专有名词一律用大白话处理,首先说说根节点,我们哪里见过根节点啊?嗯?可能大家忘了,我们在对象判决中提到过根节点【传送门】,就是GCRoots,枚举呢就是遍历查找,那我们之前就提到过一个场景,可达性分析,我们要查找这个对象是否是垃圾对象,就看它是否在GCRoots的引用链上,但是你要是挨个检查,无疑会浪费不少时间。

而且根节点枚举这一步骤是必须暂停所有的用户线程的,也就是我们常说的Stop the Wrold,那为什么执行这一操作需要暂停所有用户线程呢?到底是谁在影响可达性分析结果的准确性呢?我们来思考一下:那就想想如果我们不暂停用户线程,会发生什么?可能对象之间的引用频繁会发生变化,这样,我们就无法保证可达性分析的准确性了,因为可能这一时刻是垃圾,下一刻就不是啦啊,所以我们必须保证根节点枚举这一过程执行时,引用关系必须要冻结。

那当用户线程停顿下来之后,我们也不需要一个不漏的检查完所有执行上下文和所有引用的位置,虚拟机这么聪明,肯定是能直接得到我们的对象引用的,所以我们要吊打🔨虚拟机(邪恶😈),据虚拟机交代:是有一组称为:OopMap的数据结构储存的,在类加载动作完成之后,虚拟机就会把什么偏移量上是什么类型给计算出来,并进行存储,那这样收集器在扫描时就可直接得到引用信息了。

安全点

在OopMap的帮助下,Hotspot会快速准确的完成GCRoots枚举,那问题来了,如果为每个指令都进行存储,空间有点顶不住啊💥,这样垃圾回收带来的成本将是几何倍的增加。那虚拟机撒谎了嘛?又一顿严刑拷打,虚拟机哭了:我没有让OopMap都存储啊,我只是在特定位置上进行存储,这样的位置称为“安全点”。

于是,我们找到了安全点,安全点说:老大交代了,用户程序在执行垃圾回收时,代码指令流必须要到达我这,才能暂停进行垃圾收集,(你这不就是卡点嘛)。接下来问了问收集器,收集器说:“这样的卡点不能太多,太多我等待时间长,太少。我执行次数太频繁,会增大运行时内存的负荷。

那啥样的地方会产生安全点呢?当然是繁华的地方,繁华的地方路特别多,程序跑的时间也特别长,”长时间执行“最明显的特征就是指令的复用,比如:方法调用、循环跳转、异常跳转等。

那我们知道,java是多线程的啊,那我怎么能让这么多线程在垃圾回收的时候都跑到最近的安全点呢?这里我们有两个方法:抢先式中断(Preemptive Suspension)和主动式中断(Voluntary Suspension)。

线程中断方案

抢先式中断

当垃圾收集需要中断线程时,由系统把所有线程先中断,说白了就是断电,然后看看谁不在安全点上,恢复这个线程执行,再过一段时间中断,直到跑到安全点上。

主动式中断

当垃圾收集需要中断线程时,不对线程进行操作,只是简单设置一个标志位,交给所有线程主动去轮询,当发现结果为true时,就自己在最近的安全点上主动挂起,轮询操作的指令必须足够精简,因为轮询操作会在代码频繁出现,这样才能保证它足够高效。在HotSpot中,该操作采用内存保护陷阱的方式,将汇编指令精简到一条。

安全区域

前面我们提到,线程执行可以自己跑到安全点,要是线程不执行呢?嗯?线程还有不执行的时候?线程睡着了吗??对,就是睡着了😴,那虚拟机还要等它醒?重新分配处理器时间吗?那显然是不可能的,虚拟机一脸拒绝,那我们就需要引入安全区域(Safe Region)来帮我们搞定了。

点动成线,线动成面安全点拉伸自然会组成安全区域,该区域能够确保在一段代码片段之中的引用关系不会发生变化。

就是说线程如果开进了这块区域,就会被标识已进入该区域,虚拟机在进行根节点枚举的操作时,就必须一直等待,直到收到该操作执行完的信号时方可离开。

📢题外话

😩我发现我哪有锻炼的时间呢?

是黑洞时间太多了嘛?其实也不是,每天20:00左右到家,吃饭,娱乐一小会,然后就写文章写到快22:50左右,然后就23:00,还锻炼个鬼啊?

就和健身一样,时间记录也是一件蛮难的事情,从我开始记录以来,好像都没有坚持到一周过,我原来是咋记录的呢?就是每天简要的记一下自己做事用的时间,表格记录着:上班,看书,写作,玩,第二天还是上班,看书,写作,玩,我感觉太千篇一律,无聊透顶。

是我把时间记录想的太简单了,序言提到:时间记录是一个复杂而完整的体系,需要有清晰的记录原则,恰当的时间分类,简单的记录行为,还有对记录效率的思考和对时间如何分析利用的深度探索。

没深入看,就先当自己学会了,在实践中慢慢探索吧,就这么着,明个儿再看😂。

本文转载自: 掘金

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

数据可视化分析平台开源方案

发表于 2021-08-17

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

对当前开源的数据可视化平台做了一个功能调研,大家可以根据需要尝试使用。

Superset(推荐使用)

  • github.com/apache/incu…
  • Superset 的 Airbnb 开源的数据可视化工具,目前属于 Apache 孵化器项目,主要用于数据分析师进行数据可视化工作
  • 快速创建可交互的、直观形象的数据集合
  • 有丰富的可视化方法来分析数据,且具有灵活的扩展能力
  • 丰富的在线 SQL 编辑器,以及一个简单的工作流来创建任何结果集的可视化
  • 具有可扩展的、高粒度的安全模型,可以用复杂规则来控制访问权限
  • 使用简单的语法,就可以控制数据在 UI 中的展现方式
  • 支持大多数 SQL 数据库
  • 与 Druid 深度结合,可快速的分析大数据
  • 配置缓存来快速加载仪表盘

redash(推荐使用)

  • github.com/getredash/r…
  • 支持 ClickHouse
  • 美观程度相比 Superset 不够精美
  • 支持简单的报警规则
  • 可以把 Dashboard 分享出去
  • 支持的图表类型有限

Grafana(推荐使用)

  • github.com/grafana/gra…
  • Grafana 是一款用 Go 语言开发的开源数据可视化工具
  • 展示方式:快速灵活的客户端图表,面板插件有许多不同方式的可视化指标和日志,官方库中具有丰富的仪表盘插件,比如热图、折线图、图表等多种展示方式
  • 官方支持以下数据源:Graphite、InfluxDB、OpenTSDB、Prometheus、Elasticsearch、CloudWatch、KairosDB
  • 支持通知提醒:以可视方式定义最重要指标的警报规则,Grafana 将不断计算并发送通知,在数据达到阈值时通过 Slack、PagerDuty 等获得通知

DataEase(可以试用)

  • github.com/dataease/da…
  • 新产品,很有生命力
  • 国产,团队其他开源作品 JumpServer
  • 图表展示:支持 PC 端、移动端及大屏;
  • 图表制作:支持丰富的图表类型(基于 Apache ECharts 实现)、支持拖拉拽方式快速制作仪表板;
  • 数据引擎:支持直连模式、本地模式(基于 Apache Doris / Kettle 实现);
  • 数据连接:支持关系型数据库、Excel 等文件、Hadoop 等大数据平台、NoSQL 等各种数据源。

Davinci(可以等待新作品)

  • github.com/edp963/davi…
  • 国产
  • 宜信开发的达芬奇,也是 Java 系
  • 功能还是比较全面的,只是在国内还没有大范围的使用
  • 目前已经不再迭代,研发团队在准备新品发布,据说比这个更棒

Metabase(可以试用)

  • github.com/metabase/me…
  • 目前不支持 ClickHouse
  • 让团队中的成员在不知道 SQL 的情况下提出问题
  • 丰富美丽的仪表板与自动刷新和全屏模式
  • 分析师和数据专家专属 SQL 模式
  • 为你的团队创建规范细分和指标以供使用
  • 通过重命名、注释和隐藏字段为你的团队人性化数据

Kibana

  • github.com/elastic/kib…
  • Kibana 是为 Elasticsearch设计的开源分析和可视化平台

Zeppelin

  • github.com/apache/zepp…
  • 来自 Apache 项目
  • 支持 ClickHouse
  • Zeppelin 更像是一个 Notebook,而不是一个单纯的 BI 工具

本文转载自: 掘金

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

大学四年一路走来,我把这些私藏的算法学习工具全贡献出来了!

发表于 2021-08-17

大家都知道,数据结构和算法一直是学习编程和求职路上的一个大的拦路虎,而且不管是大厂还是小厂,在笔试和面试时都是在重点考察数据结构+算法题。

这篇文章就把自己当时在学习数据结构和算法路上私藏的一些比较好用的神器工具网站和资源做一波梳理和总结,相信看完一定会有你喜欢的!

话不多说,上菜!


Data Structure Visualization

我们都知道数据结构和算法一个比较难以掌握的原因就是很难形象地在脑海中形成一个结构化的画面,尤其是当一些结构和算法比较复杂时,对人的想象能力有一定要求。

Data Structure Visualization这个网站则提供了一种将数据结构和算法进行可视化的功能,并开发了交互式的动画展示,便于理解和掌握数据结构+算法。

目前该网站已经把包括像列表、堆、栈、队列、树、哈希表、图、查找、排序、递归、动态规划等一系列主要的数据结构和算法都进行了可视化展示,非常便于初学者理解和掌握。

我们以排序算法里的「堆排序为例」,这个网站可以给出完整的可视化过程,可以说非常nice了!

而且在动画下方还可以暂停播放以及自行调节画幅大小以及动画速度等一些参数,非常便于学习。


Big-O Cheat Sheet

提到数据结构和算法的学习,一个绕不过去的问题那就是算法复杂度,包括时间复杂度分析和空间复杂度分析。

复杂度通常会使用大O记号来表示,比如冒泡排序的平均时间复杂度是O(n^2),而快速排序的平均时间复杂度则是O(nlog(n))。

除此之外还有包括像堆、栈、队列、链表、跳表、哈希、B-Tree、堆排序、选择排序、归并排序等等一系列数据结构和算法的复杂度最好都是能要求在理解的基础上熟记的。

Big-O Cheat Sheet这个网站则把常见的数据结构和算法的各种复杂度进行了对比+整理+归纳,并制备了精美的表格,可供查阅+复习+背诵,一目了然,非常清楚。


VisuAlgo

VisuAlgo这个网站也提供将常见数据结构和算法进行动态可视化的功能,并且支持中文。

VisuAlgo一个比较不错的功能是除了可以动态演示算法之外,还可以按步骤进行动画演示甚至是交互,并且包含了每个步骤的解释,的确有点酷炫了。

这地方给一个归并排序的动画示例看看效果:


Algorithm Visualizer

Algorithm Visualizer同样是一款交互式算法可视化网站,并且支持Java、JavaScript、C++等主流编程语言。

该网站内容分为三大部分,最左侧可以自由选择不同的数据结构和算法(目前支持包括二叉树、图、排序、查找、动归、贪心等很多数据结构以及算法);中间部分则进行算法可视化展示以及控制台的打印输出;最右侧用于展示算法的源码并支持修改运行并看到效果。


牛客在线编程

牛客题库里面的在线编程模块用于进行数据结构和算法的练习还是十分不错的,里面有不少题目是剑指offer上的题,这些基本都是求职前必刷的。

除了支持提交多种编程语言的代码之外,还有一个比较好的点是可以看到大家的题解、讨论、甚至是提交的代码,而且又是中文社区网站,这样交流和参考起来还是挺方便的。


codeforces

codeforces别名CF,一看这名字就知道有点东西。它是一家俄罗斯的网站,而且据说最早是由一群颇具开源精神的大学生们维护的。

codeforces的比赛系统和积分系统一直是被大家所熟知的。大家在上面用它的比赛系统还是比较多的,每个用户都有Rating积分,很多人都在上面享受上分带来的成就感。

除此之外,上面的题集质量也还不错,各种难度都有,在里面刷题也是非常不错的选择。


HackerRank

HackerRank其实和上面刚介绍的codeforces有点像,主要内容也包括数据结构和算法题的题库练习与比赛。

除此之外,HackerRank在其他相关的计算机技术主题上都有涉猎,比如像编程语言、SQL、数据库等等。


LeetCode

LeetCode力扣我想就不用多说了。

俗话说得好,熟刷算法300遍,不会做题也会吹。没错,就算上面提到的所有网站都没有兴趣不想看,那这里LeetCode上的数据结构和算法的题目基本是必刷的。

还记得我们那时候用LeetCode时才几百道题,现在的LeetCode题量和那时相比感觉都翻了好多倍了。

一般来说,LeetCode题量慢慢上来之后,再加上多回顾多总结多发散,慢慢地对于数据结构和算法这一块就会变得越来越有心得。没办法,这玩意没啥捷径,多思考多练习才是关键。


知名OJ

除了上面这些网站之外,还有一些口碑一直不错的高校OJ系统有兴趣的也可以看一看。

  • 北大OJ:

  • 中科大OJ:

  • 杭电OJ:

  • 哈工大OJ


写在最后

好了,今天的分享就到这里了,希望这些学习和练习数据结构+算法的工具和资源能够对大家有所帮助。

另外最近花了大把力气,把自用的编程学习资源做了个大整理。

都是纯肝货,目录如下。

该内容 GitHub github.com/rd2coding/R… 已经收录,里面还有我整理的6大编程方向的自学路线+知识点大梳理、我的简历、面试考点、几本硬核pdf笔记,以及我的程序员人生,欢迎star。

整理不易,欢迎支持,我们下篇见!

本文转载自: 掘金

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

Python获取全部基金前十大持仓股并进行选股分析

发表于 2021-08-16

选股逻辑

股市有风险,投资需谨慎。

投资方向:跟随基金机构进行选股,简单来说,就是优先筛选那些基金公司重仓持有的股票。

目标设定

获取全部基金前十大持仓股的持股数、市值等信息,分析出排名比较靠前的基金重仓股。

爬取全部基金代码信息

注:使用python内置sqlite数据库

建表存储基金代码信息

1
2
3
4
5
6
7
8
9
10
11
lua复制代码-- 全部标的信息
create table if not exists targets_all
(
  uid     string,
  name     string,
  id       string,
  category string,
  tag     string,
  type     string,
  version  datetime not null default (datetime('now', 'localtime'))
);

参考:juejin.cn/post/699352…

image-20210816231040667

根据基金代码获取相应持仓信息

建表存储持仓信息

1
2
3
4
5
6
7
8
9
10
11
12
13
php复制代码-- 基金前十大持仓股
create table if not exists top10_stocks_in_fund
(
  uid string,
  fund_id string,
  rank_num integer,
  stock_id string,
  stock_name string,
  hold_rate float,
  hold_count float,
  hold_value float,
  update_time string
)

爬取并存入数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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
104
105
106
107
108
109
110
111
112
113
114
python复制代码# coding:utf-8
import logging
from collections import defaultdict
from bs4 import BeautifulSoup as BS
import requests
import uuid
​
from common.constant import CONN_KEY
from util import db_util
from common import global_var as gv
​
logging.basicConfig(level=logging.INFO)
​
​
# 将请求到的数据进行解析
def _parse_content(content):
   result = content.replace("var apidata={", "")
   result = result[:-2]
​
   content = result.split("",arryear:")[0].replace("content:"", "")
   content = BS(content).find_all("tr")
​
   result = defaultdict(list)
​
   for tr in content:
       date = tr.parent.parent.previous_sibling.previous_sibling.find("font").text
       td = tr.find_all("td")
       if len(td) == 9:
           result[date].append({
               "rank_num": td[0].text,
               "stock_id": td[1].text,
               "stock_name": td[2].text,
               "hold_rate": float(td[6].text.replace("%", ""))/100,
               "hold_count": td[7].text,
               "hold_value": eval(td[8].text.replace(",", ""))
          })
       elif len(td) == 7:
           result[date].append({
               "rank_num": td[0].text,
               "stock_id": td[1].text,
               "stock_name": td[2].text,
               "hold_rate": float(td[4].text.replace("%", ""))/100,
               "hold_count": td[5].text,
               "hold_value": eval(td[6].text.replace(",", ""))
          })
​
   return result
​
​
# 根据基金代码获取基金持仓
def get_top10_stocks_by_fund(fund_code):
   base_url = "https://fundf10.eastmoney.com/FundArchivesDatas.aspx?type=jjcc&code={fund_code}&topline=10"
   url = base_url.format(fund_code=fund_code)
   response = str(requests.get(url).content, "utf-8")
   return _parse_content(response)
​
​
# 判断当前基金的股票持仓是否存在
def _is_exist(conn, fund_id, stock_id, update_time):
   query_sql = f"select 1 from top10_stocks_in_fund " \
               f"where fund_id = '{fund_id}'" \
               f"and stock_id = '{stock_id}'" \
               f"and update_time = '{update_time}'"
​
   res = conn.query(query_sql)
   if res.fetchone():
       return True
   return False
​
​
# 更新基金前10大持仓股信息
def update_top10_stocks_by_fund():
   # 初始化数据库连接池
   db_util.init_conn_pool()
   conn = gv.get_value(CONN_KEY)
   query_sql = "select * from targets_all where type = '基金'"
   targets = conn.query(query_sql)
​
   # 记录更新成功和失败的条数
   success_count = 0
   failure_count = 0
​
   for (uid, fund_name, fund_id, category, tag, type, version) in targets:
       fund_id = str(fund_id).zfill(6)
       logging.info(f"正在请求{fund_name}-{fund_id}的持仓股信息...")
       stocks_in_fund = get_top10_stocks_by_fund(fund_id)
​
       # 获取持仓信息
       for update_time, stocks in stocks_in_fund.items():
           # 遍历各条持股信息
           for stock in stocks:
               # 若当前持股信息已存在,则不再插入
               if _is_exist(conn, fund_id, stock["stock_id"], update_time):
                   continue
​
               # 插入新的持股信息
               insert_sql = f"insert into top10_stocks_in_fund" \
                            f"(uid,fund_id,rank_num,stock_id,stock_name,hold_rate,hold_count,hold_value,update_time)" \
                            f"values('{uuid.uuid4()}','{fund_id}',{stock['rank_num']}," \
                            f"'{stock['stock_id']}','{stock['stock_name']}',{stock['hold_rate']}," \
                            f"{stock['hold_count']},{stock['hold_value']},'{update_time}')"
​
               if conn.operate(insert_sql):
                   logging.info("插入成功!")
                   success_count = success_count+1
               else:
                   logging.warning(f"插入失败:{insert_sql}")
                   failure_count = failure_count+1
​
   logging.info(f"全部基金持仓信息更新完毕,成功:{success_count}条,失败:{failure_count}条。")
​
​
if __name__ == "__main__":
   update_top10_stocks_by_fund()

image-20210816231409806

分析第一/二季度基金排名靠前的重仓股

1
2
3
4
5
sql复制代码select stock_id "股票代码", stock_name "股票名称", cast(sum(hold_value) as double) "基金持有总市值"
from top10_stocks_in_fund
where update_time = '2021-06-30'
group by 1, 2
order by 3 desc

image-20210816232230853

1
2
3
4
5
sql复制代码select stock_id "股票代码", stock_name "股票名称", cast(sum(hold_count) as double) "基金持有数(万股)"
from top10_stocks_in_fund
where update_time = '2021-06-30'
group by 1, 2
order by 3 desc

image-20210816232322994

结论

金融市场偏好能源消费、银行券商等行业龙头股,深入研究持股数和市值变化,可以为加仓换股提供比较有意义的参考。(注:基金持仓信息大多是每季度末更新,距离越久,参考意义越不大)

本文转载自: 掘金

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

进程间通信方式详解 进程间通信方式

发表于 2021-08-16
  • 进程间通信方式
    • 引言
    • 共享内存
    • 信号量
      • 信号量的工作原理
      • 理解信号量
    • 管道
      • 匿名管道
      • 命名管道
    • 消息队列
      • 什么是消息队列?
      • 特点
    • 信号
      • 关于信号
      • 特点
    • 套接字
    • 结语

文章已收录我的仓库:Java学习笔记与免费书籍分享

进程间通信方式

引言

在操作系统中,一个进程可以理解为是关于计算机资源集合的一次运行活动,其就是一个正在执行的程序的实例。从概念上来说,一个进程拥有它自己的虚拟CPU和虚拟地址空间,任何一个进程对于彼此而言都是相互独立的,这也引入了一个问题 —— 如何让进程之间互相通信?

由于进程之间是互相独立的,没有任何手段直接通信,因此我们需要借助操作系统来辅助它们。举个通俗的例子,假如A与B之间是独立的,不能彼此联系,如果它们想要通信的话可以借助第三方C,比如A将信息交给C,C再将信息转交给B —— 这就是进程间通信的主要思想 —— 共享资源。

这里要解决的一个重要的问题就是如何避免竞争,即避免多个进程同时访问临界区的资源。

共享内存

共享内存是进程间通信中最简单的方式之一。共享内存允许两个或更多进程访问同一块内存。当一个进程改变了这块地址中的内容的时候,其它进程都会察觉到这个更改。

你可能会想到,我直接创建一个文件,然后进程不就都可以访问了?

是的,但这个方法有几个缺陷:

  • 访问文件需要陷入系统调用,由用户态切入内核态,然后执行内核指令。这样做效率是非常低的,并且是不受用户掌握的。
  • 直接访问磁盘是非常慢的,比访问内存要慢上几百倍。从某种意义上说,这是共享磁盘不算共享内存。

Linux下采用共享内存的方式来使进程完成对共享资源的访问,它将磁盘文件复制到内存,并创建虚拟地址到该内存的映射,就好像该资源本来就在进程空间之中,此后我们就可以像操作本地变量一样去操作它们了,实际的写入磁盘将由系统选择最佳方式完成,例如操作系统可能会批量处理加排序,从而大大提高IO速度。

如同上图一样,进程将共享内存映射到自己的虚拟地址空间中,进程访问共享进程就好像在访问自己的虚拟内存一样,速度是非常快的。

共享内存的模型应该是比较好理解的:在物理内存中创建一个共享资源文件,进程将该共享内存绑定到自己的虚拟内存之中。

这里要解决的一个问题是如何将同一块共享内存绑定到自己的虚拟内存中,要知道在不同进程中使用malloc函数是会顺序分配空闲内存,而不会分配同一块内存,那么要如何去解决这个问题呢?

Linux操作系统已经想办法帮我们解决了这个问题,在#include <sys/ipc.h>和#include <sys/shm.h>头文件下,有如下几个shm系列函数:

  • shmget函数:由ftok()函数获取需要共享文件资源标识符(IPC键),将该资源标识符作为参数获取共享内存区域的唯一标识ID。

ftok()函数用以标识系统IPC资源,例如这里的共享资源、下文的消息队列、管道……都属于IPC资源。

IPC: Inter-Process Communication Communication/20394231),进程间通信),IPC是指两个进程的数据之间产生交互。

  • shmat函数:通过由shmget函数获取的标识符,建立由共享内存到进程独立空间的映射。
  • shmdt函数:释放映射。

由于我们主要走Java/Go开发岗位,我们与这些Linux系统下的C函数打交道的次数可能为0,因此在学习中我不会详细的描述函数的具体使用方法,当我们用上时Bing搜索即可。

通过上述几个函数,每个独立的进程只要有统一的共享内存标识符便可以建立起虚拟地址到物理地址的映射,每个虚拟地址将被翻译成指向共享区域的物理地址,这样就实现了对共享内存的访问。

还有一种相像的实现是采用mmap函数,mmap通常是直接对磁盘的映射——因此不算是共享内存,存储量非常大,但访问慢;shmat与此相反,通常将资源保存在内存中创建映射,访问快,但存储量较小。

共享内存对比其他几种方式是效率最高的,因为无需进行多次复制,直接对内存操作,不过要注意一点,操作系统并不保证任何并发问题,例如两个进程同时更改同一块内存区域,正如你和你的朋友在线编辑同一个文档中的同一个标题,这会导致一些不好的结果,所以我们需要借助信号量或其他方式来完成同步。

信号量

信号量是迪杰斯特拉最先提出的一种为解决同步不同执行线程问题的一种方法,进程与线程抽象来看大同小异,所以信号量同样可以用于同步进程间通信。

信号量的工作原理

信号量 s 是具有非负整数值的全局变量,由两种特殊的原子操作来实现,这两种原子操作称为 P 和 V :

  • P(s):如果 s 的值大于零,就给它减1,然后立即返回,进程继续执行。;如果它的值为零,就挂起该进程的执行,等待 s 重新变为非零值。
  • V(s):V操作将 s 的值加1,如果有任何进程在等在 s 值变为非0,那么V操作会重启这些等待进程中的其中一个(随机地),然后由该进程执行P操作将 s 重新置为0,而其他等待进程将会继续等待。

理解信号量

信号量并不用来传送资源,而是用来保护共享资源,理解这一点是很重要的,信号量 s 的表示的含义为同时允许最大访问资源的进程数量,它是一个全局变量。来考虑一个上面简单的例子:两个进程同时修改而造成错误,我们不考虑读者而仅仅考虑写者进程,在这个例子中共享资源最多允许一个进程修改资源,因此我们初始化 s 为1。

开始时,A率先写入资源,此时A调用P(s),将 s 减一,此时 s = 0,A进入共享区工作。

此时,进程B也想进入共享区修改资源,它调用P(s)发现此时s为0,于是挂起进程,加入等待队列。

A工作完毕,调用V(s),它发现s为0并检测到等待队列不为空,于是它随机唤醒一个等待进程,并将s加1,这里唤醒了B。

B被唤醒,继续执行P操作,此时s不为0,B成功执行将s置为0并进入工作区。

此时C想要进入工作区……

可以发现,在无论何时只有一个进程能够访问共享资源,这就是信号量做的事情,他控制进入共享区的最大进程数量,这取决于初始化s的值。此后,在进入共享区之前调用P操作,出共享区后调用V操作,这就是信号量的思想。

在Linux下并没有直接的P&V函数,而是需要我们根据这几个基本的sem函数族进行封装:

  • semget:初始化或获取一个信号量,这个函数需要接受ftok()的返回值以及初始s的值,它将全局计数变量s绑定在由ftok标识的共享资源上,并返回一个唯一标识的信号量组ID。
  • semop:这个函数接受上面函数返回的信号量组ID以及一些其他参数,根据参数的不同有一些不同的操作,他将对与该信号量组ID绑定的全局计数变量 s 进行一些操作,P&V操作便是基于此实现。
  • semctl:这个函数接受上面函数返回的信号量组ID以及一些其他参数,主要进行控制信号量相关信息,如删除该信号量等。

管道

正如其名,管道就如同生活中的一根管道,一端输送,而另一端接收,双方不需要知道对方,只需要知道管道就好了。

管道是一种最基本的进程间通信机制。 管道由pipe函数来创建: 调用pipe函数,会在内核中开辟出一块缓冲区用来进行进程间通信,这块缓冲区称为管道,它有一个读端和一个写端。管道被分为匿名管道和有名管道。

匿名管道

匿名管道通过pipe函数创建,这个函数接收一个长度为2的Int数组,并返回1或0表示成功或者失败:

int pipe(int fd[2])

这个函数打开两个文件描述符,一个读端文件,一个写端,分别存入fd[0]和fd[1]中,然后可以作为参数调用write和read函数进行写入或读取,注意fd[0]只能读取文件,而fd[1]只能用于写入文件。

你可能有个疑问,这要怎么实现通信?其他进程又不知道这个管道,因为进程是独立的,其他进程看不到某一个进程进行了什么操作。

是的,‘其他’进程确实是不知道,但是它的子进程却可以!这里涉及到fork派生进程的相关知识,一个进程派生一个子进程,那么子进程将会复制父进程的内存空间信息,注意这里是复制而不是共享,这意味着父子进程仍然是独立的,但是在这一时刻,它们所有的信息又是相等的。因此子进程也知道该全局管道,并且也拥有两个文件描述符与管道挂钩,所以匿名管道只能在具有亲缘关系的进程间通信。

还要注意,匿名管道内部采用环形队列实现,只能由写端到读端,由于设计技术问题,管道被设计为半双工的,一方要写入则必须关闭读描述符,一方要读出则必须关闭写入描述符。因此我们说管道的消息只能单向传递。

步骤

注意管道是堵塞的,如何堵塞将依赖于读写进程是否关闭文件描述符。假设读管道,如果读到空时,假设此时写端口还没有被完全关闭,那么操作系统会假设还有数据要读,此时读进程将会被堵塞,直到有新数据或写端口被关闭;如果管道为空,且写端口也被关闭,此时操作系统会认为已经没有东西可读,会直接退出,返回0。

当写端口在写管道时,如果管道满了,如果读端未关闭,写端会被堵塞;如果读端关闭,此时操作系统会认为这样的写管道是没有意义的,因为没有人接收,因此会发送终止信号导致进程

管道内部由内核管理,在半双工的条件下,保证数据不会出现并发问题。

命名管道

了解了匿名管道之后,有名管道便很好理解了。在匿名管道的介绍中,我们说其他进程不知道管道和文件描述符的存在,所以匿名管道只适用于具有亲缘关系的进程,而命名管道则很好的解决了这个问题 —— 现在管道有一个唯一的名称了,任何进程都可以访问这个管道。

注意,操作系统将管道看作一个抽象的文件,但管道并不是普通的文件,管道存在于内核空间中而不放置在磁盘(有名管道文件系统上有一个标识符,没有数据块),访问速度更快,但存储量较小,管道是临时的,是随进程的,当进程销毁,所有端口自动关闭,此时管道也是不存在的,操作系统将所有IO抽象的看作文件,例如网络也是一种文件,这意味着我们可以采用任何文件方法操作管道,理解这种抽象是很重要的,命名管道就利用了这种抽象。

Linux下,采用mkfifo函数创建,可以传入要指定的‘文件名’,然后其他进程就可以调用open方法打开这个特殊的文件,并进行write和read操作(那肯定是字节流对吧)。

注意,命名管道适用于任何进程,除了这一点不同外,其余大多数都与匿名管道相同。

消息队列

什么是消息队列?

消息队列亦称报文队列,也叫做信箱,是Linux的一种通信机制,这种通信机制传递的数据会被拆分为一个一个独立的数据块,也叫做消息体,消息体中可以定义类型与数据,克服了无格式承载字节流的缺陷(现在收到void*后可以知道其原本的格式惹):

1
2
3
4
c复制代码struct msgbuf {
long mtype; /* 消息的类型 */
char mtext[1]; /* 消息正文 */
};

同管道类似,它有一个不足就是每个消息的最大长度是有上限的,整个消息队列也是长度限制的。

内核为每个IPC对象维护了一个数据结构struct ipc_perm,该数据结构中有指向链表头与链表尾部的指针,保证每一次插入取出都是O(1)的时间复杂度。

1. msgget

**功能:**创建或访问一个消息队列

原型:

1
2
3
4
5
6
> c复制代码#include <sys/types.h>
> #include <sys/ipc.h>
> #include <sys/msg.h>
> int msgget(key_t key, int msgflag);
>
>

参数:
key:某个消息队列的名字,用ftok()产生,消息队列为PIC资源,该key标识了此消息队列,如果传入key存在,则返回对应消息队列ID,否则,创建并返回消息队列ID。
msgflag:有两个选项IPC_CREAT和IPC_EXCL,单独使用IPC_CREAT,如果消息队列不存在则创建之,如果存在则打开返回;单独使用IPC_EXCL是没有意义的;两个同时使用,如果消息队列不存在则创建之,如果存在则出错返回。

返回值:成功返回一个非负整数,即消息队列的标识码,失败返回-1

2. msgctl
功能:消息队列的控制函数

原型:

1
2
3
4
5
6
> c复制代码#include <sys/types.h>
> #include <sys/ipc.h>
> #include <sys/msg.h>
> int msgctl(int msqid, int cmd, struct msqid_ds *buf);
>
>

参数:
msqid:由msgget函数返回的消息队列标识码
cmd:有三个可选的值,在此我们使用IPC_RMID

  • IPC_STAT 把msqid_ds结构中的数据设置为消息队列的当前关联值
  • IPC_SET 在进程有足够权限的前提下,把消息队列的当前关联值设置为msqid_ds数据结构中给出的值
  • IPC_RMID 删除消息队列

返回值:
成功返回0,失败返回-1

3. msgsnd
**功能:**把一条消息添加到消息队列中

原型:

1
2
3
4
5
6
> c复制代码#include <sys/types.h>
> #include <sys/ipc.h>
> #include <sys/msg.h>
> int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
>
>

参数:
msgid:由msgget函数返回的消息队列标识码
msgp:指针指向准备发送的消息
msgze:msgp指向的消息的长度(不包括消息类型的long int长整型)
msgflg:默认为0

**返回值:**成功返回0,失败返回-1

4. msgrcv
功能:是从一个消息队列接受消息

原型:
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

**参数:**与msgsnd相同

**返回值:**成功返回实际放到接收缓冲区里去的字符个数,失败返回-1

特点

  • 与管道不同,消息队列的生命周期随内核,不会随进程销毁而销毁,需要我们显示的调用接口删除或使用命令删除。
  • 消息队列可以双向通信。
  • 克服了管道只能承载无格式字节流的缺点。

信号

关于信号

一个进程可以发送信号给另一个进程,一个信号就是一条消息,可以用于通知一个进程组发送了某种类型的事件,该进程组中的进程可以采取处理程序处理事件。

Linux下unistd.h头文件下定义了如图中的常量,当你在shell命令行键入ctrl + c时,内核就会前台进程组的每一个进程发送SIGINT信号,中止进程。

我们可以看到上述只有30个信号,因此操作系统会为每一个进程维护一个int类型变量sig,利用其中30位代表是否有对应信号事件,每一个进程还有一个int类型变量block,与sig对应,其30位表示是否堵塞对应信号(不调用处理程序)。如果存在多个相同的信号同时到来,多余信号正常情况下会被存储在一个等待队列中等待。

我们要理解进程组是什么,每个进程属于一个进程组,可以有多个进程属于同一个组。每个进程拥有一个进程ID,称为pid,而每个进程组拥有一个进程组ID,称为pgid,默认情况下,一个进程与其子进程属于同一进程组。

软件方面(诸如检测键盘输入是硬件方面)可以利用kill函数发送信号,kill函数接受两个参数,进程ID和信号类型,它将该信号类型发送到对应进程,如果该pid为0,那么会发送到属于自身进程组的所有进程。

接收方可以采用signal函数给对应事件添加处理程序,一旦事件发生,如果未被堵塞,则调用该处理程序。

Linux下有一套完善的函数用以处理信号机制。

特点

  • 信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式。
  • 信号可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以利用它来通知用户空间进程发生了哪些系统事件。
  • 如果该进程当前并未处于执行态,则该信号就由内核保存起来,直到该进程恢复执行再传递给它;如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。
  • 信号有明确生命周期,首先产生信号,然后内核存储信号直到可以发送它,最后内核一旦有空闲,会适当处理信号。
  • 处理程序是可以被另一个处理程序中断的,因此这可能造成并发问题,所以**在处理程序中的代码应该是线程安全的,**通常通过设置block位图以堵塞所有信号。

套接字

Socket套接字是用与网络中不同主机的通信方式,多用于客户端与服务器之间,在Linux下也有一系列C语言函数,诸如socket、connect、bind、listen与accept,对于原理的学习,更好的是对Java中的套接字socket源码进行剖析。

结语

对于工作而言,我们可能一辈子都用不上这些操作,但作为对于操作系统的学习,认识到进程间是如何通信还是很有必要的。

面试的时候对于这些方法我们不需要掌握到很深的程度,但我们必须要讲的来有什么通信方式,这些方式都有什么特点,适用于什么条件,大致是如何操作的,能说出这些,基本足以让面试官对你十分满意了。

本文转载自: 掘金

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

1…560561562…956

开发者博客

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