在分布式系统中,缓存是提升系统性能、降低数据库压力的核心组件,而Redis凭借超高的读写性能、丰富的数据结构,成为了缓存场景的首选。但单节点Redis存在无法回避的短板:单点故障会导致整个缓存服务不可用;单节点内存上限限制了数据存储规模;单节点无法应对高并发的读写压力。
Redis高可用架构的演进,正是为了解决这些核心问题,从最简单的主从复制,到具备自动故障转移的哨兵模式,再到支持分布式分片的集群模式,形成了一套完整的高可用解决方案。
一、Redis高可用的核心目标
高可用架构的设计,始终围绕四个核心目标展开,也是判断架构是否满足业务需求的核心标准:
- 故障自愈:当节点出现故障时,系统能自动完成主从切换,无需人工介入,最大程度降低服务不可用时间。
- 数据一致性:保证主从节点之间的数据同步,尽可能降低数据丢失的风险,满足业务的数据一致性要求。
- 读写分离:通过主节点处理写请求,从节点处理读请求,水平扩展系统的读性能,应对高并发读场景。
- 线性扩容:支持节点的动态扩容,突破单节点的内存与性能上限,应对海量数据的存储与访问需求。
二、主从复制:高可用的基础架构
主从复制是Redis高可用的基石,哨兵与集群架构的底层数据同步能力,都基于主从复制实现。它采用一主多从的架构,主节点负责处理写请求,从节点负责同步主节点的数据,并处理读请求。
2.1 主从复制的核心原理
主从复制的核心分为两个阶段:全量同步与增量同步,基于PSYNC命令实现,目前主流的PSYNC2.0版本,优化了断线重连后的同步逻辑,大幅降低了全量同步的触发概率。
2.1.1 核心概念
- 复制偏移量:主节点与从节点都会维护一个复制偏移量,主节点每处理一个写命令,就会将偏移量增加命令的字节长度;从节点每同步并执行一个命令,也会更新自己的偏移量。通过对比主从的偏移量,就能判断数据是否一致。
- 复制积压缓冲区:主节点内部维护的一个固定长度的先进先出队列,默认大小1MB,用于存储最近执行的写命令。当从节点断线重连时,主节点会对比偏移量,如果偏移量对应的命令还在缓冲区中,就会直接发送增量命令,无需全量同步。
- 运行ID(runid):每个Redis节点启动时都会生成一个唯一的40位运行ID,用于标识节点身份。从节点首次同步时,会记录主节点的运行ID,断线重连时会发送该ID,主节点只有在ID匹配时,才会尝试增量同步,否则触发全量同步。
2.1.2 全量同步流程
全量同步是从节点首次连接主节点,或无法进行增量同步时触发的完整数据同步流程,核心是基于RDB快照实现:
- 从节点向主节点发送
PSYNC ? -1命令,请求进行全量同步。 - 主节点收到命令后,执行
bgsave命令,在后台生成RDB快照文件。 - 主节点在生成RDB的同时,将新收到的写命令写入复制积压缓冲区。
- 主节点RDB生成完成后,将RDB文件发送给从节点。
- 从节点收到RDB文件后,清空自身所有数据,加载RDB文件到内存。
- 主节点将复制积压缓冲区中的写命令发送给从节点,从节点执行这些命令,完成全量同步后的增量数据补齐。
- 同步完成后,主节点后续的所有写命令,都会实时发送给从节点,从节点执行命令,保持数据一致,这个阶段称为命令传播。
2.1.3 增量同步流程
增量同步是从节点断线重连后,在满足条件的情况下,仅同步断线期间主节点执行的写命令,无需全量同步,大幅提升同步效率:
- 从节点断线重连后,向主节点发送
PSYNC <主节点runid> <自身复制偏移量>命令。 - 主节点收到命令后,首先校验runid是否与自身一致,不一致则触发全量同步。
- runid校验通过后,主节点检查从节点发送的偏移量是否在复制积压缓冲区的范围内。如果不在,触发全量同步。
- 如果偏移量在缓冲区范围内,主节点将缓冲区中从该偏移量开始的所有写命令发送给从节点。
- 从节点执行收到的命令,更新自身复制偏移量,完成增量同步,进入持续的命令传播阶段。
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 启动与验证
- 分别启动三个节点:
redis-server redis-6379.conf
redis-server redis-6380.conf
redis-server redis-6381.conf
- 登录主节点,查看复制状态:
redis-cli -p 6379
127.0.0.1:6379> info replication
输出结果中,role:master,connected_slaves:2,表示两个从节点已成功连接,主从复制部署完成。
2.3 主从复制的核心局限
主从复制实现了读写分离与数据备份,但存在明显的短板:
- 无自动故障转移能力:主节点宕机后,需要人工将从节点切换为主节点,同时修改所有从节点的replicaof配置与客户端的连接地址,故障恢复时间长,无法满足高可用要求。
- 单节点内存瓶颈:所有数据都存储在主节点,主节点的内存上限就是整个集群的存储上限,无法应对海量数据的存储需求。
- 写性能瓶颈:所有写请求都必须由主节点处理,主节点的性能上限就是整个集群的写性能上限,无法水平扩展写能力。
三、哨兵模式:自动故障转移的高可用方案
哨兵模式(Redis Sentinel)在主从复制的基础上,增加了哨兵节点集群,实现了故障的自动发现与自动转移,解决了主从复制的核心痛点。
3.1 哨兵的核心功能
哨兵节点是专门用于监控Redis节点的独立进程,不存储业务数据,仅负责监控、选主、通知与配置管理,核心功能如下:
- 监控:哨兵节点会持续向所有主从节点发送PING命令,检测节点的存活状态。
- 自动故障转移:当主节点发生故障时,哨兵集群会自动从从节点中选出一个最优节点作为新的主节点,将其他从节点切换为复制新主节点,并完成客户端的地址通知。
- 通知:当故障转移完成后,哨兵会将新的主节点地址通知给客户端,客户端会自动切换连接地址。
- 配置中心:客户端无需直接连接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一致性算法,核心规则是:
- 每个标记主节点为客观下线的哨兵,都会向其他哨兵发送请求,要求选举自己为领导者。
- 每个哨兵在一轮选举中,只能给第一个收到请求的哨兵投票。
- 获得超过半数哨兵投票的节点,成为领导者。如果本轮选举没有选出领导者,会进入下一轮选举,直到选出领导者为止。
3.2.4 故障转移执行
哨兵领导者选举完成后,会执行完整的故障转移流程,核心步骤如下:
- 筛选合格的从节点:从所有从节点中,筛选出存活状态正常、复制偏移量最大、优先级最高的从节点,作为新的主节点候选。
- 提升新主节点:向选中的从节点发送
slaveof no one命令,将其升级为主节点。 - 切换其他从节点的复制源:向其他所有从节点发送
slaveof <新主节点地址> <端口>命令,让它们复制新的主节点。 - 更新旧主节点的状态:将原来的主节点标记为新主节点的从节点,当它恢复正常后,会自动复制新主节点的数据。
- 通知客户端新主节点地址:哨兵集群更新主节点的地址信息,客户端通过哨兵获取到新的主节点地址,自动切换连接。
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 启动与验证
- 先启动主从节点,再启动三个哨兵节点:
redis-sentinel sentinel-26379.conf
redis-sentinel sentinel-26380.conf
redis-sentinel sentinel-26381.conf
- 登录哨兵节点,查看监控状态:
redis-cli -p 26379
127.0.0.1:26379> sentinel master mymaster
输出结果中,num-slaves:2,num-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 哨兵模式的核心局限
哨兵模式实现了自动故障转移,解决了主从复制的单点故障问题,但依然存在无法回避的短板:
- 单节点存储瓶颈:所有数据依然存储在主节点,主节点的内存上限决定了整个集群的存储能力,无法应对TB级别的海量数据存储。
- 写性能无法水平扩展:所有写请求依然由主节点处理,无法通过增加节点来提升写性能,无法应对高并发写场景。
- 单节点高并发压力:当业务数据量过大,单个主节点的CPU、内存、网络都会成为瓶颈,无法通过分布式架构分散压力。
四、Redis Cluster集群:分布式分片的高可用方案
Redis Cluster是Redis官方提供的分布式集群解决方案,采用去中心化的架构,将数据分散存储在多个主节点上,每个主节点负责一部分数据,同时每个主节点都配有从节点,实现故障自动转移,彻底解决了单节点的存储与性能瓶颈。
4.1 集群的核心原理
Redis Cluster的核心是哈希槽分片机制与去中心化的Gossip通信协议,实现了数据的分布式存储与节点的状态同步。
4.1.1 哈希槽分片机制
Redis Cluster将整个数据空间划分为16384个哈希槽(Hash Slot),每个key都会映射到其中一个槽位,每个主节点负责管理一部分槽位,核心规则如下:
- 槽位计算:当客户端写入一个key时,会通过
CRC16(key) % 16384计算出该key对应的槽位编号,然后将数据写入负责该槽位的主节点。 - 槽位分配:集群创建时,会将16384个槽位平均分配给所有主节点,比如3个主节点,每个节点负责约5461个槽位。
- 数据路由:客户端可以连接集群中的任意一个节点,当请求的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的故障转移流程与哨兵模式类似,但由集群节点自身完成,无需单独的哨兵节点,核心流程如下:
- 主观下线:当节点A持续向节点B发送PING消息,节点B在
cluster-node-timeout配置的时间内没有回复PONG消息,节点A会将节点B标记为主观下线。 - 客观下线:节点A会将节点B的主观下线状态通过Gossip消息广播给集群中的其他节点。当集群中超过半数的主节点都标记节点B为主观下线时,节点B会被标记为客观下线。
- 从节点选举:如果下线的节点是主节点,它的所有从节点会发起选举,请求集群中其他主节点投票。复制偏移量最大的从节点优先级最高,获得超过半数主节点投票的从节点,会升级为新的主节点。
- 故障转移完成:新的主节点会接管原来主节点负责的所有槽位,向集群中所有节点广播自己的新身份,其他节点会更新槽位映射信息。原来的主节点恢复后,会成为新主节点的从节点。
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 集群创建与验证
- 分别启动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个从节点:
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 数据一致性与防脑裂优化
脑裂是指由于网络分区,导致集群中出现两个主节点,同时处理写请求,最终导致数据不一致的问题。核心优化方案如下:
- 主节点最小从节点数配置:设置
min-replicas-to-write 1,要求主节点至少有1个正常连接的从节点,否则拒绝处理写请求,避免网络分区时主节点继续写入数据,导致数据丢失。 - 主从最大延迟配置:设置
min-replicas-max-lag 10,要求从节点的复制延迟不能超过10秒,否则主节点拒绝处理写请求,保证主从数据的一致性。 - 节点超时时间合理配置:哨兵模式的
down-after-milliseconds与集群模式的cluster-node-timeout,建议设置为5000-15000毫秒,避免网络抖动导致的误判,同时保证故障转移的及时性。
6.2 复制性能优化
- 无盘同步配置:开启
repl-diskless-sync yes,主节点生成RDB时直接通过网络发送给从节点,无需落盘,大幅降低磁盘IO压力,提升全量同步的性能。 - 复制积压缓冲区调优:将
repl-backlog-size调大至100MB,减少断线重连后全量同步的触发概率,降低主节点的性能开销。 - 关闭透明大页:Linux系统中关闭透明大页(THP),避免RDB生成与AOF重写时的内存分配延迟,提升Redis的性能与稳定性。
- 主从节点同机房部署:主从节点部署在同一个可用区,降低网络延迟,减少复制延迟,提升数据一致性。
6.3 集群模式核心避坑点
- 避免跨槽操作:Redis Cluster不支持跨多个槽位的批量操作,比如
MGET、MSET等命令,如果key分布在不同的槽位,会报错。可以使用hashtag技术,将相关的key强制分配到同一个槽位,格式为{hashtag}key,计算槽位时只会使用大括号内的内容。 - 关闭全量槽位覆盖要求:设置
cluster-require-full-coverage no,避免当某个主节点故障,槽位没有接管时,整个集群无法提供服务,保证部分槽位故障时,其他槽位依然可以正常使用。 - 节点数量控制:集群的主节点数量建议控制在3-20个,节点数量过多会导致Gossip协议的通信开销大幅增加,降低集群的稳定性。
- bigkey与hotkey优化:bigkey会导致数据迁移卡顿、节点内存不均;hotkey会导致单个节点的压力过大。需要拆分bigkey,使用本地缓存缓解hotkey的压力,保证集群的负载均衡。
6.4 持久化最佳实践
- 混合持久化模式:开启
aof-use-rdb-preamble yes,结合RDB与AOF的优势,RDB用于快速恢复数据,AOF用于保证数据不丢失,平衡性能与数据安全性。 - RDB持久化配置:建议设置
save 3600 1,每小时生成一次RDB快照,避免频繁生成RDB导致的磁盘IO压力。 - AOF持久化配置:设置
appendfsync everysec,每秒刷盘一次,平衡性能与数据安全性,最多丢失1秒的数据,满足绝大多数业务的要求。 - 持久化节点分离:主节点关闭持久化,在从节点开启持久化,避免主节点生成RDB与AOF重写时的性能开销,保证主节点的读写性能。
Redis高可用架构的选型,本质上是在业务需求、性能、成本、运维复杂度之间做平衡。主从复制是基础,哨兵模式解决了自动故障转移的核心痛点,集群模式则实现了海量数据的分布式存储与线性扩容。理解三种架构的底层原理,掌握核心的优化与避坑方案,才能根据业务的实际场景,选择最合适的架构,搭建出稳定、高性能、高可用的Redis服务。