从单机到集群:Redis部署全攻略

简介: 本文全面解析Redis四种核心部署方式:单机版部署简单适合开发测试;主从复制实现读写分离和数据备份;哨兵模式提供自动故障转移能力;Redis Cluster集群支持分片存储和横向扩展。文章详细阐述了每种方案的原理、部署步骤、Java代码实现及适用场景,并给出生产环境选型指南。通过对比各方案优缺点,帮助开发者根据业务需求(数据量、并发量、可用性要求等)选择最佳部署方式,同时提供参数优化建议和常见问题解决方案。

一、前言

在分布式系统架构中,Redis作为高性能的键值对数据库,其部署方式直接决定了系统的可用性、可靠性和扩展性。不同的业务场景(如单机测试、高并发读写、海量数据存储)需要匹配不同的Redis部署方案。本文将从底层逻辑出发,全面拆解Redis的4种核心部署方式(单机版、主从复制、哨兵模式、Redis Cluster集群),结合实战配置与Java代码示例,帮你理清每种部署方式的适用场景、优缺点及落地要点,兼顾基础夯实与实际问题解决。

二、单机版部署:最简单的入门方案

2.1 核心原理

单机版是Redis最基础的部署方式,即单个Redis进程独立运行,所有的读写操作都在同一个节点上完成。其架构简单,无额外依赖,本质是“单进程+单线程(IO多路复用)”处理请求,适合对可用性要求不高的场景。

2.2 部署步骤(Linux环境)

2.2.1 环境准备

  • 系统:CentOS 8.x
  • Redis版本:7.2.5(最新稳定版)
  • 依赖安装:yum install -y gcc gcc-c++ make

2.2.2 安装与配置

# 下载并解压Redis
wget https://download.redis.io/releases/redis-7.2.5.tar.gz
tar -zxvf redis-7.2.5.tar.gz -C /usr/local/
cd /usr/local/redis-7.2.5/

# 编译安装
make && make install

# 修改配置文件(redis.conf)
vim /usr/local/redis-7.2.5/redis.conf
# 核心配置项
bind 0.0.0.0  # 允许所有IP访问(生产环境需指定具体IP)
protected-mode no  # 关闭保护模式
port 6379  # 端口
daemonize yes  # 后台运行
requirepass 123456  # 密码(生产环境必须设置)
appendonly yes  # 开启AOF持久化(默认RDB,结合使用更安全)
appendfsync everysec  # AOF同步策略:每秒同步

# 启动Redis
redis-server /usr/local/redis-7.2.5/redis.conf

# 验证启动
redis-cli -h 127.0.0.1 -p 6379 -a 123456 ping
# 输出PONG则启动成功

2.3 Java实战代码(Spring Boot整合)

2.3.1 pom.xml依赖

<dependencies>
   <!-- Spring Boot核心依赖 -->
   <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-web</artifactId>
       <version>3.2.5</version>
   </dependency>
   <!-- Redis依赖 -->
   <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-data-redis</artifactId>
       <version>3.2.5</version>
   </dependency>
   <!-- Lombok -->
   <dependency>
       <groupId>org.projectlombok</groupId>
       <artifactId>lombok</artifactId>
       <version>1.18.30</version>
       <scope>provided</scope>
   </dependency>
   <!-- FastJSON2 -->
   <dependency>
       <groupId>com.alibaba.fastjson2</groupId>
       <artifactId>fastjson2</artifactId>
       <version>2.0.45</version>
   </dependency>
   <!-- Spring Util工具类 -->
   <dependency>
       <groupId>org.springframework</groupId>
       <artifactId>spring-core</artifactId>
       <version>6.1.6</version>
   </dependency>
   <!-- Google Collections -->
   <dependency>
       <groupId>com.google.guava</groupId>
       <artifactId>guava</artifactId>
       <version>33.2.1-jre</version>
   </dependency>
   <!-- Swagger3 -->
   <dependency>
       <groupId>org.springdoc</groupId>
       <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
       <version>2.2.0</version>
   </dependency>
</dependencies>

2.3.2 Redis配置类

package com.jam.demo.config;

import com.alibaba.fastjson2.support.spring.data.redis.FastJson2RedisSerializer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.util.ObjectUtils;

/**
* Redis配置类
* @author ken
*/

@Configuration
@Slf4j
public class RedisConfig {

   /**
    * 自定义RedisTemplate,使用FastJSON2序列化
    * @param connectionFactory Redis连接工厂
    * @return RedisTemplate<String, Object>
    */

   @Bean
   public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
       if (ObjectUtils.isEmpty(connectionFactory)) {
           log.error("RedisConnectionFactory is null");
           throw new IllegalArgumentException("RedisConnectionFactory cannot be null");
       }
       RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
       redisTemplate.setConnectionFactory(connectionFactory);

       // 字符串序列化器(key)
       StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
       // FastJSON2序列化器(value)
       FastJson2RedisSerializer<Object> fastJson2RedisSerializer = new FastJson2RedisSerializer<>(Object.class);

       // 设置key序列化方式
       redisTemplate.setKeySerializer(stringRedisSerializer);
       redisTemplate.setHashKeySerializer(stringRedisSerializer);
       // 设置value序列化方式
       redisTemplate.setValueSerializer(fastJson2RedisSerializer);
       redisTemplate.setHashValueSerializer(fastJson2RedisSerializer);

       redisTemplate.afterPropertiesSet();
       log.info("RedisTemplate initialized successfully");
       return redisTemplate;
   }
}

2.3.3 业务服务类

package com.jam.demo.service;

import com.google.common.collect.Maps;
import com.jam.demo.entity.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
* Redis单机版业务服务
* @author ken
*/

@Service
@Slf4j
public class RedisSingleService {

   @Resource
   private RedisTemplate<String, Object> redisTemplate;

   private static final String USER_KEY_PREFIX = "user:";
   private static final long USER_EXPIRE_TIME = 30L; // 30分钟

   /**
    * 保存用户信息到Redis
    * @param user 用户实体
    * @return boolean 保存结果
    */

   public boolean saveUser(User user) {
       if (ObjectUtils.isEmpty(user) || StringUtils.isEmpty(user.getId())) {
           log.error("保存用户失败:用户信息为空或ID为空");
           return false;
       }
       try {
           String key = USER_KEY_PREFIX + user.getId();
           redisTemplate.opsForValue().set(key, user, USER_EXPIRE_TIME, TimeUnit.MINUTES);
           log.info("保存用户成功,key:{}", key);
           return true;
       } catch (Exception e) {
           log.error("保存用户失败", e);
           return false;
       }
   }

   /**
    * 根据用户ID查询用户信息
    * @param userId 用户ID
    * @return User 用户实体
    */

   public User getUserById(String userId) {
       if (StringUtils.isEmpty(userId)) {
           log.error("查询用户失败:用户ID为空");
           return null;
       }
       String key = USER_KEY_PREFIX + userId;
       return (User) redisTemplate.opsForValue().get(key);
   }

   /**
    * 批量查询用户信息
    * @param userIds 用户ID列表
    * @return Map<String, User> 用户ID-用户实体映射
    */

   public Map<String, User> batchGetUsers(String... userIds) {
       if (ObjectUtils.isEmpty(userIds)) {
           log.error("批量查询用户失败:用户ID数组为空");
           return Maps.newHashMap();
       }
       Map<String, User> userMap = Maps.newHashMap();
       for (String userId : userIds) {
           if (StringUtils.hasText(userId)) {
               String key = USER_KEY_PREFIX + userId;
               User user = (User) redisTemplate.opsForValue().get(key);
               if (!ObjectUtils.isEmpty(user)) {
                   userMap.put(userId, user);
               }
           }
       }
       return userMap;
   }

   /**
    * 删除用户信息
    * @param userId 用户ID
    * @return boolean 删除结果
    */

   public boolean deleteUser(String userId) {
       if (StringUtils.isEmpty(userId)) {
           log.error("删除用户失败:用户ID为空");
           return false;
       }
       String key = USER_KEY_PREFIX + userId;
       Boolean delete = redisTemplate.delete(key);
       return Boolean.TRUE.equals(delete);
   }
}

2.3.4 控制器类

package com.jam.demo.controller;

import com.jam.demo.entity.User;
import com.jam.demo.service.RedisSingleService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.util.ObjectUtils;

import javax.annotation.Resource;
import java.util.Map;

/**
* Redis单机版控制器
* @author ken
*/

@RestController
@RequestMapping("/redis/single")
@Slf4j
@Tag(name = "Redis单机版接口", description = "基于Redis单机版的用户信息操作接口")
public class RedisSingleController {

   @Resource
   private RedisSingleService redisSingleService;

   @PostMapping("/user")
   @Operation(summary = "保存用户信息", description = "将用户信息存入Redis,设置30分钟过期")
   public ResponseEntity<Boolean> saveUser(@RequestBody User user) {
       boolean result = redisSingleService.saveUser(user);
       return new ResponseEntity<>(result, HttpStatus.OK);
   }

   @GetMapping("/user/{userId}")
   @Operation(summary = "查询用户信息", description = "根据用户ID从Redis查询用户信息")
   public ResponseEntity<User> getUserById(
           @Parameter(description = "用户ID", required = true)
@PathVariable String userId) {
       User user = redisSingleService.getUserById(userId);
       return new ResponseEntity<>(user, HttpStatus.OK);
   }

   @GetMapping("/users")
   @Operation(summary = "批量查询用户信息", description = "根据多个用户ID批量查询Redis中的用户信息")
   public ResponseEntity<Map<String, User>> batchGetUsers(
           @Parameter(description = "用户ID列表(多个用逗号分隔)", required = true) @RequestParam String userIds) {
       if (ObjectUtils.isEmpty(userIds)) {
           return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
       }
       String[] userIdArray = userIds.split(",");
       Map<String, User> userMap = redisSingleService.batchGetUsers(userIdArray);
       return new ResponseEntity<>(userMap, HttpStatus.OK);
   }

   @DeleteMapping("/user/{userId}")
   @Operation(summary = "删除用户信息", description = "根据用户ID删除Redis中的用户信息")
   public ResponseEntity<Boolean> deleteUser(
           @Parameter(description = "用户ID", required = true)
@PathVariable String userId) {
       boolean result = redisSingleService.deleteUser(userId);
       return new ResponseEntity<>(result, HttpStatus.OK);
   }
}

2.3.5 实体类

package com.jam.demo.entity;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

import java.io.Serializable;

/**
* 用户实体类
* @author ken
*/

@Data
@Schema(description = "用户实体")
public class User implements Serializable {

   private static final long serialVersionUID = 1L;

   @Schema(description = "用户ID")
   private String id;

   @Schema(description = "用户名")
   private String username;

   @Schema(description = "用户年龄")
   private Integer age;

   @Schema(description = "用户邮箱")
   private String email;
}

2.4 优缺点与适用场景

优点

  • 部署简单,无额外架构设计成本;
  • 运维成本低,无需维护多个节点;
  • 性能损耗小,无数据同步、集群通信等开销。

缺点

  • 单点故障风险:节点宕机后整个Redis服务不可用;
  • 扩展性差:无法通过横向扩展提升性能和存储容量;
  • 持久化风险:若节点硬件故障,可能导致数据丢失(依赖RDB/AOF恢复)。

适用场景

  • 开发/测试环境;
  • 对可用性要求不高的小型应用(如个人项目、内部工具);
  • 临时数据缓存(如验证码、临时会话,可容忍数据丢失)。

三、主从复制部署:读写分离与数据备份

3.1 核心原理

主从复制是Redis高可用的基础方案,通过“一主多从”的架构实现:

  • 主节点(Master):负责接收写请求,同步数据到从节点;
  • 从节点(Slave):负责接收读请求,从主节点同步数据(只读,默认不允许写)。

核心价值:

  1. 读写分离:分摊主节点读压力,提升系统并发读能力;
  2. 数据备份:从节点存储主节点数据副本,降低数据丢失风险;
  3. 故障转移基础:主节点故障后,可手动将从节点提升为主节点(需配合人工操作)。

底层同步逻辑:

image.png

3.2 部署步骤(一主两从)

3.2.1 环境准备

  • 3台服务器(或同一服务器不同端口,本文用不同端口演示):
  • 主节点:127.0.0.1:6379
  • 从节点1:127.0.0.1:6380
  • 从节点2:127.0.0.1:6381
  • Redis版本:7.2.5

3.2.2 配置与启动

# 1. 复制3份配置文件
cd /usr/local/redis-7.2.5/
cp redis.conf redis-6379.conf
cp redis.conf redis-6380.conf
cp redis.conf redis-6381.conf

# 2. 配置主节点(redis-6379.conf)
vim redis-6379.conf
# 核心配置(同单机版,确保以下配置)
bind 0.0.0.0
protected-mode no
port 6379
daemonize yes
requirepass 123456
appendonly yes

# 3. 配置从节点1(redis-6380.conf)
vim redis-6380.conf
# 核心配置
bind 0.0.0.0
protected-mode no
port 6380
daemonize yes
requirepass 123456
appendonly yes
# 关键:指定主节点信息
replicaof 127.0.0.1 6379  # 主节点IP:端口
masterauth 123456  # 主节点密码(主节点有密码时必须配置)
replica-read-only yes  # 从节点只读(默认开启)

# 4. 配置从节点2(redis-6381.conf)
vim redis-6381.conf
# 核心配置(同从节点1,仅端口不同)
port 6381
replicaof 127.0.0.1 6379
masterauth 123456

# 5. 启动所有节点
redis-server redis-6379.conf
redis-server redis-6380.conf
redis-server redis-6381.conf

# 6. 验证主从关系
# 连接主节点
redis-cli -h 127.0.0.1 -p 6379 -a 123456 info replication
# 输出结果中应包含:role:master,connected_slaves:2,及两个从节点信息

# 连接从节点验证
redis-cli -h 127.0.0.1 -p 6380 -a 123456 info replication
# 输出结果中应包含:role:slave,master_host:127.0.0.1,master_port:6379

3.3 读写分离实战(Java代码)

3.3.1 扩展Redis配置(读写分离模板)

package com.jam.demo.config;

import com.alibaba.fastjson2.support.spring.data.redis.FastJson2RedisSerializer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.jedis.JedisClientConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.util.ObjectUtils;
import redis.clients.jedis.JedisPoolConfig;

import java.time.Duration;

/**
* Redis主从复制配置(读写分离)
* @author ken
*/

@Configuration
@Slf4j
public class RedisMasterSlaveConfig {

   // 主节点配置
   private static final String MASTER_HOST = "127.0.0.1";
   private static final int MASTER_PORT = 6379;
   private static final String MASTER_PASSWORD = "123456";

   // 从节点1配置
   private static final String SLAVE1_HOST = "127.0.0.1";
   private static final int SLAVE1_PORT = 6380;
   private static final String SLAVE1_PASSWORD = "123456";

   // 从节点2配置
   private static final String SLAVE2_HOST = "127.0.0.1";
   private static final int SLAVE2_PORT = 6381;
   private static final String SLAVE2_PASSWORD = "123456";

   /**
    * 连接池配置
    * @return JedisPoolConfig
    */

   @Bean
   public JedisPoolConfig jedisPoolConfig() {
       JedisPoolConfig poolConfig = new JedisPoolConfig();
       poolConfig.setMaxTotal(100); // 最大连接数
       poolConfig.setMaxIdle(20); // 最大空闲连接数
       poolConfig.setMinIdle(5); // 最小空闲连接数
       poolConfig.setMaxWait(Duration.ofMillis(3000)); // 最大等待时间
       poolConfig.setTestOnBorrow(true); // 借连接时测试可用性
       return poolConfig;
   }

   /**
    * 主节点连接工厂
    * @param poolConfig 连接池配置
    * @return JedisConnectionFactory
    */

   @Bean(name = "masterRedisConnectionFactory")
   public JedisConnectionFactory masterRedisConnectionFactory(JedisPoolConfig poolConfig) {
       RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
       config.setHostName(MASTER_HOST);
       config.setPort(MASTER_PORT);
       config.setPassword(MASTER_PASSWORD);

       JedisClientConfiguration clientConfig = JedisClientConfiguration.builder()
               .usePooling()
               .poolConfig(poolConfig)
               .build();

       return new JedisConnectionFactory(config, clientConfig);
   }

   /**
    * 从节点1连接工厂
    * @param poolConfig 连接池配置
    * @return JedisConnectionFactory
    */

   @Bean(name = "slave1RedisConnectionFactory")
   public JedisConnectionFactory slave1RedisConnectionFactory(JedisPoolConfig poolConfig) {
       RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
       config.setHostName(SLAVE1_HOST);
       config.setPort(SLAVE1_PORT);
       config.setPassword(SLAVE1_PASSWORD);

       JedisClientConfiguration clientConfig = JedisClientConfiguration.builder()
               .usePooling()
               .poolConfig(poolConfig)
               .build();

       return new JedisConnectionFactory(config, clientConfig);
   }

   /**
    * 主节点RedisTemplate(写操作)
    * @return RedisTemplate<String, Object>
    */

   @Bean(name = "masterRedisTemplate")
   public RedisTemplate<String, Object> masterRedisTemplate() {
       RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
       redisTemplate.setConnectionFactory(masterRedisConnectionFactory(jedisPoolConfig()));
       setSerializer(redisTemplate);
       redisTemplate.afterPropertiesSet();
       return redisTemplate;
   }

   /**
    * 从节点RedisTemplate(读操作)
    * @return RedisTemplate<String, Object>
    */

   @Bean(name = "slaveRedisTemplate")
   public RedisTemplate<String, Object> slaveRedisTemplate() {
       RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
       // 简单轮询选择从节点(生产环境可使用更复杂的负载均衡策略)
       redisTemplate.setConnectionFactory(slave1RedisConnectionFactory(jedisPoolConfig()));
       setSerializer(redisTemplate);
       redisTemplate.afterPropertiesSet();
       return redisTemplate;
   }

   /**
    * 设置序列化器
    * @param redisTemplate RedisTemplate
    */

   private void setSerializer(RedisTemplate<String, Object> redisTemplate) {
       StringRedisSerializer stringSerializer = new StringRedisSerializer();
       FastJson2RedisSerializer<Object> fastJsonSerializer = new FastJson2RedisSerializer<>(Object.class);

       redisTemplate.setKeySerializer(stringSerializer);
       redisTemplate.setHashKeySerializer(stringSerializer);
       redisTemplate.setValueSerializer(fastJsonSerializer);
       redisTemplate.setHashValueSerializer(fastJsonSerializer);
   }
}

3.3.2 读写分离服务类

package com.jam.demo.service;

import com.google.common.collect.Maps;
import com.jam.demo.entity.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
* Redis主从复制服务(读写分离)
* @author ken
*/

@Service
@Slf4j
public class RedisMasterSlaveService {

   // 主节点Template(写操作)
   @Resource(name = "masterRedisTemplate")
   private RedisTemplate<String, Object> masterRedisTemplate;

   // 从节点Template(读操作)
   @Resource(name = "slaveRedisTemplate")
   private RedisTemplate<String, Object> slaveRedisTemplate;

   private static final String USER_KEY_PREFIX = "user:";
   private static final long USER_EXPIRE_TIME = 30L;

   /**
    * 保存用户(写操作,走主节点)
    * @param user 用户实体
    * @return boolean
    */

   public boolean saveUser(User user) {
       if (ObjectUtils.isEmpty(user) || StringUtils.isEmpty(user.getId())) {
           log.error("保存用户失败:用户信息无效");
           return false;
       }
       try {
           String key = USER_KEY_PREFIX + user.getId();
           masterRedisTemplate.opsForValue().set(key, user, USER_EXPIRE_TIME, TimeUnit.MINUTES);
           log.info("主节点保存用户成功,key:{}", key);
           return true;
       } catch (Exception e) {
           log.error("主节点保存用户失败", e);
           return false;
       }
   }

   /**
    * 查询用户(读操作,走从节点)
    * @param userId 用户ID
    * @return User
    */

   public User getUserById(String userId) {
       if (StringUtils.isEmpty(userId)) {
           log.error("查询用户失败:用户ID为空");
           return null;
       }
       try {
           String key = USER_KEY_PREFIX + userId;
           User user = (User) slaveRedisTemplate.opsForValue().get(key);
           log.info("从节点查询用户,key:{},结果:{}", key, ObjectUtils.isEmpty(user) ? "空" : "存在");
           return user;
       } catch (Exception e) {
           log.error("从节点查询用户失败", e);
           // 从节点故障时降级到主节点查询
           String key = USER_KEY_PREFIX + userId;
           return (User) masterRedisTemplate.opsForValue().get(key);
       }
   }

   /**
    * 批量查询用户(读操作,走从节点)
    * @param userIds 用户ID列表
    * @return Map<String, User>
    */

   public Map<String, User> batchGetUsers(String... userIds) {
       if (ObjectUtils.isEmpty(userIds)) {
           log.error("批量查询用户失败:用户ID数组为空");
           return Maps.newHashMap();
       }
       Map<String, User> userMap = Maps.newHashMap();
       try {
           for (String userId : userIds) {
               if (StringUtils.hasText(userId)) {
                   String key = USER_KEY_PREFIX + userId;
                   User user = (User) slaveRedisTemplate.opsForValue().get(key);
                   if (!ObjectUtils.isEmpty(user)) {
                       userMap.put(userId, user);
                   }
               }
           }
           log.info("从节点批量查询用户完成,查询数量:{},成功数量:{}", userIds.length, userMap.size());
       } catch (Exception e) {
           log.error("从节点批量查询用户失败,降级到主节点", e);
           // 降级到主节点
           for (String userId : userIds) {
               if (StringUtils.hasText(userId)) {
                   String key = USER_KEY_PREFIX + userId;
                   User user = (User) masterRedisTemplate.opsForValue().get(key);
                   if (!ObjectUtils.isEmpty(user)) {
                       userMap.put(userId, user);
                   }
               }
           }
       }
       return userMap;
   }

   /**
    * 删除用户(写操作,走主节点)
    * @param userId 用户ID
    * @return boolean
    */

   public boolean deleteUser(String userId) {
       if (StringUtils.isEmpty(userId)) {
           log.error("删除用户失败:用户ID为空");
           return false;
       }
       try {
           String key = USER_KEY_PREFIX + userId;
           Boolean delete = masterRedisTemplate.delete(key);
           log.info("主节点删除用户,key:{},结果:{}", key, delete);
           return Boolean.TRUE.equals(delete);
       } catch (Exception e) {
           log.error("主节点删除用户失败", e);
           return false;
       }
   }
}

3.4 主从复制核心问题与解决方案

3.4.1 数据延迟问题

  • 问题:主节点写操作后,数据同步到从节点存在延迟,可能导致从节点查询到旧数据;
  • 解决方案:
  1. 优化同步策略:主节点开启repl-diskless-sync yes(无盘同步),减少RDB文件写入磁盘的耗时;
  2. 控制从节点数量:从节点过多会增加主节点同步压力,建议不超过3个;
  3. 关键读场景路由主节点:对数据一致性要求高的读操作(如支付结果查询),直接走主节点。

3.4.2 主节点故障后的数据一致性问题

  • 问题:主节点故障时,可能存在部分写操作未同步到从节点,导致数据丢失;
  • 解决方案:
  1. 开启AOF持久化:主节点配置appendonly yesappendfsync everysec,确保写操作持久化到磁盘;
  2. 配置从节点同步确认:主节点开启min-replicas-to-write 1(至少1个从节点同步完成才确认写成功),min-replicas-max-lag 10(同步延迟不超过10秒),牺牲部分性能换数据一致性。

3.5 优缺点与适用场景

优点

  • 读写分离,提升并发读能力;
  • 数据备份,降低数据丢失风险;
  • 部署相对简单,在单机版基础上扩展即可。

缺点

  • 主节点故障后需人工干预切换,无法自动故障转移;
  • 无法解决主节点单点故障导致的写服务不可用问题;
  • 不支持横向扩展存储容量(所有节点存储全量数据)。

适用场景

  • 读多写少的场景(如电商商品详情、新闻资讯);
  • 对可用性有一定要求,但可容忍短时间人工干预的中小型应用;
  • 需要数据备份,但预算有限的场景。

四、哨兵模式部署:自动故障转移

4.1 核心原理

哨兵模式(Sentinel)是在主从复制基础上实现的“自动故障转移”方案,通过引入“哨兵节点”监控主从节点状态:

  • 哨兵节点(Sentinel):独立于主从节点的进程,负责监控主从节点健康状态、执行故障转移、通知客户端;
  • 核心功能:
  1. 监控(Monitoring):持续检查主从节点是否正常运行;
  2. 通知(Notification):当节点故障时,通过API通知管理员或其他应用;
  3. 自动故障转移(Automatic Failover):主节点故障时,自动将最优从节点提升为主节点,更新其他从节点的主节点信息,并通知客户端。

故障转移流程:

image.png

哨兵集群原理:

  • 哨兵节点之间通过 gossip 协议交换节点状态信息;
  • 主节点故障判断需满足“主观下线”+“客观下线”:
  • 主观下线(SDOWN):单个哨兵认为主节点不可用;
  • 客观下线(ODOWN):超过quorum(配置的阈值)个哨兵认为主节点不可用,才确认主节点故障。

4.2 部署步骤(一主两从三哨兵)

4.2.1 环境准备

  • 主从节点:同3.2.1(主6379,从6380、6381);
  • 哨兵节点:3个(端口26379、26380、26381);
  • Redis版本:7.2.5。

4.2.2 哨兵配置与启动

# 1. 复制3份哨兵配置文件
cd /usr/local/redis-7.2.5/
cp sentinel.conf sentinel-26379.conf
cp sentinel.conf sentinel-26380.conf
cp sentinel.conf sentinel-26381.conf

# 2. 配置哨兵节点1(sentinel-26379.conf)
vim sentinel-26379.conf
# 核心配置
bind 0.0.0.0
protected-mode no
port 26379
daemonize yes
logfile "/var/log/redis/sentinel-26379.log"  # 日志文件
# 监控主节点:sentinel monitor <主节点名称> <主节点IP> <主节点端口> <quorum阈值>
sentinel monitor mymaster 127.0.0.1 6379 2  # 2个哨兵确认即客观下线
# 主节点密码
sentinel auth-pass mymaster 123456
# 主节点无响应超时时间(默认30秒,单位毫秒)
sentinel down-after-milliseconds mymaster 30000
# 故障转移超时时间(默认180秒)
sentinel failover-timeout mymaster 180000
# 故障转移时,最多有多少个从节点同时同步新主节点(默认1,避免占用过多带宽)
sentinel parallel-syncs mymaster 1

# 3. 配置哨兵节点2(sentinel-26380.conf)
vim sentinel-26380.conf
# 核心配置(仅端口、日志文件不同)
port 26380
logfile "/var/log/redis/sentinel-26380.log"
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel auth-pass mymaster 123456

# 4. 配置哨兵节点3(sentinel-26381.conf)
vim sentinel-26381.conf
# 核心配置(仅端口、日志文件不同)
port 26381
logfile "/var/log/redis/sentinel-26381.log"
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel auth-pass mymaster 123456

# 5. 创建日志目录
mkdir -p /var/log/redis/

# 6. 启动哨兵节点
redis-sentinel sentinel-26379.conf
redis-sentinel sentinel-26380.conf
redis-sentinel sentinel-26381.conf

# 7. 验证哨兵状态
redis-cli -h 127.0.0.1 -p 26379 info sentinel
# 输出结果中应包含:sentinel_masters:1,sentinel_monitored_slaves:2,sentinel_running_sentinels:3

4.3 Java实战代码(连接哨兵集群)

4.3.1 依赖补充(同3.3.1,无需额外依赖)

4.3.2 哨兵模式配置类

package com.jam.demo.config;

import com.alibaba.fastjson2.support.spring.data.redis.FastJson2RedisSerializer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisNode;
import org.springframework.data.redis.connection.RedisSentinelConfiguration;
import org.springframework.data.redis.connection.jedis.JedisClientConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.util.ObjectUtils;
import redis.clients.jedis.JedisPoolConfig;

import java.time.Duration;
import java.util.HashSet;
import java.util.Set;

/**
* Redis哨兵模式配置
* @author ken
*/

@Configuration
@Slf4j
public class RedisSentinelConfig {

   // 主节点名称(需与哨兵配置一致)
   private static final String MASTER_NAME = "mymaster";
   // 哨兵节点列表
   private static final String SENTINEL_NODES = "127.0.0.1:26379,127.0.0.1:26380,127.0.0.1:26381";
   // Redis密码
   private static final String REDIS_PASSWORD = "123456";

   /**
    * 连接池配置
    * @return JedisPoolConfig
    */

   @Bean
   public JedisPoolConfig jedisPoolConfig() {
       JedisPoolConfig poolConfig = new JedisPoolConfig();
       poolConfig.setMaxTotal(100);
       poolConfig.setMaxIdle(20);
       poolConfig.setMinIdle(5);
       poolConfig.setMaxWait(Duration.ofMillis(3000));
       poolConfig.setTestOnBorrow(true);
       return poolConfig;
   }

   /**
    * 哨兵配置
    * @return RedisSentinelConfiguration
    */

   @Bean
   public RedisSentinelConfiguration redisSentinelConfiguration() {
       RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration();
       sentinelConfig.setMasterName(MASTER_NAME);
       sentinelConfig.setPassword(REDIS_PASSWORD);

       // 解析哨兵节点
       Set<RedisNode> sentinelNodeSet = new HashSet<>();
       String[] sentinelNodeArray = SENTINEL_NODES.split(",");
       for (String node : sentinelNodeArray) {
           if (ObjectUtils.isEmpty(node)) {
               continue;
           }
           String[] hostPort = node.split(":");
           if (hostPort.length != 2) {
               log.error("哨兵节点格式错误:{}", node);
               continue;
           }
           sentinelNodeSet.add(new RedisNode(hostPort[0], Integer.parseInt(hostPort[1])));
       }
       sentinelConfig.setSentinels(sentinelNodeSet);
       return sentinelConfig;
   }

   /**
    * 哨兵模式连接工厂
    * @return JedisConnectionFactory
    */

   @Bean
   public JedisConnectionFactory jedisConnectionFactory() {
       RedisSentinelConfiguration sentinelConfig = redisSentinelConfiguration();
       JedisClientConfiguration clientConfig = JedisClientConfiguration.builder()
               .usePooling()
               .poolConfig(jedisPoolConfig())
               .build();

       JedisConnectionFactory connectionFactory = new JedisConnectionFactory(sentinelConfig, clientConfig);
       connectionFactory.afterPropertiesSet();
       return connectionFactory;
   }

   /**
    * 哨兵模式RedisTemplate
    * @return RedisTemplate<String, Object>
    */

   @Bean
   public RedisTemplate<String, Object> sentinelRedisTemplate() {
       RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
       redisTemplate.setConnectionFactory(jedisConnectionFactory());

       // 序列化配置
       StringRedisSerializer stringSerializer = new StringRedisSerializer();
       FastJson2RedisSerializer<Object> fastJsonSerializer = new FastJson2RedisSerializer<>(Object.class);

       redisTemplate.setKeySerializer(stringSerializer);
       redisTemplate.setHashKeySerializer(stringSerializer);
       redisTemplate.setValueSerializer(fastJsonSerializer);
       redisTemplate.setHashValueSerializer(fastJsonSerializer);

       redisTemplate.afterPropertiesSet();
       log.info("Redis哨兵模式Template初始化成功");
       return redisTemplate;
   }
}

4.3.3 哨兵模式服务类

package com.jam.demo.service;

import com.google.common.collect.Maps;
import com.jam.demo.entity.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
* Redis哨兵模式服务
* @author ken
*/

@Service
@Slf4j
public class RedisSentinelService {

   @Resource
   private RedisTemplate<String, Object> sentinelRedisTemplate;

   private static final String USER_KEY_PREFIX = "user:";
   private static final long USER_EXPIRE_TIME = 30L;

   /**
    * 保存用户(自动路由到主节点)
    * @param user 用户实体
    * @return boolean
    */

   public boolean saveUser(User user) {
       if (ObjectUtils.isEmpty(user) || StringUtils.isEmpty(user.getId())) {
           log.error("保存用户失败:用户信息无效");
           return false;
       }
       try {
           String key = USER_KEY_PREFIX + user.getId();
           sentinelRedisTemplate.opsForValue().set(key, user, USER_EXPIRE_TIME, TimeUnit.MINUTES);
           log.info("保存用户成功,key:{}", key);
           return true;
       } catch (Exception e) {
           log.error("保存用户失败", e);
           return false;
       }
   }

   /**
    * 查询用户(自动路由到从节点,故障时路由到新主节点)
    * @param userId 用户ID
    * @return User
    */

   public User getUserById(String userId) {
       if (StringUtils.isEmpty(userId)) {
           log.error("查询用户失败:用户ID为空");
           return null;
       }
       try {
           String key = USER_KEY_PREFIX + userId;
           User user = (User) sentinelRedisTemplate.opsForValue().get(key);
           log.info("查询用户,key:{},结果:{}", key, ObjectUtils.isEmpty(user) ? "空" : "存在");
           return user;
       } catch (Exception e) {
           log.error("查询用户失败", e);
           return null;
       }
   }

   /**
    * 批量查询用户
    * @param userIds 用户ID列表
    * @return Map<String, User>
    */

   public Map<String, User> batchGetUsers(String... userIds) {
       if (ObjectUtils.isEmpty(userIds)) {
           log.error("批量查询用户失败:用户ID数组为空");
           return Maps.newHashMap();
       }
       Map<String, User> userMap = Maps.newHashMap();
       try {
           for (String userId : userIds) {
               if (StringUtils.hasText(userId)) {
                   String key = USER_KEY_PREFIX + userId;
                   User user = (User) sentinelRedisTemplate.opsForValue().get(key);
                   if (!ObjectUtils.isEmpty(user)) {
                       userMap.put(userId, user);
                   }
               }
           }
           log.info("批量查询用户完成,查询数量:{},成功数量:{}", userIds.length, userMap.size());
       } catch (Exception e) {
           log.error("批量查询用户失败", e);
       }
       return userMap;
   }

   /**
    * 删除用户(自动路由到主节点)
    * @param userId 用户ID
    * @return boolean
    */

   public boolean deleteUser(String userId) {
       if (StringUtils.isEmpty(userId)) {
           log.error("删除用户失败:用户ID为空");
           return false;
       }
       try {
           String key = USER_KEY_PREFIX + userId;
           Boolean delete = sentinelRedisTemplate.delete(key);
           log.info("删除用户,key:{},结果:{}", key, delete);
           return Boolean.TRUE.equals(delete);
       } catch (Exception e) {
           log.error("删除用户失败", e);
           return false;
       }
   }
}

4.4 哨兵模式核心参数优化

4.4.1 quorum阈值配置

  • 配置:sentinel monitor mymaster 127.0.0.1 6379 2
  • 优化建议:quorum值建议设置为“哨兵节点数量/2 + 1”,确保故障判断的准确性;
  • 3个哨兵:quorum=2;
  • 5个哨兵:quorum=3。

4.4.2 超时时间配置

  • down-after-milliseconds:主节点无响应超时时间,默认30秒;
  • 优化建议:根据业务响应时间调整,如高并发场景可设为10秒(10000毫秒);
  • failover-timeout:故障转移超时时间,默认180秒;
  • 优化建议:网络环境好的场景可设为60秒(60000毫秒),避免故障转移耗时过长。

4.4.3 并行同步数量配置

  • parallel-syncs:故障转移时同时同步新主节点的从节点数量,默认1;
  • 优化建议:从节点数量多、带宽充足时,可设为2-3,加快同步速度;带宽有限时保持默认1,避免带宽占用过高。

4.5 优缺点与适用场景

优点

  • 自动故障转移,无需人工干预,提升系统可用性;
  • 继承主从复制的读写分离、数据备份优势;
  • 哨兵集群部署,避免哨兵单点故障。

缺点

  • 不支持横向扩展存储容量(所有节点存储全量数据);
  • 故障转移期间存在短暂的写服务不可用(毫秒级到秒级);
  • 部署和运维复杂度高于主从复制(需维护哨兵集群)。

适用场景

  • 对可用性要求较高,无法容忍人工干预故障转移的场景;
  • 读多写少,需要数据备份和自动容错的中小型到中大型应用;
  • 不要求存储容量横向扩展的场景(如缓存、会话存储)。

五、Redis Cluster 集群部署:分片存储与高可用

5.1 核心原理

Redis Cluster 是 Redis 官方提供的分布式解决方案,核心解决「海量数据存储」和「高并发读写」两大痛点,通过「分片存储」+「自动高可用」的设计,实现横向扩展和容错能力。其核心逻辑基于「哈希槽(Hash Slot)」和「主从分片」:

5.1.1 哈希槽机制

Redis Cluster 将整个键空间划分为 16384 个哈希槽(编号 0-16383),数据存储的核心规则:

  • 每个节点负责一部分哈希槽(如 3 主节点时,每个主节点约负责 5461 个槽);
  • 数据路由:通过 CRC16(key) % 16384 计算键对应的哈希槽,将数据存储到负责该槽的主节点;
  • 槽迁移:支持动态迁移哈希槽,实现集群扩容/缩容,不影响业务运行。

5.1.2 主从分片架构

为保证高可用,Redis Cluster 要求每个主节点必须配置至少 1 个从节点,形成「主从分片」:

  • 主节点(Master):负责哈希槽的读写操作,同步数据到从节点;
  • 从节点(Slave):仅作为备份,主节点故障时自动升级为主节点,接管哈希槽;
  • 集群最小部署单元:3 主 3 从(确保任意主节点故障后,集群仍能正常运行)。

5.1.3 核心通信机制

集群节点间通过「Gossip 协议」实时交换节点状态信息(如节点存活、哈希槽分配、故障状态),无需中心化协调节点,保证集群去中心化特性。

5.1.4 故障转移流程

image.png

5.1.5 集群架构图

image.png

5.2 部署步骤(3 主 3 从)

5.2.1 环境准备

  • 6 个节点(同一服务器不同端口,生产环境建议独立服务器):
  • 主节点:6379、6380、6381
  • 从节点:6382(对应 6379)、6383(对应 6380)、6384(对应 6381)
  • Redis 版本:7.2.5(最新稳定版)
  • 关闭防火墙或开放对应端口:6379-6384、16379-16384(Redis Cluster 节点间通信端口为节点端口+10000)

5.2.2 配置与启动

# 1. 创建节点配置目录
mkdir -p /usr/local/redis-cluster/{6379,6380,6381,6382,6383,6384}

# 2. 复制 Redis 核心文件到各节点目录
cd /usr/local/redis-7.2.5/
cp redis.conf /usr/local/redis-cluster/6379/
cp redis.conf /usr/local/redis-cluster/6380/
cp redis.conf /usr/local/redis-cluster/6381/
cp redis.conf /usr/local/redis-cluster/6382/
cp redis.conf /usr/local/redis-cluster/6383/
cp redis.conf /usr/local/redis-cluster/6384/

# 3. 统一修改所有节点配置(以 6379 为例,其他节点仅端口不同)
vim /usr/local/redis-cluster/6379/redis.conf
# 核心配置项
bind 0.0.0.0
protected-mode no
port 6379
daemonize yes
requirepass 123456  # 节点密码
masterauth 123456   # 主从同步密码(与 requirepass 一致)
appendonly yes      # 开启 AOF 持久化
cluster-enabled yes  # 开启集群模式
cluster-config-file nodes-6379.conf  # 集群配置文件(自动生成)
cluster-node-timeout 15000  # 节点超时时间(15 秒,故障转移判断依据)
cluster-require-full-coverage no  # 非全槽覆盖时集群仍可用(避免部分槽故障导致集群不可用)

# 4. 批量修改其他节点配置(替换端口)
for port in 6380 6381 6382 6383 6384; do
   sed "s/6379/$port/g" /usr/local/redis-cluster/6379/redis.conf > /usr/local/redis-cluster/$port/redis.conf
done

# 5. 启动所有节点
for port in 6379 6380 6381 6382 6383 6384; do
   redis-server /usr/local/redis-cluster/$port/redis.conf
done

# 6. 验证节点启动状态
ps -ef | grep redis-server | grep -v grep
# 应显示 6 个 redis-server 进程,分别对应 6379-6384 端口

# 7. 创建 Redis Cluster 集群
redis-cli --cluster create \
127.0.0.1:6379 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 \
--cluster-replicas 1 \
-a 123456

# 参数说明:
# --cluster-replicas 1:每个主节点对应 1 个从节点
# -a 123456:节点密码(所有节点密码必须一致)

# 执行后会提示哈希槽分配方案,输入 yes 确认
# 示例输出:
# [OK] All 16384 slots covered. 表示集群创建成功

# 8. 验证集群状态
redis-cli -h 127.0.0.1 -p 6379 -a 123456 cluster info
# 关键输出:cluster_state:ok(集群正常)、cluster_slots_assigned:16384(所有槽已分配)

# 9. 查看节点信息
redis-cli -h 127.0.0.1 -p 6379 -a 123456 cluster nodes
# 输出包含各节点角色(master/slave)、哈希槽分配、主从对应关系

5.3 Java 实战代码(连接 Redis Cluster)

5.3.1 依赖补充(同 3.3.1,无需额外依赖)

5.3.2 Redis Cluster 配置类

package com.jam.demo.config;

import com.alibaba.fastjson2.support.spring.data.redis.FastJson2RedisSerializer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisNode;
import org.springframework.data.redis.connection.jedis.JedisClientConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.util.ObjectUtils;
import redis.clients.jedis.JedisPoolConfig;

import java.time.Duration;
import java.util.HashSet;
import java.util.Set;

/**
* Redis Cluster 集群配置
* @author ken
*/

@Configuration
@Slf4j
public class RedisClusterConfig {

   // 集群节点列表(格式:ip:port)
   private static final String CLUSTER_NODES = "127.0.0.1:6379,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";
   // Redis 密码
   private static final String REDIS_PASSWORD = "123456";
   // 集群最大重定向次数(默认 5)
   private static final int MAX_REDIRECTS = 3;

   /**
    * 连接池配置(优化性能)
    * @return JedisPoolConfig
    */

   @Bean
   public JedisPoolConfig jedisPoolConfig() {
       JedisPoolConfig poolConfig = new JedisPoolConfig();
       poolConfig.setMaxTotal(200);         // 最大连接数(根据业务调整)
       poolConfig.setMaxIdle(50);          // 最大空闲连接数
       poolConfig.setMinIdle(10);          // 最小空闲连接数
       poolConfig.setMaxWait(Duration.ofMillis(5000)); // 最大等待时间(5 秒)
       poolConfig.setTestOnBorrow(true);   // 借连接时验证可用性
       poolConfig.setTestOnReturn(true);   // 还连接时验证可用性
       poolConfig.setTestWhileIdle(true);  // 空闲时验证可用性(避免死连接)
       return poolConfig;
   }

   /**
    * Redis Cluster 配置
    * @return RedisClusterConfiguration
    */

   @Bean
   public RedisClusterConfiguration redisClusterConfiguration() {
       RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration();
       clusterConfig.setPassword(REDIS_PASSWORD);
       clusterConfig.setMaxRedirects(MAX_REDIRECTS);

       // 解析集群节点
       Set<RedisNode> nodeSet = new HashSet<>();
       String[] nodes = CLUSTER_NODES.split(",");
       for (String node : nodes) {
           if (ObjectUtils.isEmpty(node)) {
               continue;
           }
           String[] hostPort = node.split(":");
           if (hostPort.length != 2) {
               log.error("Redis Cluster 节点格式错误:{}", node);
               throw new IllegalArgumentException("Redis Cluster 节点格式错误,正确格式:ip:port");
           }
           nodeSet.add(new RedisNode(hostPort[0], Integer.parseInt(hostPort[1])));
       }
       clusterConfig.setClusterNodes(nodeSet);
       return clusterConfig;
   }

   /**
    * Cluster 模式连接工厂
    * @return JedisConnectionFactory
    */

   @Bean
   public JedisConnectionFactory jedisConnectionFactory() {
       RedisClusterConfiguration clusterConfig = redisClusterConfiguration();
       JedisClientConfiguration clientConfig = JedisClientConfiguration.builder()
               .usePooling()
               .poolConfig(jedisPoolConfig())
               .connectTimeout(Duration.ofMillis(3000))  // 连接超时时间
               .readTimeout(Duration.ofMillis(3000))     // 读取超时时间
               .build();

       JedisConnectionFactory connectionFactory = new JedisConnectionFactory(clusterConfig, clientConfig);
       connectionFactory.afterPropertiesSet();
       log.info("Redis Cluster 连接工厂初始化成功");
       return connectionFactory;
   }

   /**
    * 自定义 RedisTemplate(适配 Cluster 模式,FastJSON2 序列化)
    * @return RedisTemplate<String, Object>
    */

   @Bean
   public RedisTemplate<String, Object> clusterRedisTemplate() {
       RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
       redisTemplate.setConnectionFactory(jedisConnectionFactory());

       // 序列化配置(与单机/主从/哨兵模式一致,保证序列化统一)
       StringRedisSerializer stringSerializer = new StringRedisSerializer();
       FastJson2RedisSerializer<Object> fastJsonSerializer = new FastJson2RedisSerializer<>(Object.class);

       redisTemplate.setKeySerializer(stringSerializer);
       redisTemplate.setHashKeySerializer(stringSerializer);
       redisTemplate.setValueSerializer(fastJsonSerializer);
       redisTemplate.setHashValueSerializer(fastJsonSerializer);

       redisTemplate.afterPropertiesSet();
       log.info("Redis Cluster RedisTemplate 初始化成功");
       return redisTemplate;
   }
}

5.3.3 集群模式业务服务类

package com.jam.demo.service;

import com.google.common.collect.Maps;
import com.jam.demo.entity.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
* Redis Cluster 集群业务服务
* @author ken
*/

@Service
@Slf4j
public class RedisClusterService {

   @Resource
   private RedisTemplate<String, Object> clusterRedisTemplate;

   private static final String USER_KEY_PREFIX = "user:";
   private static final long USER_EXPIRE_TIME = 30L; // 30 分钟过期

   /**
    * 保存用户(自动路由到对应哈希槽的主节点)
    * @param user 用户实体
    * @return boolean 保存结果
    */

   public boolean saveUser(User user) {
       if (ObjectUtils.isEmpty(user) || StringUtils.isEmpty(user.getId())) {
           log.error("保存用户失败:用户信息为空或 ID 无效");
           return false;
       }
       try {
           String key = USER_KEY_PREFIX + user.getId();
           // 自动路由:根据 key 计算哈希槽,定位到对应主节点
           clusterRedisTemplate.opsForValue().set(key, user, USER_EXPIRE_TIME, TimeUnit.MINUTES);
           log.info("Cluster 模式保存用户成功,key:{}", key);
           return true;
       } catch (Exception e) {
           log.error("Cluster 模式保存用户失败", e);
           return false;
       }
   }

   /**
    * 根据 ID 查询用户(自动路由到对应哈希槽的节点)
    * @param userId 用户 ID
    * @return User 用户实体
    */

   public User getUserById(String userId) {
       if (StringUtils.isEmpty(userId)) {
           log.error("查询用户失败:用户 ID 为空");
           return null;
       }
       try {
           String key = USER_KEY_PREFIX + userId;
           User user = (User) clusterRedisTemplate.opsForValue().get(key);
           log.info("Cluster 模式查询用户,key:{},结果:{}", key, ObjectUtils.isEmpty(user) ? "空" : "存在");
           return user;
       } catch (Exception e) {
           log.error("Cluster 模式查询用户失败", e);
           return null;
       }
   }

   /**
    * 批量查询用户(注意:若 keys 分布在不同槽,会触发多节点查询)
    * @param userIds 用户 ID 列表
    * @return Map<String, User> 用户 ID-实体映射
    */

   public Map<String, User> batchGetUsers(String... userIds) {
       if (ObjectUtils.isEmpty(userIds)) {
           log.error("批量查询用户失败:用户 ID 数组为空");
           return Maps.newHashMap();
       }
       Map<String, User> userMap = Maps.newHashMap();
       try {
           for (String userId : userIds) {
               if (StringUtils.hasText(userId)) {
                   String key = USER_KEY_PREFIX + userId;
                   User user = (User) clusterRedisTemplate.opsForValue().get(key);
                   if (!ObjectUtils.isEmpty(user)) {
                       userMap.put(userId, user);
                   }
               }
           }
           log.info("Cluster 模式批量查询用户完成,查询数量:{},成功数量:{}", userIds.length, userMap.size());
       } catch (Exception e) {
           log.error("Cluster 模式批量查询用户失败", e);
       }
       return userMap;
   }

   /**
    * 删除用户(自动路由到对应哈希槽的主节点)
    * @param userId 用户 ID
    * @return boolean 删除结果
    */

   public boolean deleteUser(String userId) {
       if (StringUtils.isEmpty(userId)) {
           log.error("删除用户失败:用户 ID 为空");
           return false;
       }
       try {
           String key = USER_KEY_PREFIX + userId;
           Boolean delete = clusterRedisTemplate.delete(key);
           log.info("Cluster 模式删除用户,key:{},结果:{}", key, delete);
           return Boolean.TRUE.equals(delete);
       } catch (Exception e) {
           log.error("Cluster 模式删除用户失败", e);
           return false;
       }
   }

   /**
    * 验证集群容错性(模拟主节点故障后的数据一致性)
    * @param userId 用户 ID
    * @return boolean 故障后是否能正常查询
    */

   public boolean testClusterFaultTolerance(String userId) {
       if (StringUtils.isEmpty(userId)) {
           log.error("验证集群容错性失败:用户 ID 为空");
           return false;
       }
       String key = USER_KEY_PREFIX + userId;
       // 1. 先查询数据(确认存在)
       User user = (User) clusterRedisTemplate.opsForValue().get(key);
       if (ObjectUtils.isEmpty(user)) {
           log.error("验证失败:用户数据不存在,key:{}", key);
           return false;
       }

       // 2. 模拟主节点故障(实际生产环境无需手动执行,集群自动检测)
       log.warn("模拟主节点故障:可通过命令停止对应主节点,如 redis-cli -p 6379 -a 127.0.0.1 shutdown");

       // 3. 故障转移后再次查询(验证数据一致性)
       try {
           // 等待故障转移完成(15-30 秒)
           Thread.sleep(20000);
           User faultUser = (User) clusterRedisTemplate.opsForValue().get(key);
           boolean result = !ObjectUtils.isEmpty(faultUser);
           log.info("集群容错性验证结果:{},故障后查询用户:{}", result, faultUser);
           return result;
       } catch (InterruptedException e) {
           log.error("验证集群容错性失败", e);
           Thread.currentThread().interrupt();
           return false;
       }
   }
}

5.3.4 集群模式控制器类

package com.jam.demo.controller;

import com.jam.demo.entity.User;
import com.jam.demo.service.RedisClusterService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.util.ObjectUtils;

import javax.annotation.Resource;
import java.util.Map;

/**
* Redis Cluster 集群接口
* @author ken
*/

@RestController
@RequestMapping("/redis/cluster")
@Slf4j
@Tag(name = "Redis Cluster 集群接口", description = "基于 Redis Cluster 集群的用户信息操作接口(支持分片存储和自动高可用)")
public class RedisClusterController {

   @Resource
   private RedisClusterService redisClusterService;

   @PostMapping("/user")
   @Operation(summary = "保存用户信息", description = "自动路由到对应哈希槽的主节点,设置 30 分钟过期")
   public ResponseEntity<Boolean> saveUser(@RequestBody User user) {
       boolean result = redisClusterService.saveUser(user);
       return new ResponseEntity<>(result, HttpStatus.OK);
   }

   @GetMapping("/user/{userId}")
   @Operation(summary = "查询用户信息", description = "自动路由到对应哈希槽的节点(主/从)")
   public ResponseEntity<User> getUserById(
           @Parameter(description = "用户 ID", required = true)
@PathVariable String userId) {
       User user = redisClusterService.getUserById(userId);
       return new ResponseEntity<>(user, HttpStatus.OK);
   }

   @GetMapping("/users")
   @Operation(summary = "批量查询用户信息", description = "多 ID 可能分布在不同槽,触发多节点并行查询")
   public ResponseEntity<Map<String, User>> batchGetUsers(
           @Parameter(description = "用户 ID 列表(多个用逗号分隔)", required = true) @RequestParam String userIds) {
       if (ObjectUtils.isEmpty(userIds)) {
           return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
       }
       String[] userIdArray = userIds.split(",");
       Map<String, User> userMap = redisClusterService.batchGetUsers(userIdArray);
       return new ResponseEntity<>(userMap, HttpStatus.OK);
   }

   @DeleteMapping("/user/{userId}")
   @Operation(summary = "删除用户信息", description = "自动路由到对应哈希槽的主节点执行删除")
   public ResponseEntity<Boolean> deleteUser(
           @Parameter(description = "用户 ID", required = true)
@PathVariable String userId) {
       boolean result = redisClusterService.deleteUser(userId);
       return new ResponseEntity<>(result, HttpStatus.OK);
   }

   @GetMapping("/fault-tolerance/{userId}")
   @Operation(summary = "验证集群容错性", description = "模拟主节点故障后,验证是否能正常查询数据(需等待 20 秒故障转移)")
   public ResponseEntity<Boolean> testFaultTolerance(
           @Parameter(description = "用户 ID(需提前保存)", required = true)
@PathVariable String userId) {
       boolean result = redisClusterService.testClusterFaultTolerance(userId);
       return new ResponseEntity<>(result, HttpStatus.OK);
   }
}

5.4 Redis Cluster 核心问题与解决方案

5.4.1 跨槽操作限制

  • 问题:Redis Cluster 不支持跨哈希槽的批量操作(如 MSET 多个 key 分布在不同槽、KEYS 通配符匹配多槽 key),会抛出 CROSSSLOT Keys in request don't hash to the same slot 异常;
  • 解决方案:
  1. 强制哈希槽:使用 {hashTag} 让多个 key 映射到同一槽,如 user:{1001}:nameuser:{1001}:age(CRC16 计算仅基于 {} 内的内容);
  2. 避免跨槽批量操作:将批量操作拆分为单 key 操作,或使用分布式锁保证数据一致性;
  3. 工具类封装:通过代码封装自动处理跨槽操作(如遍历节点执行查询)。

5.4.2 集群扩容/缩容

5.4.2.1 扩容步骤(新增 1 主 1 从)

# 1. 启动新增节点(6385 主、6386 从)
redis-server /usr/local/redis-cluster/6385/redis.conf
redis-server /usr/local/redis-cluster/6386/redis.conf

# 2. 将新增主节点加入集群
redis-cli --cluster add-node 127.0.0.1:6385 127.0.0.1:6379 -a 123456

# 3. 将新增从节点加入集群,并指定主节点(6385)
# 先获取 6385 节点的 ID(通过 cluster nodes 命令)
redis-cli --cluster add-node 127.0.0.1:6386 127.0.0.1:6379 -a 123456 --cluster-slave --cluster-master-id 6385节点ID

# 4. 迁移哈希槽到新增主节点(按需分配槽数量)
redis-cli --cluster reshard 127.0.0.1:6379 -a 123456
# 执行后按提示输入:
# 1. 需要迁移的槽数量(如 2000)
# 2. 接收槽的主节点 ID(6385 节点 ID)
# 3. 迁移来源(输入 all 从所有主节点迁移,或输入具体节点 ID 定向迁移)
# 4. 输入 yes 确认迁移

5.4.2.2 缩容步骤(移除 6385 主、6386 从)

# 1. 将 6385 主节点的哈希槽迁移到其他主节点
redis-cli --cluster reshard 127.0.0.1:6379 -a 123456
# 提示输入:
# 1. 迁移槽数量(6385 节点的所有槽,如 2000)
# 2. 接收槽的主节点 ID(如 6379)
# 3. 迁移来源(6385 节点 ID)
# 4. 输入 yes 确认迁移

# 2. 移除从节点 6386
redis-cli --cluster del-node 127.0.0.1:6386 6386节点ID -a 123456

# 3. 移除主节点 6385(需确保无哈希槽)
redis-cli --cluster del-node 127.0.0.1:6385 6385节点ID -a 123456

# 4. 停止节点
redis-cli -h 127.0.0.1 -p 6385 -a 123456 shutdown
redis-cli -h 127.0.0.1 -p 6386 -a 123456 shutdown

5.4.3 数据一致性问题

  • 问题:主从同步存在延迟,可能导致从节点查询到旧数据;主节点故障时,未同步的写操作可能丢失;
  • 解决方案:
  1. 优化同步参数:主节点配置 repl-diskless-sync yes(无盘同步),减少 RDB 写入耗时;
  2. 强一致性配置:通过 WAIT 1 5000 命令(等待至少 1 个从节点同步完成,超时 5 秒),牺牲性能换一致性;
  3. 持久化强化:所有节点开启 AOF 持久化,配置 appendfsync everysec,确保数据持久化到磁盘;
  4. 避免单节点依赖:核心业务数据不依赖单个主节点,通过集群冗余保证可用性。

5.5 核心参数优化

5.5.1 节点超时时间(cluster-node-timeout)

  • 默认值:15000 毫秒(15 秒);
  • 作用:判断节点故障的超时时间,超时未响应则标记为故障;
  • 优化建议:
  • 高并发、低延迟场景:设为 5000-10000 毫秒(5-10 秒),加快故障转移;
  • 网络不稳定场景:设为 20000-30000 毫秒(20-30 秒),避免误判故障。

5.5.2 全槽覆盖开关(cluster-require-full-coverage)

  • 默认值:yes(要求所有槽可用,否则集群不可用);
  • 问题:若部分槽故障(如主从全挂),整个集群无法提供服务;
  • 优化建议:设为 no,允许非全槽覆盖时集群仍可用(故障槽对应的 key 不可访问,其他槽正常服务)。

5.5.3 哈希槽迁移并行数(cluster-migration-barrier)

  • 默认值:1(主节点至少有 1 个正常从节点时,才允许迁移槽);
  • 作用:避免主节点无备份时迁移槽,导致数据丢失;
  • 优化建议:保持默认 1,确保迁移过程中数据有备份。

5.6 优缺点与适用场景

优点

  • 分片存储:解决单节点存储容量限制,支持海量数据存储;
  • 横向扩展:支持动态扩容/缩容,无需停机;
  • 自动高可用:主节点故障后自动故障转移,无需人工干预;
  • 去中心化:无中心节点,节点间对等通信,避免单点故障;
  • 继承读写分离:从节点分摊读压力,提升并发读能力。

缺点

  • 部署和运维复杂:需维护多个节点,扩容/缩容需手动操作哈希槽迁移;
  • 不支持跨槽批量操作:限制部分 Redis 命令使用(如 MSETKEYSSINTER 等);
  • 主从同步延迟:仍存在数据一致性风险;
  • 资源开销大:每个节点需独立资源(内存、CPU、磁盘),集群整体资源消耗高于单节点/主从/哨兵。

适用场景

  • 海量数据存储场景(如用户行为日志、商品库缓存,数据量超单节点存储上限);
  • 高并发读写场景(如电商秒杀、社交平台消息缓存,QPS 超单节点承载能力);
  • 对可用性和扩展性要求极高的大型分布式系统(如互联网核心业务、金融支付系统);
  • 无法容忍人工干预故障转移,需要自动容错的场景。

六、四种部署方式对比与选型指南

6.1 核心特性对比

部署方式 核心优势 核心劣势 可用性 扩展性 适用场景
单机版 部署简单、运维成本低 单点故障、无扩展性 开发/测试环境、小型工具
主从复制 读写分离、数据备份 手动故障转移、无扩展性 读扩展 读多写少、中小型应用
哨兵模式 自动故障转移、读写分离 无存储扩展性、运维较复杂 中高 读扩展 中小型到中大型应用、需自动容错
Redis Cluster 分片存储、自动高可用、横向扩展 部署复杂、不支持跨槽操作 全扩展 大型分布式系统、海量数据、高并发

6.2 选型决策流程

image.png

6.3 生产环境最佳实践

  1. 小型应用(QPS < 1 万,数据量 < 10GB):
  • 选型:哨兵模式(1 主 2 从 + 3 哨兵);
  • 理由:自动故障转移,部署成本适中,满足高可用需求。
  1. 中大型应用(QPS 1-10 万,数据量 10-100GB):
  • 选型:Redis Cluster(3 主 3 从);
  • 理由:支持横向扩展,满足高并发和海量数据需求,自动高可用。
  1. 超大型应用(QPS > 10 万,数据量 > 100GB):
  • 选型:Redis Cluster 集群 + 读写分离代理(如 Codis、Twemproxy);
  • 理由:代理层解决跨槽操作限制,支持更灵活的负载均衡和运维管理。

七、总结

Redis 的四种部署方式各有侧重,从简单到复杂,从单节点到分布式,覆盖了从开发测试到大型生产环境的全场景需求:

  • 单机版是入门基础,适合非生产环境;
  • 主从复制是高可用的基石,核心解决读写分离和数据备份;
  • 哨兵模式在主从复制基础上实现自动故障转移,提升系统可用性;
  • Redis Cluster 是分布式解决方案,核心解决分片存储和横向扩展,适合海量数据和高并发场景。

选型的核心是匹配业务需求:无需过度设计(如小型应用无需 Cluster),也不可忽视风险(如生产环境不可用单机版)。同时,无论选择哪种部署方式,都需关注持久化配置、主从同步、故障转移等核心要点,确保数据安全和服务稳定。

本文所有实战代码均基于 JDK 17 和 Redis 7.2.5 开发,严格遵循《阿里巴巴 Java 开发手册》,可直接编译运行;部署命令经过验证,可直接用于生产环境部署参考。

目录
相关文章
|
1天前
|
数据采集 人工智能 安全
|
11天前
|
云安全 监控 安全
|
2天前
|
自然语言处理 API
万相 Wan2.6 全新升级发布!人人都能当导演的时代来了
通义万相2.6全新升级,支持文生图、图生视频、文生视频,打造电影级创作体验。智能分镜、角色扮演、音画同步,让创意一键成片,大众也能轻松制作高质量短视频。
947 150
|
2天前
|
编解码 人工智能 机器人
通义万相2.6,模型使用指南
智能分镜 | 多镜头叙事 | 支持15秒视频生成 | 高品质声音生成 | 多人稳定对话
|
16天前
|
机器学习/深度学习 人工智能 自然语言处理
Z-Image:冲击体验上限的下一代图像生成模型
通义实验室推出全新文生图模型Z-Image,以6B参数实现“快、稳、轻、准”突破。Turbo版本仅需8步亚秒级生成,支持16GB显存设备,中英双语理解与文字渲染尤为出色,真实感和美学表现媲美国际顶尖模型,被誉为“最值得关注的开源生图模型之一”。
1664 8
|
7天前
|
人工智能 自然语言处理 API
一句话生成拓扑图!AI+Draw.io 封神开源组合,工具让你的效率爆炸
一句话生成拓扑图!next-ai-draw-io 结合 AI 与 Draw.io,通过自然语言秒出架构图,支持私有部署、免费大模型接口,彻底解放生产力,绘图效率直接爆炸。
619 152
|
9天前
|
人工智能 安全 前端开发
AgentScope Java v1.0 发布,让 Java 开发者轻松构建企业级 Agentic 应用
AgentScope 重磅发布 Java 版本,拥抱企业开发主流技术栈。
592 16
|
9天前
|
人工智能 自然语言处理 API
Next AI Draw.io:当AI遇见Draw.io图表绘制
Next AI Draw.io 是一款融合AI与图表绘制的开源工具,基于Next.js实现,支持自然语言生成架构图、流程图等专业图表。集成多款主流大模型,提供智能绘图、图像识别优化、版本管理等功能,部署简单,安全可控,助力技术文档与系统设计高效创作。
673 151