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

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


  • 首页

  • 归档

  • 搜索

feign多服务间的调用

发表于 2021-10-24

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

上文我们把我们项目注册到服务器上了,但是在微服务中,我们会有多个服务,同时也会使用A服务调用B服务的接口。springcloud netflix这里有两种方式ribbon和feign,我们分别介绍。

1.ribbon

ribbon说白了就是使用restTemplate。上文编写了被调用方的代码,下文将编写调用方的代码。

1.修改pom文件

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
js复制代码<?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.2.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.baocl</groupId>
<artifactId>eureka-consumer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>eureka-consumer</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR1</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.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</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>

2.修改Application

然后修改EurekaConsumerApplication类,需要在新建RestTemplate的bean。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
js复制代码import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
@EnableDiscoveryClient

public class EurekaConsumerApplication {

@Bean
@LoadBalanced // ribbon注解
public RestTemplate restTemplate() {
return new RestTemplate();
}

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

}

3.新建调用方接口DcController

新建普通接口,调用上文被调用方的服务。

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
js复制代码import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
@RequestMapping("/eureka1")
public class DcController {

@Autowired
LoadBalancerClient loadBalancerClient;
@Autowired
RestTemplate restTemplate;
@Autowired
DiscoveryClient discoveryClient;

@GetMapping("/consumer")
public ResponseEntity<String> dc() {

//String url = "http://eureka-client/aaa/dc?dk=3001";
String url = "http://EUREKA-CLIENT/aaa/dc?dk=3001";
System.out.println(url);
// ServiceInstance serviceInstance =
// loadBalancerClient.choose("eureka-client");// 获取服务提供者信息
// // String url = "http://localhost:" + serviceInstance.getPort() + "/aaa/dc";
// System.out.println(serviceInstance.getPort() );
// System.out.println(url);
// ResponseEntity r = restTemplate.getForEntity("http://eureka-client/aaa/dc",
// String.class);
// System.out.println("r.getHeaders().getHost():"+r.getHeaders().getHost());
// System.out.println("r.getHeaders().getLocation():"+r.getHeaders().getLocation());
// System.out.println("r.getHeaders().getOrigin():"+r.getHeaders().getOrigin());
return restTemplate.getForEntity(url, String.class); // ribbon应用
}
}

有一个坑 在调用时候需要String url = “http://eureka-client/aaa/dc?dk=3001”,而不是String url =”http://localhost:2002/aaa/dc?dk=3001”,否则会报错java.lang.IllegalStateException: No instances available for localhost.

4.修改application

最后是配置文件application.yml,这里我们使用.yml文件,都是基础配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
js复制代码eureka:
client:
registery-fetch-interval-seconds: 5000
serviceUrl:
defaultZone: http://localhost:1001/eureka/
eureka-client:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
server:
port: 3001
spring:
application:
name: eureka-consumer

输入http://localhost:3001/eureka1/consumer 就可以调用到上文的client服务了,同时还可以配置负载均衡策略。在NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule 处配置。

2.feign

1.修改pom文件

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
js复制代码<?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.2.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.baocl</groupId>
<artifactId>eureka-consumer-feign</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>eureka-consumer-feign</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR1</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-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
<version>1.4.7.RELEASE</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</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
js复制代码org.springframework.beans.factory.BeanDefinitionStoreException: Failed to read candidate component class: file [F:\workspace\springcloud\tse9\eureka-consumer-feign\target\classes\com\cloud\DcClient.class]; nested exception is org.springframework.core.annotation.AnnotationConfigurationException: Attribute 'value' in annotation [org.springframework.cloud.netflix.feign.FeignClient] must be declared as an @AliasFor 'serviceId', not 'name'.

解决办法可以参考:www.pianshen.com/article/223…

2.修改EurekaConsumerFeignApplication

首先在EurekaConsumerFeignApplication使用@EnableFeignClients中开启feign

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
js复制代码import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;


@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class EurekaConsumerFeignApplication {

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

3.新建接口DcClient

新建一个接口DcClient,配置调用其他服务的名称和接口名称。(被调用方客户端的服务与接口信息)

1
2
3
4
5
6
7
8
9
10
11
12
js复制代码import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

@FeignClient(name = "eureka-client")
public interface DcClient {

@GetMapping("/aaa/dc?dk=3002")
String consumer();

@GetMapping("/aaa/dc?dk=3002")
String consumer2();
}

4.新建调用接口DcController

业务接口的书写,同时注入dcClient调用其他服务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
js复制代码import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.cloud.DcClient;

@RestController
@RequestMapping("/eureka2")
public class DcController {
@Autowired
DcClient dcClient;
@GetMapping("/consumer")

public String dc() {
return dcClient.consumer();
}
}

5.修改application.properties

下文是最基础的配置。

1
2
3
js复制代码spring.application.name=eureka-consumer-feign
server.port=3002
eureka.client.serviceUrl.defaultZone=http://localhost:1001/eureka/

然后调用http://localhost:3002/eureka2/consumer 可以发现接口掉通了 ,在被调用方也有相应的日志。

3.ribbon和Eureka整合原理

基本流程就是 客户端从eureka中获取到缓存的服务列表,获取到目的服务的信息,然后通过自身的负载均衡策略,去调用对应的服务。
在这里插入图片描述

1.负载均衡策略

  1. BestAvailableRule 选择一个最小的并发请求的server。
  2. AvailabilityFilteringRule 过滤掉那些因为一直连接失败的被标记为circuit tripped的后端server,并过滤掉那些高并发的的后端server(active connections 超过配置的阈值)。
  3. WeightedResponseTimeRule 根据响应时间分配一个weight,响应时间越长,weight越小,被选中的可能性越低。
  4. RetryRule 对选定的负载均衡策略机上重试机制。
  5. RoundRobinRule roundRobin方式轮询选择server.。
  6. RandomRule 随机选择一个server。
  7. ZoneAvoidanceRule 复合判断server所在区域的性能和server的可用性选择server。

​

本文转载自: 掘金

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

springboot自定义start讲解(start中配置从

发表于 2021-10-24

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

在springboot相比于springmvc提供了一个极为重要的功能,就是自定义start(自定义jar包),同时spring也提供了一大波start。如spring-boot-starter-data-redis,spring-boot-starter-amqp。本文将教你如何自定义start。例子为从start中获取数据源。

1.自定义start能干什么

在架构考虑时,或许有一些通用方法抽取成start,在项目中使用时,直接引入即可。

2.创建自定义start

1.创建一个新的maven项目

创建一个普通的maven项目,作为start的开发环境

2.新建application.properties

在maven中创建application.properties文件。同时添加以下代码,作为常量配置。

1
2
3
4
js复制代码login.className = com.mysql.jdbc.Driver
login.url=jdbc:mysql://localhost:3306/humanresource?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
login.user=root
login.password=root

3.创建LoginCheckProperties

创建类LoginCheckProperties,其中需要@ConfigurationProperties注解,prefix = “login” 的意思是调用项目中 ,application.properties中配置的参数。

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
js复制代码@ConfigurationProperties(prefix = "login")
public class LoginCheckProperties {

private String className;
private String url;
private String user;
private String password;

public String getClassName() {
return className;
}

public void setClassName(String className) {
this.className = className;
}

public String getUrl() {
return url;
}

public void setUrl(String url) {
this.url = url;
}

public String getUser() {
return user;
}

public void setUser(String user) {
this.user = user;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}
}

4.Config类

其中@EnableConfigurationProperties(LoginCheckProperties.class) 注解为固定写法,意义是实现自动配置上文中的Properties类。

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
bash复制代码@Configuration
@EnableConfigurationProperties(LoginCheckProperties.class)
@MapperScan(basePackages = MasterDataSourceConfig.PACKAGE, sqlSessionFactoryRef = "masterSqlSessionFactory")
public class MasterDataSourceConfig {

// 精确到 master 目录,以便跟其他数据源隔离
static final String PACKAGE = "com.airboot.bootdemo.dao.master";
static final String MAPPER_LOCATION = "classpath*:mapper/master/*.xml";

private String className;
private String url;
private String user;
private String password;

public MasterDataSourceConfig(LoginCheckProperties loginCheckProperties) {
this.className = loginCheckProperties.getClassName();
this.url = loginCheckProperties.getUrl();
this.user = loginCheckProperties.getUser();
this.password = loginCheckProperties.getPassword();
}

@Bean(name = "masterDataSource")
public DataSource masterDataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(className);
dataSource.setUrl(url);
dataSource.setUsername(user);
dataSource.setPassword(password);
return dataSource;
}

@Bean(name = "masterTransactionManager")
public DataSourceTransactionManager masterTransactionManager() {
return new DataSourceTransactionManager(masterDataSource());
}

@Bean(name = "masterSqlSessionFactory")
public SqlSessionFactory masterSqlSessionFactory(@Qualifier("masterDataSource") DataSource masterDataSource)
throws Exception {
final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(masterDataSource);
sessionFactory.setMapperLocations(
new PathMatchingResourcePatternResolver().getResources(MasterDataSourceConfig.MAPPER_LOCATION));
return sessionFactory.getObject();
}
}

5.添加META-INF/spring.factories

最后在resources中新建文件夹META-INF,在其中建立文件spring.factories路径为上文Config中的路径。(需要向被引用者暴露)

1
2
3
4
5
6
js复制代码org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.*.*.*.*A,\
com.*.*.*.*B,\
com.*.*.*.*C,\
com.*.*.*.*D,\
com.*.*.*.*Util

6.显示start注释(选读)

如果想要在jar中加入注释 则需要在pom文件中修改以下代码 这样会生成一个 .jar 和一个:source.jar 需要将这两个jar包都加入项目中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
js复制代码<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>2.4</version>
<configuration>
<aggregate>true</aggregate>
</configuration>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
<configuration>
<additionalparam>-Xdoclint:none</additionalparam>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

7.打包

使用命令mvn install。即可生成jar包。

8.引入jar包

1
2
3
4
5
js复制代码       <dependency>
<groupId>cn.baocl</groupId>
<artifactId>check-spring-boot-starter</artifactId>
<version>1.1-SNAPSHOT</version>
</dependency>

在引用项目中的pom文件配置groupId,artifactId,version即可。下文为start中的pom。与上文配置需要对应。

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
js复制代码<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>cn.baocl</groupId>
<artifactId>check-spring-boot-starter</artifactId>
<version>1.1-SNAPSHOT</version>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>

<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<!-- Druid 数据连接池依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.17</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<version>2.2.1.RELEASE</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
<version>8.0.16</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
<version>2.2.1.RELEASE</version>
</dependency>
</dependencies>

</project>

9.使用

然后就可以使用了,本文实现的为主项目定义一个从数据源。所以在主项目中就可以使用上文定义的数据源啦。

10.项目结构

在这里插入图片描述

​

本文转载自: 掘金

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

我的新书《图解Java并发编程》上市啦!

发表于 2021-10-23

关于这本书

我的新书《图解Java并发编程》上市啦!大概六七年前,我在工作中经常遇到系统并发问题,于是我决定深入学习Java并发知识。也是在学习的过程中写了一个Java并发主题的专栏发布在博客上,这些年也陆陆续续收到读者的反馈和肯定。很多读者问该如何系统地去学习Java并发知识,我在给他们解答的过程中萌生了写这本书的想法,计划对该专栏进行扩展优化从而形成一本对读者有用的书。

为了让读者能更好地理解Java并发原理,我绘制了总共两百余张示意图,并且提供了一百多个案例代码。这些都能极大地帮助读者理解其中的原理,以达到“一图胜千言”和“一例胜千言”的效果。

本书关键词

我尝试列出以下关键词来让读者了解本书的内容。线程调度、内存模型、阻塞唤醒、线程协作、指令重排、乐观锁、线程池、悲观锁、AQS、线程状态、synchronized、volatile、死锁、非阻塞、Lock、信号量、优先级、阻塞队列、线程饥饿、读写锁、竞争条件、互斥共享、数据竞争、可见性、CAS、CPU、中断、原子、同步、I/O,其实这些关键词也是Java并发的核心内容,如果我们能掌握这些相关内容,那么就可以说我们基本已经掌握了Java并发知识。

本书特色

  • 本书通篇大量采用图解,总共绘制了两百多张示意图帮助读者理解,对每个关键点和难点都尽量给出图示,使读者能轻松理解Java并发相关工具和概念的思想。
  • 本书提供了大量代码案例,总共编写了一百多个代码案例来讲解Java并发工具和问题,让读者能从代码角度去理解并发,书中的相关代码同步发布在 github.com/sea-boat/ja… 。
  • 本书所讲解的Java并发知识都是我们工程中常见的,所以本书能够很好地帮助我们在实际项目开发中理解相关的实现原理。
  • 本书的主题是讲解Java并发原理机制,重点偏向于对并发问题和工具的讲解和分析,而不是讲解如何使用Java并发API。
  • 本书脉络结构比较清晰,由基础概念到高层工具,循序渐进。各知识点的连贯性较强,有Java基础的人基本都能阅读。

本书实际效果图:


京东链接:
20200407134053974.jpg

本文转载自: 掘金

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

Windows10 配置Maven环境变量(图文教程)

发表于 2021-10-23

本文教你如何在windows10电脑上配置maven环境变量

一、下载maven

(1)官网下载地址:maven.apache.org/

image.png

(2)下载完成之后,可以得到一个zip压缩包

image.png

二、配置环境变量

(1)首先,解压maven压缩包

image.png

(2)打开“高级系统设置”

image.png

(3)打开“环境变量”

image.png

(4)新增“MAVEN_HOME”变量

image.png

(5)配置“Path”变量

image.png

三、修改默认配置

(1)在maven根目录创建一个repo空目录

image.png

(2)编辑“settings.xml”文件

image.png

(3)新增maven本地依赖包默认安装目录

image.png

(4)修改默认镜像源为阿里镜像源

因为默认情况是从国外镜像库进行下载,所以一般情况会比较慢,这里修改为阿里云提供镜像库,能够提供提供jar包的下载速度,从而节省不少时间呢。

image.png

1
2
3
4
5
6
7
8
> xml复制代码<mirror>
> <id>alimaven</id>
> <mirrorOf>central</mirrorOf>
> <name>aliyun maven</name>
> <url>http://maven.aliyun.com/nexus/content/repositories/central/</url>
> </mirror>
>
>

四、验证配置结果

打开命令控制台窗口(win+R),输入命令:mvn -v

image.png

五、下载常用依赖包

mvn help:system

image.png

image.png

命令执行完成之后,就可以在repo目录中看到下载好的依赖jar包了

image.png

到这里,maven环境变量就配置好了。

本文转载自: 掘金

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

【设计模式从入门到精通】07-装饰者模式 装饰者模式

发表于 2021-10-23

笔记来源:尚硅谷Java设计模式(图解+框架源码剖析)

[TOC]

装饰者模式

1、星巴克咖啡订单项目

星巴克咖啡订单项目(咖啡馆):

  • 1)咖啡种类/单品咖啡:Espresso(意大利浓咖)、ShortBlack、LongBlack(美式咖啡)、Decaf(无因咖啡)
  • 2)调料:Mik、Soy(豆浆)、Chocolate
  • 3)要求在扩展新的咖啡种类时,具有良好的扩展性、改动方便、维护方便
  • 4)使用 OO 的来计算不同种类咖啡的费用:客户可以点单品咖啡,也可以单品咖啡+调料组合

2、方案 1-解决星巴克咖啡订单项目(较差的方案)

image-20211023165815602

方案 1-解决星巴克咖啡订单问题分析

  • 1)Drink 是一个抽象类,表示饮料
  • 2)description 就是对咖啡的描述,比如咖啡的名字
  • 3)cost 方法就是计算费用,Drink 类中做成一个抽象方法
  • 4)Decaf 就是单品咖啡,继承 Drink,并实现 cost
  • 5)Espresso && Milk 就是单品咖啡+调料,这个组合很多
  • 6)问题:这样设计,会有很多类。当我们增加一个单品咖啡,或者一个新的调料,类的数量就会倍增,出现类爆炸

3、方案 2-解决星巴克咖啡订单项目(好点的方案)

前面分析到方案 1 因为咖啡单品+调料组合会造成类的倍增,因此可以做改进,将调料内置到 Drink 类,这样就不会造成类数量过多。从而提高项目的维护性(如图)

image-20211023170550925

说明:Milk、Soy、Chocolate 可以设计为 Boolean,表示是否要添加相应的调料

方案 2-解决星巴克咖啡订单问题分析

  • 1)方案 2 可以控制类的数量,不至于造成很多的类
  • 2)在增加或者删除调料种类时,代码的维护量很大
  • 3)考虑到用户可以添加多份调料时,可以将 hasMilk 返回一个对应 int
  • 4)考虑使用装饰者模式

4、装饰者模式

定义

1)装饰者模式:动态地将新功能附加到对象上。在对象功能扩展方面,它比继承更有弹性,装饰者模式体现了开闭原则(OCP)

2)这里提到的动态的将新功能附加到对象和 OCP 原则,在后面的应用实例上会以代码的形式体现,请同学们注意体会

原理

  • 1)装饰者模式就像打包一个快递
    • 主体:比如陶瓷、衣服(Component)
    • 包装:比如报纸填充、塑料泡沫、纸板、木板(Decorator)
  • 2)主体(Component):比如前面的 Drink
  • 3)具体的主体(ConcreteComponent):比如前面的各个单品咖啡
  • 4)装饰者(Decorator):比如各调料
  • 4)Component 与 ConcreteComponent 之间,如果 ConcreteComponent 类很多,还可以设计一个缓冲层,将共有的部分提取出来,抽象成一个类

image-20211023181332821

5、装饰者模式解决星巴克咖啡订单项目

image-20211023181701216

说明

  • 1)Drink 就是抽象类 Component
  • 2)ShortBlack 单品咖啡就是具体的主体
  • 3)Decorator 是一个装饰类,含有一个被装饰的对象(Drink)
  • 4)Decorator 的 cost 方法进行一个费用的叠加,递归地计算价格

装饰者模式下的订单:2份巧克力 + 一份牛奶的 LongBlack

image-20211023182107273

说明

  • 1)Milk 包含了 LongBlack
  • 2)一份 Chocolate 包含了 Milk + LongBlack
  • 3)一份 Chocolate 包含了 Chocolate + Milk + LongBlack
  • 4)这样不管是什么形式的单品咖啡 + 调料组合,通过递归方式可以方便的组合和维护

UML类图

image-20211023194905182

核心代码

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
java复制代码// 抽象主体
public abstract class Drink {
private String desc;
private Float price;

public String getDesc() {
return desc;
}

protected void setDesc(String desc) {
this.desc = desc;
}

public Float getPrice() {
return price;
}

protected void setPrice(Float price) {
this.price = price;
}

public abstract Float cost();
}
// 具体主体
public class Coffee extends Drink {

@Override
public Float cost() {
return super.getPrice();
}
}
public class Decaf extends Coffee {
public Decaf() {
setDesc("无因咖啡");
setPrice(20.0f);
}
}
public class Espresso extends Coffee {
public Espresso() {
setDesc("意大利浓咖");
setPrice(30.0f);
}
}
public class ShortBlack extends Coffee {
public ShortBlack() {
setDesc("短黑咖啡");
setPrice(40.0f);
}
}
public class LongBlack extends Coffee {
public LongBlack() {
setDesc("美式咖啡");
setPrice(50.0f);
}
}
//装饰者
public class Decorator extends Drink {
private Drink drink;

public Decorator(Drink drink) {
this.drink = drink;
}

@Override
public Float cost() {
return super.getPrice() + drink.cost();
}
}
public class Milk extends Decorator {
public Milk(Drink drink) {
super(drink);
setDesc("牛奶");
setPrice(3.0f);
}
}
public class Soy extends Decorator {
public Soy(Drink drink) {
super(drink);
setDesc("豆浆");
setPrice(4.0f);
}
}
public class Chocolate extends Decorator {
public Chocolate(Drink drink) {
super(drink);
setDesc("巧克力");
setPrice(5.0f);
}
}
// 调用者
public class CoffeeBar {
public static void main(String[] args) {
Drink drink = new Espresso();
System.out.println("意大利浓咖:" + drink.cost() + "美元"); // 意大利浓咖:30.0美元

drink = new Milk(drink);
System.out.println("意大利浓咖 + 1份牛奶:" + drink.cost() + "美元"); // 意大利浓咖 + 1份牛奶:33.0美元

drink = new Chocolate(drink);
System.out.println("意大利浓咖 + 1份牛奶 + 1份巧克力:" + drink.cost() + "美元"); // 意大利浓咖...:38.0美元

drink = new Chocolate(drink);
System.out.println("意大利浓咖 + 1份牛奶 + 2份巧克力:" + drink.cost() + "美元"); // 意大利浓咖...:43.0美元
}
}

6、JDK 源码分析

Java 的 IO 结构,FilterlnputStream 就是一个装饰者

image-20211023210853809

核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码// 是一个抽象类,即Component
public abstract class InputStream implements Closeable {}
// 是一个装饰类,即Decorator
public class FilterInputStream extends InputStream {
protected volatile InputStream in;
protected FilterInputStream(InputStream in) {
this.in = in;
}
}
// FilterInputStream子类,也继承了被装饰的对象 in
public class DataInputStream extends FilterInputStream implements DataInput {
public DataInputStream(InputStream in) {
super(in);
}

分析

  • 1)InputStream 是抽象类,类似我们前面讲的 Drink
  • 2)FileInputStream 是 InputStream 子类,类似我们前面的 DeCaf、LongBlack
  • 3)FilterInputStream 是 InputStream 子类,类似我们前面的 Decorator,修饰者
  • 4)DataInputStream 是 FilterInputStream 子类,类似前面的Milk,Soy等,具体的修饰者
  • 5)FilterInputStream 类有protected volatile InputStream in;,即含被装饰者
  • 6)分析得出在 JDK 的 IO 体系,就是使用装饰者模式

本文转载自: 掘金

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

如何一条命令,榨干机器的所有内存?

发表于 2021-10-23

最近在验证一些机器的内存分配规律的时候,学习到了一些技能,趁着周末有时间写点东西,跟大家分享一下。

大家可能有遇到类似的场景,想要对机器进行压测模拟 OOM 的场景,但是无奈机器的规格实在太高,若用代码去实现,大家可以想象一下如何实现?个人感觉还是有点麻烦的。

那么有没有好有的办法,不用写代码,用几个简单的命令直接就可以向机器申请内存呢?或者更极端点,直接把机器的内存给榨干了。。

若你经常使用 linux,你会发现 df -Th 后,一定会有 tmpfs 类型的文件系统挂载在 /dev/shm 下面,虽然你大概率不会关注到它。

1
2
3
4
5
6
7
8
shell复制代码$ df -Th
Filesystem Type Size Used Avail Use% Mounted on
devtmpfs devtmpfs 910M 0 910M 0% /dev
tmpfs tmpfs 919M 0 919M 0% /dev/shm
tmpfs tmpfs 919M 896K 918M 1% /run
tmpfs tmpfs 919M 0 919M 0% /sys/fs/cgroup
/dev/vda1 ext4 40G 11G 27G 28% /
tmpfs tmpfs 184M 0 184M 0% /run/user/0

而这个 tmpfs 就是明哥今天要介绍的主角。

tmpfs,顾名思义,是临时文件系统,是一种基于内存的文件系统。

它和虚拟磁盘 ramdisk比较类似像,但不完全相同,和ramdisk一样,tmpfs可以使用RAM,但它也可以使用swap分区来存储,而且传统的ramdisk是个块设备,要用mkfs来格式化它,才能真正地使用它;而tmpfs是一个文件系统,并不是块设备,只是安装它,就可以使用了。tmpfs是最好的基于RAM的文件系统。

这意味着,你往挂载了 tmpfs 的目录下写入的文件,都会直接写入内存中。

假如你想占用机器 10G 的内存,那我只要先创建一个临时目录 /tmp/memory ,并指定 tmpfs 的文件系统类型及大小 10240M 挂载到该目录下。

1
shell复制代码$ mount -t tmpfs -o size=10240M tmpfs /tmp/memory

接着咱使用 dd 命令,往该目录下写入多少内容,就会占用多少内存,由于我们的目的是占用内存,因此 if 直接使用 /dev/zero

1
shell复制代码$ dd if=/dev/zero of=/tmp/memory/block

当 dd 写入完成后,你再使用 free 去查看可用内存,会发现剩余的内存可分配的内存少了 10G。

如果你想用完机器的所有内存,完全可以在 mount 的时候,指定 size 为机器的内存大小,但你要清楚你在做什么,否则执行完 dd ,你的机器可能就挂了。

利用上面这个方法,其实还可以做更多的事情,比如你在机器你有两个 NUMA Node ,但你只想占用 NUMA Node 0 的内存,那就可以指定 NUMA Node 0 的内存,怎么办呢?

首先利用 lscpu 找到 NUMA Node 0 上的所有 cpu 核

1
shell复制代码$ node0_cpus=$(lscpu | grep "NUMA node0" | awk '{print $NF}')

然后使用 taskset 工具来指定对应的 cpu 核来执行创建 tmpfs 目录和 dd 的过程

1
2
3
4
5
6
7
8
shell复制代码$ cat > /root/mem_alloc.sh <<EOF
#!/bin/bash
tmpdir=`mktemp`
mount -t tmpfs -o size=1024M tmpfs ${tmpdir}
dd if=/dev/zero of=${tmpdir}/block
EOF

$ taskset -c "${node0_cpus}" sh /root/mem_alloc.sh

执行完成后,如果你所占用的内存,没有超过 NUMA Node 0 的本地内存,那么你使用 numactl 就会发现上面命令都只占用了 NUMA Node0 的内存。

絮叨一下

我在掘金上写过很多的 Python 相关文章,其中包括 Python 实用工具,Python 高效技巧,PyCharm 使用技巧,很高兴得到了很多知乎朋友的认可和支持。

在他们的鼓励之下,我将过往文章分门别类整理成三本 PDF 电子书

PyCharm 中文指南

《PyCharm 中文指南》使用 300 多张 GIF 动态图的形式,详细讲解了最贴合实际开发的 105个 PyCharm 高效使用技巧,内容通俗易懂,适合所有 Python 开发者。

在线体验地址:pycharm.iswbm.com

Python 黑魔法指南

《Python黑魔法指南》目前迎来了 v3.0 的版本,囊集了 100 多个开发小技巧,非常适合在闲时进行碎片阅读。

在线体验地址:magic.iswbm.com

Python 中文指南

学 Python 最好的学习资料永远是 Python 官方文档,可惜现在的官方文档大都是英文,虽然有中文的翻译版了,但是进度实在堪忧。为了照顾英文不好的同学,我自己写了一份 面向零基础的朋友 的在线 Python 文档 – 《Python中文指南》

在线体验地址:python.iswbm.com

**有帮助的话,记得帮我 点个赞哟~

本文转载自: 掘金

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

我用Python连夜离线了100G图片,只为了防止网站被消失

发表于 2021-10-23

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

20 行代码,变身技术圈多肉小达人

用 Python 爬取 100G Cosers 图片

本篇博客目标

爬取目标

  • 目标数据源:www.cosplay8.com/pic/chinaco…,又是一个 Cos 网站,该类网站很容易 消失 在互联网中,为了让数据存储下来,我们盘它。

为了防止网站被消失,我用Python连夜离线了100G图片

使用的 Python 模块

  • requests,re,os

重点学习内容

  • 今日的重点学习,可放在详情页分页抓取上,该技巧在之前的博客中没有涉及,编写代码过程中重点照顾一下。

列表页与详情页分析

通过开发者工具,可以便捷的分析出目标数据所在的标签。

为了防止网站被消失,我用Python连夜离线了100G图片
点击任意图片,进入详情页,得到目标图片为单页展示,即每页展示一张图片。

1
2
3
4
5
6
7
8
html复制代码<a href="javascript:dPlayNext();" id="infoss">
<img
src="/uploads/allimg/210601/112879-210601143204.jpg"
id="bigimg"
width="800"
alt=""
border="0"
/></a>

同时获取列表页与详情页 URL 生成规则如下:

列表页

  • www.cosplay8.com/pic/chinaco…
  • www.cosplay8.com/pic/chinaco…
  • www.cosplay8.com/pic/chinaco…

详情页

  • www.cosplay8.com/pic/chinaco…
  • www.cosplay8.com/pic/chinaco…
  • www.cosplay8.com/pic/chinaco…

注意详情页首页无序号 1,顾爬取获取总页码的同时,需存储首页图片。

编码时间

目标网站对图片进行了分类,即 国内 cos,国外 cos,汉服圈,Lolita,因此在爬取时可以对其进行动态输入,即爬取目标源自定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
python复制代码
def run(category, start, end):
# 生成待爬取的列表页
wait_url = [
f"http://www.cosplay8.com/pic/chinacos/list_{category}_{i}.html" for i in range(int(start), int(end)+1)]
print(wait_url)

url_list = []
for item in wait_url:
# get_list 函数在后文提供
ret = get_list(item)

print(f"已经抓取:{len(ret)} 条数据")
url_list.extend(ret)


if __name__ == "__main__":

# http://www.cosplay8.com/pic/chinacos/list_22_2.html
category = input("请输入分类编号:")
start = input("请输入起始页:")
end = input("请输入结束页:")
run(category, start, end)

上述代码首先基于用户的输入,生成目标网址,然后将目标网址一次传递到 get_list 函数中,该函数代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
python复制代码def get_list(url):
"""
获取全部详情页链接
"""
all_list = []

res = requests.get(url, headers=headers)
html = res.text
pattern = re.compile('<li><a href="(.*?)">')
all_list = pattern.findall(html)

return all_list

通过正则表达式 <li><a href="(.*?)"> 匹配列表页中所有详情页地址,并将其进行整体返回。

在 run 函数中继续增加代码,获取详情页图片素材,并对抓取到的图片进行保存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
python复制代码def run(category, start, end):
# 待爬取的列表页
wait_url = [
f"http://www.cosplay8.com/pic/chinacos/list_{category}_{i}.html" for i in range(int(start), int(end)+1)]
print(wait_url)

url_list = []
for item in wait_url:
ret = get_list(item)

print(f"已经抓取:{len(ret)} 条数据")
url_list.extend(ret)

print(url_list)
# print(len(url_list))
for url in url_list:
get_detail(f"http://www.cosplay8.com{url}")

由于匹配到的详情页地址为相对地址,顾对地址进行格式化操作,生成完整地址。
get_detail 函数代码如下:

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
python复制代码def get_detail(url):
# 请求详情页数据
res = requests.get(url=url, headers=headers)
# 设置编码
res.encoding = "utf-8"
# 得到网页源码
html = res.text

# 拆解页码,保存第一张图片
size_pattern = re.compile('<span>共(\d+)页: </span>')
# 获取标题,后续发现发表存在差异,顾正则表达式有修改
# title_pattern = re.compile('<title>(.*?)-Cosplay中国</title>')
title_pattern = re.compile('<title>(.*?)-Cosplay(中国|8)</title>')
# 设置图片正则表达式
first_img_pattern = re.compile("<img src='(.*?)' id='bigimg'")
try:
# 尝试匹配页码
page_size = size_pattern.search(html).group(1)
# 尝试匹配标题
title = title_pattern.search(html).group(1)
# 尝试匹配地址
first_img = first_img_pattern.search(html).group(1)

print(f"URL对应的数据为{page_size}页", title, first_img)
# 生成路径
path = f'images/{title}'
# 路径判断
if not os.path.exists(path):
os.makedirs(path)

# 请求第一张图片
save_img(path, title, first_img, 1)

# 请求更多图片
urls = [f"{url[0:url.rindex('.')]}_{i}.html" for i in range(2, int(page_size)+1)]

for index, child_url in enumerate(urls):
try:
res = requests.get(url=child_url, headers=headers)

html = res.text
first_img_pattern = re.compile("<img src='(.*?)' id='bigimg'")
first_img = first_img_pattern.search(html).group(1)

save_img(path, title, first_img, index)
except Exception as e:
print("抓取子页", e)

except Exception as e:
print(url, e)

上述代码核心逻辑已经编写到注释中,重点在 title 正则匹配部分,初始编写正则表达式如下:

1
perl复制代码<title>(.*?)-Cosplay中国</title>

后续发现不能全部匹配成功,修改为如下内容:

1
perl复制代码<title>(.*?)-Cosplay(中国|8)</title>

,缺少的 save_img 函数代码如下:

1
2
3
4
5
6
7
8
9
10
python复制代码def save_img(path, title, first_img, index):
try:
# 请求图片
img_res = requests.get(f"http://www.cosplay8.com{first_img}", headers=headers)
img_data = img_res.content

with open(f"{path}/{title}_{index}.png", "wb+") as f:
f.write(img_data)
except Exception as e:
print(e)

为了防止网站被消失,我用Python连夜离线了100G图片

本文转载自: 掘金

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

sql语句中的case when then else end

发表于 2021-10-23

case具有两种格式。简单case函数和case搜索函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
sql复制代码--简单case函数
case sex

when '1' then '男'

when '2' then '女'

else '其他' end


--case搜索函数

case when sex = '1' then '男'

when sex = '2' then '女'

else '其他' end

这两种方式,可以实现相同的功能。简单case函数的写法相对比较简洁,但是和case搜索函数相比,功能方面会有些限制,比如写判定式。

还有一个需要注重的问题,case函数只返回第一个符合条件的值,剩下的case部分将会被自动忽略。

–比如说,下面这段sql,你永远无法得到“第二类”这个结果

1
2
3
sql复制代码case when col_1 in ( 'a', 'b') then'第一类'
when col_1 in ('a') then '第二类'
else'其他'end

下面我们来看一下,使用case函数都能做些什么事情。

一,已知数据按照另外一种方式进行分组,分析。

有如下数据:(为了看得更清楚,我并没有使用国家代码,而是直接用国家名作为primary key)

国家(country) 人口(population)
中国 600
美国 100
加拿大 100
英国 200
法国 300
日本 250
德国 200
墨西哥 50
印度 250

根据这个国家人口数据,统计亚洲和北美洲的人口数量。应该得到下面这个结果。

洲 人口
亚洲 1100
北美洲 250
其他 700

想要解决这个问题,你会怎么做?生成一个带有洲code的view,是一个解决方法,但是这样很难动态的改变统计的方式。

假如使用case函数,sql代码如下:

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
sql复制代码select sum(population),

case country

when '中国' then'亚洲'

when '印度' then'亚洲'

when '日本' then'亚洲'

when '美国' then'北美洲'

when '加拿大' then'北美洲'

when '墨西哥' then'北美洲'

else '其他' end

from table_a

group by case country

when '中国' then'亚洲'

when '印度' then'亚洲'

when '日本' then'亚洲'

when '美国' then'北美洲'

when '加拿大' then'北美洲'

when '墨西哥' then'北美洲'

else '其他' end;

同样的,我们也可以用这个方法来判定工资的等级,并统计每一等级的人数。sql代码如下;

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
sql复制代码
select

case when salary <= 500 then '1'

when salary > 500 and salary <= 600 then '2'

when salary > 600 and salary <= 800 then '3'

when salary > 800 and salary <= 1000 then '4'

else null end salary_class,

count(*)

from table_a

group by

case when salary <= 500 then '1'

when salary > 500 and salary <= 600 then '2'

when salary > 600 and salary <= 800 then '3'

when salary > 800 and salary <= 1000 then '4'

else null end;

二,用一个sql语句完成不同条件的分组。

有如下数据

国家(country) 性别(sex) 人口(population)
中国 1 340
中国 2 260
美国 1 45
美国 2 55
加拿大 1 51
加拿大 2 49
英国 1 40
英国 2 60

按照国家和性别进行分组,得出结果如下

国家 男 女
中国 340 260
美国 45 55
加拿大 51 49
英国 40 60

普通情况下,用union也可以实现用一条语句进行查询。但是那样增加消耗(两个select部分),而且sql语句会比较长。

下面是一个是用case函数来完成这个功能的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
sql复制代码select country,

sum( case when sex = '1' then

population else 0 end), --男性人口

sum( case when sex = '2' then

population else 0 end) --女性人口

from table_a

group by country;

这样我们使用select,完成对二维表的输出形式,充分显示了case函数的强大。

三,在check中使用case函数。

在check中使用case函数在很多情况下都是非常不错的解决方法。可能有很多人根本就不用check,那么我建议你在看过下面的例子之后也尝试一下在sql中使用check。

下面我们来举个例子

公司a,这个公司有个规定,女职员的工资必须高于1000块。假如用check和case来表现的话,如下所示

1
2
3
4
5
6
7
8
9
sql复制代码constraint check_salary check

( case when sex = '2'

then case when salary > 1000

then 1 else 0 end

else 1 end = 1 )

假如单纯使用check,如下所示

1
2
3
4
sql复制代码
constraint check_salary check

( sex = '2' and salary > 1000 )

女职员的条件倒是符合了,男职员就无法输入了。

本文转载自: 掘金

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

mysql快速查询表的结构和注释,字段等信息

发表于 2021-10-23

本文使用的数据库连接工具为Navicat Premium 12

开发中难免会遇到需要导出某一个表的结构信息,字段信息或者注释信息,接下介绍一种快速查询以及导出的方式:

mysql所承建的所有表都保存在information_schema这个库里,而所有的字段都在columns 这张表里面,所以想查啥就去看对应的字段就行。

1
2
3
4
5
6
7
8
sql复制代码select
COLUMN_NAME 列名,
DATA_TYPE 字段类型,
CHARACTER_MAXIMUM_LENGTH 长度,
IS_NULLABLE 是否为空,
COLUMN_COMMENT 备注
FROM INFORMATION_SCHEMA.COLUMNS
where table_name = 'sys_resource_info'

查询结构如下:

在这里插入图片描述

完整版sql语句以及解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
sql复制代码SELECT
COLUMN_NAME 字段,
COLUMN_TYPE 类型, --包括数据类型和长度
DATA_TYPE 字段类型,
IS_NULLABLE 是否为空,
CHARACTER_MAXIMUM_LENGTH 长度. --主要是varchar类型的长度
COLUMN_DEFAULT 默认值,
COLUMN_COMMENT 备注
FROM
INFORMATION_SCHEMA. COLUMNS
WHERE
-- developerclub为数据库名称,到时候只需要修改成你要导出表结构的数据库即可
table_schema = 'developerclub'
AND -- article为表名,到时候换成你要导出的表的名称
-- 如果不写的话,默认会查询出所有表中的数据,这样可能就分不清到底哪些字段是哪张表中的了,所以还是建议写上要导出的名名称
table_name = 'article'

快速拷贝信息到excel表中:

1.选中查询结果,右键–》复制为—>制表符分割值(字段名和数据)

2.注意查看查询结果的右下角的总条数:14再加上1(1为标题行)

3.新建一个excel表,将数据拷贝里即可

4.如果是往word文档的表格里粘贴,需要插入相同列相同行的表格,这样才能准确完整的将所有数据录入到表格中。

在这里插入图片描述

本文转载自: 掘金

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

延时任务从入门到精通

发表于 2021-10-23

1. 背景

在日常开发中,延时任务是一个无法避免的话题。为了达到延时这一目的,在不同场景下会有不同的解决方案,对各个方案优缺点的认知程度决定了架构决策的有效性。

本文章,以电商订单超时未支付为业务场景,推导多种解决方案,并对每个方案的优缺点进行分析,所涉及的方案包括:

1.数据库轮询方案。2.单机内存解决方案。3.分布式延时队列方案。

最后,为了提升研发效率,我们将使用声明式编程思想,对分布式延时队列方案进行封装,有效的分离 业务 与 技术。

1.1 业务场景

业务场景非常简单,就是大家最熟悉的电商订单,相信很多细心的小伙伴都发现,我们在电商平台下单后,如果超过一定的时间还未支付,系统自动将订单设置为超时自动取消,从而释放绑定的资源。

核心流程如下:

1.在电商平台下单,生成待支付订单;2.在规定的时间内没有完成支付,系统将自动取消订单,订单状态变成“超时取消”;3.在规定的时间内完成支付,订单将变成“已支付”

订单状态机如下:

图片

状态机

1.2 基础组件简介

整个 Demo 采用 DDD 的设计思路,为了便于理解,先介绍所涉及的基础组件:

1.2.1. OrderInfo

订单聚合根,提供构建和取消等业务方法。具体的代码如下:

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
less复制代码@Data
@Entity
@Table(name = "order_info")
public class OrderInfo {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "status")
@Enumerated(EnumType.STRING)
private OrderInfoStatus orderStatus;

@Column(name = "create_time")
private Date createTime = new Date();

/**
* 取消订单
*/
public void cancel() {
setOrderStatus(OrderInfoStatus.CANCELLED);
}

/**
* 创建订单
* @param createDate
* @return
*/
public static OrderInfo create(Date createDate){
OrderInfo orderInfo = new OrderInfo();
orderInfo.setCreateTime(createDate);
orderInfo.setOrderStatus(OrderInfoStatus.CREATED);
return orderInfo;
}
}

1.2.2 OrderInfoRepository

基于 Spring Data Jpa 实现,主要用于数据库访问,代码如下:

1
2
3
csharp复制代码public interface OrderInfoRepository extends JpaRepository<OrderInfo, Long> {
List<OrderInfo> getByOrderStatusAndCreateTimeLessThan(OrderInfoStatus created, Date overtime);
}

Spring Data 会根据 方法签名 或 @Query 注解生成代理对象,无需我们写任何代码,便能实现基本的数据库访问。

1.2.3. OrderInfoService

应用服务层,面向 User Case,主要完成业务流程编排,核对代码如下:

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
typescript复制代码@Service
@Slf4j
public class OrderInfoService {
@Autowired
private ApplicationEventPublisher eventPublisher;

@Autowired
private OrderInfoRepository orderInfoRepository;


/**
* 生单接口 <br />
* 1. 创建订单,保存至数据库
* 2. 发布领域事件,触发后续处理
* @param createDate
*/
@Transactional(readOnly = false)
public void create(Date createDate){
OrderInfo orderInfo = OrderInfo.create(createDate);
this.orderInfoRepository.save(orderInfo);
eventPublisher.publishEvent(new OrderInfoCreateEvent(orderInfo));
}

/**
* 取消订单
* @param orderId
*/
@Transactional(readOnly = false)
public void cancel(Long orderId){
Optional<OrderInfo> orderInfoOpt = this.orderInfoRepository.findById(orderId);
if (orderInfoOpt.isPresent()){
OrderInfo orderInfo = orderInfoOpt.get();
orderInfo.cancel();
this.orderInfoRepository.save(orderInfo);
log.info("success to cancel order {}", orderId);
}else {
log.info("failed to find order {}", orderId);
}
}

/**
* 查找超时未支付的订单
* @return
*/
@Transactional(readOnly = true)
public List<OrderInfo> findOvertimeNotPaidOrders(Date deadLine){
return this.orderInfoRepository.getByOrderStatusAndCreateTimeLessThan(OrderInfoStatus.CREATED, deadLine);
}
}

1.2.4. OrderController

对外暴露的 Web 接口,提供接口创建订单,主要用于测试,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
less复制代码@RestController
@RequestMapping("order")
public class OrderController {
@Autowired
private OrderInfoService orderInfoService;

/**
* 生成新的订单,主要用于测试
*/
@PostMapping("insertTestData")
public void createTestOrder(){
Date date = DateUtils.addMinutes(new Date(), -30);
date = DateUtils.addSeconds(date, 10);
this.orderInfoService.create(date);
}
}

所依赖的组件介绍完了,让我们进入第一个方案。

2. 数据库轮询方案

这是最简单的方案,每个订单都保存了创建时间,只需要写个定时任务,从数据库中查询出已经过期但是尚未支付的订单,依次执行订单取消即可。

2.1. 方案实现

核心流程如下:

图片

数据库轮询方案

1.用户创建订单,将订单信息保存到数据库;2.设定一个定时任务,每一秒触发一次检查任务;3.任务按下面步骤执行•先从数据库中查找 超时未支付 的订单;•依次执行定的 Cancel 操作;•将变更保存到数据库;

核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
less复制代码@Service
@Slf4j
public class DatabasePollStrategy {
@Autowired
private OrderInfoService orderInfoService;

/**
* 每隔 1S 运行一次 <br/>
* 1. 从 DB 中查询过期未支付订单(状态为 CREATED,创建时间小于 deadLintDate)
* 2. 依次执行 取消订单 操作
*/
@Scheduled(fixedDelay = 1 * 1000)
public void poll(){
Date now = new Date();
Date overtime = DateUtils.addMinutes(now, -30);
List<OrderInfo> overtimeNotPaidOrders = orderInfoService.findOvertimeNotPaidOrders(overtime);
log.info("load overtime Not paid orders {}", overtimeNotPaidOrders);
overtimeNotPaidOrders.forEach(orderInfo -> this.orderInfoService.cancel(orderInfo.getId()));
}
}

2.2. 方案小结

1.优点:简单•开发简单。系统复杂性低,特别是在 Spring Schedule 帮助下;•测试简单。没有外部依赖,逻辑集中,方便快速定位问题;•上线简单。没有繁琐的配置,复杂的申请流程;2.缺点:•数据库负担重。不停的轮询,会加重数据库的负载;•时效性不足。任务最高延时为轮询时间,不适合时效要求高的场景(在订单场景已经足够);•存在大量无效轮询。在没有过期订单的情况下,出现大量的无效扫描;•没有消峰能力。短时间出现大量过期订单,会造成任务集中执行,出现明显的业务高峰;

总之,该方案非常适合业务量级小,业务迭代快的项目。

3. 单机内存解决方案

对于延时任务,JDK 为我们准备了大量工具,使用这些工具可以解决我们的问题。

3.1 DelayQueue

DelayQueue 是一种特殊的阻塞队列,可以为每个任务指定延时时间,只有在延时时间到达后,才能获取任务。

整体结构如下:

图片

延时队列

核心流程如下:

1.用户下单完成后,向延时队列提交一个任务;2.时间达到后,后台工作线程从队列中读取任务;3.工作线程调用 CancelOrder 方法 对过期未支付的订单执行取消操作;

核心代码如下:

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
java复制代码@Slf4j
@Service
public class DelayQueueStrategy implements SmartLifecycle {
private final DelayQueue<DelayTask> delayTasks = new DelayQueue<>();
private final Thread thread = new OrderCancelWorker();
private boolean running;
@Autowired
private OrderInfoService orderInfoService;

@TransactionalEventListener
public void onOrderCreated(OrderInfoCreateEvent event){
// 将 订单号 放入延时队列
this.delayTasks.offer(new DelayTask(event.getOrderInfo().getId(), 10));
log.info("success to add Delay Task for Cancel Order {}", event.getOrderInfo().getId());
}

/**
* 启动后台线程,消费延时队列中的任务
*/
@Override
public void start() {
if (this.running){
return;
}
this.thread.start();

this.running = true;
}

/**
* 停止后台线程
*/
@Override
public void stop() {
if (!this.running){
return;
}
this.thread.interrupt();
this.running = false;
}

@Override
public boolean isRunning() {
return this.running;
}

@Override
public boolean isAutoStartup() {
return true;
}


/**
* 延时任务
*/
@Value
private static class DelayTask implements Delayed{
private final Long orderId;
private final Date runAt;

private DelayTask(Long orderId, int delayTime) {
this.orderId = orderId;
this.runAt = DateUtils.addSeconds(new Date(), delayTime);
}

/**
* 获取剩余时间
* @param timeUnit
* @return
*/
@Override
public long getDelay(TimeUnit timeUnit) {
return timeUnit.convert(getRunAt().getTime() - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}

@Override
public int compareTo(Delayed delayed) {
if (delayed == this) {
return 0;
} else {
long d = this.getDelay(TimeUnit.NANOSECONDS) - delayed.getDelay(TimeUnit.NANOSECONDS);
return d == 0L ? 0 : (d < 0L ? -1 : 1);
}
}
}

/**
* 后台线程,消费延时队列中的消息
*/
private class OrderCancelWorker extends Thread {
@Override
public void run() {
// 根据中断状态,确定是否退出
while (!Thread.currentThread().isInterrupted()){
DelayTask task = null;
try {
// 从队列中获取任务
task = delayTasks.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 取消订单
if (task != null){
orderInfoService.cancel(task.getOrderId());
log.info("Success to Run Delay Task, Cancel Order {}", task.getOrderId());
}
}
}
}
}

这个方案,思路非常简单,但是有一定的复杂性,需要对工作线程的生命周期进行手工维护。相对来说,JDK 已经为我们的这种场景进行了封装,也就是基于 DelayQueue 的 ScheduledExecutorService。

3.2 ScheduledExecutorService

ScheduledExecutorService 是基于 DelayQueue 构建的定时调度组件,相对之前的 Timer 有非常大的优势。

整体架构如下:

图片

ScheduleExecutorService

核心流程如下:

1.用户下单完成后,向 ScheduledExecutorService 注册一个定时任务;2.时间达到后,ScheduledExecutorService 将启动任务;3.线程池线程调用 CancelOrder 方法 对过期未支付的订单执行取消操作;

核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
java复制代码@Slf4j
@Service
public class ScheduleExecutorStrategy {
@Autowired
private OrderInfoService orderInfoService;

private ScheduledExecutorService scheduledExecutorService;

public ScheduleExecutorStrategy(){
BasicThreadFactory basicThreadFactory = new BasicThreadFactory.Builder()
.namingPattern("Schedule-Cancel-Thread-%d")
.daemon(true)
.build();
this.scheduledExecutorService = new ScheduledThreadPoolExecutor(1, basicThreadFactory);
}

@TransactionalEventListener
public void onOrderCreated(OrderInfoCreateEvent event){
// 添加定时任务
this.scheduledExecutorService.schedule(new CancelTask(event.getOrderInfo().getId()), 5, TimeUnit.SECONDS);
log.info("Success to add cancel task for order {}", event.getOrderInfo().getId());
}

private class CancelTask implements Runnable{
private final Long orderId;

private CancelTask(Long orderId) {
this.orderId = orderId;
}

@Override
public void run() {
// 执行订单取消操作
orderInfoService.cancel(this.orderId);
log.info("Success to cancel task for order {}", this.orderId);
}
}
}

相对 DelayQueue 方案,ScheduledExecutorService 代码量少了很多,避免了繁琐的细节。

3.3 小结

优点:

1.避免了对DB的轮询,降低 DB 的压力;2.整体方案简单,使用 JDK 组件完成,没有额外依赖;

缺点:

1.任务容易丢失。任务存储于内存中,服务重启或机器宕机,会造成内存任务丢失;2.单机策略,缺少集群能力。

为了解决 单机内存方案 的问题,我们需要引入分布式方案。

在单机内存方案中,除了 延时队列 实现外,还有一种 “时间轮” 方案,能够大幅降低内存消耗,有兴趣的伙伴可以研究一下。

4. 分布式延时队列方案

内存队列自身存在很多限制,在实际工作中,我们一般会引入分布式解决方案。

4.1 基于 Redis 延时队列

Redis 是最常用的基础设施,作为一个数据结构服务器,在丰富的数据结构帮助下,可以封装成多种高级结构,延时队列便是其中一种。

为了避免重复发明轮子,我们直接使用 Redisson 中的 延时队列。

整体架构与 DelayQueue 基本一致,只是将 内存延时队列 升级为 分布式延时队列,在此就不在论述。

首先,在 pom 中引入 Redisson 相关依赖

1
2
3
4
5
xml复制代码<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.16.2</version>
</dependency>

然后,在 application 配置文件中增加 redis 相关配置

1
2
3
ini复制代码spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.database=0

最后,就可以注入核心组件 RedissonClient 了

1
2
java复制代码@Autowired
private RedissonClient redissonClient;

流程整合后的代码如下:

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
typescript复制代码@Slf4j
@Service
public class RDelayQueueStrategy implements SmartLifecycle {
private boolean running;

private Thread thread = new OrderCancelWorker();

private RBlockingQueue<Long> cancelOrderQueue;

private RDelayedQueue<Long> delayedQueue;

@Autowired
private OrderInfoService orderInfoService;

@Autowired
private RedissonClient redissonClient;

/**
* 创建 Redis 队列
*/
@PostConstruct
public void init(){
this.cancelOrderQueue = redissonClient.getBlockingQueue("DelayQueueForCancelOrder");
this.delayedQueue = redissonClient.getDelayedQueue(cancelOrderQueue);
}

@TransactionalEventListener
public void onOrderCreated(OrderInfoCreateEvent event){
this.delayedQueue.offer(event.getOrderInfo().getId(), 5L, TimeUnit.SECONDS);
log.info("success to add Delay Task for Cancel Order {}", event.getOrderInfo().getId());
}

/**
* 启动后台线程
*/
@Override
public void start() {
if (this.running){
return;
}
thread.start();
this.running = true;

}

/**
* 停止后台线程
*/
@Override
public void stop() {
if (!this.running){
return;
}
thread.interrupt();
this.running = false;
}

@Override
public boolean isRunning() {
return this.running;
}

@Override
public boolean isAutoStartup() {
return true;
}

private class OrderCancelWorker extends Thread {
@Override
public void run() {
// 根据中断状态,确定是否退出
while (!Thread.currentThread().isInterrupted()){
Long orderId = null;
try {
// 从队列中获取 订单号
orderId = cancelOrderQueue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 取消订单
if (orderId != null){
orderInfoService.cancel(orderId);
log.info("Success to Run Delay Task, Cancel Order {}", orderId);
}
}
}
}
}

这个方案非常简单,应用于大多数业务场景。但是,Redis 本身是遵循 AP 而非 CP 模型,在集群切换时会出现消息丢失的情况,所以对于一致性要求高的场景,建议使用 RocketMQ 方案。

4.2 基于 RocketMQ 延时队列

RocketMQ 是 阿里开源的分布式消息中间件,其整体设计从 Kafka 借鉴了大量思想,但针对业务场景增加了部分扩展,其中延时队列便是其中最为重要的一部分。

整体架构设计如下:

图片

RocketMQ 延时队列

核心流程如下:

1.用户下单完成后,向 RocketMQ 提交一个消息;2.时间达到后,消费线程从工作队列中获取消息;3.消费线程解析消息后调用 CancelOrder 方法 对过期未支付的订单执行取消操作;

首先,需要增加 RocketMQ 相关依赖

1
2
3
4
5
xml复制代码<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.1</version>
</dependency>

然后,在 application 添加相关配置

1
2
ini复制代码rocketmq.name-server=http://127.0.0.1:9876
rocketmq.producer.group=delay-task-demo

最后,我们就可以使用 RocketMQTemplate 发送消息

1
2
java复制代码@Autowired
private RocketMQTemplate rocketMQTemplate;

注:RocketMQ 并不支持任意的时间,而是提供了几个固定的延时时间,一般情况下可以满足我们的业务需求,如果现有固定延时无法满足需求,可以通过多次投递的方式进行解决。比如,RocketMQ 最大支持 2H 延时,而业务需要延时 24H,只需在消息体中增加期望执行时间,获取消息后,如果尚未达到期望执行时间,将消息重新发送回延时队列;如果达到期望执行时间,则执行对于的任务。

发送延时消息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typescript复制代码@Service
@Slf4j
public class RocketMQBasedDelayStrategy {
private static final String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";
@Autowired
private RocketMQTemplate rocketMQTemplate;

@TransactionalEventListener
public void onOrderCreated(OrderInfoCreateEvent event){
// 将数据 发送至 RocketMQ 的延时队列
Message<String> message = MessageBuilder
.withPayload(String.valueOf(event.getOrderInfo().getId()))
.build();
this.rocketMQTemplate.syncSend("delay-task-topic", message, 200, 2);
log.info("success to sent Delay Task to RocketMQ for Cancel Order {}", event.getOrderInfo().getId());
}
}

构建 Consumer 消费消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
less复制代码@Service
@Slf4j
@RocketMQMessageListener(topic = "delay-task-topic", consumerGroup = "delay-task-consumer-group")
public class RocketMQBasedDelayTaskConsumer implements RocketMQListener<MessageExt> {
@Autowired
private OrderInfoService orderInfoService;

/**
* 接收消息回调,执行取消订单操作
* @param message
*/
@Override
public void onMessage(MessageExt message) {
byte[] body = message.getBody();
String idAsStr = new String(body);
orderInfoService.cancel(Long.valueOf(idAsStr));
}
}

4.3 小结

一般互联网公司都会使用 RocketMQ 方案来解决延时问题。

优点,主要来自于分布式服务特性:

1.高性能。作为削峰填谷的利器,发送端、服务器、消费端都提供较高性能;2.高可用。Redis、RocketMQ 都提供了丰富的部署模式,是高可用的基础;3.可扩展。Redis、RocketMQ 集群具有良好的扩展能力;

缺点:

1.需要中间支持。首先,需要基础设施的支持,Redis、RocketMQ 都会增加运维成本;2.需要学习新的 API。需要掌握新的 API,增加学习成本,使用不当还可能出现问题;

5. 声明式编程

架构设计中有一个非常重要的原则:有效分离技术和业务,避免两者的相互影响。

5.1 声明式编程

声明式编程(英语:Declarative programming)是一种编程范式,与命令式编程相对立。它描述目标的性质,让计算机明白目标,而非流程。声明式编程不用告诉计算机问题领域,从而避免随之而来的副作用。而命令式编程则需要用算法来明确的指出每一步该怎么做。

每引入一个中间件,研发人员都需要学习一套新的API,如何有效降低接入成本是一个巨大的挑战,而最常用的重要手段之一就是:声明式编程。

简单来说,就是将能力抽象化,使其能够通过配置的方式灵活的应用于需要的场景。

首先,让我们先看下最终的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
kotlin复制代码@Service
@Slf4j
public class RocketMQBasedDelayService {
@Autowired
private OrderInfoService orderInfoService;

/**
* 通过 RocketMQBasedDelay 指定方法为延时方法,该 注解做两件事:<br />
* 1. 基于 AOP 技术,拦截对 cancelOrder 的调用,将参数转为为 Message, 并发送到 RocketMQ 的延时队列
* 2. 针对 cancelOrder 方法,创建 DefaultMQPushConsumer 并订阅相关消息,进行消息处理
* @param orderId
*/
@RocketMQBasedDelay(topic = "delay-task-topic-ann",
delayLevel = 2, consumerGroup = "CancelOrderGroup")
public void cancelOrder(Long orderId){
if (orderId == null){
log.info("param is invalidate");
return;
}
this.orderInfoService.cancel(orderId);
log.info("success to cancel Order for {}", orderId);
}
}

相比于普通方法,增加 @RocketMQBasedDelay 便可以赋予方法延时能力,这便是“声明式编程”的威力

1.首先在方法上添加 @RocketMQBasedDelay 注解,配置延时队列名称,延时时间,消费者信息;2.当方法被调用时,并不会直接执行,而是将请求转发给 RocketMQ 的延时队列,然后直接返回;3.当到达消息延时时间时,Consumer 从 延时队列中获取消息,并调用 cancelOrder 方法来处理业务流程。

使用这种方式,大大减少了接入成本,降低了出错的概率。

5.2 核心设计

核心设计如下:

图片

RocketMQBasedDelay

在启动时,增加了两个扩展点:

1.扫描 @RocketMQBasedDelay 注解方法,为方法增加 SendMessageInterceptor 拦截器;2.扫描 @RocketMQBasedDelay 注解方法,生成 RocketMQConsumerContainer 托管对象,并完成 DefaultMQPushConsumer 的配置和启动;

具体的执行流程如下:

1.当方法被调用时,调用被 SendMessageInterceptor 拦截,从而改变原有执行规则,新的流程如下:•从 @RocketMQBasedDelay 获取相关的配置参数;•对请求参数进行序列化处理;•使用 RocketMQTemplate 发送延时消息;•直接返回,中断原有方法调用;2.当延时时间到达时,RocketMQConsumerContainer 中的 DefaultMQPushConsumer 会获取到消息进行业务处理:•反序列化调用参数;•调用业务方法;•返回消费状态;

5.3 核心实现

核心组件,主要分为两类:

1.工作组件。•SendMessageInterceptor。拦截请求,将请求转发至 RocketMQ 的延时队列;•RocketMQConsumerContainer。对 DefaultMQPushConsumer 的封装,主要完成 Consumer 的配置,注册监听器,消息到达后触发任务的执行;2.配置组件。•RocketMQConsumerContainerRegistry。对 Spring 容器中的 Bean 进行扫描,将@RocketMQBasedDelay注解的方法封装成 RocketMQConsumerContainer,并注册到 Spring 容器中;•RocketMQBasedDelayConfiguration。向 Spring 容器注册 AOP 拦截器 和 RocketMQConsumerContainerRegistry;

RocketMQBasedDelay 注解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
less复制代码@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RocketMQBasedDelay {
/**
* RocketMQ topic
* @return
*/
String topic();

/**
* 延时级别
* @return
*/
int delayLevel();

/**
* 消费者组信息
* @return
*/
String consumerGroup();
}

该注解可以放置在方法之上,并在 运行时 生效。

SendMessageInterceptor 核心代码如下:

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
typescript复制代码/**
* 拦截方法调用,并将请求封装成 Message 发送至 RocketMQ 的 Topic
*/
@Slf4j
public class SendMessageInterceptor implements MethodInterceptor {
@Autowired
private RocketMQTemplate rocketMQTemplate;


@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
Method method = methodInvocation.getMethod();

// 1. 获取 方法上的注解信息
RocketMQBasedDelay rocketMQBasedDelay = method.getAnnotation(RocketMQBasedDelay.class);

// 2. 将请求参数 转换为 MQ
Object[] arguments = methodInvocation.getArguments();
String argData = serialize(arguments);
Message<String> message = MessageBuilder
.withPayload(argData)
.build();

// 3. 发送 MQ
this.rocketMQTemplate.syncSend(rocketMQBasedDelay.topic(), message , 200, rocketMQBasedDelay.delayLevel());
log.info("success to sent Delay Task to RocketMQ for {}", Arrays.toString(arguments));
return null;
}

private String serialize(Object[] arguments) {
Map<String, String> result = Maps.newHashMapWithExpectedSize(arguments.length);
for (int i = 0; i < arguments.length; i++){
result.put(String.valueOf(i), SerializeUtil.serialize(arguments[i]));
}
return SerializeUtil.serialize(result);
}

}

RocketMQConsumerContainer 源码如下:

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
typescript复制代码/**
* Consumer 容器,用于对 DefaultMQPushConsumer 的封装
*/
@Data
@Slf4j
public class RocketMQConsumerContainer implements InitializingBean, SmartLifecycle {
private DefaultMQPushConsumer consumer;
private boolean running;
private String consumerGroup;
private String nameServerAddress;
private String topic;
private Object bean;
private Method method;

@Override
public boolean isAutoStartup() {
return true;
}

@Override
public void start() {
if (this.running){
return;
}
try {
this.consumer.start();
} catch (MQClientException e) {
e.printStackTrace();
}
this.running = true;
}

@Override
public void stop() {
this.running = false;
this.consumer.shutdown();
}

@Override
public boolean isRunning() {
return running;
}

@Override
public void afterPropertiesSet() throws Exception {
// 构建 DefaultMQPushConsumer
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer();
consumer.setConsumerGroup(this.consumerGroup);
consumer.setNamesrvAddr(this.nameServerAddress);

// 订阅 topic
consumer.subscribe(topic, "*");
// 增加拦截器
consumer.setMessageListener(new DefaultMessageListenerOrderly());
this.consumer = consumer;
}

private class DefaultMessageListenerOrderly implements MessageListenerOrderly {

@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
for (MessageExt messageExt : msgs) {
log.debug("received msg: {}", messageExt);
try {
long now = System.currentTimeMillis();

// 从 Message 中反序列化数据,获得方法调用参数
byte[] body = messageExt.getBody();
String bodyAsStr = new String(body);
Map deserialize = SerializeUtil.deserialize(bodyAsStr, Map.class);
Object[] params = new Object[method.getParameterCount()];

for (int i = 0; i< method.getParameterCount(); i++){
String o = (String)deserialize.get(String.valueOf(i));
if (o == null){
params[i] = null;
}else {
params[i] = SerializeUtil.deserialize(o, method.getParameterTypes()[i]);
}
}

// 执行业务方法
method.invoke(bean, params);
long costTime = System.currentTimeMillis() - now;
log.debug("consume {} cost: {} ms", messageExt.getMsgId(), costTime);
} catch (Exception e) {
log.warn("consume message failed. messageId:{}, topic:{}, reconsumeTimes:{}", messageExt.getMsgId(), messageExt.getTopic(), messageExt.getReconsumeTimes(), e);
context.setSuspendCurrentQueueTimeMillis(1000);
return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
}
}

return ConsumeOrderlyStatus.SUCCESS;
}
}
}

RocketMQConsumerContainerRegistry 源码如下:

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
kotlin复制代码/**
* 基于 BeanPostProcessor#postProcessAfterInitialization 对每个 bean 进行处理
* 扫描 bean 中被 @RocketMQBasedDelay 注解的方法,并将方法封装成 RocketMQConsumerContainer,
* 以启动 DefaultMQPushConsumer
*/
public class RocketMQConsumerContainerRegistry implements BeanPostProcessor {
private final AtomicInteger id = new AtomicInteger(1);
@Autowired
private GenericApplicationContext applicationContext;
@Value("${rocketmq.name-server}")
private String nameServerAddress;

/**
* 对每个 bean 依次进行处理
* @param bean
* @param beanName
* @return
* @throws BeansException
*/
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
// 1. 获取 @RocketMQBasedDelay 注解方法
Class targetCls = AopUtils.getTargetClass(bean);
List<Method> methodsListWithAnnotation = MethodUtils.getMethodsListWithAnnotation(targetCls, RocketMQBasedDelay.class);

// 2. 为每个 @RocketMQBasedDelay 注解方法 注册 RocketMQConsumerContainer
for(Method method : methodsListWithAnnotation){
String containerBeanName = targetCls.getName() + "#" + method.getName() + id.getAndIncrement();
RocketMQBasedDelay annotation = method.getAnnotation(RocketMQBasedDelay.class);
applicationContext.registerBean(containerBeanName, RocketMQConsumerContainer.class, () -> createContainer(bean, method, annotation));
}

return bean;
}

/**
* 构建 RocketMQConsumerContainer
* @param proxy
* @param method
* @param annotation
* @return
*/
private RocketMQConsumerContainer createContainer(Object proxy, Method method, RocketMQBasedDelay annotation) {
Object bean = AopProxyUtils.getSingletonTarget(proxy);
RocketMQConsumerContainer container = new RocketMQConsumerContainer();
container.setBean(bean);
container.setMethod(method);
container.setConsumerGroup(annotation.consumerGroup());
container.setNameServerAddress(nameServerAddress);
container.setTopic(annotation.topic());
return container;
}
}

RocketMQBasedDelayConfiguration 源码如下:

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
typescript复制代码@Configuration
public class RocketMQBasedDelayConfiguration {

/**
* 声明 RocketMQConsumerContainerRegistry,扫描 RocketMQBasedDelay 方法,
* 创建 DefaultMQPushConsumer 并完成注册
* @return
*/
@Bean
public RocketMQConsumerContainerRegistry rocketMQConsumerContainerRegistry(){
return new RocketMQConsumerContainerRegistry();
}

/**
* 声明 AOP 拦截器
* 在调用 @RocketMQBasedDelay 注解方法时,自动拦截,将请求发送至 RocketMQ
* @return
*/
@Bean
public SendMessageInterceptor messageSendInterceptor(){
return new SendMessageInterceptor();
}

/**
* 对 @RocketMQBasedDelay 标注方法进行拦截
* @param sendMessageInterceptor
* @return
*/
@Bean
public PointcutAdvisor pointcutAdvisor(@Autowired SendMessageInterceptor sendMessageInterceptor){
return new DefaultPointcutAdvisor(new AnnotationMatchingPointcut(null, RocketMQBasedDelay.class), sendMessageInterceptor);
}
}

5.4 小结

声明式编程,在设计时会有比较明显的门槛,但这种代价换来的是 使用上的便利性。这种一次性投入,多次创造价值的做法,非常推荐应用,大大提升研发效率、降低错误出现概率。

6. 小结

本文,以自动对超时未支付订单执行取消操作为业务场景,先后介绍了

1.DB 轮询方案;2.基于延时队列和ScheduleExecutorService的单机内存方案;3.基于 Redis 和 RocketMQ 的分布式延时队列方案;

并详细阐述了各个方案优缺点,希望各位伙伴能在实际开发中根据业务场景选择最优解决方案。

最后,对“声明式编程”进行了简单介绍,通过技术手段降低接入成本。

按照惯例,附上源码 源码

本文转载自: 掘金

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

1…474475476…956

开发者博客

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