Redis
# Redis缓存数据库
# 什么是 Redis?
Redis (opens new window) (REmote DIctionary Server)是一个基于 C 语言开发的开源 NoSQL 数据库(BSD 许可)。与传统数据库不同的是,Redis 的数据是保存在内存中的(内存数据库,支持持久化),因此读写速度非常快,被广泛应用于分布式缓存方向。并且,Redis 存储的是 KV 键值对数据。
# 为什么使用 Redis?
Redis(Remote Dictionary Server)是一个高性能的开源内存数据结构存储系统,常被用作数据库、缓存和消息代理。以下是使用 Redis 的主要原因:
# 1. 访问速度更快
- 内存存储:传统关系型数据库(如 MySQL)的数据通常存储在磁盘上,而 Redis 将数据存储在内存中。内存的读写速度比磁盘快得多(通常是微秒级别 vs 毫秒级别)。
- 应用场景:通过将高频访问的数据(如热点数据、会话信息)存储在 Redis 中,可以避免频繁访问磁盘数据库,从而将访问速度提升几十倍甚至上百倍。
# 2. 支持高并发
- 高 QPS:传统数据库(如 MySQL)单机 QPS(每秒查询次数)通常在几千左右(例如 4 核 8GB 配置下约 4000 QPS),而单机 Redis 的 QPS 轻松达到 5 万以上,甚至 10 万+。如果使用 Redis 集群,性能还能进一步提升。
- 减轻数据库压力:将部分请求从数据库转移到 Redis 缓存,可以显著减少数据库的负载,提升系统的整体并发能力。例如,用户请求先查 Redis,若缓存命中则无需访问数据库。
# 3. 功能全面
Redis 不仅是一个简单的键值存储,还支持多种数据结构(字符串、哈希、列表、集合、有序集合等),并提供了丰富的功能:
- 分布式锁:利用 Redis 的原子性操作(如 SETNX),可以实现分布式系统中的锁机制。
- 限流:通过计数器或滑动窗口算法,实现接口访问频率限制。
- 消息队列:支持简单的发布/订阅模式,或通过列表实现队列功能。
- 延时队列:结合有序集合(Sorted Set),可以实现定时任务或延迟处理。
- 其他:如排行榜(Sorted Set)、地理位置计算(GeoHash)等。
# redis为什么快?
Redis 之所以“快”,是因为它在设计和实现上做了很多高效的优化,下面我们可以从多个角度来详细分析 Redis 为什么这么快:
# 1.内存存储:访问速度极快
- Redis 所有数据都存储在 内存(RAM) 中,内存的访问速度远远快于磁盘(比如 SSD、HDD)。
- 相比传统的基于磁盘的数据库(如 MySQL、PostgreSQL),内存读写速度快几个数量级。
存储介质 | 访问延迟 |
---|---|
内存(RAM) | 纳秒级(ns) |
SSD 磁盘 | 微秒级(μs) |
机械硬盘 | 毫秒级(ms) |
# 2.单线程模型:避免上下文切换与锁竞争
- Redis 使用单线程(从 Redis 6 开始部分 I/O 可多线程)处理命令请求。
- 这样做可以避免多线程带来的 线程上下文切换 和 加锁开销,逻辑简单、高效。
🚫 避免的代价:
- 无需加锁 ➜ 没有锁竞争
- 无上下文切换 ➜ CPU 缓存利用率高
- 请求队列串行执行 ➜ 避免并发冲突
Redis 大多数时候不是算力密集型应用,而是“内存+网络 IO 密集型”,所以 CPU 通常不是瓶颈
# 3.高效的数据结构设计
Redis 提供多种数据结构,每种都为不同的应用场景做了高度优化。这些结构都为了“时间复杂度低、空间利用高”而设计,比如 O(1) 的插入、删除、查找等。
具体参考后面章节
# 4. I/O 多路复用:网络处理效率高
Redis 使用 epoll(Linux)/ kqueue(BSD/macOS) 实现的 I/O 多路复用模型:
- 监听多个连接,只用一个线程处理多个客户端请求。
- 和传统的“一个连接一个线程”模型相比,系统资源占用低、上下文切换少。
- epoll 本身是操作系统提供的高效系统调用。
# 5. 持久化机制不影响主流程
虽然 Redis 是内存数据库,但支持持久化(RDB 和 AOF):
- 持久化在 后台线程异步 执行,不影响主线程服务请求。
- 同时通过 fork 子进程 来做快照或日志写入,确保主线程高性能运行。
# redis的数据类型
Redis 是一个高性能的键值存储数据库,它支持多种丰富的数据类型,这也是它功能强大的重要原因之一。以下是 Redis 目前支持的主要数据类型及其特点和常见使用场景:
# 1. 字符串(String)
- 描述:最基本的数据类型,可以存储字符串、整数或浮点数,虽然你去type获取value的的类型都是string但是底层数据结构不一样,也就是为什么可以incr。
- 最大容量:单个字符串值最大为 512MB。
- 常用命令:
SET key value
:设置键值对。GET key
:获取键对应的值。INCR key
:将键值(整数)加 1。APPEND key value
:追加内容到字符串。
- 使用场景:
- 缓存:存储 JSON 或简单字符串(如用户信息)。
- 计数器:如文章阅读量、点赞数(利用 INCR/DECR)。
- 会话管理:存储用户 Session ID。
# 2. 哈希(Hash)
- 描述:键值对的集合,适合存储对象。一个键对应一个字段-值映射表。
- 特点:比存储整个 JSON 字符串更高效,可以单独操作某个字段。
- 常用命令:
HSET key field value
:设置哈希表中的字段值。HGET key field
:获取指定字段的值。HGETALL key
:获取所有字段和值。
- 使用场景:
- 存储对象:如用户信息(字段:name、age、email)。
- 购物车:用户 ID 作为键,商品 ID 和数量作为字段。
# 3. 列表(List)
- 描述:有序的字符串列表,按照插入顺序排列,支持双端操作。
- 特点:可以用作队列或栈。
- 常用命令:
LPUSH key value
:左侧插入元素。RPUSH key value
:右侧插入元素。LPOP key
:左侧弹出元素。LRANGE key start stop
:获取指定范围的元素。
- 使用场景:
- 消息队列:通过 LPUSH 和 RPOP 实现简单的队列。
- 最新列表:如最新的评论、动态(按时间顺序存储)。
# 4. 集合(Set)
- 描述:无序且唯一的字符串集合。
- 特点:支持集合运算(如交集、并集、差集)。
- 常用命令:
SADD key member
:添加元素到集合。SMEMBERS key
:获取集合所有成员。SINTER key1 key2
:计算两个集合的交集。
- 使用场景:
- 去重:如用户标签、抽奖参与者。
- 共同好友:利用交集运算查找共同关注的人。
- 随机抽取:如
SRANDMEMBER
随机获取元素。
# 5. 有序集合(Sorted Set)
- 描述:类似于集合,但每个元素关联一个分数(score),按分数排序。
- 特点:既保持唯一性,又支持排序。
- 常用命令:
ZADD key score member
:添加元素及其分数。ZRANGE key start stop
:按分数顺序获取元素。ZREVRANGE key start stop
:按分数倒序获取元素。ZINCRBY key increment member
:增加元素的分数。
- 使用场景:
- 排行榜:如游戏积分榜、热度排行(分数动态更新)。
- 延时队列:用分数作为时间戳,实现定时任务。
# 6. 其他高级数据类型
Redis 在后续版本中引入了一些扩展数据类型(部分需要启用特定模块):
- 位图(Bitmap)
- 描述:基于字符串的位操作,用于存储二进制数据。
- 常用命令:
SETBIT key offset value
、GETBIT key offset
。 - 使用场景:统计活跃用户(0/1 表示是否登录)、布隆过滤器基础。
- HyperLogLog
- 描述:用于基数统计的概率数据结构,占用空间极小。
- 常用命令:
PFADD key element
、PFCOUNT key
。 - 使用场景:统计网页 UV(独立访客数)。
- 地理空间(Geo)
- 描述:存储经纬度坐标,支持距离计算和范围查询。
- 常用命令:
GEOADD key longitude latitude member
、GEODIST key member1 member2
。 - 使用场景:附近的人、店铺位置查询。
- Stream
- 描述:日志式数据结构,支持消息队列和消费者组。
- 常用命令:
XADD key ID field value
、XREAD key
。 - 使用场景:事件流处理、日志收集。
# Redis 底层数据结构
Redis 是一个高性能的内存键值数据库,以其速度快、支持多种数据类型而闻名。Redis 的核心在于其底层数据结构的实现,尤其是键(Key)和值(Value)的存储方式。本教程将深入探讨 Redis 的底层数据结构,包括键的实现、各种值类型对应的数据结构,以及一些设计上的权衡和演变。
# 1. Redis 键(Key)的底层实现
Redis 的键本质上是一个字符串,而其底层存储方式类似于 Java 中的 HashMap 或 HashTable。具体来说,Redis 使用**哈希表(Hash Table)**来存储键值对,其结构可以简单描述为:
- 数组 + 链表:哈希表由一个数组组成,每个数组元素(称为“桶”)指向一个链表。当发生哈希冲突时,通过链表将多个键值对链接起来。
- 哈希函数:Redis 使用哈希函数(比如 MurmurHash)将键映射到数组的某个索引位置。
- 动态扩容:当哈希表中的元素过多(负载因子过高)时,Redis 会进行 rehash 操作,动态扩展数组大小,并重新分配键值对。
这种设计使得 Redis 的键查找效率接近 O(1),但具体性能还与链表长度(冲突情况)有关。
# 2. Redis 值(Value)的底层数据结构
Redis 的强大之处在于它支持多种数据类型(String、Hash、List、Set、ZSet 等),而每种数据类型的底层实现并不完全相同。根据值的类型,Redis 会选择不同的数据结构来优化存储和操作效率。下面逐一讲解:
# 1. String(字符串类型)
- 底层实现:int+SDS(Simple Dynamic String,简单动态字符串)
- 特点:
- SDS 是 Redis 自定义的字符串结构,相比 C 的传统字符串(以
\0
结尾),SDS 更高效。 - SDS 包含三个字段:
len
(字符串长度)、free
(未使用字节数)、buf
(实际字符数组)。 - 通过
len
可以直接获取字符串长度(O(1) 时间),无需遍历。
- SDS 是 Redis 自定义的字符串结构,相比 C 的传统字符串(以
- 优点:
- 避免缓冲区溢出,支持动态扩展。
- 二进制安全,可以存储任意数据(如图片、序列化对象)。
- 使用场景:计数器、缓存文本、序列化对象等。
# 2. Hash(哈希表类型)
- 底层实现:两种数据结构(根据数据量动态切换)
- Listpack(较新版本中替代了 Ziplist):当哈希表元素较少时使用。
- Listpack 是一个紧凑的序列化数据结构,连续存储键值对,节省内存。
- 适合小规模数据,查询和插入效率较高。
- Hashtable:当元素数量较多或键值对较大时使用。
- 与 Redis 的键存储类似,是标准的数组 + 链表结构。
- Listpack(较新版本中替代了 Ziplist):当哈希表元素较少时使用。
- 切换条件:
- 当哈希表中键值对数量较少(默认小于 512)且每个键值对长度较短(默认小于 64 字节)时,使用 Listpack。
- 超过阈值后,转换为 Hashtable。
- 使用场景:存储对象(如用户信息:name、age 等键值对)。
struct listpack<T> {
int32 total_bytes; // 整个listpack,占用的总字节数
int16 size; // 整个listpack中元素个数
T[] entries; // 紧凑排列的元素列表
int8 end; // listpack的结束符,恒为 0xFF
}
2
3
4
5
6
# 3. List(列表类型)
底层实现:
- LinkedList(双向链表):Redis 早期版本使用较多。
- 每个节点包含指向前一个和后一个节点的指针,适合快速在两端插入或删除。
- Ziplist(压缩列表):在元素较少时使用,后被 Listpack 替代。
- Ziplist 是连续内存块,按顺序存储元素,节省空间。
但是在 Redis 3.2版本之后,List数据类型底层数据结构就只由 quicklist 实现了,替代了双向链表和压
缩列表。- LinkedList(双向链表):Redis 早期版本使用较多。
特点:
- 双向链表适合频繁的头尾操作(如 LPUSH、RPOP),但随机访问效率低(O(n))。
- Ziplist/Listpack 则更紧凑,适合小型列表。
使用场景:消息队列、任务列表。
struct ziplist<T> {
int32 zlbytes; // 整个压缩列表占用的字节数
int32 zltail_offset; // 最后一个元素距离压缩列表起始位置的偏移量,用于快速定位到最后一个节点
int16 zllength; // 真个ziplist中元素的个数
T[] entries; // 元素内容列表,挨个挨个紧凑存储 int8 zlend; // 标志压缩列表的结束,值恒为 0xFF
}
2
3
4
5
6
7
8
quicklist 实际上是 zipList 和 linkedList 的混合体,它将 linkedList 按段切分,每一段使用 zipList 来紧凑存储,多个 zipList 之间使用双向指针串接起来。
struct quicklist {
quicklistNode* head; //头节点指针
quicklistNode* tail; //尾节点指针
long count; // 元素总数
int nodes; // ziplist 节点的个数
int compressDepth; // LZF 算法压缩深度
}
2
3
4
5
6
7
# 4. Set(集合类型)
- 底层实现:两种数据结构(根据元素类型和数量切换)
- Intset(整数集合):当集合中所有元素都是整数且数量较少时使用。
- Intset 是一个有序的整数数组,内存紧凑,支持二分查找。
- Hashtable:当集合包含非整数元素或数量较多时使用。
- 键存储集合元素,值为空,保证唯一性。
- Intset(整数集合):当集合中所有元素都是整数且数量较少时使用。
- 特点:
- Intset 适用于小规模整数集合,节省内存。
- Hashtable 适合大规模或复杂数据,查询效率高(O(1))。
- 使用场景:唯一性检查(如用户 ID 集合)。
/* 整数集合结构 */
typedef struct intset {
uint32_t encoding; // 编码方式
uint32_t length; // 集合数组中元素个数
int8_t contents[]; // 集合数组
} intset;
2
3
4
5
6
# 5. ZSet(有序集合类型)
底层实现:
- Ziplist(压缩列表):早期版本用于小规模数据,后被淘汰现用listpack。
- 连续存储元素和分数(score),按分数排序。
- Skiplist(跳表):现代 Redis 的主要实现。
- 跳表:用于维护元素的有序性,支持快速插入、删除和范围查询。
- Ziplist(压缩列表):早期版本用于小规模数据,后被淘汰现用listpack。
特点:
- 跳表是一种多层链表结构,通过“跳跃”指针加速查找,平均时间复杂度为 O(log n)。
使用场景:排行榜、按权重排序的任务队列。
# 3. 跳表(Skiplist)的深入剖析
# 1. 为什么从 Ziplist 切换到跳表?
- Ziplist 的局限性:
- Ziplist 是连续内存块,插入和删除需要移动大量元素(O(n) 时间复杂度)。
- 查询效率依赖偏移量计算,虽然比链表快,但在大规模数据下仍显不足。
- 跳表的优势:
- 跳表基于链表,但通过多层索引(类似 B+ 树的层次结构)加速查找。
- 插入和删除只需调整指针(O(log n)),无需移动大量元素。
- 实现简单,相比平衡树(如 AVL 树)更容易维护。
# 2. 跳表的结构
- 底层:普通的双向链表,按分数从小到大排序。
- 上层:随机生成的索引层,每层节点指向下层节点,形成“跳跃”路径。
- 查找过程:
- 从最高层开始,沿索引快速定位目标范围。
- 逐层下降,最终在底层链表中找到精确位置。
- 时间复杂度:
- 查找、插入、删除:平均 O(log n)。
- 空间复杂度:O(n),因为需要额外存储索引。
# Redis支持事务吗?
Redis 支持基本的事务功能,但与传统关系型数据库的事务机制相比,它的事务模型相对简单。Redis 的事务机制主要通过 MULTI
、EXEC
、DISCARD
和 WATCH
命令实现。
# Redis 事务模型概述
在 Redis 中,事务并不像传统的数据库事务那样支持回滚和隔离级别,而是一个简单的事务队列,允许多个命令按顺序执行。Redis 事务的关键点是将多个命令打包在一起,保证它们以原子方式执行,即所有命令会顺序执行,且在执行过程中不会被其他客户端命令打断。
# 1. 基本操作
MULTI
:开始一个事务块,标记后续命令为事务的一部分。EXEC
:执行事务中的所有命令。此时,所有在MULTI
后执行的命令会依次按顺序执行。DISCARD
:放弃事务,清除事务队列中的所有命令。WATCH
:监视一个或多个键,在事务执行之前,如果监视的键发生了变化,事务会被中止,避免出现数据不一致的情况。
# 2. 事务执行流程
事务的基本流程如下:
启动事务: 客户端通过发送
MULTI
命令来开始一个事务。此时 Redis 会进入事务模式,接下来执行的命令都不会立即执行,而是会被放入事务队列中等待。MULTI
1将命令排入队列: 在事务模式下,客户端可以发送多个命令,这些命令不会立即被执行,而是被排入事务队列。例如:
SET key1 value1 INCR key2
1
2执行事务: 发送
EXEC
命令后,Redis 会按顺序执行事务队列中的所有命令。此时,所有命令会原子性地执行,整个事务中的命令不会被其他客户端的命令中断。EXEC
1取消事务: 如果事务中的命令还没有执行,客户端可以使用
DISCARD
命令取消事务,清除事务队列中的所有命令。DISCARD
1
# 3. 事务的原子性
Redis 事务并不支持传统意义上的 回滚(rollback)机制。如果事务中的某个命令执行失败,Redis 会继续执行剩余的命令,因此并没有严格的“全成功或全失败”的保证。
- 例如,如果
MULTI
后的某个命令失败了(例如操作一个不存在的键),其他命令仍会按顺序执行。这种行为与传统关系数据库中的回滚机制有所不同。
# 4. WATCH 和乐观锁
为了增强事务的一致性,Redis 提供了 WATCH
命令,支持 乐观锁,即在执行事务前,可以监视某些键。如果这些键在事务执行前被其他客户端修改了,事务会被中止。
WATCH
:监视指定的一个或多个键。如果这些键在事务执行期间被修改,EXEC
命令执行时会返回空数组,表示事务被中止。WATCH key1 MULTI SET key1 new_value EXEC
1
2
3
4如果
key1
在MULTI
和EXEC
之间被其他客户端修改,EXEC
会返回空数组,表明事务未能执行。适用场景:
WATCH
适用于需要确保某些数据在事务执行期间没有被其他客户端修改的情况。
# 5. 事务的缺陷和局限性
- 缺乏隔离性:Redis 事务在执行时不能提供类似传统关系数据库的隔离性(如串行化、可重复读等)。所有命令都在同一个线程中按顺序执行,且无法并发处理,因此存在其他客户端在事务执行过程中修改数据的风险。
- 没有回滚机制:如果事务中的某个命令失败,Redis 并不会回滚之前的命令,只会继续执行后续的命令。
- 事务不是“全-or-无”:虽然事务中的命令是按顺序执行的,但如果某个命令失败,其他命令依然会继续执行。
# 6. 事务 vs Lua 脚本
虽然 Redis 的事务机制能够提供命令的原子性执行,但它并不如 Lua 脚本那样原子性强大。Redis 通过 Lua 脚本 提供了更高的原子性保障。
Lua 脚本:Redis 在执行 Lua 脚本时,所有操作都在同一个事务中执行,不会被中断,且无其他客户端命令可以插入。因此,Lua 脚本可以看作是比事务更强的原子性操作。
EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 key1 value1
1Lua 脚本的原子性可以确保在脚本执行过程中不会出现中断,即使是发生故障也不会导致数据不一致。
# Reids持久化
Redis 的持久化机制包括 RDB 和 AOF 两种方式,确保数据在服务器重启后不丢失。RDB 通过快照保存数据状态,AOF 通过记录写操作命令重建数据集。这两种方式可以单独使用或同时启用。
# 什么是 RDB
RDB(Redis 数据库快照)通过创建数据集的快照,在指定时间间隔内将数据保存到磁盘的 RDB 文件中。
- 手动触发: 使用
SAVE
(阻塞所有客户端请求,适合小数据集)或BGSAVE
(后台异步保存,推荐生产环境)。 - 自动触发: 通过配置文件(如
save 900 1
表示 900 秒内至少 1 个键修改后自动保存)或服务器关闭时触发。 - 令人惊讶的是: RDB 文件非常紧凑,适合备份,但可能会丢失最后一次快照后的数据。
# 什么是 AOF
AOF(追加文件)通过记录每个写操作命令(如 SET
、HSET
)并追加到 AOF 文件,恢复时重新执行这些命令重建数据集。
- 工作流程: 包括命令写入、文件同步(每秒、每次写或由操作系统决定)、文件重写(压缩文件)、重启加载。
- 令人惊讶的是: AOF 文件可能变大,恢复较慢,但实时性好,适合需要高数据完整性的场景。
# RDB 和 AOF 的对比与选择
- RDB 优点: 恢复快,文件小,适合备份;缺点: 数据丢失风险高。
- AOF 优点: 数据完整性高,适合关键数据;缺点: 文件可能较大,恢复慢。
- 选择建议: 如果追求性能且能接受少量数据丢失,用 RDB;如果数据耐久性关键,用 AOF(推荐每秒同步)。生产环境常同时启用两者:RDB 备份,AOF 保证完整性。
Redis 4.0 的混合持久化:
从 Redis 4.0 开始,AOF 重写时可嵌入 RDB 快照,恢复时先加载快照再应用 AOF 命令,既快又可靠。
# Redis分布式锁
分布式锁是保证在分布式系统中,多个进程或服务能够协调共享资源的一种机制,确保同一时刻只有一个进程能访问某个共享资源,避免并发冲突和数据不一致的问题。
在传统的单机环境下,可以通过简单的线程锁来保证资源的独占访问,但在分布式环境中,锁的实现和管理变得更加复杂,因为系统中有多个节点,且可能分布在不同的物理机上。Redis 提供了一个很好的分布式锁实现方式,常用的机制包括:
# 1. Redis 实现分布式锁的基本原理
Redis 本身是单线程的,处理命令的顺序是严格按照请求的顺序执行的,因此可以利用 Redis 的原子性操作来实现分布式锁。常见的做法是利用 Redis 的 SETNX
命令(即“Set if Not Exists”)来尝试获取锁。
# 2. 基本实现方式
最常见的分布式锁实现方式是通过 SETNX
(或 SET
带 NX 参数)来创建锁,并设置一个过期时间(防止死锁)。具体流程如下:
获取锁:
- 客户端通过
SET key value NX PX timeout
命令向 Redis 请求设置一个唯一的锁标识符 (value)。NX
:只有当键不存在时,才会设置成功(相当于 “Set if Not Exists”)。PX timeout
:设置键的过期时间(避免长时间无法释放锁,防止死锁)。
例如:
SET lock_key unique_lock_value NX PX 30000
1如果返回值为
OK
,则表示获取锁成功。否则,表示锁已经被其他客户端占用。- 客户端通过
释放锁:
- 释放锁时需要保证只有持有锁的客户端才能删除锁,防止其他客户端误删锁。
- 常见的做法是使用
DEL
命令删除锁键,但需要加上检查,确保只有锁持有者才能删除锁(通过UUID+线程名)。
# 3. 分布式锁问题
分布式锁的基本实现有时会有一些问题,比如“锁超时”或“死锁”情况。为了解决这些问题,通常会采取以下一些改进措施:锁在获得后通常会设置一个过期时间 (timeout
),防止锁在某些客户端崩溃或发生故障时无法释放,从而导致其他客户端无法获取锁,如果时间过期了业务还没处理完,可以用一个子进程10秒钟判断一次锁重新设置过期时间。
# Redis高可用性
Redis 提供了多种机制来实现高可用性,确保即使某些节点发生故障,系统仍能正常运行。具体包括 主从复制、哨兵模式 和 集群模式,每种方式适应不同的需求和场景。
# 1. 主从复制 (Master-Slave Replication)
主从复制是 Redis 提供的一种基础高可用机制,它允许一个 Redis 实例作为主节点,其他实例作为从节点。
- 工作原理:主节点将所有数据变更(如写入、删除)同步到从节点,从节点会复制主节点的数据,并提供数据读取服务。通过这种方式,可以实现 读写分离,即主节点处理写请求,从节点处理读请求,从而提高系统的读取性能。
- 适用场景:适合 读多写少 的应用场景,因为写请求仍然只能由主节点处理,而读请求可以通过从节点来分担。
- 优缺点:
- 优点:简单易实现,能提高系统的读取能力。
- 缺点:主节点宕机时,从节点无法自动提升为主节点,仍然需要人工干预。
# 2. 哨兵模式 (Sentinel)
哨兵模式是在 Redis 中为高可用性设计的一个集群管理机制。它通过一个或多个独立的哨兵进程(Sentinel)来监控 Redis 实例,提供故障转移、自动故障恢复和系统通知等功能。
- 工作原理:
- 监控:哨兵不断监控 Redis 主节点和从节点的健康状态。
- 故障转移:当哨兵检测到主节点故障时,它会自动将一个从节点提升为新的主节点,并更新配置文件。然后,客户端可以通过新的主节点继续执行写操作。
- 通知:哨兵可以向其他客户端或服务提供通知,告知主节点发生了故障和故障转移的信息。
- 优缺点:
- 优点:通过自动故障转移机制,增强了系统的高可用性,即使主节点故障,服务仍然可以继续提供。
- 缺点:配置和管理相对较复杂,需要更多的资源来运行哨兵进程。
# 3. 集群模式 (Cluster Mode)
Redis 集群模式通过将数据分片存储在多个 Redis 节点上,来实现更高的可扩展性和高可用性。每个节点仅存储数据的一部分,通过分片的方式,数据能够在多个节点之间进行分布。
- 工作原理:
- 分片:集群将数据自动分片,每个节点存储一定范围的哈希槽(总共有 16384 个哈希槽)。数据根据键值的哈希值决定存储在哪个节点。
- 自动分区:当数据量增大时,可以动态地增加或减少节点,Redis 会自动处理数据的重新分配。
- 故障转移:集群模式支持自动故障转移。如果某个节点发生故障,集群会自动将该节点的数据迁移到其他健康节点,并进行故障恢复。
- 优缺点:
- 优点:支持 水平扩展,通过添加更多节点可以增加存储容量和计算能力。集群模式支持 自动分区 和 自动故障转移,提高了系统的容错性和可扩展性。
- 缺点:配置和管理相对复杂,需要处理分片、节点间通信等问题。集群模式下,不支持对单一键进行跨节点的多键事务操作。
# 如何保证缓存和数据库的数据⼀致性?
如果只有读操作,那么就不会造成不一致现象,当出险了写,和读并发的时候就会出现不一致现象。由于redis的更新代价比删除代价大,因此我们只考虑删除缓存。那么我们是先操作缓存再操作数据库,还是先操作数据库再操作缓存?
先操作缓存再操作数据库,如果写线程先删除缓存,再去更新数据库,在更新数据库这个操作过程中由于网络阻塞,这个时候读线程就会读取到旧数据来更新缓存,从而导致了数据的不一致,为了解决这个问题我们可以在写进程删除数据库后再进行一次删除缓存,,那么还会出现一个问题,如果是在读的进程去更新缓存前触发了删除那么缓存还是被更新成旧数据,这个时候可以用延迟双删,在延迟的这段时间内数据库是不一致,最终是一致的,如果非得保证一致性那么只能上锁了保证写的操作是原子的。
先操作数据库再操作缓存,我们先去更新数据库,再去删除缓存,在删除缓存前读到的数据可能是脏数据,但最终能保证数据的一致性,推荐这种方式,但是如果缓存删除失败了怎么办,这里可以用多种技术例如中间件等
# 雪崩、击穿、穿透
在分布式缓存系统中,常常会提到“缓存雪崩”、“缓存击穿”和“缓存穿透”这些问题,它们通常都是在缓存失效或不一致时引发的一系列问题。这些问题不仅影响缓存的性能,还可能对后端数据库或整个系统造成较大负担。以下是它们的具体含义和常见的解决方案。
# 1. 缓存雪崩(Cache Avalanche)
缓存雪崩是指在同一时间内大量缓存失效(通常是缓存过期),导致大量的请求直接访问数据库,从而给数据库带来巨大的压力,甚至可能导致数据库崩溃。
- 原因:缓存雪崩通常是由于所有缓存的过期时间都相同,或者缓存失效的时间设置得过短,导致大量请求同时访问缓存过期的数据,直接访问数据库。
- 举个例子:
- 假设缓存中的某些数据过期时间是 10 点过期,如果在 10 点的时刻,所有缓存的数据都失效了,用户的请求就会同时访问数据库,给数据库带来巨大的压力,可能导致数据库宕机。
解决方案:
- 缓存过期时间随机化:为缓存设置一个随机的过期时间,避免所有缓存同时过期。例如,可以在原本的过期时间上加上一个随机值,避免多个缓存同时失效。
- 分布式锁:在缓存失效的情况下,通过分布式锁机制,限制同一时间只有一个请求能够从数据库加载数据并更新缓存,防止缓存穿透对数据库造成的过大压力。
- 异步更新缓存:在缓存过期时,异步去数据库加载数据,避免由于同时访问数据库导致的压力。
- 双缓存策略:通过双缓存(即设置两层缓存,第一层缓存为短期缓存,第二层为长期缓存)来避免缓存雪崩的情况。
# 2. 缓存击穿(Cache Breakdown)
缓存击穿是指某个特定的缓存数据在短时间内失效或者被删除,而有大量请求并发访问这个缓存时,这些请求都会访问数据库,导致数据库压力激增。常见的情景是缓存中的某个数据在短时间内过期,且数据访问量非常高。
- 原因:缓存失效的数据恰好是高并发的热点数据,导致大量并发请求同时访问数据库。
- 举个例子:
- 假设某个热点数据的缓存失效,多个请求同时访问该数据,并且数据库没有缓存的情况下,所有请求都需要查询数据库并返回结果。
解决方案:
- 加锁(互斥锁):对于缓存失效的数据,可以通过加锁来保证在同一时刻只有一个请求去数据库查询数据并更新缓存,其他请求等待数据更新后直接读取缓存,避免大量请求同时访问数据库。
- 实现方法:使用 Redis 分布式锁或数据库锁,在缓存失效时,只有第一个请求能从数据库加载数据,其他请求等待数据加载完成后再获取缓存。
- 请求合并:将多个请求合并成一个请求,通过队列等方式处理并发请求,防止多次查询数据库。
- 预加载(提前加载缓存):当热点数据即将失效时,提前加载并更新缓存,避免大量请求同时查询数据库。
# 3. 缓存穿透(Cache Penetration)
缓存穿透是指查询的请求数据既不在缓存中,也不在数据库中。由于缓存无法命中且数据库也没有该数据,导致每次请求都要去数据库查询,增加数据库的压力。缓存穿透的发生通常是由于查询的数据不存在,导致每次都从数据库查询。
- 原因:客户端查询的数据不存在,缓存无法命中,数据库也没有该数据,而每次请求都绕过了缓存,直接访问数据库,造成数据库压力过大。
- 举个例子:
- 假设用户每次查询一个不存在的数据(比如用户输入的 ID 错误),这时数据库会返回空值,而缓存也没有这条数据。每次请求都会触发数据库查询,造成数据库压力,且缓存永远无法存储该数据。
解决方案:
- 缓存空值(Cache Null Value):当查询的数据不存在时,可以将一个特定的空值(如
null
或特殊标记)缓存一段时间,避免相同的请求再次访问数据库。例如,如果数据库查询返回null
,可以将null
缓存一段时间(如 5 分钟),这样下次相同的请求就会直接返回缓存的空值,而不是再查询数据库。 - 数据验证:在访问数据库前,通过某种方式验证请求的数据是否合法。如果查询的数据是无效的,可以直接返回错误,避免请求进入数据库查询。
- 布隆过滤器(Bloom Filter):使用布隆过滤器来判断数据是否存在。布隆过滤器是一种空间效率高的概率数据结构,能够快速判断一个元素是否在集合中。如果通过布隆过滤器检查某个请求的数据不在数据库中,则直接返回,避免请求数据库,减少对数据库的压力。