问题

Redis的优势,解决了哪些问题?

答案

1. 核心优势

1.1 极高的性能

特点

  • 单线程模型:避免线程切换和锁竞争(Redis 6.0+ 网络IO多线程)
  • 内存存储:数据全在内存,避免磁盘IO
  • 高效数据结构:底层优化的C实现
  • IO多路复用:epoll/kqueue处理并发连接

性能指标

# 官方基准测试(单实例)
GET: ~100,000 OPS
SET: ~80,000 OPS
INCR: ~100,000 OPS
LPUSH: ~100,000 OPS

# 实际生产环境(优化后)
QPS可达10万+

对比关系型数据库

MySQL查询:~1000 QPS(有索引)
Redis查询:~100,000 QPS
性能提升:100倍+

1.2 丰富的数据结构

五大基础类型

  • String:缓存、计数器、分布式锁
  • List:消息队列、时间线
  • Hash:对象存储、购物车
  • Set:标签、去重、集合运算
  • ZSet:排行榜、延时队列

高级数据结构

  • Bitmap:签到、在线状态
  • HyperLogLog:UV统计
  • Geo:地理位置、附近的人
  • Stream:消息队列(5.0+)
  • BloomFilter:去重过滤(通过模块)

优势:一个Redis顶多个数据库

// 传统方案:多个系统协作
MySQL: 存储用户信息
Memcached: 缓存
RabbitMQ: 消息队列
ElasticSearch: 排行榜

// Redis方案:一站式解决
Redis Hash: 用户信息
Redis String: 缓存
Redis List: 消息队列
Redis ZSet: 排行榜

1.3 持久化机制

RDB(快照)

  • 定期全量备份
  • 适合灾难恢复
  • 恢复速度快

AOF(追加日志)

  • 记录每条写命令
  • 数据安全性高
  • 支持自动重写

混合持久化(Redis 4.0+):

# RDB + AOF结合
# RDB作为基础快照
# AOF记录增量修改

解决问题:内存数据不丢失


1.4 高可用架构

主从复制

Master(写) -> Slave1(读)
             -> Slave2(读)
             -> Slave3(读)

哨兵模式(Sentinel)

  • 自动故障转移
  • 监控主从状态
  • 通知客户端切换

集群模式(Cluster)

  • 数据分片(16384槽)
  • 水平扩展
  • 去中心化

解决问题:单点故障、容量瓶颈


1.5 原子性操作

单命令原子性

INCR counter  # 原子递增
GETSET key value  # 原子读取并设置

Lua脚本

-- 原子性检查并删除(分布式锁释放)
if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
else
    return 0
end

事务支持

MULTI
SET key1 value1
SET key2 value2
EXEC  # 原子性执行

解决问题:并发竞态条件


1.6 过期策略与内存管理

过期策略

  • 惰性删除:访问时检查是否过期
  • 定期删除:定时扫描过期key
  • 主动删除:内存不足时淘汰

内存淘汰策略

maxmemory-policy allkeys-lru  # 常用策略

# 可选策略:
noeviction: 不淘汰(返回错误)
allkeys-lru: LRU淘汰所有key
volatile-lru: LRU淘汰有过期时间的key
allkeys-random: 随机淘汰
volatile-ttl: 淘汰最快过期的key

解决问题:自动管理内存,防止溢出


2. 解决的核心问题

2.1 数据库性能瓶颈

问题场景

// 热点数据频繁查询
SELECT * FROM user WHERE id = 1001;  // 每秒10000次
// 数据库扛不住

Redis解决方案

// 缓存策略
String user = redisTemplate.opsForValue().get("user:1001");
if (user == null) {
    user = userMapper.selectById(1001);
    redisTemplate.opsForValue().set("user:1001", user, 1, TimeUnit.HOURS);
}
return user;

效果

  • 数据库压力降低99%
  • 响应时间从100ms降到1ms

2.2 缓存穿透

问题:大量请求不存在的数据,击穿缓存直达数据库

解决方案1:缓存空值

String user = redisTemplate.opsForValue().get("user:9999");
if (user == null) {
    user = userMapper.selectById(9999);
    if (user == null) {
        // 缓存空值,防止穿透
        redisTemplate.opsForValue().set("user:9999", "", 5, TimeUnit.MINUTES);
    } else {
        redisTemplate.opsForValue().set("user:9999", user, 1, TimeUnit.HOURS);
    }
}

解决方案2:BloomFilter

// 启动时加载所有ID到布隆过滤器
BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), 10000000);

// 查询前先判断
if (!bloomFilter.mightContain(userId)) {
    return null;  // 一定不存在
}
// 可能存在,继续查询

2.3 缓存击穿

问题:热点key过期瞬间,大量请求打到数据库

解决方案1:互斥锁

public String getUser(String userId) {
    String key = "user:" + userId;
    String user = redisTemplate.opsForValue().get(key);
    
    if (user == null) {
        String lockKey = "lock:" + key;
        // 只有一个线程能获取锁
        Boolean lock = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
        
        if (lock) {
            try {
                // 查询数据库
                user = userMapper.selectById(userId);
                redisTemplate.opsForValue().set(key, user, 1, TimeUnit.HOURS);
            } finally {
                redisTemplate.delete(lockKey);
            }
        } else {
            // 等待后重试
            Thread.sleep(100);
            return getUser(userId);
        }
    }
    return user;
}

解决方案2:永不过期

// 逻辑过期而非真实过期
class CacheData {
    String data;
    long expireTime;
}

// 异步刷新
if (cacheData.expireTime < System.currentTimeMillis()) {
    // 返回旧数据
    // 异步线程池刷新
    executor.submit(() -> refreshCache(key));
}

2.4 缓存雪崩

问题:大量key同时过期,数据库瞬间压力巨大

解决方案1:过期时间加随机值

// 不要设置统一过期时间
int randomSeconds = ThreadLocalRandom.current().nextInt(300); // 0-5分钟
redisTemplate.opsForValue().set(key, value, 3600 + randomSeconds, TimeUnit.SECONDS);

解决方案2:高可用集群

# 哨兵模式,主节点宕机自动切换
# 或使用Redis Cluster集群

解决方案3:限流降级

// Sentinel限流
@SentinelResource(value = "getUser", blockHandler = "handleBlock")
public String getUser(String userId) {
    // 查询逻辑
}

public String handleBlock(String userId, BlockException e) {
    return "系统繁忙,请稍后再试";
}

2.5 分布式锁

问题:分布式环境下的资源竞争

Redis实现

public class RedisDistributedLock {
    public boolean tryLock(String lockKey, String requestId, int expireTime) {
        Boolean result = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, requestId, expireTime, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(result);
    }
    
    public void unlock(String lockKey, String requestId) {
        String script = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "   return redis.call('del', KEYS[1]) " +
            "else " +
            "   return 0 " +
            "end";
        redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
            Collections.singletonList(lockKey), requestId);
    }
}

解决问题

  • 库存扣减
  • 防止重复下单
  • 定时任务防重

2.6 分布式Session

问题:集群环境下Session不共享

Redis解决方案

// Spring Session配置
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 3600)
public class SessionConfig {
    // 自动将Session存储到Redis
}

// 使用方式不变
@GetMapping("/user")
public User getUser(HttpSession session) {
    return (User) session.getAttribute("user");
}

优势

  • 多台服务器共享Session
  • Session持久化,服务器重启不丢失
  • 支持跨域Session

2.7 实时排行榜

问题:MySQL排序查询慢,且频繁更新

Redis ZSet实现

// 更新分数
redisTemplate.opsForZSet().incrementScore("game:rank", "player:1001", 100);

// 获取Top 10(实时)
Set<ZSetOperations.TypedTuple<String>> top10 = 
    redisTemplate.opsForZSet().reverseRangeWithScores("game:rank", 0, 9);

// 查询个人排名
Long rank = redisTemplate.opsForZSet().reverseRank("game:rank", "player:1001");

性能对比

-- MySQL方案
SELECT * FROM scores ORDER BY score DESC LIMIT 10;
-- 100万数据:~500ms

-- Redis方案
ZREVRANGE game:rank 0 9 WITHSCORES
-- 100万数据:~1ms

2.8 消息队列

问题:轻量级消息队列需求

Redis List实现

// 生产者
redisTemplate.opsForList().leftPush("queue:tasks", task);

// 消费者(阻塞)
String task = redisTemplate.opsForList().rightPop("queue:tasks", 5, TimeUnit.SECONDS);

Redis Stream实现(5.0+):

// 支持消费组、ACK、持久化
redisTemplate.opsForStream().add("stream:tasks", record);

适用场景

  • 异步任务处理
  • 日志收集
  • 简单MQ场景(非核心业务)

3. 性能优化最佳实践

3.1 合理使用数据结构

  • 小对象用压缩编码:控制Hash/ZSet元素数量
  • 大文本拆分:避免大Key
  • 选择合适类型:计数用String,去重用Set

3.2 避免阻塞操作

# 慎用的命令
KEYS *        # 改用SCAN
FLUSHALL     # 慎用
SMEMBERS     # 大Set改用SSCAN

3.3 批量操作

// 使用Pipeline
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
    for (String key : keys) {
        connection.get(key.getBytes());
    }
    return null;
});

3.4 监控与调优

# 慢查询日志
SLOWLOG GET 10

# 内存分析
MEMORY DOCTOR

# 监控指标
INFO stats

4. 面试答题总结

标准回答模板

Redis的核心优势包括:

  1. 高性能:单线程+内存存储+IO多路复用,QPS达10万+
  2. 丰富数据结构:String/List/Hash/Set/ZSet等,一站式解决多种场景
  3. 持久化:RDB+AOF保证数据不丢失
  4. 高可用:主从复制、哨兵、集群,支持故障转移
  5. 原子性:支持Lua脚本和事务

解决的核心问题:

  • 性能:缓存热点数据,降低数据库压力99%
  • 缓存三大问题:穿透(布隆过滤器)、击穿(互斥锁)、雪崩(随机过期)
  • 分布式:分布式锁、Session共享
  • 实时统计:排行榜、UV统计(HyperLogLog)
  • 消息队列:List/Stream实现轻量级MQ

常见追问

  • 为什么Redis这么快? → 内存存储、单线程避免锁、高效数据结构、IO多路复用
  • Redis如何保证高可用? → 主从复制、哨兵自动切换、集群分片
  • Redis单线程为什么还能支持高并发? → IO多路复用、内存操作快、避免上下文切换