本文详细讲解 Redis 主从复制、哨兵模式和集群模式的原理与配置,提供完整的 Python 连接代码示例,确保你的应用在生产环境中实现高可用性和可扩展性。
前言
在掌握了 Redis 的所有核心数据结构和高级特性后,我们面临最后一个关键挑战:如何让 Redis 在生产环境中**不宕机**?单点 Redis 实例一旦故障,将导致整个应用不可用。这就是 Redis 高可用(High Availability) 和 集群(Cluster) 架构要解决的根本问题。
本篇读者收益:
- 深入理解 Redis **主从复制**、**哨兵模式(Sentinel)** 和 集群模式(Cluster) 的架构原理。
- 掌握使用
redis-py连接 Redis Sentinel 实现自动故障转移和读写分离。 - 掌握使用
redis-py或redis-cluster连接 Redis Cluster 实现数据分片和水平扩展。 - 了解云服务(如 AWS ElastiCache、Azure Cache)的连接要点。
先修要求:已掌握 Redis 基础连接和操作(详见系列前四篇)。
关键要点:
- 主从复制:数据冗余的基础,从节点提供读扩展,但不具备自动故障转移能力。
- 哨兵模式(Sentinel) :在复制基础上增加了监控、通知和自动故障转移,实现真正的高可用。
- 集群模式(Cluster) :通过数据分片(sharding)实现水平扩展,兼具高可用和可扩展性。
- 从单机到哨兵再到集群,是一个在**复杂度、可用性和扩展性**之间的权衡过程。
背景与原理简述
随着业务增长,单机 Redis 会遇到两个核心瓶颈:
- 可用性瓶颈:单个节点故障导致服务完全中断。
- 性能/容量瓶颈:单机内存、CPU、网络带宽有限。
Redis 提供了三种进阶部署方案来解决这些问题:
- **主从复制(Replication)**:一个主节点(master)负责写操作,多个从节点(slave)复制主节点数据并提供读服务。解决了**读扩展**和**数据备份**,但没有自动故障转移。
- **哨兵模式(Sentinel)**:在复制基础上,引入专门的哨兵进程来监控节点健康状态,并在主节点故障时**自动选举新的主节点**,实现高可用。
- **集群模式(Cluster)**:将数据自动分片到多个主节点上,每个主节点都有对应的从节点。同时解决了**水平扩展**和**高可用**问题。
环境准备与快速上手
安装必要的 Python 库
Bash
# 安装 redis-py(支持 Sentinel 和基础 Cluster 连接)
pip install "redis[hiredis]"
# 对于更完整的 Cluster 支持,推荐安装 redis-py-cluster
pip install redis-py-cluster
基础连接测试
Python
# filename: setup.py
import os
import redis
from redis.sentinel import Sentinel
from redis.cluster import RedisCluster
from redis.exceptions import RedisError, ConnectionError
print("Redis 高可用与集群连接演示环境就绪")
核心用法与代码示例
主从复制(Replication)
架构概述
- 一个主节点(可写可读)
- 一个或多个从节点(只读,异步复制主节点数据)
- 客户端手动处理读写分离
Python 连接示例
Python
# filename: replication_demo.py
def replication_setup():
"""演示主从复制的基本连接(手动读写分离)"""
# 连接主节点(写操作)
master = redis.Redis(
host='redis-master-host',
port=6379,
password='your_password',
decode_responses=True
)
# 连接从节点(读操作)
slave = redis.Redis(
host='redis-slave-host',
port=6379,
password='your_password',
decode_responses=True
)
# 写入只能在主节点
master.set('global:counter', 100)
# 读取可以在从节点(注意复制延迟)
value = slave.get('global:counter')
print(f"从节点读取的值: {value}")
return master, slave
# 注意:生产环境不建议手动管理主从,推荐使用 Sentinel
哨兵模式(Sentinel)
架构概述
- 多个哨兵进程(Sentinel)组成集群,监控 Redis 节点
- 自动故障检测和主节点选举
- 客户端通过哨兵发现当前的主节点
Python 连接 Sentinel
Python
# filename: sentinel_demo.py
def sentinel_connection():
"""连接 Redis Sentinel 集群"""
# 1. 定义哨兵节点列表
sentinel_nodes = [
('sentinel1.example.com', 26379),
('sentinel2.example.com', 26379),
('sentinel3.example.com', 26379),
]
# 2. 创建 Sentinel 对象
sentinel = Sentinel(
sentinel_nodes,
socket_timeout=0.1,
password='your_sentinel_password', # 如果哨兵有密码
decode_responses=True
)
# 3. 获取主节点和从节点客户端
# service_name 是在哨兵配置中定义的集群名称
master = sentinel.master_for(
'my-redis-cluster', # service_name
socket_timeout=0.1,
password='your_redis_password',
decode_responses=True
)
slave = sentinel.slave_for(
'my-redis-cluster', # service_name
socket_timeout=0.1,
password='your_redis_password',
decode_responses=True
)
return master, slave, sentinel
def sentinel_operations():
"""使用 Sentinel 客户端进行操作"""
try:
master, slave, sentinel = sentinel_connection()
# 写入操作 - 使用主节点
master.set('sentinel:test', 'hello from master')
master.incr('sentinel:counter')
# 读取操作 - 使用从节点
value = slave.get('sentinel:test')
counter = slave.get('sentinel:counter')
print(f"从节点读取: {value}, 计数器: {counter}")
# 发现当前主从节点信息
current_master = sentinel.discover_master('my-redis-cluster')
current_slaves = sentinel.discover_slaves('my-redis-cluster')
print(f"当前主节点: {current_master}")
print(f"当前从节点: {current_slaves}")
except RedisError as e:
print(f"Sentinel 操作失败: {e}")
# 运行示例
sentinel_operations()
哨兵故障转移演示
Python
# filename: sentinel_failover.py
import time
import threading
def monitor_sentinel_status():
"""监控哨兵状态(模拟故障转移观察)"""
sentinel = Sentinel([
('localhost', 26379),
('localhost', 26380),
('localhost', 26381),
], socket_timeout=0.5)
while True:
try:
master_addr = sentinel.discover_master('my-redis-cluster')
slaves = sentinel.discover_slaves('my-redis-cluster')
print(f"[{time.strftime('%H:%M:%S')}] 主节点: {master_addr}, 从节点: {slaves}")
except RedisError as e:
print(f"监控错误: {e}")
time.sleep(2)
def sentinel_auto_failover_test():
"""测试哨兵的自动故障转移"""
master, slave, sentinel = sentinel_connection()
# 持续写入数据
def writer():
counter = 0
while True:
try:
master.set('failover:test', f'value_{counter}')
master.incr('failover:counter')
counter += 1
time.sleep(1)
except RedisError as e:
print(f"写入失败: {e}, 等待故障转移...")
time.sleep(3)
# 持续读取数据
def reader():
while True:
try:
value = slave.get('failover:test')
counter = slave.get('failover:counter')
print(f"读取: {value}, 计数器: {counter}")
except RedisError as e:
print(f"读取失败: {e}")
time.sleep(1)
# 启动读写线程
threading.Thread(target=writer, daemon=True).start()
threading.Thread(target=reader, daemon=True).start()
# 运行监控
monitor_sentinel_status()
# 注意:运行此示例需要真实的哨兵环境
# sentinel_auto_failover_test()
集群模式(Cluster)
架构概述
- 数据自动分片到 16384 个槽位(slots)
- 每个节点负责一部分槽位
- 客户端直接路由命令到正确的节点
- 每个主节点都有对应的从节点
Python 连接 Cluster
Python
# filename: cluster_demo.py
def cluster_connection():
"""连接 Redis Cluster"""
# 启动节点列表(不需要所有节点,能连接一个即可发现整个集群)
startup_nodes = [
{"host": "redis-cluster-node1.example.com", "port": 6379},
{"host": "redis-cluster-node2.example.com", "port": 6379},
{"host": "redis-cluster-node3.example.com", "port": 6379},
]
try:
# 方法1: 使用 redis-py-cluster (推荐)
from redis.cluster import RedisCluster
rc = RedisCluster(
startup_nodes=startup_nodes,
password='your_cluster_password',
decode_responses=True,
skip_full_coverage_check=True, # 避免不必要的全集群检查
socket_connect_timeout=5,
socket_timeout=5,
retry_on_timeout=True,
max_connections_per_node=20
)
return rc
except ImportError:
# 方法2: 使用 redis-py 内置的集群支持(基础功能)
print("redis-py-cluster 未安装,使用 redis-py 基础集群支持")
rc = redis.Redis(
host=startup_nodes[0]['host'],
port=startup_nodes[0]['port'],
password='your_cluster_password',
decode_responses=True
)
return rc
def cluster_operations():
"""集群基本操作"""
try:
rc = cluster_connection()
# 基本操作 - API 与单机版基本一致
rc.set('cluster:key1', 'value1')
rc.set('cluster:key2', 'value2')
value1 = rc.get('cluster:key1')
value2 = rc.get('cluster:key2')
print(f"获取值: key1={value1}, key2={value2}")
# 计数器操作
rc.incr('cluster:counter')
counter = rc.get('cluster:counter')
print(f"计数器: {counter}")
# 获取集群信息
if hasattr(rc, 'cluster_info'):
info = rc.cluster_info()
print(f"集群状态: {info.get('cluster_state')}")
print(f"已知节点数: {info.get('cluster_known_nodes')}")
# 获取键所在的槽位和节点
key_slot = rc.cluster_keyslot('cluster:key1')
print(f"key1 的槽位: {key_slot}")
except RedisError as e:
print(f"集群操作失败: {e}")
# 运行示例
cluster_operations()
集群分片与路由
Python
# filename: cluster_sharding.py
def cluster_sharding_demo():
"""演示集群的数据分片特性"""
rc = cluster_connection()
# 存储多个键,它们会被自动分配到不同节点
keys = []
for i in range(10):
key = f'sharding:key:{i}'
rc.set(key, f'value_{i}')
keys.append(key)
# 验证键分布在不同的槽位
for key in keys:
slot = rc.cluster_keyslot(key)
# 获取负责该槽位的节点
node = rc.nodes_manager.get_node_from_slot(slot)
print(f"键 {key} -> 槽位 {slot} -> 节点 {node}")
# 批量操作的限制:跨槽位的 MSET/MGET 会失败
try:
# 这可能会失败,因为 keys 可能在不同的槽位
result = rc.mget(keys)
print(f"批量获取成功: {result}")
except redis.RedisError as e:
print(f"跨槽位批量操作失败: {e}")
# 解决方案:使用 pipeline 或哈希标签确保键在同一个槽位
def cluster_hash_tags():
"""使用哈希标签确保相关键在同一个槽位"""
rc = cluster_connection()
# 使用 {user123} 作为哈希标签,确保所有 user123 相关的键在同一个槽位
user_id = "user123"
rc.set(f"user:{{{user_id}}}:profile", "profile_data")
rc.set(f"user:{{{user_id}}}:session", "session_data")
rc.set(f"user:{{{user_id}}}:preferences", "prefs_data")
# 现在可以安全地进行批量操作
keys = [
f"user:{{{user_id}}}:profile",
f"user:{{{user_id}}}:session",
f"user:{{{user_id}}}:preferences"
]
try:
values = rc.mget(keys)
print(f"使用哈希标签批量获取: {values}")
except redis.RedisError as e:
print(f"哈希标签批量操作失败: {e}")
cluster_hash_tags()
性能优化与容量规划
连接池配置实践
Python
# filename: production_connection_pools.py
def production_connection_configs():
"""生产环境连接配置示例"""
# Sentinel 连接池配置
sentinel_pool = redis.sentinel.SentinelConnectionPool(
'my-redis-cluster',
redis.sentinel.Sentinel([
('sentinel1', 26379),
('sentinel2', 26379),
('sentinel3', 26379),
]),
password='your_password',
max_connections=50,
socket_timeout=5,
socket_connect_timeout=5,
retry_on_timeout=True,
health_check_interval=30
)
# Cluster 连接池配置
cluster_pool = RedisCluster(
startup_nodes=[{"host": "node1", "port": 6379}],
password='your_password',
max_connections=100, # 每个节点的最大连接数
socket_connect_timeout=5,
socket_timeout=5,
retry_on_timeout=True,
reinitialize_steps=10 # 每10次命令后重新初始化连接
)
监控与健康检查
Python
# filename: health_monitoring.py
class RedisHealthMonitor:
"""Redis 健康监控器"""
def __init__(self, client):
self.client = client
def check_connectivity(self):
"""检查基本连通性"""
try:
return self.client.ping()
except (ConnectionError, RedisError):
return False
def get_info(self, section=None):
"""获取 Redis 信息"""
try:
if hasattr(self.client, 'info'):
return self.client.info(section)
else:
# Cluster 的特殊处理
return self.client.cluster_info()
except RedisError:
return None
def monitor_memory_usage(self):
"""监控内存使用情况"""
info = self.get_info('memory')
if info:
return {
'used_memory': info.get('used_memory_human', 'N/A'),
'used_memory_peak': info.get('used_memory_peak_human', 'N/A'),
'memory_fragmentation_ratio': info.get('mem_fragmentation_ratio', 'N/A')
}
return None
def check_replication_status(self):
"""检查复制状态(适用于主从和集群)"""
info = self.get_info('replication')
if info:
role = info.get('role')
if role == 'master':
return {
'role': 'master',
'connected_slaves': info.get('connected_slaves', 0)
}
elif role == 'slave':
return {
'role': 'slave',
'master_link_status': info.get('master_link_status', 'down'),
'master_host': info.get('master_host', 'unknown')
}
return None
安全与可靠性
- 使用安全组/防火墙限制访问来源
- Redis 节点部署在私有子网
- 哨兵和集群节点间使用专用网络
- 配置适当的
save规则或启用 AOF - 定期测试备份恢复流程
- 跨可用区部署提高容灾能力
案例
云服务连接示例(AWS ElastiCache)
Python
# filename: aws_elasticache.py
def connect_aws_elasticache():
"""连接 AWS ElastiCache Redis"""
# ElastiCache Redis (Cluster Mode Disabled) - 使用 Sentinel
if os.getenv('REDIS_MODE') == 'sentinel':
sentinel = Sentinel([
('primary-endpoint', 26379),
('secondary-endpoint', 26379),
], socket_timeout=1)
client = sentinel.master_for(
'my-cluster',
socket_timeout=1,
password=os.getenv('REDIS_PASSWORD')
)
# ElastiCache Redis (Cluster Mode Enabled) - 使用 Cluster
elif os.getenv('REDIS_MODE') == 'cluster':
# 获取配置端点
configuration_endpoint = os.getenv('REDIS_CLUSTER_CONFIG_ENDPOINT')
host, port = configuration_endpoint.split(':')
client = RedisCluster(
startup_nodes=[{"host": host, "port": int(port)}],
password=os.getenv('REDIS_PASSWORD'),
skip_full_coverage_check=True,
decode_responses=True
)
else:
# 单机模式
client = redis.Redis(
host=os.getenv('REDIS_HOST'),
port=int(os.getenv('REDIS_PORT')),
password=os.getenv('REDIS_PASSWORD'),
decode_responses=True
)
return client
# 生产环境配置管理器
class RedisConnectionManager:
"""生产环境 Redis 连接管理器"""
_clients = {}
@classmethod
def get_client(cls, service_name='default'):
"""获取 Redis 客户端(单例模式)"""
if service_name not in cls._clients:
if os.getenv('REDIS_CLUSTER_ENABLED') == 'true':
cls._clients[service_name] = connect_aws_elasticache()
else:
# 其他连接逻辑
pass
# 健康检查
try:
cls._clients[service_name].ping()
except RedisError:
# 重新建立连接
cls._clients[service_name] = connect_aws_elasticache()
return cls._clients[service_name]
小结
从单机到哨兵再到集群,Redis 提供了完整的解决方案来满足不同规模应用的高可用和可扩展性需求。哨兵模式为读写分离和自动故障转移提供了优雅的解决方案,而集群模式则通过数据分片实现了真正的水平扩展。