Redis面试题
Redis 数据结构整理
1. String(字符串)
- 类型:简单的 key-value 结构,支持二进制存储,最大 512MB。
- 底层实现:
- 短字符串(≤39字节):
SDS
(简单动态字符串)。 - 长字符串:
embstr
(短)或raw
(长)。
- 短字符串(≤39字节):
- 应用场景:
- 缓存、计数器、分布式锁(SET NX PX)。
2. Hash(哈希表)
- 类型:存储键值对的集合,类似
Map<String, String>
。 - 底层实现:
- 小数据量(字段少、值小):压缩列表(ziplist),节省内存。
- 大数据量:哈希表(hashtable),O(1) 查询。
- 应用场景:
- 存储对象(如用户信息),减少 key 数量。
3. List(列表)
- 类型:双向链表,可当作栈或队列使用。
- 底层实现:
- 短列表(元素小于64字节,总数少于512):压缩列表(ziplist),占用更少内存。
- 长列表:双向链表(linkedlist),支持 O(1) 头尾操作。
- 应用场景:
- 消息队列、任务队列、时间轴(微博)。
4. Set(集合)
- 类型:无序、唯一元素集合,支持集合运算。
- 底层实现:
- 元素个数少(≤512)且每个元素≤64字节:整数数组(intset),节省空间。
- 否则:哈希表(hashtable),O(1) 查询。
- 应用场景:
- 黑名单、唯一值存储、好友推荐(交集)。
5. Sorted Set(有序集合,ZSet)
- 类型:Set 的变体,每个元素有一个
score
进行排序。 - 底层实现:
- 小数据量:压缩列表(ziplist)。
- 大数据量:跳表(skiplist),O(logN) 查询。
- 应用场景:
- 排行榜、延时队列、任务调度。
6. HyperLogLog(基数统计)
- 类型:基于概率的去重计数,误差 0.81%。
- 底层实现:
- 基于 稀疏矩阵 + 线性计数 进行基数估计,节省存储(12KB 统计亿级数据)。
- 应用场景:
- 统计 UV、去重 ID。
7. Bitmaps(位图)
- 类型:二进制位存储数据,可按位计算。
- 底层实现:
- 直接基于 String 存储二进制位,1 字节存 8 个状态位。
- 应用场景:
- 签到、活跃用户、布隆过滤器(结合 Bitmaps + 哈希)。
8. Geospatial(地理位置)
- 类型:存储经纬度,可计算两点间距离和范围查询。
- 底层实现:
- ZSet(跳表)+ GeoHash 编码 实现存储与查询。
- 应用场景:
- LBS(最近商家、附近好友)。
HyperLogLog(基数统计)完善与补充
1. 什么是基数?
- 基数(Cardinality) 是指集合中 不重复元素 的个数。
- 例如:
- A = {1, 2, 3, 4, 5},B = {3, 5, 6, 7, 9}
- 基数 = 1, 2, 4, 6, 7, 9(去重后共有 6 个元素)
2. HyperLogLog 解决了什么问题?
- 适用于 海量数据 的去重计数问题,例如:
- 注册 IP 数
- 每日访问 IP 数(UV 统计)
- 在线用户数
- 共同好友数
- 核心特点:
- 计算近似 不重复元素个数,允许误差(标准误差 0.81%)。
- 占用内存固定,仅需 12KB 即可统计 亿级数据。
- 适合 高并发、大规模数据 场景,不适合 小数据量高精度统计。
3. HyperLogLog 工作原理
HyperLogLog 使用 概率算法 进行基数估算,主要原理如下:
- 哈希映射:将元素哈希为二进制值,例如
hash(“A”) = 110100…
。 - 前导零统计:找出哈希值二进制表示中最左侧的连续零的个数
k
。 - 存储最大
k
:多个哈希桶存储不同元素的最大k
值。 - 估算基数:基于 哈希桶的均值 + 统计学公式 计算基数。
HyperLogLog 牺牲精度换取超高的存储效率,是大数据统计中的重要技术之一。
4. HyperLogLog vs 其他计数方式
方法 | 是否去重 | 是否精确 | 空间占用 | 适用场景 |
---|---|---|---|---|
HashSet | ✅ 是 | ✅ 是 | O(n) | 小规模数据 |
Bitmap | ✅ 是 | ✅ 是 | O(n/8) | 固定范围数据(如用户ID) |
HyperLogLog | ✅ 是 | ❌ 近似 | O(12KB) | 超大规模数据 |
5. HyperLogLog 的应用场景
- 统计 UV(独立访问用户数):
- 适用于海量访问网站,如统计日活 DAU(Daily Active Users)。
- 日志分析:
- 统计不重复 IP、独立用户 ID 等数据。
- 搜索引擎:
- 统计搜索关键词总数(去重统计)。
- 社交平台:
- 计算共同好友数、活跃用户数。
- 广告反作弊:
- 统计唯一点击广告用户数,防止刷量。
6. HyperLogLog 的局限性
- 不能获取去重后的具体元素,仅统计基数。
- 误差 0.81%,不适合对精度要求极高的场景。
- 仅支持集合合并(PFMERGE),不能删除单个元素。
总结
HyperLogLog 是 Redis 提供的 高效基数统计工具,用于近似去重计数,适用于大数据量但对精度要求不高的场景。它的固定存储占用(12KB)使其在高并发、海量数据环境中表现优异。
Bitmap(位图)完善与补充
1. 什么是 Bitmap?
- Bitmap(位图)是一种使用 二进制位(0 或 1)表示集合中元素的方法。
- 每个元素只占用 1 bit,相比 HashSet 或 List,大幅减少存储空间。
2. 位图的存储方式
- Bitmap 基于 Redis String 类型,但按 位(bit) 操作。
- 每个 bit 仅存储
0
或1
,可用于表示二元状态(如在线/离线)。 - 1 字节(Byte)= 8 个 bit,因此:
1000
个用户的状态仅需1000 / 8 = 125 字节
。
3. Bitmap 的应用场景
- 用户状态存储
1
代表 在线,0
代表 离线。- 应用示例:
- 统计 7 天内活跃用户(每日 1 bit)。
- 标记 用户是否完成某个任务。
- 签到系统(连续签到统计)
- 每天 1 bit,
1
代表签到,0
代表未签到。 - 可以快速计算 连续签到天数、缺勤次数。
- 每天 1 bit,
- 访问记录(UV 统计)
- 统计 某天是否访问过某个页面(如
1
代表访问过)。
- 统计 某天是否访问过某个页面(如
- 布隆过滤器(Bloom Filter)
- 位图可用于 快速判断元素是否存在,提高查询效率。
4. Bitmap vs 其他数据结构
数据结构 | 占用空间 | 适用场景 |
---|---|---|
HashSet | 大 | 适用于小规模数据 |
BitSet | 中 | JVM 内存中的位存储 |
Bitmap | 小 | 适用于海量二元状态存储 |
5. Bitmap 的优缺点
✅ 优点:
- 节省空间:每个 bit 仅占 1/8 字节,适用于海量数据存储。
- 支持快速查询:位操作时间复杂度 O(1)。
- 适用于二元状态:签到、活跃、访问记录等。
❌ 缺点:
- 不支持直接删除元素(只能手动清 0)。
- 仅适用于固定范围的数据(如
用户ID
连续时)。 - 无法存储非 0/1 状态(如评分、积分等需用更复杂结构)。
总结
Bitmap 适用于 存储二元状态的海量数据(如活跃用户、签到、访问记录等),极大节省存储空间,在高效数据分析和统计场景中广泛应用。
Geospatial(地理位置)完善与补充
1. 什么是 Geospatial(地理位置)?
- Redis 3.2 版本引入,用于存储地理位置(经纬度)并执行地理计算。
- 核心功能:
- 存储 经纬度坐标(longitude, latitude)。
- 计算 两点之间的距离。
- 进行 范围查询(如查找附近的人或店铺)。
2. Geospatial 底层实现
- 数据结构:ZSet(跳表)+ GeoHash 编码
GeoHash
将 经纬度 编码成 一个字符串,再存入 Sorted Set。score
代表 地理位置的 GeoHash 值,可以快速范围查找。- ZSet 的特性使得 查询附近位置的效率极高(O(logN))。
3. Geospatial 的应用场景
✅ 1. 查找附近的人(LBS 位置服务)
- 共享单车、外卖、打车软件等应用。
- 例如:查找 3 公里内的骑手,5 公里内的餐厅。
✅ 2. 物流 & 路线规划
- 计算 两个城市之间的距离,评估 最优路线。
✅ 3. 线下门店推荐
- 例如:查找 10 公里范围内的加油站、商店、电影院。
✅ 4. 位置打卡 & 地理围栏
- 例如:用户进入某区域(商场、景点)后触发特定行为。
4. Geospatial 主要命令(示例)
🔹 添加地理位置(GEOADD)
GEOADD china:city 121.4737 31.2304 "shanghai" 120.5853 31.2989 "suzhou"
✅ 存储多个城市的经纬度。
🔹 计算两地之间的距离(GEODIST)
GEODIST china:city suzhou shanghai km
✅ 计算苏州与上海之间的距离,单位 km(可用 m、mi、ft)。
🔹 查找某范围内的地点(GEORADIUS – 已废弃,使用 GEOSEARCH)
GEORADIUS china:city 121.49295 31.22337 30 km
✅ 查找 以(121.49295, 31.22337)为中心,半径 30 km 内的地点。
✅ Redis 6.2 之后推荐使用 GEOSEARCH
,支持更丰富的查询方式。
🔹 返回某个地点的经纬度(GEOPOS)
GEOPOS china:city shanghai
✅ 获取 上海的经纬度(可用于地图展示)。
🔹 返回 GeoHash 编码(GEOHASH)
GEOHASH china:city shanghai
✅ 获取 上海的 GeoHash 编码,用于快速查询相邻区域。
5. Geospatial 优缺点
✅ 优点:
- 查询效率高:基于 ZSet + GeoHash,查询复杂度 O(logN)。
- 存储紧凑:相比传统数据库地理计算,Redis 占用更少。
- 支持多个单位(m/km/mi/ft),适用于不同需求。
❌ 缺点:
- 不支持路线规划(仅计算两点直线距离,无法考虑道路)。
- GeoHash 存储精度有限(误差随 GeoHash 长度变化)。
总结
Redis Geospatial 适用于 LBS(基于位置的服务),可实现 附近查找、距离计算、位置存储。
它 基于 ZSet + GeoHash,查询 高效、存储紧凑,适用于 共享单车、外卖、地图导航 等应用。
Redis 缓存穿透
缓存穿透 指的是:用户查询数据库中不存在的数据,缓存也不会存储这些无效查询,导致每次查询都需要访问数据库,进而造成 数据库压力过大。
解决方案
方案一:缓存空值(NULL 缓存)
原理:
- 如果数据库查询结果为空(
null
),仍然将null
结果存入 Redis,并设置短期过期时间(如 5~60 分钟)。 - 下次查询时,直接返回
null
,避免访问数据库。
优点:
✅ 实现简单,无需额外组件。
✅ 适用于业务中查询 key 较为固定的场景。
缺点:
❌ 占用额外缓存,但可以通过短期过期时间控制内存开销。
❌ 可能引发缓存与数据库不一致,可通过数据库更新时删除缓存来缓解。
方案二:布隆过滤器(Bloom Filter)
原理:
- 预加载数据库中的 key 到 布隆过滤器(Bloom Filter)。
- 查询时,先在布隆过滤器中判断 key 是否可能存在:
- 不存在 → 直接返回,避免查询数据库。
- 可能存在 → 继续查询 Redis 或数据库。
优点:
✅ 占用内存极少,适用于大规模数据过滤。
✅ 无需存储无效数据,不会污染 Redis。
缺点:
❌ 存在 误判(可能会认为某个不存在的 key 存在)。
❌ 删除困难(传统布隆过滤器无法移除元素,但可用计数布隆过滤器(Counting Bloom Filter)解决)。
方案三:限流与黑名单
原理:
- 对恶意 IP 进行封禁,防止爬虫或攻击请求大量无效 key。
- 限流策略(Rate Limiting):
- 固定窗口限流(如 10s 内最多允许 10 次请求)。
- 令牌桶算法(控制请求速率,防止短时间内突发流量)。
优点:
✅ 可减少恶意流量对数据库的压力。
✅ 适用于爬虫拦截、恶意用户封禁等场景。
缺点:
❌ 需要额外开发限流逻辑,不适用于业务侧的正常查询。
方案四:数据预加载(Warm Up)
原理:
- 将热门 key 预加载到 Redis,避免初次查询缓存未命中。
- 数据库新增数据时,主动更新 Redis。
优点:
✅ 适用于热点数据,减少首次查询的数据库压力。
✅ 可与缓存淘汰策略(LFU/LRU)结合,提高命中率。
缺点:
❌ 只适用于 热点数据,对随机查询作用不大。
3. 方案对比
方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
缓存空值 | 实现简单,快速响应 | 占用内存,可能数据不一致 | 业务 key 数量有限,更新频率低 |
布隆过滤器 | 节省内存,无需存储无效 key | 误判问题,删除困难 | 适用于大规模数据 |
限流+黑名单 | 可减少恶意流量 | 无法根本解决缓存穿透 | 适用于防爬虫、恶意攻击 |
数据预加载 | 提高缓存命中率 | 仅适用于热点数据 | 适用于固定范围的查询 |
4. 总结
- 对于少量无效查询,可以 缓存空值 解决。
- 对于 海量随机查询,建议使用 布隆过滤器 过滤掉不存在的 key。
- 对于恶意攻击,可以 限流+黑名单 机制拦截。
- 对于热点数据,可以 预加载,提升缓存命中率。
通常,综合使用多种方案 能够更好地防止 Redis 缓存穿透 问题。
Redis 缓存击穿
1. 什么是缓存击穿?
缓存击穿指的是:一个热点 key 过期的瞬间,大量并发请求同时查询该 key,导致请求全部打到数据库,瞬间压垮数据库。
2. 解决方案
方案一:互斥锁(setnx)
原理:
- 当多个线程同时查询 Redis,发现 key 过期时,只有一个线程可以获取互斥锁,其他线程等待。
- 线程 1 查询数据库,并将数据写回 Redis。
- 其他线程稍后重新查询缓存,避免大量请求同时访问数据库。
优缺点:
✅ 优点:
- 无额外的内存消耗,保证数据一致性。
- 逻辑清晰,实现简单。
❌ 缺点:
- 线程等待锁,影响性能。
- 需要注意 死锁 问题(可用
setnx
+expire
机制避免)。
方案二:逻辑过期(Lazy Rebuild)
原理:
- 不让 Redis 的 key 物理过期,而是为 key 添加逻辑过期时间。
- 每次查询 Redis 时,若发现逻辑时间已过期,则由后台线程异步更新缓存,但仍然返回旧数据。
实现方式:在SpringBoot框架里自然可以配置
优缺点:
✅ 优点:
- 请求无阻塞,查询即返回旧值,不影响用户体验。
- 减少数据库压力,由后台异步更新数据。
❌ 缺点:
- 数据可能不一致(查询可能返回旧数据)。
- 额外的 Redis 内存消耗(存储过期时间信息)。
- 实现复杂度较高,需要后台线程维护更新任务。
3. 方案对比
方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
互斥锁(setnx) | 确保一致性,简单易懂 | 可能阻塞请求,影响性能 | 适用于数据一致性要求高的场景 |
逻辑过期(Lazy Rebuild) | 低延迟,不阻塞 | 可能返回旧数据,实现复杂 | 适用于热点数据、更新频率不高的场景 |
4. 总结
- 互斥锁 适用于数据一致性要求高的业务(如订单、支付等)。
- 逻辑过期 适用于热点数据,可降低数据库压力,提高查询性能。
- 两者可以结合:热点数据使用逻辑过期,非热点数据使用互斥锁。
Redis 缓存雪崩
1. 什么是缓存雪崩?
缓存雪崩指的是:在同一时刻,大量缓存 key 同时失效,或者 Redis 整体宕机,导致数据库瞬间承受巨量请求,可能直接崩溃。
常见触发场景:
- 大量 key 设置了相同 TTL,导致它们同时过期。
- Redis 宕机,所有缓存数据瞬间失效。
- 流量突增,导致缓存未命中,请求全打到数据库。
2. 解决方案
方案一:为 key 设置不同 TTL,避免同时失效
原理:
- 给 key 设定随机过期时间,避免大量 key 同时过期。
实现:
int expireTime = 60 + new Random().nextInt(30); // 60~90 秒随机过期
redis.setex("key", expireTime, value);
✅ 优点:实现简单,有效缓解缓存雪崩。
❌ 缺点:适用于缓存过期导致的雪崩,但无法解决 Redis 宕机的问题。
方案二:使用 Redis 集群,分散请求压力
原理:
- 采用 主从架构 或 分片集群,让多个 Redis 实例共同分担压力,避免单点故障。
实现方式:
- 主从架构(Replication): 读写分离,Redis 只读实例承载查询请求。
- 分片集群(Cluster): 把数据分布到多个 Redis 节点,减少单个节点压力。
✅ 优点:保证 Redis 高可用,即使部分节点故障,也能继续提供服务。
❌ 缺点:增加系统复杂性,需要额外运维成本。
方案三:添加熔断、限流、降级机制
原理:
- 通过熔断、限流、降级等手段,防止数据库被击穿。
实现方式:
- 限流(如 Redis + Guava RateLimiter)
- 限制高并发请求,防止短时间内请求暴增。
- 熔断(如 Sentinel、Hystrix)
- 当数据库压力过大时,直接返回默认数据,避免拖垮系统。
- 降级
- 若 Redis 和数据库都不可用,则返回默认数据或错误提示。
✅ 优点:防止数据库直接崩溃,保证系统可用性。
❌ 缺点:部分用户可能获取不到最新数据,体验受影响。
方案四:使用多级缓存(L1 + L2)
原理:
- L1(本地缓存,Caffeine/Guava) + L2(Redis 缓存),分层存储数据,减少 Redis 依赖。
实现方式:
- 本地缓存(Caffeine/Guava):存储热点数据,降低 Redis 压力。
- Redis 作为二级缓存,存储大部分数据。
- Redis 失效时,本地缓存仍可提供部分数据。
✅ 优点:提升访问速度,降低对 Redis 依赖,减少雪崩影响。
❌ 缺点:本地缓存占用应用服务器内存,适用于热点数据。
3. 方案对比
方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
不同 TTL | 实现简单,避免缓存同时失效 | 不能解决 Redis 宕机问题 | 大量 key 过期时 |
Redis 集群 | 保证高可用,减少单点故障 | 复杂度高,增加运维成本 | 大规模分布式缓存 |
熔断 + 限流 + 降级 | 防止数据库被拖垮 | 可能影响部分用户体验 | 高并发场景 |
多级缓存(L1 + L2) | 降低 Redis 依赖,提高性能 | 额外占用服务器内存 | 热点数据 |
4. 总结
- 如果是大量 key 同时过期引发雪崩 → TTL 随机化。
- 如果 Redis 整体宕机 → 采用 Redis 集群。
- 如果流量过大导致雪崩 → 熔断 + 限流 + 降级。
- 如果需要进一步优化 → 采用多级缓存。
项目中为什么要用 Redis?
1. 高性能(读写快)
- 数据存储在内存,读取速度远远快于 磁盘数据库(如 MySQL)。
- 示例:
- 第一次访问数据:从 数据库 读取(慢)。
- 后续访问:直接从 Redis 缓存 获取(快)。
- 数据库更新 时,同步修改缓存,保证一致性。
对比数据库查询:
存储方式 | 访问速度 |
---|---|
MySQL(磁盘) | 毫秒级(ms) |
Redis(内存) | 微秒级(μs),比数据库快 100 倍 |
2. 高并发(减轻数据库压力)
- 直接访问缓存,避免 数据库高并发查询,提升系统吞吐量。
- 应用场景:
- 热点数据(如商品详情页、热门文章)。
- 排行榜(访问量大的数据不必每次查询数据库)。
3. 支持多种数据结构(灵活应用)
- String:用户信息、计数器。
- Hash:存储对象(如用户信息:id、name、email)。
- List:消息队列、社交动态(微博、朋友圈)。
- Set:标签、推荐系统(去重)。
- Sorted Set:排行榜、积分系统。
4. 分布式锁(保证并发安全)
- 使用
SET NX EX
实现分布式锁,防止多个线程同时修改同一资源。 - 应用场景:
- 秒杀系统(防止超卖)。
- 任务调度(防止任务重复执行)。
5. 过期策略(自动清理数据)
- 支持 TTL(Time to Live),可自动删除过期数据。
- 应用场景:
- 限时优惠券(10 分钟后自动失效)。
- 用户登录状态(Session 过期)。
6. 消息队列(提高系统解耦能力)
- Redis Pub/Sub 或 List 可用作 消息队列,用于异步处理任务。
- 应用场景:
- 订单处理:用户下单后,异步通知库存系统。
- 日志收集:大数据分析系统。
总结
✅ 高性能(内存读写,速度快)
✅ 高并发(缓解数据库压力)
✅ 数据结构丰富(适配不同场景)
✅ 分布式锁(保证并发安全)
✅ 消息队列(提升系统解耦)
Redis 适用于 缓存、分布式锁、排行榜、队列、限流 等高性能场景,在项目中发挥重要作用!
Redis 为什么快?
✅ (1)基于内存(无磁盘 I/O 开销)
- Redis 直接 在内存中读写,无需像 MySQL 进行磁盘读写,速度快 100 倍+。
✅ (2)单线程,避免 CPU 线程切换开销
- 无锁争抢,所有请求按 FIFO(先进先出) 执行,避免 死锁。
✅ (3)I/O 多路复用(非阻塞高效处理请求)
- epoll + 事件驱动,Redis 单线程也能处理大量连接,高效利用 CPU。
✅ (4)优化的数据结构
- 跳表(Sorted Set)、哈希表(Hash)、压缩列表(List)、整数集合(Set),内存占用低,访问速度快。
✅ (5)Pipeline 机制(减少 RTT 开销)
- 支持批量命令执行,减少 TCP 往返延迟(RTT),提高吞吐量。
✅ (6)高效的持久化机制(AOF + RDB)
- AOF 采用增量日志,防止数据丢失。
- RDB 定期快照,减轻内存负担,恢复速度快。
总结
🔹 Redis 为什么快:纯内存操作、单线程无锁、I/O 多路复用、优化数据结构、Pipeline 批处理、高效持久化。
阻塞 I/O(Blocking I/O) vs 非阻塞 I/O(Non-blocking I/O) vs 异步 I/O(Asynchronous I/O)
在计算机网络和操作系统中,I/O(输入输出)是非常关键的概念。不同的 I/O 模型会影响系统的并发性能和资源使用效率。下面我们详细对比 阻塞 I/O、非阻塞 I/O 和 异步 I/O 三种模式的区别和特点。
1. 阻塞 I/O(Blocking I/O)
定义:
- 在 阻塞 I/O 模型中,当一个线程发起 I/O 操作(如读取文件或网络数据)时,它会 阻塞当前线程,直到操作完成后才返回。也就是说,线程在发起 I/O 操作时会 等待,直到操作完成(例如,读取完数据或接收到网络响应)。
特点:
- 阻塞等待:当线程发起 I/O 操作时,必须等待操作完成才能继续执行后续代码。线程被阻塞在 I/O 操作上。
- 简单易懂:编程模型简单,线程直接发起 I/O 操作并等待结果返回,程序流程自然。
- 资源消耗:每个 I/O 操作都占用一个线程,并且线程处于阻塞状态,不能做其他任务。对于大量 I/O 请求时,系统性能容易受到影响,线程池可能迅速耗尽。
适用场景:
- 低并发、低负载:对于 I/O 请求较少的场景,阻塞 I/O 足够使用。
- 简单应用:如果系统负载不高且不需要处理高并发请求,使用阻塞 I/O 足够。
缺点:
- 资源浪费:线程在等待 I/O 完成时无法进行其他任务,浪费 CPU 资源。
- 高并发性能差:当并发请求增加时,阻塞 I/O 导致线程过多,从而增加上下文切换和资源消耗,性能下降。
示意:
线程 -> 发起 I/O 请求 -> 等待 I/O 完成 -> 继续执行后续代码
2. 非阻塞 I/O(Non-blocking I/O)
定义:
- 非阻塞 I/O 模型指的是,当线程发起 I/O 操作时,如果数据 没有准备好(例如文件或网络数据还没有到达),线程不会被阻塞,立即返回一个错误或者状态指示,线程可以继续执行其他任务。当数据准备好时,线程可以重新检查或再次发起 I/O 操作。
特点:
- 立即返回:线程发起 I/O 操作后,如果操作不能立即完成(如数据不可用),线程会返回一个状态信息(通常是 EAGAIN 错误),表示无法完成当前操作。
- 不阻塞线程:线程不会被阻塞,而是返回继续执行其他任务,直到数据可用。
- 轮询或事件驱动:线程通常需要使用轮询(不断检查是否有数据)或者通过事件驱动来管理 I/O 操作的完成。
适用场景:
- 高并发、低延迟要求:非阻塞 I/O 适合处理大量并发请求(如 Web 服务器、游戏服务器),能避免线程因为等待 I/O 操作而被阻塞。
缺点:
- 复杂性高:程序需要不断检查 I/O 操作的状态,编程模型复杂,代码通常需要使用多线程或事件驱动模型(如 epoll、select)来处理。
- CPU 消耗:如果程序使用轮询来检查 I/O 状态(如不断查询数据是否准备好),可能会浪费 CPU 时间。
示意:
线程 -> 发起 I/O 请求 -> 数据未准备好 -> 返回(不阻塞)-> 继续执行其他代码 -> 再次发起请求,直到数据准备好
3. 异步 I/O(Asynchronous I/O)
定义:
- 在 异步 I/O 模型中,线程发起 I/O 操作后,立即返回,不等待操作完成,而是将 I/O 操作交给操作系统或内核去处理。等到 I/O 操作完成后,操作系统会通知线程或回调函数,以便线程可以处理结果。这意味着线程 不需要等待 操作完成,可以执行其他任务。
特点:
- 操作系统处理:I/O 操作是由操作系统或内核线程处理的,线程并不需要等待操作完成,可以继续处理其他任务。
- 回调机制:线程发起 I/O 操作后,操作系统会在 I/O 完成时通知线程,通常通过 回调函数 或 事件通知 来触发后续操作。
- 极低的资源消耗:系统通过异步处理 I/O,可以显著减少对线程的需求,避免了阻塞和轮询操作,节省了大量资源。
适用场景:
- 极高并发、高负载:异步 I/O 非常适合高并发、高性能的系统,如大型 Web 服务、数据流处理等场景。
缺点:
- 编程复杂度高:编写异步 I/O 程序通常比阻塞或非阻塞 I/O 更复杂,需要理解回调、事件通知等机制。错误处理和状态管理也更加繁琐。
- 依赖操作系统:实现异步 I/O 通常依赖操作系统提供的异步 I/O 支持,这在不同平台之间可能存在差异。
示意:
线程 -> 发起 I/O 请求 -> 立即返回 -> 继续执行其他代码 -> 操作系统处理 I/O -> I/O 完成后触发回调函数
4. 阻塞 I/O、非阻塞 I/O、异步 I/O 的比较
特性 | 阻塞 I/O | 非阻塞 I/O | 异步 I/O |
---|---|---|---|
线程行为 | 线程等待 I/O 完成 | 线程立即返回,继续执行 | 线程立即返回,I/O 操作由内核处理 |
程序模型 | 简单,线性执行 | 复杂,需要轮询或事件驱动 | 复杂,依赖回调或事件通知 |
CPU 使用 | I/O 操作期间阻塞,浪费 CPU | I/O 操作期间不阻塞,但可能会轮询,浪费 CPU | 无需轮询,由内核管理,CPU 使用高效 |
适用场景 | 简单应用、低并发 | 高并发、实时系统 | 高并发、大量请求的高效系统 |
性能 | 对于低并发较好,但高并发性能差 | 性能高,但编程复杂 | 极高性能,能处理海量并发 |
编程复杂度 | 低 | 高,需要轮询或事件驱动 | 高,需要回调或事件通知 |
总结
- 阻塞 I/O:线程在 I/O 操作上阻塞,适合简单的应用或低并发场景。
- 非阻塞 I/O:线程立即返回,适合高并发场景,但编程复杂度较高,需要使用轮询或事件驱动模型。
- 异步 I/O:线程无需等待 I/O 操作完成,由操作系统处理,适合处理大量并发请求的高效系统,但编程复杂度较高。
每种 I/O 模型都有其适用的场景,选择合适的 I/O 模型可以根据系统的需求来优化性能和资源利用。
Redis 的持久化方式:RDB 和 AOF
Redis 提供了两种持久化机制来保证数据在断电、重启或其他故障后能够恢复,分别是 RDB(Redis DataBase) 和 AOF(Append Only File)。它们的原理、优势和适用场景有所不同,可以根据不同的需求选择合适的持久化方式,甚至可以同时启用。
1. RDB(Redis DataBase)持久化
原理:
- RDB 持久化的核心思想是 定期生成数据快照。它会在设定的时间间隔内,将 Redis 内存中的所有数据保存到磁盘中,生成一个 RDB 文件(通常命名为
dump.rdb
)。 - 当 Redis 重启时,它会加载 RDB 文件中的数据来恢复内存中的状态。
特点:
- 快照式持久化:RDB 会在指定时间间隔内,按照某个条件(如一定数量的写操作或指定的时间)将数据存储为一个快照。
- 性能较好:由于 RDB 是在后台进行快照操作的,它对 Redis 主进程的性能影响较小,适合处理大规模的 Redis 实例。
- 数据丢失风险:RDB 生成的快照是一个时间点的快照,如果 Redis 在上一次快照后崩溃,可能会丢失这期间的数据(具体丢失量取决于配置的保存频率)。
优点:
- 更快的恢复速度:RDB 文件通常比较小,而且 Redis 重启时从 RDB 文件加载数据的速度较快。
- 节省资源:RDB 持久化不需要每个写操作都记录,而是定期进行快照,节省了 I/O 资源。
- 适合备份:RDB 文件可以用作备份,便于在生产环境中进行定期的数据备份。
缺点:
- 数据丢失:如果 Redis 崩溃且 RDB 没有生成快照或快照未完成,可能会丢失大量数据,尤其是在高并发场景下。
- 不适合实时性要求高的应用:由于 RDB 生成快照有一定的延迟,因此不适用于对数据持久化要求非常实时的应用场景。
RDB 配置:
通过配置文件(redis.conf
)中的 save
参数来设置快照的生成条件。例如:
save 900 1 # 900秒内至少有 1 次写操作,则生成快照
save 300 10 # 300秒内至少有 10 次写操作,则生成快照
save 60 10000 # 60秒内至少有 10000 次写操作,则生成快照
2. AOF(Append Only File)持久化
原理:
- AOF 持久化的核心思想是将 Redis 执行的 所有写命令都 追加到 AOF 文件(通常命名为
appendonly.aof
)中。每次 Redis 执行写操作时,会将该操作记录到 AOF 文件中。 - Redis 重启时,会重新执行 AOF 文件中的所有命令,从而恢复数据。
特点:
- 日志式持久化:AOF 会记录 Redis 执行的所有写命令,并且这些命令会按顺序追加到文件末尾。
- 较慢的性能:由于每个写操作都需要记录到 AOF 文件,因此对性能有一定的影响,尤其是在高频写入的场景中。
- 数据安全性更高:AOF 可以确保数据的持久化完整性,理论上不易丢失数据,特别是在通过设置合适的写入策略时。
优点:
- 高数据安全性:AOF 文件可以记录所有写操作,可以确保数据恢复的完整性,适用于要求较高的数据一致性。
- 更精确的持久化:AOF 持久化方式记录的是每个写操作,因此可以非常精确地恢复 Redis 数据。
- 支持重写机制:随着 AOF 文件不断增长,Redis 提供了 AOF 重写功能,能够定期压缩 AOF 文件,减小文件体积。
缺点:
- 性能开销:每次写操作都会被记录到 AOF 文件中,这会影响 Redis 的性能,尤其是在高写入负载的环境下。
- AOF 文件大小:随着时间推移,AOF 文件会逐渐增大,可能需要通过重写机制定期清理。
- 恢复速度较慢:相比 RDB 文件,AOF 恢复数据的速度较慢,因为需要依次执行文件中的所有命令。
AOF 配置:
可以通过 appendonly
参数启用 AOF 持久化:
appendonly yes # 启用 AOF 持久化
appendfsync everysec # 每秒同步一次
appendfsync
配置有三种选择:
- always:每个写操作都同步到 AOF 文件(性能最差,但数据最安全)。
- everysec:每秒同步一次 AOF 文件(常用配置,性能与安全平衡)。
- no:不进行同步,由操作系统控制(性能最好,但数据安全性最低)。
3. RDB 和 AOF 的组合使用
Redis 允许同时启用 RDB 和 AOF,这种组合使用方式可以充分利用两种持久化方式的优点。
- 数据恢复优先使用 AOF:在 Redis 重启时,会优先使用 AOF 文件来恢复数据,因为 AOF 文件能够保证更高的恢复精度。
- RDB 快照加速启动:如果同时启用 RDB,Redis 在启动时会先加载 RDB 快照,从而加速启动过程,减少恢复时间。
- AOF 文件的重写:为了避免 AOF 文件过大,Redis 提供了 AOF 文件的重写机制,将旧的 AOF 文件压缩成一个新的文件,从而减少文件大小和 I/O 开销。
4. RDB 与 AOF 的对比
特性 | RDB | AOF |
---|---|---|
持久化方式 | 快照式持久化 | 日志式持久化 |
性能 | 性能较好,适合高并发应用 | 相对较慢,尤其是在写操作频繁时 |
数据安全性 | 相对较低,存在数据丢失的风险 | 较高,记录所有写操作,数据恢复更精确 |
恢复速度 | 快速恢复 | 较慢,恢复时需要执行所有写命令 |
文件大小 | 文件较小,适合备份 | 文件随着写操作增大,需要定期重写 |
适用场景 | 高性能要求的应用,较少数据丢失容忍的场景 | 对数据持久化要求严格的应用,如银行等 |
总结
组合使用:可以同时启用 RDB 和 AOF,在性能和数据恢复之间找到合适的平衡。
RDB:适合用在对性能要求较高的场景,如缓存系统,数据丢失可以接受且恢复速度要求快的场景。
AOF:适合用在对数据完整性要求高的场景,如需要保证每个写操作都被持久化的应用,但会影响性能。
Redis 4.0 引入的 RDB-AOF 混合持久化模式
在 Redis 4.0 之前,持久化方式主要是 RDB(快照) 和 AOF(日志)。
RDB 适用于高效存储和快速恢复,但可能会丢失部分数据。
AOF 能提供更高的数据安全性,但写入和恢复速度较慢,尤其是在 AOF 文件过大时恢复变得更加耗时。
为了结合两者的优点,Redis 4.0 引入了 RDB-AOF 混合持久化模式(Hybrid Persistence)。这种模式结合了 RDB 和 AOF,使得持久化更加高效,同时兼顾数据安全性和恢复速度。
1. RDB-AOF 混合模式的原理
在混合模式下,Redis 生成 AOF 文件的方式有所不同:
- 在 触发 AOF 重写(Rewrite)时,Redis 会先 生成一个 RDB 快照 并写入 AOF 文件。
- 之后,所有新增的写操作 以 AOF 日志的方式追加 到这个 RDB 快照之后。
这样做的好处:
- 文件更小:相比传统的 AOF 文件,RDB-AOF 文件的大小大大减少,避免了 AOF 过大带来的问题。
- 恢复更快:在 Redis 重新启动时,先加载 RDB 快照(速度快),然后再回放 AOF 日志(数据更完整)。
- 写入效率更高:RDB 作为起始状态,减少了 AOF 需要存储的命令数量,从而减少了 I/O 开销,提高了性能。
2. 具体流程
- Redis 触发 AOF 重写(bgrewriteaof)
- 传统 AOF 方式是遍历现有数据,并重新生成更精简的 AOF 日志文件。
- 混合模式下,Redis 先生成一个新的 RDB 快照,并将其写入 AOF 文件的头部。
- 持续追加 AOF 命令
- RDB 快照写入后,Redis 开始以 AOF 方式记录新的写命令,并追加到该文件后面。
- Redis 重启
- 重新启动 Redis 时,优先从 RDB 部分加载数据(相比传统 AOF,速度更快)。
- 然后回放 AOF 命令,确保数据完整性。
3. RDB-AOF 持久化模式的优势
特性 | 传统 AOF(仅日志) | RDB(仅快照) | RDB-AOF 混合模式 |
---|---|---|---|
数据完整性 | 高,能保证数据不丢失 | 可能丢失最近修改的数据 | 高,优先加载 RDB,再回放 AOF |
写入性能 | 较低,所有写操作都记录日志 | 高,定期快照,开销较小 | 适中,RDB 初始化,AOF 追加 |
恢复速度 | 慢,需要回放所有命令 | 快,只需加载快照 | 快,加载快照后再回放少量日志 |
文件大小 | 大,存储所有操作命令 | 小,只存储数据快照 | 适中,存储快照+少量日志 |
适用场景 | 需要高可靠性,要求所有数据可恢复 | 适用于对数据完整性要求不高的缓存场景 | 平衡数据安全性和性能的场景 |
4. 如何启用 RDB-AOF 混合模式?
要启用混合持久化,需要修改 redis.conf
配置文件:
appendonly yes # 开启 AOF 持久化
aof-use-rdb-preamble yes # 启用 RDB-AOF 混合模式
appendonly yes
:开启 AOF 机制。aof-use-rdb-preamble yes
:开启 RDB-AOF 混合模式。
在 Redis 运行时,也可以通过 命令行 开启:
CONFIG SET appendonly yes
CONFIG SET aof-use-rdb-preamble yes
5. 适用场景
适用于
✅ 需要数据完整性(但不希望 AOF 影响性能的场景)。
✅ 数据恢复速度要求高(避免 AOF 过大导致恢复慢)。
✅ 需要兼顾性能和数据安全性的应用(如社交平台、金融系统、日志分析等)。
不适用于
❌ 对 AOF 日志全量存储有特殊需求的场景(如严格审计场景)。
❌ 极端高并发、对性能要求极致的应用(如纯缓存模式,直接使用 RDB 更高效)。
6. 总结
- Redis 4.0 引入 RDB-AOF 混合持久化,结合了 RDB 的高效存储和 AOF 的高数据安全性。
- 采用 RDB 作为 AOF 的起点,使得 AOF 文件更小,数据恢复更快,写入更高效。
- 适用于 对数据一致性要求高,同时希望提高持久化性能 的场景。
- 开启方式:在
redis.conf
里设置aof-use-rdb-preamble yes
。
这一机制大大优化了 Redis 的持久化能力,使其更适用于 高并发、低延迟的业务场景。
RDB-AOF 混合模式默认启用吗?
在 Redis 4.0 之后,RDB-AOF 混合持久化(Hybrid Persistence)被引入,允许 Redis 在 AOF 持久化模式下同时存储 RDB 快照和 AOF 日志,以提高数据恢复速度。
但是,默认情况下,该模式是❌未启用的,需要手动修改 Redis 配置文件(redis.conf
)或者在运行时动态设置。