Redis深度解析:从数据结构到Spring Boot实战

40 阅读21分钟

引言:为什么Redis如此重要?

Redis从一个小众的缓存工具成长为如今不可或缺的分布式系统组件。在现代应用架构中,Redis已经远远超越了简单的缓存角色,成为了高性能数据存储、消息队列、会话存储等多功能于一体的核心技术。

记得在早期项目中,我们常常使用本地缓存或者关系型数据库来处理热点数据,经常面临性能瓶颈和扩展性问题。自从引入Redis后,系统的响应时间从几百毫秒降低到个位数,这种性能提升带来的用户体验改善是颠覆性的。

一、Redis核心特性解析

1.1 内存存储的威力

Redis之所以快,首要原因在于它是基于内存的存储系统。与传统磁盘存储相比,内存的读写速度要快几个数量级。但Redis的强大之处不仅仅在于此:

java

// 传统文件操作 vs Redis操作
long start = System.currentTimeMillis();
// 传统方式:文件读写
File file = new File("data.txt");
FileWriter writer = new FileWriter(file);
writer.write("Hello World");
writer.close();

// Redis方式(Jedis 是 Java 语言操作 Redis 数据库的官方推荐客户端,轻量且高效,能直接对接 Redis 命令。)
Jedis jedis = new Jedis("localhost");
jedis.set("greeting", "Hello World");

System.out.println("Redis操作耗时: " + (System.currentTimeMillis() - start) + "ms");

1.2 单线程架构的精妙设计

很多人对Redis的单线程模型存在误解,认为这是性能瓶颈。实际上,这正是Redis的设计精妙之处:

  • 避免上下文切换:单线程避免了多线程的上下文切换开销
  • 原子性操作:所有命令都是原子执行的
  • 非阻塞I/O:使用多路复用技术处理并发连接

二、Redis数据结构深度剖析

Jedis 是 Java 语言操作 Redis 数据库的官方推荐客户端,轻量且高效,能直接对接 Redis 命令。

核心特点

  • API 设计简洁,与 Redis 原生命令高度一致,上手成本低。
  • 性能优异,无过多封装,响应速度快,适合高性能场景。
  • 支持 Redis 全特性,包括字符串、哈希、列表等数据结构,以及事务、管道、发布订阅等功能。

接下来,我们使用的都是Jedis库来进行代码示例。

2.1 String:不仅仅是字符串

String是Redis最基本的数据类型,但它的能力远超普通字符串:

java

// 计数器场景 - 文章阅读量
public class ArticleViewCounter {
    private Jedis jedis;
    
    public ArticleViewCounter() {
        this.jedis = new Jedis("localhost");
    }
    
    public void incrementView(Long articleId) {
        String key = "article:view:" + articleId;
        // INCR命令是原子操作,完美解决并发问题
        jedis.incr(key);
    }
    
    public Long getViews(Long articleId) {
        String key = "article:view:" + articleId;
        String views = jedis.get(key);
        return views != null ? Long.parseLong(views) : 0L;
    }
}

2.2 Hash:对象存储的最佳选择

Hash类型特别适合存储对象,相比String的JSON序列化,它在更新和存储效率上更有优势:

java

public class UserService {
    private Jedis jedis;
    
    public void saveUser(User user) {
        String key = "user:" + user.getId();
        Map<String, String> userMap = new HashMap<>();
        userMap.put("name", user.getName());
        userMap.put("email", user.getEmail());
        userMap.put("age", String.valueOf(user.getAge()));
        
        jedis.hset(key, userMap);
        // 设置过期时间 - 24小时
        jedis.expire(key, 24 * 60 * 60);
    }
    
    public User getUser(Long id) {
        String key = "user:" + id;
        Map<String, String> userMap = jedis.hgetAll(key);
        
        if (userMap.isEmpty()) {
            return null;
        }
        
        User user = new User();
        user.setId(id);
        user.setName(userMap.get("name"));
        user.setEmail(userMap.get("email"));
        user.setAge(Integer.parseInt(userMap.get("age")));
        
        return user;
    }
}

2.3 List:消息队列的轻量级实现

Redis List可以实现简单的消息队列,特别适合处理峰值流量:

java

public class EmailQueue {
    private static final String QUEUE_KEY = "queue:emails";
    private Jedis jedis;
    
    public void sendEmail(String to, String subject, String content) {
        EmailMessage message = new EmailMessage(to, subject, content);
        String messageJson = objectToJson(message);
        // LPUSH + RPOP 实现队列
        jedis.lpush(QUEUE_KEY, messageJson);
    }
    
    public void processEmails() {
        while (true) {
            // BRPOP 是阻塞版本,避免频繁轮询
            List<String> messages = jedis.brpop(30, QUEUE_KEY);
            if (messages != null) {
                String messageJson = messages.get(1);
                EmailMessage message = jsonToObject(messageJson, EmailMessage.class);
                sendActualEmail(message);
            }
        }
    }
}

2.4 Set:去重与集合运算

Set类型的去重特性和集合运算能力,在很多场景下非常实用:

java

public class SocialMediaService {
    private Jedis jedis;
    
    // 共同关注功能
    public Set<String> getMutualFollowers(Long userId1, Long userId2) {
        String key1 = "user:followers:" + userId1;
        String key2 = "user:followers:" + userId2;
        
        // SINTER 命令求交集
        return jedis.sinter(key1, key2);
    }
    
    // 推荐可能认识的人
    public Set<String> recommendFriends(Long userId) {
        String userKey = "user:followers:" + userId;
        
        // 获取用户的所有关注者
        Set<String> followers = jedis.smembers(userKey);
        Set<String> recommendations = new HashSet<>();
        
        for (String follower : followers) {
            String followerKey = "user:followers:" + follower;
            // 求差集:关注者的关注者中,排除已关注的和自己
            Set<String> potential = jedis.sdiff(followerKey, userKey);
            potential.remove(userId.toString());
            recommendations.addAll(potential);
        }
        
        return recommendations;
    }
}

2.5 Sorted Set:排行榜的实现利器

Sorted Set是实现排行榜功能的完美选择:

java

public class Leaderboard {
    private static final String LEADERBOARD_KEY = "leaderboard:game";
    private Jedis jedis;
    
    public void addScore(String player, double score) {
        jedis.zadd(LEADERBOARD_KEY, score, player);
    }
    
    public List<PlayerScore> getTopPlayers(int limit) {
        Set<Tuple> topScores = jedis.zrevrangeWithScores(LEADERBOARD_KEY, 0, limit - 1);
        
        return topScores.stream()
            .map(tuple -> new PlayerScore(tuple.getElement(), tuple.getScore()))
            .collect(Collectors.toList());
    }
    
    public Long getPlayerRank(String player) {
        // 排名从0开始,所以+1得到实际排名
        Long rank = jedis.zrevrank(LEADERBOARD_KEY, player);
        return rank != null ? rank + 1 : null;
    }
}

三、Redis持久化机制对比

3.1 RDB:快照持久化

RDB是Redis默认的持久化方式,通过创建数据集的快照来工作:

优点:

  • 文件紧凑,适合备份和灾难恢复
  • 最大化Redis性能,父进程不需要磁盘I/O
  • 恢复大数据集时比AOF更快

缺点:

  • 可能丢失最后一次快照之后的数据
  • 数据集很大时,fork过程可能耗时较长

3.2 AOF:追加式持久化

AOF记录每个写操作,提供更强的持久性保证:

配置示例:

text

appendonly yes
appendfsync everysec  # 平衡性能和数据安全

AOF重写机制:
随着时间推移,AOF文件会变大,Redis会定期重写AOF文件以压缩体积。

四、Spring Boot集成Redis实战

4.1 基础配置

yaml

# application.yml
spring:
  redis:
    host: localhost
    port: 6379
    password: 
    database: 0
    lettuce:
      pool:
        max-active: 8
        max-idle: 8
        min-idle: 0
        max-wait: -1ms
    timeout: 2000ms

4.2 配置类详解

java

@Configuration
@EnableCaching
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        
        // 使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
        Jackson2JsonRedisSerializer<Object> serializer = 
            new Jackson2JsonRedisSerializer<>(Object.class);
        
        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.activateDefaultTyping(
            mapper.getPolymorphicTypeValidator(), 
            ObjectMapper.DefaultTyping.NON_FINAL
        );
        serializer.setObjectMapper(mapper);
        
        template.setValueSerializer(serializer);
        template.setKeySerializer(new StringRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }
    
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(10))  // 默认过期时间
            .disableCachingNullValues();       // 不缓存空值
            
        return RedisCacheManager.builder(factory)
            .cacheDefaults(config)
            .build();
    }
}

4.3 使用Spring Cache注解

java

@Service
public class ProductService {
    
    @Cacheable(value = "products", key = "#id")
    public Product getProductById(Long id) {
        // 模拟数据库查询
        return productRepository.findById(id)
                .orElseThrow(() -> new ProductNotFoundException(id));
    }
    
    @CachePut(value = "products", key = "#product.id")
    public Product updateProduct(Product product) {
        return productRepository.save(product);
    }
    
    @CacheEvict(value = "products", key = "#id")
    public void deleteProduct(Long id) {
        productRepository.deleteById(id);
    }
    
    @Caching(evict = {
        @CacheEvict(value = "products", key = "#id"),
        @CacheEvict(value = "productList", allEntries = true)
    })
    public void refreshProductCache(Long id) {
        // 同时清除单个产品和产品列表缓存
    }
}

4.4 自定义Redis工具类

java

@Component
public class RedisUtil {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    /**
     * 设置缓存
     */
    public void set(String key, Object value, long time) {
        redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
    }
    
    /**
     * 获取缓存
     */
    public Object get(String key) {
        return redisTemplate.opsForValue().get(key);
    }
    
    /**
     * 删除缓存
     */
    public Boolean delete(String key) {
        return redisTemplate.delete(key);
    }
    
    /**
     * 批量删除
     */
    public Long delete(Collection<String> keys) {
        return redisTemplate.delete(keys);
    }
    
    /**
     * 实现分布式锁
     */
    public Boolean tryLock(String key, String value, long expire) {
        return redisTemplate.opsForValue()
                .setIfAbsent(key, value, Duration.ofSeconds(expire));
    }
    
    /**
     * 释放分布式锁
     */
    public Boolean releaseLock(String key, String value) {
        String currentValue = (String) redisTemplate.opsForValue().get(key);
        if (Objects.equals(currentValue, value)) {
            return redisTemplate.delete(key);
        }
        return false;
    }
}

五、Redis高级特性与最佳实践

5.1 管道技术提升性能

Redis 管道技术(Pipeline)是一种优化 Redis 客户端与服务器通信效率的机制,通过批量发送多个命令并一次性接收响应,减少因频繁网络往返(Round Trip)带来的性能开销。

为什么需要管道技术?

  • 普通模式下,客户端发送一个命令后需等待服务器响应,再发送下一个命令,存在大量网络延迟(尤其客户端与服务器跨网络时)。
  • 管道技术允许客户端一次性发送多个命令,服务器依次执行后批量返回结果,将多次网络往返压缩为一次,大幅提升批量操作效率。

管道技术的适用场景

  • 需要执行大量连续的 Redis 命令(如批量插入、批量查询)。
  • 命令之间无依赖关系(即后一个命令不需要前一个命令的结果作为参数)。
  • 对执行效率要求高,希望减少网络开销的场景(如数据迁移、批量统计)。

java

@Service
public class BatchOperationService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    public void batchInsertUsers(List<User> users) {
        redisTemplate.executePipelined(new RedisCallback<Object>() {
            @Override
            public Object doInRedis(RedisConnection connection) throws DataAccessException {
                for (User user : users) {
                    String key = "user:" + user.getId();
                    byte[] keyBytes = redisTemplate.getStringSerializer().serialize(key);
                    byte[] valueBytes = redisTemplate.getValueSerializer().serialize(user);
                    connection.set(keyBytes, valueBytes);
                }
                return null;
            }
        });
    }
}

5.2 Lua脚本保证原子性

在 Redis 中,Lua 脚本是保证复杂操作原子性的重要手段。Redis 会将整个 Lua 脚本作为一个不可分割的执行单元,在脚本执行期间,不会被其他客户端的命令打断,从而确保脚本内所有操作的原子性。

为什么 Lua 脚本能保证原子性?

Redis 执行 Lua 脚本时,会进入单线程执行模式

  • 从脚本开始执行到结束,Redis 不会处理其他任何客户端的命令请求。
  • 脚本内的所有命令会连续执行,中间不会插入其他操作,因此不会出现 “部分执行” 的情况。

这种特性使得 Lua 脚本非常适合处理多步依赖操作(如 “先判断再修改”“批量操作 + 条件逻辑” 等),避免了普通命令因分步执行可能导致的竞态问题。

java

@Service
public class InventoryService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    private static final String DEDUCT_SCRIPT = 
        "local current = tonumber(redis.call('get', KEYS[1])) " +
        "if current == nil then " +
        "   return -1 " +
        "end " +
        "if current < tonumber(ARGV[1]) then " +
        "   return -2 " +
        "end " +
        "redis.call('decrby', KEYS[1], ARGV[1]) " +
        "return 0";
    
    public boolean deductInventory(String productId, int quantity) {
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setScriptText(DEDUCT_SCRIPT);
        script.setResultType(Long.class);
        
        Long result = redisTemplate.execute(script, 
            Collections.singletonList("inventory:" + productId), 
            String.valueOf(quantity));
            
        return result == 0;
    }
}

5.3 缓存问题解决方案

5.3.1 缓存穿透

缓存穿透是指客户端请求的数据在缓存(如 Redis)和数据库中都不存在,导致请求每次都穿透缓存直达数据库,从而给数据库带来巨大压力(尤其在高并发场景下)。

缓存穿透的危害

  • 数据库频繁处理无效请求,可能被压垮(如恶意攻击时,大量请求查询不存在的数据)。
  • 缓存失去作用,系统整体性能急剧下降。

产生原因

  1. 业务逻辑问题:用户查询不存在的业务数据(如查询 ID 为 -1 的用户)。
  2. 恶意攻击:故意构造大量不存在的 key 发起请求,消耗数据库资源。
  3. 数据过期 / 删除:缓存和数据库中都已删除的数据,仍有请求访问。

解决方案

针对缓存穿透,常见的解决策略有以下几种:

1. 缓存空值(空对象)

  • 原理:当数据库查询结果为空时,仍将这个空结果(如 null 或特定标记)缓存起来,并设置较短的过期时间(避免长期占用缓存空间)。
  • 效果:后续相同的请求会直接命中缓存中的空值,不再访问数据库。
  • 示例:查询 user:10086 时,若数据库无此用户,缓存 user:10086 → null,过期时间设为 5 分钟。
  • 注意:需合理设置过期时间,避免缓存大量空值导致内存浪费。

2. 布隆过滤器(Bloom Filter)

  • 原理:在缓存层之前增加一个布隆过滤器,预先存储所有存在的 key(如数据库中已有的用户 ID、商品 ID)。请求到来时,先通过布隆过滤器判断 key 是否存在:

    • 若不存在,直接返回空(无需访问缓存和数据库);
    • 若可能存在(布隆过滤器有极小误判率),再走正常的缓存 + 数据库流程。
  • 优势:占用内存小、查询速度快,适合海量数据场景(如亿级 key)。

  • 注意

    • 布隆过滤器存在误判(可能把不存在的 key 判定为存在),但不会漏判(存在的 key 一定能判定为存在)。
    • 需要定期同步数据库中的新增 / 删除 key 到布隆过滤器。

3. 接口层限流与校验

  • 原理:在接口层对请求进行合法性校验(如 ID 格式、范围),过滤明显无效的请求(如负数 ID、超范围 ID)。
  • 示例:用户 ID 为正整数,直接拦截 ID ≤ 0 的请求。
  • 补充:结合限流措施(如 Redis 限流),防止恶意请求洪水攻击。

4. 数据预热与主动缓存

  • 原理:对于核心业务数据,提前加载到缓存中;对于新增数据,在写入数据库时同步更新缓存,避免缓存中出现 “应该存在却不存在” 的 key。

java

@Service
public class CachePenetrationSolution {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    public Object getDataWithBloomFilter(String key) {
        // 1. 先查询布隆过滤器
        if (!bloomFilter.mightContain(key)) {
            return null;
        }
        
        // 2. 查询缓存
        Object value = redisTemplate.opsForValue().get(key);
        if (value != null) {
            return value;
        }
        
        // 3. 查询数据库
        value = database.get(key);
        if (value != null) {
            redisTemplate.opsForValue().set(key, value, 30, TimeUnit.MINUTES);
        } else {
            // 缓存空值,防止穿透
            redisTemplate.opsForValue().set(key, "", 5, TimeUnit.MINUTES);
        }
        
        return value;
    }
}

5.3.2 缓存雪崩

缓存雪崩是指大量缓存数据在同一时间过期失效,或缓存服务突然宕机,导致所有请求瞬间穿透到数据库,造成数据库压力骤增甚至崩溃的现象。

核心成因

  1. 缓存集中过期:批量设置缓存时使用了相同的过期时间(如凌晨 1 点统一过期),到期后大量请求直达数据库。
  2. 缓存服务故障:Redis 等缓存集群宕机、网络中断,导致缓存完全不可用。
  3. 热点数据集中失效:某类热点数据(如促销商品)缓存过期,且并发请求量极大。

解决方案

1. 避免缓存集中过期

  • 给缓存过期时间添加随机偏移量(如基础过期 1 小时,再加 0-30 分钟随机值),分散过期时间点。
  • 核心数据设置永不过期,通过后台异步任务定期更新(如每天凌晨低峰期刷新)。
  • 分层缓存设计:不同层级缓存设置不同过期时间(如本地缓存 + 分布式缓存),减少同一时间失效概率。

2. 提升缓存服务可用性

  • 搭建缓存集群(如 Redis 主从 + 哨兵、Redis Cluster),避免单点故障。
  • 开启缓存服务的持久化(RDB+AOF),宕机后可快速恢复数据。
  • 配置熔断降级机制:当缓存服务响应超时,暂时返回默认值或降级服务,避免雪崩扩散。

3. 流量控制与兜底

  • 数据库层添加限流措施(如使用 Sentinel、Nginx 限流),限制并发请求数。
  • 接口层设置降级开关:缓存雪崩时,关闭非核心功能接口,优先保障核心业务。
  • 本地缓存兜底:关键接口在应用本地维护一份临时缓存(如 Caffeine),缓存失效时先返回本地数据。

4. 热点数据特殊防护

  • 热点数据缓存过期时间设为永久,或延长至远大于业务周期。
  • 针对热点 key,提前预热缓存(如促销活动前手动加载数据到缓存)。
  • 使用 “互斥锁”:缓存失效时,只允许一个线程去数据库查询并更新缓存,其他线程等待重试,避免并发穿透。

java

@Service
public class CacheAvalancheSolution {
    
    public void setDataWithRandomExpire(String key, Object value) {
        // 设置随机过期时间,避免大量key同时过期
        Random random = new Random();
        int expireTime = 1800 + random.nextInt(600); // 30-40分钟随机
        redisTemplate.opsForValue().set(key, value, expireTime, TimeUnit.SECONDS);
    }
}

六、Redis主从复制

Redis 主从复制(Master-Slave Replication)是实现高可用和读写分离的基础,通过将主节点(Master)的数据同步到从节点(Slave),可分担主节点的读压力,同时在主节点故障时提供数据备份。

核心原理

  1. 数据同步:从节点主动连接主节点,主节点将数据全量同步给从节点,之后通过增量同步(命令传播)保持数据一致。
  2. 读写分离:主节点负责写操作,从节点负责读操作(默认从节点只读),提升整体吞吐量。
  3. 故障备份:主节点故障时,可手动或通过哨兵(Sentinel)将从节点晋升为主节点。

主从复制配置步骤

假设我们有 3 台服务器(或同一服务器的不同端口),规划如下:

  • 主节点(Master):192.168.1.100:6379
  • 从节点 1(Slave1):192.168.1.101:6379
  • 从节点 2(Slave2):192.168.1.102:6379

6.1 准备工作

  • 所有节点安装相同版本的 Redis(版本兼容很重要,建议主从版本一致)。
  • 确保主从节点之间网络互通(关闭防火墙或开放 Redis 端口)。
  • 主节点无需特殊配置(默认允许被从节点连接),但建议设置密码(requirepass)增强安全性。

6.2 主节点(Master)配置

修改主节点的 redis.conf 配置文件:

conf

# 绑定地址(允许所有IP访问,生产环境建议指定具体IP)
bind 0.0.0.0
# 端口
port 6379
# 设置密码(可选,建议设置)
requirepass "master_password"
# 开启持久化(可选,防止主节点重启后数据丢失)
appendonly yes
appendfilename "appendonly.aof"

重启主节点使配置生效:

bash

redis-server /path/to/redis.conf

6.3 从节点(Slave)配置

从节点需指定主节点的地址、端口和密码,有两种配置方式:

方式 1:修改 redis.conf(永久生效)

编辑从节点的 redis.conf

conf

# 绑定地址
bind 0.0.0.0
# 端口(与主节点不同,避免冲突)
port 6379
# 配置主节点信息
replicaof 192.168.1.100 6379  # 主节点IP和端口
masterauth "master_password"  # 主节点的密码(若主节点设置了密码)
# 从节点只读(默认开启,建议保持)
replica-read-only yes

重启从节点:

bash

redis-server /path/to/redis.conf

6.4 验证主从关系

  • 在主节点执行,查看从节点列表:

    bash

    redis-cli -h 192.168.1.100 -p 6379 -a "master_password"
    192.168.1.100:6379> info replication
    

    输出中 slave0slave1 会显示从节点的 IP、端口和状态(online 表示正常)。

  • 在从节点执行,查看主节点信息:

    bash

    redis-cli -h 192.168.1.101 -p 6379
    192.168.1.101:6379> info replication
    

    输出中 master_hostmaster_port 会显示主节点信息,master_link_status:up 表示连接正常。

6.5 测试数据同步

  • 在主节点设置一个键值对:

    bash

    192.168.1.100:6379> set test "hello replication"
    OK
    
  • 在从节点查询该键,验证是否同步成功:

    bash

    192.168.1.101:6379> get test
    "hello replication"  # 成功同步
    

6.6 进阶配置与注意事项

  1. 级联复制:从节点可以再作为其他从节点的主节点(如 Master → Slave1 → Slave2),减轻主节点的同步压力。只需在 Slave2 的配置中设置 replicaof 192.168.1.101 6379 即可。

  2. 主节点写负载:主节点需要处理所有写操作和同步请求,高并发场景下建议减少主节点数量(通常 1 主多从)。

  3. 数据延迟:从节点数据同步存在轻微延迟(毫秒级),不适合强一致性场景(如金融交易的实时对账)。

  4. 主节点故障处理

    • 手动切换:在从节点执行 replicaof no one,使其成为新主节点,其他从节点再指向新主节点。
    • 自动切换:结合 Redis 哨兵(Sentinel)实现主从自动故障转移(推荐生产环境使用)。
  5. 安全配置

    • 主从节点都建议设置密码(requirepass 和 masterauth)。
    • 限制 replicaof 命令只能在配置文件中设置,避免客户端恶意修改(rename-command REPLICAOF "")。

通过主从复制,既能提高 Redis 的读性能,又能实现数据备份,是构建 Redis 高可用架构的基础。

七、Redis 集群

Redis 集群(Redis Cluster)是官方提供的分布式解决方案,用于解决单节点性能瓶颈和单点故障问题,支持数据分片存储自动故障转移水平扩展,最多可容纳 16384 个节点。

核心特性

  1. 数据分片:采用哈希槽(Hash Slot)机制,将 16384 个槽(0-16383)分配给集群中的节点,每个 key 通过 CRC16(key) % 16384 计算所属槽位,实现数据分布式存储。
  2. 高可用:每个主节点(Master)可配置多个从节点(Slave),主节点故障时,从节点自动晋升为主节点。
  3. 去中心化:集群无中心节点,每个节点对等通信,客户端可连接任意节点获取全集群信息。
  4. 水平扩展:支持动态添加 / 删除节点,自动重新分配槽位,无需停机。

集群配置步骤(以 3 主 3 从为例)

假设规划 6 个节点(可在同一服务器用不同端口模拟):

  • 主节点:638063816382
  • 从节点:6383(对应 6380)、6384(对应 6381)、6385(对应 6382)

7.1. 准备节点配置文件

为每个节点创建独立的配置文件(如 redis-6380.conf 至 redis-6385.conf),核心配置如下(以 6380 为例):

conf

# 端口
port 6380
# 开启集群模式
cluster-enabled yes
# 集群配置文件(自动生成,记录槽位和节点信息)
cluster-config-file nodes-6380.conf
# 集群节点超时时间(毫秒,超过此时间认为节点故障)
cluster-node-timeout 15000
# 绑定地址(生产环境建议指定具体IP)
bind 0.0.0.0
# 关闭保护模式(允许跨网络访问)
protected-mode no
# 数据持久化(可选)
appendonly yes
# 密码(所有节点密码需一致,否则无法通信)
requirepass "cluster_password"
masterauth "cluster_password"

其他节点配置仅需修改 port 和 cluster-config-file 文件名(如 6381 对应 nodes-6381.conf)。

7.2. 启动所有节点

分别启动 6 个节点:

bash

redis-server /path/to/redis-6380.conf
redis-server /path/to/redis-6381.conf
redis-server /path/to/redis-6382.conf
redis-server /path/to/redis-6383.conf
redis-server /path/to/redis-6384.conf
redis-server /path/to/redis-6385.conf

启动后,每个节点会生成对应的 nodes-xxxx.conf 文件,初始状态为 “未加入集群”。

7.3. 创建集群

使用 redis-cli --cluster 工具初始化集群(需 Redis 5.0+,低版本用 redis-trib.rb):

bash

# 语法:redis-cli --cluster create 节点列表 --cluster-replicas 从节点数量
redis-cli -a cluster_password --cluster create \
127.0.0.1:6380 127.0.0.1:6381 127.0.0.1:6382 \
127.0.0.1:6383 127.0.0.1:6384 127.0.0.1:6385 \
--cluster-replicas 1
  • --cluster-replicas 1 表示每个主节点分配 1 个从节点。
  • 执行后会显示主从分配方案和槽位分配计划,输入 yes 确认。

7.4. 验证集群状态

连接任意节点,查看集群信息:

bash

# 连接节点(需指定 -c 启用集群模式客户端)
redis-cli -c -h 127.0.0.1 -p 6380 -a cluster_password

# 查看集群状态
127.0.0.1:6380> cluster info
# 输出示例:cluster_state:ok(集群正常)、cluster_slots_assigned:16384(所有槽位已分配)

# 查看节点列表(主从关系和槽位范围)
127.0.0.1:6380> cluster nodes
# 输出中包含每个节点的 ID、角色(master/slave)、所属主节点、负责的槽位等信息

7.5. 测试数据分片与自动跳转

在集群模式客户端中设置 key,会自动路由到对应槽位的主节点:

bash

127.0.0.1:6380> set name "redis-cluster"
# 若当前节点不是目标槽位,会显示 "-> Redirected to slot [xxx] located at 127.0.0.1:xxxx"
OK

# 在任意节点查询,会自动跳转至存储节点
127.0.0.1:6381> get name
"redis-cluster"

7.6 进阶操作

添加新节点

  1. 启动新节点(如 6386),配置与其他节点一致(cluster-enabled yes)。

  2. 将新节点加入集群(先作为空节点):

    bash

    redis-cli -a cluster_password --cluster add-node 127.0.0.1:6386 127.0.0.1:6380
    
  3. 分配槽位给新节点(若作为主节点):

    bash

    redis-cli -a cluster_password --cluster reshard 127.0.0.1:6380
    # 按提示输入需迁移的槽位数、目标节点ID、源节点ID,完成后槽位自动分配
    
  4. 若作为从节点,关联主节点:

    bash

    # 连接新节点,执行 cluster replicate 主节点ID
    redis-cli -c -p 6386 -a cluster_password
    127.0.0.1:6386> cluster replicate <master-node-id>
    

删除节点

  1. 若节点是从节点,直接删除:

    bash

    redis-cli -a cluster_password --cluster del-node 127.0.0.1:6380 <node-id>
    
  2. 若节点是主节点,需先迁移其所有槽位到其他主节点,再删除(迁移步骤类似添加节点的 reshard 操作)。

故障转移测试

  1. 停止一个主节点(如 6380):

    bash

    redis-cli -p 6380 -a cluster_password shutdown
    
  2. 观察其从节点(如 6383)是否自动晋升为主节点:

    bash

    redis-cli -c -p 6383 -a cluster_password cluster nodes | grep master
    

注意事项

  1. 密码一致性:所有节点必须使用相同的密码(requirepass 和 masterauth),否则节点间无法通信。
  2. 槽位完整性:集群状态为 ok 的前提是 16384 个槽位全部被分配,否则无法正常读写。
  3. 数据迁移:动态迁移槽位时,数据会在线同步,不影响集群可用性,但需控制迁移速度(避免阻塞)。
  4. 客户端适配:客户端需支持集群模式(如 JedisCluster、Lettuce),否则需手动处理槽位路由。
  5. 备份策略:集群不直接支持整体备份,需对每个主节点单独进行持久化(RDB/AOF)。

Redis 集群通过分片和自动故障转移,完美解决了单节点的性能和可用性瓶颈,适合数据量较大、并发量高的生产环境。

spring boot 集成redis 请参考以下链接:

Spring Boot : 集成Redis全面指南-CSDN博客