1.4 java面试题 ,Redis 必问三大块,哨兵,集群
从 Java 面试之王的角度,Redis 必问三大块:数据结构与使用场景、缓存三大问题及解决方案、分布式锁与高可用。我会把这些拆成能直接背的干货,配上代码和血泪教训。
一、五种基本数据结构 + 选型心法
面试官最爱问“你用 Redis 做过什么”,你必须把数据结构和业务场景对应起来。
| 结构 | 核心能力 | 典型场景 | 注意事项 |
|---|---|---|---|
| String | 缓存 KV、计数器 | 短信验证码、分布式锁 value | 单个最大 512MB,数字可 incr |
| Hash | 存对象字段 | 用户信息、购物车 | 比 String(json) 更省内存,可部分更新 |
| List | 有序队列、最新 N 条 | 消息队列(简单)、最新评论 | 阻塞弹出可做简易消费者 |
| Set | 去重、交并差集 | 标签、共同好友、抽奖 | 支持随机弹出 spop |
| Sorted Set | 带权重的排序集合 | 排行榜、延迟队列 | zrangebyscore 做分页 |
血泪教训:别用 HGETALL 扫描大 Hash,拆成小 Hash 或改用 String + JSON 分页。
二、缓存三兄弟:穿透、击穿、雪崩(Java 侧组合拳)
1. 缓存穿透(查不存在的数据)
现象:恶意请求 id=-1 直接打到 DB。
方案:
- 布隆过滤器:用 Redisson 的
RBloomFilter,先判断是否存在。RBloomFilter<Long> filter = redisson.getBloomFilter("userFilter"); filter.tryInit(1000000L, 0.03); filter.add(userId); // 查询前 if (!filter.contains(userId)) return null; - 缓存空值:null 也缓存,过期时间设短(如 60s)。
2. 缓存击穿(热点 key 过期)
现象:某热门商品过期瞬间,大量请求直冲 DB。
方案:
- 互斥锁:拿不到锁的线程等待或返回旧值。
public String getProduct(String id) { String value = redis.get(id); if (value == null) { // 只让一个线程去查 DB if (redis.setnx("lock:" + id, "1", 10, TimeUnit.SECONDS)) { try { value = db.query(id); redis.set(id, value, 30, TimeUnit.MINUTES); } finally { redis.del("lock:" + id); } } else { Thread.sleep(50); return getProduct(id); // 重试 } } return value; } - 永不过期:把过期时间存 value 里,代码判断逻辑过期,异步刷新。
3. 缓存雪崩(大批 key 同时过期或 Redis 宕机)
方案:
- 过期时间加随机尾数:
expire = 3600 + random.nextInt(300)。 - 多级缓存:本地 Caffeine + Redis,Redis 挂时降级到本地。
- 集群高可用:哨兵 / Cluster,配合熔断(如 Resilience4j)。
三、分布式锁(王者必考)
Redisson 的正确打开方式
RLock lock = redisson.getLock("order:pay:" + orderId);
try {
// 尝试加锁,30s 过期,看门狗自动续期
if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
// 执行业务
}
} finally {
// 保证只有锁的持有者才能释放
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
老手考点:
- 不手写 SETNX+过期:原子性无法保证,用 Redisson 内置 Lua 脚本。
- 看门狗机制:锁持有期间自动续期,防止业务超时锁被释放。
- 红锁 RedLock:强一致性场景才用,大部分主从哨兵已经足够。
四、持久化:RDB vs AOF
- RDB:快照,fork 子进程写磁盘,恢复快,但可能丢最后几分钟数据。
- AOF:追加写命令,最多丢 1s 数据,文件大,恢复慢。
- 生产配置:
混合持久化(4.0+)save 900 1 save 300 10 appendonly yes appendfsync everysecaof-use-rdb-preamble yes兼得两者优点。
五、高可用与集群
- 主从:读写分离,无自动故障转移。
- 哨兵:监控、自动选主、通知客户端。
- Cluster:分片存储,16384 个 slot,CRC16(key) % 16384 路由,支持横向扩展。
Java 客户端适配:
- Jedis 直连要处理主从切换。
- Lettuce 是线程安全管道,天然适配 Cluster 和 Sentinel。
- Redisson 封装了分布式对象和锁,与 Spring 集成友好。
六、Spring Boot 集成与缓存注解
@Cacheable(value = "user", key = "#id", unless = "#result == null")
public User getUser(Long id) { ... }
@CachePut(value = "user", key = "#user.id")
public User update(User user) { ... }
@CacheEvict(value = "user", key = "#id")
public void delete(Long id) { ... }
必须自定义配置:
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues();
return RedisCacheManager.builder(factory).cacheDefaults(config).build();
}
七、面试话术模板
当面试官让你聊聊 Redis,你可以这样串起来:
“我用 Redis 主要做三件事:缓存、分布式锁和高并发计数。缓存上,用布隆过滤器+空值解决穿透,加互斥锁解决击穿,随机过期时间+集群预防雪崩。分布式锁用 Redisson 看门狗自动续期,避免死锁。Redis 我部署的是 Cluster 模式,用 Lettuce 客户端连接。持久化选混合模式,AOF 每秒刷盘,RDB 做备份。另外,还会用 Sorted Set 做排行榜,用 List 做消息队列,Spring Cache 注解简化缓存代码。”
这样回答,既展示了广度,又有生产细节和填坑经验,面试官自然会觉得你是真正带兵打过仗的。
有哪个点需要细挖,告诉我,我继续用代码砸透。
好的,这次我们聚焦在 Redis 在金融场景下的一个典型难点,我会给你一个既可用于面试、又能真正学到知识的完整分析,并附上代码案例。
困难:Redis 分布式锁在银行转账场景下,因主从切换导致锁丢失,引发资金重复扣款
1. 背景
在贷款放款或存款转账流程中,我们使用 Redis 分布式锁来防止并发操作同一笔订单。最初使用了简单的 SETNX + EXPIRE 方案,后来升级为 Redisson 的 RLock(看门狗自动续期)。但在一次生产事故中,Redis 主节点突然宕机,哨兵进行主从切换,此时一把锁恰好在主节点上尚未同步到新主,导致新主认为锁不存在,另一个线程成功获取了同一把锁,造成同一笔资金被重复扣款。
2. 问题定位
- 现象:对账系统发现一笔转账产生了两次扣款,间隔仅 200ms。
- 日志分析:两个线程几乎同时获取了
lock:transfer:ORDER123锁。 - Redis 监控:在时间窗口内,主节点发生了一次故障转移,且该锁的 key 在新主上不存在。
- 根因:Redisson 默认使用异步复制,
SETNX成功后,数据可能还未同步到从节点,主宕机后锁信息丢失。
3. 初期方案的代码(存在隐患)
// 隐患方案:普通 Redisson 锁在主从切换时可能丢失
RLock lock = redissonClient.getLock("lock:transfer:" + orderId);
try {
if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
// 执行转账
}
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
4. 解决方案选型与落地
我们对比了三种方案:
| 方案 | 原理 | 优势 | 劣势 | 结论 |
|---|---|---|---|---|
| Redisson 红锁 (RedLock) | 多主 Redis 半数以上加锁成功 | 高可靠 | 成本高、运维复杂、性能下降 | 备选 |
| Zookeeper 锁 | 临时顺序节点 + Watcher | 强一致 | 性能差、不适合高并发 | 不适合我们 |
| Redis 主从 + 业务幂等兜底 | 接受锁丢失,业务侧强幂等 | 性能高、成本低 | 必须有幂等机制 | 采纳 |
最终我们选择了保留 Redisson 普通锁 + 数据库唯一索引强制幂等的组合方案,并加强监控。
4.1 业务层:强制幂等(核心防线)
在转账表中增加一个唯一约束字段 idempotent_key:
ALTER TABLE transfer_record ADD UNIQUE KEY uk_idempotent (idempotent_key);
Java 代码:
@Transactional
public void transfer(String idempotentKey, TransferDTO dto) {
// 1. 尝试插入幂等记录
try {
transferMapper.insertIdempotentRecord(idempotentKey, dto);
} catch (DuplicateKeyException e) {
// 已处理过,直接返回
log.warn("重复请求被幂等拦截: {}", idempotentKey);
return;
}
// 2. 执行真正的转账逻辑
doTransfer(dto);
}
这样即使锁丢失,两个线程同时进入,数据库唯一约束也会阻止第二次扣款。
4.2 锁升级:红锁(高安全场景备用)
对于涉及大额资金的交易,我们单独使用了 Redisson 的 RedLock:
@Bean
public RedissonRedLock redLock(RedissonClient client1,
RedissonClient client2,
RedissonClient client3) {
RLock lock1 = client1.getLock("lock:transfer:" + orderId);
RLock lock2 = client2.getLock("lock:transfer:" + orderId);
RLock lock3 = client3.getLock("lock:transfer:" + orderId);
return new RedissonRedLock(lock1, lock2, lock3);
}
// 使用
RedissonRedLock redLock = redLockProvider.getLock(orderId);
try {
if (redLock.tryLock(5, 30, TimeUnit.SECONDS)) {
// 执行高价值转账
}
} finally {
redLock.unlock();
}
红锁要求在所有独立 Redis 实例中多数加锁成功才算成功,任一主宕机不影响总体。
4.3 监控与兜底
- 锁持有时间监控:当锁持有超过 500ms 时触发告警。
- 资金对账脚本:每小时对比银行核心系统和 Redis 锁日志,发现异常加锁记录自动报警。
5. 收获
- 深刻理解了 Redis 分布式锁的局限性:它适用于高性能、可容忍极小概率不一致的场景,不能替代业务层的幂等设计。
- 学会了分层防御:锁是第一层,业务幂等是第二层,对账是第三层。
- 掌握了 RedLock 的实现原理和使用限制,知道其在强一致场景下的价值。
- 推动了团队制定分布式锁使用规范:所有涉及资金的分布式锁,必须有幂等兜底方案。
6. 可运行的完整 Demo(模拟幂等拦截)
@Service
public class TransferService {
@Autowired
private RedissonClient redissonClient;
@Autowired
private TransferMapper transferMapper;
public String doTransfer(String orderId, BigDecimal amount) {
String idempotentKey = "transfer:" + orderId;
RLock lock = redissonClient.getLock("lock:" + idempotentKey);
try {
if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
// 1. 幂等检查
if (transferMapper.existsByIdempotentKey(idempotentKey)) {
return "重复请求,已忽略";
}
// 2. 插入幂等键 (DB唯一约束)
transferMapper.insertIdempotent(idempotentKey, orderId, amount);
// 3. 执行扣款
accountMapper.debit(amount);
return "转账成功";
}
return "系统繁忙";
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
在这个代码里,即使极端情况下锁被两个线程同时获取,数据库的 INSERT 也会因为唯一约束而阻止重复扣款,从而保证资金安全。
好的,这次我们聚焦在缓存读写策略和Redis生产配置两个关键方向上。这两块是面试中衡量你是否真正在生产环境用过 Redis 的重要标尺。
一、利用缓存提升读写效率的核心策略
缓存不是简单的“查不到就写进去”,老练的 Java 工程师要根据数据特性选择不同策略。
1. Cache Aside(旁路缓存,最常用)
思想:应用程序直接与缓存和数据库交互,缓存不负责写入数据库。
读:先查缓存 → 命中返回,未命中查 DB → 写入缓存并返回
写:先更新 DB → 删除或更新缓存
适用场景:读多写少,如客户信息、利率表、产品配置。
public Product getProduct(String id) {
// 1. 查缓存
String cacheKey = "product:" + id;
Product product = (Product) redisTemplate.opsForValue().get(cacheKey);
if (product != null) {
return product;
}
// 2. 查数据库(加分布式锁防止缓存击穿)
RLock lock = redissonClient.getLock("lock:product:" + id);
try {
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
// 双重检查
product = (Product) redisTemplate.opsForValue().get(cacheKey);
if (product != null) return product;
product = productMapper.selectById(id);
if (product != null) {
// 3. 写入缓存,加随机过期时间防雪崩
redisTemplate.opsForValue().set(cacheKey, product,
30 + ThreadLocalRandom.current().nextInt(10), TimeUnit.MINUTES);
}
return product;
}
} finally {
lock.unlock();
}
return null;
}
2. Read/Write Through(读写穿透)
思想:应用程序只与缓存交互,缓存组件负责同步数据库。
读:查缓存,缓存未命中时由缓存组件加载 DB 数据
写:写缓存,由缓存组件同步写入 DB
适用场景:缓存层与 DB 强绑定,如使用 Redis 内置模块或第三方缓存框架。
Java 实现:Spring Cache 抽象层配合 CacheLoader 实现。
@Cacheable(value = "user", key = "#id", unless = "#result == null")
public User getUser(Long id) {
return userMapper.selectById(id);
}
@CachePut(value = "user", key = "#user.id")
public User update(User user) {
userMapper.updateById(user);
return user;
}
3. Write Behind(异步回写)
思想:写入时只写缓存,异步批量写入 DB。
适用场景:写频率极高、允许短暂数据丢失的场景(如用户行为日志、计数器)。金融核心业务不用。
// 计数器示例:用 Redis 缓存计数,定时刷到 DB
public void incrementPageView(String pageId) {
redisTemplate.opsForValue().increment("pv:" + pageId);
}
@Scheduled(fixedDelay = 5000)
public void syncToDatabase() {
Set<String> keys = redisTemplate.keys("pv:*");
for (String key : keys) {
Long count = Long.parseLong((String) redisTemplate.opsForValue().get(key));
pageViewMapper.updateCount(key, count);
}
}
4. 多级缓存架构(银行高性能场景常用)
请求 → 本地缓存(Caffeine,10s过期) → Redis(5min过期) → DB
@Cacheable(value = "localCache", cacheManager = "caffeineCacheManager")
public RateTable getRateTable(String type) {
// 先从 Caffeine 一级缓存取,取不到进方法体
RateTable rate = redisTemplate.opsForValue().get("rate:" + type);
if (rate != null) return rate;
rate = rateMapper.selectByType(type);
if (rate != null) {
redisTemplate.opsForValue().set("rate:" + type, rate, 5, TimeUnit.MINUTES);
}
return rate;
}
优势:热点数据直接内存返回(纳秒级),Redis 兜底,DB 最终防线。
二、银行场景下的缓存使用规范
- 写入后删除缓存还是更新缓存?
删除。直接删除缓存,下次读时再加载,避免并发写导致缓存与 DB 数据不一致。 - 双删策略:
写 DB 前先删缓存 → 写 DB → 延迟再删一次(防止并发读写入旧数据)。 - Canal 订阅 Binlog 刷新缓存:
DB 变更后,Canal 推送消息到 MQ,消费者刷新 Redis,确保缓存与 DB 最终一致。 - 金融数据禁用 Write Behind:
任何可能丢失写入的策略都不适用于资金交易。
三、项目中 Redis 参数配置(生产级)
1. Redis 服务器核心配置 (redis.conf)
# 内存管理
maxmemory 4gb # 最大内存(根据服务器 70% 原则)
maxmemory-policy allkeys-lru # 淘汰策略:LRU 驱逐(缓存场景)
# maxmemory-policy noeviction # 银行核心业务可选:不驱逐,满时写失败
# 持久化
save 900 1 # 15分钟内至少1次修改触发 RDB
save 300 10 # 5分钟内至少10次
save 60 10000 # 1分钟内至少1万次
appendonly yes # 开启 AOF
appendfsync everysec # 每秒刷盘,性能与安全平衡
auto-aof-rewrite-percentage 100 # AOF 文件增长100%时触发重写
auto-aof-rewrite-min-size 64mb
# 连接与超时
timeout 300 # 客户端空闲5分钟断开
tcp-keepalive 300 # TCP 探活
maxclients 10000 # 最大客户端连接数
# 慢查询日志
slowlog-log-slower-than 10000 # 超过 10ms 记录
slowlog-max-len 128 # 保留 128 条
# 安全(生产必须)
requirepass your_strong_password
rename-command FLUSHDB ""
rename-command FLUSHALL ""
rename-command KEYS ""
2. Java 客户端配置(Lettuce + Spring Boot)
spring:
redis:
host: redis-bank-core.example.com
port: 6379
password: ${REDIS_PASSWORD} # 从环境变量取,不能硬编码
timeout: 5000ms
lettuce:
pool:
max-active: 20 # 最大活跃连接(根据并发量调整)
max-idle: 10 # 最大空闲连接
min-idle: 5 # 最小空闲连接
max-wait: 3000ms # 获取连接最大等待时间
time-between-eviction-runs: 60s # 空闲连接回收周期
cluster:
# 如果使用集群模式
nodes:
- 10.0.1.1:6379
- 10.0.1.2:6379
- 10.0.1.3:6379
max-redirects: 3
3. 自定义 RedisTemplate 配置(银行金融数据序列化)
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// Key 用 String 序列化,可读性好
StringRedisSerializer stringSerializer = new StringRedisSerializer();
template.setKeySerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);
// Value 用 JSON 序列化(金融数据不用 JDK 序列化,不安全且不可读)
Jackson2JsonRedisSerializer<Object> jsonSerializer =
new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// 金融数据精度保证
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
jsonSerializer.setObjectMapper(mapper);
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
}
4. 缓存管理器配置(序列化 + 过期时间统一管理)
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues(); // 不缓存 null,防穿透需用布隆过滤器
// 不同业务不同过期时间
Map<String, RedisCacheConfiguration> configs = new HashMap<>();
configs.put("user", defaultConfig.entryTtl(Duration.ofMinutes(10)));
configs.put("rate", defaultConfig.entryTtl(Duration.ofHours(24)));
configs.put("product", defaultConfig.entryTtl(Duration.ofMinutes(5)));
return RedisCacheManager.builder(factory)
.cacheDefaults(defaultConfig)
.withInitialCacheConfigurations(configs)
.build();
}
四、生产监控与常见问题
1. 关键监控指标
| 指标 | 命令 | 告警阈值 |
|---|---|---|
| 内存使用率 | INFO memory |
> 80% |
| 命中率 | INFO stats (keyspace_hits/keyspace_misses) |
< 90% |
| 连接数 | INFO clients |
> 80% maxclients |
| 慢查询 | SLOWLOG GET 10 |
> 10ms 需关注 |
| 主从延迟 | INFO replication (lag) |
> 1s |
2. 常见问题排查
- 热 key 问题:用
redis-cli --hotkeys扫描,对热 key 加随机前缀打散。 - 大 key 问题:用
redis-cli --bigkeys扫描,单个 key > 10MB 建议拆分。(对应的value占用的内存过大 value>10kb,或者元素过多
)
- 缓存穿透:使用布隆过滤器(Redisson
RBloomFilter)。 - 缓存雪崩:过期时间加随机尾数,多级缓存,集群高可用。
五、面试话术模板
“缓存提升读写效率,我主要用 Cache Aside 模式,写 DB 后删除缓存,读时先查缓存未命中再查 DB 并回种。金融数据我会特别加上分布式锁防击穿,过期时间加随机尾数防雪崩。
Redis 参数配置上,内存管理设 4GB 并开启
allkeys-lru或noeviction;持久化开启 AOFeverysec保证数据安全;客户端用 Lettuce 连接池,设置合理的max-active;序列化用 JSON 而非 JDK。生产上我会通过
SLOWLOG和INFO监控慢查询和命中率,用布隆过滤器防穿透,并在涉及资金的场景中,始终以数据库为准,Redis 仅作性能加速,不替代事务保障。”
这样既展现了缓存策略的深度,又体现了生产配置的实战经验,面试官会认为你对 Redis 是真操盘过的。
在 Redis 的高可用方案中,主从复制、哨兵模式和集群模式是三个层次递进的方案。面试官问这个,是想考察你能否根据不同的业务场景(特别是金融场景)选择正确的架构,并清楚它们的原理、缺陷和演进路线。
下面我从架构、原理、优缺点、故障转移、银行选型等维度,给出一个清晰的对比。
一、主从复制模式(Master-Slave)
架构
- 一主(Master)多从(Slave)。
- 主负责读写,从负责读(分担读压力)并异步复制主的数据。
- 没有自动故障转移,主宕机后需要手动将从提升为主。
原理
- 从节点启动后发送
PSYNC命令,主节点生成 RDB 快照 + 增量缓冲区传给从节点。 - 正常运行期间,主将写命令异步发送给所有从节点。
优点
- 简单:容易搭建和配置。
- 读写分离:提升读并发能力。
- 数据冗余:从节点可做备份。
缺点
- 无自动故障转移:主宕机后不可写,需人工介入。
- 主从延迟:异步复制,从节点可能读不到最新数据(金融场景敏感)。
- 单点写入:写能力受限于主节点。
适用场景
- 读多写少的非核心业务,或作为备份使用。
二、哨兵模式(Sentinel)
架构
- 在主从基础上,增加一组 Sentinel 进程(通常 3 个或 5 个)。
- Sentinel 负责监控、自动故障转移、通知客户端。
原理
- Sentinel 定期向主从节点发送心跳(PING)。
- 如果 Sentinel 集群主观认为主节点下线(SDOWN),会进行投票,达到法定票数(quorum)后判定客观下线(ODOWN)。
- 选举一个 Sentinel 作为领导者,执行故障转移:提升一个从节点为新主,并通知其他从节点复制新主。
- 客户端通过 Sentinel API 获取当前主节点地址,不需要硬编码 IP。
优点
- 自动故障转移:主宕机后秒级切换,保证高可用。
- 兼容主从:对现有主从架构侵入小。
- 读写分离:依然保留主从读取能力。
缺点
- 仍然是单主写入:写性能有上限,无法水平扩展。
- 主从切换期间短暂不可写入。
- 异步复制可能丢失少量数据:主宕机瞬间,未同步到从的数据会丢失。
- 运维复杂度中等:需要维护 Sentinel 集群。
适用场景
- 单实例内存不足以支撑全量数据,但数据量在 10~20GB 以内,对高可用有要求。
- 绝大多数中小型业务的 Redis 高可用方案。
三、集群模式(Cluster)
架构
- 去中心化:多个主节点,每个主节点负责一部分 哈希槽(slot,0~16383)。
- 每个主节点可配多个从节点,从节点复制主节点数据,故障时提升为主。
- 客户端可连接任一节点,通过
MOVED/ASK重定向找到正确节点。
原理
- 数据通过
CRC16(key) % 16384映射到 slot,每个节点管理一段 slot。 - 节点之间通过 Gossip 协议 交换集群信息,包括成员变化、slot 分配等。
- 故障转移:当某主节点宕机,其从节点通过集群内的选举算法自动提升为新主(基于 epoch 和投票)。
- 可在线扩容:新增节点后,通过
reshard重新分配 slot。
优点
- 水平扩展:增加节点即可提升总容量和吞吐量。
- 自动故障转移:主节点宕机后从节点自动接管。
- 高可用 + 高性能:无中心瓶颈,多主写入,并发性能极高。
缺点
- 客户端限制:不支持多数据库(只有 DB0),不支持多 key 跨 slot 操作(除非用 hash tag)。
- 运维复杂:需要管理多个节点,reshard 过程需谨慎。
- 数据一致性弱于单独主从:主从异步复制,主宕机时可能丢少量数据(可通过
wait命令缓解)。
适用场景
- 数据量巨大(> 几十 GB),或写入 TPS 极高(十万级)。
- 需要水平扩展的在线核心业务。
四、三种方案对比表
| 维度 | 主从复制 | 哨兵模式 | 集群模式 |
|---|---|---|---|
| 高可用 | 无自动切换 | 自动故障转移 | 自动故障转移 |
| 扩展性 | 读扩展,写不可扩 | 读扩展,写不可扩 | 读写均可水平扩展 |
| 数据一致性 | 异步复制,从节点有延迟 | 异步复制,切换时可能丢少量数据 | 异步复制,切换时可能丢少量数据 |
| 故障转移速度 | 人工分钟级 | 秒级自动 | 秒级自动 |
| 客户端连接 | 需手动切换 | Sentinel 提供动态发现 | 节点 redirect,智能客户端 |
| 运维复杂度 | 低 | 中 | 高 |
| 内存/性能上限 | 单机 | 单机(写) | 分布式,可扩充 |
| 典型场景 | 备份、读多写少 | 中小规模业务 | 大型业务、高并发 |
五、银行核心系统的选型实践
1. 核心交易缓存(如账户余额、额度)
方案:哨兵模式
- 原因:数据量不大(通常几十 GB 内),但要求高可用和自动切换。集群模式增加复杂度,哨兵足够。
- 配置:3 节点 Sentinel,1 主 2 从,
appendfsync everysec,混合持久化。
2. 实时风控/日志处理(高吞吐、大数据量)
方案:集群模式
- 原因:交易流水、行为日志数据量数百 GB,写入 TPS 高,集群提供水平扩展和多主写入。
- 配置:6 节点集群(3 主 3 从),开启 AOF,定期清理过期数据。
3. 产品配置、利率表(静态数据,高可用读)
方案:主从 + 本地缓存
- 原因:数据几乎不变化,主从提供冗余读,本地 Caffeine 缓存进一步减少 Redis 压力。
- 自动故障切不敏感,因为本地缓存可短时兜底。
4. 混合模式
在实际项目中,我们通常会混合使用:
- 核心资金:哨兵模式(或集群),保证高可用。
- 非核心查询:主从 + 哨兵。
- 跨数据中心灾备:两个独立的哨兵集群,通过同步工具做数据最终一致。
六、面试模板话术
“Redis 的高可用我经历过三个阶段的演进。主从复制主要做读写分离和备份,但没有自动故障转移,金融系统绝不用在主库。
哨兵模式增加了监控和自动切换,是单主架构下最成熟的高可用方案,我们核心交易缓存都用哨兵,一主两从 + 三哨兵,保证主宕机后 10 秒内完成切换。
集群模式解决了水平扩展和多主写入问题,适用于日志、风控等大数据量场景,但运维复杂,不支持跨 slot 事务。集群模式下主节点故障也是秒级自动提升从节点。
选择上,单机内存够用且核心业务用哨兵,大数据量高吞吐用集群,配合本地缓存和多级容灾,保证金融系统稳定运行。”
这样回答,既讲清了原理,又给出了针对不同业务量的选型依据,充分体现了你的架构实践经验。
Redis Cluster 是 Redis 官方提供的分布式运行方案,它解决了单机 Redis 在内存容量、吞吐量和故障恢复上的上限问题。相比主从和哨兵,它不再只有一个主节点负责全部写入,而是通过分片实现了真正的水平扩展。
下面从原理、架构、数据分片、高可用、客户端交互、限制以及银行实践等维度,给你一个老练的答案。
一、Redis Cluster 解决了什么问题?
- 容量瓶颈:单机内存有限(通常 32GB 已很大),集群可存 TB 级数据。
- 写入瓶颈:单机 QPS 有上限,集群可线性提升写入吞吐。
- 高可用:无需额外哨兵组件,集群内置主从切换,任意主节点宕机,其从节点自动接管。
二、数据分片:哈希槽(Hash Slot)
这是 Redis Cluster 最核心的设计。
整个集群划分为 16384 个哈希槽(slot),每个主节点负责一部分 slot。
key 到 slot 的映射:
slot = CRC16(key) & 16383
CRC16计算出 16 位校验和,再对 16384 取余。- Hash Tag:如果 key 中包含
{},则只用{}里的部分计算 slot,从而强制多个相关 key 落入同一 slot,以支持批量操作。
优点:
- 迁移 slot 即可迁移数据,灵活扩缩容。
- 去中心化:任何节点都知道“每个 slot 由谁负责”,无需中央协调者。
三、集群架构与去中心化通信
- 去中心化:每个节点都维护了集群的完整拓扑(所有节点、slot 映射),通过 Gossip 协议 在节点间交换信息。
- Gossip 协议:节点随机选择其他节点交换状态,最终收敛,分布式容错性好。
- 配置信息:节点启动时,通过
cluster-config-file nodes.conf记录集群信息,重启后复用。
端口:默认服务端口为 6379,集群总线端口为 服务端口+10000(如 16379),用于节点间通信,必须开放。
四、客户端交互:MOVED 与 ASK 重定向
- 客户端连接任意一个节点。
- 如果请求的 key 对应的 slot 不在此节点,节点返回 MOVED 错误,包含正确的节点地址。
- 客户端应向新地址重新发送请求。
智能客户端(如 Lettuce、JedisCluster)会缓存 slot 映射表,避免每次重定向。 - 当 slot 迁移进行中时,目标节点返回 ASK 临时重定向,客户端仅本次重定向,不更新缓存。
Java 示例:
spring:
redis:
cluster:
nodes: 10.0.0.1:6379,10.0.0.2:6379,10.0.0.3:6379
max-redirects: 3
使用 LettuceConnectionFactory 自动适配集群,无需代码改动。
五、高可用与故障转移(内置)
每个主节点可配置 1 个或多个从节点,从节点异步复制主节点数据。
故障转移流程:
- 集群中的其他主节点通过 Gossip 感知到某主节点不可达(PFAIL → FAIL)。
- 该主节点的从节点发起选举,获得集群中过半数主节点投票后,提升为新主节点。
- 原主节点重启后自动成为新主的从节点。
与哨兵区别:哨兵需要额外进程,集群自身节点完成选举,更简洁。
配置关键点:
- 必须为每个主节点设置从节点,否则主宕机后对应 slot 不可用。
- 如果设置
cluster-require-full-coverage yes(默认),任一 slot 不可用时集群停止服务。在生产中,根据业务容忍度可设为no,部分 slot 异常时仍接受部分请求。
六、集群的限制与注意事项
- 多数据库不支持:只有一个 DB0,
SELECT命令被禁用。 - 多 key 操作限制:若不使用 Hash Tag,不同 key 可能分布在不同节点,无法直接操作
MGET、MSET、跨 slot 事务、Lua 脚本等。 - 事务不支持跨 slot:
MULTI/EXEC只能操作同一 slot 的 key。 - 主从异步复制:与主从模式类似,主节点宕机可能丢失尚未同步的数据。可通过
min-replicas-to-write和wait命令缓解。
七、与哨兵模式的对比
| 维度 | Redis Cluster | Redis Sentinel |
|---|---|---|
| 数据分布 | 分片存储(多个主节点) | 单主节点存储全部数据 |
| 写入扩展 | 支持水平扩展 | 有限单机写入 |
| 高可用 | 内置主从切换 | 依赖 Sentinel 进程 |
| 运维复杂度 | 较高,需处理 slot 迁移 | 中等 |
| 典型数据量 | 数百 GB 以上 | 几十 GB 以内 |
八、银行核心系统实践
- 实时风控与日志:数据量巨大(数百 GB),写入 QPS 高,采用 Redis Cluster,6 节点(3 主 3 从),跨机房部署。
- 核心交易缓存(余额、额度):数据量不大但强一致性要求高,仍使用 哨兵模式,避免跨 slot 操作限制和 Gossip 通信的复杂性。
- 扩容策略:业务增长时,在线
reshard,将部分 slot 迁移至新节点,秒级完成槽分配,数据迁移后台进行,影响极小。 - 监控:关键指标包括
cluster_state(必须为 1)、cluster_slots_ok、迁移过程中的migrating数量。使用redis-cli --cluster check定期体检。 - Hash Tag:在必须跨 key 操作的场景(如批量更新同一客户的多条记录),使用
{customerId}让这些 key 落入同一 slot。
九、面试模板话术
“Redis Cluster 我理解核心是基于 16384 个哈希槽的数据分片,每个主节点管理一部分 slot,通过 Gossip 协议实现去中心化通信。客户端使用智能客户端缓存 slot 映射,遇到 MOVED 重定向自动切换节点。
高可用方面,Cluster 内置主从切换,当主节点宕机,从节点向其他主节点投票后自动提升,无需额外哨兵。
我在银行项目中对日志和风控等大数据量场景使用 Cluster,通过在线
reshard实现平滑扩容。而对核心资金缓存则使用哨兵模式,因为数据量小,且需要避免跨 slot 限制带来的复杂度。踩过的坑包括多 key 操作失败、迁移时 ASK 重定向处理不当,后来规范了 Hash Tag 使用,并升级客户端到最新版本。监控上重点关注
cluster_state和 slot 健康度,确保集群稳定。”
这样既讲清了原理,又通过对比、限制和实践,展示了你对 Redis Cluster 的全面掌控。
Redis Cluster 是分布式方案,它在解决容量和性能问题的同时,也带来了诸多限制。作为一名在银行核心系统摸爬滚打过的 Java 工程师,我对这些限制的判断标准只有一个:会不会导致资金风险,以及对业务开发有多大的侵入性。
下面我按功能、一致性、运维三个维度,把 Cluster 的限制讲透,并给出我们在实际项目中的应对或规避方案。
一、Cluster 功能限制(最直接影响代码编写)
1. 不支持跨 Slot 的多 Key 操作
这是 Cluster 最核心的限制。所有多 key 命令(如 MGET、MSET、DEL、RENAME 等)都要求所有 key 落在同一个 slot 内,否则直接报错 CROSSSLOT Keys in request don't hash to the same slot。
- 事务(MULTI/EXEC) 也必须在一个 slot 内,无法实现跨分片的原子操作。
- Lua 脚本同样如此,
EVAL执行时所有操作的 key 必须属于同一 slot。
银行场景痛点:
在批量冻结一批账户余额时,我们原本想用 MSET 一次性更新多个账户的缓存,但 Cluster 下这些账户对应的 key 可能分散在不同节点,直接报错。最终改用 Hash Tag(如 {freeze:20240101}account:1001)强制让同批操作的 key 落入同一 slot,但这样又引入了热点问题。
应对策略:
- 设计 key 时主动使用 Hash Tag,但需评估热点风险。
- 拆解多 key 操作为多个单 key 命令(性能下降,需用 Pipeline 优化,但仍不能跨节点)。
- 对强原子性要求的场景,根本不用 Redis,而是用数据库事务或分布式事务(如 Seata TCC)兜底。
2. 不支持多数据库(DB)
Redis Cluster 只有 DB0,SELECT 命令被禁用。
影响:
以前在单机 Redis 中,我们习惯用不同 DB 隔离业务(如 DB0 缓存、DB1 队列)。迁移到 Cluster 后,必须通过 key 前缀 或 不同集群 来隔离,改造成本大。
3. 部分命令受限或行为变化
- RENAME:如果旧 key 和新 key 不在同一 slot,直接报错。
- SORT、KEYS、SCAN 等:仅作用于当前连接的节点,无法全局扫描。
- Pub/Sub:消息只会发布在当前节点,不会广播到整个集群(需客户端连接所有节点订阅)。
银行实践:
我们曾用 KEYS 扫描所有会话 key 做批量失效,Cluster 下只能遍历所有节点分别执行 SCAN 再聚合,代码复杂度增加。
二、一致性与数据安全限制(金融系统最敏感)
1. 主从异步复制导致数据丢失风险
Cluster 的从节点复制依然是异步的。如果主节点宕机且没有启用 min-replicas-to-write,最后一段未同步的数据会永久丢失。
资金场景:
假设一笔转账的缓存(如账户余额)在主节点写入成功,但还没来得及同步到从节点,主节点即刻宕机。新选举的主节点没有这条记录,业务系统以为余额未变,可能导致重复扣款。
应对:
- 设置
min-replicas-to-write和min-replicas-max-lag,强制至少一个从节点同步成功才返回,但会降低性能和可用性。 - 核心金融数据最终一致性不以 Redis 为准,永远用数据库 + 事务做权威源,Redis 仅做可丢失的查询加速。
2. 网络分区下的脑裂风险
当集群发生网络分区,少数派分区可能无法完成主节点选举,导致对应 slot 不可用。如果配置 cluster-require-full-coverage yes(默认),整个集群会拒绝所有请求,影响范围极大。
我们的配置:
对于非交易的关键服务(如用户行为日志),我们设为 no,保证部分可用;对交易核心,宁可整个集群停止,也不允许部分 slot 服务,避免不一致。
三、运维与扩展性限制
1. Reshard 迁移影响性能
扩容或缩容时,需要在线将部分 slot 及其数据从一个节点迁移到另一个节点。迁移过程中,访问迁移中的 key 会触发 ASK 重定向,且迁移本身消耗网络和 CPU,可能导致请求延迟抖动。
银行案例:
在核心交易时段,我们严禁执行 reshard。所有扩缩容操作放在凌晨低峰期,并通过监控观察迁移进度和客户端超时率。
2. 节点间 Gossip 通信的开销
每个节点都要维护整个集群的拓扑信息,定期通过 Gossip 协议交换状态。当集群规模很大(超过 100 节点)时,消息量巨大,可能影响正常请求的带宽和延迟。
3. 客户端必须智能支持
传统单机客户端(如老的 Jedis 直连)无法处理 MOVED/ASK 重定向。必须使用支持 Cluster 的客户端(Lettuce、JedisCluster 等)。但这意味着客户端侧必须缓存 slot 映射表,内存占用增加,且初始启动或拓扑变更时存在短暂的服务发现延迟。
4. 版本升级更复杂
集群版本升级需要考虑节点兼容性,通常需滚动重启,并确保 cluster-require-full-coverage 配置合理,否则重启期间可能导致集群停止服务。
四、面试时的总结话术
“Redis Cluster 的限制,我从三个层面来看。
功能上,跨 slot 操作被禁,多 DB 不支持,事务和 Lua 必须限定在同一 slot,这对我们银行批量冻结等业务冲击很大,我们通过 Hash Tag 和业务重构解决。
一致性上,异步复制会丢数据,这是金融场景的硬伤,所以我们绝不将 Redis 作为资金数据的最终权威源,它只做缓存,核心数据由数据库和 TCC 事务保障。
运维上,Reshard 会干扰在线业务,必须低峰执行;Gossip 通信和客户端重定向也需要额外处理。总之,Cluster 是性能利器,但数据强一致和跨分区操作不是它的强项,适合大规模、可容忍少量不一致的非资金场景,核心交易还是得靠哨兵或数据库。”
这样既把限制讲透,也展示了你在银行约束下如何权衡和避坑,正是面试官想听到的答案。
Redis Cluster 的设计原则是数据分散,但业务中总有多个 key 需要一起操作的需求。Hash Tag 就是官方给出的“打破规则”的方法,它强制让一组相关 key 落到同一个 slot,从而实现多 key 操作。
一、原理:如何改变 key 的 slot 计算?
默认情况下,slot 由整个 key 计算:
slot = CRC16(key) & 16383
而 Hash Tag 的规则是:如果 key 中包含 {},则只取 {} 内的子串来计算 slot。
{user:1001}:balance→ 只计算user:1001{user:1001}:orders→ 只计算user:1001- 于是这两个 key 必定落在同一个 slot,可安全执行
MGET、事务、Lua 等。
注意:
- 必须是第一个
{和第一个}之间的内容,不支持嵌套。 - 如果
{}内为空(如abc{}def),则整个 key 参与计算。 - 如果没有
{},整个 key 参与计算。
二、使用方式
在 Java 代码中,无需特殊客户端配置,只需在构造 key 时嵌入 Hash Tag。
String balanceKey = "{user:1001}:balance";
String ordersKey = "{user:1001}:orders";
redisTemplate.opsForValue().multiGet(Arrays.asList(balanceKey, ordersKey));
这样 multiGet 可以正常执行,否则会抛出 CROSSSLOT 异常。
常见模式:
- 按用户聚合:
{user:1001}:profile,{user:1001}:cart - 按订单聚合:
{order:2001}:items,{order:2001}:status - 按时间批次聚合:
{batch:20240101}:account:1001,{batch:20240101}:account:1002
三、Hash Tag 的优点
- 支持多 key 操作:事务、Lua、
MGET/MSET等均可跨多个键原子执行。 - 无需改架构:原生支持,无需额外中间件。
- 灵活控制分布:可以让紧密相关的数据本地化,减少跨节点网络开销。
四、Hash Tag 的缺点与风险(银行重点)
1. 热点问题(最致命)
如果大量数据集中在同一个 Hash Tag(例如超级客户 {vip:1}),这个 slot 所在的节点 QPS 和内存会暴增,失去水平扩展的意义。
银行案例:某对公大客户每日交易量极高,所有带 {corp:888}: 的 key 都涌入一个节点,导致该节点 CPU 飙高,最终触发限流。后来我们改为按子账户分散,用业务代码聚合结果。
2. 数据分布不均衡
Hash Tag 可能让某些 slot 特别“重”,而其他 slot 空闲。运维需监控每个 slot 的 key 数量和内存占用,必要时手动迁 slot 或重新设计 Tag。
3. 代码侵入性
所有涉及 key 的地方都必须规范使用 Tag,若新同事遗漏,可能导致线上跨 slot 报错。需通过 Code Review 和单元测试强制检查。
4. 可读性下降
{user:1001}:balance 比 user:1001:balance 多了括号,对维护不算友好。
5. 客户端缓存失效
Lettuce 等客户端会缓存 slot 映射,当集群拓扑变更(reshard)时,短暂期间可能出现 MOVED 重定向,但通常很快恢复。
五、银行生产实践中的权衡
在贷款放款流程中,一笔放款单涉及多个 key(额度、账户状态、放款记录)。我们使用 Hash Tag {loan:2001} 聚合这些 key,然后通过 Lua 脚本原子操作,保证额度扣减和状态变更的原子性。
优化策略:
- 细化粒度:不用大范围 Tag(如整个产品),只用业务单号级别,避免热点。
- 容量规划:评估单节点能承载的热点业务量,超出就拆分 Tag。
- 监控告警:对每个 slot 的 QPS 和内存进行监控,出现倾斜立即调整。
兜底方案:如果发现热点,临时降级为单 key 操作 + 数据库事务保证一致性,牺牲性能换取安全。
六、面试模板话术
“Redis Cluster 多 key 操作靠 Hash Tag 实现。它强制只计算
{}内子串的 slot,让相关 key 落在同一节点,从而支持MGET、事务和 Lua。
在我们银行放款流程里,我用{loan:2001}:聚合放款单相关的所有 key,通过 Lua 保证额度扣减的原子性。
但 Hash Tag 的最大风险是热点——如果 Tag 粒度太粗,某个大客户的所有 key 都进一个节点,会打垮那个分片。所以粒度要细(通常到单号),并持续监控 slot 分布。如果发现热点,就拆分 Tag 或者降级为单 key 操作加数据库事务兜底。
Hash Tag 是用可控的复杂性,换多 key 操作的原子性,在金融系统中使用务必保守且监控到位。”
这样回答,既把 Hash Tag 的原理和用法讲透,也体现了在金融约束下的谨慎和工程实践,符合你四年老手的定位。
Redis 的过期策略是保证内存有效利用和数据时效性的核心机制。面试官常问这个,是想看你是否理解它多层级配合的设计思路,以及是否能针对不同业务场景(尤其是金融场景)做出合理配置。
下面我从三大删除策略、内存淘汰策略、参数配置、银行实践四个维度给你一个透彻的回答。
一、三大过期键删除策略(协作配合)
Redis 不会只依赖单一策略,而是同时使用惰性删除和定期删除,来在 CPU 和内存之间取得平衡。
1. 惰性删除
- 原理:当客户端访问某个 key 时,Redis 会先检查该 key 是否已过期。如果过期,就立即删除,然后返回空。
- 优点:对 CPU 最友好,只在必要的时候才检查,不额外浪费资源。
- 缺点:如果过期 key 一直没被访问,它就会一直占着内存,成为“内存垃圾”。
- 源码实现:在
expireIfNeeded()函数中调用。
2. 定期删除
- 原理:Redis 默认每 100ms 会随机抽取部分设置了过期时间的 key,检查并删除其中已过期的 key。
- 流程:
- 随机从过期字典中取出 20 个 key。
- 删除其中已过期的 key。
- 如果删除的 key 超过 25%(即 5 个),则重复步骤 1(认为当前库过期 key 较多)。
- 为避免阻塞,每次定期删除有总执行时间上限(默认 25ms)。
- 优点:主动清理,减少内存浪费。
- 缺点:随机抽样可能导致一些过期 key 漏网。
3. 为什么不用定时删除?
定时删除(为每个 key 设一个定时器)虽然即时,但会创建大量定时器,严重消耗 CPU。Redis 基于单线程,必须避免这种开销。
4. 淘汰策略兜底
当内存达到 maxmemory 时,即使某些 key 还没过期,也会按内存淘汰策略强制删除,这是最后一道防线。
二、内存淘汰策略(maxmemory-policy)
当 Redis 内存使用达到 maxmemory 上限时,会根据配置的淘汰策略来处理新的写入请求。
| 策略 | 行为 |
|---|---|
| noeviction | 不淘汰,所有写请求返回错误(读正常) |
| allkeys-lru | 在所有 key 中,淘汰最近最少使用的 key |
| volatile-lru | 在设置了过期时间的 key 中,淘汰最近最少使用的 key |
| allkeys-lfu | 在所有 key 中,淘汰访问频率最低的 key |
| volatile-lfu | 在设置了过期时间的 key 中,淘汰访问频率最低的 key |
| allkeys-random | 在所有 key 中,随机淘汰 |
| volatile-random | 在设置了过期时间的 key 中,随机淘汰 |
| volatile-ttl | 在设置了过期时间的 key 中,淘汰剩余 TTL 最短的 key |
LRU 实现:Redis 的 LRU 是近似 LRU,随机采样 5 个 key(可配置),淘汰其中最旧的,避免了全量排序的性能开销。
LFU 实现:4.0 引入,通过 lfu-log-factor 等参数调整计数器增长速度。
银行选型建议:
- 核心交易数据(账户余额、额度):配置
noeviction,永远不允许因内存满而丢失关键数据。写操作失败后由上游业务通过 MQ 重试或直接拒绝。 - 普通缓存(产品列表、用户信息):配置
allkeys-lru,自动淘汰冷数据,保证内存使用高效。
三、关键配置参数
# 内存上限(根据服务器内存 70% 原则设置)
maxmemory 4gb
# 淘汰策略
maxmemory-policy allkeys-lru
# 定期删除频率(不建议调整)
hz 10 # 默认 10,执行定期删除等后台任务的频率
# 淘汰策略采样数量
maxmemory-samples 5 # LRU/LFU 采样数,越大越精确但消耗 CPU
四、银行场景中的特殊考量
1. 资金数据绝不淘汰
- 账户余额、冻结额度等关键数据,设置
maxmemory-policy noeviction,并且在应用层也加一层保护:写入前检查内存使用率,超过阈值提前告警并扩容,而不是等到写入失败。
2. 过期时间随机化防雪崩
- 大量 key 同时过期可能引发缓存雪崩。我们会在过期时间上加一个随机偏移:
redisTemplate.opsForValue().set(key, value, 60 + ThreadLocalRandom.current().nextInt(30), TimeUnit.SECONDS);
3. 配合业务双删
- 对于核心配置更新,我们会在更新数据库后立即删除缓存,并延迟几秒再次删除,防止在更新间隙读到旧值,也避免无效 key 积压。
4. 监控过期数据量
- 通过
INFO stats中的expired_keys指标监控过期删除速率。 - 如果
expired_keys长期为 0 但内存持续增长,说明定期删除未能清理垃圾,需检查hz参数或排查是否有大量 key 未设过期时间。
五、面试模板话术
“Redis 的过期策略是惰性删除 + 定期删除 + 内存淘汰三层保障。惰性删除在访问时清理,不额外消耗 CPU;定期删除每 100ms 随机抽检,循环删除过期 key,直到比例低于 25% 或超时 25ms,这是内存和 CPU 的折中。内存满时按
maxmemory-policy淘汰。在银行核心系统中,对资金数据我绝对不用 LRU,配置
noeviction防止数据丢失。对非核心缓存用allkeys-lru自动腾空间。我还通过过期时间加随机尾数防止雪崩,并监控expired_keys指标确保清理正常。”
这样回答既解释了原理,又带出了具体的配置和银行实践,能充分体现你的深度和工程思维。
Redis 的淘汰策略是内存管理的最后防线。它与过期删除策略不同——过期删除是删除已过期的 key,而淘汰策略是内存满时强制删除 key(即使没过期),为新写入腾出空间。
下面我从策略分类、LRU/LFU 原理、选择决策、银行实践四个维度,给你一个能体现老练工程师深度的回答。
一、什么时候触发淘汰?
当 Redis 内存使用达到 maxmemory 上限时,Redis 会根据配置的淘汰策略来删除一些 key,然后接受新的写入。如果策略是 noeviction(不淘汰),则直接返回错误。
相关命令:
maxmemory 4gb # 最大内存
maxmemory-policy allkeys-lru # 淘汰策略
maxmemory-samples 5 # LRU/LFU 采样数
二、8 种淘汰策略分类与含义
按作用域分两类
- volatile-xxx:只从设置了过期时间的 key 中淘汰。
- allkeys-xxx:从所有 key 中淘汰。
按淘汰算法分四类
| 策略 | 作用域 | 淘汰逻辑 |
|---|---|---|
| noeviction | 不淘汰 | 内存满后,所有写请求返回错误(READONLY),读请求正常 |
| allkeys-lru | 所有 key | 近似 LRU,淘汰最近最少访问的 key |
| volatile-lru | 有过期时间的 key | 近似 LRU,淘汰最近最少访问的 key |
| allkeys-lfu | 所有 key | 近似 LFU,淘汰访问频率最低的 key(4.0+) |
| volatile-lfu | 有过期时间的 key | 近似 LFU,淘汰访问频率最低的 key(4.0+) |
| allkeys-random | 所有 key | 随机淘汰 |
| volatile-random | 有过期时间的 key | 随机淘汰 |
| volatile-ttl | 有过期时间的 key | 淘汰剩余 TTL 最短的 key(越早过期越先删) |
快速记忆:
- 需要纯缓存,允许丢数据 →
allkeys-lru(最常用)。 - 既要缓存又要持久化数据(有特殊 key 不能丢)→
volatile-lru。 - 希望保留热门数据,淘汰低频的 →
allkeys-lfu或volatile-lfu。 - 数据访问模式随机,不在乎淘汰哪个 →
random系列。 - 绝对不能丢数据 →
noeviction。
三、LRU 和 LFU 的实现原理(面试核心)
Redis 的 LRU 和 LFU 都是近似算法,不是精确的全局淘汰。
1. 近似 LRU
- Redis 在每个对象中记录一个 24 位的
lru时钟字段,记录最后一次访问的时间戳。 - 当需要淘汰时,Redis 随机采样
maxmemory-samples个 key(默认 5 个),比较它们的lru时间戳,淘汰最旧的那一个。 - 为什么不精确? 因为 Redis 单线程,无法维护完整的 LRU 链表。
- 缺点::偶尔访问一次的会被保留,高频的反而会被淘汰。只是因为高频的比较早。
- 优势:性能极高,内存占用小。采样数越大,结果越接近精确 LRU,但 CPU 消耗也增加。
配置调优:maxmemory-samples 10 可提高准确性。
2. 近似 LFU(4.0+)
LFU 淘汰访问频率最低的 key,避免 LRU 偶尔被访问一次就“续命”的弊端。
- Redis 将原来的 24 位
lru字段拆分:- 前 16 位:
ldt(Last Decrement Time),记录上次衰减时间(分钟级)。 - 后 8 位:
logc(Logarithmic Counter),对数计数器,记录访问频率(非精确次数,而是对数增长,上限 255)。
- 前 16 位:
- 衰减机制:每次访问时,根据当前时间与
ldt的差值,按照lfu-decay-time衰减计数器(默认 1 分钟衰减 1)。长期不访问的 key 计数器会降低。 - 增长机制:每次访问时计数器增加,增长概率与
lfu-log-factor有关(因子越大,增长越慢,防止高频 key 迅速打满计数器)。
配置调优:
lfu-log-factor 10 # 增长因子,越大增长越慢
lfu-decay-time 1 # 衰减时间,分钟
四、如何选择淘汰策略?(结合场景)
| 业务场景 | 推荐策略 | 原因 |
|---|---|---|
| 纯缓存(商品信息、用户会话) | allkeys-lru |
自动淘汰冷数据,内存利用率高 |
| 缓存 + 持久化数据(排行榜、计数器) | volatile-lru 或 volatile-lfu |
只淘汰有过期时间的,持久 key 不受影响 |
| 数据访问频率差异大,需要保留高热度数据 | allkeys-lfu |
避免 LRU 误淘汰“低频但重要”的数据 |
| 数据没有明显冷热规律,访问均匀 | allkeys-random 或 volatile-random |
简单高效,省 CPU |
| 金融核心数据(余额、额度) | noeviction | 绝对不允许数据被淘汰丢失 |
| 消息队列、临时事件处理 | volatile-ttl |
越早过期越先删除,符合业务规律 |
五、银行生产环境实践
1. 核心交易缓存 → noeviction
- 账户余额、贷款额度、冻结资金等 key 绝不设置过期,且内存上限预留足够空间。
- 当内存使用达到 80% 时,监控告警并立即扩容,绝不触发淘汰。
- 应用层增加保护:写入前检查 Redis 内存,若超过阈值则熔断,走数据库直接读写降级。
2. 普通业务缓存 → allkeys-lru
- 客户信息、产品列表、汇率查询等,可容忍数据丢失。
- 设置
maxmemory 4gb,maxmemory-policy allkeys-lru,maxmemory-samples 10。 - 定期检查命中率和内存碎片,确保 LRU 正常工作。
3. 会话管理 → volatile-lfu
- 会话 token 有固定过期时间,希望淘汰不活跃的会话。
- 设置
maxmemory-policy volatile-lfu,lfu-log-factor 10,既保留活跃用户,又淘汰长时间不访问的。
4. 监控淘汰事件
- 通过
INFO stats中的evicted_keys指标,监控淘汰速率。 - 如果该值持续增长,说明内存不够了,需要扩容或优化 key 设计。
- 告警规则:
evicted_keys增速 > 100/分钟,立即通知。
六、面试模板话术
“Redis 淘汰策略是内存满时的最后防线,分 volatile 和 allkeys 两类作用域,LRU、LFU、random、noeviction 四种算法。默认是 noeviction。
实际中,我通常对纯缓存业务用
allkeys-lru,它基于近似 LRU,随机采样 key 比较lru时间戳,性能高。4.0 后的 LFU 用对数计数器和衰减机制,更能保留高频数据。在银行核心系统里,资金数据用
noeviction,防止任何情况下丢失关键 key。一旦内存接近上限就告警并扩容,而不是依赖淘汰。我通过
evicted_keys监控淘汰情况,并结合maxmemory-samples调优准确度。选择淘汰策略的关键是搞清楚数据能不能丢,能丢就用 LRU/LFU,不能丢就用 noeviction 并从架构上保证内存足够。”
这样回答,既有算法细节,又有策略对比和金融场景的严格约束,能充分体现你对 Redis 内存管理的全面掌控。
你问的“两个策略”是指 过期删除策略 和 内存淘汰策略。它们在 Redis 内存管理中既各司其职又相互配合,下面我从核心区别、内在联系、选择决策三个维度给你讲透。
一、核心区别
| 维度 | 过期删除策略 | 内存淘汰策略 |
|---|---|---|
| 触发条件 | key 设置了 TTL 且已到期 | 内存使用达到 maxmemory 上限 |
| 作用对象 | 仅设置了过期时间的 key | 看策略:volatile-* 仅有过期 key;allkeys-* 所有 key |
| 目的 | 清理逻辑上已失效的数据 | 内存满时强制腾空间,保证新写入成功 |
| 算法 | 惰性删除 + 定期删除 | LRU、LFU、random、TTL、noeviction |
| 配置 | 无开关(默认工作),hz 调频率 |
maxmemory-policy 显式指定 |
| 影响 | 被动/主动清理垃圾 | 内存满时淘汰,可能丢数据 |
| 典型行为 | 访问过期 key 返回空并删除 | 写操作时删除其他 key 来腾空间 |
一句话:过期删除策略是按时间清理逻辑垃圾,淘汰策略是按空间清理内存垃圾。
二、内在联系与协作
1. 它们是 Redis 内存管理的“双层防线”
- 第一层:过期删除策略持续清理已过期的 key,尽量减缓内存增长。
- 第二层:如果过期删除不够快、或大量 key 根本没设 TTL,内存达到
maxmemory,淘汰策略强行介入。
举例:
内存使用 70%:过期删除正常工作,内存平稳。
内存使用 95%:大量 key 同时过期,定期删除来不及清,内存接近上限。
内存使用 100%:淘汰策略触发,按 LRU/LFU 删除部分 key。
2. 过期删除策略减少淘汰策略的压力
如果过期删除机制高效(hz 适当调大,如 100),能在 key 到期后较快删除,就能降低触发淘汰策略的概率,避免“误伤”热点数据。
3. 淘汰策略可以弥补过期删除策略的缺陷
- 惰性删除的缺陷:冷数据过期后一直没人访问,占着内存。淘汰策略(特别是 LRU/LFU)会自动识别这些“冷数据”并淘汰。
- 定期删除的缺陷:随机抽样可能漏掉部分过期 key。淘汰策略会从全部 key 中按算法选择,不会遗漏。
4. volatile-ttl 策略直接依赖过期时间
这个淘汰策略删除剩余 TTL 最短的 key,它本质上就是基于过期删除策略的 TTL 信息来做决策,是两者最直接的交集。
5. 在金融系统中的应用协同
- 核心交易数据:不设 TTL(永不过期),淘汰策略设为
noeviction。这样数据既不会被过期删除,也不会被淘汰,绝对安全。 - 普通缓存:设 TTL(如 30 分钟),淘汰策略
allkeys-lru。过期删除先清理大部分,LRU 再兜底清理剩余冷数据,双重保险。
三、如何根据业务选择这两个策略?
| 数据类型 | TTL 设置 | 淘汰策略 | 原因 |
|---|---|---|---|
| 资金数据(余额、额度) | 不设 TTL | noeviction |
数据绝不能丢,内存够用 |
| 会话 token | 设 30 分钟 | volatile-lfu |
过期自动清理,淘汰低频会话 |
| 产品列表缓存 | 设 60 分钟 | allkeys-lru |
过期清理 + 冷数据淘汰 |
| 排行榜 | 不设 TTL | noeviction |
持久保存,手动更新 |
| 临时计算结果 | 设 2 分钟 | volatile-ttl |
越早过期越先删 |
四、监控两个策略的工作状态
- 过期删除效率:
INFO stats中的expired_keys,如果该值长期不增长但内存持续增,说明定期删除不够,需调大hz。 - 淘汰事件:
INFO stats中的evicted_keys,如果持续增长,说明内存不足,需扩容或优化设计。 - 内存使用率:
INFO memory的used_memory_rss / maxmemory,超过 80% 就该关注。
五、面试模板话术
“Redis 的过期删除和内存淘汰是两层防线。过期删除负责清理到期 key,惰性删除在访问时做,定期删除每 100ms 随机抽检;淘汰策略负责内存满时强制腾空间。
它们协同工作:过期删除先清理逻辑垃圾,减轻内存压力;淘汰策略是最后防线,按 LRU/LFU 等算法删除冷数据。volatile-ttl 策略直接利用 TTL 信息淘汰即将到期的 key。
在银行系统中,资金数据不设 TTL + noeviction,保证绝对不丢;普通缓存设 TTL + allkeys-lru,过期清理主动清,LRU 兜底清冷数据。通过
expired_keys和evicted_keys监控两者的效率,确保内存健康。”
这样既把区别和联系讲清楚,又结合了银行实践和监控手段,正是面试官想听到的答案。
更多推荐
所有评论(0)