redis-哨兵模式配置整理
- 哨兵模式概述
- 核心架构组成
-
- [1. 哨兵节点 (Sentinel)](#1. 哨兵节点 (Sentinel))
- [2. 数据节点](#2. 数据节点)
- 哨兵工作机制详解
-
- [1. 监控机制](#1. 监控机制)
- 故障转移详细流程
-
- [1. 故障发现阶段](#1. 故障发现阶段)
- 配置详解
-
- [1. 基本哨兵配置(通常位于/etc/redis/sentinel.conf)](#1. 基本哨兵配置(通常位于/etc/redis/sentinel.conf))
- [2. 关键参数说明](#2. 关键参数说明)
- [启动 Sentinel 监控有几种方式](#启动 Sentinel 监控有几种方式)
-
- [1. 使用 redis-sentinel 命令启动](#1. 使用 redis-sentinel 命令启动)
- [2. 使用 redis-server 命令启动](#2. 使用 redis-server 命令启动)
- [3. 完整的启动脚本示例](#3. 完整的启动脚本示例)
- [4. 验证哨兵状态](#4. 验证哨兵状态)
- [5. 停止哨兵服务](#5. 停止哨兵服务)
- 业务中连接方式
-
- [1. 自动发现机制](#1. 自动发现机制)
- [2. 连接状态管理](#2. 连接状态管理)
- 部署最佳实践
-
- [1. 哨兵节点部署方案](#1. 哨兵节点部署方案)
- [2. 一种可行的网络拓扑方式](#2. 一种可行的网络拓扑方式)
- 故障场景分析
-
- [1. 主节点故障](#1. 主节点故障)
- [2. 网络分区场景](#2. 网络分区场景)
- 监控与运维
-
- [1. 关键监控指标](#1. 关键监控指标)
- [2. 日常运维命令](#2. 日常运维命令)
- 优缺点分析
- [Redis Sentinel 自动发现机制的 Lua 实现](#Redis Sentinel 自动发现机制的 Lua 实现)
-
- [完整的 Lua 实现方案](#完整的 Lua 实现方案)
-
- [1. 核心 Sentinel 发现类](#1. 核心 Sentinel 发现类)
- [2. 连接池管理](#2. 连接池管理)
- [3. 使用示例和配置](#3. 使用示例和配置)
- [4. 应用层封装](#4. 应用层封装)
- [5. 故障转移处理增强](#5. 故障转移处理增强)
- 核心特性说明
- 另一种基于skyent框架实现示例
哨兵模式概述
Redis 哨兵(Sentinel)是 Redis 官方提供的高可用性解决方案,用于管理 Redis主从架构,实现自动故障检测和故障转移

核心架构组成
1. 哨兵节点 (Sentinel)
bash
# 哨兵节点配置示例
port 26379
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
sentinel parallel-syncs mymaster 1
2. 数据节点
- 主节点 (Master):处理写操作和读操作
- 从节点 (Slave):复制主节点数据,处理读操作
哨兵工作机制详解
1. 监控机制

- 可以配置N个哨兵节点只需要监控主节点 - 哨兵会自动发现主节点下的所有从节点
- 例如可以在同一机器上启动多个哨兵 - 使用不同端口即可(26379、26380、26381),但线上不建议,因为一旦机器挂掉则对应的监控哨兵都会挂掉
- 配置方式 - 每个哨兵配置文件都指向同一个主节点即可
故障转移详细流程
1. 故障发现阶段

配置详解
1. 基本哨兵配置(通常位于/etc/redis/sentinel.conf)
bash
# sentinel.conf
port 26379
daemonize yes
pidfile /var/run/redis-sentinel.pid # 每个哨兵用不同的pid文件
logfile "/var/log/redis/sentinel.log" # 每个哨兵用不同的日志文件
# 监控配置
sentinel monitor mymaster 192.168.1.100 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
sentinel parallel-syncs mymaster 1
# 密码认证(如有)
sentinel auth-pass mymaster MyPassword
其中:
mymaster是给主节点起的名字,2表示至少需要2个哨兵同意才能判断主节点客观下线。例如有三个哨兵,所以通常设置为2(多数原则)
重要提醒:
1. 关键问题 :注意添加的sentinel_XXX.conf,不可以出现相同的 sentinel myid 会导致哨兵无法相互识别
2. 自动生成内容 :哨兵启动后会自动在配置文件中添加运行时信息,不要手动修改这些内容
3. 启动顺序 :先启动Redis主从,再启动哨兵
4. 等待时间:给哨兵足够的时间进行相互发现(30-60秒)
如果出现因为myid相同知道哨兵节点无法互相识别,将对应配置中的最后一部分删掉,然后重新启动既可以:从 "# Generated by CONFIG REWRITE" 开始到文件末尾的所有内容
2. 关键参数说明
| 参数 | 默认值 | 说明 |
|---|---|---|
| down-after-milliseconds | 30000 | 主观下线判定时间 |
| failover-timeout | 180000 | 故障转移超时时间 |
| parallel-syncs | 1 | 同时同步的从节点数 |
| quorum | 2 | 客观下线所需票数 |
启动 Sentinel 监控有几种方式
1. 使用 redis-sentinel 命令启动
bash
# 分别启动三个哨兵
redis-sentinel /path/to/sentinel1.conf
redis-sentinel /path/to/sentinel2.conf
redis-sentinel /path/to/sentinel3.conf
2. 使用 redis-server 命令启动
bash
# 也可以使用 redis-server 命令
redis-server /path/to/sentinel1.conf --sentinel
redis-server /path/to/sentinel2.conf --sentinel
redis-server /path/to/sentinel3.conf --sentinel
# 后台启动
redis-server /path/to/sentinel1.conf --sentinel --daemonize yes
3. 完整的启动脚本示例
bash
#!/bin/bash
SENTINEL_CONF_DIR="/etc/redis/sentinel"
SENTINEL_PORTS=(26379 26380 26381)
for port in "${SENTINEL_PORTS[@]}"; do
config_file="${SENTINEL_CONF_DIR}/sentinel-${port}.conf"
echo "启动哨兵端口: $port, 配置文件: $config_file"
redis-sentinel $config_file --daemonize yes
done
echo "所有哨兵节点启动完成"
4. 验证哨兵状态
bash
# 连接哨兵查看信息
redis-cli -p 26379
127.0.0.1:26379> info sentinel
# 或者查看主节点信息
127.0.0.1:26379> sentinel masters
127.0.0.1:26379> sentinel slaves mymaster # 查看从节点
127.0.0.1:26379> sentinel sentinels mymaster # 查看其他哨兵
5. 停止哨兵服务
bash
# 查看进程
ps aux | grep redis-sentinel
# 优雅停止
redis-cli -p 26379 shutdown
redis-cli -p 26380 shutdown
redis-cli -p 26381 shutdown
重要提醒
- 确保配置文件路径正确
- 每个哨兵使用不同的端口和pid文件
- 检查防火墙规则,确保哨兵节点之间可以通信
- 查看日志文件确认启动是否成功
启动后,哨兵会自动相互发现并组成哨兵集群来监控你的Redis主从架构,可以进入到对应的哨兵端口,使用info sentinel查看sentinels=X, 其中X为哨兵集群中的哨兵数量
业务中连接方式
1. 自动发现机制
java
// Java客户端示例
Set<String> sentinels = new HashSet<>();
sentinels.add("192.168.1.10:26379");
sentinels.add("192.168.1.11:26379");
sentinels.add("192.168.1.12:26379");
JedisSentinelPool pool = new JedisSentinelPool("mymaster", sentinels);
try (Jedis jedis = pool.getResource()) {
jedis.set("key", "value");
}
2. 连接状态管理

部署最佳实践
1. 哨兵节点部署方案
bash
# 推荐部署奇数个哨兵节点
节点分布:
- 哨兵1: 192.168.1.10:26379
- 哨兵2: 192.168.1.11:26379
- 哨兵3: 192.168.1.12:26379
# 数据节点
- 主节点: 192.168.1.100:6379
- 从节点1: 192.168.1.101:6379
- 从节点2: 192.168.1.102:6379
2. 一种可行的网络拓扑方式

故障场景分析
1. 主节点故障
bash
# 故障转移日志示例
1. +sdown master mymaster 192.168.1.100 6379
2. +odown master mymaster 192.168.1.100 6379
3. +vote-for-leader sentinel1 1
4. +elected-leader master mymaster
5. +failover-state-select-slave master mymaster
6. +selected-slave slave 192.168.1.101:6379
7. +failover-state-send-slaveof-noone
8. +failover-state-wait-promotion
9. +promoted-slave slave 192.168.1.101:6379
10. +failover-state-reconf-slaves
11. +switch-master mymaster 192.168.1.100 6379 192.168.1.101 6379
2. 网络分区场景

监控与运维
1. 关键监控指标
bash
# 查看哨兵状态
redis-cli -p 26379 info sentinel
# 监控指标
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
2. 日常运维命令
bash
# 查看主节点信息
redis-cli -p 26379 sentinel get-master-addr-by-name mymaster
# 手动故障转移
redis-cli -p 26379 sentinel failover mymaster
# 查看从节点信息
redis-cli -p 26379 sentinel slaves mymaster
# 添加监控
redis-cli -p 26379 sentinel monitor newmaster 192.168.1.200 6379 2
优缺点分析
优点 ✅
1. 自动故障转移 :无需人工干预
2. 配置中心 :客户端自动发现主节点
3. 监控完备 :全面的节点健康检查
4. 官方支持:Redis 官方维护,稳定性高
缺点 ❌
1. 写操作单点 :主节点仍然是写瓶颈
2. 数据一致性 :异步复制可能导致数据丢失
3. 配置复杂 :需要正确配置多个参数
4. 资源消耗:需要额外的哨兵节点
Redis Sentinel 自动发现机制的 Lua 实现
完整的 Lua 实现方案
1. 核心 Sentinel 发现类
lua
local redis = require("resty.redis")
local cjson = require("cjson")
local RedisSentinel = {}
RedisSentinel.__index = RedisSentinel
function RedisSentinel.new(sentinels, master_name, options)
local self = setmetatable({}, RedisSentinel)
self.sentinel_hosts = sentinels or {}
self.master_name = master_name or "mymaster"
self.options = options or {}
self.current_master = nil
self.sentinel_timeout = self.options.timeout or 1000 -- 毫秒
self.retry_count = self.options.retry_count or 3
self.connection_pool = {}
return self
end
-- 从 Sentinel 获取当前主节点信息
function RedisSentinel:get_master_from_sentinel(sentinel_host)
local red = redis:new()
red:set_timeout(self.sentinel_timeout)
local ok, err = red:connect(sentinel_host.host, sentinel_host.port)
if not ok then
return nil, "Failed to connect to sentinel: " .. err
end
-- 如果配置了密码
if sentinel_host.password then
local auth_ok, auth_err = red:auth(sentinel_host.password)
if not auth_ok then
red:close()
return nil, "Sentinel auth failed: " .. auth_err
end
end
-- 查询主节点信息
local res, err = red:command("sentinel", "get-master-addr-by-name", self.master_name)
if not res or type(res) ~= "table" or #res < 2 then
red:close()
return nil, "Failed to get master from sentinel: " .. (err or "invalid response")
end
red:close()
return {
host = res[1],
port = tonumber(res[2])
}
end
-- 自动发现主节点(轮询所有 Sentinel)
function RedisSentinel:discover_master()
for i = 1, self.retry_count do
for _, sentinel in ipairs(self.sentinel_hosts) do
local master, err = self:get_master_from_sentinel(sentinel)
if master then
-- 验证主节点是否可用
local is_alive, valid_err = self:validate_master(master)
if is_alive then
self.current_master = master
return master
else
ngx.log(ngx.WARN, "Master validation failed: ", valid_err)
end
else
ngx.log(ngx.WARN, "Sentinel ", sentinel.host, ":", sentinel.port, " failed: ", err)
end
end
-- 所有 Sentinel 都失败,等待后重试
if i < self.retry_count then
ngx.sleep(0.1) -- 等待 100ms 后重试
end
end
return nil, "All sentinels failed after " .. self.retry_count .. " retries"
end
-- 验证主节点是否可用
function RedisSentinel:validate_master(master)
local red = redis:new()
red:set_timeout(1000) -- 1秒超时
local ok, err = red:connect(master.host, master.port)
if not ok then
return false, "Connect failed: " .. err
end
-- 如果配置了密码
if self.options.password then
local auth_ok, auth_err = red:auth(self.options.password)
if not auth_ok then
red:close()
return false, "Auth failed: " .. auth_err
end
end
-- 执行 PING 命令验证
local pong, ping_err = red:ping()
if not pong then
red:close()
return false, "Ping failed: " .. ping_err
end
red:close()
return true
end
-- 获取当前主节点连接
function RedisSentinel:get_master_connection()
-- 如果当前主节点为空或需要刷新,则重新发现
if not self.current_master then
local master, err = self:discover_master()
if not master then
return nil, err
end
end
-- 创建到主节点的连接
local red = redis:new()
red:set_timeout(self.options.redis_timeout or 5000)
local ok, err = red:connect(self.current_master.host, self.current_master.port)
if not ok then
-- 连接失败,可能是故障转移,重新发现
local master, discover_err = self:discover_master()
if not master then
return nil, "Reconnect failed and rediscovery also failed: " .. discover_err
end
-- 使用新发现的主节点重连
ok, err = red:connect(master.host, master.port)
if not ok then
return nil, "Reconnect with new master failed: " .. err
end
end
-- 认证
if self.options.password then
local auth_ok, auth_err = red:auth(self.options.password)
if not auth_ok then
red:close()
return nil, "Redis auth failed: " .. auth_err
end
end
-- 选择数据库
if self.options.database then
local select_ok, select_err = red:select(self.options.database)
if not select_ok then
red:close()
return nil, "Select database failed: " .. select_err
end
end
return red
end
-- 获取从节点列表
function RedisSentinel:get_slaves()
for _, sentinel in ipairs(self.sentinel_hosts) do
local red = redis:new()
red:set_timeout(self.sentinel_timeout)
local ok, err = red:connect(sentinel.host, sentinel.port)
if ok then
if sentinel.password then
red:auth(sentinel.password)
end
local slaves, err = red:command("sentinel", "slaves", self.master_name)
red:close()
if slaves and type(slaves) == "table" then
local slave_list = {}
for _, slave_info in ipairs(slaves) do
if type(slave_info) == "table" then
local slave = {}
for i = 1, #slave_info, 2 do
if slave_info[i] == "ip" then
slave.host = slave_info[i+1]
elseif slave_info[i] == "port" then
slave.port = tonumber(slave_info[i+1])
elseif slave_info[i] == "flags" then
slave.flags = slave_info[i+1]
end
end
-- 只包含正常运行的从节点
if not slave.flags or not slave.flags:find("s_down") then
table.insert(slave_list, slave)
end
end
end
return slave_list
end
end
end
return nil, "Failed to get slaves from all sentinels"
end
-- 获取只读连接的从节点
function RedisSentinel:get_slave_connection()
local slaves, err = self:get_slaves()
if not slaves then
return nil, err
end
-- 随机选择一个从节点(负载均衡)
if #slaves > 0 then
local slave = slaves[math.random(1, #slaves)]
local red = redis:new()
red:set_timeout(self.options.redis_timeout or 5000)
local ok, err = red:connect(slave.host, slave.port)
if ok then
-- 认证和数据库选择
if self.options.password then
red:auth(self.options.password)
end
if self.options.database then
red:select(self.options.database)
end
return red
end
end
return nil, "No available slaves"
end
return RedisSentinel
2. 连接池管理
lua
local RedisConnectionPool = {}
RedisConnectionPool.__index = RedisConnectionPool
function RedisConnectionPool.new(sentinel, pool_size)
local self = setmetatable({}, RedisConnectionPool)
self.sentinel = sentinel
self.pool_size = pool_size or 10
self.master_pool = {}
self.slave_pool = {}
return self
end
function RedisConnectionPool:get_master_connection()
-- 尝试从连接池获取
for i = #self.master_pool, 1, -1 do
local conn = table.remove(self.master_pool, i)
if conn and self:is_connection_alive(conn) then
return conn
end
end
-- 连接池为空或连接无效,创建新连接
return self.sentinel:get_master_connection()
end
function RedisConnectionPool:get_slave_connection()
-- 尝试从连接池获取
for i = #self.slave_pool, 1, -1 do
local conn = table.remove(self.slave_pool, i)
if conn and self:is_connection_alive(conn) then
return conn
end
end
-- 连接池为空或连接无效,创建新连接
return self.sentinel:get_slave_connection()
end
function RedisConnectionPool:release_master_connection(conn)
if conn and #self.master_pool < self.pool_size then
table.insert(self.master_pool, conn)
else
if conn then
conn:close()
end
end
end
function RedisConnectionPool:release_slave_connection(conn)
if conn and #self.slave_pool < self.pool_size then
table.insert(self.slave_pool, conn)
else
if conn then
conn:close()
end
end
end
function RedisConnectionPool:is_connection_alive(conn)
local ok, err = conn:ping()
return ok and true or false
end
function RedisConnectionPool:close_all()
for _, conn in ipairs(self.master_pool) do
conn:close()
end
for _, conn in ipairs(self.slave_pool) do
conn:close()
end
self.master_pool = {}
self.slave_pool = {}
end
3. 使用示例和配置
lua
-- config.lua
local config = {
sentinels = {
{ host = "192.168.1.10", port = 26379 },
{ host = "192.168.1.11", port = 26379 },
{ host = "192.168.1.12", port = 26379 }
},
master_name = "mymaster",
options = {
password = "your_password",
database = 0,
timeout = 1000,
retry_count = 3,
redis_timeout = 5000
}
}
return config
4. 应用层封装
lua
-- app_redis.lua
local RedisSentinel = require("redis_sentinel")
local RedisConnectionPool = require("redis_connection_pool")
local config = require("config")
local _M = {}
_M._VERSION = "1.0"
local sentinel = RedisSentinel.new(
config.sentinels,
config.master_name,
config.options
)
local connection_pool = RedisConnectionPool.new(sentinel, 20)
-- 执行写操作(主节点)
function _M:execute_write(command, ...)
local conn, err = connection_pool:get_master_connection()
if not conn then
return nil, err
end
local ok, result = pcall(function()
return conn[command](conn, ...)
end)
-- 释放连接回连接池
connection_pool:release_master_connection(conn)
if not ok then
return nil, result
end
return result
end
-- 执行读操作(从节点)
function _M:execute_read(command, ...)
local conn, err = connection_pool:get_slave_connection()
if not conn then
-- 从节点失败,降级到主节点
conn, err = connection_pool:get_master_connection()
if not conn then
return nil, err
end
end
local ok, result = pcall(function()
return conn[command](conn, ...)
end)
-- 释放连接回连接池
if conn then
connection_pool:release_slave_connection(conn)
end
if not ok then
return nil, result
end
return result
end
-- 批量操作
function _M:execute_pipeline(commands, use_slave)
local conn, err
if use_slave then
conn, err = connection_pool:get_slave_connection()
if not conn then
conn, err = connection_pool:get_master_connection()
end
else
conn, err = connection_pool:get_master_connection()
end
if not conn then
return nil, err
end
-- 开始管道
conn:init_pipeline()
for _, cmd in ipairs(commands) do
local command = cmd.command
local args = cmd.args or {}
conn[command](conn, unpack(args))
end
local results, err = conn:commit_pipeline()
-- 释放连接
if use_slave then
connection_pool:release_slave_connection(conn)
else
connection_pool:release_master_connection(conn)
end
if not results then
return nil, err
end
return results
end
-- 强制刷新主节点信息
function _M:refresh_master_info()
return sentinel:discover_master()
end
-- 获取当前主节点信息
function _M:get_current_master()
return sentinel.current_master
end
-- 健康检查
function _M:health_check()
local master_conn, err = connection_pool:get_master_connection()
if not master_conn then
return false, "Master connection failed: " .. (err or "unknown")
end
local ok, ping_err = master_conn:ping()
connection_pool:release_master_connection(master_conn)
if not ok then
return false, "Master ping failed: " .. (ping_err or "unknown")
end
return true, "Healthy"
end
return _M
5. 故障转移处理增强
lua
-- failover_handler.lua
local _M = {}
function _M:handle_failover(sentinel, old_master, new_master)
ngx.log(ngx.INFO, "Failover detected: ",
old_master.host, ":", old_master.port, " -> ",
new_master.host, ":", new_master.port)
-- 清理连接池中旧的连接
if connection_pool then
connection_pool:close_all()
end
-- 发送通知(可选)
self:send_failover_notification(old_master, new_master)
-- 更新应用状态
self:update_application_state(new_master)
end
function _M:send_failover_notification(old_master, new_master)
-- 实现通知逻辑,如发送邮件、短信、Webhook等
local message = string.format(
"Redis failover occurred: %s:%d -> %s:%d",
old_master.host, old_master.port,
new_master.host, new_master.port
)
-- 示例:记录到日志文件
ngx.log(ngx.WARN, message)
end
function _M:update_application_state(new_master)
-- 更新应用配置或状态
-- 例如:刷新本地缓存、更新配置中心等
end
return _M
核心特性说明
1. 自动故障检测
- 连接失败时自动重试其他 Sentinel
- 验证主节点可用性
- 支持故障转移后的自动重连
2. 负载均衡
- 从节点读操作的随机选择
- 连接池管理减少连接开销
- 支持读写分离
3. 容错机制
- 多 Sentinel 节点轮询
- 从节点失败时降级到主节点
- 连接池健康检查
4. 性能优化
- 连接复用减少开销
- 异步操作支持
- 合理的超时和重试配置
另一种基于skyent框架实现示例
lua
local skynet = require "skynet"
local redis = require "skynet.db.redis"
local cjson = require "cjson"
local cfgmgr = require "cfgmgr"
local redis_db
---从哨兵那里获取真正的master ,返回ip 端口
local function get_master_info(sentinel_cfg)
for _, conn in ipairs(sentinel_cfg.hosts) do
local ret, db = pcall(redis.connect, conn)
if ret and db then
local ret = db:sentinel("get-master-addr-by-name", sentinel_cfg.master )
db:disconnect()
return ret[1], ret[2]
end
end
end
---根据配置去找有效的redis连接
local function get_available_redis()
local sentinel_cfg = cfgmgr.get_redis_sentinel_cfg()
local conn = {}
--有哨兵配置就走哨兵配置
if sentinel_cfg and #sentinel_cfg.hosts > 0 then
conn = sentinel_cfg.redis_conn or {}
conn.host, conn.port = get_master_info(sentinel_cfg)
ERROR("get_available_redis form sentinel", conn)
if not conn.host then
return
end
else
conn = cfgmgr.get_redis_cfg()
ERROR("get_available_redis form common redis", conn)
end
local ret, db = pcall(redis.connect, conn)
if ret and db then
ERROR("get_available_redis success!!", conn)
return db
end
end
local function redis_conn_timer()
if not redis_db then
redis_db = get_available_redis()
else
local result = redis_db:ping()
-- DEBUG("[init][ping] result=", result)
if not result then
redis_db = nil
end
end
local update_time = 100
if not redis_db then
ERROR("[init] redis connect error!please check!")
else
update_time = 1000
end
skynet.timeout(update_time, redis_conn_timer)
end
function init()
DEBUG("[init] Start init redis service ...")
cjson.encode_sparse_array(true, 1, 1)
redis_conn_timer()
DEBUG("[init] connect to reids server success, Init server service success!")
end
function response.do_cmd(cmd, key,...)
--DEBUG("[redis do cmd]:",cmd,key, ...)
local result = nil
if redis_db then
result = redis_db[cmd](redis_db, key,...)
else
ERROR("[redis do cmd] ERROR ", cmd,key, ...)
end
return result
end