一、缓存穿透(Cache Penetration)
问题描述: 客户端请求的数据既不在缓存中,也不在数据库中(例如恶意请求非法ID),导致请求直接穿透缓存层到达数据库,引发数据库压力激增。
解决方案:
- 布隆过滤器(Bloom Filter)
- 原理:在缓存层前加一层布隆过滤器,将所有可能的合法 Key 哈希存储。请求到达时先经过布隆过滤器检查,若不存在则直接拒绝,避免查询数据库。
- 优点:内存占用极低,适合大规模数据。
- 缺点:存在误判率(可通过调整哈希函数和位数组大小优化),且不支持删除操作。
- 缓存空对象(Cache Null)
- 原理:即使查询数据库未命中,仍然在缓存中存储一个空值(如
key:null),并设置较短的过期时间(如 5 分钟)。 - 优点:实现简单,能拦截短期重复请求。
- 缺点:可能缓存大量无效 Key,占用内存;需结合清理策略(如定期删除空对象)。
- 原理:即使查询数据库未命中,仍然在缓存中存储一个空值(如
- 请求校验(Validation)
- 原理:在业务层对请求参数进行合法性校验(如 ID 必须为数字、长度限制等),过滤无效请求。
- 示例:拦截明显非法的 ID(如负数、超长字符串)。
二、缓存击穿(Cache Breakdown)
问题描述: 某个热点 Key 在缓存中过期的瞬间,大量并发请求同时涌入,直接击穿缓存层访问数据库,导致数据库瞬时压力过大。
解决方案:
-
互斥锁(Mutex Lock)
-
原理:当缓存失效时,使用分布式锁(如 Redis 的
SETNX或 RedLock)控制只有一个线程能重建缓存,其他线程等待锁释放后直接读取新缓存。 -
代码示例:
def get_data(key): data = redis.get(key) if data is None: if redis.setnx("lock:" + key, 1, ex=5): # 获取锁 data = db.query(key) redis.set(key, data, ex=300) redis.delete("lock:" + key) else: time.sleep(0.1) # 等待重试 return get_data(key) return data -
优点:保证数据库安全。
-
缺点:锁竞争可能增加延迟,需设置合理的锁超时时间。
-
-
逻辑过期(Logical Expiration)
-
原理:缓存不设置物理过期时间,而是在 Value 中存储过期时间戳。业务读取时判断是否过期,若过期则异步更新缓存,当前线程返回旧数据。
-
数据结构示例:
{ "value": "真实数据", "expire_time": 1717000000 // 逻辑过期时间戳 }
-
-
永不过期 + 异步更新(No Expire)
- 原理:缓存不设置过期时间,通过后台任务定期异步更新热点数据。
- 适用场景:数据变更频率低的热点 Key(如商品基础信息)。
-
热点数据预热(Preheating)
- 原理:在系统启动或高峰期前,提前加载热点数据到缓存,并分散设置不同的过期时间,避免同时失效。
三、缓存雪崩
缓存雪崩是指在缓存系统中,大量缓存数据同时失效或缓存服务宕机,导致所有请求直接涌向数据库,引发数据库压力激增甚至崩溃的现象。其核心问题在于系统未能有效应对缓存层的大规模失效,导致后端资源过载。
解决方案
-
分散缓存过期时间
- 随机化过期时间:为每个缓存设置基础过期时间(如24小时)加随机值(如0~3600秒),避免同时失效。
- 示例代码:
expire_time = base_time + random.randint(0, 3600)
-
高可用缓存架构
- Redis集群/哨兵模式:通过主从复制和自动故障转移,确保单节点故障时服务不中断。
- 多级缓存:结合本地缓存(如Caffeine、Ehcache)与分布式缓存(如Redis),即使分布式缓存宕机,本地缓存仍可缓解部分压力。
-
永不过期策略+异步更新
- 不设置TTL,通过后台定时任务或数据变更时主动更新缓存。
- 风险控制:需监控数据一致性,避免脏数据长期存在。
-
互斥锁/队列控制并发
-
当缓存失效时,仅允许一个线程查询数据库并重建缓存,其他线程等待。
-
伪代码逻辑:
def get_data(key): data = cache.get(key) if data is None: if lock.acquire(key): # 获取分布式锁 data = db.query(key) cache.set(key, data, expire) lock.release(key) else: time.sleep(0.1) return get_data(key) # 重试 return data
-
-
熔断与降级机制
- 监控数据库负载:当QPS超过阈值时,触发熔断,直接返回默认值或错误页面。
- 降级策略:如返回兜底数据(如推荐热门商品)、限流(仅允许50%请求通过)。
-
预热与热点数据隔离
- 预热:高峰前预先加载高频数据到缓存。
- 热点数据特殊处理:对极高频数据(如明星新闻)采用独立缓存集群或永不失效。
与其他缓存问题的对比
| 问题类型 | 触发条件 | 解决思路 |
|---|---|---|
| 缓存雪崩 | 大量缓存同时失效/服务宕机 | 分散过期、多级缓存、熔断降级 |
| 缓存击穿 | 单个热点数据过期 | 互斥锁、永不过期热点数据 |
| 缓存穿透 | 查询不存在的数据(如id=-1) | 布隆过滤器、缓存空值、参数校验 |
扩展优化
- 熔断降级:结合 Sentinel 或 Hystrix 实现数据库访问熔断,极端情况下返回默认值。
- 多级缓存:使用本地缓存(如 Caffeine)+ Redis 分层缓存,减少击穿风险。
- 监控告警:监控缓存命中率和数据库 QPS,异常时及时预警。
四、布隆过滤器
布隆过滤器(Bloom Filter)是一种空间效率极高的概率型数据结构,用于快速判断一个元素是否可能存在于一个集合中。它的核心特点是用极小的内存代价换取高效的查询性能,但存在一定的误判率(即可能误报元素存在,但绝不会漏报元素不存在)。
核心原理
-
数据结构
- 布隆过滤器基于一个二进制位数组(Bit Array) 和多个哈希函数(Hash Functions) 实现。
- 初始时所有位均为
0,插入元素时通过多个哈希函数将元素映射到位数组的多个位置,并将这些位置置为1。
-
操作流程
-
添加元素: 将元素通过多个哈希函数计算得到多个哈希值,将这些值对应的位数组下标置为
1。例如:插入元素 "A",哈希后得到位置 2、5、9 → 将这三个位置置为 1。 -
查询元素: 将元素通过相同的哈希函数计算得到多个位置,检查这些位置是否全为
1:- 若全为
1→ 可能存在(可能误判)。 - 任一位置为
0→ 一定不存在。
例如:查询元素 "B",哈希后得到位置 2、5、8 → 若位置8为0,则 "B" 一定不在集合中。 - 若全为
-
-
误判率
-
布隆过滤器可能将实际不存在的元素误判为存在(False Positive),但不会将存在的元素误判为不存在。
-
误判率与位数组大小、哈希函数数量、元素数量有关,可通过公式调整参数:
P≈(1−e−kn/m)kP≈(1−e−kn/m)k
- mm:位数组大小
- nn:元素数量
- kk:哈希函数数量
-
五、MySQL与Redis的数据同步
在 Redis 与 MySQL 的数据同步场景中,核心目标是 保证数据最终一致性 并 避免缓存脏数据。以下是 6 种主流同步方案及其适用场景:
一、Cache-Aside 模式(经典缓存模式)
核心逻辑:由应用层控制缓存读写,不依赖数据库主动同步
# 读操作
def get_data(key):
data = redis.get(key) # 1.先查缓存
if not data:
data = mysql.query(key) # 2.缓存未命中则查数据库
redis.set(key, data) # 3.回填缓存
return data
# 写操作
def update_data(key, value):
mysql.update(key, value) # 1.先更新数据库
redis.delete(key) # 2.再删除缓存(非更新!)
特点:
- 优点:架构简单,适合大部分场景
- 缺点:存在短暂不一致窗口(删除缓存后到下次查询前的间隙)
- 优化:结合延迟双删(下文说明)
二、双写模式(Write-Through)
核心逻辑:将 Redis 视为主要数据源,所有写操作同时更新数据库和缓存
public void updateUser(User user) {
// 1.同步更新数据库
userDAO.update(user);
// 2.同步更新缓存
redis.set("user:"+user.getId(), user);
}
特点:
- 优点:数据一致性更强
- 缺点:
- 写操作性能下降(需要两次写)
- 并发写时可能产生时序问题(需分布式锁)
- 适用场景:写少读多,对一致性要求高的业务(如账户余额)
三、延迟双删策略(应对缓存-数据库不一致)
核心逻辑:在数据库更新后,执行两次缓存删除操作,这里主要需要注意的是防止第一次删除之后,数据还没有写进数据库时,有线程对 Redis 进行修改,这就出现数据库跟 Redis 数据不一致的情况
def update_data(key, value):
# 第一次删除
redis.delete(key)
# 更新数据库
mysql.update(key, value)
# 延迟指定时间后再次删除
Thread.sleep(500) # 延迟时间需根据业务调整
redis.delete(key)
设计要点:
- 第一次删除:防止旧数据被后续读操作回填
- 第二次删除:消除数据库主从同步延迟期间的脏数据
- 延迟时间 = 主从复制延迟时间 + 业务读操作耗时
四、基于 Binlog 的异步同步(解耦方案)
架构流程:
MySQL --> Binlog --> 数据变更事件 --> 同步服务 --> 更新Redis
实现方式:
- 使用 Canal 或 Debezium 监听 MySQL Binlog
- 将变更事件发送到消息队列(如 Kafka)
- 消费者服务解析事件并更新 Redis
特点:
- 优点:完全解耦,对业务代码无侵入
- 缺点:架构复杂度高,同步延迟约 100ms-1s
- 适用场景:需要异构系统同步(如同步到 ES、HBase)
五、分布式事务方案(强一致性)
核心逻辑:通过事务消息保证数据库与缓存操作的原子性
// RocketMQ 事务消息示例
public void updateWithTransaction(User user) {
// 1.发送半消息
Message msg = new Message("user_update", user.toString());
TransactionSendResult sendResult = producer.sendMessageInTransaction(msg);
// 2.执行本地事务(更新数据库)
if(executeLocalTransaction(user)) {
producer.commitTransaction(sendResult);
redis.delete(user.getId()); // 3.提交后删除缓存
} else {
producer.rollbackTransaction(sendResult);
}
}
特点:
- 优点:保证强一致性
- 缺点:性能损耗大(TPS下降约30-50%)
- 适用场景:金融交易等强一致性要求场景
六、多级缓存策略(混合方案)
架构设计:
复制
请求 -> 本地缓存 -> Redis -> 数据库
↑ ↑
└── 同步服务 ─┘
同步策略:
- 本地缓存(如 Caffeine)设置短过期时间(如 30s)
- Redis 设置较长过期时间(如 10min)
- 通过后台服务定期同步 MySQL 数据到 Redis
- 数据变更时发送广播事件刷新所有节点本地缓存
特点:
- 优点:抗突发流量能力强
- 缺点:数据延迟可能达到分钟级
- 适用场景:高并发读场景(如商品详情页)
方案选择建议
| 方案 | 一致性级别 | 延迟 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| Cache-Aside | 最终一致 | 低 | 低 | 通用场景 |
| 双写模式 | 强一致 | 无 | 中 | 写少读多的关键数据 |
| 延迟双删 | 最终一致 | 可控 | 低 | 高频写场景 |
| Binlog同步 | 最终一致 | 100ms+ | 高 | 异构系统同步 |
| 分布式事务 | 强一致 | 无 | 高 | 金融/交易系统 |
| 多级缓存 | 最终一致 | 分钟级 | 中 | 高并发读场景 |
六、Redis持久化方式
Redis作为缓存时,数据的持久化主要通过两种机制实现:RDB(Redis Database) 和 AOF(Append-Only File)。这两种方式可以单独使用,也可以结合使用,以满足不同场景下对数据持久化和性能的需求。以下是详细说明:
1. RDB(快照持久化)
原理:
RDB 是 Redis 默认的持久化方式,通过生成某个时间点的数据快照(Snapshot)来实现持久化。快照文件(.rdb)是二进制格式,记录了 Redis 在某一时刻的所有数据。
触发机制:
-
手动触发:通过
SAVE(阻塞主进程)或BGSAVE(后台异步)命令手动生成快照。 -
自动触发:在
redis.conf中配置规则,例如:bash
复制
save 900 1 # 900秒内至少1个键被修改时触发 save 300 10 # 300秒内至少10个键被修改时触发 save 60 10000 # 60秒内至少10000个键被修改时触发
工作流程:
- Redis 主进程 fork 一个子进程。
- 子进程将内存数据写入临时 RDB 文件。
- 写入完成后替换旧的 RDB 文件。
优点:
- 文件紧凑:适合备份和灾难恢复。
- 恢复速度快:直接加载二进制文件到内存。
- 对性能影响小:子进程处理写入,主进程无阻塞(除 fork 的瞬间)。
缺点:
- 数据丢失风险:两次快照之间的数据可能丢失(取决于触发频率)。
- 大数据量时 fork 可能延迟:数据量过大时,fork 子进程可能短暂阻塞主进程。
2. AOF(追加日志持久化)
原理:
AOF 记录所有写操作命令(如 SET, DEL),以文本日志形式追加到文件中。重启时重新执行这些命令恢复数据。
配置选项:
-
开启 AOF:在
redis.conf中设置:appendonly yes -
同步策略(
appendfsync):- always:每次写操作都同步到磁盘,最安全但性能最低。
- everysec(默认):每秒同步一次,平衡性能与安全。
- no:由操作系统决定同步时机,性能最好但可能丢失数据。
AOF 重写(Rewrite):
- 目的:压缩 AOF 文件体积(例如合并多个命令为最终状态的命令)。
- 触发方式:
- 手动触发:
BGREWRITEAOF命令。 - 自动触发:根据
auto-aof-rewrite-percentage和auto-aof-rewrite-min-size配置。
- 手动触发:
优点:
- 数据更安全:可配置为几乎零数据丢失。
- 可读性强:AOF 文件为文本格式,便于人工分析。
缺点:
- 文件体积大:日志文件可能比 RDB 大数倍。
- 恢复速度慢:需要逐条执行命令,耗时长。
3. 混合持久化(Redis 4.0+)
原理: 结合 RDB 和 AOF 的优势。在 AOF 重写时,将当前数据以 RDB 格式写入 AOF 文件头部,后续增量数据仍以 AOF 格式追加。
配置:
aof-use-rdb-preamble yes
优点:
- 快速恢复:RDB 部分快速加载。
- 低数据丢失:后续 AOF 日志记录增量操作。
4. 作为缓存的持久化建议
- 场景需求决定策略:
- 接受数据丢失(如缓存可重建):仅用 RDB 或关闭持久化(
save "")。 - 需减少数据丢失:启用 AOF(
appendfsync everysec)或混合持久化。
- 接受数据丢失(如缓存可重建):仅用 RDB 或关闭持久化(
- 性能权衡:
- RDB 适合大规模数据快速备份。
- AOF 适合对数据一致性要求高的场景。
- 监控与优化:
- 监控磁盘 I/O 和内存使用,避免持久化影响性能。
- 定期备份 RDB/AOF 文件到其他服务器。
5. 持久化配置示例
# RDB 配置
save 900 1
save 300 10
save 60 10000
dbfilename dump.rdb # RDB 文件名
dir /var/lib/redis # 持久化文件存储路径
# AOF 配置
appendonly yes
appendfsync everysec
appendfilename "appendonly.aof"
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
# 混合持久化
aof-use-rdb-preamble yes
总结
- RDB:适合对性能敏感且允许少量数据丢失的场景(如缓存)。
- AOF:适合对数据一致性要求高的场景。
- 混合模式:平衡恢复速度与数据安全,推荐在 Redis 4.0+ 中使用。
七、Redis数据过期策略
Redis 的数据过期策略主要分为两类:主动过期策略和被动过期策略。此外,当内存不足时,Redis 还会触发内存淘汰策略(与过期策略相关但不同)。以下是详细说明:
一、被动过期策略(惰性删除,Lazy Expiration)
机制:
当客户端尝试访问某个键(Key)时,Redis 会先检查该键是否设置了过期时间,并判断是否已过期。如果已过期,则直接删除该键,并返回 nil(或执行其他逻辑,如删除后触发回调)。
优点:
- 低CPU消耗:仅在访问时检查,避免不必要的资源浪费。
- 简单直接:对性能影响小,适合高频访问的键。
缺点:
- 内存泄漏风险:如果某些键长期不被访问,即使已过期也会一直占用内存。
- 依赖访问频率:对冷数据(不常访问的键)清理效率低。
二、主动过期策略(定期删除,Periodic Expiration)
机制: Redis 会周期性(默认每秒 10 次)运行一个后台任务,随机从设置了过期时间的键中抽取一批样本(默认每次检查 20 个键),删除其中已过期的键。如果本轮检查中发现超过 25% 的键已过期,则立即重复此过程,直到过期键比例低于 25%。
优点:
- 主动清理冷数据:减少内存泄漏风险。
- 可控的CPU占用:通过调整频率和样本数量平衡性能与清理效率。
缺点:
- 无法完全实时:可能存在少量过期键未被及时清理。
- 参数敏感:需合理配置检查频率和样本数量(通过
hz参数调整)。
三、内存淘汰策略(Eviction Policies)
当内存达到 maxmemory 限制时,Redis 会根据配置的策略删除键,即使某些键未过期。淘汰策略通过 maxmemory-policy 配置,常见策略包括:
| 策略 | 说明 |
|---|---|
noeviction | 不删除键,直接返回错误(默认策略)。 |
volatile-lru | 从设置了过期时间的键中,删除最近最少使用(LRU)的键。 |
volatile-ttl | 从设置了过期时间的键中,删除剩余生存时间(TTL)最短的键。 |
volatile-random | 从设置了过期时间的键中,随机删除一个键。 |
allkeys-lru | 从所有键中删除最近最少使用(LRU)的键。 |
allkeys-random | 从所有键中随机删除一个键。 |
volatile-lfu | 从设置了过期时间的键中,删除最不经常使用(LFU)的键(Redis 4.0+)。 |
allkeys-lfu | 从所有键中删除最不经常使用(LFU)的键(Redis 4.0+)。 |
关键点:
- 如果键未设置过期时间,
volatile-*类策略不会处理它们。 LRU(Least Recently Used)和LFU(Least Frequently Used)是两种不同的算法,前者基于访问时间,后者基于访问频率。
四、持久化与复制中的过期处理
- RDB 持久化
- 生成 RDB 文件:过期键不会被保存到 RDB 文件。
- 加载 RDB 文件:主节点(Master)会忽略过期键;从节点(Replica)会保留所有键,依赖主节点同步删除操作。
- AOF 持久化
- 写入 AOF:当键因过期被删除时,会追加一条
DEL命令到 AOF 文件。 - AOF 重写:重写时忽略已过期的键。
- 写入 AOF:当键因过期被删除时,会追加一条
- 主从复制
- 从节点不会主动删除过期键,而是等待主节点同步
DEL命令。 - 读取从节点时,可能返回已过期的键(需依赖主节点同步逻辑)。
- 从节点不会主动删除过期键,而是等待主节点同步
五、总结
Redis 通过 惰性删除 + 定期删除 的组合策略处理过期键,确保内存高效利用。实际应用中需注意:
- 合理设置过期时间,避免大量键集中过期(可能导致性能波动)。
- 根据业务场景选择合适的内存淘汰策略(如优先保留热数据)。
- 监控内存使用情况,避免达到
maxmemory极限。
八、如何将千万级热点数据缓存
如果把所有的的数据都放入Redis 不太现实也没必要,比如缓存热点 top20万。为了保证 Redis 中缓存的 20 万数据都是热点数据,可以从 内存淘汰策略、数据访问设计 和 运维监控 三个层面综合优化。以下是具体方案:
一、内存淘汰策略优化
1. 选择 LFU(Least Frequently Used)淘汰策略
- 配置:将
maxmemory-policy设置为allkeys-lfu(Redis 4.0+ 支持)。 - 原理:优先淘汰最不频繁访问的键,保留高频访问的热点数据。
- 对比 LRU:
- LRU(Least Recently Used)只关注“最近是否访问”,可能误留短期突发访问的非热点数据。
- LFU 通过统计访问频率,更适合长期稳定的热点场景。
2. 设置合理的过期时间(TTL)
- 为所有缓存键设置合理的过期时间,即使未被淘汰,冷数据也会因过期自动清理。
- 例如:结合业务特征,对低频数据设置较短 TTL,对热点数据设置较长 TTL。
二、数据访问设计优化
1. 分层缓存
- 前置本地缓存:在应用层(如本地内存)缓存极热点数据(如 Top 1% 的热点),减少 Redis 频繁访问的压力。
- 动态识别热点:
- 使用实时统计工具(如 Redis 的
OBJECT freq命令、redis-cli --hotkeys)识别高频访问的键。 - 结合日志分析(如 ELK 或 Prometheus)离线挖掘历史热点数据。
- 使用实时统计工具(如 Redis 的
2. 主动预热与更新
- 冷启动预热:系统启动时,预先从数据库加载历史热点数据到 Redis。
- 异步更新:后台任务定期将最新热点数据同步到 Redis,例如通过消息队列监听数据库变更。
3. 业务逻辑优化
- 缓存穿透防护:对不存在的键设置短 TTL 的空值(避免恶意请求穿透到数据库)。
- 批量查询合并:将多个请求合并为一次批量查询,减少冷数据缓存污染(例如使用
MGET替代多次GET)。
三、运维与监控保障
1. 监控 Redis 内存与命中率
- 使用
INFO命令或监控工具(如 Grafana + Prometheus)关注:- 内存使用率(
used_memory) - 缓存命中率(
keyspace_hits / (keyspace_hits + keyspace_misses)) - 淘汰键数量(
evicted_keys)
- 内存使用率(
2. 动态调整参数
- 调优 LFU 衰减系数:通过
lfu-decay-time控制访问频率的衰减速度,避免旧热点长期占据缓存。 - 调整淘汰样本量:增大
maxmemory-samples(默认 5)以提高淘汰策略的准确性。
3. 容量规划与扩容
- 若热点数据规模持续增长,可考虑:
- 横向扩容 Redis 集群(如 Redis Cluster)。
- 使用分片策略,按业务拆分多个 Redis 实例。
四、示例配置
# redis.conf 关键配置
maxmemory 20gb # 根据实际内存设置上限(如 20 万数据所需内存)
maxmemory-policy allkeys-lfu # 使用 LFU 淘汰策略
lfu-log-factor 10 # 控制 LFU 计数器增长速率(默认 10,值越小频率统计越敏感)
lfu-decay-time 1 # 访问频率衰减时间(单位分钟,默认 1)
五、总结
| 方向 | 具体措施 |
|---|---|
| 淘汰策略 | 启用 allkeys-lfu,优先保留高频访问数据。 |
| 数据分层 | 本地缓存极热点 + Redis 缓存次热点,动态识别并预热。 |
| 监控与调优 | 实时关注命中率与淘汰数据,调整 LFU 参数。 |
| 业务设计 | 合并查询、设置 TTL、防护缓存穿透。 |
九、Redis 如何实现分布式锁
Redis 分布式锁是用于在分布式系统中协调多个进程或服务对共享资源的互斥访问的核心机制。以下是 Redis 分布式锁的详细实现原理、关键步骤及注意事项:
1. 基础实现原理
Redis 分布式锁的核心是通过 原子性操作 在 Redis 中创建一个唯一的键值对,表示锁的持有状态。常用命令为 SET key value NX EX timeout:
NX:仅当键不存在时设置值(保证互斥性)。EX:设置键的过期时间(防止死锁)。value:唯一标识锁的持有者(避免误删他人锁)。
2. 实现步骤
(1) 获取锁
SET lock_key unique_value NX EX 30
unique_value:必须是一个唯一标识(如 UUID + 线程 ID),用于安全释放锁。EX 30:设置锁的自动过期时间(如 30 秒),防止客户端崩溃导致锁无法释放。
(2) 释放锁
释放锁必须通过 原子性操作 验证持有者身份,防止误删:
-- Lua 脚本保证原子性
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
- 通过
EVAL执行脚本,确保GET和DEL的原子性。
3. 关键问题与解决方案
(1) 锁超时与自动续期(Watchdog)
-
问题:业务执行时间超过锁的过期时间,导致锁提前释放。
-
解决:启动后台线程(看门狗)定期(如每 10 秒)续期锁:
if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("EXPIRE", KEYS[1], ARGV[2]) else return 0 end- Redisson 等库已内置此机制。
(2) 锁误删
- 问题:客户端 A 释放了客户端 B 的锁(如因过期时间设置不合理)。
- 解决:严格通过
unique_value验证锁归属。
(3) 单点故障
-
问题:单节点 Redis 宕机导致锁失效。
-
解决:使用 RedLock 算法(Redis 多节点分布式锁):
-
向多个独立 Redis 节点依次请求锁。
-
当超过半数节点(如 3/5)成功获取锁,且总耗时小于锁超时时间时,认为成功。
-
释放时向所有节点发送删除请求。
- 争议点:对系统时钟敏感,需确保节点间时钟同步。
-
**4. **RedLock的简单介绍
如果线程一在Redis的master节点上拿到了锁,但是加锁的key还没同步到slave节点。恰好这时,master节点发生故障,一个slave节点就会升级为master节点。线程二就可以获取同个key的锁啦,但线程一也已经拿到锁了,锁的安全性就没了。
为了解决这个问题,Redis作者 antirez提出一种高级的分布式锁算法:Redlock。Redlock核心思想是这样的:
搞多个Redis master部署,以保证它们不会同时宕掉。并且这些master节点是完全相互独立的,相互之间不存在数据同步。同时,需要确保在这多个master实例上,是与在Redis单实例,使用相同方法来获取和释放锁。
我们假设当前有5个Redis master节点,在5台服务器上面运行这些Redis实例。
RedLock的实现步骤:如下
- 1.获取当前时间,以毫秒为单位。
- 2.按顺序向5个master节点请求加锁。客户端设置网络连接和响应超时时间,并且超时时间要小于锁的失效时间。(假设锁自动失效时间为10秒,则超时时间一般在5-50毫秒之间,我们就假设超时时间是50ms吧)。如果超时,跳过该master节点,尽快去尝试下一个master节点。
- 3.客户端使用当前时间减去开始获取锁时间(即步骤1记录的时间),得到获取锁使用的时间。当且仅当超过一半(N/2+1,这里是5/2+1=3个节点)的Redis master节点都获得锁,并且使用的时间小于锁失效时间时,锁才算获取成功。(如上图,10s> 30ms+40ms+50ms+4m0s+50ms)
- 如果取到了锁,key的真正有效时间就变啦,需要减去获取锁所使用的时间。
- 如果获取锁失败(没有在至少N/2+1个master实例取到锁,有或者获取锁时间已经超过了有效时间),客户端要在所有的master节点上解锁(即便有些master节点根本就没有加锁成功,也需要解锁,以防止有些漏网之鱼)。
简化下步骤就是:
- 按顺序向5个master节点请求加锁
- 根据设置的超时时间来判断,是不是要跳过该master节点。
- 如果大于等于3个节点加锁成功,并且使用的时间小于锁的有效期,即可认定加锁成功啦。
- 如果获取锁失败,解锁!
5. 代码示例
import redis.clients.jedis.Jedis;
import java.util.UUID;
public class RedisLock {
private static final String LOCK_KEY = "resource_lock";
private String lockValue = UUID.randomUUID().toString();
private Jedis client = new Jedis("localhost");
public void performWithLock() {
// 尝试获取锁
String result = client.set(LOCK_KEY, lockValue, "NX", "EX", 30);
boolean acquired = result != null && result.equalsIgnoreCase("OK");
if (acquired) {
try {
// 执行业务逻辑
System.out.println("Working with the resource...");
} finally {
// 释放锁(通过评估Lua脚本)
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else " +
"return 0 end";
client.eval(script, 1, LOCK_KEY, lockValue);
}
} else {
// 获取锁失败
System.out.println("Failed to acquire lock");
}
}
public static void main(String[] args) {
RedisLock example = new RedisLock();
example.performWithLock();
}
}
总结
Redis 分布式锁的核心是 原子性操作 和 唯一标识验证。实际应用中需结合业务场景选择单节点锁或 RedLock,并通过自动续期、异常处理等机制提高可靠性。对于高可用场景,建议使用成熟的库(如 Redisson)而非自行实现。
十、Redis 集群方案
Redis 集群的常见方案主要包括 主从复制、哨兵模式(Sentinel) 和 分片集群(Redis Cluster)。每种方案适用于不同的场景,解决不同的问题(如高可用、负载均衡、数据分片等)。以下是详细说明:
一、主从复制(Master-Slave Replication)
核心原理
- 主节点(Master):负责处理写操作(
SET、DEL等),并将数据变更异步复制到一个或多个从节点。 - 从节点(Slave):复制主节点的数据副本,处理读操作(
GET等)。从节点默认只读。
工作流程
1. 连接建立
- 从节点配置:
通过
slaveof <master_ip> <master_port>命令或在配置文件中设置,从节点记录主节点信息。 - 建立 Socket 连接: 从节点向主节点发起 Socket 连接(默认端口 6379),主节点接受连接后,双方建立长连接。
- 身份认证(可选):
若主节点配置了
requirepass,从节点需发送AUTH <password>完成认证。
2. 数据同步(全量/增量)
- PSYNC 命令:
从节点发送
PSYNC命令,携带Replication ID和offset(首次复制时为空,触发全量复制)。 - 全量复制(Full Resynchronization):
- BGSAVE 生成 RDB:主节点 fork 子进程生成 RDB 快照,同时将后续写命令缓存到 复制缓冲区(Replication Buffer)。
- 传输 RDB:RDB 生成后,主节点将其发送给从节点,从节点清空旧数据并加载 RDB。
- 发送缓冲区命令:主节点将复制缓冲区中的写命令发送给从节点,确保数据一致性。
- 增量复制(Partial Resynchronization):
若从节点断线重连且主节点的 复制积压缓冲区(Repl Backlog Buffer) 中存有缺失的数据(通过
offset判断),则直接发送缺失的写命令,避免全量复制。
3. 命令传播(Phase)
- 持续同步: 数据同步完成后,主节点将每次收到的写命令异步发送给从节点(通过长连接),从节点实时执行这些命令以保持数据一致。
优点
- 读写分离:从节点分担读请求压力。
- 数据冗余:主从节点数据一致,避免单点故障导致数据丢失。
- 简单易用:配置简单,适合小规模场景。
缺点
- 主节点单点故障:主节点宕机后需手动切换从节点为主节点。
- 数据不一致风险:异步复制可能导致主从数据短暂不一致。
- 存储限制:所有节点存储全量数据,无法横向扩展存储容量。
示例架构
Master (写) --> Slave1 (读)
--> Slave2 (读)
二、哨兵模式(Sentinel)
核心原理
- 哨兵(Sentinel):独立进程,监控主从节点的健康状态,并在主节点故障时自动选举新主节点,完成故障转移。
- 高可用保障:通过多个哨兵节点组成集群,避免哨兵自身单点故障。
工作流程
- 监控:哨兵定期向主从节点发送心跳检测。
- 故障判定:若主节点未响应,哨兵集群通过投票机制确认主节点下线。
- 选举新主:从从节点中选择一个(如优先级高、复制偏移量最新)提升为新主节点。
- 切换配置:通知客户端和应用更新主节点地址。
优点
- 自动故障恢复:无需人工干预,保障服务可用性。
- 高可用性:哨兵集群自身具备容错能力。
缺点
- 写操作单点:主节点仍是写操作瓶颈。
- 存储限制未解决:数据仍全量存储,无法横向扩展。
示例架构
Sentinel1
Sentinel2 --> Master --> Slave1
Sentinel3 --> Slave2
三、分片集群(Redis Cluster)
核心原理
- 数据分片:将数据分散到多个节点,每个节点存储部分数据(通过哈希槽
hash slot实现)。 - 自动分片与路由:客户端或代理可直接将请求路由到正确的节点。
- 高可用:每个分片由主从节点组成,主节点故障时从节点自动接替。
关键机制
- 哈希槽(Hash Slot):
- 总共有
16384个槽,每个键通过CRC16(key) % 16384计算所属槽。 - 槽被分配到不同主节点,例如:节点A负责槽0-5000,节点B负责5001-10000等。
- 总共有
- 节点间通信:通过
Gossip协议交换节点状态和槽分配信息。 - 故障转移:主节点宕机时,其从节点自动升级为新主节点。
优点
- 横向扩展:支持动态增加节点,突破单机内存限制。
- 高可用与负载均衡:数据分片和主从复制结合,同时解决扩展性和可用性问题。
- 去中心化:无需代理,客户端直接连接集群节点。
缺点
- 运维复杂度高:需处理数据迁移、槽分配等问题。
- 不支持多键操作:跨槽的
事务或Lua脚本可能失败。 - 客户端兼容性:需使用支持集群协议的客户端(如
JedisCluster)。
示例架构
Node1(Master,槽0-5000) --> Node1-Slave
Node2(Master,槽5001-10000) --> Node2-Slave
Node3(Master,槽10001-16383) --> Node3-Slave
四、方案对比
| 方案 | 核心能力 | 适用场景 | 缺点 |
|---|---|---|---|
| 主从复制 | 数据冗余、读写分离 | 小规模数据备份、读多写少 | 主节点单点故障、无法扩展存储 |
| 哨兵模式 | 自动故障转移、高可用 | 中小规模高可用需求 | 存储和写性能无法扩展 |
| 分片集群 | 数据分片、横向扩展 | 大数据量、高并发、高可用 | 运维复杂、不支持跨分片事务 |
五、如何选择?
- 主从复制+哨兵:适合中小规模应用,需要高可用但数据量不大。
- 分片集群:适合超大规模数据或高并发场景,需同时扩展存储和性能。
十一、Redis为什么使用IO多路复用
I/O 多路复用(I/O Multiplexing)是一种通过单个线程高效管理多个 I/O 流的机制。它的核心思想是:让一个线程能够同时监听多个文件描述符(如套接字)的读写状态,当某个文件描述符就绪(如可读或可写)时,再触发对应的处理逻辑。常见的实现方式包括
select、poll、epoll(Linux)和kqueue(BSD/macOS)等系统调用。
Redis 为什么使用 I/O 多路复用?
Redis 是单线程的(核心命令执行和网络 I/O 处理),但它需要支持高并发连接和低延迟响应。I/O 多路复用模型是 Redis 高性能的关键设计之一,主要原因如下:
- 单线程架构的天然匹配
- Redis 的核心逻辑(命令解析、数据操作)是单线程的,避免了多线程的锁竞争和上下文切换开销。
- I/O 多路复用允许单线程同时处理多个客户端连接,解决了传统阻塞 I/O 模型中“一个线程处理一个连接”的资源浪费问题。
- 高并发下的高效资源利用
- 传统模型(如多线程/多进程)中,每个连接需要一个线程/进程,当并发连接数达到数万时,线程切换和内存占用会成为瓶颈。
- I/O 多路复用通过事件驱动机制,单线程即可管理成千上万的连接,系统资源消耗与连接数无关(仅与活跃连接数相关)。
- 非阻塞 I/O 的完美配合
- Redis 使用非阻塞 I/O:当某个客户端请求未就绪时(如等待数据到达),线程不会阻塞,而是继续处理其他就绪的请求。
- I/O 多路复用负责监听哪些连接已就绪(例如有数据可读或可写),仅处理这些就绪的连接,避免空等。
- 适应 Redis 的工作场景
- Redis 是内存数据库,性能瓶颈通常是网络 I/O,而非 CPU 或磁盘。
- 大多数操作是简单的键值读写(耗时极短),单线程顺序执行命令可以避免竞态条件,保证原子性。
- 跨平台的高效实现
- Redis 根据操作系统自动选择最优的 I/O 多路复用实现:
- Linux →
epoll - BSD/macOS →
kqueue - 其他 →
select/poll
- Linux →
- 这些实现能有效减少轮询开销(例如
epoll的时间复杂度是 O(1),而select是 O(n))。
I/O 多路复用在 Redis 中的作用
- 高吞吐量 单线程即可处理数万并发连接,适合读多写少的场景(如缓存、消息队列)。
- 低延迟 通过事件循环(Event Loop)快速响应就绪的请求,避免线程阻塞。
- 资源高效 减少线程/进程数量,降低内存和 CPU 开销(尤其适合云环境)。
- 简单性和可靠性 单线程模型避免了多线程的复杂性(如锁、线程同步),代码更易维护。