Redis技术笔记-从三大缓存问题到高可用集群落地实战
目录
前言
一、非关系数据库介绍
(一)定义
(二)分类
二、Redis 介绍
(一)简介
(二)核心特点
(三)Redis 适用场景
(四)Redis 不适用场景
三、Redis的容错机制
四、缓存策略与优化
(一)缓存穿透
问题
解决方案
(二)缓存击穿
问题
解决方案
(三)缓存雪崩
问题
解决方案
五、搭建Redis集群
(一)Redis集群介绍
(二)基本环境准备
主机准备
集群环境准备
(三)创建集群
(四)查看集群
(五)测试集群
验证数据分布
测试数据自动备份
测试服务高可用(故障转移)
搭建Web服务器并连接Redis集群
前言
- 前面的博客详细介绍了MySQL的基本使用和优化,可在主页查看相关的MySQL博客进行了解。
- 本文介绍Redis及缓存的三大问题缓存穿透、缓存击穿、缓存雪崩和Redis集群的搭建,下一篇介绍Redis的主从配置和Redis的哨兵及Redis的数据持久化。
注:如果想进一步了解Redis的使用的可以参考Redis中文文档教程
一、非关系数据库介绍
(一)定义
- 非关系型数据库泛指不遵循传统关系模型(表、行、列、SQL)的数据存储系统。
- 它们通过更灵活的数据模型、横向扩展架构以及针对特定场景的优化,解决高并发、海量数据、高可用与多活等问题。
(二)分类
数据模型 | 代表系统 | 典型场景 | 关键特性与注意事项 |
---|---|---|---|
键值对 | Redis | 缓存、会话、分布式锁 | 全内存、单线程命令执行、RDB/AOF 持久化、主从+哨兵/集群高可用 |
文档 | MongoDB | 内容管理、订单详情 | BSON 存储、动态 Schema、副本集、分片集群 |
检索 | ElasticSearch | 全文检索、日志分析 | 倒排索引、近实时搜索、分片+副本、DSL 查询 |
列族 | HBase | 写密集型、宽表存储 | LSM-Tree、Rowkey 设计决定性能、依赖 HDFS |
图 | Neo4j | 关系网络、推荐 | ACID 事务、Cypher 查询、节点-边-属性模型 |
时序 | Prometheus | 监控指标 | 拉模式采集、PromQL、本地 TSDB、Alertmanager 告警 |
向量 | Milvus / Qdrant | 语义检索、推荐 | 支持 IVF、HNSW 索引、GPU 加速、高维向量近似搜索 |
二、Redis 介绍
(一)简介
Redis(Remote Dictionary Server)是开源的、基于内存的键值存储系统,支持多种数据结构(String、Hash、List、Set、Sorted Set、Bitmap、HyperLogLog、Stream、Geo 等)。
(二)核心特点
特性维度 | 技术实现 | 优点 |
---|---|---|
高性能 | 全内存存储 + 单线程网络模型 + 非阻塞 I/O | 微秒级响应,缓存层 QPS 10 万级,读写极快,显著降低后端负载 |
多数据结构 | 原生支持 String、Hash、List、Set、Sorted Set、Bitmap、HyperLogLog、Geo、Stream | 同一实例即可覆盖缓存、计数、队列、排行榜、去重、位置服务等场景,减少组件数量 |
原子操作 | 单线程顺序执行命令,无锁竞争 | 无需显式加锁即可实现 INCR、SET NX、Lua 脚本等业务级原子逻辑,避免并发问题 |
高可用 | 主从复制 + Sentinel 自动故障转移 + Cluster 分片 | 故障秒级切换,水平扩展无业务侵入,确保服务持续在线 |
双持久化 | RDB 快照(快速恢复)+ AOF 追加日志(低丢数) | 宕机后分钟级恢复,数据丢失控制在秒级,兼顾速度与可靠性 |
轻量消息 | Pub/Sub、Stream 消费组、Lua 原子脚本 | 无需引入 Kafka 即可完成低延迟消息与事件驱动,架构更简洁 |
(三)Redis 适用场景
- 缓存:将热点查询结果、会话状态或配置信息缓存在内存,降低后端数据库压力。
- 排行榜:使用有序集合(ZSET)按分数排序,实现实时积分、销量或热度排行。
- 计数器:利用 INCR、DECR 等原子指令,支持高并发 UV、PV、库存扣减。
- 社交网络:以集合(SET)或哈希(HASH)存储好友关系、共同关注、点赞列表。
- 消息队列:通过 LIST 的 LPUSH/BRPOP 或 STREAM 实现轻量级任务队列与事件流。
- 地理位置:基于 GEO 命令计算两点距离、附近人搜索。
- 分布式锁:SET key value NX PX ttl 保证跨节点互斥。
- 发布/订阅:频道广播实现实时通知、配置推送。
(四)Redis 不适用场景
- 超大规模冷数据:单实例内存有限,亿级冷日志建议落盘到 HDFS/S3。
- 复杂事务:不支持跨键 ACID 事务,金融转账等场景需关系型数据库。
- 大对象存储:单值超过 512 MB 时需拆片或使用对象存储。
- 长时间持久化:RDB/AOF 极端宕机仍可能丢数,关键订单需双写。
三、Redis的容错机制
机制 | 作用 | 实现要点 |
---|---|---|
哈希槽分片 | 将 16384 个槽均匀分布到各主节点,实现水平扩展;迁移期间新旧节点均可用,ASK 重定向保障读写不中断 | 节点增减时自动迁移槽,客户端按 CRC16(key) mod 16384 定位槽 |
主从复制 | 每个主节点拥有 ≥1 个从节点,异步复制数据,支持读写分离;主故障时复制偏移量最大的从节点晋升 | 全量 RDB + 增量 AOF,可配置 min-replicas-max-lag 控制复制延迟 |
故障检测 | 每秒心跳探测,主观下线 → 客观下线;仅主节点投票,避免脑裂 | 基于半数以上主节点投票,cluster-node-timeout 定义超时阈值 |
故障转移 | 哨兵或集群自动选主,<1 秒完成切换;切换后通过 CLUSTER SETSLOT 广播新拓扑 | 从节点按复制偏移量与节点 ID 选举,重新分配槽并更新客户端路由表 |
Gossip 协议 | 节点间周期性交换状态,维护无中心集群视图;网络分区时依赖多数派收敛 | 每秒随机选 3 个节点交换 ping/pong 消息,消息体含节点 ID、槽位、故障标记 |
客户端重定向 | 节点返回 MOVED/ASK 响应,客户端重发请求;主流客户端内置缓存,无需停机 | MOVED 指示槽位已迁移,ASK 临时转发请求,客户端缓存槽位映射 |
四、缓存策略与优化
(一)缓存穿透
问题
原因 | 目标 |
---|---|
大量非法请求查询数据库不存在的数据 → 既无法命中缓存,也查不到 DB → 每次请求都穿透缓存直达数据库。 | 让“空结果”也具备缓存能力,堵住流量洪峰。 |
解决方案
- 缓存空结果
- 对于查询结果为空的数据,在缓存中记录短时间的空值标记(如
##
),后续相同查询直接返回空值,避免重复回源。 - 作用:阻断针对不存在数据的恶意或异常高频请求。
- 建议:空值标记设置 30–120 秒过期,既防止缓存穿透,又避免长期占用内存。
- 对于查询结果为空的数据,在缓存中记录短时间的空值标记(如
- 参数合法性校验
- 在网关或业务入口层对查询参数进行范围、格式、签名等校验,非法请求直接拒绝。
- 作用:在缓存层之前拦截无效流量,降低缓存与数据库压力。
- 建议:将 ID 范围、正则规则、签名密钥配置化,支持热更新,提升灵活性。
- 使用布隆过滤器
- 将所有可能存在的数据键写入布隆过滤器;查询先过过滤器,不存在则立即返回,存在再走缓存与数据库。
- 作用:以极小内存代价拦截 100% 不存在的数据请求,提升缓存命中率。
- 建议:过滤器采用位图或 Redis 模块实现,定期异步重建,保持假阳性率低于 1%。
(二)缓存击穿
问题
原因 | 目标 |
---|---|
热点 Key 失效瞬间,大量并发请求同时回源 → DB 瞬时 QPS 飙升,甚至崩溃。 | 保证同一时刻仅一个线程回源,其余线程等待。 |
解决方案
- 互斥锁
- 在缓存失效瞬间,通过分布式锁(如 Redis SET NX PX)仅允许一个线程回源数据库,其余线程阻塞等待。
- 作用:避免并发回源穿透数据库,有效防止击穿风险。
- 建议:注意加锁的开销,可能会导致大量线程阻塞等待锁,形成锁竞争,降低并发性能。
- 软过期+互斥锁
- 在缓存 value 中写入逻辑过期时间 T1(T1 < 实际 TTL T2)。
- 读取时先检查 T1:若未过期直接返回;若已过期,仅让一个线程获取分布式锁回源更新,其余线程仍返回旧值。
- 作用:相比单纯的互斥锁方案,能进一步减少读请求线程的阻塞时间。
- 建议:合理设置逻辑过期时间和互斥锁的过期时间,结合重试机制,可以提高系统在高并发场景下的稳定性和性能。
- 静态数据+Lazy Expiration
- 在 Redis 中不设置 TTL,使 key 表面“永不过期”;在 value 头部写入 8-byte 逻辑过期时间戳。读取时先比对当前时间:
- 未过期:直接返回;
- 已过期:仅让一个线程抢分布式锁,后台异步线程执行回源并写回新值,其余线程仍读旧值,无阻塞。
- 作用:性能最好,避免了频繁的缓存失效和重建,系统吞吐最稳定。
- 建议:
- 逻辑过期时间:在缓存值中设置一个逻辑过期时间,当取值时判断过期时间是否已经到达。
- 异步更新:如果逻辑过期时间已过,则启动一个异步线程来更新缓存,而不是阻塞当前请求。
- 互斥锁:避免多线程同时更新缓存,使用互斥锁来保证只有一个线程能进行更新操作。
- 在 Redis 中不设置 TTL,使 key 表面“永不过期”;在 value 头部写入 8-byte 逻辑过期时间戳。读取时先比对当前时间:
- 自动续期
- 为热点 key 预设 30 min TTL,并启动周期任务:在 key 剩余 10 min 时回源刷新数据,成功后重置 TTL 为 30 min。
- 作用:避免缓存频繁失效,减少对数据库的压力。
- 建议:
- 续期任务使用 Lua 脚本保证 GET + SET PX 原子操作,避免并发回源。
- 采用分布式定时框架(如 Quartz + Redis 分布式锁)单实例执行,防止多节点重复刷新。
- 暂时缓存不失效
- 活动开始前,通过预热脚本把热点数据一次性加载到 Redis 并 不设置 TTL;活动结束后,统一脚本或消息触发 批量删除。
- 作用:避免热点数据频繁失效,提升系统的性能。
- 建议:
- 预热阶段
- 使用预热任务灰度执行,写入完成后通过
CLUSTER NODES
确认所有槽位同步成功,再开放流量。 - 写入命令:
SET key value
(无 PX/EX 参数)。
- 使用预热任务灰度执行,写入完成后通过
- 活动结束清理
- 采用
SCAN
+UNLINK
分批删除,避免一次性DEL
大 key 导致节点阻塞; - 或把热点 key 统一放在
hot:{bizId}:*
命名空间,活动后用FLUSHDB
/UNLINK
通配删除。
- 采用
- 兜底策略
- 预热失败或数据变更时,通过后台补偿任务以 Lazy Expiration 方式异步修正,保证最终一致。
- 预热阶段
(三)缓存雪崩
问题
原因 | 目标 |
---|---|
大批 Key 同时失效 或 缓存集群整体故障 → 流量瞬间压垮 DB。 | 让失效分散化 + 集群高可用 + 下游限流降级。 |
解决方案
- 分散过期时间
- 在原始 TTL 上叠加 0-300 秒随机偏移,使用纳秒级随机避免伪随机聚集;对同一业务批次 key 采用滑动窗口方式错峰。
- 作用:避免缓存key在同一时间失效,防止大量 key 同时回源。
- 提前演练压测
- 上线前模拟缓存全量失效、节点宕机、网络延迟三种场景;输出 QPS-RT 曲线与数据库安全阈值,据此调整连接池、线程池及分片数。
- 作用:提前暴露瓶颈,给出容量基线。
- 缓存高可用+后端数据库限流
- 双缓存热备:主备集群跨机架部署,使用客户端双写或异步复制;
- 数据库限流:Hystrix 线程池隔离,熔断阈值按压测峰值 80% 设置,熔断后降级本地缓存或默认值。
- 作用:缓存故障时仍保证核心链路可用。
- 服务降级
- 全局开关:30 秒内 Redis 失败 ≥5 次即开启,请求直接返回配置中心兜底数据;
- 自愈探针:后台任务每 30 秒探测 Redis,连续两次成功即关闭开关,恢复实时数据。
- 作用:缓存连续异常时快速兜底,保护下游。
五、搭建Redis集群
- 步骤:基本环境准备 → 创建集群 → 查看集群信息→ 测试集群
(一)Redis集群介绍
- Redis 集群通过数据分片与复制机制,实现了高可用性和水平扩展。
- 集群将 16384 个哈希槽(Hash Slot)均匀分布到各个主节点上,每个主节点负责一部分槽的数据。
- 每个槽通常会有多个副本存储在不同的从节点上,以防止节点故障导致数据丢失。
- 当有新的节点加入或节点移除时,槽会自动迁移,确保数据分布的均衡性。
(二)基本环境准备
主机准备
主机名 | IP地址 | 角色 |
---|---|---|
Redis50 | 192.168.88.50 | Master |
Redis51 | 192.168.88.51 | Slave |
Redis52 | 192.168.88.52 | Master |
Redis53 | 192.168.88.53 | Slave |
Redis54 | 192.168.88.54 | Master |
Redis55 | 192.168.88.55 | Slave |
Nginx56 | 192.168.88.56 | Web服务器 |
- 注:用于构建集群的主机不应存储任何数据,并且不应设置连接密码,配置SELINUX和关闭防火墙。
集群环境准备
- 注:所有主机都需要执行以下步骤,仅将bind之后的IP地址修改为对应主机的IP地址。
yum -y install redis #安装Redis服务vim /etc/redis.conf #修改配置文件 #69行 bind 192.168.88.50 #指定 Redis 服务监听的 IP 地址#92行 port 6379 #指定 Redis 服务监听的端口号#838行 cluster-enabled yes #启用 Redis 集群模式#846行 cluster-config-file nodes-6379.conf #指定集群配置文件的路径和名称#852行 cluster-node-timeout 5000 #设置集群节点间通信的超时时间(单位:毫秒)systemctl start redis #启动Redis服务ss -antlup | grep redis #检查Redis服务的端口信息 客户端端口(6379)处理客户端请求 集群总线端口(16379)Redis节点之间的内部通信
(三)创建集群
- 注:在任意一台主机执行一次即可。
redis-cli --cluster create \192.168.88.50:6379 192.168.88.52:6379 192.168.88.54:6379 \192.168.88.51:6379 192.168.88.53:6379 192.168.88.55:6379 \--cluster-replicas 1#解释 #redis-cli --cluster create 创建 Redis 集群,自动分配哈希槽(16384 个槽)并建立主从关系 #IP地址 指定节点列表 #-cluster-replicas 1 指定每个主节点的从节点数量 #Redis集群的创建工具会严格按照节点列表的顺序分配主从角色,比如节点列表中的前三个作为主节点,后三个作为从节点
(四)查看集群
redis-cli --cluster info 192.168.88.50:6379
#连接到指定的 Redis 集群节点(192.168.88.50:6379),并输出该节点所在集群的整体状态信息
(五)测试集群
验证数据分布
- 目的:确认写入的三条测试键已根据 CRC16 算法均匀落入三个主节点的哈希槽,且可通过任意节点透明访问。
redis-cli -c -h 192.168.88.51 -p 6379 #-c 启用集群模式 -h <host> 指定Redis服务器的IP地址 -p <port> 指定Redis服务器的端口号set test1 t1 #设置测试用的键值对 set test2 t2 set test3 t3redis-cli --cluster info 192.168.88.50:6379 #查看集群状态信息,预期数据均匀分布在三个主节点上 #比如分布在50、52、54主机上
测试数据自动备份
- 目的:验证 Slave 实时同步 Master 数据,实现零人工干预的备份。
#通过命令行连接Slave节点查看数据,预期能看到之前存储的test1和test2和test3键 redis-cli -c -h 192.168.88.51 -p 6379 192.168.88.51:6379> keys *redis-cli -c -h 192.168.88.53 -p 6379 192.168.88.53:6379> keys *redis-cli -c -h 192.168.88.55 -p 6379 192.168.88.55:6379> keys *
测试服务高可用(故障转移)
- 目的:验证 Master 宕机 → Slave 自动升主 → 恢复后自动降级为从 的完整流程。
- 当前集群拓扑
Master Slot Range Slave Redis50 0-5460 Redis51 Redis52 5461-10922 Redis53 Redis54 10923-16383 Redis55
- 模拟 Redis50 Master 宕机
#Redis52执行 systemctl stop redis
- 查看故障转移结果
#Redis50主机执行 #查看集群状态信息 redis-cli --cluster info 192.168.88.50:6379#预期能看到Redis53节点上升为主节点
- 原 Master 恢复并自动降级
#Redis52执行 systemctl start redis#Redis50执行 redis-cli --cluster info 192.168.88.50:6379#预期能看到Redis52自动成为Redis53的从节点
搭建Web服务器并连接Redis集群
- 目的:在 Nginx56 主机上完成 LNMP + Redis-Cluster 的端到端集成,实现 高并发缓存读写、水平扩展、故障自愈 三大目标。
- 注:在Nginx56主机上搭建LNMP平台,php-fpm以非sock的方式运行,搭建可以参考Nginx技术笔记-从LNMP架构到反向代理、负载均衡、灰度发布的全攻略
- 配置Redis PHP扩展
tar -xf redis-cluster-4.3.0.tgz #解压 Redis 集群的源代码包(redis-cluster-4.3.0.tgz)到当前目录,需要提前下载cd redis-4.3.0 phpize #生成 PHP 扩展的配置脚本(configure),为后续编译做准备./configure --with-php-config=/usr/bin/php-config #配置 PHP 扩展的编译参数,指定 PHP 的配置路径(php-config)make && make install #make 根据 Makefile 编译 Redis 扩展 #make install 将编译后的扩展文件(如 redis.so)安装到指定目录(如 /usr/lib64/php/modules/)ls /usr/lib64/php/modules/redis.so #验证 redis.so 文件是否已成功生成vim /etc/php.ini #修改 PHP 的全局配置文件,启用 Redis 扩展 #737行 extension_dir = "/usr/lib64/php/modules/" #指定 PHP 扩展文件的存放路径#739行 extension = "redis.so" #启用 Redis 扩展systemctl restart php-fpm #重启 PHP-FPM 服务,使配置生效 php -m | grep redis #验证 Redis 扩展是否已成功加载
- 编写存储脚本
vim /usr/local/nginx/html/set.php <?php #定义一个包含 Redis 集群节点地址和端口的数组 $redis_list = ['192.168.88.50:6379','192.168.88.51:6379','192.168.88.52:6379','192.168.88.53:6379','192.168.88.54:6379','192.168.88.55:6379' ];#初始化一个 Redis 集群客户端对象 #NULL 表示不使用密码认证 #$redis_list 传入的 Redis 节点列表 $client = new RedisCluster(NULL, $redis_list);#向 Redis 集群中写入三个键值对 $client->set("test50","t50"); $client->set("test52","t52"); $client->set("test54","t54C"); echo "SAVE OK\n"; ?>
- 编写查看脚本
vim /usr/local/nginx/html/get.php <?php#定义Redis集群节点列表,用于建立 RedisCluster 客户端连接 $redis_list = ['192.168.88.50:6379','192.168.88.51:6379','192.168.88.52:6379','192.168.88.53:6379','192.168.88.54:6379','192.168.88.55:6379' ];#初始化一个 Redis 集群客户端对象 $client = new RedisCluster(NULL, $redis_list);#从 Redis 集群中读取三个键的值,并输出到客户端 echo $client->get("test50"); echo $client->get("test52"); echo $client->get("test54"); ?>
- 访问测试
curl http://192.168.88.56/set.php #返回结果 SAVE OKcurl http://192.168.88.56/get.php #返回结果 t50 t52 t54
- 命令行验证数据分布
# 以集群模式连接到 Redis 节点并查看数据 redis-cli -c -h 192.168.88.50 -p 6379 192.168.88.50:6379> keys * #列出当前节点上所有的键redis-cli -c -h 192.168.88.52 -p 6379 192.168.88.52:6379> keys *redis-cli -c -h 192.168.88.54 -p 6379 192.168.88.54:6379> keys *