一、缓存
1. 缓存穿透、击穿与雪崩
缓存穿透
- 定义:查询不存在的数据,导致每次请求都会查询数据库。
- 解决方案:
- null值缓存:
- 优点:实现简单。
- 缺点:内存消耗较大,尤其是当大量无效key存在时。
- 布隆过滤器:
- 优点:内存占用少。
- 缺点:可能存在误判(假阳性),但通过增加数组大小可以降低误判率。
- null值缓存:
缓存击穿
- 定义:某个热点key过期后,大量并发请求直接打到数据库,导致数据库压力过大。
- 解决方案:
- 互斥锁:
- 优点:保证数据一致性。
- 缺点:性能较低,因为同一时间只能有一个线程获取锁并查询数据库。
- 逻辑过期:
- 优点:性能好,避免所有请求同时访问数据库。
- 缺点:用户体验较差,数据一致性不高。
- 互斥锁:
缓存雪崩
- 定义:大量key在同一时间段内过期或Redis崩溃,直接请求数据库导致数据库压力骤增。
- 解决方案:
- 给key添加随机TTL值:避免大量key同时过期。
- 利用Redis集群:提高系统的可用性。
- 缓存业务降级限流:在极端情况下限制流量,保护数据库。
- 多级缓存:使用多级缓存机制,如本地缓存+分布式缓存。
2. 双写一致
先删缓存还是先删数据库?
- 读操作:
- 缓存命中则直接返回。
- 否则查询数据库,写入缓存,并设置超时时间。
- 写操作:
- 延迟双删:首先删除缓存,然后写数据库,延时一段时间再次删除缓存以确保数据更新。
- 缺点:延时时间难以精确控制,可能导致脏数据。
- 互斥锁:保证强一致性,但性能较低。
- 共享锁/排他锁:用于控制并发读写。
- 异步通知:例如使用Canal监听数据库变化并更新缓存,减少代码侵入。
- 延迟双删:首先删除缓存,然后写数据库,延时一段时间再次删除缓存以确保数据更新。
3. Redis 数据持久化
RDB(快照)持久化
- 原理:定期将内存中的数据快照保存到磁盘文件中。
- Copy-On-Write:在写操作时复制一份新数据,避免脏数据影响。
AOF(Append Only File)持久化
- 原理:记录每个写操作的日志,故障恢复时通过重放日志恢复数据。
- 优势:提供更细粒度的数据恢复能力,适合需要高数据完整性的场景。
4.数据过期策略
惰性删除
- 工作方式:在访问key时检测是否过期,如果过期则删除。
- 优缺点:
- 优点:CPU友好。
- 缺点:内存不友好,可能导致大量过期key长期占用内存。
定时删除
- 工作方式:后台定时任务检查并删除过期key,分为slow和fast模式。
- Slow模式:默认每秒运行10次,每次执行不超过25ms。
- Fast模式:频率更高,间隔小于2ms,每次执行不超过1ms。
- 优缺点:
- 优点:CPU和内存友好。
- 缺点:执行时间和频率不确定,可能影响内存使用效率。
大多数情况下两者配合使用。
5. 数据淘汰策略
Redis提供了八种数据淘汰策略来应对内存不足的情况:
- Noeviction:不淘汰任何数据,写操作失败。
- Allkeys-lru:从所有key中选择最近最少使用的key进行淘汰。
- Allkeys-random:随机淘汰key。
- Volatile-lru:仅对设置了过期时间的key采用LRU算法淘汰。
- Volatile-ttl:根据剩余生存时间(TTL)排序,优先淘汰TTL小的key。
- Volatile-random:从设置了过期时间的key中随机淘汰。
- Allkeys-lfu:基于LFU(Least Frequently Used)算法淘汰所有key中最不常用的。
- Volatile-lfu:基于LFU算法淘汰设置了过期时间的key中最不常用的。
这些策略可以根据具体的应用场景灵活配置,以达到最佳的内存管理和应用性能平衡。
二、分布式
1. Redis 分布式锁的基础实现
1.1 底层命令:SETNX
- SETNX 是 Redis 提供的一个原子性操作,全称是 SET if Not eXists。
- 它的作用是:只有当键不存在时,才会设置键值对。
1.2 获取锁
SET lock value NX EX 10
- NX:表示只有在键 lock 不存在时才设置。
- EX 10:设置键的过期时间为 10 秒,防止死锁。
- value:通常是一个唯一标识符(如 UUID),用于确保释放锁时的安全性。
1.3 释放锁
DEL lock
- 删除键以释放锁。
- 问题:
- 如果锁的过期时间到了,但业务逻辑尚未执行完,可能会导致误删其他线程的锁。
- 因此,释放锁时需要验证 value 是否与当前线程设置的值一致。
改进后的释放锁逻辑:
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
如果Redis中指定的键的值等于给定的值,则删除该键,否则不执行任何操作。
- 使用 Lua 脚本保证原子性。
1.4 不可重入性
- Redis 原生锁不支持重入性。
- 如果同一个线程尝试多次获取锁,会导致死锁。
2 Redis 集群:
2.1 Redis 主从复制
关键概念
1. replid(复制 ID)
- 每个主节点都有一个唯一的 replid,用于标识主节点的数据版本。
- 从节点在首次同步时会保存主节点的 replid,用于后续的增量同步。
- 如果主节点重启,会生成一个新的 replid,此时从节点无法进行增量同步,必须触发全量同步。
2. offset(偏移量)
- offset 表示主节点写入的命令序列号。
- 每次主节点执行写操作时,offset 都会递增。
- 从节点通过 offset 来记录自己已经同步到的位置。
3. 复制缓冲区
- 主节点维护一个固定大小的复制缓冲区,用于存储最近的写操作日志。
- 当从节点断开连接后重新连接时,如果从节点的 offset 在复制缓冲区范围内,主节点可以直接发送缺失的日志,而无需进行全量同步。
全量同步(Full Resynchronization)
触发场景
- 当主节点和从节点第一次建立连接时。
- 从节点请求同步,但主节点判断 replid 不一致时(比如从节点从未与当前主节点同步过,或者主节点重启后生成了新的 replid)。
同步流程
请求同步:
- 从节点向主节点发送 PSYNC <replid> <offset> 请求。
- 如果主节点发现 replid 不匹配或从节点没有提供有效的 replid,则触发全量同步。
生成 RDB 文件:
- 主节点生成一个 RDB 快照文件(包含当前所有数据的状态),并将该文件发送给从节点。
清空从节点数据:
- 从节点在接收到 RDB 文件后,会清空自己的现有数据。
加载 RDB 文件:
- 从节点加载主节点发送的 RDB 文件,恢复到主节点的数据状态。
发送缓冲区日志:
- 在生成 RDB 文件的过程中,主节点可能会继续接收写操作。
- 主节点将这些写操作记录在复制缓冲区中,并在 RDB 文件传输完成后,将这些日志发送给从节点执行。
完成同步:
- 从节点执行主节点发送的日志,保证与主节点的数据完全一致。
增量同步(Partial Resynchronization)
触发场景
- 当从节点已经与主节点完成过一次全量同步后,网络中断或其他原因导致从节点需要重新同步。
- 主节点判断从节点的 replid 和 offset 是有效的(即从节点之前已经与当前主节点同步过)。
同步流程
请求同步:
- 从节点向主节点发送 PSYNC <replid> <offset> 请求。
- 主节点检查 replid 是否一致,以及 offset 是否在复制缓冲区范围内。
判断是否支持增量同步:
- 如果 replid 一致且 offset 在复制缓冲区范围内,则主节点支持增量同步。
- 如果不满足条件(例如 replid 不一致或 offset 超出范围),则退回到全量同步。
发送缺失的日志:
- 主节点计算从节点缺失的日志范围:主节点的 offset - 从节点的 offset。
- 将这部分缺失的日志发送给从节点。
从节点执行日志:
- 从节点接收到缺失的日志后,依次执行这些命令,追赶上主节点的数据状态。
2.2 Redis Sentinel 和 Redis Cluster
Redis Sentinel(哨兵模式)
- 功能:监控 Redis 主从节点的状态,自动进行故障转移。
- 脑裂问题:通过多数派决策机制解决,即只有当大多数哨兵认为主节点不可用时,才会触发故障转移。
Redis Cluster
- 功能:提供高可用性和水平扩展能力,通过分片(sharding)将数据分布到多个节点上,使用16384个哈希槽分配实例,依据key查询。
- 特点:
- 每个节点负责一部分数据,支持自动故障转移。
- 支持多主多从架构,提高系统的可靠性和性能。
- 提供了一致性哈希算法来分配数据,减少数据迁移带来的影响。
- 互相监测以确保可用
3. Redisson 分布式锁
- Redisson 是一个基于 Redis 的 Java 客户端,提供了分布式锁的高级实现。
3.1 Watch Dog 续期
- Redisson 提供了自动续期功能(Watch Dog),避免因锁超时而导致业务未完成的情况。
- 当锁即将过期时,Watch Dog 会自动延长锁的有效期。
工作原理:
- 默认情况下,Redisson 锁的有效期为 30 秒。
- 如果业务逻辑未完成,Watch Dog 每隔 10 秒会自动续期一次,将锁的有效期重新设置为 30 秒。
- 当业务逻辑完成后,手动释放锁时,Watch Dog 停止续期。
3.2 可重入性
- Redisson 支持可重入锁。
- 同一线程可以多次获取同一把锁,而不会死锁。
实现原理:
- 使用 Redis 的 Hash 数据结构存储锁信息。
键值对格式:
{
"lock":
{
"thread-id": 12345,
"count": 2
}
}
- thread-id:记录持有锁的线程 ID。
- count:记录重入次数。
- 每次获取锁时,count 自增;每次释放锁时,count 自减。当 count 为 0 时,删除锁。
3.3 主从一致性
- Redis 单实例的分布式锁存在主从一致性问题。
- 如果主节点宕机,从节点可能尚未同步最新的锁状态,导致数据不一致。
解决方案:
- 使用 Redis Sentinel 或 Redis Cluster 高可用架构。
- 在 Redisson 中,可以通过配置哨兵模式或集群模式来解决主从一致性问题。
3.4 Redlock 算法
- Redlock 是 Redis 官方提出的一种分布式锁算法,旨在提高锁的可靠性。
核心思想:
- 在多个独立的 Redis 实例上创建锁。
- 只有当客户端成功在大多数实例(n/2+1)上获取锁时,才算真正获取到锁。
流程:
- 客户端依次向多个 Redis 实例请求锁。
- 记录每个实例的响应时间和锁的获取结果。
- 如果成功获取锁的实例数超过半数,且总耗时小于锁的有效时间,则认为获取锁成功。
- 如果失败,释放已获取的锁,并重试。
优点:
- 提高了锁的可靠性和可用性。
- 即使部分 Redis 实例宕机,仍能正常工作。
缺点:
- 实现复杂。
- 性能较低(需要与多个实例交互)。
- 运维成本高(需要维护多个 Redis 实例)。
4. 对比总结
特性 | Redis 原生锁 | Redisson | Redlock |
实现复杂度 | 简单 | 中等 | 复杂 |
性能 | 高 | 较高 | 较低 |
可靠性 | 较低(主从一致性问题) | 较高 | 高 |
可重入性 | 不支持 | 支持 | 不支持 |
续期机制 | 手动 | 自动(Watch Dog) | 手动 |
适用场景 | 简单业务、单实例 Redis | 复杂业务、高并发场景 | 高可靠性要求、多实例 Redis |
5. 注意事项
锁的过期时间:
- 设置合理的锁过期时间,避免死锁。
- 使用 Watch Dog 自动续期时,需确保业务逻辑能够在合理时间内完成。
主从一致性:
- 单实例 Redis 存在主从一致性问题,建议使用 Redis Sentinel 或 Redis Cluster。
Redlock 的使用:
- Redlock 适用于高可靠性场景,但在高并发环境下性能较差,需权衡利弊。
安全性:
- 释放锁时务必验证 value,避免误删其他线程的锁。
小结
- Redis 原生锁:简单易用,适合轻量级场景,但存在不可重入性和主从一致性问题。
- Redisson:功能强大,支持可重入性、自动续期和主从一致性,适合复杂业务场景。
- Redlock:高可靠性,但实现复杂、性能较低,适合极端高可用性需求。
同步方式优缺点对比
特性 | 全量同步 | 增量同步 |
适用场景 | 初次同步、replid 不一致、offset 超出范围 | 网络中断后重新连接、数据变化较小 |
性能消耗 | 高(需要生成 RDB 文件并传输) | 低(仅传输缺失的日志) |
数据一致性 | 强(从节点清空数据后重新加载) | 强(基于日志追加更新) |
网络带宽占用 | 高 | 低 |
三、I/O
Redis 以其高性能和灵活性在现代应用开发中扮演着重要角色。它的设计充分利用了内存操作的高效性,并通过独特的网络模型实现了高效的 I/O 处理。以下是关于 Redis 的内存操作、线程模型以及其网络模型的详细解析。
1. 内存操作与单线程
内存操作
- Redis 是一个内存数据库,所有的数据都存储在内存中,这使得它能够提供极高的读写速度。
- 由于内存访问速度远高于磁盘,Redis 可以快速地处理大量请求。
单线程模型
- Redis 采用单线程来处理客户端请求,这意味着在同一时间点上只有一个任务被执行,从而避免了多线程编程中的并发问题(如死锁、竞态条件等)。
- 单线程模式下的 Redis 非常适合执行短小且快速的操作,因为这些操作不会阻塞其他请求的处理。
2. I/O 多路复用
I/O 多路复用是一种允许单个线程同时监听多个文件描述符的技术,适用于高并发场景下提高服务器性能。
常见 I/O 模型
- 阻塞 I/O:当尝试从 socket 读取数据时,如果当前没有可用的数据,则该线程会被阻塞,直到有数据可读为止。这种方式效率较低,因为它会导致线程等待。
- 非阻塞 I/O:尝试读取数据时,如果没有数据立即返回错误,而不是等待。虽然这种方法可以避免线程被阻塞,但它需要不断轮询,导致 CPU 使用率增加。
- I/O 多路复用:通过 select/poll/epoll 等机制,可以让一个线程同时监控多个 socket 的状态变化,只有当某个 socket 准备好进行读写操作时才触发相应的事件处理函数。这种机制可以显著提高系统吞吐量。
Select/Poll/Epoll
- Select:支持同时监视多个文件描述符的状态变化,但存在最大文件描述符数量限制,且每次调用都需要遍历所有文件描述符,效率不高。
- Poll:类似于 select,但是去除了文件描述符数量的限制,但在大并发情况下依然存在性能瓶颈。
- Epoll:Linux 特有的 I/O 事件通知接口,相比于 select 和 poll,它更高效,尤其是在处理大量连接时表现尤为突出。
3. Redis 网络模型
单线程 + I/O 多路复用 + 事件派发
- Redis 的早期版本采用了单线程模型结合 I/O 多路复用技术(通常使用 epoll),来实现高效的请求处理。
- 这种模型的核心思想是利用单线程配合 I/O 多路复用来管理大量的网络连接,确保每个连接都能得到及时响应。
- 当接收到客户端请求后,Redis 会将其分发给对应的事件处理器执行具体的命令逻辑。
多线程模型(Redis 6.0+)
- 在 Redis 6.0 版本中引入了多线程 I/O 支持,旨在进一步提升性能,特别是在高并发环境下。
- 网络多线程获取处理:多个线程负责接收和发送网络请求,这样可以并行化 I/O 操作,减少单一 I/O 线程成为瓶颈的可能性。
- 本地单线程执行:尽管 I/O 操作被并行化,核心的数据处理仍然由主线程完成,保证了数据的一致性和安全性。
- 网络多线程返回:经过处理后的响应再由多个 I/O 线程异步返回给客户端,提高了整体的响应速度。
总结:
Redis 的主从复制机制通过 replid 和 offset 实现了高效的数据同步,既支持初次同步时的全量传输,也支持断点续传式的增量同步,大大提升了系统的可用性和性能。结合 Redis Sentinel 和 Redis Cluster,可以构建高可用性和高性能的分布式系统。
对于分布式锁,Redisson 提供了比原生 Redis 更强大的功能,包括可重入性、自动续期(Watch Dog)、以及对主从一致性的支持。Redlock 算法虽然提供了更高的可靠性,但在实现复杂度和性能上有一定的代价,适用于极端高可用性需求的场景。根据具体的业务需求选择合适的方案是至关重要的。
Redis 的设计巧妙地结合了内存操作的高速度和单线程模型的安全性,通过 I/O 多路复用技术实现了高效的网络通信。随着 Redis 6.0 引入多线程 I/O,它能够在保持数据一致性的同时,进一步提升在高并发环境下的性能表现。对于开发者而言,理解 Redis 的这些特性有助于更好地优化应用程序的架构设计。