Redis原理

174 阅读13分钟

1. Redis为什么这么快?

1.1 Redis有多快?

官网说明:redis.io/topics/benc…

我们可以通过 redis-benchmark -t set,lpush -n 100000 -q 来进行查看set和lpush请求秒中能执行多少次

[root@VM-0-5-centos src]# ./redis-benchmark -t set,lpush -n 100000 -q
SET: 101832.99 requests per second
LPUSH: 103519.66 requests per second

1.2 Redis 为什么这么快?

  1. 因为是纯内存操作
  2. 单线程模型,无需创建线程销毁线程,上下文切换导致CPU损耗,避免线程之间竞争(Redis6.0之前采用的是单线程模型)
  3. 异步非阻塞I/O,多路复用处理并发连接

1.3 为什么Redis6.0之前采用单线程模型呢?

1.3.1 性能瓶颈不在CPU

Redis官网的描述是这样的 redis.io/topics/faq#… 简单总结一下:CPU不是redis的瓶颈,redis的瓶颈是跟网络和机器内存有关。

因为Redis的绝大部分操作时基于内存的,所以执行速度非常快。根据Redis官网的描述,在理想情况下Redis每秒可以提交一百万次请求,每次请求提交所需要的时间都是纳秒级别,既然每次操作这么,单线程就可以搞定,还何必用对线程呢?

1.3.2 线程上下文切换

多线程情况下会发生上下文切换。线程是由CPU调度的,CPU在一个核在一个时间片只能同时执行一个线程,线程切换回发生一系列操作,例如保护原线程的执行现场,载入另一个线程的执行现场,涉及到相关指令的恢复和保存。

1.3.3 I/O多路复用

由上面所述可知:Redis的瓶颈是内存和网络。内存瓶颈很好理解,redis缓存数据在内存上,需要大量内存空间,可以通过集群分片,来解决内存不足问题,例如Redis自身的无中心集群分片方案以及基于Codis这种基于代理的集群分片方案。

对于网络瓶颈,Redis在网络I/O上模型上采用多路复用技术。虽说是使用单线程模型处理用户请求,但是它却使用了I/O多路复用技术“并行”的处理来自客户端的多个连接,同时等待多个连接发送的请求。使用此技术,极大减少了系统开销,系统无需为每个客户端连接创建监听线程。

I/O 指的是网络 I/O。
多路指的是多个 TCP 连接(Socket 或 Channel)。
复用指的是复用一个或多个线程。
它的基本原理就是不再由应用程序自己监视连接,而是由内核替应用程序监视文件描述符fd(Linux 系统将所有设备都当作文件来处理,而 Linux 用文件描述符来标识每个文件对象。)

客户端操作的时候会产生不同事件的socket。在服务端I/O多路复用程序会把消息放入消息队列中,然后通过事件分发器,转发到不同的事件处理器中 多路复用有很多实现,以select为例,当用户进程调用了多路复用器,进程会阻塞。内核会监视多路复用器所负责的所有socket,当任务一个socket数据准备好了,多路复用器就会返回。这时候用户进程在调用read操作,把数据从内核缓冲区拷贝到用户缓冲中。 所以,I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪(readable)状态,select()函数就可以返回。

Redis 的多路复用, 提供了 select, epoll, evport, kqueue 几种选择,在编译的时候来选择一种。源码 ae.c(src目录下)

evport 是 Solaris 系统内核提供支持的;
epoll 是 LINUX 系统内核提供支持的;
kqueue 是 Mac 系统提供支持的;
select 是 POSIX 提供的,一般的操作系统都有支撑(保底方案;
源码 ae_epoll.c、ae_select.c、ae_kqueue.c、ae_evport.c

1.3.4 Redis6.0为何引入多线程

Redis6.0引入多线程的部分,只是用于处理网络数据的读写和协议解析,执行命令仍然是单线程。 由图中可以看出,Redis在处理网络数据时候,调用select的过程中会阻塞线程,如果并发量过高,会有性能损失。遇到这种情况,就可以使用多线程可以解决,还有效利用CPU的多核优势。
新增参数:

io-threads 4 # 开启 4 个 IO 线程
io-threads-do-reads yes # 请求解析也是用 IO 线程

2. 内存回收

Redis中所有数据都是存储在内存当中的,在某些情况下需要对占用的内存进行回收。内存回收的方式分为两类,一类是key过期了,一类是内存使用达到上限。

2.1 过期策略

  • 定时过期 每个设置了过期时间的key,会给他们创建一个定时器,到过期时间就会立即清除,但是该方式会占用大量的CPU资源去处理过期数据,从而影响缓存响应时间和吞吐量。
  • 惰性过期 只有当访问一个 key 时,才会判断该 key 是否已过期,过期则清除。该策略可以最 大化地节省 CPU 资源,却对内存非常不友好。极端情况可能出现大量的过期 key 没有再 次被访问,从而不会被清除,占用大量内存。
  • 定期过期 每隔一定的时间,会扫描一定数量的数据库的 expires 字典中一定数量的 key,并清 除其中已过期的 key
    Redis 中同时使用了惰性过期和定期过期两种过期策略。

2.2 淘汰策略

当redis的内存达到最大限度的时候,需要淘汰算法来决定清理掉哪些数据,以保证新数据的存入,最大内存可在redis.conf中进行配置# maxmemory <bytes>

淘汰策略也可在redis.conf中进行配置

# maxmemory-policy noeviction

# volatile-lru -> Evict using approximated LRU among the keys with an expire set.
# allkeys-lru -> Evict any key using approximated LRU.
# volatile-lfu -> Evict using approximated LFU among the keys with an expire set.
# allkeys-lfu -> Evict any key using approximated LFU.
# volatile-random -> Remove a random key among the ones with an expire set.
# allkeys-random -> Remove a random key, any key.
# volatile-ttl -> Remove the key with the nearest expire time (minor TTL)
# noeviction -> Don't evict anything, just return an error on write operations.

LRU,Least Recently Userd:最近最少使用,判断最近被使用的时间,最远的将被优先淘汰

LFU,Least Frequently Used,最不常用,4.0 版本新增

Random,随机删除。

  • volatile-lru 根据 LRU 算法删除设置了超时属性(expire)的键,直到腾出足够内存为止。如果没有可删除的键对象,回退到 noeviction 策略。
  • allkeys-lru 根据 LRU 算法删除键,不管数据有没有设置超时属性,直到腾出足够内存为止。
  • volatile-lfu 在带有过期时间的键中选择最不常用的
  • allkeys-lfu 在所有的键中选择最不常用的,不管数据有没有设置超时属性。
  • volatile-random 在带有过期时间的键中随机选择。
  • allkeys-random 随机删除所有键,直到腾出足够内存为止。
  • volatile-ttl 根据键值对象的 ttl 属性,删除最近将要过期数据。如果没有,回退到 noeviction 策略。
  • noeviction 默认策略,不会删除任何数据,拒绝所有写入操作并返回客户端错误信息(error)OOM command not allowed when used memory,此时 Redis 只响应读操作 动态修改淘汰策略:
redis> config set maxmemory-policy volatile-lru

3. 持久化机制

我们都知道Redis的数据都存放在内存中,如果断电或者宕机,都会导致内存数据丢失。为了防止重启之后数据不丢失,Redis提供了两种持久化的方案,一种是RDB快照,一种是AOF。

3.1 RDB (Redis DataBase)

RDB是Redis默认的持久化方案。当满足一定条件的时候,会把内存中的数据写入磁盘中,生成一个dump.rdb文件。Redis重启的时候会加载这个dump.rdb文件恢复数据。

3.1.1 触发方式

RDB有两种形式会触发:

  • 自动触发
  1. 配置规则触发 redis.conf中定义了把数据保存到磁盘的触发频率,如果不需要RDB方案,注释save或者配置成空字符“”.
save 900 1 # 900 秒内至少有一个 key 被修改(包括添加)
save 300 10 # 400 秒内至少有 10 个 key 被修改
save 60 10000 # 60 秒内至少有 10000 个 key 被修改

RDB的文件位置目录

# 文件路径,默认是启动目录下
dir ./
# 文件名称
dbfilename dump.rdb
# 是否是 LZF 压缩 rdb 文件
rdbcompression yes
# 开启数据校验
rdbchecksum yes
  1. shutdown触发,保证服务器正常关闭
  2. flushall,RDB文件时空的
  • 手动触发
  1. save指令 生成快照的时候会阻塞当前Redis服务器,Redis不能处理其他命令。

  2. bgsave指令 执行 bgsave 时,Redis 会在后台异步进行快照操作,快照同时还可以响应客户端请求。

3.1.2 RDB恢复演示

给k1,k2设置,然后关闭redis服务不保存(shutdown会触发save),由于此时还未大道RDB的持久化的save规则的条件,重启服务之后,会发现为之前的设值并未生效。若使用shutdown或者满足配置规则的条件时,将会成功持久化到RDB文件中。

127.0.0.1:6379> set k1 1
OK
127.0.0.1:6379> set k2 2
OK
127.0.0.1:6379> shutdown nosave
not connected> 
-- 重新连接
[root@VM-0-5-centos ~]# redis-server  /usr/local/src/redis-6.0.9/redis.conf 
[root@VM-0-5-centos ~]# redis-cli 
127.0.0.1:6379> mget k1 k2
1) (nil)
2) (nil)

也可以使用save/bgsave来直接执行备份操作

127.0.0.1:6379> set k1 1
OK
127.0.0.1:6379> set k2 2
OK
127.0.0.1:6379> bgsave
Background saving started
127.0.0.1:6379> shutdow nosave
(error) ERR unknown command `shutdow`, with args beginning with: `nosave`, 
127.0.0.1:6379> shutdown nosave
not connected> 
[root@VM-0-5-centos ~]# redis-server  /usr/local/src/redis-6.0.9/redis.conf 
[root@VM-0-5-centos ~]# redis-cli 
127.0.0.1:6379> hget k1 k2
(error) WRONGTYPE Operation against a key holding the wrong kind of value
127.0.0.1:6379> mget k1 k2
1) "1"
2) "2

3.1.3 RDB的优势和劣势

  • 优势 RDB 是一个非常紧凑(compact)的文件,它保存了 redis 在某个时间点上的数据集。这种文件非常适合用于进行备份和灾难恢复
    生成 RDB 文件的时候,redis 主进程会 fork()一个子进程来处理所有保存工作,主进程不需要进行任何磁盘 IO 操作。
    RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
  • 劣势 RDB 方式数据没办法做到实时持久化/秒级持久化。另外bgsave 每次运行都要执行 fork 操作创建子进程,频繁执行成本过高。
    一定时间间隔备份数据,如果redis宕机,容易丢失最后一次快照之后的数据修改。

3.2 AOF (Append Only File)

Redis默认不开启AOF,其采用日志的形式来记录每个写操作,并追加到文件中。
Redis重启的时候会根据日志文件的内容把写指令从前到后一次性完成数据恢复

3.2.1 配置

# 开关
appendonly no
# 文件名
appendfilename "appendonly.aof"

文件内容

由于操作系统的缓存机制,AOF的数据并没有实时的写入磁盘,而是进入操作系统的硬盘缓存。

appendfsync everysec

该参数就是空值硬盘缓存写入磁盘,默认是everysec。

  • no 表示不执行 fsync,由操作系统保证数据同步到磁盘,速度最快,但是不太安全;
  • always 表示每次写入都执行 fsync,以保证数据同步到磁盘,效率很低;
  • everysec 表示每秒执行一次 fsync,可能会导致丢失这 1s 数据。通常选择 everysec ,兼顾安全性和效率

AOF持久化会不断的写入命令到AOF文件中,随着业务的不断推进,AOF会越来越大,文件越大,占用的内存也越大,AOF的恢复时间也越久。

为了解决这一问题,Redis增加了重写机制,当AOF文件的大小超过所设定的阈值的时候,Redis就会启动AOF文件的内容压缩。

可以使用命令 bgrewriteaof 来重写

AOF 文件重写并不是对原文件进行重新整理,而是直接读取服务器现有的键值对,然后用一条命令去代替之前记录这个键值对的多条命令,生成一个新的文件后去替换原来的 AOF 文件。也就是说若服务器中有一条key为k1,val为1,不管前面针对这个key执行了多少命令,只保存现有的命令。

# 重写触发机制
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
  • auto-aof-rewrite-percentage 默认值为 100。aof 自动重写配置,当目前 aof 文件大小超过上一次重写的 aof 文件大小的百分之多少进行重写,即当 aof 文件增长到一定大小的时候,Redis 能够调用bgrewriteaof对日志文件进行重写。当前 AOF 文件大小是上次日志重写得到 AOF 文件大小的二倍(设置为 100)时,自动启动新的日志重写过程
  • auto-aof-rewrite-min-size 默认 64M。设置允许重写的最小 aof 文件大小,避免了达到约定百分比但尺寸仍然很小的情况还要重写。
编写一个循环的lua脚本,调用一万次set操作
for  i=1,10000,1 do
  redis.call('set','k1',1)
end

查看AOF文件发现里面有很多的命令操作

另外我们可以发现AOF文件增大为274KB

-rw-r--r-- 1 root root 274K Nov 29 19:00 appendonly.aof

此时我们执行bgrewriteaof命令,发现aof文件内容和大小都有所变化

-rw-r--r-- 1 root root 103 Nov 29 19:06 appendonly.aof

此时会有个问题:假如重写的时候,执行了修改数据的操作,怎么办?
这个不用担心,Redis已经为我们考虑到了,在子进程执行AOF重写时,主进程除了会将命令追加到现有的AOF文件中,还会追加到重写缓冲中。

两个和AOF相关的参数

  • no-appendfsync-on-rewrite 设置为 yes 表示 rewrite 期间对新写操作不 fsync,暂时存在内存中,等 rewrite 完成后再写入,默认为 no,建议修改为 yes。Linux 的默认 fsync策略是 30 秒。可能丢失 30 秒数据
  • aof-load-truncated 指redis在恢复时,会忽略最后一条可能存在问题的指令。默认值yes。即在aof写入时,可能存在指令写错的问题(突然断电,写了一半),这种情况下,yes会log并继续,而no会直接恢复失败.

3.2.3 优缺点

  • 优点 AOF 持久化的方法提供了多种的同步频率,即使使用默认的同步频率每秒同步一次,Redis 最多也就丢失 1 秒的数据而已
  • 缺点
  1. 对于具有相同数据的的 Redis,AOF 文件通常会比 RDF 文件体积更大(RDB存的是数据快照)
  2. 高并发的情况下,RDB会比AOF具有更好的性能保证

3.3 两种比较

如果可以忍受一小段时间内的数据丢失,RDB是最好的选择,而且RDB恢复数据的速度要比AOF快,否则的话就是用AOF

我们也可以将两者都开启。这种情况下,redis重启会优先使用AOF来恢复原始数据,因为通常情况下AOF保存的数据要比较完整。

既然同时使用跟单独开启AOF差不多,为什么还要同时开启呢?
RDB更适合用于备份数据库(AOF在不断变化不好备份), 快速重启,而且不会有AOF可能潜在的bug,留着作为一个万一的手段。