[TOC]
Redis
测试环境的搭建
Win11 + WSL Ubuntu + redis7.0.15
reidis安装
# 1. 更新系统源,安装基础依赖
sudo apt update && sudo apt upgrade -y
sudo apt install -y wget curl net-tools
# 2. 安装 Redis 稳定版APT 官方源)
sudo apt install redis-server -y
# 3. 验证安装查看版本,确认安装成功)
redis-cli --version
redis.conf配置
# 1. 备份原始配置防止改错回滚)
sudo cp /etc/redis/redis.conf /etc/redis/redis.conf.bak
# 2. 批量修改核心配置无需手动编辑)
sudo sed -i 's/bind 127.0.0.1 -::1/bind 0.0.0.0/g' /etc/redis/redis.conf # 允许所有IP访问
sudo sed -i 's/protected-mode yes/protected-mode no/g' /etc/redis/redis.conf # 关闭保护模式
sudo sed -i 's/dir \.\//dir \/var\/lib\/redis/g' /etc/redis/redis.conf # 数据目录
sudo sed -i 's/# maxmemory <bytes>/maxmemory 256MB/g' /etc/redis/redis.conf # 内存限制
sudo sed -i 's/# maxmemory-policy noeviction/maxmemory-policy allkeys-lru/g' /etc/redis/redis.conf # 内存淘汰策略
sudo sed -i 's/daemonize yes/daemonize no/g' /etc/redis/redis.conf # 关闭后台运行适配systemd)
# 3. 修复配置文件+数据目录权限解决WSL权限bug)
sudo mkdir -p /var/lib/redis /var/log/redis
sudo chown -R redis:redis /etc/redis/redis.conf /var/lib/redis /var/log/redis
sudo chmod 644 /etc/redis/redis.conf
sudo chmod 770 /var/lib/redis /var/log/redis
redis.service服务配置
# 1. 备份原始服务配置
sudo cp /usr/lib/systemd/system/redis-server.service /usr/lib/systemd/system/redis-server.service.bak
# 2. 批量修改服务配置解决超时/notify模式bug)
sudo sed -i 's/Type=notify/Type=simple/g' /usr/lib/systemd/system/redis-server.service # 替换启动模式
sudo sed -i '/\[Service\]/a TimeoutStartSec=300' /usr/lib/systemd/system/redis-server.service # 延长超时时间
sudo sed -i 's/--supervised systemd//g' /usr/lib/systemd/system/redis-server.service # 删除WSL不兼容参数
sudo sed -i 's/ExecStart=.*/ExecStart=\/usr\/bin\/redis-server \/etc\/redis\/redis.conf --daemonize no/g' /usr/lib/systemd/system/redis-server.service # 统一启动命令
# 3. 重新加载systemd配置,清除失败状态
sudo systemctl daemon-reload
sudo systemctl reset-failed redis-server
验证
# 1. 启动 Redis 并设置开机自启
sudo systemctl start redis-server
sudo systemctl enable redis-server
# 2. 验证服务状态显示 active (running) 即为成功)
sudo systemctl status redis-server
# 3. 验证 Redis 功能返回 PONG 即为正常)
redis-cli ping
# 4. 验证核心配置生效输出对应值即为配置成功)
redis-cli CONFIG GET bind # 输出 "0.0.0.0"
redis-cli CONFIG GET maxmemory # 输出 "268435456"256MB)
redis-cli CONFIG GET protected-mode # 输出 "no"
# 5. 验证端口监听显示 0.0.0.0:6379 即为外网访问生效)
sudo netstat -tulpn | grep 6379
Redis的应用场景
| 场景名称 | 技术点 | 缺陷 | 方案 |
|---|---|---|---|
| 缓存热点数据 | 1. 数据结构:String、Hash 2. 核心特性:过期策略、内存淘汰策略 3. 命令: SET/GET、HGET/HMSET、EXPIRE4. 性能优化:Pipeline、批量操作 |
1. 缓存穿透 2. 缓存击穿 3. 缓存雪崩 4. 数据一致性问题 5. 热key导致节点压力 |
见异常处理 |
| 存储Session | 1. 数据结构:String(序列化Session)、Hash 2. 核心特性:过期策略 3. 核心命令: HMSET/HMGETALL、EXPIRE、DEL4. 集群特性:RedisCluster保证Session全局可访问 |
1. 单点故障导致Session不可用 2. 序列化/反序列化的性能损耗 3. 过期时间内的内存占用 4. 集群场景数据分片不均 |
见高可用 |
| 分布式锁 | 1. SETNX+EXPIRE、DEL、lua脚本2. 进阶:Redis Redlock算法 3. 特性:原子性、过期自动释放 |
1. SETNX+EXPIRE非原子,加锁成功设置过期失败导致死锁2. 主从切换导致锁丢失 3. 锁超时释放 4. 不可冲入、不可阻塞 |
见分布式锁 |
| 排行榜 | 1. 数据结构:Sorted Set,score排序 2. 核心命令: ZADD、ZRANGE/ZREVEAGE、ZSCORE、ZINCRBY;3. 扩展:Bitmap辅助统计 |
1. 数据量大情况下SortedSet性能下降 2. 实时性要求高,频繁 ZADD、ZINCRBY导致redis节点压力大3. 无法直接实现分组排行榜、分页查询 4. 分数相同排序不可定制 |
|
| 简单消息队列 | 1. 数据结构:List、Stream 2. 核心命令: LPUSH/RPOP、BRPOP;XADD、XREADGROUP、XACK3. 特性: Pub/Sub,广播模式 |
1. List队列:消费者宕机未处理消息丢失;不支持重复消费、消费组 2. Stream队列:数据挤压导致内存占用过高;消费组配置复杂,运维成本高 3. Pub/Sub:无持久化、消费者理线丢失消息 |
消息队列 |
| 计数/限流 | 1. 计数核心:String(INCR/DECR)、Hash(HINCRYBY) 2. 限流核心:固定窗口(INCR+EXPIRE)、滑动窗口(SortedSet记录时间戳) 3. 特性:院子命令保证计数准确 |
1. 高并发INCR导致节点CPU飙升、计数溢出String最大数值限制2. 固定窗口临界问题、滑动窗口大数据量下SortedSet性能下降 3. 计数/限流无持久化,redis宕机数据丢失 |
Redis特性
数据类型
数据类型在底层会根据数据量大小做编码切换
基础类型
| 名称 | 说明 | 底层核心 | 场景 |
|---|---|---|---|
| String | 基本类型,存文本、数字、二进制 | SDS | 缓存会话、页面数据、计数器 |
| Hash | 键值对集合,适合存对象 | 哈希表+ziplist | |
| List | 有序字符串列表,底层双向链表 | 快速列表quicklist | 消息队列 |
| Set | 无序不重复集合,查找去重效率高 | 哈希表+整数集合 | 标签等需要去重的集合运算场景 |
| Zset | 类似Set,多一个权重值,底层跳表实现 | 哈希表+跳表 | 排行榜、积分榜 |
后续新增的高级类型
| 名称 | 说明 | 底层核心 | 场景 |
|---|---|---|---|
| BitMap | 位存储,空间利用率极高 | SDS | 签到等简单标识信息 |
| HyperLogLog | 概率性数据结构,用于估算技术,固定大小 | 基数估算算法+字符串 | 网站UV,对精度要求不高但数据量大 |
| GEO | 地理位置信息,支持经纬度存储和空间查询,底层Zset | 附近的人、配送距离 | |
| Stream | 消息队列专用,比List多两个特性:自动生辰给全局唯一消息ID;相比Pub/Sub可以消息持久话 | 消息队列 |
数据类型的优化:
底层原理
redis的核心是 单线程事件驱动模型 + 高效内存数据结构 + 按需持久化机制
单线程
单线程无并发问题 ,避免了上下文切换、锁竞争的性的性能消耗,保证命令执行的原子性
单线程下能保证效率的原因
所有操作基于内存,CPU直接通过总线访问内存,无协议、磁盘定位等开销;
核心操作都是O(1)、O(lgN)的高效算法 如哈希、**跳表**;
采用非阻塞IO+事件驱动,避免网络、IO等待事件驱动模型(Reactor)
redis将所有操作抽象为事件
- 文件事件:处理套接字的连接、读、写操作,依赖操作系统的IO多路复用,避免单线程阻塞
- 事件事件:处理定时/周期任务
graph TD A[aeEventLoop 事件循环] --> B[文件事件File Event] A --> C[时间事件Time Event] B --> D[套接字操作连接读写] C --> E[定时任务过期键清理持久化] B --> F[aeFileEvent 事件处理器] F --> G[aeAcceptHandler处理新连接] F --> H[aeReadHandler处理读请求] F --> I[aeWriteHandler处理写响应]
多路复用
redis核心架构:命令执行是单线程的,写入场景的单线程有很多的有点,但是读取的场景并没有单线程的需求,当某个读请求卡住会因为这个读取而阻塞其他请求,所以我们想要保证单线程执行写入的同时,”多线程”同时监听多个客户端的套接字连接,全程不阻塞其他套接字的处理
多路复用在网络IO层
| 多路复用函数 | 操作系统 | 核心特点 | 性能 |
|---|---|---|---|
| epoll | Linux(2.6 及以上) | 事件驱动、高效、支持海量文件描述符(万级以上) | 最高(Redis 首选,生产环境主流) |
| kqueue | macOS、FreeBSD | 功能与 epoll 类似,事件驱动、高效 |
次高(类 Unix 系统的最优选择) |
| select | 所有操作系统(兼容性最好) | 轮询模式、低效、支持的文件描述符数量有限(默认 1024) | 最低(仅用于兼容老旧系统,不推荐高并发场景 |
多路复用相当于队列?将请求排队,先把请求存起来不阻塞他们?
| 对比维度 | Redis多路复用 | 消息队列 |
|---|---|---|
| 工作层级 | 网络 IO 层(操作系统 / Redis 底层) | 业务逻辑层(应用层) |
| 处理对象 | 未完成 IO 的请求(数据未传输) | 已完成 IO 的请求(数据已到达服务端) |
| 核心目标 | 解决单线程 IO 阻塞,提升连接并发能力 | 解决业务并发冲突,保证执行一致性 |
| 排队逻辑 | 无显式排队,仅处理「就绪的请求」,无序但不阻塞 | 显式排队,所有请求按序执行,严格串行 |
| 是否产生延迟 | 几乎无延迟(仅唤醒 / 处理就绪 IO) | 有明显延迟(请求入队等待消费) |
| 一依赖 | 操作系统原生支持(epoll/kqueue),Redis 封装使用 | 独立的中间件,需单独部署维护 |
| 与redis关系 | Redis底层核心机制,必须依赖 | 与 Redis 无关,是业务层的并发控制方案 |
多路复用并不是让请求排队,而是请求IO就绪了自己来找我,多路复用是不让IO卡线程,消息队列是不让业务卡业务
医院场景示例
多路复用:医院的「大门分诊台」(网络 IO 层)
- 工作对象:刚到医院门口、还没进入诊疗区的病人(还未完成网络 IO 的请求,数据还没传到 Redis);
- 核心工作:判断「哪个病人已经走到分诊台(IO 就绪)」,让他进入诊疗区,避免工作人员跑到大门口挨个等病人(阻塞 IO);
- 排队逻辑:病人不用排队,只要走到分诊台(IO 就绪),就会被依次接待,未到的病人不会占用工作人员时间;
- 核心目的:让工作人员能高效接待「同时到达的大量病人」,不被某个走得慢的病人堵在大门口。
消息队列:医院的「诊疗区叫号机」(业务逻辑层)
- 工作对象:已经进入诊疗区、挂完号等待看病的病人(已完成网络 IO,请求已经到达 Redis / 业务服务端);
- 核心工作:让病人按号排队,避免多个病人同时挤到医生诊室(业务并发冲突);
- 排队逻辑:病人必须按顺序排队,叫到号才能进入诊室执行业务(看病),全程串行;
- 核心目的:让医生能有序处理病人,避免并发冲突,保证业务执行的一致性。
二者的配合(如果医院同时有分诊台 + 叫号机)
病人先经过分诊台(多路复用) 进入诊疗区,再通过叫号机(消息队列) 排队看病,二者分工明确,缺一不可,这也是实际生产中「Redis 多路复用 + 业务层 MQ」的真实配合逻辑。
Redis高可用
Reid部署方案的演进,核心是为了解决单点Redis的两个核心问题:
- 可用性问题:单点redis可靠性差,宕机后服务不可用
- 扩展性问题:单节点的内存QPS瓶颈,无法支撑大规模业务的高并发、大数据量需求
持久化机制
Redis的持久化核心是将内存写入磁盘,宕机时避免数据丢失
RDB快照持久化
原理
- 主线程
fork子进程(写实复制COW),子线程遍历内存数据,形成二进制RDB文件 - 主线程继续处理请求,修改数据会单独 复制一份副本,不影响子进程快照
同步策略:触发
手动save/bgsave、配置定时策略、主从复制主节点自动触发
优缺点
- 优点
- 文件体积小,相对AOF文件RDB的二进制压缩文件小很多
- RDB是数据的完整快照,只需要加载RDB到内存,和AOF相比无需 执行命令重放,恢复速度远快于AOF
- 快照由子进程完成,父进程只在
fork期间短暂阻塞,适合高并发 - RDB完整快照适合 定期备份场景、数据归档
- 缺点
- RDB是定时快照,快照期间宕机会丢失两个快照间隙的数据
- fork阻塞问题:当写入数据量大的时候,
fork子进程耗时越长,阻塞时间越长,影响redis可用性
RDB因其原理导致只适合用于对恢复速度要求高 ,数据丢失容忍度高的场景。如定时备份、容灾等场景
AOF追加日志
原理
- 不保存数据本身,记录所有写命令到AOF文件(类似MySQL binlog的
statement模式),重启时重放AOF存的写命令恢复数据 - AOF重写:
fork子进程遍历内存数据,生成最终状态命令集,多次INCR合并压缩文件体积
核心同步刷盘策略
- always:每次写命令同步刷盘,最安全性能,IO开销巨大redis性能急剧下降一般用于数据持久性要求极高的场景如金融交易、核心账务数据
- everysec:每秒刷盘,平衡性能和可靠性,最多丢掉1s数据
- no:由操作系统决定刷盘时机,性能最好,但是可靠性差
优缺点
- 优点
- 安全性可控,
always模式下可以实现数据领丢失,可靠性远高于RDB - 数据恢复完整,AOF记录所有写命令,重启时重放可完整恢复数据
- 文件可读性高:AOF文件是明文redis敏玲,可直接查看,便于排查数据问题
- 无fork阻塞风险,追加命令时无需
fork子进程,仅在AOF重写是fork,对主进程阻塞影响小于 RDB
- 安全性可控,
- 缺点
- 文件体积大,存的是全部命令追加,文件远大于RDB,占用更多磁盘空间(AOF重写机制)
AOF重写机制
为避免对同一条记录多次SET情况导致AOF文件爆炸,优化出一个AOF重写机制(默认不开启,也就是会记录所有SET),最终只保留该数据的最新一条有效SET,丢弃冗余SET.
AOF重写机制并不是修改原AOF文件,而是生成一份全新的精简AOF文件流程如下:
- Redis主进程
fork一个子进程,负责生成新的AOF - 子进程遍历Redis中所有key,为每个key生成最终的有效命令,写入新AOF
- 在子进程中生成新AOF期间,主进程继续处理正常写请求,同时 将这些新的命令追加到AOF重写缓冲区
- 子进程完成新AOF文件后,主进程会将AOF重写缓冲区所有命令追加到新AOF中
- 最后用新AOF替代旧AOF
除此之外AOF重写还有两个重要优化:
- 合并批量命令,如对一个列表执行100次LPUSH,重写会合并成一条LPUSH命令
- 忽略无效命令:对不存在的key执行DEL或对非列表执行LPUSH等无效 命令会被直接忽略过滤
触发方式
手动触发
127.0.0.1:6379> BGREWRITEAOF Background append only file rewriting started自动触发
修改配置文件,当增长率100%时,redis自动执行BGREWRITEAOF# AOF 文件最小重写大小(默认 64MB),小于该大小不会触发重写 auto-aof-rewrite-min-size 64mb # AOF 文件增长率(默认 100%),即当前 AOF 文件大小 ÷ 上一次重写后的 AOF 文件大小 ≥ 100% 时触发 auto-aof-rewrite-percentage 100
混合持久化
绝大多数生产环境优选
由于RDB和AOF各自的短板,redis4.0版本引入了混合持久化,结合RDB和AOF的优点:
AOF文件的前半部分是RDB格式的完整快照,后半部分是AOF的增量写命令。在这种模式下,redis重启先加载RDB格式完整快照恢复大部分数据,再重放AOF格式的增量命令,兼顾了RDB的快速恢复和AOF的数据完整性
开启混合配置
# 先开启 AOF(混合持久化依赖 AOF)
appendonly yes
# 开启混合持久化(Redis 4.0+ 支持,默认 yes,推荐开启)
aof-use-rdb-preamble yes
# 其他配置(沿用 AOF 和 RDB 的核心配置)
appendfsync everysec
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
核心流程
混合持久化的核心是AOF重写成RDB+AOF混合格式的文件
- Redis 执行
BGREWRITEAOF命令,主进程fork()子进程。 - 子进程遍历 Redis 内存中的所有数据,将数据以 RDB 格式写入临时文件(前半部分)。
- 父进程将重写期间接收的写命令,追加到「AOF 重写缓冲区」。
- 子进程完成 RDB 格式数据写入后,通知父进程。
- 父进程将「AOF 重写缓冲区」中的命令,以 AOF 格式追加到临时文件末尾(后半部分)。
- 父进程用临时混合文件替换原有的 AOF 文件,重写完成。
高可用方案
主从架构
主从架构是最基础的高可用和数据备份方案,采用一主多从结构,核心是数据复制,主节点处理所有请求(也可以做读写分离只负责写入),从节点 通过复制主节点数据形成副本,实现数据备份和分担主节点的读压力
纯主从架构中,主节点宕机redis服务会处于可读不可写状态,直到主节点恢复
主节点:核心节点,处理所有
SET/HSET等命令,同时接受从节点复制请求,同步数据给从节点从节点:只读节点,仅复制主节点,提供读服务,分担主节点读压力
核心工作流程
- 全量复制(首次同步/大故障后同步)
- 从节点向主节点发送同步请求
- 主节点接收请求后,执行
bgsave,fork子进程复制RDB快照 ,同时将快照创建期间接受的写命令缓存到复制积压缓冲区(repl_backlog_buffer) - 从节点获取到RDB文件,加载RDB文件
- 主节点将复制积压缓冲区的增量写命令发送给 从节点
- 从节点执行增量命令
- 增量同步
主节点后续执行的所有写命令都会通过复制积压缓冲区(repl_backlog_buffer),试试发送给从节点- 主节点每执行一个写,就将命令追加到复制积压缓冲区
- 从节点定期向主节点发送心跳,同时携带同步的偏移量
- 主节点根据偏移量,将复制积压缓冲区中未同步命令发送给从节点
- 从节点 执行,保持和主节点的数据一致性,实现增量同步
级联复制,主从复制在一主多从情况下,为了缓解主节点的复制压力,可以让从节点作为其他从节点的复制结点,形成主->从->从的链式结构
复制积压缓冲区和AOF的区别
复制积压缓冲区是专服务于主从同步的,AOF是服务于redis磁盘持久化的,当开启混合持久化模式时同步依然是走先同步AOF(RDB+AOF),之后在同步复制积压缓冲区给从库(即便AOF中可能和复制积压缓冲区中有重复的部分),实现同步。
整个同步流程
阶段 1:从库发起同步请求,主库初始化同步
- 从库执行
slaveof 主库IP 端口,向主库发送同步请求,携带自身标识;- 主库接收到请求后,标记该从库为「待同步节点」,初始化复制积压缓冲区(若未初始化),同时记录「主库运行 ID(runid)」和「当前主库偏移量(master_repl_offset=0)」。
阶段 2:主库 fork 子进程,生成「RDB+AOF 混合文件」(全量数据准备)
- 主库执行
fork系统调用创建子进程(利用 COW 机制,不阻塞主进程),子进程负责遍历主库内存全量数据,生成 RDB 二进制快照;- 主进程继续处理客户端写命令,此时会做3 个关键操作(核心:同一份命令多端写入):
- 写入AOF 缓冲区(最终刷盘到混合格式的 AOF 文件,持久化落地);
- 写入复制积压缓冲区(内存环形缓冲区,供主从同步使用);
- 写入复制客户端缓冲区(实时推送给从库,若从库还未准备好,先暂存);
- 子进程生成 RDB 完成后,主库会将fork 期间主进程产生的所有增量写命令,以AOF 明文格式拼接在 RDB 文件后,生成 **「RDB 全量 + AOF 增量」的混合文件 **(这就是混合持久化的产物,和 AOF 重写的文件格式完全一致)。
阶段 3:主库传输混合文件,从库加载全量数据(全量同步核心)
主库通过 TCP 长连接,将混合文件完整传输给从库;
从库接收混合文件后,
先清空自身内存数据
,执行两步加载:
- 第一步:加载混合文件的 RDB 部分,快速恢复主库 fork 瞬间的全量数据(RDB 加载速度远快于纯 AOF);
- 第二步:加载混合文件的 AOF 部分,重放主库 fork 期间的增量命令,恢复到主库「当前最新数据状态」;
加载完成后,从库记录主库 runid和自身偏移量(slave_repl_offset),并向主库发送「加载完成确认」。
阶段 4:进入常态增量同步,依赖复制积压缓冲区(核心阶段,长期运行)
这是主从同步的常态阶段,全量同步完成后永久运行,复制积压缓冲区是核心载体,AOF 仅做自身持久化,流程如下:
主库处理客户端
任意写命令
(SET/HSET/DEL 等),执行后做
3 个必选操作:
- ✅ 更新自身内存数据;
- ✅ 将命令写入AOF 缓冲区(按混合格式刷盘,保障主库自身宕机不丢数据);
- ✅ 将命令写入复制积压缓冲区,同时主库偏移量(master_repl_offset)自增(每 1 字节命令 + 1);
主库通过长连接,将该写命令实时推送给从库;
从库接收命令后,立即在本地执行,更新自身内存数据,同时从库偏移量(slave_repl_offset)自增(与主库偏移量保持一致);
从库以1 秒为间隔,向主库发送心跳包(PING),心跳包中携带自身当前偏移量;
主库接收心跳包后,
对比主从偏移量:
- 若两者一致:回复 PONG,同步状态正常;
- 若从库偏移量 < 主库偏移量:说明从库漏同步了命令,主库从复制积压缓冲区中提取「从库偏移量→主库偏移量」之间的所有增量命令,推送给从库,从库执行后补全偏移量。
阶段 5:短暂断连后恢复,走「部分重同步」(复制积压缓冲区的核心价值)
若主从网络短暂波动导致断连,只要复制积压缓冲区未被覆盖,就不会触发全量同步,流程如下:
- 主从断连期间,主库继续处理写命令,正常写入 AOF 和复制积压缓冲区,主库偏移量持续自增;
- 从库重连主库后,向主库发送重同步请求,携带「之前记录的主库 runid」+「自身断连前的偏移量」;
- 主库验证:
- 若runid 一致(主库未重启)+ 从库偏移量在复制积压缓冲区的有效范围内(未被新命令覆盖):触发部分重同步;
- 主库从复制积压缓冲区中,提取「从库偏移量到当前主库偏移量」的所有增量命令,一次性推送给从库;
- 从库执行所有增量命令,更新自身偏移量,快速恢复与主库的偏移量一致,回到「阶段 4 的常态增量同步」。
阶段 6:极端情况触发「全量重同步」(兜底机制)
若从库断连时间过长,复制积压缓冲区中的增量命令被新命令覆盖,或主库重启(runid 变化),主库会拒绝部分重同步,重新回到阶段 2,再次生成「RDB+AOF 混合文件」,走全量同步流程,完成后回到阶段 4。
| 对比维度 | 复制积压缓冲区 | AOF |
|---|---|---|
| 核心用途 | 服务于「主从增量同步 / 部分重同步」,仅用于主从之间的数据补全 | 服务于「Redis 数据持久化」,防止 Redis 宕机后内存数据丢失,用于重启恢复数据 |
| 存储形式 | 「内存级」环形缓冲区,数据存储在内存中,断电即失 | 「磁盘级」日志文件,数据存储在磁盘上,断电后数据不会丢失 |
| 存储内容 | 仅存储「最新的写命令」(二进制格式,精简),仅保留近期命令,用于补全从库同步缺口 | (未开启混合持久化)存储「所有写命令」(明文 Redis 命令格式),按执行顺序完整追加,用于完整恢复内存数据 |
| 生命周期 | 随Redis主进程启动创建,关闭而销毁; 环形特性,写满后会覆盖旧命令; 从库长期断连,主库会释放对应的缓冲区资源; |
随Redis启动(开启AOF),文件永久保存在磁盘; 无限追加; |
| 作用对象 | 仅作用域主从同步,与客户端无关 | 仅作用域redis本身,用于redis持久化,与主从架构无关 |
| 配置参数 | 1. repl-backlog-size:缓冲区大小 2.repl-bakclog-ttl:断连后缓冲区保留时间 |
1. appendonly yes:开启AOF 2. appendfsync:刷盘策略 3. aotu-aof-rewrite-*:自动重写配置 |
增量/全量复制判断逻辑,缓冲区实现原理
Redis主从复制是根据复制积压缓冲区的偏移量来判断增量复制和全量复制的。
核心结论
- 增量复制的前提:从节点的偏移量在主节点复制积压缓冲区有效范围内,切主从节点的ID匹配,此时触发增量复制
- 全量复制触发场景
- 从节点首次复制
- 主从运行ID不匹配(主节点重启、故障恢复后runid会变更)
- 从节点的偏移量已不在主节点的复制积压缓冲区(指针被覆盖)
复制积压缓冲区原理
复制积压缓冲区的底层是一个固定大小的连续字节环形数组。
包含数据主体和3个管理标记,3个标记如下
- 主库偏移量,标记主库当前写入进度
- 起始偏移量,标记缓冲区最久的数据,用于判定增量同步可行性
- 缓冲区长度,管理环形覆盖逻辑,限定缓冲区最大内存
异常场景实例
纯主从架构,当主节点宕机会发生什么?
主节点宕机,redis进入 可读不可写状态
场景一:修复主节点。主节点刷新runid,期间从节点会携带两个信息(旧runid和偏移量)持续尝试重连主节点,主节点验证runid不匹配,直接触发全量复制。即主从架构 主节点宕机重启后 所有从节点都会执行一次全量复制 会造成主节点网络带宽被大量占用(向多个从节点传输RDB快照,可以通过级联同步规避);从节点服务阻塞直到完成同步
场景二:等不了修复了,需要手动选择一个从节点作为新的主节点。
为了业务不中断,选择一个从节点作为新的主节点。
从节点如何变成主节点:
选择一个健康、数据同步完整度最高的从节点,解除从节点身份
# 核心命令:取消从节点身份,变为可写主节点 127.0.0.1:6380> SLAVEOF NO ONE OK # 验证:查看节点角色,确认已变为master(可写) 127.0.0.1:6380> INFO replication # Replication role:master # 角色为master connected_slaves:0 # 暂无从节点 master_runid:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # 新生成的runid(晋升后自动生成)逐个登录其他从节点,设置主节点指向
# 核心命令:指向新主节点的IP和端口 127.0.0.1:6381> SLAVEOF 192.168.1.101 6380 # 新主节点IP:端口 OK # 若新主节点有密码认证,需配置认证密码 127.0.0.1:6381> CONFIG SET masterauth 123456 OK主从全量同步,切换业务读写地址,完成切换
那么当原主节点恢复时会发生什么?
- 原主节点恢复后任然保持主节点和原有数据,形成双主并存并互不感知的情形
- 数据必然存在不一致:原主节点宕机前未同步给从节点的偏移量、新主节点在宕机期间接收到的新写入数据,这两部分数据互为缺失,而Redis无自动合并能力
- 这种局面会导致业务读写混乱、数据丢失/错乱,必须人工介入处理
处置方式:核心是保留新主节点,将原主节点降级或直接废弃,处置步骤
- 隔离原主节点,防止后续新的写入
- 处理数据差异:如果原主节点存储的非核心数据可以直接废弃;如果是核心数据就只能通过人工甄别迁移到新主节点上(高危操作)
- 原主节点降级或废弃
- 验证业务状态
主从架构的缺陷
- 主节点宕机后无自动故障转移,依赖人工介入
- 主节点宕机恢复后的双主冲突,数据不一致且不具备合并能力
- 主节点重启或新选举主节点,必然触发全量复制(runid的更新),引发性能风暴
- 单主节点性能瓶颈、可靠性健壮性不佳
- 无统一的节点状态感知监控,各节点的状态(同步进度、偏移量、节点存活)相互独立,无法全局感知管理
哨兵模式
哨兵模式是Redis官方提供的企业级高可用方案,基于主从架构扩展,核心是 自动化故障检测和故障转移
flowchart TB
subgraph 哨兵集群
A[sentinel1]---B[sentinel2]
end
哨兵集群 ---> 数据节点
subgraph 数据节点
C[master] --> D[slave1] --> E[slave2]
end
数据节点 --> 客户端
哨兵节点
- 监控:实时监控数据节点健康状态,定时发送心跳请求
- 通知:当节点出现故障时候,通过配置的脚本通知运维人员
- 故障转移:主节点宕机后,自动选举从节点晋升为主节点,更新其他从节点和客户端配置
- 配置提供者:客户端通过哨兵获取当前主节点地址,无需硬编码主节点IP:PORT
- 哨兵节点通常部署奇数个,避免选举脑裂,单哨兵存在故障风险
启动哨兵节点
# 格式:redis-sentinel <哨兵配置文件路径>
redis-sentinel /usr/local/redis/conf/sentinel.conf
# 或等价于(以哨兵模式启动 Redis)
redis-server /usr/local/redis/conf/sentinel.conf --sentinel
工作原理
- 监听
- 哨兵节点启动后,通过配置文件指定监控的主节点
- 定期PING,判断主节点存活
- 主节点超时未响应30s,尚明将主节点标记为
主观下线 - 集群哨兵间通信,交换割接点对主节点状态判断
- 超过半数哨兵判断主观下线,则将主节点标记为
客观下线,触发故障转移流程
- 故障转移
- 选举master哨兵:集群 通过Raft选举出
master sentinel,仅由MS执行故障转移 - 选举新主节点:
mater sentinel遍历数据节点,根据优先级、复制偏移量、运行规则等,选举 出最优从节点未主节点 - 晋升主节点:
master sentiel向选中的节点发送SLAVEOF NO ONE晋升为主节点 - 更新其他从节点:
master sentiel向其他数据节点发送SLAVE OF newip:port,设置新主节点(会全量同步?) - 哨兵集群更新配置将心的主节点作为 后续监控主节点
- 通知客户端:通过配置脚本通知客户端更新新主节点地址
- 旧主节点恢复(可选):旧主节点宕机恢复后,会被哨兵自动配置为新主节点的从节点,复制新主节点数据
- 选举master哨兵:集群 通过Raft选举出
核心配置(sentinel.conf)
# 格式:sentinel monitor <监控名称> <主节点IP> <主节点端口> <法定票数>
# 含义:监控一个名为 mymaster 的主节点,法定票数为 2(需至少 2 个哨兵认为主节点下线,才触发故障转移)
sentinel monitor mymaster 192.168.1.100 6379 2
# 主节点密码(若主节点有认证,必须配置)
sentinel auth-pass mymaster 123456
# 主节点主观下线超时时间(默认 30000 毫秒 = 30 秒)
sentinel down-after-milliseconds mymaster 30000
# 故障转移超时时间(默认 180000 毫秒 = 3 分钟)
sentinel failover-timeout mymaster 180000
# 故障转移时,同时同步的从节点数量(默认 1,减少对新主节点的压力)
sentinel parallel-syncs mymaster 1
# 故障通知脚本(可选,节点故障时执行,用于告警)
sentinel notification-script mymaster /usr/local/redis/sentinel/notify.sh
优劣分析
优点
- 自动化故障转移
- 基于主从架构
- 高可靠:哨兵集群
- 无需修改客户端配置(硬编码)
缺点
- 仅解决了高可用,未解决扩展性,哨兵模式依旧是一主多从,主节点性能瓶颈依然存在
- 写操作几种在单主节点,无法实现写入的负载均衡
- 故障转移期间,Redis服务可能有短暂的抖动,对实时性高的业务会有影响
- 配置和运维复杂度高于 主从架构
集群部署
RedisCluster是Redis3.0+ 官方提供的分布式集群方案,核心是分片存储+分布式高可用
flowchart TB
subgraph Redis集群
A[master1:0-5460]---B[master2:5461-10922] --- C[master3:10923-16383]
A --> D[slave1]
B --> E[slave2]
C --> F[slave3]
end
Redis集群 --> 客户端
核心概念
集群采用五中心架构,所有节点地位平等
- 槽位:将数据空间划分为16384个槽位,每个键值对通过
CRC16(key) % 16384计算,映射到其中一个槽位 - 主节点与槽位:每个主节点负责一部分槽位
- 主从节点对应:每个主节点至少配备1个从节点
- 集群通信:所有结点通过
Gossip协议定期交换集群信息(节点状态、槽位分配、故障信息),保障集群的一致性
节点要求
- 集群节点最少为3个主节点+3个从节点
- 每个节点需开启集群模式
工作原理
数据分片与存储
- 客户端发送
SET key value命令,先通过CRC16(key) % 16384计算出key对应的槽位(如槽位 1000)。 - 客户端(或节点)查询集群槽位分配信息,找到负责槽位 1000 的主节点(如主节点 1)。
- 客户端将命令发送到该主节点,主节点执行命令,将数据存储在本地。
- 主节点将数据同步给自身的从节点,形成数据副本。
- 客户端发送
故障检测与故障转移
- 集群中每个节点定期向其他节点发送
PING命令,判断节点是否存活。 - 若某个主节点在超时时间内(默认 15 秒)未返回响应,发送
PING的节点将其标记为「主观下线」。 - 若集群中超过半数的主节点都将该主节点标记为「主观下线」,则将其标记为「客观下线」。
- 该主节点的从节点通过「Raft 算法」选举出一个新主节点,晋升为主节点,接管原主节点的槽位。
- 集群通过 Gossip 协议更新槽位分配信息,客户端后续请求将发送到新主节点。
- 集群中每个节点定期向其他节点发送
客户端重定向
若客户端将命令发送到了不负责对应槽位的节点,该节点会返回
MOVED重定向响应,告知客户端正确的主节点地址,客户端后续将命令发送到正确节点。
核心配置(redis.conf)
# 开启 Redis 集群模式(默认 no,改为 yes)
cluster-enabled yes
# 集群配置文件名称(自动生成,无需手动编辑,记录集群槽位、节点信息等)
cluster-config-file nodes-6379.conf
# 集群节点超时时间(默认 15000 毫秒 = 15 秒,节点超时未响应则标记为下线)
cluster-node-timeout 15000
# 开启持久化(推荐,避免集群重启后数据丢失)
appendonly yes
appendfsync everysec
# 其他基础配置(端口、密码等)
port 6379
requirepass 123456
masterauth 123456
集群的创建
Redis提供了redis-cli --cluster可快速搭建集群
准备6个redis节点,分别配置不同端口,开启集群模式
启动6个节点
执行集群搭建命令,自动分配槽位和主从关系
# 格式:redis-cli --cluster create <节点1IP:端口> <节点2IP:端口> ... --cluster-replicas 1 # --cluster-replicas 1:表示每个主节点对应 1 个从节点 redis-cli --cluster create 127.0.0.1:6379 127.0.0.1:6380 127.0.0.1:6381 127.0.0.1:6382 127.0.0.1:6383 127.0.0.1:6384 --cluster-replicas 1 -a 123456验证集群状态
redis-cli -c -p 6379 -a 123456 127.0.0.1:6379> CLUSTER INFO # 查看集群整体信息 127.0.0.1:6379> CLUSTER NODES # 查看集群所有节点信息
优劣分析
优点
- 水平扩展:通过数据分片,将数据分散到多个主节点,增加主节点扩展内存和QPS
- 分布式高可用:每个节点都有从节点,无需哨兵,主从自行切换
- 无中心架构:所有结点地位平等,无单点故障风险
- 支持读写分离:从节点可以提供读服务,分担主节点读压力
缺点
- 配置和运维复杂度高:集群的搭建、扩缩容、故障排查难度远高于主从和哨兵
- 不支持跨槽位操作:跨槽位命令需要客户端拆分命令分别执行
- 数据迁移有开销:扩缩容期间会占用网络带宽和节点资源,会影响服务性能
- 不分功能受限:如事务、Lua脚本等在集群模式下有一定限制
异常场景
因为集群部署是数据分片的,那么范围查询跨分片时会怎么样?如何解决
范围查询在单节点内不受影响;
范围查询超过单节点会直接失效或额外处理;解决方案:
- 使用哈希标签,强制相关key落入同槽位
- 客户端/中间件(Twemproxy、Codis):分布式场景下通用解决方案,客户端或中间件一次连接集群所有节点,在每个节点上执行
SCAN,最后将所有结果聚合返回完整数据 - 业务侧重新设计数据结构(业务侧让步)
如果分片节点和它相关的从节点都故障宕机了,会如何?只是分片主节点宕机 主从切换过程中是不是会丢数据?
分片节点和相关的从节点同时故障宕机,会导致该分片相关的所有业务中断,只能人工介入修复;分片节点宕机,主从切换必然会丢失一部分未同步的数据,复制积压缓冲区原理中的数据
CRC算法
Gossip协议
读写分离中间件
Codis、Twemproxy
异地多活
分布式存储
MinIO、Ceph
云厂商Redis托管
花钱买服务,相当于把Redis这块业务卖给云厂商,由他们去负责细节的实现,只管使用(财力雄厚方案)
心跳模式
节点间的心跳是双向、定时的,核心目的是检测节点的存活 和 节点状态/偏移量,不同架构下的心跳略有差异,单核心一致
主从架构下的心跳模式
从节点主动发起,主节点被动响应
slave -> master,发送PING包,携带3个信息(自身节点ID+已同步的偏移量+主节点runid)
master响应,收到PING后返回PONG包(包含主节点当前的偏移量+自身状态)
slave接受到PONG后,判断主节点存活,更新自身记录的主节点偏移量,为后续增量同步做准备
若PING超时(默认60s),判定主节点宕机,停止增量同步,进入重连流程主节点也会主动发送增量数据包,不属于心跳,是数据同步,和心跳并行互不干扰
哨兵模式下的心跳模式
哨兵模式的心跳由哨兵节点(sentinel)发起,数据节点(主/从)被动响应,核心是为了监控数据节点的健康状态
sentinel -> master/slave,发送PING
master/slave 响应PONG(自身状态+是否可读+是否主节点)
sentinel在30s内未收到PONG响应,则将该节点标记为主观下线
哨兵集群间通过心跳同步节点状态,超过半数哨兵未收到PONG则标记为客观下线,触发故障转移
*
Redis异常
缓存异常
| 异常名称 | 描述 |
|---|---|
| 缓存穿透 | 高危!通常是恶意攻击。靶数据不存在,缓存、数据库中均没有 |
| 缓存击穿 | 某热点key过期瞬间,大量请求打到数据库。常见于秒杀、抢车票情形 |
| 缓存雪崩 | 大批量key同时过期,或redis服务宕机,请求全打到数据库 |
缓存穿透、击穿、雪崩的应对策略
穿透
入口校验
参数和方法性校验,如数据类型、数值大小(ID < 0)等直接返回-
public User getUser(Long userId) { String key = "user:" + userId; // 1. 先查布隆过滤器 if (!bloomFilter.mightContain("user_bloom", String.valueOf(userId))) { return null; // 布隆过滤器说不存在,直接返回 } // 2. 查缓存 User user = cache.get(key); if (user != null) { return user; } // 3. 查数据库 user = userDao.findById(userId); if (user != null) { cache.set(key, user); } return user; } 缓存空值
已经打到数据库返回null的数据也在内存中缓存一个空标识,设置较短过期时间,防止同一个不存在的key反复穿透
击穿
- 互斥锁:缓存失效时加锁限制只让一个请求数据库重建缓存,其他请求等待。分布式环境用redis
SETNX实现分布式锁 - 热点数据定时刷新
雪崩
- 大量key同时过期情形:过期时间随机分布,避免集体失效
- redis宕机情形
- Redis高可用部署:主从架构+哨兵模式、集群部署
- 本地缓存:本地缓存Caffeine/GuavaCache,转移部分压力到JVM缓存
- 服务熔断:Sentinel/Hystrix,监听压力过大直接熔断,避免系统崩溃
- 缓存预热:系统启动主动加载热点数据到缓存,避免冷启动大量请求穿透
一致性问题
双写一致性问题,即不同数源之间同步间隙产生的不一致情形
布隆过滤器
布隆过滤器(BloomFilter)是一种概率型数据结构,不存在一定,存在不一定。
原理
布隆过滤器由一个位数组和k个独立的哈希函数组成。添加元素通过k哈希 函数算出k个位置,置1;查询时计算同样k位置,全1则可能存在,存在1个0则一定不存在。误判发生在不同元素哈希冲突时
布隆过滤器不支持删除,只能新增。想要删除某个元素,只能把整个过滤器重建
位数组的某个位可能是多个元素共同设置的,导致其中一个删除时会将另一个误判为不存在,所以不支持删除
适用场景:大数量,允许小概率误判允许一定的不可靠性),只判断存在性
- 爬虫url去重
- 垃圾邮件过滤
- 推荐系统去重
- 分布式缓存
RedisBloom模块详解
# 创建过滤器时指定误判率和预期容量
BF.RESERVE myfilter 0.001 10000000
# 误判率 0.1%,容量 1000 万
# 批量添加
BF.MADD myfilter item1 item2 item3
# 批量查询
BF.MEXISTS myfilter item1 item2 item3
# 查看过滤器信息
BF.INFO myfilter
Java Redisson操作
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("user_bloom");
// 初始化,预期插入 5000 万,误判率 3%
bloomFilter.tryInit(50000000L, 0.03);
bloomFilter.add("user:10086");
boolean exists = bloomFilter.contains("user:10086");
误判率和参数选择
误判率取决于三个因素:位数组大小m,哈希函数个数k,已插入元素数量n
理论上最优哈希函数个数 k = (m/n) & ln2,大约是0.7倍的位数组和元素数量之比
实际工程中一般这样估算:
- 误判率1%,每个元素约10bit
- 误判率0.1%,每个元素约15bit
- 误判率0.01%,每个元素约20bit
降低误判率通过增大位数组,增加哈希函数来降低,但是哈希冲突不可避免无法降低误判率到0),其实是用空间换可靠性
布隆过滤器的变种
- counting bloom fileter:每个位置 不是0/1,而是计数器,可以删除,代价是空间翻好几倍
- cuckoo filter:支持删除,空叫效率和原版差不多,Facebook在用
- scalable bloom filter:支持动态扩容,元素超了自动加一层过滤
RedisBloom 模块只支持cuckoo filter,用CF.ADD/CF.EXISTS操作
Redis实现布隆过滤器的方式
- 位图手动实现 (不推荐使用,自己实现自己选哈希、计算数组大小、写代码逻辑容易出错)
SETBIT/GETBIT自己管理哈希函数和位数组 - 官方RedisBloom
由一个位数组和k个哈希函数组成
倾斜
数据分布/访问负载不均,导致单点压力过大
分布式锁和消息队列
分布式锁
多台应用服务器、多Redis节点(集群/主从)下,并发更新缓存需要用分布式锁,保证跨应用、跨节点的并发安全
基础方案:SETNX + EXPIRE
分布式锁的基础实现,核心是利用SETNX的原子性,存在缺陷
// 加锁
public boolean tryLock(String lockKey, String requestId, int expireSeconds) {
// 1. SETNX 加锁
Long result = redisTemplate.opsForValue().setIfAbsent(lockKey, requestId);
if (result != null && result == 1) {
// 2. EXPIRE 设置过期时间,防止死锁(加锁成功后进程宕机,锁无法释放)
redisTemplate.expire(lockKey, expireSeconds, TimeUnit.SECONDS);
return true;
}
return false;
}
// 解锁
public void unlock(String lockKey, String requestId) {
// 校验请求ID,防止误删其他线程的锁(重要)
if (requestId.equals(redisTemplate.opsForValue().get(lockKey))) {
redisTemplate.delete(lockKey);
}
}
// 缓存更新逻辑(伪代码)
public void updateCache(String cacheKey, Object newData) {
String lockKey = "lock:" + cacheKey; // 锁key与缓存key绑定
String requestId = UUID.randomUUID().toString(); // 唯一标识,用于解锁校验
try {
// 尝试加锁,超时时间30秒
boolean locked = tryLock(lockKey, requestId, 30);
if (!locked) {
// 加锁失败,重试或直接返回(根据业务场景)
return;
}
// 加锁成功,安全更新缓存
redisTemplate.opsForValue().set(cacheKey, newData);
} finally {
// 解锁,释放资源
unlock(lockKey, requestId);
}
}
SETNX 和 EXPIRE是两个独立的命令,两者一起运行的时候没有原子性,存在SETNX 加锁成功后服务宕机EXPIRE无法执行,造成死锁
优化方案 SET key value NX EX,原子加锁
redis支持SET命令组合参数,将加锁和设置 过期时间合并为一个原子命令,解决SETNX 和EXPIRE的缺陷
SET lock:user:1001 uuid-12345 NX EX 30
// 原子加锁(推荐)
public boolean tryLockAtomic(String lockKey, String requestId, int expireSeconds) {
// setIfAbsent 重载方法,直接实现 NX + EX 原子操作
Boolean result = redisTemplate.opsForValue().setIfAbsent(
lockKey,
requestId,
expireSeconds,
TimeUnit.SECONDS
);
// 避免空指针(RedisTemplate 集群环境下可能返回 null)
return Boolean.TRUE.equals(result);
}
// 原子解锁(必须用 Lua 脚本,保证「校验+删除」原子性)
public void unlockAtomic(String lockKey, String requestId) {
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(lockKey),
requestId
);
}
进阶方案:Redisson分布式锁
Redisson
Redisson,封装了完善的分布式锁(可重入、可阻塞、自动续期),支持单机、集群、哨兵等多种部署模式
- 自动实现了
SET NX EX原子加锁 - 内置WatchDog机制,自动给未执行完的业务续期所时间,避免锁在业务执行期间过期
- 支持可重入锁、公平锁、读写锁等锁类型
- 解锁使用
Lua脚本,保证原子性
示例
// 1. 注入 RedissonClient(提前配置好连接)
@Autowired
private RedissonClient redissonClient;
// 2. 缓存更新加锁逻辑
public void updateCacheWithRedisson(String cacheKey, Object newData) {
String lockKey = "lock:" + cacheKey;
// 获取可重入锁
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试加锁,最多等待10秒,锁自动过期30秒
boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (!locked) {
throw new RuntimeException("获取分布式锁失败,无法更新缓存");
}
// 加锁成功,安全更新缓存
redisTemplate.opsForValue().set(cacheKey, newData);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("加锁过程被中断", e);
} finally {
// 解锁(只有当前线程持有锁时才会释放)
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
分布式锁的兜底RedLock
为了解决主从架构中主节点加锁成功后宕机,从节点未同步到锁信息而造成锁丢失的情形,抽象出一层RedLock(多台redis) 专用于 分布式锁的申请、释放
Redis主从模式,存在主节点加锁后,同步期间宕机导致锁丢失的问题
- 建立多个单独的主节点Redis(专注于分布式锁,不涉及业务数据,不需要同步)
- 加锁时对所有节点发送
SET NX EX超过半数成功才认为加锁成功 - 解锁时发送所有节点发起释放锁命令,超过半数认为删除锁成功
graph TD
A[1.业务发起写入请求] --> B[2.客户端向RedLock申请分布式锁]
B --> C[3.超过半数RedLock Redis加锁成功,返回客户端加锁成功]
C --> D[4.执行业务redis写入操作]
D --> E[5.业务写入结束,向RedLock申请释放分布式锁]
E --> F[6.超过半数RedLock redis释放锁成功,返回客户端解锁成功]
单机场景(本地锁)
synchronized/ReentrantLock
消息队列
给我一个选择分布式锁,不选消息队列的理由?
分布式锁这么繁琐,人生苦短我选MQ!
分布式锁和消息队列都是并发控制的核心方案,适用场景的核心差异在于请求是否需要实时执行、能否接受串行化的性能损耗、业务是否试单节点操作
消息队列(MQ)vs分布式锁
在大多数生产环境,消息队列的优先级都高于分布式锁,以下环境优先使用MQ:
- 非实时性要求并发更新场景,允许请求有短暂延迟的,如库存扣减、缓存更新、订单状态同步、积分变更
- 高并发峰值场景,如秒杀、大促、活动引流,需要削峰填谷,避免请求直接打垮redis/MySQL
- 单节点业务操作,业务操作之恶换手机一个核心资源,串行处理无冲突
- 需要失败重试,数据可靠性要求高的
单体MQ不适合的场景:(但是我可以联合分布式事务避免原子性问题)
- 强实时性要求:请求需要立即执行立即返回结果,不允许等待耗时的
- 多资源组合,涉及多个资源、多个库,如同时更新余额、库存、订单状态,需要通过锁保证操作的原子性
- 大量查询极少数写入场景,串行会 导致查询延迟过高,影响用户体验
- 没有MQ部署的轻量化应用(没有你用个屁)
| 维度 | 消息队列 | 分布式锁 |
|---|---|---|
| 核心思路 | 排队串行,消除并发竞争 | 几所互斥,控制并发竞争 |
| 实时性 | 低,排队等待消费存在延迟 | 高,拿到锁立即执行,无额外延迟 |
| 性能 | 串行处理,但消费端TPS有限,可以通过消费端分片提升 | 并行处理,拿到锁的请求同时执行TPS高 |
| 复杂度 | 简单,生产/消费端简单开发,无需处理锁逻辑 | 高,需要封装加解锁、处理锁丢失/死锁/续期,RedLock等复杂技术 |
| 适用操作 | 单业务操作,支持串行处理 | 多业务组合操作(事务型操作),需要并行执行业务 |
| 峰值处理 | 串行处理无惧峰值 | 高并发情形锁竞争激烈,易导致请求重试/超时 |
| 失败恢复 | 天然支持(消息持久化、失败重试、死信队列) | 徐手动实现(获取锁失败重试、执行失败回滚) |
| 部署成本 | 部署 MQ(RocketMQ/Kafka/RabbitMQ) | 需部署redis,主从、集群、RedLock等 |
总结
在符合MQ适用场景的前提下,MQ+事务的方案 整体是优于分布式锁方案的,MQ更稳定、更少踩坑、可维护性更强,是生产环境高并发核心业务的优选。如果一定要说一个分布式锁优于MQ+事务的场景,那么就是单纯的查询业务,极少的写入需求场景
消息队列的缺陷
消息队列保证串行执行,不保证执行结果的原子性,原子性保障需要通过数据库事务、分布式事务等其他技术实现
消息队列无法保障执行结果的原子性的原因
- 消息队列只负责投递消息,不负责监督操作执行结果
消息队列的职责是:
保证消息可靠投递不丢失、不重复;
保证消息按顺序被消费串行执行;
消息队列不关心消费端的内部操作及执行结果;- 多操作原子性,一来底层资源的事务支持,与消息包无关
如更新用户余额和扣减库存的两个操作,本质是对数据库的操作,他们的原子性应该有存储的事务机制来保障,与MQ的投递无关
如果是同库操作,通过数据库本地事务保障执行的原子性
如果是不同库,通过**分布式事务**保障执行的原子性- 消息重试会导致重复执行,破幻原子性
MQ的失败重试机制当执行失败时会重新投递消息包
- 假设操作1执行成功,操作2执行失败,MQ重新投递消息,会导致操作以被重复执行
- 即使做了幂等性处理,也只能保证操作1不被重复执行,依然无法解决操作1成功操作2失败的中间状态,无法保障原子性
| 概念 | 核心含义 | 保障主体 | 失败场景 |
|---|---|---|---|
| 串行执行 | 多个操作顺序执行,不被其他请求打断 | 消息队列 | 操作1执行成功,操作2执行失败 |
| 原子性 | 多个操作构成一个整体,要么全部成功要么全部失败 | 事务 | 操作1成功,操作2失败回滚操作1 |
如何解决消息队列的缺陷(执行一致性问题)
MQ+(分布式)事务,兼顾串行化和原子性
场景
- 简单场景(单库)
- 把操作1操作2打成一个消息包,发送到MQ
- 消费端接受消息,开启数据库本地事务
- 串行执行操作1、操作2
- 执行成功COMMIT,向MQ返回ACK,消息处理成功
- 任意操作失败,回滚事务
ROLLBACK,不向MQ返回ACK,MQ会重新投递消息,直到成功或进入死信
- 复杂场景(多库):MQ+分布式事务
- 把操作1操作2打成一个消息包,发送到MQ
- 消费端接收消息,开启Seata全局事务
- 串行调用操作1操作2,每个服务内部开启数据库本地事务
- Seata会自动记录操作前快照、操作后快照,实现自动回滚
- 如果都执行成功,则全局 提交事务,MQ返回ACK
- 如果任意操作失败,全局回滚 事务,不向MQ返回ACK,等待重新投递直到成功或进入死信
分布式事务
Seata、TCC、SAGA
术语
COW
copy-on-write 写时复制 是Linux系统的一种内存管理机制,Redis深度依赖COW实现 RDB持久化、AOF重写、主从复制等核心功能
COW核心逻辑:当多个进程共享同一块内存数据时,只有 当某个进程对数据执行写入操作时,才会为该进程复制一份新的内存副本,供其单独修改,未执行写入操作时仍共享内存数据
Linux底层实现
Linux通过页表、内存也管理内存,COW基于这两个核心结构实现
- 内存共享:父进程创建子进程(如RDB持久化
fork子进程)时,系统不会立即复制父进程的所有内存数据,而是父子共享同一套内存,仅在页表中标记内存页为只读- 写时触发:当父进程对共享内存页执行写入操作时,系统检测到只读内存被写入,立即为该内存页复制一份新的物理副本,更新父进程页表指向该副本,允许父进程修改副本,而子进程页表仍然指向只读的原始页
- 独立修改:父子进程后续内存操作相互隔离,子进程始终能访问到
fork创建瞬间的原始内存数据,父进程则操作新的内存副本疑问? 那么当父进程写入之后,子进程访问的还是原副本 什么时候会合并?会存在子进程读取到fork之前的数据,新的进程访问到父进程写入完成后的数据 导致两个进程间数据不一致的情况吗
Redis为什么需要COW
Redis是单进程单线程内存数据库,所有正常的读写请求都在主进程处理,而RDB持久化、AOF重写、主从同步等操作,需要读取全量内存写入磁盘/同步从库
直接让主进程执行会有以下问题
- 阻塞主进程:全量读、写会导致主进程无法处理正常请求,阻塞业务服务
- 数据一致性:操作过程中如果主进程修改数据,会导致持久化/同步数据不完整、不一致
fork子进程+COW机制,解决了阻塞主进程和不一致问题(读快照)
COW的隐患
高写入场景下,会带来内存占用飙升、磁盘I/O压力增大,是Redis运维中需要关注的重点
- fork后,redis主进程大量写操作,会 触发大量内存也的COW复制,导致内存占用迅速飙升,内存不足会触发内存Swap,redis性能会急剧下降
- fork子进程曹总,本身就会造成短暂阻塞,当redis占用大时,fork操作耗时越长,会短暂阻塞主进程
- 子进程的磁盘I/O竞争,fork进程持久化/同步时,会 大量读写磁盘,会造成磁盘带宽占用急剧上升,印象redis主进程AOF追加写(AOF默认每秒刷盘),严重甚至会阻塞主进程
针对以上隐患的优化
- 系统层面
- 关闭透明大表
- 保证足够的物理内存(预留内存)
- 降低fork频率
- redis层面
- 合理设置
maxmemory,避免redis内存占用过高 - 选择核实的内存淘汰策略,高写入场景使用
allkeys-lru/volatile-lru等淘汰策略,及时释放无用内存 - 主从架构优化:关闭主库的RDB/AOF操作,在从库上做持久化操作,主库只负责读写请求
- 监控fork耗时:监听fork耗时,超过阈值告警
- 合理设置
跳表
跳表的本质是多层链表,底层链表保存所有元素,上层是下层的子表,通过分层索引查找优化。
Redis的跳表笔常规的跳表多一个回退指针、并且score允许重复
为了删除节点时可以快速定位 到前驱节点,不需要 重新遍历
随机层级的概率算法
层数n: 0.25 ^ (n-1)*0.75
#define ZSKIPLIST_MAXLEVEL 32 #define ZSKIPLIST_P 0.25 int zslRandomLevel(void) { static const int threshold = ZSKIPLIST_P*RAND_MAX; int level = 1; while (random() < threshold) level += 1; return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL; }
跳表和红黑树的比较
相比较红黑树,多层链表
- 实现更简单
- 范围查询效率高
- 并发友好,只需要锁相关节点,红黑树旋转会锁一片节点
- 内存占用可控,跳表每个节点平均1.33个指针,红黑树每个节点3个指针
红黑树

红黑树是平衡二叉查找树
二叉查找树BST
左子树所有节点值 < 根节点值,右侧所有节点值 > 根节点值;
中序遍历可以得到有序序列
理想情况下查询/插入/删除效率O(lgN),如果插入有序数据则会退化为单链表
红黑树是一种近似平衡的二叉查找树,不追求绝对平衡(左右树高差不超过1),通过严格颜色规则维护平衡
特性
- 每个节点要么红色要么黑色
- 根节点必须黑色
- 所有叶子结点必须黑色
- 红色结点子结点必须黑色
- 从任意结点到其他所有叶子结点路径包含的黑色节点数相同
核心操作:自平衡操作
- 旋转:左旋、右旋,调整节点的子树结构,不改变二叉查找树的有序性
- 变色:修改节点的颜色,满足颜色规则
性能
- 查询/插入/删除的平均和最坏时间复杂度均为O(lgN)
- 旋转操作的次数少,维护成本低于AVL树
AVL树严格平衡,每个节点维护平衡因子,旋转次数更多,适合查询效率要求极高,吸入操作少的场景(如数据库索引辅助结构)
SDS
TPS
版本演变
Redis4.0 引入混合持久化
结合RDB和AOF,RDB存储全量数据,AOF存储增量命令。兼顾RDB的快速回复和AOF的数据完整性
Redis6.0+ 多线程IO优化
核心命令执行仍单线程,仅将网络读取和响应写入拆分为多线程,解决单线程高并发情形下网络带宽的瓶颈