抓娃小兵的Java开发记录

前言

以下记录的是小兵这几年工作中常用到的一些技术,仅为小兵一家之言。但能保证记录的内容都是小兵实践过的干货,有需要者可放心食用。之后也会持续对本文进行更新,建议收藏!

记录-编写JAR

无依赖时:

#1、写主类,并编译为.class
cat > Hello.java <<EOF
public class Hello {
    public static void main(String[] args) {
        System.out.println("hello world");
        if (args != null) {
            for (String arg : args) {
                System.out.println("param: " + arg);
            }
        }
    }
}
EOF

javac Hello.java 


#2、配置jar
cat > jar.conf <<EOF
Manifest-Version: 1.0
Main-Class: Hello
EOF

#3、打成jar(将需要打包的东西都移动到 jardir)
mkdir jardir && mv jar.conf Hello.class ./jardir 
jar -cvfm myJar.jar jardir/jar.conf -C jardir .

#4、运行jar
java -jar myJar.jar
java -jar myJar.jar k1=v1 k2=v2

有依赖时:

#1、写主类,并编译为.class
cat > Demo.java <<EOF
import redis.clients.jedis.Jedis;
public class Demo {
    public static void main(String[] args) throws Exception {
        String host = System.getProperty("host", "localhost");
        String port = System.getProperty("port", "6379");
        String cmd = System.getProperty("cmd", "set");
        String key = System.getProperty("key", "hello");
        String value = System.getProperty("value", "world");
        Jedis jedis = new Jedis("localhost");
        System.out.println( host + ":" + port + " connect success!");
        if (cmd.equals("set")) {
            jedis.set(key, value);
            System.out.println("set " + key + " = " + value);
        } else if (cmd.equals("get")) {
            System.out.println("get " + key + " = " + jedis.get(key));
        } else {
            System.out.println(cmd + ", no this operation!");
        }
        jedis.close();
    }
}
EOF

#编译前需要下载相关依赖
mkdir libs && curl -sO https://static.runoob.com/download/jedis-2.9.0.jar && mv jedis-2.9.0.jar ./libs

javac -classpath libs/*.jar Demo.java


#2、配置jar
cat > jar.conf <<EOF
Manifest-Version: 1.0
Class-Path: libs/`ls libs|xargs|sed 's/ /\n libs\//g'`
Main-Class: Demo
EOF

#3、打成jar
mkdir jardir && mv jar.conf Demo.class ./jardir 
jar -cvfm myJar.jar jardir/jar.conf -C jardir .

#4、运行jar(注意,此时依赖仍在libs中,移植时需要把依赖也同时移走,确保myJar.jar与libs处于同一目录,使用eclipse/idea打的运行时jar包是做了一层封装才没体现出依赖)
java -jar myJar.jar
java -Dcmd=get -jar myJar.jar
java -Dkey=testKey -Dvalue=testValue -jar myJar.jar 
java -Dcmd=get -Dkey=testKey -jar myJar.jar

指定 jar 运行参数的几种方式:

0)指定堆大小(默认是1G)

java -jar -Xms256m -Xmx256m demo.jar

1)通过main方法中的args获取

java -jar demo.jar k1=v1 k2=v2

2)通过System.getProperty获取

java -Dcmd=get -jar demo.jar

3)通过@value("${server.port}") 或指定默认值 @value("${server.port:8080}")获取

java -jar demo.jar --server.port=9090

记录-Docker 

相关视频教程:

尚硅谷Docker实战教程(docker教程天花板)_哔哩哔哩_bilibili

Kubernetes(K8S) 入门进阶实战完整教程,黑马程序员K8S全套教程(基础+高级)_哔哩哔哩_bilibili

docker安装要求在centos7.0以上(cat centos-release),内核版本在3.10以上(uname -r)

安装docker后,我们可以通过下载对应镜像(images),快速启动相应容器(container)。就好比下载了一个父类之后,快速地new一个实例出来,且这个new出来的实例功能直接就可以对外提供服务(如本文记录的mysql服务、redis服务、es服务、甚至jar/war服务等),一般我们可以通过docker来快速搭建我们的开发测试环境。容器多了之后,就可以使用docker-compose或者k8s来管理了。

安装docker

#1. 删除docker
yum -y remove docker-ce docker-ce-cli containerd.io
rm -rf /var/lib/docker

#2. 安装需要的安装包
yum -y install gcc gcc-c++ yum-utils
#3. 设置镜像的仓库(#默认国外,推荐使用阿里云)
yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo 

#4.更新yum软件包索引
makecache fast

#5. 安装最新版或指定版docker
yum -y install docker-ce-20.10.10 docker-ce-cli-20.10.10 containerd.io

#6. 启动docker、开机自启
systemctl start docker
systemctl enable docker

#7.使用docker version查看是否安装成功
docker --version

#8.测试hello world
docker run hello-world
#9.查看hello world镜像是否正常
docker images

docker常用命令

#查看已有镜像
docker images
#查看当前运行的容器
docker ps
#查看所有的容器
docker ps -a
#进入容器
docker exec -it container-id/name /bin/bash
#删除容器
docker rm -f container-id/name
#运行容器(镜像不存在时会自动下载)
docker run --name container-name images:tag  (多数会加 -d -p -v等参数)
#启动容器
docker start container-id/name
#停止容器
docker stop container-id/name
#重启容器
docker restart container-id/name
#查看日志:
docker logs container-id/name
#查看docker情况
docker system df
docker stats 
docker top
#更新容器
docker container update --restart=always 容器名字

更多docker内容

记录-MySQL

相关视频教程:

黑马程序员 MySQL数据库入门到精通,从mysql安装到mysql高级、mysql优化全囊括_哔哩哔哩_bilibili

现在mysql最常用的版本应该就是5.6、5.7、8.0了。mysql5.7之后的一个版本就是mysql8.0,之所以版本号跨度这么大是因为这5.7之后的版本区别确实很大,很多地方不一样了。其具体改动本文不涉及,读者可自行查阅,对于大部分开发者来说,5.7版本确实已经足矣。

安装mysql

一.卸载原有mysql
1.1检查自己的liunx是否安装过mysql,如果有的话,就删除(XXXX是自己的mysql目录)
rpm -qa | grep mysql
rpm -e --nodeps mysql-xxxx

1.2查询所有的mysql对应的文件夹
whereis mysql
find / -name mysql

#有的话删除相关目录或者文件
rm -rf /usr/bin/mysql /usr/include/mysql /data/mysql /data/mysql/mysql
   
#验证一下是否删除干净
whereis mysql
find / -name mysql


二、下载解压
cd /tmp
wget https://dev.mysql.com/get/Downloads/MySQL-5.7/mysql-5.7.35-linux-glibc2.12-x86_64.tar.gz
tar -zxvf mysql-5.7.35-linux-glibc2.12-x86_64.tar.gz
mv mysql-5.7.35-linux-glibc2.12-x86_64 /usr/local/mysql

三、修改data,并编译和初始化,新建my.cnf
mkdir /usr/local/mysql/data
cd /usr/local/mysql
./bin/mysqld --initialize --datadir=/usr/local/mysql/data --basedir=/usr/local/mysql
#以上过程出现的临时密码需要记录下来,后续要用
#新建/my.cnf配置
cat > /usr/local/mysql/my.cnf <<EOF
[mysqld]
datadir=/usr/local/mysql/data
port = 3306
sql_mode=NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES
max_connections=400
character_set_server=utf8mb4
EOF

四、启动mysql服务
/usr/local/mysql/support-files/mysql.server start
登录mysql ,并修改你的初始密码!(密码为上面生成的临时密码)
mysql -u root -p
复制下面的命令修改密码:(如123456)
set password for root@localhost = password('123456');
授权
use mysql;
update user set user.Host='%' where user.User='root';
flush privileges;

mysql多实例安装脚本(deploy_mysql.sh)参考以下:

#!/bin/bash
#初始化约定:
#安装版本:5.7.35
#使用端口:3306
#安装目录(basedir):/usr/local/mysql5.7.35
#数据目录(datadir):/data/mysql$port,即默认为 /data/mysql3306
#配置文件:存于对应数据目录下conf/my.cnf,即默认为 /data/mysql3306/conf/my.cnf
#
#输入参数:
#$1:端口号:如不输入,则使用3306端口。
#如:sh mysql_deploy.sh 或 sh mysql_deploy.sh 3307 
#
#输出:
#success:active (running)
#fail:部署失败原因

#步骤1:根据是否存在basedir来判断是否第一次安装。如果是第一次安装则下载安装mysql$version。
#步骤2:根据输入端口判断对应的datadir是否存在,存在则不允许安装,端口被占用也也不允许安装
#步骤3:生成配置文件,开始安装并通过systemd启动。(root初始化密码123456)
#步骤4:通过systemd管理mysql。即 systemctl start|stop|restart|enable|disable mysql${port}

#卸载mysql
#卸载某实例
#systemctl stop mysql${port}
#rm -rf $mysql_datadir /etc/systemd/system/mysql${port}
#全部卸载
# rm -rf /tmp/install_pkg/mysql* /usr/local/mysql* /data/mysql* /etc/systemd/system/mysql*

###########################################################################################
default_port=3306
port=$([ -n "$1" ] && echo "$1" || echo "$default_port")
version=5.7.35
init_pwd=123456
download_pkg_url=https://dev.mysql.com/get/Downloads/MySQL-5.7/mysql-${version}-linux-glibc2.12-x86_64.tar.gz
mysql_basedir=/usr/local/mysql$version
mysql_datadir=/data/mysql$port


instance_exist_check(){
    #检查该端口对应的实例datadir是否存在,检查该端口是否被占用
    if [ -d ${mysql_datadir} ];then
        if [ "`ls -A ${mysql_datadir}`" != "" ];then
            echo "fail, ${mysql_datadir} is not empty"
            exit 1
        fi
    fi

    lsof -i:${port} >/dev/null 2>&1
    if [ $? -eq 0 ];then
        echo "fail, port ${port} is used"
        exit 1
    fi
}


download_mysql_server() {
    #下载mysql安装包
    echo '>>> check whether the mysql-server installation package exists ...'
    if [ ! -d "$mysql_basedir" ]; then
        mkdir -p /tmp/install_pkg && cd /tmp/install_pkg
        if [ ! -f "mysql-$version-linux-glibc2.12-x86_64.tar.gz" ]; then
            echo ">>> download installation package from ${download_pkg_url}"
            wget $download_pkg_url
        fi
        if [ ! -f "mysql-$version-linux-glibc2.12-x86_64.tar.gz" ]; then
            echo ">>> download installation package fail!"
            exit 1
        fi
        tar -zxf mysql-$version-linux-glibc2.12-x86_64.tar.gz
        mv mysql-$version-linux-glibc2.12-x86_64 $mysql_basedir
    else
        echo "$mysql_basedir have already exists"
    fi
}


get_serverid(){
    #生成server_id,采用INET_ATON函数算法得到IP的整数值,再加上端口得到server-id
    ip=`ifconfig eth0|grep 'inet '|awk '{print $2}'`
    A=$(echo $ip | cut -d '.' -f1)
    B=$(echo $ip | cut -d '.' -f2)
    C=$(echo $ip | cut -d '.' -f3)
    D=$(echo $ip | cut -d '.' -f4)
    result=$(($A<<24|$B<<16|$C<<8|$D))
    echo $(($result + $port))
}


generate_config_file(){
mkdir -p $mysql_datadir/data
mkdir -p $mysql_datadir/conf
mkdir -p $mysql_datadir/log
mkdir -p $mysql_datadir/run
mkdir -p $mysql_datadir/tmp
mkdir -p $mysql_datadir/binlog
mkdir -p $mysql_datadir/relaylog
mkdir -p $mysql_datadir/undo
server_id=`get_serverid`
cat > $mysql_datadir/conf/my.cnf <<EOF
[client]
port            = ${port}
socket          = $mysql_datadir/run/mysql.sock
default-character-set = utf8mb4
####################################################
[mysqld]
#####basic settings
port            = ${port}
socket          = $mysql_datadir/run/mysql.sock
pid_file = $mysql_datadir/run/mysql.pid
lower_case_table_names = 0
back_log =  2048  
#
max_connections = 5000   
#
max_user_connections=4900  
max_connect_errors = 9999
transaction_isolation = READ-COMMITTED
max_allowed_packet = 1024M
open_files_limit=8192
read_only=off    
init_connect="set names utf8mb4"
skip-name-resolve
character_set_server = utf8mb4
collation-server = utf8mb4_general_ci
skip-character-set-client-handshake
secure-file-priv= $mysql_datadir/tmp
interactive_timeout = 1800  
wait_timeout = 1800  
##### log file setting
#general_log_file = $mysql_datadir/log/mysql.log 
log_error = $mysql_datadir/log/mysql-err.log
slow_query_log=1
log_queries_not_using_indexes =0
long_query_time = 1
slow_query_log_file = $mysql_datadir/log/mysql-slow.log
basedir = ${mysql_basedir}
datadir = $mysql_datadir/data
tmpdir= $mysql_datadir/tmp
 
##### query cache setting
query_cache_size = 0
query_cache_limit = 2M
query_cache_type=0 
##### binlog setting
log_bin= $mysql_datadir/binlog/binlog
binlog_format=row
binlog_cache_size = 8M
max_binlog_cache_size=4G
max_binlog_size = 1G
expire_logs_days = 7
sync_binlog=1
innodb_flush_log_at_trx_commit = 1
#####memory/performance settings
sort_buffer_size = 2M
join_buffer_size = 2M
key_buffer_size = 128M
read_buffer_size = 2M
read_rnd_buffer_size = 8M  
bulk_insert_buffer_size = 16M  
myisam_sort_buffer_size = 32M
thread_cache_size = 512
thread_stack = 512K
table_definition_cache = 4096
table_open_cache = 4096
max_length_for_sort_data = 16k
#####innodb engine setting
innodb_undo_directory= $mysql_datadir/undo
innodb_data_home_dir = $mysql_datadir/data
default_storage_engine = INNODB
innodb_file_per_table=1
innodb_buffer_pool_size = 4301M
innodb_buffer_pool_instances = 8
innodb_write_io_threads=16
innodb_read_io_threads=16
innodb_log_buffer_size = 16M
innodb_log_file_size = 256M
innodb_log_group_home_dir = $mysql_datadir/data
innodb_sort_buffer_size=32M
innodb_log_files_in_group = 4
innodb_max_dirty_pages_pct = 75
innodb_stats_on_metadata = 0
innodb_flush_method = O_DIRECT
innodb_lock_wait_timeout = 8
innodb_io_capacity = 2000   
innodb_io_capacity_max = 6000
innodb_thread_concurrency = 128
innodb_purge_threads = 4
innodb_checksum_algorithm = crc32
innodb_read_ahead_threshold = 0
innodb_use_native_aio = ON
innodb_adaptive_flushing = ON
innodb_strict_mode = 1
innodb_file_format = Barracuda
innodb_autoinc_lock_mode = 2
innodb_file_format_check = ON
innodb_online_alter_log_max_size = 100G
innodb_change_buffering = inserts
#####replication settings
server_id = ${server_id}
log_slave_updates=on
relay_log_space_limit=10G
skip-slave-start = true
gtid-mode = ON
enforce_gtid_consistency = ON
relay_log_recovery = on
master_info_repository = TABLE
relay_log_info_repository = TABLE
relay_log = $mysql_datadir/relaylog/relay
#relay_log_info_file = $mysql_datadir/relaylog/relay.info
#master_info_file = $mysql_datadir/relaylog/master.info
slave_load_tmpdir = $mysql_datadir/tmp
slave_net_timeout=180
##### for switchover
replicate-ignore-table=c2c_db.t_node_status
##### other settings
ft_min_word_len = 4
tmp_table_size = 128M
max_heap_table_size = 2048M
myisam_max_sort_file_size = 10G
myisam_repair_threads = 1
#slave_parallel_workers = 8
#open_files_limit=8192
performance_schema = OFF 
log_bin_trust_function_creators = on
sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
####################################################
[mysqldump]
quick
max_allowed_packet = 16M
####################################################
[mysql]
no-auto-rehash
EOF
}


manage_mysql_by_systemd(){
echo ">>> manage mysql by systemd ..."
egrep "^mysql" /etc/group >& /dev/null || groupadd mysql
id mysql &> /dev/null || useradd mysql -g mysql
chown mysql:mysql -R $mysql_datadir
#可另外为mysql用户设置密码,并将mysql用户加到wheel组(即可sudo)
#passwd mysql
#usermod -aG wheel mysql
# 新增systemctl管理mysql
cat > /etc/systemd/system/mysql$port.service << EOF
[Unit]
Description=MySQL Server
After=network.target
 
[Install]
WantedBy=multi-user.target
 
[Service]
User=mysql
Group=mysql
Type=forking
TimeoutSec=0
PIDFile=$mysql_datadir/run/mysql.pid
ExecStart=$mysql_basedir/bin/mysqld --defaults-file=$mysql_datadir/conf/my.cnf --user=mysql --daemonize
EOF

# 重新加载systemctl
systemctl daemon-reload 
}


install_mysql_server(){
    #初始化mysql提示缺失libnuma.so.1时,需要 yum -y install numactl.x86_64
    echo ">>> initialize mysql instance in port $port ..."
    $mysql_basedir/bin/mysqld --defaults-file=$mysql_datadir/conf/my.cnf --initialize --basedir=$mysql_basedir --datadir=$mysql_datadir/data
}


start_mysql_service(){
    #启动mysql服务(通过systemctl start|stop|status|restart mysql$port管理)
    manage_mysql_by_systemd
    echo ">>> start mysql$port service ..."
    systemctl start mysql$port
}


initlize_pwd(){
    echo ">>> initlize pwd for instance ..."
    #修改密码:临时密码在mysql错误日志中:A temporary password is generated for root@localhost: xxxxxx+x.xxxx
    tmp_pwd=`cat $mysql_datadir/log/mysql-err.log|grep 'temporary password is generated'|awk '{print $NF}'`
    echo "$mysql_basedir/bin/mysqladmin -uroot -p${tmp_pwd} password ****** -S $mysql_datadir/run/mysql.sock"
    $mysql_basedir/bin/mysqladmin -uroot -p${tmp_pwd} password ${init_pwd} -S $mysql_datadir/run/mysql.sock
    $mysql_basedir/bin/mysql -uroot -p${init_pwd} -S $mysql_datadir/run/mysql.sock -e "update mysql.user set user.Host='%' where user.User='root'; flush privileges;select user,host from mysql.user where user = 'root';"
    systemctl enable mysql$port
    systemctl status mysql$port
}


deploy_mysql_instance(){
    instance_exist_check
    download_mysql_server
    generate_config_file
    install_mysql_server
    start_mysql_service
    initlize_pwd
    exit 0
}


deploy_mysql_instance

安装mysql(docker)

#生成映射目录及配置文件
mkdir -p /data/docker/mysql23306/conf
mkdir -p /data/docker/mysql23306/data
mkdir -p /data/docker/mysql23306/log
cat > /data/docker/mysql23306/conf/my.cnf <<EOF
[client]
default_character_set = utf8mb4
[mysqld]
collation_server = utf8_general_ci
character_set_server = utf8mb4
EOF

#启动mysql实例(本机23306端口映射容器的3306端口)
docker run -d -p 23306:3306 --privileged=true -v /data/docker/mysql23306/conf:/etc/mysql/conf.d -v /data/docker/mysql23306/data:/var/lib/mysql -v /data/docker/mysql23306/log:/var/log/mysql -e MYSQL_ROOT_PASSWORD=123456 --restart=on-failure --name mysql23306 mysql:5.7

#启动/停止/重启
docker start/stop/restart mysql23306
#进入容器、登录mysql
docker exec -it mysql23306 bash
mysql -u root -p

MySQL主从同步

主从同步的前提是要开启 binlog,即在my.cnf中设置 log-bin 项。并且主从的 server-id 需不一致。

#一、主节点(master)同步设置
#创建同步帐号并授权(节点注意修改)
CREATE USER 'repl'@'%' IDENTIFIED BY 'repl_pwd';
GRANT replication SLAVE, replication client ON *.* TO 'repl'@'%';
FLUSH PRIVILEGES;
#查看主节点同步状态,记录File与Postion的内容
show master status;

#二、从节点(slave)同步设置
#进入从节点,连接master,注意MASTER_LOG_FILE、MASTER_LOG_POS与主库保持一致
change master to master_host='主节点ip',master_user='repl',master_password='repl_pwd',master_port=3306, master_log_file='mysql-bin.000001',master_log_pos=767; 
#启动从库slave进程
start slave; 
#查看是否配置成功:Slave_IO Running、Slave_SQL Running进程必须是YES状态
show slave status\G;
#在slave节点上执行,由于从库随时会提升成主库,不能写在配置文件里
set global read_only=1;

#三、测试
#进入主节点建库建表插数据,观察从节点是否已经同步
create database db1;
create table db1.tb1 (id int, name varchar(11));
insert into db1.tb1 values(1,'zs'); 

#连接从库查看:
mysql -uroot -p123456 -e "select * from db1.tb1;\G"

注:上述开启主从同步后,仅是同步MASTER_LOG_POS之后的数据。master上如果已有数据,则需另外同步到从库。方法如下:

同步主库数据步骤如下:
1、主库上一定要开启二进制日志,并且选择在主库的某个时间段使用mysqldump备份
2、将备份的数据导入到新的从库中
3、查看导出主库时的日志位置。在从库选择从这个位置开始change,并启动

#1、备份主库数据(主库上一定要开启二进制日志)
/usr/local/mysql/bin/mysql -uroot -p123456 -N -e "show databases;"|grep -Ev "information_schema|performance_schema|sys|mysql"|xargs /usr/local/mysql/bin/mysqldump -uroot -p123456 --routines --single_transaction --master-data=2 --databases > /tmp/master_bk.sql

#备份参数说明:
--排除系统库:information_schema|performance_schema|sys|mysql
--routines:导出存储过程和函数
--single_transaction:导出开始时设置事务隔离状态,并使用一致性快照开始事务,然后unlock tables;而lock-tables是锁住一张表不能写操作,直到dump完毕。
--master-data:默认等于1,将dump起始(change master to)binlog点和pos值写到结果中,等于2是将change master to写到结果中并注释。

#2、将备份数据导入到新从库中
scp /tmp/master_bk.sql root@slaveip:/tmp

#3、登录从库载入主节点数据,并开启主从。其中的master_log_file|pos在master_bk.sql中(30行附近)。
/usr/local/mysql/bin/mysql -uroot -p123456
> source /tmp/master_bk.sql;
> change master to master_host='主节点ip',master_user='repl',master_password='repl_pwd',master_port=3306, master_log_file='mysql-bin.000001',master_log_pos=767;
> start slave;
> set global read_only = 1; 

 记录-后台开发

相关视频教程:

黑马程序员JVM完整教程,Java虚拟机快速入门,全程干货不拖沓_哔哩哔哩_bilibili
黑马程序员Java进阶教程Tomcat核心原理解析_哔哩哔哩_bilibili
2021版最新SpringBoot2_权威教程_请直接从P112开始学习新版视频--置顶评论有直达链接-_雷丰阳_尚硅谷_哔哩哔哩_bilibili

安装JDK

#1.下载jdk tar.gz格式压缩包
cd /tmp
wget --no-cookies --no-check-certificate --header "Cookie: gpw_e24=http%3A%2F%2Fwww.oracle.com%2F; oraclelicense=accept-securebackup-cookie" "http://download.oracle.com/otn-pub/java/jdk/8u141-b15/336fa29ff2bb4ef291e347e091f7f4a7/jdk-8u141-linux-x64.tar.gz"
#2.解压压缩包
tar -zxvf jdk-8u141-linux-x64.tar.gz -C /usr/local
#3.修改配置文件
vim /etc/profile
export JAVA_HOME=/usr/local/jdk1.8.0_141
export JRE_HOME=$JAVA_HOME/jre
export CLASSPATH=.:$JAVA_HOME/lib:$JRE_HOME/lib
export PATH=$PATH:$JAVA_HOME/bin
#4.刷新配置文件
source /etc/profile
#5.验证jdk1.8是否安装成功
java -version

卸载JDK

#查看java安装的目录:
which java
#删除安装的文件:
rm -rf /usr/local/jdk1.8.0_141
#删除(java相关)环境变量:
vim /etc/profile
#刷新配置文件:
source /etc/profile

安装JDK(yum方式)

#默认安装在 /usr/lib/jvm
#jdk8
yum install -y java-1.8.0-openjdk*
#jdk11
yum -y install java-11-openjdk*
#查看版本
java -version

#删除jdk
yum -y remove java-1.8.0-openjdk*
yum -y remove tzdata-java.noarch 

#注意:通过 yum 安装 jdk,是不会自动配置 JAVA_HOME 环境变量的。如果有一些服务依赖这个环境变量就会启动失败。可通过以下方式找到jdk位置并设置JAVA_HOME:
ls -lrt /etc/alternatives/java
#以上命令可找出jre的位置,截取前面位置即得jdk位置
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.312.b07-1.el7_9.x86_64
然后同样对vim /etc/profile进行设置

ps: 

vim /etc/profile后,新开终端或电脑重启之后就无效了
解决办法: vim /etc/bashrc 然后在最后加上环境变量内容 source /etc/profile

安装Tomcat

# 1、下载
cd /tmp
wget --no-check-certificate https://dlcdn.apache.org/tomcat/tomcat-8/v8.5.84/bin/apache-tomcat-8.5.84.tar.gz
# 2、解压
tar -zxvf apache-tomcat-8.5.56.tar.gz
mv apache-tomcat-8.5.84 /opt/tomcat8.5.84
# 3、启动
cd /opt/tomcat8.5.84
./bin/startup.sh
# 4、停止
./bin/shutdown.sh

#查看日志
tail -200f logs/catalina.out


## 默认tomcat是使用8080端口,如机器有多个tomcat,需要修改tomact端口:
vim conf/server.xml
找到以下三个端口(shutdown、http connect、ajp connect):
Server port="8005"  Connector port="8080"  Connector port="8009"
分别修改为其它端口如:
Server port="8006"  Connector port="8081"  Connector port="8010"

tomcat实战-优化

<!-- tomcat优化1:设置JVM参数
#如果没有配置JAVA_OPTS变量,JVM在启动的时候会自动设置Heap size的值,其初始空间(即-Xms)是物理内存的1/64,最大空间(-Xmx)是物理内存的1/4。可以利用JVM提供的-Xmn -Xms -Xmx等选项可进行设置。默认一个线程大小为1M(-Xss控制)。
所以可以 vim bin/catalina.sh  在配置(/# OS)前加上以下配置: -->
JAVA_OPTS="-Xms1024M -Xmx1024M -Xmn512M -Dspring.profiles.active=test"


<!-- tomcat优化2:优化连接器
tomcat8080端口连接器默认为:
<Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />

#连接数相关的参数配置和优化如下:
1.maxThreads:Tomcat使用线程来处理接收的每个请求。这个值表示Tomcat可创建的最大的线程数。默认值200。可以根据机器的时期性能和内存大小调整,一般可以在400-500。最大可以在1000左右。
2.acceptCount:指定当所有可以使用的处理请求的线程数都被使用时,可以放到处理队列中的请求数,超过这个数的请求将不予处理。默认值100。
3.minSpareThreads:Tomcat初始化时创建的线程数。默认值10。
4.maxSpareThreads:最大备用线程数,一旦创建的线程超过这个值,Tomcat就会关闭不再需要的socket线程。默认值是-1(无限制)。一般不需要指定。
5.enableLookups:是否反查域名,默认值为true。为了提高处理能力,应设置为false。
6.connectionTimeout:网络连接超时,默认值20000,单位:毫秒。设置为0表示永不超时,这样设置有隐患的。通常可设置为30000毫秒。
7.compression:gzip压缩传输,取值on/off/force,默认值off,
8.compressionMinSize:表示压缩响应的最小值,只有当响应报文大小大于这个值的时候才会对报文进行压缩,如果开启了压缩功能,默认值就是 2048。
根据以往经验,常用参数配置如下: -->
<Connector port="8080"   
          protocol="HTTP/1.1"   
          connectionTimeout="20000" 
          redirectPort="8443"   
          minSpareThreads="50" 
          enableLookups="false" 
          acceptCount="300" 
          maxThreads="500" 
          URIEncoding="UTF-8" 
          compression="on" 
          compressionMinSize="2048" 
          compressableMimeType="application/json,application/javascript,text/html,text/xml,text/css,text/plain,text/json"/>
			  
<!--  tomcat优化3:禁用AJP协议
一般都是使用Nginx+tomcat的架构,用不着AJP协议,所以可以直接把AJP连接器禁用 -->
<!-- <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />-->

tomcat实战-使用脚本自动发布

cat > autoDeployTomcat.sh <<EOF
#! /bin/sh
#
#自动化部署使用方法:
# 1)在tomcat根路径下建文件夹,如war
# 2)在第一步创建的文件夹下放入此脚本(可以添加权限:chmod +x autoDeploy.sh)
# 3)把需要部署war包放到此文件夹下,执行此脚本即可自动部署并备份。
#
echo '####################开始自动部署####################'
#当前路径
cpath=`pwd`
echo "当前路径: " $cpath
wpath=${cpath##*/}
echo "当前war文件夹:" $wpath
#获取tomcat路径
tpath=$(dirname `pwd`)
echo "TomcatPath:"  $tpath
cd $tpath
#获取tomcat名称
tname=${tpath##*/}
echo "TomcatName:" $tname
#查看tomcat是否在运行
PID=$(ps -ef |grep tomcat |grep -w $tname|grep -v 'grep'|awk '{print $2}')
if [ -z "$PID" ];then
  echo "No tomcat process is running"
else
  #如果在运行,则停止tomcat服务
  echo "Tomcat is running, shutdown now! PID:" $PID
 ./bin/shutdown.sh
  sleep 2
fi

#将原来的war包备份
echo "Backup the old tomcat instance"
mkdir -p ./$wpath/warbak
mv ./webapps/ROOT.war ./$wpath/warbak/ROOT.war.$(date +%Y%m%d%H%M)-bak
bk_num=`ls ./$wpath/warbak|wc -l`
#最大备份数
max_bk_num=100
if [ "$bk_num" -ge "$max_bk_num" ]; then
    echo '#########################################################'
    echo '##################### 警告 ##############################'
    echo '############ 备份数据过多,开始清理旧数据!##############'
    echo '############ 备份数据过多,开始清理旧数据!##############'
    echo '############ 备份数据过多,开始清理旧数据!##############'
    echo '#########################################################'
    echo '#########################################################'
    for i in `ls -tr ./$wpath/warbak|head -20`
    do
        rm -rf "./$wpath/warbak/$i"
    done;
fi
cp ./$wpath/ROOT.war ./webapps/
sleep 1
if kill -0 "$PID" 2>/dev/null;then
  kill -9 $PID
fi
rm -rf ./webapps/ROOT
#重新启动tomcat
echo "Tomcat is restart..."
./bin/startup.sh
echo '####################部署结束####################'
sleep 5
echo '查看启动日志'
tail -300 ./logs/catalina.out
EOF

CURD(后端)

最基本的后台开发可以认为是由 springboot 2.3+ mysql 实现基本的CURD。这也是我们程序猿大部分时间的工作,其它后续工作基本都是在CURD的基础上完成。

配套代码v1.0.0分支便是记录一个最基础的 curd 项目,代码内容仅包括:代码生成工具 + 提供CURD接口(swagger页面展示 /swagger-ui.html)

代码生成工具

<!-- pom.xml -->
<dependency>
	<groupId>com.baomidou</groupId>
	<artifactId>mybatis-plus-boot-starter</artifactId>
	<version>3.5.2</version>
</dependency>

<dependency>
	<groupId>com.baomidou</groupId>
	<artifactId>mybatis-plus-generator</artifactId>
	<version>3.5.2</version>
</dependency>
<dependency>
	<groupId>org.apache.velocity</groupId>
	<artifactId>velocity-engine-core</artifactId>
	<version>2.0</version>
</dependency>
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.OutputFile;
import com.baomidou.mybatisplus.generator.config.rules.DateType;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.fill.Column;

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * 代码生成工具类
 *
 * @author jvxb
 * @since 2022-12-10
 */
public class CodeGenerateUtil {
    //  配置需要生成的表名(多个用逗号隔开)、数据库连接
    private static final String TABLES = "sys_user";
    private static final String URL = "jdbc:mysql://localhost:3306/demo?useUnicode=true&serverTimezone=GMT%2B8&characterEncoding=utf8&useSSL=false";
    private static final String USERNAME = "root";
    private static final String PASSWORD = "123456";
    private static final String AUTHOR = "jvxb";
    //  -----------------------------------------------------------------------------
//  是否开启覆盖已生成的 Entity 和 Mapper文件(为防止覆盖原有方法,默认不允许覆盖。字段变化时建议手动处理)
  private static final boolean OVERRIDE_ENTITY_MAPPER = false;
//    private static final boolean OVERRIDE_ENTITY_MAPPER = true;
    //  -----------------------------------------------------------------------------
//  当前项目路径,父包名与模块名(projectDir\src\main\java下的模块)
    private static final String projectDir = System.getProperty("user.dir");
    private static final String PARENT_PACKAGE_NAME = "com.jvxb";
    private static final String MODULE_NAME = "demo";

    public static void main(String[] args) {
        //生成代码
        generateCode();
        //删除代码
//        deleteCode();
    }

    public static void generateCode() {
        FastAutoGenerator.create(URL, USERNAME, PASSWORD)
                .globalConfig(builder -> {
                    builder.author(AUTHOR) // 设置作者
                            .enableSwagger() // 开启 swagger 模式
                            .disableOpenDir() //生成代码后不打开目录
                            .dateType(DateType.ONLY_DATE) //使用 Date
                            .outputDir(projectDir + "\\src\\main\\java"); // 指定输出目录
                })
                .packageConfig(builder -> {
                    builder.parent(PARENT_PACKAGE_NAME) // 设置父包名
                            .moduleName(MODULE_NAME) // 设置父包模块名
                            .pathInfo(Collections.singletonMap(OutputFile.xml,
                                    projectDir + "\\src\\main\\resources\\mapper")); // 设置mapperXml生成路径
                })
                .strategyConfig(builder -> {
                    builder.addInclude(TABLES);
                    builder.controllerBuilder()
                            .enableRestStyle();
                    builder.serviceBuilder()
                            .formatServiceFileName("%sService");
                    builder.entityBuilder()
                            .addTableFills(new Column("create_time", FieldFill.INSERT))
                            .addTableFills(new Column("update_time", FieldFill.INSERT_UPDATE))
                            .enableColumnConstant()
                            .enableLombok();
                    builder.mapperBuilder()
                            .enableBaseResultMap() // 开启BaseResultMap
                            .enableBaseColumnList(); // 开启BaseColumnList
                    if (OVERRIDE_ENTITY_MAPPER) {
                        builder.entityBuilder().fileOverride();
                        builder.mapperBuilder().fileOverride();
                    }
                })
                .execute();
    }


    private static void deleteCode() {
        for (String table : TABLES.split(",")) {
            String entityName = NamingStrategy.capitalFirst(NamingStrategy.underlineToCamel(table));
            String parentPackageName = PARENT_PACKAGE_NAME.replace(".", "\\");
            List<String> pathArr = new ArrayList<>();
            pathArr.add(String.format("%s\\src\\main\\java\\%s\\%s\\controller\\%sController.java", projectDir, parentPackageName, MODULE_NAME, entityName));
            pathArr.add(String.format("%s\\src\\main\\java\\%s\\%s\\service\\%sService.java", projectDir, parentPackageName, MODULE_NAME, entityName));
            pathArr.add(String.format("%s\\src\\main\\java\\%s\\%s\\service\\impl\\%sServiceImpl.java", projectDir, parentPackageName, MODULE_NAME, entityName));
            pathArr.add(String.format("%s\\src\\main\\java\\%s\\%s\\entity\\%s.java", projectDir, parentPackageName, MODULE_NAME, entityName));
            pathArr.add(String.format("%s\\src\\main\\java\\%s\\%s\\mapper\\%sMapper.java", projectDir, parentPackageName, MODULE_NAME, entityName));
            pathArr.add(String.format("%s\\src\\main\\resources\\mapper\\%sMapper.xml", projectDir, entityName));
            for (String path : pathArr) {
                File file = new File(path);
                if (file.exists()) {
                    System.out.println(String.format("delete file [%s] ...", file.getAbsolutePath()));
                    file.delete();
                } else {
                    System.out.println(String.format("file [%s] does not exist", file.getAbsolutePath()));
                }
            }
        }
    }
}

swagger配置

<! -- pom.xml -->
<dependency>
	<groupId>io.springfox</groupId>
	<artifactId>springfox-swagger2</artifactId>
	<version>2.9.2</version>
	<exclusions>
		<exclusion>
			<groupId>io.swagger</groupId>
			<artifactId>swagger-models</artifactId>
		</exclusion>
	</exclusions>
</dependency>
<dependency>
	<groupId>io.swagger</groupId>
	<artifactId>swagger-models</artifactId>
	<version>1.5.22</version>
</dependency>
<dependency>
	<groupId>io.springfox</groupId>
	<artifactId>springfox-swagger-ui</artifactId>
	<version>2.9.2</version>
</dependency>
@Configuration
@EnableSwagger2
@Profile({"dev", "test"})
public class SwaggerConfig {

    @Bean
    public Docket Docket() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(ApiInfo())
                .select()
                .apis(RequestHandlerSelectors.withClassAnnotation(Api.class))
                .paths(Predicates.not(PathSelectors.regex("/error.*")))
                .build();
    }

    private ApiInfo ApiInfo() {
        return new ApiInfoBuilder().title("标题:抓娃小兵测试项目的接口文档")
                .description("这是一段关于接口文档的简介")
                .build();
    }
}

RestController

@Api(tags = "系统用户")
@RestController
@RequestMapping("/demo/sysUser")
public class SysUserController extends BaseCURDController<SysUser> {

    @Autowired
    private SysUserService sysUserService;

    @ApiOperation("查找列表")
    @GetMapping("/getList")
    public ResponseDataVo<List<SysUser>> getList(String keyword) {
        QueryWrapper wrapper = new QueryWrapper();
        wrapper.like(StrUtil.isNotBlank(keyword), SysUser.USER_NAME, keyword);
        List<SysUser> list = sysUserService.list(wrapper);
        return ResponseDataVo.success(list);
    }

}


/**
 * <p>================================================
 * <p>Title: 基础 CURD 控制器
 * <p>Description: 通过继承本类,子类控制器可以直接使用增删改查接口。
 * <p>================================================
 *
 * @author jvxb
 * @version 1.0
 */
public abstract class BaseCURDController<T> {

    protected IService<T> commonService;

    @Autowired
    public void setCommonService(IService<T> commonService) {
        this.commonService = commonService;
    }

    @ApiOperation("新增")
    @PostMapping("/save")
    public ResponseDataVo<T> save(@RequestBody T t) {
        commonService.save(t);
        return ResponseDataVo.success(t);
    }

    @ApiOperation("修改")
    @PostMapping("/update")
    public ResponseDataVo<Void> update(@RequestBody T t) {
        commonService.updateById(t);
        return ResponseDataVo.success();
    }

    @ApiOperation("删除id")
    @PostMapping("/delete/{id}")
    public ResponseDataVo<Void> delete(@PathVariable("id") String id) {
        commonService.removeById(id);
        return ResponseDataVo.success();
    }

    @ApiOperation("查找id")
    @GetMapping("/get/{id}")
    public ResponseDataVo<T> get(@PathVariable("id") String id) {
        return ResponseDataVo.success(commonService.getOne(new QueryWrapper<T>().eq("id", id)));
    }

}

记录-前端开发(vue)

参考地址:vue3官方文档

安装node

#1、下载
cd /tmp
wget https://npm.taobao.org/mirrors/node/v14.17.0/node-v14.17.0-linux-x64.tar.gz
#2、解压
tar -zxvf node-v14.17.0-linux-x64.tar.gz -C /usr/local
#3、配置路径
cd /usr/local/node-v14.17.0-linux-x64
ln -s /usr/local/node-v14.17.0-linux-x64/bin/npm /usr/bin/npm
ln -s /usr/local/node-v14.17.0-linux-x64/bin/node /usr/bin/node 
vim /etc/profile
NODE_HOME=/usr/local/node-v14.17.0-linux-x64
PATH=$NODE_HOME/bin:$PATH
source /etc/profile
#4、检测
node -v
npm -v

CURD(前端)

创建项目( 参考 Vue3中文文档

# npm使用淘宝镜像,并验证是否成功
npm config set registry https://registry.npm.taobao.org
npm config get registry

# 通过npm init vite-app <project-name> 即可初始化项目,如:
npm init vite-app demo-web
cd demo-web
npm install
npm run dev

# 按提示访问页面
localhost:3000


#注:如果需要取消淘宝镜像:
npm config set registry https://registry.npmjs.org

CURD页面示例:

#1、安装elementUi依赖
npm install element-plus --save
npm install axios --save

#2、在main.js使用elementUi
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'

const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')


#3、将HelloWorld页面改为如下CURD页面:
<template>
    <div style="padding: 1rem 1rem;display: flex; justify-content: space-between; align-items: flex-start">
        <el-form :inline="true" class="demo-form-inline" @submit.native.prevent>
            <el-form-item>
                <el-input v-model="searchOpt.keyword" placeholder="用户名称/号码" clearable
                          @keydown.enter.native="search"></el-input>
            </el-form-item>

            <el-form-item>
                <el-config-provider :locale="locale">
                    <el-date-picker
                            v-model="searchOpt.createTime"
                            type="daterange"
                            range-separator="至"
                            start-placeholder="开始日期"
                            end-placeholder="结束日期"
                            value-format="YYYY-MM-DD"
                    />
                </el-config-provider>
            </el-form-item>

            <el-form-item>
                <el-button type="primary" @click="search">查询</el-button>
            </el-form-item>
        </el-form>
        <el-form :inline="true" class="demo-form-inline">
            <el-button @click="operateUser({}, '新增用户')" type="primary" style="margin: .5rem">添加用户</el-button>
        </el-form>
    </div>
    <div>
        <el-table :data="tableData"
                  element-loading-text="加载中"
                  border
                  style="width: 100%">
            <el-table-column type="selection" width="55"/>
            <el-table-column prop="loginName" label="登录名"/>
            <el-table-column prop="userName" label="用户名称"/>
            <el-table-column prop="roleName" label="所属角色"/>
            <el-table-column prop="phone" label="手机号"/>
            <el-table-column label="是否有效">
                <template #default="scope">
                    {{ scope.row.valid === 0 ? '否' : '是' }}
                </template>
            </el-table-column>
            <el-table-column prop="createTime" label="创建时间"/>
            <el-table-column fixed="right" label="操作" width="220">
                <template #default="scope">
                    <el-button type="info" size="small" @click="operateUser(scope.row, '查看用户')">查看
                    </el-button>
                    <el-button type="primary" size="small" @click="operateUser(scope.row, '编辑用户')">编辑
                    </el-button>
                    <el-button type="danger" size="small" @click="del(scope.row.id)">删除</el-button>
                </template>
            </el-table-column>
        </el-table>
    </div>
    <div>
        <el-dialog :title="dialogName"
                   v-model="dialogVisible"
                   :before-close="handleClose"
                   width="50%">
            <div style="background: white; padding: 24px">
                <el-form label-width="130px" :model="formData" ref="formData" style="width: 70%; margin-left: 10%">
                    <el-form-item>
                        <template #label>
                            <span class="require">*</span>登录名
                        </template>
                        <el-input v-model="formData.loginName"/>
                    </el-form-item>
                    <el-form-item>
                        <template #label>
                            <span class="require">*</span>登录密码
                        </template>
                        <el-input type="password" v-model="formData.password"></el-input>
                    </el-form-item>
                    <el-form-item>
                        <template #label>
                            <span class="require">*</span>用户姓名
                        </template>
                        <el-input v-model="formData.userName"></el-input>
                    </el-form-item>
                    <el-form-item>
                        <template #label>
                            手机号
                        </template>
                        <el-input v-model="formData.phone"></el-input>
                    </el-form-item>
                    <el-form-item>
                        <template #label>
                            账号是否有效
                        </template>
                        <el-radio v-model="formData.valid" :label="1">是</el-radio>
                        <el-radio v-model="formData.valid" :label="0">否</el-radio>
                    </el-form-item>
                    <el-form-item label="所属角色">
                        <el-select v-model="formData.roleId" placeholder="请选择用户角色" @change="getRoleKeyValue">
                            <el-option
                                    v-for="role in roleData"
                                    :key="role.roleName"
                                    :value="role.roleId"
                                    :label="role.roleName"
                            />
                        </el-select>
                    </el-form-item>
                    <el-form-item v-if="dialogName === '新增用户'">
                        <el-button type="primary" @click="saveOrUpdate">新增</el-button>
                    </el-form-item>
                    <el-form-item v-if="dialogName === '编辑用户'">
                        <el-button type="primary" @click="saveOrUpdate">保存</el-button>
                    </el-form-item>
                </el-form>
            </div>
        </el-dialog>
    </div>
</template>

<script>
    import axios from "axios";

    export default {
        data() {
            return {
                searchOpt: {keyword: '', createTime: ''},
                dialogName: '',
                dialogVisible: false,
                formData: {},
                roleData: [{'roleName': 'admin', 'roleId': 1}, {'roleName': 'dev', 'roleId': 2}, {
                    'roleName': 'test',
                    'roleId': 3
                }],
                tableData: []
            }
        },
        mounted() {
            this.loadData();
        },
        methods: {
            loadData: function () {
                let _this = this;
                axios.get('http://localhost:8080/demo/sysUser/getList' + _this.buildQuery(_this.searchOpt))
                    .then(resp => {
                        _this.tableData = resp.data.data;
                    }).catch(error => {
                    console.log(error);
                });
            },
            operateUser: function (row, type) {
                this.dialogName = type;
                this.formData = JSON.parse(JSON.stringify(row));
                this.dialogVisible = true;
            },
            search: function () {
                this.loadData();
            },
            handleClose: function () {
                this.dialogName = '';
                this.dialogVisible = false;
                this.formData = {};
            },
            getRoleKeyValue: function (kid) {
                for (var idx in this.roleData) {
                    if (this.roleData[idx].roleId === kid) {
                        this.formData.roleName = this.roleData[idx].roleName;
                        break;
                    }
                }
            },
            saveOrUpdate: function () {
                let type = this.formData.id ? 'update' : 'save';
                let _this = this;
                axios.post("http://localhost:8080/demo/sysUser/" + type, this.formData)
                    .then(resp => {
                        _this.dialogVisible = false;
                        _this.loadData();
                    }).catch(error => {
                    console.log(error);
                });
            },
            del: function (e) {
                let _this = this;
                axios.post("http://localhost:8080/demo/sysUser/delete/" + e)
                    .then(resp => {
                        console.log(resp);
                        _this.loadData();
                    }).catch(error => {
                    console.log(error);
                });
            },
            buildQuery: function(params) {
                let paramsArray = [];
                Object.keys(params).forEach(key => paramsArray.push(key + '=' + params[key]));
                return '?' + paramsArray.join('&');
            }
        }
    }
</script>
<style scoped>
    .require {
        color: red;
        padding-right: 5px;
    }
</style>

 记录-DevOps

相关视频教程: 

黑马程序员Java教程自动化部署Jenkins从环境配置到项目开发_哔哩哔哩_bilibili

DevOps来自Development(开发)和Operations(运维)的缩写,是一组为了能够实现更快、更可靠的的发布更高质量的产品的过程和方法的统称。

用于促进应用开发、应用运维和质量保障(QA)部门之间的沟通、协作与整合。

 CI的英文名称是Continuous Integration(持续集成)。

持续集成(CI)是在源代码变更后自动检测、拉取、构建和(在大多数情况下)进行单元测试(自动化测试)的过程,从而确定新代码和原有代码能否正确地集成在一起。

CD分为Continuous Delivery(持续交付)和Continuous Deployment(持续部署)

CI、CD是实现DevOps的方法。

Jenkins就是一个开源的、提供友好操作界面的持续集成(CI)工具。

安装mvn

#1、下载
cd /tmp
wget --no-check-certificate https://archive.apache.org/dist/maven/maven-3/3.6.1/binaries/apache-maven-3.6.1-bin.tar.gz

#2、解压
tar -zxvf apache-maven-3.6.1-bin.tar.gz -C /usr/local

#3、设置setting(镜像地址、下载地址等)
cd /usr/local/apache-maven-3.6.1
mkdir repo
vim conf/settings.xml (:set paste    :set nopaste)
<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
  <localRepository>/opt/apache-maven-3.6.1/repo</localRepository>
  <pluginGroups></pluginGroups>
  <proxies></proxies>
  <servers></servers>
  <mirrors>
     <mirror>
         <id>alimaven</id>
         <name>aliyun maven</name>
         <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
         <mirrorOf>central</mirrorOf>
     </mirror>
  </mirrors>
  <profiles>
	    <profile>  
	        <id>jdk18</id>  
	        <activation>  
	            <activeByDefault>true</activeByDefault>  
	            <jdk>1.8</jdk>  
	        </activation>  
	        <properties>  
	            <maven.compiler.source>1.8</maven.compiler.source>  
	            <maven.compiler.target>1.8</maven.compiler.target>  
	            <maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion>  
	        </properties>   
	    </profile>  
 </profiles>
</settings>

#4、配置mvn路径
vim /etc/profile
export MAVEN_HOME=/usr/local/apache-maven-3.6.1
export PATH=$PATH:$MAVEN_HOME/bin
source /etc/profile

#5、检测
mvn -version

安装git

yum方式最简单快速,但是版本不可选。

#安装git
yum -y install git
#查看版本
git --version

可以通过源码方式安装任意版本

#1、下载
cd /tmp
wget --no-check-certificate https://mirrors.edge.kernel.org/pub/software/scm/git/git-2.21.0.tar.gz
#2、安装依赖
yum -y install curl-devel expat-devel gettext-devel openssl-devel zlib-devel gcc perl-ExtUtils-MakeMaker
#3、解压
tar -zxvf git-2.21.0.tar.gz -C /usr/local
#4、编译/安装
cd /usr/local/git-2.21.0
./configure --prefix=/usr/local/git
make && make install
#5、配置变量
# 删除已有的 git
yum remove git
vim /etc/profile 
GIT_HOME=/usr/local/git
export PATH=$PATH:$GIT_HOME/bin
export PATH=$PATH:$GIT_HOME/libexec/git-core/
source /etc/profile
#6、查看版本
git --version

#拉取代码
git clone https://gitee.com/jvxb/demo.git

如需push代码,则需配置git帐号与提交公钥:

#查看git帐号邮箱信息
git config user.name
git config user.email

#修改用户名和邮箱地址
git config --global user.name jvxb
git config --global user.email jvxb@qq.com

%% 查看相关配置信息
git config --list

配置记住用户验证信息
git config --global credential.helper store

#生成密钥(公钥),三次回车后保留密钥到(/root/.ssh/id_rsa 和 /root/.ssh/id_rsa.pub)
ssh-keygen -t rsa -C "jvxb@qq.com"

#添加公钥:打开gitee/GitHub的setting->ssh setting -> add ssh key
粘贴到以下内容(不要后面的邮箱):cat /root/.ssh/id_rsa.pub


#测试
ssh -T git@github.com
ssh -T git@gitee.com

#克隆
git clone https://gitee.com/jvxb/demo.git

安装jenkins

安装 jenkins 有docker、war、rmp、yum等方式。其中yum、war包的方式容易受jdk版本影响(较新版本需要JDK11及以上),此处选择使用rpm方式部署(jdk8可支持)。

卸载 jenkins
rpm -e jenkins
find / -iname jenkins | xargs -n 1000 rm -rf
#1、下载
wget --no-check-certificate https://repo.huaweicloud.com/jenkins/redhat/jenkins-2.305-1.1.noarch.rpm
#2、安装
rpm -ivh jenkins-2.305-1.1.noarch.rpm

安装完以后重要的目录说明:
  /usr/lib/jenkins/jenkins.war    WAR包 
  /etc/sysconfig/jenkins       配置文件
  /var/lib/jenkins/       默认的JENKINS_HOME目录
  /var/log/jenkins/jenkins.log    Jenkins日志文件
  
#3.修改配置文件
修改用户名为root和修改端口为9090,否则容易爆权限不足或端口冲突
sed -i 's/JENKINS_USER="jenkins"/JENKINS_USER="root"/;s/JENKINS_PORT="8080"/JENKINS_PORT="9090"/' /etc/sysconfig/jenkins

4.配置git/mvn/jdk路径
jendkins后续的拉取、打包、运行代码需要git/maven/java环境,可将它们软连接/usr/bin:
ln -s "$JAVA_HOME/bin/java" /usr/bin/java 
ln -s "$MAVEN_HOME/bin/mvn" /usr/bin/mvn
ln -s "$GIT_HOME/bin/git" /usr/bin/git

5、启动并设置开机自启
systemctl start jenkins
/sbin/chkconfig jenkins on

6、访问jenkins,
页面输入 ip:port,第一次须按提示找到密码输入
cat /var/lib/jenkins/secrets/initialAdminPassword

#注意:有防火墙记得开放端口
firewall-cmd --zone=public --list-ports
firewall-cmd --zone=public --add-port=9090/tcp --permanent
firewall-cmd --reload

7、新手入门:安装插件
先跳过,后续再安装:选择合适的插件 -> 无 -> 安装。
手工安装插件前可先更新站点:Manage Jenkins -> Manage Plugins -> Advanced -> Update Site -> 替换为以下地址(后最好重启): 
https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json
#安装中文插件
Manage Plugins -> Available:
* 输入 local:找到 Localization: Chinese (Simplified)&Localization Support,安装并重启
* 输入 Maven Integration 安装maven插件
注:如果不能通过页面安装插件,可自行下载后再在[系统管理→插件管理→高级→上传]中上传phi文件

#关闭安全警告、过期插件、升级通知等,取消勾选以下配置:
系统管理->系统配置->使用统计
系统管理->系统配置->管理监控配置->插件/Plugin/更新通知
系统管理->全局安全配置->隐藏的安全警告(全部取消勾选)

8、开始使用:新建项目流水线
新建任务 -> 自由风格 -> 输入项目描述 -> Build -> 构建如下:
git --version
maven -version
java -version

jenkins构建实战:拉取代码并使用 java -jar 启动

BUILD_ID=dontKillMe
project_name=demo
project_branch=master
project_port=8080
cicd_dir=/data/cicd
project_addr="https://gitee.com/jvxb/$project_name.git"
echo "================================"
echo "project_name = $project_name, project_port = $project_port, branch = $project_branch"
echo "================================"
mkdir -p $cicd_dir
cd $cicd_dir
if [ -d "$project_name" ]; 
then
    cd $project_name/
    git pull 
else
    git clone $project_addr 
    cd $project_name/
fi
git checkout $project_branch
mvn clean package
pid=`lsof -i:$project_port|awk 'NR == 2 {print $2}'`
if [[ -n "$pid" ]];
then
    echo "exists pid $pid, execute kill/shutdown and restart"
    kill -9 $pid
else
    echo "no pid in port $project_port, ready start .."
fi 
#通过jar命令启动
cd target/
nohup java -jar ROOT.war --server.port=$project_port > nohup.out 2>&1 &

jenkins构建实战:拉取代码并使用 tomcat 启动

BUILD_ID=dontKillMe
project_name=demo
project_branch=master
cicd_dir=/data/cicd
tomcat_dir=/opt/tomcat8.5.84
project_addr="https://gitee.com/jvxb/$project_name.git"
echo "===================================================="
echo "project_name = $project_name, branch=$project_branch"
echo "===================================================="
mkdir -p $cicd_dir
cd $cicd_dir
if [ -d "$project_name" ]; 
then
    cd $project_name/
    git pull 
else
    git clone $project_addr 
    cd $project_name/
fi
git checkout $project_branch
mvn clean package
mkdir -p $tomcat_dir/war
mv target/ROOT.war $tomcat_dir/war
cd $tomcat_dir/war
sh autoDeploy.sh

jenkins构建实战:拉取代码并使用npm运行

BUILD_ID=dontKillMe
project_name="simple-web"
project_branch=master
cicd_dir=/data/cicd
dist=/data/meet_u/dist
mkdir -p $cicd_dir
mkdir -p $dist
project_addr="https://gitee.com/jvxb/$project_name.git"
echo "===================================================="
echo "project_name = $project_name, branch=$project_branch"
echo "===================================================="
mkdir -p $cicd_dir
cd $cicd_dir
if [ -d "$project_name" ]; 
then
    cd $project_name/
    git pull 
else
    git clone $project_addr 
    cd $project_name/
fi
npm install
npm run build-prd 
cp -rf ./dist/* $dist

上述构建过程只是手动输入来指定了git的分支并在本机上运行jar/war,一般来说,jenkins需要从某个git源下载代码并打包成jar/war/dist并发送到远端机器去部署。

上述过程需要用到以下插件:Maven IntegrationVersion、GitLab、GitLab API Plugin(由于jenkins版本不同,叫法会略有不同,大体上是一样的),并且需要在Manage Jenkins 中添加主机。


记录-Redis

相关视频教程:

黑马程序员Redis入门到精通,深入剖析Redis缓存技术,Java企业级解决方案redis教程_哔哩哔哩_bilibili

安装Redis

#下载编译安装
wget http://download.redis.io/releases/redis-4.0.12.tar.gz
tar -zxvf redis-5.0.4.tar.gz
mv redis-5.0.4 /usr/local/redis-5.0.4
cd /usr/local/redis-5.0.4
make
cd src && make install
#安装完成,查看版本
/usr/local/bin/redis-server -v

#启动(默认本机6379端口,一般会指定配置文件启动)
/usr/local/bin/redis-server 

#指定配置文件启动(解压包中有原redis.conf文件)
/usr/local/bin/redis-server /usr/local/redis-5.0.4/redis.conf


一般会改动的几个重要配置:
· bind 127.0.0.1:允许访问机器的IP,默认只有本机才能访问,一般设置为 bind 0.0.0.0 来让其他ip也可以访问。
· port 6379:redis 实例启动的端口,默认为 6379
· protected-mode yes:不让外部网络访问,只让本地访问,将其改为no。
· daemonize no:是否以守护进程的方式运行,默认是 no,也就是说你把启动窗口关闭了,将其改为yes。

生产环境时,可以选择禁用一些高位命令:
# 进入 redis.conf 找到 SECURITY 区域;添加如下配置
rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command KEYS ""

redis多实例安装脚本(deploy_redis.sh)参考以下:

#!/bin/bash
#初始化约定:
#安装版本:5.0.4
#使用端口:3306
#安装目录(basedir):/usr/local/redis5.0.4
#数据目录(datadir):/data/redis$port,即默认为 /data/redis6379
#配置文件:存于对应数据目录下conf/redis.conf,即默认为 /data/redis6379/conf/redis.conf
#
#输入参数:
#$1:端口号:如不输入,则使用6379端口。
#如:sh redis_deploy.sh 或 sh redis_deploy.sh 6380 
#
#输出:
#success:active (running)
#fail:部署失败原因

#步骤1:根据是否存在basedir来判断是否第一次安装。如果是第一次安装则下载安装redis$version。
#步骤2:根据输入端口判断对应的datadir是否存在,存在则不允许安装,端口被占用也也不允许安装
#步骤3:生成配置文件,开始安装并通过systemd启动
#步骤4:通过systemd管理redis。即 systemctl start|stop|restart|enable|disable redis${port}

#卸载redis
#卸载某实例
#systemctl stop redis${port}
#rm -rf $redis_datadir /etc/systemd/system/redis${port}
#全部卸载
# rm -rf /tmp/install_pkg/redis* /usr/local/redis* /data/redis* /etc/systemd/system/redis*

###########################################################################################
default_port=6379
port=$([ -n "$1" ] && echo "$1" || echo "$default_port")
version=5.0.4
init_pwd=
maxmemory=1024M
memory_policy=allkeys-lru
download_pkg_url=http://download.redis.io/releases/redis-$version.tar.gz
redis_basedir=/usr/local/redis$version
redis_datadir=/data/redis$port


instance_exist_check(){
    #检查该端口对应的实例datadir是否存在,检查该端口是否被占用
    if [ -d ${redis_datadir} ];then
        if [ "`ls -A ${redis_datadir}`" != "" ];then
            echo "fail, ${redis_datadir} is not empty"
            exit 1
        fi
    fi
    lsof -i:${port} >/dev/null 2>&1
    if [ $? -eq 0 ];then
        echo "fail, port ${port} is used"
        exit 1
    fi
}


download_redis_server() {
    #下载redis安装包
    echo '>>> check whether the redis-server installation package exists ...'
    if [ ! -d "$redis_basedir" ]; then
        mkdir -p /tmp/install_pkg && cd /tmp/install_pkg
        if [ ! -f "redis-$version.tar.gz" ]; then
            echo ">>> download installation package from ${download_pkg_url}"
            wget $download_pkg_url
        fi
        if [ ! -f "redis-$version.tar.gz" ]; then
            echo ">>> download installation package fail!"
            exit 1
        fi
		tar -zxf redis-$version.tar.gz
        mv redis-$version $redis_basedir
    else
        echo "$redis_basedir have already exists"
    fi
}


generate_config_file(){
mkdir -p $redis_datadir/data
mkdir -p $redis_datadir/conf
mkdir -p $redis_datadir/log
mkdir -p $redis_datadir/run
mkdir -p $redis_datadir/tmp
set_maxmemory=$([ -n "$maxmemory" ] && echo "maxmemory $maxmemory" || echo "")
set_maxmemory_policy=$([ -n "$memory_policy" ] && echo "maxmemory-policy $memory_policy" || echo "")
set_requirepass=$([ -n "$init_pwd" ] && echo "requirepass $init_pwd" || echo "")
cat > $redis_datadir/conf/redis.conf <<EOF
bind 0.0.0.0
protected-mode no
port $port
daemonize yes
pidfile /data/redis$port/run/redis.pid
logfile /data/redis$port/log/redis.log
dir /data/redis$port/data
appendonly yes
$set_maxmemory 
$set_maxmemory_policy
$set_requirepass
EOF
}


manage_redis_by_systemd(){
echo ">>> manage redis by systemd ..."
egrep "^redis" /etc/group >& /dev/null || groupadd redis
id redis &> /dev/null || useradd redis -g redis
chown redis:redis -R $redis_datadir
# 新增systemctl管理redis
cat > /etc/systemd/system/redis$port.service << EOF
[Unit]
Description=redis Server
After=network.target
 
[Install]
WantedBy=multi-user.target
 
[Service]
User=redis
Group=redis
Type=forking
TimeoutSec=0
PIDFile=$redis_datadir/run/redis.pid
ExecStart=/usr/local/redis$version/src/redis-server /data/redis$port/conf/redis.conf
EOF

# 重新加载systemctl
systemctl daemon-reload 
}


install_redis_server(){
    echo ">>> install redis server in version $version ..."
    if [ -f "/usr/local/bin/redis-server" ]; then 
        current_version=`redis-server -v|awk '{print $3}'`
        if [ "$current_version" != "v=$version" ]; then
            echo "watch out, exist old version $current_version, not deploy version $version, deploy version would override it!!"
            echo "watch out, exist old version $current_version, not deploy version $version, deploy version would override it!!"
            echo "watch out, exist old version $current_version, not deploy version $version, deploy version would override it!!"
        fi
    fi
    if [ ! -f "$redis_basedir/src/redis-server" ]; then 
        cd $redis_basedir
        make
        cd src && make install
    fi

}


start_redis_service(){
    #启动redis服务(通过systemctl start|stop|status|restart redis$port管理)
    manage_redis_by_systemd
    echo ">>> start redis$port service ..."
    systemctl start redis$port
    systemctl enable redis$port
    systemctl status redis$port
}


deploy_redis_instance(){
    instance_exist_check
    download_redis_server
    generate_config_file
    install_redis_server
    start_redis_service
    exit 0
}


deploy_redis_instance

安装Redis(docker)

#下载最新版镜像
docker pull redis
#下载指定版本镜像(推荐)
docker pull redis:5.0.4

#配置redis 
mkdir -p /docker/redis/6380/conf
cat > /docker/redis/6380/conf/redis.conf <<EOF
bind 0.0.0.0
protected-mode no
port $port
daemonize yes
pidfile /data/redis6380/run/redis.pid
logfile /data/redis6380/log/redis.log
dir /data/redis$port/data
appendonly yes
EOF

#运行镜像 
docker run -p 6380:6379 --name redis6380 --privileged=true -v /docker/redis/6380/conf/redis.conf:/etc/redis/redis.conf -v /docker/redis/6380/data:/data -d redis:5.0.4 redis-server /etc/redis/redis.conf
 
#查看容器
docker ps
#进入容器
docker exec -it redis6380 /bin/bash
#连接redis
./redis-cli
set k1 v1

Redis主从同步

redis 可以通过 slaveof master_ip master_port 命令让当前实例成为master实例的从节点。该命令可以在从节点执行,也可以从节点的配置文件中加入。成为从节点后,从节点只提供读操作,不提供写操作。

#方式一:命令行直接执行(实例重启后无法继续同步不推荐)
127.0.0.1:6380> slaveof 127.0.0.1 6379

#方式二:配置文件配置(服务重启后仍继续同步,推荐)
slaveof 127.0.0.1 6379

成为主从结构后,主节点数据发生改变时会自动同步到从节点。但是主节点挂了之后从节点无法自动变为主节点。如果主节点挂了,可以在从节点执行 slaveof no one 以使自己恢复写入。

为了让从节点能够在主节点异常后自动成为主节点,redis提供了哨兵模式和cluster模式。

Redis Cluster

Redis集群是一个由多个主从节点群组成的分布式服务集群,它具有复制、高可用和分片特性。Redis集群不需要sentinel哨兵也能完成节点移除和故障转移的功能。需要将每个节点设置成集群模式,这种集群模式没有中心节点,可水平扩展,据官方文档称可以线性扩展到上万个节点(官方推荐不超过1000个节点)。redis集群的性能和高可用性均优于之前版本的哨兵模式,且集群配置非常简单。redis集群的运用主要是针对海量数据+高并发+高可用的场景。

搭建三主三从的Redis集群示例:

#修改部署脚本deploy_redis.sh:修改redis.conf配置,添加如下三行(开启集群模式)
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000

#部署启动6个实例(m1:6381,m2:6382,m3:6383作为master,s1:6384,s2:6385,s3:6386作为从)
echo {6381,6382,6383,6384,6385,6386} |xargs -n 1 sh deploy_redis.sh

#创建新集群命令:命令create,(redis5.0后,才可以使用cluster create命令构建,注意中途有询问需输入yes)
/usr/local/redis5.0.4/src/redis-cli create --replicas 1 127.0.0.1:6381 127.0.0.1:6382 127.0.0.1:6383 127.0.0.1:6384 127.0.0.1:6385 127.0.0.1:6386

#通过集群模式(-c)连接redis,并查看主从关系(cluster nodes)
redis-cli -p 6381 -c
>cluster nodes

redis cluster集群操作(新增/删除节点、分配槽位)

#创建新的实例redis6387,6388,将6387其作为master加入原有的集群(注意配置文件中开启集群模式)
echo {6387,6388} |xargs -n 1 sh deploy_redis.sh

#通过check命令检查集群情况,可以查看到M-S节点和槽位情况
redis-cli --cluster check 127.0.0.1:6381
redis-cli --cluster add-node 127.0.0.1:6387 127.0.0.1:6381

#检查集群情况,发现已经新增加了一个master6387,且上面还没有任何槽位:slots: (0 slots) master
redis-cli --cluster check 127.0.0.1:6381
   
#加入新master后,重新分配槽位reshard。输入4096(16384/4主)个槽位分给新节点(根据节点id),从all中拿(也可以从某个节点id拿),yes确认分配
redis-cli --cluster reshard 127.0.0.1:6381
#重新分配槽位后,检查槽位情况,发现是1-3号master节点都匀了4096/3个槽位给新节点
redis-cli --cluster check 127.0.0.1:6381
(可以看到6387的槽位是:slots:[0-1364],[5461-6826],[10923-12287] (4096 slots) master)
#为新加入的6387主节点添加一个从节点6388
redis-cli --cluster add-node 127.0.0.1:6388 127.0.0.1:6387 --cluster-slave --cluster-master-id 6387的id
 
#集群缩容一组主从:1、删从节点 2、重新分配槽位 3、删除主节点
#1.1、找到要删除的从节点id(过程中可以多次check)
redis-cli --cluster check 127.0.0.1:6381
#1.2、删除从节点6388
redis-cli --cluster del-node 127.0.0.1:6388 6388-id
#2、重新分配槽位。可以分三次分配,或者一次分配给一个节点
redis-cli --cluster reshard 127.0.0.1:6381
#3、删除主节点6387
redis-cli --cluster del-node 127.0.0.1:6387 6387-id


#免交互式方式扩缩容示例:
#redis-cli --cluster reshard 127.0.0.1:6381 --cluster-from all --cluster-to $(redis-cli -c -p 6387 cluster nodes|awk '/1:6387/{print $1}') --cluster-slots 4096 --cluster-yes
#redis-cli --cluster del-node 127.0.0.1:6388 $(redis-cli -c -h 127.0.0.1 -p 6388 cluster nodes|awk '/1:6398/{print $1}')

Redis 使用

Redis 集成到 springboot,只需简单几步。

第一步:引入依赖、配置地址

默认使用lettuce,这里使用jedis,并使用redis作为cache(可选)

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
	<exclusions>
		<exclusion>
			<groupId>io.lettuce</groupId>
			<artifactId>lettuce-core</artifactId>
		</exclusion>
	</exclusions>
</dependency>

<dependency>
	<groupId>redis.clients</groupId>
	<artifactId>jedis</artifactId>
</dependency>

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-cache</artifactId>
</dependency>


<!-- application.yml中:-->
spring: 
  redis:
    host: jvxb.com
    port: 6379
    #没有密码则无需配置
    password: 123456
    #配置jedis客户端
    jedis:
      pool:
        # 连接池最大连接数 默认8 ,负数表示没有限制
        max-active: 20
        # 连接池中的最大空闲连接 默认8
        max-idle: 10
        # 连接池中的最小空闲连接 默认0
        min-idle: 5
        # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认-1
        max-wait: -1ms
    timeout: 5000ms  #连接超时时间(毫秒)

第二步:配置序列化方式、缓存组

不设置序列化方式的话,通过redisTemplate存入redis时格式不是很好辨认,通过序列化,我们可以更方便地查找到具体key和value;再通过缓存组可以少写大量重复的写缓存/查缓存代码。

import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

/**
 * 1)redis序列化配置,Object序列化为Json,比自带的jdk序列化方式,具有更快的速度
 * 这个主要是根据redis存储的数据类型需求决定,key一般都是String,但是value可能不一样,一般有两种,String和 Object;
 * 如果k-v都是String类型,我们可以直接用 StringRedisTemplate,这个是官方建议的,也是最方便的,直接导入即用,无需多余配置!
 * 如果k-v是Object类型,则需要自定义 RedisTemplate
 * 
 * 2)配置cache
 *
 * @author jvxb
 * @since 202-12-26
 */
@Configuration
@EnableCaching
public class RedisConfig {

    /**
     * 使用方式:直接引入
     * @Autowired 
     * RedisTemplate<String, Object> redisTemplate
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        // key采用String的序列化方式
        redisTemplate.setKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        redisTemplate.setValueSerializer(genericJackson2JsonRedisSerializer);
        // hash的key也采用String的序列化方式
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        //hash的value序列化方式采用jackson
        redisTemplate.setHashValueSerializer(genericJackson2JsonRedisSerializer);
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        return redisTemplate;
    }

    /**
     * 缓存管理器
     * 相关注解:
     * @Cacheable:使用缓存。在方法执行前如果有数据,则直接返回缓存数据;没有则调用方法并将方法返回值放进缓存。
     * @CachePut:更新缓存,将方法的返回值放到缓存中
     * @CacheEvict:清空缓存
     * 相关配置:
     * key:缓存key,可以不指定,默认为方法参数
     * value/cacheNames: 缓存key的前缀,必须指定
     * condition:调用前判断,缓存的条件。(condition为false时不进行缓存,默认true)
     * unless:执行后判断,不缓存的条件。 (unless为true时不进行缓存,默认false)
     * 相应示例:
     * @Cacheable(value = "menuById")
     * @Cacheable(value = "menuById", key = "#id")
     * @Cacheable(value = "menuById", key = "'id-' + #id", condition = "#id != null", unless = "#result == null")
     * @CachePut(value = "newMenu", key = "#id")
     * @CacheEvict(value = "menuById", key = "#id")
     */
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        RedisCacheConfiguration defaultCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofHours(1))
                .serializeKeysWith(
                        RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new GenericJackson2JsonRedisSerializer()))
                .computePrefixWith(name -> name + ":");
        RedisCacheManager.RedisCacheManagerBuilder builder = RedisCacheManager.RedisCacheManagerBuilder
                .fromConnectionFactory(redisConnectionFactory).cacheDefaults(defaultCacheConfiguration);
        return builder.build();
    }
}

第三步:使用redisTemplate & 缓存组功能

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * Redis控制器
 *
 * @author jvxb
 * @since 2022-12-26
 */
@RestController
@RequestMapping("redis")
public class RedisController {

    @Autowired
    RedisTemplate<String, Object> redisTemplate;

    @GetMapping("/set")
    public Object set(String key, String value) {
        redisTemplate.opsForValue().set(key, value);
        return "success";
    }

    @GetMapping("/get")
    public Object get(String key) {
        Object value = redisTemplate.opsForValue().get(key);
        return value;
    }

    @GetMapping("/del")
    public Object del(String key) {
        Object value = redisTemplate.delete(key);
        return value;
    }

    @GetMapping("/hset")
    public Object hset(String key, String hashKey, String value) {
        redisTemplate.opsForHash().put(key, hashKey, value);
        return "success";
    }

    @GetMapping("/hget")
    public Object hget(String key, String hashKey) {
        Object value = redisTemplate.opsForHash().get(key, hashKey);
        return value;
    }

    @GetMapping("/cacheable")
    @Cacheable(value = "cacheTest", key = "#id")
    public String cacheable(String id) {
        System.out.println("执行方法-save/find缓存");
        if (id == null) return "null id";
        return "your id = " + id;
    }

    @GetMapping("/cachePut")
    @CachePut(value = "cacheTest", key = "#id")
    public String cachePut(String id) {
        System.out.println("执行方法-save缓存");
        if (id == null) return "null id";
        return "your id = " + id;
    }

    @GetMapping("cacheEvict")
    @CacheEvict(value = "cacheTest", key = "#id")
    public String cacheEvict(String id) {
        System.out.println("执行方法-evict缓存");
        if (id == null) return "null id";
        return "your id = " + id;
    }

}


记录-Nginx

相关视频教程:

尚硅谷Nginx教程(亿级流量nginx架构设计)_哔哩哔哩_bilibili

安装nginx

#通过yum方式安装nginx
yum install nginx -y
#查看版本
nginx -v
#启动nginx
systemctl start nginx

#访问80端口
curl 127.0.0.1


#yum方式安装的默认地址和配置的默认地址
/etc/nginx/nginx.conf  //yum方式安装后默认配置文件的路径
/usr/share/nginx/html  //nginx网站默认存放目录
/usr/share/nginx/html/index.html //网站默认主页路径
   

拓展:nginx基本操作
nginx -v  //查看nginx版本
nginx -t  //检查nginx.conf配置是否正常
yum -y instal nginx  //安装nginx
systemctl start nginx //启动nginx
systemctl stop nginx   //停止nginx
systemctl reload nginx   //重载nginx

#找出非法ip
cat /var/log/nginx/access.log | awk -F\" '{A[$(NF-1)]++}END{for(k in A)print A[k],k}' | sort -n |tail
122 58.144.7.66
337 106.91.201.75
2270 122.200.77.170  #显然这个ip不正常,而且这不是nginx所知道的真实ip,而是$http_x_forwarded_for变量

常用配置

#设定首页(例如vue)
server {
    listen       80;
    server_name  localhost;
    location / {
        root   /usr/share/nginx/dist;
        index  index.html;
    }
}


#简单转发
server {
  listen 80;
  location /mapi {
    rewrite ^/mapi/(.*)$ /$1 break;
    proxy_pass http://localhost:8080;
  }
}


#upstream转发(默认轮询,可设置权重、重试次数、超时时间)
upstream backend_server { 
    server 127.0.0.1:8080;  
    server 127.0.0.1:8081;  
    server 127.0.0.1:8082 max_fails=3 fail_timeout=30s;
}
server {
    listen       80;
    server_name  localhost;
    location /mapi {
        rewrite ^/mapi/(.*)$ /$1 break;
        proxy_pass http://backend_server; 
        proxy_set_header Host $http_host;
        proxy_set_header Cookie $http_cookie;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}


#443转发到80端口
server {
	listen 443;
	server_name localhost;
	root /data/front_page/dist;
	ssl off;
	rewrite ^(.*)$ http://l27.0.0.1/$1 permanent;
}


记录-ES

相关视频教程:

【尚硅谷】ElasticSearch教程入门到精通(基于ELK技术栈elasticsearch 7.x+8.x新特性)_哔哩哔哩_bilibili

推荐安装es 6.2.2 、es 6.8.12 、 es 7.6.2版本以下以springboot2.3配套的7.6.2版本为记录。

在这里插入图片描述

安装es

cd /tmp/install_pkg
wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.6.2-linux-x86_64.tar.gz
tar -vxf elasticsearch-7.6.2-linux-x86_64.tar.gz
mv elasticsearch-7.6.2 /usr/local/es7.6.2

#新增es用户并授权
groupadd es
useradd es
chown -R es /usr/local/es7.6.2

#后台启动(不能以root用户,先切到es用户,后台启动 -d)
su es
/usr/local/es7.6.2/bin/elasticsearch -d

#访问测试
curl localhost:9200

#注:默认情况下,es只限本机访问,通过配置放开
echo "http.host: 0.0.0.0" >> /usr/local/es7.6.2/config/elasticsearch.yml
#也可以指定http端口或tcp端口
echo "http.port: 9200" >> /usr/local/es7.6.2/config/elasticsearch.yml
echo "transport.tcp.port: 9300" >> /usr/local/es7.6.2/config/elasticsearch.yml

#启动错误
1、启动若出现报错:[1]: max file descriptors [65535] for elasticsearch process is too low, increase to at least [65536]
编辑  /etc/security/limits.conf  新增以下配置并保存退出:
es soft nofile 65536
es hard nofile 65536
(ulimit -Hn 命令可以查看当前用户的最大文件数,如果esdmin的最大文件数没改过来,则需要退出当前用户重新登录,重新登录后可发现文件数已经修改)

2、启动若出现报错:[1]: max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]
可在/etc/sysctl.conf中添加一行:vm.max_map_count=262144
并使其执行生效:sysctl -p /etc/sysctl.conf

3、若本地能访问,外网无法访问,可能要打开防火墙
firewall-cmd --zone=public --list-ports
firewall-cmd --zone=public --add-port=9200/tcp --permanent
firewall-cmd --zone=public --add-port=9300/tcp --permanent
firewall-cmd --reload

es多实例安装脚本(deploy_es.sh)参考以下:

#!/bin/bash
#初始化约定:
#安装版本:6.8.12 / 7.6.2(默认)
#安装端口:9200
#安装目录(basedir):/usr/local/es7.6.2
#数据目录(datadir):/data/es$port,即默认为 /data/es9200
#配置文件:存于对应数据目录下conf/es.conf,即默认为 /data/es9200/conf/es.conf
#
#输入参数:
#$1:端口号:如不输入,则使用9200端口。
#如:./es_deploy.sh 或 ./es_deploy.sh 9201 
#
#输出:
#success:active (running)
#fail:部署失败原因
 
#步骤1:根据是否存在basedir来判断是否第一次安装。如果是第一次安装则下载安装es6.8.12。
#步骤2:根据输入端口判断对应的datadir是否存在,存在则不允许安装,端口被占用也也不允许安装
#步骤3:生成配置文件,开始安装并通过systemd启动
#步骤4:通过systemd管理es。即 systemctl start|stop|restart|enable|disable es${port}
 
#卸载es
#卸载某实例
#systemctl stop es${port}
#rm -rf $es_datadir /etc/systemd/system/es${port}
#全部卸载
# rm -rf /tmp/install_pkg/es* /usr/local/es* /data/es* /etc/systemd/system/es*
 
###########################################################################################
default_port=9200
port=$([ -n "$1" ] && echo "$1" || echo "$default_port")
tcp_port=`expr $port + 100`
#version=6.8.12
version=7.6.2
pkg_suffix=$([ ${version:0:1} -ge '7' ] && echo "-linux-x86_64" || echo "")
maxmemory=1024M
download_pkg_url=https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-$version$pkg_suffix.tar.gz
es_basedir=/usr/local/es$version
es_datadir=/data/es$port

 
instance_exist_check(){
    #检查该端口对应的实例datadir是否存在,检查该端口是否被占用
    port_datadir=/data/es${port}
    if [ -d ${port_datadir} ];then
        if [ "`ls -A ${port_datadir}`" != "" ];then
            echo "fail, ${port_datadir} is not empty"
            exit 1
        fi
    fi
    lsof -i:${port} >/dev/null 2>&1
    if [ $? -eq 0 ];then
        echo "fail, http port ${port} is used"
        exit 1
    fi
    lsof -i:${tcp_port} >/dev/null 2>&1
    if [ $? -eq 0 ];then
        echo "fail, tcp port ${tcp_port} is used"
        exit 1
    fi
}
 
 
download_es_server() {
    #下载es安装包
    echo '>>> check whether the es-server installation package exists ...'
    if [ ! -d "$es_basedir" ]; then
        mkdir -p /tmp/install_pkg && cd /tmp/install_pkg
        if [ ! -f "elasticsearch-$version$pkg_suffix.tar.gz" ]; then
            echo ">>> download installation package from ${download_pkg_url}"
            wget $download_pkg_url
        fi
        if [ ! -f "elasticsearch-$version$pkg_suffix.tar.gz" ]; then
            echo ">>> download installation package fail!"
            exit 1
        fi
        tar -zxf elasticsearch-$version$pkg_suffix.tar.gz
        mv elasticsearch-$version $es_basedir
    else
        echo "$es_basedir have already exists"
    fi
}
 
 
generate_config_file(){
#沿用解压后的文件即可
cp -r $es_basedir $es_datadir
#修改1:集群名称、允许外网访问
cat >> $es_datadir/config/elasticsearch.yml <<EOF
discovery.type: single-node
network.host: 0.0.0.0
http.port: $port
transport.tcp.port: $tcp_port
path.data: $es_datadir/data
path.logs: $es_datadir/logs
http.cors.enabled: true
http.cors.allow-origin: "*"
bootstrap.memory_lock: false
bootstrap.system_call_filter: false
EOF
#修改2:es jvm内存(按需分配,默认1g,官方建议是主机总内存的一半,且不超过32g)
sed -i -e "s/-Xms1g/-Xms$maxmemory/" -e "s/-Xmx1g/-Xmx$maxmemory/" $es_datadir/config/jvm.options
}
 
 
manage_es_by_systemd(){
echo ">>> manage es by systemd ..."
egrep "^es" /etc/group >& /dev/null || groupadd es
id es &> /dev/null || useradd es -g es
chown es:es -R $es_datadir
# 新增systemctl管理es
cat > /etc/systemd/system/es$port.service << EOF
[Unit]
Description=es Server
After=network.target
 
[Install]
WantedBy=multi-user.target
 
[Service]
User=es
Group=es
Type=simple
TimeoutSec=0
LimitNOFILE=100000
LimitNPROC=100000
ExecStart=$es_datadir/bin/elasticsearch
EOF
 
# 重新加载systemctl
systemctl daemon-reload
}
 
 
install_es_server(){
    #无需安装,但是需要修改一些参数。
    vm_count=`cat /etc/sysctl.conf|grep vm.max_map_count`
    if [ ! -n "$vm_count" ];then
        echo 'vm.max_map_count=262144' >> /etc/sysctl.conf
        /usr/sbin/sysctl -p /etc/sysctl.conf >& /dev/null
    fi
	
}
 
 
start_es_service(){
    #启动es服务(通过systemctl start|stop|status|restart es$port管理)
    manage_es_by_systemd
    echo ">>> start es$port service ..."
    systemctl start es$port
    systemctl enable es$port
    systemctl status es$port
}
 
 
deploy_es_instance(){
    instance_exist_check
    download_es_server
    generate_config_file
    install_es_server
    start_es_service
    exit 0
}
 
 
deploy_es_instance

安装es(docker)

sudo docker pull elasticsearch:7.6.2

sudo mkdir -p /data/docker/es9200/config
sudo mkdir -p /data/docker/es9200/data
sudo mkdir -p /data/docker/es9200/plugins
 
echo -e "discovery.type: single-node\nnetwork.host: 0.0.0.0" >> /data/docker/es9200/config/elasticsearch.yml
 
#授权(生产环境不建议777,须通过其他授权)
chmod -R 777 /data/docker/es9200
 
sudo docker run --name es9200 \
 -p 9200:9200  -p 9300:9300 \
 -e ES_JAVA_OPTS="-Xms128m -Xmx128m" \
 -v /data/docker/es9200/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
 -v /data/docker/es9200/data:/usr/share/elasticsearch/data \
 -v /data/docker/es9200/plugins:/usr/share/elasticsearch/plugins \
 -d elasticsearch:7.6.2

es集群

模拟三实例的集群(默认使用内存为1G,测试时如内存紧张可以修改为256M等)

echo 9201 9202 9203 | xargs -n 1 sh deploy_es.sh

es7的集群模式需要在配置文件加上集群相关的配置:cluster.name、node.name、cluster.initial_master_nodes、discovery.seed_hosts 等。es启动时会根据seed_hosts中的节点地址彼此发现组成集群。并且谁先启动谁就会成为主节点。

注:es6时对应配置为discovery.zen.minimum_master_nodes、discovery.zen.ping.unicast.hosts

#删除原有实例的nodes
rm -rf /data/es920{1,2,3}/data/nodes

#添加实例的集群配置
sed -i -e '/discovery.type/d' -e "\$acluster.name: es_cluster\nnode.name: 127.0.0.1:9301\ncluster.initial_master_nodes: [\"127.0.0.1:9301\", \"127.0.0.1:9302\", \"127.0.0.1:9303\"]\ndiscovery.seed_hosts: [\"127.0.0.1:9301\", \"127.0.0.1:9302\", \"127.0.0.1:9303\"]" /data/es9201/config/elasticsearch.yml
sed -i -e '/discovery.type/d' -e "\$acluster.name: es_cluster\nnode.name: 127.0.0.1:9302\ncluster.initial_master_nodes: [\"127.0.0.1:9301\", \"127.0.0.1:9302\", \"127.0.0.1:9303\"]\ndiscovery.seed_hosts: [\"127.0.0.1:9301\", \"127.0.0.1:9302\", \"127.0.0.1:9303\"]" /data/es9202/config/elasticsearch.yml
sed -i -e '/discovery.type/d' -e "\$acluster.name: es_cluster\nnode.name: 127.0.0.1:9303\ncluster.initial_master_nodes: [\"127.0.0.1:9301\", \"127.0.0.1:9302\", \"127.0.0.1:9303\"]\ndiscovery.seed_hosts: [\"127.0.0.1:9301\", \"127.0.0.1:9302\", \"127.0.0.1:9303\"]" /data/es9203/config/elasticsearch.yml

#最终elasticsearch.yml配置如下:
network.host: 0.0.0.0
http.port: 9201
transport.tcp.port: 9301
path.data: /data/es9201/data
path.logs: /data/es9201/logs
http.cors.enabled: true
http.cors.allow-origin: "*"
bootstrap.memory_lock: false
bootstrap.system_call_filter: false
cluster.name: es_cluster
node.name: 127.0.0.1:9301
cluster.initial_master_nodes: ["127.0.0.1:9301", "127.0.0.1:9302", "127.0.0.1:9303"]
discovery.seed_hosts: ["127.0.0.1:9301", "127.0.0.1:9302", "127.0.0.1:9303"]

#重启实例(重启后自动构成集群)
systemctl restart es920{1,2,3}

#查看集群节点
curl http://localhost:9201/_cat/nodes


#回收集群
systemctl stop es920{1,2,3}
systemctl disable es920{1,2,3}
rm -rf /data/es920{1,2,3}
rm -rf /etc/systemd/system/es920{1,2,3}.service

默认所有节点都有权限成为主节点和读写磁盘。对应权限可以通过 node.master: true/false,node.data: true/false来控制。(条件允许可以设置主节点不为数据节点,提示效率)

es kibana

Kibana是一个针对Elasticsearch的开源分析及可视化平台,用来搜索、查看、处理存储在Elasticsearch索引中的数据。使用Kibana,可以通过各种图表进行高级数据分析及展示。

#0、注意:kibana的安装前提是需要有jdk和node.js

#1、下载安装(最好与es同一版本,否则可能不兼容)
wget https://artifacts.elastic.co/downloads/kibana/kibana-7.6.2-linux-x86_64.tar.gz
tar -zxvf kibana-7.6.2-linux-x86_64.tar.gz
mv kibana-7.6.2-linux-x86_64 /usr/local/kibana7.6.2

#2、修改kibana端口号(默认5601)、host(默认localhost)、连接es(默认localhost:9200)、语言(默认英文)
cat >> /usr/local/kibana7.6.2/config/kibana.yml <<EOF
server.port: 5601
server.host: "改为机器ip(内网),否则远程连接不了"
elasticsearch.hosts: ["http://localhost:9201"]
i18n.locale: "zh-CN"
EOF

#3、启动
cd /usr/local/kibana7.6.2/bin
nohup ./kibana --allow-root  &

#4、停止(实际是node服务)
ps -ef|grep node
找到对应pid后 kill -9

#页面访问,通过开发者工具【Dev-Tools】可以快速增删改查数据
机器ip(外网):5601

kibana除开发者工具[【Dev Tools】外,它的 发现【Discover】、可视化【Visualize】、仪表盘【Dashboards】、日志【logs】功能,都是及其强大且好用的,此处不做展开。

 es CURD

在es7之前,索引(index)支持多种type,所以索引相当于是一个数据库,type相当于是一张表,type下的document相当于表中的数据。

在es7后取消了type的概念(每个索引只有一个type = _doc)。索引就相当于是一张表,mapping相当于表结构,doucoument相当于是表中的数据。

假设es对应的表结构如下:

CREATE TABLE `user` (
  `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `user_name` varchar(50) NOT NULL COMMENT '用户名',
  `age` int(11) DEFAULT NULL COMMENT '年龄',
  `phone` varchar(50) DEFAULT NULL COMMENT '手机号码',
  `gender` tinyint(1) DEFAULT '3' COMMENT '性别:1男 2女 3未知',
  `birthday` date DEFAULT NULL COMMENT '生日',
  `remark` text DEFAULT NULL COMMENT '用户标记',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '修改时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

则es通过http接口(默认localhost:9200)进行增删改查操作如下:

快速插入(默认自动生成_id)

POST /user/_doc
{
  "id" : 1,
  "user_name" : "张三",
  "phone" : "13333333333",
  "gender" : 1,
  "birthday" : "2012-12-12",
  "remark": "法外狂徒",
  "create_time": "2022-12-12 13:13:13"
}

快速插入/修改(指定_id,存在相同id则更新)

PUT /user/_doc/2
{
  "id" : 2,
  "user_name" : "李四",
  "phone" : "13444444444",
  "gender" : 2,
  "birthday" : "2012-12-14",
  "remark": "良好市民",
  "create_time": "2022-12-14 14:14:14"
}

快速查询索引情况(别名aliases、映射mappings、设置setting)

GET /user

 快速查询文档(整个索引)

GET /user/_search

 快速查询文档(根据_id)

GET /user/_doc/2

快速删除(根据_id)

DELETE /user/_doc/2

快速删除(根据索引)

DELETE /user

复杂查询(dsl语法)

POST /user/_search
{
    "query": {
        "bool": {
            "must": [
                {
                    "term": {
                        "remark.keyword": "法外狂徒"
                    }
                }
            ]
        }
    }
}

常用dsl语法如下:

{
    "query": {
        "bool": {
            "must": [
                {
                    "terms": {
                        "text_filed_name.keyword": [
                            "value1",
                            "value2"
                        ]
                    }
                },
                {
                    "term": {
                        "keyword_filed_name1": "value3"
                    }
                },
                {
					"exists": {
						"field": "field_name"
					}
				}
				{
					"range": {
						"timestamp": {
							"from": 1658419200000,
							"to": 1668419200000
						}
					}
				}
            ],
            "must_not": [
                {
                    "term": {
                        "field_name4": "value4"
                    }
                },
                {
					"exists": {
						"field": "field_name"
					}
				}
            ],
            "should": [
                "match": {
                    "filed_name": "将词分割开来,匹配倒排索引,查找包含任意分割的词的字段"
                },
                "match_phrase": {
                    "filed_name": "将词分割开来,匹配倒排索引,查找这个短语(必须同时包含)。"
                },
                {
                    "wildcard": {
                        "filed_name": "*value(text时加.keyword)*"
                    }
                }
            ]
        }
    },
    "aggs": {
        "keywrod_filed_name": {
            "terms": {
                "field": "keywrod_filed_name"
            }
        }
    },
    "from": 0,
    "size": 100,
    "sort": [ { "field_name": "desc" } ]
}

可见dsl语法还是比较难用且难记的。但是我们可以通过 sql 的形式来进行查询

#方式一(推荐):使用rest风格查询
POST /_xpack/sql
{
    "query": "show tables"
}

POST /_xpack/sql?format=json
{
	"query": "SELECT * FROM user ORDER BY id DESC LIMIT 5"
}

#将sql转为dsl语法
POST /_xpack/sql/translate
{
	"query": "SELECT * FROM user ORDER BY id DESC LIMIT 5"
}

#方式一在7.x后已过时,简化为:
POST _sql 
{
	"query": "SELECT * FROM user ORDER BY time DESC LIMIT 5"
}

POST _sql/translate
{
	"query": "SELECT * FROM user ORDER BY time DESC LIMIT 5"
}

#方式二:使用SQL CLI,启动后可以直接输入sql
/data/es9201/bin/elasticsearch-sql-cli http://localhost:9201

es 中文插件

在中文数据检索场景中,为了提供更好的检索效果,需要在ES中集成中文分词器,因为ES默认是按照英文的分词规则进行分词的,当文本是中文时基本上都是单字分词,对中文分词效果不理想。

#未安装中文分词器前,可以看到分词的结果是按单字来分词,效果不理想
POST /_analyze
{
    "text": "这里是一些需要分词的内容"
}

ES常用的中文分词器有hanlp、ansj、结巴、IK,其中IK分词器会比较简单易用一些,也是大多数公司的选择,下面记录在ES中如何集成IK这个中文分词器。


#前往github:
https://github.com/medcl/elasticsearch-analysis-ik/releases/tag

#具体下载地址
wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.6.2/elasticsearch-analysis-ik-7.6.2.zip

#上传到服务器,解压(所有实例都需要有)
mkdir -p /data/es920{1,2,3}/plugins/ik
unzip elasticsearch-analysis-ik-7.6.2.zip -d /data/es9201/plugins/ik
unzip elasticsearch-analysis-ik-7.6.2.zip -d /data/es9202/plugins/ik
unzip elasticsearch-analysis-ik-7.6.2.zip -d /data/es9203/plugins/ik

#重启服务
systemctl restart es9201

#安装中文分词器后,可以看到分词的结果比较合理的(IK分词器有两种分词模式:ik_max_word 和 ik_smart 模式。ik_max_word将文本做最细粒度拆分,ik_smart将文本做最粗粒度拆分)。
POST /_analyze
{
    "analyzer": "ik_smart",
    "text": "这里是一些需要分词的内容"
}

自定义词库:

#自定义词库(一行一个)
vim /data/es9201/plugins/ik/config/my.dic
抓娃小兵
只因你太美

#载入自定义词库
vim /data/es9201/plugins/ik/config/IKAnalyzer.cfg.xml
#用户可以在这里配置自己的扩展字典
<entry key="ext_dict">my.dic</entry>

es 索引

索引可以说是es中最重要的内容。索引是一个虚拟的空间,类似于关系型数据库的table。一个索引至少由一个分片组成,索引可以包含一个主分片和多个副本分片。

当我们查看一个索引时,可以看到其 aliases(别名)mappings(映射)settings(设置)信息。

GET /user
{
    "user": {
        "aliases": {},
        "mappings": {
            ...
        },
        "settings": {
            "index": {
                "creation_date": "1671538197644",
                "number_of_shards": "5",
                "number_of_replicas": "1",
                "uuid": "eaXIsxVhTb2hZitr9vDZ2A",
                "version": {
                    "created": "6082399"
                },
                "provided_name": "user"
            }
        }
    }
}

索引无需提前创建,第一条数据插入即可创建完成(使用场景:非严格数据模型限制规范的场景,如日志、监控,默认字段类型为long、text)。如果确定了数据模型,最好提前创建好索引(指定别名、映射、分片等)。需要注意的是,索引建立后,分片个数 和 mapping 不可以更改。对于mapping只可以新增字段,而不能修改或删除字段。

索引命名(建议)

  • 业务类型命名:名称 + 数字版本号
  • 日志类型命名:名称 + 时间

分片和副本区别

当分片设置为5,数据量为30G时,es会自动帮我们把数据均衡地分配到5个分片上,即每个分片大概有6G数据,当你查询数据时,es会把查询发送给每个相关的分片,并将结果组合在一起。(es6中默认分片数是5,es7中默认分片数是1)

而副本,就是对分布在5个分片的数据进行复制。因为分片是把数据进行分割而已,数据依然只有一份,这样的目的是保障查询的高效性,副本则是多复制几份分片的数据,这样的目的是保障数据的高可靠性,防止数据丢失。es中默认副本数为1。

索引别名

ES中可以为索引添加别名,一个别名可以指向到多个索引中,同时在添加别名时可以设置筛选条件,指向一个索引的部分数据,实现在关系数据库汇总的视图功能,这就是ES中别名的强大之处。

只要有可能,尽量使用别名,推荐为es的每个索引都使用别名,因为在未来重建索引的时候,别名会赋予你更多的灵活性(修改分片/mapping)。别名的常见应用如下:

  • 实现正在运行的集群上的一个索引到另一个索引之间的无缝切换。试想一下这种场景,由于业务变换,由于业务原因或索引字段修改,我们需要将业务数据从原有索引1变换到新的索引2上,如果没有别名,我们必须修改和中断业务系统,但是有了别名,只需要修改别名,另起指向新的索引2即可,这样的操作可以在用户无任何感知的情况下完成。
  • 使数据检索等操作更加方便。假如有两个月的日志数据,分别存放在index_202208和index_202209两个索引中,没有使用别名进行检索时,我们需要同时写上两个索引名称进行检索,使用索引后,我们可以令别名同时指向这两个索引,检索时只需要使用这个别名就可以同时在两个索引中尽心检索。
  • 为一个索引中的部分数据创建别名,例如,一个索引中存放了一整年的数据,现在新增一个业务场景,更多的是对其中某一个月的数据进行检索,这时,我们可以在创建别名时,通过设置过滤条件filter,可以单独令别名指向一个月的数据,使得检索更加高效。

利用别名,进行索引切换演示:

//1)假设原有索引为 myidx_v1
POST myidx_v1
{
   "id": 1,
   "name": "张三",
   "remark": "法外狂徒",
   "phone": "13300003333",
   "create_time": "2022-12-13 13:13:13",
   "birth": "2022-12-23"
}

//2)为myidx_v1设置别名
POST _aliases
{
    "actions": [
        {
            "add": {
                "index": "myidx_v1",
                "alias": "myidx"
            }
        }
    ]
}

//3)新增索引 myidx_v2 (修改部分字段类型及分片数)
GET myidx_v1  
#拿到原索引信息后,新索引中将部分字段设为keyword、将remark字段设为ik分词、并将分片数量设为3
PUT myidx_v2
{
	"aliases": {},
	"mappings": {
		"_doc": {
			"properties": {
				"birth": {
					"type": "date"
				},
				"create_time": {
					"type": "keyword"
				},
				"id": {
					"type": "long"
				},
				"name": {
					"type": "text",
					"analyzer": "ik_max_word"
				},
				"phone": {
					"type": "keyword"
				},
				"remark": {
					"type": "text",
					"analyzer": "ik_smart"
				}
			}
		}
	},
	"settings": {
		"index": {
			"number_of_shards": "3",
			"number_of_replicas": "1"
		}
	}
}

//4)将myidx_v1的数据同步到新索引myidx_v2
//size 可选,每次批量提交1000个,可以提高效率,建议每次提交5-15M的数据
POST _reindex
{
  "source": {
    "index": "myidx_v1",
	"size":1000
  },
  "dest": {
    "index": "myidx_v2"
  }
}

//测试,同样的查询条件,对比索引,可以看到myidx_v2设置了分词器后已经生效。
POST myidx/_search
POST myidx_v1/_search
POST myidx_v2/_search
{
	"query": {
		"bool": {
			"must": [
				{
					"term": {
						"name": {
							"value": "张三"
						}
					}
				}
			]
		}
	}
}

//5)将别名映射到新索引 myidx_v2,此时查询别名,发现已经是使用了新索引 myidx_v2
POST _aliases
{
	"actions": [
		{
			"remove": {
				"index": "myidx_v1",
				"alias": "myidx"
			}
		},
		{
			"add": {
				"index": "myidx_v2",
				"alias": "myidx"
			}
		}
	]
}

索引模板

很多时候,为了更好地控制一个索引的大小,我们会把一个大索引分为很多小索引,如一个日志索引 access_log,我们需要把它分为access_log_202201 - access_log_202212这样的12个小索引。这些索引除了名称不同,其它内容完全相同(分片、映射等)。但是我们又不想手动创建这些小索引。这时候我们可以通过索引模板来简化索引的创建,通过索引模板我们可以将配置和映射应用到新创建的索引中。然后通过别名来实现这些索引的查询:

#注意,es7.x后mappings中无需_doc,es6.x的mappings中需要_doc
PUT _template/access_log_template
{
    "index_patterns": ["access_log_*"],
    "aliases": {
        "access_log": {}
    },
    "settings": {
        "number_of_shards": 1,
        "index.number_of_replicas": 1
    },
    "mappings": {
        "properties": {
            "id": {
                "type": "keyword"
            },
            "sourceIp": {
                "type": "keyword"
            },
            "userId": {
                "type": "keyword"
            },
            "userName": {
                "type": "keyword"
            },
            "requestMethod": {
                "type": "keyword"
            },
            "requestUri": {
                "type": "keyword"
            },
            "requestParams": {
                "type": "text"
            },
            "requestTime": {
                "type": "long"
            },
            "requestTimeStr": {
                "type": "keyword"
            },
            "executeTime": {
                "type": "long"
            }
        }
    }
}

es 使用

ES 集成到 springboot,只需简单几步。

第一步:引入依赖、配置地址

Java中最常用的客户端有 spring-data-elasticsearch 和 High Level API,spring-data-elasticsearch用起来更简单方便,High Level API用起来更灵活,可以适配不同es版本。此处用High Level API。

#可以通过version指定具体版本,如到6.8.12。springboot2.3时对应版本为7.6.2
<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
    <version>7.6.2</version>
</dependency>

<!-- application.yml中:-->
elasticsearch:
  schema: http
  address: jvxb.com:9201,jvxb.com:9202,jvxb.com:9203
  connectTimeout: 5000
  socketTimeout: 5000
  connectionRequestTimeout: 5000
  maxConnectNum: 100
  maxConnectPerRoute: 100

配置 RestHighLevelClient

package com.jvxb.demo.config;

import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.ArrayList;
import java.util.List;

/**
 * @Deacription ElasticSearch 配置
 **/
@Configuration
public class EsConfiguration {
    /**
     * 协议
     */
    @Value("${elasticsearch.schema:http}")
    private String schema;

    /**
     * 集群地址,如果有多个用“,”隔开
     */
    @Value("${elasticsearch.address}")
    private String address;

    /**
     * 连接超时时间
     */
    @Value("${elasticsearch.connectTimeout}")
    private int connectTimeout;

    /**
     * Socket 连接超时时间
     */
    @Value("${elasticsearch.socketTimeout}")
    private int socketTimeout;

    /**
     * 获取连接的超时时间
     */
    @Value("${elasticsearch.connectionRequestTimeout}")
    private int connectionRequestTimeout;

    /**
     * 最大连接数
     */
    @Value("${elasticsearch.maxConnectNum}")
    private int maxConnectNum;

    /**
     * 最大路由连接数
     */
    @Value("${elasticsearch.maxConnectPerRoute}")
    private int maxConnectPerRoute;

    @Bean(name = "restHighLevelClient")
    public RestHighLevelClient restHighLevelClient() {
        // 拆分地址
        List<HttpHost> hostLists = new ArrayList<>();
        String[] hostList = address.split(",");
        for (String addr : hostList) {
            String host = addr.split(":")[0];
            String port = addr.split(":")[1];
            hostLists.add(new HttpHost(host, Integer.parseInt(port), schema));
        }
        // 转换成 HttpHost 数组
        HttpHost[] httpHost = hostLists.toArray(new HttpHost[]{});
        // 构建连接对象
        RestClientBuilder builder = RestClient.builder(httpHost);
        // 异步连接延时配置
        builder.setRequestConfigCallback(requestConfigBuilder -> {
            requestConfigBuilder.setConnectTimeout(connectTimeout);
            requestConfigBuilder.setSocketTimeout(socketTimeout);
            requestConfigBuilder.setConnectionRequestTimeout(connectionRequestTimeout);
            return requestConfigBuilder;
        });
        // 异步连接数配置
        builder.setHttpClientConfigCallback(httpClientBuilder -> {
            httpClientBuilder.setMaxConnTotal(maxConnectNum);
            httpClientBuilder.setMaxConnPerRoute(maxConnectPerRoute);
            return httpClientBuilder;
        });
        return new RestHighLevelClient(builder);
    }
}

第二步:创建索引映射

新建索引模板 access_log_template 并创建索引映射的实体类。

import lombok.Data;

/**
 * 访问来源记录到es的 access_log_yyyyMM 索引,并通过模板映射到access_log索引
 *
 * @author jvxb
 * @since 2022-12-28
 */
@Data
public class AccessLog {

    private String id;
    //来源ip
    private String sourceIp;
    //请求人id
    private String userId;
    //请求人姓名
    private String userName;
    //请求路径
    private String requestUri;
    //请求方式(get/post)
    private String requestMethod;
    //请求参数
    private String requestParams;
    //请求时间
    private Long requestTime;
    private String requestTimeStr;
    //方法执行时间(ms)
    private Long executeTime;
 
}

第三步:使用RestHighLevelClient 

package com.jvxb.demo.controller;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.jvxb.demo.entity.AccessLog;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.delete.DeleteResponse;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.action.update.UpdateResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.IdsQueryBuilder;
import org.elasticsearch.index.query.TermQueryBuilder;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.*;

@Slf4j
@RestController
@RequestMapping("/es")
@Api(tags = "es控制器")
public class EsController {

    @Autowired
    private RestHighLevelClient restHighLevelClient;

    @PostMapping("/save")
    public Object save(@RequestBody AccessLog accessLog) {
        try {
            //使用uuid作为主键
            accessLog.setId(IdUtil.simpleUUID());
            Date currentDate = new Date();
            accessLog.setRequestTime(currentDate.getTime());
            accessLog.setRequestTimeStr(DateUtil.formatDateTime(currentDate));
            IndexRequest indexRequest = new IndexRequest()
                    .index("access_log")
                    .id(accessLog.getId())
                    .source(JSON.toJSONString(accessLog), XContentType.JSON);
            IndexResponse response = restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);
            log.info("创建状态:{}", response.status());
            return response;
        } catch (Exception e) {
            return e.getMessage();
        }
    }

    @GetMapping("/get/{id}")
    public Object get(@PathVariable("id") String id) {
        AccessLog accessLog = null;
        try {
            GetRequest getRequest = new GetRequest("access_log", id);
            GetResponse getResponse = restHighLevelClient.get(getRequest, RequestOptions.DEFAULT);
            // 将 JSON 转换成对象
            if (getResponse.isExists()) {
                accessLog = JSON.parseObject(getResponse.getSourceAsBytes(), AccessLog.class);
                log.info("accessLog:{}", accessLog);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return accessLog;
    }

    @PostMapping("/get/ids")
    public Object getIds(@RequestBody Map param) {
        List<AccessLog> result = new ArrayList<>();
        try {
            IdsQueryBuilder idsQueryBuilder = new IdsQueryBuilder().addIds(((List<String>) param.get("ids")).toArray(new String[]{}));
            SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().query(idsQueryBuilder);
            SearchRequest searchRequest = new SearchRequest().source(searchSourceBuilder);
            SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
            //获取到结果
            SearchHits hits = response.getHits();
            Iterator<SearchHit> iterator = hits.iterator();
            while (iterator.hasNext()) {
                result.add(JSONObject.parseObject(iterator.next().getSourceAsString(), AccessLog.class));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    @GetMapping("/getList")
    public Object getList(String userName) {
        List<AccessLog> result = new ArrayList<>();
        try {
            //构造查询语句
            SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
            if (StrUtil.isNotBlank(userName)) {
                TermQueryBuilder userNameTermQueryBuilder = new TermQueryBuilder("userName", userName);
                searchSourceBuilder.query(userNameTermQueryBuilder);
            }
            log.info("dsl:{}", searchSourceBuilder.toString());
            //发起查询
            SearchRequest searchRequest = new SearchRequest().indices("access_log").source(searchSourceBuilder);
            SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
            //获取到结果
            SearchHits hits = response.getHits();
            Iterator<SearchHit> iterator = hits.iterator();
            while (iterator.hasNext()) {
                result.add(JSONObject.parseObject(iterator.next().getSourceAsString(), AccessLog.class));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    @PostMapping("/update")
    public Object update(@RequestBody AccessLog accessLog) {
        try {
            //如果需要更新为null值则使用 JSON.toJSONString(accessLog, SerializerFeature.WriteMapNullValue)
            UpdateRequest updateRequest = new UpdateRequest().index("access_log").id(accessLog.getId())
                    .doc(JSON.toJSONString(accessLog), XContentType.JSON);
            UpdateResponse response = restHighLevelClient.update(updateRequest, RequestOptions.DEFAULT);
            log.info("更新状态:{}", response.status());
            return response;
        } catch (Exception e) {
            e.printStackTrace();
            return e.getMessage();
        }
    }

    @PostMapping("/delete/{id}")
    public Object delete(@PathVariable String id) {
        try {
            DeleteRequest deleteRequest = new DeleteRequest().index("access_log").id(id);
            DeleteResponse response = restHighLevelClient.delete(deleteRequest, RequestOptions.DEFAULT);
            log.info("删除状态:{}", response.status());
            return response;
        } catch (Exception e) {
            e.printStackTrace();
            return e.getMessage();
        }
    }
}

记录-Kafka

相关视频教程:

【尚硅谷】Kafka3.x教程(从入门到调优,深入全面)_哔哩哔哩_bilibili

kafka3.0之前,需要配合zk来使用,kafka3.0后zk模式依然可以使用,但已经开始去除了对zk的依赖,对于开发者来说少维护一个组件还是美滋滋的,但是官方宣布从kafka3.0开始放弃对JDK8的支持,kafka4.0之后完全弃用JDK8。(会整活)

下面以kafka3.1为示例,记录它的使用。(以下记录还是基于jdk1.8)

kafka安装

 可以前往官网( Apache Kafka)下载kafka3.1版本后再上传到服务器。也可以通过wget下载。

#1、下载kafka
mkdir -p /tmp/install_pkg && cd /tmp/install_pkg
wget https://archive.apache.org/dist/kafka/3.1.0/kafka_2.13-3.1.0.tgz

#2、解压
tar -zxvf kafka_2.13-3.1.0.tgz
mv kafka_2.13-3.1.0 /opt/kafka3.1.0

#3、修改配置(kraft代替了zk,配置含义附在后面)
vim /opt/kafka3.1.0/config/kraft/server.properties
process.roles=broker,controller
node.id=1
controller.quorum.voters=1@本机(内网)ip:9093(集群时格式:node.id1@host1:9093,node.id2@host2:9093,node.id3@host3:9093)
listeners=PLAINTEXT://本机(内网)ip:9092,CONTROLLER://本机(内网)ip:9093
advertised.listeners = PLAINTEXT://本机(外网)ip:9092
log.dirs=/opt/kafka3.1.0/data

#4、格式化存储目录(3.0新增,多实例时每个实例机器都需要执行一遍,使用相同的uuid)
mkdir -p /opt/kafka3.1.0/data
/opt/kafka3.1.0/bin/kafka-storage.sh random-uuid
$得到一个uuid
/opt/kafka3.1.0/bin/kafka-storage.sh format -t $得到的uuid -c /opt/kafka3.1.0/config/kraft/server.properties

#可以查看格式化后的存储结果(在数据目录下多出的原信息meta.properties)
cat /opt/kafka3.1.0/data/meta.properties

#5、启动并测试
#启动kafka
nohup /opt/kafka3.1.0/bin/kafka-server-start.sh /opt/kafka3.1.0/config/kraft/server.properties &
#创建topic(副本数不能大于实例数)
/opt/kafka3.1.0/bin/kafka-topics.sh --create --topic test_topic --partitions 3 --replication-factor 1 --bootstrap-server localhost:9092
#查看topic
/opt/kafka3.1.0/bin/kafka-topics.sh  --list --bootstrap-server 本机ip:9092
#生成数据(控制台)
/opt/kafka3.1.0/bin/kafka-console-producer.sh --bootstrap-server 本机ip:9092 --topic test_topic
#消费数据(控制台)
/opt/kafka3.1.0/bin/kafka-console-consumer.sh --bootstrap-server 本机ip:9092 --topic test_topic
#停止kafka
/opt/kafka3.1.0/bin/kafka-server-stop.sh /opt/kafka3.1.0/config/kraft/server.properties


#使用systemd管理kafka(推荐)
cat > /etc/systemd/system/kafka.service <<EOF
[Unit]
Description=kafka Server
After=network.target

[Install]
WantedBy=multi-user.target

[Service]
Type=simple
ExecStart=/opt/kafka3.1.0/bin/kafka-server-start.sh /opt/kafka3.1.0/config/kraft/server.properties
ExecStop=/opt/kafka3.1.0/bin/kafka-server-stop.sh /opt/kafka3.1.0/config/kraft/server.properties
TimeoutSec=0
EOF
systemctl daemon-reload
systemctl start/stop/status kafka


#####配置解释#####
process.roles:一个节点可以充当 broker 或 controller 或同时充当。多个角色用逗号分开。
node.id:作为集群中的节点ID,唯一标识,在不同的服务器上这个值不同。其实就是kafka2.0中的broker.id,只是在3.0版本中kafka实例不再只担任broker角色,也有可能是controller角色,所以改名叫做node节点。
controller.quorum.voters:这个配置用于指定controller主控选举的投票节点,所有process.roles包含controller角色的规划节点都要参与。多个节点时其配置格式为:node.id1@host1:9093,node.id2@host2:9093,node.id3@host3:9093
listeners:默认broker 将使用 9092 端口,而 kraft controller控制器将使用 9093端口。
advertised.listeners:指定kafka暴漏的地址(云服务器外网ip),只在内网使用时可注释。
log.dirs:kafka 将存储数据的日志目录(启动前需建好)

kafka使用

kafka 集成到 springboot,只需简单几步。

第一步:引入依赖、配置地址

#可以通过version指定具体版本,springboot2.3时对应版本为2.5.14
<dependency>
	<groupId>org.springframework.kafka</groupId>
	<artifactId>spring-kafka</artifactId>
	<version>2.5.14.RELEASE</version>
</dependency>

<!-- application.yml中:-->
spring:
 kafka:
      bootstrap-servers: jvxb.com:7882 #连接kafka的地址,多个地址用逗号分隔
      consumer:
        group-id: test_group  #不同group会同时消费topic的数据,同一group下只会消费一次topic的数据
        key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
        value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
        enable-auto-commit: true  #开启自动提交offset
        auto-commit-interval: 1000ms #自动提交offset的间隔,默认值是 5000ms
      producer:
        acks: 1 #默认:表示leader必须应答此请求并写入消息到本地日志则请求被认为成功。(0:不等待,-1:等待且写到所有ISR副本)
        retries: 3
        key-serializer: org.apache.kafka.common.serialization.StringSerializer
        value-serializer: org.apache.kafka.common.serialization.StringSerializer

第二步:发送/消费消息

使用 KafkaTemplate发送消息,使用@KafkaListener监听消息

public interface KafkaService {

    void send(String topic, String message);

    Set<String> listTopics() throws Exception;

}
@Slf4j
@Service
public class KafkaServiceImpl implements KafkaService {

    @Autowired
    KafkaTemplate kafkaTemplate;

    /**
     * 生产者端:指定往主题推送数据
     */
    @Override
    public void send(String topic, String message) {
        ListenableFuture listenableFuture = kafkaTemplate.send(topic, message);
        listenableFuture.addCallback(new ListenableFutureCallback<SendResult<String, String>>() {
            @Override
            public void onSuccess(SendResult<String, String> result) {
                System.out.println(result);
                log.info("发送成功回调:{}", result.getProducerRecord().value());
            }

            @Override
            public void onFailure(Throwable ex) {
                log.info("发送失败回调", ex);
            }
        });
    }

    @Override
    public Set<String> listTopics() throws Exception {
        Properties pro = new Properties();
        List<String> bootstrapServers = (List<String>) kafkaTemplate.getProducerFactory().getConfigurationProperties().get("bootstrap.servers");
        pro.put("bootstrap.servers", bootstrapServers.stream().collect(Collectors.joining(",")));
        ListTopicsResult result = KafkaAdminClient.create(pro).listTopics();
        KafkaFuture<Set<String>> names = result.names();
        return names.get();
    }

    /**
     * 消费者端:消费监听的主题数据
     */
    @KafkaListener(topics = {"test_topic", "test_topic2"})
    public void handlerMsg(ConsumerRecord<String, String> consumerRecord) {
        System.out.println(consumerRecord);
        System.out.println("主题[test_topic]接收到消息:消息值:" + consumerRecord.value() + ", 消息偏移量:" + consumerRecord.offset());
    }
}
@RestController
@RequestMapping("/kafka")
@Api(tags = "kafka控制器")
public class KafkaController {

    @Autowired
    KafkaService kafkaService;

    @PostMapping("/send")
    public ResponseDataVo send(@RequestBody Map<String, String> param) {
        Assert.notBlank(param.get("topic"), "topic must not be null");
        Assert.notBlank(param.get("message"), "message must not be null");
        kafkaService.send(param.get("topic"), param.get("message"));
        return ResponseDataVo.success();
    }

    @GetMapping("listTopics")
    public ResponseDataVo getTopicList() throws Exception {
        Set<String> topics = kafkaService.listTopics();
        return ResponseDataVo.success(topics);
    }

}

Kafka界面管理

之前常用的kafka界面管理工具有Kafka-manager、kafka-eagle、Kafdrop、Kafka Web Console等(推荐使用Kafka-manager、kafka-eagle 或 Kafdrop

虽然现在kafka3.0可以使用Raft协议来代替zk,但是kafka-manager(后改名为CMAK)已经无法兼容。如果想要对kafka运维监控,并且集成自有告警系统,需要自研。

记录-Rabbitmq

安装rabbitmq

#以下开始安装 rabbitMq 3.9.9(发布于2021.11.11) + erlang 23.3(rabbitmq依赖)

#安装前置依赖
yum -y install gcc glibc-devel make ncurses-devel openssl-devel xmlto perl wget gtk2-devel binutils-devel xz

#安装erlang
cd /tmp
wget http://erlang.org/download/otp_src_23.3.tar.gz
tar -zxvf otp_src_23.3.tar.gz
cd otp_src_23.3/

# 编译安装
# 指定路径
./configure --prefix=/usr/local/erlang
make install

# 配置环境变量
echo 'export PATH=$PATH:/usr/local/erlang/bin' >> /etc/profile
source /etc/profile

# 测试
erl


#安装rabbitmq
# 下载
cd /tmp
wget https://github.com/rabbitmq/rabbitmq-server/releases/download/v3.9.9/rabbitmq-server-generic-unix-3.9.9.tar.xz

# 解压'*.tar.xz'需要两次
# 第一次解压(*.tar.xz ->*.tar)
xz -d rabbitmq-server-generic-unix-3.9.9.tar.xz
# 第二次解压(*.tar -> *)
tar -xvf rabbitmq-server-generic-unix-3.9.9.tar

mv rabbitmq_server-3.9.9/ rabbitmq3.9.9
mv rabbitmq3.9.9/ /usr/local/

# 启动,并后台运行(默认是5672端口、15672界面管理端口)
cd /usr/local/rabbitmq3.9.9/sbin
./rabbitmq-server -detached

# 停止
./rabbitmqctl stop

# 状态
./rabbitmqctl status

# 启用web插件(可以在web中进行用户管理)
./rabbitmq-plugins enable rabbitmq_management

# 查看用户列表
./rabbitmqctl list_users且只能在本机登录,外网登录需要添加用户
#第一步:添加 admin 用户并设置密码
./rabbitmqctl add_user admin 123456
#第二步:添加 admin 用户为administrator角色
./rabbitmqctl set_user_tags admin administrator
#第三步:设置 admin 用户的权限,指定允许访问的vhost以及write/read
./rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"

需要修改默认端口(5672、15672)时要改动两个地方: 

vim /usr/local/rabbitmq3.9.9/etc/rabbitmq/rabbitmq.conf
添加以下:
#默认端口为5672
listeners.tcp.default=修改后的端口
#界面管理端口(默认端口为15672)
management.tcp.port=修改后的端口

vim /usr/local/rabbitmq3.9.9/sbin/rabbitmq-defaults
在最后添加一行:
CONFIG_FILE=/etc/rabbitmq/rabbitmq.conf

安装rabbitmq(docker)

docker run -d --name rabbit5672 -p 5672:5672 -p 15672:15672 -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=123456 rabbitmq:management 

Rabbitmq使用

rabbitmq 集成到 springboot,只需简单几步。

第一步:引入依赖、配置地址

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

spring: 
  rabbitmq:
    host: jvxb.com
    port: 7872
    username: admin
    password: 123456
    virtual-host: /
    listener:
      simple:
        acknowledge-mode: manual #消费者手动确认

第二步:绑定 交换机与队列

rabbitmq消息发送时是先发送到交换机,并在发送时通过路由键来发送到具体的队列。所以需要先绑定交换机与队列(通过绑定键)。可以在管理界面中绑定(推荐),也可以在代码中绑定。

  • 如果是广播(fanout)交换机时,消息会发送到与其关联的所有队列。
  • 如果是主题(topic)交换机时,消息会发送到与其路由键匹配的队列。
  • 如果是直连(direct)交换机时,消息会发送到与其路由键相等的队列。

本次以主题交换机为例进行消息的收发,模拟收到订单数据时,同步数据到邮箱队列与短信队列。

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitMqConfig {
 
    public static final String ORDER_TOPICEXCHANGE = "order_topic_exchange";
    public static final String SMS_QUEUE = "sms.topic.queue";
    public static final String MAIL_QUEUE = "mail.topic.queue";
    //只有消息的路由键等于sms.topic.binding,才能路由到邮箱(mail)队列
    public static final String MAIL_ORDER_BINDING_KEY = "mail.topic.binding";
    //只要消息的路由键以 topic.binding结尾,则路由到短信(sms)队列
    public static final String SMS_ORDER_BINDING_KEY = "#.topic.binding";
 
    //声明订单主题交换机
    @Bean
    TopicExchange orderTopicExchange() {
        return new TopicExchange(ORDER_TOPICEXCHANGE);
    }
 
    //短信队列
    @Bean
    public Queue smsTopicQueue() {
        return new Queue(SMS_QUEUE);
    }
 
    //邮件队列
    @Bean
    public Queue mailTopicQueue() {
        return new Queue(MAIL_QUEUE, true);  //true 是否持久
    }
 
    //订单交换机与短信队列绑定
    @Bean
    Binding smsBinding() {
        return BindingBuilder.bind(smsTopicQueue()).to(orderTopicExchange()).with(SMS_ORDER_BINDING_KEY);
    }
 
    //订单交换机与邮件队列绑定
    @Bean
    Binding mailBinding() {
        return BindingBuilder.bind(mailTopicQueue()).to(orderTopicExchange()).with(MAIL_ORDER_BINDING_KEY);
    }
}

第三步:发送/消费消息

使用 RabbitTemplate 发送消息,使用@RabbitListener监听消息

public interface RabbitmqService {

    void send(String topic, String routingKey, String message);

}
import com.jvxb.demo.config.RabbitMqConfig;
import com.jvxb.demo.service.RabbitmqService;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @author jvxb
 * @since 2023-02-04
 */
@Service
public class RabbitmqServiceImpl implements RabbitmqService {

    @Autowired
    RabbitTemplate rabbitTemplate;

    @Override
    public void send(String exchange, String routingKey, String message) {
        rabbitTemplate.convertAndSend(exchange, routingKey, message);
    }

    @RabbitListener(queues = {RabbitMqConfig.SMS_QUEUE, RabbitMqConfig.MAIL_QUEUE})
    public void handleSmsMsg(String msg, Message message) {
        String consumerQueue = message.getMessageProperties().getConsumerQueue();
        try {
            System.out.println(String.format("队列%s接收到消息:%s", consumerQueue, msg));
        } catch (Exception e) {
            //可以通过另外途径(mysql/redis)记录这条消息,或者投递到死信队列。
            System.out.println(String.format("队列%s处理消息%s异常:%s,", consumerQueue, msg, e.getMessage()));
        }
    }

}
@RestController
@RequestMapping("/rabbitmq")
@Api(tags = "rabbitmq控制器")
public class RabbitmqController {

    @Autowired
    RabbitmqService rabbitmqService;

    @PostMapping("/send")
    public ResponseDataVo send(@RequestBody Map<String, String> param) {
        Assert.notBlank(param.get("exchange"), "exchange must not be null");
        Assert.notBlank(param.get("routingKey"), "routingKey must not be null");
        Assert.notBlank(param.get("message"), "message must not be null");
        rabbitmqService.send(param.get("exchange"), param.get("routingKey"), param.get("message"));
        return ResponseDataVo.success();
    }
}

可以测试发现当 routingKey = sms.topic.queue 时两个队列都能收到消息,当 routingKey = mail.topic.queue 时,只有 mail Queue 收到消息。符合使用主题交换机的预期。

记录-日志采集(EFK)

相关视频教程:

尚硅谷大数据Filebeat教程(filebeat日志采集系统)_哔哩哔哩_bilibili

EFK(es,filebeat,kibana)是经典架构ELK(es,logstash,kibana)的变种。

filebeat 是 logstash 的轻量级实现,二者都可以用来收集和转发日志。filebeat虽说功能没logstash那么强大,但是胜在轻量、简单易用,在大部分场景下也足够用了。

日志采集的经典链路是:

filebeat -> es -> kibana

filebeat -> kafka -> es -> kibana

日志采集

cd /tmp
wget https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-7.12.1-linux-x86_64.tar.gz
tar zxvf filebeat-7.12.1-linux-x86_64.tar.gz
mv filebeat-7.12.1-linux-x86_64 /opt/filebeat
#配置需要采集的日志目录及输出地址
cat > /opt/filebeat/mybeat.yml <<EOF
filebeat.inputs: 
- type: log
  enabled: true
  paths: 
    - /var/log/nginx/*.log
  # 添加自定义字段
  fields: 
    #内网ip
    intranet_ip: `ifconfig eth0|grep 'inet '|awk '{print $2}'`
  # true 为添加到根节点,false为添加到子节点中  
  fields_under_root: true
output.kafka:
  hosts: ["jvxb.com:7882"]
  topic: 'nginx_log'
  partition.round_robin:
    reachable_only: false
  compression: gzip
EOF

#通过systemctl控制filebeat
cat > /etc/systemd/system/filebeat.service <<EOF
[Unit]
Description=filebeat Server
After=network.target

[Install]
WantedBy=multi-user.target

[Service]
Type=simple
ExecStart=/opt/filebeat/filebeat -e -c /opt/filebeat/mybeat.yml
EOF

#启动filebeat(并设置开机自启)
systemctl start filebeat
systemctl enable filebeat

#nginx日志中有新消息时,filebeat会将消息直接转发到kafka。

采集到后最终的日志格式如下:

{
    "@timestamp": "2023-01-16T15:27:32.818Z",
    "@metadata": {
        "beat": "filebeat",
        "type": "_doc",
        "version": "7.12.1"
    },
    "host": {
        "name": "jvxb.com"
    },
    "agent": {
        "type": "filebeat",
        "version": "7.12.1",
        "hostname": "jvxb.com",
        "ephemeral_id": "d4cdcceb-af45-462d-9fa9-3a2192c329d4",
        "id": "0dcee92b-9303-4682-ac61-9c43788326c8",
        "name": "jvxb.com"
    },
    "log": {
        "file": {
            "path": "/var/log/nginx/access.log"
        },
        "offset": 4998
    },
    "message": "120.229.8.20 - - [16/Jan/2023:23:27:25 +0800] \"GET / HTTP/1.1\" 304 0 \"-\" \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36\" \"-\"",
    "input": {
        "type": "log"
    },
    "intranet_ip": "10.0.10.0",
    "ecs": {
        "version": "1.8.0"
    }
}

日志消费

如nginx 默认的日志格式为:

log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

根据最终上报到kafka时的数据格式,我们需要对其进行解析,主要是需要解析以下内容。

120.229.8.20 - - [17/Jan/2023:00:00:22 +0800] "GET / HTTP/1.1" 304 0 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36" "-"

对应 java 代码如下:

@Data
@ToString
public class NginxAccessLog {

    public static final String INDEX_NAME = "nginx_access_log";

    String nginx_ip;
    String remote_addr;
    String remote_user;
    String time_local;
    Long time;
    String request;
    String request_url;
    Integer status;
    Integer body_bytes_sent;
    String http_referer;
    String http_user_agent;
    String http_x_forwarded_for;
}
public interface NginxAccessLogService {

    NginxAccessLog resolveAccessLog(String message);

    void saveAccessLog(NginxAccessLog nginxAccessLog);

}
@Slf4j
@Service
public class NginxAccessLogServiceImpl implements NginxAccessLogService {

    @Autowired
    private RestHighLevelClient restHighLevelClient;

    @KafkaListener(topics = {"nginx_log"})
    public void handlerNginxLog(ConsumerRecord<String, String> consumerRecord) {
        System.out.println("消费nginx_log: " + consumerRecord.value());
        JSONObject data = JSONObject.parseObject(consumerRecord.value());
        String path = data.getJSONObject("log").getJSONObject("file").getString("path");
        if (path.endsWith("access.log")) {
            String message = data.getString("message");
            NginxAccessLog accessLog = resolveAccessLog(message);
            accessLog.setNginx_ip(data.getString("intranet_ip"));
            saveAccessLog(accessLog);
        }
    }

    @Override
    public NginxAccessLog resolveAccessLog(String message) {
        String accessLogReg = "(\\S+) - (\\S+) \\[(.+)\\] \"([^\"]+)\" (\\d+) (\\d+) \"([^\"]+)\" \"([^\"]+)\" \"([^\"]+)\"$";
        String remoteAddr = message.replaceFirst(accessLogReg, "$1");
        if (remoteAddr == null || remoteAddr.length() > 16) {
            log.error("解析nginx access_log文本异常,文本内容 {}", message);
            return null;
        }
        NginxAccessLog accessLog = new NginxAccessLog();
        accessLog.setRemote_addr(remoteAddr);
        accessLog.setRemote_user(message.replaceFirst(accessLogReg, "$2"));
        accessLog.setTime_local(message.replaceFirst(accessLogReg, "$3"));
        accessLog.setTime(DateUtil.parse(accessLog.getTime_local(), "dd/MMM/yyyy:HH:mm:ss +0800", Locale.ENGLISH).getTime());
        accessLog.setTime_local(DateUtil.format(DateUtil.date(accessLog.getTime()), DatePattern.NORM_DATETIME_PATTERN));
        accessLog.setRequest(message.replaceFirst(accessLogReg, "$4"));
        accessLog.setRequest_url(accessLog.getRequest().split(" ")[1]);
        accessLog.setStatus(Integer.valueOf(message.replaceFirst(accessLogReg, "$5")));
        accessLog.setBody_bytes_sent(Integer.valueOf(message.replaceFirst(accessLogReg, "$6")));
        accessLog.setHttp_referer(message.replaceFirst(accessLogReg, "$7"));
        accessLog.setHttp_user_agent(message.replaceFirst(accessLogReg, "$8"));
        accessLog.setHttp_x_forwarded_for(message.replaceFirst(accessLogReg, "$9"));
        return accessLog;
    }

    @Override
    public void saveAccessLog(NginxAccessLog nginxAccessLog) {
        if (nginxAccessLog == null) {
            return;
        }
        try {
            IndexRequest indexRequest = new IndexRequest()
                    .index(NginxAccessLog.INDEX_NAME)
                    .source(JSON.toJSONString(nginxAccessLog), XContentType.JSON);
            IndexResponse response = restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);
            log.info("创建状态:{}", response.status());
        } catch (IOException e) {
            log.info("创建失败:{}", e.getMessage(), e);
        }
    }

}

存入es后,通过kibana等进行查看。


记录-SpringCloud

相关视频教程:

尚硅谷SpringCloud框架开发教程(SpringCloudAlibaba微服务分布式架构丨Spring Cloud)

通过springcloud,我们可以快速整合和实现常用功能如:服务发现注册、配置中心、服务网关、负载均衡、断路器、消息总线、数据监控等。

springboot、springcloud、springcloud alibaba版本关系参考:spring-cloud-alibaba/wiki/版本说明

父pom.xml参考:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.jvxb</groupId>
    <artifactId>cloud_demo</artifactId>
    <version>1.0.1-SNAPSHOT</version>
    <packaging>pom</packaging>
    <description>Demo project for Spring Cloud</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.12.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <springboot.version>2.3.12.RELEASE</springboot.version>
        <java.version>1.8</java.version>
        <lombok.version>1.18.20</lombok.version>
        <hutool.version>5.6.7</hutool.version>
        <fastjson.version>1.2.83</fastjson.version>
        <mysql.version>5.1.48</mysql.version>
        <mybatisplus.version>3.5.2</mybatisplus.version>
        <swagger2.version>2.9.2</swagger2.version>
        <swaggermodel.version>1.5.22</swaggermodel.version>
        <velocity.version>2.0</velocity.version>
        <jedis.version>3.3.0</jedis.version>
        <elasticsearch.version>7.6.2</elasticsearch.version>
        <spring-kafka.version>2.5.14.RELEASE</spring-kafka.version>
        <amqp.version>2.3.12.RELEASE</amqp.version>
        <spring-cloud.version>Hoxton.SR12</spring-cloud.version>
        <spring-cloud-alibaba.version>2.2.9.RELEASE</spring-cloud-alibaba.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>${hutool.version}</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>${fastjson.version}</version>
        </dependency>
    </dependencies>

    <!-- dependencyManagement中定义的只是依赖的声明,并不实现引入,子项目直接声明需要用的依赖即可,且无需另外指定版本。 -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
                <version>${springboot.version}</version>
            </dependency>

            <dependency>
                <groupId>io.springfox</groupId>
                <artifactId>springfox-swagger2</artifactId>
                <version>${swagger2.version}</version>
                <exclusions>
                    <exclusion>
                        <groupId>io.swagger</groupId>
                        <artifactId>swagger-models</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>

            <dependency>
                <groupId>io.swagger</groupId>
                <artifactId>swagger-models</artifactId>
                <version>${swaggermodel.version}</version>
            </dependency>

            <dependency>
                <groupId>io.springfox</groupId>
                <artifactId>springfox-swagger-ui</artifactId>
                <version>${swagger2.version}</version>
            </dependency>

            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-jdbc</artifactId>
                <version>${springboot.version}</version>
            </dependency>
            
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql.version}</version>
            </dependency>

            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
                <version>${mybatisplus.version}</version>
            </dependency>

            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-generator</artifactId>
                <version>${mybatisplus.version}</version>
            </dependency>

            <dependency>
                <groupId>org.apache.velocity</groupId>
                <artifactId>velocity-engine-core</artifactId>
                <version>${velocity.version}</version>
            </dependency>

            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
                <version>${springboot.version}</version>
                <exclusions>
                    <exclusion>
                        <groupId>io.lettuce</groupId>
                        <artifactId>lettuce-core</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>

            <dependency>
                <groupId>redis.clients</groupId>
                <artifactId>jedis</artifactId>
                <version>${jedis.version}</version>
            </dependency>

            <dependency>
                <groupId>org.elasticsearch.client</groupId>
                <artifactId>elasticsearch-rest-high-level-client</artifactId>
                <version>${elasticsearch.version}</version>
            </dependency>

            <dependency>
                <groupId>org.springframework.kafka</groupId>
                <artifactId>spring-kafka</artifactId>
                <version>${spring-kafka.version}</version>
            </dependency>

            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-amqp</artifactId>
                <version>${amqp.version}</version>
            </dependency>

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

</project>

Nacos

Nacos 官方文档

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service的首字母简称,一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。

Nacos = 注册中心 + 配置中心。(等于早期的 eureka + config + bus)

Nacos 安装

可前往github下载稳定版本,如 Tags · alibaba/nacos · GitHub

或直接下载安装启动

wget https://github.com/alibaba/nacos/releases/download/2.1.1/nacos-server-2.1.1.tar.gz
tar -xvf nacos-server-2.1.1.tar.gz
cd nacos\bin
#启动命令(standalone代表着单机模式运行,非集群模式)
sh startup.sh -m standalone

默认端口为 8848,访问 ip:8848/nacos 即可进入管理页面,默认帐密都是 nacos。

#注意:有防火墙记得开放端口
firewall-cmd --zone=public --list-ports
firewall-cmd --zone=public --add-port=8848/tcp --permanent
firewall-cmd --reload

Nacos集成

服务发现

1、在服务的pom.xml中引入nacos服务发现的依赖
<dependency>
	<groupId>com.alibaba.cloud</groupId>
	<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

2、在服务的bootstrap.yml中配置nacos地址(注意不是application.yml)
spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848

3、在服务主启动类上开启服务注册发现功能:@EnableDiscoveryClient

进行服务注册和发现后,可以使用 RestTemplate 进行服务间方法调用。

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

@GetMapping("/createOrder/{id}")
public ResponseDataVo createOrder(@PathVariable("id") Integer id) {
	ResponseDataVo forObject = restTemplate.getForObject("http://order/order/" + id, ResponseDataVo.class);
	return ResponseDataVo.success(new User(id, "用户" + id).toString() + "创建订单" + forObject.getData());
}

配置管理

1、在服务的pom.xml中引入nacos服务发现的依赖
<dependency>
	<groupId>com.alibaba.cloud</groupId>
	<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

2、在服务的bootstrap.yml中配置nacos地址(注意不是application.yml)
spring:
  cloud:
    nacos:
      config:
        server-addr: 127.0.0.1:8848

3、通过 Spring Cloud 原生注解 @RefreshScope 实现配置自动更新
@RestController
@RequestMapping("/user")
@RefreshScope
public class UserController {

    @Value("${testConfig:false}")
    private String testConfig;
	
	@RequestMapping("/getConfig")
    public ResponseDataVo getConfig() {
        return ResponseDataVo.success(testConfig);
    }
	
}

4、在nacos -> 配置管理 -> 配置列表 中,通过 dataId 对配置进行管理。
${prefix}-${spring.profiles.active}.${file-extension}

dataId示例:user、user-dev  (就是服务名 或者 服务名-环境)

Nacos集群

生产环境上,微服务只有单节点是不允许的,所以需要搭建Nacos集群。

并且nacos默认的数据源是使用的内置数据源,是存在内存中的,重启就会失效。启动nacos集群的时候内存中的数据肯定就不能共享,需要另行配置数据源(多为mysql)。

Nacos集群搭建参考:Nacos2.1.1在Linux中的集群部署详解

#第一步:建立nacos数据库为数据源
#创建nacos数据库
CREATE DATABASE `nacos` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
#执行nacos/conf/nacos-mysql.sql


#第二步:为节点添加数据源与节点配置(以单台机器部署集群为例,此时需要修改启动端口)
cp -r nacos nacos1 
#2.1 配置数据源
cat >> nacos1/conf/application.properties <<EOF
spring.datasource.platform=mysql	
db.num=1
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=root
db.password.0=123456
EOF

#2.2 配置集群节点
cp nacos1/conf/cluster.conf.example nacos1/conf/cluster.conf
vim nacos1/conf/cluster.conf 将example改为具体节点
127.0.0.1:8841
127.0.0.1:8843
127.0.0.1:8845

#注:以集群的方式启动,Nacos默认占用的内存是2G。可以根据实际情况修改:
vim nacos1/bin/startup.sh 
将 -Xms2g -Xmx2g -Xmn1g 改为 -Xms256m -Xmx256m -Xmn125m

#2.3 将一个节点,复制成3个节点,并将3个节点的启动端口分别修改为8841 8843 8845。
cp -r nacos1 nacos2
cp -r nacos1 nacos3
vim nacos1/conf/application.properties 将 server.port 改为 8841
vim nacos2/conf/application.properties 将 server.port 改为 8843
vim nacos3/conf/application.properties 将 server.port 改为 8845

#2.4 启动节点:
sh nacos1/bin/startup.sh
sh nacos2/bin/startup.sh
sh nacos3/bin/startup.sh

#第三步:配置负载均衡
upstream nacos-cluster {
    server localhost:8841;
	server localhost:8843;
	server localhost:8845;
}

server {
    listen       8999;
    server_name  localhost;

    location /nacos {						#监听的请求路径为/nacos
        proxy_pass http://nacos-cluster; 	#反向代理配置
    }
}

#此时项目中配置地址为:
spring:
  cloud:
    nacos:
      discovery:
        server-addr: ip:8999
      config:
        server-addr: ip:8999

OpenFeign

通过 restTemplate + ribbon 我们可以实现通过服务名来调用其他微服务。

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

ResponseDataVo forObject = restTemplate.getForObject("http://order/order/" + id, ResponseDataVo.class);

但还是不够优雅。通过 OpenFeign 我们可以像调普通方法一样进行微服务间的方法调用。下面记录OpenFeign的集成:

OpenFeign集成

第一步:引入依赖

#1、引入依赖
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

第二步:使用 openFeign

若服务端有接口如下:

@RestController
@RequestMapping("/order")
public class OrderController {

    @GetMapping("/{id}")
    public ResponseDataVo<Order> getById(@PathVariable("id") Integer id) {
        return ResponseDataVo.success(new Order(id, "订单" + id));
    }

    @GetMapping("/timeout")
    public ResponseDataVo<Order> timeout(Integer id) {
        try {
            Thread.sleep(id);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return ResponseDataVo.success(new Order(id, "订单" + id));
    }

}

则客户端中: 

先在主启动类上添加启用feign:@EnableFeignClients

//然后写服务类,通过@FeignClient表明该方法由对应服务实现。
@Service
@FeignClient("order")
public interface OrderService {

    @GetMapping("/order/{id}")
    ResponseDataVo<Order> getById(@PathVariable("id") Integer id);

    @GetMapping("/order/timeout")
    ResponseDataVo<Order>  timeout(@RequestParam("id") Integer id);
}


//3、与普通服务类一样调用即可
@Autowired
OrderService orderService; 

ResponseDataVo<Order> forObject = orderService.getById(id);

注意,openFeign底层用的也是ribbon,默认超时时间为1s,一般来说这个时间是过短的。所以需要配置它的超时时间(在bootstrap.yml 或 application.yml 中配置都行)。

ribbon:
  ConnnectTimeout: 5000 #tcp建立连接的时间
  ReadTimeout: 5000 # 设置读取时间

另外 openFeign 可以开启日志打印。

import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FeignConfig {
    /**
     * feign 日志记录级别
     * NONE:无日志记录(默认)
     * BASIC:只记录请求方法和 url 以及响应状态代码和执行时间。
     * HEADERS:记录请求和响应头的基本信息。
     * FULL:记录请求和响应的头、正文和元数据。
     *
     * @return Logger.Level
     */
    @Bean
    public Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
}
#为需要的服务类打印openFeign的日志
logging:
  level:
    com.jvxb.user.service.OrderService: debug

Hystrix

通过openFeign,我们可以像调用本地方法一样进行微服务间的方法调用。但是仍有一个问题,调用时很可能会超时或者出现异常。虽然说我们可以手动通过 try-catch 来使程序继续运行,但是在高负载的情况下,如果不做任何处理的话,此类问题可能会导致服务消费者的资源耗竭甚至整个系统的奔溃。我们把这种因提供者的不可用从而导致消费者不可用,并将不可用逐渐放大的现象称为雪崩效应。要想防止雪崩效应,必须有一个强大的容错机制。该机制需实现以下两点:(1)为网路请求设置超时;(2)使用断路器模式。Hystrix就是一个实现超时机制与断路器模式的工具类库。

Hystrix主要负责解决雪崩效应,进行服务熔断,服务降级和资源隔离。。下面记录Hystrix断路器的使用。

Hystrix集成

第一步:引入依赖,开启断路保护

<!-- Feign中已经包含Hystrix功能,如果已经使用Feign无需继续引入 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

在application.yml中开启hystrix,且关闭或修改hystrix的默认超时时间(默认1s)。如果hystrix开启时其会代替 ribbon 的超时时间。

#开启hystrix
feign:
  hystrix:
    enabled: true

#关闭hystrix超时时间,默认为true
hystrix:
  command:
    default:
      execution:
        timeout:
          enabled: false

#修改hystrix超时时间,默认为1000
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 3000

第二步:进行容错处理

(1)服务调用方接口容错处理

定义容错处理类,并通过Feign的 fallback 属性指定

@Service
@FeignClient(value = "order", fallback = OrderMockServiceImpl.class)
public interface OrderService {

    @GetMapping("/order/{id}")
    ResponseDataVo getById(@PathVariable("id") Integer id);

    @GetMapping("/order/timeout")
    ResponseDataVo<Order>  timeout(@RequestParam("id") Integer id);
}

异常容错类通过继承Feign接口类,在其实现中进行容错处理。

@Service
public class OrderMockServiceImpl implements OrderService {
    @Override
    public ResponseDataVo getById(Integer id) {
        return ResponseDataVo.error("服务调用异常,请稍后重试!");
    }

    @Override
    public ResponseDataVo<Order> timeout(Integer id) {
        return ResponseDataVo.error("服务调用异常,请稍后重试!");
    }
}

(2)服务提供方接口容错处理

服务提供方可以单独引入hystrix依赖。

<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

服务提供方改造feign接口方法实现,添加@HystrixCommand注解

先在主启动类上添加启用hystrix:@EnableCircuitBreaker

@GetMapping("/order/timeout")
@HystrixCommand(fallbackMethod = "timeOutErrorHandler", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2000") })
public ResponseDataVo<Order> timeout(@RequestParam("id") Integer id)
    try {
        //方法休眠超过2秒,一定会发生错误,也就会调用下边的fallbakcMethod方法
        TimeUnit.SECONDS.sleep(id);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return "服务正常调用";
}


/**
 * 这个就是当上边方法异常时的“兜底”方法
 */
public String timeOutErrorHandler() {
    return "对不起,系统处理超时";
}

以上示例理论上是要返回“服务调用正常”,如果造成了超时错误,所以返回兜底的 fallback 中的“对不起,系统处理超时”,而且这个返回是会在2秒后。上述方法存在问题,代码膨胀,耦合度高,业务逻辑混乱

上述方法存在问题,代码膨胀(每个方法都要写fallbackMethod 的话),耦合度高,业务逻辑混乱,解决方法:

@DefaultProperties(defaultFallback = "global_fallback", commandProperties = { .. 可选 }),即定义一个统一的默认处理,然后方法上直接使用@HystrixCommand修饰即可。

第三步:服务熔断

当启用服务降级时,会默认启用服务熔断机制,我们只需要对一些参数进行配置就可以了,就是在上边的 @HystrixCommand 中的一些属性,比如:

@HystrixCommand(fallbackMethod = "timeOutErrorHandler", commandProperties = {
		@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2000"),
		@HystrixProperty(name = "circuitBreaker.enabled", value = "true"),//开启断路器
		@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "20"),//请求次数的峰值
		@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"),//检测错误次数的时间范围
		@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60")//请求失败率达到多少比例后会打开断路器})
})

请求失败数量超过一定比例(默认50%),断路器会切换到开路状态(Open)。 这时所有请求会直接失败而不会发送到服务提供方。断路器保持在开路状态一段时间后(默认5秒),自动切换到半开路状态(HALF-OPEN)。

这时会判断下一次请求的返回情况,如果请求成功,断路器切回闭路状态(CLOSED), 否则重新切换到开路状态(OPEN)。 Hystrix的断路器就像我们家庭电路中的保险丝, 一旦后端服务不可用, 断路器会直接切断请求链, 避免发送大量无效请求影响系统吞吐量, 并且断路器有自我检测并恢复的能力。

值得注意的是:执行回退逻辑并不代表断路器已经打开,请求失败、超时、被拒绝以及断路器打开时都会执行回退逻辑。只有当请求的失败率达到阈值(默认是5秒内20次失败),断路器才会打开。

Sentinel

Hystrix断路器,主要负责解决雪崩效应,进行服务熔断,服务降级和资源隔离。

Sentinel,主要负责熔断降级,系统负载保护,多样化的流量控制,实时监控和控制台。

总的来说,Hystrix与Sentinel的功能基本一致,但Sentinel 的功能更强大,也更值得推荐。(且Hystrix目前已停更)

Sentinel集成

第一步:下载并启动Sentinel

#下载sentinel
wget https://github.com/alibaba/Sentinel/releases/download/1.8.6/sentinel-dashboard-
1.8.6.jar
#启动sentinel,默认是8080端口。 可以通过 --server.port = 8081 修改
java -jar sentinel-dashboard-1.8.6.jar
#浏览器访问(初始帐密:sentinel)
http://机器ip:8080

第二步:微服务引入sentinel

<dependency>
	<groupId>com.alibaba.cloud</groupId>
	<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

<dependency>
	<groupId>com.alibaba.cloud</groupId>
	<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

<!-- 后续做持久化用 -->
<dependency>
	<groupId>com.alibaba.csp</groupId>
	<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>

application.yml中配置sentinel地址

spring:
  cloud:
    sentinel:
      #取消懒加载,默认须服务接口发生调用才加载到sentinel
      eager: true
      transport:
        #配置sentinel dashboard地址
        dashboard: localhost:7880
        #sentinel数据传输端口,默认从8719开始一直往下找
        port: 8719

启动服务,刷新sentinel控制台,可以看到服务已经载入sentinel。此时可以通过控制台对服务接口进行各种控制。

sentinel异常处理

当违反规则时,出来的异常信息页面不够友好和统一,我们可以通过设置统一的异常处理类,针对不同规则显示不同异常信息。

@Component
public class SentinelBlockExceptionHandler implements BlockExceptionHandler {

    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws Exception {
        Map rs = new HashMap();
        rs.put("code", 500);
        rs.put("data", null);
        if (e instanceof FlowException) {
            rs.put("msg", "接口限流了");
        } else if (e instanceof DegradeException) {
            rs.put("msg", "服务降级了");
        } else if (e instanceof ParamFlowException) {
            rs.put("msg", "参数限流了");
        } else if (e instanceof AuthorityException) {
            rs.put("msg", "权限规则不过");
        } else if (e instanceof SystemBlockException) {
            rs.put("msg", "系统保护");
        }
        httpServletResponse.setContentType("application/json;charset=utf-8");
        httpServletResponse.getWriter().write(JSON.toJSONString(rs, SerializerFeature.WriteMapNullValue));
    }
}

也可以通过 @SentinelResource 的方式进行异常处理,@SentinelResource资源的异常处理有两种方式:

  • blockHandler:sentinel定义的失败调用或限制调用,若本次访问被限流或服务降级,则调用blockHandler指定的接口
  • fallback:失败调用,若本接口出现未知异常,则调用fallback指定的接口。

当两个都配置时,也就是当出现sentinel定义的异常时,调用blockHandler,出现其它异常时,调用fallback。

Sentinel监控

不需任何配置,sentinel中可以实时观察到当前服务接口的访问情况。

Sentinel流控

在Sentinel的流控规则中,

  • 阈值类型有:QPS、并发线程数
  • 流控模式有:直接、关联、链路
  • 流控效果有:直接失败、warm up、排队等待

阈值类型释义:

  • QPS:每秒钟查询的次数达到阈值时进行限流。
  • 并发线程数:当某个资源的线程数并发阈值时进行限流。

流控模式释义:

  • 直接:根据调用来源进行限流,默认为default,即针对所有的来源(我不是针对谁)。
  • 关联:指资源同时被两个接口访问时,如果其中一个接口超过qps阈值时,可以对另一个接口进行限流(用于某接口提高优先级)。
  • 链路:指资源同时被两个接口访问时,如果超过阈值时,可以只对其中一个接口进行限流(我只是针对你)。

流控效果释义:

  • 快速失败:当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出FlowException。这种方式适用于对系统处理能力确切已知的情况下,比如通过压测确定了系统的准确水位时。
  • Warm Up:预热/冷启动方式,当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。
  • 排队等待:排队等待即为匀速排队,该方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。


Sentinel熔断

Sentinel的熔断策略有:慢调用比例、异常比例、异常数

  • 慢调用比例:在统计时长内,达到最小请求数,且慢请求数(超过最大RT)与总请求数的比例超过阈值时,进行一定时长的熔断。
  • 异常比例:在统计时长内,达到最小请求数,且异常请求数与总请求数的比例超过阈值时,进行一定时长的熔断。
  • 异常数:在统计时长内,达到最小请求数,且异常请求数超过阈值时,进行一定时长的熔断。

* Sentinel1.8后熔断才有半开状态,默认的熔断行为是抛出DegradeException

Sentinel热点

热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。(需要额外引入sentinel-parameter-flow-control依赖)


系统规则

系统保护规则是从应用级别的入口流量进行控制,从单台机器的总体 Load、RT、入口 QPS 、CPU 使用率和线程数五个维度监控应用数据,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。 系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量 (进入应用的流量) 生效。

  • Load(仅对 Linux/Unix-like 机器生效):当系统 load1 超过阈值,且系统当前的并发线程数超过 系统容量时才会触发系统保护。系统容量由系统的 maxQps * minRt 计算得出。设定参考值一般 是 CPU cores * 2.5。
  • RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
  • 线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
  • 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
  • CPU使用率:当单台机器上所有入口流量的 CPU使用率达到阈值即触发系统保护。


授权规则

授权规则可以对请求方来源做判断和控制,有白名单和黑名单两种方式。

  • 白名单:来源(origin)在白名单内的调用者允许访问
  • 黑名单:来源(origin)在黑名单内的调用者不允许访问

Sentinel是通过RequestOriginParser这个接口的parseOrigin来获取请求的来源的(如给请求头设置origin=gateway)。
注意,如果配置了黑名单,且请求来源存在黑名单中,则拒绝请求,如果配置了白名单,且请求来源存在白名单中则放行。Sentinel 不支持一个黑白名单规则同时配置黑名单和白名单,因此不存优先级的问题。如果请求中没有Origin,则授权规则限流无效。

集群流控

集群流控可以精确地控制整个集群的调用总量,结合单机限流兜底,可以更好地发挥流量控制的效果。(需要额外开发)

持久化

大部分sentinel的规则持久化可以使用nacos实现,但是如果本身不想使用nacos,就为了sentinel的持久化而引入nacos,所以可以改为使用mysql进行持久化,持久化的规则有:授权规则、降级规则、流控规则、热点规则、系统规则。

Gateway

Gateway概念

简单来说,网关 = 路由 + 过滤。

常见的网关有Zuul、Gateway。Zuul是比较早期的一代网关,目前已经停止维护了,所以现在更多使用的是Gateway网关。简单说下二者区别:

Zuul:使用的是阻塞式的 API,不支持长连接,比如 websockets。底层是servlet;Zuul处理的是http请求;没有提供异步支持,流控等均由hystrix支持

Gateway:底层依然是servlet,但使用了webflux,多嵌套了一层框架;提供了异步支持,提供了负载均衡、流量控制

gateway有三个核心概念:

  • Route(路由):路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由
  • Predicate(断言):开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由
  • Filter(过滤):指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改

使用网关后,一般的请求流程即为  web -> nginx -> gateway网关 -> service

下面记录Gateway网关的使用。

Gateway路由

第一步:引入依赖

<dependency>
	<groupId>com.alibaba.cloud</groupId>
	<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

<dependency>
	<groupId>com.alibaba.cloud</groupId>
	<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

第二步:配置注册中心

在bootstrap.yml中配置注册中心地址,参考nacos

spring:
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
      config:
        server-addr: localhost:8848

第三步:配置路由

#方式一:动态路由

spring: 
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由

开启从注册中心动态创建路由的功能,gateway即可利用微服务名进行路由。如直接通过gateway端口 + 微服务名 + 微服务接口地址 就能直接访问到该微服务上的接口。

#方式二:静态路由

spring:
  cloud:
    gateway:
      routes:
        - id: user_service  #路由的ID,没有固定规则但要求唯一,建议配合服务名
          uri: http://localhost:8080    #匹配后提供服务的路由地址,http不能少
          predicates:
            - Path=/user/**     # 断言,路径相匹配的进行路由,多个匹配时使用第一个
 
        - id: order service
          uri: http://localhost:8081
          predicates:
            - Path=/order/**

通过gateway端口 + 微服务接口地址,只要满足 断言条件,也能访问到具体微服务上的接口。不过这样的硬编码对路由更新不太友好,需要重启服务才能更新路由。

最常用的断言 predicates 有 Path、Host、After、Before、Cookie、Header、Method等,能满足绝大部分的情况。

#方式三:代码配置

@Configuration
public class GateWayConfig {
    /**
     * 配置了一个id为your_route_name的路由规则:
     * 当访问地址 gateway服务端口/guonei时,会转发到地址:http://news.baidu.com/guonei
     */
    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        RouteLocatorBuilder.Builder routes = builder.routes();
        routes.route("your_route_name",
                r -> r.path("/guonei").uri("http://news.baidu.com/guonei")
        ).build();
        return routes.build();
    }
}

代码配置的方法与写在配置文件中的静态路由方式基本一致,实现效果也一样。

Gateway过滤器

使用过滤器,可以在请求被路由前或者之后对请求进行修改。

只有两种种类的过滤器:GatewayFilter(单一)、GlobalFilter(全局),默认的内置过滤器加起来已经有40多种。下面记录自定义一个全局过滤器 GlobalFilter 来使用。通过该全局过滤器实现访问日志的打印。

@Component
@Slf4j
public class LogGlobalFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String userName = exchange.getRequest().getQueryParams().getFirst("userName");
        log.info("userName " + userName + " 访问 " + exchange.getRequest().getURI());
        if (userName == null) {
            //非法用户,结束访问。
            log.info("非法用户访问。来源ip:" + exchange.getRequest().getHeaders().getFirst("从nginx中传来的来源ip"));
            ServerHttpResponse response = exchange.getResponse();
            DataBuffer buffer = response.bufferFactory().wrap("非法登录".getBytes(StandardCharsets.UTF_8));
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
            return response.writeWith(Mono.just(buffer));
        }
        return chain.filter(exchange);
    }

    //拦截器的顺序,越小越排前面
    @Override
    public int getOrder() {
        return 0;
    }
}

Sleuth

一个分布式系统往往有很多个服务单元。由于服务单元数量众多,业务的复杂性,如果出现了错误和异常,很难去定位。主要体现在,一个请求可能需要调用很多个服务,而内部服务的调用复杂性,决定了问题难以定位。所以微服务架构中,必须实现分布式链路追踪,去跟进一个请求到底有哪些服务参与,参与的顺序又是怎样的,从而达到每个请求的步骤清晰可见,出了问题,很快定位。

目前,链路追踪组件有Google的Dapper,Twitter 的Zipkin,以及阿里的Eagleeye (鹰眼)等,它们都是非常优秀的链路追踪开源组件。

下面记录如何在Spring Cloud Sleuth中集成Zipkin。在Spring Cloud Sleuth中集成Zipkin非常的简单,只需要引入相应的依赖和做相关的配置即可。

Seata

使用微服务后,单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源,业务操作需要调用三个服务来完成。此时每个服务内部的数据一致性由本地事务来保证, 但是全局的数据一致性问题没法保证。

一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题

Seata是一款开源的的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。

记录-数据监控(Prometheus + Grafna)

Prometheus 是一套开源的监控&报警&时间序列数据库的组合。

Grafana 是一个开源的监控数据分析和可视化套件。

通过 Prometheus + Grafana,我们就可以很方便的对我们的数据进行采集和监控。

Prometheus的工作流程

  • 数据来源:prometheus server 定期从配置好的 jobs 或者 exporters 中拉 metrics。或者接受来自pushgateway发过的 metrics,或者从其他的 prometheus server 中拉取 metrics。
  • 图形显示:在图形界面中,可视化采集数据,可以使用别人写好的grafana模板。类似于通过kibana将 es 中的数据可视化。
  • 告警情况:prometheus server 在本地存储收集到的metrics,并运行已经定义好的arlt.rules,记录新的时间序列或者向alertmanager推送报警,Alertmanager根据配置文件,对接受的警报进行处理,发出告警
     

部署Prometheus

#下载:https://prometheus.io/download/#prometheus
wget https://github.com/prometheus/prometheus/releases/download/v2.37.5/prometheus-2.37.5.linux-amd64.tar.gz
#解压:
tar -zxvf prometheus-2.37.5.linux-amd64.tar.gz
mv prometheus-2.37.5.linux-amd64 /opt/prometheus
#启动:
cd /opt/prometheus
./prometheus &

#访问:(默认端口9090,修改时可在启动命令后添加 --web.listen-address=:9091)
ip:9090


#使用systemd管理prometheus(推荐)
cat > /etc/systemd/system/prometheus.service <<EOF
[Unit]
Description=prometheus Server
After=network.target
[Install]
WantedBy=multi-user.target
[Service]
Type=simple
ExecStart=/opt/prometheus/prometheus --config.file=/opt/prometheus/prometheus.yml --storage.tsdb.path=/data/prometheus --web.enable-admin-api --web.enable-lifecycle
EOF
systemctl daemon-reload
systemctl start/stop/status prometheus

--web.listen-address=:9091  该参数为指定启动端口

--storage.tsdb.path=/data/prometheus  指定promethus数据的存储路径

--web.enable-admin-api   该参数为开启API服务,开启后可通过http请求管理监控数据

--web.enable-lifecycle   该参数为开启动态加载配置文件(修改配置后需要使用curl -X POST http://ip:port/-/reload 热更新)

在promethus页面,有

  • Alerts:查看告警情况,
  • Graph:查看数据情况,
  • Status:查看监控的配置、资源情况,

部署Grafna

#下载:
wget https://dl.grafana.com/enterprise/release/grafana-enterprise-9.0.4.linux-amd64.tar.gz
#解压:
tar -zxvf grafana-enterprise-9.0.4.linux-amd64.tar.gz
mv grafana-9.0.4 /opt/grafana
#启动
cd /opt/grafana
./bin/grafana-server &

#浏览器访问:(默认端口3000,可修改conf/defaults.ini#http_port,初始的账号密码为:admin。 第一次登录完成后需要修改密码。)
ip:3000

#使用systemd管理grafna(推荐)
cat > /etc/systemd/system/grafana.service  <<EOF
[Unit]
Description=grafana server
[Install]
WantedBy=multi-user.target
[Service]
ExecStart=/opt/grafana/bin/grafana-server -homepath=/opt/grafana
ExecReload=/bin/kill -HUP $MAINPID
KillMode=process
EOF
systemctl daemon-reload
systemctl start/stop/status grafana

在 grafna 页面,主要是要配置自己需要的 Dashboard,以便快速查看需要的数据。(自定义创建或者使用模板导入)

Prometheus监控

prometheus server 定期从配置好的 jobs 或者 exporters 中拉 metrics。或者接受来自pushgateway发过的 metrics,或者从其他的 prometheus server 中拉取 metrics。prometheus 官网上已经提供了各种各样的exporter。如 MySQL Exporter(监控mysql), Redis Exporter(监控redis), Kafka Exporter(监控kakfa),  RabbitMQ Exporter(监控rabbitmq), Nginx Exporter(监控nginx),Node Exporter(监控Linux主机)等。

所有可以向Prometheus提供监控样本数据的程序都可以被称为一个Exporter

监控Prometheus状态

vim prometheus.yml 可看到如下内容:默认就是只配置了promethus自身状态的监控。在 prometheus 页面的 Status - Targets 可以查看到 job = promethus 的状态 state 为UP。

global:    #全局配置
  scrape_interval: 15s #设置每15s采集数据一次,默认1分钟
  evaluation_interval: 15s # 每15秒计算一次规则。默认1分钟
alerting:    #告警配置
  alertmanagers:
    - static_configs:
        - targets:
rule_files:    #告警规则
scrape_configs:    #配置数据源,称为target,每个target用job_name命名
  - job_name: "prometheus"    #默认job即为promethus本身(可以将127.0.0.1改为自身ip)
    static_configs:
      - targets: ["127.0.0.1:9090"]

监控Linux主机

第一步:部署 node_exporter 

主机数据通过node_exporter来采集。

#下载:https://prometheus.io/download/#node_exporter
wget https://github.com/prometheus/node_exporter/releases/download/v1.3.1/node_exporter-1.3.1.linux-amd64.tar.gz
#解压:并移动到指定目录
tar xzvf node_exporter-1.3.1.linux-amd64.tar.gz
mv node_exporter-1.3.1.linux-amd64 /opt/node_exporter
#添加systemd管理
cat > /etc/systemd/system/node_exporter.service <<EOF
[Unit]
Description=node_exportier
[Install]
WantedBy=multi-user.target
[Service]
ExecStart=/opt/node_exporter/node_exporter
EOF
#启动node_exporter
systemctl start/stop/status node_exporter

第二步:prometheus配置采集node_exporter

vim prometheus.yml 后添加对node_exporter的监控 job(其中127.0.0.1需换成真实ip)

  - job_name: "nodes"   #监控本机基础数据(磁盘/内存/CPU/网络..)
    static_configs:
     - targets: ["127.0.0.1:9100"]

也可使用以下语句一建添加配置:

cat >> /opt/prometheus/prometheus.yml <<EOF
  - job_name: "node_exporter"  #监控本机基础数据(磁盘/内存/CPU/网络..)
    static_configs:
      - targets: ["`ifconfig eth0|grep 'inet '|awk '{print $2}'`:9100"] 
EOF

修改后检查和更新prometheus.yml

/opt/prometheus/promtool check config prometheus.yml
curl -X POST http://127.0.0.1:9090/-/reload

更新后可以在 prometheus 页面的 Status - Targets 查看到最新的 jobs。

第三步:grafna展示数据

3.1 添加数据源

找到Configuration - Add data source,选择对应的数据源,如Prometheus,输入其地址即可。

3.2 添加仪表板

可以选择从官网中导入仪表板模板 选择Dashboards – import - 9276 ,并选择数据源为Prometheus即可查看 node_exporter 采集的主机基础监控(cpu/内存/磁盘/网络)数据。

可以选择从官网中导入仪表板模板 选择Dashboards – import - 1860 ,并选择数据源为Prometheus即可查看 node_exporter 采集的监控数据。

监控MySql服务

#0)mysql库创建相应用户并赋权:
GRANT REPLICATION CLIENT, PROCESS ON *.* TO 'mysqld_exporter'@'127.0.0.1' identified by '123456';
GRANT SELECT ON performance_schema.* TO 'mysqld_exporter'@'127.0.0.1';
flush privileges;

#1)下载解压:https://prometheus.io/download/#mysqld_exporter
wget https://github.com/prometheus/mysqld_exporter/releases/download/v0.14.0/mysqld_exporter-0.14.0.linux-amd64.tar.gz
tar -zxvf mysqld_exporter-0.14.0.linux-amd64.tar.gz
mv mysqld_exporter-0.14.0.linux-amd64 /opt/mysqld_exporter

#2)配置.my.cnf。即为mysql_exporter添加获取mysql监控数据的账号,如果不写端口,默认为3306(根据自己安装的mysql实际情况填写),.my.cnf默认放置在启动用户的家目录,启动时无需指定;也可以随意放置在任意目录,在启动时通过 --config.my-cnf={conf_dir}/.my.cnf指定配置文件。mysql_exporter默认启动端口为9104,可以通过 web.listen-address指定。
cat > /opt/mysqld_exporter/.my.cnf <<EOF
[client]
user=mysqld_exporter
password=123456
port=13306
EOF

#3)添加systemd管理
cat > /etc/systemd/system/mysqld_exporter.service <<EOF
[Unit]
Description=mysqld_exporter
[Install]
WantedBy=multi-user.target
[Service]
ExecStart=/opt/mysqld_exporter/mysqld_exporter --config.my-cnf=/opt/mysqld_exporter/.my.cnf
EOF

#4)启动mysqld_exporter
systemctl daemon-reload
systemctl start/stop/status mysqld_exporter
#访问测试
curl localhost:9104/metrics

#5)配置prometheus.yml对mysql_exporter的采集(ip修改为真实ip):
cat >> /opt/prometheus/prometheus.yml <<EOF
  - job_name: "mysql_export"  # 监控mysql数据
    static_configs:
      - targets: ["127.0.0.1:9104"]  
EOF
#6)重载配置
/opt/prometheus/promtool check config prometheus.yml
curl -X POST http://127.0.0.1:9090/-/reload
#7)配置仪表盘
Grafana - Dashboard - Import - 763

监控Redis服务

#1)下载解压:https://prometheus.io/download/
wget https://github.com/oliver006/redis_exporter/releases/download/v1.45.0/redis_exporter-v1.45.0.linux-amd64.tar.gz
tar -zxvf redis_exporter-v1.45.0.linux-amd64.tar.gz
mv redis_exporter-v1.45.0.linux-amd64 /opt/redis_exporter
#2)添加systemd管理
cat > /etc/systemd/system/node_exporter.service <<EOF
redis_exporter-v1.45.0.linux-amd64/
[Unit]
Description=redis_exporter
[Install]
WantedBy=multi-user.target
[Service]
ExecStart=/opt/redis_exporter/redis_exporter -redis.addr 127.0.0.1:6379 -web.listen-address 0.0.0.0:9122
EOF
#3)启动node_exporter
systemctl daemon-reload
systemctl start/stop/status redis_exporter
#4)配置prometheus.yml对redis_exporter的采集(ip修改为真实ip):
cat >> /opt/prometheus/prometheus.yml <<EOF
  - job_name: "redis_export"  # 监控redis数据
    static_configs:
      - targets: ["127.0.0.1:9122"]  
EOF
#5)重载配置
/opt/prometheus/promtool check config prometheus.yml
curl -X POST http://127.0.0.1:9090/-/reload
#6)配置仪表盘
Grafana - Dashboard - Import - 763

记录-Netty

Netty 是由 JBOSS 提供的一个 Java 开源框架。Netty 提供异步的、基于事件驱动的网络应用程序框架,用以快速开发高性能、高可靠性的网络 IO 程序,是目前最流行的 NIO 框架,Netty 在互联网领域、大数据分布式计算领域、游戏行业、通信行业等获得了广泛的应用,知名的 Elasticsearch 、Dubbo 框架内部都采用了 Netty。

记录-分布式调度(XxlJob)

常见的分布式任务调度框架有:Elastic-job、lts、Quartzxxl-job、TBSchedule等。

下面记录 xxl-job 的使用。

记录-流程引擎(Activity)

工作流(Workflow),就是通过计算机对业务流程自动化执行管理。它主要解决的是“使在多个参与者之间按照某种预定义的规则自动进行传递文档、信息或任务的过程,从而实现某个预期的业务目标,或者促使此目标的实现”。

Activiti是一个工作流引擎, activiti可以将业务系统中复杂的业务流程抽取出来,使用专门的建模语言BPMN2.0进行定义,业务流程按照预先定义的流程进行执行,实现了系统的流程由activiti进行管理,减少业务系统由于流程变更进行系统升级改造的工作量,从而提高系统的健壮性,同时也减少了系统开发维护成本。


记录-服务编排(Conductor)

每个微服务都提供了n多个接口,当有新功能需要开发时,传统做法是根据需求来进行定制化开发。如新写一个业务类,在业务类里面进行具体调用(服务A->服务B->服务C),但是这种定制化开发的方式不够灵活,第二天可能需求又变为 服务A->服务C->服务B->服务D了,这时我们就得又重新修改业务类重新发版上线。

对此情况,服务编排(Conductor)就出现了。它要求每个服务提供最细粒度的接口,然后可以基于这些原子粒度的接口进行一系列的编排(以json方式),并且提供了超时、重试、异常处理等机制,使服务调用更加灵活稳健。无论你的需求怎么变,我只需要改一下编排过程即可!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值