Redis相关
前言
整理一些关于redis相关的知识,其中包含一些底层原理。
知识点总结
总结了一些从基础到难场景的问题,这里面比较特殊的,比如DBA关心的,我会标注。
一、Redis 基础问题
1. Redis 是什么?它的主要应用场景有哪些?
Redis(Remote Dictionary Server)是一款开源的、基于内存的高性能键值存储系统,支持多种数据结构,并具备持久化、高可用、分布式等特性。它常被用作数据库、缓存、消息中间件,广泛应用于实时数据处理场景。
Redis 的核心特性
- 高性能:数据存储在内存中,读写速度达微秒级(10万+ QPS)。
- 多样数据结构:支持字符串(String)、哈希(Hash)、列表(List)、集合(Set)、有序集合(ZSet)、流(Stream)等。
- 持久化:通过 RDB(快照)和 AOF(追加日志)保障数据可靠性。
- 高可用:主从复制、哨兵(Sentinel)、集群(Cluster)模式支持故障自动切换。
- 分布式:支持数据分片(Sharding)、跨节点操作协调。
Redis 的主要应用场景
- 1. 缓存加速
- 场景:缓解数据库压力,提升高频读请求响应速度。
- 实现:
1
SET product:1001 "{name: '手机', price: 2999}" EX 300 # 缓存商品数据,5分钟过期
- 优势:
- 支持灵活的过期策略(TTL)。
- 结合淘汰策略(LRU/LFU)自动清理冷数据。
- 2. 会话存储(Session Storage)
- 场景:微服务架构中共享用户登录状态。比如常用的结合SpringSession模块使用
- 实现:
1
2HSET session:abc123 user_id 1001 last_active 1620000000
EXPIRE session:abc123 3600 # 会话1小时后过期 - 优势:
- 避免粘性会话(Sticky Session)导致的负载不均。
- 集群模式保障高可用。
- 3. 实时排行榜
- 场景:游戏积分榜、电商销量排行。
- 实现:
1
2ZADD leaderboard 5000 "user:A" 3000 "user:B" # 插入分数
ZREVRANGE leaderboard 0 9 WITHSCORES # 获取Top 10 - 优势:
- 有序集合(ZSet)天然支持排序和范围查询。
- 时间复杂度 O(log N),适合高频更新。
- 【备注】 结合这种二维表结构存储特性,可以实现一些链路监控数据的统计,笔者在工作中实现过。但要注意,如果使用ZSet数据结构,虽然redis提供一些聚合统计操作(如:ZUNIONSTORE和ZINTERSTORE)的指令,但是实际使用时,聚合统计的性能可能会很低,慎重使用这类操作,这种场景如需要,请替换其它实时计算/近实时计算方案来实现。高版本的RedisTimeSerials也可以实现。具体场景文末有介绍。
- 4. 消息队列
- 场景:异步任务处理、事件驱动架构。
- 实现方案:
- List 结构:简易队列(LPUSH/BRPOP)。
1
2LPUSH task_queue "send_email:user@example.com"
BRPOP task_queue 30 # 阻塞获取任务,超时30秒 - Streams:支持多消费者组、消息回溯。
1
2XADD orders * user_id 1001 product_id 2002 # 发布订单事件
XREADGROUP GROUP order_group consumer1 COUNT 1 STREAMS orders > # 消费
- List 结构:简易队列(LPUSH/BRPOP)。
- 5. 分布式锁
- 场景:防止多节点并发操作导致数据错误(如库存超卖)。
- 实现:
1
SET lock:order_1001 <unique_token> NX EX 30 # 获取锁(30秒自动释放)
- 原子释放(Lua脚本):
1
2
3
4
5if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
- 6. 实时计数器
- 场景:网站PV/UV统计、API调用限流。
- 实现:
1
2
3INCR page_view:homepage # 页面访问量+1
PFADD daily_uv 192.168.1.1 # HyperLogLog统计UV
CL.THROTTLE user:1001 100 60 60 1 # 令牌桶限流(Redis-Cell模块)
- 7. 实时数据分析(TimeSeries)
- 场景:用户行为追踪、实时监控。
TS.CREATE
创建时间序列的函数,官网介绍 - 实现:
1
2
3TS.CREATE api_latency RETENTION 86400000 # 创建时间序列(保留24小时)
TS.ADD api_latency * 45 # 记录当前时间戳的延迟45ms
TS.RANGE api_latency - + AGGREGATION avg 60000 # 按分钟聚合平均值
- 场景:用户行为追踪、实时监控。
- 8. 社交网络关系
- 场景:共同关注、好友推荐。
- 实现:
1
2SADD user:1001:follows 2001 2002 # 用户1001关注2001和2002
SINTER user:1001:follows user:2001:follows # 获取共同关注列表
Redis 的适用场景总结
场景类型 | 推荐使用 Redis 的原因 | 替代方案对比 |
---|---|---|
高频读缓存 | 内存读写快,支持丰富数据结构 | Memcached(仅简单键值) |
实时排行榜 | ZSet 天然支持排序和范围查询 | 数据库+定时任务(延迟高) |
分布式协调 | 原子操作和 Lua 脚本实现复杂逻辑 | ZooKeeper(强一致,性能较低) |
消息队列 | Streams 支持多消费者组和消息回溯 | Kafka(高吞吐,但复杂度高) |
何时不推荐使用 Redis?
- 海量数据存储:内存成本高,数据量超单机内存时需分片(Redis Cluster)。
- 复杂事务:需跨键事务时受限(要求所有键在同一槽)。
- 强一致性场景:主从异步复制可能导致数据短暂不一致。
总结
Redis 凭借其 内存高速访问、灵活数据结构 和 分布式能力,成为实时数据处理的核心工具。适合缓存、会话管理、实时排行榜等场景,但在海量数据存储或复杂分析场景中需结合其他技术(如数据库、大数据平台)使用。
2. Redis 与其他数据库(如 MySQL)的核心区别是什么?
Redis 与 MySQL 的核心区别主要体现在 数据模型、设计目标、性能特性 和 适用场景 上。以下是详细对比:
一、核心区别对比
特性 | Redis | MySQL |
---|---|---|
数据模型 | 键值存储(Key-Value),支持多种扩展数据结构(如 Hash、List、ZSet) | 关系型模型(表结构),支持 SQL 和 JOIN 操作 |
存储位置 | 数据主要存储在内存中,支持持久化到磁盘 | 数据存储在磁盘中,通过内存缓存加速读取 |
读写性能 | 微秒级延迟,10万+ QPS(适合高并发实时操作) | 毫秒级延迟,数千 QPS(依赖索引和查询复杂度) |
事务支持 | 支持简单事务(无回滚),依赖 Lua 脚本原子性 | 完整 ACID 事务,支持复杂事务和回滚 |
数据持久化 | 可选 RDB(快照)或 AOF(日志)持久化 | 默认通过事务日志(如 InnoDB redo log)保障持久性 |
扩展性 | 原生支持集群分片(Cluster 模式) | 需通过分库分表或中间件(如 Vitess)扩展 |
适用场景 | 缓存、实时数据处理、高频读写场景 | 复杂查询、事务性操作、持久化业务数据存储 |
二、详细对比分析
- 1. 数据模型与查询能力
- Redis:
- 数据结构丰富:如用
ZSet
实现排行榜,HyperLogLog
统计 UV。 - 简单查询:仅支持键值查询和部分范围操作(如
ZRANGE
),无法实现 JOIN 或复杂聚合。1
2ZADD leaderboard 95 "user:A" 80 "user:B" # 插入有序集合数据
ZREVRANGE leaderboard 0 9 WITHSCORES # 获取 Top 10
- 数据结构丰富:如用
- MySQL:
- 关系模型:数据以表形式存储,支持复杂 SQL 查询(如多表关联、子查询)。
- 索引优化:通过 B+树索引加速查询,支持全文索引(FULLTEXT)和空间索引。
1
2
3
4SELECT users.name, orders.total
FROM users
JOIN orders ON users.id = orders.user_id
WHERE orders.created_at > '2023-01-01';
- Redis:
- 2. 性能与延迟
- Redis:
- 内存读写:数据存储在内存,单操作延迟通常在 1~10 微秒。
- 瓶颈:受限于内存容量和网络带宽(如大 Key 传输)。
- MySQL:
- 磁盘 IO:数据持久化在磁盘,即使使用缓冲池(Buffer Pool),单查询延迟在 1~10 毫秒。
- 瓶颈:高并发写入时锁竞争(如行锁、表锁)、复杂查询执行计划优化。
- Redis:
- 3. 数据一致性与事务
- Redis:
- 弱一致性:主从复制异步同步,故障切换可能丢失部分数据。
- 事务限制:
MULTI/EXEC
仅保证命令原子性,不保证回滚(如中间命令失败继续执行)。
- MySQL:
- 强一致性:通过 Redo Log、Undo Log 和锁机制实现 ACID。
- 完整事务:支持
BEGIN
、COMMIT
、ROLLBACK
,隔离级别可配置(如 Read Committed)。
- Redis:
- 4. 扩展与高可用
- Redis:
- 水平扩展:Cluster 模式自动分片(16384 个 Slot),支持在线扩容。
- 高可用:哨兵(Sentinel)自动故障转移,主从切换秒级完成。
- MySQL:
- 垂直扩展:通过更强大的单机硬件提升性能(如 CPU、内存)。
- 水平扩展:需分库分表,依赖中间件或应用层路由(如 ShardingSphere)。
- 高可用:主从复制 + MHA 或基于 GTID 的集群(如 InnoDB Cluster),故障恢复分钟级。
- Redis:
- 5. 资源消耗与成本
- Redis:
- 内存成本高:存储相同数据的内存开销远高于磁盘。
- 运维简单:无复杂查询优化需求,集群管理相对轻量。
- MySQL:
- 磁盘成本低:适合存储海量数据(如 TB 级)。
- 运维复杂:需优化索引、SQL 语句、参数调优(如
innodb_buffer_pool_size
)。
- Redis:
三、典型应用场景
- 适合 Redis 的场景
- 高频读缓存:缓存数据库查询结果(如商品详情)。
- 实时计数器:PV/UV 统计、API 限流。
- 会话存储:分布式系统共享 Session。
- 消息队列:轻量级异步任务处理(使用 Streams 或 List)。
- 排行榜/社交关系:利用 ZSet 实现实时排序。
- 适合 MySQL 的场景
- 事务性操作:订单支付、库存扣减(需 ACID 保障)。
- 复杂查询:多表关联分析、报表生成。
- 持久化存储:用户信息、交易记录等核心业务数据。
- 全文搜索:结合
FULLTEXT
索引实现文本检索(如商品搜索)。
四、协同使用建议
在实际系统中,Redis 和 MySQL 通常 互补使用:
- 缓存加速:Redis 作为 MySQL 的前置缓存,减少数据库压力。
1
客户端 → Redis(缓存热点数据) → 缓存未命中 → MySQL → 回写 Redis
- 异步处理:Redis 处理实时请求,MySQL 异步持久化结果。
1
用户请求 → Redis(实时计数) → 定时任务 → 同步到 MySQL(报表统计)
- 削峰填谷:Redis 缓冲高并发写入,MySQL 批量消费。
1
突发流量 → Redis Streams(消息队列) → 后台服务逐批写入 MySQL
五、总结
维度 | Redis | MySQL |
---|---|---|
核心定位 | 内存优先的高性能数据操作 | 磁盘优先的关系型数据管理 |
优势场景 | 实时性要求高、数据结构灵活、读写并发量大 | 复杂查询、强一致性事务、海量数据持久存储 |
使用哲学 | 「速度第一」的缓存与实时数据处理 | 「可靠第一」的业务核心数据存储 |
决策建议:
- 若需要 低延迟、高吞吐、灵活数据结构,选择 Redis。
- 若需要 复杂查询、强事务、数据持久化,选择 MySQL。
- 多数互联网系统会 同时使用两者,通过分层架构兼顾性能与可靠性。
3. Redis 支持哪些数据类型?分别举一个实际应用场景。
Redis 支持多种核心数据类型,每种类型针对特定场景设计。以下是常见数据类型及其典型应用场景:
常规应用
- 1. String(字符串)
- 特点:二进制安全,可存储文本、数字或序列化数据。
- 场景:缓存用户会话信息。
1
SET user:1001 "{name: 'Alice', last_login: 1620000000}" EX 3600
- 通过
EX
设置过期时间,自动清理无效会话。
- 通过
- 2. List(列表)
- 特点:双向链表,支持快速头尾操作。
- 场景:消息队列(简易版)。
1
2LPUSH orders "order:2023" # 生产者入队
BRPOP orders 30 # 消费者阻塞式出队- 注意:更复杂的消息队列建议使用 Streams(支持多消费者组)。
- 3. Hash(哈希表)
- 特点:键值对集合,适合存储对象。
- 场景:存储商品信息。
1
2HSET product:1001 name "Laptop" price 999 stock 50
HINCRBY product:1001 stock -1 # 扣减库存- 直接操作字段,避免序列化整个对象。
- 4. Set(集合)
- 特点:无序唯一集合,支持交并差运算。
- 场景:用户标签系统。
1
2SADD user:1001:tags "tech" "gaming" # 添加标签
SINTER user:1001:tags user:1002:tags # 共同兴趣标签- 快速实现共同好友、兴趣匹配等功能。
- 5. Sorted Set(有序集合)
- 特点:元素按
score
排序,支持范围查询。 - 场景:实时排行榜。
1
2ZADD leaderboard 1000 "user:A" # 添加分数
ZREVRANGE leaderboard 0 9 WITHSCORES # 获取 Top 10- 适用于游戏积分、热搜榜单等场景。
- 特点:元素按
- 6. Streams(流)
- 特点:日志结构数据,支持多消费者组。
- 场景:消息队列(支持回溯)。
1
2XADD orders * product_id 1001 user_id 2001 # 发布订单
XREADGROUP GROUP order_group consumer1 COUNT 1 STREAMS orders >- 替代 Kafka 的轻量级方案,适合事件溯源。
其他高级类型
- Bitmaps(位图)
- 场景:用户签到统计。
1
2SETBIT signin:202302 1001 1 # 用户 1001 在 2023-02 签到
BITCOUNT signin:202302 # 统计当月签到人数
- 场景:用户签到统计。
- HyperLogLog(基数统计)
- 场景:统计独立 IP 访问量。
1
2PFADD daily_ips "192.168.1.1" "10.0.0.1"
PFCOUNT daily_ips # 估算独立 IP 数(误差约 0.81%)
- 场景:统计独立 IP 访问量。
- Geospatial(地理空间)
- 场景:附近的人查询。
1
2GEOADD locations 116.40 39.90 "user:A" # 添加坐标
GEORADIUS locations 116.41 39.91 10 km # 查找 10km 内用户
- 场景:附近的人查询。
总结:如何选择数据类型?
需求 | 推荐类型 |
---|---|
简单键值存储 | String |
对象存储(多字段) | Hash |
队列/栈操作 | List 或 Streams |
去重集合运算 | Set |
排序+范围查询 | Sorted Set |
消息队列(高级) | Streams |
位级操作(如签到) | Bitmaps |
大数据量去重统计 | HyperLogLog |
地理位置服务 | Geospatial |
最佳实践:优先使用原生类型而非序列化字符串,以利用 Redis 的高效操作。例如:使用 Hash 代替 String 存储 JSON 对象,可直接修改字段而无需反序列化。
4. 为什么 Redis 读写性能高?单线程模型为何高效?
为什么 Redis 读写性能高?
Redis 的读写性能极高,主要归功于以下几个设计特点:
- 1. 内存存储
- Redis 将数据存储在内存中,内存的访问速度远高于磁盘(纳秒级 vs 毫秒级)。
- 数据操作不需要磁盘 I/O,因此读写速度极快。
- 2. 单线程模型
- Redis 使用单线程处理命令,避免了多线程的上下文切换和锁竞争。
- 单线程模型简化了设计,减少了线程切换的开销。
- 3. 非阻塞 I/O
- Redis 使用多路复用技术(如 epoll、kqueue)处理多个客户端连接。
- 通过事件驱动模型,Redis 可以高效地处理大量并发请求。
- 4. 高效的数据结构
- Redis 内置了多种高效的数据结构(如哈希表、跳跃表、压缩列表),这些数据结构经过优化,操作时间复杂度低。
- 5. 纯内存操作
- Redis 的所有操作都在内存中完成,不需要频繁访问磁盘。
- 持久化操作(如 RDB 和 AOF)是异步的,不会阻塞主线程。
- 6. 优化的网络模型
- Redis 使用单线程处理网络 I/O,避免了多线程的网络竞争。
- 通过批量处理(Pipeline)减少网络往返时间(RTT)。
单线程模型为何高效?
Redis 的单线程模型看似简单,但在实际应用中表现出极高的性能,主要原因如下:
- 1. 避免上下文切换
- 多线程模型需要频繁切换线程上下文,消耗 CPU 资源。
- 单线程模型避免了上下文切换,CPU 可以专注于处理请求。
- 2. 无锁竞争
- 多线程模型需要加锁来保证数据一致性,锁竞争会降低性能。
- 单线程模型无需加锁,所有操作都是原子的,避免了锁开销。
- 3. 内存操作无瓶颈
- Redis 的数据存储在内存中,内存的访问速度极快,单线程足以充分利用内存带宽。
- 对于内存操作,单线程的性能已经接近硬件极限。
- 事件驱动模型
- Redis 使用事件驱动模型(Reactor 模式),通过多路复用技术处理多个客户端连接。
- 单线程可以高效地处理大量并发请求,而不会成为性能瓶颈。
- 批量处理
- Redis 支持 Pipeline,可以批量处理多个命令,减少网络往返时间(RTT)。
- 单线程模型下,批量处理可以进一步提升吞吐量。
- 简单可靠
- 单线程模型简化了 Redis 的设计和实现,降低了出错的概率。
- 调试和维护更加方便。
单线程模型的局限性
尽管单线程模型在大多数场景下表现优异,但也存在一些局限性:
- CPU 密集型任务:
- 单线程无法充分利用多核 CPU 的性能。
- 对于复杂的计算任务(如 Lua 脚本执行),可能会成为性能瓶颈。
- 阻塞操作:
- 某些操作(如持久化、大 Key 删除)可能会阻塞主线程,影响性能。
- 高并发场景:
- 单线程模型在处理极高并发时可能会达到性能上限。
Redis 6.0 的多线程改进
为了克服单线程模型的局限性,Redis 6.0 引入了多线程支持,但仅限于 网络 I/O 和 持久化:
- 多线程网络 I/O:
- Redis 6.0 使用多线程处理网络读写,提升高并发场景下的性能。
- 命令执行仍然是单线程的,保证了数据操作的原子性。
- 多线程持久化:
- Redis 6.0 支持多线程执行持久化操作(如 AOF 重写),减少对主线程的影响。
总结
Redis 的高性能主要得益于 内存存储、单线程模型、非阻塞 I/O 和 高效的数据结构。单线程模型通过避免上下文切换和锁竞争,简化了设计并提升了性能。尽管单线程模型存在一些局限性,但在大多数场景下,Redis 的性能已经足够优秀。对于更高并发的需求,Redis 6.0 的多线程改进进一步提升了性能。
5. Redis 的持久化机制(RDB 和 AOF)有什么区别?如何选择?
RDB(Redis Database)
- 全称:Redis Database(数据快照)。
- 原理:定期将内存数据生成二进制快照(
.rdb
文件)保存到磁盘,通过SAVE
(阻塞)或BGSAVE
(后台异步)触发。 - 特点:
- 高性能:适合大规模数据备份与恢复。
- 低一致性:可能丢失最后一次快照后的写入数据。
AOF(Append Only File)
- 全称:Append Only File(追加日志)。
- 原理:记录所有写操作命令(文本格式),通过
fsync
策略(如everysec
)同步到磁盘(.aof
文件)。 - 特点:
- 高可靠性:最多丢失 1 秒数据(默认配置)。
- 低性能:频繁写入时可能影响吞吐量,需定期重写(
BGREWRITEAOF
)压缩日志。
对比总结
特性 | RDB | AOF |
---|---|---|
数据格式 | 二进制快照 | 文本命令日志 |
恢复速度 | 快(直接加载快照) | 慢(重放命令) |
磁盘占用 | 小(压缩存储) | 大(需定期重写优化) |
适用场景 | 容灾备份、快速恢复 | 高数据安全要求(如金融交易) |
最佳实践:生产环境通常同时启用 RDB 和 AOF,用 RDB 做冷备,AOF 保障实时数据安全。
6. 什么是缓存雪崩、缓存穿透、缓存击穿?如何解决?
- 缓存穿透
- 问题:大量请求不存在的 key(如恶意攻击)。
- 解决:布隆过滤器过滤无效请求;缓存空值并设置短过期时间。
- 缓存雪崩
- 问题:大量 key 同时过期,请求直接打到数据库。
- 解决:随机化过期时间;集群部署;热点数据永不过期。
- 缓存击穿
- 问题:热点 key 过期后高并发请求瞬间压垮数据库。
- 解决:互斥锁(SETNX)重建缓存;逻辑过期(不设置 TTL,后台更新)。
7. Redis 的过期策略和内存淘汰机制有哪些?
Redis 的 过期策略 和 内存淘汰机制 是管理内存资源的核心机制,直接影响性能和稳定性。
一、过期策略(Expiration Policies)
Redis 通过以下 三种策略 组合处理过期键的删除:
- 1. 定时删除(主动删除)
- 原理:为每个设置了过期时间的键创建定时器,到期立即删除。
- 优点:内存释放及时。
- 缺点:大量定时器占用 CPU 资源,影响吞吐量。
- 适用场景:不推荐默认使用,仅适用于少量需精准删除的键。
- 2. 惰性删除(被动删除)
- 原理:在访问键时检查是否过期,若过期则删除。
- 优点:对 CPU 友好,无额外开销。
- 缺点:内存泄漏风险(长期未访问的过期键无法释放)。
- 代码逻辑:
1
2
3if (key.expire_time < now()) {
delete_key(key);
}
- 3. 定期删除(折中方案)
- 原理:周期性(默认 10 Hz)随机扫描一定数量的键,删除其中已过期的键。
- 流程:
- 随机抽取
20
个键检查。 - 删除其中已过期的键。
- 若过期键比例超过
25%
,重复步骤 1。
- 随机抽取
- 优点:平衡 CPU 和内存压力。
- 缺点:过期键可能不会立即删除。
- 配置参数:
hz
(控制扫描频率,默认 10,范围 1~500)。
二、内存淘汰机制(Eviction Policies)
当内存达到 maxmemory
限制时,Redis 根据 maxmemory-policy
配置决定淘汰策略:
- 1. 不淘汰(No Eviction)【默认】
- 策略:
noeviction
- 行为:拒绝所有写入操作(返回错误),读操作正常。
- 适用场景:数据不允许丢失,且需确保内存不超限(需严格监控)。
- 策略:
- 2. 全体键淘汰
- 策略:
allkeys-lru
:淘汰最近最少使用的键(LRU 近似算法)。allkeys-lfu
(Redis 4.0+):淘汰访问频率最低的键(LFU 算法)。allkeys-random
:随机淘汰任意键。
- 适用场景:Redis 作为缓存,允许丢失数据以腾出内存。
- 策略:
- 3. 仅过期键淘汰
- 策略:
volatile-lru
:从设置了过期时间的键中淘汰 LRU 键。volatile-lfu
(Redis 4.0+):淘汰 LFU 键。volatile-random
:随机淘汰设置了过期时间的键。volatile-ttl
:优先淘汰剩余存活时间(TTL)较短的键。
- 适用场景:需保留部分持久化数据,仅淘汰缓存类数据。
- 策略:
三、关键注意事项
1. 算法近似性
- LRU/LFU 非精准:为节省内存,Redis 使用概率算法(如采样 5 个键选最久未使用的)。
- 调整精度:通过
maxmemory-samples
(默认 5)配置采样数量,值越大精度越高,CPU 开销越大。
2. LFU 优化(Redis 4.0+)
- 原理:基于访问频率,通过衰减机制处理“历史热点”。
- 配置:
lfu-log-factor
:调整计数器增长速度(越大增速越慢)。lfu-decay-time
:计数器衰减时间(单位分钟)。
3. 淘汰策略选择
- 缓存场景:优先
allkeys-lru
或allkeys-lfu
(高命中率)。 - 混合数据:若部分数据可丢失,用
volatile-ttl
或volatile-lru
。 - 随机访问:无明显热点时,
allkeys-random
可能更公平。
- 缓存场景:优先
四、如何查看当前淘汰策略?
- 查看配置:输出示例:
1
redis-cli config get maxmemory-policy
1
21) "maxmemory-policy"
2) "noeviction" - 运行时信息:输出示例:
1
redis-cli info memory | grep maxmemory-policy
1
maxmemory_policy:noeviction
五、如何修改淘汰策略?
在 redis.conf
中设置:
1 | maxmemory 4gb # 设置最大内存限制(必须配置才会触发淘汰) |
或运行时动态调整:
1 | redis-cli config set maxmemory-policy volatile-lfu |
六、Redis 内存淘汰策略对比
策略 | 淘汰范围 | 淘汰规则 | 适用场景 | 优点 | 缺点 |
---|---|---|---|---|---|
noeviction |
无淘汰 | 内存满时拒绝写入,返回错误 | 数据不可丢失的场景(如金融交易记录) | 数据绝对安全 | 服务可能因内存不足拒绝写入,需人工干预 |
volatile-lru |
有过期时间的键 | 淘汰 最近最少使用 的键 | 混合数据集(部分数据需长期保留,部分可淘汰) | 保护未设置过期的数据 | 需合理设置键的过期时间,否则可能无效淘汰 |
allkeys-lru |
所有键 | 淘汰 最近最少使用 的键 | 纯缓存场景(所有数据可淘汰,需高缓存命中率) | 自动优化热点数据保留 | 冷数据可能被频繁访问的短期数据挤占 |
volatile-lfu |
有过期时间的键 | 淘汰 最不经常使用 的键(基于访问频率) | 需要长期保留高频访问数据(如热门商品信息) | 精准识别并保留热点数据 | 需Redis 4.0+,计算频率增加轻微CPU开销 |
allkeys-lfu |
所有键 | 淘汰 最不经常使用 的键 | 明确依赖访问频率的缓存(如新闻热点排行榜) | 高效保留高频数据 | 短期突发访问可能导致误淘汰 |
volatile-random |
有过期时间的键 | 随机淘汰 | 临时数据管理,无明确访问规律(如短期会话数据) | 实现简单,开销低 | 可能淘汰重要数据,缓存命中率不稳定 |
allkeys-random |
所有键 | 随机淘汰 | 测试环境或数据价值均等的场景 | 快速释放内存 | 生产环境不推荐,数据淘汰不可控 |
volatile-ttl |
有过期时间的键 | 淘汰 剩余存活时间(TTL)最短 的键 | 明确短期有效的数据(如验证码、临时令牌) | 优先清理即将失效的数据,避免重复存储 | 不适用于无过期时间或TTL分布不均的场景 |
七、关键选择因素
- 数据生命周期:
- 有明确过期时间:优先考虑
volatile-lru
、volatile-lfu
或volatile-ttl
。 - 无过期时间:选择
allkeys-lru
或allkeys-lfu
。
- 有明确过期时间:优先考虑
- 数据访问模式:
- 热点数据集中(如二八法则):
allkeys-lru
或allkeys-lfu
。 - 访问频率差异大:
allkeys-lfu
更精准。 - 无规律访问:
allkeys-random
或volatile-random
。
- 热点数据集中(如二八法则):
- 数据重要性:
- 部分数据不可淘汰:使用
volatile-*
策略并仅为临时数据设置过期时间。 - 所有数据可淘汰:选择
allkeys-*
策略。
- 部分数据不可淘汰:使用
- Redis 版本:
- LFU 策略(
volatile-lfu
/allkeys-lfu
)需 Redis 4.0+。 - 低版本(如 3.x)仅支持 LRU 和 TTL 策略。
- LFU 策略(
八、示例场景
- 电商平台商品缓存:
- 策略:
allkeys-lfu
- 原因:商品访问频率差异大,需长期保留热门商品,淘汰冷门商品。
- 策略:
- 用户会话管理:
- 策略:
volatile-ttl
- 原因:会话数据设置固定 TTL(如 30 分钟),优先淘汰即将过期的会话。
- 策略:
- 新闻热点排行榜:
- 策略:
allkeys-lfu
- 原因:基于实时点击量动态调整,高频访问新闻需长期保留。
- 策略:
- 临时验证码存储:
- 策略:
volatile-lru
- 原因:验证码短期有效(5分钟),淘汰最近未使用的以释放空间。
- 策略:
8. 如何实现 Redis 的分布式锁?有哪些注意事项?
实现 Redis 分布式锁的核心目标是确保在分布式系统中对共享资源的互斥访问。以下是常见实现方案和注意事项:
基础实现方案
- 1. SET NX + EX 命令
1
SET lock_key unique_value NX EX 30
- NX:当 key 不存在时设置值(获取锁)
- EX:设置过期时间(避免死锁)
unique_value
需唯一(如 UUID+线程 ID),用于安全释放锁
- 2. 释放锁(Lua 脚本保证原子性)
1
2
3
4
5if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
注意事项
- 1. 原子性操作
- 必须合并加锁与过期时间设置:避免
SETNX + EXPIRE
分步执行时进程崩溃导致死锁。 - 释放锁需验证值:确保只有锁的持有者能释放锁,防止误删。
- 必须合并加锁与过期时间设置:避免
- 2. 锁续期(Watchdog)
- 问题:业务未完成但锁已过期。
- 方案:启动后台线程定期续期(如 Redisson 的
lockWatchdogTimeout
)。
- 3. 集群风险(主从异步复制)
- 场景:主节点写入锁后崩溃,从节点未同步导致锁丢失。
- 可选方案:使用 RedLock 算法(需部署多独立 Redis 实例),但存在争议(需权衡 CP 与 AP)。
- 4. 重入性
- 需求:同一线程多次获取锁需支持重入。
- 实现:通过 ThreadLocal 记录重入次数,或直接使用 Redisson 客户端。
- 5. 异常处理
- 超时机制:避免死等,可使用
tryLock
带超时参数。 - 锁释放:确保
finally
块中释放锁,避免异常导致锁泄漏。
- 超时机制:避免死等,可使用
高级方案
RedLock 算法
- 向 N 个独立 Redis 实例顺序请求加锁。
- 当超过半数(N/2 + 1)成功且耗时小于锁有效期时,视为加锁成功。
- 释放时向所有实例发送删除命令。
争议点:网络分区或时钟跳跃可能导致锁失效,需结合业务容忍度评估。
推荐实践
- 优先使用成熟库:如 Redisson(支持可重入锁、看门狗、RedLock)。
- 简化场景:若业务允许短暂重复,可接受 SETNX 基础方案 + 合理超时。
- 监控:通过 Redis 的
INFO
命令监控锁竞争情况,优化超时时间。
代码示例(Redisson 实现)
1 | RedissonClient redisson = Redisson.create(config); |
Redis分布式锁与Zookeeper分布式锁之间的比较
Redis 和 Zookeeper 都支持分布式锁,但它们在实现和适用场景上有所不同。
Redis 分布式锁
- 实现方式:
- 基于
SETNX
(SET if Not eXists)命令,确保只有一个客户端能设置成功。 - 通常结合
EXPIRE
设置锁的过期时间,避免死锁。
- 基于
- 优点:
- 性能高:Redis 基于内存,响应速度快。
- 简单易用:实现相对简单,适合高并发场景。
- 缺点:
- 可靠性较低:Redis 是 AP 系统(主从复制是异步的,只能保证弱一致性,无法保证强一致性),网络分区时可能丢失锁。
- 锁续期复杂:需额外机制(如 Redlock)处理锁续期问题。
Zookeeper 分布式锁
- 实现方式:
- 基于临时顺序节点,客户端创建节点,最小节点获得锁。
- 通过 Watch 机制监听前序节点释放锁。
- 优点:
- 可靠性高:Zookeeper 是 CP 系统,保证强一致性。
- 锁续期简单:临时节点在会话结束时自动删除,避免死锁。
- 缺点:
- 性能较低:Zookeeper 基于磁盘,性能不如 Redis。
- 实现复杂:需处理会话管理和节点监听。
选择建议
- Redis 分布式锁:适合高并发、对一致性要求不高的场景,如缓存、限流等。
- Zookeeper 分布式锁:适合对一致性要求高的场景,如分布式事务、配置管理等。
总结
- Redis:性能高,实现简单,适合高并发场景。
- Zookeeper:可靠性高,适合强一致性场景。
二、Redis 进阶
1. Redis 事务(MULTI/EXEC)的原子性如何理解?与数据库事务有何不同?
Redis事务的原子性及其与传统数据库事务的差异可以从以下几个方面理解:
1. Redis事务的原子性
- 执行过程的不可分割性:
当使用MULTI
开启事务后,所有命令会被缓存在队列中,直到EXEC
触发执行。Redis保证事务中的命令在EXEC
阶段连续且原子地执行,不会被其他客户端的命令打断。这是Redis事务原子性的核心。 - 错误处理与原子性:
- 语法错误(入队时检测):
如果命令本身存在语法错误(如命令不存在、参数错误),Redis会在入队时直接拒绝该命令,并在EXEC
时放弃整个事务(所有命令都不执行)。此时原子性得到保证。 - 运行时错误(执行时发生):
如果命令语法正确但执行失败(如对字符串执行LPOP
),错误命令不会影响其他命令的执行,事务会继续执行剩余命令。此时原子性不成立,因为部分操作成功,部分失败。
- 语法错误(入队时检测):
- 无回滚机制:
Redis事务不支持回滚(Rollback)。即使某些命令失败,已执行的命令结果也不会撤销,需要开发者自行处理部分失败的情况。
2. 与传统数据库事务的差异
特性 | Redis事务 | 数据库事务(如MySQL) |
---|---|---|
原子性保证 | 仅保证命令队列的连续执行,运行时错误不中断事务。 | 完全原子性:失败时自动回滚所有操作。 |
隔离性 | 串行化执行,无并发问题(单线程模型)。 | 支持多级别隔离(如读已提交、可重复读等)。 |
回滚机制 | 无回滚,需手动补偿。 | 支持自动回滚。 |
错误处理 | 运行时错误不影响后续命令。 | 错误通常导致事务中止并回滚。 |
使用场景 | 适合简单操作,高吞吐场景。 | 适合需要强一致性和复杂操作的场景。 |
3. 示例对比
Redis事务:
1
2
3
4
5MULTI
SET a 100
LPOP a # a是字符串,执行失败
SET b 200
EXEC- 结果:
a
被设置为100,LPOP a
失败,b
被设置为200。部分成功,无回滚。
- 结果:
数据库事务:
1
2
3
4
5BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE user = 'A';
UPDATE accounts SET balance = balance + 100 WHERE user = 'B';
-- 若此处失败(如B不存在)
COMMIT;- 结果:若第二个
UPDATE
失败,整个事务回滚,A
的余额不变。
- 结果:若第二个
4. 总结
- Redis事务的原子性:
更侧重于执行过程的不可中断性,而非结果的一致性。适合对一致性要求不高、需要高性能的场景(如批量操作)。 - 数据库事务的原子性:
通过ACID严格保证,适用于需要强一致性的场景(如金融交易)。
关键区别:Redis事务在运行时错误时仍会继续执行,而数据库事务会中止并回滚,确保“全成功或全失败”。需根据场景选择合适的事务模型。
2. Redis 的发布订阅(Pub/Sub)模式如何工作?适合什么场景?
Redis 的 发布订阅(Pub/Sub) 是一种消息通信模式,允许多个订阅者(Subscriber)实时接收发布者(Publisher)发送的消息。
【备注】 Redis的发布订阅,由于其存在致命的场景-消息不可靠,所以一般都是作为辅助集群通知的场景来使用,而且使用的时候必定需要有相对应的兜底机制来保证一旦未收到消息时,还有路可走。
一、Pub/Sub 工作原理
- 1. 核心概念
- 频道(Channel):消息传递的逻辑通道,订阅者需绑定到指定频道。
- 发布者(Publisher):通过
PUBLISH
命令向频道发送消息。 - 订阅者(Subscriber):通过
SUBSCRIBE
命令订阅频道,实时接收消息。
- 2. 工作流程
- 订阅频道:
1
SUBSCRIBE news.sports # 订阅 "news.sports" 频道
- 客户端进入订阅模式,阻塞等待消息。
- 发布消息:
1
PUBLISH news.sports "Liverpool wins!" # 向频道发送消息
- 消息传递:
- Redis 服务器将消息推送给所有订阅该频道的客户端。
- 无持久化:消息仅在连接的订阅者存活时传递,离线订阅者无法获取历史消息。
- 订阅频道:
- 3. 高级特性
- 模式订阅(Pattern Matching):
1
PSUBSCRIBE news.* # 订阅所有以 "news." 开头的频道
- 支持通配符(
*
和?
),匹配多个频道。
- 支持通配符(
- 退订:
1
2UNSUBSCRIBE news.sports # 退订指定频道
PUNSUBSCRIBE news.* # 退订模式匹配的频道
- 模式订阅(Pattern Matching):
二、适用场景
- 1. 实时消息通知
- 场景:聊天室、即时通讯、游戏内广播。
- 示例:
- 用户订阅聊天频道
chat:room1
,任何消息实时推送至所有在线成员。 - 游戏服务器通过频道
game:updates
广播玩家位置变动。
- 用户订阅聊天频道
- 2. 事件驱动系统
- 场景:微服务间解耦通信、状态变更通知。
- 示例:
- 订单服务在订单创建后发布事件到
order:created
,库存服务订阅该频道并扣减库存。
- 订单服务在订单创建后发布事件到
- 3. 轻量级监控与日志分发
- 场景:服务器状态监控、日志实时聚合。
- 示例:
- 多台服务器发布心跳信息到
monitor:heartbeat
,监控中心订阅并检测异常节点。
- 多台服务器发布心跳信息到
- 4. 动态配置更新
- 场景:全局配置实时生效。
- 示例:
- 管理员通过
config:update
频道发布新配置参数,所有服务实例订阅并热加载配置。
- 管理员通过
三、不适用场景
1. 需要消息持久化
- 问题:订阅者断开连接后,消息丢失。
- 替代方案:使用 Redis Streams(支持消息持久化和消费者组)。
2. 严格的消息顺序与可靠性
- 问题:Pub/Sub 不保证消息顺序或重试机制。
- 替代方案:Apache Kafka 或 RabbitMQ(提供事务和确认机制)。
3. 高吞吐量持久化队列
- 问题:大量消息可能压垮内存,且无持久化。
- 替代方案:Redis Streams 或专用消息队列(如 NSQ)。
四、性能与注意事项
- 1. 性能影响
- 优势:轻量级,单机支持数万级 QPS。
- 瓶颈:
- 广播消息时,订阅者数量增加会线性提升 CPU 和带宽消耗。
- 避免高频消息(如每秒百万级),可能导致 Redis 主线程阻塞。
- 2. 使用建议
- 短连接慎用:订阅者需保持长连接,频繁重连易丢失消息。
- 客户端管理:
- 使用连接池维持订阅状态。
- 为每个订阅频道分配独立连接,避免阻塞其他操作。
- 监控:通过
INFO clients
观察订阅连接数,避免资源耗尽。
- 3. 代码示例(Python)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import redis
# 订阅者
def subscriber():
r = redis.Redis()
pubsub = r.pubsub()
pubsub.subscribe("news.sports")
for message in pubsub.listen():
if message["type"] == "message":
print(f"收到消息: {message['data'].decode()}")
# 发布者
def publisher():
r = redis.Redis()
r.publish("news.sports", "Match starts at 8 PM!")
五、总结
场景类型 | 是否适合 Pub/Sub | 原因 |
---|---|---|
实时聊天 | ✅ 适合 | 低延迟,无需持久化 |
订单事件通知 | ⚠️ 谨慎使用 | 需结合 ACK 机制或改用 Streams |
日志广播 | ✅ 适合 | 实时性强,允许少量丢失 |
关键配置推送 | ⚠️ 需补充机制 | 需重试机制确保订阅者收到 |
设计建议:
- 简单实时场景:优先使用 Pub/Sub(如通知、聊天)。
- 复杂场景:结合 Redis Streams 或专业消息队列,确保可靠性和扩展性。
3. Redis 的 Pipeline 是什么?为什么能提升性能?
Redis 的 Pipeline(管道) 是一种客户端优化技术,用于将多个命令批量发送到服务器并一次性读取响应,从而显著减少网络开销。以下是其核心机制和性能优势的详细分析:
一、Pipeline 是什么?
- 1. 基本原理
- 常规模式:客户端发送一个命令 → 等待响应 → 再发送下一个命令(
请求-响应
循环)。 - Pipeline 模式:客户端将多个命令打包一次性发送 → 服务器按顺序执行 → 所有响应一次性返回。
1
2# 示例:命令行中使用 Pipeline
(echo -en "SET key1 val1\r\nGET key1\r\n"; sleep 1) | nc redis-server 6379
- 常规模式:客户端发送一个命令 → 等待响应 → 再发送下一个命令(
- 2. 与事务(MULTI/EXEC)的区别
- Pipeline:仅优化网络传输,不保证原子性。
- 事务:通过
MULTI/EXEC
包裹的命令原子执行,但每次仍需多次网络往返。
二、Pipeline 如何提升性能?
- 1. 减少网络往返时间(RTT)
- 问题:单命令模式下,N 次命令需要 N 次 RTT(Round-Trip Time)。
- 优化:Pipeline 将 N 次命令压缩为 1 次 RTT。
1
2常规模式:RTT × N
Pipeline 模式:RTT × 1 + 命令执行总耗时
- 2. 降低网络带宽占用
- 批量发送:减少每个命令的 TCP 包头开销(如三次握手、ACK 确认)。
- 适用场景:高延迟网络(如跨机房调用)效果更显著。
- 3. 服务器处理优化
- 顺序执行:服务器按接收顺序依次执行命令,无需频繁切换上下文。
- 响应缓冲:所有响应缓存在内存中,批量返回减少 I/O 次数。
三、性能提升实测对比
- 1. 测试场景
- 命令数量:10,000 次
SET
操作。 - 网络延迟:模拟 1ms RTT。
- 命令数量:10,000 次
- 2. 耗时对比
模式 总耗时(理论) 实际示例 常规模式 10,000 × 1ms = 10s ~10-12s(受吞吐限制) Pipeline 1 × 1ms + 执行时间 ≈ 0.1s ~0.1-0.3s - 3. 吞吐量对比
- Pipeline 可将吞吐量提升 5-10 倍(取决于命令复杂度与网络环境)。
四、Pipeline 的使用注意事项
- 1. 合理设置批量大小
- 过小:无法充分利用 RTT 优化。
- 过大:可能导致服务器内存压力或客户端阻塞。
- 建议值:每批次 100-1000 个命令(根据数据大小调整)。
- 2. 避免混合读写
- 问题:Pipeline 中的后续命令无法依赖前面命令的结果。
1
2
3# 错误示例:GET 依赖前一个 INCR 的结果
pipe.incr("counter")
pipe.get("counter") # 获取的是旧值! - 解决方案:需事务(
MULTI/EXEC
)或 Lua 脚本保证原子性。
- 问题:Pipeline 中的后续命令无法依赖前面命令的结果。
- 3. 客户端实现差异
- 同步 vs 异步:
- 同步:
redis-py
的pipeline()
默认原子化提交。 - 异步:部分客户端支持非阻塞 Pipeline。
- 同步:
- 代码示例(Python):
1
2
3
4
5
6
7
8import redis
r = redis.Redis()
# 创建 Pipeline
pipe = r.pipeline()
pipe.set("key1", "val1")
pipe.get("key1")
responses = pipe.execute() # 提交并获取所有响应
- 同步 vs 异步:
- 4. 监控与调优
- 服务器内存:大量 Pipeline 可能占用输出缓冲区(通过
client-output-buffer-limit
配置)。 - 慢查询:避免单个 Pipeline 包含耗时命令(如
KEYS *
),导致阻塞其他请求。
- 服务器内存:大量 Pipeline 可能占用输出缓冲区(通过
五、适用场景 vs 不适用场景
场景 | 是否推荐 | 原因 |
---|---|---|
批量写入/查询(如初始化数据) | ✅ 推荐 | 显著减少网络开销 |
实时交互式操作(如每个命令需立即响应) | ❌ 不推荐 | Pipeline 延迟响应 |
依赖前序命令结果的场景 | ❌ 不推荐 | 需改用事务或 Lua 脚本 |
高并发低延迟环境(如游戏) | ✅ 推荐 | 最大化吞吐量 |
六、进阶优化:Pipeline 与批量命令
1. 原生批量命令
- 示例:
MSET
、HMGET
、DEL key1 key2...
。 - 优势:服务器端原子执行,比 Pipeline 更高效。
- 适用场景:同类操作(如批量插入键值对)。
- 示例:
2. 混合策略
方案:将同类命令合并为批量操作,异类命令使用 Pipeline。
1
2
3
4MSET key1 val1 key2 val2 # 批量写入
PIPELINE:
GET key1
HGETALL user:1001
总结
Pipeline 的核心价值在于将网络延迟从 O(N) 优化为 O(1) ,尤其适合批量操作和高延迟网络环境。但其本质是客户端优化,不改变服务器执行逻辑。在实际使用中,需结合业务需求、命令类型和客户端特性,权衡批量大小与资源消耗,必要时搭配事务或 Streams 实现复杂场景。
4. 什么是 Redis 的慢查询?如何分析和优化?
1. 什么是 Redis 慢查询?
Redis 慢查询指 执行时间超过预设阈值 的命令操作,这些命令可能阻塞单线程的 Redis 服务,导致整体性能下降。
- 阈值配置:通过
slowlog-log-slower-than
参数设置(单位:微秒,默认 10,000μs=10ms)。 - 日志容量:由
slowlog-max-len
控制(默认 128 条),先进先出。
2. 如何查看慢查询日志?
使用 SLOWLOG
命令:
1 | SLOWLOG GET 5 # 获取最近 5 条慢查询记录 |
输出示例:
1 | 1) 1) (integer) 14 # 日志 ID |
3. 慢查询常见原因
- 3.1 高风险命令
- O(N) 复杂度命令:
KEYS *
(全量遍历键,复杂度 O(N))SMEMBERS
(获取大集合所有成员)LRANGE mylist 0 -1
(获取超长列表)ZRANGE
大范围查询
- 阻塞式命令:
FLUSHDB
/FLUSHALL
(清空数据库)- 大 Key 的
DEL
操作(释放内存耗时)
- O(N) 复杂度命令:
- 3.2 大 Key 问题
- 定义:单个 Key 的 Value 大小超过 1MB,或集合元素超 10,000 个。
- 影响:序列化/反序列化耗时、网络传输延迟、内存碎片。
- 3.3 不合理持久化配置
- RDB 快照:
save
规则过密导致频繁 fork。 - AOF 重写:
auto-aof-rewrite-percentage
设置不当,重写期间占用大量 CPU。
- RDB 快照:
- 3.4 内存压力
- 频繁淘汰:内存达到
maxmemory
后持续触发淘汰策略(如allkeys-lru
),增加 CPU 开销。
- 频繁淘汰:内存达到
4. 分析工具与方法
- 4.1 内置命令
- 实时监控: 输出示例:
1
INFO commandstats # 查看所有命令的调用次数和耗时
1
cmdstat_keys:calls=2,usec=142512,usec_per_call=71256.00
- 大 Key 扫描:
1
redis-cli --bigkeys # 扫描各类型最大 Key
- 实时监控:
- 4.2 外部工具
- 5.1 避免高风险命令
替代方案:
高风险命令 替代方案 KEYS *
SCAN
分页遍历(非阻塞)SMEMBERS
SSCAN
分批获取成员大 Key 的 DEL
渐进式删除(分批次 UNLINK
)代码示例:
1
2
3
4
5
6
7# 使用 SCAN 代替 KEYS *
cursor = 0
while True:
cursor, keys = redis.scan(cursor, match="user:*", count=100)
process(keys)
if cursor == 0:
break
- 5.2 拆分大 Key
- 策略:
- 分片存储:如将
user:1000:friends
拆分为user:1000:friends:1
、user:1000:friends:2
。 - 压缩存储:使用
HASH
代替JSON String
,或用ZSTD
压缩序列化数据。
- 分片存储:如将
- 示例:
1
2
3
4
5# 原始大 Key(1MB JSON String)
SET user:1000_profile "{...}"
# 优化为 Hash
HSET user:1000_profile name "Alice" age 30 ...
- 策略:
- 5.3 配置调优
调整慢查询阈值:
1
CONFIG SET slowlog-log-slower-than 5000 # 调整为 5ms
内存管理:
- 设置合理的
maxmemory
和淘汰策略(如volatile-lfu
)。 - 启用内存碎片整理:
CONFIG SET activedefrag yes
。
- 设置合理的
持久化优化:
- RDB 配置低频
save
(如save 3600 1
)。 - AOF 使用
everysec
策略,并关闭appendfsync
。
- RDB 配置低频
- 5.4 客户端优化
- Pipeline 批量操作:合并多个命令减少网络往返。
1
2
3
4pipe = redis.pipeline()
for i in range(1000):
pipe.set(f"key:{i}", i)
pipe.execute() - 连接池配置:复用连接,避免频繁建立/断开。
- Pipeline 批量操作:合并多个命令减少网络往返。
- 5.5 架构升级
- 集群模式:对大容量或高并发场景,使用 Redis Cluster 分片数据。
- 读写分离:通过副本节点(Replica)分担读负载。
- 6. 监控与告警
- 关键指标:
- 慢查询数量(
slowlog_len
) - 内存使用率(
used_memory
) - 每秒操作数(
instantaneous_ops_per_sec
)
- 慢查询数量(
- 告警规则示例(Prometheus):
1
2
3
4
5
6
7- alert: RedisSlowQueriesHigh
expr: increase(redis_slowlog_entries[1m]) > 10
for: 5m
labels:
severity: warning
annotations:
summary: "Redis 慢查询激增 (实例 {{ $labels.instance }})"
- 关键指标:
7. 总结
步骤 | 操作 |
---|---|
识别慢查询 | 使用 SLOWLOG GET 或监控工具 |
定位问题根源 | 分析命令类型、Key 大小、内存/CPU 指标 |
优化命令与数据 | 替换高风险命令、拆分大 Key |
调整配置 | 内存策略、持久化参数、慢查询阈值 |
客户端改进 | Pipeline、连接池、异步操作 |
架构升级 | 集群化、读写分离 |
通过组合使用上述策略,可显著降低 Redis 慢查询对系统性能的影响,确保高吞吐与低延迟。
5. Lua 脚本在 Redis 中的作用是什么?为什么能保证原子性?
Lua 脚本在 Redis 中的作用
Lua 脚本允许在 Redis 服务端 原子性地执行多个命令,解决以下核心问题:
- 原子性操作:将多个命令组合为一个不可分割的操作,避免竞态条件。
- 减少网络开销:批量执行命令,节省多次网络往返时间(RTT)。
- 复杂逻辑处理:实现条件判断、循环、计算等编程逻辑(如分布式锁、限流算法)。
示例场景:
- 库存扣减:检查库存是否充足 → 扣减库存 → 记录日志,需确保原子性。
- 分布式锁:通过
SETNX
+EXPIRE
+ 条件判断实现锁的获取与续期。
为什么 Lua 脚本能保证原子性?
- 1. Redis 单线程模型
Redis 使用单线程处理命令,Lua 脚本执行期间 独占主线程,其他客户端命令必须等待脚本执行完成。 - 2. 脚本执行的隔离性
- 无并发干扰:脚本中的所有命令按顺序执行,中间不会插入其他客户端的操作。
- 状态一致性:脚本内的操作基于执行开始时的数据快照,确保逻辑一致性。
- 3. 错误处理机制
- 语法错误:脚本加载时直接报错,不会执行(如
redis.call("UNKNOWN")
)。 - 运行时错误:脚本执行到错误命令时 终止,但已执行的命令不会回滚(需开发者保证逻辑正确性)。
1
2-- 示例:对非哈希类型的键执行 HGET
redis.call("HGET", "non_hash_key", "field") -- 抛出错误,后续命令不再执行
- 语法错误:脚本加载时直接报错,不会执行(如
Lua脚本的执行流程
- 脚本加载与编译
- 接收脚本:客户端通过
EVAL
或EVALSHA
提交 Lua 脚本。 - SHA1 摘要:Redis 计算脚本内容的 SHA1 哈希值(如 c6b9a943…),作为脚本的唯一标识。
- 缓存机制:脚本首次执行时会被编译并缓存,后续通过 EVALSHA 直接调用缓存(避免重复传输)。
- 接收脚本:客户端通过
- 执行环境初始化
- 沙盒环境:每个 Redis 实例维护一个独立的 Lua 环境,限制危险函数(如
os.execute
)。 - 全局隔离:每个脚本在独立 Lua 协程中运行,避免全局变量污染。
- 沙盒环境:每个 Redis 实例维护一个独立的 Lua 环境,限制危险函数(如
- 原子化执行
- 单线程模型:Redis 主线程按顺序执行 Lua 脚本,期间阻塞其他命令。
- 原子性保证:脚本内的所有 Redis 命令(
redis.call()
)连续执行,无并发干扰。
- Redis 命令交互
- 命令执行:通过
redis.call("SET", "key", "value")
调用 Redis 命令。 - 错误处理:
redis.call()
:命令失败时抛出 Lua 错误,中断脚本。redis.pcall()
:返回错误表({err=”…”}),脚本可继续执行。
- 命令执行:通过
- 结果返回
- 序列化输出:Lua 脚本的返回值会被转换为 Redis 协议格式(如字符串、数组)返回给客户端。
与 Redis 事务(MULTI/EXEC)的对比
特性 | Lua 脚本 | 事务(MULTI/EXEC) |
---|---|---|
原子性 | ✅ 严格原子性(无其他命令干扰) | ✅ 原子性,但期间可能穿插其他客户端命令 |
错误处理 | ❌ 运行时错误不回滚已执行操作 | ❌ 仅语法错误回滚,运行时错误继续执行 |
复杂逻辑支持 | ✅ 支持条件判断、循环、计算 | ❌ 仅支持命令队列 |
性能 | ⚡️ 高(减少网络开销) | ⚡️ 中(仍需多次网络往返) |
Lua 脚本的局限性
- 无回滚机制:已执行的命令无法撤销,需通过业务逻辑补偿(如记录操作日志)。
- 阻塞风险:长耗时脚本会导致 Redis 主线程阻塞(需通过
SCRIPT KILL
终止)。 - 调试困难:缺乏服务端调试工具,需通过日志或
redis.log
输出信息。
最佳实践
- 1. 控制脚本复杂度
- 避免长循环:确保脚本在 毫秒级 完成。
- 分阶段处理:使用
SCAN
替代KEYS
遍历大数据集。
- 2. 使用 SHA1 缓存脚本
1
2
3
4-- 首次加载脚本并生成 SHA1 标识
local script_sha = redis.call("SCRIPT", "LOAD", script_content)
-- 后续通过 SHA1 执行
redis.call("EVALSHA", script_sha, numkeys, ...) - 3. 超时控制
配置lua-time-limit
(默认 5 秒),超时后可通过SCRIPT KILL
或SHUTDOWN NOSAVE
终止。 - 4. 代码示例(原子扣减库存) 调用方式:
1
2
3
4
5
6
7
8
9
10local key = KEYS[1] -- 库存键
local change = tonumber(ARGV[1]) -- 变更数量
local current = tonumber(redis.call("GET", key) or "0")
if current + change < 0 then
return nil -- 库存不足
end
redis.call("SET", key, current + change) -- 更新库存
return current + change1
EVAL "脚本内容" 1 stock:1001 -5 # 扣减 5 个库存
总结
关键点 | 说明 |
---|---|
原子性本质 | 独占执行线程,隔离外部操作 |
适用场景 | 需严格原子性的复杂操作(如库存、分布式锁) |
不适用场景 | 需回滚的事务、长耗时任务 |
替代方案 | 简单操作用 Pipeline,需回滚用 Redis 事务 + WATCH |
通过合理设计 Lua 脚本,可显著提升 Redis 在高并发场景下的数据一致性和性能,但需警惕阻塞风险和错误处理逻辑。
6. Redis 的集群模式(Cluster)如何实现数据分片(Sharding)?
Redis Cluster 通过 哈希槽(Hash Slot)分片 实现数据分布式存储,确保高可用与水平扩展。以下是其核心机制与工作流程:
一、数据分片原理
- 1. 哈希槽分配
- 总槽数:固定 16384 个槽(CRC16算法结果取模
16384
)。 - 键映射规则:
1
slot = CRC16(key) % 16384
- 哈希标签(Hash Tag):用
{}
指定部分 key 参与计算,强制多个键映射到同一槽。1
2
3# 示例:user:{1001}:profile 与 user:{1001}:orders 映射到同一槽
SET user:{1001}:profile "data"
SET user:{1001}:orders "data"
- 哈希标签(Hash Tag):用
- 总槽数:固定 16384 个槽(CRC16算法结果取模
- 2. 槽分配管理
- 节点职责:每个主节点负责处理一组槽(可通过
CLUSTER ADDSLOTS
手动分配或自动均衡)。 - 集群状态:所有节点维护完整的槽映射表(通过 Gossip 协议同步)。
- 节点职责:每个主节点负责处理一组槽(可通过
- 3. 客户端请求路由
- 直接访问:客户端发送命令至任意节点:
- 命中本地槽:直接处理。
- 槽位于其他节点:返回
MOVED <slot> <target-node-ip:port>
重定向响应。
- Smart Client:主流客户端(如 Jedis、redis-py)缓存槽映射表,直接路由请求至目标节点。
- 直接访问:客户端发送命令至任意节点:
二、集群节点通信
- 1. Gossip 协议
- 信息交换:节点间通过
PING/PONG
消息传递集群状态(槽分配、节点在线状态)。 - 故障检测:若节点 A 在
cluster-node-timeout
(默认 15 秒)内未收到节点 B 的 PONG,标记 B 为疑似下线(PFAIL),超过半数主节点确认后标记为下线(FAIL)。
- 信息交换:节点间通过
- 2. 数据迁移与平衡
- 槽迁移:通过
CLUSTER SETSLOT <slot> IMPORTING/MIGRATING
转移槽所有权。 - 在线迁移:使用
MIGRATE
命令原子化迁移键数据,期间对客户端请求返回ASK
重定向临时路由。
- 槽迁移:通过
三、高可用与故障转移
- 1. 主从复制
- 副本节点:每个主节点可配置 1 个或多个从节点(通过
CLUSTER REPLICATE <master-id>
)。 - 数据同步:异步复制主节点数据。
- 副本节点:每个主节点可配置 1 个或多个从节点(通过
- 2. 自动故障转移
- 触发条件:主节点被多数主节点判定为 FAIL 状态。
- 选举流程:
- 从节点发起选举,优先级高的节点胜出。
- 新主节点接管原主节点的槽,广播更新集群配置。
四、分片管理操作示例
- 1. 查看槽分配
1
CLUSTER SLOTS # 显示槽范围与对应主从节点
- 2. 手动迁移槽
1
2# 将槽 1000 从节点 A 迁移到节点 B
redis-cli --cluster reshard <node-A-ip:port> --cluster-from <node-A-id> --cluster-to <node-B-id> --cluster-slots 1000 --cluster-yes - 3. 集群扩容
1
2
3# 添加新节点并分配槽
redis-cli --cluster add-node <new-node-ip:port> <existing-node-ip:port>
redis-cli --cluster rebalance <new-node-ip:port> --cluster-weight <node-id>=1
五、核心优缺点
- 优点
- 自动分片:数据均匀分布,支持动态扩缩容。
- 高可用:主从切换保障服务连续性。
- 无中心节点:去中心化架构避免单点瓶颈。
- 缺点
- 跨槽操作限制:事务(MULTI)、Lua 脚本中的键需在同一槽。
- 【备注】 这一点,通常简单的场景直接使用哈希标签来解决,即在key中固定用大括号圈注某一个值,下文有介绍。
- 客户端复杂度:需支持集群协议的 Smart Client。
- 网络分区风险:脑裂场景下可能丢失写入(需合理配置
min-slaves-to-write
)。
- 跨槽操作限制:事务(MULTI)、Lua 脚本中的键需在同一槽。
六、分片方案对比
方案 | 描述 | 适用场景 |
---|---|---|
Redis Cluster | 官方原生分片,自动槽管理,高可用 | 大规模数据,需水平扩展 |
客户端分片 | 由客户端计算哈希,直连多个单机节点 | 简单分片,无高可用需求 |
代理分片(Twemproxy) | 代理中间件统一路由,客户端无感知 | 兼容旧客户端,维护成本高 |
七、最佳实践
- 预分配足够槽:避免后期迁移成本。
- 监控槽分布:确保均匀分配(使用
redis-cli --cluster check
)。 - 合理使用哈希标签:优化事务和批量操作。
- 设置合理超时:调整
cluster-node-timeout
平衡故障检测速度与误判率。
总结
Redis Cluster 通过哈希槽分片与去中心化架构,实现了数据的分布式存储与高可用。开发与运维中需关注槽均衡、客户端兼容性及网络分区处理,以充分发挥其横向扩展能力。
PS. 关于哈希标签(Hash Tag)
原理
- 哈希标签:在键名中使用
{}
包裹的部分作为哈希计算的输入,而非整个键名。 - 规则:若键名中存在
{...}
,则仅对{}
内的内容计算哈希值,否则使用整个键名。
示例
- 目标:确保
user:1001:profile
和user:1001:orders
分配到同一槽。 - 设计键名:在
{}
内使用相同标签(如用户ID):1
2
3# 键名示例
user:{1001}:profile
user:{1001}:orders - 验证槽位:
1
2redis-cli -c CLUSTER KEYSLOT user:{1001}:profile # 输出槽位(如 12345)
redis-cli -c CLUSTER KEYSLOT user:{1001}:orders # 输出相同槽位 12345
适用场景
- 事务操作(如
MULTI
/EXEC
)。 - Lua 脚本涉及多个键。
- 需要原子性操作跨键的业务逻辑(如订单与库存扣减)。
一些其它场景的比较
相比之下,还是直接使用哈希标签来的简单快速
| 方法 | 适用场景 | 复杂度 | 推荐指数 |
|————————|———————————|————|————–|
| 哈希标签 | 常规业务设计(90%场景) | 低 | ⭐⭐⭐⭐⭐ |
| 手动槽迁移 | 特殊分片需求(如数据局部性) | 高 | ⭐⭐ |
| Lua脚本同槽约束 | 原子性操作(事务/脚本) | 中 | ⭐⭐⭐⭐ |
7. Redis 主从复制的原理是什么?如何保证数据一致性?
Redis 主从复制通过 异步数据同步 实现主节点(Master)与从节点(Replica)之间的数据冗余,其核心原理和一致性保障机制如下:
一、主从复制工作原理
- 1. 复制流程
- 连接建立
- 从节点发送
REPLICAOF <master-ip> <master-port>
命令,向主节点发起复制请求。 - 主节点验证后建立连接,并为从节点分配复制缓冲区。
- 从节点发送
- 全量同步(Full Sync)
- 生成 RDB 快照:主节点执行
BGSAVE
生成当前数据的 RDB 文件。 - 传输 RDB:RDB 文件通过网络传输到从节点。
- 加载 RDB:从节点清空旧数据,加载 RDB 文件完成初始化。
- 积压缓冲区同步:主节点将生成 RDB 期间的新写入命令存入
repl_backlog
,RDB 传输完成后发送这些命令到从节点。
- 生成 RDB 快照:主节点执行
- 增量同步(Partial Sync)
- 主节点持续将新写入命令通过 异步方式 发送给从节点。
- 从节点接收并执行这些命令,保持数据实时更新。
- 连接建立
- 2. 断线重连优化
- 复制偏移量(Replication Offset)
主从节点各自维护一个偏移量计数器(master_repl_offset
和slave_repl_offset
)。 - 复制积压缓冲区(Repl Backlog)
主节点维护固定大小的环形缓冲区(默认 1MB),存储最近写入的命令。
若从节点断线后重连,且其偏移量仍在缓冲区范围内,则触发 增量同步;否则触发 全量同步。
- 复制偏移量(Replication Offset)
二、数据一致性保障
- 1. 最终一致性模型
- 异步复制:主节点写入成功后立即响应客户端,随后异步同步到从节点。
- 潜在不一致窗口:主从节点间存在短暂数据延迟(毫秒级到秒级,取决于网络和负载)。
- 2. 强一致性配置
通过以下参数强制主节点仅在数据同步到指定数量的从节点后才响应客户端写入(牺牲可用性换取一致性):触发条件:当活跃从节点数或延迟不满足时,主节点拒绝写入(返回错误)。1
2min-replicas-to-write 1 # 至少 1 个从节点确认
min-replicas-max-lag 10 # 从节点延迟不超过 10 秒 - 3. 同步策略优化
全量同步风险控制
- 增大
repl-backlog-size
(如 512MB),降低断线后全量同步概率。 - 避免主节点在高峰期执行
BGSAVE
(可通过repl-diskless-sync
配置无盘复制)。
- 增大
增量同步可靠性
- 使用
WAIT
命令阻塞客户端,直到数据同步到指定数量的从节点:1
2SET key value
WAIT 1 1000 # 等待 1 个从节点确认,超时 1000ms
- 使用
- 4. 故障恢复机制
- 主节点宕机:需手动或通过哨兵(Sentinel)/集群(Cluster)自动提升从节点为新主节点。
- 脑裂防护:配置
min-replicas-to-write
防止原主节点在隔离期间接受写入导致数据冲突。
三、监控与调优
- 1. 关键指标
- 主节点:
1
2
3
4
5
6
7INFO replication
# 输出示例
role:master
connected_slaves:2
master_repl_offset:123456
repl_backlog_active:1
repl_backlog_size:1048576 - 从节点:
1
2
3role:slave
master_link_status:up
slave_repl_offset:123456
- 主节点:
- 2. 调优建议
- 网络优化:主从节点部署在同机房或低延迟网络环境。
- 缓冲区配置:
1
2repl-backlog-size 512mb # 增大积压缓冲区
repl-diskless-sync yes # 无盘复制(适用于 SSD) - 持久化策略:主节点关闭
AOF
或使用appendfsync no
降低磁盘压力。
四、主从复制缺陷与替代方案
问题 | 解决方案 |
---|---|
异步复制导致数据丢失风险 | 启用 WAIT 命令或哨兵自动故障转移 |
单主节点写入瓶颈 | 使用 Redis Cluster 分片写入 |
全量同步资源消耗大 | 优化 repl-backlog 和网络带宽 |
五、总结
Redis 主从复制通过 异步全量/增量同步 实现数据冗余,其一致性模型为 最终一致性,适用于读扩展和灾备场景。通过配置 min-replicas-to-write
和 WAIT
命令可实现强一致性,但需权衡性能与可靠性。实际使用中需结合监控指标(如 master_repl_offset
)和故障转移机制(如哨兵)确保高可用。
8. Redis 的脑裂问题是什么?如何避免?
Redis 的 脑裂问题(Split-Brain) 指在网络分区或节点通信故障时,集群中出现多个主节点同时接受写入,导致数据冲突与丢失的严重问题。以下是其成因、危害及避免方案:
一、脑裂问题的成因
- 1. 网络分区
主节点与从节点/哨兵(Sentinel)之间网络断开,导致集群分裂为多个独立子集群:- 子集群 A:原主节点(Master-A)仍存活,但因网络隔离无法与哨兵通信。
- 子集群 B:哨兵选举出新主节点(Master-B),客户端开始向 Master-B 写入。
- 2. 误判与故障转移
- 哨兵误判:若哨兵集群的
quorum
(仲裁数)配置过低,可能误判原主节点下线,触发非必要故障转移。 - 双主写入:Master-A 和 Master-B 均接受客户端写入,数据分叉。
- 哨兵误判:若哨兵集群的
二、脑裂的危害
- 数据不一致:两个主节点的写入无法自动合并,导致键覆盖或冲突。
- 数据丢失:网络恢复后,原主节点(Master-A)可能被强制同步新主节点数据,覆盖其隔离期间的写入。
- 系统混乱:客户端可能随机连接不同主节点,出现不可预测的结果。
三、避免脑裂的核心方案
- 1. 合理配置哨兵参数
- 增加哨兵节点数:部署至少 3 个哨兵节点,提高决策可靠性。
- 调整仲裁阈值:
1
sentinel monitor mymaster 192.168.1.1 6379 2 # quorum=2(多数派决策)
- quorum:故障转移需至少
quorum
个哨兵同意。 - majority:实际执行故障转移需超过半数哨兵节点在线。
- quorum:故障转移需至少
- 2. 启用主节点写保护
通过 Redis 配置限制主节点在失去多数从节点连接时停止写入:1
2min-replicas-to-write 1 # 至少 1 个从节点存活
min-replicas-max-lag 10 # 从节点复制延迟不超过 10 秒- 效果:当主节点无法同步到足够从节点时,拒绝写入,避免孤立主节点继续服务。
- 3. 优化客户端路由
- 使用支持集群感知的客户端:如 JedisCluster、Lettuce,自动重定向到有效主节点。
- 降级策略:客户端检测到多个主节点时,暂停写入或切换只读模式。
- 4. 网络架构优化
- 避免单点网络故障:主节点与从节点跨机架/可用区部署。
- 心跳检测:缩短哨兵的
down-after-milliseconds
(默认 30 秒),快速检测节点故障。
- 5. 数据恢复与人工干预
- 强制切换前校验:网络恢复后,人工对比原主节点与新主节点的数据差异。
- 数据合并工具:使用
redis-audit
或自定义脚本修复冲突键。
四、Redis Cluster 的脑裂防护
Redis Cluster 通过 多数派原则 和 故障转移超时 降低脑裂风险:
- 主节点失效判定:需大多数主节点确认故障,才允许从节点接管。
- 节点通信超时:
cluster-node-timeout
(默认 15 秒)控制节点状态判断速度。 - 写保护:节点在失去半数以上主节点连接时,拒绝写入。
五、监控与告警
- 1. 关键指标
- 主从连接状态:
connected_slaves
。 - 复制延迟:
master_repl_offset
与slave_repl_offset
差值。 - 哨兵决策日志:监控
+switch-master
事件。
- 主从连接状态:
- 2. 告警规则
1
2
3
4
5
6
7
8# Prometheus 示例:检测主节点数量异常
- alert: RedisMultipleMasters
expr: count(redis_instance_info{role="master"} ) > 1
for: 1m
labels:
severity: critical
annotations:
summary: "Redis 集群存在多个主节点(脑裂风险)"
六、总结
措施 | 效果 | 适用场景 |
---|---|---|
合理配置哨兵 quorum | 减少误判,避免非必要故障转移 | 主从 + Sentinel 架构 |
启用主节点写保护 | 防止孤立主节点继续写入 | 所有主从复制场景 |
使用 Redis Cluster | 内置多数派决策,降低脑裂概率 | 大规模分布式环境 |
客户端降级策略 | 避免向无效主节点写入 | 高可用性要求严格的业务 |
核心原则:在网络分区不可避免时,通过牺牲部分可用性(拒绝写入)保障数据一致性。结合监控与自动化工具,快速定位并恢复脑裂状态,最大限度降低影响。
三、Redis 实战与优化
1. 如何保证 Redis 与数据库的双写一致性?
这个问题可以直接拿实际应用场景中的情况来说明,笔者之前工作中是使用的二级缓存架构 (Caffeine + Redis) 模式,为了要解决部分HotKey问题。直接看二级缓存场景是怎么解决的。
先要知道两个必须要明白的场景(用redis就跑不掉),一个更新,一个失效
关于缓存的更新策略
- 写策略:
- 写穿透(Write-Through):在更新数据库的同时,同步更新本地缓存和 Redis 缓存。确保缓存和数据库的数据一致。
- 写回(Write-Back):先更新本地缓存,然后异步批量更新 Redis 和数据库。这种方式性能较高,但存在数据丢失的风险。
- 写删除(Write-Delete)【建议】:更新数据库后,删除本地缓存和 Redis 缓存中的数据,后续请求会重新加载最新数据。
- 读策略:
- 先读本地缓存,未命中则读 Redis:如果 Redis 也未命中,则从数据库加载数据并更新两级缓存。
- 设置本地缓存过期时间:避免本地缓存数据长时间不一致。
关于缓存失效的机制
一般就是主动和被动的两种
- 主动失效:
- 当数据库数据更新时,主动失效或更新本地缓存和 Redis 缓存。
- 可以通过消息队列(如 Kafka、RocketMQ)或数据库的 binlog 监听(如 Canal)来通知缓存更新。
- 被动失效:
- 为本地缓存和 Redis 缓存设置合理的过期时间,确保缓存数据定期刷新。
- 本地缓存的过期时间应短于 Redis 缓存的过期时间,避免本地缓存数据长期不一致。
二级缓存架构下我们怎么做的
- 写策略:就是使用的写删除,并发场景,能很大程度避免不一致问题。
- 双写一致性问题:我们采用的是实现起来较为简单的方案。写策略只用“写删除”方式
- 先用写删除策略,先更新数据库,再直接删除Redis,后续靠读回写Redis。先保证Redis与数据库一致
- 本地缓存:借助Redis的发布订阅模式,当发出删除Redis命令的同时,发布更新本地缓存的消息到每一台机器中,再执行本地缓存的删除
- 本地缓存兜底保障:可能存在未收到redis广播消息的情况,使用Caffeine的自动过期策略来保证最终一致性,设置key一定的过期时间。
- 高级一点,引入红锁,但是可能会引入其它问题,综合考量
- 双写一致性问题:我们采用的是实现起来较为简单的方案。写策略只用“写删除”方式
- 读策略:先读本地,未命中则读Redis,redis没有命中再读数据库,读取之后回写redis和本地缓存。
- 体系架构:用Spring CacheManager统一托管
- 监控告警:
- 监控本地缓存和Redis命中率、数据一致性等指标
- 设置告警机制
2. 大 Key 和热 Key 问题如何识别与解决?
在 Redis 中,大 Key 和热 Key 问题会影响性能,以下是识别与解决这些问题的步骤:
- 识别大 Key 和热 Key
- 大 Key
- 定义:大 Key 是指包含大量数据(如字符串、列表、集合等)的 Key。
- 识别方法:
- 使用
redis-cli --bigkeys
命令扫描数据库,找出大 Key。 - 通过
MEMORY USAGE <key>
命令查看特定 Key 的内存使用情况。
- 使用
- 热 Key
- 定义:热 Key 是访问频率极高的 Key。
- 识别方法:
- 使用
redis-cli --hotkeys
命令(Redis 6.0+)找出热 Key。 - 通过
MONITOR
命令实时监控访问模式,识别高频访问的 Key。
- 使用
- 解决大 Key 问题
- 数据拆分
- 方法:将大 Key 拆分为多个小 Key。
- 示例:
- 原 Key:
user:12345:data
- 拆分后:
user:12345:data:part1
,user:12345:data:part2
, …
- 原 Key:
- 数据压缩
- 方法:对存储的数据进行压缩,减少内存占用。
- 示例:使用 Gzip 或 Snappy 压缩数据后再存储。
- 使用其他数据结构
- 方法:根据需求选择更合适的数据结构。
- 示例:将大列表改为多个小列表,或使用 HyperLogLog 进行基数统计。
- 解决热 Key 问题
- 缓存热 Key(二级缓存)
- 方法:在客户端或代理层缓存热 Key 数据,减少 Redis 访问。
- 示例:使用本地缓存(如 Guava Cache)缓存热 Key。
- 读写分离
- 方法:将读请求分散到多个从节点,减轻主节点压力。
- 示例:配置 Redis 主从复制,将读请求导向从节点。
- Key 分片
- 方法:将热 Key 分散到多个 Key 上,减少单个 Key 的压力。
- 示例:
- 原 Key:
hot:key
- 分片后:
hot:key:1
,hot:key:2
, …
- 原 Key:
- 其他优化措施
- 定期清理
- 方法:定期清理不再使用的 Key,释放内存。
- 示例:使用
EXPIRE
或DEL
命令清理过期或无用 Key。
- 监控与报警
- 方法:设置监控和报警机制,及时发现大 Key 和热 Key。
- 示例:使用 Prometheus 和 Grafana 监控 Redis 性能,设置报警规则。
3. 【DBA】 Redis 的内存碎片是如何产生的?如何优化?
【备注】 这部分笔者在工作中还没有涉及过
碎片产生的原因
- 频繁的内存分配与释放
- Redis 在处理数据时,会频繁分配和释放内存(如键的创建、删除、值更新等)。
- 这种操作会导致内存中出现大量不连续的小块空闲内存,无法被有效利用。
- 不同大小的键值对
- Redis 存储的键值对大小不一,分配的内存块大小也不同。
- 当释放大块内存后,剩余的小块内存可能无法满足后续的内存分配需求。
- 内存分配器的行为
- Redis 默认使用 jemalloc 或 libc 等内存分配器。
- 这些分配器为了提高性能,可能会将内存划分为不同大小的内存池,导致内存碎片。
- 数据过期或删除
- 当键过期或被删除时,释放的内存可能无法立即被重新利用,从而形成碎片。
- Redis 的持久化机制
- 在执行 RDB 或 AOF 持久化时,Redis 可能会创建子进程,子进程会复制父进程的内存空间,导致内存使用量增加,进一步加剧内存碎片问题。
优化方法
- 启用内存碎片整理
- Redis 4.0 及以上版本支持内存碎片整理功能(通过配置
activedefrag
参数)。 - 相关配置:
1
2
3
4activedefrag yes
active-defrag-ignore-bytes 100mb
active-defrag-threshold-lower 10
active-defrag-threshold-upper 100 - 作用:
- 当内存碎片超过一定阈值时,Redis 会自动整理内存碎片。
- 合理设置内存分配器
- Redis 默认使用 jemalloc,它在大多数场景下表现良好。
- 可以通过以下命令查看当前使用的内存分配器:
1
redis-cli info memory | grep mem_allocator
- 如果内存碎片问题严重,可以尝试切换内存分配器(如从 jemalloc 切换到 libc,或反之)。
- 优化键的过期策略
- 避免大量键同时过期,导致内存集中释放。
- 可以通过设置随机过期时间,分散键的过期时间。
- 控制键值对的大小
- 尽量避免存储过大的键值对,减少内存分配的不连续性。
- 对于大对象,可以考虑拆分为多个小对象存储。
- 定期重启 Redis
- 在业务低峰期,定期重启 Redis 实例,释放内存碎片。
- 重启后,Redis 会重新分配内存,减少碎片。
- 监控内存碎片率
- 使用
INFO memory
命令监控内存碎片率:1
redis-cli info memory | grep mem_fragmentation_ratio
- **
mem_fragmentation_ratio
**:- 该值表示内存碎片率,计算公式为:
used_memory_rss / used_memory
。 - 正常情况下,该值应接近 1。如果大于 1.5,说明内存碎片较严重。
- 该值表示内存碎片率,计算公式为:
- 限制内存使用
- 通过配置
maxmemory
参数限制 Redis 的最大内存使用量。 - 当内存达到上限时,Redis 会根据淘汰策略(如 LRU、LFU)删除部分键,释放内存。
- 使用 Redis 6.0 的 lazyfree 机制
- Redis 6.0 引入了
lazyfree
机制,可以异步释放大对象的内存,减少对主线程的阻塞。 - 相关配置:
1
2
3lazyfree-lazy-eviction yes
lazyfree-lazy-expire yes
lazyfree-lazy-server-del yes
4. 如何实现 Redis 的高可用(如哨兵模式、Cluster 模式)?
1. 哨兵模式(Sentinel)
- 实现步骤
- 配置主从复制
- 部署一个主节点(Master)和多个从节点(Slave)。
- 在从节点的配置文件中指定主节点的地址:
1
replicaof <master-ip> <master-port>
- 部署哨兵节点
- 部署多个哨兵节点(Sentinel),通常至少需要 3 个哨兵节点以确保高可用。
- 在哨兵节点的配置文件中指定监控的主节点:
1
sentinel monitor mymaster <master-ip> <master-port> <quorum>
mymaster
:主节点的别名。<quorum>
:仲裁数,表示至少需要多少个哨兵节点同意才能进行故障转移。
- 启动哨兵
- 启动 Redis 主从节点和哨兵节点。
- 哨兵会自动监控主节点的状态,并在主节点故障时选举新的主节点。
- 故障转移
- 当主节点不可用时,哨兵会选举一个从节点升级为主节点,并通知其他从节点切换主节点。
- 客户端通过哨兵获取新的主节点地址。
- 配置主从复制
- 优点
- 实现简单,适合中小规模部署。
- 支持自动故障转移,提高可用性。
- 缺点
- 主从模式下,写操作集中在主节点,可能存在性能瓶颈。
- 数据分片需要客户端实现。
2. 集群模式(Cluster)
- 实现步骤
- 部署 Redis 节点
- 部署多个 Redis 节点,每个节点既可以作为主节点,也可以作为从节点。
- 至少需要 6 个节点(3 个主节点 + 3 个从节点)。
- 配置集群
- 在每个节点的配置文件中启用集群模式:
1
2
3cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 15000 - 使用
redis-cli
创建集群:1
redis-cli --cluster create <node1-ip>:<port> <node2-ip>:<port> ... --cluster-replicas 1
--cluster-replicas 1
表示每个主节点有一个从节点。
- 在每个节点的配置文件中启用集群模式:
- 数据分片
- Redis Cluster 将数据分为 16384 个槽(slot),每个主节点负责一部分槽。
- 客户端根据键的哈希值将请求路由到对应的节点。
- 故障转移
- 当主节点不可用时,集群会自动将其从节点升级为主节点。
- 如果主节点和从节点都不可用,集群会进入故障状态,部分数据不可访问。
- 部署 Redis 节点
- 优点
- 支持数据分片,适合大规模数据存储。
- 高可用性,自动故障转移。
- 无需额外的哨兵节点。
- 缺点
- 部署和配置相对复杂。
- 客户端需要支持集群协议。
3. 哨兵模式 vs 集群模式
特性 | 哨兵模式(Sentinel) | 集群模式(Cluster) |
---|---|---|
数据分片 | 不支持,需客户端实现 | 支持,自动分片 |
高可用性 | 支持,自动故障转移 | 支持,自动故障转移 |
部署复杂度 | 简单 | 较复杂 |
适用场景 | 中小规模部署 | 大规模部署 |
性能瓶颈 | 写操作集中在主节点 | 写操作分散到多个节点 |
4. 其他高可用方案
- 4.1 Proxy 模式
- 使用代理(如 Twemproxy、Codis)实现数据分片和高可用。
- 优点:对客户端透明,支持多种 Redis 集群方案。
- 缺点:增加了一层代理,可能成为性能瓶颈。
- 4.2 云服务托管
- 使用云服务商提供的 Redis 托管服务(如 AWS ElastiCache、阿里云 Redis)。
- 优点:无需自行维护,支持自动扩展和高可用。
- 缺点:成本较高,依赖云服务商。
总结
- 哨兵模式 适合中小规模部署,实现简单,支持自动故障转移。
- 集群模式 适合大规模部署,支持数据分片和高可用,但部署和配置较复杂。
- 根据业务需求和数据规模选择合适的方案,同时可以结合代理模式或云服务托管进一步提升高可用性。
5. Redis 的并发竞争问题(如多个客户端同时写)如何解决?
在 Redis 中,多个客户端同时写入可能导致数据不一致或覆盖。以下是解决并发竞争问题的常见方法:
- 使用事务(MULTI/EXEC)
Redis 支持事务,通过MULTI
和EXEC
命令将多个操作打包执行,确保这些操作按顺序执行,不会被其他客户端打断。1
2
3
4MULTI
SET key1 value1
SET key2 value2
EXEC
- 使用事务(MULTI/EXEC)
- 使用 WATCH 命令
WATCH
用于监控一个或多个键,如果在事务执行前这些键被修改,事务将不会执行。1
2
3
4
5WATCH key1
val = GET key1
MULTI
SET key1 new_value
EXEC
- 使用 WATCH 命令
使用 Lua 脚本
Redis 支持 Lua 脚本,脚本在执行时是原子的,适合处理复杂逻辑。1
EVAL "local val = redis.call('GET', KEYS[1]); if val == ARGV[1] then redis.call('SET', KEYS[1], ARGV[2]) end" 1 key1 value1 value2
分布式锁
使用分布式锁(如 Redlock)确保同一时间只有一个客户端能执行写操作。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27import redis
import time
def acquire_lock(conn, lockname, acquire_timeout=10):
identifier = str(time.time())
end = time.time() + acquire_timeout
while time.time() < end:
if conn.setnx('lock:' + lockname, identifier):
return identifier
time.sleep(0.001)
return False
def release_lock(conn, lockname, identifier):
pipe = conn.pipeline(True)
while True:
try:
pipe.watch('lock:' + lockname)
if pipe.get('lock:' + lockname) == identifier:
pipe.multi()
pipe.delete('lock:' + lockname)
pipe.execute()
return True
pipe.unwatch()
break
except redis.exceptions.WatchError:
pass
return False
- 使用乐观锁
通过版本号或时间戳实现乐观锁,更新前检查数据是否被修改。
- 使用乐观锁
- 使用 Redis 模块
Redis 模块如 RediSearch 或 RedisJSON 提供更高级的并发控制机制。
- 使用 Redis 模块
- 总结
- 事务:适合简单操作。
- WATCH:适合需要监控的场景。
- Lua 脚本:适合复杂逻辑。
- 分布式锁:适合分布式环境。
- 乐观锁:适合高并发场景。
- Redis 模块:适合特定需求。
6. 【DBA】 如何监控 Redis 的性能指标(如 QPS、内存使用率、延迟)?
监控 Redis 的性能指标(如 QPS、内存使用率、延迟等)是确保其稳定运行的关键。以下是常用的监控方法和工具:
1. 使用 Redis 内置命令
Redis 提供了多个命令来获取性能指标:
1.1
INFO
命令
INFO
命令返回 Redis 的详细状态信息,包括内存、客户端、持久化、统计等。1
INFO
- 内存使用:
used_memory
、used_memory_rss
- QPS:
instantaneous_ops_per_sec
- 连接数:
connected_clients
- 持久化:
rdb_last_bgsave_status
、aof_last_bgrewrite_status
- 延迟:
latency
(需开启延迟监控)
- 内存使用:
1.2
SLOWLOG
命令
SLOWLOG
用于查看执行时间超过指定阈值的命令,帮助分析性能瓶颈。1
SLOWLOG GET 10 # 获取最近的 10 条慢查询
1.3
LATENCY
命令
LATENCY
用于监控 Redis 的延迟情况。1
2LATENCY LATEST # 查看最新的延迟事件
LATENCY HISTORY command_name # 查看某个命令的延迟历史1.4
MEMORY
命令
MEMORY
命令用于分析内存使用情况。1
2MEMORY STATS # 查看内存统计信息
MEMORY USAGE key_name # 查看某个键的内存占用2. 使用 Redis 监控工具
以下工具可以更方便地监控 Redis 的性能指标:
2.1 Redis CLI 监控
使用redis-cli
的--stat
选项实时监控 Redis 的状态。1
redis-cli --stat
2.2 RedisInsight
RedisInsight 是 Redis 官方提供的图形化监控工具,支持实时性能监控、慢查询分析、内存分析等。- 下载地址:RedisInsight
2.3 Grafana + Prometheus
通过 Prometheus 收集 Redis 指标,并使用 Grafana 进行可视化。- 步骤:
- 使用
redis_exporter
导出 Redis 指标。 - 配置 Prometheus 抓取
redis_exporter
的数据。 - 在 Grafana 中导入 Redis 仪表盘(如 ID 11835)。
- 使用
- 步骤:
2.4 Datadog
Datadog 是一个 SaaS 监控平台,支持 Redis 的性能监控。- 步骤:
- 安装 Datadog Agent。
- 启用 Redis 集成。
- 在 Datadog 仪表盘中查看 Redis 指标。
- 步骤:
2.5 Zabbix
Zabbix 是一个开源的监控工具,支持 Redis 的性能监控。- 步骤:
- 配置 Zabbix Server。
- 使用 Zabbix Agent 或自定义脚本收集 Redis 指标。
- 在 Zabbix 仪表盘中查看 Redis 数据。
- 步骤:
3. 监控关键指标
以下是一些需要重点监控的 Redis 性能指标:
- 3.1 QPS(每秒查询数)
- 指标:
instantaneous_ops_per_sec
- 说明:反映 Redis 的处理能力。
- 指标:
- 3.2 内存使用率
- 指标:
used_memory
、used_memory_rss
- 说明:监控内存使用情况,避免内存不足。
- 指标:
- 3.3 连接数
- 指标:
connected_clients
- 说明:监控客户端连接数,避免连接数过多导致性能下降。
- 指标:
- 3.4 延迟
- 指标:
latency
- 说明:监控命令执行延迟,及时发现性能瓶颈。
- 指标:
- 3.5 持久化状态
- 指标:
rdb_last_bgsave_status
、aof_last_bgrewrite_status
- 说明:确保 RDB 和 AOF 持久化正常工作。
- 指标:
- 3.6 命中率
- 指标:
keyspace_hits
、keyspace_misses
- 说明:计算命中率(
keyspace_hits / (keyspace_hits + keyspace_misses)
),评估缓存效果。
- 指标:
- 3.7 网络流量
- 指标:
total_net_input_bytes
、total_net_output_bytes
- 说明:监控网络流量,避免带宽瓶颈。
- 指标:
4. 自动化监控与告警
- 使用 Prometheus、Zabbix 或 Datadog 等工具设置告警规则,当关键指标(如内存使用率、延迟)超过阈值时,及时通知运维人员。
- 示例:
- 内存使用率 > 80%
- 延迟 > 100ms
- 连接数 > 1000
5. 最佳实践
- 定期分析慢查询:使用
SLOWLOG
定期分析慢查询,优化性能。 - 监控主从同步:如果使用主从架构,监控
master_repl_offset
和slave_repl_offset
,确保主从同步正常。 - 容量规划:根据业务增长趋势,提前规划 Redis 的内存和性能扩展。
7. Redis 在分布式场景下如何实现延迟队列?
1. 使用有序集合(Sorted Set)
Redis 的有序集合(Sorted Set)非常适合实现延迟队列。每个元素都有一个分数(score),可以用来表示任务的执行时间。
配合额外的一定频率执行的定时器去扫描当前时间之前的内容,就可以实现延时队列功能了。
实现步骤:
- 添加任务:
- 将任务的执行时间作为分数,任务内容作为成员,添加到有序集合中。
- 使用
ZADD
命令:例如:1
ZADD delay_queue <timestamp> <task>
1
ZADD delay_queue 1633072800 "send_email_to_user_123"
- 获取到期任务:
- 使用
ZRANGEBYSCORE
命令获取当前时间之前的所有任务。 - 使用
ZREMRANGEBYSCORE
命令移除这些任务。 - 示例:
1
2ZRANGEBYSCORE delay_queue 0 <current_timestamp>
ZREMRANGEBYSCORE delay_queue 0 <current_timestamp>
- 使用
- 处理任务:
获取到期的任务后,进行相应的处理。
为了确保操作的原子性,可以使用 Lua 脚本将多个操作合并为一个原子操作。在 Redis 中执行:1
2
3
4
5
6
7local tasks = redis.call('ZRANGEBYSCORE', KEYS[1], 0, ARGV[1])
if #tasks > 0 then
redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, ARGV[1])
return tasks
else
return nil
end1
EVAL "local tasks = redis.call('ZRANGEBYSCORE', KEYS[1], 0, ARGV[1]) if #tasks > 0 then redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, ARGV[1]) return tasks else return nil end" 1 delay_queue <current_timestamp>
2. 使用 Redis 的过期键和发布订阅机制
实现步骤:
- 设置过期键:
- 为每个任务设置一个键,并设置过期时间为任务的延迟时间。
- 使用
SET
和EXPIREAT
命令:1
2SET task:send_email_to_user_123 "content"
EXPIREAT task:send_email_to_user_123 <timestamp>
- 订阅过期事件:
- 使用 Redis 的
PSUBSCRIBE
命令订阅键过期事件。 - 配置 Redis 启用键空间通知:
1
CONFIG SET notify-keyspace-events Ex
- 订阅过期事件:
1
PSUBSCRIBE __keyevent@0__:expired
- 使用 Redis 的
- 处理过期事件:
- 当键过期时,Redis 会发布一个事件,订阅者可以接收到这个事件并处理相应的任务。
3. 使用 Redis Streams
Redis 5.0 引入了 Streams,也可以用来实现延迟队列。
实现步骤:
- 添加任务:
- 使用
XADD
命令将任务添加到 Stream 中。 - 示例:
1
XADD delay_queue * task "send_email_to_user_123"
- 使用
- 消费任务:
- 使用
XREAD
命令消费 Stream 中的任务。 - 示例:
1
XREAD BLOCK 0 STREAMS delay_queue 0
- 使用
- 延迟处理:
- 可以通过在任务中添加一个延迟时间字段,并在消费时检查是否到达执行时间。
8. 如何通过 Redis 实现排行榜、秒杀系统、好友关系等功能?
通过 Redis 实现排行榜、秒杀系统、好友关系等功能,可以充分利用 Redis 的高性能和丰富的数据结构。以下是具体实现方法:
1. 排行榜
Redis 的 Sorted Set
(有序集合)非常适合实现排行榜功能。
实现步骤:
- 添加分数:使用
ZADD
命令将用户及其分数添加到有序集合中。 - 更新分数:使用
ZINCRBY
命令增加用户的分数。 - 获取排名:使用
ZRANK
获取用户的排名,或使用ZREVRANK
获取倒序排名。 - 获取排行榜:使用
ZRANGE
或ZREVRANGE
获取指定范围的用户及其分数。
示例代码:
1 | # 添加用户分数 |
2. 秒杀系统
Redis 的原子操作和高并发能力非常适合实现秒杀系统。
实现步骤:
- 库存预减:使用
DECR
或INCRBY
命令原子性地减少库存。 - 防止超卖:使用 Lua 脚本确保库存检查和减少操作的原子性。
- 用户限购:使用
SETNX
或INCR
命令限制每个用户的购买数量。
示例代码:
1 | # 初始化库存 |
3. 好友关系
Redis 的 Set
(集合)非常适合存储好友关系。
实现步骤:
- 添加好友:使用
SADD
命令将用户 ID 添加到对方的好友集合中。 - 删除好友:使用
SREM
命令从对方的好友集合中移除用户 ID。 - 获取好友列表:使用
SMEMBERS
命令获取用户的好友列表。 - 共同好友:使用
SINTER
命令获取两个用户的共同好友。
示例代码:
1 | # 添加好友 |
总结对比
功能 | 核心数据结构 | 关键命令/操作 | 适用场景 |
---|---|---|---|
排行榜 | Sorted Set (ZSET) | ZADD、ZRANGE、ZINCRBY | 游戏积分、活动排名 |
秒杀 | String + List | DECR、RPUSH/LPOP、Lua 脚本 | 高并发库存扣减、订单队列 |
好友 | Set + Hash | SADD、SINTER、HSET | 社交网络关注/粉丝关系管理 |
9. Lua脚本计数器限流带来的问题
通过 Redis Lua 脚本实现计数器限流(如固定窗口、滑动窗口算法)虽然能保证原子性,但在实际应用中可能面临以下核心问题及解决方案:
一、核心问题分析
- 1. 时间同步问题
- 问题:依赖客户端或 Redis 服务器时间可能导致窗口计算不准确。
- 示例:客户端与 Redis 时钟不同步,导致限流窗口偏移。
- 临界值漏洞:固定窗口在时间边界(如 00:59 → 01:00)可能放过双倍流量。
- 解决方案:
- 使用 Redis 的
TIME
命令获取统一时间戳。 - 改用 滑动窗口算法(如基于
ZSET
存储请求时间戳)。
- 使用 Redis 的
- 问题:依赖客户端或 Redis 服务器时间可能导致窗口计算不准确。
- 2. 原子性陷阱
- 问题:虽 Lua 脚本整体原子,但部分逻辑需严格组合。
- 示例:首次初始化计数器时,需同时设置过期时间,若逻辑错误会导致计数器永不过期。
- 代码风险:
1
2
3
4
5
6
7local count = redis.call('GET', key)
if not count then
redis.call('SET', key, 1)
redis.call('EXPIRE', key, window) -- 若此处失败,计数器将无过期时间
else
redis.call('INCR', key)
end - 解决方案:
- 使用
SET
的NX
和EX
参数一步完成初始化和过期:1
2
3
4redis.call('SET', key, 1, 'NX', 'EX', window)
if not ok then
redis.call('INCR', key)
end
- 使用
- 问题:虽 Lua 脚本整体原子,但部分逻辑需严格组合。
- 3. 性能瓶颈
- 问题:高并发下频繁调用 Lua 脚本,导致 Redis 单线程阻塞。
- 示例:每秒数千次限流请求,每个请求触发脚本执行,增加延迟。
- 监控指标:
slowlog
中脚本执行时间超过 1ms。redis-cli --latency
显示平均延迟升高。
- 优化方案:
- 合并多个操作为一个脚本(如同时更新计数器和时间戳)。
- 客户端本地缓存部分计数(如使用 Guava RateLimiter 结合 Redis 校验)。
- 问题:高并发下频繁调用 Lua 脚本,导致 Redis 单线程阻塞。
- 4. 集群兼容性问题
- 问题:Redis 集群要求所有操作的 Key 位于同一槽(Slot)。
- 示例:使用
counter_key
和timestamp_key
未绑定同一槽时,脚本报错。
- 示例:使用
- 解决方案:
- 使用 哈希标签(Hash Tag) 强制 Key 路由到同一槽:
1
local key = "{user123}:rate_limit" -- 所有衍生 Key 自动同槽
- 使用 哈希标签(Hash Tag) 强制 Key 路由到同一槽:
- 问题:Redis 集群要求所有操作的 Key 位于同一槽(Slot)。
- 5. 资源泄漏与清理
- 问题:异常情况下计数器未正确过期,导致内存泄漏。
- 场景:脚本执行中途崩溃,未正确设置
EXPIRE
。
- 场景:脚本执行中途崩溃,未正确设置
- 防御措施:
- 所有写操作关联过期时间,即使更新时也续期:
1
2redis.call('INCR', key)
redis.call('EXPIRE', key, window) -- 每次操作重置过期时间 - 定期扫描清理无过期时间的 Key(需权衡性能)。
- 所有写操作关联过期时间,即使更新时也续期:
- 问题:异常情况下计数器未正确过期,导致内存泄漏。
- 6. 算法局限性
- 问题:固定窗口算法精度低,滑动窗口实现复杂。
- 固定窗口缺陷:允许窗口切换时双倍流量。
- 滑动窗口开销:需维护大量时间戳(如
ZSET
存储),内存占用高。
- 优化方案:
- 令牌桶算法:结合计数器和时间戳动态计算可用令牌。
- 分层限流:粗粒度(分钟级) + 细粒度(秒级)结合,降低计算开销。
- 问题:固定窗口算法精度低,滑动窗口实现复杂。
二、Lua 脚本实现示例(令牌桶算法)
1 | local key = KEYS[1] |
潜在问题:
- 计算
delta * rate
时未处理小数,导致精度丢失。 - 未处理 Redis 执行
SET
失败的情况(如内存不足)。
三、生产环境优化建议
- 监控与告警:
- 使用
INFO commandstats
监控脚本执行频率和耗时。 - 配置 Prometheus 告警规则,检测 Redis 内存和延迟异常。
- 使用
- 降级策略:
- Redis 不可用时,客户端降级为本地限流(如漏桶算法)。
- 使用熔断器(如 Hystrix)避免雪崩效应。
- 动态配置:
- 将速率(rate)和容量(capacity)作为参数传入,支持热更新。
- 结合配置中心(如 ZooKeeper、Nacos)动态调整阈值。
- 性能压测:
- 使用
redis-benchmark
模拟高并发限流请求:1
redis-benchmark -n 10000 -c 50 EVAL "$(cat token_bucket.lua)" 1 rate_limit_key 10 100
- 使用
四、替代方案对比
方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
Redis Lua 脚本 | 原子性、轻量级 | 性能瓶颈、实现复杂算法困难 | 中小规模、低复杂度限流 |
Redis Module (RedisCell) | 高性能、支持分布式令牌桶 | 需加载模块、版本兼容性 | 高并发、精准限流 |
Nginx 限流模块 | 高性能、低延迟 | 仅限 HTTP 流量、配置分散 | Web API 限流 |
分布式中间件(Sentinel) | 支持多语言、动态规则 | 运维复杂、额外资源开销 | 微服务架构、多协议限流 |
10. Redis下常见的限流实现算法
一、固定窗口计数器
原理:在固定时间窗口(如1分钟)内限制请求数量,超出阈值则拒绝。
问题:存在时间窗口临界值双倍请求漏洞。
Redis 实现:
1 | -- KEYS[1]: 限流键 |
优点:简单高效,内存占用低。
缺点:无法应对突发流量,临界漏洞明显。
二、滑动窗口计数器
原理:统计最近时间窗口(如1分钟)内的请求数,精准度更高。
Redis 实现(ZSET):
1 | -- KEYS[1]: 限流键 |
优点:精准控制时间窗口。
缺点:ZSET 内存消耗大,高频请求下性能下降。
三、令牌桶算法
原理:以恒定速率生成令牌,请求获取令牌后通过,允许突发流量。
Redis 实现:
1 | -- KEYS[1]: 令牌桶键 |
优点:支持突发流量,控制平滑。
缺点:需维护令牌数和时间戳,实现较复杂。
四、漏桶算法
原理:以固定速率处理请求,超出桶容量则拒绝。
Redis 实现(LIST):
1 | -- KEYS[1]: 漏桶键 |
优点:严格限制请求速率。
缺点:无法应对突发流量,队列管理复杂。
五、分布式限流(集群模式)
原理:通过哈希标签确保所有 Key 路由到同一槽。
示例代码(滑动窗口适配集群):
1 | -- KEYS[1]: {user123}:rate_limit (哈希标签强制同槽) |
关键点:所有相关 Key 需使用 {tag}
保证哈希一致性。
六、算法对比与选型
算法 | 适用场景 | 突发流量 | 内存开销 | 实现复杂度 |
---|---|---|---|---|
固定窗口 | 简单低频场景(如API鉴权) | 不支持 | 低 | 简单 |
滑动窗口 | 精准控制(如秒级限流) | 不支持 | 高 | 中等 |
令牌桶 | 允许突发(如下载限速) | 支持 | 中 | 复杂 |
漏桶 | 恒定速率(如短信发送) | 不支持 | 中 | 复杂 |
七、生产环境优化建议
- 性能调优:
- 使用 Pipeline 或 Lua 脚本合并多个命令。
- 滑动窗口算法中,按时间分片(如1秒/片)减少 ZSET 长度。
- 容错设计:
- 设置
maxmemory-policy
避免 OOM。 - 客户端本地缓存 + Redis 校验,降级保底。
- 设置
- 动态配置:
1
2-- 从 Redis Hash 读取动态参数
local rate = redis.call('HGET', 'rate_config', 'user_api') - 监控告警:
- 监控
slowlog
和memory usage
。 - 配置 Prometheus 统计限流拒绝率。
- 监控
通过合理选择算法及优化实现,Redis 可高效支撑百万级 QPS 的分布式限流需求。
四、Redis 底层原理
1. Redis 的 SDS(简单动态字符串)和 C 字符串有什么区别?
Redis 的 SDS(Simple Dynamic String,简单动态字符串)与 C 字符串的主要区别如下:
- 长度获取
- C 字符串:需要遍历整个字符串,时间复杂度为 O(n)。
- SDS:直接通过
len
属性获取,时间复杂度为 O(1)。
- 缓冲区溢出
- C 字符串:容易因未分配足够内存导致缓冲区溢出。
- SDS:通过
free
属性记录剩余空间,自动扩展内存,避免溢出。
- 内存分配
- C 字符串:每次修改都需重新分配内存。
- SDS:采用预分配和惰性释放策略,减少内存分配次数。
- 二进制安全
- C 字符串:以
\0
结尾,不能包含空字符,不适用于二进制数据。 - SDS:可以存储任意二进制数据,包括空字符。
- 兼容性
- SDS:兼容部分 C 字符串函数,可直接使用如
printf
等函数。
- 数据结构
- C 字符串:仅以
\0
结尾的字符数组。 - SDS:包含
len
、free
和字符数组的结构体。
示例代码
1 | struct sdshdr { |
2. Redis 的跳跃表(SkipList)是如何实现的?为什么用于有序集合?
跳跃表的实现原理
Redis跳跃表由两个核心结构组成:
- zskiplist:表示整个跳跃表,包含头尾指针、节点数量和最大层数。
1
2
3
4
5typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length; // 节点数量
int level; // 最大层数
} zskiplist; - zskiplistNode:表示跳跃表节点,包含成员对象、分值、后退指针及多层索引。
1
2
3
4
5
6
7
8
9typedef struct zskiplistNode {
robj *obj; // 成员对象(如字符串)
double score; // 分值(排序依据)
struct zskiplistNode *backward; // 后退指针(双向链表)
struct zskiplistLevel {
struct zskiplistNode *forward; // 前进指针
unsigned int span; // 跨度(节点距离)
} level[]; // 柔性数组,表示多层索引
} zskiplistNode;
- zskiplist:表示整个跳跃表,包含头尾指针、节点数量和最大层数。
关键特性
- 层(Level):每个节点有1~32层,层数由幂次定律随机生成(越高的层数概率越低)。
- 跨度(Span):记录当前节点到下一个节点在本层的距离,用于快速计算排名(Rank)。
- 后退指针(Backward):构成双向链表,支持逆序遍历。
核心操作
- 插入节点
- 随机生成新节点的层数(例如,zslRandomLevel()函数)。
- 从最高层开始查找插入位置,记录每层的前驱节点(update[]数组)和排名(rank[]数组)。
- 创建新节点,更新各层的前驱指针和跨度。
- 查询节点
- 从最高层开始,比较目标分值和当前节点的分值。
- 若当前层无法继续,下降到下一层,直到找到目标或遍历完所有层。
- 时间复杂度为平均 O(logN),最坏 O(N)。
- 插入节点
跳表结构示例







Redis选择跳跃表的原因
- 对比平衡树的优势
- 实现简单:平衡树(如红黑树)需要复杂的旋转操作维护平衡,而跳跃表通过随机层数简化了插入和删除逻辑。
- 高效的范围查询:跳跃表天然支持顺序遍历,有序集合的 ZRANGE、ZREVRANGE 等命令可直接通过双向链表实现,时间复杂度 O(logN + M)(M为范围大小);平衡树需要中序遍历。
- 内存效率:跳跃表通过稀疏索引(多层指针)减少冗余数据,而平衡树需要存储父/子节点指针。
- 与有序集合需求的契合
- 双权重排序:有序集合按分值(Score)排序,分值相同时按成员对象(Member)字典序排序。跳跃表可通过联合分值比较和成员对象比较实现这一点。
- 高效排名操作:通过节点的跨度(Span)属性,ZRANK、ZREVRANK 等命令可直接计算排名,无需额外遍历。
- 动态扩展性:跳跃表适合元素数量多或成员较长的场景(如存储用户ID和分数),而压缩列表(Ziplist)在数据量大时性能下降。
- 工程实践考量
- 与哈希表配合:有序集合实际由 跳跃表(按分值排序) 和 哈希表(按成员快速查找分值) 共同实现,两者结合兼顾了范围查询和单点查询的效率。
- 集群支持:跳跃表还用于Redis集群的内部数据结构(如维护槽分配信息),其简洁性降低了集群实现的复杂度。
延申问题,Redis中的跳表与红黑树比较
在 Redis 中,跳表(Skip List) 用于实现有序集合(Sorted Set),而 红黑树(Red-Black Tree) 并未直接使用。两者均为高效的有序数据结构,但在实现复杂度、性能特性和适用场景上有显著差异。以下是详细对比:
一、核心特性对比
特性 | 跳表(Skip List) | 红黑树(Red-Black Tree) |
---|---|---|
数据结构 | 多层链表结构,通过概率平衡 | 自平衡二叉搜索树,通过颜色标记和旋转保持平衡 |
时间复杂度 | 插入/删除/查找:平均 O(log N),最坏 O(N) | 插入/删除/查找:严格 O(log N) |
范围查询效率 | O(log N + M)(M为范围长度),支持高效顺序遍历 | O(log N + M),需中序遍历,性能略低于跳表 |
实现复杂度 | 简单(无需旋转或复杂平衡操作) | 复杂(需处理颜色标记、旋转等平衡逻辑) |
内存占用 | 较高(每个节点有多个指针,层数随机生成) | 较低(每个节点仅需存储左右子节点指针和颜色标记) |
并发性能 | 天然支持无锁并发(如 Redis 单线程模型下无竞争) | 需复杂锁机制(不适用于 Redis 的单线程设计) |
适用场景 | 需要高效范围查询和顺序访问的场景(如 Redis 的 ZRANGE) | 需要严格平衡和低内存占用的场景(如 C++ STL 的 std::map ) |
二、Redis 选择跳表的原因
1. 实现简单性与维护成本
- 跳表:代码量少(Redis 有序集合的跳表实现约 200 行),调试和维护成本低。
- 红黑树:需处理复杂的旋转和颜色调整逻辑(代码量是跳表的 3-5 倍),易出错。
2. 范围查询优势
- 跳表:通过高层指针快速定位范围起点,顺序遍历后续节点即可完成范围查询(如
ZRANGE
)。1
2
3
4
5
6// Redis 源码示例:跳表范围遍历
zskiplistNode *zn = zsl->header->level[0].forward;
while (zn != NULL && range_start <= zn->score) {
// 处理节点
zn = zn->level[0].forward;
} - 红黑树:需中序遍历子树,缓存局部性较差,性能略低于跳表。
- 跳表:通过高层指针快速定位范围起点,顺序遍历后续节点即可完成范围查询(如
3. 与 Redis 设计哲学的契合
- 单线程模型:跳表的无锁特性天然适合 Redis 的单线程架构,无需处理并发竞争。
- 功能需求:有序集合需支持 分值(Score)相同元素的存储(按字典序排序),跳表通过多层链表轻松实现,而红黑树需额外处理。
4. 概率平衡的灵活性
- 跳表:通过随机层数实现“概率平衡”,插入时不需立即调整结构,性能更稳定。
- 红黑树:每次插入/删除都可能触发旋转和颜色调整,导致性能波动。
三、性能对比示例
- 1. 插入操作
- 跳表:
1
2
3
4
5
6
7
8
9
10
11
12# 伪代码:跳表插入
def insert(key, score):
update = [] # 记录每层的前驱节点
current = header
for level in max_level downto 0:
while current.forward[level].score < score:
current = current.forward[level]
update[level] = current
new_node = create_node(key, score, random_level())
for level in 0 to new_node.level:
new_node.forward[level] = update[level].forward[level]
update[level].forward[level] = new_node- 耗时:平均 **O(log N)**,仅需调整前后指针。
- 红黑树:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15# 伪代码:红黑树插入
def insert(key, score):
node = create_node(key, score)
bst_insert(node) # 标准二叉搜索树插入
while node.parent.color == RED: # 颜色调整与旋转
if node.parent == node.parent.parent.left:
uncle = node.parent.parent.right
if uncle.color == RED:
recolor(node)
else:
if node == node.parent.right:
left_rotate(node.parent)
right_rotate(node.parent.parent)
# 类似处理右子树情况
root.color = BLACK- 耗时:严格 **O(log N)**,但旋转和颜色调整带来额外开销。
- 跳表:
- 2. 范围查询(ZRANGE)
- 跳表:
- 从高层指针快速定位起始位置,沿底层链表顺序遍历。
- 时间复杂度:O(log N + M),M 为返回元素数量。
- 红黑树:
- 需中序遍历子树,可能触发多次指针跳转。
- 时间复杂度:O(log N + M),但常数因子更高。
- 跳表:
四、内存占用分析
结构 | 跳表节点内存 | 红黑树节点内存 |
---|---|---|
基本字段 | 分值(Score)、成员(Key)、层数组 | 分值(Score)、成员(Key)、左右子节点指针、颜色标记 |
指针数量 | 平均 1.33 层(Redis 默认最大 32 层) | 2 个指针(左/右子节点) |
总内存 | 较高(约比红黑树多 30%-50%) | 较低 |
五、适用场景总结
场景 | 推荐数据结构 | 原因 |
---|---|---|
高频范围查询(如排行榜) | 跳表 | 范围遍历效率高,代码简单 |
严格内存限制环境 | 红黑树 | 节点内存占用更低 |
并发读写场景 | 跳表 | 天然适合无锁实现(如 Java 的 ConcurrentSkipListMap ) |
需要严格平衡的场景 | 红黑树 | 保证最坏情况下 O(log N) 性能 |
六、Redis 中跳表的实际实现优化
层数概率控制:
Redis 跳表通过ZSKIPLIST_P = 0.25
控制层数生成概率,使层数分布更均匀(越高层概率越低)。1
2
3
4
5
6
7// Redis 源码:随机生成层数
int zslRandomLevel(void) {
int level = 1;
while ((random() & 0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
level++;
return (level < ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}字典序支持:
当分值(Score)相同时,跳表通过比较成员(Key)的字典序维持有序性。内存压缩:
Redis 7.0 引入的 Listpack 结构优化了小规模有序集合的内存占用,仅在元素数量超过阈值时使用跳表。
总结
Redis 选择跳表而非红黑树,主要基于 实现简单性、高效范围查询 和 与单线程模型的契合。尽管跳表的内存占用略高,但其在有序集合的核心操作(插入、删除、范围遍历)上表现更优,且代码更易于维护。红黑树虽在严格平衡和内存效率上有优势,但更适合需要低内存开销或严格最坏情况性能的场景(如 C++ STL)。
延申
Java并发包中,也有类似的跳表实现,就是:ConcurrentSkipListMap
详细看我的另外一篇文章:https://nimbusk.cc/post/42af528b.html
3. Redis 的 Hash 表扩容机制(渐进式 Rehash)是如何工作的?
背景与设计动机
传统哈希表(如Java HashMap)在扩容时一次性迁移所有数据,可能因数据量庞大导致服务停顿。
Redis作为高性能内存数据库,需保证高可用性,因此采用渐进式Rehash机制,将数据迁移分摊到多次操作中完成
渐进式Rehash的核心步骤
- 初始化Rehash
- 分配新哈希表ht[1],大小为原表ht[0]的2倍(扩容)或满足当前数据量的最小2的幂(缩容)。
- 设置rehashidx=0,标识Rehash开始,后续操作将逐步迁移ht[0]的数据到ht[1]。
- 分步迁移数据
- 触发条件:每次对字典执行增删改查操作时,除执行操作本身外,顺带迁移ht[0]中rehashidx对应索引的所有键值对到ht[1],完成后rehashidx++。
- 迁移范围:以哈希桶(bucket)为单位,每次迁移一个桶内的数据,避免单次操作耗时过长。
- 完成Rehash
当ht[0]所有数据迁移完毕,rehashidx设为-1,释放ht[0]内存,将ht[1]设为新的ht[0],并创建新的空ht[1]备用。
一则rehash工作示意图:




扩容与缩容的触发条件
操作类型 | 触发条件 | 说明 |
---|---|---|
扩容 | 负载因子load_factor = ht[0].used / ht[0].size ≥ 1 | 默认情况下,当元素数量≥哈希表大小时触发扩容。若正在执行BGSAVE/BGREWRITEAOF,阈值提高至5。 |
缩容 | 负载因子load_factor ≤ 0.1 | 当元素数量≤哈希表大小的10%时触发缩容,避免内存浪费。 |
Rehash期间的操作处理
- 查询操作
先在ht[0]查找,未找到则到ht[1]查找,确保数据迁移过程中查询不遗漏。 - 新增操作
所有新数据直接写入ht[1],保证ht[0]只减不增,加速迁移完成。 - 删除/更新操作
同时在ht[0]和ht[1]执行,确保数据一致性。
优势与局限性
优势 | 局限性 |
---|---|
1. 避免服务阻塞:分摊计算量,保证Redis单线程的高性能。 | 1. 内存占用增加:需同时维护两个哈希表,可能瞬间占用双倍内存。 |
2. 平滑扩容:支持动态调整哈希表大小,适应数据量变化。 | 2. 潜在数据不一致:迁移过程中并发操作可能需额外处理(如双表查询)。 |
3. 可控性:通过rehashidx和分步迁移,控制迁移进度。 | 3. 内存突增风险:满容时Rehash可能触发大量Key驱逐。 |
4. Redis 的事件驱动模型(Reactor 模式)如何实现高并发?
Redis 的事件驱动模型基于 Reactor 模式实现高并发,其核心在于单线程事件循环 + I/O 多路复用 + 非阻塞异步处理的组合设计
一、核心组件与工作流程
Redis 的 Reactor 模式主要由以下组件构成:
- I/O 多路复用模块
- 使用 epoll(Linux)、kqueue(BSD)或 select 等系统调用监听多个套接字(socket)的读写事件。
- 将就绪事件封装为 aeFiredEvent 结构,存入事件队列 。
- 事件分派器(Dispatcher)
- 主循环(aeMain 函数)调用 aeProcessEvents 函数,从事件队列中取出就绪事件 。
- 根据事件类型(读/写)分发给对应的事件处理器(Handler)。
- 事件处理器(Handler)
- 连接应答处理器:处理客户端连接请求(accept)。
- 命令请求处理器:读取客户端发送的命令(read)。
- 命令回复处理器:向客户端返回执行结果(write)。
二、高并发的实现原理
- I/O 多路复用:单线程管理海量连接
- 机制:通过 epoll 等系统调用,单线程可同时监听成千上万个套接字,无需为每个连接创建独立线程。
- 优势:减少线程切换开销和内存占用,避免多线程锁竞争 。
- 单线程事件循环:原子性与顺序性
- 原子操作:所有事件处理(如读取命令、执行、写回结果)均在单线程内完成,无需考虑并发安全问题 。
- 顺序处理:事件队列保证请求按到达顺序被处理,避免竞态条件。
- 非阻塞异步处理:高效资源利用
- 非阻塞 I/O:套接字设置为非阻塞模式,读写操作立即返回,避免线程因等待 I/O 而阻塞。
- 异步事件驱动:仅在事件就绪时触发处理逻辑,CPU 资源集中于实际计算任务 。
- 事件分层的优先级设计
- 文件事件优先于时间事件:Redis 优先处理客户端请求(文件事件),再处理定时任务(如 serverCron)。
- 避免长阻塞:时间事件需在合理时间内完成,确保主循环快速响应新请求。
三、性能优化扩展(Redis 6.0+)
虽然 Redis 核心逻辑仍为单线程,但在高版本中引入多线程 I/O 进一步提升吞吐量:
- 主线程负责命令执行:保证原子性和顺序性。
- 多线程处理网络 I/O:
- 主线程接收连接并分发到 I/O 线程池。
- I/O 线程负责读取请求和写回结果,减轻主线程压力 。
四、与其他模型的对比
模型 | 优势 | 劣势 |
---|---|---|
Redis 单线程 Reactor | 简单、无锁、高吞吐(适用于内存操作) | 单线程 CPU 密集型任务可能成为瓶颈 |
多线程 Reactor | 更高网络吞吐(如 Redis 6.0 的 I/O 多线程) | 需处理线程同步问题 |
多进程模型 | 隔离性好(如 Nginx) | 进程间通信开销大 |
5. Redis 的持久化过程中,写时复制(Copy-on-Write)是如何应用的?
Redis在持久化过程中通过写时复制(Copy-On-Write, COW)技术实现高效的数据快照生成,主要应用于RDB持久化和AOF重写场景。
以下是其核心实现机制及作用分析:
一、RDB持久化中的写时复制
RDB通过bgsave命令生成内存快照,关键步骤如下:
- fork子进程
- 主进程调用fork()创建子进程,父子进程共享同一内存空间(代码段+数据段)。
- COW机制触发条件:当父进程(主线程)收到写请求时,修改共享内存页前会复制该页到新内存区域,子进程继续读取原页内容。
- 数据一致性保障
- 子进程生成快照时,数据状态在fork()瞬间被冻结,后续父进程的修改不影响快照内容。
- 内存页分离导致额外内存消耗,但通常不超过原内存的2倍(仅修改页被复制)。
- 性能优化
- 非阻塞主进程:子进程负责磁盘I/O,父进程持续处理客户端请求。
- 减少全量复制开销:COW仅复制被修改的页,而非整个数据集。
二、AOF重写中的写时复制
AOF重写(bgrewriteaof命令)同样依赖COW技术:
- 子进程生成新AOF文件
- 子进程遍历当前数据库状态,将数据转换为写命令写入临时文件。
- 父进程继续处理命令,并将新写入操作记录到AOF缓冲区和重写缓冲区。
- 合并与替换
- 重写完成后,子进程通知父进程将重写缓冲区内容追加到临时文件。
- 临时文件原子替换旧AOF文件,确保数据完整性。
三、COW技术的优势与限制
- 核心优势
- 内存高效:避免全量复制,仅复制修改页,降低内存冗余。
- 低延迟:主进程无阻塞,保障高吞吐量。
- 潜在风险
- 内存突增:频繁写入时,COW可能导致内存占用短暂上升(极端情况接近2倍原内存)。
- fork耗时:大内存实例的fork()操作可能延迟(需监控latest_fork_usec指标优化)。
6. Redis 的通信协议(RESP)是什么格式?
Redis 的通信协议 RESP(Redis Serialization Protocol)是一种简单、高效的文本协议,专为 Redis 客户端与服务端的高性能交互设计。其核心规则如下:
一、RESP 的数据类型及格式
RESP 通过 前缀字符 标识数据类型,所有消息以 \r\n
(CRLF)结尾。
数据类型 | 前缀 | 格式示例 | 说明 |
---|---|---|---|
简单字符串 | + |
+OK\r\n |
表示成功响应(如 SET 返回 “OK”)。 |
错误信息 | - |
-ERR unknown command\r\n |
表示错误响应,包含错误描述。 |
整数 | : |
:1000\r\n |
表示整数值(如 INCR 返回的计数)。 |
批量字符串 | $ |
$5\r\nhello\r\n |
二进制安全的字符串,$-1 表示 nil 。 |
数组 | * |
*2\r\n$3\r\nget\r\n$3\r\nkey\r\n |
表示命令或嵌套数据,*-1 表示空数组。 |
二、协议规则详解
- 1. 简单字符串(Simple Strings)
- 格式:
+<string>\r\n
- 示例:
+PONG\r\n
(PING
命令的响应)。 - 限制:不含
\r\n
,适合非二进制安全的短文本。
- 格式:
- 2. 错误信息(Errors)
- 格式:
-<error-type> <message>\r\n
- 示例:
-ERR wrong number of arguments\r\n
。 - 用途:服务端返回错误时使用。
- 格式:
- 3. 整数(Integers)
- 格式:
:<number>\r\
n` - 示例:
:42\r\n
(STRLEN
返回字符串长度)。 - 范围:64 位有符号整数。
- 格式:
- 4. 批量字符串(Bulk Strings)
- 格式:
$<length>\r\n<bytes>\r\n
- 示例:
$5\r\nhello\r\n
:字符串 “hello”。$-1\r\n
:表示nil
(如不存在的键)。
- 特性:支持二进制数据,长度精确控制。
- 格式:
- 5. 数组(Arrays)
- 格式:
*<count>\r\n<element-1>...<element-n>
- 示例:
*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n
:对应命令SET key value
。*-1\r\n
:表示空数组(某些命令的特殊返回)。
- 嵌套:数组元素可以是任意 RESP 类型。
- 格式:
五、协议优势
- 高效解析:前缀字符+长度声明,避免复杂解析。
- 二进制安全:支持任意字节数据(如图片、序列化对象)。
- 人类可读:调试时可直接查看原始协议内容。
- 轻量级:无冗余元数据,减少网络传输开销。
总结
RESP 以简洁的文本格式实现高效通信,是 Redis 高性能的核心支撑之一。理解 RESP 协议有助于:
- 开发自定义客户端或代理工具。
- 优化网络传输(如批处理命令)。
- 调试复杂交互场景(如管道、事务)。
五、场景与开放性问题
1. 如果 Redis 的内存满了,会发生什么?如何提前预防?
会发生什么?
当 Redis 的内存使用达到上限(通过 maxmemory
配置)时,Redis 会根据配置的 内存淘汰策略 来处理新写入的请求。具体行为如下:
内存淘汰策略:
Redis 提供了多种内存淘汰策略,常见的有:noeviction
(默认):拒绝所有写请求,返回错误,读请求正常处理。allkeys-lru
:从所有键中淘汰最近最少使用的键(LRU)。volatile-lru
:从设置了过期时间的键中淘汰最近最少使用的键。allkeys-random
:从所有键中随机淘汰一个键。volatile-random
:从设置了过期时间的键中随机淘汰一个键。volatile-ttl
:从设置了过期时间的键中淘汰剩余时间最短的键。
写请求失败:
如果配置了noeviction
策略,Redis 会拒绝写请求,并返回(error) OOM command not allowed when used memory > 'maxmemory'
错误。性能下降:
如果频繁触发淘汰机制,Redis 的性能可能会下降,因为淘汰过程需要额外的计算和内存操作。
如何提前预防 Redis 内存满的问题?
为了避免 Redis 内存满的问题,可以采取以下措施:
合理设置内存上限
- 通过
maxmemory
参数设置 Redis 的最大内存使用量,确保不超过物理内存的 70%-80%,避免系统 OOM(Out of Memory)。 - 示例配置:
1
maxmemory 4gb
- 通过
选择合适的内存淘汰策略
- 根据业务需求选择合适的淘汰策略。例如:
- 如果数据都可以淘汰,使用
allkeys-lru
。 - 如果只有部分数据可以淘汰,使用
volatile-lru
或volatile-ttl
。
- 如果数据都可以淘汰,使用
- 示例配置:
1
maxmemory-policy allkeys-lru
- 根据业务需求选择合适的淘汰策略。例如:
监控内存使用情况
- 使用 Redis 的
INFO memory
命令监控内存使用情况。 - 使用监控工具(如 Prometheus、Grafana)实时查看 Redis 的内存使用率、键数量等指标。
- 使用 Redis 的
优化数据结构
- 使用合适的数据结构存储数据,避免浪费内存。例如:
- 使用 Hash 存储对象,而不是多个独立的字符串键。
- 使用压缩列表(ziplist)存储小规模的列表或哈希。
- 使用合适的数据结构存储数据,避免浪费内存。例如:
设置合理的过期时间
- 对临时数据设置过期时间(TTL),避免无用数据长期占用内存。
- 示例命令:
1
SET key value EX 3600 # 设置 key 的过期时间为 3600 秒
分片(Sharding)
- 如果单机内存不足,可以使用 Redis Cluster 或客户端分片将数据分布到多个 Redis 实例中。
定期清理无用数据
- 定期扫描并清理无用的大 Key 或过期 Key。
- 使用
SCAN
命令代替KEYS
命令,避免阻塞 Redis。
使用外部缓存
- 对于不常访问的数据,可以将其存储到磁盘或其他存储系统中,减少 Redis 的内存压力。
启用持久化
- 如果数据允许丢失,可以启用 RDB 或 AOF 持久化,避免内存满时数据完全丢失。
总结
Redis 内存满的问题可以通过合理配置内存上限、选择淘汰策略、优化数据结构和监控内存使用来预防。关键在于根据业务需求设计合理的缓存策略,并结合监控和运维手段,确保 Redis 的稳定性和性能。
2. 如何设计一个支持百万 QPS 的 Redis 架构?
设计一个支持 百万 QPS 的 Redis 架构需要从多个方面进行优化和设计,包括 水平扩展、高可用性、性能优化 和 运维监控。以下是详细的设计思路:
1. 水平扩展
为了支持百万 QPS,单机 Redis 无法满足需求,必须通过 分布式架构 实现水平扩展。
- 1.1 Redis Cluster
- 数据分片:将数据分布到多个 Redis 节点上,每个节点负责一部分数据(通过哈希槽分配)。
- 高可用:每个分片可以配置主从复制,主节点故障时从节点自动切换为主节点。
- 扩展性:可以通过增加节点动态扩展集群容量。
- 1.2 客户端分片
- 如果不想使用 Redis Cluster,可以在客户端实现分片逻辑,将请求路由到不同的 Redis 实例。
- 优点:灵活性高,可以根据业务需求定制分片规则。
- 缺点:增加了客户端的复杂性。
- 1.3 Proxy 层
- 使用代理(如 Twemproxy、Codis)统一管理 Redis 实例,客户端只需与代理交互。
- 优点:简化客户端逻辑,支持动态扩缩容。
- 缺点:代理可能成为性能瓶颈。
2. 高可用性
为了保证系统的高可用性,需要设计 主从复制 和 故障转移 机制。
- 2.1 主从复制
- 每个主节点配置多个从节点,主节点负责写操作,从节点负责读操作。
- 通过主从复制实现数据冗余,主节点故障时从节点可以接管。
- 2.2 哨兵模式(Sentinel)
- 使用 Redis Sentinel 监控主从节点的健康状态,自动进行故障转移。
- 优点:自动化程度高,适合中小规模集群。
- 缺点:故障转移期间可能出现短暂的服务不可用。
- 2.3 Redis Cluster
- Redis Cluster 自带高可用性,节点故障时会自动进行主从切换。
- 优点:无需额外组件,适合大规模集群。
- 缺点:配置和管理相对复杂。
3. 性能优化
为了支持百万 QPS,需要对 Redis 的性能进行深度优化。
- 3.1 数据结构优化
- 使用合适的数据结构存储数据,例如:
- 使用 Hash 存储对象,而不是多个独立的字符串键。
- 使用压缩列表(ziplist)存储小规模的列表或哈希。
- 避免使用大 Key,将大 Key 拆分为多个小 Key。
- 使用合适的数据结构存储数据,例如:
- 3.2 Pipeline
- 使用 Pipeline 批量发送命令,减少网络往返时间(RTT)。
- 优点:显著提升批量操作的性能。
- 3.3 Lua 脚本
- 使用 Lua 脚本将多个操作合并为一个原子操作,减少网络开销。
- 优点:保证原子性,提升性能。
- 3.4 连接池
- 使用连接池管理客户端连接,避免频繁创建和销毁连接。
- 优点:减少连接建立的开销,提升性能。
- 3.5 多线程客户端
- 使用多线程客户端(如 Jedis、Lettuce)并发访问 Redis。
- 优点:充分利用多核 CPU 的性能。
4. 缓存策略
为了减轻 Redis 的压力,可以设计多级缓存策略。
- 4.1 本地缓存
- 在应用层使用本地缓存(如 Guava、Caffeine),缓存热点数据。
- 优点:减少对 Redis 的访问,降低延迟。
- 4.2 缓存分层
- 将缓存分为多层,例如:
- 第一层:本地缓存。
- 第二层:Redis 集群。
- 第三层:数据库。
- 优点:逐层过滤请求,减轻后端压力。
- 将缓存分为多层,例如:
- 4.3 缓存预热
- 在高峰期前提前加载热点数据到缓存中,避免冷启动问题。
5. 运维与监控
为了确保系统的稳定性,需要完善的运维和监控体系。
- 5.1 监控指标
- 监控 Redis 的关键指标,包括:
- QPS、延迟、内存使用率、连接数、命中率等。
- 使用工具:Prometheus + Grafana、Redis 自带的
INFO
命令。
- 监控 Redis 的关键指标,包括:
- 5.2 自动化运维
- 使用自动化工具(如 Ansible、Kubernetes)管理 Redis 集群的部署和扩缩容。
- 优点:减少人工操作,提高效率。
- 5.3 日志与告警
- 记录 Redis 的慢查询日志和错误日志。
- 设置告警规则,及时发现和处理异常。
6. 容灾与备份
为了应对极端情况,需要设计容灾和备份方案。
- 6.1 多机房部署
- 在多个机房部署 Redis 集群,避免单点故障。
- 使用跨机房同步工具(如 Redis Replication)保证数据一致性。
- 6.2 数据备份
- 定期备份 Redis 数据(RDB 或 AOF),并将备份文件存储到远程存储系统(如 S3、HDFS)。
- 优点:防止数据丢失。
7. 示例架构图
1 | +-------------------+ +-------------------+ +-------------------+ |
总结
设计一个支持百万 QPS 的 Redis 架构需要从 水平扩展、高可用性、性能优化 和 运维监控 等多个方面综合考虑。通过合理的分片策略、缓存优化和自动化运维,可以构建一个高性能、高可用的 Redis 集群。
3. Redis 和 Memcached 的区别是什么?为什么选择 Redis?
Redis 和 Memcached 的区别
Redis 和 Memcached 都是高性能的内存缓存系统,但它们在功能、性能和适用场景上有显著区别。以下是两者的主要对比:
特性 | Redis | Memcached |
---|---|---|
数据类型 | 支持多种数据结构:字符串、哈希、列表、集合、有序集合等。 | 仅支持简单的键值对(字符串)。 |
持久化 | 支持 RDB 和 AOF 两种持久化机制,数据可以持久化到磁盘。 | 不支持持久化,数据仅存储在内存中。 |
性能 | 单线程模型,性能极高,适合复杂操作。 | 多线程模型,性能极高,适合简单键值操作。 |
内存管理 | 支持多种内存淘汰策略(LRU、LFU 等)。 | 使用 LRU 算法淘汰数据。 |
分布式 | 支持 Redis Cluster,内置分布式解决方案。 | 需要客户端实现分布式(如一致性哈希)。 |
功能丰富性 | 支持事务、Lua 脚本、发布订阅、流水线等高级功能。 | 功能较为简单,主要用于缓存。 |
适用场景 | 缓存、会话存储、消息队列、排行榜、实时分析等。 | 主要用于简单的键值缓存。 |
为什么选择 Redis?
选择 Redis 的主要原因在于其 功能丰富性 和 灵活性,尤其是在需要复杂数据结构和高级功能的场景下。以下是选择 Redis 的具体理由:
- 1. 支持多种数据结构
Redis 不仅支持简单的键值对,还支持哈希、列表、集合、有序集合等数据结构。这使得 Redis 可以用于更复杂的场景,例如:- 排行榜:使用有序集合(ZSET)实现。
- 消息队列:使用列表(List)实现。
- 社交网络:使用集合(Set)存储好友关系。
- 2. 持久化支持
Redis 支持 RDB 和 AOF 两种持久化机制,可以将内存中的数据保存到磁盘,避免数据丢失。这对于需要数据持久化的场景非常重要。 - 3. 高可用性和分布式
- Redis 支持主从复制和哨兵模式,可以实现高可用性。
- Redis Cluster 提供了内置的分布式解决方案,支持水平扩展。
- 4. 丰富的功能
- 事务:支持简单的事务操作(MULTI/EXEC)。
- Lua 脚本:支持执行 Lua 脚本,实现复杂的原子操作。
- 发布订阅:支持消息的发布和订阅,适用于实时消息系统。
- 流水线:支持批量操作,减少网络开销。
- 5. 性能优异
虽然 Redis 是单线程模型,但其性能极高,尤其是在处理复杂操作时表现优异。通过 Pipeline 和 Lua 脚本,可以进一步提升性能。 - 6. 社区和生态
Redis 拥有活跃的社区和丰富的生态系统,支持多种语言的客户端,文档和工具也非常完善。
何时选择 Memcached?
尽管 Redis 功能更强大,但在某些场景下,Memcached 仍然是更好的选择:
- 简单键值缓存:如果只需要简单的键值缓存,Memcached 的性能和内存利用率可能更高。
- 多线程模型:Memcached 的多线程模型可以更好地利用多核 CPU,适合高并发的简单读写场景。
- 内存利用率:Memcached 的内存管理更为简单,适合存储大量小对象。
总结
- 选择 Redis:当需要复杂数据结构、持久化、高可用性、分布式支持或高级功能(如事务、Lua 脚本)时,Redis 是更好的选择。
- 选择 Memcached:当只需要简单的键值缓存,且对性能和内存利用率有极高要求时,Memcached 可能更适合。
4. 如何实现 Redis 与数据库的缓存一致性(如旁路缓存策略)?
实现 Redis 与数据库的缓存一致性 是一个常见的挑战,尤其是在高并发场景下。旁路缓存策略(Cache-Aside Pattern) 是一种常用的解决方案,其核心思想是:应用层负责管理缓存,缓存和数据库之间的数据同步由应用逻辑控制。
以下是实现缓存一致性的详细方案:
1. 旁路缓存策略的工作原理
- 读请求:
- 先查询缓存(Redis),如果命中缓存,直接返回数据。
- 如果未命中缓存,则查询数据库,将结果写入缓存,并返回数据。
- 写请求:
- 先更新数据库。
- 然后删除缓存(或更新缓存)。
- 读请求:
2. 实现缓存一致性的关键点
为了保证缓存与数据库的一致性,需要注意以下几点:- 2.1 写操作时删除缓存
- 在更新数据库后,删除缓存(而不是更新缓存),以避免并发写操作导致缓存与数据库不一致。
- 删除缓存后,下次读请求会从数据库加载最新数据到缓存。
- 2.2 读操作时加载缓存
- 如果缓存未命中,从数据库加载数据并写入缓存。
- 需要处理 缓存击穿 问题(即大量请求同时查询同一个未命中缓存的数据),可以通过互斥锁或分布式锁解决。
- 2.3 处理并发写问题
- 在高并发场景下,可能会出现以下问题:
- 写后读不一致:写操作更新了数据库,但缓存未及时删除,导致读请求读到旧数据。
- 写后写不一致:多个写操作顺序不一致,导致缓存与数据库不一致。
- 解决方案:
- 使用 分布式锁 保证写操作的顺序性。
- 在写操作完成后,延迟一段时间再删除缓存(双删策略)。
- 在高并发场景下,可能会出现以下问题:
- 2.4 处理缓存与数据库的失败场景
- 如果数据库更新成功但缓存删除失败,会导致缓存与数据库不一致。
- 解决方案:
- 引入 重试机制,确保缓存最终被删除。
- 使用 消息队列 异步删除缓存。
- 2.1 写操作时删除缓存
3. 具体实现步骤
以下是基于旁路缓存策略的具体实现步骤:3.1 读请求流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14def get_data(key):
# 1. 先查缓存
data = redis.get(key)
if data:
return data
# 2. 缓存未命中,查数据库
data = db.query(key)
if not data:
return None
# 3. 将数据写入缓存
redis.set(key, data)
return data3.2 写请求流程
1
2
3
4
5
6def update_data(key, value):
# 1. 更新数据库
db.update(key, value)
# 2. 删除缓存
redis.delete(key)3.3 处理并发写问题
这个是一个相对比较完美的解决方案1
2
3
4
5
6
7
8
9
10
11
12
13
14
15def update_data_with_lock(key, value):
# 1. 获取分布式锁
lock = acquire_lock(key)
if not lock:
raise Exception("Failed to acquire lock")
try:
# 2. 更新数据库
db.update(key, value)
# 3. 删除缓存
redis.delete(key)
finally:
# 4. 释放锁
release_lock(lock)3.4 处理缓存删除失败
1
2
3
4
5
6
7
8
9
10
11
12
13
14def update_data_with_retry(key, value):
# 1. 更新数据库
db.update(key, value)
# 2. 删除缓存,失败时重试
retry_count = 3
while retry_count > 0:
try:
redis.delete(key)
break
except Exception as e:
retry_count -= 1
if retry_count == 0:
raise e
4. 优化与进阶
- 4.1 双删策略
- 在更新数据库后,先删除缓存,延迟一段时间后再删除一次缓存。
- 目的是防止在删除缓存后、数据库更新完成前,有其他请求将旧数据写入缓存。
- 4.2 异步更新缓存
- 使用消息队列(如 Kafka、RabbitMQ)异步更新缓存,降低对写请求的性能影响。
- 4.3 缓存预热
- 在系统启动或数据更新后,提前加载热点数据到缓存中,避免冷启动问题。
- 4.4 监控与告警
- 监控缓存命中率、缓存与数据库的一致性,及时发现和修复问题。
- 4.1 双删策略
5. 总结
通过 旁路缓存策略,可以实现 Redis 与数据库的缓存一致性。核心要点包括:- 写操作时先更新数据库,再删除缓存。
- 读操作时先查缓存,未命中时从数据库加载数据并写入缓存。
- 处理并发写问题和缓存删除失败场景,确保数据一致性。
5. 在微服务架构中,Redis 可以承担哪些角色?
在微服务架构中,Redis 凭借其高性能、灵活的数据结构和分布式能力,可承担以下关键角色:
1. 分布式缓存(Cache)
- 作用:缓解数据库压力,加速高频读请求。
- 典型场景:
- 热点数据缓存:如商品详情、用户信息。
- 查询结果缓存:缓存复杂计算的 API 响应(设置合理 TTL)。
1
SET product:1001 "{name: 'Laptop', price: 999}" EX 300 # 缓存 5 分钟
- 优化技巧:
- 使用 Hash 结构缓存对象字段,避免序列化整个 JSON。
- 结合
EXPIRE
和主动更新(双删策略)保证数据一致性。
2. 分布式会话存储(Session Store)
- 作用:解决无状态微服务的会话共享问题。
- 场景:
- 用户登录状态存储在 Redis,跨服务实例共享。
1
2HSET session:abc123 user_id 1001 last_active 1620000000
EXPIRE session:abc123 3600 # 1 小时过期
- 用户登录状态存储在 Redis,跨服务实例共享。
- 优势:
- 避免粘性会话(Sticky Session)导致的负载不均。
- 支持集群模式实现高可用。
3. 分布式锁(Distributed Lock)
- 作用:协调跨服务的临界资源访问(如库存扣减)。
- 实现方案:
1
SET lock:order_1001 <unique_value> NX EX 30 # 获取锁(30 秒自动释放)
- 释放锁时通过 Lua 脚本验证值,避免误删其他客户端的锁。
- 推荐工具:使用 Redisson 客户端,内置 Watchdog 自动续期。
4. 消息队列与事件总线(Message Queue/Event Bus)
- 作用:实现服务间异步通信,解耦生产者和消费者。
- 方案对比:
方案 特点 适用场景 Pub/Sub 实时广播,无持久化 通知类消息(如配置更新) Streams 支持多消费者组、消息回溯和ACK机制 订单事件流、日志收集 List 简易队列(LPUSH/BRPOP) 任务队列(需自建可靠性机制) - Streams 示例:
1
2XADD orders * user_id 1001 product_id 2002 # 发布订单事件
XREADGROUP GROUP order_group consumer1 COUNT 1 STREAMS orders > # 消费
5. 限流与计数器(Rate Limiting & Counter)
- 作用:防止 API 滥用,保障系统稳定性。
- 实现方式:
- 滑动窗口限流:使用
ZSET
记录请求时间戳,定期清理过期数据。 - 令牌桶算法:通过
INCR
和EXPIRE
结合实现。1
2# 每分钟最多 100 次请求
CL.THROTTLE user:1001:api_write 100 60 60 1 # 使用 Redis-Cell 模块
- 滑动窗口限流:使用
- 场景:登录失败重试限制、API 调用配额管理。
6. 服务发现与配置中心(Service Discovery & Config)
- 作用:动态管理微服务实例地址和全局配置。
- 实现方案:
- 服务注册:实例启动时通过
SET
注册自身元数据(IP+Port)。 - 健康检查:通过
EXPIRE
设置 TTL,实例定期刷新(如HEARTBEAT
)。 - 配置推送:使用 Pub/Sub 或 Streams 广播配置变更事件。
1
2HMSET service:payment instances:1 "10.0.0.1:8080" instances:2 "10.0.0.2:8080"
PUBLISH config:update "new_feature_toggle=true" # 通知所有服务
- 服务注册:实例启动时通过
7. 实时数据聚合与分析
- 作用:快速统计高频访问的指标(如 UV、PV)。
- 数据结构:
- HyperLogLog:近似统计独立访客(误差约 0.81%)。
1
PFADD daily_uv 192.168.1.1 10.0.0.1 # 统计 UV
- Sorted Set:实时排行榜(如游戏积分)。
1
2ZADD leaderboard 5000 "user:A" 3000 "user:B"
ZREVRANGE leaderboard 0 9 WITHSCORES # Top 10
- HyperLogLog:近似统计独立访客(误差约 0.81%)。
8. 分布式事务协调
- 作用:简化 Saga 事务模式的补偿机制。
- 实现逻辑:
- 事务步骤状态存储在 Redis Hash 中。
- 每个步骤成功后更新状态。
- 若某步骤失败,触发补偿操作并回滚状态。
1
HSET tx:1001 step1 "completed" step2 "failed"
9. 实时监控与告警
- 作用:收集微服务的关键指标(如 QPS、错误率)。
- 实现方案:
- 时间序列数据:使用
TS.ADD
(RedisTimeSeries 模块)。 - 异常检测:结合
ZRANGE
和滑动窗口统计异常峰值。1
2TS.CREATE api_errors RETENTION 86400000 # 保留 24 小时
TS.ADD api_errors * 10 # 记录当前时间戳的 10 次错误
- 时间序列数据:使用
10. 分布式作业调度
- 作用:协调跨服务的定时任务执行。
- 方案:
- Sorted Set:按执行时间戳排序,消费者轮询获取到期任务。
- Redisson 的 RDelayedQueue:内置延迟队列实现。
1
ZADD scheduled_jobs <timestamp> "job:cleanup"
总结:Redis 在微服务中的定位
角色 | Redis 优势 | 替代方案 |
---|---|---|
缓存与会话存储 | 低延迟、高吞吐 | Memcached、Hazelcast |
分布式锁与协调 | 原子性操作、Lua 脚本支持 | ZooKeeper、etcd |
消息队列 | 轻量级、低延迟 | Kafka、RabbitMQ |
实时统计 | 内置数据结构(HLL、Sorted Set) | Prometheus、InfluxDB |
最佳实践建议:
- 避免持久化依赖:Redis 定位为缓存/临时存储,重要数据需落盘到数据库。
- 资源隔离:不同用途的 Redis 实例或集群分库(如
SELECT 0
缓存,SELECT 1
会话)。 - 监控告警:关注内存使用(
used_memory
)、延迟(latency
)、慢查询(slowlog
)等指标。
Redis特性
数据结构
这部分主要以3.0版本介绍为主,会穿插补充跟6.0版本差异的数据结构
简单动态字符串
Redis没有直接使用C语言传统的字符串表示(以空字符结尾的字符数组,以下简称C字符串),而是自己构建了一种名为简单动态字符串(simple dynamic string,SDS)的抽象类型,并将SDS用作Redis的默认字符串表示。
在Redis里面,C字符串只会作为字符串字面量(stringliteral)用在一些无须对字符串值进行修改的地方,比如打印日志:redisLog(REDIs_WARNING,"Redis is now ready to exit, bye bye...");
当Redis需要的不仅仅是一个字符串字面量,而是一个可以被修改的字符串值时,Redis就会使用SDS来表示字符串值,比如在Redis的数据库里面,包含字符串值的键值对在底层都是由SDS实现的。
这部分结构,完全可以与Java语言中的ArrayList数据结构来对比。
结构示意图如下所示:
1 | struct sdshdr |
为什么要设计SDS
常数复杂度取字符串长度
因为C字符串并不记录自身的长度信息,所以为了获取一个C字符串的长度,程序必须遍历整个字符串,对遇到的每个字符进行计数,直到遇到代表字符串结尾的空字符为止,这个操作的**复杂度为O(N)**。
和C字符串不同,因为SDS在1en属性中记录了SDS本身的长度,所以获取一个SDS长度的复杂度仅为O(1)。举个例子,对于下图所示的SDS来说,程序只要访问SDS的len属性,就可以立即知道SDS的长度为5字节
杜绝缓冲区溢出
这部分也得益于SDS记录长度,当发现空间不够存储时,会扩容,然后再进行操作。
减少修改字符串时带来的内存重分配次数
SDS通过未使用空间解除了字符串长度和底层数组长度之间的关联:在SDS中,buf数组的长度不一定就是字符数量加一,数组里面可以包含未使用的字节,而这些字节的数量就由SDS的free属性记录。通过未使用空间,SDS实现了空间预分配和情性空间释放两种优化策略。
- 空间预分配:空间预分配用于优化SDS的字符串增长操作:当SDS的API对一个SDS进行修改,并且需要对SDS进行空间扩展的时候,程序不仅会为SDS分配修改所必须要的空间,还会为SDS分配额外的未使用空间。大致的过程分为下面两种情况:
- 如果对SDS进行修改之后,SDS的长度(也即是len属性的值)将小于1MB,那么程序分配和len属性同样大小的未使用空间,这时SDS len属性的值将和
free属性的值相同。举个例子,如果进行修改之后,SDS的len将变成13字节,那么程序也会分配13字节的未使用空间,SDS的buf数组的实际长度将变成13+13+1=27字节(额外的一字节用于保存空字符)。 - 如果对SDS进行修改之后,SDS的长度将大于等于1MB,那么程序会分配1MB的未使用空间。举个例子,如果进行修改之后,SDS的1en将变成30MB,那么程序会分配1MB的未使用空间,SDS的buf数组的实际长度将为30MB+1MB+1byte。
- 如果对SDS进行修改之后,SDS的长度(也即是len属性的值)将小于1MB,那么程序分配和len属性同样大小的未使用空间,这时SDS len属性的值将和
- 惰性空间释放:情性空间释放用于优化SDS的字符串缩短操作:当SDS的API需要缩短SDS保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待将来使用。
二进制安全
为了确保Redis可以适用于各种不同的使用场景,SDS的API都是二进制安全的(binary-safe),所有SDS的API都会以处理二进制的方式来处理SDS存放在buf数组里的数据,程序不会对其中的数据做任何限制、过滤、或者假设,数据在写人时是什么样的,它被读取时就是什么样。
这也是我们将SDS的buf属性称为字节数组的原因-—Redis不是用这个数组来保存字符,而是用它来保存一系列二进制数据。
兼容部分C字符串函数
总结
C 字符串和 SDS 之间的区别
C字符串 | SDS |
---|---|
获取字符串长度的复杂度为O(N) | 获取字符串长度的复杂度为O(1) |
API是不安全的,可能会造成缓冲区溢出 | API是安全的,不会造成缓冲区溢出 |
修改字符串长度N次必然需要执行N次内存重分配 | 修改字符串长度N次最多需要执行N次内存重分配 |
只能保存文本数据 | 可以保存文本或者二进制数据 |
可以使用所有<string.h> 库中的函数 |
可以使用一部分<string.h> 库中的函数 |
其它知识点
这里面补充一些关于redis底层相关结构的特性
Redis Streams
Redis Streams 是 Redis 5.0 引入的专为消息流设计的数据结构,支持 多消费者组、消息回溯、ACK 确认 等特性,适合构建高可靠的消息队列系统。以下从核心原理、使用场景及完整示例进行解析:
一、核心工作原理
- 1. 消息结构
- 消息 ID:格式为
<时间戳-序号>
(如1629450000000-0
),支持自定义或自动生成。 - 字段值对:消息内容以键值对形式存储(最多支持 1GB)。
1
XADD orders * user_id 1001 product_id 2002 status "pending" # * 表示自动生成ID
- 输出示例:
"1629450000000-0"
- 输出示例:
- 消息 ID:格式为
- 2. 消息存储
- 内存存储:所有消息按 ID 顺序存储在内存中。
- 持久化:通过 RDB/AOF 机制持久化到磁盘。
- 容量控制:使用
MAXLEN
限制流长度(自动淘汰旧消息)。1
XADD temperature_stream MAXLEN ~ 1000 * value 25.3 # 保留约1000条最新数据
- 3. 消费者组(Consumer Group)
- 角色划分:
- 生产者:通过
XADD
发布消息。 - 消费者组:多个消费者共享消息处理负载。
- 消费者:组内独立处理消息的实例。
- 生产者:通过
- 关键机制:
- **Pending Entries List (PEL)**:记录已分配给消费者但未确认的消息。
- ACK 确认:消费者处理完成后发送
XACK
移除 PEL 条目。 - 消息重投:若消费者超时未确认,消息将重新分配给其他消费者。
- 角色划分:
二、核心操作命令与示例
- 1. 创建消费者组
1
XGROUP CREATE orders order_group $ MKSTREAM # 创建流及消费者组,$ 表示从最新消息开始消费
- 2. 生产消息
1
2XADD orders * user_id 1001 action "create_order"
XADD orders * user_id 1002 action "cancel_order" - 3. 消费者读取消息
- 独立消费者读取:
1
XREAD COUNT 2 STREAMS orders 0 # 从ID 0开始读取2条消息
- 消费者组内消费:
1
XREADGROUP GROUP order_group consumer1 COUNT 1 STREAMS orders > # 读取未分配给其他消费者的消息
>
表示获取新消息,若需重试处理旧消息,可指定具体 ID。
- 独立消费者读取:
- 4. 消息确认
1
XACK orders order_group 1629450000000-0 # 确认处理完成
- 5. 查看待处理消息
1
XPENDING orders order_group # 显示未确认消息数量、最早/最晚ID等
- 6. 重新分配失败消息
1
XCLAIM orders order_group consumer2 3600000 1629450000000-0 # 将消息转移给consumer2,最小空闲时间1小时
三、高级特性
- 1. 消息阻塞消费
1
XREADGROUP GROUP order_group consumer1 BLOCK 5000 COUNT 1 STREAMS orders > # 阻塞5秒等待新消息
- 2. 消息范围查询
1
2XRANGE orders 1629450000000 1629450060000 # 查询时间范围内的消息
XREVRANGE orders + - COUNT 10 # 逆序获取最近10条消息 - 3. 监控指标
- 查看流信息:
1
2XLEN orders # 消息总数
XINFO STREAM orders # 详细信息(长度、消费者组等) - 消费者组状态:
1
2XINFO GROUPS orders # 列出所有消费者组
XINFO CONSUMERS orders order_group # 查看组内消费者状态
- 查看流信息:
四、典型应用场景
- 1. 订单事件流
- 生产者(订单服务):
1
2XADD orders * order_id 3001 user_id 1001 status "created"
XADD orders * order_id 3001 user_id 1001 status "paid" - 消费者组(库存服务、物流服务):
1
2
3
4
5# 库存服务消费扣减库存
XREADGROUP GROUP order_group inventory_worker COUNT 1 STREAMS orders >
# 物流服务消费生成运单
XREADGROUP GROUP order_group logistics_worker COUNT 1 STREAMS orders >
- 生产者(订单服务):
- 2. IoT 设备数据收集
- 生产者(设备端):
1
XADD sensor_data MAXLEN ~ 10000 * device_id 1 temperature 25.3 humidity 60
- 消费者组(数据分析服务):
1
2XGROUP CREATE sensor_data analytics_group $ MKSTREAM
XREADGROUP GROUP analytics_group spark_consumer COUNT 100 STREAMS sensor_data >
- 生产者(设备端):
- 3. 实时日志处理
- 生产者(应用服务):
1
XADD logs * level "ERROR" message "DB connection failed" service "payment"
- 消费者组(报警服务、日志存档服务):
1
2
3
4
5# 报警服务实时过滤ERROR日志
XREADGROUP GROUP log_group alert_consumer COUNT 10 STREAMS logs >
# 存档服务批量消费日志到HDFS
XREADGROUP GROUP log_group archive_consumer COUNT 100 STREAMS logs >
- 生产者(应用服务):
五、对比其他消息队列
特性 | Redis Streams | Kafka | RabbitMQ |
---|---|---|---|
消息持久化 | 支持(RDB/AOF) | 支持(磁盘持久化) | 支持(内存/磁盘) |
消费者组 | 原生支持 | 原生支持 | 需要插件(如Quorum Queues) |
吞吐量 | 10万+/秒(依赖内存和网络) | 百万+/秒 | 万+/秒 |
顺序保证 | 严格按ID顺序 | 分区内有序 | 队列有序 |
适用场景 | 轻量级实时消息、简单事件溯源 | 高吞吐日志流、复杂事件处理 | 复杂路由、企业级消息协议 |
六、生产环境最佳实践
- 合理设置消费者组参数:
1
XGROUP CREATE orders order_group $ MKSTREAM ENTRIESREAD 1000 # 跟踪最近1000条消息
- 批量处理优化性能:
1
XREADGROUP GROUP order_group consumer1 COUNT 100 STREAMS orders > # 一次读取100条
- 异常处理机制:
- 重试策略:消息处理失败时,记录错误并重新放回队列。
- 死信队列:将多次重试失败的消息转移到独立 Stream 人工处理。
- 内存监控:
1
MEMORY USAGE orders # 查看流内存占用
总结
Redis Streams 通过 消息持久化、消费者组 和 ACK 机制 实现了高可靠的消息队列功能,适用于实时事件处理、日志收集等场景。其优势在于:
- 低延迟:内存操作实现微秒级响应。
- 灵活消费:支持多消费者组、消息回溯、阻塞等待。
- 无缝集成:复用 Redis 基础设施,无需额外部署中间件。
注意事项:
- 内存容量限制大规模历史数据存储(需设置
MAXLEN
或定期归档)。 - 复杂路由需求(如 Topic 路由)需结合其他数据结构(如 Hash)实现。
Redis中的红锁(Read Lock)
Redis 红锁(RedLock)是一种分布式锁算法,旨在通过多个独立的 Redis 节点提供高可靠的锁机制。其核心原理如下:
一、设计目标
解决单点 Redis 锁的 主从故障切换导致锁失效 问题,确保在部分节点故障时仍能保证锁的安全性。
二、工作原理
1. 节点要求
- 独立部署:至少 5 个 无主从关系的 Redis 节点(允许部分节点故障)。
- 时钟同步:节点间时钟偏差需控制在较小范围(依赖 NTP 服务)。
2. 加锁流程
- 获取当前时间(T1)。
- 依次请求锁:向所有节点发送
SET lock_name random_value NX PX ttl
命令。 - 统计成功节点数:若超过半数(如 5 个中 3 个)返回成功,且总耗时(T2-T1) < 锁有效时间(TTL),则认为加锁成功。
- 调整有效时间:锁的实际有效时间 = TTL - (T2-T1)。
3. 释放锁
向所有节点发送 Lua 脚本:仅当值匹配时删除键。
1
2
3
4
5if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
三、关键机制
1. 多数派原则(Quorum)
- 容错能力:允许最多
(N-1)/2
个节点故障(如 5 节点允许 2 个故障)。 - 防脑裂:确保同一时间只有一个客户端能获得多数节点认可。
- 容错能力:允许最多
2. 锁续约(可选)
- 看门狗线程:在锁过期前定期(如每 TTL/3 时间)向所有节点续期锁。
- 续约条件:需再次满足多数派成功。
3. 随机值(Unique Token)
- 唯一标识:每个锁持有者生成全局唯一随机值(如 UUID),避免误删其他客户端的锁。
四、争议与局限性
- 1. 时钟漂移风险
- 问题:节点间时钟不同步可能导致锁提前失效。
- 缓解:使用 NTP 同步时钟,缩短 TTL 减少影响。
- 2. GC 停顿与网络延迟
- 场景:客户端 A 获取锁后发生长时间 GC,锁过期且客户端 B 获得锁,导致资源冲突。
- 解决方案:结合 Fencing Token(如 ZooKeeper 的 zxid)确保操作顺序性。
- 3. 性能开销
- 多节点操作:加锁/解锁需访问所有节点,延迟高于单节点方案。
五、适用场景
- 高可靠性需求:如金融交易、库存扣减等需强一致性的场景。
- 容忍一定延迟:对锁获取时间敏感度较低的业务。
六、RedLock 实现示例(Python)
1 | import redis |
七、替代方案对比
方案 | 优点 | 缺点 |
---|---|---|
RedLock | 容错性强,无单点故障 | 实现复杂,需多节点部署 |
ZooKeeper | 强一致性,内置顺序保证 | 写性能较低,依赖 ZAB 协议 |
etcd | 高可用,支持租约(Lease) | 需要维护额外基础设施 |
总结
Redis 红锁通过 多数派投票机制 和 独立节点部署 提升分布式锁的可靠性,适用于对数据一致性要求较高的场景。实际使用中需结合时钟同步、Fencing Token 等机制规避潜在风险,并根据业务需求权衡性能与安全性。
Redis时间序列
RedisTimeSeries 是 Redis 的官方模块,专为高效存储和查询时间序列数据(如监控指标、IoT 传感器数据)设计。其核心实现原理围绕 内存优化存储、压缩算法 和 快速聚合计算,以下从数据结构、写入与查询机制、压缩策略等角度详细解析:
一、核心数据结构与存储模型
- 1. 时间序列创建
每个时间序列通过TS.CREATE
定义,包含以下元数据:- Key:唯一标识(如
temperature:sensor1
)。 - Retention:数据保留时间(如 30 天自动过期)。
- Labels:标签键值对(如
{device: "sensor1", region: "east"}
),用于快速过滤。 - Encoding:压缩编码方式(默认
COMPRESSED
)。
- Key:唯一标识(如
- 2. 数据存储结构
时间序列数据按 时间分块(Chunk) 存储,每块包含多个数据点(timestamp-value pairs),其内部采用 双流压缩算法(Delta-of-Delta + XOR) 优化存储空间:- Delta-of-Delta 编码:
将时间戳差值(timestamp delta)的差值再次压缩,适合时间间隔稳定的序列(如每秒采集)。 - XOR 编码:
对相邻数值的二进制位进行异或运算,仅存储变化部分(如浮点数的小幅波动)。
- 新数据点
(t1, v1)
写入当前活跃块。 - 当块达到预设大小(默认 128 个点)或时间跨度阈值时,压缩并转为只读。
- 新数据写入新块,旧块在内存中保留或持久化到磁盘。
- Delta-of-Delta 编码:
二、写入与查询优化
- 1. 高效写入
- 内存优先:新数据直接写入内存中的活跃块,延迟极低(微秒级)。
- 批量写入:支持
TS.MADD
一次插入多个数据点,减少网络开销。1
TS.MADD temperature:sensor1 1629450000 25.3 temperature:sensor1 1629450060 25.5
- 2. 快速查询
- 时间范围过滤:
利用块的时间元数据快速定位目标块,跳过无关数据。1
TS.RANGE temperature:sensor1 1629450000 1629453600
- 聚合计算:
支持SUM
、AVG
、MAX
等聚合函数,并可在查询时指定时间桶(Bucket)。1
TS.RANGE temperature:sensor1 1629450000 1629453600 AGGREGATION avg 3600000 # 按小时平均
- 降采样(Downsampling):
在查询阶段动态合并数据点,减少返回数据量。1
TS.RANGE temperature:sensor1 - + AGGREGATION avg 60000 # 每分钟一个点
- 时间范围过滤:
- 3. 多维度查询
通过 标签索引 支持类 SQL 的过滤与分组:1
TS.MQUERY FILTER region="east" AGGREGATION max 3600000 # 按地区分组查最大值
三、压缩与持久化
1. 压缩策略
- 块级压缩:每个块独立压缩,平衡查询效率与存储成本。
- 自适应编码:根据数据类型(整型、浮点型)自动选择最优压缩算法。
- 无损压缩:确保数据精确还原,适合需要高精度查询的场景。
2. 持久化机制
- RDB/AOF 集成:时间序列数据随 Redis 持久化策略保存到磁盘。
- 内存控制:通过
MAXSIZE
限制单个序列的内存占用,旧块自动淘汰或持久化。
四、性能优化技术
1. 预聚合(Pre-aggregation)
在写入阶段定义规则,提前计算并存储聚合结果(如每分钟平均值),加速高频查询:1
2TS.CREATE temperature:sensor1:avg_1m RETENTION 90d
TS.CREATERULE temperature:sensor1 temperature:sensor1:avg_1m AGGREGATION avg 600002. 多时间序列并行处理
- 集群支持:时间序列通过 Key 分片到不同 Redis 节点,横向扩展处理能力。
- Pipeline 批处理:减少客户端与服务器间的往返延迟。
五、对比传统时间序列数据库
特性 | RedisTimeSeries | InfluxDB |
---|---|---|
存储引擎 | 内存优先,块压缩 | 磁盘优化,TSM 结构 |
查询延迟 | 微秒级(内存直接访问) | 毫秒级(依赖索引) |
扩展性 | 依赖 Redis Cluster 分片 | 原生分布式架构 |
适用场景 | 实时监控、高频写入、低延迟查询 | 长期存储、复杂分析、高吞吐批处理 |
六、应用场景示例
- 实时监控系统:
1
2TS.ADD server:cpu_usage 1629450000 85.3
TS.RANGE server:cpu_usage - + AGGREGATION avg 10000 # 每10秒均值 - IoT 设备管理:
1
TS.MQUERY FILTER device_type="thermometer" AGGREGATION max 3600000
- 金融行情分析:
1
TS.CREATERULE stock:AAPL stock:AAPL:1min AGGREGATION last 60000 # 每分钟最后成交价
七、总结
RedisTimeSeries 的核心设计围绕 内存高效存储、实时查询 和 压缩优化,通过分块存储、双流压缩算法和预聚合机制,在保证低延迟的同时大幅降低存储开销。其优势在于:
- 极速响应:适合实时监控和告警场景。
- 无缝集成:作为 Redis 模块,复用现有基础设施与生态工具。
- 灵活查询:支持多维过滤、动态聚合与降采样。
适用局限:
- 数据规模受限于内存容量,需合理设置保留策略。
- 复杂分析(如跨序列关联)不如专业时序数据库强大。
对于需要 亚毫秒级延迟 和 Redis 生态集成 的场景,RedisTimeSeries 是轻量且高效的选择;而对于 海量历史数据分析,可将其与 Flink、Prometheus 等系统结合使用。
引用
[1] 黄建宏. Redis设计与实现. 机械工业出版社 2014