Redis 基础
什么是 Redis?
Redis (REmote DIctionary Server)是一个基于 C 语言开发的开源 NoSQL 数据库(BSD 许可)。与传统数据库不同的是,Redis 的数据是保存在内存中的(内存数据库,支持持久化),因此读写速度非常快,被广泛应用于分布式缓存方向。并且,Redis 存储的是 KV 键值对数据。
为了满足不同的业务场景,Redis 内置了多种数据类型实现(比如 String、Hash、Sorted Set、Bitmap、HyperLogLog、GEO)。并且,Redis 还支持事务、持久化、Lua 脚本、发布订阅模型、多种开箱即用的集群方案(Redis Sentinel、Redis Cluster)。
Redis 没有外部依赖,Linux 和 OS X 是 Redis 开发和测试最多的两个操作系统,官方推荐生产环境使用 Linux 部署 Redis。
Redis 为什么这么快?
Redis 内部做了非常多的性能优化,比较重要的有下面 4 点:
- 纯内存操作 (Memory-Based Storage) :这是最主要的原因。Redis 数据读写操作都发生在内存中,访问速度是纳秒级别,而传统数据库频繁读写磁盘的速度是毫秒级别,两者相差数个数量级。
- 高效的 I/O 模型 (I/O Multiplexing & Single-Threaded Event Loop) :Redis 使用单线程事件循环配合 I/O 多路复用技术,让单个线程可以同时处理多个网络连接上的 I/O 事件(如读写),避免了多线程模型中的上下文切换和锁竞争问题。虽然是单线程,但结合内存操作的高效性和 I/O 多路复用,使得 Redis 能轻松处理大量并发请求。
- 优化的内部数据结构 (Optimized Data Structures) :Redis 提供多种数据类型(如 String, List, Hash, Set, Sorted Set 等),其内部实现采用高度优化的编码方式(如 ziplist, quicklist, skiplist, hashtable 等)。Redis 会根据数据大小和类型动态选择最合适的内部编码,以在性能和空间效率之间取得最佳平衡。
- 简洁高效的通信协议 (Simple Protocol - RESP) :Redis 使用的是自己设计的 RESP (REdis Serialization Protocol) 协议。这个协议实现简单、解析性能好,并且是二进制安全的。客户端和服务端之间通信的序列化/反序列化开销很小,有助于提升整体的交互速度。
那既然都这么快了,为什么不直接用 Redis 当主数据库呢?主要是因为内存成本太高,并且 Redis 提供的数据持久化仍然有数据丢失的风险。
Redis 是单线程还是多线程?
Redis 在 6.0 版本之前是单线程的,从 6.0 版本开始引入了多线程。
Redis 6.0 之前为什么是单线程?
Redis 使用单线程的主要原因:
- 避免上下文切换开销:多线程环境下,线程间的上下文切换会带来 CPU 开销,影响性能
- 避免锁竞争:多线程需要加锁来保证数据一致性,锁竞争会降低性能
- 内存操作本身很快:Redis 的数据操作都在内存中完成,CPU 不是瓶颈,I/O 才是
- I/O 多路复用:使用 epoll、select 等 I/O 多路复用技术,单线程也能高效处理大量并发连接
Redis 6.0 为什么引入多线程?
Redis 6.0 引入多线程主要是为了提升网络 I/O 的读写性能,而不是为了处理命令执行。
- 主线程:仍然负责命令的执行
- I/O 线程:负责网络 I/O 的读写(解析请求、发送响应)
这样的设计既保持了命令执行的原子性(单线程执行),又提升了网络 I/O 的性能。
可以通过 io-threads 配置项开启多线程:
io-threads 4 # 开启 4 个 I/O 线程
除了 Redis,你还知道其他分布式缓存方案吗?
分布式缓存的话,比较老牌同时也是使用的比较多的还是 Memcached 和 Redis。不过,现在基本没有看过还有项目使用 Memcached 来做缓存,都是直接用 Redis。
Memcached 是分布式缓存最开始兴起的那会,比较常用的。后来,随着 Redis 的发展,大家慢慢都转而使用更加强大的 Redis 了。
有一些大厂也开源了类似于 Redis 的分布式高性能 KV 存储数据库,例如,腾讯开源的 Tendis。Tendis 基于知名开源项目 RocksDB 作为存储引擎 ,100% 兼容 Redis 协议和 Redis4.0 所有数据模型。
Redis 数据类型
Redis 有哪些数据类型?
Redis 提供了丰富的数据类型,常见的有:
- String(字符串):最基本的数据类型,可以存储字符串、整数或浮点数
- Hash(哈希):键值对集合,适合存储对象
- List(列表):字符串列表,按照插入顺序排序
- Set(集合):无序的字符串集合,不允许重复
- Sorted Set(有序集合):带分数的字符串集合,按分数排序
- Bitmap(位图):字符串的二进制操作,适合做统计
- HyperLogLog:用于基数统计(去重统计)
- GEO(地理位置):存储地理位置信息
Redis 内部数据结构
Redis 内部使用了多种数据结构来优化不同数据类型的存储:
- **SDS (Simple Dynamic String)**:用于字符串存储
- ziplist(压缩列表):用于小数据量的 List、Hash、Sorted Set
- quicklist:List 的内部实现,是 ziplist 和双向链表的结合
- skiplist(跳跃表):Sorted Set 的内部实现
- dict(哈希表):Hash 和 Set 的内部实现
- intset(整数集合):小整数 Set 的优化实现
Redis 持久化
Redis 持久化机制有哪些?
Redis 提供了两种持久化机制:
- RDB(Redis Database):快照持久化
- AOF(Append Only File):追加式持久化
RDB 持久化
RDB 是 Redis 的默认持久化方式,它会将某个时间点的数据快照保存到磁盘。
优点:
- 文件紧凑,适合做备份
- 恢复速度快
- 对 Redis 性能影响小
缺点:
- 可能会丢失最后一次快照后的数据
- 数据量大时,fork 子进程可能会阻塞主进程
触发方式:
- 手动执行
SAVE或BGSAVE命令 - 根据配置文件的规则自动触发
- 主从复制时,从节点接收到 RDB 文件
配置示例:
save 900 1 # 900秒内至少1个key发生变化
save 300 10 # 300秒内至少10个key发生变化
save 60 10000 # 60秒内至少10000个key发生变化
AOF 持久化
AOF 持久化通过记录每个写命令来持久化数据,类似于 MySQL 的 binlog。
优点:
- 数据更安全,最多丢失1秒数据(fsync everysec)
- 可读性好,便于分析
缺点:
- 文件体积大
- 恢复速度较慢
- AOF 重写时会占用较多资源
AOF 同步策略:
always:每次写入都同步,最安全但性能最低everysec:每秒同步一次,平衡安全性和性能(推荐)no:由操作系统决定何时同步,性能最好但可能丢失较多数据
配置示例:
appendonly yes
appendfsync everysec
RDB 和 AOF 如何选择?
- 生产环境建议两者都开启,RDB 用于备份,AOF 用于数据恢复
- 如果只选一个,优先选择 AOF(数据安全更重要)
- 混合持久化(Redis 4.0+):RDB 和 AOF 混合使用,兼顾速度和安全性
Redis 过期键删除策略
Redis 过期键的删除策略
Redis 采用了惰性删除 + 定期删除的组合策略:
-
惰性删除(Lazy Expiration):当访问一个 key 时,检查它是否过期,如果过期就删除。这种方式不会主动扫描,对 CPU 友好,但可能导致过期 key 长期占用内存。
-
定期删除(Periodic Expiration):Redis 会每隔一段时间(默认 100ms)随机抽取一些设置了过期时间的 key,检查并删除过期的 key。通过限制扫描的时长和频率,减少对 CPU 的影响。
大量 key 过期会导致什么问题?
如果过期 key 没有及时清理,可能会导致:
- 内存占用过高,甚至引发内存溢出(OOM)
- 过期 key 已经失效,但在 Redis 真正删除它们之前,仍然会占用内存空间
解决方案:
- 避免 key 集中过期:在设置键的过期时间时尽量随机一点
- 开启 lazy free 机制:修改
redis.conf,将lazyfree-lazy-expire设置为yes,Redis 会在后台异步删除过期的 key,不会阻塞主线程
Redis 内存淘汰策略
Redis 内存淘汰策略有哪些?
当 Redis 运行内存达到了配置的最大内存阈值时(maxmemory 参数),会触发内存淘汰。Redis 提供了 8 种内存淘汰策略:
- volatile-lru:从已设置过期时间的数据集中移除最近最少使用的数据
- volatile-lfu(4.0+):从已设置过期时间的数据集中移除最不经常使用的数据
- volatile-random:从已设置过期时间的数据集中随机移除数据
- volatile-ttl:从已设置过期时间的数据集中移除将要过期的数据
- allkeys-lru:从所有数据集中移除最近最少使用的数据(推荐)
- allkeys-lfu(4.0+):从所有数据集中移除最不经常使用的数据
- allkeys-random:从所有数据集中随机移除数据
- no-eviction(默认):禁止驱逐数据,当内存不足时,新写入操作会报错
allkeys-xxx 表示从所有的键值中淘汰数据,而 volatile-xxx 表示从设置了过期时间的键值中淘汰数据。
配置示例:
maxmemory 2gb
maxmemory-policy allkeys-lru
Redis 缓存问题
缓存穿透
问题描述: 大量请求查询数据库中不存在的数据,导致请求直接打到数据库上。
解决方案:
- 参数校验:对请求参数进行校验,过滤掉明显不合法的请求
- 缓存空值:如果查询结果为空,将空结果也缓存起来,并设置较短的过期时间
- 布隆过滤器:在缓存之前加一层布隆过滤器,判断数据是否存在,不存在直接返回
缓存击穿
问题描述: 热点数据过期,大量请求同时查询数据库,导致数据库压力骤增。
解决方案:
- 热点数据永不过期:设置热点数据不过期,或者在异步线程中更新
- 互斥锁:使用分布式锁,只允许一个线程查询数据库并更新缓存
- 多级缓存:本地缓存 + Redis 缓存
缓存雪崩
问题描述: 大量缓存同时过期,或者 Redis 服务宕机,导致大量请求直接打到数据库。
解决方案:
- 过期时间随机化:在设置过期时间时,加上随机值,避免大量 key 同时过期
- 热点数据永不过期:关键数据设置永不过期,或者定期异步更新
- 多级缓存:本地缓存 + Redis 缓存,提高可用性
- 限流降级:当大量请求打到数据库时,进行限流和降级处理
- Redis 高可用:使用 Redis 集群、主从复制等方案提高可用性
Redis 高可用方案
Redis 主从复制
工作原理:
- 从节点连接到主节点,发送 SYNC 命令
- 主节点执行 BGSAVE 生成 RDB 文件,并发送给从节点
- 主节点在生成 RDB 期间,将写命令记录到缓冲区
- RDB 文件发送完成后,主节点将缓冲区的命令发送给从节点
- 之后主节点的每个写命令都会异步发送给从节点
优点:
- 读写分离,提高读性能
- 数据备份,提高数据安全性
缺点:
- 主节点故障需要手动切换
- 主从延迟可能影响数据一致性
Redis 哨兵模式(Sentinel)
Redis Sentinel 是 Redis 的高可用解决方案,它可以监控主从节点的健康状态,并在主节点故障时自动进行故障转移。
功能:
- 监控:监控主从节点是否正常运行
- 通知:当被监控节点出现问题时,通知管理员
- 故障转移:主节点故障时,自动将从节点升级为主节点
- 配置提供者:客户端连接时,返回当前主节点地址
工作流程:
- 每个 Sentinel 节点都会向主节点、从节点和其他 Sentinel 节点发送 PING 命令
- 如果节点在
down-after-milliseconds时间内没有回复,Sentinel 会标记为主观下线(SDOWN) - 当足够多的 Sentinel 节点(超过配置的 quorum)都标记为主观下线时,标记为客观下线(ODOWN)
- 在 Sentinel 节点中选举出 Leader,由 Leader 进行故障转移
- 选择新的主节点,并通知其他从节点切换主节点
配置示例:
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 10000
Redis 集群(Cluster)
Redis Cluster 是 Redis 的分布式解决方案,通过分片(Sharding)将数据分布到多个节点。
特点:
- 无中心化:所有节点相互连接,节点之间使用 gossip 协议通信
- 数据分片:使用哈希槽(Hash Slot)将数据分布到 16384 个槽中
- 高可用:每个分片可以设置主从复制
- 故障转移:节点故障时自动进行故障转移
哈希槽分配:
- Redis 集群有 16384 个哈希槽(0-16383)
- 每个节点负责一部分哈希槽
- 客户端根据 key 计算 CRC16 值,然后对 16384 取模,得到哈希槽
优点:
- 水平扩展,可以动态添加节点
- 无单点故障
- 自动故障转移
缺点:
- 客户端需要支持集群模式
- 批量操作可能涉及多个节点(需要使用 hash tag)
- 事务和 Lua 脚本的 key 必须在同一个节点
Redis 事务
Redis 事务的特点
Redis 事务具有以下特点:
- 不保证原子性:Redis 事务中的命令如果出现错误,不会回滚已执行的命令
- 不支持回滚:Redis 不支持事务回滚(官方解释是为了性能考虑)
- 命令队列:事务中的命令会被放入队列,执行时一次性执行
事务命令:
MULTI:开启事务EXEC:执行事务DISCARD:取消事务WATCH:监控 key,如果事务执行前 key 被修改,事务会失败
示例:
MULTI
SET key1 value1
SET key2 value2
EXEC
Redis 事务为什么不支持回滚?
官方解释是:
- Redis 命令失败通常是由于编程错误,应该在开发阶段发现并修复
- 回滚机制会增加复杂度,影响 Redis 的简洁性和性能
- Redis 追求简单和速度,而不是严格的 ACID 特性
Redis 分布式锁
如何实现 Redis 分布式锁?
基本实现:
SET key value NX EX seconds
NX:只有当 key 不存在时才设置EX:设置过期时间
问题:
- 锁过期时间问题:如果业务执行时间超过锁的过期时间,锁会被其他进程获取
- 误删锁问题:进程 A 获取锁后,如果执行时间过长,锁过期后被进程 B 获取,进程 A 执行完后可能误删进程 B 的锁
解决方案:
- 使用 Lua 脚本保证原子性
- 设置锁的值(如 UUID),删除时验证值是否匹配
- 使用 Redlock 算法(需要多个独立的 Redis 实例)
Lua 脚本实现:
-- 获取锁
if redis.call("get", KEYS[1]) == false then
return redis.call("set", KEYS[1], ARGV[1], "EX", ARGV[2], "NX")
else
return false
end
-- 释放锁
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
其他常见问题
Redis 和 Memcached 的区别
| 特性 | Redis | Memcached |
|---|---|---|
| 数据类型 | 丰富(String、Hash、List、Set、Sorted Set 等) | 简单(仅 String) |
| 持久化 | 支持(RDB、AOF) | 不支持 |
| 集群模式 | 原生支持(Redis Cluster) | 需要客户端实现 |
| 事务 | 支持 | 不支持 |
| 过期策略 | 支持多种 | 支持 |
| 单线程 | 6.0 之前单线程,6.0+ 多线程 I/O | 多线程 |
| 内存管理 | 支持多种淘汰策略 | LRU |
| 适用场景 | 缓存、消息队列、计数器等 | 纯缓存 |
Redis 为什么使用跳跃表而不是平衡树?
Redis 的 Sorted Set 内部使用跳跃表(SkipList)而不是平衡树,主要原因:
- 实现简单:跳跃表的实现比平衡树简单
- 范围查询高效:跳跃表是链表结构,范围查询更高效
- 插入删除高效:跳跃表的插入删除操作更简单
- 内存占用:在某些场景下,跳跃表的内存占用更少
Redis 的 I/O 多路复用
Redis 使用 I/O 多路复用技术(如 epoll、select),使得单线程可以同时处理多个客户端连接。
原理:
- 将多个 I/O 操作注册到同一个线程的事件循环中
- 当某个 I/O 操作就绪时,操作系统会通知线程
- 线程处理就绪的 I/O 操作,然后继续等待下一个就绪事件
这样避免了为每个连接创建线程的开销,提高了并发性能。
总结
Redis 作为高性能的内存数据库,在分布式系统中扮演着重要角色。掌握 Redis 的核心概念、持久化机制、高可用方案和缓存问题的解决方案,对于系统设计和面试都非常重要。
在实际项目中,需要根据业务场景选择合适的持久化策略、内存淘汰策略和高可用方案,同时要注意缓存穿透、缓存击穿、缓存雪崩等问题的预防和处理。
参考资料:
Originally published on mlangTse's Blog. View source