Redis 高可用全链路拆解:从主从复制到集群架构的原理与实践

简介: Redis高可用演进:从主从复制(基础读写分离)、哨兵模式(自动故障转移)到Cluster集群(分布式分片+线性扩容),分别解决单点故障、存储/性能瓶颈问题。本文详解原理、部署、Spring Boot整合及优化避坑指南。

在分布式系统中,缓存是提升系统性能、降低数据库压力的核心组件,而Redis凭借超高的读写性能、丰富的数据结构,成为了缓存场景的首选。但单节点Redis存在无法回避的短板:单点故障会导致整个缓存服务不可用;单节点内存上限限制了数据存储规模;单节点无法应对高并发的读写压力。

Redis高可用架构的演进,正是为了解决这些核心问题,从最简单的主从复制,到具备自动故障转移的哨兵模式,再到支持分布式分片的集群模式,形成了一套完整的高可用解决方案。

一、Redis高可用的核心目标

高可用架构的设计,始终围绕四个核心目标展开,也是判断架构是否满足业务需求的核心标准:

  1. 故障自愈:当节点出现故障时,系统能自动完成主从切换,无需人工介入,最大程度降低服务不可用时间。
  2. 数据一致性:保证主从节点之间的数据同步,尽可能降低数据丢失的风险,满足业务的数据一致性要求。
  3. 读写分离:通过主节点处理写请求,从节点处理读请求,水平扩展系统的读性能,应对高并发读场景。
  4. 线性扩容:支持节点的动态扩容,突破单节点的内存与性能上限,应对海量数据的存储与访问需求。

二、主从复制:高可用的基础架构

主从复制是Redis高可用的基石,哨兵与集群架构的底层数据同步能力,都基于主从复制实现。它采用一主多从的架构,主节点负责处理写请求,从节点负责同步主节点的数据,并处理读请求。

2.1 主从复制的核心原理

主从复制的核心分为两个阶段:全量同步增量同步,基于PSYNC命令实现,目前主流的PSYNC2.0版本,优化了断线重连后的同步逻辑,大幅降低了全量同步的触发概率。

2.1.1 核心概念

  • 复制偏移量:主节点与从节点都会维护一个复制偏移量,主节点每处理一个写命令,就会将偏移量增加命令的字节长度;从节点每同步并执行一个命令,也会更新自己的偏移量。通过对比主从的偏移量,就能判断数据是否一致。
  • 复制积压缓冲区:主节点内部维护的一个固定长度的先进先出队列,默认大小1MB,用于存储最近执行的写命令。当从节点断线重连时,主节点会对比偏移量,如果偏移量对应的命令还在缓冲区中,就会直接发送增量命令,无需全量同步。
  • 运行ID(runid):每个Redis节点启动时都会生成一个唯一的40位运行ID,用于标识节点身份。从节点首次同步时,会记录主节点的运行ID,断线重连时会发送该ID,主节点只有在ID匹配时,才会尝试增量同步,否则触发全量同步。

2.1.2 全量同步流程

全量同步是从节点首次连接主节点,或无法进行增量同步时触发的完整数据同步流程,核心是基于RDB快照实现:

  1. 从节点向主节点发送PSYNC ? -1命令,请求进行全量同步。
  2. 主节点收到命令后,执行bgsave命令,在后台生成RDB快照文件。
  3. 主节点在生成RDB的同时,将新收到的写命令写入复制积压缓冲区。
  4. 主节点RDB生成完成后,将RDB文件发送给从节点。
  5. 从节点收到RDB文件后,清空自身所有数据,加载RDB文件到内存。
  6. 主节点将复制积压缓冲区中的写命令发送给从节点,从节点执行这些命令,完成全量同步后的增量数据补齐。
  7. 同步完成后,主节点后续的所有写命令,都会实时发送给从节点,从节点执行命令,保持数据一致,这个阶段称为命令传播

2.1.3 增量同步流程

增量同步是从节点断线重连后,在满足条件的情况下,仅同步断线期间主节点执行的写命令,无需全量同步,大幅提升同步效率:

  1. 从节点断线重连后,向主节点发送PSYNC <主节点runid> <自身复制偏移量>命令。
  2. 主节点收到命令后,首先校验runid是否与自身一致,不一致则触发全量同步。
  3. runid校验通过后,主节点检查从节点发送的偏移量是否在复制积压缓冲区的范围内。如果不在,触发全量同步。
  4. 如果偏移量在缓冲区范围内,主节点将缓冲区中从该偏移量开始的所有写命令发送给从节点。
  5. 从节点执行收到的命令,更新自身复制偏移量,完成增量同步,进入持续的命令传播阶段。

2.2 主从复制的部署实现

采用一主两从的架构,使用Redis 7.2.5版本,三个节点分别部署在6379(主)、6380(从)、6381(从)端口。

2.2.1 主节点配置(redis-6379.conf)

bind 0.0.0.0

port 6379

daemonize yes

pidfile /var/run/redis_6379.pid

logfile "redis-6379.log"

dbfilename dump-6379.rdb

dir /usr/local/redis/data

appendonly yes

appendfilename "appendonly-6379.aof"

aof-use-rdb-preamble yes

repl-backlog-size 104857600

repl-diskless-sync yes

2.2.2 从节点配置(redis-6380.conf)

bind 0.0.0.0

port 6380

daemonize yes

pidfile /var/run/redis_6380.pid

logfile "redis-6380.log"

dbfilename dump-6380.rdb

dir /usr/local/redis/data

appendonly yes

appendfilename "appendonly-6380.aof"

aof-use-rdb-preamble yes

replicaof 127.0.0.1 6379

replica-read-only yes

6381端口的从节点配置,仅需修改端口、pidfile、日志文件、rdb文件名等与端口相关的配置,replicaof配置保持一致。

2.2.3 启动与验证

  1. 分别启动三个节点:

redis-server redis-6379.conf
redis-server redis-6380.conf
redis-server redis-6381.conf

  1. 登录主节点,查看复制状态:

redis-cli -p 6379
127.0.0.1:6379> info replication

输出结果中,role:masterconnected_slaves:2,表示两个从节点已成功连接,主从复制部署完成。

2.3 主从复制的核心局限

主从复制实现了读写分离与数据备份,但存在明显的短板:

  1. 无自动故障转移能力:主节点宕机后,需要人工将从节点切换为主节点,同时修改所有从节点的replicaof配置与客户端的连接地址,故障恢复时间长,无法满足高可用要求。
  2. 单节点内存瓶颈:所有数据都存储在主节点,主节点的内存上限就是整个集群的存储上限,无法应对海量数据的存储需求。
  3. 写性能瓶颈:所有写请求都必须由主节点处理,主节点的性能上限就是整个集群的写性能上限,无法水平扩展写能力。

三、哨兵模式:自动故障转移的高可用方案

哨兵模式(Redis Sentinel)在主从复制的基础上,增加了哨兵节点集群,实现了故障的自动发现与自动转移,解决了主从复制的核心痛点。

3.1 哨兵的核心功能

哨兵节点是专门用于监控Redis节点的独立进程,不存储业务数据,仅负责监控、选主、通知与配置管理,核心功能如下:

  1. 监控:哨兵节点会持续向所有主从节点发送PING命令,检测节点的存活状态。
  2. 自动故障转移:当主节点发生故障时,哨兵集群会自动从从节点中选出一个最优节点作为新的主节点,将其他从节点切换为复制新主节点,并完成客户端的地址通知。
  3. 通知:当故障转移完成后,哨兵会将新的主节点地址通知给客户端,客户端会自动切换连接地址。
  4. 配置中心:客户端无需直接连接Redis节点,而是连接哨兵集群,从哨兵集群获取主节点的地址,实现配置的统一管理。

3.2 哨兵模式的核心原理

哨兵模式的核心逻辑分为四个阶段:主观下线判定、客观下线判定、哨兵领导者选举、故障转移执行。

3.2.1 主观下线(SDOWN)

单个哨兵节点检测到主节点在down-after-milliseconds配置的时间内,没有正确回复PING命令,就会将该主节点标记为主观下线。 主观下线是单个哨兵的判断,存在误判的可能,比如网络抖动导致单个哨兵与主节点通信异常,但主节点本身正常运行,因此需要多个哨兵的共同确认,才能进入客观下线阶段。

3.2.2 客观下线(ODOWN)

当哨兵将主节点标记为主观下线后,会向其他哨兵节点发送SENTINEL is-master-down-by-addr命令,询问其他哨兵是否也认为该主节点已经下线。 当收到同意下线的哨兵数量达到配置的quorum值时,该哨兵会将主节点标记为客观下线,正式进入故障转移流程。

3.2.3 哨兵领导者选举

故障转移操作只能由一个哨兵节点执行,因此需要在所有哨兵节点中选举出一个领导者,负责后续的故障转移操作。选举采用Raft一致性算法,核心规则是:

  1. 每个标记主节点为客观下线的哨兵,都会向其他哨兵发送请求,要求选举自己为领导者。
  2. 每个哨兵在一轮选举中,只能给第一个收到请求的哨兵投票。
  3. 获得超过半数哨兵投票的节点,成为领导者。如果本轮选举没有选出领导者,会进入下一轮选举,直到选出领导者为止。

3.2.4 故障转移执行

哨兵领导者选举完成后,会执行完整的故障转移流程,核心步骤如下:

  1. 筛选合格的从节点:从所有从节点中,筛选出存活状态正常、复制偏移量最大、优先级最高的从节点,作为新的主节点候选。
  2. 提升新主节点:向选中的从节点发送slaveof no one命令,将其升级为主节点。
  3. 切换其他从节点的复制源:向其他所有从节点发送slaveof <新主节点地址> <端口>命令,让它们复制新的主节点。
  4. 更新旧主节点的状态:将原来的主节点标记为新主节点的从节点,当它恢复正常后,会自动复制新主节点的数据。
  5. 通知客户端新主节点地址:哨兵集群更新主节点的地址信息,客户端通过哨兵获取到新的主节点地址,自动切换连接。

3.3 哨兵模式的部署实现

基于之前的一主两从架构,部署3个哨兵节点,分别运行在26379、26380、26381端口,形成三哨兵集群,避免哨兵单点故障。

3.3.1 哨兵节点配置(sentinel-26379.conf)

bind 0.0.0.0

port 26379

daemonize yes

pidfile /var/run/redis-sentinel-26379.pid

logfile "sentinel-26379.log"

dir /usr/local/redis/data

sentinel monitor mymaster 127.0.0.1 6379 2

sentinel down-after-milliseconds mymaster 5000

sentinel failover-timeout mymaster 30000

sentinel parallel-syncs mymaster 1

其他两个哨兵节点的配置,仅需修改端口、pidfile、日志文件名等与端口相关的配置,监控主节点的配置保持一致。

3.3.2 启动与验证

  1. 先启动主从节点,再启动三个哨兵节点:

redis-sentinel sentinel-26379.conf
redis-sentinel sentinel-26380.conf
redis-sentinel sentinel-26381.conf

  1. 登录哨兵节点,查看监控状态:

redis-cli -p 26379
127.0.0.1:26379> sentinel master mymaster

输出结果中,num-slaves:2num-sentinels:3,表示哨兵已成功监控到主从节点与其他哨兵节点,部署完成。

3.4 Spring Boot整合哨兵模式

基于JDK 17与Spring Boot 3.2.4,实现Redis哨兵模式的客户端接入,项目配置与代码如下。

3.4.1 pom.xml依赖配置

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

   <modelVersion>4.0.0</modelVersion>
   <parent>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-parent</artifactId>
       <version>3.2.4</version>
       <relativePath/>
   </parent>
   <groupId>com.jam.demo</groupId>
   <artifactId>redis-sentinel-demo</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <name>redis-sentinel-demo</name>
   <properties>
       <java.version>17</java.version>
       <fastjson2.version>2.0.52</fastjson2.version>
       <guava.version>33.1.0-jre</guava.version>
       <mybatis-plus.version>3.5.6</mybatis-plus.version>
       <springdoc.version>2.5.0</springdoc.version>
   </properties>
   <dependencies>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-data-redis</artifactId>
       </dependency>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-web</artifactId>
       </dependency>
       <dependency>
           <groupId>org.springdoc</groupId>
           <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
           <version>${springdoc.version}</version>
       </dependency>
       <dependency>
           <groupId>com.baomidou</groupId>
           <artifactId>mybatis-plus-boot-starter</artifactId>
           <version>${mybatis-plus.version}</version>
       </dependency>
       <dependency>
           <groupId>com.alibaba.fastjson2</groupId>
           <artifactId>fastjson2</artifactId>
           <version>${fastjson2.version}</version>
       </dependency>
       <dependency>
           <groupId>com.google.guava</groupId>
           <artifactId>guava</artifactId>
           <version>${guava.version}</version>
       </dependency>
       <dependency>
           <groupId>org.projectlombok</groupId>
           <artifactId>lombok</artifactId>
           <version>1.18.32</version>
           <scope>provided</scope>
       </dependency>
       <dependency>
           <groupId>com.mysql</groupId>
           <artifactId>mysql-connector-j</artifactId>
           <scope>runtime</scope>
       </dependency>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-test</artifactId>
           <scope>test</scope>
       </dependency>
   </dependencies>
   <build>
       <plugins>
           <plugin>
               <groupId>org.springframework.boot</groupId>
               <artifactId>spring-boot-maven-plugin</artifactId>
               <configuration>
                   <excludes>
                       <exclude>
                           <groupId>org.projectlombok</groupId>
                           <artifactId>lombok</artifactId>
                       </exclude>
                   </excludes>
               </configuration>
           </plugin>
       </plugins>
   </build>
</project>

3.4.2 application.yml配置文件

spring:
 data:
   redis:
     sentinel:
       master: mymaster
       nodes:
         - 127.0.0.1:26379
         - 127.0.0.1:26380
         - 127.0.0.1:26381
     lettuce:
       pool:
         max-active: 8
         max-idle: 8
         min-idle: 2
         max-wait: -1ms
 datasource:
   url: jdbc:mysql://127.0.0.1:3306/demo_db?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false
   username: root
   password: root
   driver-class-name: com.mysql.cj.jdbc.Driver
springdoc:
 swagger-ui:
   path: /swagger-ui.html
 api-docs:
   path: /v3/api-docs

3.4.3 Redis配置类

package com.jam.demo.config;

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.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

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

@Configuration
public class RedisConfig {

   /**
    * 配置RedisTemplate,设置序列化规则
    * @param connectionFactory Redis连接工厂
    * @return RedisTemplate实例
    */

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

       StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
       redisTemplate.setKeySerializer(stringRedisSerializer);
       redisTemplate.setHashKeySerializer(stringRedisSerializer);

       GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
       redisTemplate.setValueSerializer(jsonRedisSerializer);
       redisTemplate.setHashValueSerializer(jsonRedisSerializer);

       redisTemplate.afterPropertiesSet();
       return redisTemplate;
   }
}

3.4.4 业务实体类

package com.jam.demo.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

import java.io.Serializable;

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

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

   private static final long serialVersionUID = 1L;

   @TableId(type = IdType.AUTO)
   @Schema(description = "用户ID", example = "1")
   private Long id;

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

   @Schema(description = "年龄", example = "20")
   private Integer age;

   @Schema(description = "邮箱", example = "zhangsan@example.com")
   private String email;
}

3.4.5 业务Controller

package com.jam.demo.controller;

import com.jam.demo.entity.User;
import com.jam.demo.service.UserService;
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.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
* 用户管理Controller
* @author ken
*/

@Slf4j
@RestController
@RequestMapping("/user")
@Tag(name = "用户管理", description = "用户相关操作接口")
public class UserController {

   @Autowired
   private UserService userService;

   /**
    * 根据ID查询用户信息
    * @param id 用户ID
    * @return 用户信息
    */

   @GetMapping("/{id}")
   @Operation(summary = "查询用户", description = "根据用户ID查询用户详情")
   public User getUserById(
           @Parameter(description = "用户ID", required = true, example = "1")

           @PathVariable Long id) {
       return userService.getUserById(id);
   }

   /**
    * 新增用户
    * @param user 用户信息
    * @return 新增结果
    */

   @PostMapping
   @Operation(summary = "新增用户", description = "创建新的用户信息")
   public Boolean addUser(@RequestBody User user) {
       return userService.addUser(user);
   }

   /**
    * 更新用户信息
    * @param user 用户信息
    * @return 更新结果
    */

   @PutMapping
   @Operation(summary = "更新用户", description = "更新已有的用户信息")
   public Boolean updateUser(@RequestBody User user) {
       return userService.updateUser(user);
   }

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

   @DeleteMapping("/{id}")
   @Operation(summary = "删除用户", description = "根据用户ID删除用户信息")
   public Boolean deleteUser(
           @Parameter(description = "用户ID", required = true, example = "1")

           @PathVariable Long id) {
       return userService.deleteUser(id);
   }
}

3.4.6 业务Service实现

package com.jam.demo.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.jam.demo.entity.User;
import com.jam.demo.mapper.UserMapper;
import com.jam.demo.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import java.util.concurrent.TimeUnit;

/**
* 用户服务实现类
* @author ken
*/

@Slf4j
@Service
public class UserServiceImpl implements UserService {

   private static final String USER_CACHE_PREFIX = "user:info:";

   @Autowired
   private RedisTemplate<String, Object> redisTemplate;

   @Autowired
   private UserMapper userMapper;

   @Autowired
   private PlatformTransactionManager transactionManager;

   @Override
   public User getUserById(Long id) {
       if (ObjectUtils.isEmpty(id)) {
           log.warn("查询用户信息,用户ID为空");
           return null;
       }

       String cacheKey = USER_CACHE_PREFIX + id;
       User user = (User) redisTemplate.opsForValue().get(cacheKey);
       if (!ObjectUtils.isEmpty(user)) {
           log.info("从缓存中查询到用户信息,用户ID:{}", id);
           return user;
       }

       user = userMapper.selectById(id);
       if (!ObjectUtils.isEmpty(user)) {
           redisTemplate.opsForValue().set(cacheKey, user, 30, TimeUnit.MINUTES);
           log.info("从数据库中查询到用户信息,写入缓存,用户ID:{}", id);
       }

       return user;
   }

   @Override
   public Boolean addUser(User user) {
       if (ObjectUtils.isEmpty(user) || !StringUtils.hasText(user.getUsername())) {
           log.warn("新增用户信息,参数不合法");
           return Boolean.FALSE;
       }

       DefaultTransactionDefinition def = new DefaultTransactionDefinition();
       TransactionStatus status = transactionManager.getTransaction(def);

       try {
           int result = userMapper.insert(user);
           if (result > 0) {
               String cacheKey = USER_CACHE_PREFIX + user.getId();
               redisTemplate.opsForValue().set(cacheKey, user, 30, TimeUnit.MINUTES);
               transactionManager.commit(status);
               log.info("新增用户信息成功,用户ID:{}", user.getId());
               return Boolean.TRUE;
           }
           transactionManager.rollback(status);
           return Boolean.FALSE;
       } catch (Exception e) {
           transactionManager.rollback(status);
           log.error("新增用户信息异常,用户信息:{}", user, e);
           throw new RuntimeException("新增用户信息异常", e);
       }
   }

   @Override
   public Boolean updateUser(User user) {
       if (ObjectUtils.isEmpty(user) || ObjectUtils.isEmpty(user.getId())) {
           log.warn("更新用户信息,参数不合法");
           return Boolean.FALSE;
       }

       DefaultTransactionDefinition def = new DefaultTransactionDefinition();
       TransactionStatus status = transactionManager.getTransaction(def);

       try {
           int result = userMapper.updateById(user);
           if (result > 0) {
               String cacheKey = USER_CACHE_PREFIX + user.getId();
               redisTemplate.opsForValue().set(cacheKey, user, 30, TimeUnit.MINUTES);
               transactionManager.commit(status);
               log.info("更新用户信息成功,用户ID:{}", user.getId());
               return Boolean.TRUE;
           }
           transactionManager.rollback(status);
           return Boolean.FALSE;
       } catch (Exception e) {
           transactionManager.rollback(status);
           log.error("更新用户信息异常,用户信息:{}", user, e);
           throw new RuntimeException("更新用户信息异常", e);
       }
   }

   @Override
   public Boolean deleteUser(Long id) {
       if (ObjectUtils.isEmpty(id)) {
           log.warn("删除用户信息,用户ID为空");
           return Boolean.FALSE;
       }

       DefaultTransactionDefinition def = new DefaultTransactionDefinition();
       TransactionStatus status = transactionManager.getTransaction(def);

       try {
           int result = userMapper.deleteById(id);
           if (result > 0) {
               String cacheKey = USER_CACHE_PREFIX + id;
               redisTemplate.delete(cacheKey);
               transactionManager.commit(status);
               log.info("删除用户信息成功,用户ID:{}", id);
               return Boolean.TRUE;
           }
           transactionManager.rollback(status);
           return Boolean.FALSE;
       } catch (Exception e) {
           transactionManager.rollback(status);
           log.error("删除用户信息异常,用户ID:{}", id, e);
           throw new RuntimeException("删除用户信息异常", e);
       }
   }
}

3.4.7 Mapper接口

package com.jam.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.User;
import org.apache.ibatis.annotations.Mapper;

/**
* 用户Mapper接口
* @author ken
*/

@Mapper
public interface UserMapper extends BaseMapper<User> {
}

3.4.8 Service接口

package com.jam.demo.service;

import com.jam.demo.entity.User;

/**
* 用户服务接口
* @author ken
*/

public interface UserService {

   User getUserById(Long id);

   Boolean addUser(User user);

   Boolean updateUser(User user);

   Boolean deleteUser(Long id);
}

3.4.9 启动类

package com.jam.demo;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
* 项目启动类
* @author ken
*/

@SpringBootApplication
@MapperScan("com.jam.demo.mapper")
public class RedisSentinelDemoApplication {

   public static void main(String[] args) {
       SpringApplication.run(RedisSentinelDemoApplication.class, args);
   }
}

3.5 哨兵模式的核心局限

哨兵模式实现了自动故障转移,解决了主从复制的单点故障问题,但依然存在无法回避的短板:

  1. 单节点存储瓶颈:所有数据依然存储在主节点,主节点的内存上限决定了整个集群的存储能力,无法应对TB级别的海量数据存储。
  2. 写性能无法水平扩展:所有写请求依然由主节点处理,无法通过增加节点来提升写性能,无法应对高并发写场景。
  3. 单节点高并发压力:当业务数据量过大,单个主节点的CPU、内存、网络都会成为瓶颈,无法通过分布式架构分散压力。

四、Redis Cluster集群:分布式分片的高可用方案

Redis Cluster是Redis官方提供的分布式集群解决方案,采用去中心化的架构,将数据分散存储在多个主节点上,每个主节点负责一部分数据,同时每个主节点都配有从节点,实现故障自动转移,彻底解决了单节点的存储与性能瓶颈。

4.1 集群的核心原理

Redis Cluster的核心是哈希槽分片机制去中心化的Gossip通信协议,实现了数据的分布式存储与节点的状态同步。

4.1.1 哈希槽分片机制

Redis Cluster将整个数据空间划分为16384个哈希槽(Hash Slot),每个key都会映射到其中一个槽位,每个主节点负责管理一部分槽位,核心规则如下:

  1. 槽位计算:当客户端写入一个key时,会通过CRC16(key) % 16384计算出该key对应的槽位编号,然后将数据写入负责该槽位的主节点。
  2. 槽位分配:集群创建时,会将16384个槽位平均分配给所有主节点,比如3个主节点,每个节点负责约5461个槽位。
  3. 数据路由:客户端可以连接集群中的任意一个节点,当请求的key对应的槽位不在当前节点时,节点会返回MOVED重定向指令,告诉客户端该槽位对应的节点地址,客户端会自动重定向到目标节点执行请求。

4.1.2 核心概念区分:MOVED重定向与ASK重定向

这是集群模式中最容易混淆的两个概念,核心区别如下:

  • MOVED重定向:表示槽位已经永久迁移到了其他节点,当前节点不再负责该槽位。客户端收到MOVED指令后,会更新本地的槽位映射缓存,后续该槽位的请求都会直接发送到新的节点。
  • ASK重定向:表示槽位正在从当前节点迁移到目标节点,部分数据已经迁移到目标节点。客户端收到ASK指令后,只会将当前请求临时重定向到目标节点,不会更新本地的槽位映射缓存,直到槽位迁移完成,收到MOVED指令后才会更新缓存。

4.1.3 Gossip通信协议

Redis Cluster采用去中心化的架构,没有专门的配置中心节点,所有节点之间通过Gossip协议进行通信,同步节点状态、槽位分配、故障信息等,核心通信消息类型如下:

  • PING消息:节点每秒会随机选择几个其他节点发送PING消息,检测节点的存活状态,同时携带自身的状态信息与已知的其他节点的状态信息。
  • PONG消息:节点收到PING消息后,会回复PONG消息,携带自身的状态信息。
  • MEET消息:当新节点加入集群时,会向集群中的节点发送MEET消息,通知新节点的存在,其他节点会将新节点加入到集群的节点列表中。
  • FAIL消息:当节点确认某个节点已经下线时,会向集群中所有节点发送FAIL消息,通知其他节点该节点已下线,所有节点收到后,会将该节点标记为下线状态。

4.1.4 故障转移原理

Redis Cluster的故障转移流程与哨兵模式类似,但由集群节点自身完成,无需单独的哨兵节点,核心流程如下:

  1. 主观下线:当节点A持续向节点B发送PING消息,节点B在cluster-node-timeout配置的时间内没有回复PONG消息,节点A会将节点B标记为主观下线
  2. 客观下线:节点A会将节点B的主观下线状态通过Gossip消息广播给集群中的其他节点。当集群中超过半数的主节点都标记节点B为主观下线时,节点B会被标记为客观下线
  3. 从节点选举:如果下线的节点是主节点,它的所有从节点会发起选举,请求集群中其他主节点投票。复制偏移量最大的从节点优先级最高,获得超过半数主节点投票的从节点,会升级为新的主节点。
  4. 故障转移完成:新的主节点会接管原来主节点负责的所有槽位,向集群中所有节点广播自己的新身份,其他节点会更新槽位映射信息。原来的主节点恢复后,会成为新主节点的从节点。

4.2 集群模式的部署实现

采用3主3从的架构,6个节点分别运行在7001-7006端口,其中7001、7002、7003为主节点,7004、7005、7006分别为对应的从节点。

4.2.1 节点配置(redis-7001.conf)

bind 0.0.0.0

port 7001

daemonize yes

pidfile /var/run/redis-7001.pid

logfile "redis-7001.log"

dbfilename dump-7001.rdb

dir /usr/local/redis/cluster/data

appendonly yes

appendfilename "appendonly-7001.aof"

aof-use-rdb-preamble yes

cluster-enabled yes

cluster-config-file nodes-7001.conf

cluster-node-timeout 15000

cluster-require-full-coverage no

其他5个节点的配置,仅需修改端口、pidfile、日志文件、rdb文件名、cluster-config-file等与端口相关的配置即可。

4.2.2 集群创建与验证

  1. 分别启动6个节点:

redis-server redis-7001.conf
redis-server redis-7002.conf
redis-server redis-7003.conf
redis-server redis-7004.conf
redis-server redis-7005.conf
redis-server redis-7006.conf

  1. 创建集群,设置1个主节点对应1个从节点:

redis-cli --cluster create 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 127.0.0.1:7006 --cluster-replicas 1

执行命令后,输入yes确认槽位分配,集群创建完成。 3. 验证集群状态:

redis-cli -c -p 7001
127.0.0.1:7001> cluster info

输出结果中,cluster_state:ok,表示集群状态正常;cluster_slots_assigned:16384,表示所有槽位都已分配。

4.3 Spring Boot整合集群模式

基于JDK 17与Spring Boot 3.2.4,实现Redis Cluster集群的客户端接入,核心配置如下,业务代码与哨兵模式完全一致,无需修改。

4.3.1 application.yml配置文件

spring:
 data:
   redis:
     cluster:
       nodes:
         - 127.0.0.1:7001
         - 127.0.0.1:7002
         - 127.0.0.1:7003
         - 127.0.0.1:7004
         - 127.0.0.1:7005
         - 127.0.0.1:7006
       max-redirects: 3
     lettuce:
       pool:
         max-active: 16
         max-idle: 16
         min-idle: 4
         max-wait: -1ms

五、三种高可用架构选型对比

架构类型 核心定位 核心优势 核心劣势 节点规模 数据分片 故障转移 适用场景
主从复制 基础数据同步与读写分离 部署简单、运维成本低、读写分离 无自动故障转移、单节点存储与性能瓶颈 3-5节点 不支持 人工手动切换 小型项目、测试环境、数据量小、并发量低的场景
哨兵模式 中小规模自动故障转移高可用 自动故障转移、部署简单、运维成本低、读写分离 单节点存储与性能瓶颈、写能力无法扩展 3-10节点 不支持 哨兵集群自动切换 中小型项目、数据量在10GB以内、并发量中等、需要自动故障转移的场景
集群模式 大规模分布式高可用 分布式分片、线性扩容、读写性能水平扩展、自动故障转移 部署复杂、运维成本高、跨槽操作有限制 6节点起步,最大支持1000+节点 16384哈希槽分片 集群节点自动切换 中大型项目、数据量超过20GB、高并发读写、需要线性扩容的场景

六、核心优化与避坑指南

6.1 数据一致性与防脑裂优化

脑裂是指由于网络分区,导致集群中出现两个主节点,同时处理写请求,最终导致数据不一致的问题。核心优化方案如下:

  1. 主节点最小从节点数配置:设置min-replicas-to-write 1,要求主节点至少有1个正常连接的从节点,否则拒绝处理写请求,避免网络分区时主节点继续写入数据,导致数据丢失。
  2. 主从最大延迟配置:设置min-replicas-max-lag 10,要求从节点的复制延迟不能超过10秒,否则主节点拒绝处理写请求,保证主从数据的一致性。
  3. 节点超时时间合理配置:哨兵模式的down-after-milliseconds与集群模式的cluster-node-timeout,建议设置为5000-15000毫秒,避免网络抖动导致的误判,同时保证故障转移的及时性。

6.2 复制性能优化

  1. 无盘同步配置:开启repl-diskless-sync yes,主节点生成RDB时直接通过网络发送给从节点,无需落盘,大幅降低磁盘IO压力,提升全量同步的性能。
  2. 复制积压缓冲区调优:将repl-backlog-size调大至100MB,减少断线重连后全量同步的触发概率,降低主节点的性能开销。
  3. 关闭透明大页:Linux系统中关闭透明大页(THP),避免RDB生成与AOF重写时的内存分配延迟,提升Redis的性能与稳定性。
  4. 主从节点同机房部署:主从节点部署在同一个可用区,降低网络延迟,减少复制延迟,提升数据一致性。

6.3 集群模式核心避坑点

  1. 避免跨槽操作:Redis Cluster不支持跨多个槽位的批量操作,比如MGETMSET等命令,如果key分布在不同的槽位,会报错。可以使用hashtag技术,将相关的key强制分配到同一个槽位,格式为{hashtag}key,计算槽位时只会使用大括号内的内容。
  2. 关闭全量槽位覆盖要求:设置cluster-require-full-coverage no,避免当某个主节点故障,槽位没有接管时,整个集群无法提供服务,保证部分槽位故障时,其他槽位依然可以正常使用。
  3. 节点数量控制:集群的主节点数量建议控制在3-20个,节点数量过多会导致Gossip协议的通信开销大幅增加,降低集群的稳定性。
  4. bigkey与hotkey优化:bigkey会导致数据迁移卡顿、节点内存不均;hotkey会导致单个节点的压力过大。需要拆分bigkey,使用本地缓存缓解hotkey的压力,保证集群的负载均衡。

6.4 持久化最佳实践

  1. 混合持久化模式:开启aof-use-rdb-preamble yes,结合RDB与AOF的优势,RDB用于快速恢复数据,AOF用于保证数据不丢失,平衡性能与数据安全性。
  2. RDB持久化配置:建议设置save 3600 1,每小时生成一次RDB快照,避免频繁生成RDB导致的磁盘IO压力。
  3. AOF持久化配置:设置appendfsync everysec,每秒刷盘一次,平衡性能与数据安全性,最多丢失1秒的数据,满足绝大多数业务的要求。
  4. 持久化节点分离:主节点关闭持久化,在从节点开启持久化,避免主节点生成RDB与AOF重写时的性能开销,保证主节点的读写性能。

Redis高可用架构的选型,本质上是在业务需求、性能、成本、运维复杂度之间做平衡。主从复制是基础,哨兵模式解决了自动故障转移的核心痛点,集群模式则实现了海量数据的分布式存储与线性扩容。理解三种架构的底层原理,掌握核心的优化与避坑方案,才能根据业务的实际场景,选择最合适的架构,搭建出稳定、高性能、高可用的Redis服务。

目录
相关文章
|
11天前
|
人工智能 安全 Linux
【OpenClaw保姆级图文教程】阿里云/本地部署集成模型Ollama/Qwen3.5/百炼 API 步骤流程及避坑指南
2026年,AI代理工具的部署逻辑已从“单一云端依赖”转向“云端+本地双轨模式”。OpenClaw(曾用名Clawdbot)作为开源AI代理框架,既支持对接阿里云百炼等云端免费API,也能通过Ollama部署本地大模型,完美解决两类核心需求:一是担心云端API泄露核心数据的隐私安全诉求;二是频繁调用导致token消耗过高的成本控制需求。
5557 13
|
18天前
|
人工智能 JavaScript Ubuntu
5分钟上手龙虾AI!OpenClaw部署(阿里云+本地)+ 免费多模型配置保姆级教程(MiniMax、Claude、阿里云百炼)
OpenClaw(昵称“龙虾AI”)作为2026年热门的开源个人AI助手,由PSPDFKit创始人Peter Steinberger开发,核心优势在于“真正执行任务”——不仅能聊天互动,还能自动处理邮件、管理日程、订机票、写代码等,且所有数据本地处理,隐私完全可控。它支持接入MiniMax、Claude、GPT等多类大模型,兼容微信、Telegram、飞书等主流聊天工具,搭配100+可扩展技能,成为兼顾实用性与隐私性的AI工具首选。
22111 118

热门文章

最新文章