面试题-Redis

面试题-Redis

数据结构

Redis数据结构有哪些

String、Hash、List、Set、Zset、GEO、Bitmap、HyperLogLog、Stream

String(字符串)
String 是 Redis 最基础的数据结构,本质是字节序列,可存储文本、数字甚至二进制数据(如图片),最大容量为 512MB。它不仅支持简单的键值存储,还提供了丰富的操作:比如对数字进行自增(INCR)、自减(DECR),实现计数器功能;通过APPEND追加内容;用SUBSTR截取子串等。典型场景包括存储用户 Token、商品库存(用INCRBY/DECRBY原子操作更新)、简单的缓存数据等。

Hash(哈希)
Hash 适用于存储 “对象型” 数据,结构类似 Java 中的 HashMap,由键值对(field-value)组成,每个 Hash 可包含多个字段,字段和值均为字符串。它支持单独操作某个字段(如HSET设置字段、HGET获取字段值、HDEL删除字段),也可批量操作(如HMSET批量设置、HMGET批量获取)。由于只需传输或操作单个字段而非整个对象,Hash 比 String 更节省带宽,适合存储用户信息(如user:1000 {name: "张三", age: "20"})、商品属性等。

List(列表)
List 是有序的字符串集合,底层通过双向链表或压缩列表实现,支持在两端高效操作元素:左侧插入(LPUSH)、右侧插入(RPUSH)、左侧弹出(LPOP)、右侧弹出(RPOP),时间复杂度均为 O (1)。也可通过LRANGE获取指定范围的元素(如LRANGE list 0 9获取前 10 个元素)。由于有序且支持两端操作,List 常用作消息队列(用LPUSH生产、RPOP消费)、栈(LPUSH+LPOP)、最新消息列表(如朋友圈动态,只保留最近 N 条)等。

Set(集合)
Set 是无序的字符串集合,元素唯一(自动去重),底层用哈希表或整数集合实现,支持高效的增删查操作(O (1)),以及集合运算:交集(SINTER,如共同好友)、并集(SUNION,如合并两个标签的用户)、差集(SDIFF,如 A 有 B 没有的元素)。典型场景包括存储用户标签(避免重复)、抽奖系统(SRANDMEMBER随机取元素)、去重统计(如 UV 计数的中间存储)等。

Zset(有序集合)
Sorted Set 在 Set 的基础上,为每个元素关联一个 “分数(score)”,并按分数从小到大排序(分数可重复,元素唯一)。底层用跳表实现,支持快速插入(ZADD)、按分数范围查询(ZRANGEBYSCORE)、按排名查询(ZRANGE取前 N 名)、按元素获取排名(ZRANK)等操作。由于有序且支持范围查询,常用于排行榜(如游戏积分排名)、带权重的消息队列(按优先级消费)、时间线数据(用时间戳作为分数)等。

Bitmap(位图)
Bitmap 并非独立数据结构,而是 String 的特殊用法:将字符串视为二进制位的数组,通过位操作(SETBIT设置某一位、GETBIT获取某一位、BITCOUNT统计 1 的个数)处理数据。它极大节省空间(1 字节可存储 8 个布尔值),适合存储 “是 / 否” 型数据,如用户签到(每天用一位表示,365 天仅需 46 字节)、活跃用户标记(某时段是否登录)等。

HyperLogLog
HyperLogLog 用于 “基数统计”(估算集合中不重复元素的个数),只需占用约 12KB 内存,就能统计上亿级别的数据,误差率约 0.81%。它不存储具体元素,只记录统计信息,支持PFADD添加元素、PFCOUNT获取基数、PFMERGE合并多个 HyperLogLog。适合 UV 统计(如网站独立访客数)、搜索关键词去重计数等场景,无需精确结果但需节省空间时非常实用。

Geo(地理空间)
Geospatial 用于存储地理位置信息(经纬度),支持添加位置(GEOADD)、计算两点距离(GEODIST)、根据坐标范围查询元素(GEORADIUS)、根据元素查询坐标(GEOPOS)等操作。底层通过 Sorted Set 实现(用经纬度编码为分数),适合 LBS 服务,如 “附近的人”“范围内的店铺” 等场景。

Stream
Stream 是 Redis 5.0 新增的消息队列结构,支持持久化、消费组(Consumer Group)、消息确认(ACK)等特性,类似 Kafka 的轻量版。它按时间戳有序存储消息,每个消息有唯一 ID,消费组可实现消息分片消费,避免重复处理。适合需要可靠消息传递的场景,如分布式系统的事件通知、日志收集等。

Redis数据结构的应用场景

String:缓存对象、常规计数、库存信息、共享Session信息、分布式锁。

List:最新消息信息、消息队列(消息可能丢失可靠性差,功能不完整,无消息确认机制、无消息优先级、无死信队列)。

Hash:缓存对象。

Set:点赞、共同关注、抽奖活动。

Zset:排序场景,排行榜、时间线数据、带权重的消息队列。

BitMap:比如用户签到。

HyperLogLog:百万级网页UV计数。

GEO:存储地理位置、计算两点距离、根据坐标范围查询元素。

Stream:消息队列(支持持久化、消费者组、消息确认)。

Zset用过吗,底层是怎么实现的

Redis中Zset可以实现排行榜、带权重的消息队列等,Zset底层有两种实现:当数据量小(默认128)并且每个元素的键和值都比较小(默认64),会使用ziplist,但元素数量或大小超过阈值时,会转为skiplist+dict的底层实现,skiplist负责分数排序和范围查询(ZRange),字典负责快速精确映射,二者共享数据实体。

为什么还需要dict?

若仅用skiplist实现Zset会导致通过member查找score要遍历跳表,而dict可以将这一复杂度降到O(1)。

跳表的底层实现

用于Zset的实现,跳表底层是由数据节点形成的链表和多级指针组成,每个节点包括元素值和分数,还有不提供层级中指向下一个指针,指针指向同层级的下一个指针,层级由概率随机生成,查询时,从表头的最高层级开始,沿着同级指针链前进,当下一个节点的分数大于目标分数或达到尾部,则降低到当前节点的低一层,继续同样的流程,直到找到目标元素,这样设计可以使层级越高跳过的非目标元素越多,最终的复杂度为O(log n)。

跳表的的层级是怎么生成的

跳表的层级是通过随机算法生成的,核心是 “概率性提升”。新节点创建时,初始层级为 1,然后通过类似 “抛硬币” 的方式决定是否提升层级:每次生成一个随机数,若小于预设概率(Redis 中为 50%),则层级加 1,重复此过程,直到随机数不满足条件或达到最大层级(Redis 默认最大层级为 32)。这种随机策略能保证层级分布较为均匀,上层节点数量约为下层的一半,从而维持跳表的查询效率 —— 层级越高的节点,越能快速跳过更多元素,平衡查询路径的长度。

Redis的跳表为什么不用B+树或红黑树替代

Redis 的跳表没有用 B + 树替代,主要出于实现复杂度和操作效率的考虑。跳表的插入、删除操作只需调整相关的局部指针,逻辑简单,无需像 B + 树那样通过分裂维护平衡性,而且B+树节点预分配可能导致内存浪费,B+树的特性更适合磁盘IO。

红黑树为了维持 “平衡”,需要复杂的旋转和颜色调整操作,这会显著增加插入、删除场景的实现难度与执行开销。

ziplist是怎么实现的

ziplist 是 Redis 为 Zset、list(版本)、Hash 在 “小数据量集合” 设计的紧凑内存存储结构,核心目标是通过连续内存布局减少指针开销,节省内存。连续内存存储,没有冗余指针,内存利用率极高。

头部包含 4 个字段:zlbytes(4 字节,记录 ziplist 总字节数,用于快速计算内存大小)、zltail(4 字节,记录最后一个 entry 的偏移量,支持从尾部反向遍历)、zllen(2 字节,记录 entry 数量,若超过 65535 则需遍历所有 entry 统计)、zlend(1 字节,固定值 0xFF,标记 ziplist 结束)。每个 entry 的结构则根据数据类型(整数或字符串)和长度动态调整,包含prevlen(记录前一个 entry 的长度,用于反向遍历,长度 1 或 5 字节,根据前一个 entry 大小决定)、encoding(1 或 2 字节,标记数据类型和长度,比如 “00xxxxxx” 表示短字符串,“11000000” 表示 32 位整数)、data(存储实际数据,字符串直接存字节,整数则按编码压缩存储)。这种动态结构让不同大小的数据都能紧凑存储,比如存储小整数时,data字段仅占 1-4 字节,远少于普通链表的指针开销。但 ziplist 的缺点也很明显:插入或删除中间元素时,需要移动后续所有 entry 的内存,时间复杂度 O (n),因此仅适合元素数量少(默认 List/Hash/Zset 的阈值分别为 512、512、128)的场景,数据量超阈值后会转为链表或跳表等结构。

说一下Redis的listpack

listpack 是 Redis 5.0 + 推出的ziplist 替代结构,核心解决 ziplist 的 “连锁更新” 缺陷,同时保留紧凑存储的优势,目前已逐步替代 ziplist,成为 Hash、Zset 等结构的底层实现之一。

ziplist 的 “连锁更新” 源于prevlen字段:若一个 entry 的长度从 < 254 字节变为≥254 字节,其后续所有 entry 的prevlen(原本占 1 字节)都需从 1 字节扩容为 5 字节,引发连续的内存调整;反之则需缩容,同样可能连锁触发。而 listpack 通过重构 entry 结构解决了这一问题:listpack 的每个 entry 不再有prevlen,只保留节点自身的len并放在尾部,且len的长度固定为 1 或 2 字节(1 字节存≤127 字节的长度,2 字节存≤65535 字节的长度,超过则不支持)。反向遍历时:通过头部的尾指针(tail offset),指向最后一个元素的起始位置,再通过每个元素的len字段,反向计算前一个元素的起始位置。此外,listpack 的 entry 编码更高效,支持更多数据类型的压缩存储,比如对短字符串的编码位数进一步优化,内存利用率比 ziplist 更高。目前 listpack 已在 Redis 的 Hash、Zset 中替代 ziplist,彻底解决级联更新带来的性能问题。

哈希表的扩容流程

Redis 的哈希表(dict)基于 “链地址法” 解决冲突,底层用两个 ht 结构(ht [0]、ht [1])实现渐进式扩容,Redis单线程实现,避免一次性迁移数据阻塞业务,当负载因子 >1,此时 ht [1] 的容量会设为第一个大于等于已存元素数*2 的 2 的幂;缩容的触发条件是负载因子 < 0.1,此时 ht [1] 的容量同样设为第一个大于等于 used 的 2 的幂。然后初始化 ht [1]:分配对应容量的数组,接下来是核心的 “渐进式迁移”:设置rehashidx=0(标记迁移开始),之后每次对哈希表执行增删改查操作时,都会顺带将 ht [0] 中rehashidx索引对应的 bucket(桶)内所有 entry 迁移到 ht [1],迁移完成后rehashidx自增 1;同时,Redis 还会在定时任务中批量迁移部分桶,加速迁移进程。最后,当 ht [0] 的所有桶都迁移到 ht [1] 后,将 ht [1] 赋值给 ht [0],重置 ht [1] 为空,rehashidx=-1(标记迁移结束),扩容流程完成。这种渐进式方式确保迁移过程中哈希表仍能正常服务,避免了一次扩容阻塞业务。

扩容时的增删改查怎么完成

查询操作:先在 ht [0] 中计算哈希索引,遍历对应 bucket 的链表查找目标 entry;若未找到,且处于 rehash 阶段(rehashidx≠-1),则再到 ht [1] 的对应 bucket 中查找,最终返回结果。这样能确保即使部分数据已迁移到 ht [1],也能正确查询到。

插入操作:若处于 rehash 阶段,新 entry 会直接插入到 ht [1],而非 ht [0],确保 ht [0] 的数据只减不增,加速迁移。

删除操作:需同时在 ht [0] 和 ht [1] 中查找目标 entry,找到后即删除(若两个表都存在则都删除),确保不会因数据分布在两个表中导致删除不彻底。

修改操作:先在 ht [0] 中查找,找到则直接修改;若未找到且处于 rehash 阶段,再到 ht [1] 中查找,找到后修改;若都未找到,则按插入逻辑处理(插入到 ht [1])。

整个过程中,所有操作都会顺带触发rehashidx对应的 bucket 迁移,确保 rehash 持续推进,同时不影响业务操作的正确性和性能。

Redis中String的底层实现

Redis用C语言实现,但Redis的 String 并非 C 语言原生字符串(字符数组),而是自定义的SDS。

SDS 相比 C 字符串有五大优势:一是快速获取长度,通过len字段可 O (1) 获取长度,而 C 字符串需遍历到 ‘\0’,时间复杂度 O (n),这对 Redis 频繁执行STRLLEN等命令至关重要;二是避免缓冲区溢出,执行sdscat(追加字符串)时,会先检查alloc - len是否足够,不足则自动扩容,而 C 字符串的strcat可能越界;三是内存预分配和惰性空间释放,避免频繁内存分配;四是二进制安全,SDS 不依赖 ‘\0’ 判断结束,buf可存储图片、视频等二进制数据,而 C 字符串会将 ‘\0’ 视为结束符,无法存储含 ‘\0’ 的二进制数据。这些特性让 SDS 完美适配 Redis 的 String 场景,比如存储用户 Token、商品库存、二进制文件等。

为什么不用c的字符串

长度获取效率低。C 字符串是 “以 ‘\0’ 结尾的字符数组”,获取长度需遍历数组直到 ‘\0’,时间复杂度 O (n)。而 Redis 频繁需要获取字符串长度(如STRLLEN命令、内存回收、扩容判断等),O (n) 的效率会严重拖累性能 —— 比如存储 1MB 的字符串,C 字符串获取长度需遍历 100 万次,而 SDS 通过len字段可 O (1) 完成,差距悬殊。

其次是缓冲区溢出风险高。C 字符串的strcatstrcpy等函数不检查目标数组的剩余空间,若拼接的字符串超过数组容量,会直接越界覆盖相邻内存,导致程序崩溃或数据损坏。Redis 作为高性能数据库,需处理大量动态字符串拼接(如APPEND命令),C 字符串的溢出风险完全无法接受,而 SDS 的自动扩容机制能彻底规避这一问题。

第三是不支持二进制安全存储。C 字符串以 ‘\0’ 作为结束标识,若存储的二进制数据(如图片、压缩文件)中包含 ‘\0’,会被误判为字符串结束,导致数据截断。而 Redis 的 String 需要支持二进制数据存储(如用户上传的头像二进制流),C 字符串的这一缺陷直接限制了使用场景,SDS 通过len字段判断长度,完美解决二进制安全问题。

最后是内存管理繁琐且低效。C 字符串的修改(如追加、修剪)需要手动分配和释放内存,频繁修改会导致大量内存碎片,且容易出现 “内存泄漏” 或 “重复释放” 的 bug。Redis 的 String 修改频率极高(如INCR自增、APPEND追加),依赖手动内存管理会大幅增加开发复杂度和运行风险,而 SDS 的预分配、惰性释放机制能自动优化内存使用,减少管理成本。

线程模型

Redis为什么快

Redis 是纯内存数据库,所有数据都存储在内存中,读写操作无需经过磁盘 IO—— 而磁盘 IO的速度比内存慢数个数量级,这是 Redis 快的基础。但仅靠内存不够,Redis 还通过高效的底层数据结构进一步降低内存操作的耗时:比如 String 用 SDS替代 C 字符串,支持 O (1) 获取长度、避免缓冲区溢出;List 用双向链表或压缩列表实现,首尾操作 O (1);Zset 用跳表 + 字典组合,兼顾排序查询和快速定位,这些结构都经过针对性优化。

其次,Redis 采用单线程模型处理核心命令(命令解析、执行),避免了多线程场景下的 “线程切换开销” 和 “锁竞争”—— 多线程切换需要保存 / 恢复线程上下文,锁竞争会导致线程阻塞,而 Redis 的核心瓶颈是网络 IO 而非 CPU,单线程足够处理万级并发。同时,Redis 通过IO 多路复用机制让单线程能高效管理大量客户端连接:一个线程可同时监控多个网络连接的 “就绪状态”(可读 / 可写),仅当连接有数据时才处理,避免了传统阻塞 IO “一个连接一个线程” 的资源浪费,大幅提升并发连接处理能力。

此外,Redis 还有诸多细节优化:比如内存分配高效(使用 jemalloc 作为内存分配器,按大小分类管理内存,减少碎片);持久化异步化(RDB 快照、AOF 重写由子线程处理,不阻塞主线程);定期任务轻量化(如过期键删除、内存回收采用 “惰性删除 + 定期删除” 结合,避免集中处理导致的卡顿)。这些优化共同作用,让 Redis 能在单线程下实现每秒数十万次的读写操作。

Redis哪些使用了多线程
  1. 持久化相关操作:这是 Redis 最早引入多线程的场景。比如 RDB 持久化的bgsave命令,会通过fork创建子进程,由子进程负责遍历内存数据、生成 RDB 文件,主线程继续处理命令,避免磁盘 IO 阻塞;AOF 持久化中,Redis 4.0 + 支持 AOF 重写(bgrewriteaof),同样通过子进程完成(避免主线程处理大文件重写),在 Redis 6.0 + 。默认 AOF 刷盘(fsync)由主线程执行,若开启aof_threads,可将刷盘操作交给专门的线程,减少主线程的 IO 等待。
  2. 网络 IO 多线程:Redis 6.0 + 为解决 “大请求 / 大响应” 的网络 IO 耗时问题(处理所有网络 IO 操作,但大请求场景的收益更显著),引入了 IO 多线程。核心逻辑是:将 “网络连接的 accept” “数据的 read” “响应的 write” 这三个耗时的 IO 操作,从主线程拆分到独立的 IO 线程(默认 4 个,可配置),而 “命令解析”“命令执行” 仍由主线程单线程处理。比如客户端发送大体积数据(如 SET 一个 10MB 的字符串),IO 线程负责读取数据并解析成协议对象,再交给主线程执行,避免主线程卡在 read 操作上;执行完后,主线程将结果交给 IO 线程,由 IO 线程负责 write 回客户端,大幅提升大流量场景下的并发处理能力。
  3. 异步删除操作:Redis 4.0 + 引入unlink命令(替代del),针对 “大 key 删除”(如包含百万元素的 Hash),会将删除操作交给后台线程异步执行。若用del删除大 key,主线程需遍历整个数据结构释放内存,耗时可能达秒级,导致命令阻塞;而unlink仅标记 key 为 “待删除”,后台线程逐步释放内存,不影响主线程。此外,过期键的 “惰性删除” 若遇到大 key,也会触发后台线程处理,避免主线程卡顿。
  4. 辅助性多线程:比如 Redis 集群模式下,节点间的 “槽位迁移”“数据同步” 由专门的线程处理,不占用主线程资源;还有日志打印、统计信息收集、客户端超时清理等低优先级操作,也会交给后台线程执行,确保主线程专注于核心命令处理。
Redis怎么实现的IO多路复用

第一步是初始化 IO 多路复用实例。Redis 启动时,会根据操作系统类型选择对应的 IO 多路复用接口:在 Linux 下调用epoll_create创建一个 epoll 实例(本质是内核中的事件表),用于管理所有客户端连接的 FD;同时,Redis 会绑定监听端口(如 6379),创建一个 “监听 FD”(用于接收新客户端连接),并将该 FD 注册到 epoll 实例中,注册的事件类型为 “可读事件”(当有新客户端连接时,监听 FD 会变为可读)。

第二步是注册客户端 FD 与事件。当新客户端通过 TCP 连接 Redis 时,主线程调用accept获取新的客户端 FD,然后调用epoll_ctl将该 FD 注册到 epoll 实例中,注册的事件类型根据需求设置:比如 “可读事件”(客户端发送数据时,FD 可读)、“可写事件”(Redis 需要向客户端写响应时,若 TCP 缓冲区未满,FD 可写)。Redis 对客户端 FD 默认使用边缘触发(ET)模式(仅在 FD 状态从 “未就绪” 变为 “就绪” 时通知一次),而非水平触发(LT,只要 FD 就绪就持续通知)——ET 模式能减少事件通知的次数,避免重复处理同一事件,提升效率,但要求 Redis 必须一次性读取完 FD 中的所有数据,避免数据残留。

第三步是事件循环与处理。Redis 主线程进入无限循环,调用epoll_wait等待 epoll 实例中的就绪事件:若没有就绪事件,主线程会阻塞在epoll_wait上(不占用 CPU);当有 FD 就绪(如监听 FD 有新连接、客户端 FD 有数据),epoll_wait会返回就绪的 FD 列表。主线程遍历该列表,分场景处理:若就绪的是 “监听 FD”,则调用accept获取新客户端 FD 并注册到 epoll;若就绪的是 “客户端 FD” 且为 “可读事件”,则读取客户端发送的命令数据,解析后执行;若为 “可写事件”,则将之前缓存的响应数据写入 TCP 缓冲区,完成后取消 “可写事件” 注册(避免 FD 持续可写导致重复通知)。

Redis的网络模型是怎样的

先看Redis 6.0 前的单线程网络模型:整个网络交互与命令执行都由主线程完成,流程可分为 “初始化→事件循环→命令处理→定期任务” 四步。初始化阶段:主线程创建监听 FD,绑定端口,初始化 IO 多路复用实例(如 epoll),并将监听 FD 注册为 “可读事件”。事件循环阶段:主线程调用epoll_wait等待就绪事件,若没有就绪事件则阻塞;若有就绪事件(新连接、客户端数据),则处理事件:对于新连接,调用accept获取客户端 FD,设置为非阻塞模式并注册到 epoll;对于客户端数据,读取数据、解析为 RESP 协议格式的命令(如 GET key),然后执行命令(操作内存数据),最后将结果缓存,注册客户端 FD 的 “可写事件”(等待 TCP 缓冲区空闲后写入响应)。命令处理完成后,主线程会定期(每 100ms 左右)执行 “定期任务”:如删除过期键、检查持久化条件、清理超时客户端连接,避免这些任务长期占用主线程。这种模型的优势是简单无锁,缺点是当遇到大请求(如 read 大体积数据、write 大响应)时,IO 操作会阻塞主线程,影响并发。

再看Redis 6.0 后的 IO 多线程网络模型:核心是 “拆分网络 IO 与命令执行”,将耗时的 “accept、read、write” 交给 IO 线程,“命令解析、执行” 仍由主线程单线程处理,流程更细致。初始化阶段:除了创建监听 FD、初始化 epoll,还会创建一组 IO 线程(默认 4 个,可通过io-threads配置),分为 “IO 读线程” 和 “IO 写线程”(部分版本共用线程池)。事件循环阶段:主线程仍负责调用epoll_wait获取就绪事件,但处理逻辑拆分:对于新连接,主线程accept后将客户端 FD 分配给 IO 读线程;IO 读线程负责读取客户端数据,解析为协议对象(如命令参数),放入主线程的 “命令队列”;主线程从队列中取出协议对象,执行命令(单线程,无锁),将结果放入 “响应队列”,并分配给 IO 写线程;IO 写线程负责将响应数据写入客户端 FD 的 TCP 缓冲区,完成后清理资源。这种模型中,IO 线程仅处理网络数据的读写与解析,不触碰核心数据(内存中的键值对),因此无需加锁;主线程专注于命令执行,避免了 IO 操作的阻塞,大幅提升大流量场景下的并发能力(如每秒处理连接数从万级提升到十万级)。

事务

如何实现Redis的原子性

单线程模型是原生原子性的基础。Redis 的核心命令执行(如GETINCRHSET)由主线程单线程串行处理,不存在多线程并发竞争 —— 同一时间只有一个命令在执行,不会出现 “一个命令执行到一半被另一个命令打断” 的情况。例如执行INCR counter时,从读取counter当前值、加 1、写回新值的整个过程,完全由主线程独占执行,中间不会插入其他命令,天然保证了原子性。这是 Redis 最底层的原子性保障,也是所有原生命令原子性的根源。

内置原子命令覆盖常见场景。Redis 针对高频原子操作设计了专属命令,避免用户通过 “多命令组合”(非原子)实现需求。例如,“检查键是否存在并设置值”(避免并发覆盖)可通过SET key value NX(仅当键不存在时设置)实现,该命令将 “检查 + 设置” 合并为单个原子操作;“自增并返回新值” 通过INCR实现,无需先GETSET(两步操作非原子);哈希表的 “字段不存在时设置” 通过HSETNX实现,同样是原子的。这些命令在底层被设计为不可拆分的执行单元,直接保证操作原子性。

Lua 脚本实现复杂原子逻辑。当原生命令无法满足复杂需求(如 “先判断键的类型,再根据类型执行不同操作”)时,Redis 支持通过 Lua 脚本将多命令封装为一个 “原子执行单元”。Redis 执行 Lua 脚本时,会将脚本内的所有命令视为一个整体,执行期间不允许插入其他客户端的命令(即 “脚本级串行”),确保脚本内的逻辑要么全执行,要么全不执行(若脚本执行中报错,已执行的命令不会回滚,但后续命令会终止,需业务层提前规避错误)。例如,实现 “扣减库存前先判断库存是否充足,充足则扣减并返回 1,否则返回 0” 的逻辑,可通过 Lua 脚本将 “GET stock→判断→DECR stock” 封装为原子操作,避免并发下的超卖问题。

Redis 事务(MULTI/EXEC)也能保证批量命令的原子性。用户通过MULTI标记事务开始,后续命令会被 “入队” 而非立即执行,直到调用EXEC才会批量执行所有入队命令 —— 执行期间同样不会插入其他命令,确保 “要么所有命令执行,要么所有命令不执行”(若入队时命令语法错误,EXEC会直接放弃所有命令;若执行中命令报错,已执行的命令无法回滚,需业务层处理)。例如,通过MULTI+HSET user:100 name zhangsan+SADD user:ids 100+EXEC,可原子性完成 “设置用户信息 + 添加用户 ID 到集合” 的两步操作。

除了lua脚本有没有其他方式保证原子性

内置原子命令。Redis 针对高频原子操作设计了原子命令,避免用户通过 “多命令组合”(非原子)实现需求。例如,“检查键是否存在并设置值”(避免并发覆盖)可通过SET key value NX(仅当键不存在时设置)实现,该命令将 “检查 + 设置” 合并为单个原子操作;“自增并返回新值” 通过INCR实现,无需先GETSET(两步操作非原子);哈希表的 “字段不存在时设置” 通过HSETNX实现,同样是原子的。这些命令在底层被设计为不可拆分的执行单元,直接保证操作原子性。

Redis 事务(MULTI/EXEC)也能保证批量命令的原子性。用户通过MULTI标记事务开始,后续命令会被 “入队” 而非立即执行,直到调用EXEC才会批量执行所有入队命令 —— 执行期间同样不会插入其他命令,确保 “要么所有命令执行,要么所有命令不执行”(若入队时命令语法错误,EXEC会直接放弃所有命令;若执行中命令报错,已执行的命令无法回滚,需业务层处理)。例如,通过MULTI+HSET user:100 name zhangsan+SADD user:ids 100+EXEC,可原子性完成 “设置用户信息 + 添加用户 ID 到集合” 的两步操作。

持久化

Redis怎么实现持久化的

Redis 通过两种核心机制实现持久化,分别是 RDB(快照持久化)和 AOF(追加日志持久化),并支持混合持久化(结合两者优势),以平衡数据可靠性与性能。

RDB 机制是在指定时间点生成内存数据的二进制快照文件(默认 dump.rdb),触发方式分为手动和自动:手动用bgsave命令(创建子进程处理,不阻塞主线程)或save命令(主线程执行,会阻塞);自动则通过配置快照规则(如save 900 1表示 900 秒内至少 1 次写操作触发),依赖 “脏数据计数器” 和 “上次快照时间戳” 判断条件。执行时子进程通过 “写时复制” 机制遍历内存数据,生成临时文件后原子替换旧 RDB,确保快照完整性,其优势是文件小、加载快,适合备份与迁移,但间隔性生成快照会导致宕机时丢失期间数据。

AOF 机制则是将所有修改数据的命令(如 SET、INCR)按执行顺序以 Redis 协议格式追加到 AOF 文件(默认 appendonly.aof),核心是 “命令追加 - 刷盘 - 重写” 流程:写命令先入 AOF 缓冲区,再按appendfsync配置的刷盘策略(always实时刷盘、everysec每秒刷盘、no依赖系统刷盘)写入磁盘;可通过AOF重写解决 AOF 文件膨胀,通过bgrewriteaof命令触发或自动触发(当 AOF 文件大小超过上一次重写后大小的 2 倍且大于最小aof重写大小默认64),根据内存数据生成最小化等效命令重写文件,重写期间主线程将新命令写入 “重写缓冲区”,最终将新 AOF 文件覆盖旧文件。AOF优势是数据丢失少(默认丢 1 秒内数据),但文件体积大、加载慢。

混合持久化(Redis 4.0 + 默认开启)则将 RDB 快照作为 AOF 文件头部,后续追加重写后的 AOF 命令,重启时先快速加载 RDB 恢复大部分数据,再执行少量 AOF 命令恢复增量数据,兼顾 RDB 的加载速度与 AOF 的可靠性。

内存淘汰策略和过期删除策略有什么区别

从核心目的看,过期删除策略的目的是 “保证数据时效性”,确保业务访问到的是未过期的新鲜数据,避免使用逻辑上已失效的键(如过期的验证码、临时会话);而内存淘汰策略的目的是 “保证 Redis 服务可用性”,当内存满时通过淘汰部分键释放空间,避免 Redis 因内存不足拒绝所有写命令,核心是平衡内存使用与服务稳定性。

从处理对象看,过期删除策略仅针对 “已设置过期时间的键”(如SET key value EX 10),未设置过期时间的键不在其处理范围内,核心是清理 “逻辑上已失效” 的键;而内存淘汰策略处理的是 “所有键”,无论是否设置过期时间,当 Redis 内存使用达到maxmemory配置的上限时,会按策略选择部分键删除以释放内存,即使键未过期,若符合策略也可能被淘汰。

从触发条件看,过期删除策略的触发时机是 “键过期后”,具体通过 “惰性删除”(访问键时检查过期)和 “定期删除”(每隔 100ms 随机检查部分过期键)触发,与内存是否充足无关;而内存淘汰策略的触发条件是 “Redis 使用内存达到maxmemory上限”,且后续有新写命令需要申请内存,此时若内存不足,才会执行淘汰策略删除键,若内存未达上限,即使键符合淘汰条件也不会被处理。

说一下内存淘汰策略

Redis 的内存淘汰策略是当内存使用达到maxmemory上限时,用于选择部分键删除以释放内存的规则,分为三大类共 8 种策略(Redis 6.0+)。

第一类是 “不淘汰策略”:noeviction(默认策略),当内存满且有新写命令时,直接返回错误(读命令正常执行),不删除任何键,适合 Redis 作为持久化数据库而非缓存的场景,需确保内存充足或手动清理数据。

第二类是 “仅淘汰有过期时间的键”:包含 4 种策略,仅针对EXPIRE集合(已设置过期时间的键)选择淘汰对象,适合 Redis 中同时存在 “需持久化的核心键”(未设过期)和 “可淘汰的缓存键”(设过期)的场景:

  • volatile-lru:淘汰EXPIRE集合中 “最近最少使用” 的键(如一周未被访问的缓存);
  • volatile-lfu:淘汰EXPIRE集合中 “最近访问频率最低” 的键(比 LRU 更精准识别非热点数据);
  • volatile-ttl:淘汰EXPIRE集合中 “剩余过期时间最短” 的键(优先清理即将过期的键);
  • volatile-random:随机淘汰EXPIRE集合中的键,适合对淘汰精度无要求的场景。

第三类是 “淘汰所有键”:包含 3 种策略,对 Redis 中的所有键(无论是否设过期)进行淘汰,仅适合 Redis 纯缓存场景(所有键均可丢失,无核心持久化数据):

  • allkeys-lru:淘汰所有键中 “最近最少使用” 的键(最常用的缓存淘汰策略,如电商商品详情页缓存);
  • allkeys-lfu:淘汰所有键中 “最近访问频率最低” 的键(适合热点数据更稳定的场景,如用户常看的固定栏目);
  • allkeys-random:随机淘汰所有键中的键,仅用于测试或特殊场景。
说一下过期删除策略

Redis 未采用 “定时删除”(每隔一段时间全量检查所有过期键)或 “立即删除”(键过期时立即删除)的极端策略,而是通过 “惰性删除 + 定期删除” 的混合策略,平衡 CPU 开销与内存占用,核心逻辑如下:

惰性删除是 “被动触发” 的删除机制,仅当客户端访问某个键时,Redis 才会检查该键是否过期:若未过期则正常返回数据;若已过期则立即删除该键,并返回 “nil”(空值)。这种策略的优势是 “CPU 友好”,无需主动消耗 CPU 资源扫描过期键,仅在必要时(访问键)执行检查;缺点是 “内存不友好”,若过期键长期未被访问,会一直占用内存,导致 “内存泄漏”(如过期的临时会话键未被访问,持续占用内存)。

定期删除是 “主动触发” 的删除机制,用于弥补惰性删除的内存问题,Redis 会每隔 100ms(可通过hz配置调整,默认 10,即每秒 10 次)执行一次定期删除任务,具体流程是:先从EXPIRE集合(存储所有设过期时间的键)中随机选择 20 个键,检查其是否过期,若过期则删除;若其中过期键的比例超过 25%,则重复该流程(再选 20 个键检查),直到过期键比例≤25% 或执行时间超过 2ms(避免占用过多 CPU)。这种策略的优势是 “平衡 CPU 与内存”,通过 “随机抽样 + 比例控制”,既避免全量扫描的 CPU 消耗,又能定期清理部分过期键,减少内存泄漏;缺点是无法保证所有过期键都被及时删除,仍有少量过期键可能长期存在。

两种策略配合工作:定期删除主动清理大部分过期键,减少内存堆积;惰性删除确保客户端访问时,不会获取到过期键,保证数据时效性。例如:一个设为 10 秒过期的键,10 秒后过期,若未被访问,可能在某次定期删除中被随机选中删除;若在定期删除前被访问,则会被惰性删除立即清理。

Redis的缓存失效会不会立即删除

Redis 的缓存失效后不会立即删除,而是通过 “惰性删除” 和 “定期删除” 的混合策略延迟清理,当缓存键设置的过期时间到期(如SET code 1234 EX 10,10 秒后失效),Redis 不会在过期时立即删除该键,而是让键继续留在内存中:若此时没有客户端访问该键就不会触发惰性删除,Redis 仅会在 定期删除任务(每隔 100ms)中,随机抽样部分过期键时,才有可能选中并删除它,若该键未被抽样到,就会继续留在内存,直到下一次定期删除或被访问触发惰性删除。

为什么要这样设计过期删除策略

Redis 的过期删除策略(惰性删除 + 定期删除)是典型的 “资源权衡设计”,核心目标是在CPU 开销内存占用数据时效性三者之间找到平衡,完全贴合 Redis “高性能单线程” 的核心架构,避免极端策略带来的致命问题。

若采用 “定时删除”(为每个过期键设置定时器,过期即删除),虽然能保证过期键立即清理,内存无浪费,但会带来巨大的 CPU 负担:每个键过期都需要触发删除操作,当过期键数量庞大(如十万级)时,定时器会频繁抢占主线程资源,导致正常命令响应延迟 —— 而 Redis 是单线程模型,CPU 资源极度珍贵,这种策略会直接拖垮服务性能,显然不可接受。

若采用 “立即删除”(键过期瞬间主动删除),看似直观,却会引发 “突发性性能抖动”:假设某一时刻有大量键同时过期(如缓存集中失效),Redis 会瞬间执行大量删除操作,阻塞主线程,导致所有客户端请求超时,这对高并发场景是致命的(例如秒杀活动中,大量商品缓存同时过期,可能引发服务雪崩)。

而 “惰性删除” 的设计,本质是 “延迟清理,按需执行”:只有当客户端访问过期键时,才触发删除检查,平时不消耗 CPU 资源。这种 “懒处理” 极大降低了 CPU 开销,契合 Redis 单线程对 “低延迟” 的追求,但代价是过期键可能长期滞留内存(如从未被访问的过期键),导致内存浪费。

因此需要 “定期删除” 作为补充:通过每隔 100ms 随机抽样部分过期键并清理,既能主动释放一部分内存(缓解内存泄漏风险),又通过 “随机抽样 + 比例控制”(每次检查 20 个键,过期比例超 25% 则重复,否则停止,且单次执行不超过 2ms)避免了全量扫描的 CPU 消耗。这种 “抽样清理” 的逻辑,用少量 CPU 资源换来了内存占用的可控性,是对惰性删除的完美补充。

集群

Redis主从集群了解吗

主从集群是 Redis 最基础的分布式架构,核心是 “一主多从” 的数据复制关系,目标是实现数据冗余备份读写分离,缓解主节点的读压力。

其架构由一个主节点(Master)和多个从节点(Slave)组成:主节点负责处理所有写请求(如 SET、INCR)和部分读请求,从节点通过 “主从同步” 机制复制主节点的数据,仅处理读请求(如 GET、HGET)。数据同步分为 “全量同步” 和 “增量同步”:首次连接或从节点长期断连后,主节点会生成 RDB 快照发送给从节点(全量同步),并缓存同步期间的写命令;后续正常运行时,主节点将写命令实时同步给从节点(增量同步),通过 “偏移量” 确保数据一致。

这种架构的优势是简单易部署,通过读写分离提升读并发能力(如将查询请求分流到从节点),同时从节点作为数据副本,降低主节点宕机的数据丢失风险。但缺点也明显:主节点仍是单点 —— 若主节点故障,所有写请求中断,需手动将从节点切换为主节点;且所有节点存储全量数据,无法解决 “单节点内存上限” 问题(如数据量达 100GB,单节点内存无法承载)。

了解Redis哨兵机制吗

哨兵模式是在主从集群基础上引入的高可用解决方案,核心目标是解决主从集群的单点故障问题,其架构由 “主从节点” 和 “哨兵节点” 两部分组成:主从节点负责数据存储与读写,哨兵节点(通常部署 3 个及以上,奇数个)是独立的监控进程,通过三大功能保障高可用:一是监控,哨兵定期向所有主从节点发送 PING 命令,检测节点存活状态;二是故障转移,当主节点故障(被标记为 “客观下线”),从从节点中选择一个 “数据最新、优先级最高” 的从节点,晋升为新主节点,并通知其他从节点复制新主节点,同时告知客户端新主节点地址;三是配置中心,客户端无需硬编码主节点地址,只需连接哨兵,即可动态获取当前主节点信息,避免故障转移后主节点丢失。

哨兵模式解决了主从集群的人工切换问题,让架构具备自动恢复能力,适合 “数据量中等、需高可用但无需大规模扩展” 的场景(如电商订单缓存)。但它仍未解决 “全量数据存储” 的问题 —— 所有节点仍存储完整数据,无法突破单节点内存限制。

Redis的集群模式(分片集群)了解吗

分片集群是为了聚焦拓展存储能力并发能力设计的分布式架构,核心通过数据分片突破单节点内存上限,同时内置高可用机制,无需依赖哨兵。

其核心设计是 “哈希槽(Hash Slot)”:将所有数据划分为 16384 个槽(0-16383),每个主节点负责一部分槽(如 3 个主节点可分别负责 0-5460、5461-10922、10923-16383)。数据存储时,通过CRC16(key) % 16384计算键对应的槽,再分配到负责该槽的主节点,实现数据分片存储(每个节点仅存部分数据)。

架构上,集群由多个主节点和从节点组成:主节点负责处理所属槽的读写请求,从节点复制主节点数据,当主节点故障时,从节点通过 “选举机制” 晋升为新主节点(类似哨兵的故障转移,但集成在集群内部)。集群采用 “无中心架构”,节点间通过 Gossip 协议同步状态(如槽分配、节点存活),客户端访问时,若键不在当前节点的槽中,节点会返回重定向信息(MOVED/ASK),指引客户端到正确节点。

分片集群的优势是支持水平扩展(通过增加主节点扩展槽数量和存储能力),同时内置高可用,适合 “数据量大(如 TB 级)、高并发读写” 的场景(如用户行为日志、大规模缓存)。其缺点是部署和运维复杂度高于主从集群和哨兵模式。

Redis主从同步是怎么实现的

Redis主从同步分为全量同步增量同步两种场景,流程围绕 “偏移量(offset)” 和 “复制积压缓冲区” 展开。

全量同步(首次同步 / 断连后无法增量)

当从节点首次连接主节点,或从节点与主节点断连时间过长、主节点的 “复制积压缓冲区”(repl_backlog_buffer)中已无对应偏移量的数据时,会触发全量同步,流程如下:

  1. 从节点发起同步请求
  2. 主节点生成 RDB 快照:主节点收到请求后,执行bgsave命令创建子进程,子进程遍历内存数据生成 RDB 文件,同时主节点将生成 RDB 期间收到的写命令存入复制积压缓冲区;
  3. 主节点发送 RDB 与缓冲命令:主节点先将 RDB 文件发送给从节点,从节点接收后清空本地数据,加载 RDB 文件恢复主节点的基础数据;RDB 发送完成后,主节点再将复制积压缓冲区中的写命令逐发给从节点,从节点执行这些命令,实现 “与主节点数据对齐”;
  4. 同步完成,进入增量阶段:从节点执行完缓冲命令后,与主节点数据一致,后续同步切换为增量模式。

增量同步

全量同步完成后,主从节点进入增量同步阶段,核心是 “基于偏移量的命令同步”:

主节点维护 “主偏移量(master_repl_offset)”,每执行一条写命令,偏移量 + 1;同时将写命令写入 “复制积压缓冲区”(固定大小的环形缓冲区,默认 1MB,可通过repl-backlog-size调整);

从节点维护 “从偏移量(slave_repl_offset)”,每执行一条主节点发来的命令,偏移量 + 1;从节点会定期向主节点发送REPLCONF ACK <slave_offset>命令(心跳),告知主节点自己的当前偏移量;

主节点收到心跳后,对比主偏移量与从偏移量:若从偏移量小于主偏移量,说明从节点落后,主节点从复制积压缓冲区中提取 “从偏移量之后的所有命令”,发送给从节点;从节点执行这些命令,更新从偏移量,保持与主节点同步,若从节点偏移量已经落后于复制积压缓冲区则执行全量同步。

Redis主从和集群能保证数据一致性吗
  1. 主从架构的一致性问题

主从同步默认是 “异步复制”:主节点执行完写命令后,立即向客户端返回 “成功”,再异步将命令发送给从节点 —— 若主节点在 “返回成功后、发送命令前” 宕机(如突然断电),未发送的命令会丢失,从节点无法同步这部分数据,此时主从数据不一致;即使主节点正常发送命令,从节点接收、执行命令也存在网络延迟(如跨机房部署),期间客户端读从节点可能获取到旧数据(“读写分离延迟” 问题)。

此外,主从架构若未配合哨兵,主节点故障后需手动切换从节点为主节点,切换期间服务不可用,也可能导致数据更新中断。

  1. 集群架构的一致性问题

集群基于主从架构实现,继承了主从异步复制的一致性问题:每个主节点的写命令异步同步给其从节点,存在上述 “主节点宕机丢数据”“从节点读旧数据” 的问题;同时,集群的 “分片特性” 会导致跨分片操作的一致性问题 —— 例如 “同时修改分片 A 的键和分片 B 的键”,两个分片的主节点各自异步同步数据,可能出现 “分片 A 同步完成、分片 B 未同步” 的情况,导致整体数据不一致。

另外,集群故障转移(从节点晋升主节点)存在延迟:主节点故障后,从节点需先检测到故障(通过 Gossip 协议的心跳),再发起选举、晋升为主节点,整个过程约 1-3 秒,期间该分片的写请求会失败,读请求可能获取到旧数据(从节点未完全同步)。

哨兵集群是怎么选举主节点的

故障检测:哨兵集群中所有哨兵节点定期(默认每 1 秒)向主节点和从节点发送PING命令检测存活状态;若某哨兵发现主节点在down-after-milliseconds(默认 30000ms)内未返回有效响应,会将主节点标记为 “主观下线(SDOWN)”;该哨兵随后向其他哨兵发送SENTINEL is-master-down-by-addr命令询问,当超过quorum(配置的最小确认哨兵数)个哨兵同意主节点下线时,主节点被标记为 “客观下线(ODOWN)”,触发选举流程。

选举领头哨兵:确认主节点客观下线后,哨兵们会发起领头哨兵选举;首个发起选举的哨兵向其他哨兵发送竞选请求,每个哨兵在一次故障转移中仅能投 1 票,得票超过哨兵总数半数的哨兵成为领头哨兵,负责后续故障转移操作。

筛选候选从节点:领头哨兵从故障主节点的所有从节点中筛选候选者,先排除已下线、与主节点断连过久(超过down-after-milliseconds * 10)或repl-priority(从节点优先级)为 0 的从节点,保留符合条件的从节点。

选择新主节点:对候选从节点,按 “repl-priority值越小越优先→复制偏移量越大(数据越新)越优先→运行 ID(runid)越小(启动越早)越优先” 的顺序排序,选择排名第一的从节点作为新主节点。

执行晋升与同步:领头哨兵向选中的从节点发送SLAVEOF NO ONE命令,使其成为新主节点;向其他从节点发送SLAVEOF <新主节点地址>命令,让它们复制新主节点;同时通过PUBLISH命令通知客户端新主节点信息,完成故障转移,恢复集群服务。

Redis集群如何选举主节点的

故障检测:集群中所有节点通过 “Gossip 协议” 定期(默认每 1 秒)向其他节点发送PING命令,检测节点存活;若 Slave A1 多次发送PING给 Master A 均未收到响应,且超过cluster-node-timeout配置的时间(默认 15000ms),Slave A1 会标记 Master A 为 “疑似下线(PFAIL)”;Slave A1 再将 “Master A 疑似下线” 的信息通过 Gossip 协议同步给其他节点,当其他主节点也标记 Master A 为 PFAIL,且超过 “集群中主节点数量的半数” 时,Master A 被标记为 “确定下线(FAIL)”;

从节点发起选举请求:Slave A1 检测到 Master A 确定下线后,会向集群中所有主节点发送FAILOVER_AUTH_REQUEST命令,请求其他主节点为自己 “投票”(从节点无法给其他从节点投票,仅主节点有投票权);

主节点投票:每个主节点收到FAILOVER_AUTH_REQUEST后,会检查两个条件:1)是否已给其他从节点投过票(每个主节点在一次故障转移中仅能投 1 票);2)该从节点的 “数据有效性”(复制偏移量是否足够大,确保数据较新);若满足条件,主节点返回FAILOVER_AUTH_ACK,表示投票支持;

从节点晋升主节点:Slave A1 统计收到的FAILOVER_AUTH_ACK数量,若超过 “集群中主节点总数的半数”(如集群有 3 个主节点,需至少 2 票),则 Slave A1 成功当选为新主节点;

同步集群信息:新主节点(原 Slave A1)会通过 Gossip 协议向集群所有节点发送 “自己成为新主节点” 的信息,其他从节点(如 Slave A2)会停止复制旧主节点(Master A),改为复制新主节点;同时,新主节点接管旧主节点负责的所有哈希槽,集群恢复正常服务。

场景

为什么要用Redis,能解决业务中什么问题

使用 Redis 的核心原因在于其 “高性能” “多数据结构” “原子操作” 和 “分布式友好” 的特性,能解决传统数据库在高并发场景下的诸多瓶颈。从性能角度,Redis 基于内存存储,读写速度远快于依赖磁盘的数据库(如 MySQL),可轻松支撑每秒数十万次操作,能有效缓解业务高峰期的数据库压力 —— 比如电商秒杀时,大量商品详情查询请求可直接命中 Redis 缓存,避免 MySQL 因高并发读而崩溃。

从功能角度,Redis 支持丰富的数据结构(如哈希、集合、有序集合)和原子操作,能简化业务逻辑实现,例如用有序集合(ZSET)快速实现排行榜功能,用哈希(HASH)高效存储用户信息。

从分布式角度,Redis 支持主从复制、集群和哨兵模式,可实现数据冗余和高可用,解决单点故障问题;同时,其原子命令和 Lua 脚本能轻松实现分布式锁,解决分布式系统中的并发冲突(如商品超卖)。

此外,Redis 的过期策略和持久化机制,能平衡缓存时效性与数据可靠性,满足业务对 “快速响应 + 数据不丢失” 的需求。

Redis的应用场景有哪些

Redis 的应用场景广泛,核心围绕其 “高性能”“多数据结构”“原子操作”和“分布式友好” 等特性展开。最典型的是作为缓存,存储热点数据(如商品详情),减少数据库访问压力,提升接口响应速度。在计数场景中,利用 INCR、DECR 等原子命令实现高频计数,如文章阅读量、视频播放次数、接口调用次数统计,避免并发下的计数偏差。分布式锁也是常见场景,通过 SET key value NX EX 命令实现跨服务的资源竞争控制(如秒杀下单、库存扣减),确保操作原子性。有序集合(ZSET)适合实现排行榜功能,如游戏积分排名、商品销量榜单,支持快速插入和范围查询。消息队列场景中,可用 List 的 LPUSH 和 RPOP 命令实现简单的生产者 - 消费者模型,处理异步任务(如订单通知、日志收集)。此外,Redis 还可用于存储临时数据(如验证码、购物车、用户Token)、list实现最新消息队列、利用分布式友好的特性存储共享Session信息比如用户Token、地理位置服务(GEO 类型存储经纬度,实现附近的人功能)等。

Redis为什么比MySQL快,两者并发性能是怎么样的(QPS)

Redis 比 MySQL 快的核心原因在于底层设计的差异:从存储介质看,Redis 数据存于内存,读写操作无需磁盘 IO(内存速度约为磁盘的 10 万倍),而 MySQL 数据主要存于磁盘,即使有内存缓存(如 InnoDB Buffer Pool),仍需处理磁盘IO和复杂的页管理;从线程模型看,Redis 核心命令采用单线程执行,避免多线程切换和锁竞争的开销,而 MySQL 是多线程模型,线程间的锁竞争(如表锁、行锁)会显著影响并发性能;从数据结构看,Redis 针对每种数据类型设计了高效结构(如 SDS、跳表),操作复杂度低(多数为 O (1) 或 O (logN)),而 MySQL 需解析 SQL、执行查询优化、遍历 B + 树索引,逻辑更复杂;从协议看,Redis 使用简单的 RESP 协议,解析速度快,而 MySQL 的 SQL 协议需复杂的语法分析和权限校验。

并发性能上,Redis 单机 QPS 通常可达 10 万 - 20 万(取决于数据大小和命令类型),而 MySQL 单机 QPS 一般在几千,差距主要源于内存与磁盘的速度差异,以及两者在处理逻辑复杂度上的不同。

本地缓存和引入Redis作缓存有什么区别

本地缓存(如 Java 的 Caffeine、Guava)与 Redis 缓存的核心区别体现在 “存储位置”“一致性”“适用场景” 三个维度。从存储位置看,本地缓存存于应用进程内存中,访问无需网络开销,速度极快(微秒级),但受限于单进程内存容量(无法过大);Redis 缓存存于独立服务器,需通过网络访问(毫秒级),但可通过集群扩展容量,不受单个应用进程限制。从一致性看,本地缓存是进程内私有数据,多实例部署时,不同实例的缓存更新难以同步,易出现数据不一致(如 A 实例更新了数据,B 实例仍用旧缓存);Redis 是集中式存储,所有应用实例访问同一数据源,更新后可立即生效,一致性更好。从适用场景看,本地缓存适合存储 “高频访问、极少变更” 的数据(如配置信息、静态字典),利用其零网络开销提升性能;Redis 适合 “分布式系统共享数据”“数据需频繁更新”“缓存容量大” 的场景(如用户会话、商品库存),确保多实例数据一致且可扩展。

Redis除了缓存还有其他什么应用

除缓存外,Redis 凭借“高性能”“多数据结构”“原子操作”和“分布式友好” 等特性,还有诸多重要应用。分布式锁是典型场景,通过 SET key value NX EX 命令实现跨服务的互斥访问,解决分布式系统中的资源竞争(如秒杀下单时防止超卖),配合 Lua 脚本可实现锁的原子性释放。计数器场景中,INCR、DECR 等命令支持高并发下的精准计数,如接口调用次数、商品销量、用户点赞数,避免传统数据库计数的性能瓶颈。消息队列方面,利用 List 的 LPUSH(生产者)和 RPOP(消费者)命令可实现简单的异步消息传递,支持削峰填谷(如订单创建后异步处理物流信息),而 Pub/Sub(发布订阅)模式可实现消息的多播(如实时通知)。有序集合(ZSET)适合实现排行榜和范围统计,如游戏积分排名、用户活跃度榜单,支持按分数快速排序和查询。此外,Redis 还可用于限流(基于 ZSET 实现滑动窗口限流)、地理位置服务(GEO 类型存储经纬度,实现附近的人、商户推荐)、临时数据存储(如验证码、会话信息)等。

Redis能作分布式锁的原理

Redis 实现分布式锁的核心是利用其 “单线程原子操作” 特性。多个客户端竞争锁时,通过SET key value NX EX ttl命令实现互斥:NX选项确保 “仅当键不存在时才设置成功”,即只有一个客户端能成功创建键,获得锁;EX ttl选项为键设置过期时间,避免客户端持有锁后崩溃导致锁永久无法释放(死锁)。释放锁时,需通过 Lua 脚本原子执行 “判断 value 是否为当前客户端标识 + 删除键” 的操作,确保只有锁的持有者能释放,防止误删其他客户端的锁。这种机制依托 Redis 的单线程执行模型,保证了锁操作的原子性,从而在分布式系统中实现跨进程、跨服务的资源竞争控制。

Redis的大Key问题

大 Key 指的是占用内存过大的键(通常认为超过 1MB 或包含 1 万以上元素),其危害主要体现在四方面:一是内存占用不均,大 Key 所在节点内存使用率远高于其他节点,易触发内存淘汰或 OOM;二是操作阻塞,删除、修改大 Key(如DEL一个百万级元素的集合)会耗费大量 CPU 时间,单线程模型下会阻塞其他命令执行,导致服务响应延迟;三是网络开销大,大 Key 的读写需传输大量数据,占用带宽,尤其在主从同步或集群数据迁移时,可能引发网络拥塞;四是持久化受阻,RDB 生成或 AOF 重写时处理大 Key 会耗时过长,影响持久化效率。

大Key问题的解决办法

解决大 Key 问题的核心是 “拆分” 与 “优化操作”。首先是拆分大 Key,按业务逻辑将其拆分为多个小 Key:例如将包含 100 万用户信息的大 Hash,按用户 ID 范围拆分为 100 个小 Hash(如user:1-10000user:10001-20000),每个小 Hash 仅存储 1 万用户数据;大 List 可按时间或序号分段,拆分为多个小 List。此外,删除大 Key 时用UNLINK命令(异步删除,将删除操作放入后台线程,不阻塞主线程)替代DEL;对过期数据进行定期清。堆积大量过期数据会造成大Key的产生,例如在HASH数据类型中以增量的形式不断写入大量数据而忽略了数据的时效性。可以通过定时任务的方式对失效数据进行清理。

Redis的热Key问题

热 Key 指的是被高频访问的键(如热门商品详情、爆款活动信息),其危害在于集中式压力:大量请求涌向存储热 Key 的 Redis 节点,导致该节点 CPU 使用率飙升、网络带宽耗尽,成为系统瓶颈;若热 Key 所在节点故障或过期,所有请求会瞬间转移到其他节点或穿透到数据库,引发级联故障;在主从架构中,热 Key 的频繁写操作会导致从节点同步压力增大,出现数据延迟。此外,若热 Key 过期,大量请求会同时穿透到数据库,可能引发 “缓存击穿”,压垮数据库。

热Key问题的解决办法

解决热 Key 问题的核心是 “分散访问压力”。首先是提前识别热 Key,通过客户端埋点统计访问频率,或利用 Redis 监控工具(如 Redis Insight)追踪高频命令,及时发现热 Key。其次是多副本存储,将热 Key 复制到多个 Redis 节点(如集群中不同槽位的节点),客户端访问时通过随机或轮询策略选择节点,分散单节点压力。另外,结合本地缓存,在应用服务内存中缓存热 Key(如用 Caffeine),减少对 Redis 的直接访问;设置热 Key 永不过期或合理延长过期时间,避免缓存击穿,若需更新可主动触发过期或覆盖。对于读写频繁的热 Key,还可采用 “读写分离 + 多从节点” 架构,将读请求分流到多个从节点,降低主节点压力。

如何保证MySQL和Redis缓存的一致性问题

保证 MySQL 与 Redis 缓存一致性的核心是 “避免缓存与数据库数据出现脏数据”,需结合业务场景选择合理的缓存更新策略,同时规避并发更新带来的冲突。生产中最常用的是 “Cache-Aside(旁路缓存)策略”:读操作时,先查 Redis,命中则直接返回;未命中则查 MySQL,再将结果写入 Redis 并设置过期时间;写操作时,先更新 MySQL 数据,成功后删除 Redis 缓存(而非直接更新缓存),后续读请求会从 MySQL 加载最新数据并重写缓存。这种方式能避免 “先更缓存再更 DB” 或 “并发更新缓存” 导致的脏数据 —— 比如若先更缓存,DB 更新失败会导致缓存脏;若直接更新缓存,多线程并发更新时可能出现 “后更新的线程覆盖先更新的线程结果” 的问题,而删除缓存可让后续读请求自动同步最新数据。

为应对 “DB 更新成功但 Redis 删除失败” 的极端情况,可引入 “重试机制”(如用消息队列异步重试删除缓存),或给缓存设置较短的过期时间(即使删除失败,缓存也会自动失效,最终恢复一致性)。此外,对并发写场景,可通过分布式锁或数据库乐观锁控制更新顺序,确保 DB 数据更新的原子性,从源头减少缓存与 DB 的不一致风险;对实时性要求极高的业务,可采用 “Write-Through(写透)策略”(更新 DB 时同步更新缓存),但需承担同步更新的性能开销,适合数据变更频率低、实时性要求高的场景。

缓存击穿、缓存穿透、缓存雪崩是什么,怎么解决

缓存击穿、穿透、雪崩是缓存使用中常见的三大问题,核心都是 “缓存未命中导致大量请求直接打向数据库”,但触发原因和解决思路不同。

缓存击穿指 “单个热点 Key 过期时,大量并发请求同时穿透到 MySQL”,比如秒杀活动中的热门商品 Key 过期,瞬间数百上千请求直接查询数据库,可能导致 DB 过载。解决办法包括:给热点 Key 设置 “永不过期”(或超长过期时间),通过业务逻辑主动更新缓存(而非依赖自动过期);用互斥锁(如 Redis 的 SET NX 命令)控制并发,当缓存未命中时,只有一个线程能查询 DB 并回写缓存,其他线程等待重试;或提前预热热点 Key,在过期前主动更新缓存,避免过期瞬间的请求穿透。

缓存穿透指 “查询不存在的数据(如用户查 ID 为 - 1 的商品),Redis 和 MySQL 均无数据,导致每次请求都穿透到 DB”,若有恶意请求高频发起此类查询,会持续压垮数据库。解决办法有:在 Redis 中缓存 “空值”(如查询不存在的商品时,缓存键为goods:-1,值为null并设置短期过期时间),避免后续请求重复穿透;用布隆过滤器(提前将所有存在的商品 ID、用户 ID 等存入布隆过滤器),查询前先通过布隆过滤器判断数据是否存在,不存在则直接返回,无需查 Redis 和 DB;在接口层增加参数校验(如拦截 ID 为负数、超出合理范围的请求),从源头阻断无效请求。

缓存雪崩指 “大量 Key 同时过期,或 Redis 集群整体宕机,导致所有请求集中穿透到 MySQL”,比如缓存服务重启、或初始化时大量 Key 设置了相同过期时间,到期后瞬间引发 “缓存集体失效”,DB 面临海量请求压力。解决办法包括:给 Key 的过期时间增加 “随机值”(如基础过期时间 1 小时,加 5-10 分钟随机值),避免大量 Key 同时过期;部署 Redis 集群(主从 + 哨兵或分片集群),确保单点故障时其他节点能接管服务,减少集群宕机风险;给 DB 层增加 “限流熔断” 机制(如用 Sentinel、Hystrix),当请求量超过阈值时拒绝部分请求,避免 DB 被压垮;提前预热缓存,在业务高峰期前将热点数据加载到 Redis,避免高峰期缓存未命中。

说一下布隆过滤器的原理

布隆过滤器是一种 “空间高效的概率型数据结构”,核心作用是 “快速判断一个元素是否存在于一个庞大的集合中”,存在一定的误判率(即 “可能存在” 但不会 “一定不存在”),适合解决缓存穿透、URL 去重等场景。

其原理基于 “bit 数组” 和 “多个哈希函数”:首先初始化一个固定大小的 bit 数组(默认值为 0),并定义多个独立的哈希函数(如 3 个、4 个);当插入一个元素时,将元素分别传入每个哈希函数,每个哈希函数会计算出一个 “数组下标”,然后将 bit 数组中对应下标的值从 0 置为 1;当查询一个元素时,同样将元素传入所有哈希函数,得到多个下标,若所有下标对应的 bit 值均为 1,则判断 “元素可能存在于集合中”(存在误判,因为不同元素可能通过哈希函数映射到相同下标,导致 bit 位被共同置 1);若有任何一个下标对应的 bit 值为 0,则判断 “元素一定不存在于集合中”(因为若元素存在,所有哈希函数映射的 bit 位都应被置 1)。

布隆过滤器的关键特点是 “空间效率高”(用 bit 存储,1MB 可存储约 800 万个元素)、“查询速度快”(仅需多次哈希计算和 bit 位判断,时间复杂度为 O (k),k 为哈希函数数量),但存在 “误判率” 和 “无法删除元素” 的局限 —— 误判率可通过调整 “bit 数组大小” 和 “哈希函数数量” 控制(数组越大、哈希函数越多,误判率越低,但空间和时间开销会增加);无法删除元素是因为删除一个元素时,若将对应 bit 位置 0,可能会影响其他元素的判断(多个元素共享部分 bit 位),不过部分改进版布隆过滤器(如计数布隆过滤器)可通过 “计数 bit” 解决删除问题,但会增加空间开销。

如何设计秒杀场景下处理超卖问题

秒杀场景的核心挑战是 “高并发下的库存控制”,超卖的本质是 “库存扣减操作非原子化”,导致多个请求同时扣减时出现 “库存为负” 的情况,需从 “前端限流、后端削峰、库存原子扣减、DB 兜底” 四个层面设计方案,确保库存准确。

首先是前端限流,减少无效请求进入后端:前端页面添加 “按钮置灰”(点击一次后禁用,防止重复提交)、“验证码 / 滑块验证”(过滤部分机器人请求),同时通过 JS 控制请求频率(如 1 秒内最多发起 1 次请求),从源头减少并发量;此外,可采用 “预排队” 机制(如用户进入秒杀页面先获取排队号,只有排队号靠前的用户才能参与秒杀),避免所有用户同时发起请求。

其次是后端接口限流与削峰,控制请求流量:用 “令牌桶”“漏桶” 等限流算法(如 Redis 的 INCR 命令统计请求数,超过阈值则拒绝),限制单接口每秒的请求量(如每秒 1000 次);引入消息队列(如 RabbitMQ、Kafka),将秒杀请求异步入队,后端消费者按 “库存数量” 控制消费速度(如库存 100 件,则只消费 100 个请求),避免大量请求直接冲击业务层和数据库,同时实现 “削峰填谷”。

核心是 “库存原子扣减”,确保并发安全:采用 “Redis 预减库存” 作为第一道防线,秒杀开始前将商品库存写入 Redis(如stock:goods:1001 100),用户请求时通过 Redis 的原子命令(如DECR stock:goods:1001)扣减库存,若返回值≥0 则表示扣减成功,库存充足,允许继续下单;若返回值 <0 则直接拒绝,说明库存不足,避免后续请求到 DB。为确保 Redis 与 MySQL 库存一致,扣减 Redis 库存后,将下单请求写入消息队列,由消费者异步更新 MySQL 库存(此时即使 Redis 库存扣减成功,MySQL 未更新,也可通过 “最终一致性” 保证 —— 若消息队列消费失败,可重试);同时,Redis 扣减库存时可结合 Lua 脚本(如 “判断库存 > 0 再扣减”),避免并发下的判断与扣减分离导致的超卖。

最后是 DB 层兜底,确保最终库存准确:MySQL 库存表添加 “唯一索引”(如unique key (goods_id, user_id)),防止同一用户重复下单;采用 “乐观锁” 控制库存更新(如update goods_stock set stock = stock - 1 where goods_id = 1001 and stock > 0),只有库存大于 0 时才允许更新,避免超卖;若业务对实时性要求极高,也可采用 “悲观锁”(如select * from goods_stock where goods_id = 1001 for update),但需注意悲观锁会降低并发性能,适合库存极少的场景。此外,秒杀结束后需对比 Redis 与 MySQL 库存,若存在差异(如 Redis 库存为 0 但 MySQL 仍有库存),需人工或脚本同步,确保最终数据一致。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

赛博猿神

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值