0%

1
2
3
4
5
6
7
8
9
10
11
内存淘汰策略分类
早期版本的 Redis 有以下 6 种淘汰策略:
noeviction:不淘汰任何数据,当内存不足时,新增操作会报错,Redis 默认内存淘汰策略;
allkeys-lru:淘汰整个键值中最久未使用的键值;
allkeys-random:随机淘汰任意键值;
volatile-lru:淘汰所有设置了过期时间的键值中最久未使用的键值;
volatile-random:随机淘汰设置了过期时间的任意键值;
volatile-ttl:优先淘汰更早过期的键值。
在 Redis 4.0 版本中又新增了 2 种淘汰策略:
volatile-lfu:淘汰所有设置了过期时间的键值中,最少使用的键值;
allkeys-lfu:淘汰整个键值中最少使用的键值。

其实redis的事务是个假事务,没有实现原子性,

若要php支持事务,必须一起执行,其中incr会报错

$status =  $redis->multi()->lPush($key1, '1123')->lPush($key2, '2123')->incr("age","age")->exec();
try {
    $redis = new Redis();
    $redis->connect('192.168.75.132', 6379);
    //开启事务
    $redis->multi();
    $redis->setex('keyTest', 60, 1);
    $redis->get('keyTest');
    $redis->incr('keyTest');
    $redis->get('keyTest');
    //执行事务
    $ret = $redis->exec();
    print_r($ret);
} catch (Exception $e){
    echo $e->getMessage();
}
//输出
Array
(
    [0] => 1
    [1] => 1
    [2] => 2
    [3] => 2
)

取消事务

try {
    $redis = new Redis();
    $redis->connect('192.168.75.132', 6379);
    //先设置缓存keyTest为1
    $redis->setex('keyTest', 60, 1);
    //开启事务
    $redis->multi();
    $redis->setex('keyTest', 60, 10);
    $redis->get('keyTest');
    $redis->incr('keyTest');
    $redis->get('keyTest');
    //取消事务
    $redis->discard();
    $ret = $redis->get('keyTest');
    var_dump($ret);
    //查看keyTest
} catch (Exception $e){
    echo $e->getMessage();
}
//输出
string(1) "1"

监视键,并执行事务

try {
    $redis = new Redis();
    $redis->connect('192.168.75.132', 6379);
    //先设置缓存keyTest为1
    $redis->setex('keyTest', 60, 1);
    //监视keyTest
    $redis->watch(array('keyTest'));
    //假设在开始监视之后,执行事务之前,keyTest被并发操作redis的其他用户修改了
    $redis->setex('keyTest', 60, 10);
    //开启事务
    $redis->multi();
    $redis->incr('keyTest');
    //执行事务
    $ret = $redis->exec();
    var_dump($ret);
    $ret = $redis->get('keyTest');
    var_dump($ret);
    //查看keyTest
} catch (Exception $e){
    echo $e->getMessage();
}
//输出 
bool(false)
string(2) "10"

redis抢购

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
<?php
header("content-type:text/html;charset=utf-8");
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$redis->watch("mywatchlist");
$len = $redis->hlen("mywatchlist");
$rob_total = 100; //抢购数量
if ($len < $rob_total) {
$redis->multi();
$redis->hSet("mywatchlist", "user_id_" . mt_rand(1, 999999), time());
$rob_result = $redis->exec();
//file_put_contents("log.txt", $len . PHP_EOL, FILE_APPEND);
if ($rob_result) {
$mywatchlist = $redis->hGetAll("mywatchlist");
echo '抢购成功' . PHP_EOL;
echo '剩余数量:' . ($rob_total - $len - 1) . PHP_EOL;
echo '用户列表:' . PHP_EOL;
print_r($mywatchlist);
exit;
} else {
exit('手气不好,再抢购!');
}
} else {
exit('已卖光');
}

定时删除;

惰性删除;

定期删除;

定时删除策略是怎么样的?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
定时删除策略的做法是,在设置 key 的过期时间时,同时创建一个定时事件,当时间到达时,由事件处理器自动执行 key 的删除操作。
# By default "hz" is set to 10. Raising the value will use more CPU when
# Redis is idle, but at the same time will make Redis more responsive when
# there are many keys expiring at the same time, and timeouts may be
# handled with more precision.
#
# The range is between 1 and 500, however a value over 100 is usually not
# a good idea. Most users should use the default of 10 and raise this up to
# 100 only in environments where very low latency is required.
hz 10
定时删除策略的优点:

可以保证过期 key 会被尽快删除,也就是内存可以被尽快地释放。因此,定时删除对内存是最友好的。
定时删除策略的缺点:

在过期 key 比较多的情况下,删除过期 key 可能会占用相当一部分 CPU 时间,在内存不紧张但 CPU 时间紧张的情况下,将 CPU 时间用于删除和当前任务无关的过期键上,
无疑会对服务器的响应时间和吞吐量造成影响。所以,定时删除策略对 CPU 不友好。

惰性删除策略是怎么样的?

1
2
3
4
5
6
7
8
9
惰性删除策略的做法是,不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。

惰性删除策略的优点:

因为每次访问时,才会检查 key 是否过期,所以此策略只会使用很少的系统资源,因此,惰性删除策略对 CPU 时间最友好。
惰性删除策略的缺点:

如果一个 key 已经过期,而这个 key 又仍然保留在数据库中,那么只要这个过期 key 一直没有被访问,它所占用的内存就不会释放,造成了一定的内存空间浪费。
所以,惰性删除策略对内存不友好。

定期删除策略是怎么样的?

1
2
3
4
5
6
7
8
9
10
11
定期删除策略的做法是,每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。
定期删除策略的优点:

通过限制删除操作执行的时长和频率,来减少删除操作对 CPU 的影响,同时也能删除一部分过期的数据减少了过期键对空间的无效占用。
定期删除策略的缺点:

内存清理方面没有定时删除效果好,同时没有惰性删除使用的系统资源少。
难以确定删除操作执行的时长和频率。如果执行的太频繁,定期删除策略变得和定时删除策略一样,对CPU不友好;如果执行的太少,那又和惰性删除一样了,
过期 key 占用的内存不会及时得到释放。
#Redis 过期删除策略是什么?
前面介绍了三种过期删除策略,每一种都有优缺点,仅使用某一个策略都不能满足实际需求。

所以, Redis 选择「惰性删除+定期删除」这两种策略配和使用,以求在合理使用 CPU 时间和避免内存浪费之间取得平衡。

Redis 是怎么实现定期删除的?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
定期删除策略的做法:每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。

1、这个间隔检查的时间是多长呢?

在 Redis 中,默认每秒进行 10 次过期检查一次数据库,此配置可通过 Redis 的配置文件 redis.conf 进行配置,配置键为 hz 它的默认值是 hz 10。

特别强调下,每次检查数据库并不是遍历过期字典中的所有 key,而是从数据库中随机抽取一定数量的 key 进行过期检查。

2、随机抽查的数量是多少呢?

我查了下源码,定期删除的实现在 expire.c 文件下的 activeExpireCycle 函数中,其中随机抽查的数量由 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 定义的,它是写死在代码中的,数值是 20。

也就是说,数据库每轮抽查时,会随机选择 20 个 key 判断是否过期。

接下来,详细说说 Redis 的定期删除的流程:

从过期字典中随机抽取 20 个 key;
检查这 20 个 key 是否过期,并删除已过期的 key;
如果本轮检查的已过期 key 的数量,超过 5 个(20/4),也就是「已过期 key 的数量」占比「随机抽取 key 的数量」大于 25%,则继续重复步骤 1;如果已过期的 key 比例小于 25%,
则停止继续删除过期 key,然后等待下一轮再检查。
可以看到,定期删除是一个循环的流程。

那 Redis 为了保证定期删除不会出现循环过度,导致线程卡死现象,为此增加了定期删除循环流程的时间上限,默认不会超过 25ms。

针对定期删除的流程伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
do {
//已过期的数量
expired = 0;
//随机抽取的数量
num = 20;
while (num--) {
//1. 从过期字典中随机抽取 1 个 key
//2. 判断该 key 是否过期,如果已过期则进行删除,同时对 expired++
}

// 超过时间限制则退出
if (timelimit_exit) return;

/* 如果本轮检查的已过期 key 的数量,超过 25%,则继续随机抽查,否则退出本轮检查 */
} while (expired > 20/4);

keys *阻塞进程,消耗比较大,慎用

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
#!/bin/bash

# 设置要遍历的 Redis 数据库数量
db_count=16

# 输出文件名
output_file="never_expire_keys.txt"

# 循环遍历 0 到 15 的数据库
for (( db=0; db<$db_count; db++ )); do
echo "正在检查数据库 $db 的永不过期键..."
# 连接到当前数据库
redis-cli select $db

# 查询当前数据库中的所有键,并检查它们的 TTL
redis-cli keys "*" | while read -r key; do
ttl=$(redis-cli ttl "$key")

# 如果 TTL 等于 -1,表示键永不过期
if [ "$ttl" -eq -1 ]; then
# 将永不过期的键和它所属的数据库编号写入文件
echo "$db:$key" >> "$output_file"
fi
done
done

echo "永不过期的键已写入到 $output_file 文件中"

以下是redis-7版本以下适用

string

1
2
3
4
5
6
int编码:当字符串长度小于等于12字节并且字符串可以表示为整数时,Redis会使用int编码。
这样可以节省内存,并且在执行一些命令时可以直接进行数值计算。
embstr编码:当字符串长度小于等于39字节时,Redis会使用embstr编码。这种编码方式会将
字符串和存储它的结构体一起分配在内存中,这样可以减少内存碎片和结构体的开销。
raw编码:当字符串长度大于39字节或者字符串不能表示为整数时,Redis会使用raw编码。
这种编码方式直接将字符串存储在一个结构体中,没有进行任何优化。

hash

1
2
3
4
ziplist(压缩列表):当Hash类型的元素比较少,且元素的大小比较小(小于64字节)时,Redis采用ziplist作为Hash类型的内部编码。
ziplist是一种紧凑的、压缩的列表结构,可以节省内存空间。但是,ziplist只能进行线性查找,不支持快速的随机访问。
hashtable(字典):当Hash类型的元素比较多,或者元素的大小比较大(大于64字节)时,Redis采用hashtable作为Hash类型的内部编码。
hashtable是一种基于链表的哈希表结构,可以快速地进行随机访问。但是,hashtable需要占用更多的内存空间。

list

1
2
3
4
5
6
7
8
9
ziplist
ziplist是一种特殊的编码方式,它可以将小数据量的列表存储在一个连续的内存块中,节省了内存空间,同时还可以提高存取效率。
ziplist编码的列表最大长度为2^16-1个元素,每个元素可以是字符串类型、整数类型或浮点数类型。在ziplist中,
每个元素都被存储为一个字节数组,并包含一个前缀和一个后缀,用于标识该元素的类型和长度。

linkedlist
linkedlist是一种常规的双向链表结构,它可以存储任意长度的列表,并且支持高效的插入和删除操作。在linkedlist中,
每个节点都包含了一个指向前一个节点和后一个节点的指针,以及一个存储元素数据的指针。
linkedlist适用于存储大数量的列表,它没有像ziplist那样的内存限制,但是会占用更多的内存空间。

set

1
2
3
4
5
intset(整数集合):当Set类型只包含整数类型的数据,并且元素数量较少(小于512个)时,Redis会使用intset作为Set类型的内部编码。
intset是一种紧凑的、压缩的整数集合结构,可以节省内存空间,并且支持快速的查找、插入和删除操作。在intset中,
所有元素都按照从小到大的顺序排列,并且可以使用不同的编码方式(16位、32位、64位)存储不同大小范围内的整数。
hashtable(字典):当Set类型包含字符串类型或者元素数量较多时,Redis会使用hashtable作为Set类型的内部编码。hashtable是一种基于链表的哈希表结构,
可以快速地进行随机访问、插入和删除操作。在hashtable中,每个元素都被存储为一个字符串,并且使用哈希函数将字符串映射到一个桶中,然后在桶中进行查找、插入和删除操作。

zset

1
2
3
4
ziplist编码:当Zset中元素个数小于128个,并且所有元素的长度都小于64字节时,Redis会使用ziplist编码存储Zset。 
这种编码方式可以节省内存空间,并且可以提高存取效率,但是不支持随机访问和范围查询。
skiplist编码:当Zset中元素个数大于等于128个,或者有一个元素的长度大于64字节时,Redis会使用skiplist编码存储Zset。
这种编码方式支持高效的随机访问和范围查询,但是需要占用更多的内存空间。

1
aof-use-rdb-preamble 选项设置为 yes,并且要同时启用 RDB 和 AOF 两种持久化

启用 AOF 模式

1
2
3
4
5
6
7
将 appendonly 设置为 yes。默认是 no。
always: 每次写操作后都同步。
everysec: 每秒同步一次。
no: 由操作系统决定何时同步。
默认设置是 everysec。
当 Redis 进行 AOF 重写或快照保存时,避免主进程 fsync 的延迟?
设置 no-appendfsync-on-rewrite 为 yes。默认是 no

启用RDB

1
2
3
save 3600 1        # 3600秒内如果超过1个key被修改则生成 RDB
save 300 100 # 300秒内如果超过100个key被修改则生成 RDB
save 60 10000 # 60秒内如果超过10000个key被修改则生成 RDB

混合持久化的优缺点

优点:

1
2
3
4
更快的启动速度:混合持久化结合了RDB的速度优势,所以Redis可以更快地重新启动,不用等待很久。
数据安全:利用AOF的方式,即使服务器突然断电,也只会丢失极短的时间内的数据。
文件更小巧:因为混合持久化结合了 RDB 和 AOF 的优势,所以文件大小和冗余度都可以得到控制。
两全其美:简单说,它就是RDB和AOF的结合体,带来了两者的好处。

缺点:

1
2
3
稍微复杂:因为它结合了两种技术,所以处理起来比单一的 RDB 或 AOF 要复杂一点。
可能占更多空间:在某些情况下,保存数据的文件可能会比只使用 RDB 或AOF 的文件要大一些。
写入速度:可能会稍慢一些,特别是当数据需要经常被保存到硬盘时(比如当 appendfsync 配置为“always”时)

域名注册与解析位于阿里云

安装acme.sh

1
2
3
curl https://get.acme.sh | sh
或者
wget -O - https://get.acme.sh | sh

这个自动安装过程完成了以下几个步骤(上面那一步默认已经执行了这个操作,如果成功了,这步省略)

1
2
3
拷贝sh脚本到~/.acme.sh/
创建alias别名acme.sh=~/.acme.sh/acme.sh
启动定时器crontab

配置阿里云解析

运行如下命令,配置阿里云api接口的key和secret,其中的值需要到阿里云控制台中去寻找。

1
2
export Ali_Key="xxxxxxxxxxxx"
export Ali_Secret="yyyyyyyyyyyyyy"

这两个配置将永久保存在文件~/.acme.sh/account.conf中

为域名申请证书

运行如下命令,一键申请证书。

这一步需要在dns后台加一个泛域名的解析,也就是*

1
acme.sh --issue --dns dns_ali -d *.example.com

证书申请成功后,保存在~/.acme.sh/*.example.com目录下

将证书部署到nginx

运行如下命令,自动将证书部署到nginx。

比如我们要部署www域名,这个www也要在dns后台先解析,acmh只是申请一个通用证书

1
acme.sh --install-cert -d www.example.com --key-file /etc/nginx/cert/www.example.com.key --fullchain-file /etc/nginx/cert/www.example.com.pem 

该命令中的参数将自动保存在~/.acme.sh/www.example.com目录下的www.example.com.conf文件里,定时器更新证书的时候实现自动部署。

配置nginx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
server {
listen 80;
listen 443;
server_name www.example.com;

ssl on;
ssl_certificate /etc/nginx/cert/www.example.com.pem;
ssl_certificate_key /etc/nginx/cert/www.example.com.key;
ssl_session_timeout 5m;
ssl_session_cache shared:SSL:20m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location / {
proxy_pass http://localhost:3000;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
}
}

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
 location /file {
charset utf-8;
alias /usr/share/nginx/html/files; #文件根目录
autoindex off;
autoindex_exact_size off;
autoindex_localtime on;
# 按理说只用配置这一个,但是下面不生效,只能复制几份
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Origin, X-Requested-With, Content-Type, Accept' always;
# 配置如果是json文件就为下载模式
if ($request_filename ~* ^.*?\.(json)$) {
add_header Content-Disposition attachment; # 添加响应头,配置文件作为附件下载
add_header Content-Type application/octet-stream;
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Origin, X-Requested-With, Content-Type, Accept' always;
}
#默认为预览,这个都可以不配
location ~* \.(jpg|png)$ {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Origin, X-Requested-With, Content-Type, Accept' always;
}
}

安装htpassed工具

1
2
3
yum -y install httpd-tools
或者
apt install apache2-utils

创建用户名和密码

1
htpasswd -c /etc/nginx/.htpasswd username

修改nginx配置文件

1
2
3
4
5
6
7
8
9
10
11
server {
listen ;
server_name localhost;
.......
#新增下面两行
auth_basic "Please input password"; #这里是验证时的提示信息
auth_basic_user_file /etc/nginx/.htpasswd;
location /{
.......
}
}

之前服务器单体架构mysql是直接安装在服务器的,没有买托管,这就造成一个问题,如果想要远程连接mysql就要开启3306防火墙端口,全是恶意ip进行攻击。。。。

nginx的stream模块可以有效限制远程ip访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
stream {
server {
listen 13306; # 需要开启云服务器防火墙
#allow 123.149.112.119; # 允许这个ip访问
# 允许192.168.110.1到192.168.255.254 虚拟机适用
#allow 192.168.110.0/16;
# deny all; # 除了allow的ip都禁止
# 禁止192.168.110.1访问
deny 192.168.110.1;
# 禁止192.168.110.1到192.168.255.254
deny 192.168.110.0/16;
# allow all; 允许所有
proxy_connect_timeout 1s;
proxy_timeout 3s;
proxy_pass 127.0.0.1:3306;
}
}