Redis知识
Redis为什么这么快
- Redis 基于内存,内存的访问速度比磁盘快很多;
- Redis 基于采用单线程模型;命令执行采用单线程,避免了多线程的锁竞争、上下文切换和死锁问题,Redis 6.0后网络I/O改为多线程,但命令执行仍为单线程,核心逻辑不变;
- Redis 使用 I/O 多路复用(如 epoll)来处理大量并发连接。I/O 多路复用允许一个线程同时监控多个客户端连接的输入输出,而不会阻塞在某个连接上。
- Redis 内置了高效数据类型/结构,性能非常高;(字符串(SDS动态字符串)、哈希(ziplist/哈希表)、有序集合(跳表+哈希表)等)。
5种基本数据类型
String(字符串)
String 是一种二进制安全的数据类型,可以用来存储任何类型的数据比如字符串、整数、浮点数、图片(图片的 base64 编码或者解码或者图片的路径)、序列化后的对象。
底层数据结构:简单动态字符串(SDS)
常用操作命令:
SET key value
:设置键值(如SET name "Redis"
)GET key
:获取值(如GET name
→ "Redis")INCR key
:数值自增(如INCR counter
)APPEND key value
:追加字符串(如APPEND name " DB"
→ "Redis DB")
使用场景
1.需要存储常规数据的场景
- 举例:缓存 Session、Token、图片地址、序列化后的对象(相比较于 Hash 存储更节省内存)。
- 相关命令:
SET
、GET
。
2.需要计数的场景
- 举例:用户单位时间的请求数(简单限流可以用到)、页面单位时间的访问数。
- 相关命令:
SET
、GET
、INCR
、DECR
。
3.分布式锁
利用 SETNX key value
命令可以实现一个最简易的分布式锁(存在一些缺陷,通常不建议这样实现分布式锁)。
List(列表)
底层数据结构:快速链表(QuickList,由多个压缩列表(ZipList)双向链接构成)
特点:双向操作高效,内存紧凑,元素较少时使用压缩列表。
常用操作命令:
LPUSH key value
:头部插入(如LPUSH tasks "task1"
)RPOP key
:尾部弹出(如RPOP tasks
→ "task1")LRANGE key start end
:范围查询(如LRANGE tasks 0 -1
获取全部)
使用场景
信息流展示
- 举例:最新文章、最新动态。
- 相关命令:
LPUSH
、LRANGE
。
消息队列
List
可以用来做消息队列,只是功能过于简单且存在很多缺陷,不建议这样做。
相对来说,Redis 5.0 新增加的一个数据结构 Stream
更适合做消息队列一些,只是功能依然非常简陋。和专业的消息队列相比,还是有很多欠缺的地方比如消息丢失和堆积问题不好解决。
Hash(哈希)
Redis 中的 Hash 是一个 String 类型的 field-value(键值对) 的映射表,特别适合用于存储对象,后续操作的时候,你可以直接修改这个对象中的某些字段的值。
底层数据结构:压缩列表(ZipList)或哈希表(HashTable)
常用操作命令:
HSET key field value
:设置字段值(如HSET user:1000 name "Alice"
)HGET key field
:获取字段值(如HGET user:1000 name
→ "Alice")HGETALL key
:获取所有字段(如HGETALL1000
)
使用场景
对象数据存储场景
- 举例:用户信息、商品信息、文章信息、购物车信息。
- 相关命令:
HSET
(设置单个字段的值)、HMSET
(设置多个字段的值)、HGET
(获取单个字段的值)、HMGET
(获取多个字段的值)
Set(集合)
Redis 中的 Set 类型是一种无序集合,集合中的元素没有先后顺序但都唯一,Set 提供了判断某个元素是否在一个 Set 集合内的重要接口。
基于 Set 轻易实现交集、并集、差集的操作,比如你可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。这样的话,Set 可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程。
底层数据结构:整数集合(IntSet)或哈希表(HashTable)
常用操作命令:
SADD key member
:添加成员(如SADD tags "db"
)SINTER key1 key2
:求交集(如SINTER tags1 tags2
)SMEMBERS key
:获取所有成员(如SMEMBERS tags
)
使用场景
需要存放的数据不能重复的场景
- 举例:网站 UV 统计(数据量巨大的场景还是
HyperLogLog
更适合一些)、文章点赞、动态点赞等场景。 - 相关命令:
SCARD
(获取集合数量) 。
需要获取多个数据源交集、并集和差集的场景
- 举例:共同好友(交集)、共同粉丝(交集)、共同关注(交集)、好友推荐(差集)、音乐推荐(差集)、订阅号推荐(差集+交集) 等场景。
- 相关命令:
SINTER
(交集)、SINTERSTORE
(交集)、SUNION
(并集)、SUNIONSTORE
(并集)、SDIFF
(差集)、SDIFFSTORE
(差集)。
需要随机获取数据源中的元素的场景
- 举例:抽奖系统、随机点名等场景。
- 相关命令:
SPOP
(随机获取集合中的元素并移除,适合不允许重复中奖的场景)、SRANDMEMBER
(随机获取集合中的元素,适合允许重复中奖的场景)。
ZSet(有序集合)
Sorted Set 类似于 Set,但和 Set 相比,Sorted Set 增加了一个权重参数 score
,使得集合中的元素能够按 score
进行有序排列,还可以通过 score
的范围来获取元素的列表。
底层数据结构:压缩列表(ZipList)或跳表(SkipList)+ 哈希表
常用操作命令:
ZADD key score member
:添加成员(如ZADD rank 90 "Alice"
)ZRANGE key start end
:按分数升序查询(如ZRANGE rank 0 -1 WITHSCORES
)ZREVRANK key member
:获取逆序排名(如ZREVRANK rank "Alice"
)
使用场景
需要随机获取数据源中的元素根据某个权重进行排序的场景
- 举例:各种排行榜比如直播间送礼物的排行榜、朋友圈的微信步数排行榜、王者荣耀中的段位排行榜、话题热度排行榜等等。
- 相关命令:
ZRANGE
(从小到大排序)、ZREVRANGE
(从大到小排序)、ZREVRANK
(指定元素排名)。
特殊类型
Bitmap(位图)
Bitmap 存储的是连续的二进制数字(0 和 1),通过 Bitmap, 只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身 。我们知道 8 个 bit 可以组成一个 byte,所以 Bitmap 本身会极大的节省储存空间。
底层结构:基于 String 类型,将字符串视为二进制位数组(最大支持 2³² 位)
常用命令:
命令 | 功能 | 示例 |
---|---|---|
SETBIT key 偏移量 值(0/1) | 设置位值 | SETBIT sign:user666 9 1 → 记录用户第 10 天签到 |
GETBIT key 偏移量 | 获取位值 | GETBIT sign:user666 9 → 返回 1 (已签到) |
BITCOUNT key | 统计 1 的数量 | BITCOUNT sign:user666 → 返回当月签到天数 |
BITOP AND/OR 新key key1 key2 | 位运算 | BITOP AND active_users day1 day2 → 取两天均活跃用户 |
使用场景
需要保存状态信息(0/1 即可表示)的场景
- 举例:用户签到情况、活跃用户情况、用户行为统计(比如是否点赞过某个视频)。
- 相关命令:
SETBIT
、GETBIT
、BITCOUNT
、BITOP
。
HyperLogLog(基数统计)
HyperLogLog 是一种有名的基数计数概率算法 ,基于 LogLog Counting(LLC)优化改进得来,并不是 Redis 特有的,Redis 只是实现了这个算法并提供了一些开箱即用的 API。
Redis 提供的 HyperLogLog 占用空间非常非常小,只需要 12k 的空间就能存储接近2^64
个不同元素。
底层结构:基于 概率算法,使用 12KB 固定内存空间估算超大基数(标准误差 0.81%)
常用命令
命令 | 功能 | 示例 |
---|---|---|
PFADD key 元素 | 添加元素 | PFADD uv:20240605 "user123" |
PFCOUNT key | 估算基数 | PFCOUNT uv:20240605 → 返回当日 UV 估值 |
PFMERGE 新key key1 key2 | 合并统计 | PFMERGE uv:month uv:202406* → 合并整月数据 |
使用场景
数量巨大(百万、千万级别以上)的计数场景
- 举例:热门网站每日/每周/每月访问 ip 数统计、热门帖子 uv 统计。
- 相关命令:
PFADD
、PFCOUNT
。
Geospatial(地理空间)
Geospatial index(地理空间索引,简称 GEO) 主要用于存储地理位置信息。通过 GEO 我们可以轻松实现两个位置距离的计算、获取指定位置附近的元素等功能。
底层结构:基于 ZSet(有序集合) 实现,使用 GeoHash 算法 将经纬度编码为 52 位整数作为 ZSet 的 Score 值
常用命令
命令 | 功能 | 示例 |
---|---|---|
GEOADD key 经度 纬度 成员 | 添加地理位置 | GEOADD shops 116.397128 39.916527 "商家A" |
GEODIST key 成员1 成员2 [单位] | 计算两地距离 | GEODIST shops "商家A" "商家B" km → 返回距离(千米) |
GEORADIUS key 经度 纬度 半径 单位 | 查询半径内位置 | GEORADIUS shops 116.40 39.90 5 km → 返回附近商家 |
GEOPOS key 成员 | 获取坐标 | GEOPOS shops "商家A" → 116.397128, 39.916527 |
使用场景
需要管理使用地理空间数据的场景
- 举例:附近的人,两个商家的距离
- 相关命令:
GEOADD
、GEORADIUS
、GEORADIUSBYMEMBER
Stream(流)
底层结构:持久化的消息队列,支持多消费者组
常用核心命令:
命令 | 功能 | 示例 |
---|---|---|
XADD key * field value | 添加消息 | XADD orders * product_id 5001 user_id 100 |
XREAD COUNT 10 STREAMS key 0 | 读取消息 | XREAD COUNT 10 STREAMS orders 0 → 获取最新 10 条 |
XGROUP CREATE key group $ | 创建消费组 | XGROUP CREATE orders group1 $ |
XREADGROUP GROUP group1 consumer1 | 消费组读取 | XREADGROUP GROUP group1 consumer1 COUNT 1 STREAMS orders > |
使用场景
- 消息队列:订单处理流水线(支持消息回溯和消费组)。
- 事件溯源:记录用户操作日志序列
Redis6.0为什么要引入多线程
Redis最初是单线程模型,主要处理网络I/O和命令执行,随着硬件发展,单线程模型在处理网络I/O时成为性能瓶颈,特别是在高并发场景下,网络I/O的等待时间会显著影响整体性能
所以为了提高网络 I/O 的并行度,Redis 6.0 对于网络 I/O 采用多线程来处理。但是对于命令的执行,Redis 仍然使用单线程来处理,所以不要误解 Redis 有多线程同时执行命令。
多线程主要用于:
网络写操作(向客户端返回结果),Redis 6.0 默认开启
网络读操作(接收客户端请求),需手动修改配置文件,在 redis.conf
中设置
//读请求也使用io多线程
io-threads-do-reads yes
同时, Redis.conf 配置文件中提供了IO 多线程个数的配置项:
// io-threads ,表示启用 N-1 个 I/0 多线程(主线程也算一个 I/0 线程)
io-threads 4
线程数设置(io-threads N
)
- 官方建议:
- 4 核 CPU → 设为 2 或 3;
- 8 核 CPU → 设为 6。
- 线程数需小于 CPU 核心数,避免过度竞争导致性能下降
缓存读写策略
Cache Aside Pattern(旁路缓存)
Cache Aside Pattern 是我们平时使用比较多的一个缓存读写模式,比较适合读请求比较多的场景。
Cache Aside Pattern 中服务端需要同时维系 db 和 cache,并且是以数据库的结果为准。
下面我们来看一下这个策略模式下的缓存读写步骤。
写:
- 先更新数据库
- 然后直接删除缓存。
读 :
- 先读Redis,命中则返回;
- 未命中则读数据库,回填缓存后返回。
Cache Aside Pattern 的缺陷:
缺陷 1:首次请求数据一定不在 cache 的问题
解决办法:可以将热点数据可以提前放入 cache 中。
缺陷 2:写操作比较频繁的话导致 cache 中的数据会被频繁被删除,这样会影响缓存命中率 。
解决办法:
- 强一致场景:更新数据库的时候同样更新缓存,不过我们需要加一个锁/分布式锁来保证更新 的时候不存在线程安全问题。
- 最终一致性:更新数据库的时候同样更新缓存,但是给缓存加一个比较短的过期时间,这样的话就可以保证即使数据不一致的话影响也比较小
Read/Write Through Pattern(读写穿透)
读写穿透模式将缓存作为数据访问的统一入口,应用程序不直接操作数据库,而是通过缓存层代理所有读写请求。缓存层负责与数据库同步数据,确保数据一致性。
写(Write Through):
- 更新缓存:应用将数据写入缓存层。
- 同步更新数据库:缓存层立即将数据写入数据库,保证缓存与数据库强一致
关键点:数据加载由缓存层完成,对应用透明。
读(Read Through):
- 查询缓存:应用先请求缓存获取数据。
- 缓存命中:若数据在缓存中且有效,直接返回数据。
- 缓存未命中:若数据不在缓存中,缓存层自动从数据库加载数据,写入缓存后返回给应用
关键点:写操作需同时更新缓存和数据库,且由缓存层保证原子性。
增强一致性的辅助技术
延迟双删(Delay-Double-Delete)
- 作用:解决旁路缓存模式下并发读写导致的脏数据问题。
流程:
写请求先删缓存 → 更新数据库 → 延迟一段时间(如500ms)→ 再次删缓存
原理:
延迟等待可能存在的并发读请求完成旧数据回填,二次删除清理残留脏数据
消息队列(MQ)
- 作用:
- 保证缓存操作的可靠性(如删除失败重试)。
- 解耦数据库操作与缓存更新。
- 应用场景:
- 旁路缓存中:数据库更新成功后,发送MQ消息触发缓存删除。
- 读写穿透中:缓存层更新数据库失败时,通过MQ重试。
Canal(Binlog监听)
- 作用:
- 监听数据库Binlog日志,异步触发缓存更新/删除,完全解耦应用代码。
- 和MQ协同:
- 旁路缓存:Canal捕获数据库变更 → 发送消息到MQ → 消费者删除缓存。
- 读写穿透:Canal可替代缓存层自带的同步机制,提供更可靠的最终一致性保障。
它们之间的关系:
旁路缓存和读写穿透是基础模式,而延迟双删、消息队列和Canal是解决这些模式中缓存一致性问题的具体方案。
在实际应用中,这些技术常常组合使用:
- 可以使用旁路缓存模式
- 配合延迟双删解决短期不一致
- 使用消息队列处理异步更新
- 通过Canal实现最终一致性
选择哪种方案取决于具体场景:
- 对一致性要求高的场景:读写穿透 + Canal
- 对性能要求高的场景:旁路缓存 + 延迟双删
- 对可靠性要求高的场景:旁路缓存 + 消息队列
最佳实践:
- 小规模应用:旁路缓存 + 延迟双删
- 大规模应用:旁路缓存 + 消息队列 + Canal
- 特殊场景:读写穿透 + 消息队列
这些技术都是为了解决缓存一致性问题,但各有侧重,需要根据具体业务场景选择合适的组合方案。
Redis的持久化
Redis持久化机制是将内存中的数据保存到磁盘,防止服务宕机时数据丢失的关键功能。它主要包含RDB快照、AOF日志和混合持久化(RDB-AOF) 三种方式,各自适用不同场景。
RDB(Redis Database)快照持久化
原理:定时生成内存数据的二进制快照(dump.rdb
),保存某一时刻的全量数据。
触发方式:
- 自动触发:通过配置
save <seconds> <changes>
规则(如save 900 1
表示900秒内至少1个键修改)。 - 手动触发:
SAVE
:同步阻塞主线程,直到快照完成(生产环境慎用)。BGSAVE
:异步生成快照,主进程fork子进程处理(利用写时复制COW技术,写操作不阻塞主线程)。
- 其他场景:主从复制时主节点生成RDB、执行
FLUSHALL
或SHUTDOWN
命令。
优点:
- 文件紧凑(二进制压缩),恢复速度快,适合备份与灾难恢复。
- 对性能影响较小(子进程处理)。
缺点:
- 可能丢失最后一次快照后的数据(依赖定时策略)。
- 数据量较大时,
fork
子进程可能阻塞主线程(内存越大阻塞越长)
AOF(Append Only File)日志持久化
原理:记录所有写操作命令(文本格式),重启时重放命令恢复数据。
工作流程:
- 命令追加:写操作先写入AOF缓冲区,再根据策略同步到磁盘。
- 同步策略(
appendfsync
配置):always
:每次写操作同步磁盘(数据安全,性能差)。everysec
(默认):每秒同步(平衡安全与性能)。no
:由操作系统决定同步时间(性能最优,可能丢数据)。
- AOF重写:压缩日志体积(如合并多条命令),因为AOF会记录对同一个key的多次写操作,但只有最后一次写操作才有意义。通过执行bgrewriteaof命令,可以让AOF文件执行重写功能,用最少的命令达到相同效果。由
BGREWRITEAOF
子进程异步执行,避免阻塞。
优点:
- 数据安全性高(最多丢失1秒数据)。
- 可读性强(文本格式),支持故障修复。
缺点:
- 文件体积大,恢复速度慢于RDB。
- 高写入负载时,频繁
fsync
可能影响性能。
混合持久化(RDB-AOF)
原理:Redis 4.0+支持,结合RDB和AOF优势。AOF文件由RDB格式快照 + 增量AOF命令组成。
- 触发条件:开启
aof-use-rdb-preamble yes
后执行BGREWRITEAOF
。 - 工作流程:
- 首先生成当前数据的RDB二进制快照写入AOF文件。
- 后续增量命令以AOF格式追加。
优点:
- 恢复速度快(优先加载RDB快照)。
- 数据安全性高(增量命令不丢失)。
适用场景:对数据安全性和恢复速度均有要求的场景。
对比
特性 | RDB | AOF | 混合持久化 |
---|---|---|---|
数据安全性 | 较低(可能丢数据) | 高(最多丢1秒数据) | 高 |
恢复速度 | 快(二进制加载) | 慢(需重放命令) | 快(RDB优先加载) |
文件体积 | 小(压缩存储) | 大(记录所有命令) | 中等 |
性能影响 | 低(子进程处理) | 中高(频繁fsync ) | 中 |
适用场景 | 备份、容灾、对丢失数据不敏感 | 金融交易等高安全性场景 | 兼顾安全与速度的综合需求 |
- 优先RDB:需要定期备份(如每日备份)、容忍分钟级(命令自定义)数据丢失的场景(缓存数据)。
- 优先AOF:要求数据零丢失(如交易系统),可接受恢复时间较长。
- 混合模式:生产环境推荐方案,尤其Redis 4.0+版本。
Redis内存管理
Redis如何判断Key是否过期
过期字典是一个独立的哈希表(Hash Table),与存储键值对的主字典分离。
- 键(Key):指向 Redis 数据库中的某个键(即键对象的指针)。
- 值(Value):存储一个
long long
类型的整数,表示该键的毫秒级 UNIX 时间戳(绝对过期时间)。
示例:
- 执行
EXPIRE user:1000 3600
后,过期字典中会记录:user:1000 → 1717027200000
(假设当前时间戳为1717023600000
+ 3600秒)。
判断 Key 是否过期的流程
当需要判断某个 Key 是否过期时,Redis 按以下步骤操作:
1.检查是否存在过期时间
- 在过期字典中查找该 Key:
- 若不存在,说明未设置过期时间,直接返回有效。
- 若存在,获取其存储的过期时间戳(
expire_time
)。
2.比较时间戳
- 获取当前系统时间戳(
current_time
),单位为毫秒。 - 若
current_time > expire_time
,则判定为已过期;否则为未过期。
3.删除过期键
- 若判定为过期,Redis 会立即删除该键(惰性删除策略),并同步更新主字典和过期字典。
Redis过期Key删除策略
惰性删除
当客户端访问 Key 时(如执行 GET
命令),Redis 会先检查 Key 是否过期。若过期则立即删除并返回 nil
,否则正常返回数据。
- 优点 :对CPU,只会在使用该key时才会进行过期检查,对于很多用不到的key不用浪费时间进行过期检查
- 缺点 :对内存不友好,如果一个key已经过期,但是一直没有使用,那么该key就会一直存在内存中,内存永远不会释放
定期删除
Redis 默认每秒执行 10 次(通过 hz
参数配置)后台任务,每次随机抽取部分设置了过期时间的 Key 进行检查:
- 从每个数据库的过期字典中随机选择 20 个 Key;
- 删除其中已过期的 Key;
- 若过期 Key 比例超过 25%,则重复步骤 1-2,直到比例低于阈值或达到 CPU 时间上限(默认单次任务最多占用 25ms)
定期清理有两种模式:
- SLOW模式是定时任务,执行频率默认为10hz,每次不超过25ms,以通过修改配置文件redis.conf的hz选项来调整这个次数
- FAST模式执行频率不固定,但两次间隔不低于2ms,每次耗时不超过1ms
优点:可以通过限制删除操作执行的时长和频率来减少删除操作对 CPU 的影响。另外定期删除,也能有效释放过期键占用的内存。
缺点:难以确定删除操作执行的时长和频率。
Redis的过期删除策略:惰性删除 + 定期删除两种策略进行配合使用
数据淘汰策略
当Redis中的内存不够用时,此时在向Redis中添加新的key,那么Redis就会按照某一种规则将内存中的数据删除掉,这种数据的删除规则被称之为内存的淘汰策略。
不淘汰策略
1.noeviction
(默认策略)
- 机制:内存满时拒绝所有写入操作(返回错误),但允许读取和删除
- 适用场景:对数据完整性要求极高且能接受写入失败的系统(如关键配置存储)
全局淘汰策略(对所有 Key 生效)
2.allkeys-lru
- 机制:基于 LRU(最近最少使用)算法,淘汰最久未被访问的 Key。
- 适用场景:通用缓存系统(如热点新闻、商品详情),保留高频访问数据。
3.allkeys-lfu
(Redis 4.0+)
- 机制:基于 LFU(最不经常使用)算法,淘汰访问频率最低的 Key。
- 适用场景:需按访问频次淘汰的场景(如短时高频数据缓存)。
4.allkeys-random
- 机制:随机淘汰任意 Key。
- 适用场景:数据访问模式均匀且无优先级差异的场景。
仅淘汰过期 Key 策略(对设置了 TTL 的 Key 生效)
5.volatile-lru
(部分云服务默认,如阿里云 Tair)
- 机制:在过期 Key 中使用 LRU 算法淘汰最久未访问的 Key。
- 适用场景:需保留永久数据(无 TTL)但清理低频缓存(如用户会话)。
6.volatile-lfu
(Redis 4.0+)
- 机制:在过期 Key 中使用 LFU 算法淘汰访问频率最低的 Key。
- 适用场景:过期数据中存在明显高频/低频差异(如广告点击统计)。
7.volatile-random
- 机制:在过期 Key 中随机淘汰。
- 适用场景:无需关注具体被淘汰的过期数据(如临时日志)。
8.volatile-ttl
- 机制:优先淘汰 剩余存活时间最短(TTL 最小) 的 Key。
- 适用场景:需快速清理即将过期的数据(如限时优惠券)
LRU算法(最近最少使用):基于 时间局部性原理,认为 最近被访问的数据更可能被再次使用。淘汰策略优先移除 最久未被访问 的数据。使用场景:有明显 热点数据 且访问模式随时间变化(如浏览器缓存、数据库缓冲区)
LFU算法(最不经常使用):基于 访问频率,认为 历史访问次数高的数据未来更可能被使用。淘汰策略优先移除 访问频率最低 的数据。使用场景:长期稳定热点数据(如推荐系统热门商品、搜索引擎关键词缓存)
Redis生产问题
缓存穿透
请求的数据在缓存和数据库中均不存在,导致每次请求都直达数据库(如恶意攻击查询不存在的 ID)
解决方法:
- 缓存空数据:对查询为空的请求,缓存空值(如
null
)并设置较短 TTL(如 5 分钟),后续请求直接返回空。 - 使用布隆过滤器,但是存在一定的误判率。
- 接口限流,根据用户或者 IP 对接口进行限流,对于异常频繁的访问行为,还可以采取黑名单机制,例如将异常 IP 列入黑名单。缓存击穿和雪崩都可以配合接口限流来解决。
缓存击穿
当某个热点数据缓存突然失效(如过期),大量并发请求同时穿透缓存直达数据库,导致数据库瞬时压力激增。
解决方法:
- 永不过期(不推荐):设置热点数据永不过期,做逻辑过期,给某个字段设置上过期时间,每次get时比较。
- 提前预热(推荐):针对热点数据提前预热,将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期。
- 加锁(互斥锁/分布式锁):在缓存失效后,通过设置互斥锁确保只有一个请求去查询数据库并更新缓存。
缓存雪崩
大量缓存数据在同一时间大规模失效,可能原因缓存同时过期或者Redis服务故障,导致所有请求涌向数据库,引发连锁崩溃。
解决方法:
- 分散缓存过期时间,给不同的Key的TTL添加随机值,如(
基础时间 + 随机分钟
),避免同时失效 - 高可用与集群化,部署 Redis 主从集群、哨兵模式或云托管服务(如 AWS ElastiCache),避免单点故障
- 熔断降级,引入熔断器(如 Hystrix、Sentinel),当数据库压力过大时,直接返回默认值或错误页,保护后端
- 多级缓存架构,结合本地缓存 + Redis + 数据库,层级兜底(如本地缓存失效后读 Redis)
Redis集群
主从复制
单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,就需要搭建主从集群,主节点处理写操作,从节点异步复制数据,支持读写分离。
主从数据同步原理
Replication Id:简称replid,是数据集的标记,id一致则说明是同一数据集。每一个master都有唯一的replid,slave则会继承master节点的replid
offset:偏移量,随着记录在repl_baklog中的数据增多而逐渐增大。slave完成同步时也会记录当前同步的offset。如果slave的offset小于master的offset,说明slave数据落后于master,需要更新。
主从全量同步:
主从增量同步(slave重启或后期数据变化):
哨兵模式
哨兵的作用
Redis提供了哨兵(Sentinel)机制来实现主从集群的自动故障恢复。哨兵的结构和作用如下:
- 监控:Sentinel 会不断检查您的master和slave是否按预期工作
- 自动故障恢复:如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主
- 通知:Sentinel充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端
服务状态监控
Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令:
- 主观下线:如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线。
- 客观下线:若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好超过Sentinel实例数量的一半。
哨兵选主规则
- 首先判断主与从节点断开时间长短,如超过指定值就排该从节点
- 然后判断从节点的slave-priority值,越小优先级越高
- 如果slave-prority一样,则判断slave节点的offset值,越大优先级越高
- 最后是判断slave节点的运行id大小,越小优先级越高。
redis集群(哨兵模式)脑裂
分片集群
主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决:
- 海量数据存储问题
- 高并发写的问题
使用分片集群可以解决上述问题,分片集群特征:
- 集群中有多个master,每个master保存不同数据
- 每个master都可以有多个slave节点
- master之间通过ping监测彼此健康状态
- 客户端请求可以访问集群任意节点,最终都会被转发到正确节点
数据读写
Redis 分片集群引入了哈希槽的概念,Redis 集群有 16384 个哈希槽,每个 key通过 CRC16 校验后对 16384 取模来决定放置哪个槽,集群的每个节点负责一部分 hash 槽。