数据结构

1.Redis底层的数据结构

常见的有五种数据类型: String(字符串) , Hash(哈希) , List(列表) , Set(集合) , Zset(有序集合);

新增的四种数据类型: BitMap , HyperLogLog , GEO , Stream;

String类型的应用场景:缓存对象,常规计数,分布式锁,共享session信息;
List类型:消息队列; 但有问题:1.生产者需要自行实现全局唯一ID; 2.不能以消费者组的形式消费数据;
Hash类型:缓存对象,购物车等;
Set类型:聚合计算, 比如点赞,共同关注,抽奖活动等;
Zset类型:排序场景, 比如排行榜,电话和姓名的排序等;

BitMap: 二值状态计的场景,比如签到,判断用户登录状态,连续签到用户总数等;
HyperLogLog:海量数据基数统计场景,比如百万级网页UV计数;
GEO:存储地理位置信息的场景,比如滴滴叫车;
Stream: 消息队列, 相比于List,有两个特有的性质:1.可以自动生成全局唯一消息ID 2.支持以消费组形式消费数据;

2.Zset:

类似于用Zset实现排行榜功能:

1.添加排行榜数据:

1
2
3
ZADD rank 100 user1
ZADD rank 80 user2
ZADD rank 120 user3

2.查看排行榜(从高到低)

1
ZREVRANGE rank 0 -1 WITHSCORES

3.查看(前三名)

1
ZREVRANGE rank 0 2 WITHSCORES  (左右都包)

4.查询某个用户的排名

1
ZREVRANK rank user1

5.查询某个用户的点赞数

1
ZSCORE rank user1

6.查询点赞100~200的用户

1
ZRANGEBYSCORE rank 200 100 WITHSCORES

7.增加点赞数(如果是-1就是减少一个点赞)

1
ZINCRBY rank 1 user1

8.获取排行榜总人数

1
ZCARD rank

2.Set和Zset的区别:

一、Set 和 ZSet 的核心区别

对比维度 Set(集合) ZSet(有序集合)
是否有序 ❌ 无序 ✅ 按 score 有序
是否重复 ❌ 不允许 ❌ 不允许
排序依据 score(分值)
是否支持排名
底层结构 HashTable SkipList + Hash
是否支持范围查询
典型场景 去重、关系集合 排行榜、积分、延迟队列

Set:

1️⃣ 添加元素

1
SADD tags java redis mysql

2️⃣ 删除元素

1
SREM tags redis

3️⃣ 判断是否存在

1
SISMEMBER tags java

4️⃣ 获取所有元素

1
SMEMBERS tags

5️⃣ 获取元素个数

1
SCARD tags

6️⃣ 随机获取元素(抽奖常用)

1
SRANDMEMBER tags 2

7️⃣ 随机弹出元素

1
SPOP tags

8️⃣ 集合运算(面试重点 ⭐⭐⭐)

1
2
3
SINTER set1 set2   # 交集
SUNION set1 set2 # 并集
SDIFF set1 set2 # 差集

Zset:

1️⃣ 添加 / 更新元素

1
ZADD rank 100 user1

2️⃣ 增减分(核心)

1
2
ZINCRBY rank 10 user1
ZINCRBY rank -5 user1

3️⃣ 获取排行榜(高 → 低)

1
ZREVRANGE rank 0 9 WITHSCORES

4️⃣ 查询某个成员排名

1
ZREVRANK rank user1

5️⃣ 查询成员分数

1
ZSCORE rank user1

6️⃣ 按分数范围查询

1
ZRANGEBYSCORE rank 60 100 WITHSCORES

7️⃣ 按排名范围查询

1
ZRANGE rank 0 9 WITHSCORES

8️⃣ 删除元素

1
ZREM rank user1

9️⃣ 获取成员数量

1
ZCARD rank

🔟 清理数据(排行榜维护)

1
2
ZREMRANGEBYRANK rank 100 -1
ZREMRANGEBYSCORE rank 0 59

3.Zset底层怎么实现的:

Redis 的 ZSet 底层使用「Hash 表 + 跳表(SkipList)」实现:
Hash 用于根据 member 快速查 score,跳表用于按 score 有序排列并支持范围查询和排名。

跳表的结构(核心)<具体图解看小林>;

链表在查找时需要从到尾依次查找,时间复杂度时O(N); 跳表是在链表的基础上改过来的,实现了一种多层的有序链表;时间复杂度是O(logN)

1️⃣ 多层链表结构

1
2
3
Level 3:    1 ---------> 9
Level 2: 1 -----> 5 -> 9
Level 1: 1 → 3 → 5 → 7 → 9 → 11 → 13
  • 最底层:完整数据
  • 上层:抽样出来的索引
  • 层数越高,节点越少

2️⃣ 节点结构(实现角度)

每个节点包含:

1
2
value / score // 元素和元素的权重
forward[] // 指向各层下一个节点

二、层级结构是怎么来的?(核心机制)

1️⃣ 每个节点都有“高度(level)” 一个节点能存储多个高度信息;

  • 节点不是只在一层
  • 而是:
1
2
Level 1 一定有
Level 2 / 3 / 4 ... 看运气

示意:

1
2
3
节点 A:Level 1
节点 B:Level 1 , 2
节点 C:Level 1 , 2 , 3

2️⃣ 随机生成层数(最关键)层高最大限制是64

插入节点时:

1
P(节点升一层) = 1/2 或 1/4

示例(概率递减):

1
2
3
4
只有1 层:50%
有2 层:25%
有3 层:12.5%
有4 层:6.25%

层数越高,节点越少

这正是“索引层”的来源。

4.跳表怎么设置层高的:

跳表在创建节点时,会生成(0~1)的随机数,如果这个数小于0.25(也就是1/4的概率),那么层数就增加一层,然后继续生成随机数,直到随机数大于0.25;

5.Redis为什么使用跳表不使用B+树:

一、跳表 vs B+ 树(核心对比)

对比点 跳表(SkipList) B+ 树
主要使用场景 内存 磁盘 / 数据库
实现复杂度 简单 非常复杂
查询复杂度 O(log n) O(log n)
节点结构 链表 + 多层索引 多叉树
维护成本

二、为什么 不使用 B+ 树

1. B+ 树是为「磁盘 IO」设计的

B+ 树的核心优势是:

  • 减少磁盘 IO 次数

但 Redis:

  • 数据 全在内存
  • 没有磁盘随机 IO 问题

B+ 树的最大优势在 Redis 场景中用不上

2. B+ 树实现和维护成本高

B+ 树需要处理:

  • 节点分裂
  • 节点合并
  • 复杂的父子指针关系

B+树插入和删除可能会导致节点分裂和合并;

3.写入性能与代价:

B+树插入数据导致页分裂时,需要移动大量数据或者改变树的结构;
跳表的插入和删除操作是局部的,只需要修改节点前后指针,并随机生成层高即可;

6.压缩链表的实现;

7.Redis中的listpack;

<看小林>

8.哈希表是怎么扩容的:

是通过rehash扩容的, 当数据增多触发rehash操作时:
1.给哈希表2分配一个比哈希表1大2倍的空间;
2.把哈希表1中的数据迁移到哈希表2中;
3.哈希表1中的空间被释放,在哈希表2中创建一个空白的哈希表3,为下一次rehash做准备;

rehash在数据迁移时如果数据量太大,会导致阻塞Redis,所以Redis采用的是渐进式Rehash;
步骤:
1.给哈希表2分配空间;
2.在rehash期间,每次哈希表进行 新增,删除,查找,或更新操作时, 会顺便将哈希表1中的旧数据迁移到哈希表2上;

rehash期间,元素的操作会在两张表上进行;

9.Redis中字符串是用什么存储的?为什么不用c语言中的字符串?

Redis 中的字符串使用 SDS(Simple Dynamic String)存储;

Redis 的 SDS 本质是 带元信息的动态字符串,简化结构如下:

1
2
3
4
5
6
struct sdshdr {
int len; // 已使用的字符串长度
int alloc; // 已分配的总空间(不包含结尾 '\0')
unsigned char flags; // SDS 类型(sdshdr8/16/32/64)
char buf[]; // 实际存储字符串的字节数组
};

O(1)复杂度获取字符串
SDS 在数据结构中显式维护了字符串长度字段 len,因此获取字符串长度时不需要像 C 语言字符串那样遍历查找 \0,可以直接 O(1) 得到长度,效率更高。

SDS 是二进制安全的。
由于字符串的真实长度由 len 字段记录,而不是依赖 \0 作为结束符,因此 SDS 可以存储任意二进制数据,包括图片、序列化对象等,不会因为中间包含 \0 而被截断。

SDS 不会发生缓冲区溢出。
SDS 通过 alloc 字段记录已分配的空间大小,在进行拼接或修改操作前会先检查剩余空间是否足够,不够时自动扩容,从机制上避免了 C 字符串常见的缓冲区溢出问题。

10.Redis的Zset,在项目里具体用法是什么?

1.需要给元素排序,取排名,按分数范围筛选的场景;
常见的有:排行榜,延迟任务队列,带权重的消息队列;

延迟任务队列: 比如电商订单30分钟未支付自动取消,定时发送通知; 用任务ID作为元素,把”任务执行时间戳(创建时间 + 30 分钟)”作为分数,后台开一个线程不断用ZRANGEBYSCORE查询”分数小于当前时间戳”的任务,执行完就用ZREM删掉这样就能实现定时任务,不用复杂的调度框架;

带权重的消息队列: 给消息用分数设置权重,消费者优先处理分数高的消息,保证核心业务的优先级;

线程模型

1.Redis为什么快?

单线程Redis吞吐量就能达到10W/秒;

原因:
1.大部分操作都是在内存中完成,并且采用了高效的数据结构;
2.单线程模型避免了线程之间切换的开销;
3.采用了IO多路复用机制处理大量客户端Socket请求;

2.Redis哪些地方使用了多线程?

Redis单线程指的是(接收客户端请求->解析请求->进行数据读写操作->发送数据给客户端)这个过程是一个线程(主线程)来完成的;

但是Redis程序并不是单线程的,Redis在启动时会启动后台线程(BIO);

Redis 的核心命令执行是单线程的,但在 I/O、后台任务和部分网络处理上使用了多线程。

Redis 使用多线程的地方(重点 )


1️⃣ 后台持久化线程(一直都有)

使用场景

  • RDB 持久化
  • AOF 重写
  • 主从复制(部分阶段)

实现方式

  • 使用 fork 子进程(不是线程)
  • 避免阻塞主线程

这是 多进程,不是多线程,容易被追问区分


2️⃣ 异步删除(Redis 4.0+)

使用场景

  • UNLINK
  • FLUSHDB ASYNC
  • FLUSHALL ASYNC

为什么用线程?

  • 大 key 删除会阻塞主线程
  • 删除涉及大量内存释放

特点

  • 主线程只做逻辑删除
  • 实际释放内存交给 后台线程

3️⃣ 网络 I/O 多线程(Redis 6.0+,重点)

使用场景

  • Socket 读写
  • 协议解析(部分)

特点

  • 命令执行仍然是 单线程
  • 多线程只负责:
    • 读请求
    • 写响应

配置项

1
2
io-threads 4
io-threads-do-reads yes

4️⃣ 定期任务 / BIO 线程

Redis 内部有 BIO 线程池

BIO 类型 用途
BIO_CLOSE_FILE 关闭文件
BIO_AOF_FSYNC AOF 刷盘
BIO_LAZY_FREE 异步释放内存

本质都是 “慢操作下放”

3.Redis怎么实现IO多路复用?<详细过程看小林>

IO 多路复用是指一个线程同时监听多个 IO 事件,当某个连接就绪时再进行处理,而不是为每个连接创建一个线程。

Redis 基于事件驱动模型实现 IO 多路复用,通过封装的 ae 事件框架,在不同操作系统下选择最优的多路复用机制,例如在 Linux 上使用 epoll。主线程通过 epoll_wait 同时监听大量客户端 socket 的读写事件,当某个连接就绪时触发对应的回调函数,完成请求读取、命令解析、执行以及结果返回。整个过程中命令执行仍由单线程完成,从而避免了线程切换和锁竞争,同时借助 IO 多路复用在单线程下实现了高并发网络处理。

“多路”指的是多个网络连接客户端,”复用”指的是复用同一个线程;

4.Redis的网络模型

Redis 采用基于 Reactor 模式的单线程 I/O 多路复用网络模型
使用 select / poll / epoll / kqueue 等机制,在一个线程中高效处理大量客户端连接

多线程网络 I/O

但命令执行依然单线程

I/O 多路复用 ≠ 多线程

  • Redis 6.0 之前
    👉 I/O 多路复用 + 单线程
  • Redis 6.0 之后
    👉 I/O 多路复用 + 多线程处理网络读写
    👉 命令执行仍然是单线程

Redis 使用 I/O 多路复用机制监听大量连接。
在 Redis 6.0 之前,网络 IO 和命令执行都由单线程完成;
在 6.0 之后,引入了多线程来并行处理网络读写和协议解析,
但命令执行仍然是单线程,因此 I/O 多路复用和多线程并不矛盾。

在 Redis(尤其是 6.0+)的语境里,网络 I/O 一般“包含”
网络读写 + 协议解析(以及响应序列化)

I/O 多路复用和事件分发由主线程单线程完成,Redis 6.0 以后将 socket 读写、RESP 协议解析以及响应序列化交给 I/O 线程并行处理,而命令执行仍然是单线程

事务

1.如何实现Redis原子性

核心原因:命令执行是单线程的

Redis 在任意时刻只会有一个命令在执行, 命令执行过程中不会被打断, 因此 单条命令天然是原子操作;

一般用lua脚本来保证多条命令的原子性

或者用事务(MULTI / EXEC)保证多条命令的原子性(不推荐)

1
2
3
4
MULTI
INCR a
INCR b
EXEC

Redis 事务的原子性特点:

Redis 事务不保证回滚
但保证 命令顺序 & 原子执行

Redis 为什么不需要锁?

因为:

1
2
3
单线程执行命令
→ 不存在并发写
→ 不需要加锁

日志

1.Redis有哪两种持久化方式?分别的优缺点是什么?

Redis 有两种持久化方式:RDB 和 AOF
RDB 适合快速恢复和备份,AOF 提供更高的数据安全性,生产环境通常 RDB + AOF 混合使用

1. AOF(Append Only File)

是什么?

以日志方式记录每一条写命令

2. RDB(Redis DataBase)

是什么?

定期生成某一时刻 Redis 内存数据的快照,并保存到磁盘

一.AOF的实现方法?

1. 命令追加(Append)

Redis每执行完一个命令,就会把命令以追加的形式写入文件中;


2. AOF 缓冲区(AOF Buffer)

Redis 不会每条命令都立刻写磁盘,而是:

  • 先写入 AOF 缓冲区(内存)
  • 再统一刷盘

目的:

  • 减少系统调用
  • 提升性能

3. 刷盘策略(appendfsync)把AOF中的数据写入Redis

决定 AOF 数据什么时候真正落盘

策略 行为 安全性 性能
always 每条命令都 fsync 最高 最差
everysec(默认) 每秒 fsync 一次 较好
no 交给 OS 最低 最好

everysec 是性能和安全的平衡


AOF 重写(Rewrite)机制

为什么需要重写?

  • AOF 只增不减
  • 文件会越来越大

二.RDB是如何实现的?

RDB 通过在某一时刻对 Redis 内存数据做快照,并由子进程将快照写入磁盘文件来实现持久化,利用 fork + 写时复制保证性能。

Redis提供了两个命令来生成RDB文件,分别是SAVEBGSAVE,区别在于是否在主线程里执行:

SAVE:

执行流程

1
2
3
4
5
6
7
客户端发送 SAVE

主线程直接遍历内存

主线程写 RDB 文件

写完才继续处理请求

BGSAVE:

一次 RDB 快照的完整流程:

1
2
3
4
5
6
7
8
9
触发 RDB

主线程 fork 子进程

子进程遍历内存数据

生成临时 RDB 文件

原子替换旧 RDB 文件

生成的文件通常是:dump.rdb

子进程拥有主进程内存的“逻辑副本”, 实际物理内存共享

三、RDB 的三个核心实现机制


1.fork 子进程(关键)


2. 写时复制(Copy-On-Write,COW)


3. 原子替换文件


写时复制是操作系统在 fork 后提供的一种内存隔离机制。当 Redis 主线程修改某个内存页时,操作系统会复制该页,使父进程看到新数据,而子进程仍然看到 fork 时的旧数据。子进程基于 fork 时刻的内存快照生成 RDB 文件,后续的修改不会写入本次 RDB,也不存在“备份数据写入下一次 RDB”的过程。

三、RDB 数据到底是“怎么来的”?(关键)

正确理解:读 → 序列化 → 写磁盘

在 BGSAVE 中:

1️⃣ fork 后
2️⃣ 子进程 直接读取内存中的数据
3️⃣ 把数据 序列化成 RDB 格式
4️⃣ 写入磁盘文件

📌 注意:

没有任何一步是“把内存页复制到 RDB 文件”


四、那写时复制(COW)到底干了什么?

COW 只解决一个问题:

子进程读到的内存数据,在整个快照期间保持不变


举个完整例子(强烈推荐看)

fork 时刻

1
key = 1
  • 父进程 & 子进程
  • 共享同一块内存页

fork 之后,主线程修改数据

1
SET key 2

发生的事:

  1. OS 发现该页被共享
  2. 复制该内存页
  3. 主进程写新页(key=2)
  4. 子进程继续读旧页(key=1)

📌 于是:

  • RDB 里是 key=1
  • 内存中最终是 key=2

五、所以:没被修改的数据去哪了?

👉 一直在原来的内存页里

  • 子进程可以直接读取
  • 一样会被写进 RDB 文件

📌 没有“没被复制就没进 RDB”这回事


六、用一句话打穿你的困惑

COW 决定“是否复制内存页”,
RDB 决定“是否把数据序列化写磁盘”,
两者完全不是一回事。


七、再用一个终极类比(非常重要)

复印书 📚

  • 内存:书架上的书
  • fork:给你一张“书架访问权限”
  • COW:别人改某一页时,复印一页给他
  • RDB:你坐在书架前,一页页抄到笔记本里

📌 你抄书(写 RDB)
不需要先把整本书复印一遍

RDB 的触发方式:

1️⃣ 手动触发

命令 行为
SAVE 阻塞主线程(不推荐)
BGSAVE 后台 fork(常用)

2️⃣ 自动触发(配置)

1
2
3
save 900 1
save 300 10
save 60 10000

两者优缺点:

Redis 持久化方式优缺点对比表(图表形式)

持久化方式 优点 缺点
RDB (快照) ✅ 文件体积小,二进制压缩 ✅ Redis 重启恢复速度快 ✅ 对运行时性能影响小(异步 BGSAVE) ✅ 适合备份和灾难恢复 ❌ 数据安全性低,可能丢失最近一次快照后的数据 ❌ fork 子进程成本高,大内存场景易产生延迟抖动 ❌ 不够实时,不适合强一致性场景
AOF (追加日志) ✅ 数据安全性高(最多丢 1 秒数据) ✅ 以写命令记录,数据完整性强 ✅ 文件可读,可手工修复 ✅ 更适合在线业务 ❌ 文件体积大,依赖 rewrite 压缩 ❌ Redis 重启恢复慢(需重放命令) ❌ 持续写磁盘,性能开销高于 RDB

缓存淘汰和过期删除

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

1.内存淘汰策略是在内存满的时候,Redis会触发内存淘汰策略来淘汰一些不必要的内存资源;
2.过期删除策略是将已经过期的键值对进行删除,Redis采用的删除策略是惰性删除+定期删除;

2.介绍Redis中的内存淘汰策略:

一、Redis 为什么需要内存淘汰策略

  • Redis 基于内存存储
  • 内存资源有限
  • used_memory > maxmemory
    必须淘汰部分数据,否则写操作会失败

二、Redis 八种内存淘汰策略(重点)

Redis 的淘汰策略可以从 两条维度 来记:

  • 是否设置过期时间
  • 按什么规则淘汰

不进行数据淘汰的策略(1种):

1. noeviction(默认策略)

  • 不淘汰任何数据
  • 内存满了之后:
    • 写操作(SET / LPUSH 等)直接返回错误
    • 读操作正常

📌 适用场景

  • 数据绝对不能丢(如金融核心数据)

进行数据淘汰的策略:

二、基于「过期时间」的淘汰策略

2️⃣ volatile-ttl

  • 只淘汰 设置了过期时间的 Key
  • 优先淘汰 剩余 TTL 最短 的 Key

场景:缓存数据都设置了过期时间


3️⃣ volatile-random

  • 设置了过期时间的 Key
  • 随机淘汰

场景:对命中率要求不高


4️⃣ volatile-lru ⭐

  • 设置了过期时间的 Key
  • 淘汰 最近最少使用(LRU) 的 Key

场景:

  • 典型缓存系统(常用

5️⃣ volatile-lfu ⭐

  • 设置了过期时间的 Key
  • 淘汰 使用频率最低(LFU) 的 Key

场景:

  • 热点数据明显、访问频率差异大

三、基于「所有 Key」的淘汰策略

6️⃣ allkeys-random

  • 所有 Key
  • 随机淘汰

场景:

  • 数据价值相近

7️⃣ allkeys-lru ⭐⭐⭐(最常用)

  • 所有 Key
  • 淘汰 最近最少使用(LRU) 的 Key

场景:

  • 通用缓存场景(面试最推荐

8️⃣ allkeys-lfu ⭐⭐⭐

  • 所有 Key
  • 淘汰 使用频率最低(LFU) 的 Key

场景:

  • 高并发、热点 Key 非常明显

3.介绍Redis中的过期删除策略:

Redis的过期删除策略是选择惰性删除+定期删除两者的配合使用;

Redis惰性删除策略是由db.c文件中的expireNeeded函数实现的,Redis在访问或者修改key之前,都会调用expireNeeded函数来检查,检查key是否过期;过期则删除,不过期则正常返回键值对给客户端;

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

不会,Redis的过期删除策略是选择惰性删除+定期删除两者的配合使用;

惰性删除的做法是,不主动删除过期键,每次从数据库访问key时,都会检测key是否过期,如果过期即删除;
定期删除是,每隔一段时间随机从数据库中抽取一定数量的key进行检查,并删除其中的过期key;

5.不过期立即删除的原因:

在key过多时,删除过期key可能会占用相当一部分cpu时间,在cpu时间紧张的情况下,会对服务器的响应时间和吞吐量造成影响;

集群

1.Redis主从同步中的增量和完全同步怎么实现?

redis中从结构只有一个主节点,可以有多个从节点;
从节点需要执行slaveof命令来确定主节点;
从节点连接主节点的第一次需要做全量同步;
主从节点数据同步分为全量同步和增量同步;
全量同步有三个阶段:
1.从节点发送自己的replid 和 offset 给主节点来和主节点的 replid 来匹配,匹配成功说明不是第一次连接,不需要做全量同步只需要做增量同步,否则需要做全量同步; 如果是全量同步,主节点会返回自己的replid 和 offset给从节点,让从节点继承自己的replid;
2.主节点执行bgsave生成RDB操作,并且将未来得及存储在RDB中的命令存储到缓冲区repl_baklog中, 然后将RDB传给从节点,从节点执行brush指令,清空自身然后执行RDB文件;
3.主节点将缓存在repl_baklog中的数据发送给从节点,从节点执行剩下的命令实现数据同步;

增量同步两个阶段:
1.和全量同步第一阶段一样,replid匹配说明是做增量同步;

2.主节点将缓存在repl_baklog中的数据发送给从节点,从节点执行剩下的命令实现数据同步;

repl_backlog相当于一个环形数组,可以覆盖原先传输给从节点的数据;但是如果从节点宕机,repl_backlog就会发生错误,覆盖未传输的数据,就要等从节点修复好执行全量同步;

执行全量同步的两种情况:
1.第一次连接
2.从节点宕机导致repl_backlog覆盖未传输的数据;


修正:
第一阶段从节点还会和prior_replid匹配,
如果 replid = 主节点当前 replid 且 offset 合法 → 执行增量同步
如果 replid = 主节点的 prior_replid (历史id)也可以尝试执行增量同步(PSYNC 2 特性)
否则才执行全量同步

第一次连接不一定增量同步,
如果主节点重启过,产生新的replid,
从节点有主节点的 旧 replid(主节点 prior_replid == 从节点 replid)
offset 仍然在 backlog 范围内
仍然可以增量同步!

全量同步流程(完整步骤)

① 从节点发送 PSYNC,请求开始同步


② 主节点返回 FULLRESYNC

1
FULLRESYNC <replid> <offset>

主节点:

  • 生成新的 replication id
  • 告知当前复制起点 offset

③ 主节点生成 RDB 快照

  • fork 子进程
  • 生成 RDB 文件(内存快照)

fork 期间:

  • 主线程仍可处理请求
  • 新写入命令会写入 replication buffer

④ 发送 RDB 给从节点

  • 从节点接收 RDB
  • 清空旧数据
  • 加载新数据

⑤ 发送增量命令

  • 主节点把:
    • RDB 期间缓存的写命令
    • 加载完成后新的写命令
  • 发送给从节点

到此主从数据 完全一致

🔹 增量同步流程

① 从节点重连后发送 PSYNC

1
PSYNC <replid> <offset>

② 主节点判断

  • replid 是否一致
  • offset 是否仍在 repl_backlog_buffer 范围内

③ 返回 CONTINUE

1
+CONTINUE

④ 主节点发送缺失的命令

  • 从 offset 之后
  • 从 repl_backlog_buffer 中补发

无需 RDB,效率极高

2.Redis主从集群可以保证数据一致性吗?

Redis主从和集群在CAP理论都属于AP模型,即在面临网络分区时选择保证可用性和分区容忍性,而牺牲强一致性;这意味着在网络分区的情况下,Redis主从复制和集群可以继续提供服务并保持可用,但可能出现部分节点之间的数据不一致;

3.哨兵机制的原理是什么?

一、什么是 Redis 哨兵机制

Redis Sentinel 是一套 分布式高可用(HA)解决方案,主要作用:

  1. 监控(Monitoring)
    监控 Master 和 Slave 是否正常运行
  2. 故障发现(Failure Detection)
    判断 Master 是否下线
  3. 自动故障转移(Failover)
    Master 挂了,自动选举新的 Master
  4. 通知(Notification)
    将变更通知客户端或运维系统

4.哨兵机制的选主节点的算法介绍一下?

当Redis集群的主节点故障时,Sentinel集群将从剩下的从节点中选举一个新的主节点,有以下步骤:
1.故障节点主观下线;
2.故障节点客观下线;
3.Sentinel集群选举Leader;
4.Sentinel Leader决定新主节点;

1. 故障节点主观下线(SDOWN)

发生对象
单个 Sentinel 对 Master 的判断

判定方式

  • Sentinel 会周期性向 Master 发送 PING
  • 在配置的 down-after-milliseconds 时间内:
    • 未收到有效回复

2. 故障节点客观下线(ODOWN)

发生对象
Sentinel 集群的共识判断

判定流程

  1. Sentinel 发现 Master SDOWN

  2. 向其他 Sentinel 发送:

    1
    is-master-down-by-addr
  3. 其他 Sentinel 返回对故障节点的判断

  4. 如果:

    • 同意下线的 Sentinel 数量 ≥ quorum

结果

  • Master 被标记为 客观下线(ODOWN)
  • 如果是从节点操作到此为止,如果是主节点,则需要从从节点中选举一个节点升级为主节点;

3.Sentinel 集群选举 Leader

如果需要从Redis集群中选一个节点作为主节点,首先需要从Sentinel集群中选举一个Sentinel节点作为Leader;每一个Sentinel节点都能成为Leader,当主节点被判定为客观下线(ODOWN)后,所有 Sentinel 会发起投票选举过程,每个 Sentinel 在一次故障转移中只能投出一票,获得超过半数 Sentinel 投票的节点将被选为 Leader Sentinel。

4.Sentinel Leader决定新主节点

Sentinel Leader 负责后续的故障转移流程,包括从存活的从节点中选择新的主节点并完成主从切换,从而避免多个 Sentinel 同时执行切换操作导致冲突:

1.过滤故障节点;
2.选择优先级slave-priority最大的从节点作为主节点,如果不存在则继续;
3.选择复制偏移量(数据写入量的字节,记录了多少数据;主服务器会把偏移量同步给从服务器,当主从的偏移量一致,则数据是完全同步)最大的从节点作为主节点,如不存在则继续;
4.选择runid(Redis每次启动的时候随机生成runid作为Redis的标识)最小的从节点作为主节点;

5.Redis 分片集群模式(Redis Cluster)及优缺点;

一、什么是 Redis 分片集群模式(Redis Cluster)

Redis Cluster 是 Redis 提供的 去中心化分布式集群方案,通过**数据分片(Sharding)**将数据分布在多个主节点上,实现 高可用 + 高扩展性

核心机制:

  • 将整个键空间划分为 16384 个 Hash Slot(哈希槽)
  • 每个 Master 负责一部分 Slot(分配槽有: 平均分配手动分配两种)
  • 每个 Master 可配置多个 Slave
  • 客户端可直接与任意节点通信(无代理)

二、Redis Cluster 的工作原理(简述)

  1. 客户端对 Key 做 CRC16 运算
  2. 计算 CRC16(key) % 16384 得到 Slot
  3. 根据 Slot 定位到对应的 Master
  4. 若访问错误节点,返回 MOVED / ASK 重定向

Redis Cluster 主要优缺点

优点 缺点
支持数据分片,单节点压力小 架构复杂,运维成本高
支持水平扩展,可动态扩容 跨 Slot 的多 Key 操作不支持
多主写入,性能高 不支持强一致性,可能丢数据
主从 + 自动故障转移,高可用 事务 / Lua 脚本受 Slot 限制

6.Cluster集群客户端是怎么知道该访问哪个分片的?

Redis Cluster 中,客户端通过 Hash Slot 计算 确定 Key 属于哪个分片,如果访问了错误节点,集群会通过 MOVED / ASK 重定向 指引客户端访问正确的分片。

场景

1.为什么用Redis?

Redis具有高性能高并发两种特性;

Redis数据存储在内存中的,查询的速度非常快;底层数据结构也是高效的;

单台设备的Redis的QPS(Query Per Second)是MySql的10倍,Redis单机的QPS可以轻松破10W;

Redis很适于作数据库的缓存;

2.为什么Redis比MySql要快?

1.内存存储: Redis 的数据主要存储在内存中,内存访问是纳秒级,MySQL 的数据主要存储在磁盘中,即使有 Buffer Pool,本质仍依赖磁盘 IO,随机磁盘 IO 相比内存 IO 慢几个数量级

2.简单数据结构: Redis 使用为内存场景优化的 Key-Value 数据结构(如哈希表、跳表),读写路径短,常见操作复杂度为 O(1) 或 O(log N);而 MySQL 主要基于 B+ 树索引,需要经历索引查找、可能的回表以及行记录解析,执行链路更长,因此在简单读写场景下性能明显不如 Redis。

3.线程模型:

Redis:

  • 命令执行是 单线程
  • 通过 IO 多路复用(epoll) 处理高并发连接
  • 无锁竞争、无上下文切换

MySQL:

  • 多线程模型

  • 存在:

    • 锁竞争
    • 上下文切换
    • 事务调度

3.本地缓存和Redis缓存的区别

本地缓存是进程级内存缓存,访问速度最快但无法共享;Redis 是分布式缓存,支持多实例共享和高可用,但需要网络通信。

**本地缓存: **本地缓存是将数据存储在应用进程的内存中,通过本地方法直接访问,不涉及网络通信,因此访问速度最快;但每个应用实例都有独立的一份缓存数据,无法在多实例之间共享,数据随着进程的重启而失效,一致性和扩展性较差,通常适用于热点数据或一致性要求不高的场景。

分布式缓存(Redis): 分布式缓存以 Redis 为代表,将数据存储在独立的缓存服务内存中,通过网络提供统一访问,支持多应用实例共享数据,并具备过期机制、持久化和高可用能力;虽然相比本地缓存存在一定的网络开销,但在分布式系统中能够更好地保证数据一致性和系统扩展性,适合高并发访问场景。

4.Redis的应用场景是什么?

Redis是一种基于内存的数据库,对数据的读写操作都是在内存中完成,因此读写速度非常快,常用于 缓存,消息队列,分布式锁;

Redis 实现分布式锁的基本命令

1
SET lock_key unique_value NX EX 30

参数含义

参数 作用
NX key 不存在才设置(保证互斥)
EX 30 30 秒过期(防死锁)
unique_value 唯一标识(UUID / 线程ID)

为什么不用 SETNX + EXPIRE

  • 因为不是原子操作
  • 可能 SETNX 成功后进程崩溃,EXPIRE 没执行 → 死锁

正确的解锁方式(Lua 脚本)

原子校验 + 删除

1
2
3
4
5
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end

Lua 脚本在 Redis 中是原子执行的

Redis 是怎么实现消息队列的?

Redis 并不是专业 MQ,但可以当轻量 MQ 用


1️⃣ 基于 List 实现(最早、最简单)

核心命令

1
2
LPUSH queue msg
BRPOP queue 0

2️⃣ 基于 Pub/Sub(发布订阅)

命令

1
2
SUBSCRIBE channel
PUBLISH channel message
优点 缺点
实时性强 消息不持久化
广播模式 订阅者不在线就丢

3️⃣ 基于 Stream(重点,面试推荐)

Redis 5.0 引入:真正的消息队列

核心命令

1
2
3
XADD stream * field value
XREADGROUP GROUP g1 c1 COUNT 1 STREAMS stream >
XACK stream g1 id

5.Redis支持并发操作吗?

支持并发访问,但不支持并发执行命令。

  • ✅ 多个客户端 可以同时连接、同时发请求
  • ❌ 同一时刻 只有一个命令在执行

单个Redis命令具有原子性,多个Redis命令通过事务保证原子性;

6.Redis分布式锁的实现原理?什么场景下用到分布式锁?

分布式锁是用于分布式环境下并发控制的一种机制,用于控制某个资源在同一时刻只能被一个应用所使用;

Redis 分布式锁通过 SET lock_key unique_value NX EX 10 实现,利用 NX 保证互斥,EX 10防止死锁,解锁时使用 Lua 脚本校验 value 原子删除。
适用于多实例部署下的共享资源控制,如秒杀、分布式定时任务和防止重复操作。但在主从切换和网络分区场景下存在一致性风险,不适合强一致性业务。

lock_key就是key键;
unique_value是客户端生成的唯一标识,区分来自不同客户端的锁操作;
NX代表只有在lock_key不存在时,才对lock_key进行设置操作;
EX 10表示设置lock_key的过期时间为10s,避免客户端发生异常而无法释放锁;

7.Redis的大key问题是什么?

一、大 Key
Key 对应的 value 占用内存过大,或包含的元素数量过多,即使 key 本身很短。不是 key 名字长,而是 value 大

一般认为字符串类型的String ≥ 1MB:几乎一定是大 Key;或者集合类型的元素数量超过1万,也算是大key;

二、大 Key 会带来哪些问题?(重点)

1️⃣ 阻塞 Redis 主线程(最严重)

Redis 单线程执行命令, 一条命令执行几百 ms ~ 几秒, 整个 Redis 无法响应其他请求


2️⃣ 网络 IO 压力巨大


3️⃣ 内存碎片 & 内存不可控

  • 一次性申请 / 释放大块内存,容易触发内存抖动

4️⃣ 影响主从复制 & 集群稳定性

  • 大 Key 同步慢
  • 复制延迟
  • 集群节点负载不均

5️⃣ 过期 / 删除风险极高

1
DEL big_key
  • 同步删除
  • 直接卡住 Redis

这是线上事故高发点


三、怎么判断 Redis 是否存在大 Key?(必会)

1️⃣ 官方工具(推荐)

1
redis-cli --bigkeys
  • 扫描所有 key
  • 输出各类型最大 key

⚠️ 线上慎用(会扫全库)


2️⃣ 采样方式(生产推荐)

1
SCAN cursor COUNT 100

配合:

  • STRLEN
  • HLEN
  • LLEN
  • SCARD
  • ZCARD

String

1
STRLEN key

Hash

1
HLEN key

List

1
LLEN key

Set

1
SCARD key

ZSet

1
ZCARD key

SCAN 是渐进式,不阻塞


3️⃣ 监控慢查询

1
SLOWLOG get

发现:

  • DEL
  • HGETALL
  • LRANGE

四、大 Key 如何解决?(核心)

1️⃣ 拆分 Key(最根本)

❌ 错误

1
user:1001 → 一个 Hash 存全部数据

✅ 正确

1
2
3
user:1001:base
user:1001:order
user:1001:cart

2️⃣ 大 Key 删除 —— 用 UNLINK(必背)

1
DEL big_key

1
UNLINK big_key

📌 UNLINK 异步删除,不阻塞主线程


3️⃣ List / Hash 拆分存储

1
2
3
4
list:page:1
list:page:2
hash:{userId}:1
hash:{userId}:2

4️⃣ 控制元素数量

  • List 限制长度
  • Hash 限制 field 数
  • 定期清理

8.什么是热key?

一、什么是热 Key?

热 Key
在短时间内被大量并发访问的 Key,即使它本身很小,也会对 Redis 造成巨大压力。

1.QPS集中在特定的key: Redis实例的总QPS为10,000,而其中一个key的每秒访问量达到了7000;
2.带宽使用率集中在特定的key: 对一个拥有上千个成员且总大小为1MB的HASH Key每秒发送大量的HGETALL操作请求;
3.CPU使用时间占比集中在特定的key: 对一个拥有数万个成员的Key(Zset类型),发送大量的ZRANGE操作请求;

9.怎么解决热key问题?

  • 利用本地缓存 (Local Cache)
    将识别出的热点数据同步到应用服务器的内存中(使用 Guava Cache 或 Caffeine)。请求先访问本地内存,不访问 Redis,从而彻底分流压力。

  • Key 分散(加随机后缀)
    将热点 Key 复制为多个副本并散落在不同的分片(Slot)上。

    • 例如:将 hot_key 存储为 hot_key_1, hot_key_2, hot_key_n
    • 读取时:客户端随机选择一个后缀进行请求,将单点压力平摊到整个集群。
  • 读写分离
    对于读多写少的热点,增加从节点(Slave)数量,通过负载均衡将读请求分散到多个从节点。

  • 多级缓存(本地 + Redis)

    1
    2
    3
    L1:本地缓存
    L2:Redis
    L3:DB
    • 热点请求直接命中 L1
    • Redis 压力大幅降低

    限流 + 降级(兜底方案)

    场景

    • 突发流量
    • 秒杀、活动

    手段

    • 接口限流
    • 返回兜底数据
    • 服务降级

10.如何保证Redis和MySql数据缓存一致性问题?

Redis 是缓存,MySQL 是数据源,无法做到强一致,只能保证最终一致。

1️⃣ 读流程(读缓存)

1
2
3
4
5
6
7
8
9
读请求

查 Redis

未命中

查 MySQL

写回 Redis

2️⃣ 写流程(关键)

✅ 正确顺序(必背)

1
2
3
更新 MySQL

删除 Redis

如果先删缓存,会写回旧数据

删缓存失败怎么办?(面试加分点)

方案 1️⃣:重试 + 延迟删除(常用)(延迟双删)

1
2
3
4
5
更新 DB

删除缓存

延迟 500ms 再删一次

📌 防止并发读回写旧值


方案 2️⃣:异步消息补偿(进阶)

  • 删除失败 → 发 MQ
  • 消费者重试删除

11.缓存雪崩,击穿,穿透是什么?怎么解决?

1.什么是缓存穿透?

缓存穿透 是指查询一个在数据库和缓存中都不存在的数据。由于缓存无法命中(Miss),这个请求会直接穿透缓存,直达数据库。如果有大量这样的请求同时发生,数据库将不堪重负,甚至可能被压垮。

核心问题: 查询的是一个系统内根本不存在的数据。

解决方法:

方案一:缓存空对象(Null Object Caching / 缓存默认值)
这是最常用、最直接的解决方案。

思路:当从数据库查询不到数据时,我们仍然将这个“空结果”(例如 null)进行缓存,并为其设置一个较短的过期时间(比如 1-5 分钟)。

流程:

请求查询 Key。

缓存未命中。

查询数据库,发现数据不存在。

将 Key 和一个表示“空”的特殊值(如 “NULL”)写入缓存,并设置过期时间。

后续再有相同 Key 的请求时,缓存直接返回这个空值,而不会访问数据库。

优点:

实现简单,效果立竿见影。

缺点:

内存浪费:如果恶意攻击者构造大量不同的无效Key,会导致缓存中存储大量无用的空对象,占用内存空间。

数据短期不一致:如果在空对象缓存有效期内,数据库里新增了该数据,会导致短期内读到旧的空数据。可以通过设置较短的过期时间或主动删除空缓存来缓解。

方案二:布隆过滤器(Bloom Filter)

这是一个更高效、更专业的内存节约型解决方案。

思路:在缓存之前,加一道“守卫”——布隆过滤器。它是一个概率型数据结构,可以告诉你 “某个元素一定不存在” 或 “可能存在”。

工作原理:

初始化:将所有可能存在的、合法的Key(例如所有有效的用户ID)预先加载到布隆过滤器中。

查询过程:

当一个查询请求过来时,首先去布隆过滤器判断这个Key是否存在。

如果布隆过滤器说 “不存在”,那么这个Key一定不存在于数据库中。此时可以直接返回空结果,无需查询缓存和数据库。

如果布隆过滤器说 “可能存在”,那么再按正常流程去查询缓存和数据库。

优点:

空间效率极高:占用内存非常小,因为它不存储数据本身,只存储数据的“指纹”(哈希位)。

性能极好:查询时间复杂度是 O(k),k是哈希函数的个数,速度非常快。

缺点:

存在误判率:布隆过滤器判断“可能存在”时,有极小的概率是误判(即Key其实不存在)。但这个概率可以通过调整布隆过滤器的参数(如位数组大小、哈希函数数量)来控制到非常低。

删除困难:标准的布隆过滤器不支持删除元素(但有其变种如 Counting Bloom Filter 支持)。

需要数据预热:需要系统在启动或某个时刻,将有效的数据同步到布隆过滤器中。

2.缓存雪崩:

1.什么是缓存雪崩?

缓存雪崩 是指在某一时刻,大量缓存数据同时失效(或缓存服务宕机),导致所有原本应该访问缓存的请求,瞬间都涌向了数据库。数据库无法承受如此巨大的瞬时压力,从而造成数据库响应缓慢甚至崩溃,进而导致整个系统瘫痪。

核心问题: 大量缓存集中失效或缓存服务不可用。

场景一:大量缓存Key同时过期
这是最典型的缓存雪崩场景。

原因:在给缓存数据设置过期时间时,如果采用了统一的过期时间(例如,都在凌晨零点过期),那么所有这些缓存都会在同一时刻失效。

例子:一个电商网站,在每天零点刷新优惠券、秒杀商品等信息,如果这些缓存都设置在零点过期,那么零点时所有相关请求都会直接访问数据库。

场景二:Redis集群宕机
原因:Redis服务节点因为断电、网络故障、内存爆满、硬件问题等原因宕机。

例子:主从集群中,主节点宕机且从节点未能成功切换,或者整个Redis集群不可用。

场景三:缓存预热问题

原因:系统刚重启后,缓存是空的(冷启动)。如果此时有大量用户请求涌入,所有请求都会穿透到数据库。

解决方法:

方案一:差异化过期时间(过期时间加随机值)
方案二:缓存永不过期 + 后台更新
方案三:构建高可用的Redis集群
方案四:服务降级与熔断
方案五:请求限流与队列

3.缓存击穿:

  1. 什么是缓存击穿?
    缓存击穿 是指一个热点Key在缓存过期的瞬间,同时有大量的并发请求这个Key。这些请求发现缓存过期后,都会同时去访问数据库,瞬间给数据库带来巨大的压力,就像在缓存屏障上”击穿”了一个洞。

  1. 产生缓存击穿的常见场景
    热点商品秒杀:某个热门商品参与限时秒杀活动,缓存过期时大量用户同时点击。

爆款新闻/视频:突发事件或热门内容的详情页,缓存失效时被大量用户访问。

明星/网红动态:顶级明星发布动态时,其个人主页缓存失效。

系统定时任务:某些定时刷新的热点数据,在刷新时刻被高频访问。

方案一:互斥锁(Mutex Lock)
这是最常用、最有效的解决方案。
思路:当缓存失效时,不让所有请求都去访问数据库,而是只让一个请求去数据库查询并重建缓存,其他请求等待,直到缓存重建完成

方案二:逻辑过期(永不过期 + 异步更新)
思路:缓存实际上不设置过期时间,而是在value中存储一个逻辑过期时间。当发现数据逻辑过期时,触发异步更新,当前请求仍然返回旧数据。

方案三:热点Key永不过期

思路:对于特别热点的Key,直接设置为永不过期,通过后台任务或数据更新时主动刷新缓存。

12.如何设计秒杀场景处理高并发以及超卖现象?

秒杀系统通过前端和网关限流削减流量, 核心库存操作放在 Redis 中使用 Lua 脚本原子扣减,防止超卖;
下单流程通过 MQ 异步化,避免高并发直接打数据库;
MySQL 作为最终兜底校验库存,并通过补偿机制保证一致性。

第 1 步:前端就开始挡人(不是摆设)

为什么?

线上 80% 请求是无效的(疯狂点、脚本)

真实做法

  • 倒计时结束前按钮禁用
  • 人机校验(滑块)
  • 接口签名(防刷)

📌 前端挡掉 ≈ 50% 垃圾流量


第 2 步:网关限流(救命)

1
用户 → API Gateway

限什么?

  • IP 限流
  • 用户限流
  • 总 QPS 上限(如 5 万 / 秒)

超过怎么办?

  • 直接返回:系统繁忙

📌 不做这一步,后面全白搭


第 3 步:先不碰 DB,只碰 Redis(核心)

Redis 里放什么?

1
2
stock:product:1001 = 1000
user:1001:product:1001 = 是否抢过

3.1 一人一单校验(真实线上必有)

1
2
3
4
-- 如果用户已经抢过,直接返回
if redis.call("exists", KEYS[2]) == 1 then
return -2
end

📌 防刷 + 防重复下单


3.2 Redis 扣库存(真正防超卖)

1
2
3
4
5
6
7
8
local stock = tonumber(redis.call("get", KEYS[1]))
if stock <= 0 then
return -1
end

redis.call("decr", KEYS[1])
redis.call("set", KEYS[2], 1)
return stock - 1

📌 Lua:一条原子操作
📌 Redis 单线程:绝对不会并发超卖


执行完 Redis 后发生了什么?

  • 库存已经减少
  • 用户资格已锁
  • DB 还没动

第 4 步:立刻返回用户结果(真实做法)

返回什么?

1
2
3
4
{
"code": 0,
"msg": "已进入排队"
}

📌 不能等下单完成
📌 秒杀系统 ≠ 实时下单系统


二、那订单是啥时候写进 MySQL 的?

第 5 步:写 MQ(削峰)

1
2
3
4
5
Redis 扣库存成功

发送 MQ 消息

排队

MQ 里放什么?

1
2
3
4
{
"userId": 1001,
"productId": 1001
}

📌 MQ 是 DB 的缓冲区


第 6 步:订单服务慢慢消费(安全)

1
2
3
4
5
MQ

订单服务

MySQL

写订单时还要不要校验?

一定要

1
2
3
UPDATE product
SET stock = stock - 1
WHERE id = 1001 AND stock > 0;

📌 DB 是最后一道保险


三、如果中途失败,真实线上怎么兜底?


情况 1:Redis 扣成功,MQ 失败

解决

  • 发送失败重试
  • 多次失败 → 补偿库存

情况 2:订单创建失败

解决

  • 回滚 Redis 库存
  • 删除用户资格 key

情况 3:用户抢到不付款

解决

  • 延迟队列(30 分钟)
  • 未支付 → 回滚库存

四、为什么不用直接加分布式锁?

真实原因

❌ QPS 太高
❌ 锁竞争严重
❌ Redis Lua 已经是“无锁原子”

📌 秒杀不用分布式锁是正确答案

二、黑马点评的真实秒杀架构

1
2
3
4
5
6
7
8
9
10
11
12
13
用户

Nginx(限流)

秒杀接口

Redis(Lua 扣库存 + 一人一单)

阻塞队列 / MQ

订单线程

MySQL

三、黑马点评是如何防止超卖的?(重点)

1️⃣ 库存不放在 MySQL,放在 Redis

活动开始前

1
seckill:stock:voucherId = 1000

📌 MySQL 不参与高并发


2️⃣ 使用 Lua 脚本完成三件事(原子)

Lua 做了什么?

  1. 判断库存是否 > 0
  2. 判断用户是否已经下过单
  3. 扣减库存 + 标记用户已下单

Lua 脚本逻辑(核心)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 1. 判断库存
if tonumber(redis.call('get', KEYS[1])) <= 0 then
return 1 -- 库存不足
end

-- 2. 判断是否重复下单
if redis.call('sismember', KEYS[2], ARGV[1]) == 1 then
return 2 -- 重复下单
end

-- 3. 扣库存 + 记录用户
redis.call('decr', KEYS[1])
redis.call('sadd', KEYS[2], ARGV[1])

return 0

📌 Redis 单线程 + Lua 原子执行 = 永不超卖


四、黑马点评是如何抗高并发的?

1️⃣ 秒杀接口只做“很轻”的事

秒杀接口 不做
❌ 创建订单
❌ 操作 MySQL
❌ 复杂业务逻辑

它只做:

  • 执行 Lua
  • 返回结果

📌 QPS 能轻松扛住


2️⃣ 使用阻塞队列 / MQ 削峰

黑马点评的实现

  • 使用 Java BlockingQueue
  • 单线程异步下单
1
2
3
4
5
Redis 抢成功

订单信息放入队列

后台线程慢慢消费

📌 真实生产中可换 MQ(Kafka / RocketMQ)


五、订单是如何安全写入 MySQL 的?

1️⃣ 再次校验(一人一单)

1
2
SELECT COUNT(*) FROM voucher_order
WHERE user_id = ? AND voucher_id = ?;

📌 防止极端情况下重复订单


2️⃣ 数据库库存兜底(最终防线)

1
2
3
UPDATE seckill_voucher
SET stock = stock - 1
WHERE voucher_id = ? AND stock > 0;

📌 即使 Redis 出问题,DB 也不会超卖