Redis的过期策略和内存淘汰机制

Redis的过期策略和内存淘汰机制

一、背景
线上你写代码的时候,想当然的认为写进 redis 的数据就一定会存在,后面导致系统各种 bug,谁来负责?
常见的有两个问题:
往 redis 写入的数据怎么没了?
可能有同学会遇到,在生产环境的 redis 经常会丢掉一些数据,写进去了,过一会儿可能就没了。我的天,同学,你问这个问题就说明 redis 你就没用对啊。redis 是缓存,你给当存储了是吧?
啥叫缓存?用内存当缓存。内存是无限的吗,内存是很宝贵而且是有限的,磁盘是廉价而且是大量的。可能一台机器就几十个 G 的内存,但是可以有几个 T 的硬盘空间。redis 主要是基于内存来进行高性能、高并发的读写操作的。
那既然内存是有限的,比如 redis 就只能用 10G,你要是往里面写了 20G 的数据,会咋办?当然会干掉 10G 的数据,然后就保留 10G 的数据了。那干掉哪些数据?保留哪些数据?当然是干掉不常用的数据,保留常用的数据了。
数据明明过期了,怎么还占用着内存?
这是由 redis 的过期策略来决定。

3种过期策略

定时删除

含义:

在设置key的过期时间的同时,为该key创建一个定时器,让定时器在key的过期时间来临时,对key进行删除

优点:

保证内存被尽快释放

缺点:

  1. 若过期key很多,删除这些key会占用很多的CPU时间,在CPU时间紧张的情况下,CPU不能把所有的时间用来做要紧的事儿,还需要去花时间删除这些key
  2. 定时器的创建耗时,若为每一个设置过期时间的key创建一个定时器(将会有大量的定时器产生),性能影响严重
  3. 没人用

惰性删除

含义:

key过期的时候不删除,每次从数据库获取key的时候去检查是否过期,若过期,则删除,返回null。

优点:

删除操作只发生在从数据库取出key的时候发生,而且只删除当前key,所以对CPU时间的占用是比较少的,而且此时的删除是已经到了非做不可的地步(如果此时还不删除的话,我们就会获取到了已经过期的key了)

缺点:

若大量的key在超出超时时间后,很久一段时间内,都没有被获取过,那么可能发生内存泄露(无用的垃圾占用了大量的内存)

定期删除

含义:

每隔一段时间执行一次删除(在redis.conf配置文件设置hz,1s刷新的频率)过期key操作

优点:

通过限制删除操作的时长和频率,来减少删除操作对CPU时间的占用–处理”定时删除”的缺点
定期删除过期key–处理”惰性删除”的缺点

缺点:

在内存友好方面,不如”定时删除”
在CPU时间友好方面,不如”惰性删除”

难点:

合理设置删除操作的执行时长(每次删除执行多长时间)和执行频率(每隔多长时间做一次删除)(这个要根据服务器运行情况来定了)

redis的过期策略和内存淘汰机制

1、定期删除+惰性删除

定期删除:指的是redis默认是每隔100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除

惰性删除:在你获取某个key的时候,redis会检查一下 ,这个key如果设置了过期时间那么是否过期了,如果过期了此时就会删除,不会给你返回任何东西

2、如果大量过期key堆积在内存里,导致redis内存块耗尽了,怎么办?

内存淘汰机制:

redis.conf中配置:

1
# maxmemory-policy noeviction

  • noeviction:当内存使用达到阈值的时候,所有引起申请内存的命令会报错。

  • allkeys-lru:在主键空间中,优先移除最近未使用的key。

  • volatile-lru:在设置了过期时间的键空间中,优先移除最近未使用的key。

  • allkeys-random:在主键空间中,随机移除某个key。

  • volatile-random:在设置了过期时间的键空间中,随机移除某个key。

  • volatile-ttl:在设置了过期时间的键空间中,具有更早过期时间的key优先移除。

Maven 添加阿里云镜像

Maven 添加阿里云镜像

setting.xml

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

代码中配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<repositories>
<repository>
<id>public</id>
<name>aliyun nexus</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
</repository>
</repositories>

<pluginRepositories>
<pluginRepository>
<id>public</id>
<name>aliyun nexus</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>

(十一)Redis的“事务”及Lua脚本操作

(十一)Redis的“事务”及Lua脚本操作

“事务”

redis提供简单的事务命令,由multi和exec组成,实际上相当于将多个命令添加到一个执行的集合内,multi为begin,exec为commit,discard相当于rollback,watch相当于锁,对象若在事务执行前被修改则事务被打断。

因此redis的事务机制为乐观锁,如果在高并发场景下,如果多个客户端同时对一个key进行了watch,只要有一个客户端提交成功,其他客户端的操作都是无效的,因此redis事务不适合在高并发场景下使用,使用lua脚本可以更好的解决事务解决不了的场景。

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

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

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

  • 开始事务。
  • 命令入队。
  • 执行事务。

如果事务中出现错误:

  1. 还未exec就报错:如果事务中出现语法错误,则事务会成功回滚,整个事务中的命令都不会提交;
  2. 成功执行exec后才报错:如果事务中出现的不是语法错误,而是执行错误,不会触发回滚,该事务中仅有该错误命令不会提交,其他命令依旧会继续提交,因此这里的”事务”打了个引号,和我们通常理解的数据库事务完全不一样。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#begin 开启一个redis事务
multi

#commit 提交事务里的命令队列
exec

#rollback 回滚
discard

#锁,监视一个或多个key,被监视的key若在提交事务前被修改,则事务被打断
watch key [key ...

#取消监视
unwatch

LUA脚本

介绍

Redis2.6之后新增的功能,我们可以在redis中通过lua脚本操作redis。与事务不同的是事务是将多个命令添加到一个执行的集合,执行的时候仍然是多个命令,会受到其他客户端的影响,而脚本会将多个命令和操作当成一个命令在redis中执行,也就是说该脚本在执行的过程中,不会被任何其他脚本或命令打断干扰。正是因此这种原子性,lua脚本才可以代替multi和exec的事务功能。同时也是因此,在lua脚本中不宜进行过大的开销操作,避免影响后续的其他请求的正常执行。

使用lua脚本的好处

  • lua脚本是作为一个整体执行的.所以中间不会被其他命令插入;
  • 可以把多条命令一次性打包,所以可以有效减少网络开销;
  • lua脚本可以常驻在redis内存中,所以在使用的时候,可以直接拿来复用,也减少了代码。

应用

redis脚本使用eval命令执行lua脚本,其中numkeys表示lua script里有多少个key参数,redis脚本根据该数字从后面的key和arg中取前n个作为key参数,之后的都作为arg参数:

1
eval script numkeys key [key ...] arg [arg ...]

场景1:记录IP登录次数

利用hash记录所有登录的IP次数

key参数的数量必须和numkey一致,使用key或者argv可以实现一样的效果。

如下面第一个命令里用了三个key,代表后面的三个参数分别对应脚本里的key1 key2 key3.

第二个命令里用了一个key,代表了后面第一个参数对应脚本里的key1,后面第二和第三个参数对应脚本里的argv1和argv2

1
2
3
4
5
6
eval "return redis.call('hincrby', KEYS[1], KEYS[2], KEYS[3])" 3 h_host host_192.168.145.1 1

※ 可以看到存在3key
eval "return redis.call('hincrby', KEYS[1], ARGV[1], ARGV[2])" 1 h_host host_192.168.145.1 1

※ 配置了1key, 2个argv参数

场景2:当10秒内请求3次后拒绝访问

  1. 给访问ip的key递增
  2. 判断该访问次数若为首次登录则设置过期时间10秒
  3. 若不是首次登录则判断是否大于3次,若大于则返回0,否则返回1。
    1
    2
    3
    eval "local request_times = redis.call('incr',KEYS[1]);
    if request_times == 1 then redis.call('expire',KEYS[1], ARGV[1]) end;
    if request_times > tonumber(ARGV[2]) then return 0 end return 1;" 1 test_127.0.0.1 10 3

通过上面的例子也可以看出,我们可以在redis里使用eval命令调用lua脚本,且该脚本在redis里作为单条命令去执行不会受到其余命令的影响,非常适用于高并发场景下的事务处理。同样我们可以在lua脚本里实现任何想要实现的功能,迭代,循环,判断,赋值 都是可以的。

lua脚本缓存

redis脚本也支持将脚本进行持久化,这样的话,下次再使用就不用输入那么长的lua脚本了。事实上使用eval执行的时候也会缓存,eval与load不同的是eval会将lua脚本执行并缓存,而load只会将脚本缓存。相同点是它们都使用sha算法进行缓存,因此只要lua脚本内容相同,eval与load缓存的sha码就是一样的。而缓存后的脚本,我们可以使用evalsha命令直接调用,极大的简化了我们的代码量,不用重复的将lua脚本写出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#eval 执行脚本并缓存
eval script numkeys key [key ...] arg [arg ...]

#load 缓存lua脚本
script load script

#使用缓存的脚本sha码调用脚本
evalsha sha1 numkeys key [key ...] arg [arg ...]

#使用sha码判断脚本是否已缓存
script exist sha1 [sha1 ...]

#清空所有缓存的脚本
script flush

#杀死当前正在执行的所有lua脚本
script kill

(九)Redis服务器相关命令

(九)Redis服务器相关命令

验证密码是否正确

使用auth命令验证密码是否正确,如果当前未登陆进行验证通过后会转为登陆状态,如果当前已登陆,会返回验证结果成功或失败:

1
auth password

查看服务器信息

1
2
3
4
5
6
7
8
9
INFO [section]

info cpu

# CPU
used_cpu_sys:5495.206247
used_cpu_user:5338.722012
used_cpu_sys_children:0.627837
used_cpu_user_children:0.214709

统计当前库下key的数量

1
dbsize

查看配置信息

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

config get port

config get max*

config get *


Docker:0>config get port
1) "port"
2) "6379"

Docker:0>config get max*
1) "maxmemory"
2) "0"
3) "maxmemory-samples"
4) "5"
5) "maxclients"
6) "10000"
7) "maxmemory-policy"
8) "noeviction"

修改当前配置信息(动态修改)

Config Set 命令可以动态地调整 Redis 服务器的配置(configuration)而无须重启,但此时配置文件中仍是修改前的配置,可搭配config rewrite命令一起使用:

1
config set parameter value

重写配置文件

Config rewrite 命令对启动 Redis 服务器时所指定的 redis.conf 配置文件进行改写。与config
set不同,set之后会将配置信息修改而无需重启服务,但此时redis.conf配置文件里记录的参数仍是set之前的值,如果将redis服务重启后会读取conf文件中的配置,这时候读到的还是set之前的配置,因此我们可以在set配置之后使用rewrite命令将当前的配置回写至配置文件内,这样就能不停机修改配置信息了,因此config set和config rewrite是配合使用的:

1
2
3
4
5
6
7
8
9
10
11
12
config rewrite

config set slowlog-max-len 256

config get slowlog-max-len

config rewrite

config rewrite 命令对启动 Redis 服务器时所指定的 redis.conf 配置文件进行改写写回配置文件


cat /opt/redis6/

config resetstat

使用Config Resetstat 命令重置 INFO 命令中的某些统计数据,包括:

  • Keyspace hits (键空间命中次数)
  • Keyspace misses (键空间不命中次数)
  • Number of commands processed (执行命令的次数)
  • Number of connections received (连接服务器的次数)
  • Number of expired keys (过期key的数量)
  • Number of rejected connections (被拒绝的连接数量)
  • Latest fork(2) time(最后执行 fork(2) 的时间)
  • The aof_delayed_fsync counter(aof_delayed_fsync 计数器的值)

切换数据库

redis共有16个db,从db0~db15,使用select index命令在数据库之间进行切换:

1
select index

time

Time 命令用于返回当前服务器时间,返回一个包含两个字符串的列表: 第一个字符串是当前时间(以 UNIX 时间戳格式表示),而第二个字符串是当前这一秒钟已经逝去的微秒数。

1
2
3
4
5
time

Docker:0>time
1) "1597122706"
2) "914251"

DEBUG

debug object key获取 key 的调试信息,当key不存在时返回错误信息。

debug segfault 命令执行一个非法的内存访问从而让 Redis 崩溃,仅在开发时用于 BUG 调试,执行后需要重启服务。

1
2
3
debug object key

debug segfault bengku崩溃,需要重启服务

清空库

flushdb清空当前数据库下的所有数据。
flushall清空所有库下的所有数据。

1
2
flushdb
flushall

监控

monitor命令用于监听redis服务器接收到的所有命令:

1
monitor

关闭

1
SHUTDOWN [NOSAVE|SAVE]

command

查看当前Redis中所有可用命令,使用Command 命令用于返回所有的Redis命令的详细信息,以数组形式展示:

1
command

使用command count命令查看当前Redis中命令的数量:

1
command count

使用command info命令查看当前Redis中指定的命令的详细信息:

1
COMMAND INFO command-name [command-name ...]

MySQL主备复制原理

MySQL主备复制原理

  • MySQL master 将数据变更写入二进制日志( binary log, 其中记录叫做二进制日志事件binary log events,可以通过 show binlog events 进行查看)

  • MySQL slave 将 master 的 binary log events 拷贝到它的中继日志(relay log)

  • MySQL slave 重放 relay log 中事件,将数据变更反映它自己的数据

canal 工作原理

  • canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump 协议
  • MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal )
  • canal 解析 binary log 对象(原始为 byte 流)

MySQL 备份

MySQL 备份

1、备份命令

格式:

mysqldump -h主机名 -P端口 -u用户名 -p密码 –database 数据库名 > 文件名.sql

例如: mysqldump -h 192.168.1.100 -p 3306 -uroot -ppassword –database cmdb > /data/backup/cmdb.sql

2、备份压缩

导出的数据有可能比较大,不好备份到远程,这时候就需要进行压缩

格式:mysqldump -h主机名 -P端口 -u用户名 -p密码 –database 数据库名 | gzip > 文件名.sql.gz

例如: mysqldump -h192.168.1.100 -p 3306 -uroot -ppassword –database cmdb | gzip > /data/backup/cmdb.sql.gz

3、备份同个库多个表

格式:mysqldump -h主机名 -P端口 -u用户名 -p密码 –database 数据库名 表1 表2 …. > 文件名.sql

例如 mysqldump -h192.168.1.100 -p3306 -uroot -ppassword cmdb t1 t2 > /data/backup/cmdb_t1_t2.sql

4、同时备份多个库

格式:mysqldump -h主机名 -P端口 -u用户名 -p密码 –databases 数据库名1 数据库名2 数据库名3 > 文件名.sql

例如:mysqldump -h192.168.1.100 -uroot -ppassword –databases cmdb bbs blog > /data/backup/mutil_db.sql

5、备份实例上所有的数据库

格式:mysqldump -h主机名 -P端口 -u用户名 -p密码 –all-databases > 文件名.sql

例如:mysqldump -h192.168.1.100 -p3306 -uroot -ppassword –all-databases > /data/backup/all_db.sql

6、备份数据出带删除数据库或者表的sql备份

格式:mysqldump -h主机名 -P端口 -u用户名 -p密码 –add-drop-table –add-drop-database 数据库名 > 文件名.sql

例如:mysqldump -uroot -ppassword –add-drop-table –add-drop-database cmdb > /data/backup/all_db.sql

7、备份数据库结构,不备份数据

格式:mysqldump -h主机名 -P端口 -u用户名 -p密码 –no-data 数据库名1 数据库名2 数据库名3 > 文件名.sql

例如:mysqldump –no-data –databases db1 db2 cmdb > /data/backup/structure.sql

(七)两阶段提交2PC

两阶段提交2PC

当应用逐渐扩展,出现一个应用使用多个数据源的情况,这个时候本地事务已经无法满足数据一致性的要求。由于多个数据源的同时访问,事务需要跨多个数据源管理,分布式事务应运而生。其中最流行的就是两阶段提交(2PC),分布式事务由事务管理器(TM)统一管理。

两阶段提交分为准备阶段和提交阶段。

两阶段提交-commit

image

两阶段提交-rollback

image

然而两阶段提交也不能完全保证数据一致性问题,并且有==同步阻塞==的问题,所以其优化版本三阶段提交(3PC)被发明了出来。

(七)Redis数据类型之ZSet(sorted_set)

(七)Redis数据类型之ZSet(sorted_set)

zadd

zadd key [NX|XX] [CH] [INCR] score member [score member …]

往有序集合中新增成员,需要指定该成员的分数,分数可以是整形或浮点型,当分数相同时候,索引下标按照字典排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
zadd zset 1 value1 2 value2 3 value3 4 value4

1,2,3,4 表示的是各个元素的分数

Docker:0>zrange zset 0 -1
1) "value1"
2) "value2"
3) "value3"
4) "value4"

Docker:0>zrange zset 0 -1 withscores
1) "value1"
2) "1"
3) "value2"
4) "2"
5) "value3"
6) "3"
7) "value4"
8) "4"

zrange

zrange key start stop [WITHSCORES]

根据下标查看集合内所有成员(及分数),[start,stop]
[0 -1] 表示全部元素

zcard

zcard key

获取有序集合的成员数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Docker:0>zrange zset 0 -1 
1) "value1"
2) "value2"
3) "value3"
4) "value4"
Docker:0>zrange zset 0 -1 withscores
1) "value1"
2) "1"
3) "value2"
4) "2"
5) "value3"
6) "3"
7) "value4"
8) "4"
Docker:0>zcard zset
"4"

zcount

ZCOUNT key min max

从有序集合内获取指定分数区间内的成员数。

1
2
3
4
5
6
7
8
9
10
11
Docker:0>zrange zset 0 -1 withscores
1) "value1"
2) "1"
3) "value2"
4) "2"
5) "value3"
6) "3"
7) "value4"
8) "4"
Docker:0>zcount zset 2 3
"2"

zrank

ZRANK key member

返回有序集合中指定成员的索引(下标):

zrevrange

zrevrange key start stop [WITHSCORES]

与zrange功能类似,不同的是zrevrange会将集合先反序[reverse]之后再执行zrange返回。

zincrby

修改有序集合内成员的分数,将有序集合内成员的分数增加increment分值,increment可以为浮点型整型也可以为负数或正数。

zincrby key increment member

zrem

zrem key member [member …]

从集合中移除指定的成员。