文章目录
redis相关
一、什么是 Redis?
Redis(Remote Dictionary Server)是一个开源的、基于内存的、支持多种数据结构的 Key-Value 存储系统。它可以用作数据库、缓存、消息队列等。
🚀 速度快,是 Redis 最大的特点!因为它将所有数据存储在内存中。
二、缓存
1.穿透、击穿、雪崩
(1)缓存穿透
就是指请求的数据既不在缓存里,也不在数据库里,但请求依然不停地打到数据库上,导致数据库压力剧增,甚至可能崩溃。
恶意攻击:通常表现为攻击者频繁请求大量不存在的 key,导致请求绕过缓存,直接打到数据库,造成数据库压力剧增,甚至宕机。
解决方式
①缓存空值
查询数据库返回的数据为空时,仍把这个空结果进行缓存。
优点:实现简单。
缺点:空值也会占用 Redis 内存,若缓存大量无效数据,会浪费缓存资源。
②布隆过滤器
优点:内存占用较少。
缺点:实现复杂,存在误判(误判率可设置,误判率在5%及以下可接受)。
原理:
- 相当于是一个以(bit)位为单位的数组,数组中每个单元只能存储二进制数0或1。
- 在 Redis 缓存预热时,会先把缓存中的有效数据通过多个哈希函数映射到布隆过滤器的位数组上,将对应的索引位置置为 1。当有请求进来时,会用相同的哈希函数计算该数据对应的索引位置。若索引位置都为1,则说明数据“可能存在”,有一定误判的可能,但只要有一个索引位置是 0,就可以确定该数据一定不存在,直接返回,避免无效请求打到数据库。
- 误判率:数组越大、哈希函数数量越合理、插入元素越少,误判率就越低。误判率可以设置,大概不会超过5%,5%以内的误判率一般的项目也能接受,不至于高并发压倒数据库。
(2)缓存击穿
缓存击穿是指某个热点Key在缓存刚好失效时,恰好有大量并发请求同时访问该Key,由于缓存失效,请求会直接打到数据库,这些并发的请求可能会瞬间将数据库压垮。(key过期后缓存重建需要一定的时间)
解决方案
①互斥锁也就是分布式锁
在缓存失效时,只有一个线程能获取锁并查询数据库、重建缓存;其他线程等待锁释放后从缓存中读取数据,避免数据库被击穿。
优点:强一致性
缺点:性能差
②逻辑过期即不设置过期时间
缓存设置一个“逻辑过期时间”,即使过期也先返回旧数据,同时由一个线程加锁异步更新缓存。其他线程在检测到数据已过期且更新锁已被占用时,不会重复查询数据库,而是继续返回旧数据,避免并发请求同时打到数据库,从而有效防止缓存击穿,并可保证缓存数据相对较新。
优点:高可用,性能优
缺点:不能保证数据绝对一致
总结:根据自身需求选择,没有高下之分,如金钱相关选择互斥锁,互联网相关等选择逻辑过期。
(3)缓存雪崩
指在同一时间大量缓存 Key 同时失效,或者Redis 服务器整体不可用(如宕机),导致大量请求绕过缓存直达数据库,从而引发数据库压力激增,甚至造成系统崩溃的现象。
解决方案
①设置随机过期时间(TTL):为不同的缓存 Key 设置过期时间时,添加一定的随机偏移量,打散大规模缓存同时失效的风险。
②针对redis服务器宕机
- 使用 Redis 高可用架构:
- 哨兵模式(Sentinel):自动故障转移,主从切换。
- 集群模式(Cluster):分片 + 高可用,避免单点故障。
- 添加降级和限流策略:
- 使用 Nginx、Spring Cloud Gateway 等对缓存接口进行限流保护。
- 针对非核心功能或非关键数据,直接降级返回默认值,避免系统整体受影响。
- 引入多级缓存架构:
- 本地缓存(如 Guava Cache、Caffeine)作为一级缓存,优先读取,命中率高。
- Redis 作为二级缓存,进一步承压,提升整体缓存可用性。
(4)总结
降级可作为系统的保底策略,适用于穿透,击穿,雪崩。
2.双写一致性
双写一致性:指在更新数据库的同时,也要更新或删除缓存中的对应数据,以确保缓存与数据库中的数据一致,避免出现脏读或数据不一致的情况。
方案
(1)先操作数据库,再删除缓存
(2)先删除缓存,再操作数据库
以上两种情况在并发场景下都可能导致缓存被回写旧数据,产生脏数据(即缓存中是老的、错误的值)。
(3)延迟双删:删除缓存 -> 更新数据库 -> sleep -> 删除缓存
这种方案可以缓解并发脏读问题,但是依赖时间延迟,存在不确定性,仍有可能出现脏数据。
(4)互斥锁(分布式锁):强一致性但性能低
①性能优化方案:采用Redisson提供的读写锁
共享锁:读锁readLock,加锁之后,其他线程可以读共享操作,但不可写。
排它锁:独占锁writeLock,加锁之后,阻塞其他线程写操作。
(5)异步通知:允许延时一致的业务
- 使用MQ中间中间件,更新数据之后,通知缓存删除
- 利用canal中间件,伪装为mysql的一个从节点,不需要修改业务代码,canal通过读取binlog数据更新缓存。
总结
- 要求强一致性的,采用redisson提供的读写锁
- 共享锁:读锁readLock,加锁之后,其他线程可以共享读操作。
- 排他锁:独占锁writeLock也叫,加锁之后,阻塞其他线程读写操作
- 允许延时一致的业务,采用异步通知
- 使用MQ中间中间件,更新数据之后,通知缓存删除。
- 利用canal中间件,伪装为mysql的一个从节点,不需要修改业务代码,canal通过读取binlog数据更新缓存。
3.数据持久性
Redis 数据持久化是指在服务器重启后,能够恢复之前内存中的数据。Redis 提供了两种主要的持久化机制:
(1)RDB
RDB(Redis Database Backup File)-redis数据备份文件,也被叫做Redis数据快照。RDB 会在指定的时间间隔内将内存中的数据生成快照,保存到磁盘(.rdb 文件)。当 Redis 重启时,会通过读取快照文件恢复数据。
执行原理:
bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据。完成fork后读取内存数据并写入 RDB 文件。
fork采用的是copy-on-write技术:
- 当主进程执行读操作时,访问共享内存;
- 当主进程执行写操作时,则会拷贝一份数据,执行写操作。
特点:
- 节省内存、恢复速度快;
- 适合做备份(比如定期将 dump.rdb 拷走);
- 支持配置触发保存,比如:
# redis.conf文件中
save 900 1 # 900秒内至少有1个key被修改
save 300 10 # 300秒内至少有10个key被修改
save 60 10000 # 60秒内至少有10000个key被修改
- 可手动触发:
SAVE # 阻塞 Redis
BGSAVE # 后台异步执行,不阻塞 Redis
缺点:
- 不是实时持久化,可能丢失几分钟的数据;
- 保存大数据快照时可能影响性能(特别是 BGSAVE 会 fork 子进程)。
(2)AOF(Append Only File)
AOF 会将每一次写操作(如 set、del 等)记录成日志,追加写入到文件(.aof 文件)中。Redis 重启时会重新执行这些操作日志来恢复数据。
特点:
- 更安全:可以实现秒级数据恢复;
- AOF 文件可读、可重写(防止无限增大)
- 启动时优先使用 AOF 恢复数据。
- AOF默认是关闭的,需要修改redis.conf配置文件赖开启AOF
# 是否开启AOF功能,默认是no
appendonly yes
# AOF文件的名称
appendfilename "appendonly.aof"
- AOF命令记录的频率也可以通过redis.conf文件来配置
# 表示每执行一次写命令,立即记录到AOF文件
appendfsync always
# 写命令执行完先放入AOF缓冲区,然后表示每隔1秒将缓冲区数据写到AOF文件,是默认方案
appendfsync everysec
# 写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
appendfsync no
- 因为是记录命令,AOF文件会比RDB文件大的多。而且AOF会记录对同一个key的多次写操作,但只有最后一次写操作才有意义。通过执行bgrewriteaof命令,可以让AOF文件执行重写功能,用最少的命令达到相同效果。
- Redis也会在触发阈值时自动去重写AOF文件。阈值也可以在redis.conf中配置。
# AOF文件比上次文件 增长超过多少百分比则触发重写
auto-aof-rewrite-percentage 100
# AOF文件体积最小多大以上才触发重写
auto-aof-rewrite-min-size 64mb
缺点:
- 文件比 RDB 大;
- 恢复速度比 RDB 慢;
- 写入量大时性能不如 RDB;
4.数据过期策略
在 Redis 中,为了节省内存空间并保持数据的时效性,对于设置了过期时间的键(key),系统需要在合适的时候将其删除。
(1)惰性删除
机制:当客户端访问某个键时,Redis 会检查该键是否已经过期。如果过期,则立即删除该键;否则返回对应的值。
优点:对 CPU 友好:只在访问时检查,无需全局扫描,降低了系统负载。
缺点:对内存不友好:若某个键从未被访问且已过期,就会一直占用内存空间,不能及时释放。
(2)定期删除
机制:Redis 会周期性地随机抽取一些设置了过期时间的键,并检查它们是否过期,如果过期就删除。
执行模式:
- SLOW 模式(默认):以 10Hz(每秒 10 次)频率运行,每次运行时间上限为 25ms。这个频率可以通过配置文件中的 hz 参数调整。
- FAST模式:不固定执行频率,但两次间隔不少于 2ms,每次运行时间不超过 1ms,通常在主动删除触发后加快回收节奏。
优点:可以通过限制删除操作执行的频率和时长来减少操作对CPU的影响。另外定期删除,也能释放过期健占用的内存。
缺点:难以确定删除操作执行的时长和频率。
惰性删除+定期删除两种策略进行配合使用。
(3)总结
Redis 实际使用的是 惰性删除 + 定期删除相结合的策略,各自弥补对方的不足:
- 惰性删除降低了 Redis 的资源消耗。
- 定期删除保障了在没有访问的情况下也能清理过期数据,防止内存泄漏。
5.数据淘汰策略
当Redis中的内存不够用时,此时再向Redis中添加新的key,那么Redis就会按照某一种规则将内存中的数据删除掉,这种数据的删除规则被称之为内存的淘汰策略。
Redis支持8种不同策略来选择要删除的key:
- 默认
- ①noeviction(不驱逐策略): 不淘汰任何key,但是内存满时不允许写入新数据,默认就是这种策略。
- 对于设置了TTL的key
- ②volatile-ttl: 比较key的剩余TTL值,TTL越小越先被淘汰。
- ③volatile-random:随机进行淘汰。
- ④volatile-lru: 基于LRU算法进行淘汰。
- ⑤volatile-lfu: 基于LFU算法进行淘汰
- 对全体key
- ⑥allkeys-random:随机进行淘汰。
- ⑦allkeys-lru: 基于LRU算法进行淘汰
- ⑧allkeys-lfu:基于LFU算法进行淘汰
- 说明:
- TTL(Time To Live)- 剩余时间
- LRU(Least Recently Used)- 最近最少使用。用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高。
- LFU(Least Frequently Used)- 最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高。
使用建议:
- 优先使用 allkeys-lru 策略。充分利用 LRU 算法的优势,把最近最常访问的数据留在缓存中。如果业务有明显的冷热数据区分,建议使用。
- 如果业务中数据访问频率差别不大,没有明显冷热数据区分,建议使用 allkeys-random,随机选择淘汰。
- 如果业务中有置顶的需求,可以使用 volatile-lru 策略,同时置顶数据不设置过期时间,这些数据就一直不被删除,会淘汰其他设置过期时间的数据。
- 如果业务中有短时高频访问的数据,可以使用 allkeys-lfu 或 volatile-lfu 策略。
6.分布式锁
原理
本质:在单进程中,“锁”是由语言(如 synchronized)或系统调用提供的,但在分布式系统中,多个节点/进程必须通过共享介质(如 Redis、Zookeeper、数据库)来协调“互斥”访问资源。
所以:
分布式锁的核心目标就是:跨多个节点保证同一时刻只有一个客户端持有锁,并能可靠释放锁。
Redis 作为一种基于内存、支持原子命令的系统,非常适合做分布式锁。关键点是使用它的 SET 命令的原子性特性:
SET key value NX PX 5000
释义:
参数 | 含义 |
---|---|
NX | 只有当 key 不存在时才设置(互斥) |
PX 5000 | 5 秒后自动过期(防止死锁) |
key | 通常是业务相关资源的唯一标识,比如 lock:order:123 |
value | 设置为客户端的唯一标识(如 UUID、线程 ID)用于后续判断释放者身份 |
- 这条命令是原子性执行的,Redis 保证不会出现竞态条件。
- 过期时间:
如果客户端拿到锁后宕机,没有设置过期时间,这把锁将永远存在,造成死锁。
设置 PX 解决了这个问题:就算你死了,锁也会在 N 秒后被自动清除。
但——这也引出一个问题 👇 - 锁过期 vs 业务未完成的问题:
如:如果锁 5 秒过期,但业务逻辑还没跑完怎么办?
锁失效 → 另一个客户端可能加锁成功,于是就出现 多个客户端并发访问同一资源 的风险- 解决思路:
- 估算合理过期时间,但不靠谱(不可控)
- 使用“看门狗机制”:自动续期(Redisson 的做法)
- 解决思路:
- 如何安全释放锁(为什么不能直接 DEL)
假设场景如下:
客户端 A 设置 lock_key -> A_UUID
客户端 A 执行业务,超时了,Redis 过期自动删除锁
客户端 B 加锁成功,设置 lock_key -> B_UUID
客户端 A 恢复,执行 DEL lock_key(它以为锁还在)
这会导致 A 删除了 B 的锁,业务就乱套了。
正确做法:检查 value 再删除
实现:redisson实现的分布式锁
执行流程
可重入:redisson实现的分布式锁-可重入
可重入锁允许同一个线程或客户端在持有锁的情况下再次获取该锁而不会死锁,同时每次获取锁后都要对应 unlock() 释放。
主从一致性:Redis 的主从复制是异步复制,不能保证主从一致性。
增强一致性保障:使用 Redisson 提供的 RedLock 红锁机制。
7.Redis集群
(1)主从复制
单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,就需要搭建主从集群,实现读写分离。
原理:
- Replication Id:简称replid,是数据集的标记,id一致则说明是同一数据集。每一个master都有唯一的replid,slave则会继承master节点的replid。
- offset:偏移量,随着记录在repl_baklog中的数据增多而逐渐增大。slave完成同步时也会记录当前同步的offset。如果slave的offset小于master的offset,说明slave数据落后于master,需要更新。
同步过程:
①全量同步:初次同步
②增量同步:slave重启或后期数据变化
(2)哨兵模式
Redis提供了哨兵(Sentinel)机制来实现主从集群的自动故障恢复。
作用:
- 监控:Sentinel 会不断检查您的master和slave是否按预期工作
- 自动故障恢复:如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主
- 通知:Sentinel充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端
结构:至少三个
(1)服务状态监控
Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令:
- 主观下线:如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线。
- 客观下线:若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好超过Sentinel实例数量的一半。
(2)哨兵选主规则
- 首先判断主与从节点断开时间长短,如超过指定值就排除该从节点
- 然后判断从节点的slave-priority值,越小优先级越高
- 如果slave-prority一样,则判断slave节点的offset值,越大优先级越高
- 最后是判断slave节点的运行id大小,越小优先级越高。
(3)脑裂
主节点被隔离在一个网络分区中,从节点和 Sentinel 处于另一个网络分区中,sentinel无法感知到主节点,通过选举的方式提升了一个从节点作为主节点,这样就产生了两个主节点。网络恢复后,Sentinel 会将old master 降级为slave,这时再从新的master同步数据,这会导致old master中的大量数据丢失。
避免或缓解方法:
- min-slaves-to-write:设置最少的slave节点个数,比如至少有一个从节点处于连接状态才允许写入。
- min-slaves-max-lag:设置主从之间的延迟时间,延迟时间超过指定阈值,拒绝请求不可写入。
(3)分片集群
主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决:
- 海量数据存储问题
- 高并发写的问题
使用分片集群可以解决上述问题,分片集群特征:
- 集群中有多个master,每个master保存不同数据
- 每个master都可以有多个slave节点
- master之间通过ping监测彼此健康状态
- 客户端请求可以访问集群任意节点,最终都会被转发到正确节点
分片集群结构-数据读写
- Redis 分片集群引入了哈希槽的概念,Redis 集群有 16384 个哈希槽
- 将16384个插槽分配到不同的实例
- 读写数据:根据key的有效部分计算哈希值,对16384取余(有效部分,如果key前面有大括号,大括号的内容就是有效部分,如果没有,则以key本身做为有效部分)余数做为插槽,寻找插槽所在的实例
8.Redis网络模型
要想了解Redis网络模型,需要先了解用户空间和内核空间以及I/O模型。
用户空间和内核空间:
系统中一个进程使用的内存情况划分两部分:内核空间、用户空间
- 用户空间只能执行受限的命令(Ring3),而且不能直接调用系统资源,必须通过内核提供的接口来访问。
- 内核空间可以执行特权命令(Ring0),调用一切系统资源。
为了提高IO效率,会在用户空间和内核空间都加入缓冲区:
- 写数据时,要把用户缓冲数据拷贝到内核缓冲区,然后写入设备
- 读数据时,要从设备读取数据到内核缓冲区,然后拷贝到用户缓冲区
I/O模型
I/O 模型(Input/Output Model)是操作系统和程序设计中用于处理输入/输出操作(I/O)的一种机制。它决定了程序 如何与外部设备(如磁盘、网络、用户输入等)进行数据交互,以及 如何等待这些数据的到达或发送完成。
一个简单的场景:
一个在线多人聊天室服务(如微信群),功能如下:
- 很多用户可以同时连上服务器;
- 用户可以随时发消息;
- 服务器要把消息转发给其他用户;
- 每个连接都可能随时发来数据,也可能很长时间不说话。
我作为群聊中的一员,当群聊中有人发送信息后,服务器会将相关信息通过网卡发送到我的内核空间,那我怎么知道内核空间有数据了,并在什么时间将内核空间的数据拷贝到用户空间呢?当我想要发送信息时,用户空间准备好后就需要将数据拷贝到内核空间,那我如何确定内核空间是否还有余量,是否可以写入呢?这就是I/O模型的作用了。
常见的I/O模型包括:
阻塞I/O(Blocking I/O):
- 当用户程序调用 read() 时:
- 如果内核空间的 socket 缓冲区没有数据,会阻塞当前线程,直到有数据可读;
- 数据准备好后,内核将数据拷贝到用户空间,read() 才返回,程序才能继续执行。
- 同理,当调用 write() 时:
- 如果内核缓冲区满了,write() 会阻塞,直到可以写入为止。
📌 适用:连接少、逻辑简单的场景。
❌ 缺点:每个 socket 都需要一个线程,高并发时资源开销巨大
非阻塞 I/O(Non-blocking I/O):
- 当调用 read() 时:
- 如果没有数据,不会阻塞,而是立刻返回错误码 EAGAIN 或 EWOULDBLOCK;
- 拿到错误码后会再次尝试获取。
- 同样,write() 时如果缓冲区满,也会立刻返回错误码,然后再次进行尝试。
📌 优点:不会卡住线程;
❌ 缺点:程序需要不断地轮询 socket,占用大量 CPU,效率低。
I/O 多路复用(I/O Multiplexing):由内核“通知”用户空间:哪些 socket 准备好可以读了,用户程序再去操作这些就绪的 socket。
- 应用程序将多个 socket 的读写事件注册到内核;
- 通过 select / poll / epoll 等系统调用等待;
- 一旦某些 socket 就绪,系统调用返回,程序只需处理这些“活跃连接”。
📌 适用:高并发聊天室、Web 服务、网关服务等。
📌 epoll 是 Linux 下效率最高的实现,推荐使用。
Redis网络模型
Redis通过IO多路复用来提高网络性能,并且支持各种不同的多路复用实现,并且将这些实现进行封装, 提供了统一的高性能事件库。
I/O多路复用是指利用单个线程来同时监听多个Socket,并在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。目前的I/O多路复用都是采用的epolll模式实现,它在通知用户进程Socket就绪的同事,会把已经就绪的Socket写入用户空间,不需要挨个遍历Socket来判断是否就绪,提升了性能。其中Redis的网络模型就是使用I/O多路复用结合事件的处理器来应对多个Socket请求,比如,提供了连接应答处理器、命令回复处理器,命令请求处理器。在Redis6.0之后,为了提升更好的性能在命令回复,处理器使用了多线程来处理回复事件,在命令请求处理器中,对命令的转换使用了多线程,增加了命令转换速度,但在命令执行的时候仍然是单线程。