Redis 总结

[TOC]

一、Redis 基础常问

1.1、Redis 有哪些数据结构

  • 基础:字符串String、字典Hash、列表List、集合Set、有序集合SortedSet
  • 加分:HyperLogLog、Geo、Pub/Sub
  • 高级:BloomFilter、RedisSearch、Redis-ML

1.2、Redis 大量 key 设置同一时间过期

电商首页经常会使用定时任务刷新缓存,可能大量的数据失效时间都十分集中,如果失效时间一样,又刚好在失效的时间点大量用户涌入,就有可能造成缓存雪崩,一般需要在时间上加一个随机值,使得过期时间分散一些。

1.3、Redis分布式锁

先拿 setnx 来争抢锁,抢到之后,再用 expire 给锁加一个过期时间防止锁忘记了释放。

但 setnx 不能同时完成 expire 设置失效时长,不能保证 setnx 和 expire 的原子性,可以使用 set 命令完成 setnx 和 expire 的操作,并且这种操作是原子操作。

set key value [EX seconds] [PX milliseconds] [NX|XX]
EX seconds:设置失效时长,单位秒
PX milliseconds:设置失效时长,单位毫秒
NX:key不存在时设置value,成功返回OK,失败返回(nil)
XX:key存在时设置value,成功返回OK,失败返回(nil)

案例:设置name=p7+,失效时长100s,不存在时设置
set name p7+ ex 100 nx

1.4、Redis keys 和 scan

Redis 是单线程的。

keys 指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。

这个时候可以使用 scan 指令, scan 指令可以无阻塞的提取出指定模式的 key 列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用 keys 指令长。

不过,增量式迭代命令也不是没有缺点的: 举个例子, 使用 SMEMBERS 命令可以返回集合键当前包含的所有元素, 但是对于 SCAN 这类增量式迭代命令来说, 因为在对键进行增量式迭代的过程中, 键可能会被修改, 所以增量式迭代命令只能对被返回的元素提供有限的保证 。

1.5、Redis 当做异步队列

一般使用 list 结构作为队列, rpush 生产消息, lpop 消费消息。当 lpop 没有消息的时候,要适当 sleep 一会再重试。

不用 sleep 的话:list 还有个指令叫 blpop ,在没有消息的时候,它会阻塞住直到消息到来。

1.6、Redis 发布订阅

使用 pub/sub 主题订阅者模式,可以实现 1:N 的消息队列。

# 订阅频道名为 redisChat
SUBSCRIBE redisChat

# 在同一个频道 redisChat 发布消息
PUBLISH redisChat "Redis is a great caching technique"

pub/sub 有什么缺点?

在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列。

1.7、Redis 实现延时队列

Redis 实现延时任务,是通过其数据结构 ZSET(sortedset) 来实现的。ZSET 会储存一个 score 和一个 value,可以将 value按照 score 进行排序,而 SET 是无序的。

延时任务的实现分为以下几步来实现:

  1. 将任务的执行时间作为score,要执行的任务数据作为value,存放在zset中;
  2. 用一个进程定时查询zset的score分数最小的元素,可以用ZRANGEBYSCORE key -inf +inf limit 0 1 withscores命令来实现;
  3. 如果最小的分数小于等于当前时间戳,就将该任务取出来执行,否则休眠一段时间后再查询

1.8、Redis 怎么持久化?主从数据怎么交互?

RDB 做镜像全量持久化,AOF 做增量持久化。

因为RDB 会耗费较长时间,不够实时,在停机的时候会导致大量丢失数据,所以需要AOF 来配合使用。在 redis 实例重启时,会使用 RDB 持久化文件重新构建内存,再使用 AOF 重放近期的操作指令来实现完整恢复重启之前的状态。

突然机器掉电会怎样?

AOF 日志 sync 属性的配置,如果不要求性能,在每条写指令时都 sync 一下磁盘,就不会丢失数据。但是在高性能的要求下每次都 sync 是不现实的,一般都使用定时 sync,比如1s1次,这个时候最多就会丢失1s的数据。

RDB的原理

fork 是指 redis 通过创建子进程来进行 RDB 操作,cow 指的是 copy on write,子进程创建后,父子进程共享数据段,父进程继续提供读写服务,逐渐和子进程分离开来。

1.9、Redis 同步机制

第一次同步时,主节点做一次 bgsave,并同时将后续修改操作记录到内存 buffer,待完成后将 RDB 文件全量同步到复制节点,复制节点接受完成后将 RDB 镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。

后续的增量数据通过 AOF日志同步即可,有点类似数据库的 binlog。

1.10、Redis 高可用

Redis Sentinal 着眼于高可用,在 master 宕机时会自动将 slave 提升为master,继续提供服务。

Redis Cluster 着眼于扩展性,在单个 redis 内存不足时,使用 Cluster 进行分片存储。

1.11、Redis 缓存雪崩

目前电商首页以及热点数据都会去做缓存 ,一般缓存都是定时任务去刷新,或者是查不到之后去更新的,定时任务刷新就有一个问题。缓存同一时间大面积失效,那一瞬间 Redis 跟没有一样,那这个数量级别的请求直接打到数据库造成数据库 down。这就是缓存雪崩。

举个例子:如果所有首页的 Key 失效时间都是12小时,中午12点刷新的,零点有个秒杀活动大量用户涌入,假设当时每秒 6000 个请求,本来缓存在可以扛住每秒 5000 个请求,但是缓存当时所有的 Key 都失效了。此时 1 秒 6000 个请求全部落数据库,数据库必然扛不住,它会报一下警,真实情况可能 DBA 都没反应过来就直接挂了。此时,如果没用什么特别的方案来处理这个故障,DBA 很着急,重启数据库,但是数据库立马又被新的流量给打死了。

解决方法:

在批量往Redis存数据的时候,把每个 Key 的失效时间都加个随机值就好了,这样可以保证数据不会在同一时间大面积失效:

setRedis(Key,value,time + Math.random() * 10000);

1.12、Redis 缓存穿透

缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,一般数据库的 id 都是1开始自增上去的,如发起为 id值为 -1 的数据或 id 为特别大不存在的数据。像这种如果不对参数做校验,数据库 id 都是大于0的,一直用小于 0 的参数去请求,每次都能绕开 Redis 直接打到数据库,数据库也查不到,每次都这样,并发高点就容易崩掉了。

解决方法:

在接口层增加校验,比如用户鉴权校验,参数做校验,不合法的参数直接代码 Return,比如:id 做基础校验,id <=0 的直接拦截等。

Redis 还有一个高级用法布隆过滤器(Bloom Filter),这个也能很好的防止缓存穿透的发生,原理是利用高效的数据结构和算法快速判断出你这个 Key 是否在数据库中存在,不存在 return 就好了,存在就去查了DB 刷新 KV 再 return。

1.13、Redis 缓存击穿

缓存击穿跟缓存雪崩有点像,但是又有一点不一样,缓存雪崩是因为大面积的缓存失效,打崩了 DB,而缓存击穿不同的是缓存击穿是指一个 Key 非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个 Key 在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个完好无损的桶上凿开了一个洞。

解决方法:

设置热点数据永远不过期。

或者加上互斥锁就能搞定了。

二、Redis 架构

2.1、Redis 中常用的 5 种数据结构和应用场景

数据结构:

  • String:缓存、计数器、分布式锁等。
  • List:链表、队列、微博关注人时间轴列表等。
  • Hash:用户信息、Hash 表等。
  • Set:去重、赞、踩、共同好友等。
  • Zset:访问量排行榜、点击量排行榜等。

应用场景:

  • 缓存
  • 排行榜系统:Redis提供了列表和有序集合数据结构,合理地使用这些数据结构可以很方便地构建各种排行
    榜系统
  • 计数器应用:Redis天然支持计数功能而且计数的性能也非常好,可以说是计数器系统的重要选择
  • 社交网络:赞/踩、粉丝、共同好友/喜好、推送、下拉刷新等,Redis提供的数据结构可以相对比较容易地实现这些
  • 消息队列系统

2.2、Redis 为什么这么快

  • 纯内存访问,Redis 将所有数据放在内存中,内存的响应时长大约为100 纳秒
  • 数据结构简单,对数据操作也简单,Redis 中的数据结构是专门进行优化设计的
  • 非阻塞 I/O,Redis 使用 epoll 作为 I/O 多路复用技术的实现,再加上 Redis 自身的事件处理模型将 epoll 中的连接、读写、关闭都转换为事件,不在网络 I/O 上浪费过多的时间
  • 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗

2.3、Redis 阻塞

Redis是典型的单线程架构,所有的读写操作都是在一条主线程中完成的。当 Redis 用于高并发场景时,这条线程就变成了它的生命线。如果出现阻塞,哪怕是很短时间,对于应用来说都是噩梦。

导致阻塞问题的原因:

  • 内在原因:不合理地使用API或数据结构、CPU饱和、持久化阻塞等
  • 外在原因:CPU竞争、内存交换、网络问题等

2.3.1、内在原因

1)API 或数据结构使用不合理

通常Redis执行命令速度非常快,但是,如果对一个包含上万个元素的hash结构执行hgetall操作,由于数据量比较大且命令算法复杂度是O(n),这条命令执行速度必然很慢,或者使用 keys 命令从所有 key 中匹配。

对于高并发的场景应该尽量避免在大对象上执行算法复杂度超过O(n)的命令,可以:

  • 调整大对象:缩减大对象数据或把大对象拆分为多个小对象,防止一次命令操作过多的数据
  • SCAN cursor [MATCH pattern] [COUNT count]

2)CPU 饱和

单线程的Redis处理命令时只能使用一个CPU。而CPU饱和是指Redis把单核CPU使用率跑到接近100%。使用top命令很容易识别出对应Redis进程的CPU使用率。CPU饱和是非常危险的,将导致Redis无法处理更多的命令,严重影响吞吐量和应用方的稳定性。

3)持久化阻塞

对于开启了持久化功能的Redis节点,需要排查是否是持久化导致的阻塞。

  • fork阻塞:fork操作发生在RDB和AOF重写时,Redis主线程调用fork操作产生共享内存的子进程,由子进程完成持久化文件重写工作。如果fork操作本身耗时过长,必然会导致主线程的阻塞。
  • AOF刷盘阻塞:当我们开启AOF持久化功能时,文件刷盘的方式一般采用每秒一次,后台线程每秒对AOF文件做fsync操作。当硬盘压力过大时,fsync操作需要等待,直到写入完成。如果主线程发现距离上一次的fsync成功超过2秒,为了数据安全性它会阻塞直到后台线程执行fsync操作完成。这种阻塞行为主要是硬盘压力引起。
  • HugePage写操作阻塞:子进程在执行重写期间利用Linux写时复制技术降低内存开销,因此只有写操作时Redis才复制要修改的内存页。对于开启Transparent HugePages的操作系统,每次写命令引起的复制内存页单位由4K变为2MB,放大了512倍,会拖慢写操作的执行时间,导致大量写操作慢查询。

2.3.2、外在原因

1)CPU竞争

  • 进程竞争:Redis是典型的CPU密集型应用,不建议和其他多核CPU密集型服务部署在一起。当其他进程过度消耗CPU时,将严重影响Redis吞吐量。可以通过top、sar等命令定位到CPU消耗的时间点和具体进程,这个问题比较容易发现,需要调整服务之间部署结构。
  • 绑定CPU:部署Redis时为了充分利用多核CPU,通常一台机器部署多个实例。常见的一种优化是把Redis进程绑定到CPU上,用于降低CPU频繁上下文切换的开销。这个优化技巧正常情况下没有问题,但是存在例外情况,当Redis父进程创建子进程进行RDB/AOF重写时,如果做了CPU绑定,会与父进程共享使用一个CPU。子进程重写时对单核CPU使用率通常在90%以上,父进程与子进程将产生激烈CPU竞争,极大影响Redis稳定性。因此对于开启了持久化或参与复制的主节点不建议绑定CPU。

2)内存交换

内存交换(swap)对于Redis来说是非常致命的,Redis保证高性能的一个重要前提是所有的数据在内存中。如果操作系统把Redis使用的部分内存换出到硬盘,由于内存与硬盘读写速度差几个数量级,会导致发生交换后的Redis性能急剧下降。

预防内存交换:

  • 保证机器充足的可用内存。
  • 确保所有Redis实例设置最大可用内存(maxmemory),防止极端情况下Redis内存不可控的增长。
  • 降低系统使用swap优先级。

3)网络问题

连接拒绝:

  • 网络闪断(网络割接或者带宽耗尽)
  • Redis连接拒绝(超过客户端最大连接数)
  • 连接溢出(进程限制或backlog队列溢出)

网络延迟:

网络延迟取决于客户端到Redis服务器之间的网络环境。主要包括它们之间的物理拓扑和带宽占用情况。常见的物理拓扑按网络延迟由快到慢可分为:同物理机>同机架>跨机架>同机房>同城机房>异地机房。但它们容灾性正好相反,同物理机容灾性最低而异地机房容灾性最高。

网卡软中断:

网卡软中断是指由于单个网卡队列只能使用一个CPU,高并发下网卡数据交互都集中在同一个CPU,导致无法充分利用多核CPU的情况。网卡软中断瓶颈一般出现在网络高流量吞吐的场景。

2.4、redis 和 memcached 有啥区别

Memcache

  • Memcache可以利用多核优势,单实例吞吐量极高,可以达到几十万QPS,适用于最大程度扛量
  • 只支持简单的key/value数据结构,不像Redis可以支持丰富的数据类型。
  • 无法进行持久化,数据不能备份,只能用于缓存使用,且重启后数据全部丢失

Redis

  • 支持多种数据结构,如string,list,dict,set,zset,hyperloglog
  • 单线程请求,所有命令串行执行,并发情况下不需要考虑数据一致性问题
  • 支持持久化操作,可以进行aof及rdb数据持久化到磁盘,从而进行数据备份或数据恢复等操作,较好的防止数据丢失的手段
  • 支持通过Replication进行数据复制,通过master-slave机制,可以实时进行数据的同步复制,支持多级复制和增量复制

三、Redis 持久化

3.1、RDB

RDB是一种快照存储持久化方式,具体就是将Redis某一时刻的内存数据保存到硬盘的文件当中,默认保存的文件名为dump.rdb,而在Redis服务器启动时,会重新加载dump.rdb文件的数据到内存当中恢复数据。

3.1.1、开启RDB持久化方式

开启RDB持久化方式很简单,客户端可以通过向Redis服务器发送save或bgsave命令让服务器生成rdb文件,或者通过服务器配置文件指定触发RDB条件。

1)save:

save命令是一个同步操作。当客户端向服务器发送save命令请求进行持久化时,服务器会阻塞save命令之后的其他客户端的请求,直到数据同步完成。

如果数据量太大,同步数据会执行很久,而这期间Redis服务器也无法接收其他请求,所以,最好不要在生产环境使用save命令。

2)bgsave:

与save命令不同,bgsave命令是一个异步操作。当客户端发服务发出bgsave命令时,Redis服务器主进程会forks一个子进程来数据同步问题,在将数据保存到rdb文件之后,子进程会退出。

与save命令相比,Redis服务器在处理bgsave采用子线程进行IO写入,而主进程仍然可以接收其他请求,但forks子进程是同步的,所以forks子进程时,一样不能接收其他请求,这意味着,如果forks一个子进程花费的时间太久(一般是很快的),bgsave命令仍然有阻塞其他客户的请求的情况发生。

3)服务器配置自动触发:

除了通过客户端发送命令外,还有一种方式,就是在Redis配置文件中的save指定到达触发RDB持久化的条件,比如【多少秒内至少达到多少写操作】就开启RDB数据同步。

例如可以在配置文件redis.conf指定如下的选项:

# 900s内至少达到一条写命令
save 900 1
# 300s内至少达到10条写命令
save 300 10
# 60s内至少达到10000条写命令
save 60 10000

之后在启动服务器时加载配置文件。

# 启动服务器加载配置文件
redis-server redis.conf

这种通过服务器配置文件触发RDB的方式,与bgsave命令类似,达到触发条件时,会forks一个子进程进行数据同步。

3.1.2、RDB的优缺点

RDB的几个优点

  • 与AOF方式相比,通过rdb文件恢复数据比较快
  • rdb文件非常紧凑,适合于数据备份
  • 通过RDB进行数据备,由于使用子进程生成,所以对Redis服务器性能影响较小

RDB的几个缺点

  • 如果服务器宕机的话,采用RDB的方式会造成某个时段内数据的丢失,比如我们设置10分钟同步一次或5分钟达到1000次写入就同步一次,那么如果还没达到触发条件服务器就死机了,那么这个时间段的数据会丢失
  • 使用save命令会造成服务器阻塞,直接数据同步完成才能接收后续请求
  • 使用bgsave命令在forks子进程时,如果数据量太大,forks的过程也会发生阻塞,另外,forks子进程会耗费内存

3.2、AOF(Append-only file)

与RDB存储某个时刻的快照不同,AOF持久化方式会记录客户端对服务器的每一次写操作命令,并将这些写操作以Redis协议追加保存到以后缀为aof文件末尾,在Redis服务器重启时,会加载并运行aof文件的命令,以达到恢复数据的目的。

3.2.1、开启AOF持久化方式

Redis默认不开启AOF持久化方式,可以在配置文件中开启并进行更加详细的配置,如下面的redis.conf文件:

# 开启aof机制
appendonly yes

# aof文件名
appendfilename "appendonly.aof"

# 写入策略,always表示每个写操作都保存到aof文件中,也可以是everysec或no
appendfsync always

# 默认不重写aof文件
no-appendfsync-on-rewrite no

# 保存目录
dir ~/redis/

三种写入策略

在上面的配置文件中,可以通过appendfsync选项指定写入策略,有三个选项

appendfsync always
# appendfsync everysec
# appendfsync no
  • always:客户端的每一个写操作都保存到aof文件当,这种策略很安全,但是每个写请注都有IO操作,所以也很慢
  • everysec:appendfsync的默认写入策略,每秒写入一次aof文件,因此,最多可能会丢失1s的数据
  • no:Redis服务器不负责写入aof,而是交由操作系统来处理什么时候写入aof文件,更快,但也是最不安全的选择,不推荐使用

3.2.2、AOF文件重写

AOF将客户端的每一个写操作都追加到aof文件末尾,比如对一个key多次执行incr命令,这时候,aof保存每一次命令到aof文件中,aof文件会变得非常大。

incr num 1
incr num 2
incr num 3
incr num 4
incr num 5
incr num 6
...
incr num 100000

aof文件太大,加载aof文件恢复数据时,就会非常慢,为了解决这个问题,Redis支持aof文件重写,通过重写aof,可以生成一个恢复当前数据的最少命令集,比如上面的例子中那么多条命令,可以重写为:

set num 100000

aof文件是一个二进制文件,并不是像上面的例子一样,直接保存每个命令,而使用Redis自己的格式,上面只是方便演示。

3.2.3、两种重写方式

通过在redis.conf配置文件中的选项no-appendfsync-on-rewrite可以设置是否开启重写,这种方式会在每次fsync时都重写,影响服务器性以,因此默认值为no,不推荐使用。

# 默认不重写aof文件
no-appendfsync-on-rewrite no

客户端向服务器发送bgrewriteaof命令,也可以让服务器进行AOF重写。

# 让服务器异步重写追加aof文件命令
> bgrewriteaof

AOF重写方式也是异步操作,即如果要写入aof文件,则Redis主进程会forks一个子进程来处理,如下所示:

重写aof文件的好处

  • 压缩aof文件,减少磁盘占用量。
  • 将aof的命令压缩为最小命令集,加快了数据恢复的速度。

3.2.4、AOF文件损坏

在写入aof日志文件时,如果Redis服务器宕机,则aof日志文件文件会出格式错误,在重启Redis服务器时,Redis服务器会拒绝载入这个aof文件,可以通过以下步骤修复aof并恢复数据。

1、备份现在aof文件,以防万一。

2、使用redis-check-aof命令修复aof文件,该命令格式如下:

# 修复aof日志文件
$ redis-check-aof -fix file.aof

3、重启Redis服务器,加载已经修复的aof文件,恢复数据。

3.2.5、AOF的优缺点

AOF的优点:

  • AOF只是追加日志文件,因此对服务器性能影响较小,速度比RDB要快,消耗的内存较少。

AOF的缺点:

  • AOF方式生成的日志文件太大,即使通过AFO重写,文件体积仍然很大。
  • 恢复数据的速度比RDB慢。

四、Redis Key 过期策略和内存淘汰机制

4.1、Redis设置过期时间

  • expire key time(以秒为单位)–这是最常用的方式
  • setex(String key, int seconds, String value)–字符串独有的方式

注:除了字符串自己独有设置过期时间的方法外,其他方法都需要依靠expire方法来设置时间,如果没有设置时间,那缓存就是永不过期,如果设置了过期时间,之后又想让缓存永不过期,使用persist key。

4.2、过期策略

定时删除

  • 含义:在设置key的过期时间的同时,为该key创建一个定时器,让定时器在key的过期时间来临时,对key进行删除
  • 优点:保证内存被尽快释放
  • 缺点:
    • 若过期key很多,删除这些key会占用很多的CPU时间,在CPU时间紧张的情况下,CPU不能把所有的时间用来做要紧的事儿,还需要去花时间删除这些key
    • 定时器的创建耗时,若为每一个设置过期时间的key创建一个定时器(将会有大量的定时器产生),性能影响严重
    • 没人用

惰性删除

  • 含义:key过期的时候不删除,每次从数据库获取key的时候去检查是否过期,若过期,则删除,返回null。
  • 优点:删除操作只发生在从数据库取出key的时候发生,而且只删除当前key,所以对CPU时间的占用是比较少的,而且此时的删除是已经到了非做不可的地步(如果此时还不删除的话,我们就会获取到了已经过期的key了)
  • 缺点:若大量的key在超出超时时间后,很久一段时间内,都没有被获取过,那么可能发生内存泄露(无用的垃圾占用了大量的内存)

定期删除

  • 含义:每隔一段时间执行一次删除(在redis.conf配置文件设置hz,1s刷新的频率)过期key操作
  • 优点:
    • 通过限制删除操作的时长和频率,来减少删除操作对CPU时间的占用--处理"定时删除"的缺点
    • 定期删除过期key--处理"惰性删除"的缺点
  • 缺点
    • 在内存友好方面,不如"定时删除"
    • 在CPU时间友好方面,不如"惰性删除"
  • 难点
    • 合理设置删除操作的执行时长(每次删除执行多长时间)和执行频率(每隔多长时间做一次删除)(这个要根据服务器运行情况来定了)

看完上面三种策略后可以得出以下结论:

  • 定时删除和定期删除为主动删除:Redis会定期主动淘汰一批已过去的key
  • 惰性删除为被动删除:用到的时候才会去检验key是不是已过期,过期就删除
  • 惰性删除为redis服务器内置策略

定期删除可以通过:

  • 第一、配置redis.conf 的hz选项,默认为10 (即1秒执行10次,100ms一次,值越大说明刷新频率越快,最Redis性能损耗也越大)
  • 第二、配置redis.conf的maxmemory最大值,当已用内存超过maxmemory限定时,就会触发主动清理策略

4.3、Redis 采用的过期策略

惰性删除+定期删除:

  • 惰性删除流程
    • 在进行get或setnx等操作时,先检查key是否过期
    • 若过期,删除key,然后执行相应操作
    • 若没过期,直接执行相应操作
  • 定期删除流程(简单而言,对指定个数个库的每一个库随机删除小于等于指定个数个过期key)
    • 遍历每个数据库(就是redis.conf中配置的"database"数量,默认为16)
      • 检查当前库中的指定个数个key(默认是每个库检查20个key,注意相当于该循环执行20次,循环体时下边的描述)
        • 如果当前库中没有一个key设置了过期时间,直接执行下一个库的遍历
        • 随机获取一个设置了过期时间的key,检查该key是否过期,如果过期,删除key
        • 判断定期删除操作是否已经达到指定时长,若已经达到,直接退出定期删除。

4.4、RDB 对过期 key 的处理

过期 key 对 RDB 没有任何影响

  • 从内存数据库持久化数据到 RDB 文件
    • 持久化 key 之前,会检查是否过期,过期的 key 不进入 RDB 文件
  • 从 RDB 文件恢复数据到内存数据库
    • 数据载入数据库之前,会对 key 先进行过期检查,如果过期,不导入数据库(主库情况)

4.5、AOF 对过期 key 的处理

过期 key 对 AOF 没有任何影响

  • 从内存数据库持久化数据到 AOF 文件:
    • 当 key 过期后,还没有被删除,此时进行执行持久化操作(该 key 是不会进入 aof 文件的,因为没有发生修改命令)
    • 当 key 过期后,在发生删除操作时,程序会向 aof 文件追加一条 del 命令(在将来的以 aof 文件恢复数据的时候该过期的键就会被删掉)
  • AOF 重写
    • 重写时,会先判断 key 是否过期,已过期的 key 不会重写到 aof 文件

4.6、Redis 的内存淘汰机制

Redis 定义了6 种 redis 内存淘汰策略方案:

  • noeviction(默认策略):对于写请求不再提供服务,直接返回错误(DEL 请求和部分特殊请求除外)
  • allkeys-lru:从所有 key 中使用 LRU 算法进行淘汰
  • volatile-lru:从设置了过期时间的 key 中使用 LRU 算法进行淘汰
  • allkeys-random:从所有 key 中随机淘汰数据
  • volatile-random:从设置了过期时间的 key 中随机淘汰
  • volatile-ttl:在设置了过期时间的 key 中,根据 key 的过期时间进行淘汰,越早过期的越优先被淘汰

五、Redis 高可用

5.1、Redis 主从复制

Redis 主从复制 可将 主节点 数据同步给 从节点,从节点此时有两个作用:

  1. 一旦 主节点宕机从节点 作为 主节点备份 可以随时顶上来。
  2. 扩展 主节点读能力,分担主节点读压力。

5.1.1、主从复制过程

主从复制过程大体可以分为3个阶段:连接建立阶段(即准备阶段)、数据同步阶段、命令传播阶段。

5.1.1.1、连接建立阶段

该阶段的主要作用是在主从节点之间建立连接,为数据同步做好准备。

  • 步骤1,保存主节点信息:

    • 从节点服务器内部维护了两个字段,即masterhost和masterport字段,用于存储主节点的ip和port信息。
    • 需要注意的是,slaveof****是异步命令,从节点完成主节点ip和port的保存后,向发送slaveof命令的客户端直接返回OK,实际的复制操作在这之后才开始进行。
  • 步骤2,建立socket连接,从节点每秒1次调用复制定时函数replicationCron(),如果发现了有主节点可以连接,便会根据主节点的ip和port,创建socket连接。如果连接成功,则:

    • 从节点:为该socket建立一个专门处理复制工作的文件事件处理器,负责后续的复制工作,如接收RDB文件、接收命令传播等。
    • 主节点:接收到从节点的socket连接后(即accept之后),为该socket创建相应的客户端状态,并将从节点看做是连接到主节点的一个客户端,后面的步骤会以从节点向主节点发送命令请求的形式来进行。
  • 步骤3,发送ping命令,从节点成为主节点的客户端之后,发送ping命令进行首次请求,目的是:检查socket连接是否可用,以及主节点当前是否能够处理请求。从节点发送ping命令后,可能出现3种情况:

    • 返回pong:说明socket连接正常,且主节点当前可以处理请求,复制过程继续。
    • 超时:一定时间后从节点仍未收到主节点的回复,说明socket连接不可用,则从节点断开socket连接,并重连。
    • 返回pong以外的结果:如果主节点返回其他结果,如正在处理超时运行的脚本,说明主节点当前无法处理命令,则从节点断开socket连接,并重连。
  • 步骤4,身份验证:

    • 如果从节点中设置了masterauth选项,则从节点需要向主节点进行身份验证;没有设置该选项,则不需要验证。从节点进行身份验证是通过向主节点发送auth命令进行的,auth命令的参数即为配置文件中的masterauth的值。
    • 如果主节点设置密码的状态,与从节点masterauth的状态一致(一致是指都存在,且密码相同,或者都不存在),则身份验证通过,复制过程继续;如果不一致,则从节点断开socket连接,并重连。
  • 步骤5,发送从节点端口信息:

    • 身份验证之后,从节点会向主节点发送其监听的端口号(前述例子中为6380),主节点将该信息保存到该从节点对应的客户端的slave_listening_port字段中;该端口信息除了在主节点中执行info Replication时显示以外,没有其他作用。
5.1.1.2、数据同步阶段

主从节点之间的连接建立以后,便可以开始进行数据同步,该阶段可以理解为从节点数据的初始化。具体执行的方式是:从节点向主节点发送psync命令(Redis2.8以前是sync命令),开始同步。

数据同步阶段是主从复制最核心的阶段,根据主从节点当前状态的不同,可以分为全量复制和部分复制。

在Redis2.8 以前,从节点向主节点发送sync命令请求同步数据,此时的同步方式是全量复制;在Redis2.8及以后,从节点可以发送psync命令请求同步数据,此时根据主从节点当前状态的不同,同步方式可能是全量复制或部分复制。

  1. 全量复制:用于初次复制或其他无法进行部分复制的情况,将主节点中的所有数据都发送给从节点,是一个非常重型的操作。
  2. 部分复制:用于网络中断等情况后的复制,只将中断期间主节点执行的写命令发送给从节点,与全量复制相比更加高效。需要注意的是,如果网络中断时间过长,导致主节点没有能够完整地保存中断期间执行的写命令,则无法进行部分复制,仍使用全量复制。

1)全量复制

Redis通过 psync 命令进行全量复制的过程如下:

  • 从节点判断无法进行部分复制,向主节点发送全量复制的请求;或从节点发送部分复制的请求,但主节点判断无法进行部分复制
  • 主节点收到全量复制的命令后,执行 bgsave,在后台生成 RDB 文件,并使用一个缓冲区(称为复制缓冲区)记录从现在开始执行的所有写命令
  • 主节点的 bgsave 执行完成后,将 RDB 文件发送给从节点;从节点首先清除自己的旧数据,然后载入接收的RDB文件,将数据库状态更新至主节点执行bgsave时的数据库状态
  • 主节点将前述复制缓冲区中的所有写命令发送给从节点,从节点执行这些写命令,将数据库状态更新至主节点的最新状态
  • 如果从节点开启了AOF,则会触发 bgrewriteaof 的执行,从而保证AOF文件更新至主节点的最新状态

通过全量复制的过程可以看出,全量复制是非常重型的操作:

  • 主节点通过 bgsave 命令 fork 子进程进行 RDB 持久化,该过程是非常消耗 CPU、内存(页表复制)、硬盘 IO 的;关于 bgsave 的性能问题
  • 主节点通过网络将 RDB 文件发送给从节点,对主从节点的带宽都会带来很大的消耗
  • 从节点清空老数据、载入新 RDB 文件的过程是阻塞的,无法响应客户端的命令;如果从节点执行 bgrewriteaof,也会带来额外的消耗

2)部分复制

由于全量复制在主节点数据量较大时效率太低,因此 Redis2.8 开始提供部分复制,用于处理网络中断时的数据同步。

部分复制的实现,依赖于三个重要的概念:

  • 复制偏移量

    • 主节点和从节点分别维护一个复制偏移量(offset),代表的是主节点向从节点传递的字节数;主节点每次向从节点传播N个字节数据时,主节点的 offset 增加N;从节点每次收到主节点传来的N个字节数据时,从节点的 offset 增加N。
    • offset 用于判断主从节点的数据库状态是否一致:如果二者 offset 相同,则一致;如果 offset 不同,则不一致,此时可以根据两个 offset 找出从节点缺少的那部分数据。例如,如果主节点的 offset 是1000,而从节点的 offset 是500,那么部分复制就需要将 offset 为501-1000的数据传递给从节点。
  • 复制积压缓冲区

    • 复制积压缓冲区是由主节点维护的、固定长度的、先进先出(FIFO)队列,默认大小1MB;当主节点开始有从节点时创建,其作用是备份主节点最近发送给从节点的数据。注意,无论主节点有一个还是多个从节点,都只需要一个复制积压缓冲区。
    • 在命令传播阶段,主节点除了将写命令发送给从节点,还会发送一份给复制积压缓冲区,作为写命令的备份;除了存储写命令,复制积压缓冲区中还存储了其中的每个字节对应的复制偏移量(offset)。由于复制积压缓冲区定长且是先进先出,所以它保存的是主节点最近执行的写命令;时间较早的写命令会被挤出缓冲区。
    • 由于该缓冲区长度固定且有限,因此可以备份的写命令也有限,当主从节点 offset 的差距过大超过缓冲区长度时,将无法执行部分复制,只能执行全量复制。反过来说,为了提高网络中断时部分复制执行的概率,可以根据需要增大复制积压缓冲区的大小(通过配置 repl-backlog-size);例如如果网络中断的平均时间是60s,而主节点平均每秒产生的写命令(特定协议格式)所占的字节数为100KB,则复制积压缓冲区的平均需求为 6MB,保险起见,可以设置为12MB,来保证绝大多数断线情况都可以使用部分复制。
    • 从节点将 offset 发送给主节点后,主节点根据 offset 和缓冲区大小决定能否执行部分复制:
      • 如果 offset 偏移量之后的数据,仍然都在复制积压缓冲区里,则执行部分复制;
      • 如果 offset 偏移量之后的数据已不在复制积压缓冲区中(数据已被挤出),则执行全量复制。
  • 服务器运行 ID(runid)

    • 每个 Redis 节点(无论主从),在启动时都会自动生成一个随机ID(每次启动都不一样),由40个随机的十六进制字符组成;runid 用来唯一识别一个 Redis 节点。通过 info Server 命令,可以查看节点的 runid
    • 主从节点初次复制时,主节点将自己的 runid 发送给从节点,从节点将这个 runid 保存起来;当断线重连时,从节点会将这个 runid 发送给主节点;主节点根据 runid 判断能否进行部分复制:
      • 如果从节点保存的 runid 与主节点现在的 runid 相同,说明主从节点之前同步过,主节点会继续尝试使用部分复制(到底能不能部分复制还要看 offset 和复制积压缓冲区的情况);
      • 如果从节点保存的 runid 与主节点现在的 runid 不同,说明从节点在断线前同步的 Redis 节点并不是当前的主节点,只能进行全量复制。
5.1.1.3、命令传播阶段

数据同步阶段完成后,主从节点进入命令传播阶段;在这个阶段主节点将自己执行的写命令发送给从节点,从节点接收命令并执行,从而保证主从节点数据的一致性。

在命令传播阶段,除了发送写命令,主从节点还维持着心跳机制:PING 和 REPLCONF ACK。

5.1.2、主从复制面临的问题

  • 一旦 主节点宕机从节点 晋升成 主节点,同时需要修改 应用方主节点地址,还需要命令所有 从节点复制 新的主节点,整个过程需要 人工干预
  • 主节点写能力 受到 单机的限制
  • 主节点存储能力 受到 单机的限制
  • 原生复制 的弊端在早期的版本中也会比较突出,比如:Redis 复制中断 后,从节点 会发起 psync。此时如果 同步不成功,则会进行 全量同步主库 执行 全量备份 的同时,可能会造成毫秒或秒级的 卡顿

5.2、Redis Sentinel 的高可用

5.2.1、Redis Sentinel的架构

当主节点出现故障时,Redis Sentinel 能自动完成故障发现和故障转移,并通知应用方,从而实现真正的高可用。Redis Sentine 是一个分布式架构,其中包含若干个 Sentinel 节点和 Redis 数据节点,每个 Sentinel 节点会对数据节点和其余Sentinel 节点进行监控,当它发现节点不可达时,会对节点做下线标识。如果被标识的是“主节点”,它还会和其他的Sentinel 节点进行“协商”,当大多数 Sentinel 节点都认为主节点不可达时,它们会选举一个 Sentinel 节点来完成自动故障转移的工作,同时会将这个变化实时通知给 Redis 应用方。整个过程是自动的,不需要人工干预,解决了 Redis 的高可用问题。

5.2.2、Redis Sentinel 功能

Sentinel 的主要功能包括 主节点存活检测主从运行情况检测自动故障转移failover)、主从切换RedisSentinel 最小配置是 一主一从

RedisSentinel 系统可以用来管理多个 Redis 服务器,该系统可以执行以下四个任务:

  • 监控

Sentinel 会不断的检查 主服务器从服务器 是否正常运行。

  • 通知

当被监控的某个 Redis 服务器出现问题,Sentinel 通过 API 脚本管理员 或者其他的 应用程序 发送通知。

  • 自动故障转移

主节点 不能正常工作时,Sentinel 会开始一次 自动的 故障转移操作,它会将与 失效主节点主从关系 的其中一个 从节点 升级为新的 主节点,并且将其他的 从节点 指向 新的主节点

  • 配置提供者

Redis Sentinel 模式下,客户端应用 在初始化时连接的是 Sentinel 节点集合,从中获取 主节点 的信息。

5.2.3、Redis Sentinel 核心知识

  • 哨兵至少需要3个实例,来保证自己的健壮性
  • 哨兵 + redis 主从的部署架构,是不会保证数据零丢失的,只能保证 redis 集群的高可用性
  • 对于哨兵 + redis 主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试和演练

5.2.4、Redis Sentinel 的工作原理

Redis Sentinel 通过三个定时监控任务完成对各个节点的发现和监控:

  1. 每秒钟,每个 Sentinel 向它所知的 主服务器、从服务器 以及其他 Sentinel 实例 发送一次 PING 命令。(这个定时任务是节点失败判定的重要依据,实现了对每个节点的监控)
    • 如果一个 实例instance)距离 最后一次 有效回复 PING 命令的时间超过 down-after-milliseconds 所指定的值,那么这个实例会被 Sentinel 标记为 主观下线
    • 如果一个 主服务器 被标记为 主观下线,那么正在 监视 这个 主服务器 的所有 Sentinel 节点,要以 每秒一次的频率确认 主服务器 的确进入了 主观下线 状态。
    • 如果一个 主服务器 被标记为 主观下线,并且有 足够数量Sentinel(至少要达到 配置文件 指定的数量)在指定的 时间范围 内同意这一判断,那么这个 主服务器 被标记为 客观下线
  1. 每隔10秒,每个 Sentinel 会向它已知的所有 主服务器 和 从服务器 发送一次 INFO 命令获取最新的拓扑结构。当一个 主服务器 被 Sentinel 标记为 客观下线 时,Sentinel 向 下线主服务器 的所有 从服务器 发送 INFO 命令的频率,会从 10 秒一次改为 每秒一次。
  1. 每隔2秒,每个 Sentinel 节点会向 Redis 数据节点的 sentinel:hello 频道上发送该 Senitnel 节点对于主节点的判断,以及当前 Sentinel 节点的信息,同时每个 Sentinel 节点也会订阅该频道,来了解其他 Sentinel 节点以及他们对主节点的判断。这个定时任务可以完成以下两个工作:
    • 发现新的 Sentinel 节点:通过订阅主节点的 Sentinel:hello 了解其他 Sentinel 节点信息。如果是新加入的 Sentinel 节点,将该 Sentinel 节点信息保存起来,并与该 Sentinel 节点创建连接
    • Sentinel 节点之间交换主节点状态,作为后面客观下线以及领导者选举的依据。

5.3、Redis 集群

Redis 的哨兵模式基本已经可以实现高可用,读写分离 ,但是在这种模式下每台 Redis 服务器都存储相同的数据,很浪费内存,所以在redis3.0上加入了 Cluster 集群模式,实现了 Redis 的分布式存储,对数据进行分片,也就是说每台 Redis 节点上存储不同的内容;

在 Redis 的每一个节点上,都有这么两个东西,一个是插槽(slot),它的的取值范围是:0-16383,还有一个就是cluster,可以理解为是一个集群管理的插件,类似哨兵。

当我们的存取的 Key到达的时候,Redis 会根据 crc16的算法对计算后得出一个结果,然后把结果和16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,通过这个值,去找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作。

当数据写入到对应的master节点后,这个数据会同步给这个master对应的所有slave节点。

为了保证高可用,redis-cluster集群引入了主从模式,一个主节点对应一个或者多个从节点。当其它主节点ping主节点master 1 时,如果半数以上的主节点与 master 1 通信超时,那么认为master 1宕机了,就会启用master 1的从节点slave 1,将slave 1变成主节点继续提供服务。

如果master 1和它的从节点slave 1都宕机了,整个集群就会进入fail状态,因为集群的slot映射不完整。如果集群超过半数以上的master挂掉,无论是否有slave,集群都会进入fail状态。

redis-cluster采用去中心化的思想,没有中心节点的说法,客户端与Redis节点直连,不需要中间代理层,客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。

5.4、总结

主从模式:master节点挂掉后,需要手动指定新的master,可用性不高,基本不用。

哨兵模式:master节点挂掉后,哨兵进程会主动选举新的master,可用性高,但是每个节点存储的数据是一样的,浪费内存空间。数据量不是很多,集群规模不是很大,需要自动容错容灾的时候使用。

集群模式:数据量比较大,QPS要求较高的时候使用。 Redis Cluster是Redis 3.0以后才正式推出,时间较晚,目前能证明在大规模生产环境下成功的案例还不是很多,需要时间检验。

六、参考来源

redis 系列之——高可用(主从、哨兵、集群)
深入剖析Redis系列(二) - Redis哨兵模式与高可用集群

你可能感兴趣的:(Redis 总结)