Redis 核心原理与实践 🚀
梳理 Redis 知识体系

持久化
Redis 持久化拥有以下三种方式:
- 快照方式(RDB, Redis Data Base)将某一个时刻的内存数据,以二进制的方式写入磁盘;
- 文件追加方式(AOF, Append Only File),记录所有的操作命令,并以文本的形式追加到文件中;
- 混合持久化方式,Redis 4.0 之后新增的方式,混合持久化是结合了 RDB 和 AOF 的优点,在写入的时候,先把当前的数据以 RDB 的形式写入文件的开头,再将后续的操作命令以 AOF 的格式存入文件,这样既能保证 Redis 重启时的速度,又能减低数据丢失的风险。
RDB 优点
- RDB 的内容为二进制的数据,占用内存更小,更紧凑,更适合做为备份文件;
- RDB 对灾难恢复非常有用,它是一个紧凑的文件,可以更快的传输到远程服务器进行 Redis 服务恢复;
- RDB 可以更大程度的提高 Redis 的运行速度,因为每次持久化时 Redis 主进程都会 fork() 一个子进程,进行数据持久化到磁盘,Redis 主进程并不会执行磁盘 I/O 等操作;
- 与 AOF 格式的文件相比,RDB 文件可以更快的重启。
RDB 缺点
- 因为 RDB 只能保存某个时间间隔的数据,如果中途 Redis 服务被意外终止了,则会丢失一段时间内的 Redis 数据;
- RDB 需要经常 fork() 才能使用子进程将其持久化在磁盘上。如果数据集很大,fork() 可能很耗时,并且如果数据集很大且 CPU 性能不佳,则可能导致 Redis 停止为客户端服务几毫秒甚至一秒钟。
AOF 优点
- AOF 持久化保存的数据更加完整,AOF 提供了三种保存策略:每次操作保存、每秒钟保存一次、跟随系统的持久化策略保存,其中每秒保存一次,从数据的安全性和性能两方面考虑是一个不错的选择,也是 AOF 默认的策略,即使发生了意外情况,最多只会丢失 1s 钟的数据;
- AOF 采用的是命令追加的写入方式,所以不会出现文件损坏的问题,即使由于某些意外原因,导致了最后操作的持久化数据写入了一半,也可以通过 redis-check-aof 工具轻松的修复;
- AOF 持久化文件,非常容易理解和解析,它是把所有 Redis 键值操作命令,以文件的方式存入了磁盘。即使不小心使用
flushall
命令删除了所有键值信息,只要使用 AOF 文件,删除最后的flushall
命令,重启 Redis 即可恢复之前误删的数据。
AOF 缺点
- 对于相同的数据集来说,AOF 文件要大于 RDB 文件;
- 在 Redis 负载比较高的情况下,RDB 比 AOF 性能更好;
- RDB 使用快照的形式来持久化整个 Redis 数据,而 AOF 只是将每次执行的命令追加到 AOF 文件中,因此从理论上说,RDB 比 AOF 更健壮。
graph subgraph 混合持久化 A[Redis 启动] --> B{开启AOF} B -- 是 --> C{文件开头为RDB格式} C -- 是 --> D[加载RDB] D --> E[加载AOF] E --> F[正常启动] C -- 否 --> E B -- 否 --> G{开启RDB} G -- 是 --> H{有RDB文件} H -- 是 --> I[加载RDB] I --> F H -- 否 --> F G -- 否 --> F end
混合持久化优点:
- 混合持久化结合了 RDB 和 AOF 持久化的优点,开头为 RDB 的格式,使得 Redis 可以更快的启动,同时结合 AOF 的优点,有减低了大量数据丢失的风险。
混合持久化缺点:
- AOF 文件中添加了 RDB 格式的内容,使得 AOF 文件的可读性变得很差;
- 兼容性差,如果开启混合持久化,那么此混合持久化 AOF 文件,就不能用在 Redis 4.0 之前版本了。
事务
multi
命令可以让客户端从非事务模式状态,变为事务模式状态,如果客户端已经是事务状态,再执行 multi
命令会报错,但不会终止客户端为事务的状态。
graph LR A[非事务状态] -->|multi 返回ok| C(事务状态) C -->|multi 返回 error| C
客户端进入事务状态之后,执行的所有常规 Redis 操作命令(非触发事务执行或放弃和导致入列异常的命令)会依次入列,命令入列成功后会返回 QUEUED,命令会按照先进先出(FIFO)的顺序出入列,也就是说事务会按照命令的入列顺序,从前往后依次执行。
graph LR A[客户端命令] --> B(事务状态) B --> |是| C(命令人列) C --> D(返回入列结果) B --> |否| E(执行命令) E --> F(返回执行结果)
执行事务的命令是 exec
,放弃事务的命令是 discard
graph LR A[客户端命令] --> B(事务状态) B --> |是| C(exec、discard) B --> |否| F C --> |否| D(命令人列) D --> E(返回入列结果) C --> |是| F(执行命令) F --> G(返回执行结果)
错误&回滚
执行时错误: 即使事务队列中某个命令在执行期间发生了错误,事务也会继续执行,直到事务队列中所有命令执行完成
入列错误不会导致事务结束: 重复执行
multi
会导致入列错误,但不会终止事务,最终查询的结果是事务执行成功了入列错误导致事务结束:
exec
时候不会运行事务不支持事务回滚的原因有以下两个:
- 他认为 Redis 事务的执行时,错误通常都是编程错误造成的,这种错误通常只会出现在开发环境中,而 很少会在实际的生产环境中出现,所以他认为没有必要为 Redis 开发事务回滚功能;
- 不支持事务回滚是因为这种复杂的功能和 Redis 追求的简单高效的设计主旨不符合。
这里不支持事务回滚,指的是 不支持运行时错误的事务回滚。
监控
watch
命令用于客户端并发情况下,为事务提供一个乐观锁(CAS,Check And Set),也就是可以用 watch
命令来监控一个或多个变量,如果在事务的过程中,某个 监控项被修改 了,那么 整个事务 就会 终止执行。
graph LR A[watch 命令] --> B(multi 开启事务) B --> C(命令入列) C --> D(exec 执行事务) D --> E(监控值发生改变) E --> |是| F(退出事务) E --> |否| G(执行并返回结果)
watch
命令只能在客户端开启事务之前执行,在事务中执行 watch
命令会引发错误,但不会造成整个事务失败,即使在事务的执行过程中,k 值被修改了,因为调用了 unwatch
命令,整个事务依然会顺利执行。
正常情况下 Redis 事务分为三个阶段:开启事务、命令入列、执行事务。Redis 事务并不支持运行时错误的事务回滚,但在某些入列错误,如 set key
或者是 watch
监控项被修改时,提供整个事务回滚的功能。
Pipeline
管道技术(Pipeline)是 客户端 提供的一种 批处理技术,用于一次处理多个 Redis 命令,从而提高整个交互的性能。
管道技术解决了多个命令集中请求时造成网络资源浪费的问题,加快了 Redis 的响应速度,让 Redis 拥有更高的运行速度。但要注意的一点是,管道技术本质上是客户端提供的功能,而非 Redis 服务器端的功能。
graph subgraph 普通命令模式 B -->|结果一| A A[客户端] -->|命令一| B(服务器端) end
graph subgraph 管道模式 B -->|结果一+结果二...| A A[客户端] -->|命令一+命令二...| B(服务器端) end
管道技术在使用时还需注意以下几个细节:
- 发送的命令数量不会被限制,但输入缓存区也就是命令的最大存储体积为 1GB,当发送的命令超过此限制时,命令不会被执行,并且会被 Redis 服务器端断开此连接;
- 如果管道的数据过多可能会导致客户端的等待时间过长,导致网络阻塞;
- 部分客户端自己本身也有缓存区大小的设置,如果管道命令没有没执行或者是执行不完整,可以排查此情况或减少管道内的命令重新尝试执行。
过期策略
Redis 中设置过期时间主要通过以下四种方式:
- expire key seconds:设置 key 在 n 秒后过期;
- pexpire key milliseconds:设置 key 在 n 毫秒后过期;
- expireat key timestamp:设置 key 在某个时间戳(精确到秒)之后过期;
- pexpireat key millisecondsTimestamp:设置 key 在某个时间戳(精确到毫秒)之后过期;
字符串中的过期操作
字符串中几个直接操作过期时间的方法,如下列表:
- set key value ex seconds:设置键值对的同时指定过期时间(精确到秒);
- set key value px milliseconds:设置键值对的同时指定过期时间(精确到毫秒);
- setex key seconds valule:设置键值对的同时指定过期时间(精确到秒)。
移除过期时间
- 使用命令:
persist key
可以移除键值的过期时间
Redis 中维护了一个字典,存储了所有设置了过期时间的键值(过期字典)
graph LR A(客户端) --请求--> C{在缓存字典中} C -->|是| D{当前时间小于过期时间} C -->|否| E(正常键值) D -->|是| E(正常键值) D -->|否| F(结束) E --> F(结束)
Redis 会删除已过期的键值,以此来减少 Redis 的空间占用,但因为 Redis 本身是单线的,如果因为删除操作而影响主业务的执行就得不偿失了,为此 Redis 需要制定多个(过期)删除策,常见的过期策略有以下三种:
- 定时删除
- 惰性删除
- 定期删除
定时删除
在设置键值过期时间时,创建一个定时事件,当过期时间到达时,由事件处理器自动执行键的删除操作。
- 优点:保证内存可以被尽快地释放。
- 缺点:在 Redis 高负载的情况下或有大量过期键需要同时处理时,会造成 Redis 服务器卡顿,影响主业务执行。
惰性删除
不主动删除过期键,每次从数据库获取键值时判断是否过期,如果过期则删除键值,并返回 null。
- 优点:因为每次访问时,才会判断过期键,所以此策略只会使用很少的系统资源。
- 缺点:系统占用空间删除不及时,导致空间利用率降低,造成了一定的空间浪费。
graph LR A[客户端] --请求--> B{检测是否过期} B -- 是 --> C[删除键值并返回 null] B -- 否 --> D[正常返回数据]
定期删除
每隔一段时间检查一次数据库,随机删除一些过期键。Redis 默认每秒进行 10 次过期扫描,此配置可通过 Redis 的配置文件 redis.conf
进行配置,配置键为 hz 它的默认值是 hz 10
。
需要注意的是:Redis 每次扫描并不是遍历过期字典中的所有键,而是采用随机抽取判断并删除过期键的形式执行的。
- 优点: 通过限制删除操作的时长和频率,来减少删除操作对 Redis 主业务的影响,同时也能删除一部分过期的数据减少了过期键对空间的无效占用。
- 缺点: 内存清理方面没有定时删除效果好,同时没有惰性删除使用的系统资源少。
graph LR A[开始扫描] --> B[从过期字典中获取20元素] B --> C[删除过期键] C --> D{判断过期键是否超过25%} D -- 否 --> E[结束] D -- 是 --> B
Redis 使用的是 惰性删除 加 定期删除 的过期策略。
内存淘汰
- Redis 过期策略指的是 Redis 使用哪种策略,来删除已经过期的键值对
- Redis 内存淘汰机制是指当 Redis 运行内存已经超过 Redis 设置的最大内存之后,采用什么策略来删除符合条件的键值对
graph LR A[客户端] -->|发送命令| B{服务器端检查\n maxmemory 是否大于0} B -- 是 --> D{检查运行内存\n是否大于 maxmemory} D -- 是 --> E[执行淘汰策略] B -- 否 --> F[结束] D -- 否 --> F[结束] E --> F
策略分类
早期版本的 Redis 有以下 6 种淘汰策略:
noeviction
:不淘汰任何数据,当内存不足时,新增操作会报错,Redis 默认内存淘汰策略;- **
allkeys-lru
**:淘汰整个键值中最久未使用的键值; - **
allkeys-random
**:随机淘汰任意键值; - **
volatile-lru
**:淘汰所有设置了过期时间的键值中最久未使用的键值; - **
volatile-random
**:随机淘汰设置了过期时间的任意键值; - **
volatile-ttl
**:优先淘汰更早过期的键值。
在 Redis 4.0 版本中又新增了 2 种淘汰策略:
- **
volatile-lfu
**:淘汰所有设置了过期时间的键值中,最少使用的键值; - **
allkeys-lfu
**:淘汰整个键值中最少使用的键值。
其中 allkeys-xxx
表示从所有的键值中淘汰数据,而 volatile-xxx
表示从设置了过期键的键值中淘汰数据。
策略修改
设置内存淘汰策略有两种方法,这两种方法各有利弊,需要使用者自己去权衡。
- 方式一:通过
config set maxmemory-policy
命令设置。它的优点是设置之后立即生效,不需要重启 Redis 服务,缺点是重启 Redis 之后,设置就会失效。 - 方式二:通过修改 Redis 配置文件修改,设置
maxmemory-policy
策略,它的优点是重启 Redis 服务后配置不会丢失,缺点是必须重启 Redis 服务,设置才能生效。
淘汰算法
从内存淘汰策略分类上,可以得知,除了随机删除和不删除之外,主要有两种淘汰算法:LRU 算法 和 LFU 算法。
LRU 算法
LRU 全称是 Least Recently Used 译为最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。
- LRU 算法需要基于链表结构,链表中的元素按照操作顺序从前往后排列,最新操作的键会被移动到表头,当需要内存淘汰时,只需要删除链表尾部的元素即可。
- Redis 使用的是一种近似 LRU 算法,目的是为了更好的节约内存,它的实现方式是给现有的数据结构添加一个额外的字段,用于记录此键值的最后一次访问时间,Redis 内存淘汰时,会使用随机采样的方式来淘汰数据,它是随机取 5 个值(此值可配置),然后淘汰最久没有使用的那个。
- LRU 算法有一个缺点,比如说很久没有使用的一个键值,如果最近被访问了一次,那么它就不会被淘汰,即使它是使用次数最少的缓存,那它也不会被淘汰,因此在 Redis 4.0 之后引入了 LFU 算法。
LFU 算法
LFU 全称是 Least Frequently Used 翻译为最不常用的,最不常用的算法是根据总访问次数来淘汰数据的,它的核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高”。
- LFU 解决了偶尔被访问一次之后,数据就不会被淘汰的问题,相比于 LRU 算法也更合理一些。
- 在 Redis 中 LFU 存储分为两部分,16 bit 的 ldt(last decrement time)和 8 bit 的 logc(logistic counter)。
- logc 是用来存储 访问频次,8 bit 能表示的最大整数值为 255,它的值越小表示使用频率越低,越容易淘汰;
- ldt 是用来存储上一次 logc 的 更新时间。
消息队列
发布订阅模式
发布订阅模式的三个命令:
subscribe channel
普通订阅publish channel message
消息推送psubscribe pattern
主题订阅
发布订阅模式存在以下两个缺点:
- 无法持久化保存消息,如果 Redis 服务器宕机或重启,那么所有的消息将会丢失;
- 发布订阅模式是“发后既忘”的工作模式,如果有订阅者离线重连之后不能消费之前的历史消息。
然而这些缺点在 Redis 5.0 添加了 Stream 类型之后会被彻底的解决。除了以上缺点外,发布订阅模式还有另一个需要注意问题:当消费端有一定的 消息积压 时,也就是 生产者发送的消息,消费者消费不过来 时,如果超过 32M 或者是 60s 内持续保持在 8M 以上,消费端会被强行断开,这个参数是在配置文件中设置的,默认值是 client-output-buffer-limit pubsub 32mb 8mb 60
。
List 和 ZSet 的实现
List 方式是实现消息队列最简单和最直接的方式,它主要是通过 lpush 和 rpop 存入和读取实现消息队列
graph LR;A[生产者]-->|发送数据 lpush|C[List];E[消费者]-->|拉取数据 rpop|C;
当队列中如果没有数据的情况下,无限循环会一直消耗系统的资源,SDK 中可以使用 brpop
替代 rpop
来解决这个问题,b 是 blocking 的缩写,表示阻塞读,也就是当队列没有数据时,它会进入休眠状态,当有数据进入队列之后,它才会“苏醒”过来执行读取任务,brpop()
方法的第一个参数是设置超时时间的,设置 0 表示一直阻塞。
List 优点:
- 消息可以被 持久化,借助 Redis 本身的持久化(AOF、RDB 或者是混合持久化),可以有效的保存数据;
- 消费者可以 积压消息,不会因为客户端的消息过多而被强行断开。
List 缺点:
- 消息 不能被重复消费,一个消息消费完就会被删除;
- 没有主题订阅 的功能。
ZSet 版消息队列相比于之前的两种方式,List 和发布订阅方式在实现上要复杂一些,但 ZSet 因为多了一个 score(分值)属性,从而使它具备更多的功能,例如可以用它来存储时间戳,以此来实现延迟消息队列等。它的实现思路和 List 相同也是利用 zadd
和 zrangebyscore
来实现存入和读取。
ZSet 优点:
- 支持消息持久化;
- 相比于 List 查询更方便,ZSet 可以利用 score 属性很方便的完成检索,而 List 则需要遍历整个元素才能检索到某个值。
ZSet 缺点:
- ZSet 不能存储相同元素的值,也就是如果有消息是重复的,那么只能插入一条信息在有序集合中;
- ZSet 是根据 score 值排序的,不能像 List 一样,按照插入顺序来排序;
- ZSet 没有向 List 的 brpop 那样的阻塞弹出的功能。
终极方案 - Stream
在 Redis 5.0 Stream 没出来之前,消息队列的实现方式都有着各自的缺陷,例如:
- 发布订阅模式 PubSub,不能持久化也就无法可靠的保存消息,并且对于离线重连的客户端不能读取历史消息的缺陷;
- 列表实现消息队列的方式不能重复消费,一个消息消费完就会被删除;
- 有序集合消息队列的实现方式不能存储相同 value 的消息,并且不能阻塞读取消息。
并且以上三种方式在实现消息队列时,只能存储单 value 值,也就是如果你要存储一个对象的情况下,必须先序列化成 JSON 字符串,在读取之后还要反序列化成对象才行,这也给用户的使用带来的不便。
Redis 5.0 推出了 Stream 类型用于实现消息队列,它借鉴了 Kafka 的设计思路,它支持消息的持久化和消息轨迹的消费,支持 ack 确认消息的模式,让消息队列更加的稳定和可靠。
基本使用
- xadd 添加消息;
- xlen 查询消息长度;
- xdel 根据消息 ID 删除消息;
- del 删除整个 Stream;
- xrange 读取区间消息;
- xread 读取某个消息之后的消息;
- xgroup 创建消费者群组;
分布式锁
锁是一种常用的并发控制机制,用于保证一项资源在任何时候只能被一个线程使用,如果其他线程也要使用同样的资源,必须排队等待上一个线程使用完。
graph LR A[线程 1] -->|使用| B(锁🔒) B <-->D(资源) C[线程 2] -->|排队等待| B
上面说的锁指的是程序级别的锁,放到分布式环境下就不适用了,这个时候就要使用分布式锁。分布式锁比较好理解就是用于分布式环境下并发控制的一种机制,用于控制某个资源在同一时刻只能被一个应用所使用。
graph LR A[用户] -->|请求| D C[用户] -->|请求| B subgraph 分布式系统 B[应用 1] D[应用 2] end B -->|使用| E(分布式锁🔒) D -->|排队等待| E E <--> F(资源)
分布式锁的实现
分布式锁比较常见的实现方式有三种:
- Memcached 实现的分布式锁:使用 add 命令,添加成功的情况下,表示创建分布式锁成功。
- ZooKeeper 实现的分布式锁:使用 ZooKeeper 顺序临时节点来实现分布式锁。
- Redis 实现的分布式锁。
Redis 分布式锁的实现思路是使用 setnx
(set if not exists),如果创建成功则表明此锁创建成功,否则代表这个锁已经被占用创建失败,释放锁使用 del
即可,如果在锁未被删除之前,其他程序再来执行 setnx
是不会创建成功的。
死锁的问题
可以使用 expire key seconds
设置超时时间,即使出现程序中途崩溃的情况,超过超时时间之后,这个锁也会解除,不会出现死锁的情况了。
1 | 127.0.0.1:6379> setnx lock true |
但这样依然会有问题,因为命令 setnx
和 expire
处理是一前一后非原子性的,因此如果在它们执行之间,出现断电和 Redis 异常退出的情况,因为超时时间未设置,依然会造成死锁。
可以使用带参数的 set
命令来设置分布式锁,并设置超时时间了,而且 set
命令可以保证原子性
1 | 127.0.0.1:6379> set lock true ex 30 nx |
其中, ex n
为设置超时时间,nx 为元素非空判断,用来判断是否能正常使用锁。
执行超时问题
如果设置锁的最大超时时间是 30s,但业务处理使用了 35s,这就会导致原有的业务还未执行完成,锁就被释放了,新的程序和旧程序一起操作就会带来线程安全的问题。
graph LR A[应用 1] -->|同时拥有锁\n使用锁超过 30s| B[分布式锁] C[应用 2] -->|30s 后锁自动释放\n此锁被应用2获得| B
执行超时的问题处理带来线程安全问题之外,还引发了另一个问题:锁被误删。
graph LR subgraph 30s 时 A[应用 1] -->|使用锁超过 30s| B[分布式锁] C[应用 2] -->|30s 后成功创建锁| B end subgraph 35s 时 X[应用 1] -->|35s 删除锁| Y[分布式锁] Z[应用 2] --> Y end
锁被误删的解决方案是在使用 set 命令创建锁时,给 value 值设置一个归属人标识,例如给应用关联一个 UUID,每次在删除之前先要判断 UUID 是不是属于当前的线程,如果属于在删除,这样就避免了锁被误删的问题。
如果是在代码中执行删除,不能使用先判断再删除的方法,因为判断代码和删除代码不具备原子性,因此也不能这样使用,这个时候可以使用 Lua 脚本 来执行判断和删除的操作,因为多条 Lua 命令可以保证 原子性。
执行超时问题的解决:
- 把执行比较耗时的任务不要放到加锁的方法内,锁内的方法尽量控制执行时长;
- 把最大超时时间可以适当的设置长一点,正常情况下锁用完之后会被手动的删除掉,因此适当的把最大超时时间设置的长一点,也是可行的。
- 续约机制。
延迟队列
延迟队列是指把当前要做的事情,往后推迟一段时间再做。
使用场景
延迟队列的常见使用场景有以下几种:
- 超过 30 分钟未支付的订单,将会被取消
- 外卖商家超过 5 分钟未接单的订单,将会被取消
- 在平台注册但 30 天内未登录的用户,发短信提醒
等类似的应用场景,都可以使用延迟队列来实现。
实现方式
目前市面上延迟队列的实现方式基本分为三类
- 第一类是通过程序的方式实现,例如 JDK 自带的延迟队列 DelayQueue
- 优点:开发比较方便,可以直接在代码中使用,代码实现比较简单
- 缺点:不支持持久化保存,不支持分布式系统
- 第二类是通过 MQ 框架来实现,例如 RabbitMQ 可以通过 rabbitmq-delayed-message-exchange 插件来实现延迟队列
- 优点:支持分布式,支持持久化
- 缺点:框架比较重,需要搭建和配置 MQ
- 第三类就是通过 Redis 的方式来实现延迟队列
Redis 是通过有序集合(ZSet)的方式来实现延迟消息队列的,ZSet 有一个 Score 属性可以用来存储延迟执行的时间。
实现
第一种是利用
zrangebyscore
查询符合条件的所有待处理任务,循环执行队列任务。第二种实现方式是每次查询最早的一条消息,判断这条信息的执行时间是否小于等于此刻的时间,如果是则执行此任务,否则继续循环检测。
优点
- 灵活方便,Redis 是互联网公司的标配,无序额外搭建相关环境;
- 可进行消息持久化,大大提高了延迟队列的可靠性;
- 分布式支持,不像 JDK 自身的 DelayQueue;
- 高可用性,利用 Redis 本身高可用方案,增加了系统健壮性。
缺点
- 需要使用 无限循环的方式 来执行任务检查,会消耗少量的系统资源。
定时任务
键空间通知
默认情况下 Redis 服务器端是不开启键空间通知的,需要手动开启,其中 Ex 表示 开启键事件通知里面的 key 过期事件。
- redis-cli:
config set notify-keyspace-events Ex
- redis.conf:
notify-keyspace-events Ex
更多配置项说明如下:
- K:键空间通知,所有通知以
__keyspace@<db>__
为前缀 - E:键事件通知,所有通知以
__keyevent@<db>__
为前缀 - g:DEL、EXPIRE、RENAME 等类型无关的通用命令的通知
- $:字符串命令的通知
- l:列表命令的通知
- s:集合命令的通知
- h:哈希命令的通知
- z:有序集合命令的通知
- x:过期事件,每当有过期键被删除时发送
- e:驱逐(evict)事件,每当有键因为 maxmemory 政策而被删除时发送
- A:参数 g$lshzxe 的别名
以上配置项可以自由组合,例如订阅列表事件就是 El,但需要注意的是,如果 notify-keyspace-event 的值设置为空,则表示不开启任何通知,有值则表示开启通知。
功能实现
要实现定时任务需要使用 Pub/Sub 订阅者和发布者的功能,使用订阅者订阅元素的过期事件,然后再执行固定的任务,这就是定时任务的实现思路。
使用 redis-cli 开启一个客户端,监听 __keyevent@0__:expired
键过期事件,此监听值 __keyevent@0__:expired
为固定的写法,其中 0 表示第一个数据库,Redis 中一共有 16 个数据,默认使用的是第 0 个,建议新开一个非 0 的数据库专门用来实现定时任务,这样就可以避免很多无效的事件监听。
命令监听如下:
1 | 127.0.0.1:6379> psubscribe __keyevent@0__:expired |
此时开启另一个客户端,添加两条测试数据试试,命令如下:
1 | 127.0.0.1:6379> set key value ex 3 |
等过去 3 秒钟之后,监听结果如下:
1 | 127.0.0.1:6379> psubscribe __keyevent@0__:expired |
已经成功的接收到两条过期信息了
通过开启 Keyspace Notifications 和 Pub/Sub 消息订阅的方式,可以拿到每个键值过期的事件,利用这个机制实现了给每个人开启一个定时任务的功能,过期事件中可以获取到过期键的 key 值,在 key 值中可以存储每个用户的 id,例如“user_1001”的方式,其中数字部分表示用户的编号,通过此编号就可以完成给对应人发送消息通知的功能。
主从同步
主从同步(主从复制)是 Redis 高可用服务的基石,也是多机运行中最基础的一个。把主要存储数据的节点叫做主节点 (master),把其他通过复制主节点数据的副本节点叫做从节点 (slave)。
graph TD A[主节点] -->|复制| B[从节点] A -->|复制| C[从节点] A -->|复制| D[从节点]
在 Redis 中一个主节点可以拥有多个从节点,一个从节点也可以是其他服务器的主节点。
graph TD A[主节点] -->|复制| B[从节点] A -->|复制| C[从节点] A -->|复制| D[从节点] D -->|复制| F[从节点] D -->|复制| G[从节点]
主从同步的优点
- 性能方面:有了主从同步之后,可以把查询任务分配给从服务器,用主服务器来执行写操作,这样极大的提高了程序运行的效率,把所有压力分摊到各个服务器了;
- 高可用:当有了主从同步之后,当主服务器节点宕机之后,可以很迅速的把从节点提升为主节点,为 Redis 服务器的宕机恢复节省了宝贵的时间;
- 防止数据丢失:当主服务器磁盘坏掉之后,其他从服务器还保留着相关的数据,不至于数据全部丢失。
开启主从同步
- 使用
replicaof host port
命令,把自己设置为目标 IP 的从服务器 - 如果主服务设置了密码,需要在从服务器输入主服务器的密码,使用
config set masterauth 主服务密码
命令的方式 - 在执行完 replicaof 命令之后,从服务器的数据会被清空,主服务会把它的数据副本同步给从服务器
- 启动时可以使用命令
redis-server --port 6380 --replicaof 127.0.0.1 6379
将自己设置成目标服务器的从服务器
数据同步
- 完整数据同步
- 当有新的从服务器连接时,为了保障多个数据库的一致性,主服务器会执行一次 bgsave 命令生成一个 RDB 文件,然后再以 Socket 的方式发送给从服务器,从服务器收到 RDB 文件之后再把所有的数据加载到自己的程序中,就完成了一次全量的数据同步。
- 部分数据同步
- Redis 2.8 的优化方法是当从服务离线之后,主服务器会把离线之后的写入命令,存储在一个特定大小的队列中,队列是可以保证先进先出的执行顺序的,当从服务器重写恢复上线之后,主服务会判断离线这段时间内的命令是否还在队列中,如果在就直接把队列中的数据发送给从服务器,这样就避免了完整同步的资源浪费。
- 存储离线命令的队列大小默认是 1MB,使用者可以自行修改队列大小的配置项 repl-backlog-size。
- 无盘数据同步
- 如果主服务器是非固态硬盘的时候,系统的 I/O 操作是非常高的,为了缓解这个问题,Redis 2.8.18 新增了无盘复制功能,无盘复制功能不会在本地创建 RDB 文件,而是会派生出一个子进程,然后由子进程通过 Socket 的方式,直接将 RDB 文件写入到从服务器,这样主服务器就可以在不创建 RDB 文件的情况下,完成与从服务器的数据同步。
- 要使用无盘复制功能,只需把配置项 repl-diskless-sync 的值设置为 yes 即可,它默认配置值为 no。
关闭主从同步
可以使用 replicaof no one
命令来停止从服务器的复制:
1 | 127.0.0.1:6379> role #查询当前角色 |
执行了 replicaof no one
命令之后,自己就从服务器变成主服务器了,服务器类型的转换并不会影响数据,这台服务器的数据将会被保留。
注意事项
- 数据一致性问题
- 当从服务器已经完成和主服务的数据同步之后,再新增的命令会以异步的方式发送至从服务器,在这个过程中主从同步会有短暂的数据不一致,如在这个异步同步发生之前主服务器宕机了,会造成数据不一致。
- 从服务器只读性
- 默认在情况下,处于复制模式的主服务器既可以执行写操作也可以执行读操作,而从服务器则只能执行读操作。
- 可以在从服务器上执行
config set replica-read-only no
命令,使从服务器开启写模式,但需要注意以下几点:- 在从服务器上写的数据不会同步到主服务器;
- 当键值相同时主服务器上的数据可以覆盖从服务器;
- 在进行完整数据同步时,从服务器数据会被清空。
- 复制命令的变化
- Redis 5.0 之前使用的复制命令是 slaveof,在 Redis 5.0 之后复制命令才被改为 replicaof,在高版本(Redis 5+)中应该尽量使用 replicaof,因为 slaveof 命令可能会被随时废弃掉。
哨兵模式
主从复制模式,它是属于 Redis 多机运行的基础,但这种模式本身存在一个致命的问题,当主节点奔溃之后,需要人工干预才能恢复 Redis 的正常使用。需要一个自动的工具——Redis Sentinel(哨兵模式)来把手动的过程变成自动的,让 Redis 拥有自动容灾恢复(failover)的能力。
graph TD A[哨兵] -->|监视| B[主节点] subgraph B -->|复制| C[从节点] B -->|复制| D[从节点] B -->|复制| E[从节点] end
Redis Sentinel 搭建
edis Sentinel 的最小分配单位是一主一从,需要使用命令 ./src/redis-sentinel sentinel.conf
来启动 Sentinel,可以看出在启动它时必须设置一个 sentinel.conf 文件,这个配置文件中必须包含监听的 主节点信息:
1 | sentinel monitor mymaster 127.0.0.1 6379 1 |
其中:
- master-name 表示给监视的主节点起一个名称;
- ip 表示主节点的 IP;
- port 表示主节点的端口;
- quorum 表示确认主节点下线的 Sentinel 数量,如果 quorum 设置为 1 表示只要有一台 Sentinel 判断它下线了,就可以确认它真的下线了。
- 如果主节点 Redis 服务器有密码,还必须在 sentinel.conf 中添加主节点的密码
Sentinel 只需配置监听主节点的信息,它会自动监听对应的从节点。
启动 Sentinel 集群
生产环境不会只启动一台 Sentinel,因为如果启动一台 Sentinel 假如它不幸宕机的话,就不能提供自动容灾的服务了,不符高可用的宗旨,所以会在不同的物理机上启动多个 Sentinel 来组成 Sentinel 集群,来保证 Redis 服务的高可用。
启动 Sentinel 集群的方法和上面启动单台的方式一样,只需要把多个 Sentinel 监听到一个主服务器节点,那么多个 Sentinel 就会自动发现彼此,并组成一个 Sentinel 集群。
graph TD subgraph A[哨兵] -->|监视| B[主节点] F[哨兵] -->|监视| B[主节点] G[哨兵] -->|监视| B[主节点] end subgraph B -->|复制| C[从节点] B -->|复制| D[从节点] B -->|复制| E[从节点] end
Sentinel 可以监视多台主节点,而不是只能监视一台服务器。
想要监视多台主节点只需要在配置文件中设置多个
sentinel monitor master-name ip port quorum
即可,通过 master-name 来区分不同的主节点
一般情况下 Sentinel 集群的数量取大于 1 的奇数,例如 3、5、7、9,而 quorum 的配置要根据 Sentinel 的数量来发生变化,例如 Sentinel 是 3 台,那么对应的 quorum 最好是 2,如果 Sentinel 是 5 台,那么 quorum 最好是 3,它表示当有 3 台 Sentinel 都确认主节点下线了,就可以确定主节点真的下线了。
与 quorum 参数相关的有两个概念:主观下线 和 客观下线。
当 Sentinel 集群中,有一个 Sentinel 认为主服务器已经下线时,它会将这个主服务器标记为主观下线(Subjectively Down,SDOWN),然后询问集群中的其他 Sentinel,是否也认为该服务器已下线,当同意主服务器已下线的 Sentinel 数量达到 quorum 参数所指定的数量时,Sentinel 就会将相应的主服务器标记为客观下线(Objectively down,ODOWN),然后开始对其进行故障转移。
主服务竞选规则
- 新主节点竞选优先级设置
- redis.conf 中的 replica-priority 选项来设置竞选新主节点的优先级,它的默认值是 100,它的最大值也是 100,这个值越小它的权重就越高,例如从节点 A 的 replica-priority 值为 100,从节点 B 的值为 50,从节点 C 的值为 5,那么在竞选时从节点 C 会作为新的主节点。
- 新主节点竞选规则
- 新主节点的竞选会排除不符合条件的从节点,然后再剩余的从节点按照优先级来挑选
- 存在以下条件的从节点会被排除:
- 排除所有已经下线以及长时间没有回复心跳检测的疑似已下线从服务器;
- 排除所有长时间没有与主服务器通信,数据状态过时的从服务器;
- 排除所有优先级(replica-priority)为 0 的服务器。
- 符合条件的从节点竞选顺序:
- 优先级最高的从节点将会作为新主节点;
- 优先级相等则判断复制偏移量,偏移量最大的从节点获胜;
- 如果以上两个条件都相同,选择 Redis 运行时随机生成 ID 最小那个为新的主服务器。
- 旧主节点恢复上线
- 如果之前的旧主节点恢复上线,会作为从节点运行在主从服务器模式中
哨兵工作原理
首先每个 Sentinel 会以 每秒钟 1 次的频率,向已知的 主服务器、从服务器和以及其他 Sentinel 实例,发送一个 PING 命令。
- 如果最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 所配置的值(默认 30s),那么这个实例会被 Sentinel 标记为主观下线。
- 如果一个主服务器被标记为主观下线,那么正在监视这个主服务器的所有 Sentinel 节点,要以每秒 1 次的频率确认主服务器的确进入了主观下线状态。
- 如果有足够数量(quorum 配置值)的 Sentinel 在指定的时间范围内同意这一判断,那么这个主服务器被标记为客观下线。此时所有的 Sentinel 会按照规则协商自动选出新的主节点。
一个有效的 PING 回复可以是:+PONG、-LOADING 或者 -MASTERDOWN。如果返回值非以上三种回复,或者在指定时间内没有回复 PING 命令, 那么 Sentinel 认为服务器返回的回复无效(non-valid)。
Sentinel 命令操作
连接到 Sentinel 服务器,和连接 Redis 服务相同,我们可以使用 redis-cli 来连接 Sentinel。
通过 Sentinel 连接信息获取相关 Redis 客户端,再进行相关 Redis 操作,这样 Sentinel 就会帮我们做容灾恢复,就不用担心操作某一个 Redis 服务器端,因为服务器挂了之后就会导致程序不可用了。
集群模式
Redis Cluster 是 Redis 3.0 版本推出的 Redis 集群方案,它将数据分布在不同的服务区上,以此来降低系统对单主节点的依赖,并且可以大大的提高 Redis 服务的读写性能。
Redis 将所有的数据分为 16384 个 slots(槽),每个节点负责其中的一部分槽位,当有 Redis 客户端连接集群时,会得到一份集群的槽位配置信息,这样它就可以直接把请求命令发送给对应的节点进行处理。
Redis Cluster 是 无代理去中心化的运行模式,客户端发送的绝大数命令会直接交给相关节点执行,这样大部分情况请求命令无需转发,或仅转发一次的情况下就能完成请求与响应,所以集群单个节点的性能与单机 Redis 服务器的性能是非常接近的,因此在理论情况下,当水平扩展一倍的主节点就相当于请求处理的性能也提高了一倍,所以 Redis Cluster 的性能是非常高的。
graph LR subgraph 集群 A[主节点] --> B(从节点) A --> C(从节点) A --> D(从节点) E[主节点] --> F(从节点) E --> G(从节点) E --> H(从节点) end
搭建 Redis Cluster
Redis Cluster 的搭建方式有两种:
- 一种是使用 Redis 源码中提供的 create-cluster 工具快速的搭建 Redis 集群环境
- 另一种是配置文件的方式手动创建 Redis 集群环境
create-cluster 搭建的方式虽然速度很快,但是该方式搭建的集群主从节点数量固定以及槽位分配模式固定,并且安装在同一台服务器上,所以只能用于测试环境。
在实际生产环境中需要使用手动添加配置的方式搭建 Redis 集群,需要修改每个节点内的 redis.conf 文件,设置 cluster-enabled yes
表示开启集群模式,redis.conf 配置好之后,就可以启动所有的节点。
但这些节点都在各自的集群之内并未互联互通,因此接下来需要把这些节点串连成一个集群,并为它们指定对应的槽位,执行命令如下:
1 | redis-cli --cluster create 127.0.0.1:30001 127.0.0.1:30002 127.0.0.1:30003 127.0.0.1:30004 127.0.0.1:30005 127.0.0.1:30006 --cluster-replicas 1 |
其中 create 后面跟多个节点,表示把这些节点作为整个集群的节点,而 cluster-replicas 表示给集群中的主节点指定从节点的数量,1 表示为每个主节点设置一个从节点。在执行了 create 命令之后,系统会为我们指定节点的角色和槽位分配计划。
使用 redis-cli 连接并测试一下集群的运行状态:
1 | redis-cli -c -p 30001 # 连接到集群 |
动态增删节点
- 增加主节点
cluster meet [ip:port]
/redis-cli --cluster add-node [添加节点ip:port] [集群某节点ip:port]
- 添加从节点
cluster replicate [nodeId]
- 删除节点
cluster forget [nodeId]
- 重新分片,对槽位(slots)进行重新分配
redis-cli --cluster reshard [ip:port]
槽位定位算法
Redis 集群总共的槽位数是 16384 个,每一个主节点负责维护一部分槽以及槽所映射的键值数据,Redis 集群默认会对要存储的 key 值使用 CRC16 算法进行 hash 得到一个整数值,然后用这个整数值对 16384 进行取模来得到具体槽位,公式为:
slot = CRC16(key) % 16383
在 Redis 集群负载不均衡的情况下,我们可以使用 rebalance
命令重新分配各个节点负责的槽数量,从而使得各个节点的负载压力趋于平衡,从而提高 Redis 集群的整体运行效率。
故障
故障发现
故障发现里面有两个重要的概念:疑似下线(PFAIL-Possibly Fail)和确定下线(Fail),和哨兵模式里面的主观下线和客观下线的概念比较类似。
集群中的健康监测是通过定期向集群中的其他节点发送 PING 信息来确认的,如果发送 PING 消息的节点在规定时间内,没有收到返回的 PONG 消息,那么对方节点就会被标记为疑似下线。
一个节点发现某个节点疑似下线,它会将这条信息 向整个集群广播,其它节点就会收到这个消息,并且通过 PING 的方式监测某节点是否真的下线了。如果一个节点收到某个节点疑似下线的数量超过集群数量的一半以上,就可以标记该节点为确定下线状态,然后向整个集群广播,强迫其它节点也接收该节点已经下线的事实,并立即对该失联节点进行主从切换。
故障转移
当一个节点被集群标识为确认下线之后就可以执行故障转移了,故障转移的执行流程如下:
从下线的主节点的所有从节点中,选择一个从节点;
从节点会执行 SLAVEOF NO ONE 命令,关闭这个从节点的复制功能,并从从节点转变回主节点,原来同步所得的数据集不会被丢弃;
新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己;
新的主节点向集群广播一条 PONG 消息,这条 PONG 消息是让集群中的其他节点知道此节点已经由从节点变成了主节点,并且这个主节点已经接管了原本由已下线节点负责处理的槽位信息;
新的主节点开始处理相关的命令请求,此故障转移过程完成。
新主节点选举原则
新主节点选举的方法是这样的:
- 集群的纪元(epoch)是一个自增计数器,初始值为 0;
- 而每个主节点都有一次投票的机会,主节点会把这一票投给第一个要求投票的从节点;
- 当从节点发现自己正在复制的主节点确认下线之后,就会向集群广播一条消息,要求所有有投票权的主节点给此从节点投票;
- 如果有投票权的主节点还没有给其他人投票的情况下,它会向第一个要求投票的从节点发送一条消息,表示把这一票投给这个从节点;
- 当从节点收到投票数量大于集群数量的半数以上时,这个从节点就会当选为新的主节点。
布隆过滤器
HyperLogLog 可以用来做基数统计,但它没提供判断一个值是否存在的查询方法,如果使用传统的方式,例如 SQL 中的传统查询,因为数据量太多,查询效率又低有占用系统的资源,因此需要一个优秀的算法和功能来实现这个需求 —— 布隆过滤器。
使用
在 Redis 中不能直接使用布隆过滤器,但可以通过 Redis 4.0 版本之后提供的 modules(扩展模块)的方式引入。
布隆过滤器的命令主要包含以下几个:
- bf.add:添加元素
- bf.exists:判断某个元素是否存在
- bf.madd:添加多个元素
- bf.mexists:判断多个元素是否存在
- bf.reserve:设置布隆过滤器的准确率
准确率 bf.reserve 的使用必须在元素刚开始执行,否则会报错,它有三个参数:key、error_rate 和 initial_size
- error_rate:允许布隆过滤器的错误率,这个 值越低过滤器占用空间也就越大,以为此值决定了 位数组的大小,位数组是用来存储结果的,它的空间占用的越大(存储的信息越多),错误率就越低,它的默认值是 0.01。
- initial_size:布隆过滤器存储的元素大小,实际存储的值大于此值,准确率就会降低,它的默认值是 100。
原理
Redis 布隆过滤器的实现,依靠的是它数据结构中的一个位数组,每次存储键值的时候,不是直接把数据存储在数据结构中,因为这样太占空间了,它是利用几个不同的无偏哈希函数,把此元素的 hash 值均匀的存储在位数组中,也就是说,每次添加时会通过几个无偏哈希函数算出它的位置,把这些位置设置成 1 就完成了添加操作。
当进行元素判断时,查询此元素的几个哈希位置上的值是否为 1,如果全部为 1,则表示此值存在,如果有一个值为 0,则表示不存在。因为此位置是通过 hash 计算得来的,所以即使这个位置是 1,并不能确定是那个元素把它标识为 1 的,因此 布隆过滤器查询此值存在时,此值不一定存在,但查询此值不存在时,此值一定不存在。
并且当位数组存储值比较稀疏的时候,查询的准确率越高,而当位数组存储的值越来越多时,误差也会增大。
graph LR subgraph 数据存储 A0[0] ----> A1[1] --> A2[0] ----> A3[1] ----> A4[1] ----> A5[0] --> A6[0] ----> A7[1] --> A8[0] end key(Key) --> A1 key --> A3 key --> A4 key2(Key2) --> A4 key2 --> A5 key2 --> A7
使用场景
它的经典使用场景包括以下几个:
- 垃圾邮件过滤
- 爬虫里的 URL 去重
- 判断一个元素在亿级数据中是否存在
布隆过滤器在数据库领域的使用也比较广泛,例如:HBase、Cassandra、LevelDB、RocksDB 内部都有使用布隆过滤器。
RediSearch
RediSearch 是一个高性能的全文搜索引擎,它可以作为一个 Redis Module(扩展模块)运行在 Redis 服务器上。
RediSearch 主要特性如下:
- 基于文档的多个字段全文索引
- 高性能增量索引
- 文档排序(由用户在索引时手动提供)
- 在子查询之间使用 AND 或 NOT 操作符的复杂布尔查询
- 可选的查询子句
- 基于前缀的搜索
- 支持字段权重设置
- 自动完成建议(带有模糊前缀建议)
- 精确的短语搜索
- 在许多语言中基于词干分析的查询扩展
- 支持用于查询扩展和评分的自定义函数
- 将搜索限制到特定的文档字段
- 数字过滤器和范围
- 使用 Redis 自己的地理命令进行地理过滤
- Unicode 支持(需要 UTF-8 字符集)
- 检索完整的文档内容或只是 ID 的检索
- 支持文档删除和更新与索引垃圾收集
- 支持部分更新和条件文档更新
性能测试
为什么需要性能测试
性能测试的使用场景有很多,例如以下几个:
- 技术选型,比如测试 Memcached 和 Redis;
- 对比单机 Redis 和集群 Redis 的吞吐量;
- 评估不同类型的存储性能,例如集合和有序集合;
- 对比开启持久化和关闭持久化的吞吐量;
- 对比调优和未调优的吞吐量;
- 对比不同 Redis 版本的吞吐量,作为是否升级的一个参考标准。
等等,诸如此类的情况,都需要进行性能测试。
性能测试的几种方式
目前比较主流的性能测试分为两种:
- 编写代码模拟并发进行性能测试;
- 使用 redis-benchmark 进行测试。
因为自己编写代码进行性能测试的方式不够灵活,且很难短时间内模拟大量的并发数,所有作者并不建议使用这种方式。幸运的是 Redis 本身给提供了性能测试工具 redis-benchmark(Redis 基准测试)。
慢查询
Redis 慢查询作用和 MySQL 慢查询作用类似,都是为了查询出不合理的执行命令,然后让开发人员和运维人员一起来规避这些耗时的命令,从而让服务器更加高效和健康的运行。
如何进行慢查询
Redis 慢查询重要的配置项:
slowlog-log-slower-than:用于设置慢查询的评定时间,也就是说超过此配置项的命令,将会被当成慢操作记录在慢查询日志中,它执行单位是微秒(1 秒等于 1000000 微秒);
slowlog-max-len:用来配置慢查询日志的最大记录数。
slowlog-log-slower-than 和 slowlog-max-len 可以通过
config set xxx
的模式来修改,例如config set slowlog-max-len 200
设置慢查询最大记录数为 200 条。使用
slowlog show
来查询慢日志,当慢查询日志超过设定的最大存储条数之后,会把最早的执行命令依次舍弃。
慢查询其他相关命令
- 查询指定条数慢日志
slowlog get n
- 获取慢查询队列长度
slowlog len
- 清空慢查询日志
slowlog reset
性能优化
缩短键值对的存储长度
键值对的长度是和性能成反比的,做一组写入数据的性能测试,执行结果如下:
数据量 | key 大小 | value 大小 | string: set 平均耗时 | hash: hset 平均耗时 |
---|---|---|---|---|
100w | 20byte | 512byte | 1.13 微秒 | 10.28 微秒 |
100w | 20byte | 200byte | 0.74 微秒 | 8.08 微秒 |
100w | 20byte | 100byte | 0.65 微秒 | 7.92 微秒 |
100w | 20byte | 50byte | 0.59 微秒 | 6.74 微秒 |
100w | 20byte | 20byte | 0.55 微秒 | 6.60 微秒 |
100w | 20byte | 5byte | 0.53 微秒 | 6.53 微秒 |
使用 lazy free 特性
在删除的时候提供异步延时释放键值的功能,把键值释放操作放在 BIO(Background I/O)单独的子线程处理中,以减少删除对 Redis 主线程的阻塞,可以有效地避免删除 big key 时带来的性能和可用性问题。
lazy free 对应了 4 种场景,默认都是关闭的:
1 | lazyfree-lazy-eviction no |
它们代表的含义如下:
- lazyfree-lazy-eviction:表示当 Redis 运行内存超过 maxmeory 时,是否开启 lazy free 机制删除;
- lazyfree-lazy-expire:表示设置了过期时间的键值,当过期之后是否开启 lazy free 机制删除;
- lazyfree-lazy-server-del:有些指令在处理已存在的键时,会带有一个隐式的 del 键的操作,比如 rename 命令,当目标键已存在,Redis 会先删除目标键,如果这些目标键是一个 big key,就会造成阻塞删除的问题,此配置表示在这种场景中是否开启 lazy free 机制删除;
- slave-lazy-flush:针对 slave(从节点)进行全量数据同步,slave 在加载 master 的 RDB 文件前,会运行 flushall 来清理自己的数据,它表示此时是否开启 lazy free 机制删除。
建议开启其中的 lazyfree-lazy-eviction、lazyfree-lazy-expire、lazyfree-lazy-server-del 等配置,这样就可以有效的提高主线程的执行效率。
设置键值的过期时间
应该根据实际的业务情况,对键值设置合理的过期时间,这样 Redis 会自动清除过期的键值对,以节约对内存的占用,以避免键值过多的堆积,频繁的触发内存淘汰策略。
禁用耗时长的查询命令
其中 O(1) 表示可以安全使用的,而 O(N) 就应该当心了,N 表示不确定,数据越大查询的速度可能会越慢。因为 Redis 只用一个线程来做数据查询,如果这些指令耗时很长,就会阻塞 Redis,造成大量延时。
要避免 O(N) 命令对 Redis 造成的影响,可以从以下几个方面入手改造:
- 决定禁止使用 keys 命令;
- 避免一次查询所有的成员,要使用 scan 命令进行分批的,游标式的遍历;
- 通过机制严格控制 Hash、Set、Sorted Set 等结构的数据大小;
- 将排序、并集、交集等操作放在客户端执行,以减少 Redis 服务器运行压力;
- 删除(del)一个大数据的时候,可能会需要很长时间,所以建议用异步删除的方式 unlink,它会启动一个新的线程来删除目标数据,而不阻塞 Redis 的主线程。
使用 slowlog 优化耗时命令
可以根据实际的业务情况进行相应的配置,其中慢日志是按照插入的顺序倒序存入慢查询日志中,使用 slowlog get n
来获取相关的慢查询日志,再找到这些慢查询对应的业务进行相关的优化。
使用 Pipeline 批量操作数据
Pipeline(管道技术)是客户端提供的一种批处理技术,用于一次处理多个 Redis 命令,从而提高整个交互的性能。
避免大量数据同时失效
如果在大型系统中有大量缓存在同一时间同时过期,那么会导致 Redis 循环多次持续扫描删除过期字典,直到过期字典中过期键值被删除的比较稀疏为止,而在整个执行过程会导致 Redis 的读写出现明显的卡顿,卡顿的另一种原因是内存管理器需要频繁回收内存页,因此也会消耗一定的 CPU。
需要预防大量的缓存在同一时刻一起过期,最简单的解决方案就是在过期时间的基础上添加一个指定范围的随机数。
客户端使用优化
在客户端的使用上除了要尽量使用 Pipeline 的技术外,还需要注意要尽量使用 Redis 连接池,而不是频繁创建销毁 Redis 连接,这样就可以减少网络传输次数和减少了非必要调用指令。
限制 Redis 内存大小
在 64 位操作系统中 Redis 的内存大小是没有限制的,也就是配置项 maxmemory <bytes>
是被注释掉的,这样就会导致在物理内存不足时,使用 swap 空间既交换空间,而当操心系统将 Redis 所用的内存分页移至 swap 空间时,将会阻塞 Redis 进程,导致 Redis 出现延迟,从而影响 Redis 的整体性能。因此需要限制 Redis 的内存大小为一个固定的值,当 Redis 的运行到达此值时会触发内存淘汰策略。
使用物理机而非虚拟机
在虚拟机中运行 Redis 服务器,因为和物理机共享一个物理网口,并且一台物理机可能有多个虚拟机在运行,因此在内存占用上和网络延迟方面都会有很糟糕的表现,可以通过 ./redis-cli --intrinsic-latency 100
命令查看延迟时间,如果对 Redis 的性能有较高要求的话,应尽可能在物理机上直接部署 Redis 服务器。
检查数据持久化策略
Redis 4.0 之后新增了混合持久化的方式,因此在必须要进行持久化操作时,应该选择混合持久化的方式。查询是否开启混合持久化可以使用 config get aof-use-rdb-preamble
命令。
使用分布式架构来增加读写速度
Redis 分布式架构有三个重要的手段:
- 主从同步
- 哨兵模式
- Redis Cluster 集群
使用主从同步功能可以把写入放到主库上执行,把读功能转移到从服务上,因此就可以在单位时间内处理更多的请求,从而提升的 Redis 整体的运行速度。而哨兵模式是对于主从功能的升级,但当主节点奔溃之后,无需人工干预就能自动恢复 Redis 的正常使用。
Redis Cluster 是 Redis 3.0 正式推出的,Redis 集群是通过将数据分散存储到多个节点上,来平衡各个节点的负载压力,因此性能会有很大的提升。
缓存问题
缓存雪崩
缓存雪崩是指在短时间内,有大量缓存同时过期,导致大量的请求直接查询数据库,从而对数据库造成了巨大的压力,严重情况下可能会导致数据库宕机的情况叫做缓存雪崩。
graph LR A[用户] --> B(应用程序) B -->|缓存过期| C(Redis) C -->|直接查询| D(DB)
常用解决方案:
- 加锁排队:加锁排队可以起到缓冲的作用,防止大量的请求同时操作数据库,但它的缺点是增加了系统的响应时间,降低了系统的吞吐量,牺牲了一部分用户体验。
- 随机化过期时间:为了避免缓存同时过期,可在设置缓存时添加随机时间,这样就可以极大的避免大量的缓存同时失效。
- 设置二级缓存:二级缓存指的是除了 Redis 本身的缓存,再设置一层缓存(例如本地缓存),当 Redis 失效之后,先去查询二级缓存。
缓存击穿
缓存击穿指的是某个热点缓存,在某一时刻恰好失效了,然后此时刚好有大量的并发请求,此时这些请求将会给数据库造成巨大的压力。
graph LR A[用户] --> B(应用程序) B -->|缓存失效| C(Redis) C -->|直接查询| D(DB)
常用解决方案:
- 加锁排队:此处理方式和缓存雪崩加锁排队的方法类似,都是在查询数据库时加锁排队,缓冲操作请求以此来减少服务器的运行压力。
- 设置永不过期:对于某些热点缓存,可以设置永不过期,这样就能保证缓存的稳定性,注意在数据更改之后,要及时更新此热点缓存,不然就会造成查询结果的误差。
缓存穿透
graph LR A[用户] --> B(查询) B --> C(Redis) C --> |无数据|E(DB) C -->|有数据| D(完成) E -->|有数据| C E -->|无数据| D
缓存穿透是指查询数据库和缓存都无数据,因为 数据库查询无数据,出于容错考虑,不会将结果保存到缓存中,因此每次请求都会去查询数据库,这种情况就叫做缓存穿透。
常用解决方案:
使用过滤器:使用过滤器来减少对数据库的请求,例如每次查询之前,先使用布隆过滤器过滤掉一定不存在的无效请求,从而避免了无效请求给数据库带来的查询压力。
缓存空结果:把每次从数据库查询的数据都保存到缓存中,为了提高前台用户的使用体验 (解决长时间内查询不到任何信息的情况),我们可以将空结果的缓存时间设置得短一些,例如 3~5 分钟。
缓存预热
缓存预热并不是一个问题,而是使用缓存时的一个优化方案,它可以提高前台用户的使用体验。缓存预热指的是在系统启动的时候,先把查询结果预存到缓存中,以便用户后面查询时可以直接从缓存中读取,以节约用户的等待时间。
缓存预热的实现思路有以下三种:
- 把需要缓存的方法写在系统初始化的方法中,这样系统在启动的时候就会自动的加载数据并缓存数据;
- 把需要缓存的方法挂载到某个页面或后端接口上,手动触发缓存预热;
- 设置定时任务,定时自动进行缓存预热。