0%

markdown 使用整理

标题

标题分级
使用#号不同的个数来组合实现
一个最大 依次减少 一共六级
第一级和第二级在内容下会有分割线

# 大
## 大
### 小

内容

正文标准文字/段落

可以直接进行书写, 如果不换行长度够了之后会自动换行。
如果是段落也不需要缩进
If you paste a message from somewhere else
this is next line

文字加粗倾斜

可以在文字的两端加上 **或者__ 实现加粗。加上 _或者 * 实现倾斜。
*** 则是加粗倾斜

**加粗文体**  
_倾斜文字_
___加粗倾斜文字___

加粗文体
倾斜文字
加粗倾斜文字

引用

单行引用可以在开头使用 > 来实现效果

> 这是一行引用文字

这是一行引用文字

引用可以嵌套

1
2
> 第一行
>> 第二行

第一行

第二行

引用块的书写,可以开头结尾加上 >>>或者 ~~~ ,开头注意换行

>>>
If you paste a message from somewhere else

that spans multiple lines,

you can quote that without having to manually prepend `>` to every line!
>>>
If you paste a message from somewhere else
that spans multiple lines,

you can quote that without having to manually prepend `>` to every line!

代码块

行内代码可以在开头结尾使用反单引号 ` 来实现效果

`code`

code

代码块可以在开头结尾加上三个反单引号 ```来实现效果, 还可以表明代码的语言类型, 会有代码高亮的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    def function():
#indenting works just fine in the fenced code block
s = "Python code"
print s
```


```python
def function():
#indenting works just fine in the fenced code block
s = "Python code"
print s
```

这是个json格式的

```json
{
"title": "About Front Matter"
"example": {
"language": "json"
}
}

列表

带数字的列表,可以通过加前缀 1.实现。如果是无序的队列,可以通过通过增加 -、+、* 前缀来实现

1
2
1. item
2. item
  1. item
  2. item
1
2
3
4
5
- item
- item
- item
- item
- item
  • item
  • item
  • item
    • item
    • item

注释

注释通过在 md 里创建 [^*] 对象,在对应位置引用

A footnote reference tag looks like this: [^1]
This reference tag is a mix of letters and numbers. [^footnote-42]


[^1]: This text is inside a footnote.
[^footnote-42]: This text is another footnote.

A footnote reference tag looks like this: [^1]
This reference tag is a mix of letters and numbers. [^footnote-42]

[^1]: This text is inside a footnote.
[^footnote-42]: This text is another footnote.

表格

通过英文的 |和—-以及:组合实现基本的表格设置

|title1|title2|
|--|--|
|column|column|
title1 title2
column column

通过 --和: 的组合设置对其方式。

链接

md可以直接识别一个URL链接,也可以通过 [name](url) 来格式化。

http://api.*.net/customer/v2.0/weChat/auth

http://api.*.net/customer/v2.0/weChat/auth

[Link](http://api.*.net/customer/v2.0/weChat/auth)

Link

Spring Boot配置 jackson 序列化一系列的参数

背景

  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
    @Configuration
    @EnableWebMvc
    public class JinWeiConvertersConfigurer implements WebMvcConfigurer {
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    //1.需要先定义一个 convert 转换消息的对象;
    FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();


    //2、添加fastJson 的配置信息,比如:是否要格式化返回的json数据;
    FastJsonConfig fastJsonConfig = new FastJsonConfig();
    // 不忽略对象属性中的null值
    fastJsonConfig.setSerializerFeatures(
    PrettyFormat,
    WriteMapNullValue,
    WriteNullListAsEmpty,
    WriteNullStringAsEmpty,
    WriteNullNumberAsZero,
    WriteDateUseDateFormat);
    //3、在convert中添加配置信息.
    fastConverter.setFastJsonConfig(fastJsonConfig);
    //4、将convert添加到converters当中.
    converters.add(fastConverter);
    }

    }
  2. 可以注意到这个配置主要是通过 FastJsonHttpMessageConverter 来实现的,不过很快就发现了问题,项目配置的Swagger UI无法访问了,所以就有了如下的代码增加:(在配置累中增加了 addResourceHandlers 函数)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
    // 注意 :
    // 1. addResourceHandler 参数可以有多个
    // 2. addResourceLocations 参数可以是多个,可以混合使用 file: 和 classpath : 资源路径
    // 3. addResourceLocations 参数中资源路径必须使用 / 结尾,如果没有此结尾则访问不到
    // 映射到文件系统中的静态文件(应用运行时,这些文件无业务逻辑,但可能被替换或者修改)
    //过滤swagger
    registry.addResourceHandler("swagger-ui.html")
    .addResourceLocations("classpath:/META-INF/resources/“);
    registry.addResourceHandler("/webjars/**")
    .addResourceLocations("classpath:/META-INF/resources/webjars/");
    registry.addResourceHandler("/swagger-resources/**")
    .addResourceLocations("classpath:/META-INF/resources/swagger-resources/");
    registry.addResourceHandler("/swagger/**")
    .addResourceLocations("classpath:/META-INF/resources/swagger*");
    registry.addResourceHandler("/v2/api-docs/**")
    .addResourceLocations("classpath:/META-INF/resources/v2/api-docs/");
    }
  3. 好景不长,很快又发现时间等的格式有些对不上,频繁的修改我意识到是不是又哪儿不太对,通过对系统代码和配置的检查,我意识到同事搭建的这套系统默认使用的是Jackson,所以通过FastJson设置的这些空处置防范无法奏效,所以
    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
    package net.jinwei.config.converters;


    import com.alibaba.fastjson.serializer.SerializeConfig;
    import com.alibaba.fastjson.support.config.FastJsonConfig;
    import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.http.converter.HttpMessageConverter;
    import org.springframework.http.converter.StringHttpMessageConverter;
    import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
    import org.springframework.web.servlet.config.annotation.EnableWebMvc;
    import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;


    import java.math.BigDecimal;
    import java.nio.charset.Charset;
    import java.util.List;




    /**
    * 功能简要
    * 消息转换配置
    *
    * @author xbcui
    * createTime 2021/12/24 9:52 AM
    */
    @Configuration
    @EnableWebMvc
    public class JinWeiConvertersConfigurer implements WebMvcConfigurer {
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    //在json转换之前先进行string转换
    converters.add(new StringHttpMessageConverter());
    //添加json转换
    MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
    jackson2HttpMessageConverter.setObjectMapper(new ConvertersJsonMapper());
    converters.add(jackson2HttpMessageConverter);
    }


    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
    //过滤swagger
    registry.addResourceHandler("swagger-ui.html")
    .addResourceLocations("classpath:/META-INF/resources/");
    registry.addResourceHandler("/webjars/**")
    .addResourceLocations("classpath:/META-INF/resources/webjars/");
    registry.addResourceHandler("/swagger-resources/**")
    .addResourceLocations("classpath:/META-INF/resources/swagger-resources/");
    registry.addResourceHandler("/swagger/**")
    .addResourceLocations("classpath:/META-INF/resources/swagger*");
    registry.addResourceHandler("/v2/api-docs/**")
    .addResourceLocations("classpath:/META-INF/resources/v2/api-docs/");
    }
    }
    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
    package net.jinwei.config.converters;


    import com.fasterxml.jackson.core.JsonGenerator;
    import com.fasterxml.jackson.databind.JsonSerializer;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.fasterxml.jackson.databind.SerializerProvider;
    import com.fasterxml.jackson.databind.module.SimpleModule;
    import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;


    import java.io.IOException;
    import java.lang.reflect.Field;
    import java.text.SimpleDateFormat;
    import java.util.List;
    import java.util.Map;
    import java.util.Objects;
    import java.util.TimeZone;


    import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;


    /**
    * 功能简要
    * 转JSON 空值处理
    *
    * @author xbcui
    * createTime 2021/12/29 5:25 PM
    */
    public class ConvertersJsonMapper extends ObjectMapper {
    public ConvertersJsonMapper() {
    super();
    //收到未知属性时不报异常
    this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
    this.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
    this.setTimeZone(TimeZone.getTimeZone("GMT+8"));

    //Long类型转为String类型
    SimpleModule simpleModule = new SimpleModule();
    simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
    simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance);
    this.registerModule(simpleModule);
    //处理null时设置的值
    this.getSerializerProvider().setNullValueSerializer(new JsonSerializer<Object>() {
    @Override
    public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
    String fieldName = gen.getOutputContext().getCurrentName();
    try {
    //反射获取字段类型
    Field field = gen.getCurrentValue().getClass().getDeclaredField(fieldName);
    if (Objects.equals(field.getType(), String.class)) {
    //字符串型空值""
    gen.writeString("");
    return;
    } else if (Objects.equals(field.getType(), List.class)) {
    //列表型空值返回[]
    gen.writeStartArray();
    gen.writeEndArray();
    return;
    } else if (Objects.equals(field.getType(), Map.class)) {
    //map型空值返回{}
    gen.writeStartObject();
    gen.writeEndObject();
    return;
    } else if(Objects.equals(field.getType(), Long.class)){
    gen.writeNumber(0);
    return;
    }else if (Objects.equals(field.getType(), String[].class)) {
    //列表型空值返回[]
    gen.writeStartArray();
    gen.writeEndArray();
    return;
    }
    } catch (NoSuchFieldException e) {
    }
    //默认返回""
    gen.writeString("");
    }
    });
    }
    }

HttpMessageConverter

经过这次的修改,我决定总结下相关的东西。

HttpMessageConverter是什么

官方文档介绍:

HttpMessageConverter is responsible for converting from the HTTP request message to an object and converting from an object to the HTTP response body

DispatcherServlet默认已经安装了AnnotationMethodHandlerAdapter作为HandlerAdapter组件的实现类,HttpMessageConverter即由AnnotationMethodHandlerAdapter使用,将请求信息转换为对象,或将对象转换为响应信息。

HttpMessageConverter怎么生效

如何使用HttpMessageConverter 将请求信息转化并绑定到处理方法的入参当中呢?
可以通过两种方式实现:

  1. 使用@RequestBody/@ResponseBody对处理方法进行标注
  2. 使用HttpEntity/ResponseEntity作为处理方法的入参或者返回值

处理方法如何知道请求消息的格式?在处理完成后又是根据什么确定响应消息的格式
可以根据请求消息头的”Content-Type“及Accept属性确定。

HttpMessageConverter这个接口,简单说就是HTTP的request和response的转换器,在遇到@RequestBody时候SpringBoot会选择一个合适的HttpMessageConverter实现类来进行转换,内部有很多实现类,也可以自己实现,如果这个实现类能处理这个数据,那么它的canRead()方法会返回true,SpringBoot会调用他的read()方法从请求中读出并转换成实体类,同样canWrite也是。

自定义HttpMessageConverter实现

文档:
Customization of HttpMessageConverter can be achieved in Java config by overriding configureMessageConverters() if you want to replace the default converters created by Spring MVC, or by overriding extendMessageConverters() if you just want to customize them or add additional converters to the default ones.
如果想替换默认的converter,通过重写configureMessageConverters()这个方法。
而如果你想添加一个自定义converter,通过重写extendMessageConverters()这个方法。

Spring Boot参数校验机制介绍及问题排查

背景

公司新的系统版本从Spring cloud Alibaba 微服务简化为Spring Boot的一个单服务,但是代码架构还是按照服务做了简单的模块划分。
接口系统上参数校验使用了 hibernate validator, 这个包还是很好用的。hibernate validator 是对 Jakarta Bean Validation规范的实现的一种。

Jakarta Bean Validation 介绍

Jakarta Bean Validation 据说之前是Java Bean Validation, Bean Validation 技术隶属于Java EE规范,期间有多个JSR(Java Specification Request)支持(303、349、380),他是一种规范是标准,hibernate validator是对他的一种实现。所以处理几个JSR规定的一些规范注解外,hibernate validator 还提供了一些别的使用的注解。Spring Boot已经引用hibernate-validator包。所以我们Spring Boot项目不用单独导包,就可以直接使用。就像Jakarta Bean Validation 自己介绍说的

Constrain once, validate everywhere

附上文档地址:

  1. hibernate validator
  2. Jakarta Bean Validation

问题

新的系统架构现在主要在开发测试阶段,自测开发的模块时发现参数校验的几个注解不生效(@NotBlank、@NotEmpty)。
几个注解的含义

@NotNull: 不能为null,但可以为empty,用在基本类型上。
@NotEmpty:不能为null,而且长度必须大于0,用在集合类上。
@NotBlank:不能为null,而且调用trim()后,长度必须大于0,只能作用在String上。

主要校验注解介绍文档传送门

处理

  1. 最早出现这个问题时,随手查了下, 说是需要在controller层参数上加伤 @Valid注解,确实没有加,但是加上之后发现还是没有生效,但是@NotNull可以起作用(可惜它不能满足需求)。
  2. 继续肝,发现如果import的NotBlank的包,特意指定到hibernate-validator目录下的包就可以了,这里肯定有一些包上的问题。
  3. 使用 MavenHelper 和 mvn dependency:tree命令查找发现,项目里有个独立引用的hibernate-validator包而且版本比较老旧,直接干掉了。
  4. 问题解决。

另外

对于这个的检查结果,因为主要是在controller使用,建议做统一处理,可以使用@ControllerAdvice注解。

结论

spring boot项目不需要独立的引用hibernate-validator包,核心包里已经带了;另外包的版本变化还是挺大的哦。

Docker部署Logstash记

背景

之前学习Docker,练习时已经在Docker安装了es和kibana容器(此处很简单,只需注意docker镜像的版本对照)。最近公司在上马ES作为数据查询出口的主力,所以想在本地试验下Logstash数据收集同步。

步骤

安装logstash

之前安装的ES和Kibana版本都是7.6.0, 版本不用纠结。
docker pull logstash:7.6.0

获取配置文件

logstash的配置文件,如果手上没有,可以启动logstash后获取默认配置文件。
docker run -d -p 5044:5044 --name logstash logstash:7.6.0

查看日志信息 是否启动成功

docker logs -f logstash

本地环境创建所需挂载文件夹

mkdir -p ~/docker/logstash

1
2
3
docker cp logstash:/usr/share/logstash/config ~/docker/logstash/
docker cp logstash:/usr/share/logstash/data ~/docker/logstash/
docker cp logstash:/usr/share/logstash/pipeline ~/docker/logstash/

修改配置文件 logstash.ymls 的内容

1
2
3
4
http.host: "0.0.0.0"
xpack.monitoring.elasticsearch.hosts: [ "http://127.0.0.1:9200" ]
#path.config: /usr/share/logstash/config/conf.d/*.conf
path.logs: /usr/share/logstash/logs

文件夹赋权

chmod -R 777 ~/docker/logstash/

删除之前的容器,重新创建新的容器,重新启动

1
2
3
4
5
6
7
8
9
10
docker run \
--name logstash \
--restart=always \
-p 5044:5044 \
-p 9600:9600 \
-e ES_JAVA_OPTS="-Duser.timezone=Asia/Shanghai" \
-v /Users/xbcui/docker/logstash/config:/usr/share/logstash/config \
-v /Users/xbcui/docker/logstash/data:/usr/share/logstash/data -v /Users/xbcui/docker/logstash/pipeline:/usr/share/logstash/pipeline \
-v /Users/xbcui/docker/logstash/pipeline/mysql-connector-java-8.0.22.jar://usr/share/logstash/config/jars/mysql-connector-java-8.0.22.jar \
-d logstash:7.6.0

参数说明:

docker run
–name logstash\ 将容器命名为 logstash
–restart=always \ 容器自动重启
-p 5044:5044 \ 将容器的5044端口映射到宿主机5044端口 logstash的启动端口
-p 9600:9600 \ 将容器的9600端口映射到宿主机9600 端口,api端口
-e ES_JAVA_OPTS=”-Duser.timezone=Asia/Shanghai” \ 设置时区
-v /Users/xbcui/docker/logstash/data:/usr/share/logstash/data -v /Users/xbcui/docker/logstash/pipeline:/usr/share/logstash/pipeline
-v /Users/xbcui/docker/logstash/pipeline/mysql-connector-java-8.0.22.jar://usr/share/logstash/config/jars/mysql-connector-java-8.0.22.jar \ 挂载
-d logstash:7.6.0 后台运行容器,并返回容器ID

查看logstash新的容器是否启动成功

docker ps
docker logs -f --tail 200 logstash

mysql同步配置

先进入容器

docker exec -it logstash bash

查看是否装有 logstash-input-jdbc 插件

./bin/logstash-plugin list --verbose

没有的话就安装,其实插件茫茫多

./bin/logstash-plugin install logstash-input-jdbc

退出容器

exit

在文件目录 ~/docker/logstash/config/conf.d下创建 jdbc.conf 文件
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
input{
jdbc{
# 连接数据库
jdbc_connection_string => "jdbc:mysql://192.168.99.205:3306/database"
jdbc_user => "user"
jdbc_password => "pass"
# 连接数据库的驱动包 需要手动导入到docker服务中
jdbc_driver_library => "/usr/share/logstash/pipeline/mysql-connector-java-8.0.22.jar"
jdbc_driver_class => "com.mysql.cj.jdbc.Driver"
jdbc_paging_enabled => "true"
jdbc_page_size => "50000"
codec => plain { charset => "UTF-8" }

# 数据追踪
# 追踪的字段
tracking_column => "update_time"
# 上次追踪的元数据存放位置
last_run_metadata_path => "/usr/share/logstash/config/lastrun/logstash_jdbc_last_run"
# 设置时区
jdbc_default_timezone => "Asia/Shanghai"
# sql 文件地址
# statement_filepath => ""
# sql
statement => "select * from order_info"
# 是否清除 last_run_metadata_path 的记录,如果为真那么每次都相当于从头开始查询所有的数据库记录
clean_run =>false
# 这是控制定时的,重复执行导入任务的时间间隔,第一位是分钟 不设置就是1分钟执行一次
schedule => "* */1 * * *"
}
}
filter {
date {
match => ["order_time", "yyyy-MM-dd HH:mm:ss"]
}
}

output{
elasticsearch{
# 要导入到的Elasticsearch所在的主机
hosts => "192.168.98.169:9200"
# 要导入到的Elasticsearch的索引的名称
index => "jinwei_order_2360s_index"
# 类型名称(类似数据库表名)
document_type => "_doc"
# 主键名称(类似数据库表名)
document_id => "%{id}"
}

stdout{
# JSON 格式输出
codec => json_lines
}
}

将logstash.yml 中 path.config: /usr/share/logstash/config/conf.d/*.conf这一行的注释放开

附:向Docker服务下载/上传文件

下载文件
docker cp logstash:/usr/share/logstash/data ~/docker/logstash/
上传文件
docker cp 本地文件路径 ID全称:容器路径
docker cp /Users/xbcui//elasticsearch-phone-.zip 609e6107f1210f20e8689e985bb7265d7ac0a453bb23eee36aed0c9c852801ea:usr/share/elasticsearch/plugins/elasticsearch-phone.zip

RocketMQ 发送消息偶尔会无法消费

问题背景

公司全款支付业务上线在即,测试组同学反馈,测试环境偶现某些操作无效的问题。经排查是系统中使用的MQ出现了问题。
消息队列是在老的生产环境就已经接入的,之前消息的传递是没有问题的。
以上是主要背景

排查过程

  1. 经过初步排查是消息队列中的消息没有被消费导致的。也就是生产者已经将消息发送到队列中,但是消费者没有消费到。

  2. 从消息队列控制台查看这个topic和消费组的一些消息,有几个消费组的消息是积压状态(最可以从topic - consumer管理 - 弹出界面中consumer offset 后的差值,如果不为0 说明消息积压)。这里有个点,消费挤压的消费者的消费者终端都是空的,此处先记下。

  3. 接下来因为认识不足的问题,问题解决方向侧重点放在了检查消费端的堵塞上,此处问题没有得到很好的解决,但是发现不少比较不错的文档,记录在文末。

  4. 在控制台具体查看消息的一些生命情况,通过Topic 和 时间过滤 找到最近产生的几条消息数据,打开 message detail,最下面会有关于消息的 trackType 和一些操作。观察到消费失败的消息的trackType的属性是 NOT_ONLINE, 即消费者没有运行。
    这里展开说下 trackType 的类型和代表的含义。

    NOT_ONLINE 代表该Consumer没有运行
    CONSUMED 代表该消息已经被消费
    NOT_CONSUME_YET 还没被消费
    UNKNOW_EXCEPTION 报错
    CONSUMED_BUT_FILTERED 消费了,但是被过滤了,一般是被tag过滤了

  5. 在尝试联系运维同学重启了MQ之后,从消息队列的整个的配置上闲逛的时候,发现消费者的客户端的版本号存在多个,联系最近业务系统的测试和上线动作,新的测试环境部署了 流程引擎服务 的消费组名称和当前的同名,当时鉴于topic不一样,修改后未及时进行验证。

  6. 马上修改对应的服务的消费组名称, 问题解决。 不同的Topic的消费组名称也会冲突

结论

消息队列使用时命名规范问题一定要注意,不同的项目的Topic、消费组名称和tag关系需要梳理。

附录:

另附网上关于mq消息积压的案例有很多,但场景都不是很一样,从中找到排查步骤和工具不错的方案如下:

  1. RocketMQ消息积压问题
  2. 消息积压判断及解决

附录链接文字备份

Part1:

  1. 可通过 jps -m 或者 ps -ef | grep java 命令获取当前正在运行的 Java 程序,通过启动主类即可获得应用的进程 id,然后可以通过 jstack pid > j.log 命令获取线程的堆栈,在这里我建议大家连续运行 5 次该命令,分别获取 5 个线程堆栈文件,主要用于对比线程的状态是否在向前推进。
  2. 通过 jstack 获取堆栈信息后,重点搜索 线程状态。

Part2:

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
一. 定位问题
1. Console入口
主题-->Topic-->Consumer管理-->订阅组

2. 延迟数量(Delay)
消息积压数量,即当前Topic还剩下多少消息未处理,该值越大,表示积压的消息越多

3. 最后消费时间(LastConsumeTime)
当前Topic消息最后被消费的时间,该值表示消费端有多长时间未拉取消息进行消费

二. 分析问题
1. 查看rocketmq_client.log日志
grep "do flow control" rocketmq_client.log

如果出现 so do flow control 这样的日志,说明触发了消费限流,原因是:消费端积压了消息,即消费端无法消费已拉取的消息,消费端在没有将消息处理完成前,不会再向服务端拉取消息,并打印日志。

2. 消费端业务逻辑
1. 执行了长耗时的逻辑,导致消息处理很慢
2. 第三方接口调用很慢或超时

三. 解决问题
1. 消费端解决
1. 增加消费端的消费线程数或增加消费者数量,提升消费能力
2. 优化代码逻辑,降低执行时间
3. 调用第三方接口时,设置较短的超时时间,避免长时间等待,快速返回错误信息并告警

Part3:

出现场景
生产者系统会负责不停的把消息写入RocketMQ里去,然后消费者系统就是负责从RocketMQ里消费消息。

系统在生产环境是有高峰和低谷的,在晚上几个小时的高峰期内,大概就会有100多万条消息进入RocketMQ。然后消费者系统从RocketMQ里获取到消息之后,会依赖一些Redis去进行一些业务逻辑的实现。

有一天晚上就出现了一个问题,消费者系统依赖的Redis就挂掉了,导致消费者系统自己也没法运作了,此时就没法继续从RocketMQ里消费数据和处理了,消费者系统几乎就处于停滞不动的状态。然后生产者系统在晚上几个小时的高峰期内,就往MQ里写入了100多万的消息,此时都积压在MQ里了,根本没有系统消费和处理。

解决方案
一般来说有以下几种方案:

全部丢弃:如果这些消息允许丢失,那么此时可以紧急修改消费者系统的代码,在代码里对所有的消息都获取到就直接丢弃,不做任何的处理,这样可以迅速的让积压在MQ里的百万消息被处理掉,只不过处理方式就是全部丢弃而已。
等待Redis恢复:往往对很多系统而言,不能简单粗暴的丢弃这些消息,所以最常见的办法,还是先等待消费者系统底层依赖的Redis先恢复,恢复之后,就可以根据线上Topic的MessageQueue的数量来看看如何后续处理。
临时扩容消费者系统,增加机器来加快消费速度,但要考虑依赖的Redis也要能抗住压力;
临时扩容消费者系统
假如Topic有20个MessageQueue,然后只有4个消费者系统在消费,那么每个消费者系统会从5个MessageQueue里获取消息,所以此时如果你仅仅依靠4个消费者系统是肯定不够的,毕竟MQ里积压了百万消息了。

此时可以临时申请16台机器多部署16个消费者系统的实例,然后20个消费者系统同时消费,每个消费者消费一个MessageQueue的消息,此时会发现消费的速度提高了5倍,很快积压的百万消息都会被处理完毕。
但是这里同时要考虑到Redis必须要能抗住临时增加了5倍的读写压力,因为原来就4个消费者系统在读写Redis,现在临时变成了20个消费者系统了。

当你处理完百万积压的消息之后,就可以下线多余的16台机器了。

那么如果Topic总共就只有4个MessageQueue,只有4个消费者系统呢?
这个时候就没办法扩容消费者系统了,因为再多的消费者系统,还是只有4个MessageQueue,没法并行消费。

所以此时往往是临时修改那4个消费者系统的代码,让他们获取到消息然后不写入Redis,而是直接把消息写入一个新的Topic,这个速度是很快的,因为仅仅是读写MQ而已。

然后新的Topic有20个MessageQueue,然后再部署20台临时增加的消费者系统,去消费新的Topic后写入数据到NoSQL里去,这样子也可以迅速的增加消费者系统的并行处理能力,使用一个新的Topic来允许更多的消费者系统并行处理。

写在Java内存模型前面

JAVA依旧是当前比较受欢迎的面向对象的开发语言,Java 编程语言是个简单、面向对象、分布式、解释性、健壮、安全与系统无关、可移植、高性能、多线程和静态的语言。
作为程序员摊开来说Java,说起Java一般接下来会有几个定义:JDK、JRE、JVM。
JDK(Java Development kit)是针对开发的产品,是整个Java的核心,它包括了Java运行环境JRE、Java工具和Java基础类库。
JRE(Java Runtime Environment)是运行Java程序所必需的环境的集合,它包括JVM标准实现和Java核心类库。
JVM(Java virtual Machine)Java虚拟机,是整个Java实现跨平台的最核心的产品,能够与性Java写作的软件程序。所有的Java程序会被编译成.class的类文件,这种类文件可以在虚拟机上执行,也就是说,class并不直接操作机器的操作系统,而是经过虚拟机间接的与操作系统交互,有虚拟机将程序解释给本地系统执行。

关于JMM

JMM(Java Memory Model),Java内存模型,其实是一套内存模型规范,屏蔽了各种硬件和操作系统访问差异,保证Java程序在各种平台下对内存的访问的效果都一致。
Java程序的内存的分配是在JVM虚拟机内存分配机制下完成的。
Java内存共分为五部分: 虚拟机栈、 堆、 方法区、 本地方法栈、程序计数器。

程序计数器

这一部分是当前线程锁定执行字节码的行号指示器,是一块较小的内存空间。程序计数器是线程私有的。Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的。
所以在任一确定时刻,一个处理器只会执行一条线程的指令,因此,为了线程切换后能恢复到正确的执行位置,每条线程都是需要一个独立的程序计数器,各条线程之间互相不影响,独立存储。
如果说线程正在执行的是Java方法,计数器记录的就是正在执行的虚拟机字节码指令的地址,如果执行的是Native方法,这个计数器值就是空(Undefined)。这块区域也是为一个在Java虚拟机规范中没有规定任何内存溢出情况的区域。

虚拟机栈

这一部分也是线程私有的,每个方法在执行的时候会创建一个栈桢。每个栈桢存储局部变表、操作数、动态链接、方法返回地址等。每一个方法的调用到执行完毕,对应一个栈桢在虚拟机栈中的如栈和出栈。局部变量表存放了编译器克可知的各种基本数据类型、 对象引用和returnAddress类型(一条指向字节码指令的地址),局部变量表所需的内存空间在编译期间就分配完成,当进入一个方法时,该方法需要的在桢中分配多大的局部变量空间时完全确定的,在运行期间不会改变局部变量表的大小。

本地方法栈

与虚拟机栈的作用和原理相似,区别是虚拟机栈是为执行Java方法服务,本地方法栈是为执行本地方法服务的。

Java中的堆是用来存储对象本身的以及数组(数组引用是存放在Java栈中的),堆是被所有线程共享的,在JVM中只有一个堆。Java堆是垃圾收集器管理的主要区域,因为也被称为GC堆。

方法区

方法区中存储了已经被虚拟机加载的类的信息(包括类的名称、方法信息、属性字段信息)、静态变量、常量以及编译器编译后的代码等。与堆一样,方法区也是被线程共享的区域。Java虚拟机规范堆这个区域的限制非常宽松。除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾回收。运行时常量池时方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息时常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

写在前面

Junit5已经出来,但因为习惯问题此处还是主要语句Junit4的主要用法。

主要内容

  • 测试套件
  • 忽略测试方法
  • 代码运行时间测试
  • 定向异常测试

测试套件

测试套件意味着捆绑几个单元测试用例并且一起执行他们。JUnit 中,使用@RunWith 和 @Suite 注释来运行套件测试。(被@RunWith 和 @Suite注释的测试类不需要任何方法,SuiteClasse中写上要一起执行的测试用例类)

1
2
3
4
5
6
7
8
9
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
@RunWith(Suite.class)
@Suite.SuiteClasses({
TestJunit1.class,
TestJunit2.class
})
public class JunitTestSuite {
}

忽略测试

有时因为各种原因(比如上面套件中,我们类中有并不想执行的用例)我们不想执行我们的测试用例。这是需要忽略掉这个测试用例。@Ignore 注释会在这种情况时帮助我们。

  • 一个含有 @Ignore 注释的测试方法将不会被执行。
  • 如果一个测试类有 @Ignore 注释,则它的所有的测试方法将不会执行。

运行时间测试

如果想验证一个测试用例是否比起指定的毫秒数花费了更多的时间。可以在@Test 注释中使用timeout 参数。
@Test(timeout=1000)

定向异常测试

你可以测试代码是否它抛出了想要得到的异常。可以在@Test 注释中使用expected 参数。例如@Test(expected = ArithmeticException.class) 就可以用来测试被注释的方法抛出的异常是不是ArithmeticException。

写在前面

Junit5已经出来,但因为习惯问题此处还是主要语句Junit4的主要用法。

Junit应用

Junit 断言

JUnit的编写,此处就不再举例说明了,如果不太清楚可以随便查一下。我们从断言开始入手。
所有的断言都包含在 Assert 类中。
public class Assert extends java.lang.Object
这个类提供了很多有用的断言方法来编写测试用例。只有失败的断言才会被记录。Assert 类中的一些有用的方法列式如下:

方法 描述
void assertEquals(boolean expected, boolean actual) 检查两个变量或者等式是否平衡
void assertTrue(boolean expected, boolean actual) 检查条件为真
void assertFalse(boolean expected) 检查条件为假
void assertNotNull(Object object) 检查对象不为空
void assertNull(Object object) 检查对象为空
void assertSame(boolean condition) 检查两个相关对象是否指向同一个对象
void assertNotSame(boolean condition) 检查两个相关对象是否指向不同对象
void assertArrayEquals(expectedArray, resultArray) 检查两个数组是否相等

Junit 执行过程

让我们通过一个例子来更好的理解Junit中的的方法的执行过程。首先创建一个java文件,命名为LearnJunit.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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;

public class LearnJunit {

//execute only once, in the starting
@BeforeClass
public static void beforeClass() {
System.out.println("this is before class method");
}

//execute only once, in the end
@AfterClass
public static void afterClass() {
System.out.println("this is after class method");
}

//execute for each test, before executing test
@Before
public void before() {
System.out.println("this is before");
}

//execute for each test, after executing test
@After
public void after() {
System.out.println("this is after");
}

//test case 1
@Test
public void testCase1() {
System.out.println("this is the test case 1");
}

//test case 2
@Test
public void testCase2() {
System.out.println("this is the test case 2");
}
}

之后在同一文件目录下创建一个java文件用来运行测试文件,命名为TestRunner.java。

1
2
3
4
5
6
7
8
9
10
11
12
13
import org.junit.runner.JUnitCore;
import org.junit.runner.Result;
import org.junit.runner.notification.Failure;

public class TestRunner {
public static void main(String[] args) {
Result result = JUnitCore.runClasses(LearnJunit.class);
for (Failure failure : result.getFailures()) {
System.out.println(failure.toString());
}
System.out.println(result.wasSuccessful());
}
}

编译运行的结果是:

1
2
3
4
5
6
7
8
9
this is before class method
this is before
this is the test case 1
this is after
this is before
this is the test case 2
this is after
this is after class method
true

最后的trueresult.wasSuccessful()的结果,Result result = JUnitCore.runClasses(LearnJunit.class);是执行命令。

写在最前面

作为一名Java开发来说,平时说到单元测试就会想到Junit框架。一直都是简单的会用一下,详细的说起来到说不出什么,故此专门查漏一番。

Junit介绍

JUnit 是一个 Java 编程语言的单元测试框架。是起源于 JUnit 的一个统称为 xUnit 的单元测试框架之一。(xUnit是一系列测试框架的统称,最开始来源于一个叫做Smalltalk的SUnit框架。现在各种面向对象语言,Java、Python语等的类、对象等定义就是来源于Smalltalk语言,后来这些语言都借助了Sunit框架的理念,有很多通用的规范和特征,也就统称为xUnit。)
JUnit 促进了“先测试后编码”的理念,即可以先开发测试数据的一段代码,可以先测试,然后再应用。(but,一般我们都习惯了直接开发,即便是单元测试也是在开发完之后的工作了。😓)

Junit特点

  1. 是一个开放的资源框架,用于编写和运行测试。
  2. 提供注释来识别测试方法。
  3. 提供断言来测试预期结果。
  4. 提供测试运行来运行测试。
  5. 优雅简洁;测试可以自动运行并且检查自身结果并提供即时反馈。
  6. 测试可以被组织为测试套件,包含测试用例,甚至其他的测试套件。

单元测试用例

既然Junit是Java编程语言的单元测试框架。那什么是单元测试呢?
廖雪峰给的解释是,单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。

单元测试要求

一个正式的编写好的单元测试用例的特点是:已知输入和预期输出,即在测试执行前就已知。已知输入需要测试的先决条件,预期输出需要测试后置条件。
每一项需求至少需要两个单元测试用例:一个正检验,一个负检验。如果一个需求有子需求,每一个子需求必须至少有正检验和负检验两个测试用例。

Junit测试框架

Junit是一个回归测试框架。具有以下重要特性:测试工具、测试套件、测试运行器、测试分类

测试工具

测试工具是一整套固定的工具,用于基线测试。测试工具的目的是为了确保测试能够在共享且固定的环境中运行,因此保证测试结果的可重复性。它包括:
在所有测试调用指令发起前的 setUp() 方法。
在测试方法运行后的 tearDown() 方法。

测试套件

测试套件意味捆绑几个测试案例并且同时运行。在 JUnit 中,@RunWith 和 @Suite 都被用作运行测试套件。

测试运行器

测试运行器 用于执行测试案例。

测试分类

测试分类是在编写和测试 JUnit 的重要分类。

  1. 包含一套断言方法的测试断言
  2. 包含规定运行多重测试工具的测试用例
  3. 包含收集执行测试用例结果的方法的测试结果

Hexo是什么:

Hexo is a fast, simple and powerful blog framework. You write posts in Markdown (or other markup languages) and Hexo generates static files with a beautiful theme in seconds.
简单而言呢就是 Hexo是一个框架,这个框架能把我们基于markdown编写的内容,帮我们生成静态文件并上传到指定服务器,而且它有快捷、简单、有效的特点。

Hexo 安装

因为 Hexo 是基于 node 框架的,所以我们需要首先安装node.js。接着执行命令安装Hexo
npm install hexo-cli -g

Hexo 初始化

  • 在本地新建文件夹blog
  • 在文件夹下执行命令 hexo init
  • 成功之后执行命令 hexo server 启动本地服务器, 访问http://localhost:4000即可,显示的内容是即时渲染的。

Hexo命令

这是本篇的重点,如果搭建博客过程中使用hexo有别的问题,可以查找专门搭建博客的帖子。

  • hexo init [folder]
    hexo init 初始化本地文件夹为网站的根目录。 [folder] 可选参数 指定初始化目录的路径,若无指定则默认为当前目录

  • hexo clean //清除静态文件,如果发现部署站点修改的内容不生效,可以试试

  • hexo generate (hexo g) -d // 生成静态文件 -d 可选 生成后部署 等价于 hexo d -g

  • hexo server (hexo s) // 服务器运行

  • hexo deploy (hexo d) // 部署 -g 可选 表示生成后部署 等价于 hexo d -g