0%

突然发现公司订单小哥的消息列队是用redis做的,而且用的是list。这里自己也实现下简单的阻塞列队。用于处理延时消息的问题。

  • 延时列队就是一种带有延迟功能的消息列队。通常具备消息存储、过期消息的实时获取、高可用。消费熔断。
  • 业务场景
    • 订单未支付超时。
    • 订单发货提醒。
    • 短信提醒。
    • 自动收货。
    • 自动评论。
    • 自动取消订单,不发货的情况下。
  • 常用的解决方案
    • 定时轮询任务,比如jdk中TimerThread轮询数据库表、缓存中的数据。频繁的轮询容易出现过度资源消耗。对数据和缓存也有一定的影响。但是可以作为辅助手段,通常用于补偿或者初始化数据。
    • ScheduledExecutorService 周期性线程池
    • 时间轮(kafka、Netty的HashedWheelTimer)
    • 使用mysql通常是定时扫描表,找出符合条件的数据进行处理。消费成功则更新数据。
    • 使用redis可以使用zset,通过分值进行排序。定时轮询的方式去获取符合条件的记录。消费数据后删除消息。失败则重新进入队列。
    • Java中java.util.concurrent.DelayQueue
      • Jdk实现,列队处于jvm中,不支持分布式和消息持久化。
    • Rocketmp延时列队
      • 消息持久、重试、分布式等等特性。
      • 不支持任意时间精度的,支持level级别的延时消息。

DelayQueue

  • Leader-follower 模式

阅读全文 »

本章是整理知识内容,为强化知识长期更新。

练手项目

牛客网

准备一个mysql 5.7的数据库

配置本地数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
➜  employees_db tree 
.
├── Changelog
├── README
├── employees.sql
├── employees_partitioned.sql
├── employees_partitioned2.sql
├── employees_partitioned3.sql
├── load_departments.dump
├── load_dept_emp.dump
├── load_dept_manager.dump
├── load_employees.dump
├── load_salaries.dump
├── load_titles.dump
├── objects.sql
├── test_employees_md5.sql
└── test_employees_sha.sql
  • 在将文件导入数据库
1
mysql -uroot -p -t < employees.sql

解决问题

  • employees.sql 修改内容
1
2
3
4
# 38set storage_engine = InnoDB; 替换
set default_storage_engine = InnoDB;
# 44select CONCAT('storage engine: ', @@storage_engine) as INFO; 替换
select CONCAT('storage engine: ', @@default_storage_engine) as INFO;
  • 注意导入的时候会出现文件打不开的问题
1
2
3
4
5
6
7
8
9
10
11
12
SELECT 'LOADING departments' as 'INFO';
source /home/employees_db/load_departments.dump ;
SELECT 'LOADING employees' as 'INFO';
source /home/employees_db/load_employees.dump ;
SELECT 'LOADING dept_emp' as 'INFO';
source /home/employees_db/load_dept_emp.dump ;
SELECT 'LOADING dept_manager' as 'INFO';
source /home/employees_db/load_dept_manager.dump ;
SELECT 'LOADING titles' as 'INFO';
source /home/employees_db/load_titles.dump ;
SELECT 'LOADING salaries' as 'INFO';
source /home/employees_db/load_salaries.dump ;
  • 这里要将文件正确的路径写进去。

  • 如果是本地的docker环境部署mysql,可以使用docker cp 命令将文件复制到容器中。在登陆到容器执行导入脚本

1
2
3
4
5
6
// 从宿主机器拷贝到容器中
docker cp ~/Downloads/employees_db laughing_black:/home
// laughing_black 是容器的名称

// 从容器中拷贝会宿主机器中
docker cp laughing_black:/employees_db.sql .

Snowflake

Snowflake,雪花算法是由Twitter开源的分布式ID生成算法,以划分命名空间的方式将 64-bit位分割成多个部分,每个部分代表不同的含义。这种就是将64位划分为不同的段,每段代表不同的涵义,基本就是时间戳、机器ID和序列数,当然这种方案就是需要考虑时钟回拨的问题以及做一些 buffer的缓冲设计提高性能。

背景

Twitter 早期用 MySQL 存储数据,随着用户的增长,单一的 MySQL 实例没法承受海量的数据,开发团队就开始用 Cassandra 和 sharded MySQL 替代原有的系统。然而和 MySQL 不同的是,Cassandra 没有内置为每一条数据生成唯一 ID 的功能,因为在一个分布式环境下,很难有完美的 ID 生成方案。

  • 对于 Twitter 而言,这样的 ID 生成方案要满足两个基本的要求,一是每秒能生成几十万条 ID 用于标识不同的 tweet;二是这些 ID 应该可以有个大致的顺序,也就是说发布时间相近的两条 tweet,它们的 ID 也应当相近,这样才能方便各种客户端对 tweet 进行排序。
  • 第一个要求意味着 ID 生成要以一种非协作的(uncoordinated)的方式进行,例如不能有一个全局的原子变量。
  • 第二个要求使得 tweet 按 ID 排序后满足 k-sorted 条件。如果序列 A 要满足 k-sorted,当且仅当对于任意的 p, q,如果 1 <= p <= q - k (1 <= p <= q <= n),则有 A[p] <= A[q]。换句话说,如果元素 p 排在 q 前面,且相差至少 k 个位置,那么 p 必然小于或等于 q。如果 tweet 序列满足这个条件,要获取第 r 条 tweet 之后的消息,只要从第 r - k 条开始查找即可。
  • Twitter 解决这两个问题的方案非常简单高效:每一个 ID 都是 64 位数字,由时间戳、节点号和序列编号组成。其中序列编号是每个节点本地生成的序号,而节点号则由 ZooKeeper 维护。

  • 官网给出的代码是scala写出来的代码
  • 1bit,不用,因为二进制中最高位是符号位,1表示负数,0表示正数。生成的id一般都是用整数,所以最高位固定为0。
  • 41bit-时间戳,用来记录时间戳,毫秒级。
    • 41位可以表示2^41-1个数字,
      - 如果只用来表示正整数(计算机中正数包含0),可以表示的数值范围是:0 至 2^41-1,减1是因为可表示的数值范围是从0开始算的,而不是1。
      - 也就是说41位可以表示2^41-1个毫秒的值,转化成单位年则是(2^{41}-1) / (1000 * 60 * 60 * 24 *365) = 69年
  • 10bit-工作机器id,用来记录工作机器id。
    • 可以部署在2^{10} = 1024个节点,包括5位datacenterId和5位workerId
    • 5位(bit)可以表示的最大正整数是2^{5}-1 = 31,即可以用0、1、2、3、….31这32个数字,来表示不同的datecenterId或workerId
  • 12bit-序列号,序列号,用来记录同毫秒内产生的不同id。
    - 12位(bit)可以表示的最大正整数是2^{12}-1 = 4095,即可以用0、1、2、3、….4094这4095个数字,来表示同一机器同一时间截(毫秒)内产生的4095个ID序号。
阅读全文 »

源码地址

Geo

Redis 在 3.2 版本中增加了 GEO 类型用于存储和查询地理位置。

  • GEOADD:添加地理位置

  • GEOPOS:查询位置信息

  • GEODIST:距离统计

  • GEORADIUS:以给定的经纬度为中心, 找出某一半径内的元素

  • GEORADIUSBYMEMBER 找出位于指定范围内的元素,中心点是由给定的位置元素决定

  • GEOHASH:返回一个或多个位置元素HASH

  • 关于删除,使用集合函数中的zrem,geo本质存在一个集合中。

演示效果

1
2
3
4
5
// 1.杭州市 蒋村商务中心 120.075338,30.294845
// 2.杭州市 中节能西溪首座 120.081806,30.294907
// 3.杭州市 西溪蝶园 120.070452,30.294221
// 4.杭州市 西溪财富中心 120.077854,30.296342
// 5.杭州市 九橙 西溪创投中心 120.055468,30.284523
  • 使用redis-cli作为客户端测试工具 geo 集合key = hz
阅读全文 »

Tcpdump

dump the traffic on a network,根据使用者的定义对网络上的数据包进行截获的包分析工具。

安装

linux

1
2
3
4
5
6
yum install tcpdump
[root@~]# tcpdump --v
tcpdump version 4.9.2
libpcap version 1.5.3
OpenSSL 1.0.2k-fips 26 Jan 2017

Mac

1
2
3
4
5
brew install tcpdump
➜ ~ tcpdump --v
tcpdump version 4.99.1
libpcap version 1.10.1
OpenSSL 1.1.1k 25 Mar 2021
阅读全文 »

SPI 全称为 Service Provider Interface,是一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。

  • 本文对比JDK、Spring对SPI机制的实现。

  • 在实际的应用场景。

    • jdbc驱动,不同的数据有不同的驱动。

    com.mysql.cj.jdbc.Driver

    • spring 也使用spi机制,用于扩展实现。

    org.springframework.beans.BeanInfoFactory=org.springframework.beans.ExtendedBeanInfoFactory

    • dubbo也通过spi机制实现自定义扩展。
    • 日志门面接口实现类加载,SLF4J加载不同提供商的日志是现类。

SPI

演示代码

JDK 中 提供了一个 SPI 的功能,核心类是 java.util.ServiceLoader。其作用就是,可以通过类名获取在”META-INF/services/“下的多个配置实现文件。

ServiceLoader

  • META-INF/services下的文件(以Interface全路径命名)中添加具体实现类的全路径名。
  • 使用程序使用ServiceLoader动态加载实现类(根据目录META-INF/services下的配置文件找到实现类的全限定名并调用classloader来加载实现类到JVM)
  • SPI的实现类必须具有无参数的构造方法

演示代码

阅读全文 »

Zookeeper概述

Zookeeper可以让企业的IT架构逐步从集中式向分布式过度,所谓的分布式是指:把一个计算任务分解成若干个计算单元,并且分派到若干不同的计算机中去执行,然后汇总计算结果的过程。

  • Zookeeper介绍

    • Zookeeper是源代码开放的分布式协调服务,由雅虎创建,是Google Chubby开源实现。Zookeeper是一个高性能的分布式数据一致性解决方案,它将那些复杂、容易出错的分布式一致性服务封装起来,构成一个搞笑可靠的原语集,并提供一系列简单易用的接口给用户使用。
  • Zookeeper的典型应用场景

    • 数据发布/订阅 顾名思义就是一方把数据发布出来,另一方通过某种手动可以得到这些数据。
      • 通常数据订阅有两种方式:推模式和拉模式,推模式一般是服务器主动向客户端推送消息,拉模式是客户端主动去服务端获取数据(通常采用的是轮询的方式)。
      • Zookeeper采用两种方式的结合。
        • 发布者将数据发布到Zookeeper集群节点上,订阅者通过一定的方法告诉服务器,我对那个节点的数据感兴趣,那个服务器在这些节点的数据发送变化时,就通知客户端,客户端得到通知后可以去服务器获取数据信息。
        • 分布式协调/通知
          • 心跳检测:在分布式系统中,通常需要机器是否可以用,Zookeeper中我们让所有的机器都注册一个临时节点,所以只需要判断这个节点是否存在就可以了,不需要直接去连接需要检查的机器,降低系统的负载度(节点分为临时和持久)。
  • Zookpeeper重量级使用

    • Hadoop、HBase、Storm、Solr。
  • 集群角色

    • Leader、Follower、Observer
      • Leader服务器是整个Zookeeper集群工作机制的核心
      • Follower服务器是Zookeeper集群状态的跟随者
      • Oserver服务器充当一个观察者的角色
      • Leader、Follower设计模式,Observer观察者模式
  • 会话

    • 会话是指客户端Zookeeper服务器的连接,Zookeeper中的会话叫Session,客户端与服务器建立TCP的长连接来维持一个Session,客户端在启动的时候首先会与服务器建立一个TCP连接,通过这个连接客户端能够通过心跳检测与服务器保持有效的会话,也能向Zookeeper服务器发送请求并获得响应。
  • 数据节点

    • Zookeeper中的节点有两类
      • 集群中的一台机器称为一个节点
      • 数据模型中的数据单元Znode,分别为持久节点和临时节点。(其实数据节点就是一个tree节点就是Znode)
  • 版本

    • Zookeeper中的版本
      • version
        • 当前数据节点数据内容版本
      • cversion
        • 当前数据节点子节点的版本号
      • aversion
        • 当前数据节点ACL变更版本号
  • watcher(事件监听器)

    • Zookeeper允许用户在指定节点上注册一些Watcher,当数据节点发生变化的时候,Zookeeper服务器会把这个变化通知发送给感兴趣的客户端。
  • ACL权限控制

    • ACL是Access Contril Lists 的缩写,Zookeeper采用ACL策略来进行权限控制,有以下权限:
      • CREATE:创建子节点
      • READ:获取子节点
      • WRITE:更新子节点数据权限
      • DELETE:删除子节点权限
      • ADMIN:设置节点ACL权限

Zoopeeper环境搭建(集群、单机、伪集群)

  • 单机模式(设备环境有限暂时单机)
    • 准备工作
      • 下载Zookeeper(此处自行处理)
      • 解压 tar xzvf xxx.gz 解压
      • 重命名文件夹 Zookeeper 命令mv xxx xxx 后面的参数是新名字(可以不做)
      • 进入文件夹中/conf/
      • 复制配置文件zoo_sample.cfg(样例文件) 并重命名 zoo.cfg
    • 编辑zoo.cfg内容(仅供参考,具体环境自行修改)
    • tickTime = 2000
      • tickTime:基本事件单元,以毫秒为单位。这个时间是作为 Zookeeper 服务器之间或客户端与服务器之间维持心跳的时间间隔,也就是每个 tickTime 时间就会发送一个心跳。
    • initLimit= 5
      • initLimit:这个配置项用来配置Zookeeper接受客户端初始化连接时最长能忍受多少个心跳时间间隔数,当已超过5个心跳的时间(也就是tickTime)长度后Zookeeper服务器还没有收到客户端的返回信息,那么表明这个客户端连接失败。总的时间长度是5 * 2000=4s。
    • dataDir = D:\zookeeper\data
      • 顾名思义就是 Zookeeper 保存数据快照的目录,默认情况下,Zookeeper 将写数据的日志文件也保存在这个目录里)。
    • dataLogDir= D:\zookeeper\log
      • 顾名思义就是 Zookeeper 保存日志的目录。
    • synclimit = 5
      • 这个配置项表示Leader与Follower之间发送消息,请求和应答时间长长度,最长不能超过多少个tickTime的时间长度,总的时间长度是2 * 2000 = 4s。
    • clientPort = 2181
      • 这个端口就是客户端连接 Zookeeper 服务器的端口,Zookeeper 会监听这个端口,接受客户端的访问请求。
    • Server(待续)
      • 格式:server.id=host:port:port(两个port保证可以正常使用就行)
        • id:通常为整数,并且不能重复使用整数。
        • host:服务器的IP地址。
        • port: Follower端口
        • port: Leader选举投票。
      • ZooKeeper建议使用hostname,而非ip。这需要对主机的/etc/hostname和/etc/hosts做host绑定(不用的OS不同修改方式)。
      • 创建一个myid文件(放在 dataDir文件下面)
      • 写入一行数据(请查阅zoo.cfg文件)
        • 写入id位置的数据即可。表示当前系统环境Zookeeper是哪一个Server(通讯用的)。
    • 启动服务与停止服务
    • 进入bin/文件
    • 执行zkServer.cmd 或则 zkServer.sh
    • CMD直接双击运行 ,SH则 sudo sh ./zkServer.sh start 启动 stop 关闭
    • 验证 使用telnet来测试(自行安装)
      • telnet ip port 敲命令 stat 若返回数据表示当前服务器不能对外提供服务表明集群下其他服务器未启动(在Zookeeper中只要有半数的服务器正常工作就可以向外提供服务)。
    • 在此简单说明下何为伪集群就是在一台服务器上的多个Zookeeper的集群叫伪集群(伪集群 两个port不能与其他zookeeper的port一样)。
    • 单机模式就删除其他服务器运行的时候就是单机模式。

END

尝试在spring test中使用基准测试。

演示代码

  • JMH 是一个用于在 JVM 上编写基准测试的 Java 工具库,它是作为 OpenJDK 项目的一部分开发的。

  • 在与spring test集成过程中,只需要成功加载spring即可。

Console

  • 当jmh先启动,在启动spring就正常使用了。
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
# JMH version: 1.21
# VM version: JDK 1.8.0_275, OpenJDK 64-Bit Server VM, 25.275-b01
# VM invoker: /Library/Java/JavaVirtualMachines/adoptopenjdk-8.jdk/Contents/Home/jre/bin/java
# VM options: -server
# Warmup: 1 iterations, 10 s each
# Measurement: 1 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: cn.z201.jmh.AppApplicationTest.environment

# Run progress: 0.00% complete, ETA 00:00:20
# Fork: N/A, test runs in the host VM
# *** WARNING: Non-forked runs may silently omit JVM options, mess up profilers, disable compiler hints, etc. ***
# *** WARNING: Use non-forked runs only for debugging purposes, not for actual performance runs. ***
# Warmup Iteration 1:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.4.5)

[cn.z201.jmh.AppApplicationTest.environment-jmh-worker-1] [StartupInfoLogger.java : 55] Starting application using Java 1.8.0_275 on z201MacBook-Pro.local with PID 23242 (started by zengqingfeng in /Users/zengqingfeng/word/code-example/SpringBoot-JMH)
[cn.z201.jmh.AppApplicationTest.environment-jmh-worker-1] [SpringApplication.java : 679] The following profiles are active: dev
[cn.z201.jmh.AppApplicationTest.environment-jmh-worker-1] [Bootstrap.java : 68] UT026010: Buffer pool was not set on WebSocketDeploymentInfo, the default pool will be used
[cn.z201.jmh.AppApplicationTest.environment-jmh-worker-1] [ServletContextImpl.java : 371] Initializing Spring embedded WebApplicationContext
[cn.z201.jmh.AppApplicationTest.environment-jmh-worker-1] [ServletWebServerApplicationContext.java : 289] Root WebApplicationContext: initialization completed in 809 ms
[cn.z201.jmh.AppApplicationTest.environment-jmh-worker-1] [ExecutorConfigurationSupport.java : 181] Initializing ExecutorService 'applicationTaskExecutor'
[cn.z201.jmh.AppApplicationTest.environment-jmh-worker-1] [Undertow.java : 120] starting server: Undertow - 2.2.7.Final
[cn.z201.jmh.AppApplicationTest.environment-jmh-worker-1] [Xnio.java : 95] XNIO version 3.8.0.Final
[cn.z201.jmh.AppApplicationTest.environment-jmh-worker-1] [NioXnio.java : 59] XNIO NIO Implementation Version 3.8.0.Final
[cn.z201.jmh.AppApplicationTest.environment-jmh-worker-1] [Version.java : 52] JBoss Threads version 3.1.0.Final
[cn.z201.jmh.AppApplicationTest.environment-jmh-worker-1] [UndertowWebServer.java : 133] Undertow started on port(s) 9031 (http)
[cn.z201.jmh.AppApplicationTest.environment-jmh-worker-1] [StartupInfoLogger.java : 61] Started application in 2.018 seconds (JVM running for 3.629)
[cn.z201.jmh.AppApplicationTest.environment-jmh-worker-1] [AppApplicationTest.java : 40] dev
阅读全文 »

介绍

Wireshark(前称Ethereal)是一个网络数据包分析软件。网络数据包分析软件的功能是截取网络数据包,并尽可能显示出最为详细的网络数据包数据。Wireshark使用WinPCAP作为接口,直接与网卡进行数据报文交换。

官方下载地址

官方文档

安装

  • 安装完成之后大概是这样,注意mac和windows上有一定安装差异。

  • 安装好chmodBPF 注意,mac系统升级之后会导致该插件无法使用。需要重新安装软件。

The capture session could not be initiated on interface ‘en0’ (You don’t have permission to capture on that device). Please check to make sure you have sufficient permissions…
大概提示这么一句话

  • 解决方法有两种
1
sudo chmod 777 /dev/bpf*
  • 或者
1
sudo chown 你的电脑用户名:admin bp*
  • 修改完成后检查
1
ls -la | grep bp
  • 如果没有生效,重启试试。

快速使用

选中一个设备比如WIFI:en0

  • 上图显示连接到我了的小米wifi。

界面介绍

需要我们注意到地方

  • Display Filter(显示过滤器), 用于过滤。
  • Packet List Pane(封包列表), 显示捕获到的封包, 有源地址和目标地址,端口号。 颜色不同,代表Packet Details Pane(封包详细信息), 显示封包中的字段。
  • Dissector Pane(16进制数据)。
  • Miscellanous(地址栏,杂项)。

记录下使用docker-compose构建管理Redis、mysql。

准备工作

  1. 创建工作目录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ mkdir -p docker docker/mysql docker/mysql/data docker/redis docker/redis/data
# 使用 tree docker 查看目录结构 yum -y install tree 安装
$ tree docker
docker
├── mysql
│   └── data
└── redis
└── data
$ touch docker/mysql/my.cnf docker/redis/redis.conf
$ tree docker
docker
├── mysql
│   ├── data
│   └── my.cnf
└── redis
├── data
└── redis.conf
  1. 准备my.conf 、redis.conf
    1. my.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
[client]
port = 3306
socket = /var/lib/mysql/data/mysql.sock
[mysqld]
# 针对5.7版本执行group by字句出错问题解决
sql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION'
# 一般配置选项
basedir = /var/lib/mysql
datadir = /var/lib/mysql/data
port = 3306
socket = /var/lib/mysql/data/mysql.sock
lc-messages-dir = /usr/share/mysql # 务必配置此项,否则执行sql出错时,只能显示错误代码而不显示具体错误消息
character-set-server=utf8
back_log = 300
max_connections = 3000
max_connect_errors = 50
table_open_cache = 4096
max_allowed_packet = 32M
#binlog_cache_size = 4M
max_heap_table_size = 128M
read_rnd_buffer_size = 16M
sort_buffer_size = 16M
join_buffer_size = 16M
thread_cache_size = 16
query_cache_size = 64M
query_cache_limit = 4M
ft_min_word_len = 8
thread_stack = 512K
transaction_isolation = REPEATABLE-READ
tmp_table_size = 64M
#log-bin=mysql-bin
long_query_time = 6
server_id=1
innodb_buffer_pool_size = 256M
innodb_thread_concurrency = 16
innodb_log_buffer_size = 16M
  1. redis.conf 可以去这里下载 官网
1
2
3
4
5
6
7
8
wget https://raw.githubusercontent.com/redis/redis/5.0/redis.conf

# 修改内容
daemonize no ## 若使用开机启动,生成pid,该项必须设置为诶yes,否则redis将不能够正常执行开机启动(systemctl start redis,执行后一直卡着,直到超时)
#bind 127.0.0.1 ##注释掉,允许所有其他ip访问,真实使用最好坐下限制,只允许某些主机访问
protected-mode no ## 允许其他机器上的客户端连接当前redis,配置文件设置该项,则开机启动处就可以去掉--protected no
requirepass root ##设置密码
# daemonize yes,他的作用是开启守护进程模式,在该模式下,redis会在后台运行,并将进程pid号写入至redis.conf选项pidfile设置的文件中,此时redis将一直运行,除非手动kill该进程。但是在后面的命令中“-d”选项也是开启daemonize,这是docker自家的方式。

编写Docker-Compose文件

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
version : '3'
services:
mysql:
image: mysql/mysql-server:5.7.18
container_name: mysql5.7.18-dev
networks:
- net-shop-db
ports:
- "3306:3306"
volumes:
- /opt/docker/mysql/my.cnf:/etc/my.cnf # 映射数据库配置文件
- /opt/docker/mysql/data:/var/lib/mysql/data # 映射数据库保存目录到宿主机,防止数据丢失
- /opt/docker/mysql/log:/var/log/mysql # 日志文件
- /etc/localtime:/etc/localtime:ro # 设置容器时区与宿主机保持一致
restart: always # 设置容器自启模式
command: [
'--character-set-server=utf8mb4',
'--collation-server=utf8mb4_unicode_ci',
'--lower_case_table_names=1',
'--default-time-zone=+8:00']
environment:
- TZ=Asia/Shanghai # 设置容器时区与宿主机保持一致
- MYSQL_ROOT_PASSWORD=root # 设置root密码
redis:
image: redis:5.0.5
container_name: redis5.0.6-dev
networks:
- net-shop-db
ports:
- "6379:6379"
volumes:
- /etc/localtime:/etc/localtime:ro # 设置容器时区与宿主机保持一致
- /opt/docker/redis/data:/data
- /opt/docker/redis/redis.conf:/etc/redis.conf
command: redis-server /etc/redis.conf # 启动redis命令
environment:
- TZ=Asia/Shanghai # 设置容器时区与宿主机保持一致
restart: always # 设置容器自启模式

networks:
net-shop-db:
driver: bridge

基本用法

注意需要提前启动docker

  1. 创建并启动容器 docker-compose -f docker-compose.yml up -d
  2. 批量停止容器并清除容器 docker-compose -f docker-compose.yml down
  3. 重新构建容器 docker-compose -f docker-compose.yml up --build -d
  4. 启动失败,可以查看容器日志信息获取帮助 docker logs 容器名词或容器ID # 既docker-compose.yml文件参数container_name指定的值

备注

  1. docker-compose 命令不存在、未找到命令。直接从github上下载,国内下载巨慢。
1
2
3
4
5
6
7
8
# 注意查看docker 版本 和 docker-compose 的版本
cd /usr/local/bin/

curl -L https://github.com/docker/compose/releases/download/1.27.4/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
#。直接用国内的镜像
curl -L https://get.daocloud.io/docker/compose/releases/download/1.27.4/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose

chmod +x /usr/local/bin/docker-compose

参考

docker-compose 命令不存在、未找到命令

使用 docker-compose 在 Docker 中启动有密码的 Redis 容器

Docker-Compose搭建mysql、redis、zookeeper、rabbitmq、consul、elasticsearch环境

Docker Compose多容器部署

停止、删除所有的docker容器和镜像

宿主机连接docker中的mysql

Docker Compose 安装 on centos7

Docker-Compose搭建mysql、redis、zookeeper、rabbitmq、consul、elasticsearch环境

docker-compose一键安装redis+mysql+rabbitmq

centos7部署docker-ce 和 docker-compose

准备工作

  • 准备一台ESC服务器,并可以使用控制台登录。

  • 检查ESC是否安装了docker,可以选择是否卸载重装。

    • 检查docker是否安装,docker 显示相关信息则表示安装
  • 卸载docker,若不重装直接忽略,yum list installed | grep docker查看安装内容

1
2
3
4
5
6
$ docker 
# -bash: docekr: 未找到命令 则表示未安装了
# 如果安装,卸载默认安装的docer
sudo yum remove docker docker-common container-selinux docker-selinux docker-engine
安装 yum-utils,它提供了 yum-config-manager,可用来管理yum源
sudo yum install -y yum-utils

安装docker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#更新yum 
yum update
#获取官方源
wget -P /etc/yum.repos.d/ https://download.docker.com/linux/centos/docker-ce.repo
##添加yum源
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
## 也可以使用下面的方式配置docker的yum源
# 华为 二选一
wget -O /etc/yum.repos.d/docker-ce.repo https://repo.huaweicloud.com/docker-ce/linux/centos/docker-ce.repo
sudo sed -i 's+download.docker.com+repo.huaweicloud.com/docker-ce+' /etc/yum.repos.d/docker-ce.repo
# 阿里 二选一
wget https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo -O /etc/yum.repos.d/docker-ce.repo
##更新索引
sudo yum clean all##推荐先清空索引,特别是新版本docker需要换成老版本docker的时候
# 安装默认的
sudo yum install -y docker-ce docker-ce-cli containerd.io
#查看可以安装的版本
yum list docker-ce --showduplicates | sort -r
#选择安装
yum install -y docker-ce-18.03.0.ce-1.el7.centos
#安装docker
yum install docker -y
#检查
docker -v #输出版本信息

卸载docker

1
sudo yum remove docker-ce

配置网络加速

1
2
# 添加一下内容
vim /etc/docker/daemon.json
1
2
3
4
5
6
7
{
"registry-mirrors": [
"https://3oq8m6tn.mirror.aliyuncs.com",
"https://hub-mirror.c.163.com",
"https://mirror.baidubce.com"
]
}
  • 之后重新启动服务
1
2
sudo systemctl daemon-reload
sudo systemctl restart docker

设置docker相关信息

  1. 设置docker开机启动项systemctl enable docker.service
  2. 关闭docker开机启动项 systemctl stop docker.service
  3. 检查docker当前运行情况 systemctl status docker
  4. 若未启动则启动docker systemctl start docker ,若启动了可以重启systemctl restart docker
  5. 检查docker运行情况service docker status

部署docker-compsoe

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

yum install epel-release
yum install -y python-pip
# 如果pip版本可以换成pip3

#更新pip
pip install --upgrade pip
#安装pip3
yum -y install python3-pip -y
# 更新setuptools
pip install --upgrade setuptools
# 更新pip python2.7 或者 python3 更新
python -m pip install --upgrade pip
python3 -m pip install --upgrade pip

#pip 安装,需要注意pip的版本 docker 与 docker-compose有版本对应
pip install docker-compose
pip3 install docker-compose

#如果没有pip

参考文档

阿里云CentOS 7上安装配置Docker

How To Install and Use Docker Compose on CentOS 7

0x00 阅读源代码

下载源代码

  • 阅读版本号 netty-4.1.33.Final
    • 由于github下载速度是在太慢了,这里用gitee克隆一个镜像。
    • 下载版本git clone -b netty-4.1.33.Final git@gitee.com:Z201/netty.git

查看源代码结构

  • 查看项目主要文件夹
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
tree -d  -L 1    
├── all
├── bom
├── buffer (Core netty定制的buffer)
├── codec (Protocol Support)
├── codec-dns (Protocol Support)
├── codec-haproxy (Protocol Support)
├── codec-http (Protocol Support)
├── codec-http2 (Protocol Support)
├── codec-memcache (Protocol Support)
├── codec-mqtt (Protocol Support 物联网)
├── codec-redis (Protocol Support )
├── codec-smtp (Protocol Support 邮件)
├── codec-socks (Protocol Support)
├── codec-stomp (Protocol Support ws)
├── codec-xml (Protocol Support)
├── common (Core)
├── dev-tools
├── docker
├── example (抄代码的地方)
├── handler (Protocol Support)
├── handler-proxy (Protocol Support)
├── license
├── microbench (测试用的)
├── resolver (Core)
├── resolver-dns (Core)
├── tarball
├── target
├── testsuite
├── testsuite-autobahn
├── testsuite-http2
├── testsuite-osgi
├── testsuite-shading
├── transport (Transport Services)
├── transport-native-epoll (Transport Servicesnative omitted - reserved keyword in Java)
├── transport-native-kqueue (Transport Servicesnative omitted - reserved keyword in Java)
├── transport-native-unix-common Transport Services native omitted - reserved keyword in Java)
├── transport-native-unix-common-tests
├── transport-rxtx (Transport Services 串口编程 作废)
├── transport-sctp (Transport Services )
└── transport-udt (Transport Services 作废)
  • 将项目导入idea中,笔者采用mac系统。这里需要注意需要安装部分软件。
1
2
3
4
5
6
brew install autoconf automake libtool
# 如果安装了国内的镜像出现Error opening archive: Failed to open
# export HOMEBREW_BOTTLE_DOMAIN=''
# 参考文档 https://zhuanlan.zhihu.com/p/383707713
# 如果是mac系统需要手动将pom文件tcnative.classifier修改成对应系统的。
mvn install -Dmaven.test.skip=true

0x01 根据官方文档快速入门

强烈推荐认真阅读netty的官方文档。

Core

  • netty-common模块是 Netty 的核心基础包,其他模块都需要依赖它。常用的包括通用工具类和自定义并发包。
  • netty-buffer 模块中Netty自己实现了的一个更加完备的ByteBuf 工具类,用于网络通信中的数据载体。
  • netty-resover模块主要提供了一些有关基础设施的解析工具,包括 IP Address、Hostname、DNS 等。

Protocol Support

  • netty-codec模块主要负责编解码工作,提供主流协议的编辑码,还提供了抽象编解码类 ByteToMessageDecoder 和 MessageToByteEncoder,通过继承这两个类我们可以轻松实现自定义的编解码逻辑。

Transport Service

  • netty-transport 模块可以说是 Netty 提供数据处理和传输的核心模块,如 Bootstrap、Channel、ChannelHandler、EventLoop、EventLoopGroup、ChannelPipeline 等。其中 Bootstrap 负责客户端或服务端的启动工作,包括创建、初始化 Channel 等;EventLoop 负责向注册的 Channel 发起 I/O 读写操作;ChannelPipeline 负责 ChannelHandler 的有序编排,这些组件在介绍 Netty 逻辑架构的时候都有所涉及。

SQL 常见用法

1. 查找数据的查询

SELECT: 用于从数据库中选择数据

  • SELECT * FROM table_name;

DISTINCT: 用于过滤掉重复的值并返回指定列的行

  • SELECT DISTINCT column_name;

WHERE: 用于过滤记录/行

  • SELECT column1, column2 FROM table_name WHERE condition;
  • SELECT * FROM table_name WHERE condition1 AND condition2;
  • SELECT * FROM table_name WHERE condition1 OR condition2;
  • SELECT * FROM table_name WHERE NOT condition;
  • SELECT * FROM table_name WHERE condition1 AND (condition2 OR condition3);
  • SELECT * FROM table_name WHERE EXISTS (SELECT column_name FROM table_name WHERE condition);

ORDER BY: 用于结果集的排序,升序(ASC)或者降序(DESC)

  • SELECT * FROM table_name ORDER BY column;
  • SELECT * FROM table_name ORDER BY column DESC;
  • SELECT * FROM table_name ORDER BY column1 ASC, column2 DESC;

SELECT TOP: 用于指定从表顶部返回的记录数

  • SELECT TOP number columns_names FROM table_name WHERE condition;
  • SELECT TOP percent columns_names FROM table_name WHERE condition;
  • 并非所有数据库系统都支持SELECT TOP。 MySQL 中是LIMIT子句
  • SELECT column_names FROM table_name LIMIT offset, count;

LIKE: 用于搜索列中的特定模式,WHERE 子句中使用的运算符

  • % (percent sign) 是一个表示零个,一个或多个字符的通配符
  • _ (underscore) 是一个表示单个字符通配符
  • SELECT column_names FROM table_name WHERE column_name LIKE pattern;
  • LIKE ‘a%’ (查找任何以“a”开头的值)
  • LIKE ‘%a’ (查找任何以“a”结尾的值)
  • LIKE ‘%or%’ (查找任何包含“or”的值)
  • LIKE ‘_r%’ (查找任何第二位是“r”的值)
  • LIKE ‘a_%_%’ (查找任何以“a”开头且长度至少为3的值)
  • LIKE ‘[a-c]%’(查找任何以“a”或“b”或“c”开头的值)

IN: 用于在 WHERE 子句中指定多个值的运算符

  • 本质上,IN运算符是多个OR条件的简写
  • SELECT column_names FROM table_name WHERE column_name IN (value1, value2, …);
  • SELECT column_names FROM table_name WHERE column_name IN (SELECT STATEMENT);

BETWEEN: 用于过滤给定范围的值的运算符

  • SELECT column_names FROM table_name WHERE column_name BETWEEN value1 AND value2;
  • SELECT * FROM Products WHERE (column_name BETWEEN value1 AND value2) AND NOT column_name2 IN (value3, value4);
  • SELECT * FROM Products WHERE column_name BETWEEN #01/07/1999# AND #03/12/1999#;

NULL: 代表一个字段没有值

  • SELECT * FROM table_name WHERE column_name IS NULL;
  • SELECT * FROM table_name WHERE column_name IS NOT NULL;

AS: 用于给表或者列分配别名

  • SELECT column_name AS alias_name FROM table_name;
  • SELECT column_name FROM table_name AS alias_name;
  • SELECT column_name AS alias_name1, column_name2 AS alias_name2;
  • SELECT column_name1, column_name2 + ‘, ‘ + column_name3 AS alias_name;

UNION: 用于组合两个或者多个 SELECT 语句的结果集的运算符

  • 每个 SELECT 语句必须拥有相同的列数
  • 列必须拥有相似的数据类型
  • 每个 SELECT 语句中的列也必须具有相同的顺序
  • SELECT columns_names FROM table1 UNION SELECT column_name FROM table2;
  • UNION 仅允许选择不同的值, UNION ALL 允许重复

ANY|ALL: 用于检查 WHERE 或 HAVING 子句中使用的子查询条件的运算符

  • ANY 如果任何子查询值满足条件,则返回 true。
  • ALL 如果所有子查询值都满足条件,则返回 true。
  • SELECT columns_names FROM table1 WHERE column_name operator (ANY|ALL) (SELECT column_name FROM table_name WHERE condition);

GROUP BY: 通常与聚合函数(COUNT,MAX,MIN,SUM,AVG)一起使用,用于将结果集分组为一列或多列

  • SELECT column_name1, COUNT(column_name2) FROM table_name WHERE condition GROUP BY column_name1 ORDER BY COUNT(column_name2) DESC;

HAVING: HAVING 子句指定 SELECT 语句应仅返回聚合值满足指定条件的行。它被添加到 SQL 语言中,因为WHERE关键字不能与聚合函数一起使用。

  • SELECT COUNT(column_name1), column_name2 FROM table GROUP BY column_name2 HAVING COUNT(column_name1) > 5;
阅读全文 »

最近在编写爬虫程序,这里简单的做下笔记。

阅读注意事项

  1. 需要一台服务器(阿里ESC)这种,分配的公网ip。
  2. 这里使用的yum安装,所以环境配置都创建好了,不需要在手动修改。

准备工作

安装Java、maven、git、selenium运行环境

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
# 使用的机器是centos7.4
# 系统环境一个一个来安装。
# 1.检查是否安装jdk
-> yum install java-1.8.0-openjdk-devel.x86_64
-> java -version
openjdk version "1.8.0_262"
OpenJDK Runtime Environment (build 1.8.0_262-b10)
OpenJDK 64-Bit Server VM (build 25.262-b10, mixed mode)
-> yum install maven
-> mvn -version
Java version: 1.8.0_262, vendor: Oracle Corporation
Java home: /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.262.b10-0.el7_8.x86_64/jre
Default locale: zh_CN, platform encoding: UTF-8
OS name: "linux", version: "3.10.0-862.el7.x86_64", arch: "amd64", family: "unix"
-> yum install git
git version 1.8.3.1
# 查看是否生成过证书
-> ls -al ~/.ssh
# 这里配置下ssh公钥方便git拉去代码
-> ssh-keygen -t rsa -C “you email@gamil.com”
# 查看证书
-> cat ~/.ssh/id_rsa.pub
# 开始配置 selenium 运行环境
-> wget https://dl.google.com/linux/direct/google-chrome-stable_current_x86_64.rpm
-> yum localinstall google-chrome-stable_current_x86_64.rpm
# 检查版本
-> google-chrome --version
Google Chrome 85.0.4183.83
# 安装 chromedriver
-> yum install chromedriver
-> chromedriver -version
ChromeDriver 84.0.4147.89
# 这里注意 chromedriver 可能与chrome的版本不一致,去官方网站下载后解压复制到/usr/bin中
# 下载地址 https://chromedriver.chromium.org/
  • 注意 google-chrome 和 chromedriver版本是需要对应的具体可以到官方查看。
  • 在centos上运行需要配置下关键参数
1
2
3
4
5
6
7
ChromeOptions chromeOptions = new ChromeOptions();
chromeOptions.addArguments("--headless");
chromeOptions.addArguments("--disable-gpu"); // 不配置一定报错
chromeOptions.addArguments("--no-sandbox");
// 创建无界面浏览器对象
chromeOptions.setHeadless(true);
return new ChromeDriver(chromeOptions);
  • 启动代码的需要指定环境变量
1
2
System.setProperty("webdriver.chrome.chromedriver", "/usr/bin/chromedriver");
# 具体可以通过 whereis chromedriver查看。

参考文献

CentOS使用yum安装jdk

最近在搭建测试环境,很久没接触nginx了。

阅读注意事项

  1. 需要一个域名,并且在国内备案过。
  2. 需要一台服务器(阿里ESC)这种,分配的公网ip。
  3. 申请或者购买一张证书。和域名做关联,域名解析到服务器的公网ip。注意开放服务区的80、443端口。
    1. 证书有两个文件
      1. domian.name.key domain.name.pem
    2. 吧证书上传到目标nginx服务器中。
    3. 检查服务器防火墙
1
2
3
sudo firewall-cmd --permanent --zone=public --add-service=http
sudo firewall-cmd --permanent --zone=public --add-service=https
sudo firewall-cmd --reload

开始步骤

安装nginx

服务器使用的Centos服务器 使用yum安装nginx。

1
2
3
4
5
6
7
8
9
10
11
12
13
-> yum install -y nginx
#检查nginx安装位置信息
-> whereis nginx
nginx: /usr/sbin/nginx /usr/lib64/nginx /etc/nginx /usr/share/nginx /usr/share/man/man3/nginx.3pm.gz /usr/share/man/man8/nginx.8.gz
# 实际上是安装到/etc/nginx里面了
# 设置开机自动启动nginx
-> sudo systemctl enable nginx
Created symlink from /etc/systemd/system/multi-user.target.wants/nginx.service to /usr/lib/systemd/system/nginx.service.
# 切换到nginx安装目录
-> cd /etc/nginx
# 启动nginx
-> nginx
# 测试nginx 在浏览器里面请求http://ip 就可以了看到nginx页面就算安装成功

配置证书

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
# 创建证书文件并且将证书复制近文件中
-> cd /etc/nginx
-> mkdir cert
-> mv domian.name.key /etc/nginx/cert/
-> mv domian.name.pem /etc/nginx/cert/
# 配置nginx.conf文件使证书生效,并将80端口转发到443端口上。
-> vim nginx.conf
#以下属性中以ssl开头的属性代表与证书配置有关,其他属性请根据自己的需要进行配置。
server {
listen 443; #配置HTTPS的默认访问端口号为443。此处如果未配置HTTPS的默认访问端口,可能会造成Nginx无法启动。Nginx 1.15.0以上版本请使用listen 443 ssl代替listen 443和ssl on。
server_name www.certificatestests.com; #将www.certificatestests.com修改为您证书绑定的域名,例如:www.example.com。如果您购买的是通配符域名证书,要修改为通配符域名,例如:*.aliyun.com。
root html;
index index.html index.htm;
ssl_certificate cert/domain name.pem; #将domain name.pem替换成您证书的文件名称。
ssl_certificate_key cert/domain name.key; #将domain name.key替换成您证书的密钥文件名称。
ssl_session_timeout 5m;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; #使用此加密套件。
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; #使用该协议进行配置。
ssl_prefer_server_ciphers on;
location / {
root html; #站点目录。
index index.html index.htm;
}
}

server {
# listen 80 default_server;
# listen [::]:80 default_server;
# server_name _;
listen 80;
listen [::]:80;
server_name www.certificatestests.com;
return 301 https://$host$request_uri;
}
# 保存并推出、检查配置nginx配置文件是否合法、并重启nginx
-> nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
-> nginx -s reload

测试域名证书是否生效

1
2
https://domian
http://domian 检查是否安全链接即可。一般浏览器上面会出现一个安全的标志

参考文献

阿里云帮助文档-在Nginx上配置证书

CentOS 7 上安装最新版 Nginx

JMH

JMH(Java Microbenchmark Harness)是由OpenJDK Developer提供的基准测试工具(基准可以理解为比较的基础,我们将这一次性能测试结果作为基准结果,下一次的测试结果将与基准数据进行比较),它是一种常用的性能测试工具,解决了基准测试中常见的一些问题。

JMH结果分析

结果日志解释

  • 基础信息,显示Java路径、Java版本以及JMH基础配置信息
1
2
3
4
5
6
7
8
9
10
11
# JMH version: 1.21
# VM version: JDK 1.8.0_275, OpenJDK 64-Bit Server VM, 25.275-b01
# VM invoker: /Library/Java/JavaVirtualMachines/adoptopenjdk-8.jdk/Contents/Home/jre/bin/java
# VM options: -server
# Warmup: 1 iterations, 10 s each
# Measurement: 1 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 8 threads, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: cn.z201.jmh.ListBenchmark.testArrayList
# Parameters: (size = 100)
  • 预热次数。预热测试不会作为最终的统计结果。预热的目的是让 JVM 对被测代码进行足够多的优化,被测代码应该得到了充分的 JIT 编译和优化。
1
2
3
4
5
# Warmup Iteration   1: 0.002 ±(99.9%) 0.001 ms/op
Iteration 1: 0.002 ±(99.9%) 0.001 ms/op

Result "cn.z201.jmh.ListBenchmark.testArrayList":
0.002 ms/op
  • 结果表明,在拼接字符次数越多的情况下,LinkedList.add() 的性能就更好。这是得益于LinkedList双向链表结构,每次add都是在最后一个位置添加元素。
1
2
3
4
5
6
7
8
9
10
Benchmark                               (size)  Mode  Cnt    Score   Error  Units
ListBenchmark.testArrayList 100 avgt 0.002 ms/op
ListBenchmark.testArrayList 1000 avgt 0.018 ms/op
ListBenchmark.testArrayList 10000 avgt 0.194 ms/op
ListBenchmark.testCopyOnWriteArrayList 100 avgt 0.015 ms/op
ListBenchmark.testCopyOnWriteArrayList 1000 avgt 1.386 ms/op
ListBenchmark.testCopyOnWriteArrayList 10000 avgt 148.161 ms/op
ListBenchmark.testLinkedList 100 avgt 0.002 ms/op
ListBenchmark.testLinkedList 1000 avgt 0.017 ms/op
ListBenchmark.testLinkedList 10000 avgt 0.163 ms/op
阅读全文 »

本章只是回顾一次eureka版本升级后tomcat资源占用过高的问题,临时解决。

升级springBoot、SpringCloud版本

  • 升级完成后直接在测试环境测试部署,结果发现eureka的cpu资源占用过高,导致假死。通过arthas发现是tomcat线程阻塞。通过dependepency看了下springboot中内嵌tomcat的版本是9了。之前是8,测试环境一大堆人等着测试接口。首先想着降低tomcat版本。
1
2
3
查看依赖
gradle dependencies
mvn dependency:tree > output.txt # 输出到文件里

排除SpringBoot的Tomcat,指定Tomcat版本

有时候我们需要在特定情况下使用特定的Tomcat版本,这时候总不能因为Tomcat就改变SpringBoot的版本,所以可以采用排除SpringBoot中的Tomcat包,然后手动指定Tomcat的版本,当然还要引入Tomcat相关的包。

Gradle的配置
1
2
3
4
5
6
7
compile('org.springframework.boot:spring-boot-starter-web') {
exclude module: "spring-boot-starter-tomcat"
}
compile 'org.apache.tomcat.embed:tomcat-embed-core:8.5.37'
compile 'org.apache.tomcat.embed:tomcat-embed-el:8.5.37'
compile 'org.apache.tomcat.embed:tomcat-embed-logging-juli:8.5.2'
compile 'org.apache.tomcat.embed:tomcat-embed-websocket:8.5.37'

如果不指定版本,则会使用最新的Tomcat版本, 否则直接指定对应的版本号。

Maven的配置
  1. 在 pom.xml文件里面添加一个标签,添加期望的版本。
1
<tomcat.version>8.5.37</tomcat.version>
  1. 添加必要的Jar包:
1
2
3
4
5
6
7
8
9
10
<dependency> 
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-juli</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-logging-juli</artifactId>
<version>${tomcat.version}</version>
</dependency>

重新部署eureka后一切正常。处理时间短暂,没有影响大家工作。时间过了大半年了,也忘记具体是啥原因了。有机会本地复现试试。

本篇幅只是回顾使用eureka的时候如何钉钉告警。

Eureka钉钉告警

1
2
3
4
sequenceDiagram
eureka ->> eventListener : event
eventListener ->> 钉钉 : alarmNoticeSend
钉钉 ->> 开发人员 : eurekaInstanceInfo
  • 实例图

关键代码

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

@EventListener
public void listen(EurekaInstanceCanceledEvent event) {
log.error("告警通知 [{}] 服务注销 timestamp [{}] serverId [{}]",
event.getAppName(),
event.getTimestamp(),
event.getServerId());
if (!event.isReplication()) {
ServiceNotice serviceNotice = new ServiceNotice();
serviceNotice.setTitle("告警通知 服务下线");
serviceNotice.setAppName(event.getAppName());
serviceNotice.setTimestamp(event.getTimestamp());
serviceNotice.setServerId(event.getServerId());
serviceNotice.setProfiles(active);
alarmNoticeManage.createNotice(serviceNotice, "");
}
}

@EventListener
public void listen(EurekaInstanceRegisteredEvent event) {
log.info("告警通知 [{}] 服务注册 timestamp [{}] id [{}] ipAddr [{}] ",
event.getInstanceInfo().getAppName(),
event.getTimestamp(),
event.getInstanceInfo().getId(),
event.getInstanceInfo().getIPAddr()
);
if (!event.isReplication()) {
ServiceNotice serviceNotice = new ServiceNotice();
serviceNotice.setTitle("告警通知 服务上线");
serviceNotice.setAppName(event.getInstanceInfo().getAppName());
serviceNotice.setTimestamp(event.getTimestamp());
serviceNotice.setServerId(event.getInstanceInfo().getId());
serviceNotice.setProfiles(active);
alarmNoticeManage.createNotice(serviceNotice, "");
}
}
  • 效果如下
1
2
3
告警通知 服务下线
2020-07-16 16:03:20 Z-GATEWAY 192.168.31.7:z-gateway:9000 profiles dev1
@xxx
  • eureka查看源码得知有五个事件。
1
2
3
4
5
EurekaServerStartedEvent - Eureka服务端启动事件
EurekaRegistryAvailableEvent - Eureka服务端可用事件
EurekaInstanceRegisteredEvent - Eureka客户端服务注册事件
EurekaInstanceRenewedEvent - Eureka客户端续约事件
EurekaInstanceCanceledEvent - Eureka客户端下线事件

这里就比较简单了。

本篇幅只是回顾使用钉钉做异常告警需要那些关键业务信息。

为什么要做钉钉通知?

​ 事情要从我入职上家公司说起,进入公司后把线上项目clone下来大致看了下。代码风格过于滞后、编码风格混乱。进入公司第一周就出现了线上故障,嗯。我去线上检查日志,emmmm竟然没有日志输出。这次故障是由客户反馈来的。当时我非常吃惊,大伙好像很淡定的样子,习以为常了?

​ 想到当初面试的时候和总监的谈话,主要是带领团队落地微服务架构,看来必须大刀阔斧了。

​ 首先想到的时候改进日志输出、定义全局异常级别,根据异常级别输出日志。

1
2
3
4
sequenceDiagram
Java应用 ->> SpringAop全局异常拦截 : runtimeException
SpringAop全局异常拦截 ->> 钉钉 : alarmNoticeSend
钉钉 ->> 开发人员 : exceptionInfo
  • 效果图

​ 我们需要从钉钉里面看到那些异常信息呢?这是当时输出到钉钉的信息。通过编写全局拦截器,在公共基础项目里面添加了aop全局拦截。刚开始上线的时候钉钉一天动不动就几千个异常告警。刚开始大伙都很紧张,过了个把月大伙已经又麻木了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
告警信息
工程名:z201-gateway
类路径:cn.z201.cloud.gateway.VlinkFrameworkGatewayApplicationTest
方法名:alarm
异常信息:java.lang.IllegalAccessException
异常追踪:
cn.z201.cloud.gateway.VlinkFrameworkGatewayApplicationTest.alarm(VlinkFrameworkGatewayApplicationTest.java:64)
sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
java.lang.reflect.Method.invoke(Method.java:498)
org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:74)

这样的告警信息就够了吗?

​ 明显这样是不够够的,前端有安卓、ios、微信小程序、web、快应用。太多前端项目了,后端需要识别出是哪里的项目出的问题。于是又改进了一次。邀请前端开发人员在HttpHeader里面增加额外参数。为了做流量区分也增加了一些参数。

1
2
3
4
5
6
7
8
9
10
11
 {
"Content-Type":"application/json"
"Authorization":"Bearer xxxxxxxxxx" ## jwt
"Client-Business-Group-Source": "1", ## 业务组来源唯一标号
"Client-Business-Source": "1000", ## 业务来源唯一标号
"Client-Business-Activity-Source": "1", ## 查看介绍、更多 针对特殊业务流量识别
"Client-Env-Source": "1", ##客户端环境来源 1 ios 2 android 3 windows
"Client-Platform-Source": "xxx", ##客户端平台 xxx手机型号、浏览器
"Client-Start-Time": "1", ##请求时间戳
"Client-Version-Source": "1.0.0" ##客户端版本号
}

​ 通过上面的改进告警信息完善很多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
告警信息
工程名:z201-gateway
类路径:cn.z201.cloud.gateway.VlinkFrameworkGatewayApplicationTest
方法名:alarm
异常信息:java.lang.IllegalAccessException
异常扩展信息: {
"Client-Business-Source": "1000",
"Client-Business-Activity-Source": "1",
"Client-Env-Source": "1",
"Client-Platform-Source": "xxx",
"Client-Version-Source": "1.0.0"
}
异常追踪:
cn.z201.cloud.gateway.VlinkFrameworkGatewayApplicationTest.alarm(VlinkFrameworkGatewayApplicationTest.java:64)
sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
java.lang.reflect.Method.invoke(Method.java:498)
org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:74)

分布式下面临的问题!

1
2
3
4
5
sequenceDiagram
gateway ->> 应用A : httpRequest
应用A ->> 应用B : httpRequest
应用B -->> 应用A : httpResponse
应用A -->> gateway : httpResponse

​ 当调用链多的时候定位问题就有点麻烦,比如应用a调用应用b。应用b执行了异常信息直接抛出了告警信息。但是spring cloud http rpc默认是不会吧请求参数传递到后面的服务中,需要我们做下简单的扩展。

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
/**
* @author z201.coding@gmail.com
**/
public interface HttpApiConstant {

String X_REAL_IP = "x-real-ip";
/**
* 请求头跟踪id名。
*/
String HTTP_HEADER_TRACE_ID = "AppTraceId";

/**
* 请求头
*/
String HTTP_TOKEN_HEADER = "Authorization";

/**
* app租户
*/
String APP_TENANT = "Tenant";

String CLIENT_BUSINESS_GROUP_SOURCE = "Client-Business-Group-Source";

String CLIENT_BUSINESS_SOURCE = "Client-Business-Source";

String CLIENT_BUSINESS_ACTIVITY_SOURCE = "Client-Business-Activity-Source";

String CLIENT_EVN_SOURCE = "Client-Env-Source";

String CLIENT_PLATFORM_SOURCE = "Client-Platform-Source";

String CLIENT_START_TIME = "Client-Start-Time";

String CLIENT_VERSION_SOURCE = "Client-Version-Source";

}


/**
* @author z201.coding@gmail.com
* 自定义restTemplate拦截器
* 这里可以把一些参数从应用层传到内部服务
**/
@Slf4j
@ConditionalOnClass(WebMvcConfigurer.class)
public class MdcFeignInterceptorConfig implements RequestInterceptor, HttpApiConstant {

public MdcFeignInterceptorConfig() {
log.info("Loaded Z-REST-INTERCEPTOR [V1.0.0]");
}

@Override
public void apply(RequestTemplate template) {
try {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
.getRequest();
String xRealIp = request.getHeader(X_REAL_IP);
String authentication = request.getHeader(HTTP_TOKEN_HEADER);
String appTraceId = request.getHeader(HTTP_HEADER_TRACE_ID);
String businessGroupSource = request.getHeader(CLIENT_BUSINESS_GROUP_SOURCE);
String clientBusinessSource = request.getHeader(CLIENT_BUSINESS_SOURCE);
String clientBusinessActivitySource = request.getHeader(CLIENT_BUSINESS_ACTIVITY_SOURCE);
String clientEnvSource = request.getHeader(CLIENT_EVN_SOURCE);
String clientPlatformSource = request.getHeader(CLIENT_PLATFORM_SOURCE);
String clientStartTime = request.getHeader(CLIENT_START_TIME);
String clientVersionSource = request.getHeader(CLIENT_VERSION_SOURCE);
template.header(HttpHeaders.ACCEPT_ENCODING, "gzip");
template.header(X_REAL_IP, xRealIp);
template.header(HTTP_TOKEN_HEADER, authentication);
template.header(CLIENT_BUSINESS_GROUP_SOURCE, businessGroupSource);
template.header(CLIENT_BUSINESS_SOURCE, clientBusinessSource);
template.header(CLIENT_BUSINESS_ACTIVITY_SOURCE, clientBusinessActivitySource);
template.header(CLIENT_EVN_SOURCE, clientEnvSource);
template.header(CLIENT_PLATFORM_SOURCE, clientPlatformSource);
template.header(CLIENT_START_TIME, clientStartTime);
template.header(CLIENT_VERSION_SOURCE, clientVersionSource);
if (log.isDebugEnabled()) {
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
String value = request.getHeader(name);
log.debug("header {} - {}", name, value);
}
}
} catch (Exception e) {
log.error("template exception {}",e.getMessage());
}
}
}

简单的异常钉钉告警就到这里结束了。

本章是整理知识内容,为强化知识长期更新。

JVM

JVM 全称 Java Virtual Machine,也就是我们耳熟能详的 Java 虚拟机。JVM 会翻译执行 Java 字节码,然后调用真正的操作系统函数,这些操作系统函数是与平台息息相关的。

  • 如上图所示,通过JVM,Java实现了跨平台、只要class文件能正常执行,就可以在其他系统上面运行。 JVM 与操作系统之间的关系:JVM 上承开发语言,下接操作系统,它的中间接口就是字节码。

JDK

  • JDK 的全拼,Java Development Kit JVM、JRE、JDK 它们三者之间的关系,可以用一个包含关系表示。

JVM字节码

对于 Java 开发者来说,虚拟机、字节码就是其底层知识。Java Byte 由单字节 byte的指令组成,理论上最多支持256个操作码。实际上只使用了200个左右操作码。

编译java文件

这里采用xxd xx.java

1
2
3
4
5
6
7
8
9

package cn.z201.jvm;

public class Testing {

public static void main(String[] args){
System.out.println("Hello world!");
}
}
  • 通过javac编译成class文件、在用xxd查看编译后的16进制class文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
➜  jvm git:(master) ✗ javac Testing.java 
➜ jvm git:(master) ✗ xxd Testing.class
00000000: cafe babe 0000 0034 001d 0a00 0600 0f09 .......4........
00000010: 0010 0011 0800 120a 0013 0014 0700 1507 ................
00000020: 0016 0100 063c 696e 6974 3e01 0003 2829 .....<init>...()
00000030: 5601 0004 436f 6465 0100 0f4c 696e 654e V...Code...LineN
00000040: 756d 6265 7254 6162 6c65 0100 046d 6169 umberTable...mai
00000050: 6e01 0016 285b 4c6a 6176 612f 6c61 6e67 n...([Ljava/lang
00000060: 2f53 7472 696e 673b 2956 0100 0a53 6f75 /String;)V...Sou
00000070: 7263 6546 696c 6501 000c 5465 7374 696e rceFile...Testin
00000080: 672e 6a61 7661 0c00 0700 0807 0017 0c00 g.java..........
00000090: 1800 1901 000c 4865 6c6c 6f20 776f 726c ......Hello worl
000000a0: 6421 0700 1a0c 001b 001c 0100 1363 6e2f d!...........cn/
000000b0: 7a32 3031 2f6a 766d 2f54 6573 7469 6e67 z201/jvm/Testing
000000c0: 0100 106a 6176 612f 6c61 6e67 2f4f 626a ...java/lang/Obj
000000d0: 6563 7401 0010 6a61 7661 2f6c 616e 672f ect...java/lang/
000000e0: 5379 7374 656d 0100 036f 7574 0100 154c System...out...L
000000f0: 6a61 7661 2f69 6f2f 5072 696e 7453 7472 java/io/PrintStr
00000100: 6561 6d3b 0100 136a 6176 612f 696f 2f50 eam;...java/io/P
00000110: 7269 6e74 5374 7265 616d 0100 0770 7269 rintStream...pri
00000120: 6e74 6c6e 0100 1528 4c6a 6176 612f 6c61 ntln...(Ljava/la
00000130: 6e67 2f53 7472 696e 673b 2956 0021 0005 ng/String;)V.!..
00000140: 0006 0000 0000 0002 0001 0007 0008 0001 ................
00000150: 0009 0000 001d 0001 0001 0000 0005 2ab7 ..............*.
00000160: 0001 b100 0000 0100 0a00 0000 0600 0100 ................
00000170: 0000 0500 0900 0b00 0c00 0100 0900 0000 ................
00000180: 2500 0200 0100 0000 09b2 0002 1203 b600 %...............
00000190: 04b1 0000 0001 000a 0000 000a 0002 0000 ................
000001a0: 0008 0008 0009 0001 000d 0000 0002 000e ................


  • 编译后完全看不懂,jdk实际上提供了专门用来解析类文件的工具javap

javap

javap能对给定的class文件提供的字节代码进行反编译

  • 使用格式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
javap <options> <classes>

-help --help -? 输出此用法消息
-version 版本信息
-v -verbose 输出附加信息
-l 输出行号和本地变量表
-public 仅显示公共类和成员
-protected 显示受保护的/公共类和成员
-package 显示程序包/受保护的/公共类
和成员 (默认)
-p -private 显示所有类和成员
-c 对代码进行反汇编
-s 输出内部类型签名
-sysinfo 显示正在处理的类的
系统信息 (路径, 大小, 日期, MD5 散列)
-constants 显示最终常量
-classpath <path> 指定查找用户类文件的位置
-cp <path> 指定查找用户类文件的位置
-bootclasspath <path> 覆盖引导类文件的位置
  • 演示下上面的类
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
javap -c -s -v Testing

Classfile /Users/z201/word/z201.github.io.code/java-learning/learn-jvm/src/test/java/cn/z201/jvm/Testing.class
MD5 checksum b50deb6f6e7df9bb99585bc71b92d39b
Compiled from "Testing.java"
public class cn.z201.jvm.Testing
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #18 // Hello world!
#4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #21 // cn/z201/jvm/Testing
#6 = Class #22 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 Testing.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = Class #23 // java/lang/System
#17 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
#18 = Utf8 Hello world!
#19 = Class #26 // java/io/PrintStream
#20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
#21 = Utf8 cn/z201/jvm/Testing
#22 = Utf8 java/lang/Object
#23 = Utf8 java/lang/System
#24 = Utf8 out
#25 = Utf8 Ljava/io/PrintStream;
#26 = Utf8 java/io/PrintStream
#27 = Utf8 println
#28 = Utf8 (Ljava/lang/String;)V
{
public cn.z201.jvm.Testing();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 5: 0

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello world!
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 8: 0
line 9: 8
}
SourceFile: "Testing.java"

IDEA bytecode viewer

jclasslib is a bytecode viewer for Java class files

  • 在plugins中搜索jclasslib bytecode viewer
  • 安装重启IDEA后,编译项目代码(run 一次)选中代码文件 view 菜单中有 bytecode viewer 选项。

  • IDEA 显示的内容更加容易阅读。

JVM的内存布局

  • 堆(Java Heap) 也叫 Java 堆或者是 GC 堆,它是一个线程共享的内存区域,也是 JVM 中占用内存最大的一块区域,Java 中所有的对象都存储在这里。

    • 存储的是我们new来的对象,不存放基本类型和对象引用。

    • 由于创建了大量的对象,垃圾回收器主要工作在这块区域。

    • 线程共享区域,因此是线程不安全的。

    • 能够发生内存溢出,主要有OutOfMemoryError和StackOverflowError。

    • 那么什么时候发生OutOfMemoryError,什么时候发生StackOverflowError?虚拟机在扩展栈时无法申请到足够的内存空间,将抛出OutOfMemoryError异常,线程请求的栈深度超过虚拟机所允许的最大深度,将抛出StackOverflowError异常

    • Java堆区还可以划分为新生代和老年代,新生代又可以进一步划分为Eden区、Survivor 1区、Survivor 2区。具体比例参数的话,可以看一下下面这张图。

    • 合理设置老年代和新生代的空间比例对 JVM 垃圾回收的性能有很大影响,JVM 设置老年代新生代比例的参数是 -XX:NewRatio。
  • Minor GC 发生在新生代,而 Full GC 发生在老年代。

  • 大部分新生成的对象都是在 Eden 区,Eden 区满了之后便没有内存给新对象使用,Eden 区便会 Minor GC 回收无用内存,剩下的存活对象便会转移到 Survivor 区。

  • 从 Eden 区存活下来的对象首先会被复制到 From 区,当 From 区满时,此时还存活的对象会被转移到 To 区,经历了多次的 Minor GC 后,还存活的对象就会被复制到老年代,老年代的 GC 一般叫作 FullGC 或者 MajorGC。

新生代

  • 新生代是类的诞生、成长、消亡的区域,一个类在这里产生、应用,最后被垃圾回收器收集,结束生命。新生区又分为两部分:伊甸区(Eden space)和幸存区(Survivor pace),绝大多数的类都是在伊甸区被 new 出来的。幸存区有两个:0区(from Survivor 0 space)和 1区(to Survivor 1 space)。两个Survivor 区空间一样大,当Eden 区满了,则会触发普通GC,若 Eden 区中的对象经过垃圾回收没有被回收掉,会将Eden 区中的幸存对象移动到幸存0区。若幸存0区也满了,再对该区域进行垃圾回收,然后移动到1区,然后之前的0区将置为空,没有任何数据。显然,Survivor 区只是增加了对象在年轻代中逗留的时间,增加了被垃圾回收的可能性。那如果1区也满了呢?再移动到养老区。若养老区也满了,那么这时候将产生Full GC,进行养老区的内存清理。若养老区执行Full GC 之后发现依然无法进行对象的保存,就会产生 OOM 异常“OutOfMemoryError”。
  • 每次垃圾回收时都有大量的对象需要被回收。垃圾回收器在新生代采用的收集算法是Copying(复制)方法:它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块面对内存用完了,就将还存活的对象复制到另外一块,然后再把已使用的内存空间一次清理掉。由于新生代有每次垃圾回收时都有大量的对象需要被回收的特点,所以采用Copying算法,复制的存活对象较少,性能比较好。但是Copying算法还是有缺陷的,那就是内存的使用率只有一半。

OutOfMemoryError

1
java.lang.OutOfMemoryError: Java heap space
  • Java 虚拟机的堆内存设置不够,可以通过-Xms、-Xmx来调整

  • 代码中创建了大量大对象,并且长时间不能被垃圾回收器收集

  • 内存加载的数据量太大:一次性从数据库取太多数据

  • 集合类中有对对象的引用,使用后未清空,GC不能进行回收

  • 代码中存在循环产生过多的重复对象

老年代

  • 年轻代中经过垃圾回收没有回收掉的对象将被Copy到年老代。
  • 每次垃圾回收时只有少量的对象需要被回收。垃圾回收器在老年代采用的收集算法是 Mark-Compac t算法:为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。它先将需要回收的对象进行标记,然后将存活对象都向一端移动,然后清理掉端边界以外的内存。

FULL GC

  • 当老年代空间满了,就会对新生代和老年代空间进行一次全量垃圾回收,即Full GC。

方法区

  • 方法区(Method Area) 也被称为非堆区,用于和“Java 堆”的概念进行区分,它也是线程共享的内存区域,用于存储已经被 JVM 加载的类型信息、常量、静态变量、代码缓存等数据。

运行时常量池

  • Run-Time Constant Pool这是方法区的一部分,版本号、字段、方法、超类、接口等各种信息,还有一项信息就是常量池。Java 的常量池可以存放各种常量信息,不管是编译期生成的各种字面量,还是需要在运行时决定的符号引用。

程序计数器

  • 程序计数器(Program Counter Register) 线程独有一块很小的内存区域,保存当前线程所执行字节码的位置,包括正在执行的指令、跳转、分支、循环、异常处理等。

虚拟机栈

  • 虚拟机栈也叫 Java 虚拟机栈(Java Virtual Machine Stack),和程序计数器相同它也是线程独享的,用来描述 Java 方法的执行,在每个方法被执行时就会同步创建一个栈帧,用来存储局部变量表、操作栈、动态链接、方法出口等信息。当调用方法时执行入栈,而方法返回时执行出栈。
    • 线程私有区域,每一个线程都有独享一个虚拟机栈,因此这是线程安全的区域。

    • 存放基本数据类型以及对象的引用。

    • 每一个方法执行的时候会在虚拟机栈中创建一个相应栈帧,方法执行完毕后该栈帧就会被销毁。方法栈帧是以先进后出的方式虚拟机栈的。

    • 每一个栈帧又可以划分为局部变量表、操作数栈、动态链接、方法出口以及额外的附加信息。

      • 这个区域可能有两种异常:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常(通常是递归导致的);JVM动态扩展时无法申请到足够内存则抛出OutOfMemoryError异常。

本地方法栈

  • 本地方法栈(Native Method Stacks)与虚拟机栈类似,它是线程独享的,并且作用也和虚拟机栈类似。只不过虚拟机栈是为虚拟机中执行的 Java 方法服务的,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。

  • JVM 的执行流程是,首先先把 Java 代码(.java)转化成字节码(.class),然后通过类加载器将字节码加载到内存中,所谓的内存也就是我们上面介绍的运行时数据区,但字节码并不是可以直接交给操作系统执行的机器码,而是一套 JVM 的指令集。这个时候需要使用特定的命令解析器也就是我们俗称的执行引擎(Execution Engine)将字节码翻译成可以被底层操作系统执行的指令再去执行,这样就实现了整个 Java 程序的运行,这也是 JVM 的整体执行流程。

类的加载机制

符号引用和直接引用有一个重要的区别:使用符号引用时被引用的目标不一定已经加载到内存中;而使用直接引用时,引用的目标必定已经存在虚拟机的内存中了。

  • 类的生命周期会经历以下 7 个阶段:

  1. 加载阶段(Loading)
  2. 验证阶段(Verification)
  3. 准备阶段(Preparation)
  4. 解析阶段(Resolution)
  5. 初始化阶段(Initialization)
  6. 使用阶段(Using)
  7. 卸载阶段(Unloading)

加载阶段

此阶段用于查到相应的类(通过类名进行查找)并将此类的字节流转换为方法区运行时的数据结构,然后再在内存中生成一个能代表此类的 java.lang.Class 对象,作为其他数据访问的入口。

  • Java语言的类型可以分为两大类:基本类型、引用类型。基本类型是由虚拟机预先定义好的,所以不会经历单独的类加载过程。而引用类型又分为四种:类、接口、数组类、泛型参数。由于泛型参数会在编译的过程中被擦除(关于类型擦除的知识,大家可以查下资料),所以在Java中只有类、接口、数组类三种类型需要经历JVM对其进行连接和初始化的过程。

验证阶段

此步骤主要是为了验证字节码的安全性,如果不做安全校验的话可能会载入非安全或有错误的字节码,从而导致系统崩溃,它是 JVM 自我保护的一项重要举措。

  • 验证的主要动作大概有以下几个:
    • 文件格式校验包括常量池中的常量类型、Class 文件的各个部分是否被删除或被追加了其他信息等;
    • 元数据校验包括父类正确性校验(检查父类是否有被 final 修饰)、抽象类校验等;
    • 字节码校验,此步骤最为关键和复杂,主要用于校验程序中的语义是否合法且符合逻辑;
    • 符号引用校验,对类自身以外比如常量池中的各种符号引用的信息进行匹配性校验。

准备阶段

此阶段是用来初始化并为类中定义的静态变量分配内存的,这些静态变量会被分配到方法区上。这些变量所使用的内存都将在方法区(<Jdk1.8)元数据区(>=Jdk1.8)中进行分配。这时候进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化的时候随对象一起分配在Java堆中。

解析阶段

此阶段主要是用来解析类、接口、字段及方法的,解析时会把符号引用替换成直接引用。

  • 所谓的符号引用是指以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可;而直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。

  • 符号引用和直接引用有一个重要的区别:使用符号引用时被引用的目标不一定已经加载到内存中;而使用直接引用时,引用的目标必定已经存在虚拟机的内存中了。

初始化

初始化阶段 JVM 就正式开始执行类中编写的 Java 业务代码了。到这一步骤之后,类的加载过程就算正式完成了。

  • JVM 规范枚举了下述多种触发情况:
    • 当虚拟机启动时,初始化用户指定的主类;
      • 当遇到用以新建目标类实例的 new 指令时,初始化 new 指令的目标类;
      • 当遇到调用静态方法的指令时,初始化该静态方法所在的类;
      • 当遇到访问静态字段的指令时,初始化该静态字段所在的类;子类的初始化会触发父类的初始化;
      • 如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;
      • 使用反射 API 对某个类进行反射调用时,初始化这个类;
      • 当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。
阅读全文 »

工作中可能出现一个bug修复多次调教或者开发过程中需要临时保存代码到分支上的操作。但是合并主分支的时候不希望把过多的提交记录信息展示出来。这里就需要用到git rebase 命名。

案例

使用git rebase 合并多条commit

  1. 查看git log 选择要合并的记录
1
git log --oneline

2.选中一个commit id作为合并到对象,从上至下按照最新提交的记录排序。这里可以选中第三条记录。

1
git rebase -i 7893ade

  • 这里会显示可以合并的记录还有git的命令帮助信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 命令:
# p, pick <提交> = 使用提交
# r, reword <提交> = 使用提交,但修改提交说明
# e, edit <提交> = 使用提交,进入 shell 以便进行提交修补
# s, squash <提交> = 使用提交,但融合到前一个提交
# f, fixup <提交> = 类似于 "squash",但丢弃提交说明日志
# x, exec <命令> = 使用 shell 运行命令(此行剩余部分)
# b, break = 在此处停止(使用 'git rebase --continue' 继续变基)
# d, drop <提交> = 删除提交
# l, label <label> = 为当前 HEAD 打上标记
# t, reset <label> = 重置 HEAD 到该标记
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . 创建一个合并提交,并使用原始的合并提交说明(如果没有指定
# . 原始提交,使用注释部分的 oneline 作为提交说明)。使用
# . -c <提交> 可以编辑提交说明。
#
# 可以对这些行重新排序,将从上至下执行。
#
# 如果您在这里删除一行,对应的提交将会丢失。
#
# 然而,如果您删除全部内容,变基操作将会终止。
#

  • 这里将要合并的commit 前缀pick 改成 s 或者 squash 就可以开始合并了。这里使用vim :wq保存
1
2
3
pick a755a10 完善单元测试
s d3f1272 完善代码
s 547947e
  • 正常情况下,不回创建临时空间。直接将代码推送到需要存储分支就可以了。这里不直接推送主分支,建议合并过去。
1
git push -f origin branch
  • 不正常的情况,一般是合并冲突,只需要将合并解决掉, 在将合并的文件添加暂存区,在使用git rebase –continue。或者直接放弃git rebase –abort 或者直接切换到其他分支,然后删除刚才操作的文件退回rm -rf .git/rebase-merge
  • 检查git log --oneline

OpenResty

OpenResty(又称:ngx_openresty) 是一个基于 NGINX 的可伸缩的 Web 平台,由中国人章亦春发起,提供了很多高质量的第三方模块。OpenResty 是一个强大的 Web 应用服务器

安装方法

  • 通过源安装
  • 编译安装

源安装

  • 安装yum-utils
1
yum install yum-utils	
  • 添加openresty源repo
1
yum-config-manager --add-repo https://openresty.org/package/rhel/openresty.repo
  • 安装
1
yum install -y openresty
  • 启动
1
systemctl start openresty
  • 效果

  • 安装openresty命令行工具
1
yum install -y openresty-resty
  • 检查当前安装的openresty
1
/usr/local/openresty/bin/openresty -V

在mac中使用redis-cli。相比rdm我可能更喜欢这个命令行工具。

在mac中之安装redis-cli

按照标准的按照流程,需要先安装redis-server。

1
2
3
4
5
brew tap ringohub/redis-cli

brew update && brew doctor

brew install redis-cli

根据redis常用命令来学习使用redis-cli。

公司没有日志采集平台,又不太可能在业务系统里面搞事情。好在之前有玩过elk日志分析组合。直接从服务器上面撸出日志简单的用linux命令分析下吧。

Ngxin 日志格式化

  • Nginx提供的访问日志里就蕴藏着大量有用信息。今天这篇要说的就是如果修改Nginx默认日志格式,以便于我们更好的挖掘有效指标。

编辑/etc/nginx.conf配置文件,在日志部分添加下面两段代码,编辑完成后重启Nginx服务即可。

1
2
3
4
log_format main '$host - $remote_addr - [$time_local] "$request" '
'$status $upstream_response_time $request_time "$http_referer"'
'"$http_user_agent" "$http_x_forwarded_for" $body_bytes_sent ';
access_log /var/log/nginx/access.log main;

简单罗列一下变量的含义:

  • $host 访问域名
  • $remote_addr 客户端IP地址
  • $time_local 访问时间
  • $status 访问状态码
  • $upstream_response_time 应用返回到Nginx的时间
  • $request_time 请求时间
  • $http_referer 请求来源
  • $http_user_agent 访问客户端
  • $http_x_forwarded_for 客户端IP地址
  • $body_bytes_sent 返回给客户端大小

在server中不生效的问题

  • 在server中增加 access_log /var/log/nginx/access.log main;

日期显示问题

  • [01/Jul/2020:03:25:17 +0800] 官方默认是这种

  • 修改默认格式

1
2
3
4
log_format main '$host - $remote_addr - [$time_iso8601] "$request" '
'$status $upstream_response_time $request_time "$http_referer"'
'"$http_user_agent" "$http_x_forwarded_for" $body_bytes_sent ';
access_log /var/log/nginx/access.log main;
  • 在server中增加下面代码
1
2
3
4
5
6
7
8
9
10
11
12
server {

if ($time_iso8601 ~ "^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})") {
set $year $1;
set $month $2;
set $day $3;
set $hour $4;
set $minutes $5;
set $seconds $6;
}

}

Nginx日志统计

(日志文件叫access.log 在当前目录下)

统计 PV,就是日志行数

cat access.log |wc -l

UV, 即是统计 IP 数

cat access.log |awk '{print $1}' |sort |uniq -c |wc -l

使用linux grep 进行统计

grep -E "POST|GET" access.log | awk -F '"' '{print $2,$3}' | awk '{print $2}'| sort | uniq -c | sort -k1nr | head -100

耗时的请求发生时间、所请求的 URI 和耗时

cat access.log | awk '{print $4,$7,$10,$NF}' | sort -k3 -nr | head -100

统计nginx访问频次最高的100Ip**

grep -E "POST|GET" access.log | awk -F '"' '{print $(NF-1)}' | sort | uniq -c | sort -k1nr | head -100

统计nginx访问不正常(状态码400+)的前100url和频次

grep -E "POST|GET" access.log | awk -F '"' '{print $2,$3}' | awk '{if ($4>="400") {print $4,$1,$2}}' | sort | uniq -c | sort -k1nr | head -100

统计nginx访问状态码非200的前100个url和频次**

grep -E "POST|GET" access.log | awk -F '"' '{print $2,$3}' | awk '{if ($4!=200) {print $4,$1,$2}}' | sort | uniq -c | sort -k1nr | head -100

不同 URI 的平均耗时

grep -E "POST|GET" access.log | awk '{s[$10] += $NF;c[$10]++}END{for(i in s){print i,s[i]/c[i]}}' |sort -k2 -nr | head

每秒请求量统计

统计每秒的请求数,top100的时间点(精确到秒)

grep -E "POST|GET" access.log | awk '{print $4}' access.log |cut -c 14-21|sort|uniq -c|sort -nr|head -n 100

每分钟请求量统计

统计每分钟的请求数,top100的时间点(精确到分钟)

grep -E "POST|GET" access.log | awk '{print $4}' access.log |cut -c 14-18|sort|uniq -c|sort -nr|head -n 100

每小时请求量统计

统计每小时的请求数,top100的时间点(精确到小时)

grep -E "POST|GET" access.log | awk '{print $4}' access.log |cut -c 14-15|sort|uniq -c|sort -nr|head -n 100

统计蜘蛛抓取次数

grep 'Baiduspider' access.log |wc -l

好久没更新博客,最近公司让我出一个代码规范,我吓了一跳。赶忙翻出阿里《 码出高效》,不敢造次,我就补充点个人的想法吧。代码是给人看的。代码风格应该遵循极简主义

写好代码

  • 可维护性(maintainability)
    • 所谓的“维护”无外乎就是修改 bug、修改老的代码、添加新的代码之类的工作。所谓“代码易维护”就是指,在不破坏原有代码设计、不引入新的 bug 的情况下,能够快速地修改或者添加代码。
  • 可读性(readability)
    • 我们在编写代码的时候,时刻要考虑到代码是否易读、易理解。除此之外,代码的可读性在非常大程度上会影响代码的可维护性。
  • 可扩展性(extensibility)
    • 我们在不修改或少量修改原有代码的情况下,通过扩展的方式添加新的功能代码。说直白点就是,代码预留了一些功能扩展点,你可以把新功能代码,直接插到扩展点上,而不需要因为要添加一个功能而大动干戈,改动大量的原始代码。
  • 灵活性(flexibility)
    • 灵活性是一个挺抽象的评价标准。如果一段代码易扩展、易复用或者易用,我们都可以称这段代码写得比较灵活。
  • 简洁性(simplicity)
    • KISS 原则:“Keep It Simple,Stupid”。这个原则说的意思就是,尽量保持代码简单。代码简单、逻辑清晰,也就意味着易读、易维护。我们在编写代码的时候,往往也会把简单、清晰放到首位。
  • 可复用性(reusability)
    • 代码的可复用性可以简单地理解为,尽量减少重复代码的编写,复用已有的代码。在后面的很多章节中,我们都会经常提到“可复用性”这一代码评价标准。
  • 可测试性(testability)
    • 代码的可测试性差,比较难写单元测试,那基本上就能说明代码设计得有问题。

避免复杂、追求简单

日常开发中,除了解决业务问题还需要解决许多的工程问题。如何选择当下合适的方法解决问题;需要不断尝试和摸索,没有最好的方法只有更好的方法。

合理平滑的处理技术债务

技术的演变速度太快,如何避免长时间的技术债务是非常严重的问题,总之弊大于利。尽可能保持轻装上阵,避免拖油瓶项目。

关键字:

  • 一方库: 本工程内部子项目模块依赖的库(jar 包)。
  • 二方库: 公司内部发布到中央仓库,可供公司内部其它应用依赖的库(jar包)。
  • 三方库: 公司之外的开源库(jar 包)。

『避免过度封装』

特别是没有完整的技术人员编制的情况下,怎么简单怎么处理(去除中间商赚差价一个道理。)避免过度开发一方库;建议使用原生方式(综合评估代码量)避免框架过度封装。(Java的方法调用链过长是出了名的恶心)

『避免代码过度重复』

每次开发业务都会写很多的代码。定期对公司项目进行基础代码的重构。合理的拆分业务无关的基础代码

『避免版本混乱』

使用统一的版本管理。约束所有的项目jar版本依赖。防止因为过度使用三方库出现奇怪的bug。公司bom需要单独处理。尽量保持与社区版本同步,比如springboot最新版是2.1.3 ,那么公司使用的版本最好是近半年的GA版本。

『物极必反』

请勿过度依赖某框架栈或者解决方式,客观的对比相关解决方案优缺点。

『选择大于努力』

集中精力掌握核心知识。按照目前技术演变的速度,更新最快的是应用技术,其次是行业规范相关技术。最后才是革命性的技术。

阅读全文 »