一、简介
Redis是一款高性能的开源内存数据库,被广泛应用于缓存、消息队列和会话存储等场景。
在使用Redis时,选择适当的策略对于缓存命中率、数据一致性、性能优化等方面至关重要。
在使用Redis时,了解并正确选择适当的策略是优化性能和有效管理数据的关键。
本文将介绍Redis中的几种重要策略。
graph LR
A(redis的策略) ---> B(数据过期策略)
A(redis的策略) ---> C(缓存淘汰策略)
A(redis的策略) ---> D(数据持久化策略)
A(redis的策略) ---> E(高可用策略#集群策略#)
A(redis的策略) ---> F(分布式锁策略)
style B fill:#f67b6d,stroke:#f67b6d,stroke-width:2px
style C fill:#ffb025,stroke:#ffb025,stroke-width:2px
style D fill:#ffe340,stroke:#ffe340,stroke-width:2px
style E fill:#c3d779,stroke:#c3d779,stroke-width:2px
style F fill:#84d4da,stroke:#84d4da,stroke-width:2px
二、数据过期策略
我们在 set key的时候,可以给它设置一个过期时间,比如 expire key 60。指定这 key60s 后过期,60s 后,redis 是如何处理的呢?我们先来介绍几种过期策略:
graph TB
A(数据过期策略) ---> B(定时过期)
A(数据过期策略) ---> C(惰性过期)
A(数据过期策略) ---> D(定期过期)
style B fill:#f67b6d,stroke:#f67b6d,stroke-width:2px
style C fill:#ffb025,stroke:#ffb025,stroke-width:2px
style D fill:#ffe340,stroke:#ffe340,stroke-width:2px
1、定时过期
每个设置过期时间的 key 都需要创一个定时器,到过期时间就会立即对 key进行清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量CPU 资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
2、惰性过期
只有当访问一个 key 时,才会判断该 key 是否已过期,过期则清除。该策略可以最大化地节省 CPU 资源,却对内存非常不友好。极端情况可能出现大量过期 key 没有再次被访问,从而不会被清除,占用大量内存。
3、定期过期
每隔一定时间,会扫描一定数量数据库 expires 字典中一定数量key,并清除其中已过期 key。该策略是前两者一个折中方案。
通过调整定时扫描时间间隔和每次扫描限定耗时,可以在不同情况下使得 CPU 和内存资源达到最优平衡效果。
expires 字典会保存所有设置了过期时间 key 过期时间数据,其中,key是指向键空间中某个键指针,value 是该键毫秒精度 UNIX 时间戳表示过期时间。
键空间是指该 Redis 集群中保存所有键。
4、注意
Redis 中同时使用了惰性过期和定期过期两种过期策略。
-
假设Redis 当前存放 30 万个 key,并且都设置了过期时间,如果你每隔 100ms就去检查这全部 key,CPU 负载会特别高,最后可能会挂掉。
-
因此,redis 采取是定期过期,每隔 100ms 就随机抽取一定数量key 来检查和删除。
-
但是呢,最后可能会有很多已经过期 key 没被删除。这时候,redis 采用惰性删除。在你获取某个 key 时候,redis 会检查一下,这个 key 如果设置了过期时间并且已经过期了,此时就会删除。
但是呀,如果定期删除漏掉了很多过期 key,然后也没走惰性删除。就会有很多过期key 积在内存内存,直接会导致内存爆炸。或者有些时候,业务量大起来了,redis key 被大量使用,内存直接不够了,运维小哥哥也忘记加大内存了。难道 redis 直接这样挂掉?并不会!Redis 有 8 种内存淘汰策略保护自己~
三、缓存淘汰策略
-
volatile-lru:当内存不足以容纳新写入数据时,从设置了过期时间 key 中使用 LRU(最近最少使用)算法进行淘汰;
-
allkeys-lru:当内存不足以容纳新写入数据时,从所有 key 中使用 LRU(最近最少使用)算法进行淘汰;
-
volatile-random:当内存不足以容纳新写入数据时,从设置了过期时间 key 中,随机淘汰数据;
-
allkeys-random:当内存不足以容纳新写入数据时,从所有 key 中随机淘汰数据;
-
volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间 key 中,根据过期时间进行淘汰,越早过期优先被淘汰;
-
noeviction:默认策略,当内存不足以容纳新写入数据时,新写入操作会报错。
-
volatile-lfu:4.0 版本新增,当内存不足以容纳新写入数据时,在过期 key 中,使用 LFU 算法进行删除 key;
-
allkeys-lfu:4.0 版本新增,当内存不足以容纳新写入数据时,从所有 key 中使用 LFU 算法进行淘汰;
四、数据持久化策略
Redis提供了两种主要的持久化策略:RDB持久化和AOF持久化。
graph TB
A(数据持久化策略) ---> B(RDB快照持久化)
A(数据持久化策略) ---> C(AOF日志持久化)
A(数据持久化策略) ---> D(混合持久化)
style B fill:#f67b6d,stroke:#f67b6d,stroke-width:2px
style C fill:#ffb025,stroke:#ffb025,stroke-width:2px
style D fill:#ffe340,stroke:#ffe340,stroke-width:2px
1、RDB快照持久化
RDB持久化是通过创建数据集的时间点快照来实现的。Redis可以配置为在n秒内如果超过m个key被修改就自动做一次快照。
Redis的RDB快照持久化策略,将内存中的数据周期性地保存到磁盘上的二进制文件中。
这种方式适用于数据量较大且对数据完整性要求不高的场景。
2、AOF日志持久化
AOF持久化记录了服务器接收到的所有写命令,并在服务器启动时,通过重新执行这些命令来还原数据集。Redis可以配置为每次接收到写命令就立即写入磁盘,或者每秒写入一次,或者在队列命令的数量达到一定数量时写入。
Redis的AOF日志持久化策略,它将所有对Redis的写操作追加到一个日志文件中。通过重新执行这些写操作,可以在Redis启动时将数据恢复到最新状态。
AOF日志相对于RDB快照具有更高的数据完整性,但也会增加写入操作的延迟。
3、混合持久化
为了兼顾RDB快照和AOF日志的优点,Redis还提供了混合持久化策略。
在这种策略下,可以同时启用RDB快照和AOF日志,以实现更好的数据保护和恢复能力。
五、高可用策略(集群策略)
Redis的高可用策略是确保在发生故障或节点失效时,系统可以持续提供服务并保持数据的可靠性。下面将详细介绍Redis的三种高可用策略:主从复制、哨兵模式和Redis Cluster。
graph TB
A(高可用策略) ---> B(主从复制)
A(高可用策略) ---> C(哨兵模式)
A(高可用策略) ---> D(Redis Cluster)
style B fill:#f67b6d,stroke:#f67b6d,stroke-width:2px
style C fill:#ffb025,stroke:#ffb025,stroke-width:2px
style D fill:#ffe340,stroke:#ffe340,stroke-width:2px
1、主从复制
在主从复制中,一个Redis实例作为主节点,而其他实例则作为从节点。主节点负责处理写操作,并将写操作的数据同步到所有从节点上。从节点复制主节点的数据,并可以处理读操作。这种方式可以提高系统的可用性和读取性能,并在主节点故障时自动切换到从节点。
主从复制的工作流程如下:
sequenceDiagram
participant 主节点 as 主节点
participant 从节点 as 从节点
从节点->>主节点: SYNC命令请求复制数据
主节点->>主节点: 执行BGSAVE命令,保存RDB文件
主节点->>从节点: 发送RDB文件
主节点->>从节点: 发送缓冲期间的写操作命令
从节点->>从节点: 接收并加载RDB文件,恢复数据集
主节点->>从节点: 执行写操作命令,保持数据同步
-
从节点连接到主节点,并发送SYNC命令,请求复制主节点的数据。
-
主节点执行BGSAVE命令,将当前数据集保存到磁盘上的RDB文件。
-
主节点将保存的RDB文件发送给从节点,并发送缓冲期间的写操作命令。
-
从节点接收并加载RDB文件,恢复数据集,并通过执行主节点的写操作命令保持数据同步。
主从复制的优点是可以提高系统的读取性能,因为读操作可以分散到多个从节点上执行。当主节点发生故障时,可以手动或自动将一个从节点晋升为新的主节点,确保系统的可用性。
2、哨兵模式
哨兵模式是一种自动监控和故障转移的高可用策略。在哨兵模式中,有一个或多个哨兵进程监控Redis实例的状态,当主节点发生故障时,哨兵会自动选择一个从节点作为新的主节点,并将其他从节点重新配置为从新的主节点复制数据。
哨兵模式的工作流程如下:
sequenceDiagram
participant 哨兵 as 哨兵进程
participant 主节点 as 主节点
participant 从节点 as 从节点
participant 客户端 as 客户端
哨兵->>主节点: 发送PING命令检查健康状态
alt 主节点正常
主节点-->>哨兵: 返回PONG
else 主节点失效
哨兵->>从节点: 选择一个合适的从节点作为新的主节点
从节点->>从节点: 复制新的主节点
哨兵->>其他从节点: 发送重新配置命令
客户端->>新的主节点: 发送请求
end
-
哨兵进程通过发送PING命令定期检查Redis实例的健康状态。
-
当哨兵检测到主节点失效时,它会在从节点中选择一个合适的从节点,并将其晋升为新的主节点。
-
哨兵会通知其他从节点,让它们重新配置自己以复制新的主节点。
-
客户端会被重定向到新的主节点,以确保服务的连续性。
哨兵模式的优点是可以自动监控和调整系统的状态,当主节点失效时能够快速进行故障转移。
它还支持多个哨兵实例,提供更高的可用性和容错能力。
3、Redis Cluster
Redis Cluster是一种分布式策略,可以将数据分布在多个节点上,提供高可用性和横向扩展能力。它将数据划分为多个槽,并在不同的节点上存储这些槽的数据。
Redis Cluster的工作流程如下:
sequenceDiagram
participant 客户端 as 客户端
participant 节点A as 节点A
participant 节点B as 节点B
participant 节点C as 节点C
客户端->>节点A: 发送槽相关命令,映射数据到槽
节点A->>节点A: 处理和管理分配的槽的数据
节点A->>节点B: Gossip协议,交换拓扑信息和数据迁移状态
节点A->>节点C: Gossip协议,交换拓扑信息和数据迁移状态
alt 节点A失效
节点B->>节点B: 接管节点A失效的槽
节点C->>节点C: 接管节点A失效的槽
节点B->>节点C: 迁移数据以保持可用性
end
-
客户端通过发送槽相关的命令,将数据映射到正确的槽上。
-
每个节点负责处理和管理一部分槽的数据。
-
节点间通过Gossip协议进行通信,交换拓扑信息和数据迁移状态。
-
当某个节点失效时,其他节点会接管失效节点的槽,并进行数据迁移以保持数据的可用性。
Redis Cluster的优点是可以水平扩展和提高系统的容量和性能。它提供了较高的可用性,因为即使部分节点失效,整个集群仍然可以继续提供服务。
六、分布式锁策略
Redis的分布式锁是一种在分布式环境下实现互斥访问的策略,用于保证多个客户端之间对共享资源的安全访问。
下面详细介绍Redis中实现分布式锁的常见策略:
graph TB
A(分布式锁策略) ---> B(基于SETNX指令的锁)
A(分布式锁策略) ---> C(基于RedLock算法的分布式锁)
A(分布式锁策略) ---> D(基于Redisson的分布式锁)
style B fill:#f67b6d,stroke:#f67b6d,stroke-width:2px
style C fill:#ffb025,stroke:#ffb025,stroke-width:2px
style D fill:#ffe340,stroke:#ffe340,stroke-width:2px
1、基于SETNX指令的锁
Redis可以通过SETNX指令实现简单的分布式锁。通过在键上设置一个特定值,可以确保同一时间只有一个客户端可以获得锁。
基于SETNX命令的简单分布式锁是Redis中实现分布式锁的最基本方式。其核心思想是利用Redis的原子性操作来确保只有一个客户端能够成功地获取到锁。
实现步骤如下:
sequenceDiagram
participant 客户端 as 客户端
participant Redis服务器 as Redis服务器
客户端->>Redis服务器: 执行SETNX命令,尝试获取锁
alt 锁可用
Redis服务器-->>客户端: 返回1,获取锁成功
客户端->>Redis服务器: 执行操作
客户端->>Redis服务器: 执行DEL命令,释放锁
else 锁已被持有
Redis服务器-->>客户端: 返回0,获取锁失败
end
-
客户端通过执行SETNX命令尝试在Redis中设置一个特定的键作为锁。
-
如果SETNX命令返回1,表示客户端成功获取到锁。
-
如果SETNX命令返回0,表示锁已经被其他客户端持有,当前客户端获取锁失败。
-
获取到锁的客户端可以执行自己的操作,并在操作完成后通过执行DEL命令来释放锁。
简单分布式锁的优点是实现简单,并且利用了Redis的原子性操作来保证互斥性。但它存在一些问题,例如当获取锁的客户端发生故障或执行时间过长时,可能会导致死锁或锁的过期时间不确定等问题。
2、基于RedLock算法的分布式锁
RedLock算法是一种基于Redis的分布式锁算法,它通过在多个Redis实例上设置锁,提供更高的可靠性和安全性。
基于RedLock算法的复杂分布式锁是一种更复杂但更可靠的分布式锁策略,旨在解决简单分布式锁的一些问题。该算法由Redis作者提出,并在一定条件下提供了更强的可靠性。
实现步骤如下:
sequenceDiagram
participant 客户端 as 客户端
participant Redis实例1 as Redis实例1
participant Redis实例2 as Redis实例2
participant Redis实例3 as Redis实例3
participant Redis实例4 as Redis实例4
participant Redis实例5 as Redis实例5
客户端->>Redis实例1: 执行SET命令,创建锁键
Redis实例1-->>客户端: 返回结果
客户端->>Redis实例2: 执行SET命令,创建锁键
Redis实例2-->>客户端: 返回结果
客户端->>Redis实例3: 执行SET命令,创建锁键
Redis实例3-->>客户端: 返回结果
客户端->>Redis实例4: 执行SET命令,创建锁键
Redis实例4-->>客户端: 返回结果
客户端->>Redis实例5: 执行SET命令,创建锁键
Redis实例5-->>客户端: 返回结果
alt 锁创建成功的实例数量达到阈值
客户端->>Redis实例1: 执行操作
客户端->>Redis实例2: 执行操作
客户端->>Redis实例3: 执行操作
客户端->>Redis实例4: 执行操作
客户端->>Redis实例5: 执行操作
客户端->>Redis实例1: 执行DEL命令,释放锁
客户端->>Redis实例2: 执行DEL命令,释放锁
客户端->>Redis实例3: 执行DEL命令,释放锁
客户端->>Redis实例4: 执行DEL命令,释放锁
客户端->>Redis实例5: 执行DEL命令,释放锁
else 锁创建成功的实例数量未达到阈值
客户端-->>客户端: 处理未满足阈值的情况
end
-
客户端向多个Redis实例(至少是5个)发送SET命令,尝试在不同的实例上创建相同的锁键,并设置相同的随机值作为锁值。
-
客户端设置一个合理的过期时间,以确保即使持有锁的Redis实例发生故障,锁也不会永远持有。
-
客户端在大多数Redis实例上成功地创建了锁,并且锁的数量达到了设定的最低数量阈值(如3个)。
-
获取到足够数量的锁的客户端可以执行自己的操作,并在操作完成后通过执行DEL命令来释放锁。
RedLock算法通过在多个Redis实例上创建锁来提供更高的可靠性。即使其中一个Redis实例发生故障,只要大多数实例仍然可访问,分布式锁仍然可用。
但需要注意的是,RedLock算法并不能保证100%的可靠性,因为在极端情况下,网络分区或其他问题可能导致锁的争用。
3、基于Redisson的分布式锁
Redisson是一个用于Java的Redis客户端,它提供了分布式锁的高级功能,包括可重入锁、公平锁等。通过使用Redisson,可以简化分布式锁的使用和管理。
Redisson是一个基于Redis的分布式Java对象和服务的框架,它提供了分布式锁的高级功能,包括可重入锁、公平锁等,并且它是一种简单且可靠的方式来实现分布式锁。Redisson的分布式锁是基于Redis的原子操作实现的,具有高性能和可靠性。通过使用Redisson,可以简化分布式锁的使用和管理。
3.1、Redisson的分布式锁特点如下
-
基于Redis原子操作: Redisson的分布式锁利用Redis的原子操作,例如SETNX(SET if Not eXists)命令,来实现锁的获取。这确保了锁的获取和释放操作的原子性,避免了竞态条件。
-
可重入锁: Redisson的分布式锁支持可重入(Reentrant)锁,允许同一个线程多次获取同一个锁。这对于需要在同一线程内执行嵌套操作的场景非常有用。
-
锁的自动续期: Redisson的分布式锁支持锁的自动续期机制。在获取锁时,客户端可以指定一个过期时间(lockWatchdogTimeout),如果在该时间内没有显式释放锁,Redisson会自动延长锁的过期时间,确保锁不会过早地失效。
-
异步获取锁: Redisson的分布式锁支持异步获取锁的方式。通过使用异步操作,可以提高并发性能和资源利用率。
-
监控锁状态: Redisson的分布式锁可以监控锁的状态,例如锁的持有者、锁的剩余过期时间等。这使得可以对锁进行更细粒度的控制和监控。
3.2、使用Redisson的分布式锁时,通常的步骤如下
sequenceDiagram
participant 客户端 as 客户端
participant Redis服务器 as Redis服务器
participant Redisson as Redisson
客户端->>Redisson: 创建Redisson客户端实例
Redisson->>Redis服务器: 连接到Redis服务器
客户端->>Redisson: 使用Redisson客户端实例创建分布式锁对象
客户端->>Redisson: 调用分布式锁对象的lock()方法
alt 锁可用
Redisson-->>客户端: 返回锁获取成功
客户端->>Redis服务器: 执行业务逻辑
客户端->>Redisson: 调用分布式锁对象的unlock()方法
Redisson-->>Redis服务器: 释放锁
else 锁已被持有
Redisson-->>客户端: 锁获取失败,当前线程被阻塞
end
创建Redisson客户端实例,连接到Redis服务器。
使用Redisson客户端实例创建分布式锁对象。
在需要获取锁的代码块中,调用分布式锁对象的lock()方法来获取锁。如果锁已被其他客户端持有,则当前线程会被阻塞,直到获取到锁为止。
在锁获取成功后,执行自己的业务逻辑。
在业务逻辑执行完成后,调用分布式锁对象的unlock()方法来释放锁。