Redis面试深度解析

一、Redis核心概念

1.1 什么是Redis?

Redis(Remote Dictionary Server) 是一个基于内存的键值对存储系统,具有以下特点:

  • 内存存储:数据存储在内存中,读写速度极快(10万+QPS)
  • 持久化:支持RDB和AOF两种持久化方式
  • 多数据结构:支持String、Hash、List、Set、ZSet等数据结构
  • 单线程模型:命令执行单线程,避免并发问题
  • 高性能:基于IO多路复用技术,支持高并发

核心理解

复制代码
传统数据库:磁盘存储 → 慢速(毫秒级)
Redis:内存存储 → 极快(微秒级)

1.2 为什么Redis这么快?

三大核心原因

  1. 纯内存操作

    • 内存访问速度:ns级别
    • 磁盘访问速度:ms级别
    • 速度差距:100万倍
  2. 单线程模型

    复制代码
    优势:
    - 避免线程切换开销
    - 避免锁竞争
    - 避免上下文切换
    
    为什么单线程还快?
    - 内存操作快,CPU不是瓶颈
    - IO多路复用处理并发
  3. IO多路复用

    复制代码
    传统IO:一个连接一个线程
    多路复用:一个线程处理多个连接
    
    使用epoll机制:
    - 监听多个Socket
    - 有数据就处理
    - 无数据不阻塞

二、Redis作用与应用场景

2.1 核心作用

作用 说明 性能提升
缓存 减轻数据库压力 读性能提升100倍+
分布式锁 解决并发问题 保证数据一致性
消息队列 异步解耦 削峰填谷
计数器 高并发计数 支持亿级并发
排行榜 实时排序 毫秒级响应
Session共享 分布式会话 无状态服务

2.2 典型应用场景

场景1:缓存热点数据
java 复制代码
// 查询商品详情
public Product getProduct(Long id) {
    // 1. 先查缓存
    String key = "product:" + id;
    String json = redisTemplate.opsForValue().get(key);

    if (json != null) {
        return JSON.parseObject(json, Product.class);
    }

    // 2. 缓存未命中,查数据库
    Product product = productDao.selectById(id);

    // 3. 写入缓存
    redisTemplate.opsForValue().set(key, JSON.toJSONString(product),
                                     30, TimeUnit.MINUTES);
    return product;
}

为什么这样做?

复制代码
数据库QPS:1000
Redis QPS:100000+

查询流程:
┌─────────┐    Cache Hit (99%)     ┌───────┐
│  Client │ ─────────────────────→ │ Redis │
└─────────┘                        └───────┘
     │           Cache Miss (1%)
     └──────────────────────────→ ┌──────────┐
                                  │ Database │
                                  └──────────┘

效果:数据库压力降低99%
场景2:分布式锁(秒杀场景)
java 复制代码
@Service
public class SeckillService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    public boolean seckill(Long userId, Long productId) {
        String lockKey = "lock:product:" + productId;
        String requestId = UUID.randomUUID().toString();

        try {
            // 获取锁,设置过期时间防止死锁
            Boolean success = redisTemplate.opsForValue()
                .setIfAbsent(lockKey, requestId, 10, TimeUnit.SECONDS);

            if (!success) {
                return false; // 获取锁失败
            }

            // 业务逻辑:扣减库存
            Integer stock = (Integer) redisTemplate.opsForValue()
                .get("stock:" + productId);

            if (stock <= 0) {
                return false; // 库存不足
            }

            // 库存扣减
            redisTemplate.opsForValue().decrement("stock:" + productId);

            // 创建订单(异步或同步)
            createOrder(userId, productId);

            return true;

        } finally {
            // 释放锁(Lua脚本保证原子性)
            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);
        }
    }
}

核心要点

  1. 设置过期时间:防止死锁
  2. 使用唯一ID:防止误删其他线程的锁
  3. Lua脚本释放锁:保证原子性
场景3:排行榜(ZSet应用)
java 复制代码
// 游戏积分排行榜
public class RankService {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    // 更新积分
    public void updateScore(String userId, double score) {
        redisTemplate.opsForZSet().add("rank:score", userId, score);
    }

    // 获取前100名
    public Set<ZSetOperations.TypedTuple<String>> getTop100() {
        return redisTemplate.opsForZSet()
            .reverseRangeWithScores("rank:score", 0, 99);
    }

    // 获取用户排名
    public Long getUserRank(String userId) {
        return redisTemplate.opsForZSet()
            .reverseRank("rank:score", userId);
    }
}

三、Redis数据类型详解

3.1 String(字符串)

底层实现:SDS(Simple Dynamic String)

应用场景

  • 缓存对象(JSON序列化)
  • 计数器(incr/decr)
  • 分布式锁(setnx)
  • Session共享

常用命令

bash 复制代码
SET key value [EX seconds] [NX|XX]
GET key
INCR key          # 自增1
DECR key          # 自减1
INCRBY key delta  # 增加指定值

代码示例

java 复制代码
// 计数器:文章浏览量
public void incrViewCount(Long articleId) {
    String key = "article:view:" + articleId;
    redisTemplate.opsForValue().increment(key);
}

// 获取浏览量
public Long getViewCount(Long articleId) {
    String key = "article:view:" + articleId;
    Object count = redisTemplate.opsForValue().get(key);
    return count == null ? 0L : Long.parseLong(count.toString());
}

3.2 Hash(哈希)

底层实现:哈希表(ziplist或hashtable)

应用场景

  • 对象存储(用户信息、商品信息)
  • 购物车

常用命令

bash 复制代码
HSET key field value
HGET key field
HGETALL key
HMSET key field1 value1 field2 value2
HINCRBY key field delta

代码示例

java 复制代码
// 购物车实现
public class CartService {

    // 添加商品到购物车
    public void addCart(Long userId, Long productId, Integer count) {
        String key = "cart:" + userId;
        redisTemplate.opsForHash().increment(key, productId.toString(), count);
    }

    // 获取购物车
    public Map<Object, Object> getCart(Long userId) {
        String key = "cart:" + userId;
        return redisTemplate.opsForHash().entries(key);
    }

    // 删除商品
    public void removeProduct(Long userId, Long productId) {
        String key = "cart:" + userId;
        redisTemplate.opsForHash().delete(key, productId.toString());
    }
}

为什么用Hash而不是String?

复制代码
String方式:
- 整个对象序列化为JSON
- 修改一个字段需要读取整个对象
- 占用空间大

Hash方式:
- 字段独立存储
- 修改单个字段高效
- 占用空间小

3.3 List(列表)

底层实现:quicklist(双向链表+ziplist)

应用场景

  • 消息队列
  • 最新列表(朋友圈)
  • 评论列表

常用命令

bash 复制代码
LPUSH key value      # 左侧插入
RPUSH key value      # 右侧插入
LPOP key             # 左侧弹出
RPOP key             # 右侧弹出
LRANGE key start end # 范围查询
BLPOP key timeout    # 阻塞弹出

代码示例

java 复制代码
// 消息队列实现
public class MsgQueueService {

    // 生产消息
    public void sendMsg(String queue, String msg) {
        redisTemplate.opsForList().rightPush(queue, msg);
    }

    // 消费消息(阻塞)
    public String receiveMsg(String queue) {
        List<String> result = redisTemplate.opsForList()
            .leftPop(queue, 10, TimeUnit.SECONDS);
        return result != null && !result.isEmpty() ? result.get(0) : null;
    }
}

3.4 Set(集合)

底层实现:intset或hashtable

应用场景

  • 标签系统
  • 共同关注
  • 抽奖系统
  • 去重

常用命令

bash 复制代码
SADD key member         # 添加元素
SREM key member         # 删除元素
SISMEMBER key member    # 是否存在
SINTER key1 key2        # 交集
SUNION key1 key2        # 并集
SDIFF key1 key2         # 差集
SRANDMEMBER key count   # 随机返回

代码示例

java 复制代码
// 抽奖系统
public class LotteryService {

    // 添加参与者
    public void addUser(Long activityId, Long userId) {
        String key = "lottery:" + activityId;
        redisTemplate.opsForSet().add(key, userId.toString());
    }

    // 抽取N个中奖者
    public List<String> draw(Long activityId, int count) {
        String key = "lottery:" + activityId;
        return redisTemplate.opsForSet().randomMembers(key, count);
    }

    // 共同关注
    public Set<String> commonFollows(Long userId1, Long userId2) {
        String key1 = "follows:" + userId1;
        String key2 = "follows:" + userId2;
        return redisTemplate.opsForSet().intersect(key1, key2);
    }
}

3.5 ZSet(有序集合)

底层实现:ziplist或skiplist+hashtable

应用场景

  • 排行榜
  • 延迟队列
  • 范围查询

常用命令

bash 复制代码
ZADD key score member           # 添加元素
ZRANGE key start end            # 正序范围查询
ZREVRANGE key start end         # 倒序范围查询
ZRANK key member                # 正序排名
ZREVRANK key member             # 倒序排名
ZINCRBY key increment member    # 增加分数

代码示例

java 复制代码
// 热搜榜实现
public class HotSearchService {

    private static final String HOT_SEARCH_KEY = "hot:search";

    // 搜索时增加热度
    public void search(String keyword) {
        redisTemplate.opsForZSet().incrementScore(HOT_SEARCH_KEY, keyword, 1);
    }

    // 获取热搜榜Top10
    public Set<ZSetOperations.TypedTuple<String>> getTop10() {
        return redisTemplate.opsForZSet()
            .reverseRangeWithScores(HOT_SEARCH_KEY, 0, 9);
    }
}

四、Redis使用方法

4.1 基本操作命令

通用命令
bash 复制代码
# 键操作
KEYS pattern        # 查找键(生产禁用,用SCAN代替)
EXISTS key          # 判断键是否存在
DEL key             # 删除键
EXPIRE key seconds  # 设置过期时间
TTL key             # 查看剩余时间
TYPE key            # 查看类型

# 数据库操作
SELECT index        # 切换数据库(0-15)
FLUSHDB             # 清空当前库
FLUSHALL            # 清空所有库

# 性能相关
PING                # 测试连接
INFO                # 服务器信息

4.2 Java客户端使用

Jedis(传统方式)
java 复制代码
public class JedisExample {

    public static void main(String[] args) {
        // 创建连接
        Jedis jedis = new Jedis("localhost", 6379);

        // String操作
        jedis.set("name", "张三");
        String name = jedis.get("name");

        // Hash操作
        jedis.hset("user:1", "name", "李四");
        jedis.hset("user:1", "age", "25");
        Map<String, String> user = jedis.hgetAll("user:1");

        // List操作
        jedis.rpush("list", "a", "b", "c");
        List<String> list = jedis.lrange("list", 0, -1);

        // 关闭连接
        jedis.close();
    }
}
Lettuce(推荐,Spring Boot 2.x默认)
java 复制代码
public class LettuceExample {

    public static void main(String[] args) {
        // 创建客户端
        RedisClient client = RedisClient.create("redis://localhost:6379");
        StatefulRedisConnection<String, String> connection = client.connect();
        RedisCommands<String, String> commands = connection.sync();

        // 操作
        commands.set("key", "value");
        String value = commands.get("key");

        // 关闭
        connection.close();
        client.shutdown();
    }
}

五、Redis与Spring集成

5.1 Spring Boot集成Redis

步骤1:添加依赖
xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- 连接池 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>
步骤2:配置文件
yaml 复制代码
spring:
  redis:
    # 单机模式
    host: localhost
    port: 6379
    password: 123456
    database: 0

    # 连接池配置
    lettuce:
      pool:
        max-active: 8      # 最大连接数
        max-idle: 8        # 最大空闲连接
        min-idle: 0        # 最小空闲连接
        max-wait: 1000ms   # 最大等待时间

    # 超时配置
    timeout: 3000ms
    connect-timeout: 3000ms
步骤3:配置类(可选)
java 复制代码
@Configuration
public class RedisConfig {

    /**
     * RedisTemplate配置
     * 解决序列化问题
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(
            RedisConnectionFactory factory) {

        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        // JSON序列化配置
        Jackson2JsonRedisSerializer<Object> serializer =
            new Jackson2JsonRedisSerializer<>(Object.class);

        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.activateDefaultTyping(
            LaissezFaireSubTypeValidator.instance,
            ObjectMapper.DefaultTyping.NON_FINAL);
        serializer.setObjectMapper(mapper);

        // String序列化
        StringRedisSerializer stringSerializer = new StringRedisSerializer();

        // Key使用String序列化
        template.setKeySerializer(stringSerializer);
        template.setHashKeySerializer(stringSerializer);

        // Value使用JSON序列化
        template.setValueSerializer(serializer);
        template.setHashValueSerializer(serializer);

        template.afterPropertiesSet();
        return template;
    }
}

5.2 使用RedisTemplate

java 复制代码
@Service
public class UserService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // ======== String操作 ========
    public void setUser(User user) {
        redisTemplate.opsForValue().set("user:" + user.getId(), user);
    }

    public User getUser(Long id) {
        return (User) redisTemplate.opsForValue().get("user:" + id);
    }

    // ======== Hash操作 ========
    public void setUserField(Long userId, String field, Object value) {
        redisTemplate.opsForHash().put("user:" + userId, field, value);
    }

    public Object getUserField(Long userId, String field) {
        return redisTemplate.opsForHash().get("user:" + userId, field);
    }

    // ======== List操作 ========
    public void addToList(String key, Object value) {
        redisTemplate.opsForList().rightPush(key, value);
    }

    public Object popFromList(String key) {
        return redisTemplate.opsForList().leftPop(key);
    }

    // ======== Set操作 ========
    public void addToSet(String key, Object... values) {
        redisTemplate.opsForSet().add(key, values);
    }

    public Set<Object> getSet(String key) {
        return redisTemplate.opsForSet().members(key);
    }

    // ======== ZSet操作 ========
    public void addToZSet(String key, Object value, double score) {
        redisTemplate.opsForZSet().add(key, value, score);
    }

    public Set<Object> getZSetRange(String key, long start, long end) {
        return redisTemplate.opsForZSet().range(key, start, end);
    }
}

5.3 使用StringRedisTemplate

java 复制代码
@Service
public class CacheService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 设置缓存(带过期时间)
     */
    public void set(String key, String value, long timeout, TimeUnit unit) {
        stringRedisTemplate.opsForValue().set(key, value, timeout, unit);
    }

    /**
     * 获取缓存
     */
    public String get(String key) {
        return stringRedisTemplate.opsForValue().get(key);
    }

    /**
     * 删除缓存
     */
    public Boolean delete(String key) {
        return stringRedisTemplate.delete(key);
    }

    /**
     * 设置过期时间
     */
    public Boolean expire(String key, long timeout, TimeUnit unit) {
        return stringRedisTemplate.expire(key, timeout, unit);
    }
}

5.4 Spring Cache注解

java 复制代码
@Service
@CacheConfig(cacheNames = "user")
public class UserCacheService {

    @Autowired
    private UserMapper userMapper;

    /**
     * @Cacheable: 查询时使用缓存
     * 如果缓存存在,直接返回
     * 如果缓存不存在,执行方法并缓存结果
     */
    @Cacheable(key = "#id")
    public User getById(Long id) {
        System.out.println("查询数据库...");
        return userMapper.selectById(id);
    }

    /**
     * @CachePut: 更新缓存
     * 总是执行方法,并将结果更新到缓存
     */
    @CachePut(key = "#user.id")
    public User update(User user) {
        userMapper.updateById(user);
        return user;
    }

    /**
     * @CacheEvict: 删除缓存
     */
    @CacheEvict(key = "#id")
    public void delete(Long id) {
        userMapper.deleteById(id);
    }

    /**
     * 条件缓存
     */
    @Cacheable(key = "#id", condition = "#id > 0", unless = "#result == null")
    public User getByIdWithCondition(Long id) {
        return userMapper.selectById(id);
    }
}

配置Cache

java 复制代码
@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
        // 默认配置
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(30))  // 过期时间30分钟
            .serializeKeysWith(
                RedisSerializationContext.SerializationPair
                    .fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(
                RedisSerializationContext.SerializationPair
                    .fromSerializer(new GenericJackson2JsonRedisSerializer()))
            .disableCachingNullValues();  // 不缓存null值

        // 针对不同缓存的个性化配置
        Map<String, RedisCacheConfiguration> configs = new HashMap<>();
        configs.put("user", config.entryTtl(Duration.ofHours(1)));      // 用户缓存1小时
        configs.put("product", config.entryTtl(Duration.ofMinutes(10))); // 商品缓存10分钟

        return RedisCacheManager.builder(factory)
            .cacheDefaults(config)
            .withInitialCacheConfigurations(configs)
            .build();
    }
}

六、Redis核心原理

6.1 持久化机制

RDB(快照)
复制代码
工作原理:
1. 定时将内存数据生成快照
2. 保存到dump.rdb文件
3. 恢复时加载文件

触发方式:
- save 900 1    # 900秒内至少1个key变化
- save 300 10   # 300秒内至少10个key变化
- save 60 10000 # 60秒内至少10000个key变化

优点:
✓ 文件小,恢复快
✓ 性能高(fork子进程)

缺点:
✗ 可能丢失最后一次快照后的数据
✗ fork子进程消耗内存

配置

bash 复制代码
# redis.conf
save 900 1
save 300 10
save 60 10000

dbfilename dump.rdb
dir ./
AOF(追加文件)
复制代码
工作原理:
1. 记录每个写操作
2. 追加到appendonly.aof文件
3. 恢复时重新执行命令

同步策略:
- always    # 每次写入立即同步(慢但安全)
- everysec  # 每秒同步一次(推荐)
- no        # 由OS决定(快但不安全)

优点:
✓ 数据更安全(最多丢失1秒)
✓ 可读性好

缺点:
✗ 文件大
✗ 恢复慢
✗ 性能相对低

配置

bash 复制代码
# redis.conf
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec

# AOF重写(压缩)
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

如何选择?

复制代码
生产环境推荐:RDB + AOF 混合使用
- RDB做全量备份(快速恢复)
- AOF做增量备份(数据安全)

配置:
appendonly yes
aof-use-rdb-preamble yes  # AOF重写时使用RDB格式

6.2 过期策略

三种策略

  1. 定时删除

    设置过期时间时创建定时器
    到期自动删除

    优点:内存友好
    缺点:CPU不友好(大量定时器)
    Redis不采用

  2. 惰性删除

    访问key时检查是否过期
    过期则删除

    优点:CPU友好
    缺点:内存不友好(过期key占用内存)
    Redis采用

  3. 定期删除

    每隔一段时间随机检查部分key
    删除过期的key

    Redis采用:每秒10次,每次检查20个key

Redis实际策略

复制代码
惰性删除 + 定期删除

过程:
1. 客户端访问key时检查(惰性)
2. 定期随机抽查删除(定期)
3. 内存不足时触发内存淘汰(兜底)

6.3 内存淘汰策略

8种淘汰策略

bash 复制代码
# redis.conf
maxmemory 2gb                    # 最大内存
maxmemory-policy allkeys-lru     # 淘汰策略
策略 说明 适用场景
noeviction 不淘汰,写入返回错误 纯缓存,不允许丢失
allkeys-lru 所有key,淘汰最少使用 通用缓存(推荐)
allkeys-lfu 所有key,淘汰最少访问频率 热点数据缓存
allkeys-random 所有key,随机淘汰 均匀访问
volatile-lru 有过期时间的key,LRU 业务数据缓存
volatile-lfu 有过期时间的key,LFU 业务热点缓存
volatile-random 有过期时间的key,随机 -
volatile-ttl 有过期时间的key,优先淘汰快过期的 时效性数据

LRU vs LFU

复制代码
LRU(Least Recently Used):
- 淘汰最久未使用的
- 适合:周期性访问

LFU(Least Frequently Used):
- 淘汰访问频率最低的
- 适合:热点数据

推荐:allkeys-lru(Redis默认)

6.4 事务机制

Redis事务特点

复制代码
✓ 支持:批量执行、原子性(单线程)
✗ 不支持:回滚、隔离性

命令

bash 复制代码
MULTI              # 开启事务
command1
command2
EXEC               # 提交事务
DISCARD            # 放弃事务

WATCH key          # 监听key,乐观锁
UNWATCH            # 取消监听

示例

java 复制代码
// 使用SessionCallback实现事务
public void transfer(String from, String to, int amount) {
    redisTemplate.execute(new SessionCallback<Object>() {
        @Override
        public Object execute(RedisOperations operations)
                throws DataAccessException {

            operations.watch(from);  // 监听

            operations.multi();  // 开启事务
            operations.opsForValue().decrement(from, amount);
            operations.opsForValue().increment(to, amount);

            return operations.exec();  // 提交
        }
    });
}

Pipeline(管道)

java 复制代码
// 批量执行,减少网络RTT
public void batchSet(Map<String, String> data) {
    redisTemplate.executePipelined(new RedisCallback<Object>() {
        @Override
        public Object doInRedis(RedisConnection connection)
                throws DataAccessException {

            data.forEach((k, v) -> {
                connection.set(k.getBytes(), v.getBytes());
            });
            return null;
        }
    });
}

七、Redis高可用方案

7.1 主从复制

架构

复制代码
        ┌──────┐
        │Master│ (读写)
        └───┬──┘
            │
    ┌───────┼───────┐
    │       │       │
┌───▼──┐ ┌──▼──┐ ┌──▼──┐
│Slave1│ │Slave2│ │Slave3│ (只读)
└──────┘ └─────┘ └─────┘

特点:
- Master负责写
- Slave负责读
- 异步复制
- 读写分离

配置

bash 复制代码
# slave节点配置
replicaof 192.168.1.100 6379
masterauth 123456

Java实现读写分离

java 复制代码
@Configuration
public class RedisConfig {

    @Bean
    public LettuceClientConfigurationBuilderCustomizer
            lettuceClientConfigurationBuilderCustomizer() {
        return clientConfigurationBuilder -> {
            clientConfigurationBuilder.readFrom(ReadFrom.REPLICA_PREFERRED);
            // REPLICA_PREFERRED: 优先从slave读,slave不可用时从master读
        };
    }
}

7.2 哨兵模式(Sentinel)

架构

复制代码
    ┌─────────┐  监控   ┌──────┐
    │Sentinel1├────────→│Master│
    └─────────┘          └──┬───┘
                            │
    ┌─────────┐             │      ┌──────┐
    │Sentinel2├─────────────┼─────→│Slave1│
    └─────────┘             │      └──────┘
                            │
    ┌─────────┐             │      ┌──────┐
    │Sentinel3├─────────────┴─────→│Slave2│
    └─────────┘                    └──────┘

功能:
1. 监控:检查master和slave是否正常
2. 通知:故障时通知管理员
3. 故障转移:master宕机时自动选举新master
4. 配置提供:客户端连接哨兵获取master地址

配置

yaml 复制代码
spring:
  redis:
    sentinel:
      master: mymaster
      nodes:
        - 192.168.1.101:26379
        - 192.168.1.102:26379
        - 192.168.1.103:26379
    password: 123456

7.3 集群模式(Cluster)

架构

复制代码
       ┌──────────────────────────┐
       │     Hash Slot (0-16383)  │
       └───────────┬────────────────┘
                   │
    ┌──────────────┼──────────────┐
    │              │              │
┌───▼────┐    ┌────▼───┐    ┌────▼───┐
│Master1 │    │Master2 │    │Master3 │
│Slot    │    │Slot    │    │Slot    │
│0-5460  │    │5461-   │    │10923-  │
│        │    │10922   │    │16383   │
└───┬────┘    └────┬───┘    └────┬───┘
    │              │              │
┌───▼────┐    ┌────▼───┐    ┌────▼───┐
│Slave1  │    │Slave2  │    │Slave3  │
└────────┘    └────────┘    └────────┘

特点:
- 数据分片存储(16384个slot)
- 支持水平扩展
- 自动故障转移
- 去中心化

配置

yaml 复制代码
spring:
  redis:
    cluster:
      nodes:
        - 192.168.1.101:6379
        - 192.168.1.102:6379
        - 192.168.1.103:6379
        - 192.168.1.104:6379
        - 192.168.1.105:6379
        - 192.168.1.106:6379
      max-redirects: 3
    password: 123456

对比

方案 数据分片 故障转移 扩展性 复杂度
主从 手动
哨兵 自动
集群 自动

八、Redis性能优化

8.1 缓存三大问题

问题1:缓存穿透
复制代码
定义:查询不存在的数据,缓存和数据库都没有

影响:
- 每次都查数据库
- 数据库压力大
- 可能被恶意攻击

场景:
Client → Redis (miss) → DB (miss) → 返回null

问题:下次还是穿透

解决方案

方案1:缓存空值

java 复制代码
public User getUser(Long id) {
    String key = "user:" + id;

    // 1. 查缓存
    String json = redisTemplate.opsForValue().get(key);
    if (json != null) {
        if ("null".equals(json)) {
            return null;  // 缓存的空值
        }
        return JSON.parseObject(json, User.class);
    }

    // 2. 查数据库
    User user = userMapper.selectById(id);

    // 3. 缓存结果(包括null)
    if (user == null) {
        // 缓存空值,设置较短过期时间
        redisTemplate.opsForValue().set(key, "null", 5, TimeUnit.MINUTES);
    } else {
        redisTemplate.opsForValue().set(key, JSON.toJSONString(user),
                                       30, TimeUnit.MINUTES);
    }

    return user;
}

方案2:布隆过滤器

java 复制代码
@Configuration
public class BloomFilterConfig {

    @Bean
    public BloomFilter<Long> userBloomFilter() {
        // 预计元素数量100万,误判率0.01%
        BloomFilter<Long> filter = BloomFilter.create(
            Funnels.longFunnel(),
            1000000,
            0.0001
        );

        // 初始化:加载所有用户ID
        List<Long> userIds = userMapper.selectAllIds();
        userIds.forEach(filter::put);

        return filter;
    }
}

@Service
public class UserService {

    @Autowired
    private BloomFilter<Long> userBloomFilter;

    public User getUser(Long id) {
        // 1. 布隆过滤器判断
        if (!userBloomFilter.mightContain(id)) {
            return null;  // 一定不存在
        }

        // 2. 查缓存
        // 3. 查数据库
        // ...
    }
}
问题2:缓存击穿
复制代码
定义:热点key过期,大量请求同时访问

影响:
- 瞬间大量请求打到数据库
- 数据库压力激增
- 可能导致雪崩

场景:
热点商品缓存过期 → 1万并发同时查数据库

问题:数据库扛不住

解决方案

方案1:互斥锁

java 复制代码
public User getUser(Long id) {
    String key = "user:" + id;

    // 1. 查缓存
    User user = getFromCache(key);
    if (user != null) {
        return user;
    }

    // 2. 缓存未命中,加锁
    String lockKey = "lock:" + key;
    String lockValue = UUID.randomUUID().toString();

    try {
        // 尝试获取锁
        Boolean getLock = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, lockValue, 10, TimeUnit.SECONDS);

        if (getLock) {
            // 获取锁成功,查数据库
            user = userMapper.selectById(id);

            // 写入缓存
            setToCache(key, user);

            return user;
        } else {
            // 获取锁失败,等待后重试
            Thread.sleep(50);
            return getUser(id);  // 递归重试
        }
    } finally {
        // 释放锁(Lua脚本保证原子性)
        releaseLock(lockKey, lockValue);
    }
}

方案2:热点数据永不过期

java 复制代码
// 逻辑过期:缓存中存储过期时间
public class CacheData {
    private Object data;
    private LocalDateTime expireTime;
}

public User getUser(Long id) {
    String key = "user:" + id;

    // 查缓存
    CacheData cacheData = getFromCache(key);

    if (cacheData == null) {
        // 缓存未命中,重建缓存
        return rebuildCache(id);
    }

    // 检查逻辑过期
    if (cacheData.getExpireTime().isBefore(LocalDateTime.now())) {
        // 过期了,异步重建缓存
        threadPool.submit(() -> rebuildCache(id));
    }

    // 返回旧数据(保证可用性)
    return (User) cacheData.getData();
}
问题3:缓存雪崩
复制代码
定义:大量缓存同时过期,或Redis宕机

影响:
- 大量请求打到数据库
- 数据库崩溃
- 整个系统不可用

场景:
凌晨0点,昨天设置的所有缓存同时过期

解决方案

方案1:过期时间打散

java 复制代码
// 不好的做法
redisTemplate.opsForValue().set(key, value, 30, TimeUnit.MINUTES);

// 好的做法:加随机值
int randomSeconds = ThreadLocalRandom.current().nextInt(300);  // 0-300秒
redisTemplate.opsForValue().set(key, value,
    30 * 60 + randomSeconds, TimeUnit.SECONDS);

方案2:Redis高可用

复制代码
- 主从 + 哨兵
- Redis集群
- 多级缓存(本地缓存 + Redis)

方案3:限流降级

java 复制代码
@Service
public class UserService {

    // 限流:每秒最多1000个请求
    @RateLimiter(value = 1000, timeout = 100)
    public User getUser(Long id) {
        // ...
    }

    // 降级:Redis挂了,返回默认值
    @HystrixCommand(fallbackMethod = "getUserFallback")
    public User getUserWithFallback(Long id) {
        return getFromRedis(id);
    }

    public User getUserFallback(Long id) {
        return new User(id, "默认用户");
    }
}

8.2 性能优化建议

1. 合理使用数据结构
复制代码
String:简单key-value
Hash:对象存储(节省内存)
List:队列、栈
Set:去重、交并差集
ZSet:排行榜
2. 避免大key
复制代码
String:不超过10KB
List/Set/ZSet:元素不超过5000
Hash:field不超过1000

大key危害:
- 阻塞其他命令
- 内存碎片
- 主从同步慢
- 过期删除慢
3. 批量操作
java 复制代码
// 不好:多次网络IO
for (String key : keys) {
    redisTemplate.opsForValue().get(key);
}

// 好:一次网络IO
List<Object> values = redisTemplate.opsForValue().multiGet(keys);

// 更好:Pipeline
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
    keys.forEach(key -> connection.get(key.getBytes()));
    return null;
});
4. 避免使用KEYS命令
bash 复制代码
# 生产禁用
KEYS user:*

# 使用SCAN
SCAN 0 MATCH user:* COUNT 100
5. 设置合理的过期时间
java 复制代码
// 根据业务设置
redisTemplate.opsForValue().set(key, value, 30, TimeUnit.MINUTES);

// 热点数据可以长一些
redisTemplate.opsForValue().set(hotKey, value, 24, TimeUnit.HOURS);

九、Redis面试高频问题

9.1 基础问题

Q1:Redis为什么这么快?

复制代码
答:三大原因
1. 纯内存操作:ns级别的访问速度
2. 单线程模型:避免线程切换和锁竞争
3. IO多路复用:epoll机制处理并发连接

补充:
- 高效的数据结构(SDS、跳表等)
- 单线程避免上下文切换

Q2:Redis单线程为什么还能支持高并发?

复制代码
答:
1. CPU不是瓶颈,内存操作极快
2. IO多路复用处理网络请求
3. 单线程避免了并发开销

类比:
一个人(单线程)在图书馆(内存)找书 vs
一群人(多线程)在仓库(磁盘)找书

Q3:Redis有哪些数据类型?

复制代码
答:5种基本类型 + 3种特殊类型

基本类型:
1. String:字符串
2. Hash:哈希表
3. List:列表
4. Set:集合
5. ZSet:有序集合

特殊类型:
1. Bitmap:位图
2. HyperLogLog:基数统计
3. Geo:地理位置

9.2 原理问题

Q4:Redis持久化方式有哪些?

复制代码
答:RDB + AOF

RDB(快照):
- 定时保存内存快照
- 文件小,恢复快
- 可能丢数据

AOF(追加):
- 记录每个写命令
- 数据更安全
- 文件大,恢复慢

推荐:混合持久化
- RDB做全量备份
- AOF做增量备份

Q5:Redis过期键的删除策略?

复制代码
答:惰性删除 + 定期删除

惰性删除:
- 访问时检查是否过期
- CPU友好,内存不友好

定期删除:
- 定期随机检查
- 每秒10次,每次20个key

兜底:内存淘汰策略

Q6:Redis内存淘汰策略有哪些?

复制代码
答:8种策略

推荐:allkeys-lru
- 所有key
- 淘汰最少使用的
- 适合通用缓存

其他:
- volatile-lru:只淘汰有过期时间的key
- allkeys-lfu:淘汰访问频率最低的
- noeviction:不淘汰,写入报错

9.3 高可用问题

Q7:Redis集群方案有哪些?

复制代码
答:主从 + 哨兵 + 集群

主从复制:
- 读写分离
- 手动故障转移

哨兵模式:
- 自动故障转移
- 无数据分片

集群模式:
- 数据分片(16384 slot)
- 水平扩展
- 自动故障转移

Q8:Redis主从同步原理?

复制代码
答:全量同步 + 增量同步

全量同步(初次):
1. Slave发送SYNC命令
2. Master生成RDB快照
3. Master发送RDB给Slave
4. Slave加载RDB
5. Master发送同步期间的写命令

增量同步(断线重连):
1. Master维护replication buffer
2. Slave发送offset
3. Master发送offset后的命令

9.4 缓存问题

Q9:什么是缓存穿透?如何解决?

复制代码
答:查询不存在的数据

影响:
- 每次都打数据库
- 可能被攻击

解决:
1. 布隆过滤器(推荐)
2. 缓存空值
3. 参数校验

Q10:什么是缓存击穿?如何解决?

复制代码
答:热点key过期,大量请求同时访问

影响:
- 瞬间大量请求打数据库

解决:
1. 互斥锁(保证一致性)
2. 热点数据永不过期(保证可用性)
3. 提前异步刷新

Q11:什么是缓存雪崩?如何解决?

复制代码
答:大量缓存同时过期

影响:
- 数据库瞬间压力暴增

解决:
1. 过期时间打散(加随机值)
2. Redis高可用(集群)
3. 限流降级
4. 多级缓存

9.5 分布式锁问题

Q12:如何实现Redis分布式锁?

java 复制代码
答:SETNX + 过期时间 + 唯一标识 + Lua脚本

// 获取锁
SET lock:key uuid EX 10 NX

// 释放锁(Lua脚本)
if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
else
    return 0
end

要点:
1. NX:不存在才设置
2. EX:防止死锁
3. UUID:防止误删
4. Lua:保证原子性

Q13:Redis分布式锁有什么问题?

复制代码
答:主要问题和解决方案

问题1:主从同步延迟
- 场景:Master获取锁后宕机,Slave未同步
- 解决:Redlock算法(多个Master)

问题2:锁超时
- 场景:业务执行超过锁超时时间
- 解决:看门狗机制(Redisson)

问题3:不可重入
- 场景:同一线程无法再次获取锁
- 解决:Hash结构记录重入次数

推荐:使用Redisson框架

9.6 实战问题

Q14:如何保证缓存与数据库一致性?

复制代码
答:先更新数据库,再删除缓存(推荐)

方案对比:
1. 先删缓存,再更新DB:可能不一致
2. 先更新DB,再删缓存:推荐
3. 先更新DB,再更新缓存:浪费性能

延迟双删:
1. 删除缓存
2. 更新数据库
3. 延迟500ms后再删除缓存

最终方案:
- 业务代码:先更新DB,再删缓存
- 兜底:订阅binlog,异步更新缓存

Q15:Redis如何实现延迟队列?

复制代码
答:使用ZSet

原理:
- score存储执行时间戳
- 定时扫描score < 当前时间的任务

示例:
ZADD delay:queue <timestamp> <task>
ZRANGEBYSCORE delay:queue 0 <now>
ZREM delay:queue <task>

Q16:Redis如何实现限流?

复制代码
答:多种方案

方案1:计数器(简单)
INCR key
EXPIRE key 1

方案2:滑动窗口(ZSet)
ZADD key <timestamp> <request_id>
ZREMRANGEBYSCORE key 0 <now-60s>
ZCARD key

方案3:令牌桶(Lua脚本)
推荐使用Redisson的RateLimiter

相关推荐
思成不止于此2 小时前
【MySQL 零基础入门】DQL 核心语法(四):执行顺序与综合实战 + DCL 预告篇
数据库·笔记·学习·mysql
weixin_462446232 小时前
SpringBoot切换Redis的DB
数据库·spring boot·redis
哇哈哈&2 小时前
安装wxWidgets3.2.0(编译高版本erlang的时候用,不如用rpm包),而且还需要高版本的gcc++19以上,已基本舍弃
linux·数据库·python
雨中飘荡的记忆2 小时前
HBase实战指南
大数据·数据库·hbase
数据库学啊3 小时前
车联网时序数据库哪家好
数据库·时序数据库
鱼鱼块3 小时前
从后端拼模板到 Vue 响应式:前端界面的三次进化
前端·vue.js·面试
Luna-player4 小时前
在javaweb项目中,在表中的数据中什么是一对一,一对多,多对多
数据库·oracle
一 乐4 小时前
家政管理|基于SprinBoot+vue的家政服务管理平台(源码+数据库+文档)
前端·javascript·数据库·vue.js·spring boot
愤怒的代码4 小时前
第 3 篇:ArrayList / LinkedList / fail-fast 深度解析(5 题)
面试