Redis常见知识点总结(上)
Redis 基础
什么是 Redis?
1. Redis 概述
Redis(REmote DIctionary Server)是一种基于 C 语言开发的开源、高性能的内存数据库(BSD 许可)。它本质上是一个键值对(Key-Value)存储系统,可以用作数据库、缓存和消息代理。由于 Redis 主要将数据存储在内存中,因此具有极高的读写性能,被广泛应用于高并发场景,如分布式缓存、会话管理、排行榜、限流、消息队列等。
2. Redis 的核心特性
- 多种数据类型:Redis 不仅支持字符串(String),还提供了列表(List)、集合(Set)、有序集合(Sorted Set)、哈希(Hash)、位图(Bitmap)、HyperLogLog、GEO 等多种数据结构,以适应不同的业务需求。
- 高性能:由于 Redis 采用内存存储,单机 QPS(每秒查询次数)可以达到十万级别,远超传统关系型数据库(如 MySQL)。
- 持久化机制:支持 RDB(定期快照)和 AOF(追加日志)两种持久化方式,保证数据在意外宕机后不会丢失。
- 分布式架构:
- 主从复制(Replication):支持一主多从架构,提高读取性能并实现数据冗余备份。
- Sentinel(哨兵):自动故障转移和高可用保障。
- Cluster(集群模式):支持数据分片,提高可扩展性和吞吐量。
- 事务支持:通过
MULTI
、EXEC
、DISCARD
以及WATCH
关键字,实现简单的事务机制。 - 消息队列:提供**发布/订阅(Pub/Sub)**机制,可用于实时消息推送。
- Lua 脚本:内置 Lua 解释器,可使用
EVAL
运行脚本,提高复杂操作的执行效率。
3. Redis 的适用场景
Redis 主要用于以下几种场景:
- 缓存:加速数据库查询(如 MySQL),存储热点数据,减少数据库压力。
- 会话存储:在分布式系统中存储用户会话(如电商网站的购物车)。
- 排行榜和计数器:使用
Sorted Set
实现排行榜,INCR
计数器用于统计点赞、浏览量等。 - 分布式锁:使用
SET NX
和EXPIRE
组合实现高效的分布式锁。 - 消息队列:基于
List
结构实现生产者-消费者模式,也可以使用Pub/Sub
进行消息广播。 - 地理位置计算:
GEO
结构可存储地理位置信息,支持距离计算。
4. Redis 安装与使用
Redis 主要运行在 Linux 服务器上,官方推荐生产环境使用 Linux 部署 Redis。你可以:
在本地安装 Redis:
# Ubuntu 安装 sudo apt update sudo apt install redis-server # Mac 安装(使用 Homebrew) brew install redis
启动 Redis 服务器:
redis-server
连接 Redis:
redis-cli
如果只是想体验 Redis,Redis 官网还提供了一个在线 Redis 体验环境(部分命令可能无法使用)。
5. Redis 与其他数据库的对比
特性 | Redis | MySQL | MongoDB |
---|---|---|---|
数据存储 | 内存 | 磁盘 | 磁盘 |
数据模型 | 键值对 | 关系型表 | 文档存储 |
读写性能 | 高(内存操作) | 中(磁盘 + 索引) | 高(文档查询) |
事务支持 | 有限事务支持 | 完整 ACID 事务 | 事务支持较弱 |
分布式 | 原生支持 Cluster | 依赖 MySQL 主从 | 原生分片 |
结论
Redis 由于超高的性能、丰富的数据结构、强大的持久化能力和分布式支持,已经成为现代互联网架构中不可或缺的一部分。它不仅能用作缓存,还能用于消息队列、排行榜、会话存储、分布式锁等多个场景,是开发者的必备技术之一。
Redis 为什么这么快?
1. 基于内存存储
Redis 所有数据都存储在内存,相比于基于磁盘的数据库(如 MySQL),内存的读写速度要快 几个数量级。
- 内存访问:纳秒级(ns),约 100ns
- 磁盘访问(HDD):毫秒级(ms),约 10ms
- 磁盘访问(SSD):微秒级(µs),约 100µs
这意味着 Redis 的数据访问比传统数据库快 10万倍以上。
2. 单线程+IO 多路复用(高效事件处理)
Redis 采用单线程 + IO 多路复用(Reactor 模式),极大地减少了线程切换带来的开销:
- 单线程模型:避免了多线程同步和锁竞争,减少了上下文切换的开销。
- IO 多路复用:Redis 使用
epoll
、select
、kqueue
等高效的 IO 多路复用技术,能够同时监听多个连接,而不会因为阻塞而影响性能。
单线程并不代表 Redis 只能处理一个请求,实际上 Redis 通过高效的事件循环机制,可以在短时间内处理大量并发请求。
3. 高效的数据结构
Redis 采用了优化过的数据结构,使得数据存储和访问更快:
- String(简单动态字符串 SDS):预分配空间,减少扩容操作,避免碎片化。
- List(双向链表或压缩列表):支持快速插入和删除。
- Set(哈希表或整数集合):O(1) 时间复杂度的查找。
- Sorted Set(跳表):比 B+ 树更高效的排序方式,查询性能优秀。
- HyperLogLog、GEO 等优化存储结构,占用更少内存。
4. 高效的通信协议
Redis 使用**RESP(REdis Serialization Protocol)**协议,解析简单高效:
- 文本格式:易于解析,减少 CPU 消耗。
- 流水线(Pipeline):允许多个命令一起发送,提高吞吐量。
- 二进制安全:不受数据格式限制。
5. 纯 C 语言实现,优化良好的代码
Redis 采用 C 语言编写,核心代码小巧精悍,执行效率极高:
- C 语言本身比 Java、Python 这些高级语言运行速度更快。
- 内存管理优化,减少 GC(垃圾回收)开销。
- 数据结构和算法经过精心优化,最大化 CPU 利用率。
Redis 为什么不能作为主数据库?
虽然 Redis 非常快,但并不适合作为主数据库,主要有以下几点原因:
内存成本高
- Redis 存储在内存中,而内存比磁盘贵很多,大量数据存储在 Redis 会成本高昂。
- 对于 TB 级数据,存储在 MySQL + SSD 便宜得多,而 Redis 主要用于缓存热点数据。
持久化机制有数据丢失风险
- 虽然 Redis 提供 RDB 和 AOF 持久化,但都可能存在数据丢失的情况:
- RDB 是定期快照,如果 Redis 崩溃,最近的变更数据会丢失。
- AOF 可能存在写入延迟,导致部分数据未持久化。
- 虽然 Redis 提供 RDB 和 AOF 持久化,但都可能存在数据丢失的情况:
缺乏 SQL 查询能力
- Redis 只支持 Key-Value 方式访问数据,不像 MySQL 这样支持复杂查询、事务、索引等。
不适用于强一致性场景
- Redis 默认是最终一致性,不保证数据严格一致,而传统关系型数据库(如 MySQL)可以提供强一致性(ACID 事务)。
总结
Redis 之所以快,主要是因为:
- 基于内存,避免磁盘 IO 开销。
- 单线程 + IO 多路复用,减少线程切换损耗。
- 高效的数据结构,提供 O(1) 级别的读写操作。
- 高效的协议,减少网络传输开销。
- C 语言编写,性能优化到极致。
但由于内存成本高、数据持久化能力有限、缺乏 SQL 查询、弱一致性等缺点,Redis 主要用作缓存,而不是主数据库。
除了 Redis,你还知道其他分布式缓存方案吗?
1. Memcached
Memcached 是最早的分布式缓存方案之一,主要用于对象缓存,减少数据库查询次数,提高网站性能。与 Redis 相比,它的功能相对简单:
- 优点:
- 纯内存存储,速度快,性能稳定。
- 采用多线程架构,比 Redis 单线程模式在高并发下更具优势。
- 适用于短期数据存储(如会话缓存)。
- 缺点:
- 不支持持久化,服务器重启后数据丢失。
- 不支持复杂数据结构,只能存储字符串和简单的 Key-Value 对。
- 不支持发布订阅(Pub/Sub)、事务、Lua 脚本等功能。
- 适用场景:
- 适用于短期缓存,如网页片段缓存、数据库查询结果缓存。
对比 Redis:
Memcached 在某些场景(如纯 Key-Value 读取)性能比 Redis 高,但 Redis 提供了更多的数据类型、持久化、集群支持,因此 Redis 逐渐取代了 Memcached。
2. DragonflyDB
DragonflyDB 号称“全世界最快的内存数据库”,是一个完全兼容 Redis 和 Memcached API 的高性能缓存。
- 优点:
- 多线程设计,相比 Redis 单线程模型,充分利用多核 CPU,提高吞吐量。
- 更低的内存占用,采用更优的内存管理方案(如对象池化)。
- 完全兼容 Redis 和 Memcached,可无缝迁移。
- 适用场景:
- 高吞吐缓存需求,如 AI 推理缓存、广告投放、推荐系统等。
- 需要高效的多核利用的应用场景。
3. KeyDB
KeyDB 是 Redis 的高性能分支,专注于多线程、多写性能优化。
- 优点:
- 多线程架构,比 Redis 的单线程架构更高效。
- 减少上下文切换开销,优化性能,吞吐量比 Redis 高 2~5 倍。
- 支持多主节点复制(Active-Active Cluster),比 Redis 主从模式更稳定。
- 适用场景:
- 高并发应用,如大规模 API 缓存、实时数据流处理等。
4. Tendis(腾讯)
Tendis 是 腾讯开源的分布式高性能 KV 存储数据库,100% 兼容 Redis 协议,底层基于 RocksDB 存储引擎。
- 优点:
- 冷热数据混合存储,支持大规模数据持久化(相比 Redis 更节省内存)。
- 支持数据落盘(类似 RocksDB),数据量大时更加经济高效。
- 适合海量 Key-Value 存储场景,如广告系统、推荐系统等。
- 缺点:
- 开源版本已不再维护,生态不如 Redis 。
- 性能受 RocksDB 影响,延迟可能比纯内存数据库更高。
结论:为什么 Redis 仍然是首选?
虽然有很多 Redis 替代方案,但 Redis 仍然是分布式缓存的首选,原因如下:
- 成熟稳定:Redis 经过多年优化,已被广泛验证。
- 功能强大:支持持久化、集群、事务、Lua 脚本、发布订阅等,适用于各种业务场景。
- 生态完善:大量开源工具、文档、社区支持,使用门槛低。
- 运维简单:Redis 提供了完善的监控、集群管理工具,易于维护。
除非你的业务有极端高并发(需要 KeyDB)、超大数据量(需要 Tendis)、更高的吞吐量(需要 DragonflyDB),否则 Redis 依然是最好的选择!
说一下 Redis 和 Memcached 的区别和共同点
现在公司一般都是用 Redis 来实现缓存,而且 Redis 自身也越来越强大了!不过,了解 Redis 和 Memcached 的区别和共同点,有助于我们在做相应的技术选型的时候,能够做到有理有据!
共同点:
- 都是基于内存的数据库,一般都用来当做缓存使用。
- 都有过期策略。
- 两者的性能都非常高。
区别:
- 数据类型:Redis 支持更丰富的数据类型(支持更复杂的应用场景)。Redis 不仅仅支持简单的 k/v 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储。Memcached 只支持最简单的 k/v 数据类型。
- 数据持久化:Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而 Memcached 把数据全部存在内存之中。也就是说,Redis 有灾难恢复机制而 Memcached 没有。
- 集群模式支持:Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 Redis 自 3.0 版本起是原生支持集群模式的。
- 线程模型:Memcached 是多线程,非阻塞 IO 复用的网络模型;Redis 使用单线程的多路 IO 复用模型。 (Redis 6.0 针对网络数据的读写引入了多线程)
- 特性支持:Redis 支持发布订阅模型、Lua 脚本、事务等功能,而 Memcached 不支持。并且,Redis 支持更多的编程语言。
- 过期数据删除:Memcached 过期数据的删除策略只用了惰性删除,而 Redis 同时使用了惰性删除与定期删除。
相信看了上面的对比之后,我们已经没有什么理由可以选择使用 Memcached 来作为自己项目的分布式缓存了。
为什么要用 Redis?
1、访问速度更快
传统数据库数据保存在磁盘,而 Redis 基于内存,内存的访问速度比磁盘快很多。引入 Redis 之后,我们可以把一些高频访问的数据放到 Redis 中,这样下次就可以直接从内存中读取,速度可以提升几十倍甚至上百倍。
2、高并发
一般像 MySQL 这类的数据库的 QPS 大概都在 4k 左右(4 核 8g) ,但是使用 Redis 缓存之后很容易达到 5w+,甚至能达到 10w+(就单机 Redis 的情况,Redis 集群的话会更高)。
QPS(Query Per Second):服务器每秒可以执行的查询次数;
由此可见,直接操作缓存能够承受的数据库请求数量是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。进而,我们也就提高了系统整体的并发。
3、功能全面
Redis 除了可以用作缓存之外,还可以用于分布式锁、限流、消息队列、延时队列等场景,功能强大!
常见的缓存读写策略有哪些?
在分布式缓存中,读写策略的选择影响系统的性能、数据一致性和可用性。常见的缓存读写策略主要有以下几种:
1. Cache Aside(旁路缓存模式)
(最常见,适用于大多数场景)
原理
- 读取数据:
- 先查缓存,如果缓存命中,直接返回数据;
- 如果缓存未命中,查询数据库,再将结果写入缓存,返回数据。
- 更新数据:
- 先更新数据库;
- 再删除缓存(而不是更新缓存),确保下次查询时,能从数据库中读取最新数据。
示例
// 读取数据
String key = "user:123";
String value = redis.get(key);
if (value == null) {
// 缓存未命中,查询数据库
value = db.query("SELECT * FROM users WHERE id = 123");
redis.setex(key, 3600, value); // 将数据写入缓存
}
return value;
// 更新数据
db.update("UPDATE users SET name = 'Tom' WHERE id = 123");
redis.del(key); // 删除缓存,确保下次查询能拿到最新数据
优点
- 适用于数据更新不频繁的场景,缓存命中率高。
- 不会造成缓存污染(只缓存热点数据)。
- 保证数据一致性(先更新数据库,删除缓存)。
缺点
- 第一次请求会比较慢(需要先查数据库)。
- 缓存淘汰时可能引发缓存击穿(大量请求同时访问数据库)。
2. Read Through(读穿缓存模式)
(缓存和数据库打通,适用于数据库访问封装在缓存层的场景)
原理
- 先查缓存,缓存未命中时由缓存层自动回源数据库,并将数据存入缓存。
- 应用层不用关心数据库访问逻辑,所有操作都走缓存层。
示例
// 读取数据(缓存系统自动回源数据库)
String key = "product:456";
String value = cache.get(key); // 缓存系统自动处理数据库查询
return value;
优点
- 应用层逻辑简单,所有数据访问都通过缓存,封装性好。
- 降低数据库压力,缓存系统自动管理数据更新。
缺点
- 数据一致性依赖缓存系统,如果缓存系统未正确回源,可能导致脏数据。
- 实现复杂,需要缓存系统支持数据库访问。
3. Write Through(写穿缓存模式)
(适用于写操作不频繁、强一致性要求高的场景)
原理
- 所有写操作先写入缓存,再同步更新数据库,确保缓存和数据库数据一致。
- 读取数据时,直接从缓存中取。
示例
// 写入数据
String key = "order:789";
String value = "order data";
cache.set(key, value); // 先写缓存
db.update("INSERT INTO orders VALUES (...)"); // 再写数据库
优点
- 数据一致性更好(缓存和数据库始终同步)。
- 读操作更快(所有数据都在缓存中)。
缺点
- 写操作延迟更高(写入两次,缓存+数据库)。
- 数据不一定都是热点,可能导致缓存存储大量冷数据,影响缓存利用率。
4. Write Back(写回缓存模式)
(适用于写多读少、低延迟要求的场景)
原理
- 先写入缓存,不立即写入数据库。
- 由后台异步任务批量同步到数据库,减少数据库写入次数,提高写入性能。
示例
// 写入数据(先写缓存,不直接写数据库)
cache.set("user:111", "new data");
// 后台任务定期将缓存数据批量同步到数据库
batchWriteToDB();
优点
- 写入性能最高,减少数据库压力。
- 适用于高并发写入场景(如日志、计数器等)。
缺点
- 可能导致数据丢失(如果缓存宕机,未同步到数据库的数据会丢失)。
- 数据一致性较差(数据库数据可能滞后)。
5. Cache Aside vs Read/Write Through vs Write Back 总结
策略 | 读取流程 | 写入流程 | 适用场景 |
---|---|---|---|
Cache Aside | 先查缓存,未命中再查数据库 | 先写数据库,再删除缓存 | 读多写少的场景(如用户信息缓存) |
Read Through | 先查缓存,缓存未命中自动查数据库 | 先写缓存,再写数据库 | 适用于缓存和数据库一体化管理 |
Write Through | 先写缓存,再同步写数据库 | 直接读取缓存 | 强一致性、高读写需求场景 |
Write Back | 直接读缓存 | 先写缓存,后台批量写数据库 | 写多读少、高吞吐写入场景 |
6. 选型建议
- 高并发读多写少:✅ Cache Aside(旁路缓存),最常见,适用于大部分业务场景。
- 读写缓存一致性要求高:✅ Write Through(写穿),适用于金融、订单等场景。
- 写入性能要求高:✅ Write Back(写回),适用于日志、计数器、IoT 数据存储。
总结
缓存读写策略的选择需要结合业务需求、性能要求和数据一致性要求:
- 大部分业务(如用户缓存、商品缓存) → Cache Aside
- 强一致性需求(金融、电商订单) → Write Through
- 高吞吐写入场景(日志、流式计算) → Write Back
不同业务场景需要权衡性能、数据一致性和缓存利用率,合理选择缓存策略,优化系统性能! 🚀
什么是 Redis Module?有什么用?
Redis 从 4.0 版本开始,支持通过 Module 来扩展其功能以满足特殊的需求。这些 Module 以动态链接库(so 文件)的形式被加载到 Redis 中,这是一种非常灵活的动态扩展功能的实现方式,值得借鉴学习!
我们每个人都可以基于 Redis 去定制化开发自己的 Module,比如实现搜索引擎功能、自定义分布式锁和分布式限流。
目前,被 Redis 官方推荐的 Module 有:
- RediSearch:用于实现搜索引擎的模块。
- RedisJSON:用于处理 JSON 数据的模块。
- RedisGraph:用于实现图形数据库的模块。
- RedisTimeSeries:用于处理时间序列数据的模块。
- RedisBloom:用于实现布隆过滤器的模块。
- RedisAI:用于执行深度学习/机器学习模型并管理其数据的模块。
- RedisCell:用于实现分布式限流的模块。
- ……
关于 Redis 模块的详细介绍,可以查看官方文档:https://redis.io/modules。
Redis 应用
Redis 除了做缓存,还能做什么?
Redis 不仅仅是一个缓存工具,它还可以用来构建各种高效的分布式系统,比如分布式锁、限流、消息队列、排行榜、统计分析等。下面介绍 Redis 在实际业务中常见的高级应用场景:
1. 分布式锁
(解决多个服务/节点竞争资源的问题,例如订单超卖)
原理
- 使用
SET NX EX
(SET key value NX EX time
)命令,确保原子性,避免多进程同时修改同一资源。 - 业务执行完后,通过
DEL key
释放锁。 - 常见实现方式:
- 直接用
SET NX
+EXPIRE
- 采用 Redisson 提供的
RLock
(基于 Redis + Lua 脚本实现) - Redis 官方 RedLock(适用于多个 Redis 实例)
- 直接用
示例
Boolean lock = redis.setIfAbsent("lock:order", "1", 10, TimeUnit.SECONDS);
if (lock) {
try {
// 执行业务逻辑
} finally {
redis.del("lock:order"); // 释放锁
}
}
✅ 适用于防止超卖、并发任务控制。
2. 限流
(防止接口被恶意请求,例如秒杀、接口防刷)
原理
- 采用 令牌桶算法 或 滑动窗口算法 控制流量。
INCR
+EXPIRE
进行计数,Sorted Set
维护时间窗口。
示例
-- Redis 限流 Lua 脚本
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local expire = tonumber(ARGV[2])
local current = redis.call('incr', key)
if current == 1 then
redis.call('expire', key, expire)
end
if current > limit then
return 0
else
return 1
end
- 也可以使用 Redisson 的
RRateLimiter
,底层基于 Redis + Lua 实现。
✅ 适用于接口限流、秒杀防刷。
3. 消息队列
(解决分布式服务间的异步通信问题)
原理
- Redis List(基于
LPUSH
+BRPOP
)作为简单消息队列。 - Redis Stream(类似 Kafka),支持持久化、消费组和 ACK 机制。
示例
// 生产者
redis.lpush("queue:task", "task1");
// 消费者
String task = redis.brpop(5, "queue:task").get(1);
✅ 适用于任务队列、异步通知、日志收集。
4. 延时队列
(处理定时任务、订单超时取消等需求)
原理
- 基于 Sorted Set(
ZADD score
,score 代表时间戳)。 - 后台定时任务轮询
ZRANGEBYSCORE
取出到期任务并处理。
示例
// 添加任务(score 为执行时间)
redis.zadd("delay_queue", System.currentTimeMillis() + 5000, "task1");
// 取出过期任务
Set<String> tasks = redis.zrangeByScore("delay_queue", 0, System.currentTimeMillis());
✅ 适用于订单超时取消、定时任务调度。
5. 分布式 Session
(解决多个服务器间 Session 共享问题)
原理
- 传统 Session 存在单个服务器内,集群部署时无法共享。
- 采用 Redis 存储 Session,所有服务器访问相同的 Redis 实例。
示例
// 存储 Session
redis.setex("session:user123", 1800, "user_data");
// 读取 Session
String sessionData = redis.get("session:user123");
✅ 适用于分布式系统中的用户登录状态管理。
6. 排行榜
(维护点赞榜、游戏积分榜、销售榜单)
原理
- Redis Sorted Set(
ZADD
+ZREVRANGE
)天然支持排行榜。
示例
// 添加用户得分
redis.zadd("leaderboard", 100, "user1");
// 获取前 10 名用户
Set<String> topUsers = redis.zrevrange("leaderboard", 0, 9);
✅ 适用于排行榜、积分系统、活跃用户统计。
7. UV 统计(HyperLogLog)
(快速统计用户访问量,节省内存)
原理
- 使用 HyperLogLog 进行去重计数,误差 < 0.81%。
- 比 Set 占用内存更少,适用于大规模去重统计。
示例
// 记录访问用户
redis.pfadd("uv:20240209", "user123");
// 获取 UV 统计数
long uvCount = redis.pfcount("uv:20240209");
✅ 适用于网站 PV/UV 统计、独立访客计数。
8. 活跃用户统计(Bitmap)
(用于签到、活跃用户统计、二进制存储)
原理
- Redis Bitmap 使用位存储(0/1)表示用户状态。
- 可快速统计某天活跃用户、连续签到天数等。
示例
// 标记用户 123 在某天活跃
redis.setbit("active:20240209", 123, 1);
// 统计活跃用户数
long activeCount = redis.bitcount("active:20240209");
✅ 适用于签到系统、活跃用户分析。
总结
应用场景 | Redis 数据结构 | 实现方式 |
---|---|---|
分布式锁 | String | SET NX EX 或 Redisson |
限流 | String / Lua | INCR + EXPIRE 或 Redisson RRateLimiter |
消息队列 | List / Stream | LPUSH + BRPOP / Stream 主题 |
延时队列 | Sorted Set | ZADD + ZRANGEBYSCORE |
Session 共享 | String / Hash | SETEX 存储用户 Session |
排行榜 | Sorted Set | ZADD + ZREVRANGE |
UV 统计 | HyperLogLog | PFADD + PFCOUNT |
活跃用户统计 | Bitmap | SETBIT + BITCOUNT |
✅ Redis 不只是缓存,它是一个高效的分布式工具箱,适用于高并发、实时性强的业务!🚀
如何基于 Redis 实现分布式锁?
在分布式系统中,多个服务实例可能同时访问共享资源(如订单库存、秒杀商品),需要分布式锁来确保并发操作的正确性。Redis 提供了高效、可靠的分布式锁方案,下面介绍几种常见的实现方式。
1. 基于 SET NX EX
实现简单分布式锁
原理
SET key value NX EX time
NX
(Not Exists):确保原子性,只有当 key 不存在时才会创建,防止并发覆盖。EX time
(Expire Time):设置过期时间,防止死锁(进程崩溃导致锁无法释放)。
- 解锁时:
- 不能直接
DEL key
,必须判断是否是自己的锁再删除,避免误删。
- 不能直接
示例
public class RedisLock {
private static final String LOCK_KEY = "lock:order";
private static final int EXPIRE_TIME = 10; // 10 秒
private final String lockValue = UUID.randomUUID().toString(); // 防止误删锁
private final Jedis redis;
public RedisLock(Jedis redis) {
this.redis = redis;
}
// 获取锁
public boolean tryLock() {
String result = redis.set(LOCK_KEY, lockValue, "NX", "EX", EXPIRE_TIME);
return "OK".equals(result);
}
// 释放锁(必须判断是自己加的锁)
public void unlock() {
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then "
+ " return redis.call('del', KEYS[1]) "
+ "else "
+ " return 0 "
+ "end";
redis.eval(script, Collections.singletonList(LOCK_KEY),
Collections.singletonList(lockValue));
}
}
✅ 优点:简单高效,避免了多进程同时持有锁。
❌ 缺点:不支持自动续期,锁可能因网络延迟被错误释放。
2. 基于 SET NX
+ WATCH
实现可重入分布式锁
原理
- Redis 默认不支持可重入锁(同一个线程如果多次获取锁,会阻塞)。
- 解决方案:
- 使用
WATCH
监听 key,只有当前线程持有锁时,才允许递增一个计数器(标记重入次数)。
- 使用
示例
public boolean tryLock() {
redis.watch(LOCK_KEY);
String lockOwner = redis.get(LOCK_KEY);
if (lockOwner != null && lockOwner.equals(lockValue)) {
redis.multi();
redis.incr(LOCK_KEY + ":count"); // 记录重入次数
redis.exec();
return true;
}
redis.unwatch();
return tryLock(); // 尝试获取锁
}
✅ 优点:支持同一线程重复加锁。
❌ 缺点:操作较复杂,WATCH
机制依赖客户端保证原子性。
3. 使用 Redisson 实现分布式锁
(推荐,生产可用)
原理
- Redisson 是 Redis 官方推荐的 Java 分布式锁库,封装了可重入锁、读写锁、信号量等机制。
- 可重入锁:相同线程可多次加锁,不会被阻塞。
- 支持自动续期:加锁后,Redisson 会自动续期,防止锁因超时丢失。
示例
public class RedissonLockExample {
private final RedissonClient redissonClient;
public RedissonLockExample(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
}
public void executeTask() {
RLock lock = redissonClient.getLock("lock:order");
try {
boolean locked = lock.tryLock(5, 10, TimeUnit.SECONDS);
if (!locked) {
return; // 获取锁失败,直接返回
}
// 执行业务逻辑
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // 释放锁
}
}
}
✅ 优点:
- 自动续期(避免锁过期丢失)。
- 支持可重入锁、读写锁、公平锁。
- 稳定可靠,适用于生产环境。
❌ 缺点:依赖 Redisson,增加了第三方库。
4. Redis 官方 RedLock(适用于多 Redis 实例)
(适用于高可靠性场景,如订单扣减、支付)
原理
- RedLock 适用于 Redis 集群环境,防止单点故障导致锁失效:
- 多个 Redis 实例(至少 5 个)。
- 客户端同时请求多个 Redis 实例加锁,若超过半数 Redis 实例加锁成功,则认为加锁成功。
- 确保锁的超时时间一致,防止误释放锁。
实现
public boolean tryRedLock() {
int successCount = 0;
for (Jedis redisInstance : redisCluster) {
String result = redisInstance.set(LOCK_KEY, lockValue, "NX", "PX", EXPIRE_TIME);
if ("OK".equals(result)) {
successCount++;
}
}
return successCount >= redisCluster.size() / 2 + 1; // 至少超过半数实例加锁成功
}
✅ 优点:
- 高可靠性,适用于分布式集群。
- 防止单点 Redis 故障导致锁失效。
❌ 缺点:
- 开销大,需要多个 Redis 实例,适用于高可用场景(如金融、支付)。
总结
方案 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
SET NX EX | 一般业务加锁 | 简单高效 | 无法自动续期 |
SET NX + WATCH | 需要可重入锁 | 允许重入 | 逻辑复杂 |
Redisson | 推荐,大部分分布式锁场景 | 自动续期,支持可重入锁 | 依赖第三方 |
RedLock | 高可靠性场景(支付、金融) | 适用于 Redis 集群 | 实现复杂,性能开销大 |
🚀 推荐选择:
- 普通场景 →
SET NX EX
(简单快速) - 可重入锁 →
SET NX
+WATCH
/ Redisson - 高可用场景 → RedLock
Redis 分布式锁适用于高并发、分布式环境,但如果业务对强一致性要求极高(如金融系统),建议使用 ZooKeeper 提供的分布式锁(ZK 临时有序节点)。
🎯 综合来看,Redisson 是最推荐的生产级分布式锁方案! 🚀
Redis 可以做消息队列么?
结论:可以,但不推荐!
尽管 Redis 具备基本的消息队列能力(如 List
、Pub/Sub
、Stream
),但相比专业的消息队列(如 Kafka、RabbitMQ、RocketMQ),仍存在消息丢失、消息积压、持久化能力有限等问题,因此在大规模生产环境中,一般不会使用 Redis 作为主要的消息队列。
1. Redis 实现消息队列的方式
Redis 提供了多种方式来实现消息队列,主要包括以下三种方式:
方式 | 特点 | 适用场景 | 缺点 |
---|---|---|---|
List(简单队列) | 基于 LPUSH/RPOP 或 RPUSH/LPOP 实现队列 | 适用于简单的生产者-消费者模型 | 需要轮询(CPU 资源消耗大),没有持久化 |
Pub/Sub(发布订阅) | 发布者 PUBLISH 发送消息,订阅者 SUBSCRIBE 监听 | 适用于广播消息(如实时消息推送) | 无持久化,消息可能丢失,消费者断开连接会错过消息 |
Stream(Redis 5.0+) | 类似 Kafka 的消息队列,支持消费组、ACK 确认、持久化 | 高吞吐、可靠性要求高的场景 | 不能保证 至少消费一次,消息可能丢失 |
2. 使用 List 实现消息队列
Redis 最基础的消息队列方式是使用 List,通过 LPUSH
(生产者入队)和 RPOP
(消费者出队)实现 FIFO 队列。
示例
// 生产者(发送消息)
redis.lpush("queue:tasks", "task1");
redis.lpush("queue:tasks", "task2");
// 消费者(拉取消息)
String task = redis.rpop("queue:tasks");
System.out.println("消费任务:" + task);
优缺点
✅ 优点:
- 简单易用,适合小型异步任务队列。
- 通过
BLPOP
、BRPOP
(阻塞操作)优化轮询效率。
❌ 缺点:
- 不支持消息确认(消费失败消息会丢失)。
- 无法保证消息顺序(多个消费者时,Redis 不能保证消息严格按顺序消费)。
- 消息无法持久化,Redis 宕机后消息丢失。
3. 使用 Pub/Sub(发布订阅)
Pub/Sub 采用发布-订阅模式,允许一个生产者发送消息给多个消费者。
示例
// 生产者
redis.publish("channel:news", "Breaking News!");
// 消费者(监听频道)
JedisPubSub pubSub = new JedisPubSub() {
@Override
public void onMessage(String channel, String message) {
System.out.println("收到消息:" + message);
}
};
redis.subscribe(pubSub, "channel:news");
优缺点
✅ 优点:
- 支持广播,多个订阅者都能收到同一条消息。
- 低延迟,实时性高。
❌ 缺点:
- 无持久化,消费者离线时无法接收消息(消息丢失)。
- 消息堆积问题,如果消费能力不足,消息会丢失。
⚠ 适用于:
- 实时消息推送(如 WebSocket、通知系统)。
- 事件驱动架构(如 Redis 事件总线)。
4. 使用 Stream(推荐)
Redis 5.0 引入 Stream,支持持久化、消费组、ACK 机制,可以作为专业的消息队列。
示例
// 生产者(添加消息)
redis.xadd("mystream", StreamEntryID.NEW_ENTRY, Map.of("message", "task1"));
// 消费者(读取消息)
List<Map.Entry<String, List<StreamEntry>>> messages = redis.xread(
XReadParams.xReadParams().count(1).block(0),
new AbstractMap.SimpleImmutableEntry<>("mystream", StreamEntryID.LAST_ENTRY)
);
System.out.println("消费到消息:" + messages);
优缺点
✅ 优点:
- 支持持久化(RDB/AOF)。
- 支持消费者组(多个消费者可以并行处理消息)。
- 支持消息确认(ACK),防止消息丢失。
❌ 缺点:
- 不保证至少消费一次(Redis 发生故障恢复后可能丢失消息)。
- 消费组管理复杂,需要手动管理 ACK、死信队列。
⚠ 适用于:
- 日志收集、订单处理(消费组 + 持久化)。
- 高吞吐、高可用的消息队列(类似 Kafka)。
5. 为什么不推荐 Redis 作为消息队列?
虽然 Redis Stream 具备一定的消息队列能力,但相比于专业的 MQ(Kafka、RabbitMQ、RocketMQ),仍然存在以下问题:
对比项 | Redis Stream | Kafka | RabbitMQ |
---|---|---|---|
持久化 | 支持(但恢复后可能丢失消息) | 强持久化(日志存储) | 支持持久化 |
消息确认 | 手动 XACK | 自动/手动 | 自动/手动 |
消费模式 | P2P/广播 | 分区消费(支持高吞吐) | 路由模式(灵活) |
吞吐量 | 中等(10万级 QPS) | 高吞吐(百万级 QPS) | 较低(万级 QPS) |
消息堆积 | 堆积受内存限制 | 磁盘存储,无上限 | 受磁盘影响 |
集群扩展 | 手动扩容 | 天然支持分布式 | 需要额外组件 |
🚀 推荐使用
- Kafka:适用于高吞吐、大数据流处理。
- RabbitMQ:适用于事务性强、可靠性高的业务场景(如支付)。
- RocketMQ:适用于国内互联网业务(如订单、日志收集)。
6. 结论
实现方式 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
List(RPUSH/LPOP) | 简单易用,适合小型任务队列 | 轮询消耗 CPU,不支持持久化 | 轻量级队列(如异步任务) |
Pub/Sub | 低延迟,支持广播 | 无持久化,消息丢失 | 实时通知、事件推送 |
Stream | 支持持久化、消费组、ACK | 复杂度高,不保证至少消费一次 | 日志收集、分布式任务调度 |
Kafka/RabbitMQ | 高可靠、强持久化 | 比 Redis 慢 | 金融、电商、日志分析 |
🚀 推荐选择:
- 小型队列(异步任务、通知) →
Redis List
- 广播消息(事件推送) →
Redis Pub/Sub
- 可靠队列(高并发) →
Redis Stream
- 生产环境高可靠队列 → Kafka / RabbitMQ / RocketMQ
总结:Redis 可以做消息队列,但不推荐用于生产级高可靠性需求!Stream 是 Redis 内部最佳的消息队列实现,但仍不如专业的 MQ。 🚀
Redis 可以做搜索引擎么?
Redis 是可以实现全文搜索引擎功能的,需要借助 RediSearch ,这是一个基于 Redis 的搜索引擎模块。
RediSearch 支持中文分词、聚合统计、停用词、同义词、拼写检查、标签查询、向量相似度查询、多关键词搜索、分页搜索等功能,算是一个功能比较完善的全文搜索引擎了。
相比较于 Elasticsearch 来说,RediSearch 主要在下面两点上表现更优异一些:
- 性能更优秀:依赖 Redis 自身的高性能,基于内存操作(Elasticsearch 基于磁盘)。
- 较低内存占用实现快速索引:RediSearch 内部使用压缩的倒排索引,所以可以用较低的内存占用来实现索引的快速构建。
对于小型项目的简单搜索场景来说,使用 RediSearch 来作为搜索引擎还是没有问题的(搭配 RedisJSON 使用)。
对于比较复杂或者数据规模较大的搜索场景还是不太建议使用 RediSearch 来作为搜索引擎,主要是因为下面这些限制和问题:
- 数据量限制:Elasticsearch 可以支持 PB 级别的数据量,可以轻松扩展到多个节点,利用分片机制提高可用性和性能。RedisSearch 是基于 Redis 实现的,其能存储的数据量受限于 Redis 的内存容量,不太适合存储大规模的数据(内存昂贵,扩展能力较差)。
- 分布式能力较差:Elasticsearch 是为分布式环境设计的,可以轻松扩展到多个节点。虽然 RedisSearch 支持分布式部署,但在实际应用中可能会面临一些挑战,如数据分片、节点间通信、数据一致性等问题。
- 聚合功能较弱:Elasticsearch 提供了丰富的聚合功能,而 RediSearch 的聚合功能相对较弱,只支持简单的聚合操作。
- 生态较差:Elasticsearch 可以轻松和常见的一些系统/软件集成比如 Hadoop、Spark、Kibana,而 RedisSearch 则不具备该优势。
Elasticsearch 适用于全文搜索、复杂查询、实时数据分析和聚合的场景,而 RediSearch 适用于快速数据存储、缓存和简单查询的场景。
如何基于 Redis 实现延时任务?
类似的问题:
- 订单在 10 分钟后未支付就失效,如何用 Redis 实现?
- 红包 24 小时未被查收自动退还,如何用 Redis 实现?
基于 Redis 实现延时任务的功能无非就下面两种方案:
- Redis 过期事件监听
- Redisson 内置的延时队列
Redis 过期事件监听的存在时效性较差、丢消息、多服务实例下消息重复消费等问题,不被推荐使用。
Redisson 内置的延时队列具备下面这些优势:
- 减少了丢消息的可能:DelayedQueue 中的消息会被持久化,即使 Redis 宕机了,根据持久化机制,也只可能丢失一点消息,影响不大。当然了,你也可以使用扫描数据库的方法作为补偿机制。
- 消息不存在重复消费问题:每个客户端都是从同一个目标队列中获取任务的,不存在重复消费的问题。
关于 Redis 实现延时任务的详细介绍,可以看我写的这篇文章:如何基于 Redis 实现延时任务?。
Redis 数据类型
Redis 提供了多种基础数据类型和特殊数据类型,用于满足不同的存储和查询需求。理解这些数据类型的用途和底层实现,可以帮助开发者更高效地使用 Redis。
1. Redis 5 种基础数据类型
数据类型 | 常用命令 | 应用场景 |
---|---|---|
String(字符串) | SET 、GET 、INCR 、DECR | 缓存、计数器、分布式锁 |
List(列表) | LPUSH 、RPUSH 、LPOP 、LRANGE | 消息队列、时间线 |
Set(集合) | SADD 、SMEMBERS 、SISMEMBER | 去重、标签管理 |
Hash(哈希表) | HSET 、HGET 、HGETALL | 存储对象(如用户信息) |
Zset(有序集合) | ZADD 、ZRANGE 、ZREVRANGE | 排行榜、延时队列 |
2. Redis 3 种特殊数据类型
数据类型 | 常用命令 | 应用场景 |
---|---|---|
HyperLogLog | PFADD 、PFCOUNT | 统计 UV(独立用户数) |
Bitmap(位图) | SETBIT 、GETBIT 、BITCOUNT | 签到、活跃用户统计 |
Geospatial(地理位置) | GEOADD 、GEODIST 、GEORADIUS | LBS(基于位置的服务) |
3. Redis 其他数据类型
数据类型 | 功能 |
---|---|
Bloom filter(布隆过滤器) | 快速判断数据是否存在,适用于反爬虫、去重 |
Bitfield(位域) | 位操作扩展,如多个 INCR 计数器 |
4. 数据类型应用场景示例
(1)String(字符串)
适用于:缓存、分布式锁、计数器
// 存储用户 Token
redis.set("user:token:123", "abcdef", "EX", 3600);
// 计数器(增加访问量)
redis.incr("page:views");
(2)List(列表)
适用于:消息队列、时间线
// 生产者
redis.lpush("queue:tasks", "task1");
// 消费者
String task = redis.rpop("queue:tasks");
(3)Set(集合)
适用于:去重、标签系统
// 添加用户到粉丝列表
redis.sadd("user:1001:followers", "user2001", "user2002");
// 查询粉丝
Set<String> followers = redis.smembers("user:1001:followers");
(4)Hash(哈希)
适用于:存储对象
// 存储用户信息
redis.hset("user:1001", "name", "Tom");
redis.hset("user:1001", "age", "25");
// 读取用户信息
String name = redis.hget("user:1001", "name");
(5)Zset(有序集合)
适用于:排行榜、延时队列
// 添加用户积分
redis.zadd("leaderboard", 100, "user123");
// 获取前 10 名
Set<String> topUsers = redis.zrevrange("leaderboard", 0, 9);
(6)HyperLogLog
适用于:统计 UV(独立访客数)
// 记录访问用户
redis.pfadd("uv:20240209", "user123");
// 获取 UV 统计数
long uvCount = redis.pfcount("uv:20240209");
(7)Bitmap(位图)
适用于:签到、活跃用户统计
// 标记用户 123 在某天活跃
redis.setbit("active:20240209", 123, 1);
// 统计活跃用户数
long activeCount = redis.bitcount("active:20240209");
(8)Geospatial(地理位置)
适用于:LBS(基于位置的服务)
// 添加商家坐标
redis.geoadd("stores", 116.40, 39.90, "Beijing Store");
// 计算距离
double distance = redis.geodist("stores", "Beijing Store", "Shanghai Store", "km");
结论
- String:适合缓存、计数器、分布式锁。
- List:适合消息队列、时间线。
- Set:适合去重、社交标签系统。
- Hash:适合存储对象(用户信息、商品信息)。
- Zset:适合排行榜、延时任务队列。
- HyperLogLog:适合大规模 UV 统计。
- Bitmap:适合签到、活跃用户统计。
- Geospatial:适合 LBS(地图服务)。
🚀 熟练掌握 Redis 数据类型,能让你的项目更加高效! 🚀
String 的应用场景有哪些?
String 是 Redis 最简单、最常用的数据类型,它是二进制安全的,可以存储字符串、整数、浮点数、Base64 编码数据、序列化对象等。
由于 String 是 Redis 最高效的数据类型,它在缓存、计数、分布式锁、限流等场景中应用广泛。
1. String 常见应用场景
应用场景 | 示例 | 常用命令 |
---|---|---|
缓存(Session、Token、JSON 对象) | SET session:123 "user_data" EX 3600 | SET 、GET 、EXPIRE |
计数器(访问量、点赞数、限流) | INCR page:views | INCR 、DECR 、INCRBY |
分布式锁(SETNX) | SET lock:order 1 NX EX 10 | SETNX 、EXPIRE |
简单限流(单位时间请求数) | INCR user:123:requests | INCR 、EXPIRE |
存储结构化数据(JSON 字符串) | SET user:1001 '{"name": "Tom", "age": 25}' | SET 、GET |
验证码存储 | SET sms:verify:123456 7890 EX 300 | SET 、EXPIRE |
分布式唯一 ID(雪花算法) | INCR order:id | INCR |
2. 具体示例
(1)缓存 Session、Token
适用于:用户登录、OAuth 认证
// 存储用户 Session(过期时间 1 小时)
redis.set("session:123", "user_data", "EX", 3600);
// 获取 Session
String session = redis.get("session:123");
✅ 优点:
- Redis 高速缓存,可减少数据库查询。
- Session 过期可自动删除。
(2)计数器(访问量、点赞数、限流)
适用于:页面浏览数、点赞数、API 请求计数
// 统计页面访问量
redis.incr("page:views");
// 统计某个文章的点赞数
redis.incr("article:1001:likes");
✅ 优点:
- 高并发统计(INCR 操作是原子性的)。
- 适用于热点数据(如热门文章点赞数)。
(3)分布式锁
适用于:订单处理、秒杀、库存扣减
// 获取锁(10 秒超时)
redis.set("lock:order", "123", "NX", "EX", 10);
// 释放锁(使用 Lua 脚本保证原子性)
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
redis.eval(script, Collections.singletonList("lock:order"), Collections.singletonList("123"));
✅ 优点:
- 保证互斥性,防止多个进程同时操作同一资源。
- 防止死锁(EX 设定过期时间)。
(4)简单限流
适用于:限制接口请求速率
String key = "rate_limit:user:123";
long count = redis.incr(key);
if (count == 1) {
redis.expire(key, 60); // 设置 1 分钟过期
}
if (count > 10) {
System.out.println("请求过多,请稍后再试");
}
✅ 优点:
- 快速实现限流,适用于简单防刷。
- 轻量级,无须额外组件。
(5)存储结构化数据(JSON 格式)
适用于:存储用户信息、订单信息
// 存储 JSON 字符串
String userData = "{\"name\": \"Tom\", \"age\": 25}";
redis.set("user:1001", userData);
// 读取 JSON 数据
String user = redis.get("user:1001");
System.out.println(user);
✅ 优点:
- 存储灵活,适合小型 JSON 数据。
- 结合 RedisJSON 模块可实现 JSON 查询。
(6)验证码存储
适用于:短信验证码、邮箱验证码
// 存储验证码(5 分钟有效)
redis.set("sms:verify:123456", "7890", "EX", 300);
// 获取验证码
String code = redis.get("sms:verify:123456");
✅ 优点:
- 保证验证码有效期,防止重复使用。
- 高并发支持,适用于大规模验证码请求。
(7)分布式唯一 ID
适用于:订单号、用户 ID 生成
// 生成唯一 ID
long orderId = redis.incr("order:id");
System.out.println("生成订单 ID:" + orderId);
✅ 优点:
- 全局唯一 ID,适用于分布式系统。
- 递增顺序,适用于数据库索引优化。
总结
应用场景 | 适用场景 |
---|---|
缓存数据 | 存储 Session、Token、JSON、验证码 |
计数 | 访问量、点赞数、商品库存、限流 |
分布式锁 | 订单并发、秒杀库存扣减 |
唯一 ID 生成 | 订单号、自增 ID |
🚀 Redis String 适用于存储简单数据,常用于缓存、计数、分布式锁等高并发场景! 🚀
String 还是 Hash 存储对象数据更好呢?
简单对比一下二者:
- 对象存储方式:String 存储的是序列化后的对象数据,存放的是整个对象,操作简单直接。Hash 是对对象的每个字段单独存储,可以获取部分字段的信息,也可以修改或者添加部分字段,节省网络流量。如果对象中某些字段需要经常变动或者经常需要单独查询对象中的个别字段信息,Hash 就非常适合。
- 内存消耗:Hash 通常比 String 更节省内存,特别是在字段较多且字段长度较短时。Redis 对小型 Hash 进行优化(如使用 ziplist 存储),进一步降低内存占用。
- 复杂对象存储:String 在处理多层嵌套或复杂结构的对象时更方便,因为无需处理每个字段的独立存储和操作。
- 性能:String 的操作通常具有 O(1) 的时间复杂度,因为它存储的是整个对象,操作简单直接,整体读写的性能较好。Hash 由于需要处理多个字段的增删改查操作,在字段较多且经常变动的情况下,可能会带来额外的性能开销。
总结:
- 在绝大多数情况下,String 更适合存储对象数据,尤其是当对象结构简单且整体读写是主要操作时。
- 如果你需要频繁操作对象的部分字段或节省内存,Hash 可能是更好的选择。
String 的底层实现是什么?
Redis 是基于 C 语言编写的,但 Redis 的 String 类型的底层实现并不是 C 语言中的字符串(即以空字符 \0
结尾的字符数组),而是自己编写了 SDS(Simple Dynamic String,简单动态字符串) 来作为底层实现。
SDS 最早是 Redis 作者为日常 C 语言开发而设计的 C 字符串,后来被应用到了 Redis 上,并经过了大量的修改完善以适合高性能操作。
Redis7.0 的 SDS 的部分源码如下(https://github.com/redis/redis/blob/7.0/src/sds.h):
/* Note: sdshdr5 is never used, we just access the flags byte directly.
* However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* used */
uint8_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len; /* used */
uint16_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len; /* used */
uint32_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /* used */
uint64_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
通过源码可以看出,SDS 共有五种实现方式 SDS_TYPE_5(并未用到)、SDS_TYPE_8、SDS_TYPE_16、SDS_TYPE_32、SDS_TYPE_64,其中只有后四种实际用到。Redis 会根据初始化的长度决定使用哪种类型,从而减少内存的使用。
类型 | 字节 | 位 |
---|---|---|
sdshdr5 | < 1 | <8 |
sdshdr8 | 1 | 8 |
sdshdr16 | 2 | 16 |
sdshdr32 | 4 | 32 |
sdshdr64 | 8 | 64 |
对于后四种实现都包含了下面这 4 个属性:
len
:字符串的长度也就是已经使用的字节数alloc
:总共可用的字符空间大小,alloc-len 就是 SDS 剩余的空间大小buf[]
:实际存储字符串的数组flags
:低三位保存类型标志
SDS 相比于 C 语言中的字符串有如下提升:
- 可以避免缓冲区溢出:C 语言中的字符串被修改(比如拼接)时,一旦没有分配足够长度的内存空间,就会造成缓冲区溢出。SDS 被修改时,会先根据 len 属性检查空间大小是否满足要求,如果不满足,则先扩展至所需大小再进行修改操作。
- 获取字符串长度的复杂度较低:C 语言中的字符串的长度通常是经过遍历计数来实现的,时间复杂度为 O(n)。SDS 的长度获取直接读取 len 属性即可,时间复杂度为 O(1)。
- 减少内存分配次数:为了避免修改(增加/减少)字符串时,每次都需要重新分配内存(C 语言的字符串是这样的),SDS 实现了空间预分配和惰性空间释放两种优化策略。当 SDS 需要增加字符串时,Redis 会为 SDS 分配好内存,并且根据特定的算法分配多余的内存,这样可以减少连续执行字符串增长操作所需的内存重分配次数。当 SDS 需要减少字符串时,这部分内存不会立即被回收,会被记录下来,等待后续使用(支持手动释放,有对应的 API)。
- 二进制安全:C 语言中的字符串以空字符
\0
作为字符串结束的标识,这存在一些问题,像一些二进制文件(比如图片、视频、音频)就可能包括空字符,C 字符串无法正确保存。SDS 使用 len 属性判断字符串是否结束,不存在这个问题。
🤐 多提一嘴,很多文章里 SDS 的定义是下面这样的:
struct sdshdr {
unsigned int len;
unsigned int free;
char buf[];
};
这个也没错,Redis 3.2 之前就是这样定义的。后来,由于这种方式的定义存在问题,len
和 free
的定义用了 4 个字节,造成了浪费。Redis 3.2 之后,Redis 改进了 SDS 的定义,将其划分为了现在的 5 种类型。
购物车信息用 String 还是 Hash 存储更好?
结论:购物车信息建议使用 Hash 存储
由于购物车中的商品频繁修改(如添加、删除、更新数量),使用 Hash 更合适:
- 用户 ID 作为 Key,购物车的 商品 ID 作为 Field,商品数量作为 Value。
- 支持部分修改(如修改单个商品的数量,而不影响整个购物车)。
- 节省内存(Redis 对小 Hash 进行了优化)。
1. 购物车 Hash 结构示例
# 用户 ID 1001 的购物车
HSET cart:1001 product:2001 2 # 添加商品 ID 2001,数量 2
HSET cart:1001 product:2002 1 # 添加商品 ID 2002,数量 1
HINCRBY cart:1001 product:2001 1 # 增加商品 ID 2001 数量 +1
HDEL cart:1001 product:2002 # 删除商品 ID 2002
HGETALL cart:1001 # 查询购物车所有商品
DEL cart:1001 # 清空购物车
2. 为什么选择 Hash?
操作 | String 方案(JSON 存储) | Hash 方案(推荐) |
---|---|---|
添加商品 | 需要取出 JSON,修改后重新存储 | HSET cart:uid pid qty |
查询购物车 | 解析 JSON 后才能读取 | HGETALL cart:uid |
修改商品数量 | 需要修改 JSON 并重新存储 | HINCRBY cart:uid pid qty |
删除商品 | 需要修改 JSON 并重新存储 | HDEL cart:uid pid |
清空购物车 | DEL key | DEL key |
✔ Hash 方案更高效,支持部分字段查询和修改,减少带宽占用!
3. 购物车基本操作
(1)添加商品
// 用户 1001 添加商品 2001(数量 2)
redis.hset("cart:1001", "product:2001", "2");
(2)获取购物车所有商品
// 查询用户 1001 购物车
Map<String, String> cart = redis.hgetAll("cart:1001");
(3)修改商品数量
// 商品 2001 数量 +1
redis.hincrBy("cart:1001", "product:2001", 1);
(4)删除商品
// 删除商品 2001
redis.hdel("cart:1001", "product:2001");
(5)清空购物车
// 清空购物车
redis.del("cart:1001");
4. 复杂购物车存储
在实际电商系统中,购物车需要存储更多商品信息(如价格、图片、库存状态等)。可以采用 Hash + JSON 的方式:
String productInfo = "{\"price\":99.99, \"image\":\"url\", \"stock\":100}";
redis.hset("cart:1001", "product:2001", productInfo);
✔ 支持部分修改,同时存储更丰富的信息。
结论
🚀 购物车数据存储,推荐使用 Hash!
- 高效存储,支持部分字段更新,减少 JSON 解析开销。
- 低内存占用,Redis 小 Hash 优化减少空间浪费。
- 查询灵活,支持单个商品查询和批量查询。
🎯 适用于电商、在线购物等高并发场景! 🚀
使用 Redis 实现排行榜
1. 选用 Redis Sorted Set
在 Redis 中,Sorted Set
(有序集合)是一种适用于排行榜场景的数据结构。它的特点是:
- 自动排序:所有元素根据
score
进行排序。 - 唯一性:每个元素是唯一的,但
score
值可以相同。 - 高效操作:能够快速添加、删除、查询排名。
2. 常用 Redis 命令
操作 | 命令 | 说明 |
---|---|---|
添加/更新数据 | ZADD key score member | 向排行榜中添加或更新用户的分数 |
获取排行榜(降序) | ZREVRANGE key start stop WITHSCORES | 获取排行榜前 N 名(默认从 0 开始) |
获取用户排名(降序) | ZREVRANK key member | 获取用户在排行榜中的排名(0 为第一名) |
获取用户分数 | ZSCORE key member | 获取用户的当前分数 |
删除用户 | ZREM key member | 删除用户的记录 |
3. Java 代码实现
使用 Lettuce
或 Jedis
连接 Redis 并操作排行榜。
使用 Lettuce
import io.lettuce.core.RedisClient;
import io.lettuce.core.api.sync.RedisCommands;
public class RedisLeaderboard {
private static final String LEADERBOARD_KEY = "game:leaderboard";
public static void main(String[] args) {
// 连接 Redis
RedisClient redisClient = RedisClient.create("redis://localhost:6379");
var connection = redisClient.connect();
RedisCommands<String, String> redis = connection.sync();
// 添加/更新用户分数
redis.zadd(LEADERBOARD_KEY, 1500, "player1");
redis.zadd(LEADERBOARD_KEY, 2000, "player2");
redis.zadd(LEADERBOARD_KEY, 1800, "player3");
// 获取排行榜前 3 名
System.out.println("Top 3 Players:");
var topPlayers = redis.zrevrangeWithScores(LEADERBOARD_KEY, 0, 2);
topPlayers.forEach(player ->
System.out.println(player.getValue() + " -> " + player.getScore())
);
// 获取某个用户排名
Long rank = redis.zrevrank(LEADERBOARD_KEY, "player1");
System.out.println("player1 的排名: " + (rank + 1));
// 关闭连接
connection.close();
redisClient.shutdown();
}
}
使用 Jedis
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Tuple;
import java.util.Set;
public class RedisLeaderboard {
private static final String LEADERBOARD_KEY = "game:leaderboard";
public static void main(String[] args) {
// 连接 Redis
Jedis jedis = new Jedis("localhost", 6379);
// 添加/更新用户分数
jedis.zadd(LEADERBOARD_KEY, 1500, "player1");
jedis.zadd(LEADERBOARD_KEY, 2000, "player2");
jedis.zadd(LEADERBOARD_KEY, 1800, "player3");
// 获取排行榜前 3 名
System.out.println("Top 3 Players:");
Set<Tuple> topPlayers = jedis.zrevrangeWithScores(LEADERBOARD_KEY, 0, 2);
for (Tuple player : topPlayers) {
System.out.println(player.getElement() + " -> " + player.getScore());
}
// 获取某个用户排名
Long rank = jedis.zrevrank(LEADERBOARD_KEY, "player1");
System.out.println("player1 的排名: " + (rank + 1));
// 关闭连接
jedis.close();
}
}
4. 进阶优化
- 设置排行榜长度:可以使用
ZREMRANGEBYRANK
删除最低分用户,控制排行榜最大容量:jedis.zremrangeByRank(LEADERBOARD_KEY, 100, -1); // 保留前 100 名
- 使用 Lua 脚本优化原子性:保证排行榜更新操作的原子性:
local key = KEYS[1] local member = ARGV[1] local score = tonumber(ARGV[2]) redis.call('ZADD', key, score, member) return redis.call('ZREVRANK', key, member)
- 持久化数据:定期将 Redis 数据同步到数据库,避免数据丢失。
这样,你就可以用 Redis 实现一个高效的排行榜了!🚀
Redis 的有序集合底层为什么要用跳表,而不用平衡树、红黑树或者 B+树?
这道面试题很多大厂比较喜欢问,难度还是有点大的。
- 平衡树 vs 跳表:平衡树的插入、删除和查询的时间复杂度和跳表一样都是 O(log n)。对于范围查询来说,平衡树也可以通过中序遍历的方式达到和跳表一样的效果。但是它的每一次插入或者删除操作都需要保证整颗树左右节点的绝对平衡,只要不平衡就要通过旋转操作来保持平衡,这个过程是比较耗时的。跳表诞生的初衷就是为了克服平衡树的一些缺点。跳表使用概率平衡而不是严格强制的平衡,因此,跳表中的插入和删除算法比平衡树的等效算法简单得多,速度也快得多。
- 红黑树 vs 跳表:相比较于红黑树来说,跳表的实现也更简单一些,不需要通过旋转和染色(红黑变换)来保证黑平衡。并且,按照区间来查找数据这个操作,红黑树的效率没有跳表高。
- B+树 vs 跳表:B+树更适合作为数据库和文件系统中常用的索引结构之一,它的核心思想是通过可能少的 IO 定位到尽可能多的索引来获得查询数据。对于 Redis 这种内存数据库来说,它对这些并不感冒,因为 Redis 作为内存数据库它不可能存储大量的数据,所以对于索引不需要通过 B+树这种方式进行维护,只需按照概率进行随机维护即可,节约内存。而且使用跳表实现 zset 时相较前者来说更简单一些,在进行插入时只需通过索引将数据插入到链表中合适的位置再随机维护一定高度的索引即可,也不需要像 B+树那样插入时发现失衡时还需要对节点分裂与合并。
另外,我还单独写了一篇文章从有序集合的基本使用到跳表的源码分析和实现,让你会对 Redis 的有序集合底层实现的跳表有着更深刻的理解和掌握 :Redis 为什么用跳表实现有序集合。
Set 的应用场景是什么?
Redis 中 Set
是一种无序集合,集合中的元素没有先后顺序但都唯一,有点类似于 Java 中的 HashSet
。
Set
的常见应用场景如下:
- 存放的数据不能重复的场景:网站 UV 统计(数据量巨大的场景还是
HyperLogLog
更适合一些)、文章点赞、动态点赞等等。 - 需要获取多个数据源交集、并集和差集的场景:共同好友(交集)、共同粉丝(交集)、共同关注(交集)、好友推荐(差集)、音乐推荐(差集)、订阅号推荐(差集+交集) 等等。
- 需要随机获取数据源中的元素的场景:抽奖系统、随机点名等等。
使用 Set 实现抽奖系统怎么做?
如果想要使用 Set
实现一个简单的抽奖系统的话,直接使用下面这几个命令就可以了:
SADD key member1 member2 ...
:向指定集合添加一个或多个元素。SPOP key count
:随机移除并获取指定集合中一个或多个元素,适合不允许重复中奖的场景。SRANDMEMBER key count
: 随机获取指定集合中指定数量的元素,适合允许重复中奖的场景。
使用 Bitmap 统计活跃用户
1. Bitmap 原理
Bitmap 是 Redis 提供的一种存储二进制数据的方式,它通过 bit 位 存储信息,每个 bit 仅占 1 bit 空间,非常节省存储。
在用户活跃统计中:
- Key:使用
日期
(如user:active:20240209
)作为键,表示某天的数据。 - Offset:使用
用户 ID
作为偏移量,每个用户占据 1 bit。 - 值:
0
表示用户未活跃。1
表示用户活跃。
2. Redis Bitmap 命令
命令 | 作用 |
---|---|
SETBIT key offset value | 设置 key 的 offset 位置的 bit 值(0 或 1) |
GETBIT key offset | 获取 key 的 offset 位置的 bit 值 |
BITCOUNT key | 统计 key 中 bit 为 1 的数量(即活跃用户数) |
BITOP AND dest key1 key2 ... | 计算多个 key 的 交集 并存储到 dest |
BITOP OR dest key1 key2 ... | 计算多个 key 的 并集 并存储到 dest |
3. 示例:使用 Bitmap 统计活跃用户
3.1 Redis CLI 操作
(1) 记录用户活跃状态
SETBIT user:active:20240208 1 1 # 用户ID 1 当天活跃
SETBIT user:active:20240208 2 1 # 用户ID 2 当天活跃
SETBIT user:active:20240209 1 1 # 用户ID 1 次日也活跃
(2) 统计某天活跃用户
BITCOUNT user:active:20240208
# 返回值: 2 (表示当天有 2 个活跃用户)
(3) 统计连续两天都活跃的用户(交集)
BITOP AND active_both user:active:20240208 user:active:20240209
BITCOUNT active_both
# 返回值: 1 (表示 1 个用户连续两天活跃)
(4) 统计两天内有活跃记录的用户(并集)
BITOP OR active_any user:active:20240208 user:active:20240209
BITCOUNT active_any
# 返回值: 2 (表示 2 个用户在这两天至少活跃过一次)
4. Java 代码实现
使用 Lettuce
或 Jedis
操作 Redis Bitmap。
4.1 使用 Lettuce
import io.lettuce.core.RedisClient;
import io.lettuce.core.api.sync.RedisCommands;
public class RedisBitmapExample {
private static final String REDIS_URI = "redis://localhost:6379";
public static void main(String[] args) {
// 连接 Redis
RedisClient redisClient = RedisClient.create(REDIS_URI);
var connection = redisClient.connect();
RedisCommands<String, String> redis = connection.sync();
// 设置用户活跃状态
redis.setbit("user:active:20240208", 1, true);
redis.setbit("user:active:20240208", 2, true);
redis.setbit("user:active:20240209", 1, true);
// 统计某天活跃用户数
long count = redis.bitcount("user:active:20240208");
System.out.println("2024-02-08 活跃用户数: " + count);
// 统计连续两天都活跃的用户
redis.bitopAnd("active_both", "user:active:20240208", "user:active:20240209");
long bothActive = redis.bitcount("active_both");
System.out.println("连续两天活跃的用户数: " + bothActive);
// 统计两天内有活跃的用户
redis.bitopOr("active_any", "user:active:20240208", "user:active:20240209");
long anyActive = redis.bitcount("active_any");
System.out.println("两天内活跃过的用户数: " + anyActive);
// 关闭连接
connection.close();
redisClient.shutdown();
}
}
4.2 使用 Jedis
import redis.clients.jedis.Jedis;
public class RedisBitmapExample {
private static final String REDIS_HOST = "localhost";
private static final int REDIS_PORT = 6379;
public static void main(String[] args) {
// 连接 Redis
try (Jedis jedis = new Jedis(REDIS_HOST, REDIS_PORT)) {
// 设置用户活跃状态
jedis.setbit("user:active:20240208", 1, true);
jedis.setbit("user:active:20240208", 2, true);
jedis.setbit("user:active:20240209", 1, true);
// 统计某天活跃用户数
long count = jedis.bitcount("user:active:20240208");
System.out.println("2024-02-08 活跃用户数: " + count);
// 统计连续两天都活跃的用户
jedis.bitop("AND", "active_both", "user:active:20240208", "user:active:20240209");
long bothActive = jedis.bitcount("active_both");
System.out.println("连续两天活跃的用户数: " + bothActive);
// 统计两天内有活跃的用户
jedis.bitop("OR", "active_any", "user:active:20240208", "user:active:20240209");
long anyActive = jedis.bitcount("active_any");
System.out.println("两天内活跃过的用户数: " + anyActive);
}
}
}
5. 进阶优化
(1) 计算用户连续活跃天数
使用 BITPOS
找出用户首次活跃的 offset
,再使用 GETBIT
逐个查询:
BITPOS user:active:20240201 1
然后使用循环遍历 GETBIT key offset
计算连续活跃天数。
(2) 大规模用户数据优化
- 存储压缩:结合
RoaringBitmap
或HyperLogLog
进行存储优化。 - 定期清理:通过
EXPIRE
设定数据过期时间,防止 Redis 占用过多内存:EXPIRE user:active:20240208 2592000 # 30 天后自动删除
6. 适用场景
Bitmap 适用于:
- 用户活跃统计(如 DAU、WAU、MAU 计算)
- 签到记录(如连续签到奖励)
- IP 访问去重(如 PV 统计)
- 权限管理(如角色/权限位存储)
这样,你就可以用 Redis Bitmap 高效统计活跃用户了!🚀
使用 HyperLogLog 统计页面 UV 怎么做?
使用 HyperLogLog 统计页面 UV 主要需要用到下面这两个命令:
PFADD key element1 element2 ...
:添加一个或多个元素到 HyperLogLog 中。PFCOUNT key1 key2
:获取一个或者多个 HyperLogLog 的唯一计数。
1、将访问指定页面的每个用户 ID 添加到 HyperLogLog
中。
PFADD PAGE_1:UV USER1 USER2 ...... USERn
2、统计指定页面的 UV。
PFCOUNT PAGE_1:UV
Redis 持久化机制(重要)
Redis 持久化机制
Redis 提供两种持久化机制,分别是 RDB(Redis Database Snapshot) 和 AOF(Append-Only File),它们可以单独使用,也可以结合使用,以实现不同的持久化需求。
1. RDB(Redis Database Snapshot)
RDB 是 快照(Snapshot) 方式的持久化,Redis 会在特定时间间隔内,将当前数据集以 二进制文件(.rdb
)的形式存盘。
1.1 RDB 触发方式
RDB 快照可以通过以下方式触发:
触发方式 | 说明 |
---|---|
自动触发 | 通过 save 配置设定规则,达到指定条件时触发 |
手动触发 | SAVE (阻塞式)和 BGSAVE (后台异步)命令 |
Redis 退出 | Redis 关闭时,默认执行 RDB 持久化(可配置) |
1.2 配置 RDB
在 redis.conf
文件中:
# 900秒(15分钟)内至少有1次写操作
# 300秒(5分钟)内至少有10次写操作
# 60秒(1分钟)内至少有10000次写操作
save 900 1
save 300 10
save 60 10000
1.3 手动触发
SAVE
:同步阻塞式,直接创建 RDB 文件,期间 Redis 无法处理请求。SAVE
BGSAVE
:异步,Redis 创建子进程完成 RDB,期间仍可处理请求。BGSAVE
LASTSAVE
:获取最后一次 RDB 生成时间(Unix 时间戳)。LASTSAVE
1.4 RDB 文件存储
- 默认文件名:
dump.rdb
- 默认存储路径:
/var/lib/redis/dump.rdb
- RDB 文件是 二进制格式,不易读取,但恢复速度快。
1.5 RDB 优缺点
✅ 优点:
- 适合全量备份,存储快照到远程服务器或云存储。
- 恢复速度快,适用于高并发读场景(如缓存)。
- 节省存储,RDB 是二进制压缩格式,比 AOF 文件小。
❌ 缺点:
- 可能丢失数据(快照间隔内的更改未保存)。
- BGSAVE 影响性能,创建子进程可能导致短暂 I/O 压力。
2. AOF(Append-Only File)
AOF 采用 日志(Append-Only) 方式持久化,将 Redis 执行的所有写操作以 文本格式 记录下来,保证数据不丢失。
2.1 AOF 触发方式
AOF 日志会在 Redis 每次执行写操作时 被追加到 appendonly.aof
文件中,并根据配置的同步策略决定何时同步到磁盘。
2.2 配置 AOF
在 redis.conf
文件中:
# 启用 AOF
appendonly yes
# AOF 日志文件
appendfilename "appendonly.aof"
# AOF 持久化策略:
# always:每次写操作都同步到磁盘(最安全但慢)
# everysec(推荐):每秒同步一次(高性能,可能丢失 1s 数据)
# no:让操作系统决定何时写入(性能最好但不可靠)
appendfsync everysec
2.3 手动触发
- 强制同步 AOF
BGREWRITEAOF
2.4 AOF 日志重写
随着时间推移,AOF 文件会变得很大,需要定期压缩:
- Redis 通过
BGREWRITEAOF
后台异步重写 AOF 文件。 - 该命令会创建一个新的 AOF 文件,仅保留最新的数据库状态,减少日志体积。
- AOF 重写不会影响 Redis 正常工作。
2.5 AOF 存储
- 默认文件名:
appendonly.aof
- 默认存储路径:
/var/lib/redis/appendonly.aof
- AOF 是 纯文本格式,可直接编辑查看。
2.6 AOF 优缺点
✅ 优点:
- 数据安全,几乎零数据丢失(
appendfsync always
模式)。 - 易读可恢复,AOF 文件是文本格式,可手动修改恢复。
- 适合写密集型应用,如交易系统。
❌ 缺点:
- 文件大,相比 RDB,占用更多存储空间。
- 恢复速度慢,需要执行所有写操作回放日志。
- 性能开销,每次写操作都要记录日志,影响 TPS。
3. RDB vs. AOF 对比
特性 | RDB(快照) | AOF(日志) |
---|---|---|
数据丢失风险 | 可能丢失最近一次快照后的数据 | everysec 模式下最多丢失 1s 数据 |
文件大小 | 占用空间小,二进制格式 | 文件大,文本格式 |
写性能 | 适合大并发读,快照机制影响小 | 影响写入性能(需要日志追加) |
恢复速度 | 恢复快,直接加载快照 | 恢复慢,需要回放日志 |
适用场景 | 全量备份,高并发读 | 事务日志,数据安全要求高 |
4. RDB + AOF 结合使用
Redis 支持同时启用 RDB 和 AOF:
- RDB 负责定期快照,提供快速恢复能力。
- AOF 负责日志记录,保证数据安全性。
4.1 启用 RDB + AOF
在 redis.conf
中:
save 900 1 # RDB 快照
appendonly yes # 开启 AOF
appendfsync everysec # AOF 每秒同步一次
4.2 Redis 启动时恢复策略
同时有 RDB 和 AOF:
- Redis 优先加载 AOF(因为 AOF 更完整)。
- 如果 AOF 损坏,Redis 可能无法启动(可手动修复)。
仅有 RDB:
- 直接加载
dump.rdb
,可能丢失最近的更改。
- 直接加载
5. 持久化最佳实践
- 高并发读场景:仅使用 RDB,避免 AOF 性能开销。
- 高数据安全要求:启用 AOF(
appendfsync everysec
),防止数据丢失。 - 结合使用:生产环境推荐同时开启 RDB 和 AOF,权衡数据安全和恢复速度。
- 定期备份:
- 远程备份
dump.rdb
和appendonly.aof
。 - 设置
BGREWRITEAOF
,防止 AOF 文件过大。
- 远程备份
总结
方案 | 适用场景 |
---|---|
仅 RDB | 适用于缓存场景,数据丢失影响不大 |
仅 AOF | 适用于数据安全性要求高的应用(金融、交易) |
RDB + AOF | 生产环境推荐,保证数据安全 + 快速恢复 |
Redis 持久化方式的选择取决于具体业务需求,如果数据安全要求高,建议 RDB + AOF 结合使用!🚀
Redis 持久化机制(RDB 持久化、AOF 持久化、RDB 和 AOF 的混合持久化) 相关的问题比较多,也比较重要,于是我单独抽了一篇文章来总结 Redis 持久化机制相关的知识点和问题:Redis 持久化机制详解 。
Redis 线程模型(重要)
对于读写命令来说,Redis 一直是单线程模型。不过,在 Redis 4.0 版本之后引入了多线程来执行一些大键值对的异步删除操作, Redis 6.0 版本之后引入了多线程来处理网络请求(提高网络 IO 读写性能)。
Redis 单线程模型了解吗?
Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型 (Netty 的线程模型也基于 Reactor 模式,Reactor 模式不愧是高性能 IO 的基石),这套事件处理模型对应的是 Redis 中的文件事件处理器(file event handler)。由于文件事件处理器(file event handler)是单线程方式运行的,所以我们一般都说 Redis 是单线程模型。
《Redis 设计与实现》有一段话是这样介绍文件事件处理器的,我觉得写得挺不错。
Redis 基于 Reactor 模式开发了自己的网络事件处理器:这个处理器被称为文件事件处理器(file event handler)。
- 文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
- 当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关 闭(close)等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
虽然文件事件处理器以单线程方式运行,但通过使用 I/O 多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通信模型,又可以很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接,这保持了 Redis 内部单线程设计的简单性。
既然是单线程,那怎么监听大量的客户端连接呢?
Redis 通过 IO 多路复用程序 来监听来自客户端的大量连接(或者说是监听多个 socket),它会将感兴趣的事件及类型(读、写)注册到内核中并监听每个事件是否发生。
这样的好处非常明显:I/O 多路复用技术的使用让 Redis 不需要额外创建多余的线程来监听客户端的大量连接,降低了资源的消耗(和 NIO 中的 Selector
组件很像)。
文件事件处理器(file event handler)主要是包含 4 个部分:
- 多个 socket(客户端连接)
- IO 多路复用程序(支持多个客户端连接的关键)
- 文件事件分派器(将 socket 关联到相应的事件处理器)
- 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
相关阅读:Redis 事件机制详解 。
Redis 6.0 之前为什么不使用多线程?
1. Redis 主要是单线程运行
Redis 6.0 之前,核心逻辑(包括 命令执行、网络 I/O、数据处理)全部由单线程完成。但 Redis 4.0+ 已经支持 异步删除大键 和 异步刷库,利用 后台线程 减少主线程阻塞。
2. 为什么 Redis 6.0 之前不使用多线程?
Redis 选择单线程模式的核心原因有 三个:
(1) 单线程编程简单,维护性更高
- 避免竞争条件(Race Condition):
- 多线程带来的 共享数据访问问题,如 锁竞争、死锁,会导致开发复杂度上升。
- 代码简单,稳定性更好:
- 单线程避免了复杂的 线程同步机制(如 读写锁、CAS 操作),使代码逻辑清晰易维护。
(2) Redis 的性能瓶颈不在 CPU,而在内存和网络
Redis 主要依赖 内存,而非 CPU 密集型计算。
- 数据存储在内存中,查询操作通常是 O(1) 或 O(logN),消耗 CPU 资源很少。
- 网络 I/O 和内存带宽 是性能瓶颈:
- 网络延迟 远大于 Redis 的命令执行时间。
- 大多数请求都是小数据,CPU 处理速度足够快。
📌 实验数据:
- 单线程下,Redis 每秒可执行 10万+ QPS。
- CPU 占用率通常 不到 50%,但网络 I/O 和带宽可能成为限制。
(3) 多线程带来的问题
- 上下文切换成本高:
- 线程间的 上下文切换 需要 CPU 额外操作寄存器、缓存刷新,可能降低性能。
- 同步机制增加锁竞争:
- 多线程需要 加锁 控制并发,导致 CAS 失败、死锁,影响整体吞吐量。
- 性能未必能显著提升:
- Redis 采用 基于事件驱动的 I/O 多路复用(epoll/kqueue),单线程处理能力已经足够强大。
3. Redis 4.0~5.0:有限的多线程支持
虽然核心仍然是单线程,但 Redis 4.0+ 引入了一些异步特性:
版本 | 新增多线程功能 |
---|---|
4.0 | 异步删除大键(UNLINK ),异步清库(FLUSHALL ASYNC ) |
5.0 | Stream 数据结构,引入部分后台任务多线程 |
这些优化减少了 主线程阻塞,但 Redis 核心逻辑仍然是单线程。
4. Redis 6.0 之后:引入 I/O 多线程
- Redis 6.0 之后,引入 I/O 线程,让 网络数据的读取、解析 由多线程处理。
- 但 命令执行 仍然是 单线程,确保原子性。
# Redis 6.0+ 配置开启多线程(默认 1)
io-threads 4
🚀 结果:
- 读取大数据包时,性能提升 2~3 倍。
- 适用于 大数据量、高并发 的场景。
总结
为什么 Redis 6.0 之前不使用多线程?
- 单线程代码简单,稳定性高(避免死锁、竞争)。
- Redis 主要受网络和内存限制,而不是 CPU 瓶颈。
- 多线程带来的同步开销可能反而降低性能。
Redis 6.0 之后如何优化?
- 引入 I/O 多线程,加速 网络数据解析,但核心命令仍然单线程执行。
🚀 结论: Redis 的 单线程模型并不影响高性能,合理利用多线程的 I/O 优化 是 Redis 6.0 之后的改进方向。
Redis6.0 之后为何引入了多线程?
Redis6.0 引入多线程主要是为了提高网络 IO 读写性能,因为这个算是 Redis 中的一个性能瓶颈(Redis 的瓶颈主要受限于内存和网络)。
虽然,Redis6.0 引入了多线程,但是 Redis 的多线程只是在网络数据的读写这类耗时操作上使用了,执行命令仍然是单线程顺序执行。因此,你也不需要担心线程安全问题。
Redis6.0 的多线程默认是禁用的,只使用主线程。如需开启需要设置 IO 线程数 > 1,需要修改 redis 配置文件 redis.conf
:
io-threads 4 #设置1的话只会开启主线程,官网建议4核的机器建议设置为2或3个线程,8核的建议设置为6个线程
另外:
- io-threads 的个数一旦设置,不能通过 config 动态设置。
- 当设置 ssl 后,io-threads 将不工作。
开启多线程后,默认只会使用多线程进行 IO 写入 writes,即发送数据给客户端,如果需要开启多线程 IO 读取 reads,同样需要修改 redis 配置文件 redis.conf
:
io-threads-do-reads yes
但是官网描述开启多线程读并不能有太大提升,因此一般情况下并不建议开启
相关阅读:
Redis 后台线程了解吗?
我们虽然经常说 Redis 是单线程模型(主要逻辑是单线程完成的),但实际还有一些后台线程用于执行一些比较耗时的操作:
- 通过
bio_close_file
后台线程来释放 AOF / RDB 等过程中产生的临时文件资源。 - 通过
bio_aof_fsync
后台线程调用fsync
函数将系统内核缓冲区还未同步到到磁盘的数据强制刷到磁盘( AOF 文件)。 - 通过
bio_lazy_free
后台线程释放大对象(已删除)占用的内存空间.
在bio.h
文件中有定义(Redis 6.0 版本,源码地址:https://github.com/redis/redis/blob/6.0/src/bio.h):
#ifndef __BIO_H
#define __BIO_H
/* Exported API */
void bioInit(void);
void bioCreateBackgroundJob(int type, void *arg1, void *arg2, void *arg3);
unsigned long long bioPendingJobsOfType(int type);
unsigned long long bioWaitStepOfType(int type);
time_t bioOlderJobOfType(int type);
void bioKillThreads(void);
/* Background job opcodes */
#define BIO_CLOSE_FILE 0 /* Deferred close(2) syscall. */
#define BIO_AOF_FSYNC 1 /* Deferred AOF fsync. */
#define BIO_LAZY_FREE 2 /* Deferred objects freeing. */
#define BIO_NUM_OPS 3
#endif
关于 Redis 后台线程的详细介绍可以查看 Redis 6.0 后台线程有哪些? 这篇就文章。
Redis 内存管理
Redis 给缓存数据设置过期时间有什么用?
一般情况下,我们设置保存的缓存数据的时候都会设置一个过期时间。为什么呢?
内存是有限且珍贵的,如果不对缓存数据设置过期时间,那内存占用就会一直增长,最终可能会导致 OOM 问题。通过设置合理的过期时间,Redis 会自动删除暂时不需要的数据,为新的缓存数据腾出空间。
Redis 自带了给缓存数据设置过期时间的功能,比如:
127.0.0.1:6379> expire key 60 # 数据在 60s 后过期
(integer) 1
127.0.0.1:6379> setex key 60 value # 数据在 60s 后过期 (setex:[set] + [ex]pire)
OK
127.0.0.1:6379> ttl key # 查看数据还有多久过期
(integer) 56
注意 ⚠️:Redis 中除了字符串类型有自己独有设置过期时间的命令 setex
外,其他方法都需要依靠 expire
命令来设置过期时间 。另外, persist
命令可以移除一个键的过期时间。
过期时间除了有助于缓解内存的消耗,还有什么其他用么?
很多时候,我们的业务场景就是需要某个数据只在某一时间段内存在,比如我们的短信验证码可能只在 1 分钟内有效,用户登录的 Token 可能只在 1 天内有效。
如果使用传统的数据库来处理的话,一般都是自己判断过期,这样更麻烦并且性能要差很多。
Redis 如何判断数据是否过期
过期时间存储方式
Redis 通过过期字典(expires
)来记录键的过期时间。过期字典是一个 dict
(哈希表),其中:
- 键(key)指向 Redis 数据库中的某个数据键;
- 值(value)是一个
long long
类型的整数,表示该键的过期时间(以毫秒级 UNIX 时间戳存储)。
在 Redis 的 redisDb
结构中,expires
用来存储所有带有过期时间的键:
typedef struct redisDb {
dict *dict; // 数据库键空间,存储所有键值对
dict *expires; // 过期字典,保存键的过期时间
} redisDb;
过期键的删除策略
Redis 主要有三种策略来判断键是否过期并进行删除:
惰性删除(Lazy Expiration)
- 当客户端访问某个键时,Redis 先检查
expires
字典,如果键已过期,则立即删除,并返回null
。 - 这个方法的优点是不会影响 Redis 的性能,但如果一个键长期不被访问,它的过期信息就不会被处理,占用内存。
- 当客户端访问某个键时,Redis 先检查
定期删除(Active Expiration)
- Redis 每隔
100ms
会随机抽取一定数量的键进行过期检查。 - 如果键过期,则删除该键,并继续检查剩余键。
- 由于是定期触发的,因此无法保证过期键一定会被及时清理。
- Redis 每隔
内存淘汰(Eviction)
- 当 Redis 运行在内存不足的情况下,如果
maxmemory
策略允许,Redis 会主动删除过期键(或未使用的键)。 - 该策略用于确保 Redis 不会因为过多的过期数据而导致内存溢出。
- 当 Redis 运行在内存不足的情况下,如果
过期判断的具体流程
客户端请求访问某个键:
- 先查询
dict
看是否存在该键; - 如果键存在,再检查
expires
是否有对应的过期时间; - 若
expires
记录的时间小于当前时间,则删除该键,并返回null
。
- 先查询
Redis 定期删除线程:
- 每
100ms
随机选取一批键,检查它们的过期时间; - 如果发现已过期的键,则删除;
- 这种方式可以减少内存占用,但不能保证所有过期键都能被立即删除。
- 每
内存淘汰策略(如果启用
maxmemory
):- 当 Redis 占用内存超过
maxmemory
时,会根据配置的淘汰策略删除数据,包括删除过期键。
- 当 Redis 占用内存超过
示例
# 设置键 "key1" 5 秒后过期
SET key1 "Hello Redis"
EXPIRE key1 5
- 前 5 秒:执行
GET key1
会返回"Hello Redis"
; - 超过 5 秒:再执行
GET key1
,Redis 会先检查expires
,发现key1
已过期,直接删除并返回null
。
总结
- Redis 通过
expires
记录键的过期时间,在查询时检查是否已过期。 - 主要使用惰性删除和定期删除来清理过期键。
- 在内存不足时,Redis 还可以根据
maxmemory
策略主动淘汰过期键。
这种策略在性能与内存管理之间取得了平衡,保证了 Redis 的高效运行。
Redis 过期 key 删除策略了解么?
如果假设你设置了一批 key 只能存活 1 分钟,那么 1 分钟后,Redis 是怎么对这批 key 进行删除的呢?
常用的过期数据的删除策略就下面这几种:
- 惰性删除:只会在取出/查询 key 的时候才对数据进行过期检查。这种方式对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。
- 定期删除:周期性地随机从设置了过期时间的 key 中抽查一批,然后逐个检查这些 key 是否过期,过期就删除 key。相比于惰性删除,定期删除对内存更友好,对 CPU 不太友好。
- 延迟队列:把设置过期时间的 key 放到一个延迟队列里,到期之后就删除 key。这种方式可以保证每个过期 key 都能被删除,但维护延迟队列太麻烦,队列本身也要占用资源。
- 定时删除:每个设置了过期时间的 key 都会在设置的时间到达时立即被删除。这种方法可以确保内存中不会有过期的键,但是它对 CPU 的压力最大,因为它需要为每个键都设置一个定时器。
Redis 采用的那种删除策略呢?
Redis 采用的是 定期删除+惰性/懒汉式删除 结合的策略,这也是大部分缓存框架的选择。定期删除对内存更加友好,惰性删除对 CPU 更加友好。两者各有千秋,结合起来使用既能兼顾 CPU 友好,又能兼顾内存友好。
下面是我们详细介绍一下 Redis 中的定期删除具体是如何做的。
Redis 的定期删除过程是随机的(周期性地随机从设置了过期时间的 key 中抽查一批),所以并不保证所有过期键都会被立即删除。这也就解释了为什么有的 key 过期了,并没有被删除。并且,Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响。
另外,定期删除还会受到执行时间和过期 key 的比例的影响:
- 执行时间已经超过了阈值,那么就中断这一次定期删除循环,以避免使用过多的 CPU 时间。
- 如果这一批过期的 key 比例超过一个比例,就会重复执行此删除流程,以更积极地清理过期 key。相应地,如果过期的 key 比例低于这个比例,就会中断这一次定期删除循环,避免做过多的工作而获得很少的内存回收。
Redis 7.2 版本的执行时间阈值是 25ms,过期 key 比例设定值是 10%。
#define ACTIVE_EXPIRE_CYCLE_FAST_DURATION 1000 /* Microseconds. */
#define ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 25 /* Max % of CPU to use. */
#define ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE 10 /* % of stale keys after which
we do extra efforts. */
每次随机抽查数量是多少?
expire.c
中定义了每次随机抽查的数量,Redis 7.2 版本为 20 ,也就是说每次会随机选择 20 个设置了过期时间的 key 判断是否过期。
#define ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP 20 /* Keys for each DB loop. */
如何控制定期删除的执行频率?
在 Redis 中,定期删除的频率是由 hz 参数控制的。hz 默认为 10,代表每秒执行 10 次,也就是每秒钟进行 10 次尝试来查找并删除过期的 key。
hz 的取值范围为 1~500。增大 hz 参数的值会提升定期删除的频率。如果你想要更频繁地执行定期删除任务,可以适当增加 hz 的值,但这会加 CPU 的使用率。根据 Redis 官方建议,hz 的值不建议超过 100,对于大部分用户使用默认的 10 就足够了。
类似的参数还有一个 dynamic-hz,这个参数开启之后 Redis 就会在 hz 的基础上动态计算一个值。Redis 提供并默认启用了使用自适应 hz 值的能力,
这两个参数都在 Redis 配置文件 redis.conf
中:
# 默认为 10
hz 10
# 默认开启
dynamic-hz yes
多提一嘴,除了定期删除过期 key 这个定期任务之外,还有一些其他定期任务例如关闭超时的客户端连接、更新统计信息,这些定期任务的执行频率也是通过 hz 参数决定。
为什么定期删除不是把所有过期 key 都删除呢?
这样会对性能造成太大的影响。如果我们 key 数量非常庞大的话,挨个遍历检查是非常耗时的,会严重影响性能。Redis 设计这种策略的目的是为了平衡内存和性能。
为什么 key 过期之后不立马把它删掉呢?这样不是会浪费很多内存空间吗?
因为不太好办到,或者说这种删除方式的成本太高了。假如我们使用延迟队列作为删除策略,这样存在下面这些问题:
- 队列本身的开销可能很大:key 多的情况下,一个延迟队列可能无法容纳。
- 维护延迟队列太麻烦:修改 key 的过期时间就需要调整期在延迟队列中的位置,并且,还需要引入并发控制。
大量 key 集中过期怎么办?
当 Redis 中存在大量 key 在同一时间点集中过期时,可能会导致以下问题:
- 请求延迟增加: Redis 在处理过期 key 时需要消耗 CPU 资源,如果过期 key 数量庞大,会导致 Redis 实例的 CPU 占用率升高,进而影响其他请求的处理速度,造成延迟增加。
- 内存占用过高: 过期的 key 虽然已经失效,但在 Redis 真正删除它们之前,仍然会占用内存空间。如果过期 key 没有及时清理,可能会导致内存占用过高,甚至引发内存溢出。
为了避免这些问题,可以采取以下方案:
- 尽量避免 key 集中过期: 在设置键的过期时间时尽量随机一点。
- 开启 lazy free 机制: 修改
redis.conf
配置文件,将lazyfree-lazy-expire
参数设置为yes
,即可开启 lazy free 机制。开启 lazy free 机制后,Redis 会在后台异步删除过期的 key,不会阻塞主线程的运行,从而降低对 Redis 性能的影响。
Redis 内存淘汰策略了解么?
相关问题:MySQL 里有 2000w 数据,Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据?
Redis 的内存淘汰策略只有在运行内存达到了配置的最大内存阈值时才会触发,这个阈值是通过redis.conf
的maxmemory
参数来定义的。64 位操作系统下,maxmemory
默认为 0 ,表示不限制内存大小。32 位操作系统下,默认的最大内存值是 3GB。
你可以使用命令 config get maxmemory
来查看 maxmemory
的值。
> config get maxmemory
maxmemory
0
Redis 提供了 6 种内存淘汰策略:
- volatile-lru(least recently used):从已设置过期时间的数据集(
server.db[i].expires
)中挑选最近最少使用的数据淘汰。 - volatile-ttl:从已设置过期时间的数据集(
server.db[i].expires
)中挑选将要过期的数据淘汰。 - volatile-random:从已设置过期时间的数据集(
server.db[i].expires
)中任意选择数据淘汰。 - allkeys-lru(least recently used):从数据集(
server.db[i].dict
)中移除最近最少使用的数据淘汰。 - allkeys-random:从数据集(
server.db[i].dict
)中任意选择数据淘汰。 - no-eviction(默认内存淘汰策略):禁止驱逐数据,当内存不足以容纳新写入数据时,新写入操作会报错。
4.0 版本后增加以下两种:
- volatile-lfu(least frequently used):从已设置过期时间的数据集(
server.db[i].expires
)中挑选最不经常使用的数据淘汰。 - allkeys-lfu(least frequently used):从数据集(
server.db[i].dict
)中移除最不经常使用的数据淘汰。
allkeys-xxx
表示从所有的键值中淘汰数据,而 volatile-xxx
表示从设置了过期时间的键值中淘汰数据。
config.c
中定义了内存淘汰策略的枚举数组:
configEnum maxmemory_policy_enum[] = {
{"volatile-lru", MAXMEMORY_VOLATILE_LRU},
{"volatile-lfu", MAXMEMORY_VOLATILE_LFU},
{"volatile-random",MAXMEMORY_VOLATILE_RANDOM},
{"volatile-ttl",MAXMEMORY_VOLATILE_TTL},
{"allkeys-lru",MAXMEMORY_ALLKEYS_LRU},
{"allkeys-lfu",MAXMEMORY_ALLKEYS_LFU},
{"allkeys-random",MAXMEMORY_ALLKEYS_RANDOM},
{"noeviction",MAXMEMORY_NO_EVICTION},
{NULL, 0}
};
你可以使用 config get maxmemory-policy
命令来查看当前 Redis 的内存淘汰策略。
> config get maxmemory-policy
maxmemory-policy
noeviction
可以通过config set maxmemory-policy 内存淘汰策略
命令修改内存淘汰策略,立即生效,但这种方式重启 Redis 之后就失效了。修改 redis.conf
中的 maxmemory-policy
参数不会因为重启而失效,不过,需要重启之后修改才能生效。
maxmemory-policy noeviction
关于淘汰策略的详细说明可以参考 Redis 官方文档:https://redis.io/docs/reference/eviction/。
Redis相关
下文是基于一个文档转换过来的:
Redis为什么快?
- 纯内存KV操作
Redis的操作都是基于内存的,CPU不是Redis性能瓶颈,Redis的瓶颈是机器内存和网络带宽。
在计算机的世界中,CPU的速度是远大于内存的速度的,同时内存的速度也是远大于硬盘的速度。Redis的操作都是基于内存的,绝大部分请求是纯粹的内存操作,非常迅速。
- 单线程操作
使用单线程可以省去多线程时CPU上下文会切换的时间,也不用去考虑各种锁的问题,不存在加锁释放锁操作,没有死锁问题导致的性能消耗。对于内存系统来说,多次读写都是在一个CPU上,没有上下文切换效率就是最高的!既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章的采用单线程的方案了。
Redis单线程指的是网络请求模块使用了一个线程,即一个线程处理所有网络请求,其他模块该使用多线程,仍会使用了多个线程。
- I/O 多路复用
为什么Redis中要使用I/O多路复用这种技术呢?
首先,Redis是跑在单线程中的,所有的操作都是按照顺序线性执行的,但是由于读写操作等待用户输入或输出都是阻塞的,所以I/O操作在一般情况下往往不能直接返回,这会导致某一文件的I/O阻塞导致整个进程无法对其它客户提供服务,而I/O多路复用就是为了解决这个问题而出现的。
- Reactor 设计模式
Redis基于Reactor模式开发了自己的网络事件处理器,称之为文件事件处理器(File Event Handler)。
Redis合适的应用场景?
会话缓存(Session Cache)最常用的一种使用Redis的情景是会话缓存(session cache),用Redis缓存会话比其他存储(如Memcached)的优势在于:Redis提供持久化。当维护一个不是严格要求一致性的缓存时,如果用户的购物车信息全部丢失,大部分人都会不高兴的,现在,他们还会这样吗?幸运的是,随着Redis这些年的改进,很容易找到怎么恰当的使用Redis来缓存会话的文档。甚至广为人知的商业平台Magento也提供Redis的插件。
全页缓存(FPC)除基本的会话token之外,Redis还提供很简便的FPC平台。回到一致性问题,即使重启了Redis实例,因为有磁盘的持久化,用户也不会看到页面加载速度的下降,这是一个极大改进,类似PHP本地FPC。再次以Magento为例,Magento提供一个插件来使用Redis作为全页缓存后端。此外,对WordPress的用户来说,Pantheon有一个非常好的插件wp-redis,这个插件能帮助你以最快速度加载你曾浏览过的页面。
队列Redis在内存存储引擎领域的一大优点是提供list和set操作,这使得Redis能作为一个很好的消息队列平台来使用。Redis作为队列使用的操作,就类似于本地程序语言(如Python)对list的push/pop操作。如果你快速的在Google中搜索"Redis queues",你马上就能找到大量的开源项目,这些项目的目的就是利用Redis创建非常好的后端工具,以满足各种队列需求。例如,Celery有一个后台就是使用Redis作为broker,你可以从这里去查看。
排行榜/计数器Redis在内存中对数字进行递增或递减的操作实现的非常好。集合(Set)和有序集合(Sorted Set)也使得我们在执行这些操作的时候变的非常简单,Redis只是正好提供了这两种数据结构。微信搜索公众号:Java专栏,获取最新面试手册所以,我们要从排序集合中获取到排名最靠前的10个用户--我们称之为"user_scores",我们只需要像下面一样执行即可:当然,这是假定你是根据你用户的分数做递增的排序。如果你想返回用户及用户的分数,你需要这样执行:ZRANGE user_scores 0 10 WITHSCORESAgora Games就是一个很好的例子,用Ruby实现的,它的排行榜就是使用Redis来存储数据的,你可以在这里看到。
发布/订阅最后(但肯定不是最不重要的)是Redis的发布/订阅功能。发布/订阅的使用场景确实非常多。我已看见人们在社交网络连接中使用,还可作为基于发布/订阅的脚本触发器,甚至用Redis的发布/订阅功能来建立聊天系统!
Redis6.0之前为什么一直不使用多线程?
官方曾做过类似问题的回复:使用Redis时,几乎不存在CPU成为瓶颈的情况,Redis主要受限于内存和网络。例如在一个普通的Linux系统上,Redis通过使用pipelining每秒可以处理100万个请求,所以如果应用程序主要使用O(N)或O(log(N))的命令,它几乎不会占用太多CPU。
使用了单线程后,可维护性高。多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,带来了并发读写的一系列问题,增加了系统复杂度、同时可能存在线程切换、甚至加锁解锁、死锁造成的性能损耗。Redis通过AE事件模型以及IO多路复用等技术,处理性能非常高,因此没有必要使用多线程。单线程机制使得Redis内部实现的复杂度大大降低,Hash的惰性Rehash、Lpush等等,"线程不安全"的命令都可以无锁进行。
Redis6.0为什么要引入多线程?
Redis将所有数据放在内存中,内存的响应时长大约为100纳秒,对于小数据包,Redis服务器可以处理80,000到100,000 QPS,这也是Redis处理的极限了,对于80%的公司来说,单线程的Redis已经足够使用了。
但随着越来越复杂的业务场景,有些公司动不动就上亿的交易量,因此需要更大的QPS。常见的解决方案是在分布式架构中对数据进行分区并采用多个服务器,但该方案有非常大的缺点,例如要管理的Redis服务器太多,维护代价大;某些适用于单个Redis服务器的命令不适用于数据分区;数据分区无法解决热点读/写问题;数据偏斜,重新分配和放大/缩小变得更加复杂等等。
Redis有哪些高级功能?
消息队列、自动过期删除、事务、数据持久化、分布式锁、附近的人、慢查询分析、Sentinel和集群等多项功能。
为什么要用Redis?
使用缓存的目的就是提升读写性能。而实际业务场景下,更多的是为了提升读性能,带来更好的性能,带来更高的并发量。Redis的读写性能比Mysql好的多,我们就可以把Mysql中的热点数据缓存到Redis中,提升读取性能,同时也减轻了Mysql的读取压力。
Redis与memcached相对有哪些优势?
- memcached所有的值均是简单的字符串,redis作为其替代者,支持更为丰富的数据类型。
- redis的速度比memcached快很多。
- redis可以持久化其数据。
- Redis支持数据的备份,即master-slave模式的数据备份。
- 使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样。Redis直接自己构建了VM机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
- value大小:redis最大可以达到1GB,而memcache只有1MB。
怎么理解Redis中事务?
事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行,事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。不过Redis的是弱事物。
事务是Redis实现在服务器端的行为,用户执行MULTI命令时,服务器会将对应这个用户的客户端对象设置为一个特殊的状态,在这个状态下后续用户执行的查询命令不会被真的执行,而是被服务器缓存起来,直到用户执行EXEC命令为止,服务器会将这个用户对应的客户端对象中缓存的命令按照提交的顺序依次执行。
Redis提供了简单的事务,之所以说它简单,主要是因为它不支持事务中的回滚特性,同时无法实现命令之间的逻辑关系计算,当然也体现了Redis的"keep it simple"的特性。
为什么要使用pipeline?
Pipeline(流水线)机制能改善上面这类问题,它能将一组Redis命令进行组装,通过一次RTT传输给Redis,再将这组Redis命令的执行结果按顺序返回给客户端,没有使用Pipeline执行了n条命令,整个过程需要n次RTT。
使用Pipeline执行了n次命令,整个过程需要1次RTT。
Redis的过期策略以及内存淘汰机制?
redis采用的是定期删除+惰性删除策略。为什么不用定时删除策略?定时删除,用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放,但是十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求,而不是删除key,因此没有采用这一策略.定期删除+惰性删除是如何工作的呢?定期删除,redis默认每个100ms检查,是否有过期的key,有过期key则删除。需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis岂不是卡死)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。于是,惰性删除派上用场。也就是说在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除。
- noeviction:返回错误当内存限制达到,并且客户端尝试执行会让更多内存被使用的命令。
- allkeys-lru: 尝试回收最少使用的键(LRU),使得新添加的数据有空间存放。
- volatile-lru: 尝试回收最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放。
- allkeys-random: 回收随机的键使得新添加的数据有空间存放。
- volatile-random: 回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键。
- volatile-ttl: 回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放。
什么是缓存穿透?如何避免?
缓存穿透:指查询一个一定不存在的数据,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到DB去查询,可能导致DB挂掉。
解决方案:1. 查询返回的数据为空,仍把这个空结果进行缓存,但过期时间会比较短;2. 布隆过滤器:将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对DB的查询。
什么是缓存雪崩?如何避免?
缓存雪崩:设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。与缓存击穿的区别:雪崩是很多key,击穿是某一个key缓存。
解决方案:将缓存失效时间分散开,比如可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
使用Redis如何设计分布式锁?
基于Redis实现的分布式锁,一个严谨的的流程如下:
- 加锁
SET lock_key $unique_id EX $expire_time NX
操作共享资源
释放锁:Lua脚本,先GET判断锁是否归属自己,再DEL释放锁。
怎么使用Redis实现消息队列?
基于List的 LPUSH+BRPOP 的实现
足够简单,消费消息延迟几乎为零,但是需要处理空闲连接的问题。
如果线程一直阻塞在那里,Redis客户端的连接就成了闲置连接,闲置过久,服务器一般会主动断开连接,减少闲置资源占用,这个时候blpop和brpop或抛出异常,所以在编写客户端消费者的时候要小心,如果捕获到异常,还有重试。
其他缺点包括:
做消费者确认ACK麻烦,不能保证消费者消费消息后是否成功处理的问题(宕机或处理异常等),通常需要维护一个Pending列表,保证消息处理确认;不能做广播模式,如pub/sub,消息发布/订阅模型;不能重复消费,一旦消费就会被删除;不支持分组消费。
基于Sorted-Set的实现
多用来实现延迟队列,当然也可以实现有序的普通的消息队列,但是消费者无法阻塞的获取消息,只能轮询,不允许重复消息。
PUB/SUB,订阅/发布模式
优点:
典型的广播模式,一个消息可以发布到多个消费者;多信道订阅,消费者可以同时订阅多个信道,从而接收多类消息;消息即时发送,消息不用等待消费者读取,消费者会自动接收到信道发布的消息。
缺点:
消息一旦发布,不能接收。换句话就是发布时若客户端不在线,则消息丢失,不能寻回;不能保证每个消费者接收的时间是一致的;若消费者客户端出现消息积压,到一定程度,会被强制断开,导致消息意外丢失。通常发生在消息的生产远大于消费速度时;可见,Pub/Sub模式不适合做消息存储,消息积压类的业务,而是擅长处理广播,即时通讯,即时反馈的业务。
基于Stream类型的实现
基本上已经有了一个消息中间件的雏形,可以考虑在生产过程中使用。
什么是bigkey?会有什么影响?
bigkey是指key对应的value所占的内存空间比较大,例如一个字符串类型的value可以最大存到512MB,一个列表类型的value最多可以存储23-1个元素。
如果按照数据结构来细分的话,一般分为字符串类型bigkey和非字符串类型bigkey。
字符串类型:体现在单个value值很大,一般认为超过10KB就是bigkey,但这个值和具体的OPS相关。
非字符串类型:哈希、列表、集合、有序集合,体现在元素个数过多。
bigkey无论是空间复杂度和时间复杂度都不太友好,下面我们将介绍它的危害。
bigkey的危害
bigkey的危害体现在三个方面:
- 内存空间不均匀.(平衡):例如在Redis Cluster中,bigkey会造成节点的内存空间使用不均匀。
- 超时阻塞:由于Redis单线程的特性,操作bigkey比较耗时,也就意味着阻塞Redis可能性增大。
- 网络拥塞:每次获取bigkey产生的网络流量较大。
假设一个bigkey为1MB,每秒访问量为1000,那么每秒产生1000MB的流量,对于普通的千兆网卡(按照字节算是128MB/s)的服务器来说简直是灭顶之灾,而且一般服务器会采用单机多实例的方式来部署,也就是说一个bigkey可能会对其他实例造成影响,其后果不堪设想。
Redis如何解决key冲突?
遇到hash冲突采用链表进行处理。
怎么提高缓存命中率?
需要在业务需求,缓存粒度,缓存策略,技术选型等各个方面去通盘考虑并做权衡。尽可能的聚焦在高频访问且时效性要求不高的热点业务上,通过缓存预加载(预热)、增加存储容量、调整缓存粒度、更新缓存等手段来提高命中率。
Redis持久化方式有哪些?有什么区别?
RDB、AOF、混合持久化。
RDB的优缺点:
优点:RDB持久化文件,速度比较快,而且存储的是一个二进制文件,传输起来很方便。
缺点:RDB无法保证数据的绝对安全,有时候就是1s也会有很大的数据丢失。
AOF的优缺点:
优点:AOF相对RDB更加安全,一般不会有数据的丢失或者很少,官方推荐同时开启AOF和RDB。
缺点:AOF持久化的速度,相对于RDB较慢,存储的是一个文本文件,到了后期文件会比较大,传输困难。
为什么Redis需要把所有数据放到内存中?
Redis为了达到最快的读写速度,将数据都读到内存中,并通过异步的方式将数据写入磁盘,所以Redis具有快速和数据持久化的特征。如果不将数据放在内存中,磁盘I/O速度为严重影响Redis的性能。
如何保证缓存与数据库双写时的数据一致性
第一种方案:采用延时双删策略
具体的步骤就是:
先删除缓存;
再写数据库;
休眠500毫秒;
再次删除缓存。
第二种方案:异步更新缓存(基于订阅binlog的同步机制)
技术整体思路:
MySQL binlog增量订阅消费+消息队列+增量数据更新到redis。
Redis集群方案应该怎么做?
- Redis Sentinel体量较小时,选择Redis Sentinel,单主Redis足以支撑业务。
- Redis ClusterRedis官方提供的集群化方案,体量较大时,选择Redis Cluster,通过分片,使用更多内存。
- TwemproxTwemprox是Twtter开源的一个Redis和Memcached代理服务器,主要用于管理Redis和Memcached集群,减少与Cache服务器直接连接的数量。
- CodisCodis是一个代理中间件,当客户端向Codis发送指令时,Codis负责将指令转发到后面的Redis来执行,并将结果返回给客户端。一个Codis实例可以连接多个Redis实例,也可以启动多个Codis实例来支撑,每个Codis节点都是对等的,这样可以增加整体的QPS需求,还能起到容灾功能。
- 客户端分片在Redis Cluster还没出现之前使用较多,现在基本很少热你使用了,在业务代码层实现,起几个毫无关联的Redis实例,在代码层,对Key进行hash计算,然后去对应的Redis实例操作数据。这种方式对hash层代码要求比较高,考虑部分包括,节点失效后的替代算法方案,数据震荡后的自动脚本恢复,实例的监控,等等。
Redis集群方案什么情况下会导致整个集群不可用?
- 当访问一个Master和Slave节点都挂了的槽的时候,会报槽无法获取。
- 当集群Master节点个数小于3个的时候,或者集群可用节点个数为偶数的时候,基于fail的这种选举机制的自动主从切换过程可能会不能正常工作,一个是标记fail的过程,一个是选举新的master的过程,都有可能异常。
说一说Redis哈希槽的概念?
slot:称为哈希槽
Redis集群中内置了16384个哈希槽,当需要在Redis集群中放置一个key-value时,redis先对key使用crc16算法算出一个结果,然后把结果对16384求余数,这样每个key都会对应一个编号在0-16383之间的哈希槽,redis会根据节点数量大致均等的将哈希槽映射到不同的节点。
使用哈希槽的好处就在于可以方便的添加或移除节点。
当需要增加节点时,只需要把其他节点的某些哈希槽挪到新节点就可以了;
当需要移除节点时,只需要把移除节点上的哈希槽挪到其他节点就行了;
Redis集群会有写操作丢失吗?为什么?
以下情况可能导致写操作丢失:
过期key被清理
最大内存不足,导致Redis自动清理部分key以节省空间
主库故障后自动重启,从库自动同步
单独的主备方案,网络不稳定触发哨兵的自动切换主从节点,切换期间会有数据丢失。
Redis常见性能问题和解决方案有哪些?
一、缓存穿透:就是查询一个压根就不存在的数据,即缓存中没有,数据库中也没有
解决方案:使用布隆过滤器,把数据先加载到布隆过滤器中,访问前先判断是否存在于布隆过滤器中,不存在代表这笔数据压根就不存在。
缺点:布隆过滤器是不可变的,可能一开始过滤器和数据库数据时一致的,后面数据库数据变了,或变多或变少,而对应的布隆过滤器的数据也要改变,这时会比较麻烦。
二、缓存击穿:数据库中有,缓存中没有。缓存击穿实际就是一个并发问题,一般来说查询数据,先查询缓存,有直接返回,没有再查询数据库并放到缓存中之后返回,但这种场景在并发情况下就会有问题,假设同时又100个请求执行上面逻辑的代码,则可能会出现多个请求都查询数据库,因为大家同时执行,都查到了缓存中没有数据。
解决方案:加锁。如果是单机部署,则可以使用JVM级别的锁,如lock、synchronized。如果是集群部署,则需要使用分布式锁,如基于redis、zookeeper、mysql等实现的分布式锁。
三、缓存雪崩:大部分数据同时失效、过期,新的缓存又没来,导致大量的请求都去访问数据库而导致的服务器压力过大、宕机、系统崩溃。
解决方案:搭建高可用的redis集群,避免压力集中于一个节点;缓存失效时间错开,避免缓存同时失效而都去请求数据库。
热点数据和冷数据是什么
对于冷数据而言,大部分数据可能还没有再次访问到就已经被挤出内存,不仅占用内存,而且价值不大。频繁修改的数据,看情况考虑使用缓存。对于上面两个例子,寿星列表、导航信息都存在一个特点,就是信息修改频率不高,读取通常非常高的场景。
对于热点数据,比如我们的某IM产品,生日祝福模块,当天的寿星列表,缓存以后可能读取数十万次。再举个例子,某导航产品,我们将导航信息,缓存以后可能读取数百万次。
什么情况下可能会导致Redis阻塞?
数据集中过期
不合理地使用API或数据结构
CPU饱和
持久化阻塞
什么时候选择Redis,什么时候选择Memcached?
实际业务分析
如果业务中更加侧重性能的高效性,对持久化要求不高,那么应该优先选择Memcached。
如果业务中对持久化有需求或者对数据涉及到存储、排序等一系列复杂的操作,比如业务中有排行榜类应用、社交关系存储、数据排重、实时配置等功能,那么应该优先选择Redis。
Redis过期策略都有哪些?LRU算法知道吗?
- noeviction:返回错误当内存限制达到,并且客户端尝试执行会让更多内存被使用的命令。
- allkeys-lru: 尝试回收最少使用的键(LRU),使得新添加的数据有空间存放。
- volatile-lru: 尝试回收最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放。
- allkeys-random: 回收随机的键使得新添加的数据有空间存放。
- volatile-random: 回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键。
- volatile-ttl: 回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放。
参考
- 《Redis 开发与运维》
- 《Redis 设计与实现》
- 《Redis 核心原理与实战》
- Redis 命令手册:https://www.redis.com.cn/commands.html
- RedisSearch 终极使用指南,你值得拥有!:https://mp.weixin.qq.com/s/FA4XVAXJksTOHUXMsayy2g
- WHY Redis choose single thread (vs multi threads): https://medium.com/@jychen7/sharing-redis-single-thread-vs-multi-threads-5870bd44d153