Redis基础
本文收录了一些常见的redis基础题和场景题,作为个人笔记。
问题:缓存穿透、缓存击穿、缓存雪崩是什么?如何解决?
缓存穿透、缓存击穿、缓存雪崩是缓存与数据库交互中常见的性能与可用性问题,核心都是 “缓存未能有效拦截请求,导致数据库压力骤增”,但三者场景与解决思路不同:
一、缓存穿透
- 定义:查询一个不存在的数据(如 ID=-1 的用户),由于缓存中无此数据(无法命中),请求会直接穿透到数据库,且数据库也无此数据,导致每次请求都打到数据库。
- 危害:若被恶意利用(如高频查询不存在的 ID),可能导致数据库被击垮。
产生原因
- 业务逻辑误操作(如查询不存在的记录);
- 恶意攻击(如批量查询无效 ID,模拟高并发请求)。
解决方法
缓存空值(短期有效)
- 数据库查询结果为空时,仍将空值存入缓存(如
key: null),并设置较短过期时间(如 5 分钟),避免同一无效 key 反复穿透。 - 注意:需设置过期时间,防止缓存中积累大量空值占用空间。
- 数据库查询结果为空时,仍将空值存入缓存(如
布隆过滤器(Bloom Filter)前置拦截
原理:在缓存前部署布隆过滤器,预先存储所有
有效 key
(如数据库中存在的用户 ID),请求先经过布隆过滤器校验:
- 若布隆过滤器判断 key 不存在,直接返回空(无需查缓存和数据库);
- 若判断存在,再走 “缓存→数据库” 流程。
适用场景:有效 key 集合固定且不频繁变更(如用户 ID、商品 ID),存在一定误判率(可接受)。
接口层校验与限流
- 对输入参数做合法性校验(如 ID 必须为正整数),直接拦截无效请求;
- 对高频异常请求(如同一 IP 短时间大量查询无效 key)进行限流(如通过 Redis 实现 IP 级限流)。
二、缓存击穿
- 定义:一个热点 key(如热门商品 ID)的缓存突然失效(过期或被删除),此时大量并发请求同时访问该 key,因缓存未命中,所有请求瞬间穿透到数据库,导致数据库压力骤增。
- 区别于穿透:击穿的 key 是真实存在的(数据库有数据),只是缓存临时失效;穿透的 key 是不存在的。
产生原因
- 热点 key 的缓存过期时间设置不合理(如集中过期);
- 缓存服务异常(如手动删除热点 key、缓存节点宕机导致热点 key 丢失)。
解决方法
热点 key 永不过期
- 对核心热点 key(如秒杀商品)不设置过期时间,避免因过期导致的击穿;
- 需配合后台定时任务更新缓存(如每小时从数据库刷新一次数据),确保缓存数据新鲜。
互斥锁(Mutex Lock)控制并发
当缓存未命中时,先尝试获取分布式锁(如 Redis 的
1
SETNX
),只有获取锁的请求才能查询数据库并更新缓存,其他请求等待重试:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// 伪代码
String value = redis.get(key);
if (value == null) {
if (redis.setNx(lockKey, "1", 5000)) { // 获取锁,5秒过期
try {
value = db.query(key); // 查询数据库
redis.set(key, value, 3600); // 更新缓存
} finally {
redis.del(lockKey); // 释放锁
}
} else {
Thread.sleep(100); // 等待100ms后重试
return query(key); // 重试获取缓存
}
}
return value;注意:锁的过期时间需大于数据库查询时间,避免锁提前释放导致并发问题。
后台主动更新缓存
- 对热点 key,在缓存过期前(如过期前 10 分钟),通过后台任务主动从数据库查询最新数据并更新缓存,避免缓存失效的 “真空期”。
三、缓存雪崩
- 定义:在某一时刻,大量缓存 key 同时过期,或缓存服务整体宕机(如 Redis 集群崩溃),导致所有请求无法命中缓存,全部涌向数据库,造成数据库瞬间压力过大而崩溃。
- 区别于击穿:雪崩是 “批量 key 失效或缓存整体不可用”,影响面大;击穿是 “单个热点 key 失效”,影响范围较小。
产生原因
- 大量 key 设置了相同的过期时间(如整点批量过期);
- 缓存集群部署在单一节点或机房,遭遇硬件故障、网络中断等导致整体不可用;
- 缓存服务自身 bug 或负载过高(如内存溢出)引发崩溃。
解决方法
过期时间随机化,避免批量过期
为 key 设置基础过期时间(如 30 分钟),再叠加一个随机值(如 0-10 分钟),使过期时间分散,避免同一时刻大量 key 失效:
1
2
3int baseExpire = 30 * 60; // 基础30分钟
int randomExpire = new Random().nextInt(10 * 60); // 随机0-10分钟
redis.set(key, value, baseExpire + randomExpire);
多级缓存架构
- 引入本地缓存(如 Caffeine、Guava)+ 分布式缓存(如 Redis)的多级架构:
- 本地缓存:抗瞬时高并发,避免所有请求直接访问分布式缓存;
- 分布式缓存:保证数据一致性,作为本地缓存的 “数据源”。
- 即使分布式缓存雪崩,本地缓存仍能拦截部分请求。
- 引入本地缓存(如 Caffeine、Guava)+ 分布式缓存(如 Redis)的多级架构:
缓存集群高可用
- 分布式缓存(如 Redis)采用集群部署(主从 + 哨兵 / Cluster),确保单个节点宕机后,从节点自动切换为主节点,避免缓存服务整体不可用;
- 跨机房部署缓存集群,抵御单机房故障。
熔断降级与限流
- 当缓存服务不可用或数据库压力过高时,通过熔断组件(如 Sentinel、Hystrix)暂停部分非核心请求,仅允许核心请求访问数据库;
- 对数据库设置限流阈值(如每秒最大 1000 次请求),超过阈值则返回降级响应(如 “系统繁忙,请稍后再试”)。
缓存预热与快速恢复
- 系统启动或低峰期,通过脚本批量加载热点数据到缓存(缓存预热),避免高峰期缓存为空;
- 缓存崩溃后,通过备份数据(如 RDB/AOF 文件)快速恢复缓存,减少数据库承压时间。
总结:三者均需通过 “增强缓存拦截能力” 和 “保护数据库” 双维度解决 —— 穿透需拦截无效请求,击穿需保护热点 key,雪崩需分散风险并提升缓存可用性。实际应用中需结合业务场景选择方案(如核心业务优先用布隆过滤器 + 多级缓存,高并发场景必用互斥锁和随机过期时间)。
问题:如何使用 Redis 统计上亿用户的连续登录天数?
核心需求是高效记录登录状态+低成本存储+快速计算连续天数,Redis 通过紧凑数据结构和增量计算实现,适合上亿用户规模。
一、核心数据结构(节省空间 + 高效操作)
数据结构 用途 键格式示例 存储逻辑 BitMap 记录每日登录状态 login:20250807以用户 ID 为位偏移量,1 表示登录、0 表示未登录;1 亿用户仅需 12MB / 天。 String 存储用户当前连续登录天数 user:continue:days:100直接存储整数(如 5表示用户 100 连续登录 5 天)。Set 记录当天登录用户 ID(去重) daily:login:20250807存储当天登录的用户 ID,避免重复记录,用于后续增量计算。 二、实现流程
1. 实时记录用户登录状态(高并发场景)
用户登录时,通过两步操作记录:
- 去重:用 Set 判断用户是否已记录当天登录(避免重复操作);
- 标记登录:若未记录,在 Set 中添加用户 ID,并在当日 BitMap 中标记该用户为 “登录”(位值设为 1)。
2. 每日增量计算连续天数(凌晨执行)
仅针对 “前一天登录的用户” 计算,避免全量扫描:
取前一天登录用户:从 Set 中获取前一天登录的所有用户 ID;
判断连续登录
:检查该用户 “前天” 是否登录(通过前天的 BitMap):
- 若前天登录:当前连续天数 = 原天数 + 1;
- 若前天未登录:连续天数重置为 1;
更新结果:将新的连续天数存入 String 中。
三、关键优化(支撑上亿用户)
- 分片存储:按用户 ID 哈希分片(如分散到 16 个 Redis 节点),降低单节点压力;
- 异步批量计算:凌晨计算时,将用户分批用多线程处理,配合批量操作减少网络开销;
- 冷数据清理:仅保留最近 30 天的 BitMap 和 Set, older 数据归档压缩,节省空间;
- 容错机制:若某天计算失败,可通过 “当天登录状态 + 前一天连续天数” 重新计算。
四、查询方式
直接读取 String 类型的连续天数:通过
user:continue:days:{用户ID}键获取整数结果。总结:通过 BitMap 紧凑记录登录状态、Set 实现增量计算、String 存储结果,配合分片和异步处理,高效支撑上亿用户的连续登录天数统计,核心是 “只处理当天登录用户”,避免全量扫描。
问题:如何用 Redis 统计一亿个 key 场景下的双方共同好友?
核心是利用 Redis 的 Set 数据结构高效存储好友关系,并通过集合交集运算快速计算共同好友,需兼顾存储效率和计算性能。
一、数据结构设计(存储好友关系)
用 Redis 的Set存储每个用户的好友列表,适合场景:
- 好友关系具有 “唯一性”(不会重复添加);
- Set 原生支持交集运算(求共同好友的核心)。
键格式 类型 含义 示例 user:friends:{uid}Set 存储用户 uid的所有好友 IDuser:friends:100→{200, 300, 400}二、计算共同好友的核心方法
通过 Redis 的交集命令直接计算两个用户的共同好友,无需全量扫描:
基础命令:
SINTER功能:返回多个 Set 的交集(即共同元素)。
示例:计算用户 100 和用户 200 的共同好友:
1
2
3# 返回两个用户好友列表的交集
SINTER user:friends:100 user:friends:200
# 结果:如 {300, 500}(表示300和500是双方共同好友)
三、亿级 key 场景的性能优化
当用户量达亿级、好友列表庞大(如每个用户平均 100 个好友),需优化计算效率:
利用 “小集合优先” 原则
Redis 的
SINTER内部会优化计算:优先遍历 smaller Set,再检查元素是否在 larger Set 中。实际使用时,可先通过
1
SCARD
命令获取两个 Set 的大小,手动指定小 Set 在前,减少遍历次数:
1
2
3
4
5# 先查两个用户的好友数
SCARD user:friends:100 # 假设返回80
SCARD user:friends:200 # 假设返回120
# 小Set在前,执行交集
SINTER user:friends:100 user:friends:200
分片存储,分散计算压力
- 亿级 key 需 Redis 集群分片(如按用户 ID 哈希分片),避免单节点存储和计算过载;
- 若两个用户的好友 Set 在不同分片,集群会自动协同计算交集(依赖 Redis Cluster 的跨节点命令支持)。
限制单次返回数量,分页查询
若共同好友数量过多(如超过 1000),直接返回全部会占用大量带宽,可结合
1
SINTERSTORE
1
SSCAN
分页
1
2
3
4
5
6# 1. 将交集结果暂存到临时Set(过期时间10分钟)
SINTERSTORE temp:common:100:200 user:friends:100 user:friends:200
EXPIRE temp:common:100:200 600
# 2. 分页查询临时Set(每次查20个)
SSCAN temp:common:100:200 0 COUNT 20
缓存高频查询结果
对高频查询的用户对(如明星用户与粉丝),将共同好友结果缓存到 Set(设置过期时间,如 1 小时),减少重复计算:
1
2
3
4
5# 若缓存存在,直接查缓存
EXISTS cache:common:100:200
# 不存在则计算并缓存
SINTERSTORE cache:common:100:200 user:friends:100 user:friends:200
EXPIRE cache:common:100:200 3600
四、适用场与限制
- 适用场景:社交产品(如微信、微博)的共同好友推荐、互动场景(如 “你们有 3 个共同好友”);
- 限制:若用户好友列表极大(如超 10 万),
SINTER仍可能耗时(毫秒级增至秒级),需结合业务限制好友数或异步计算。
总结:核心是用 Set 存储好友关系,通过
SINTER求交集,配合 “小集合优先”、分片存储、结果缓存等优化,在亿级 key 场景下高效统计共同好友,平衡性能与资源消耗。问题:如何用 Redis 实现上亿用户的实时积分榜?
核心需求是高频更新分数(如用户行为实时加分)和快速查询排名(如个人排名、Top N 用户),Redis 的 Sorted Set(有序集合)是最优选择,需解决亿级规模下的性能与内存挑战。
一、核心数据结构:Sorted Set(有序集合)
Sorted Set 天生适合排行榜场景,通过 “成员 - 分数” 键值对存储,支持按分数排序和快速排名计算:
键格式 类型 含义 核心命令(优化点) rank:board:globalSorted Set 全局积分榜(用户 ID 为成员,分数为值) ZADD(更新分数)、ZREVRANK(查排名)、ZREVRANGE(查 Top N)二、亿级用户的核心挑战与解决方案
1. 单集合过大导致的性能问题(核心优化)
- 问题:单个 Sorted Set 存储上亿用户时,
ZADD(更新)和ZREVRANK(查排名)的 O (log N) 复杂度会因 N 过大(亿级)导致延迟升高(如从微秒级增至毫秒级)。 - 解决方案:分片存储
- 按用户 ID 哈希分片(如
hash(uid) % 100),将全局榜拆分为 100 个分片(rank:board:0至rank:board:99),每个分片存储约 1000 万用户; - 优势:单分片规模缩小 100 倍,操作延迟显著降低,且支持水平扩容(增加分片数)。
- 按用户 ID 哈希分片(如
2. 高频分数更新的效率优化
- 批量更新用 Pipeline:用户行为(如点赞、完成任务)触发分数更新时,用 Pipeline 批量执行
ZADD(如一次更新 100 个用户分数),减少网络往返次数; - 分数更新策略:
- 增量更新:仅传递分数变化量(如
ZINCRBY rank:board:1 5 uid100,直接加 5 分),避免全量传递分数; - 异步更新:非核心场景(如浏览量)可通过消息队列异步更新 Redis,降低实时写入压力。
- 增量更新:仅传递分数变化量(如
3. 排名查询的灵活实现
需支持三类查询场景,结合分片策略处理:
查询场景 实现方式 个人排名 1. 计算用户 ID 所属分片(如 shardId = uid % 100); 2. 用ZREVRANK rank:board:{shardId} uid获取分片内排名; 3. 若需全局排名,累加所有 “分数高于该用户” 的分片用户数(可缓存高频用户的全局排名)。Top N 用户(全局) 1. 分别查询每个分片的 Top N( ZREVRANGE rank:board:{shardId} 0 N-1); 2. 合并所有分片结果,取全局 Top N(客户端或中间层处理)。附近用户排名 1. 获取用户分数( ZSCORE); 2. 在分片内查询 “分数在 [score-10, score+10]” 的用户(ZRANGEBYSCORE),返回前后 N 名。4. 内存占用控制
- 用户 ID 压缩:若用户 ID 为长字符串(如 UUID),映射为整数 ID(如通过数据库自增 ID),减少 Sorted Set 中 “成员” 的存储体积;
- 冷热数据分离:长期低活跃用户(如 30 天未更新分数)迁移至 “冷榜”(单独的 Sorted Set),仅保留活跃用户在 “热榜”,降低热榜规模;
- 过期清理:非永久榜单(如活动榜)设置过期时间(
EXPIRE),自动清理无效数据。
三、高可用与容错
- 集群部署:用 Redis Cluster 管理分片,每个分片配置主从节点,主节点故障时从节点自动切换,避免单点失效;
- 持久化策略:开启 AOF+RDB 混合持久化,确保分数更新不丢失(AOF 记录实时操作,RDB 做全量备份);
- 监控告警:监控各分片的内存占用、
ZADD延迟、查询 QPS,超过阈值时告警(如分片内存超 80% 触发扩容)。
总结:核心是用 Sorted Set 存储分数,通过哈希分片解决亿级规模性能问题,结合批量更新、冷热分离优化效率,最终实现支持高频更新和灵活查询的实时积分榜。分片策略是关键,需根据用户规模动态调整分片数量(如从 100 增至 200)。
- 问题:单个 Sorted Set 存储上亿用户时,
问题:秒杀系统如何设计(核心目标:抗高并发、防超卖、保稳定)
秒杀系统需应对 “瞬时流量峰值(如 10 万 QPS)”“库存精确控制”“系统不崩溃” 三大核心挑战,需从流量拦截→请求处理→库存控制→兜底防护全链路设计,具体方案如下:
一、前端层:减少无效请求,降低入口压力
前端是流量的第一关,通过交互限制和资源优化过滤大部分无效请求:
- 限流与防重复提交
- 按钮置灰:点击后立即置灰,禁止重复点击(避免用户快速多次提交);
- 验证码 / 排队机制:秒杀开始前弹出验证码(如滑块验证),或显示 “排队中” 提示,延缓请求发送,分散流量峰值;
- 前端倒计时:精准同步服务器时间,避免用户因本地时间偏差提前请求(减少无效请求)。
- 静态资源优化
- 秒杀页面静态化:商品图片、描述等静态资源通过 CDN 分发,减轻应用服务器压力;
- 懒加载:非核心内容(如商品详情)延迟加载,优先加载秒杀按钮和倒计时组件。
二、接入层:流量过滤与限流,挡住大部分请求
通过 Nginx 和网关拦截异常流量,只允许合法请求进入后端:
Nginx 限流(第一层拦截)
基于 IP 限流:用
1
limit_req
模块限制单 IP 每秒请求数(如 10 次 / 秒),过滤恶意刷请求的 IP;
1
2
3
4
5
6
7
8# Nginx配置示例:单IP每秒最多10个请求,超过则返回503
limit_req_zone $binary_remote_addr zone=seckill:10m rate=10r/s;
server {
location /seckill {
limit_req zone=seckill burst=20 nodelay; # 允许20个突发请求
proxy_pass http://backend;
}
}黑名单:通过 Nginx 配置或 Lua 脚本,拦截历史恶意 IP(如频繁超时、参数异常的 IP)。
网关层处理(第二层拦截)
- 用 Spring Cloud Gateway 或 Kong 做路由转发,同时进行:
- 参数校验:检查商品 ID、用户 ID 是否合法(如商品是否存在、用户是否登录),直接拦截无效参数;
- 令牌桶限流:对秒杀接口设置全局 QPS 阈值(如 5 万 QPS),超过则返回 “系统繁忙”;
- 灰度分流:大促时将部分流量引流到备用集群,避免单集群过载。
- 用 Spring Cloud Gateway 或 Kong 做路由转发,同时进行:
三、服务层:异步化 + 集群化,扛住有效请求
后端服务需轻量、高效,聚焦 “快速处理有效请求”:
- 秒杀服务独立部署
- 将秒杀接口从主业务服务中拆分,独立部署集群(如 20 台服务器),避免秒杀流量冲击其他业务(如购物车、支付)。
- 异步化削峰(核心)
- 用消息队列(RabbitMQ/Kafka)承接请求:用户请求到达后,先校验库存(Redis 预减),通过后发送消息到队列,立即返回 “排队中”;
- 消费端(独立的订单服务)从队列中取消息,异步创建订单、扣减数据库库存,避免同步处理导致的服务阻塞;
- 优势:消息队列缓冲瞬时流量(如 10 万请求在队列中排队,消费端按 5 万 / 秒处理),防止服务被压垮。
- 服务集群与负载均衡
- 秒杀服务和订单服务均集群部署,通过负载均衡(如 Nginx、K8s Service)分发请求,避免单节点过载;
- 无状态设计:服务不存储本地数据(如会话、库存),依赖 Redis 和数据库,支持随时扩容。
四、数据层:库存精准控制,防超卖
库存超卖是秒杀的致命问题,需通过 “Redis 预减 + 数据库兜底 + 原子操作” 多层防护:
Redis 预减库存(快速判断)
秒杀前预热:将商品库存加载到 Redis(如
seckill:stock:1001 → 100,1001 为商品 ID);请求到达时,用 Redis 原子命令
1
DECR
预减库存(如
1
DECR seckill:stock:1001
):
- 若结果≥0:库存充足,允许进入消息队列;
- 若结果 <0:库存不足,直接返回 “已抢完”,无需进入后续流程;
优势:Redis 单命令原子性,避免并发减库存导致的超卖(如 100 个库存,101 个请求同时减,最终结果会正确为 - 1)。
数据库兜底防超卖
消息队列消费端创建订单时,执行 SQL 扣减数据库库存,用乐观锁确保最终库存正确:
1
2
3
4-- 仅当库存>0时才扣减,version确保并发安全
UPDATE seckill_stock
SET stock = stock - 1, version = version + 1
WHERE goods_id = 1001 AND stock > 0 AND version = #{version};若 SQL 影响行数 = 0:说明库存已空,回滚订单,Redis 库存补回(
INCR),避免 Redis 与数据库不一致。
库存预热与动态调整
- 秒杀前 10 分钟,通过脚本将数据库库存同步到 Redis(避免秒杀开始时 Redis 查库压力);
- 若秒杀中发现 Redis 与数据库库存不一致(如网络延迟导致),用定时任务(如每 10 秒)校准一次。
五、监控与兜底:确保系统不崩溃
- 实时监控
- 监控指标:接口 QPS、响应时间、Redis 库存、消息队列堆积量、数据库连接数;
- 告警触发:队列堆积超 10 万条、Redis 内存使用率超 80%、接口错误率超 5% 时,立即告警(短信 / 邮件)。
- 降级与熔断
- 降级:当系统压力过大(如 CPU 超 90%),关闭非核心功能(如商品详情页),优先保障秒杀接口;
- 熔断:若数据库或 Redis 响应超时,暂时停止请求处理,返回 “稍后再试”,避免级联失败。
- 事后复盘
- 记录秒杀日志(用户请求、库存变化、订单创建),用于分析流量峰值、超卖原因;
- 压测优化:定期用 JMeter 模拟 10 倍流量压测,发现瓶颈(如 Redis 性能、数据库锁冲突)并优化。
总结:秒杀系统设计核心是 “层层拦截流量 + 异步削峰 + 库存精准控制”—— 前端减少无效请求,接入层过滤异常流量,服务层用消息队列异步扛峰,数据层用 Redis + 数据库双层防超卖,最终通过监控和兜底确保系统稳定。核心原则:“能在前面挡的,绝不放后面;能异步的,绝不同步”。
- 限流与防重复提交
问题:Redis 的 RDB 和 AOF 机制是什么?有何区别?
Redis 是内存数据库,需通过持久化机制将数据从内存写入磁盘,防止重启后数据丢失。RDB 和 AOF 是两种核心持久化方式,分别通过 “快照” 和 “命令日志” 实现,各有优劣。
一、RDB(Redis Database):基于快照的持久化
- 定义:在指定时间间隔内,将内存中的全量数据生成快照(二进制文件)并写入磁盘,恢复时直接加载快照文件到内存。
核心机制
- 触发方式
- 自动触发:通过
redis.conf配置快照规则(如save 900 1表示 900 秒内有 1 次写操作则触发); - 手动触发:执行
SAVE(阻塞 Redis,直到快照生成,不建议生产用)或BGSAVE(fork 子进程生成快照,主进程继续处理请求)。
- 自动触发:通过
- 文件格式:单一二进制文件(默认
dump.rdb),存储数据的键值对压缩形式,体积小。 - 恢复过程:Redis 启动时,若检测到
dump.rdb文件,自动加载该文件到内存(加载期间会阻塞客户端请求)。
优缺点
优点 缺点 1. 文件体积小,适合备份(如每日备份); 2. 恢复速度快(直接加载二进制文件); 3. 对 Redis 性能影响小(BGSAVE 通过子进程处理,不阻塞主进程)。 1. 数据安全性低:若 Redis 崩溃,最近一次快照后的数据会丢失(如配置 save 300 10,则可能丢失 300 秒内的数据); 2. 大内存场景下,BGSAVE fork 子进程可能阻塞主进程(毫秒级,取决于内存大小)。适用场景
- 对数据完整性要求不高(允许丢失几分钟数据);
- 需要频繁备份(如灾备场景);
- 内存数据量大,追求快速恢复。
二、AOF(Append Only File):基于命令日志的持久化
- 定义:将所有写操作命令(如
SET、HSET)以文本形式追加到日志文件中,恢复时重新执行日志中的命令以重建数据。
核心机制
- 触发方式:默认关闭,需在
redis.conf中开启(appendonly yes),命令实时追加到appendonly.aof文件。 - 命令同步策略(通过
appendfsync配置,平衡安全性与性能):always:每次写命令都同步到磁盘(最安全,性能最差);everysec:每秒同步一次(默认,允许丢失 1 秒内数据,性能适中);no:由操作系统决定何时同步(性能最好,安全性最差)。
- 文件重写(Rewrite):
- 问题:AOF 文件会随命令增多而膨胀(如多次
INCR同一键会记录多条命令); - 解决:通过
BGREWRITEAOF命令(自动或手动触发),生成 “最终状态命令”(如INCR x 10替换 10 条INCR x),压缩文件体积。
- 问题:AOF 文件会随命令增多而膨胀(如多次
- 恢复过程:Redis 启动时,逐行执行 AOF 文件中的命令,重建数据(命令多则恢复慢)。
优缺点
优点 缺点 1. 数据安全性高:默认每秒同步,最多丢失 1 秒数据; 2. 日志文件是文本命令,易理解和修复(如手动删除错误命令)。 1. 文件体积大(相同数据,AOF 文件通常比 RDB 大); 2. 恢复速度慢(需重新执行所有命令); 3. 高并发写场景下, always策略可能影响性能。适用场景
- 对数据完整性要求高(如金融场景,不允许丢失过多数据);
- 可接受稍慢的恢复速度和较大的文件体积。
三、混合持久化(Redis 4.0+):结合 RDB 和 AOF 的优势
- 机制:AOF 文件中,前半部分是 RDB 快照(全量数据),后半部分是快照生成后的增量命令日志;
- 优势:恢复时先加载 RDB(快),再执行增量命令(数据全),兼顾 RDB 的快速恢复和 AOF 的高安全性;
- 配置:
aof-use-rdb-preamble yes(默认开启)。
四、如何选择?
- 单种方案:追求性能和快速恢复选 RDB;追求数据安全选 AOF(
everysec策略); - 生产环境推荐:开启混合持久化,同时保留 RDB 作为备份(如每日生成 RDB,AOF 实时记录),兼顾安全性与恢复效率。
总结:RDB 是 “全量快照”,适合备份和快速恢复;AOF 是 “命令日志”,适合高安全性场景;混合持久化结合两者优势,是生产环境的优选。
问题:Redis 单线程为什么这么快?
Redis 的 “单线程” 指的是处理客户端请求的核心线程是单线程(后台持久化、集群同步等操作由其他线程处理)。尽管单线程理论上无法利用多核 CPU,但 Redis 凭借内存操作、高效设计和 I/O 模型优化,实现了极高的性能(单机 QPS 可达 10 万 +),核心原因如下:
一、基于内存操作,避免磁盘 I/O 瓶颈
Redis 的所有数据都存储在内存中,内存读写速度(微秒级)远快于磁盘(毫秒级)。单线程处理内存操作时,无需等待磁盘 I/O,天然具备高性能基础。
二、避免多线程的 “上下文切换” 和 “锁竞争” 开销
多线程模型中,线程切换(保存 / 恢复上下文)和锁竞争(如共享资源加锁)会消耗大量 CPU 资源。而 Redis 单线程:
- 无需切换线程,减少了切换带来的时间损耗;
- 无需为共享数据加锁(单线程操作天然线程安全),避免了锁竞争和死锁风险。
三、高效的数据结构设计
Redis 为核心场景优化了数据结构,操作复杂度低,减少了单线程的计算耗时:
- 哈希表:Redis 的 KV 存储基于哈希表,平均查找 / 插入复杂度为 O (1);
- 跳表:有序集合(Sorted Set)使用跳表,范围查询和排序操作复杂度为 O (logN),优于平衡树;
- 压缩列表(ZipList)、整数集合(IntSet):对短列表、小整数集合采用紧凑存储,减少内存占用和操作耗时。
四、I/O 多路复用技术,高效处理并发连接
Redis 通过I/O 多路复用(如 Linux 的 epoll、Windows 的 IOCP)处理大量客户端连接:
- 单线程通过一个事件循环,同时监听多个客户端的 Socket 连接;
- 当某个 Socket 有数据可读 / 可写时,事件循环会触发相应操作,避免单线程因等待 I/O 而阻塞;
- 本质是 “用单线程管理多连接”,而非 “单线程串行处理所有请求”,兼顾了并发处理和单线程的简洁性。
五、精简的命令处理逻辑
Redis 的命令处理流程极简:接收命令→解析命令→执行操作→返回结果,无复杂的业务逻辑或计算。单线程专注于高效执行这些轻量操作,进一步提升速度。
六、后台线程处理 “耗时操作”
Redis 将耗时操作(如 RDB 持久化、AOF 重写、大 key 删除)交给后台线程处理,不阻塞核心单线程:
- 例如
BGSAVE通过 fork 子进程生成快照,主进程继续处理请求; - 避免了单线程被长耗时任务拖累。
总结:Redis 单线程快的核心是 “扬长避短”—— 利用内存速度优势,避免多线程开销,通过高效数据结构和 I/O 模型优化,让单线程专注于快速处理核心命令,同时将耗时操作异步化。这使得 Redis 在单线程模型下,反而比多线程模型更高效地处理高并发请求。
问题:Redis 底层的多路复用(I/O Multiplexing)机制是什么?
Redis 的单线程能高效处理数万并发连接,核心依赖I/O 多路复用技术。它允许单线程同时监控多个客户端的 Socket 连接,仅在连接有数据可读 / 可写时才进行处理,避免了单线程因等待 I/O 而阻塞,极大提升了并发处理能力。
一、为什么需要多路复用?
Redis 是单线程处理客户端请求的(核心逻辑单线程),而客户端与 Redis 的通信基于 Socket,存在以下问题:
- 若单线程逐个处理 Socket,当某个 Socket 无数据时,会陷入阻塞(等待数据),导致其他有数据的 Socket 无法被及时处理;
- 若为每个 Socket 创建线程,会引发线程切换和锁竞争的巨大开销,反而降低性能。
多路复用技术解决了这一矛盾:单线程通过一个 “监听器” 同时监控所有 Socket,仅处理 “有数据就绪” 的 Socket,无需阻塞等待,实现 “单线程高效处理多连接”。
二、Redis 的多路复用模型:适配不同操作系统
Redis 会根据操作系统自动选择最优的多路复用模型,核心实现如下:
操作系统 多路复用模型 特点 Linux epoll 性能最优,支持海量连接(无最大描述符限制),事件驱动型 BSD(Mac OS) kqueue 与 epoll 类似,高效支持大量连接 Solaris /dev/poll 适用于 Solaris 系统,支持高并发 Windows IOCP Windows 平台的异步 I/O 模型 Linux 下的 epoll 是最常用的实现,也是 Redis 高性能的关键,以下重点讲解 epoll 的工作原理。
三、epoll 的工作原理(以 Linux 为例)
epoll 通过 “事件驱动” 模式实现多路复用,核心是 3 个系统调用和 “就绪事件列表”:
- epoll_create:创建一个 epoll 实例(内核中的事件表),用于管理需要监控的 Socket。
- epoll_ctl:向 epoll 实例注册 / 修改 / 删除需要监控的 Socket 及事件类型(如 “读事件”:Socket 有数据可读;“写事件”:Socket 可写入数据)。
- epoll_wait:阻塞等待 epoll 实例中 “就绪的事件”(如某 Socket 有数据到达),返回就绪事件列表(仅包含有数据的 Socket)。
四、Redis 如何使用 epoll 处理客户端请求?
Redis 的事件循环(Event Loop)是多路复用的核心载体,流程如下:
注册事件:
- 客户端连接 Redis 时,Redis 会创建一个 Socket,并通过
epoll_ctl向 epoll 实例注册 “读事件”(等待客户端发送命令); - 当 Redis 需要向客户端发送响应时,注册 “写事件”(等待 Socket 可写入)。
- 客户端连接 Redis 时,Redis 会创建一个 Socket,并通过
事件循环(核心流程):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15while (1) {
// 1. 等待就绪事件(通过epoll_wait获取)
就绪事件列表 = epoll_wait(epoll实例, 超时时间);
// 2. 处理就绪事件
对于每个就绪事件:
if (是读事件):
从Socket读取客户端命令 → 解析并执行 → 生成响应;
若有响应需要发送,注册“写事件”;
if (是写事件):
将响应写入Socket → 完成后移除“写事件”;
// 3. 处理时间事件(如定时任务:过期键清理、AOF重写检查等)
处理到期的时间事件;
}高效性关键:
- 只处理就绪事件:
epoll_wait返回的是 “已就绪” 的 Socket,无需遍历所有连接,复杂度为 O (1); - 边缘触发(ET 模式):Redis 使用 epoll 的边缘触发模式,仅在 Socket 状态从 “无数据” 变为 “有数据” 时触发一次事件,减少事件处理次数(比水平触发更高效)。
- 只处理就绪事件:
五、Redis 的事件类型:文件事件与时间事件
多路复用监控的事件分为两类,由事件循环统一处理:
- 文件事件:与 Socket I/O 相关的事件(客户端连接、命令读取、响应发送),是 epoll 监控的核心;
- 时间事件:定时任务(如每隔 100ms 检查过期键、每隔 1s 触发 AOF 重写检查),由 Redis 自己维护的定时器驱动,在事件循环中穿插处理。
总结:Redis 的多路复用通过 epoll(或其他系统的同类模型)实现 “单线程监控多 Socket”,仅处理就绪事件,避免 I/O 阻塞;配合事件循环统一调度文件事件和时间事件,最终让单线程高效支撑数万并发连接,是 Redis 高性能的核心技术之一。
问题:Redis 的过期键删除策略是什么?
Redis 的过期键删除并非依赖单一机制,而是通过三种策略配合,平衡 CPU 资源消耗与内存占用,核心包括:惰性删除、定期删除、内存淘汰机制。
一、惰性删除(Lazy Expiration)
核心逻辑:键过期后不主动删除,仅在 “被访问时” 才检查是否过期,若过期则删除并返回空。
- 触发时机:客户端执行
GET、HGET等访问命令时,Redis 会先校验键的过期时间。 - 优点:
- 完全按需删除,不占用额外 CPU 资源(无需主动扫描过期键),对 CPU 友好。
- 缺点:
- 若过期键长期未被访问,会一直占用内存,可能导致 “内存泄漏”(过期键堆积)。
二、定期删除(Periodic Expiration)
核心逻辑:每隔一段时间主动扫描部分过期键并删除,弥补惰性删除的内存占用问题。
- 实现机制:
- 定时触发:默认每 100ms(通过
hz配置调整,1-500 范围)执行一次。 - 抽样扫描:每次随机抽取 20 个设置了过期时间的键,删除其中已过期的。
- 循环重试:若这 20 个键中过期比例超 25%,重复抽样(直到比例≤25% 或达时间上限),避免过期键集中堆积。
- 时间控制:每次执行不超过 25ms,防止阻塞主线程(单线程模型下,过长阻塞影响响应)。
- 定时触发:默认每 100ms(通过
- 优点:
- 主动清理部分过期键,减少内存浪费,缓解惰性删除的内存泄漏风险。
- 缺点:
- 抽样扫描可能漏掉部分过期键(仍需惰性删除兜底);扫描频率过高会占用 CPU,过低则清理不及时。
三、内存淘汰机制(Memory Eviction Policies)
核心逻辑:当 Redis 内存达到
maxmemory(最大内存限制)时,即使过期键未被删除,也会强制删除部分键释放内存,作为前两种策略的兜底。常见策略(Redis 6.2+):
策略 说明 volatile-lru从 “设过期时间的键” 中,删除最近最少使用的键 allkeys-lru从 “所有键” 中,删除最近最少使用的键 volatile-lfu从 “设过期时间的键” 中,删除最近最少频率使用的键 allkeys-lfu从 “所有键” 中,删除最近最少频率使用的键 noeviction(默认)不删除键,内存不足时拒绝新写入(返回错误) 作用:确保 Redis 在内存满时仍能运行,避免因内存溢出崩溃。
总结:三种策略协同工作 —— 惰性删除按需清理(省 CPU),定期删除主动抽样(控内存),内存淘汰兜底(保运行),最终在 “CPU 效率” 与 “内存占用” 之间找到平衡,支撑 Redis 高并发场景下的稳定运行。
- 触发时机:客户端执行
问题:Redis 分布式锁如何实现?
分布式锁用于解决多节点(如微服务集群)对共享资源的并发访问问题,核心需求是互斥性(同一时间仅一个节点持有锁)、安全性(不被其他节点误释放)、防死锁(锁最终能被释放)。Redis 通过原子命令、红最终能被释放)。Redis 通过原子命令、红锁算法等实现,具体方案如下:
一、基础实现:基于单节点 Redis 的分布式锁
利用 Redis 的原子命令保证锁的互斥性,是最常用的基础方案。
1. 获取锁(加锁)
通过
SET命令的扩展参数,确保 “判断锁是否存在 + 设置锁” 的原子性:NX:仅当锁键不存在时才设置(保证互斥,只有第一个请求能成功);PX:设置过期时间(避免节点宕机导致锁永久存在,防死锁);- 随机值:作为锁值,标识持有锁的节点(避免释放其他节点的锁)。
1
2
3# 命令格式:SET 锁键 随机值 NX PX 过期时间(毫秒)
SET lock:resource "uuid-123" NX PX 30000
# 成功返回OK(获取锁成功),失败返回nil(锁已被持有)2. 释放锁(解锁)
需先验证锁的持有者是否为自己,再删除锁,必须用 Lua 脚本保证两步原子性(避免 “验证后锁过期,误删新锁”):
1
2
3
4
5
6
7
8
9-- Lua脚本:仅当锁值匹配时才删除锁
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1]) -- 释放锁
else
return 0 -- 不操作(锁已被其他节点持有)
end
# 执行脚本:KEYS[1]为锁键,ARGV[1]为加锁时的随机值
EVAL "上述脚本" 1 lock:resource "uuid-123"二、关键问题与优化方案
1. 锁超时问题(锁释放但业务未完成)
- 问题:若业务处理时间超过锁的过期时间,锁会自动释放,可能导致多个节点同时操作资源。
- 优化:
- 锁续约:使用 “看门狗” 机制(如 Redisson 的
watch dog),持有锁的节点定期(如每 10 秒)延长锁的过期时间(前提是业务仍在处理); - 合理设置过期时间:根据业务最大耗时设置(如实际耗时 5 秒,设 10 秒过期)。
- 锁续约:使用 “看门狗” 机制(如 Redisson 的
2. 主从架构下的锁丢失问题
- 问题:Redis 主从同步为异步,主节点加锁后未同步到从节点即宕机,从节点升级为主节点后,新节点会允许其他请求加锁(导致锁丢失)。
- 优化:引入Redlock(红锁)算法,通过多实例投票解决单节点依赖问题。
三、Redlock(红锁)算法:解决主从一致性问题
Redlock 是 Redis 官方提出的增强方案,基于 “多个独立 Redis 实例” 实现,适用于对锁可靠性要求极高的场景。
1. 核心设计思路
- 部署 5 个完全独立的 Redis 节点(无主从、无集群关系);
- 加锁时,向所有节点尝试加锁,仅当超过半数(≥3 个)节点加锁成功,且总耗时不超过锁过期时间的 1/3,才算整体加锁成功;
- 解锁时,向所有节点发送解锁命令(无论该节点是否加锁成功)。
2. 具体步骤
客户端获取当前时间戳(毫秒);
向 5 个节点依次发送加锁请求(使用基础方案的
SET NX PX命令,锁值相同,过期时间统一,如 30 秒);计算加锁总耗时
(当前时间 - 步骤 1 的时间戳):
- 若总耗时 > 锁过期时间 → 加锁失败,向所有节点发送解锁命令;
- 若成功加锁的节点数 ≥3 → 加锁成功,锁的实际有效期 = 过期时间 - 总耗时;
执行业务逻辑:需在 “实际有效期” 内完成,否则锁可能失效;
解锁:向所有 5 个节点发送解锁命令(用 Lua 脚本验证并删除锁)。
3. 优缺点
- 优点:通过多实例投票,大幅降低单节点故障导致的锁丢失风险,可靠性更高;
- 缺点:
- 部署成本高(需 5 个独立节点);
- 加锁耗时增加(需等待多个节点响应);
- 仍存在极端场景漏洞(如时钟漂移导致的锁有效性判断错误)。
四、生产环境推荐:使用成熟框架(如 Redisson)
手动实现分布式锁易遗漏细节(如续约、红锁逻辑),生产环境建议使用封装好的框架,以 Redisson 为例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// Redisson基础锁示例(自动续约、防死锁)
RedissonClient redisson = Redisson.create(config);
RLock lock = redisson.getLock("lock:resource");
lock.lock(30, TimeUnit.SECONDS); // 加锁,30秒过期(自动续约)
try {
// 执行业务逻辑
} finally {
lock.unlock(); // 释放锁
}
// Redisson红锁示例
RLock lock1 = redisson1.getLock("lock:resource");
RLock lock2 = redisson2.getLock("lock:resource");
RLock lock3 = redisson3.getLock("lock:resource");
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
redLock.lock(30, TimeUnit.SECONDS); // 红锁加锁
try {
// 执行业务逻辑
} finally {
redLock.unlock(); // 红锁释放
}- 特性:自动实现锁续约、支持红锁算法、可重入锁(同一节点可多次获取同一锁)、公平锁等。
总结:基础方案通过
SET NX PX加锁 + Lua 解锁实现,适合大多数场景;Redlock 通过多实例投票提升可靠性,适合高要求场景;生产环境优先使用 Redisson 等框架,避免手动实现的细节漏洞。核心原则:确保互斥性、防死锁、兼容分布式环境的一致性问题。问题:Redis(缓存)和 MySQL(数据库)如何保证数据一致性?
Redis 作为缓存加速读取,MySQL 作为持久化存储,两者的数据一致性指 “缓存中的数据与数据库中的数据保持一致”。核心挑战是读写顺序冲突(如更新数据库后未同步更新缓存)和并发操作干扰(如多线程同时读写),需通过合理的 “读写策略 + 异常处理” 保障一致性。
一、核心原则:明确缓存的角色与更新策略
缓存的核心是 “加速读取”,而非存储权威数据(权威数据在 MySQL),因此一致性策略需围绕 “以数据库为准,缓存按需同步” 设计,避免缓存成为数据不一致的源头。
二、读取策略:缓存未命中时的正确处理
读取数据时,需按 “先查缓存,缓存缺失再查数据库并更新缓存” 的流程,避免直接读取数据库导致缓存失效:
基础流程:
1
2
3
41. 客户端请求数据时,先查询Redis缓存;
2. 若缓存命中(存在且未过期),直接返回缓存数据;
3. 若缓存未命中,查询MySQL数据库;
4. 将数据库查询结果写入Redis(设置合理过期时间),再返回给客户端。并发读优化:
- 当缓存失效且高并发查询同一数据时,可能导致 “缓存击穿”(大量请求直接查数据库),需加分布式锁控制:仅允许一个线程查库并更新缓存,其他线程等待重试。
三、更新策略:数据变更时的缓存同步
更新数据(新增 / 修改 / 删除)时,需同步处理缓存,核心是 “如何协调数据库更新与缓存更新的顺序”,常见方案如下:
1. Cache Aside Pattern(缓存旁路模式,最常用)
核心逻辑:更新数据库后,删除缓存(而非直接更新缓存),下次读取时再从数据库加载最新数据到缓存。
步骤:
1
2
31. 更新操作:先更新MySQL数据库;
2. 再删除Redis中对应的缓存(而非更新缓存);
3. 后续读取时,缓存未命中,从数据库加载新数据并写入缓存。为什么删缓存而非更新缓存?
- 避免 “更新缓存” 与 “更新数据库” 的内容不一致(如复杂数据结构更新容易出错);
- 减少一次写缓存的开销,尤其当更新频率远高于读取频率时。
潜在问题与解决:
问题 1
:先更数据库→删缓存之间,若有新请求读取,可能读到旧缓存(概率低,因时间窗口短)。
- 解决:缓存设置较短过期时间,即使出现旧数据,也会很快过期。
问题 2
:若删缓存失败(如 Redis 宕机),会导致缓存中一直是旧数据。
- 解决:通过重试机制(如消息队列)确保缓存删除成功,或定期全量同步缓存(兜底)。
2. 延迟双删:解决并发更新与读取的冲突
当 “更新数据库” 和 “读取数据” 并发执行时,可能出现以下异常流程:
1
2
3
4
5
6
7
8
9线程A:更新数据库(旧→新)→ 准备删缓存
线程B:查询缓存(未命中)→ 查询数据库(恰好读到线程A更新后的新数据)→ 写入缓存(新数据)
线程A:删除缓存(删除的是线程B刚写入的新数据)
→ 后续读取会重新加载新数据,无问题?不,若顺序相反:
线程A:更新数据库(旧→新)
线程B:查询缓存(未命中)→ 查询数据库(旧数据,因线程A的更新未提交)→ 写入缓存(旧数据)
线程A:删除缓存(未执行或失败)
→ 缓存中留存旧数据,与数据库新数据不一致。延迟双删方案:
1
2
31. 先删除缓存;
2. 更新数据库;
3. 延迟一段时间(如500ms),再次删除缓存。- 第一次删缓存:避免更新数据库期间,旧缓存被读取;
- 延迟再次删缓存:清除可能被并发线程写入的旧缓存(如上述线程 B 的情况);
- 延迟时间:根据业务处理耗时设置(略大于一次数据库事务的时间)。
3. Write Through(写透模式)
核心逻辑:更新数据时,先更新缓存,再更新数据库,确保缓存与数据库同时写入成功。
步骤:
1
2
31. 客户端更新数据时,先更新Redis缓存;
2. Redis同步更新MySQL数据库;
3. 两者都成功后,返回更新成功。优点:缓存与数据库强一致(同时更新);
缺点:增加一次写缓存的耗时,降低更新性能;若数据库更新失败,需回滚缓存(复杂度高)。
适用场景:对一致性要求极高,但更新频率低的场景(如金融核心数据)。
4. 基于 Binlog 的异步同步(最终一致性)
通过监听 MySQL 的 Binlog(二进制日志),异步更新 Redis 缓存,适合高并发场景(牺牲强一致性,保证最终一致)。
- 实现:
- 部署中间件(如 Canal)监听 MySQL 的 Binlog,解析数据变更;
- 中间件将变更信息发送到消息队列(如 Kafka);
- 消费端从队列获取变更信息,异步更新 Redis 缓存。
- 优点:
- 解耦数据库与缓存更新,不影响主业务链路性能;
- 可批量处理更新,适合高并发写入场景。
- 缺点:存在短暂的数据不一致(从数据库更新到缓存同步有延迟);
- 适用场景:允许短暂不一致的非核心业务(如商品详情、用户动态)。
四、异常处理:确保极端情况的一致性
- 缓存宕机:
- 降级策略:直接查询数据库,不写入缓存(避免缓存恢复后数据混乱);
- 恢复后:通过定时任务从数据库全量加载热点数据到缓存。
- 数据库宕机:
- 只读缓存:若数据库不可用,暂时返回缓存数据(需容忍可能的旧数据);
- 禁止写入:避免缓存更新后,数据库恢复时无法同步(导致数据丢失)。
- 网络分区:
- 若 Redis 与 MySQL 之间网络中断,暂停缓存更新,仅更新数据库;
- 网络恢复后,通过 Binlog 同步或全量校验修复缓存。
五、总结:根据业务选择策略
- 强一致性场景(如支付、库存):优先用 Cache Aside + 延迟双删,或 Write Through,确保数据实时一致;
- 高并发场景(如电商商品):用 Cache Aside+Binlog 异步同步,接受短暂不一致,换取性能;
- 核心原则:以数据库为权威,缓存仅作为加速层;通过 “删除缓存而非更新” 减少不一致风险;结合异步同步和定时校验作为兜底。
没有完美的方案,需在 “一致性”“性能”“复杂度” 之间权衡,优先保证核心业务的数据正确性。
问题:Redis 集群方案有哪些?各有什么特点?(含 Redis Sharding)
Redis 集群方案用于解决单节点的性能瓶颈、容量限制和单点故障风险,主流方案包括:主从复制、哨兵模式(Sentinel)、Redis Cluster、Redis Sharding(客户端分片),分别适用于不同规模和需求场景。
一、主从复制(Master-Slave Replication)
核心逻辑:通过 “一主多从” 架构,主节点处理写操作,从节点复制主节点数据并分担读操作,实现读写分离和数据备份。
1. 架构与原理
- 角色:1 个主节点(可写)+ N 个从节点(只读);
- 数据同步:
- 初始化:从节点启动时发送
SYNC命令,主节点生成 RDB 快照并同步,后续通过 “命令缓冲区” 增量同步新写命令; - 偏移量机制:主从节点通过 “复制偏移量” 确保数据同步完整性。
- 初始化:从节点启动时发送
2. 核心作用
- 读写分离:主节点承担写请求,从节点分担读请求(如 90% 读请求分配到从节点);
- 数据备份:从节点存储完整数据,避免单节点故障导致数据丢失;
- 负载均衡:分散读压力,突破单节点 CPU / 网络瓶颈。
3. 优缺点
优点 缺点 1. 架构简单,易部署; 2. 有效提升读性能; 3. 实现数据冗余备份。 1. 主节点故障需手动切换(无自动故障转移); 2. 主节点写入压力集中; 3. 从节点同步存在延迟(可能导致读写不一致)。 4. 适用场景
- 读多写少的场景(如电商商品详情页、新闻资讯);
- 对可用性要求不高(可接受手动故障转移);
- 数据量中等(单主节点内存可容纳)。
二、哨兵模式(Sentinel)
核心逻辑:在主从复制基础上,增加 “哨兵节点” 监控主从状态,主节点故障时自动将从节点升级为主节点,解决主从复制的 “手动故障转移” 问题。
1. 架构与原理
- 角色:1 个主节点 + N 个从节点 + M 个哨兵节点(通常 3 个,奇数,避免脑裂);
- 哨兵功能:
- 监控:定期
PING节点判断存活状态; - 通知:节点故障时通过 API 通知应用或其他哨兵;
- 自动故障转移:主节点宕机后,投票选举新主节点并重新配置从节点;
- 配置管理:客户端通过哨兵获取当前主节点地址(无需硬编码)。
- 监控:定期
2. 核心作用
- 自动故障转移:主节点故障后秒级切换,减少人工干预;
- 高可用保障:多哨兵节点避免单点判断错误(如网络抖动误判);
- 简化客户端接入:客户端只需连接哨兵,无需关心主节点变更。
3. 优缺点
优点 缺点 1. 实现自动故障转移,提升可用性; 2. 兼容主从复制的读写分离和备份能力; 3. 部署难度低于分布式集群。 1. 仍存在单主节点写入瓶颈; 2. 数据存储受限于单主节点内存(无法分片); 3. 哨兵集群本身需维护。 4. 适用场景
- 对可用性要求高(需自动故障转移),但数据量中等(单主节点可容纳);
- 读多写少,写操作压力未超过单主节点瓶颈(如 QPS≤10 万);
- 如社交应用的用户会话存储、中小规模电商的库存缓存。
三、Redis Cluster(官方分布式集群)
核心逻辑:Redis 官方分布式方案,通过 “分片存储” 将数据分散到多个主节点,每个主节点对应从节点,支持自动故障转移,解决海量数据和高并发问题。
1. 架构与原理
- 角色:N 个主节点(默认 3 个以上)+ 每个主节点对应 1 个以上从节点;
- 分片机制:
- 数据划分为 16384 个 “哈希槽”,每个主节点负责一部分槽(如 3 主节点各负责 5461/5461/5462 个槽);
- 路由规则:
槽编号 = CRC16(key) % 16384,按槽路由到对应主节点;
- 故障转移:主节点宕机后,其从节点通过选举升级为主节点,接管哈希槽。
2. 核心作用
- 分片存储:突破单节点内存限制(如 10 个主节点支持 10 倍容量);
- 分布式高可用:多主节点分担写压力,单个节点故障不影响整体;
- 自动扩缩容:支持动态添加 / 删除节点,自动重新分配哈希槽(无需停机)。
3. 优缺点
优点 缺点 1. 支持海量数据存储(分片机制); 2. 多主节点分担写压力,提升并发; 3. 自带故障转移,高可用; 4. 官方原生支持,兼容性好。 1. 架构复杂,部署和维护成本高; 2. 跨槽操作(如 MGET多 key 分属不同槽)需特殊处理; 3. 数据迁移(扩缩容)可能短暂影响性能。4. 适用场景
- 数据量大(单节点内存不足,如超过 100GB);
- 高并发读写(单主节点无法承载,如写 QPS>10 万);
- 大规模分布式系统(如电商平台、支付系统的核心缓存)。
四、Redis Sharding(客户端分片)
核心逻辑:由客户端(或 SDK)通过哈希算法将数据分散到多个独立 Redis 实例,实现数据分片,本质是 “客户端主导的分布式存储”,无中心化协调节点。
1. 架构与原理
- 架构:多个独立 Redis 实例(无主从关系)+ 带分片逻辑的客户端(如 Jedis、redis-py);
- 分片机制:
- 客户端通过哈希算法(如
CRC32(key) % 实例数量、一致性哈希)计算 key 对应的实例; - 读写时直接路由到目标实例,各实例独立存储数据(无同步)。
- 客户端通过哈希算法(如
2. 核心作用
- 数据分片:将海量数据分散到多个实例,突破单实例内存限制;
- 性能分摊:读写请求分散到多个实例,提升整体吞吐量;
- 架构简单:无需部署额外组件,仅需客户端实现分片逻辑。
3. 优缺点
优点 缺点 1. 架构极简,无额外集群组件(运维成本低); 2. 客户端直接操作实例,无中间代理开销; 3. 兼容所有 Redis 版本,灵活性高。 1. 无自动故障转移(实例宕机后对应数据不可用); 2. 扩缩容困难(增减实例导致路由规则变化,需迁移大量数据); 3. 客户端逻辑复杂(需实现分片、故障重试等); 4. 不支持跨实例操作(如 MGET多 key 分布在不同实例时需多次请求)。4. 适用场景
- 早期分布式场景,数据量中等且增长稳定(扩缩容频率低);
- 客户端可控(如自研客户端或成熟 SDK 支持分片);
- 对可用性要求不高(可接受手动处理故障),如内部系统的缓存、日志存储。
总结:四种方案对比与选择
方案 核心能力 适用场景 主从复制 读写分离 + 数据备份 读多写少,可用性要求低(手动故障转移) 哨兵模式 自动故障转移 + 主从能力 中规模数据,需高可用(自动切换) Redis Cluster 分片存储 + 分布式高可用 海量数据 + 高并发,需自动扩缩容 Redis Sharding 客户端分片,无中心化 早期简单分布式,扩缩容少,客户端可控 选择时需权衡数据量、并发压力、可用性要求和运维成本,避免过度设计(如小场景用 Cluster)或功能不足(如高可用场景用 Sharding)。
问题:Redis 如何配置过期时间?删除过期键的原理是什么?
一、Redis 配置过期时间的方法
Redis 通过特定命令为键设置过期时间(生存时间或过期时刻),过期后键会被标记为 “过期”,最终通过删除机制清理。核心命令如下:
1. 为已存在的键设置过期时间(相对时间)
- **
EXPIRE key seconds**:设置键的生存时间(秒级),到期后键过期。
示例:EXPIRE user:100 3600→ 用户 100 的信息 1 小时后过期。 - **
PEXPIRE key milliseconds**:设置键的生存时间(毫秒级),精度更高。
示例:PEXPIRE order:200 15000→ 订单 200 的信息 15 秒后过期。
2. 为已存在的键设置过期时刻(绝对时间)
- **
EXPIREAT key timestamp**:设置键的过期 Unix 时间戳(秒级),到达该时刻后过期。
示例:EXPIREAT task:300 1691234567→ 任务 300 在指定时间戳(2023-08-05 12:02:47)过期。 - **
PEXPIREAT key milliseconds-timestamp**:设置键的过期 Unix 时间戳(毫秒级)。
3. 新建键时直接指定过期时间
通过
SET命令的扩展参数,创建键的同时设置过期时间,避免 “先创建再设过期” 的两步操作:SET key value EX seconds:秒级过期(等价于SET+EXPIRE)。
示例:SET code:400 "123456" EX 60→ 验证码 123456 60 秒后过期。SET key value PX milliseconds:毫秒级过期。
4. 查看与移除过期时间
- **
TTL key**:返回键的剩余生存时间(秒级,-1 表示永不过期,-2 表示已过期)。 - **
PTTL key**:返回键的剩余生存时间(毫秒级)。 - **
PERSIST key**:移除键的过期时间(键变为永不过期)。
二、删除过期键的原理:三种机制协同工作
Redis 不会在键 “恰好过期时” 立即删除,而是通过惰性删除、定期删除、内存淘汰机制三种策略配合,平衡 CPU 资源与内存占用。
1. 惰性删除(Lazy Expiration)
- 核心逻辑:键过期后不主动删除,仅在 “被访问时” 才检查是否过期,若过期则删除并返回空。
- 触发时机:客户端执行
GET、HGET等访问命令时,Redis 会先校验键的过期时间。 - 优点:完全按需删除,不占用额外 CPU 资源(无需主动扫描)。
- 缺点:若过期键长期未被访问,会一直占用内存(可能导致 “内存泄漏”)。
2. 定期删除(Periodic Expiration)
- 核心逻辑:每隔一段时间主动扫描部分过期键并删除,弥补惰性删除的内存占用问题。
- 执行机制:
- 默认每 100ms(通过
hz配置调整,范围 1-500)执行一次; - 随机抽取 20 个设置了过期时间的键,删除其中已过期的;
- 若这 20 个键中过期比例超过 25%,重复抽样扫描(直到比例≤25% 或达时间上限);
- 每次执行时间不超过 25ms(避免阻塞主线程)。
- 默认每 100ms(通过
- 优点:主动清理部分过期键,减少内存浪费。
- 缺点:抽样可能漏掉部分过期键(需惰性删除兜底)。
3. 内存淘汰机制(Memory Eviction)
核心逻辑:当 Redis 内存达到
maxmemory(最大内存限制)时,即使过期键未被删除,也会强制删除部分键释放内存(作为前两种机制的兜底)。常见策略
(Redis 6.2+):
volatile-lru:从 “设过期时间的键” 中删除最近最少使用的键;allkeys-lru:从 “所有键” 中删除最近最少使用的键;noeviction(默认):不删除键,内存不足时拒绝新写入。
三、总结
- 配置过期时间:通过
EXPIRE、SET EX等命令设置相对 / 绝对过期时间,支持秒级 / 毫秒级精度。 - 删除过期键原理:三种机制协同 —— 惰性删除按需清理(省 CPU),定期删除主动抽样(控内存),内存淘汰兜底(保运行),最终在 “CPU 效率” 与 “内存占用” 之间平衡,确保 Redis 高效稳定运行。
- **
问题:Redis 主从复制的核心原理是什么?
Redis 主从复制(Master-Slave Replication)是通过 “主节点数据同步到从节点” 实现的分布式部署方案,核心是从节点自动复制主节点的数据,从而实现读写分离、数据备份和负载均衡。其核心原理可拆解为 “连接建立→初始化同步→增量同步→状态维护” 四个阶段。
一、核心目标
- 数据一致性:从节点数据与主节点保持一致(最终一致,允许短暂延迟);
- 读写分离:主节点处理写操作,从节点处理读操作,分散压力;
- 冗余备份:从节点存储完整数据,主节点故障时可作为备份。
二、核心原理:四阶段同步流程
1. 连接建立:从节点主动连接主节点
从节点通过配置(如
slaveof master_ip master_port)指定主节点地址,启动后主动发起连接:- 从节点向主节点发送
PING命令,确认主节点存活; - 主节点返回
PONG后,从节点发送身份验证命令(若主节点配置requirepass); - 验证通过后,从节点发送
REPLCONF listening-port <port>告知主节点自己的监听端口,主从连接正式建立。
2. 初始化同步:全量复制主节点数据
首次连接或从节点数据与主节点差异过大时,触发全量同步,确保从节点初始化完整数据:
- 步骤 1:主节点生成 RDB 快照
从节点发送SYNC(Redis 2.8 前)或PSYNC(Redis 2.8 后,支持部分同步)命令,请求同步数据;
主节点收到命令后,执行BGSAVE生成 RDB 快照(后台异步,不阻塞主节点处理写请求),同时将快照生成期间的新写命令存入 “复制缓冲区”(repl buffer)。 - 步骤 2:主节点发送 RDB 快照给从节点
RDB 生成后,主节点将快照文件发送给从节点;
从节点接收完成后,清空本地旧数据,加载 RDB 快照(此过程会阻塞从节点,无法处理读请求)。 - 步骤 3:主节点同步缓冲区命令
从节点加载完 RDB 后,主节点将 “复制缓冲区” 中快照生成期间的新写命令发送给从节点;
从节点执行这些命令,最终与主节点数据完全一致。
3. 增量同步:实时同步新写命令
初始化同步完成后,进入增量同步阶段,主节点实时将新写命令同步给从节点:
- 主节点记录写命令:主节点每处理一个写命令(如
SET、HSET),都会将命令写入 “复制缓冲区”,并记录自己的 “复制偏移量”(offset,累计命令字节数)。 - 从节点确认同步进度:从节点执行完主节点发送的命令后,会向主节点汇报自己的 “复制偏移量”(表示已处理到哪个位置)。
- 主节点按需发送命令:主节点对比自身偏移量与从节点偏移量,将从节点未处理的命令(复制缓冲区中偏移量之后的部分)发送给从节点,从节点执行后完成同步。
4. 状态维护:心跳检测与断线重连
- 心跳检测:
主从连接建立后,从节点每隔 1 秒向主节点发送REPLCONF ACK <offset>命令,包含自己的复制偏移量:- 主节点通过该命令确认从节点存活;
- 主节点对比偏移量,若发现从节点落后,触发增量同步。
- 断线重连:
若网络中断,从节点会定期重试连接主节点;
重连成功后,从节点发送PSYNC命令,主节点通过 “复制积压缓冲区”(repl_backlog_buffer)判断是否可进行增量同步(若从节点偏移量仍在缓冲区范围内,则增量同步;否则触发全量同步)。
三、关键技术:避免全量同步的核心机制
- 复制积压缓冲区:主节点维护一个固定大小的环形缓冲区(默认 1MB,可通过
repl-backlog-size配置),存储最近的写命令;从节点断线重连时,若其偏移量仍在缓冲区范围内,主节点直接发送偏移量后的命令(增量同步),避免全量同步。 PSYNC命令:相比旧版SYNC(仅支持全量同步),PSYNC通过 “主节点运行 ID” 和 “从节点偏移量” 判断同步方式,大幅减少全量同步的频率(全量同步耗时且占用带宽)。
四、总结
Redis 主从复制的核心是 “初始化全量复制 + 实时增量同步”:
- 从节点主动连接主节点,通过
PSYNC触发同步; - 首次同步时,主节点生成 RDB 并发送,同时缓存期间的新命令,从节点加载 RDB 后执行缓存命令;
- 日常通过增量同步,主节点实时发送新写命令,从节点通过偏移量确认进度;
- 心跳检测和断线重连确保连接稳定,复制积压缓冲区减少全量同步开销。
这种机制实现了数据的最终一致性,支撑了读写分离和数据备份,是 Redis 集群方案的基础。
问题:Redis 的核心数据结构及底层实现是什么?
Redis 提供五大核心数据结构(String、List、Hash、Set、Sorted Set),其底层实现会根据数据规模(数量、大小)动态切换,以平衡操作性能与内存效率。核心逻辑是 “小数据用紧凑存储节省内存,大数据用高效结构保证速度”,支撑多样化的业务场景。
一、核心目标
- 适配多样场景:满足字符串存储、列表操作、对象存储、集合运算、有序排序等不同业务需求;
- 平衡性能与内存:小数据采用紧凑结构减少内存占用,大数据采用高效结构保证操作复杂度(多为 O (1) 或 O (log n));
- 支持灵活操作:提供丰富的命令接口(如增删改查、范围查询、集合运算等),覆盖各类业务操作。
二、核心内容:五大数据结构及底层实现
1. String(字符串)
功能:存储字符串、整数或浮点数,支持增删改、自增(
INCR)、拼接(APPEND)、截取(SUBSTR)等操作,是 Redis 最基础的数据结构。底层实现:基于SDS(Simple Dynamic String,简单动态字符串),而非 C 语言原生字符串。
- 结构组成:
len:记录字符串长度(字节数),支持 O (1) 时间获取长度;free:记录未使用的空闲空间(字节数),减少内存重分配;buf:字节数组,存储实际数据(以\0结尾,兼容 C 语言字符串函数)。
- 核心特点:
- 二进制安全:可存储任意二进制数据(如图片、序列化对象),不依赖
\0判断结束; - 预分配空间:修改字符串时,会预分配
free空间(如字符串增长时按 “加倍” 规则扩容),减少频繁内存重分配; - 惰性释放:缩短字符串时,不立即回收多余空间,通过
free记录供后续使用。
- 二进制安全:可存储任意二进制数据(如图片、序列化对象),不依赖
2. List(列表)
功能:有序、可重复的元素集合,支持两端插入(
LPUSH/RPUSH)、两端删除(LPOP/RPOP)、索引访问(LINDEX)、范围查询(LRANGE)等,类似双向链表。底层实现:根据元素规模动态切换两种结构:
- ziplist(压缩列表):
- 触发条件:元素数量≤512 个,且单个元素长度≤64 字节(可通过
list-max-ziplist-entries和list-max-ziplist-value配置)。 - 结构特点:内存连续的数组,元素紧密排列(无指针开销),每个元素前存储 “前一个元素长度” 和 “自身编码”,通过偏移量定位元素。
- 优缺点:节省内存,但插入 / 删除可能触发 “连锁更新”(因内存连续,修改一个元素需调整后续所有元素的偏移量)。
- 触发条件:元素数量≤512 个,且单个元素长度≤64 字节(可通过
- linkedlist(双向链表):
- 触发条件:元素数量或单个元素长度超过 ziplist 阈值。
- 结构特点:每个节点包含
prev(前驱指针)、next(后继指针)和value(元素值),节点不连续存储。 - 优缺点:两端操作效率 O (1),但指针占用额外内存,随机访问效率低(O (n))。
3. Hash(哈希)
功能:键值对集合(field-value),适合存储对象(如用户信息:
name→"张三"、age→20),支持单字段操作(HSET/HGET)、批量操作(HMSET/HMGET)等。底层实现:根据元素规模动态切换两种结构:
ziplist(压缩列表):
- 触发条件:键值对数量≤512 个,且单个 value 长度≤64 字节(可通过
hash-max-ziplist-entries和hash-max-ziplist-value配置)。 - 结构特点:键值对按 “field-value-field-value” 顺序连续存储,通过偏移量定位字段和值。
- 优缺点:内存紧凑,适合小对象存储;但字段越多,查询效率越低(需遍历查找)。
- 触发条件:键值对数量≤512 个,且单个 value 长度≤64 字节(可通过
dict(哈希表):
触发条件:键值对数量或单个 value 长度超过 ziplist 阈值。
结构特点
:类似 Java HashMap,由 “数组 + 链表” 组成:
- 数组:每个元素是一个链表头(解决哈希冲突);
- 哈希函数:通过
hash(field) % 数组长度定位 field 所在链表; - 渐进式 rehash:扩容时不一次性迁移所有数据,而是分批次迁移,避免阻塞主线程。
优缺点:单字段操作效率 O (1),适合大数据量;但内存占用高于 ziplist。
4. Set(集合)
功能:无序、不可重复的元素集合,支持交集(
SINTER)、并集(SUNION)、差集(SDIFF)等运算,适合标签存储、去重场景。底层实现:根据元素类型和规模动态切换两种结构:
- intset(整数集合):
- 触发条件:元素全为整数(int16/int32/int64),且数量≤512 个(可通过
set-max-intset-entries配置)。 - 结构特点:有序数组存储,支持二分查找(O (log n)),元素类型统一(如全为 int16,若插入 int32 则整体升级)。
- 优缺点:内存占用极低,查询高效;但不支持非整数元素,类型升级会消耗额外资源。
- 触发条件:元素全为整数(int16/int32/int64),且数量≤512 个(可通过
- dict(哈希表):
- 触发条件:元素含非整数,或数量超过 intset 阈值。
- 结构特点:键为集合元素,值为
NULL(利用哈希表去重特性),与 Hash 的 dict 结构一致。 - 优缺点:增删查效率 O (1),支持任意类型元素;但内存占用高于 intset。
5. Sorted Set(有序集合)
功能:元素不可重复,关联 “分数(score)” 并按分数排序,支持按分数范围查询(
ZRANGEBYSCORE)、排名查询(ZRANK)等,适合排行榜场景。底层实现:根据元素规模动态切换两种结构:
- ziplist(压缩列表):
- 触发条件:元素数量≤128 个,且单个元素长度≤64 字节(可通过
zset-max-ziplist-entries和zset-max-ziplist-value配置)。 - 结构特点:按 “元素 + 分数” 顺序有序存储(分数从小到大),通过偏移量遍历元素。
- 优缺点:内存紧凑;但范围查询需遍历整个列表(效率低,O (n))。
- 触发条件:元素数量≤128 个,且单个元素长度≤64 字节(可通过
- skiplist(跳表)+ dict(哈希表):
- 触发条件:元素数量或单个元素长度超过 ziplist 阈值。
- 结构特点:
- 跳表:按分数排序,通过多层索引实现快速范围查询(O (log n)),每层索引是下层的子集;
- 哈希表:映射元素到分数,支持快速获取元素分数(O (1))。
- 优缺点:兼顾有序性和查询效率,支持复杂排序操作;但内存占用较高(跳表索引需额外空间)。
三、关键技术:动态切换的核心逻辑
- 阈值驱动:每种结构通过配置参数(如
max-ziplist-entries)定义切换阈值,当数据规模超过阈值时,自动从 “紧凑结构”(ziplist/intset)切换到 “高效结构”(linkedlist/dict/skiplist)。 - 透明转换:切换过程对用户透明,无需手动干预,Redis 内部自动完成数据迁移(如 ziplist 满后转为 linkedlist 时,会将原有元素重新存储)。
四、总结
Redis 核心数据结构的底层实现遵循 “动态适配” 原则:小数据用 ziplist/intset 紧凑存储(省内存),大数据用 linkedlist/dict/skiplist 高效存储(保性能)。这种设计既满足了小数据场景的内存效率,又保证了大数据场景的操作性能,使 Redis 能灵活支撑从简单缓存到复杂排行榜的多样化业务需求。
问题:如何通过 Lua 脚本在 Redis 中实现布隆过滤器?
利用 Redis 的 Lua 脚本可以将布隆过滤器的 “多步哈希 + 位操作” 封装为原子操作,避免手动执行多个命令的并发问题,同时无需依赖 RedisBloom 模块。核心逻辑是通过 Lua 脚本实现哈希函数映射、位设置与检查,基于 Redis 的 BitMap(位图)存储数据。
一、核心目标
- 原子性操作:将 “多哈希 + 多位操作” 封装为单个脚本,确保添加 / 检查元素的原子性,避免并发冲突;
- 简化调用:通过脚本参数动态传入布隆过滤器配置(位数组长度、哈希函数数量),无需手动执行多次
SETBIT/GETBIT; - 轻量实现:不依赖外部模块,仅使用 Redis 原生 BitMap 和 Lua 脚本功能,适合轻量化场景。
二、实现步骤
1. 设计哈希函数
布隆过滤器需要多个独立的哈希函数,将元素映射到位数组的不同索引。Lua 脚本中可通过以下方式实现简单哈希(示例采用简化的 MurmurHash 思路,实际可根据需求替换):
1
2
3
4
5
6
7
8
9
10-- 简化的哈希函数,返回哈希值(需根据元素和种子生成不同结果)
local function hash(element, seed, max_bits)
local hash = 0
for i = 1, #element do
hash = (hash * 31 + string.byte(element, i)) % max_bits
end
-- 结合种子增加哈希多样性
hash = (hash + seed) % max_bits
return hash
end2. 实现添加元素的 Lua 脚本
脚本功能:接收布隆过滤器键名、元素、位数组长度(m)、哈希函数数量(k),对元素执行 k 次哈希,设置对应位为 1。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24-- 布隆过滤器添加元素脚本
-- 参数:KEYS[1] = 布隆过滤器键名,ARGV[1] = 元素,ARGV[2] = 位数组长度(m),ARGV[3] = 哈希函数数量(k)
local key = KEYS[1]
local element = ARGV[1]
local max_bits = tonumber(ARGV[2])
local k = tonumber(ARGV[3])
-- 哈希函数(同上)
local function hash(element, seed, max_bits)
local hash = 0
for i = 1, #element do
hash = (hash * 31 + string.byte(element, i)) % max_bits
end
hash = (hash + seed) % max_bits
return hash
end
-- 执行k次哈希并设置位
for seed = 1, k do
local index = hash(element, seed, max_bits)
redis.call('SETBIT', key, index, 1)
end
return 1 -- 返回成功标识3. 实现检查元素的 Lua 脚本
脚本功能:接收相同参数,对元素执行 k 次哈希,检查所有对应位是否为 1(全为 1 则可能存在,否则一定不存在)。
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-- 布隆过滤器检查元素脚本
-- 参数:KEYS[1] = 布隆过滤器键名,ARGV[1] = 元素,ARGV[2] = 位数组长度(m),ARGV[3] = 哈希函数数量(k)
local key = KEYS[1]
local element = ARGV[1]
local max_bits = tonumber(ARGV[2])
local k = tonumber(ARGV[3])
-- 哈希函数(同上)
local function hash(element, seed, max_bits)
local hash = 0
for i = 1, #element do
hash = (hash * 31 + string.byte(element, i)) % max_bits
end
hash = (hash + seed) % max_bits
return hash
end
-- 执行k次哈希并检查位
for seed = 1, k do
local index = hash(element, seed, max_bits)
if redis.call('GETBIT', key, index) == 0 then
return 0 -- 存在位为0,一定不存在
end
end
return 1 -- 所有位为1,可能存在(假阳性可能)4. 调用 Lua 脚本(Redis 客户端)
添加元素:通过
EVAL命令执行添加脚本,参数为布隆过滤器键名、元素、m、k。
示例(m=1000000,k=7,添加元素 “user123”):1
EVAL "【添加脚本内容】" 1 bloom_filter "user123" 1000000 7
检查元素:执行检查脚本,返回 1 表示可能存在,0 表示一定不存在。
示例(检查 “user123”):1
EVAL "【检查脚本内容】" 1 bloom_filter "user123" 1000000 7
三、关键参数与假阳性率控制
- 位数组长度(m):越大,假阳性率越低,但内存占用越高,需根据预期元素数量(n)和可接受假阳性率(p)计算:
m ≈ -n * ln(p) / (ln(2))² - 哈希函数数量(k):最优值为
k ≈ m/n * ln(2),过多会增加计算量和位冲突,过少会提高假阳性率。
四、优缺点
- 优点:
- 无需安装额外模块,依赖 Redis 原生功能,部署简单;
- 脚本保证操作原子性,避免并发场景下的位操作冲突;
- 可灵活自定义哈希函数和参数,适配不同场景。
- 缺点:
- 哈希函数实现简单(示例为简化版),可能导致分布不均,需优化哈希算法;
- 不支持动态扩容,元素数量超过预期时假阳性率会骤升;
- 脚本执行耗时随 k 增大而增加,可能阻塞 Redis(需控制 k 值,建议 k≤10)。
五、总结
通过 Lua 脚本实现 Redis 布隆过滤器的核心是用脚本封装多哈希 + 位操作的原子逻辑,基于 BitMap 存储映射结果。适合轻量化场景(如中小规模数据去重、缓存穿透防护),且不希望依赖外部模块时使用。实际应用中需根据数据量精心设计 m 和 k,优化哈希函数分布,并控制脚本复杂度以避免性能问题。
问题:Redis 的 Bitmap、GeoHash、HyperLogLog、Streams 这几种数据结构的功能、底层实现及适用场景是什么?
除了五大核心数据结构,Redis 还提供了几种特殊数据结构,分别针对位操作、地理位置、基数统计、消息队列等场景优化,兼顾性能与内存效率。
一、Bitmap(位图)
功能:通过二进制位(bit)存储数据,用于高效处理 “是 / 否” 类型的标记(如 “用户是否签到”“设备是否在线”),支持位级别的逻辑运算。
1. 底层实现
基于String(SDS) 实现:Redis 的字符串(SDS)是二进制安全的字节数组,Bitmap 通过对字节数组的位操作(如第 n 位的 0/1)实现功能。例如,一个长度为 100 字节的字符串可表示 800 个二进制位(1 字节 = 8 位),直接通过位索引操作。
2. 核心命令
- **
SETBIT key offset value**:设置指定偏移量(offset)的位值(0 或 1)。
示例:SETBIT sign:user:100 5 1→ 用户 100 在第 5 天签到(位 5 设为 1)。 - **
GETBIT key offset**:获取指定偏移量的位值。
示例:GETBIT sign:user:100 5→ 返回 1(已签到)。 - **
BITCOUNT key [start end]**:统计指定范围内的 “1” 的个数(默认统计全部)。
示例:BITCOUNT sign:user:100→ 统计用户 100 的总签到天数。 - **
BITOP op destkey key1 [key2...]**:对多个 Bitmap 执行位运算(AND/OR/XOR/NOT),结果存入 destkey。
示例:BITOP AND active:users sign:user:100 sign:user:101→ 求两个用户共同签到的天数。
3. 适用场景
- 用户行为标记:如签到记录(1 位 / 天,1 年仅需 46 字节)、在线状态(1 位 / 用户,100 万用户仅需 125KB);
- 数据压缩存储:如黑白名单(用位标记 ID 是否在名单中);
- 快速统计与交集:如统计 “连续签到 7 天的用户”(通过 BITOP AND 多个日期的 Bitmap)。
4. 优缺点
- 优点:内存效率极高(1 位 / 状态),位运算速度快(底层是连续内存操作);
- 缺点:偏移量过大时(如 offset=1 亿)会占用额外内存(即使高位全为 0),需合理规划 key 的粒度。
二、GeoHash(地理位置)
功能:存储地理位置信息(经纬度),支持距离计算、范围查询(如 “查找附近 1km 的商家”),本质是对地理坐标的编码与索引。
1. 底层实现
基于Sorted Set实现:
- 经纬度(longitude, latitude)通过 GeoHash 算法编码为一个 64 位整数(作为 ZSet 的 “分数”),元素值为地理位置 ID;
- 编码原理:将地球表面划分为网格,通过二分法对经纬度递归划分,用二进制表示网格位置,最终合并为一个整数(值越接近,地理位置越近)。
2. 核心命令
- **
GEOADD key longitude latitude member [longitude latitude member...]**:添加地理位置。
示例:GEOADD shops 116.403874 39.914885 "shop1"→ 添加 “shop1” 的坐标(北京天安门附近)。 - **
GEODIST key member1 member2 [unit]**:计算两个位置的直线距离(unit 支持 m/km/mi/ft)。
示例:GEODIST shops shop1 shop2 km→ 计算 shop1 与 shop2 的距离(公里)。 - **
GEORADIUS key longitude latitude radius unit [WITHCOORD] [WITHDIST] [COUNT count]**:根据坐标查询范围内的位置。
示例:GEORADIUS shops 116.403874 39.914885 1 km WITHCOORD WITHDIST→ 查找天安门 1km 内的商家及坐标、距离。 - **
GEOPOS key member [member...]**:获取指定位置的经纬度。
3. 适用场景
- LBS 服务:如 “附近的人”“周边商家”“同城配送范围判断”;
- 地理围栏:如 “用户进入某区域时触发通知”(结合 GEORADIUS 定时查询)。
4. 优缺点
- 优点:复用 Sorted Set 的高效排序能力,范围查询复杂度 O (log n);
- 缺点:GeoHash 编码存在 “边缘误差”(相邻网格的编码可能不连续),高精度场景需结合其他算法修正。
三、HyperLogLog(基数统计)
功能:用于估算 “基数”(集合中不重复元素的个数,如独立用户数 UV),以极小的内存占用(约 12KB)支持海量数据统计,允许约 0.81% 的误差。
1. 底层实现
基于概率算法和哈希函数:
- 核心原理:通过哈希函数将元素映射为随机二进制串,统计 “最长连续前导 0 的个数”(如某元素哈希后为 “000101”,前导 0 长度为 3);
- 用多个 “寄存器” 存储不同分组的最长前导 0 长度,通过调和平均估算整体基数(公式:
基数 ≈ 常数 × 2^平均前导0长度)。
2. 核心命令
- **
PFADD key element [element...]**:向 HyperLogLog 添加元素(重复元素不影响结果)。
示例:PFADD uv:20231001 "user1" "user2" "user1"→ 统计 2023-10-01 的 UV(实际基数为 2)。 - **
PFCOUNT key [key...]**:估算基数(返回近似值)。
示例:PFCOUNT uv:20231001→ 返回 2(误差范围内)。 - **
PFMERGE destkey sourcekey [sourcekey...]**:合并多个 HyperLogLog,结果存入 destkey(基数为所有源集合的并集估算)。
示例:PFMERGE uv:202310 uv:20231001 uv:20231002→ 合并 10 月 1 日和 2 日的 UV。
3. 适用场景
- 海量数据基数统计:如网站 UV(独立访客)、APP 日活(DAU)、搜索关键词去重次数;
- 资源受限场景:替代 Set(存储 100 万独立元素需约 16MB,HyperLogLog 仅需 12KB)。
4. 优缺点
- 优点:内存占用极低(固定 12KB),支持海量数据,合并操作高效;
- 缺点:存在约 0.81% 的误差(不适合精确统计,如金融交易笔数),不存储原始元素(无法查询具体元素)。
四、Streams(流)
功能:Redis 5.0 新增的消息队列数据结构,支持持久化消息、多消费者组、消息确认(ACK)、回溯消费等,适合分布式消息传递场景。
1. 底层实现
基于日志结构的双向链表:
- 每条消息包含唯一 ID(格式:
时间戳-序列号,如1696666666-0,确保有序性和唯一性); - 消息内容为 Hash 结构(field-value 键值对);
- 支持 “消费者组”(Consumer Group)机制:每个组维护自己的消费偏移量,同组内消费者竞争消费,不同组可独立消费同批消息。
2. 核心命令
- **
XADD key [MAXLEN [~] count] \*|ID field value [field value...]**:添加消息到流(*表示自动生成 ID,MAXLEN限制消息最大数量)。
示例:XADD order_log * user 100 goods "phone"→ 向 order_log 流添加一条消息(自动生成 ID)。 - **
XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key...] ID [ID...]**:读取消息(支持阻塞等待新消息)。
示例:XREAD BLOCK 5000 STREAMS order_log $→ 阻塞 5 秒,读取 order_log 流中最新的消息($表示从末尾开始)。 - **
XGROUP CREATE key groupname ID**:创建消费者组(ID 为起始消费位置,$表示从最新消息开始)。
示例:XGROUP CREATE order_log group1 $→ 为 order_log 流创建 group1 消费组。 - **
XREADGROUP GROUP groupname consumername [COUNT count] [BLOCK milliseconds] STREAMS key [key...] >**:消费者组内读取消息(>表示未被组内消费的消息)。
示例:XREADGROUP GROUP group1 consumer1 STREAMS order_log >→ group1 的 consumer1 消费新消息。 - **
XACK key groupname ID [ID...]**:确认消息已处理(从组内 pending 列表移除)。
3. 适用场景
- 分布式消息队列:如订单状态变更通知、日志收集(替代 Kafka 轻量场景);
- 多角色消费:不同消费者组处理同一批消息的不同逻辑(如订单流同时被 “库存组” 和 “支付组” 消费);
- 可靠消息传递:支持消息持久化和 ACK,避免消息丢失。
4. 优缺点
- 优点:支持持久化(消息不会因 Redis 重启丢失),多组消费灵活,消息可回溯(通过 ID 重新消费);
- 缺点:相比 List(简单队列),命令复杂度高,高吞吐场景性能略低于专业消息队列(如 Kafka)。
五、总结
数据结构 核心能力 底层依赖 典型场景 核心优势 Bitmap 位级标记与统计 String 签到、在线状态、黑白名单 内存效率极高(1 位 / 状态) GeoHash 地理位置存储与范围查询 Sorted Set 附近的人、LBS 服务 复用 ZSet 高效排序能力 HyperLogLog 海量数据基数估算 概率算法 UV/DAU 统计、关键词去重 内存占用极低(12KB) Streams 持久化消息队列 日志结构链表 分布式消息传递、多组消费 支持 ACK、多组独立消费 这些结构均针对特定场景优化,实际使用需根据 “精度要求”“内存限制”“功能需求” 选择 —— 例如,精确统计用 Set,模糊统计用 HyperLogLog;简单队列用 List,复杂多组消费用 Streams。
- **
问题:Redis 事务的实现原理是什么?
Redis 事务是一组命令的集合,通过 “批量执行 + 顺序保证” 实现基本的原子性操作,核心是确保事务中的命令要么全部执行(即使部分命令出错),要么全部不执行(因入队错误或并发修改)。其实现依赖 “事务队列” 和 “乐观锁监控” 机制,与传统数据库事务的 ACID 特性有显著差异。
一、核心目标
- 批量执行:将多个命令打包为一个整体,按入队顺序依次执行,避免中间被其他客户端命令插入;
- 基础原子性:事务中的命令要么全部执行(无入队错误且未被并发修改打断),要么全部不执行(入队错误或被
WATCH机制打断); - 并发控制:通过
WATCH机制监控关键键,防止事务执行时数据已被其他客户端修改,实现类似乐观锁的效果。
二、核心原理:四阶段执行流程
1. 开启事务(
MULTI命令)作用:标记当前客户端进入 “事务上下文”,后续命令不再立即执行,而是进入事务队列;
底层操作:Redis 为当前客户端创建一个空的 “事务命令队列”(链表结构),用于缓存后续命令;
示例:
1
2127.0.0.1:6379> MULTI # 开启事务
OK # 进入事务模式
2. 命令入队(事务内命令)
作用:客户端发送的所有命令(如
SET、INCR、HSET等)被缓存到事务队列,而非立即执行;入队检查
:Redis 会对命令进行
语法校验
(如命令是否存在、参数数量是否正确):
- 若语法错误,返回具体错误信息(如
(error) ERR unknown command 'XXX'),且后续EXEC执行时整个事务会被放弃; - 若语法正确,返回
QUEUED,表示成功入队;
- 若语法错误,返回具体错误信息(如
示例:
1
2
3
4
5
6127.0.0.1:6379> SET user:100 "Alice" # 入队,语法正确
QUEUED
127.0.0.1:6379> INCR score:100 # 入队,语法正确
QUEUED
127.0.0.1:6379> INCR user:100 # 入队,语法正确(运行时可能出错,但入队不检查)
QUEUED
3. 执行事务(
EXEC命令)作用:触发事务队列中所有命令的执行,按入队顺序依次执行,执行结果按顺序返回;
执行逻辑:
若事务队列中存在语法错误(如步骤 2 中返回错误的命令),Redis 直接清空队列,返回
(error) EXECABORT Transaction discarded because of previous errors.,事务不执行;若存在
1
WATCH
监控的键,先检查这些键是否被其他客户端修改:
- 若已被修改,事务被打断,返回
(nil),所有命令不执行; - 若未被修改,继续执行;
- 若已被修改,事务被打断,返回
按入队顺序依次执行所有命令,将每个命令的结果存入结果数组;
执行完毕后,清空事务队列,退出事务上下文;
示例
(无错误且未被
1
WATCH
打断):
1
2
3
4127.0.0.1:6379> EXEC # 执行事务
1) OK # SET命令结果
2) (integer) 1 # INCR score:100结果
3) (error) ERR value is not an integer or out of range # INCR user:100运行时错误(但仍执行)
4. 取消事务(
DISCARD命令)作用:终止当前事务,清空事务队列,退出事务上下文,所有入队命令均不执行;
示例:
1
2
3
4
5
6
7
8127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET a 100
QUEUED
127.0.0.1:6379> DISCARD # 取消事务
OK
127.0.0.1:6379> EXEC # 事务已取消,执行无效
(error) ERR EXEC without MULTI
5. 乐观锁监控(
WATCH命令)作用:在事务执行前监控一个或多个键,若这些键在
WATCH后、EXEC前被其他客户端修改,则事务被打断(EXEC返回nil);底层原理:
WATCH key1 key2...会记录这些键当前的 “版本号”(Redis 通过键的 “修改次数” 隐式标记);EXECWATCH1
2
3
执行时,Redis 对比这些键的当前版本号与1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
时的版本号:
- 若版本号一致(未被修改),执行事务;
- 若任一键版本号不一致(已被修改),放弃事务;
- 示例:
```bash
# 客户端A
127.0.0.1:6379> WATCH stock # 监控库存键
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> DECR stock # 入队:减少库存
QUEUED
# 客户端B同时修改stock
127.0.0.1:6379> SET stock 99 # 修改被监控的键
# 客户端A执行事务(因stock被修改,事务被打断)
127.0.0.1:6379> EXEC
(nil) # 事务未执行
三、关键特性与局限性
1. 原子性的特殊性
- 传统数据库事务:要么全成功,要么全回滚;
- Redis 事务:
- 若存在语法错误(入队时检查),事务全不执行;
- 若存在运行时错误(如对字符串执行
INCR),错误命令返回异常,但其他命令仍会执行(无回滚机制); - 本质是 “按顺序执行的批量命令”,而非严格的原子性。
2. 隔离性的简化
- 事务执行期间,其他客户端的命令不会插入到事务中间(保证顺序性),但可以并行修改事务外的键;
- 若需隔离事务涉及的键,需通过
WATCH手动实现(类似乐观锁)。
3. 无持久性保证
- Redis 事务的持久性依赖 Redis 的持久化配置(RDB/AOF),事务本身不额外提供持久性保证;
- 若事务执行后 Redis 宕机,未持久化的命令可能丢失。
四、总结
Redis 事务通过 “
MULTI开启→命令入队→EXEC执行 /DISCARD取消” 的流程,结合WATCH的乐观锁机制,实现了 “批量命令按顺序执行” 的基础原子性。其核心价值在于简化批量操作和处理并发修改,但不支持传统事务的回滚和完整隔离性。适用场景:需批量执行多个命令且需防止并发修改的场景(如库存扣减、余额转账);不适用场景:需严格原子性(如金融交易的精确回滚)。
问题:为什么要使用缓存?
缓存是系统设计中用于临时存储高频访问数据的组件,核心目标是通过 “空间换时间” 优化系统性能,解决数据访问效率低、后端存储压力大等问题。其必要性可从以下几个核心场景展开:
1. 加速数据访问,降低延迟
- 底层原因:数据的存储介质速度差异极大 —— 内存(缓存常用介质)的读写速度通常是磁盘(如数据库、文件系统)的10 万~100 万倍(内存微秒级,磁盘毫秒级)。
- 效果:对于高频访问的数据(如用户信息、商品详情),缓存可将数据从磁盘 “迁移” 到内存,让请求跳过缓慢的磁盘 IO,直接从内存读取,显著降低响应时间(例如:从数据库查询需 100ms,缓存查询仅需 1ms)。
2. 减轻后端存储压力,避免过载
- 问题:后端存储(如数据库、分布式文件系统)的并发处理能力有限(例如:MySQL 单机每秒能处理的查询通常在万级以内),若大量请求直接访问,会导致存储负载过高,出现响应变慢、连接超时甚至崩溃。
- 缓存的作用:作为 “请求拦截器”,缓存可承接大部分高频请求(例如:80% 的请求访问 20% 的热点数据),大幅减少对后端存储的直接访问次数。例如:一个日均 1000 万请求的电商网站,若缓存命中率达 90%,则数据库仅需处理 100 万请求,压力降低 90%。
3. 提升系统并发能力与吞吐量
- 原理:缓存(尤其是分布式缓存如 Redis、Memcached)的并发处理能力远高于后端存储(内存操作支持更高的并发量)。例如:Redis 单机可轻松支持每秒 10 万 + 请求,而同等配置的数据库可能仅支持 1 万 +。
- 效果:通过缓存承接高频请求,系统能处理更多并发用户,吞吐量(单位时间处理的请求数)显著提升。例如:秒杀场景中,缓存可快速返回商品库存状态,避免大量请求冲击数据库。
4. 优化用户体验,减少等待
- 用户感知:延迟直接影响用户体验 —— 研究表明,网页加载延迟每增加 1 秒,用户流失率可能上升 7%。
- 缓存的作用:通过加速数据返回(如页面静态资源、API 响应结果),减少用户等待时间。例如:CDN 缓存静态图片后,用户打开网页的时间从 3 秒缩短到 0.5 秒,体验显著提升。
5. 减少重复计算 / 读取,节约资源
- 场景:部分数据需要通过复杂计算生成(如实时统计报表、复杂算法结果),重复计算会消耗大量 CPU / 内存资源。
- 缓存的作用:将计算结果临时存储,后续请求直接复用结果,避免重复计算。例如:某数据分析接口需扫描 100 万条数据计算结果(耗时 5 秒),缓存后可直接返回结果(耗时 0.1 秒),节省 98% 的计算资源。
6. 支持分布式 / 异地访问场景
- 问题:分布式系统中,数据可能存储在异地机房,跨机房访问的网络延迟高(例如:跨洲际访问延迟可达 100ms+)。
- 缓存的作用:通过分布式缓存(如 Redis Cluster)或 CDN(内容分发网络),在用户就近的节点缓存数据,减少跨网络访问。例如:全球用户访问某视频网站时,CDN 在各地区节点缓存视频片段,用户直接从本地节点加载,避免跨洋数据传输。
总结
缓存的核心价值是通过 “牺牲部分存储空间”,换取更快的访问速度、更低的后端压力、更高的系统并发能力,最终优化用户体验并降低系统资源消耗。它是应对高并发、大数据场景的 “标配” 技术,也是系统性能优化的关键手段之一。