Nexus

Nexus 是一个用于专门搭建 Maven 仓库的软件,除了作为 Maven 仓库,它还能够作为 Docker 镜像仓库、Yum 仓库等等。

Nexus 部署

deploy.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: nexus
name: nexus
spec:
replicas: 1
selector:
matchLabels:
app: nexus
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: nexus
spec:
initContainers:
- name: volume-mount-hack
image: busybox:latest
command:
- sh
- '-c'
- 'chown -R 200:200 /nexus-data'
volumeMounts:
- name: nexus-data
mountPath: /nexus-data
containers:
- image: sonatype/nexus3:3.37.3
name: nexus
ports:
- containerPort: 8081
env:
- name: INSTALL4J_ADD_VM_PARAMS
value: "-Xms2703m -Xmx2703m -XX:MaxDirectMemorySize=2703m -Djava.util.prefs.userRoot=${NEXUS_DATA}/javaprefs"
resources:
limits:
cpu: 2000m
memory: 2048Mi
requests:
cpu: 2000m
memory: 2048Mi
volumeMounts:
- name: nexus-data
mountPath: /nexus-data
volumes:
- name: nexus-data
# 自行修改挂载类型,不修改则需创建对应PV和PVC👇
persistentVolumeClaim:
claimName: nexus-data

---
apiVersion: v1
kind: Service
metadata:
name: nexus
labels:
app: nexus
spec:
type: NodePort
ports:
- name: nexus
port: 8081
targetPort: 8081
protocol: TCP
selector:
app: nexus

部署后,在容器内部获取 admin 密码

1
echo $(cat /nexus-data/admin.password)

默认仓库说明

  • maven-central:中央仓库,默认从 https://repo1.maven.org/maven2/ 拉取 jar 包
  • maven-releases:私库发行 jar,建议将设置改为 Allow redeploy
  • maven-snapshots:私库快照 jar,即库中的 jar 均为调试版本
  • maven-public:仓库分组,把上面三个仓库组合在一起对外提供服务,在本地 maven 的 settings.xml 文件或项目的 pom.xml 文件设置为该仓库地址,即可调用

仓库类型说明

  • group:仓库组,起到了聚合的作用,在该组中的仓库都可以通过该组的 URL 进行访问
  • hosted:私有仓库,顾名思义,用来存储自己的 jar 包
  • snapshot:快照仓库
  • release:本地项目的正式版本仓库
  • proxy:代理,Nexus 的 maven-central 就是这种类型,代理地址为 https://repo1.maven.org/maven2/ ,默认会去该地址下拉取 jar 包
  • central:中央仓库

新增代理仓库

创建仓库选择 maven2(proxy) 类型

新增代理仓库

添加到 maven-public

添加到组

Maven 配置使用私服

要在本地 Maven 在私服拉取 jar 的方式有两种:

  • settings.xml:全局配置模式
  • pom.xml:项目独享模式

如果两种方式都配置了,那么以 pom.xml 文件配置为准。

当我们通过 Maven 使用 Nexus 的 maven-public 的时候,会按照以下方式顺序访问:

  1. 本地仓库

  2. 私服 maven-releases

  3. 私服 maven-snapshots

  4. 远程阿里 maven 仓库

  5. 远程中央仓库

通过 settings.xml 文件配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- servers块中添加用户认证信息 -->
<server>
<id>nexus</id>
<username>admin</username>
<password>changeme</password>
</server>
<!-- mirrors块中添加maven-public信息 -->
<mirror>
<!-- 唯一标识符 -->
<id>nexus</id>
<!-- 名称 -->
<name>cqm maven</name>
<!-- maven-public地址 -->
<url>http://192.168.159.11:35826/repository/maven-public/</url>
<!-- *指的是访问任何仓库都使用我们的私服,可设置为central等等 -->
<mirrorOf>*</mirrorOf>
</mirror>

也可以设置为阿里的

1
2
3
4
5
6
<mirror>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<mirrorOf>central</mirrorOf>
</mirror>

通过 pom.xml 文件配置

1
2
3
4
5
6
7
8
9
10
11
12
13
<repositories>
<repository>
<id>mnexus</id>
<name>cqm nexus</name>
<url>http://192.168.159.11:35826/repository/maven-public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>

同样也可以设置为阿里的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<repositories>
<repository>
<id>alimaven</id>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
<updatePolicy>always</updatePolicy>
<checksumPolicy>fail</checksumPolicy>
</snapshots>
</repository>
</repositories>

使用 Maven 批量向 Nexus 上传

首先需要将 .m2/repository 下的相应 jar 包和 pom 文件 cp 出来,再进行 deploy。

1
2
# -DrepositoryId 参数调用了 settings.xml 中 servers 块的账号密码进行认证
find . -name "*.jar" | awk '{ gsub("\.jar$","",$0); print "mvn deploy:deploy-file -Dfile="$0".jar -DpomFile="$0".pom -Dpackaging=jar -DrepositoryId=nexus -Durl=\"http://nexus-path\""}'

MySQL

MySQL 是一款关系型数据库管理系统。

logo

一、MySQL基础

1.1 安装MySQL

docker-compose

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
version: '3.8'
services:
db:
image: mysql:5.7.35
command: --default-authentication-plugin=mysql_native_password
restart: always
ports:
- 3306:3306
environment:
MYSQL_ROOT_PASSWORD: toortoor

adminer:
image: adminer:latest
restart: always
ports:
- 8080:8080

1.2 基础SQL语句

  1. 刷新权限
1
flush privileges;
  1. 数据库基本操作
1
2
3
4
5
6
-- 创建
create database [if not exists] `db_name`;
-- 删除
drop database [if exists] `db_name`;
-- 使用
use `db_name`;
  1. 表基本操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
-- 创建
create table [if not exists] `table_name`;
-- 删除
drop table [if exists] `table_name`;
-- 查看
describe `table_name`;
-- 创建一个学生表
create table if not exists `students`(
-- 创建id列,数据类型为4位的int类型,不允许为空,自增
`id` int(4) not null auto_increment comment '学号',
-- 创建name列,数据类型为30位的varchar类型,不允许为空,默认值为匿名
`name` varchar(30) not null default '匿名' comment '姓名',
`pwd` varchar(20) not null default '123456' comment '密码',
`gender` varchar(2) not null default '女' comment '性别',
-- 创建birthday列,datetime类型,默认为空
`brithday` datetime default null comment '出生日期',
`address` varchar(100) default null comment '家庭住址',
`email` varchar(50) default null comment '邮箱',
-- 设置主键,一般一个表只有一个主键
primary key(`id`)
)
-- 设置引擎
engine=innodb
-- 默认编码
default charset=utf8;
  1. 修改表
1
2
3
4
5
6
7
8
9
10
-- 修改表名称
alter table `table_name` rename as new_`table_name`;
-- 增加字段
alter table `table_name` add age int(10);
-- 修改约束
alter table `table_name` modify age varchar(10);
-- 重命名字段
alter table `table_name` change age new_age int(10);
-- 删除字段
alter table `table_name` drop age;
  1. 查看创建语句
1
2
show create database `db_name`;
show create table `table_name`;

1.3 引擎INNODB和MYISAM区别

特性 INNODB MYISAM
事务 支持 不支持
数据行锁定 支持 不支持
外键 支持 不支持
全文索引 不支持 支持
空间占用 约为MYSIAM的2倍 较小

不同引擎在文件上的区别

  • innodb:
    • *.frm:表结构定义文件
    • ibdata1:数据文件
  • mysiam:
    • *.frm:表结构定义文件
    • *.MYD:数据文件
    • *.MYI:索引文件

二、MySQL数据操作

2.1 外键

外键就是一个表的某一列去引用另一个表的某一列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- 该示例为students中的grade_id列引用grade中的grade_id列
create table `grade`(
`grade_id` int(10) not null auto_increment comment '年级'
primary key(`grade_id`)
)engine=innodb default charset=utf8

create table `students`(
`id` int(4) not null auto_increment comment '学号',
`name` varchar(30) not null default '匿名' comment '姓名',
`grade_id` int(10) not null comment '年级',
primary key(`id`),
-- 定义外键
key `FK_grade_id` (`grade_id`),
-- 给外键添加约束
constraint `FK_grade_id` foreign key (`grade_id`) references `grade` (`grade_id`)
)engine=innodb default charset=utf8

如果要给已经存在的表添加外键

1
alter table `table_name` add constraint `FK_name` foreign key (`列名称`) references `引用的表名` (`引用的列名称`)

删除外键

1
alter table `table_name` drop foreign key 'FK_name'

2.2 DML数据操纵语言

2.2.1 insert

1
2
3
4
insert into `table_name`(`字段1`,`字段2`,`字段3`) values('值1'),('值2'),('值3');
insert into `students`(`name`) values('cqm'),('lwt');
-- 字段可以省略,但后面的值必须一一对应
insert into `students`(`name`,`pwd`,`email`) values('lwt','111','lwt@qq.com');

2.2.2 update

1
2
3
4
5
6
7
-- 如果不指定条件,那么会修改所有的值
update `table_name` set `字段`='值' where 条件;
-- 条件可以是 =|<|>|!=|between|and|or 等等
update `students` set `name`='handsome_cqm' where `id`=1;
update `students` set `name`='cqm' where `name`='handsome_cqm'
update `students` set `name`='newlwt',`email`='new@qq.com' where `name`='lwt';
update `students` set `name`='cqm' where id between 1 and 2;

2.2.3 delete

1
2
3
4
delete from `table_name` where 条件;
delete from `students` where `id`=1;
-- 清空表
truncate `table_name`;

deletetruncate 区别:

  • 都可以清空表,都不会删除表结构
  • truncate 可以重置自增列,且不会影响事务
  • delete 清空表后,如果引擎是 innodb,重启数据库自增列就会变回1,因为是存储在内存中的;如果引擎是 myisam,则不会,因为是存储在文件中的

2.3 DQL数据库查询语言

2.3.1 select

  1. 查询所有数据
1
select * from `table_name`;
  1. 查询某列数据
1
select `name`,`gander` from `students`;
  1. 给字段结果起别名
1
select `name` as '姓名',`gender` as '性别' from `students`;
  1. concat 函数
1
select concat('姓名:',`name`) from `students`;
  1. 去重
1
select distinct `字段` from `table_name`
  1. 批量操作数据
1
select `score`+1 as `new_score` from `table_name`;

2.3.2 where

  1. 逻辑运算符运用
1
2
3
select `score` from `table_name` where `score` >= 95 and `score` <= 100;
select `score` from `table_name` where `score` between 95 and 100;
select `score` from `table_name` where not `score` != 95 and `score` != 100;
  1. 模糊查询
运算符 语法 描述
is null a is null a 为空,结果为真
is not null a is not null a 不为空,结果为真
between a between b and c 若 a 在 b 和 c 之间,结果为真
like a like b 如果 a 匹配 b,结果为真
in a in ( b,c,d,e ) 如果 a 在某个集合中的值相同,结果为真

查找花名册中姓陈的名字

1
2
3
4
-- %:匹配一个或多个字符
-- _:匹配一个字符
select `name` from `table_name` where `name` like '陈%';
select `name` from `table_name` where `name` like '陈_明';

查找特定学号的信息

1
select `name` from `table_name` where `id` in (1,2,3);

查找地址为空或不空的同学

1
2
select `name` from `table_name` where `address` is null;
select `name` from `table_name` where `address` is not null;

2.3.3 联表查询

联表查询包括:

  • inner(内连接):如果表中有至少一个匹配,则返回行
  • left(外连接):即使右表中没有匹配,也从左表返回所有的行
  • right(外连接):即使左表中没有匹配,也从右表返回所有的行
  • full(外连接):只要其中一个表中存在匹配,则返回行

inner 实际就是取两个表的交集,例如有两个表,一个表有学生的基本信息,另一个表有学生的成绩,两个表都有学生的学号列,那么就可以联合起来查询

1
select `name`,`subjectno`,`score` from `students` inner join `result` where student.id = result.id

left 假设学生表中有 ccc 这么一个学生,但成绩表里没有 ccc 学生的成绩,如果使用了左连接,左表是学生表,右表是成绩表,那么也会返回 cqm 学生的值,显示为空

leftjoin

right 相反,如果成绩表里有个 ddd 学生的成绩,但学生表里没有这个学生的信息,如果使用了右连接,左表是学生表,右表是成绩表,那么也会返回 ddd 学生的成绩,注意这时候就看不到 ccc 学生的成绩信息了,因为左表中没有 ccc 学生的成绩信息

rightjoin

通过联表查询就可以查出缺考的同学

查询缺考同学

查询参加考试了的同学信息

1
select distinct `name` from `students` right join `result` on students.id = result.id where `score` is not null;

查询参加了考试的同学以及所对应的学科成绩

查询学生对应学科成绩

2.3.4 where和on的区别

在进行联表查询的时候,数据库都会在中间生成一张临时表,在使用外连接时,on 和 where 区别如下:

  • on 条件是在生成临时表时使用的条件,它不管 on 中的条件是否为真,都会返回左边表中的记录。
  • where 条件是在临时表生成好后,再对临时表进行过滤的条件。这时已经没有 left join 的含义(必须返回左边表的记录)了,条件不为真的就全部过滤掉。
  • 如果是 inner join,则无区别

2.3.5 自连接

自连接实际上就是一张表与自己连接,将一张表拆为两张表

原表

categoryid pid categoryname
2 1 计算机科学与技术学院
3 1 心理学院
4 1 体育学院
5 2 python
6 3 心理学
7 4 短跑
8 4 篮球

父类表

category categoryname
2 计算机科学与技术学院
3 心理学院
4 体育学院

子类表

category 所属父类 categoryname
5 计算机科学与技术学院 python
6 心理学院 心理学
7 体育学院 短跑
8 体育学院 篮球

自查询结果

自连接

2.3.6 分页和排序

排序 order by

  • desc:降序
  • asc:升序
1
2
-- 语法
order by desc|asc

查询语文成绩并排序

排序

分页 limit

1
2
-- 语法
limit 起始值,页面显示多少条数据

打印第一页语文成绩

分页第一页

打印第二页数学成绩

分页第二页

分页和排序配合起来就可以查询学生的前几名成绩,例如语文成绩前三名

查询语文成绩前三名

2.3.7 子查询

本质上就是在判断语句里嵌套一个查询语句,例如要查询所有语文成绩并降序排序,可以通过联表查询和子查询两种方式实现

子查询

2.4 聚合函数

2.4.1 count

count 函数用于计数,例如查询有多少学生

1
select count(`name`) from students;

函数内所带的参数不同,背后运行也不同:

  • count(字段):会忽略空值,不计数
  • count(*):不会忽略空值
  • count(1):不会忽略空值
  • 从效率上看:如果查询的列为主键,那么 count(字段) 比 count(1) 快,不为主键则反之;如果表中只有一列,则 count(*) 最快

2.4.2 sum

顾名思义,用于求和,例如查询所有成绩之和

1
select sum(`score`) as '总分' from result

2.4.3 avg

1
select avg(`score`) as '平均分' from result

2.4.4 max和min

1
select max(`score`) as '最高分' from result
1
select min(`score`) as '最低分' from result

查询所有科目的平均分、最高分和最低分

1
2
3
4
5
6
select subjectname, avg(studentresult), max(studentresult), min(studentresult)
from `result`
inner join `subject`
on `result`.subjectno = `subject`.subjectno
-- 定义字段进行分组
group by result.subjectno;

通过分组后的次要条件,查询平均分大于 80 分的科目

1
2
3
4
5
6
7
select subjectname, avg(studentresult) as 平均分
from `result`
inner join `subject`
on `result`.subjectno = `subject`.subjectno
group by `result`.subjectno
-- 分组后的次要条件
having 平均分 > 80;

2.5 MD5加密

在数据库中,密码等敏感信息都不会以明文的形式存储的,可以通过md5 进行加密

1
2
-- 更新密码以达成加密
update students set pwd=md5(pwd);
1
2
-- 插入的时候就进行加密
insert into students values(1, 'cqm', md5('12345'))

2.6 事务

事务就是一系列 SQL 语句,要么全部执行,要么全部不执行。

事务的特性(ACID):

  • 原子性(Atomicity):所有操作要么全部成功,要么全部失败
  • 一致性(Consistency):事务的执行的前后数据的完整性保持一致
  • 隔离性(Isolation):一个事务执行的过程中,不受到别的事务的干扰
  • 持久性(Durability):事务一旦结束,就会持久到数据库

隔离所导致的一些问题:

  • 脏读:一个事务读取到了另一个事务没提交的数据
  • 不可重复读:一个事务的多次查询结果不同,是因为查询期间数据被另一个事务提交而修改了
  • 虚读:一个事务A在进行修改数据的操作时,另一个事务B插入了新的一行数据,而对于事务A来看事务B添加的那行就没有做修改,就发生了虚读
  1. 事务关闭自动提交
1
set autocommit = 0;
  1. 事务开启
1
start transaction;
  1. 事务提交
1
commit;
  1. 事务回滚
1
rollback;
  1. 事务结束
1
set autocommit = 1;
  1. 保存点
1
2
3
4
5
6
-- 添加保存点
savepoint 保存点名称
-- 回滚到保存点
rollback to savepoint 保存点名称
-- 删除保存点
release savepoint 保存点名称

2.7 索引

索引是帮助 MySQL 高效获取数据的数据结构。

索引的分类:

  • 主键索引(primary key):只有一列可以作为主键,且主键的值不可重复,一般用于用户ID之类的
  • 唯一索引(unique key):唯一索引可以有多个,且值唯一
  • 常规索引(key):例如一个表中的数据经常用到,就可以添加个常规索引
  • 全文索引(fulltext):快速定位数据
  1. 查看某个表的所有索引
1
show index from `table_name`;
  1. 添加全文索引
1
alter table `table_name` add fulltext index `index_name`(`要添加索引的字段名`);
  1. 添加常规索引
1
2
-- 这样会给表中某个字段的数据全部都添加上索引,在数据量大的时候可以提高查询效率
create index `id_table_name_字段名` on `table_name`('字段名');

索引使用原则:

  • 并不是索引越多越好
  • 不要对进程变动的数据添加索引
  • 数据量小的表不需要索引
  • 索引一般用于常查询的字段上

2.8 权限管理

在 MySQL 中,用户表为 mysql.user,而权限管理其实都是在该表上操作。

  1. 创建用户
1
2
3
4
5
6
-- host可以为以下的值
-- %:允许所有ip连接
-- localhost:只允许本地连接
-- 192.168.88.%:只允许给网段连接
-- 192.168.88.10:只允许该ip连接
create user 'user_name'@'host' identified by 'user_password';
  1. 修改当前用户密码
1
set password = password('new_password');
  1. 修改指定用户密码
1
set password for user_name = password('new_password');
  1. 重命名
1
rename user user_name to new_user_name;
  1. 授权
1
2
-- 权限:select、insert、delete等等,所有权限则为all
grant 权限 on `db_name`.`table_name` to 'user_name'@'host';
  1. 授予某个用户部分权限
1
grant select,insert on mysql.user to 'cqm'@'%';
  1. 给某个用户授予全部数据库的权限
1
2
-- 基本权限都有,但不会给grant权限
grant all privileges on *.* to 'cqm'@'%';
  1. 删除用户
1
drop user 'user_name'@'host';
  1. 取消权限
1
revoke 权限 on `db_name`.`table_name` from 'user_name'@'host';
  1. 查询用户权限
1
show grants for 'user_name'@'host';

2.9 备份

备份文件都是以 .sql 为后缀的文件。

  1. 导出
1
2
3
4
5
# -d:要操作的数据库
# -h:指定主机
# -P:指定端口
# --all-databases:操作所有数据库
mysqldump -uroot -ppassword -d db1_name db2_name > db_backup.sql
  1. 导入
1
mysqldump -uroot -ppassword -d db_name < db_backup.sql

三、MySQL配置

3.1 主从同步/复制

MySQL 主从同步即每当主数据库进行了数据的操作后,就会将操作写入 binlog 文件,从数据库会启动一个 IO 线程去监控主数据库的 binlog 文件,并将 binlog 文件的内容写入自己的 relaylog 文件中,同时会启动一个 SQL 线程去监控 relaylog 文件,如果发生变化就更新数据。

主从同步流程图

主从复制的类型:

  • statement模式(sbr):只有修改数据的 SQL 语句会记录到 binlog 中,优点是减少了日志量,节省 IO,提高性能,不足是可能会导致主从节点之间的数据有差异。
  • row模式(rbr):仅记录被修改的数据,不怕无法正确复制的问题,但会产生大量的 binlog。
  • mixed模式(mbr):sbr 和 rbr 的混合模式,一般复制用 sbr,sbr 无法复制的用 rbr。

主从同步实现

  1. docker-compose.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
version: '3.8'
services:
mysql_master:
image: mysql:5.7.35
command: --default-authentication-plugin=mysql_native_password
restart: always
environment:
MYSQL_ROOT_PASSWORD: toortoor
ports:
- 3306:3306
volumes:
- ./master/my.cnf:/etc/mysql/my.cnf

mysql_slave:
image: mysql:5.7.35
command: --default-authentication-plugin=mysql_native_password
restart: always
environment:
MYSQL_ROOT_PASSWORD: toortoor
ports:
- 3307:3306
volumes:
- ./slave/my.cnf:/etc/mysql/my.cnf
  1. master/my.cnf
1
2
3
4
5
6
7
8
9
10
11
12
13
[mysqld]
server-id = 1 #节点ID,确保唯一
log-bin = mysql-bin #开启mysql的binlog日志功能
sync_binlog = 1 #控制数据库的binlog刷到磁盘上去,0不控制,性能最好;1每次事物提交都会刷到日志文件中,性能最差,最安全
binlog_format = mixed #binlog日志格式,mysql默认采用statement,建议使用mixed
expire_logs_days = 7 #binlog过期清理时间
max_binlog_size = 100m #binlog每个日志文件大小
binlog_cache_size = 4m #binlog缓存大小
max_binlog_cache_size= 512m #最大binlog缓存大
binlog-ignore-db=mysql #不生成日志文件的数据库,多个忽略数据库可以用逗号拼接,或者复制这句话,写多行
auto-increment-offset = 1 #自增值的偏移量
auto-increment-increment = 1 #自增值的自增量
slave-skip-errors = all #跳过从库错误
  1. slave/my.cnf
1
2
3
4
5
6
7
[mysqld]
server-id = 2
log-bin=mysql-bin
relay-log = mysql-relay-bin
replicate-wild-ignore-table=mysql.%
replicate-wild-ignore-table=test.%
replicate-wild-ignore-table=information_schema.%
  1. 在 master 创建复制用户并授权
1
2
3
create user 'repl_user'@'%' identified by 'toortoor';
grant replication slave on *.* to 'repl_user'@'%' identified by 'toortoor';
flush privileges;
  1. 查看 master 状态
1
2
3
4
5
6
show master status;
+------------------+----------+...
| File | Position |...
+------------------+----------+...
| mysql-bin.000003 | 844 |...
+------------------+----------+...
  1. 在从数据库配置
1
2
3
4
5
6
7
8
9
change master to
MASTER_HOST = '172.19.0.3',
MASTER_USER = 'repl_user',
MASTER_PASSWORD = 'toortoor',
MASTER_PORT = 3306,
MASTER_LOG_FILE='mysql-bin.000003',
MASTER_LOG_POS=844,
MASTER_RETRY_COUNT = 60,
MASTER_HEARTBEAT_PERIOD = 10000;
  1. 启动从配置
1
2
start slave;
show slave status\G;
  1. 如果配置失败,可以执行
1
2
stop slave;
set global sql_slave_skip_counter=1;

3.2 Mycat读写分离

在一般项目中,对于数据库的操作读要远大于写,而如果所有的操作都放在一个节点上,那么就很容易出现压力过大而宕机,读写分离就可以很好解决该问题,主要通过 mycat 的中间件来实现。

mycat架构

分库分表类型:

  • 水平拆分:将不同等级的会员信息写到不同的表中
  • 垂直拆分:将买家信息、卖家信息、商品信息、支付信息等不同信息写到不同的表中

Mycat的主要文件:

文件 说明
server.xml 设置 Mycat 账号、参数等
schema.xml 设置 Mycat 对应的物理数据库和表等
rule.xml 分库分表设置
  1. server.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!-- Mycat用户名 -->
<user name="root" defaultAccount="true">
<!-- 密码 -->
<property name="password">123456</property>
<!-- 逻辑库名 -->
<property name="schemas">TESTDB</property>
<!-- 默认逻辑库 -->
<property name="defaultSchema">TESTDB</property>
<!--No MyCAT Database selected 错误前会尝试使用该schema作为schema,不设置则为null,报
错 -->

<!-- 表级 DML 权限设置 -->
<!-- 0为禁止,1为开启 -->
<!-- 按顺序分别为insert、update、select、delete -->
<!--
<privileges check="false">
<schema name="TESTDB" dml="0110" >
<table name="tb01" dml="0000"></table>
<table name="tb02" dml="1111"></table>
</schema>
</privileges>
-->
</user>
<!-- 其他用户设置 -->
<user name="user">
<property name="password">user</property>
<property name="schemas">TESTDB</property>
<property name="readOnly">true</property>
<property name="defaultSchema">TESTDB</property>
</user>
  1. schema.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<mycat:schema xmlns:mycat="http://io.mycat/">
<!--
name为逻辑库名称,与server.xml文件对应
checkSQLschema为true,sql为select * from table_name
checkSQLschema为false,sql为select * from TESTDB.table_name
sqlMaxLimit是指如果sql中没有limit,则自动添加,如果有则不添加
-->
<schema name="TESTDB" checkSQLschema="true" sqlMaxLimit="100" randomDataNode="dn1">
<!--
name为逻辑表名
dataNode为数据节点名称
rule为规则
-->
<table name="customer" primaryKey="id" dataNode="dn1,dn2" rule="sharding-by-intfile" autoIncrement="true" fetchStoreNodeByJdbc="true">
<childTable name="customer_addr" primaryKey="id" joinKey="customer_id" parentKey="id"> </childTable>
</table>
</schema>
<!--
name为数据节点名称
dataHost为数据库地址
database为mysql中的database
实际就是将TESTDB逻辑库拆成dn1和dn2,而dn1和dn2又对应db1和db2
-->
<dataNode name="dn1" dataHost="localhost1" database="db1" />
<dataNode name="dn2" dataHost="localhost1" database="db2" />
<dataNode name="dn3" dataHost="localhost1" database="db3" />
<!--
balance:
0:不开启读写分离,所有操作都在writeHost上
1:所有读操作都随机发送到当前的writeHost对应的readHost和备用的writeHost
2:所有读操作都随机发送到所有的主机上
3:所有读操作只发送到readHost上
writeType:
0:所有写操作都在第一台writeHost上,第一台挂了再切到第二台
1:所有写操作都随机分配到writeHost
switchType: 用于是否允许writeHost和readHost之间自动切换
-1:不允许
1:允许
2:基于mysql的主从同步的状态决定是否切换
-->
<dataHost name="localhost1" maxCon="1000" minCon="10" balance="0"
writeType="0" dbType="mysql" dbDriver="jdbc" switchType="1" slaveThreshold="100">
<!-- 用此命令来进行心跳检测 -->
<heartbeat>select user()</heartbeat>
<!-- 设置读写分离 -->
<writeHost host="hostM1" url="jdbc:mysql://localhost:3306" user="root"
password="root">
<readHost host="hostS1" url="jdbc:mysql://localhost:3306" user="root"
password="root" />
</readHost>
</writeHost>
</dataHost>
</mycat:schema>
  1. rule.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 平均分算法 -->
<tableRule name="mod-long">
<rule>
<!-- 根据id值平均分 -->
<columns>id</columns>
<algorithm>mod-long</algorithm>
</rule>
</tableRule>
...
<function name="mod-long" class="io.mycat.route.function.PartitionByMod">
<!-- 切片个数 -->
<property name="count">2</property>
</function>
  1. 在 master 节点创建数据库
1
2
3
-- 与schema.xml中的database参数相同
create database db1;
create database db2;
  1. 在每个库里创建表
1
2
3
4
5
create table students(
id int(4),
name varchar(10),
primary key(`id`)
)engine=innodb default charset=utf8;
  1. 开启 Mycat,默认开启8066服务端端口和9066管理端端口
1
./mycat start
  1. 在有安装 mysql 的主机登录 Mycat,也可以通过 navicat 连接
1
mysql -uroot -ptoortoor -h 192.168.88.136 -P 8066

mycat测试一

  1. 只要在 Mycat 进行 SQL 操作,都会流到 mysql 集群中被处理,也可以看到已经实现了分库分表

mycat测试二

3.3 使用haproxy实现Mycat高可用

haproxy 可以实现 Mycat 集群的高可用和负载均衡,而 haproxy 的高可用通过 keepalived 来实现。

高可用集群架构图

  1. 安装 haproxy
1
yum -y install haproxy
  1. 修改日志文件
1
2
3
4
5
vim /etc/rsyslog.conf
# Provides UDP syslog reception
$ModLoad imudp
$UDPServerRun 514
systemctl restart rsyslog
  1. 配置 haproxy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
vim /etc/haproxy/haproxy.cfg
# haproxy的配置文件由两个部分构成,全局设定和代理设定
# 分为五段:global、defaults、frontend、backend、listen
global
# 定义全局的syslog服务器
log 127.0.0.1 local2
chroot /var/lib/haproxy
pidfile /var/run/haproxy.pid
maxconn 4000
user haproxy
group haproxy
# 设置haproxy后台守护进程形式运行
daemon
stats socket /var/lib/haproxy/stats
defaults
# 处理模式
# http:七层
# tcp:四层
# health:状态检查,只会返回OK
mode tcp
# 继承global中log的定义
log global
option tcplog
option dontlognull
option http-server-close
# option forwardfor except 127.0.0.0/8
# serverId对应的服务器挂掉后,强制定向到其他健康的服务器
option redispatch
retries 3
timeout http-request 10s
timeout queue 1m
timeout connect 10s
timeout client 1m
timeout server 1m
timeout http-keep-alive 10s
timeout check 10s
maxconn 3000
frontend mycat
# 开启本地监控端口
bind 0.0.0.0:8066
bind 0.0.0.0:9066
mode tcp
log global
default_backend mycat
backend mycat
balance roundrobin
# 监控的Mycat
server mycat1 192.168.88.135:8066 check inter 5s rise 2 fall 3
server mycat2 192.168.88.135:8066 check inter 5s rise 2 fall 3
server mycatadmin1 192.168.88.136:9066 check inter 5s rise 2 fall 3
server mycatadmin2 192.168.88.136:9066 check inter 5s rise 2 fall 3
listen stats
mode http
# 访问haproxy的端口
bind 0.0.0.0:9999
stats enable
stats hide-version
# url路径
stats uri /haproxy
stats realm Haproxy\ Statistics
# 用户名/密码
stats auth admin:admin
stats admin if TRUE
  1. 访问 haproxy

haproxy界面

3.4 使用keepalived实现去中心化

  1. 安装 keepalived
1
yum -y install keepalived
  1. 配置 Master 节点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
vim /etc/keepalived/keepalived.conf
! Configuration File for keepalived

global_defs {
# 识别节点的id
router_id haproxy01
}

vrrp_instance VI_1 {
# 设置角色,由于抢占容易出现 VIP 切换而闪断带来的风险,所以该配置为不抢占模式
state BACKUP
# VIP所绑定的网卡
interface ens33
# 虚拟路由ID号,两个节点必须一样
virtual_router_id 30
# 权重
priority 100
# 开启不抢占
# nopreempt
# 组播信息发送间隔,两个节点必须一样
advert_int 1
# 设置验证信息
authentication {
auth_type PASS
auth_pass 1111
}
# VIP地址池,可以多个
virtual_ipaddress {
192.168.88.200
}
# 将 track_script 块加入 instance 配置块
track_script{
chk_haproxy
}
}

# 定义监控脚本
vrrp_script chk_haproxy {
script "/etc/keepalived/haproxy_check.sh"
# 时间间隔
interval 2
# 条件成立权重+2
weight 2
}
  1. 配置 Slave 节点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
vim /etc/keepalived/keepalived.conf
! Configuration File for keepalived

global_defs {
router_id haproxy02
}

vrrp_instance VI_1 {
state BACKUP
interface ens33
virtual_router_id 30
priority 80
# nopreempt
advert_int 1
authentication {
auth_type PASS
auth_pass 1111
}
virtual_ipaddress {
192.168.88.200
}
track_script{
chk_haproxy
}
}

vrrp_script chk_haproxy {
script "/etc/keepalived/haproxy_check.sh"
interval 2
weight 2
}
  1. 编写监控脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
vim /etc/keepalived/haproxy_check.sh
#!/bin/bash

START_HAPROXY="systemctl start haproxy"
STOP_KEEPALIVED="systemctl stop keepalived"
LOG_DIR="/etc/keepalived/haproxy_check.log"
HAPS=`ps -C haproxy --no-header | wc -l`

date "+%F %H:%M:%S" > $LOG_DIR
echo "Check haproxy status" >> $LOG_DIR
if [ $HAPS -eq 0 ];
then
echo "Haproxy is down" >> $LOG_DIR
echo "Try to turn on Haproxy..." >> $LOG_DIR
echo $START_HAPROXY | sh
sleep 3
if [ `ps -C haproxy --no-header | wc -l` -eq 0 ];
then
echo -e "Start Haproxy failed, killall keepalived\n" >> $LOG_DIR
echo $STOP_KEEPALIVED | sh
else
echo -e "Start Haproxy successed\n" >> $LOG_DIR
fi
else
echo -e "Haproxy is running\n" >> $LOG_DIR
fi
  1. 启动 keepalived 后可以看到 VIP 被哪台服务器抢占了,通过该 VIP 就可以访问到对应的 haproxy,haproxy 就会将流量流到后面的 Mycat,再由 Mycat 来实现分表分库;haproxy 停止后 keepalived 也会通过脚本尝试去重新开启,如果开启不成功就会停止 keepalived,VIP 就由 slave 节点抢占,用户依旧可以通过 VIP 来操控数据库,且无感知。

keepalived测试一

  1. 通过 VIP 去连接 Mycat 插入数据,尝试能否实现分库分表

keepalived测试二

可以看到插入的数据都分到了 db1、db2 中

keepalived测试三

3.5 Sharding JDBC读写分离

Apache ShardingSphere 是一套开源的分布式数据库解决方案组成的生态圈,它由 JDBC、Proxy 和 Sidecar(规划中)这 3 款既能够独立部署,又支持混合部署配合使用的产品组成。

Sharding JDBC 同样也可以实现分库分表、数据分片、读写分离等功能,但与 Mycat 不同的是,Mycat 是一个独立的程序,而 Sharding JDBC 是以 jar 包的形式与应用程序融合在一起运行的。

Redis

一、简介

REmote DIctionary Server(Redis)是一个由 Salvatore Sanfilippo 写的 key-value 存储系统,是跨平台的非关系型数据库。

redis_logo

Redis 就是一款 NoSQL,而 NoSQL 就是指非关系型数据库,主要分为四种:

  1. 键值型:Redis
  2. 文档型:ElasticSearch、Mongdb
  3. 面向列:Hbase
  4. 图形化:Neo4j

二、Redis基础

2.1 Redis安装

  1. 通过 docker-compose 安装 redis
1
2
3
4
5
6
7
8
9
10
version: '3.8'
services:
redis:
image: daocloud.io/library/redis:5.0.9
restart: always
container_name: redis
environment:
- TZ=Asia/Shanghai
ports:
- 6379:6379
  1. 进入容器内部测试 redis
1
2
3
4
5
6
# 连接redis
redis-cli
# set新建键值对
set key value
# get获取键值
get key

2.2 Redis常用命令

redis 的数据存储结构有以下几种:

  • key-string(字符串):一个 key 对应一个值
  • key-hash(哈希):一个 key 对应一个 map
  • key-list(列表):一个 key 对应一个列表
  • key-set(集合):一个 key 对应一个集合
  • key-zset(有序集合):一个 key 对应一个有序的集合

redis一

2.2.1 string常用命令

  1. 设置值
1
set key value
  1. 取值
1
get key
  1. 批量操作
1
2
mset key1 value1 key2 value2 ...
mget key1 key2 ...
  1. 自增
1
incr key
  1. 自减
1
decr key
  1. 自增自减指定数量
1
2
incrby key number
decrby key number
  1. 设置值的同时指定生存时间
1
setex key seconds value
  1. 设置值,如果当前 key 不存在如同 set,如果存在则说明都不做
1
setex key value
  1. 在 key 对应的 value 后追加内容
1
append key value
  1. 查看 value 字符串长度
1
strlen key

2.2.2 hash常用命令

  1. 存储数据
1
hset key field value
  1. 获取数据
1
hget key field
  1. 批量操作
1
2
hmset key1 field1 value1 field2 value2 ...
hmget key1 firle1 field2 ...
  1. 指定自增
1
hincrby key field number
  1. 设置值,如果当前 key 不存在如同 set,如果存在则说明都不做
1
hsetnx key field value
  1. 检查 field 是否存在
1
hexists key field
  1. 删除某个 field
1
hdel key field1 field2 ...
  1. 获取当前 hash 结构中的全部 field 和 value
1
hgetall key
  1. 获取当前 hash 结构中的全部 field
1
hkeys key
  1. 获取当前 hash 结构中的全部 value
1
hvals key
  1. 获取当前 hash 中 field 的数量
1
hlen key

2.2.3 list常用命令

  1. 存储数据
1
2
3
4
5
6
7
8
9
10
11
# 从左侧插入数据
lpush key value1 value2 ...
# 从右侧插入数据
rpush key value1 value2 ...

# 如果key不存在,什么都不做,如果key存在但不是list结构,也什么都不做
lpushx key value1 value2 ...
rpushx key value1 value2 ...

# 通过索引位置添加value
lset key index value
  1. 获取数据
1
2
3
4
5
6
7
8
9
10
11
12
13
# 左侧弹出数据并移除
lpop key
# 右侧弹出数据并移除
rpop key

# 获取一定范围的数据,start从0开始,stop为-1时为最后一个value,-2时为倒数第二个value
lrange key start stop

# 根据索引位置获取value
lindex key index

# 获取整个list的长度
llen key
  1. 删除数据
1
2
3
4
5
6
7
8
# 删除list中count个value的值,当count>0,从左向右删除,但count<0,从右向左删除,但count==0,全部删除
lrem key count value

# 保留列表中指定范围内的数据,超出这个范围的都会被移除
ltrim start stop

# 将list1中的最后一个数据弹出,插入到list2中的第一个位置
rpoplpush key1 key2

2.2.4 set常用命令

  1. 存储数据
1
2
# value不允许重复,且数据无序排列
sadd key value1 value2 ...
  1. 获取数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 获取全部数据
smembers key

# 随机获取数据,并移除,可加弹出数量
spop key number

# 取多个set的交集
sinter key1 key2 ...

# 取多个set的并集
sunion key1 key2 ...

# 取多个set的差集
sdiff key1 key2 ...

# 查看当前set是否包含某个值
sismember key value
  1. 删除数据
1
srem key value1 value2 ...

2.2.5 zset常用命令

  1. 存储数据
1
2
3
4
5
# score必须是数值,value不允许重复
zadd key score1 value1 score2 value2 ...

# 修改score,如果value存在则增加分数,如果不存在则相当于zadd
zincrby key number value
  1. 获取数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 查看指定value的分数
zscore key value

# 获取value数量
zcard key

# 根据score范围查询value数量
zcount key min max

# 根据分数从小到大排序,获取指定范围内的数据,添加了withscores参数会返回value的具体score
zrange key start stop withscores

# 从大到小
zrevrange key start stop withscores

# 根据分数的范围获取数据,如果不希望包括min和max的值可以用(min max)的方式,最大最小值用±inf表示
zrangebyscore key min max withscores [limit,offset,count]
zrevrangebyscore key max min withscores [limit,offset,count]
  1. 删除数据
1
zrem key value1 value2 ...

2.2.6 key常用命令

  1. 查看所有key
1
keys *
  1. 查看某个key是否存在
1
exists key
  1. 删除key
1
del key
  1. 设置key的生存时间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 单位为s
expire key seconds

# 单位为ms
pexpire key milliseconds

# 指定生存到某个时间点
expireat key timestamp
pexpireat key millseconds

# 查看key的剩余生存时间,返回-2则key不存在,-1则没设置生存时间
ttl key
pttl key

# 移除生存时间
persist key
  1. 选择操作的库
1
2
3
4
5
# redis默认有16个库
select 0~15

# 移动key到另一个库中
move key db

2.2.7 库的常用命令

  1. 清空当前所在数据库
1
flushdb
  1. 清空所有数据库
1
flushdball
  1. 查看当前库有多少key
1
dbsize
  1. 查看最后一次操作的时间
1
lastsave
  1. 实时监控redis接收到的命令
1
monitor

三、Redis配置

3.1 Redis的AUTH

  1. docker-compose.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
version: '3.8'
services:
redis:
image: daocloud.io/library/redis:5.0.9
container_name: redis
restart: always
environment:
- TZ=Asia/Shanghai
ports:
- 6379:6379
volumes:
- ./redis.conf:/usr/local/redis/redis.conf
command: ["redis-server", "/usr/local/redis/redis.conf"]
  1. 设置 Redis 连接密码
1
2
vim redis.conf
requirepass toortoor
1
docker-compose up -d
  1. 进入 Redis 之后都需要输入密码才可以创建 key
1
2
redis-cli
auth toortoor

3.2 Redis的事务

Redis 事务可以一次执行多个命令, 并且带有以下三个重要的保证:

  • 批量操作在发送 EXEC 命令前被放入队列缓存
  • 收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行
  • 在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中

一个事务从开始到执行会经历以下几个阶段:

  • 开始事务:multi
  • 命令入队:……
  • 执行事务:exec
  • 取消事务:discard
  • 监听:在开启事务之前,先通过 watch 监听事务中要操作的 key,如果在事务过程中有其他的客户端修改了 key,那么事务将会被取消

事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。

  1. 监听
1
watch name age gander
  1. 开始事务
1
multi
  1. 命令入队
1
2
3
set name cqm
set age 22
set gander male
  1. 执行事务/取消事务
1
exec/discard

3.3 Redis的持久化

3.3.1 RDB持久化

Redis 的配置文件位于 Redis 安装目录, ROB 是默认的持久化机制。

  1. docker-compose.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
version: '3.8'
services:
redis:
image: daocloud.io/library/redis:5.0.9
container_name: redis
restart: always
environment:
- TZ=Asia/Shanghai
ports:
- 6379:6379
volumes:
- ./redis.conf:/usr/local/redis/redis.conf
- ./data:/data
# 加载redis.conf
command: ["redis-server", "/usr/local/redis/redis.conf"]
  1. redis.conf
1
2
3
4
5
6
7
8
9
vim redis.conf
# 在900秒内有1一个 key 发生了变化,就执行 RDB 持久化
save 900 1
save 300 10
save 60 10000
# 开启RDB持久化
rdbchecksum yes
# RDB持久化名称
dbfilename dump.rdb
  1. 测试持久化
1
2
3
4
set name cqm
set age 22
# 关闭redis并保存
shutdown save
  1. 可以看到 data 目录下多了个 rdb 文件,即使 redis 容器重启也不会造成 key 的丢失

redis二

3.3.2 AOF持久化

AOF 比起 RDB 有更高的数据安全性,如果同时开启了 AOF 和 RDB,那么前者比后者的优先级更高,且如果先开启了 RDB 在开启 AOF,那么 RDB 中的内容会被 AOF 的内容覆盖。

  1. redis.conf
1
2
3
4
5
6
7
8
9
# 开启AOF持久化
appendonly yes
# AOF文件名
appendfilename appendonly.aof
# AOF持久化执行策略
# always:每次执行写操作都调用fsync
# everysec:最多每秒调用一次fsync
# no:根据环境的不同在不确定的时间调用fsync
appendfsync always|everysec|no
  1. 重启 docker-compose 测试
1
2
3
set gander male
# 不RDB持久化
shutdown nosave
  1. 可以看到 data 目录下多了个 aof 文件,即 AOF 持久化生成的文件

redis三

3.4 Redis主从架构

Redis 的主从架构是指 Master 节点负责写操作,而其余的 Slave 节点负责读操作。

redis四

  1. docker-compose.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
version: '3.8'
services:
redis-master:
container_name: redis-master
image: daocloud.io/library/redis:5.0.9
restart: always
environment:
- TZ=Asia/Shanghai
ports:
- 7001:6379
volumes:
- ./redis1.conf:/usr/local/redis/redis.conf
- ./data1:/data
command: ["redis-server", "/usr/local/redis/redis.conf"]
redis-slave1:
container_name: redis-slave1
image: daocloud.io/library/redis:5.0.9
restart: always
environment:
- TZ=Asia/Shanghai
ports:
- 7002:6379
volumes:
- ./redis2.conf:/usr/local/redis/redis.conf
- ./data2:/data
command: ["redis-server", "/usr/local/redis/redis.conf"]
# 连接redis-master容器,并将该容器ip地址映射为master
links:
- redis-master:master
redis-slave2:
container_name: redis-slave2
image: daocloud.io/library/redis:5.0.9
restart: always
environment:
- TZ=Asia/Shanghai
ports:
- 7003:6379
volumes:
- ./redis3.conf:/usr/local/redis/redis.conf
- ./data3:/data
command: ["redis-server", "/usr/local/redis/redis.conf"]
links:
- redis-master:master
  1. 从节点 redis.conf
1
2
# slaveof <主节点地址> <端口>
slaveof master 6379
  1. 启动后进入容器内部通过 info 可看到节点信息

redis五

3.5 Redis哨兵模式

Redis 的主从架构有一个很明显的问题,就是当 Master 节点出现问题宕机后,那么 Redis 集群就没有可以进行写操作的 Redis 了,而哨兵就可以解决该问题。

在每个节点中都会有个哨兵与 Redis 进行连接,且哨兵与哨兵之间也会进行连接,如果 Master 节点出现故障宕机了,那么哨兵们就会选出一个 Slave 来作为新的 Master 来提供写的操作。

redis六

  1. docker-compose.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
version: '3.8'
services:
redis-master:
container_name: redis-master
image: daocloud.io/library/redis:5.0.9
restart: always
environment:
- TZ=Asia/Shanghai
ports:
- 7001:6379
volumes:
- ./redis1.conf:/usr/local/redis/redis.conf
- ./data1:/data
- ./sentinel1.conf:/data/sentinel.conf
command: ["redis-server", "/usr/local/redis/redis.conf"]
redis-slave1:
container_name: redis-slave1
image: daocloud.io/library/redis:5.0.9
restart: always
environment:
- TZ=Asia/Shanghai
ports:
- 7002:6379
volumes:
- ./redis2.conf:/usr/local/redis/redis.conf
- ./data2:/data
- ./sentinel2.conf:/data/sentinel.conf
command: ["redis-server", "/usr/local/redis/redis.conf"]
links:
- redis-master:master
redis-slave2:
container_name: redis-slave2
image: daocloud.io/library/redis:5.0.9
restart: always
environment:
- TZ=Asia/Shanghai
ports:
- 7003:6379
volumes:
- ./redis3.conf:/usr/local/redis/redis.conf
- ./data3:/data
- ./sentinel3.conf:/data/sentinel.conf
command: ["redis-server", "/usr/local/redis/redis.conf"]
links:
- redis-master:master
  1. Master 节点 sentinel.conf
1
2
3
4
5
6
# 以守护进程的方式运行redis
daemonize yes
# 指定Master节点,sentinel monitor <名称> <ip> <端口> <Slave个数>
sentinel monitor master localhost 6379 2
# 指定哨兵每隔多久检测一次redis主从架构
sentinel down-after-milliseconds master 10000
  1. Slave 节点 sentinel.conf
1
2
3
daemonize yes
sentinel monitor master master 6379 2
sentinel down-after-milliseconds master 10000
  1. 进入容器启动哨兵,当 Master 节点出现问题后,就会在两个 Slave 中选出一个作为新的 Master,而旧的 Master 启动后就会变为新的 Slave
1
redis-sentinel /data/sentinel.conf

3.6 Redis集群

Redis 集群在保证主从和哨兵的基本功能之外,还能提高 Redis 存储数据的能力,主要的特点如下

  • Redis 集群是无中心的
  • Redis 集群有ping-pang的机制
  • 投票机制,集群节点的数量必须是 2n+1
  • 分配了 16484 个 hash 槽,在存储数据时,会对 key 进行 crc16 的算法,并对 16384 进行取余,通过结果分配到对应的节点上,每个节点都有自己维护的 hash 槽
  • 每个主节点都要跟一个从节点,但这里的从节点只管备份,不管查询
  • 集群中半数的节点宕机后,那么集群就瘫痪

redis七

  1. docker-compose.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
version: '3.8'
services:
redis1:
container_name: redis1
image: daocloud.io/library/redis:5.0.9
restart: always
environment:
- TZ=Asia/Shanghai
ports:
- 7001:7001
- 17001:17001
volumes:
- ./conf.d/redis1.conf:/usr/local/redis/redis.conf
command: ["redis-server", "/usr/local/redis/redis.conf"]
redis2:
container_name: redis2
image: daocloud.io/library/redis:5.0.9
restart: always
environment:
- TZ=Asia/Shanghai
ports:
- 7002:7002
- 17002:17002
volumes:
- ./conf.d/redis2.conf:/usr/local/redis/redis.conf
command: ["redis-server", "/usr/local/redis/redis.conf"]
redis3:
container_name: redis3
image: daocloud.io/library/redis:5.0.9
restart: always
environment:
- TZ=Asia/Shanghai
ports:
- 7003:7003
- 17003:17003
volumes:
- ./conf.d/redis3.conf:/usr/local/redis/redis.conf
command: ["redis-server", "/usr/local/redis/redis.conf"]
redis4:
container_name: redis4
image: daocloud.io/library/redis:5.0.9
restart: always
environment:
- TZ=Asia/Shanghai
ports:
- 7004:7004
- 17004:17004
volumes:
- ./conf.d/redis4.conf:/usr/local/redis/redis.conf
command: ["redis-server", "/usr/local/redis/redis.conf"]
redis5:
container_name: redis5
image: daocloud.io/library/redis:5.0.9
restart: always
environment:
- TZ=Asia/Shanghai
ports:
- 7005:7005
- 17005:17005
volumes:
- ./conf.d/redis5.conf:/usr/local/redis/redis.conf
command: ["redis-server", "/usr/local/redis/redis.conf"]
redis6:
container_name: redis6
image: daocloud.io/library/redis:5.0.9
restart: always
environment:
- TZ=Asia/Shanghai
ports:
- 7006:7006
- 17006:17006
volumes:
- ./conf.d/redis6.conf:/usr/local/redis/redis.conf
command: ["redis-server", "/usr/local/redis/redis.conf"]
  1. redis{1..6}.conf,x 为 {1..6}
1
2
3
4
5
6
7
8
9
10
11
12
# 指定redis端口
port 700x
# 开启集群
cluster-enabled yes
# 集群信息文件
cluster-config-file nodes-700x.conf
# 集群对外ip
cluster-announce-ip 192.168.88.135
# 集群对外端口
cluster-announce-port 700x
# 集群总线端口
cluster-announce-bus-port 1700x
  1. 进入任意 reids 容器创建集群
1
2
# --cluster-replicas:每个主节点分配的从节点个数
redis-cli --cluster create 192.168.88.135:7001 192.168.88.135:7002 192.168.88.135:7003 192.168.88.135:7004 192.168.88.135:7005 192.168.88.135:7006 --cluster-replicas 1
  1. 由于每个主节点都被分配了不同的 hash 槽,所以要在容器内任意切换不同的 redis 节点需要加参数 -c
1
redis-cli -h 192.168.88.135 -p 7001 -c

四、Redis常见问题

4.1 Redis的删除策略

当 key 的生存时间到了,Redis 并不会立即删除该 key,而是遵守以下删除策略来进行删除

  • 定期删除:Redis 每隔一段时间就回去查看设置了生存时间的 key,默认是 100ms 查看 3 个 key
  • 惰性删除:当用户去查询已经超过了生存时间的 key,Redis 会先查看该 key 是否已经超过了生存时间,如果超过,那么 Redis 会将该 key 删除并给用户返回一个空值

4.2 Redis的淘汰机制

当 Redis 内存满的时候添加了一个新的数据,那么就会执行 Redis 的淘汰机制,通过 maxmemory-policy 来设置,参数如下

  1. volatile-lru:当内存不足时,会删除一个设置了生存时间且最近最少使用的 key

  2. allkeys-lru:当内存不足时,会删除一个设置了最近最少使用的 key

  3. volatile-lfu:当内存不足时,会删除一个设置了生存时间且最近使用频率最低的 key

  4. allkeys-lfu:当内存不足时,会删除一个设置了最近使用频率最低的 key

  5. volatile-random:当内存不足时,会随机删除一个设置了生存时间的 key

  6. allkeys-random:当内存不足时,会随机删除一个 key

  7. volatile-ttl:当内存不足时,会删除一个生存时间最少的 key

  8. noeviction:内存不足时,直接报错

4.3 缓存问题

缓存穿透

当客户查询的数据 Redis 中没有,数据库中也没有,且请求量特别大时,就会导致数据库的压力过大,解决方法如下

  • 根据 id 查询时,如果 id 是自增的,那么可以将最大的 id 放到 Reids 中,当查询数据时直接对比 id 即可
  • 如果 id 不是 int 型,那么可以将全部的 id 放入 set 中,用户查询之前可以先到 set 查看是否有该 id
  • 获取用户的 ip 地址,对该地址进行访问限制

缓存击穿

当用户查询的是热点数据时,那么并发量肯定是很高的,当 Redis 中的热点数据过期了,那么数据库的压力就会很大,甚至宕机,解决方法如下

  • 在访问热点数据时,缓存中没有的时候,可以添加一把锁,让几个请求去访问数据库,避免数据库宕机
  • 把热点数据的生存时间去掉

缓存雪崩

当大量缓存同时到期时,导致请求都去到了数据库,也很容易导致数据库宕机,解决方法如下

  • 对缓存中的数据设置一个随机的生存时间,避免同时过期

缓存倾斜

如果将热点数据放在集群中的某一 Redis 节点上时,那么大量的数据都会去到该 Redis 节点,导致节点宕机,解决方法如下

  • 主从架构,准备大量的从节点
  • 在 Tomcat 中做 JVM 缓存,在查询 Redis 前先查询 JVM 缓存

Web

一、Apache

1.1 Apache介绍

Apache HTTP Server(简称Apache)是Apache软件基金会的一个开放源码的网页服务器,是世界使用排名第一的Web服务器软件。它可以运行在几乎所有广泛使用的计算机平台上,由于其跨平台和安全性被广泛使用,是最流行的Web服务器端软件之一。它快速、可靠并且可通过简单的API扩充,将Perl/Python等解释器编译到服务器中。

Apache HTTP服务器是一个模块化的服务器,源于NCSAhttpd服务器,经过多次修改,成为世界使用排名第一的Web服务器软件。

Apache官方文档:http://httpd.apache.org/docs/

1.2 通过脚本源码安装Apache

  1. 编写安装脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
useradd -s /sbin/nologin -r www
vim apache-install.sh
#!/bin/bash

apr_version=1.7.0
apr_iconv_version=1.2.2
apr_util_version=1.6.1
apache_version=2.4.46

#检查
function check()
{
#检查是否为root用户
if [ $USER != 'root' ]
then
echo -e "\e[1;31m error:need to be root so that \e[0m"
exit 1
fi

#检查是否安装了wget
if [ `rpm -qa | grep wget | wc -l` -lt 1 ]
then
echo -e "\e[1;31m error:not found wget \e[0m"
exit 1
fi
}

#安装前准备
function install_pre()
{
#安装依赖
if [ ! `yum -y install zlib-devel pcre-devel libxml2 expat-devel &> /dev/null` ]
then
echo -e "\e[1;31m error:yum install dependency package failed \e[0m"
exit 1
fi

#下载apr
cd /usr/local
if [ ! `wget https://downloads.apache.org/apr/apr-${apr_version}.tar.bz2 &> /dev/null` ]
then
tar -xf apr-${apr_version}.tar.bz2
if [ ! -d apr-${apr_version} ]
then
echo -e "\e[1;31m error:not found apr-${apr_version} \e[0m"
exit 1
else
cd apr-${apr_version}
fi
else
echo -e "\e[1;31m error:Failed to download apr-${apr_version}.tar.bz2 \e[0m"
exit 1
fi

#安装apr
echo "apr configure..."
./configure --prefix=/usr/local/apr &> /dev/null
if [ `echo $?` -eq 0 ]
then
echo "apr make && make install..."
make && make install &> /dev/null
if [ `echo $?` -eq 0 ]
then
echo -e "\e[1;32m apr installed successfully \e[0m"
else
echo -e "\e[1;31m apr installed failed \e[0m"
exit 1
fi
else
echo -e "\e[1;31m error:apr configure failed \e[0m"
exit 1
fi

#下载apr-iconv
cd /usr/local
if [ ! `wget https://www.apache.org/dist/apr/apr-iconv-${apr_iconv_version}.tar.bz2 &> /dev/null` ]
then
tar -xf apr-iconv-${apr_iconv_version}.tar.bz2
if [ ! -d apr-iconv-${apr_iconv_version} ]
then
echo -e "\e[1;31m error:not found apr-iconv-${apr_iconv_version} \e[0m"
exit 1
else
cd apr-iconv-${apr_iconv_version}
fi
else
echo -e "\e[1;31m error:Failed to download apr-iconv-${apr_iconv_version}.tar.bz2 \e[0m"
exit 1
fi

#安装apr-iconv
echo "apr-iconv configure..."
./configure --prefix=/usr/local/apr-iconv --with-apr=/usr/local/apr &> /dev/null
if [ `echo $?` -eq 0 ]
then
echo "apr-iconv make && make install..."
make && make install &> /dev/null
if [ `echo $?` -eq 0 ]
then
echo -e "\e[1;32m apr-iconv installed successfully \e[0m"
else
echo -e "\e[1;31m apr-iconv installed failed \e[0m"
exit 1
fi
else
echo -e "\e[1;31m error:apr-iconv configure failed \e[0m"
exit 1
fi

#下载apr-util
cd /usr/local
if [ ! `wget https://www.apache.org/dist/apr/apr-util-${apr_util_version}.tar.bz2 &> /dev/null` ]
then
tar -xf apr-util-${apr_util_version}.tar.bz2
if [ ! -d apr-util-${apr_util_version} ]
then
echo -e "\e[1;31m error:not found apr-util-${apr_util_version} \e[0m"
exit 1
else
cd apr-util-${apr_util_version}
fi
else
echo -e "\e[1;31m error:Failed to download apr-util-${apr_util_version}.tar.bz2 \e[0m"
exit 1
fi

#安装apr-util
echo "apr-util configure..."
./configure --prefix=/usr/local/apr-util --with-apr=/usr/local/apr/ &> /dev/null
if [ `echo $?` -eq 0 ]
then
echo "apr-util make && make install..."
make && make install &> /dev/null
if [ `echo $?` -eq 0 ]
then
echo -e "\e[1;32m apr-util installed successfully \e[0m"
else
echo -e "\e[1;31m apr-util installed failed \e[0m"
exit 1
fi
else
echo -e "\e[1;31m error:apr-util configure failed \e[0m"
exit 1
fi
}

#下载安装Apache
function apache_install()
{
#下载Apache
cd /usr/local
if [ ! `wget https://downloads.apache.org/httpd/httpd-${apache_version}.tar.gz &> /dev/null` ]
then
tar -xf httpd-${apache_version}.tar.gz
if [ ! -d httpd-${apache_version} ]
then
echo -e "\e[1;31m error:not found httpd-${apache_version} \e[0m"
exit 1
else
cd httpd-${apache_version}
fi
else
echo -e "\e[1;31m error:Failed to download httpd-${apache_version} \e[0m"
exit 1
fi

#安装Apache
echo "Apache configure..."
./configure --prefix=/usr/local/apache --enable-mpms-shared=all --with-mpm=event --with-apr=/usr/local/apr --with-apr-util=/usr/local/apr-util --enable-so --enable-remoteip --enable-proxy --enable-proxy-fcgi --enable-proxy-uwsgi --enable-deflate=shared --enable-expires=shared --enable-rewrite=shared --enable-cache --enable-file-cache --enable-mem-cache --enable-disk-cache --enable-static-support --enable-static-ab --disable-userdir --enable-nonportable-atomics --disable-ipv6 --with-sendfile &> /dev/null
if [ `echo $?` -eq 0 ]
then
echo "Apache make && make install..."
make && make install &> /dev/null
if [ `echo $?` -eq 0 ]
then
echo -e "\e[1;32m Apache installed sucessfully \e[0m"
else
echo -e "\e[1;31m Apache installed failed \e[0m"
exit 1
fi
else
echo -e "\e[1;31m Apache configure failed \e[0m"
exit 1
fi
}

check
install_pre
apache_install
  1. 编写启动脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#!/bin/bash

apache_doc=/usr/local/apache/bin
apache_pid=/usr/local/apache/logs/httpd.pid

function apache_start()
{
apache_num=`ps -ef | grep httpd | wc -l`
if [ $apache_num -gt 1 ] && [ -f $apache_pid ]
then
echo -e "Apache [\e[1;32m running \e[0m]"
exit 1
elif [ $apache_num -eq 1 ] && [ -f $apache_pid ]
then
killall httpd
fi
cd /usr/local/apache/bin;./apachectl
echo -e "start Apache [\e[1;32m OK \e[0m]"
}

function apache_stop()
{
apache_num=`ps -ef | grep httpd | wc -l`
if [ $apache_num -eq 1 ]
then
echo -e "Apache [\e[1;31m stopping \e[0m]"
else
killall httpd
echo -e "stop Apache [\e[1;32m OK \e[0m]"
fi
}

function apache_restart()
{
cd /usr/local/apache/bin;./apachectl restart
echo -e "restart Apache [\e[1;32m OK \e[0m]"
}

function apache_status()
{
apache_num=`ps -ef | grep httpd | wc -l`
if [ $apache_num -gt 1 ] && [ -f $nginx_pid ]
then
echo -e "Apache [\e[1;32m running \e[0m]"
else
echo -e "Apache [\e[1;31m stopping \e[0m]"
fi
}

function apache_reload()
{
apache_num=`ps -ef | grep httpd | wc -l`
if [ $apache_num -gt 1 ] && [ -f $nginx_pid ]
then
cd /usr/local/apache/bin;./apachectl graceful
echo -e "reload Apache [\e[1;32m OK \e[0m]"
else
echo -e "Apache [\e[1;31m stopping \e[0m]"
fi
}

case $1 in
start)
apache_start
;;
stop)
apache_stop
;;

restart)
apache_restart
;;

status)
apache_status
;;

reload)
apache_reload
esac

1.3 多处理模块MPM

Apache HTTP 服务器被设计为一个功能强大,并且灵活的 web 服务器, 可以在很多平台与环境中工作。不同平台和不同的环境往往需要不同 的特性,或可能以不同的方式实现相同的特性最有效率。Apache 通过模块化的设计来适应各种环境。这种设计允许网站管理员通过在 编译时或运行时,选择哪些模块将会加载在服务器中,来选择服务器特性。

实际上就是用来接受请求处理请求的。

Apache的三种工作方式:

  1. Prefork MPM:使用多个进程,每个进程只有一个线程,每个进程再某个确定的时间只能维持一个连接,有点是稳定,缺点是内存消耗过高。

![Prefork MPM](Prefork MPM.png)

  1. Worker MPM:使用多个进程,每个进程有多个线程,每个线程在某个确定的时间只能维持一个连接,内存占用比较小,是个大并发、高流量的场景,缺点是一个线程崩溃,整个进程就会连同其任何线程一起挂掉。

![Worker MPM](Worker MPM.png)

  1. Event MPM:使用多进程多线程+epoll的模式。

![Event MPM](Event MPM.png)

1.4 虚拟主机

默认情况下,一个web服务器只能发布一个默认网站,也就是只能发布一个web站点,对于大网站来说还好,但对于访问量较少的小网站那就显得有点浪费了。

而虚拟主机就可以实现在一个web服务器上发布多个站点,分为基于IP地址、域名和端口三种。

Apache的虚拟主机和默认网站不能够同时存在,如果设置了虚拟主机那么默认网站也就失效了,需要在用虚拟主机发布默认站点才可解决。

基于IP:基于IP的虚拟主机需要耗费大量的IP地址,只适合IP地址充足的环境。

基于端口:需要耗费较多的端口,适合私网环境。

基于域名:需要耗费较多的域名,适合公网环境。

1.4.1 基于IP的虚拟主机

  1. 在主配文件中调用虚拟主机文件
1
2
3
vim /usr/local/apache/conf/httpd.conf
# Virtual hosts
Include conf/extra/httpd-vhosts.conf
  1. 修改虚拟主机文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 添加一个逻辑网卡,重启即失效
ifconfig eth0:1 192.168.88.100/24 up
vim /usr/local/apache/conf/extra/httpd-vhosts.conf
<VirtualHost 49.232.160.75:80>
# 管理员邮箱
# ServerAdmin webmaster@dummy-host.example.com
# web目录
DocumentRoot "/usr/local/apache/htdocs/web1"
# 域名
# ServerName dummy-host.example.com
# 给域名起别名,起到重定向作用
# ServerAlias www.dummy-host.example.com
# 错误日子
# ErrorLog "logs/dummy-host.example.com-error_log"
# 访问日志
# CustomLog "logs/dummy-host.example.com-access_log" common
</VirtualHost>

<VirtualHost 192.168.88.100:80>
DocumentRoot "/usr/local/apache/htdocs/web2"
</VirtualHost>
  1. 创建站点目录和文件
1
2
3
4
mkdir /usr/local/apache/htdocs/web{1..2}
echo 'this is web1' > /usr/local/apache/htdocs/web1/index.html
echo 'this is web2' > /usr/local/apache/htdocs/web2/index.html
./apache start
  1. 测试

基于IP虚拟主机测试

1.4.2 基于端口的虚拟主机

  1. 修改虚拟主机文件
1
2
3
4
5
6
7
8
<VirtualHost *:80>
DocumentRoot "/usr/local/apache/htdocs/web1"
</VirtualHost>

Listen 81
<VirtualHost *:81>
DocumentRoot "/usr/local/apache/htdocs/web2"
</VirtualHost>
  1. 测试

基于端口虚拟主机测试

1.4.3 基于域名的虚拟主机

  1. 修改虚拟主机文件
1
2
3
4
5
6
7
8
9
<VirtualHost *:80>
DocumentRoot "/usr/local/apache/htdocs/web1"
ServerName www.cqm1.com
</VirtualHost>

<VirtualHost *:80>
DocumentRoot "/usr/local/apache/htdocs/web2"
ServerName www.cqm2.com
</VirtualHost>
  1. 测试

基于域名虚拟主机测试

1.5 LAMP

LAMP:Linux + Apache + Mysql + PHP

作用就是构建一个PHP业务环境,用来发布PHP网站。

1.5.1 Mysql通过脚本源码安装

  1. 编写安装脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
#!/bin/bash

mysql_version=5.7.35
install_dir=/opt
data_dir=/data
wget_url="https://mirrors.tuna.tsinghua.edu.cn/mysql/downloads/MySQL-5.7/mysql-${mysql_version}-linux-glibc2.12-x86_64.tar.gz"

function loginfo(){
if [[ $? -eq 0 ]];then
echo -e "\033[32m[INFO][$(date +"%F %T")] $1 succeed! \033[0m"
else
echo -e "\033[31m[ERROR][$(date +"%F %T")] $1 failed! \033[0m"
fi
}

function mysql_install(){
echo -e "\033[32mBegin install mysql V${mysql_version} ...\033[0m"

# 安装依赖
sudo yum -y install libaio >/dev/null 2>&1
loginfo "libaio install"

# 下载mysql
echo -e "\033[32mBegin download mysql V${mysql_version} ...\033[0m"
curl -O $wget_url >/dev/null 2>&1
mv ./mysql-${mysql_version}-linux-glibc2.12-x86_64.tar.gz $install_dir
loginfo "mysql software download"

# 解压缩mysql
sudo tar -xf $install_dir/mysql-${mysql_version}-linux-glibc2.12-x86_64.tar.gz -C $install_dir
loginfo "mysql software decompression"

# 创建配置文件目录和数据目录
if [[ -d $install_dir/mysql ]];then
rm -rf $install_dir/mysql
fi
sudo ln -s $install_dir/mysql-${mysql_version}-linux-glibc2.12-x86_64 $install_dir/mysql
loginfo "create mysql config dir soft link"
if [[ -d $data_dir/mysql ]];then
rm -rf $data_dir/mysql
fi
sudo mkdir -p $data_dir/mysql
loginfo "create mysql data dir"

# 修改启动脚本
sudo sed -i "46s#basedir=#basedir=${install_dir}/mysql#" ${install_dir}/mysql/support-files/mysql.server
sudo sed -i "47s#datadir=#datadir=${data_dir}/mysql#" ${install_dir}/mysql/support-files/mysql.server
sudo cp ${install_dir}/mysql/support-files/mysql.server /etc/init.d/mysqld
sudo chmod 755 /etc/init.d/mysqld

# 创建用户组及用户
if ! grep -q '^mysql:' /etc/group
then
sudo groupadd mysql
loginfo "create user mysql"
fi
if ! grep -q '^mysql:' /etc/passwd
then
sudo useradd -r -g mysql -s /bin/false mysql
loginfo "create group mysql"
fi

# 授权
sudo chown -R mysql:mysql $install_dir/mysql
sudo chown -R mysql:mysql $data_dir/mysql

# 为二进制文件创建软连接
if [ ! -f /usr/bin/mysql ]
then
sudo ln -s /opt/mysql/bin/mysql /usr/bin/
fi

# 创建配置文件
if [ -f /etc/my.cnf ]
then
sudo rm -f /etc/my.cnf
fi
sudo bash -c "cat >> /etc/my.cnf" <<EOF
[mysqld]
datadir = /data/mysql
basedir = /opt/mysql
#tmpdir = /data/mysql/tmp_mysql
port = 3306
socket = /data/mysql/mysql.sock
pid-file = /data/mysql/mysql.pid
max_connections = 8000
max_connect_errors = 100000
max_user_connections = 3000
check_proxy_users = on
mysql_native_password_proxy_users = on
local_infile = OFF
symbolic-links = FALSE
group_concat_max_len = 4294967295
max_join_size = 18446744073709551615
max_execution_time = 20000
lock_wait_timeout = 60
autocommit = 1
lower_case_table_names = 1
thread_cache_size = 64
disabled_storage_engines = "MyISAM,FEDERATED"
character_set_server = utf8mb4
character-set-client-handshake = FALSE
collation_server = utf8mb4_general_ci
init_connect = 'SET NAMES utf8mb4'
transaction-isolation = "READ-COMMITTED"
skip_name_resolve = ON
explicit_defaults_for_timestamp = ON
log_timestamps = SYSTEM
local_infile = OFF
event_scheduler = OFF
query_cache_type = OFF
query_cache_size = 0
sql_mode = NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES,NO_AUTO_CREATE_USER,NO_ZERO_DATE,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO
log_error = /data/mysql/mysql.err
slow_query_log = ON
slow_query_log_file = /data/mysql/slow.log
long_query_time = 1
general_log = OFF
general_log_file = /data/mysql/general.log
expire_logs_days = 99
log-bin = /data/mysql/mysql-bin
log-bin-index = /data/mysql/mysql-bin.index
max_binlog_size = 500M
binlog_format = mixed
binlog_rows_query_log_events = ON
binlog_cache_size = 128k
binlog_stmt_cache_size = 128k
log-bin-trust-function-creators = 1
max_binlog_cache_size = 2G
max_binlog_stmt_cache_size = 2G
relay_log = /data/mysql/relay
relay_log_index = /data/mysql/relay.index
max_relay_log_size = 500M
relay_log_purge = ON
relay_log_recovery = ON
server_id = 1
read_buffer_size = 1M
read_rnd_buffer_size = 2M
sort_buffer_size = 64M
join_buffer_size = 64M
tmp_table_size = 64M
max_allowed_packet = 128M
max_heap_table_size = 64M
connect_timeout = 43200
wait_timeout = 43200
back_log = 512
interactive_timeout = 300
net_read_timeout = 30
net_write_timeout = 30
skip_external_locking = ON
key_buffer_size = 16M
bulk_insert_buffer_size = 16M
concurrent_insert = ALWAYS
open_files_limit = 65000
table_open_cache = 16000
table_definition_cache = 16000
default_storage_engine = InnoDB
default_tmp_storage_engine = InnoDB
internal_tmp_disk_storage_engine = InnoDB

[client]
socket = /data/mysql/mysql.sock
default_character_set = utf8mb4

[mysql]
default_character_set = utf8mb4

[ndatad default]
TransactionDeadLockDetectionTimeOut = 20000
EOF
sudo chown -R mysql:mysql /etc/my.cnf
loginfo "configure my.cnf"

# 创建SSL证书
# sudo mkdir -p ${install_dir}/mysql/ca-pem/
# sudo ${install_dir}/mysql/bin/mysql_ssl_rsa_setup -d ${install_dir}/mysql/ca-pem/ --uid=mysql
# sudo chown -R mysql:mysql ${install_dir}/mysql/ca-pem/

# sudo bash -c "cat >> ${data_dir}/mysql/init_file.sql" <<EOF
# set global sql_safe_updates=0;
# set global sql_select_limit=50000;
# EOF
# sudo chown -R mysql:mysql ${data_dir}/mysql/init_file.sql
# sudo chown -R mysql:mysql /etc/init.d/mysqld

# 初始化
${install_dir}/mysql/bin/mysqld --initialize --user=mysql --basedir=${DEPLOY_PATH}/mysql --datadir=/data/mysql
loginfo "initialize mysql"

# 客户端环境变量
echo "export PATH=\$PATH:${install_dir}/mysql/bin" | sudo tee /etc/profile.d/mysql.sh
source /etc/profile.d/mysql.sh
loginfo "configure envirement"

# 获取初始密码
mysql_init_passwd=$(grep 'A temporary password is generated' ${data_dir}/mysql/mysql.err | awk '{print $NF}')

# 启动服务
chkconfig --add mysqld
sudo systemctl start mysqld
loginfo "start mysqld"

# 修改密码
mysql --connect-expired-password -uroot -p${mysql_init_passwd} -e 'alter user user() identified by "toortoor";' >/dev/null 2>&1
loginfo "edit mysql root password"
}

mysql_install

1.5.2 PHP通过脚本源码安装

  1. 编写安装脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
#!/bin/bash

cmake_version=3.22.0
libzip_version=1.8.0
php_version=7.4.16

#检查
function check()
{
#检查是否为root用户
if [ $USER != "root" ]
then
echo -e "\e[1;31m error:need to be root so that \e[0m"
exit 1
fi

#检查是否安装了wget
if [ `rpm -qa | grep wget | wc -l` -lt 1 ]
then
echo -e "\e[1;31m error:not found wget \e[0m"
exit 1
fi
}

#安装前准备
function pre()
{
#安装依赖包
if [ ! `yum -y install gcc-c++ libxml2 libxml2-devel openssl openssl-devel bzip2 bzip2-devel libcurl libcurl-devel libjpeg libjpeg-devel libpng libpng-devel freetype freetype-devel gmp gmp-devel libmcrypt libmcrypt-devel readline readline-devel libxslt libxslt-devel gd net-snmp-* sqlite-devel oniguruma-devel &> /dev/null` ]
then
echo -e "\e[1;31m error:yum install dependency package failed \e[0m"
exit 1
fi

#下载最新版cmake
cd /usr/local
if [ ! `wget https://github.com/Kitware/CMake/releases/download/v${cmake_version}/cmake-${cmake_version}.tar.gz &> /dev/null` ]
then
tar -xf cmake-${cmake_version}.tar.gz
if [ ! -d cmake-${cmake_version} ]
then
echo -e "\e[1;31m error:no found cmake-${cmake_version} \e[0m"
exit 1
else
cd cmake-${cmake_version}
fi
else
echo -e "\e[1;31m error:Failed to download cmake-${cmake_version} \e[0m"
exit 1
fi

#安装cmake
echo "cmake configure..."
./configure &> /dev/null
if [ `echo $?` -eq 0 ]
then
echo "cmake make && make install..."
make && make install &> /dev/null
if [ `echo $?` -eq 0 ]
then
echo -e "\e[1;32m cmake installed sucessfully \e[0m"
else
echo -e "\e[1;31m cmake installed failed \e[0m"
exit 1
fi
else
echo -e "\e[1;31m error:cmake configure failed \e[0m"
exit 1
fi

#下载libzip1.1以上版本
cd /usr/local
if [ ! `wget --no-check-certificate https://libzip.org/download/libzip-${libzip_version}.tar.gz &> /dev/null` ]
then
echo "tar libzip..."
tar -xf libzip-${libzip_version}.tar.gz
if [ ! -d libzip-${libzip_version} ]
then
echo -e "\e[1;31m error:not found libzip-${libzip_version} \e[0m"
exit 1
else
cd libzip-${libzip_version}
fi
else
echo -e "\e[1;31m error:Failed to download libzip-${libzip_version}.tar.gz \e[0m"
exit 1
fi

#安装libzip
mkdir build;cd build
echo "cmake libzip..."
cmake .. &> /dev/null
if [ `echo $?` -eq 0 ]
then
echo "make && make install libzip..."
make && make install &> /dev/null
if [ `echo $?` -eq 0 ]
then
echo -e "\e[1;32m libzip install sucessfully \e[0m"
echo -e '/usr/local/lib64\n/usr/local/lib\n/usr/lib\n/usr/lib64'>> /etc/ld.so.conf
ldconfig -v &> /dev/null
else
echo -e "\e[1;31m error:libzip install failed \e[0m"
exit 1
fi
else
echo -e "\e[1;31m error:lipzip cmake failed \e[0m"
exit 1
fi
}

function php_install()
{
#下载php
cd /usr/local
if [ ! `wget https://www.php.net/distributions/php-${php_version}.tar.bz2 &> /dev/null` ]
then
echo "tar php..."
tar -xf php-${php_version}.tar.bz2
if [ ! -d php-${php_version} ]
then
echo -e "\e[1;31m error:not found php-${php_version} \e[0m"
exit 1
else
cd php-${php_version}
fi
else
echo -e "\e[1;31m error:Failed to download php-${php_version}.tar.bz2 \e[0m"
exit 1
fi

#安装php
echo "configure php..."
#要php以apache模块运行需加上--with-apxs2=/usr/localapache/bin/apxs参数
./configure --prefix=/usr/local/php --with-config-file-path=/usr/local/php/etc --with-mysqli=mysqlnd --enable-pdo --with-pdo-mysql=mysqlnd --with-iconv-dir=/usr/local/ --enable-fpm --with-fpm-user=www --with-fpm-group=www --with-pcre-regex --with-zlib --with-bz2 --enable-calendar --disable-phar --with-curl --enable-dba --with-libxml-dir --enable-ftp --with-gd --with-jpeg-dir --with-png-dir --with-zlib-dir --with-freetype-dir --enable-gd-jis-conv --with-mhash --enable-mbstring --enable-opcache=yes --enable-pcntl --enable-xml --disable-rpath --enable-shmop --enable-sockets --enable-zip --enable-bcmath --with-snmp --disable-ipv6 --with-gettext --disable-rpath --disable-debug --enable-embedded-mysqli --with-mysql-sock=/var/lib/mysql/ &> /dev/null
if [ `echo $?` -eq 0 ]
then
echo "make && make install php..."
make && make install &> /dev/null
if [ `echo $?` -eq 0 ]
then
echo -e "\e[1;32m php install sucessfully \e[0m"
else
echo -e "\e[1;31m php install failed \e[0m"
exit 1
fi
else
echo -e "\e[1;31m configure php failed \e[0m"
exit 1
fi
}

function php_set()
{
if [ ! -f /usr/local/php-${php_version}/sapi/fpm/php-fpm.service ]
then
echo -e "\e[1;31m No found php-fpm.service \e[0m"
exit 1
else
cp /usr/local/php-${php_version}/sapi/fpm/php-fpm.service /etc/systemd/system
if [ `echo $?` -ne 0 ]
then
echo -e "\e[1;31m Copy php-fpm.service failed \e[0m"
exit 1
else
sed -i '/PrivateTmp=true/a\ProtectSystem=false' /etc/systemd/system/php-fpm.service
systemctl daemon-reload
echo -e "\e[1;32m php set sucessfully \e[0m"
fi
fi
}

check
pre
php_install
php_set
  1. 配置PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
cd /usr/local/php/etc
cp php-fpm.conf.default php-fpm.conf
cp php-fpm.d/www.conf.default php-fpm.d/www.conf

egrep -v '^;|^$' php-fpm.conf
[global]
pid = run/php-fpm.pid
error_log = log/php-fpm.log
daemonize = yes
include=/usr/local/php/etc/php-fpm.d/*.conf

egrep -v '^;|^$' php-fpm.d/www.conf
[www]
user = www
group = www
listen = 127.0.0.1:9000
listen.owner = www
listen.group = www
listen.mode = 0660
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
  1. 启动
1
systemctl start php-fpm

1.5.3 PHP作为Apache模块运行

  1. 在apache主配置文件中调用子配置文件
1
2
vim /usr/local/apache/conf/httpd.conf
include conf/extra/php.conf
  1. 配置子配置文件
1
2
3
vim /usr/local/apache/conf/extra/php.con
LoadModule php7_module modules/libphp7.so
AddType application/x-httpd-php .php
  1. 配置虚拟主机
1
2
3
4
vim /usr/local/apache/conf/extra/httpd-vhosts.conf
<VirtualHost *:80>
DocumentRoot "/usr/local/apache/htdocs/web"
</VirtualHost>
  1. 写web目录
1
2
3
4
5
echo 'this is cqm web' > /usr/local/apache/htdocs/web/index.html
vim /usr/local/apache/htdocs/web/phpinfo.php
<?php
phpinfo()
?>
  1. 测试

php以模块运行

1.5.4 PHP作为独立服务运行

PHP作为独立服务运行有两种模式:

  • TCP socket模式
  • UNIX socket模式

TCP socket模式

  1. 修改www.conf文件
1
2
vim /usr/local/php/etc/php-fpm.d/www.conf
listen = 127.0.0.1:9000
  1. 配置虚拟主机文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
vim /usr/local/apache/conf/extra/httpd-vhosts.conf
<VirtualHost *:80>
DocumentRoot "/usr/local/apache/htdocs/web"
</VirtualHost>

<Directory "/usr/local/apache/htdocs/web">
Options Indexes FollowSymLinks
AllowOverride None
Require all granted
</Directory>

<IfModule dir_module>
DirectoryIndex index.php index.html
</IfModule>

<FilesMatch \.php$>
SetHandler "proxy:fcgi://127.0.0.1:9000"
</FilesMatch>
  1. 在apache主配文件添加关联
1
2
vim /usr/local/apache/conf/httpd.conf
include conf/extra/php-fpm.conf
  1. 配置子配文件
1
2
3
4
vim /usr/local/apache/conf/php-fpm.conf
# 载入需要的模块
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so

UNIX socket模式

  1. 修改www.conf文件
1
2
vim /usr/local/php/etc/php-fpm.d/www.conf
listen = /usr/local/php/etc/php-fpm.socket
  1. 配置虚拟主机
1
2
3
<FilesMatch \.php$>
SetHandler "proxy:unix:/usr/local/php/etc/php-fpm.socket|fcgi://localhost/"
</FilesMatch>

1.6 Apache常用模块

1.6.1 长连接

HTTP采用TCP进行传输,是面向连接的协议,每完成一次请求就要经历以下过程:

  • 三次握手
  • 发起请求
  • 响应请求
  • 四次挥手

那么N个请求就要建立N次连接,如果希望用户能够更快的拿到数据,服务器的压力降到最低,那么靠长连接就可以解决。

长连接实际上就是优化了TCP连接。

Apache默认开启了长连接,持续时间为5秒,在httpd-default.conf中可以定义。

1
2
3
4
5
6
7
vim /usr/local/apache/conf/extra/httpd-default.conf
# 开启长连接
KeepAlive On
# 限制每个连接允许的请求数
MaxKeepAliveRequests 500
# 长连接时间
KeepAliveTimeout 5

1.6.2 静态缓存

用户每次访问网站都会将页面中的所有元素都请求一遍,全部下载后通过浏览器渲染,展示到浏览器中。但是,网站中的某些元素我们一般都是固定不变的,比如logo、框架文件等。用户每次访问都需要加载这些元素。这样做好处是保证了数据的新鲜,可是这些数据不是常变化的,很久才变化一次。每次都请求、下载浪费了用户时间和公司带宽。

所以我们通过静态缓存的方式,将这些不常变化的数据缓存到用户本地磁盘,用户以后再访问这些请求,直接从本地磁盘打开加载,这样的好处是加载速度快,且节约公司带宽及成本。

  1. 在apache主配文件中加载缓存模块
1
2
vim /usr/local/apache/conf/httpd.conf
LoadModule expires_module modules/mod_expires.so
  1. 修改虚拟主机文件调用模块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
vim /usr/local/apache/conf/extra/httpd-vhosts.conf
<VirtualHost *:80>
DocumentRoot "/usr/local/apache/htdocs/web"

<IfMoudle expires_module>
#开启缓存
ExpiresActive on
#针对不同类型元素设置缓存时间
ExpiresByType image/gif "access plus 1 days"
ExpiresByType image/jpeg "access plus 24 hours"
ExpiresByType image/png "access plus 24 hours"
#now 相当于 access
ExpiresByType text/css "now plus 2 hour"
ExpiresByType application/x-javascript "now plus 2 hours"
ExpiresByType application/x-shockwave-flash "now plus 2 hours”
#其他数据不缓存
ExpiresDefault "now plus 0 min"
</IfModule>

</VirtualHost>

1.6.3 数据压缩

数据从服务器传输到客户端,需要传输时间,文件越大传输时间就越长,为了减少传输时间,我们一般把数据压缩后在传给客户端。

apache支持两种模式的压缩:

  • default
  • gzip

两者的区别:

  • mod_deflate 压缩速度快。
  • mod_gzip 的压缩比略高。
  • 一般情况下,mod_gzip 会比 mod_deflate 多出 4%~6% 的压缩量。
  • mod_gzip 对服务器CPU的占用要高一些,所以 mod_deflate 是专门为确保服务器的性能而使用的一个压缩模块,只需较少的资源来进行压缩。
  1. 在apache主配文件中加载压缩模块
1
2
vim /usr/local/apache/conf/httpd.conf
LoadModule deflate_module modules/mod_deflate.so
  1. 修改虚拟主机文件调用模块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
vim /usr/local/apache/conf/extra/httpd-vhosts.conf
<VirtualHost *:80>
DocumentRoot "/usr/local/apache/htdocs/web"

<IfMoudle deflate_module>
#压缩等级1-9,数字越大压缩能力越好,相应地也越耗CPU性能
DeflateCompressionLevel 4
#压缩类型,html、xml、php、css、js
AddOutputFilterByType DEFLATE text/html text/plain text/xml application/x-javascript application/x-httpd-php
AddOutputFilter DEFLATE js css
#浏览器匹配为IE1-6的不压缩
BrowserMatch \bMSIE\s[1-6] dont-vary
#设置不压缩的文件
SetEnvIfNoCase Request_URI .(?:gif|jpe?g|png)$ no-gzip dont-vary
SetEnvIfNoCase Request_URI .(?:exe|t?gz|zip|bz2|sit|rar)$ no-gzip dont-vary
SetEnvIfNoCase Request_URI .(?:pdf|doc)$ no-gzip dont-vary
</IfModule>

</VirtualHost>

1.6.4 限速

网站除了能共享页面给用户外,还能作为下载服务器存在。但是作为下载服务器时,我们应该考虑服务器的带宽和IO的性能,防止部分邪恶分子会通过大量下载的方式来攻击你的带宽和服务器IO性能。

问题:

  • 假如你的服务器被邪恶分子通过下载的方式把带宽占满了,那么你或其他用户在访问的时候就会造成访问慢或者根本无法访问。
  • 假如你的服务器被邪恶分子通过下载的方式把服务器IO占满了,那么你的服务器将会无法处理用户请求或宕机。

以上问题可以通过限速来解决,apache自带了基于宽带限速的模块:

  • ratelimit_module:只能对连接下载速度做限制,且是单线程的下载,迅雷等下载工具使用的是多线程下载。
  • mod_limitipconn:限制每 IP 的连接数,需要额外安装该模块。

ratelimit_module模块

  1. 在apache主配文件中加载压缩模块
1
2
vim /usr/local/apache/conf/httpd.conf
LoadModule ratelimit_module modules/mod_ratelimit.so
  1. 修改虚拟主机文件调用模块
1
2
3
4
5
6
7
8
9
10
11
12
vim /usr/local/apache/conf/extra/httpd-vhosts.conf
<VirtualHost *:80>
DocumentRoot "/usr/local/apache/htdocs/web"
</VirtualHost>

# Location相对路径:/usr/local/apache/htdocs/...
# Directory绝对路径:...
<Location /download>
SetOutputFiler RATE_LIMIT
#限速100k
SetEnv rate-limit 100
</Location>

mod_limitipconn模块

  1. 下载安装模块
1
2
3
4
5
6
wget http://dominia.org/djao/limit/mod_limitipconn-0.24.tar.bz2
tar -xf mod_limitipconn-0.24.tar.bz2
cd mod_limitipconn-0.24
vim Makefile
apxs = "/usr/local/apache/bin/apxs"
make && make install
  1. 在apache主配文件启用模块
1
2
vim /usr/local/apache/conf/httpd.conf
LoadModule limitipconn_module modules/mod_limitipconn.so
  1. 修改虚拟主机文件调用模块
1
2
3
4
5
6
7
8
9
<Location /download>
SetOutputFiler RATE_LIMIT
#限速100k
SetEnv rate-limit 100
#限制线程数
MaxConnPerIP 3
#对index.html文件不作限制
NoIPLimit index.html
</Location>

1.6.5 访问控制

在生产环境中,网站分为公站和私站,公站允许所有人访问,但私站就只允许内部人员访问,Require就可以实现访问控制的功能。

容器:

  • RequireAny:一个符合即可通过
  • RequireAll:所有符合才可通过
  • Requirenone:所有都不符合才可通过

普通的访问控制

  1. 修改虚拟主机文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
vim /usr/local/apache/conf/extra/httpd-vhosts.conf
<VirtualHost *:80>
DocumentRoot "/usr/local/apache/htdocs/web"
</VirtualHost>

<Directory "/usr/local/apache/htdocs/web/test">
AllowOverride None
# 拒绝所有人访问
Require all denied
# 允许该地址段的用户访问
Require ip 192.168.88
# 允许该主机访问
Require host www.cqm.com
</Directory>

用户登录验证访问控制

  1. 修改虚拟主机文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
vim /usr/local/apache/conf/extra/httpd-vhosts.conf
<VirtualHost *:80>
DocumentRoot "/usr/local/apache/htdocs/web"
</VirtualHost>

<Directory "/usr/local/apache/htdocs/web/test">
# 定义提示信息,用户访问时提示信息会出现在认证的对话框中
AuthName "Private"
# 定义认证类型,在HTTP1.0中,只有一种认证类型:basic。在HTTP1.1中有几种认证类型,如:MD5
AuthType Basic
# 定义包含用户名和密码的文本文件,每行一对
AuthUserFile "/usr/local/apache/user.dbm"
# 配合容器使用,只有条件全部符合才能通过
<RequireAll>
Require not ip 192.168.88
# require user user1 user2 (只有用户user1和user2可以访问)
# requires groups group1 (只有group1中的成员可以访问)
# require valid-user (在AuthUserFile指定的文件中的所有用户都可以访问)
Require valid-user
</RequireAll>
</Directory>
  1. 生成用户文件
1
2
3
# 生成cqm用户
/usr/local/apache/bin/htpasswd -cm /usr/local/apache/user.dbm cqm
...

1.6.6 URL重写

Apache通过mod_rewrite模块可以实现URL重写的功能,URL重写其实就是改写用户浏览器中的URL地址。

  1. 在主配文件开启模块
1
2
vim /usr/local/apache/conf/httpd.conf
LoadModule rewrite_module modules/mod_rewrite.so
  1. 修改虚拟主机文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
vim /usr/local/apache/conf/extra/httpd-vhosts.conf
<VirtualHost *:80>
DocumentRoot "/usr/local/apache/htdocs/web"
# 开启URL重写功能
RewriteEngine on
# 重写规则,跳转到百度
RewriteRule "^/$" "http://www.baidu.com" [NC,L]
# 匹配条件,根据请求头进行匹配
RewriteCond "%{HTTP_USER_AGENT}" "chrome" [NC,OR]
RewriteCond "%{HTTP_USER_AGENT}" "curl"
# 重写规则,和匹配到的条件配合使用,请求头匹配到chrome或curl则返回403状态码
RewriteRule "^/$" - [F]
</VirtualHost>

RewreteRule [flag] 部分标记规则
R:强制外部重定向
F:禁用URL,返回403HTTP状态码
G:强制URL为GONE,返回410HTTP状态码
P:强制使用代理转发
L:表明当前规则是最后一条规则,停止分析以后规则的重写
N:重新从第一条规则开始运行重写过程
C:与下一条规则关联
NS:只用于不是内部子请求
NC:不区分大小写

通过URL重写实现分流功能

1
2
3
4
5
6
7
8
9
vim /usr/local/apache/conf/extra/httpd-vhosts.conf
<VirtualHost *:80>
DocumentRoot "/usr/local/apache/htdocs/web"
RewriteEngine on
RewriteCond "%{HTTP_USER_AGENT}" "(chrome|curl)" [NC,OR]
RewriteRule "^/$" "http://pc.cqm.com" [NC]
RewriteCond "%{HTTP_USER_AGENT}" "(iPhone|Blackberry|Android|ipad)" [NC]
RewriteRule "^/$" "http://phone.cqm.com" [NC]
</VirtualHost>

1.6.7 压力测试

Apache压力测试使用ab命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ab
-A:指定连接服务器的基本的认证凭据
-c:指定一次向服务器发出请求数
-C:添加cookie
-g:将测试结果输出为“gnuolot”文件
-h:显示帮助信息
-H:为请求追加一个额外的头
-i:使用“head”请求方式
-k:激活HTTP中的“keepAlive”特性
-n:指定测试会话使用的请求数
-p:指定包含数据的文件
-q:不显示进度百分比
-T:使用POST数据时,设置内容类型头
-v:设置详细模式等级
-w:以HTML表格方式打印结果
-x:以表格方式输出时,设置表格的属性
-X:使用指定的代理服务器发送请求
-y:以表格方式输出时,设置表格属性
1
2
3
/usr/local/apache/bin/ab -n 10000 -c 200 http:...
# 并发数
per second...

二、Nginx

2.1 Nginx介绍

Nginx是一款是由俄罗斯的程序设计师Igor Sysoev所开发高性能的 Web和 反向代理服务器,也是一个 IMAP/POP3/SMTP 代理服务器。和apache一样,都是web服务器软件,因为其性能优异,所以被广大运维喜欢。又因为nginx是一个轻量级的web服务器,相比apache来说资源消耗更低。

Nginx中文文档:https://www.nginx.cn/doc/index.html

2.2 通过脚本源码安装Nginx

  1. 编写安装脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
vim nginx-install.sh
#!/bin/bash

nginx_version=1.21.3

#检测
function check()
{
#检测是否为root
if [ $USER != "root" ]
then
echo -e "\e[1;31m error:need to be root so that \e[0m"
exit 1
fi

#检测wget是否安装
if [ ! -e /usr/bin/wget ]
then
echo -e "\e[1;31m error:not found command /usr/bin/wget \e[0m"
exit 1
fi
}

#安装前准备
function install_pre()
{
# 安装依赖
#0:stdin标准输入 1:stdout标准输出 2:stderr错误输出
if [ ! `yum -y install gcc-* pcre-devel zlib-devel &> /dev/null` ]
then
echo -e "\e[1;31m error:yum install dependency package failed \e[0m"
exit 1
fi

#下载源码包
cd /usr/local/
if [ ! `wget http://nginx.org/download/nginx-${nginx_version}.tar.gz &> /dev/null` ]
then
tar -xf nginx-${nginx_version}.tar.gz
if [ ! -d nginx-${nginx_version} ]
then
echo -e "\e[1;31m error:not found nginx-${nginx_version} \e[0m"
exit 1
fi
else
echo -e "\e[1;31m error:wget file nginx-${nginx_version}.tar.gz failed \e[0m"
exit 1
fi
}

#安装
function install_nginx()
{
cd /usr/local/nginx-${nginx_version}
echo "nginx configure..."
./configure --prefix=/usr/local/nginx &> /dev/null
if [ `echo $?` -eq 0 ]
then
echo "nginx make..."
make && make install &> /dev/null
if [ `echo $?` -eq 0 ]
then
echo -e "\e[1;32m nginx install success \e[0m"
else
echo -e "\e[1;31m error:nginx install fail \e[0m"
exit 1
fi
else
echo -e "\e[1;31m error:nginx configure fail \e[0m"
exit 1
fi
}

check
install_pre
install_nginx
  1. 编写启动脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#!/bin/bash

#Source function libiary
if [ -f /etc/init.d/functions ]
then
. /etc/init.d/functions
else
echo "Not found file /etc/init.d/functions"
exit
fi

nginxd=/usr/local/nginx/sbin/nginx
nginx_pid=/usr/local/nginx/logs/nginx.pid

function nginx_start()
{
nginx_num=`ps -ef | grep nginx | wc -l`
if [ $nginx_num -gt 1 ] && [ -f $nginx_pid ]
then
echo -e "nginx [\e[1;32m running \e[0m]"
exit 1
elif [ $nginx_num -eq 1 ] && [ -f $nginx_pid ]
then
killall nginx
fi
$nginxd
}

function nginx_stop()
{
nginx_num=`ps -ef | grep nginx | wc -l`
if [ $nginx_num -eq 1 ]
then
echo -e "nginx [\e[1;31m stopping \e[0m]"
exit 1
elif [ $nginx_num -gt 1 ]
then
killall nginx
fi
}

function nginx_status()
{
nginx_num=`ps -ef | grep nginx | wc -l`
if [ $nginx_num -gt 1 ] && [ -f $nginx_pid ]
then
echo -e "nginx [\e[1;32m running \e[0m]"
else
echo -e "nginx [\e[1;31m stopping \e[0m]"
fi
}

function nginx_restart()
{
nginx_stop
nginx_start
}

function nginx_reload()
{
nginx_num=`ps -ef | grep nginx | wc -l`
if [ $nginx_num -gt 1 ] && [ -f $nginx_pid ]
then
$nginxd -s reload
else
echo -e "nginx [\e[1;31m stopping \e[0m]"
fi
}

case $1 in
start)
nginx_start
echo -e "nginx start [\e[1;32m OK \e[0m]"
;;
stop)
nginx_stop
echo -e "nginx stop [\e[1;32m OK \e[0m]"
;;
status)
nginx_status
;;
restart)
nginx_restart
echo -e "nginx restart [\e[1;32m OK \e[0m]"
;;
reload)
nginx_reload
echo -e "nginx reload [\e[1;32m OK \e[0m]"
esac

2.3 Nginx的Server块

当Nginx配置文件只有一个Server块时,那么该Server块就被Nginx认为是默认网站,所有发给Nginx的请求都会传给该Server块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
server {
# 监听80端口
listen 80;
# 域名
server_name localhost;
# 字符集
charset koi8-r;
# 访问日志路径
access_log logs/host.access.log main;
# web根路径
# /代表相对路劲,这里代表/usr/local/nginx
location / {
# 根目录路径,这里代表/usr/local/nginx/html
root html;
# 索引页
index index.html index.htm;
}
# 404状态码
error_page 404 /404.html;
location = /404.html{
root html;
}
# 50x状态码
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}

2.4 Nginx的访问控制

  1. 编写主配文件
1
2
3
4
5
6
7
8
9
10
11
12
13
location / {
root html;
index index.html index.htm;
# 允许192.168.88.0/24的用户访问
allow 192.168.88.0/24;
# 拒绝所有
deny all;
# 基于客户端IP做过滤,符合条件的允许访问,不符合的返回404
# 这里为不是192.168.88的就返回404
if ( $remote_addr !~ "192.168.88" ){
return 404;
}
}

2.5 Nginx的用户验证

  1. 编写主配文件
1
2
3
4
5
6
7
8
location / {
root html;
index index.html index.htm;
# 欢迎词
auth_basic "welcome to cqm's web";
# 存放用户文件
auth_basic_user_file /usr/local/nginx/htpasswd;
}
  1. 生成用户文件
1
/usr/local/apache/bin/htpasswd -cm /usr/local/nginx/htpasswd cqm

2.6 Nginx参数

1
2
3
4
5
6
7
8
9
10
11
# nginx中的log_format可以用来自定义日志格式
# log_format变量:
$remote_addr:记录访问网站的客户端地址
$remote_user:远程客户端用户名
$time_local:记录访问时间与时区
$request:用户的http请求起始行信息
$status:http状态码,记录请求返回的状态码,例如:200、301、404等
$body_bytes_sent:服务器发送给客户端的响应body字节数
$http_referer:记录此次请求是从哪个连接访问过来的,可以根据该参数进行防盗链设置。
$http_user_agent:记录客户端访问信息,例如:浏览器、手机客户端等
$http_x_forwarded_for:当前端有代理服务器时,设置web节点记录客户端地址的配置,此参数生效的前提是代理服务器也要进行相关的x_forwarded_for设置

2.7 Nginx防盗链

盗链用大白话讲就是抓取别人网站的资源,加以利用,以至于被抓取资源的网站消耗了带宽,而收益的是抓取资源的人。

而反盗链就可以防止别人抓取自身网站的资源。

  1. 编写主配文件
1
2
3
4
5
6
7
location / {
# 除了www.cqm.com之外,都返回403
valid_referers none blocked www.cqm.com;
if ($invalid_referer){
return 403;
}
}

2.8 Nginx虚拟主机

Nginx的虚拟主机是通过server块来实现的。

2.8.1 基于IP的虚拟主机

  1. 修改主配文件
1
2
vim /usr/local/nginx/conf/nginx.conf
include /usr/local/nginx/conf/conf.d/nginx_vhosts.conf;
  1. 修改虚拟主机文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
vim /usr/local/nginx/conf/conf.d/nginx_vhosts.conf
server {
listen 192.168.88.100;
location / {
root html/web1;
index index.html index.htm index.php;
}
}

server {
listen 192.168.88.101;
location / {
root html/web2;
index index.html index.htm index.php;
}
}
  1. 其它配置
1
2
3
4
5
# 添加一个逻辑网卡,重启即失效
ifconfig eth0:1 192.168.88.100/24 up
mkdir /usr/local/nginx/html/web{1..2}
echo 'this is web1' > /usr/local/nginx/html/web1/index.html
echo 'this is web2' > /usr/local/nginx/html/web2/index.html
  1. 测试
1
2
3
4
curl http://192.168.88.100/
this is web1
curl http://192.168.88.101/
this is web2

2.8.2 基于端口的虚拟主机

  1. 修改虚拟主机文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
vim /usr/local/nginx/conf/conf.d/nginx_vhosts.conf
server {
listen 80;
location / {
root html/web1;
index index.html index.htm index.php;
}
}

server {
listen 81;
location / {
root html/web2;
index index.html index.htm index.php;
}
}

2.8.3 基于域名的虚拟主机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
vim /usr/local/nginx/conf/conf.d/nginx_vhosts.conf
server {
listen 80;
server_name www.cqm1.com;
location / {
root html/web1;
index index.html index.htm index.php;
}
}

server {
listen 80;
server_name www.cqm2.com;
location / {
root html/web2;
index index.html index.htm index.php;
}
}

2.9 Nginx反向代理

代理最常见的使用方式就是翻墙,能够实现让国内的用户访问国外的网站。

原理:

  • 用户讲请求发给代理服务器
  • 代理服务器代替用户去获取数据
  • 代理服务器将数据发送给用户

正常没有代理的上网

正常上网流程

使用代理服务器的上网

=使用代理服务器上网流程

代理服务器又分为两种:正向代理、反向代理

正向代理:代理用户向服务器获取资源

反向代理:代理服务器去管理网络资源,用户有请求找反向代理就可以了

  1. 编写反向代理服务器主配文件
1
2
3
4
5
6
vim /usr/local/nginx/conf/nginx.conf
location / {
index index.html index.htm index.php;
# 访问代理服务器就会跳转到http://192.168.88.100
proxy_pass http://192.168.88.100;
}
  1. 反向代理其它配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
proxy_set_header Host $host;

proxy_set_header X-Real-IP $remote_addr;

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

client_max_body_size 10m; #允许客户端请求的最大单文件字节数

client_body_buffer_size 128k; #缓冲区代理缓冲用户端请求的最大字节数,

proxy_connect_timeout 90; #nginx跟后端服务器连接超时时间(代理连接超时)

proxy_send_timeout 90; #后端服务器数据回传时间(代理发送超时)

proxy_read_timeout 90; #连接成功后,后端服务器响应时间(代理接收超时)

proxy_buffer_size 4k; #设置代理服务器(nginx)保存用户头信息的缓冲区大小

proxy_buffers 4 32k; #proxy_buffers缓冲区,网页平均在32k以下的话,这样设置

proxy_busy_buffers_size 64k; #高负荷下缓冲大小(proxy_buffers*2)

proxy_temp_file_write_size 64k; #设定缓存文件夹大小,大于这个值,将从upstream服务器传

2.10 Nginx下载限速

限速方法主要分为:

  • 下载速度限制
  • 单位时间内请求数限制
  • 基于客户端的并发数限制

Nginx官方提供的限制IP连接和并发的模块有两个:

limit_req_zone:用来限制单位时间内的请求数,即速率限制,采用的漏桶算法

limit_req_conn:来限制同一时间连接数,即并发限制

单位时间内请求数限制

  1. 修改主配文件
1
2
3
4
5
6
7
8
9
10
11
12
13
# 在http快下调用模块
# $binary_remote_addr:基于ip地址做限制
# zone:创建缓存域和缓存大小
# rate:设置访问频数
limit_req_zone $binary_remote_addr zone=cqm:10m rate=1r/s;

# server块
location /test {
...
# 调用模块
# 当请求数超过5次时,就拒绝访问,并返回503状态码
limit_req zone=cqm burst=5 nodelay;
}

限制并发连接数

  1. 修改主配文件
1
2
3
4
5
6
7
8
9
10
11
# 在http快下调用模块
# $binary_remote_addr:基于ip地址做限制
# zone:创建缓存域和缓存大小
limit_req_conn $binary_remote_addr zone=cqm:10m;

# server块
location /test {
...
# 限制同一时间内下载数为1个
limit_conn cqm 1;
}

限制下载速度

  1. 修改主配文件
1
2
3
4
5
location /test {
...
# 限制下载速度为1k
limit_rate 1k;
}

2.11 Nginx的URL重写

rewrite的主要功能是实现URL地址的重定向。Nginx的rewrite功能需要PCRE软件的支持,即通过perl兼容正则表达式语句进行规则匹配的。默认参数编译nginx就会支持rewrite的模块,但是也必须要PCRE的支持。

URL模板语块:

  • set:设置变量
  • if:判断
  • return:返回值或URL
  • break:终止
  • rewrite:重定向URL

例一:根据不同域名跳转到主域名的不同目录下

  1. 创建测试目录
1
2
3
4
mkdir /usr/local/nginx/html/{cn,jp,us}
echo 'this is China' > /usr/local/nginx/html/cn/index.html
echo 'this is Japan' > /usr/local/nginx/html/jp/index.html
echo 'this is America' > /usr/local/nginx/html/us/index.html
  1. 修改hosts文件,以便解析
1
2
3
4
5
vim /etc/hosts
192.168.88.100 www.cqm.com
192.168.88.100 www.cqm.com.cn
192.168.88.100 www.cqm.com.jp
192.168.88.100 www.cqm.com.us
  1. 修改主配文件
1
include /usr/local/nginx/conf/conf.d/rewrite.conf
  1. 修改重定向文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
vim /usr/local/nginx/conf/conf.d/rewrite.conf
# 设置重定向server
server {
listen 80;
server_name www.cqm.com.cn www.cqm.com.jp www.cqm.com.us;
location / {
# 模糊匹配到cn的话,就跳转到http://www.cqm.com/cn下
if ($http_host ~ (cn)$){
set $nation cn;
rewrite ^/$ http://www.cqm.com/$nation;
}
if ($http_host ~ (jp)$){
set $nation jp;
rewrite ^/$ http://www.cqm.com/$nation;
}
if ($http_host ~ (us)$){
set $nation us;
rewrite ^/$ http://www.cqm.com/$nation;
}
}
}
server {
listen 80;
server_name www.cqm.com;
location / {
root html;
index index.html;
}
}
  1. 测试
1
2
3
4
5
6
7
curl -L http://www.cqm.com.cn
this is China
curl -L http://www.cqm.com.jp
this is Japan
curl -L http://www.cqm.com.us
this is America
-L:自动获取重定向

例二:retuen以及break的简单实用

  1. 修改重定向文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
vim /usr/local/nginx/conf/conf.d/rewrite.conf
server {
listen 80;
server_name www.cqm.com;
location / {
root html;
index index.html;
# 模糊匹配 ~
# 精确匹配 =
# 不匹配 !~
# 如果匹配请求头不是chrome的话,就返回403
if ($http_user_agent !~ 'chrome'){
return 403;
# break放在return上面的话就不会执行return操作
# break;
# return http://www.baidu.com;
}
}
}

flag

flag是放在rewrite重定向的URL后边的,格式为:rewrite URL flag

flag的选项有:

  1. last:本条规则匹配完成后继续执行到最后。
  2. break:本条规则匹配完成即终止。
  3. redirect:返回302临时重定向。
  4. permanent:返回301永久重定向。

redirect和permanent的区别:设置permanent的话,新网址就会完全继承旧网址,旧网址的排名等完全清零,如果不是暂时迁移的情况下都建议使用permanent;设置redirect的话,新网址对旧网址没有影响,且新网址也不会有排名。

2.12 Nginx优化

2.12.1 大并发

Nginx的工作模式:主进程 + 工作进程

假如Nginx服务器有4个CPU

  1. 设置主配文件来实现高并发
1
2
3
4
5
6
7
8
vim /usr/local/nginx/conf/nginx.conf
worker_processes 4;
# 指定运行的核的编号,采用掩码的方式设置编号
worker_cpu_affinity 0001 0010 0100 1000;
events {
# 单个工作进程维护的请求队列长度,根据实际情况调整
worker_connections 1024;
}

2.12.2 长连接

  1. 修改主配文件
1
2
3
4
5
6
7
vim /usr/local/nginx/conf/nginx.conf
# keepalive_timeout用来设置长连接,0代表关闭
keepalive_timeout 0;
# 设置长连接时间100s
#keepalive_timeout 100;
# 设置每秒可以接受的请求数
#keepalive_requests 8192;

2.12.3 压缩

Nginx是采用gzip进行压缩。

  1. 修改主配文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
vim /usr/local/nginx/conf/nginx.conf
# 开启缓存
gzip on;

# Nginx做为反向代理的时候启用
# off:关闭所有的代理结果数据压缩
# expired:如果header中包含”Expires”头信息,启用压缩
# no-cache:如果header中包含”Cache-Control:no-cache”头信息,启用压缩
# no-store:如果header中包含”Cache-Control:no-store”头信息,启用压缩
# private:如果header中包含”Cache-Control:private”头信息,启用压缩
# no_last_modified:启用压缩,如果header中包含”Last_Modified”头信息,启用压缩
# no_etag:启用压缩,如果header中包含“ETag”头信息,启用压缩
# auth:启用压缩,如果header中包含“Authorization”头信息,启用压缩
# any:无条件压缩所有结果数据
gzip_proxied any;

# 启用gzip压缩的最小文件,小于设置值的文件将不会压缩
gzip_min_length 1k;

# 设置压缩所需要的缓冲区大小
# 32 4K表示按照内存页(one memory page)大小以4K为单位(即一个系统中内存页为4K),申请32倍的内存空间
# 建议此项不设置,使用默认值
gzip_buffers 32 4k;

# 设置gzip压缩级别,级别越底压缩速度越快文件压缩比越小,反之速度越慢文件压缩比越大
gzip_comp_level 1;

# 用于识别http协议的版本,早期的浏览器不支持gzip压缩,用户会看到乱码,所以为了支持前期版本加了此选项。默认在http/1.0的协议下不开启gzip压缩
gzip_http_version 1.1;

# 设置需要压缩的MIME类型,如果不在设置类型范围内的请求不进行压缩
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png application/vnd.ms-fontobject font/ttf font/opentype font/x-woff image/svg+xml;

2.12.4 静态缓存

将部分数据缓存在用户本地磁盘,用户加载时,如果本地和服务器的数据一致,则从本地加载。提升用户访问速度,提升体验度。节省公司带宽成本。

  1. 修改主配文件
1
2
3
4
5
# 模糊匹配以png或gif结尾的文件
location ~* \.(png|gif)$ {
# 缓存时间为1小时
expires 1h;
}

三、Tomcat

3.1 Tomcat介绍

Tomcat 服务器是一个免费的开放源代码的 Web 应用服务器,属于轻量级应用服务器,在中小型系统和并发访问用户不是很多的场合下被普遍使用,是开发和调试 JSP 程序的首选。

实际上 Tomcat 是 Apache 服务器的扩展,但运行时它是独立运行的,所以当你运行 tomcat 时,它实际上作为一个与 Apache 独立的进程单独运行的。

Tomcat 官方文档:https://tomcat.apache.org/

3.2 Tomcat安装

  1. 安装jdk和tomcat
1
2
3
4
5
6
yum -y install java-1.8.0-openjdk*
wget https://downloads.apache.org/tomcat/tomcat-9/v9.0.43/bin/apache-tomcat-9.0.43.tar.gz
tar -xf apache-tomcat-9.0.43.tar.gz
mkdir /root/tomcat
mv apache-tomcat-9.0.43.tar.gz/* /root/tomcat
./root/tomcat/bin/startup.sh

Zookeeper

logo

ZooKeeper 是一个分布式的,开放源码的分布式应用程序协调服务,是 Hadoop 和 Hbase 的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。

一、Zookeeper基础

1.1 使用场景

  • 分布式协调组件:通过 watch 机制可以协调好节点之间的数据一致性
  • 分布式锁:通过分布式锁可以做到强一致性
  • 无状态化实现
  • 负载均衡
  • 数据发布/订阅
  • 命名服务

1.2 部署

docker-compose.yaml

1
2
3
4
5
6
7
8
9
10
11
12
version: '3.8'
services:
zookeeper:
container_name: zk01
image: zookeeper:3.7.0
restart: always
hostname: zk01
ports:
- 2181:2181
environment:
ZOO_MY_ID: 1
ZOO_SERVERS: server.1=zk01:2888:3888;2181

zoo.cfg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
dataDir=/data
dataLogDir=/datalog
# 基本时间配置(毫秒)
tickTime=2000
# 初始化连接到leader的最大时长,单位为倍数,即初始化时间为tickTime * initLimit
initLimit=5
# follower与leader数据同步的最大时长
syncLimit=2
# 保存数据的快照数量
autopurge.snapRetainCount=3
# 自动触发清除任务时间间隔,以小时为单位,默认为0,表不清除
autopurge.purgeInterval=0
# 客户端与zk的最大并发连接数
maxClientCnxns=60
# 开启standaloneEnabled模式,即独立部署
standaloneEnabled=true
# 开启adminServer
admin.enableServer=true
# 2181是为客户端提供的端口
server.1=zk01:2888:3888;2181

1.3 基本命令

  1. 启动|关闭|查看状态
1
zkServer.sh start|stop|status
  1. 进入 zk
1
zkCli.sh
  1. 查看内部数据结构
1
ls [path]

二、内部数据结构

2.1 是如何存储数据的

Zookeeper 中的数据是保存在节点上的,即 znode,多个 znode 就构成一个树的结构。

zk数据存储结构

如图,a 和 b 就是 Zookeeper 的 znode,创建 znode 方式如下

1
2
3
4
5
create /[znode_name]
# 创建节点并创建一个数据
create /[znode_name] [data_name]
# 获取数据
get [znode_name]

2.2 znode结构

Zookeeper 中的 zonode,包含以下几个部分:

  • data:保存数据
  • acl:权限
    • c:创建权限
    • w:写权限
    • r:读权限
    • d:删除权限
    • a:admin 管理者权限
  • stat:描述当前 znode 的元数据
  • child:当前节点的子节点
1
2
3
4
5
6
7
8
9
10
11
12
13
# 查看znode详细信息
get -s /[znode_name]
cZxid:创建节点的事务ID
ctime:创建节点时间
mZxid:修改节点的事务ID
mtime:修改节点时间
pZxid:添加和删除子节点的事务ID
cversion:当前节点的子节点版本号,初始值为-1,每对该节点的子节点进行操作,这个cversion都会自动增加
dataVersion:数据版本初识版本为0,每对该节点的数据进行操作,这个dataVersion都会自动增加
aclVersion:权限版本
ephemeralOwne:如果当前节点是临时节点,该值是当前节点的session id,如果不是临时节点则为0
dataLength:数据长度
numChildren:该节点的子节点个数

2.3 znode类型

  • 持久节点:在会话结束后仍会存在
  • 持久序号节点:根据先后顺序,会在结点之后带上一个数值,适用于分布式锁的场景(单调递增)
  • 临时节点:会话结束后会自动删除,适用于注册与服务发现的场景
  • 临时序号节点:跟持久序号节点相同,适用于分布式锁的场景
  • 容器节点:当容器节点中没有任何子节点时,该容器节点会被定期删除(60s)
  • TTL 节点:可以指定节点的到期时间

持久序号节点创建

1
create -s /[znode_name]

临时节点创建

1
create -e /[znode_name]

临时序号节点创建

1
create -e -s /[znode_name]

容器节点创建

1
create -c /[znode_name]

TTL 节点创建

1
2
# 通过系统配置开启
zookeeper.extendedTypesEnabled=true

持久与临时节点

持久节点

持久节点在创建后服务端会发送一个 session id,并一直保留着。

临时节点

临时节点在创建时后服务器也会发送一个 session id,在会话持续的过程中客户端会不断向服务端续约 session id 的时间,当客户端没有继续续约,而服务端内部的计时器到期时,就会将该 session id 所对应的 znode 全部删除。

2.4 持久化机制

Zookeeper 的数据是运行在内存中的,所以提供了两种持久化机制:

  • 事务日志:Zookeeper 将执行过的命令以日志的形式存储在 dataLogDir / dataDir 中,类似于 redis 的 AOF
  • 数据快照:在一定时间间隔内做一次数据快照,存储在快照文件中(snapshot),类似于 redis 的RDB

Zookeeper 通过这两种持久化机制,在恢复数据时先将快照文件中的数据恢复到内存中,再用日志文件中的数据做增量恢复,可以实现高效的持久化。

三、zkCli的使用

  1. 递归查询
1
ls -R /[znode_name]
  1. 删除节点
1
deleteall /[znode_name]
  1. 乐观锁删除
1
delete -v [version] /[znode_name]
  1. 给当前会话注册用户,并创建节点赋予该用户权限
1
2
addauth digest [user]:[password]
create /[znode_name] auth:[user]:[password]:[privileges]

四、分布式锁

在分布式的环境下,如果在一个节点去上了个锁,当请求被负载均衡分配到了其它节点,那么锁就无法形成互斥,所以节点之间使用 Zookeeper,做一个协调中心,将锁上传到 Zookeeper,其它节点要用到就去 Zookeeper 拿这个锁,这就是分布式锁。

Zookeeper 锁的分类:

  • 读锁:大家都可以读,前提是之前没有写锁。(读锁比喻成约会,大家都有机会和女神约会,约会前提是女神没结婚)
  • 写锁:只有写锁才能写,前提是不能有任何锁。(写锁比喻成结婚,结婚后只有老公能和女神约会,结婚前提是女神和其他人的关系断干净了)

4.1 上读锁

  • 创建一个临时序号节点,节点数据是 read,表示为读锁
  • 获取当前 Zookeeper 中序号比自己小的所有节点
  • 判断最小节点是否为读锁:
    • 如果是读锁:则上锁失败,因为如果最小节点是读锁,那么后面就不可能有写锁,接着为最小节点设置监听,Zookeeper 的 watch 机制会在最小节点发生变化时通知当前节点,再进行后面的步骤,被称为阻塞等待
    • 如果不是读锁:则上锁成功

4.2 上写锁

  • 创建一个临时序号节点,节点数据是 write,表示为写锁
  • 获取 Zookeeper 中的所有节点
  • 判断自己是否为最小节点:
    • 如果是:上锁成功
    • 如果不是:说明前面还有锁,所以上锁失败,接着监听最小节点,如果最小节点发生变化,则重新进行第二步

羊群效应

假设有一百个请求都是要去写锁,那么就会有一百个请求去监听最小节点,那么 Zookeeper 的压力就会非常大,解决方法是将这一百个请求按请求顺序排列,后一个请求去监听前一个请求即可,实现链式监听。

4.3 watch机制

Zookeeper 的 watch 可以看作是一个触发器,当监控的 znode 发生改变,就会触发 znode 上注册的对应事件,请求 watch 的客户端就会接收到异步通知。

zkCli.sh 中使用 watch

1
2
3
4
5
6
7
create /test
# 一次性监听,监听节点内容
get -w /test
# 监听目录,但所监听节点下创建和删除子节点不会触发监听
ls -w /test
# 与上面相对,都会触发监听
ls -R -w /test

五、集群部署

Zookeeper 的集群角色有三个:

  • Leader:处理集群所有事务的请求,集群只有一个 Leader
  • Follower:只处理读请求,参与 Leader 选举
  • Observer:只处理读请求,提升集群的性能,但不能参与 Leader 选举

docker-compose.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
version: '3.8'
services:
zk01:
container_name: zk01
image: zookeeper:3.7.0
restart: always
hostname: zk01
ports:
- 2181:2181
environment:
ZOO_MY_ID: 1
# 2888:用于集群内zk之间的通信
# 3888:用于选举投票
# 2181:客户端使用
# 要创建observer则在2181端口后加:observer
ZOO_SERVERS: server.1=zk01:2888:3888;2181 server.2=zk02:2888:3888;2181 server.3=zk03:2888:3888;2181

zk02:
container_name: zk02
image: zookeeper:3.7.0
restart: always
hostname: zk02
ports:
- 2182:2181
environment:
ZOO_MY_ID: 2
ZOO_SERVERS: server.1=zk01:2888:3888;2181 server.2=zk02:2888:3888;2181 server.3=zk03:2888:3888;2181

zk03:
container_name: zk03
image: zookeeper:3.7.0
restart: always
hostname: zk03
ports:
- 2183:2181
environment:
ZOO_MY_ID: 3
ZOO_SERVERS: server.1=zk01:2888:3888;2181 server.2=zk02:2888:3888;2181 server.3=zk03:2888:3888;2181

通过命令查看节点角色

1
2
zkServer.sh status
Mode: leader

连接集群

1
zkCli.sh -server zk01:2181,zk02:2181,zk03:2181

5.1 ZAB协议

ZAB(Zookeeper Atomic Broadcast)即 Zookeeper 原子广播协议,通过这个协议解决了集群数据一致性和崩溃恢复的问题。

ZAB 协议中节点的四种状态

  • Looking:选举状态
  • Following
  • Leading
  • Observing

初始化集群时 leader 的选举

  • 当集群中两台节点启动时,就会开始 leader 的选举,选票的格式为 (myid,zXid)
  • 第一轮投票时,每个节点会生成自己的选票,即自己的 (myid,zXid),然后将选票给到对方,这时候每个节点就会有两张选票,即自己的和对方节点的
  • 接着就会比较两张选票的 zXid,如果都相同就对比 myid,将大的一票投到投票箱中
  • 第二轮投票时,每个节点会将上一轮投出去的选票给到其它节点,然后再对比 (myid,zXid),将大的一票投出去,就能够选出 leader
  • 后来新启动的节点会发现已经有 leader了,就不用做选举的过程了
  • 可以看出初始化集群时,leader 的选举主要看 myid

崩溃恢复时的 leader 选举

在 leader 确定了之后,leader 会周期性地向 follower 发送心跳包,当 follower 没有收到 leader 发送过来的心跳包,就会进入选举过程,这时候集群不能对外提供服务。

  • 当 leader 挂了之后,follower 的状态会变成 looking
  • 接着就进行选举投票,过程和初始化集群时一样

5.2 主从同步原理

主从同步原理

5.3 NIO和BIO

NIO

用于被客户端连接的 2181 端口,使用的就是 NIO 的连接模式;客户端开启 watch 时,使用的也是 NIO。

BIO

集群在进行选举时,多个节点之间的通信端口,使用的是 BIO 的连接模式。