使用 C# 的 StackExchange / Sentinel 进行 Redis 故障转移

Posted

技术标签:

【中文标题】使用 C# 的 StackExchange / Sentinel 进行 Redis 故障转移【英文标题】:Redis failover with StackExchange / Sentinel from C# 【发布时间】:2014-10-12 15:38:10 【问题描述】:

我们目前正在使用 Redis 2.8.4 和 StackExchange.Redis(并且很喜欢它),但目前没有任何针对硬件故障等的保护措施。我正在尝试让解决方案发挥作用,我们有主/从和哨兵监控,但不能完全到达那里,搜索后我无法找到任何真正的指针。

所以目前我们已经做到了这一点:

我们在每个节点上有 3 个 redis 服务器和哨兵(由 Linux 人员设置): devredis01:6383(主) devredis02:6383(从属) devredis03:6383(从属) devredis01:26379(哨兵) devredis02:26379(哨兵) devredis03:26379(哨兵)

我能够将 StackExchange 客户端连接到 redis 服务器并写入/读取并验证数据是否正在使用 Redis 桌面管理器跨所有 redis 实例复制。

我还可以使用不同的 ConnectionMultiplexer 连接到哨兵服务、查询配置、请求主 redis 节点、请求从属节点等。

我们还可以杀死主 redis 节点并验证其中一个从属被提升为主控并且复制到另一个从属继续工作。我们可以观察到 redis 连接尝试重新连接到 master,如果我重新创建 ConnectionMultiplexer,我可以再次写入/读取到新提升的 master 并从 slave 读取。

到目前为止一切顺利!

我缺少的一点是如何将它们整合到生产系统中?

我应该从 sentinel 获取 redis 端点并使用 2 个 ConnectionMultiplexers 吗? 我到底要做什么才能检测到节点已关闭? StackExchange 可以自动为我执行此操作,还是传递一个事件以便我可以重新连接我的 redis ConnectionMultiplexer? 我是否应该处理 ConnectionFailed 事件然后重新连接,以便 ConnectionMuliplexer 找出新的主设备是什么? 大概在我重新连接时,任何写入尝试都会丢失?

我希望我不会在这里遗漏一些非常明显的东西我只是在努力将它们放在一起。

提前致谢!

【问题讨论】:

“我还可以使用不同的 ConnectionMultiplexer 连接到哨兵服务、查询配置、请求主 redis 节点、请求从属节点等”是什么意思?你是怎么做到的? 【参考方案1】:

今天(我刚刚将 StackExchange.Redis 2.1.58 配置为使用哨兵)在 redis 连接字符串或 Configuration 中指定哨兵端点和 serviceName 就足够了。其余的都被封装为this commit 的一部分。因此,您只需将 stackexchange.redis 指向您的哨兵节点,ConnectionMuliplexer 会在您每次调用 GetDatabase() 时让您启动并运行 IDatabase。

var conn = ConnectionMultiplexer.Connect("sentinel:26379,serviceName=mymaster");
var db = conn.GetDatabase();
db.StringSet("key", "value");

【讨论】:

但是你是怎么得到GetServer(...)的呢?我需要搜索密钥等。 conn.GetServer(conn.GetDatabase().IdentifyEndpoint()).Keys() 等 这是正确的方法吗? GetServer(lazyRedisConnection.Value.GetEndPoints()[0]) ?我已经对此进行了测试,它工作正常(仅通过连接使用哨兵)。但是,如果某些 redis 节点或哨兵关闭,这也有效吗? 据我了解,IdentifyEndpoint() 更可取,或者如果您想调整它,可以查看this question(但我认为您不是真的想要) d_f 我已经测试了您的解决方案,它运行良好!谢谢!【参考方案2】:

我包含了我们的 Redis 包装器,它与原来的答案有所不同,原因有很多:

我们想使用 pub/sub Sentinel 似乎并不总是在“正确”的时间向我们提供 master 更改消息(即,我们调用了 Configure() 并最终认为 slave 是 master) connectionMultiplexer 似乎并不总是每次都恢复连接,影响 pub/sub

我宁愿怀疑这主要取决于我们的 sentinel/redis 配置。无论哪种方式,尽管进行了破坏性测试,但它并不完全可靠。此外,由于哨兵“过于敏感”并在没有任何故障转移时调用故障转移,我们不得不增加超时时间,因此主更改消息需要很长时间。我认为在虚拟环境中运行也会加剧这个问题。

现在我们不再监听订阅,而是每 5 秒尝试一次写入测试,并且还对 pub/sub 进行“收到的最后一条消息”检查。如果我们遇到任何问题,我们会完全剥离连接并重建它们。这似乎有点矫枉过正,但实际上它比等待来自哨兵的主更改消息更快而且仍然更快......

如果没有各种扩展方法和其他类等,这将无法编译,但你明白了。

namespace Smartodds.Framework.Redis

    public class RedisClient : IDisposable
    
        public RedisClient(RedisEnvironmentElement environment, Int32 databaseId)
        
            m_ConnectTimeout = environment.ConnectTimeout;
            m_Timeout = environment.Timeout;
            m_DatabaseId = databaseId;
            m_ReconnectTime = environment.ReconnectTime;
            m_CheckSubscriptionsTime = environment.CheckSubscriptions;
            if (environment.TestWrite == true)
            
                m_CheckWriteTime = environment.TestWriteTime;
            

            environment.Password.ToCharArray().ForEach((c) => m_Password.AppendChar(c));

            foreach (var server in environment.Servers)
            
                if (server.Type == ServerType.Redis)
                
                    // will be ignored if sentinel servers are used
                    m_RedisServers.Add(new RedisConnection  Address = server.Host, Port = server.Port );
                
                else
                
                    m_SentinelServers.Add(new RedisConnection  Address = server.Host, Port = server.Port );
                
            
        

        public bool IsSentinel  get  return m_SentinelServers.Count > 0;  

        public IDatabase Database  get  return _Redis.GetDatabase(m_DatabaseId);  

        private ConnectionMultiplexer _Redis
        
            get
            
                if (m_Connecting == true)
                
                    throw new RedisConnectionNotReadyException();
                

                ConnectionMultiplexer redis = m_Redis;
                if (redis == null)
                
                    throw new RedisConnectionNotReadyException();
                

                return redis;
            
        

        private ConnectionMultiplexer _Sentinel
        
            get
            
                if (m_Connecting == true)
                
                    throw new RedisConnectionNotReadyException("Sentinel connection not ready");
                

                ConnectionMultiplexer sentinel = m_Sentinel;
                if (sentinel == null)
                
                    throw new RedisConnectionNotReadyException("Sentinel connection not ready");
                

                return sentinel;
            
        

        public void RegisterSubscription(string channel, Action<RedisChannel, RedisValue> handler, Int32 maxNoReceiveSeconds)
        
            m_Subscriptions.Add(channel, new RedisSubscription
            
                Channel = channel,
                Handler = handler,
                MaxNoReceiveSeconds = maxNoReceiveSeconds,
                LastUsed = DateTime.UtcNow,
            );
        

        public void Connect()
        
            _Connect(true);
        

        private void _Connect(object state)
        
            bool throwException = (bool)state;

            // if a reconnect is already being attempted, don't hang around waiting
            if (Monitor.TryEnter(m_ConnectionLocker) == false)
            
                return;
            

            // we took the lock, notify everything we are connecting
            m_Connecting = true;

            try
            
                Stopwatch sw = Stopwatch.StartNew();
                LoggerQueue.Debug(">>>>>> REDIS CONNECTING... >>>>>>");

                // if this is a reconnect, make absolutely sure everything is cleaned up first
                _KillTimers();
                _KillRedisClient();

                if (this.IsSentinel == true && m_Sentinel == null)
                
                    LoggerQueue.Debug(">>>>>> CONNECTING TO SENTINEL >>>>>> - " + sw.Elapsed);

                    // we'll be getting the redis servers from sentinel
                    ConfigurationOptions sentinelConnection = _CreateRedisConfiguration(CommandMap.Sentinel, null, m_SentinelServers);
                    m_Sentinel = ConnectionMultiplexer.Connect(sentinelConnection);
                    LoggerQueue.Debug(">>>>>> CONNECTED TO SENTINEL >>>>>> - " + sw.Elapsed);

                    _OutputConfigurationFromSentinel();

                    // get all the redis servers from sentinel and ignore any set by caller
                    m_RedisServers.Clear();
                    m_RedisServers.AddRange(_GetAllRedisServersFromSentinel());

                    if (m_RedisServers.Count == 0)
                    
                        throw new RedisException("Sentinel found no redis servers");
                    
                

                LoggerQueue.Debug(">>>>>> CONNECTING TO REDIS >>>>>> - " + sw.Elapsed);

                // try to connect to all redis servers
                ConfigurationOptions connection = _CreateRedisConfiguration(CommandMap.Default, _SecureStringToString(m_Password), m_RedisServers);
                m_Redis = ConnectionMultiplexer.Connect(connection);
                LoggerQueue.Debug(">>>>>> CONNECTED TO REDIS >>>>>> - " + sw.Elapsed);

                // register subscription channels
                m_Subscriptions.ForEach(s =>
                
                    m_Redis.GetSubscriber().Subscribe(s.Key, (channel, value) => _SubscriptionHandler(channel, value));
                    s.Value.LastUsed = DateTime.UtcNow;
                );

                if (this.IsSentinel == true)
                
                    // check subscriptions have been sending messages
                    if (m_Subscriptions.Count > 0)
                    
                        m_CheckSubscriptionsTimer = new Timer(_CheckSubscriptions, null, 30000, m_CheckSubscriptionsTime);
                    

                    if (m_CheckWriteTime != null)
                    
                        // check that we can write to redis
                        m_CheckWriteTimer = new Timer(_CheckWrite, null, 32000, m_CheckWriteTime.Value);
                    

                    // monitor for connection status change to any redis servers
                    m_Redis.ConnectionFailed += _ConnectionFailure;
                    m_Redis.ConnectionRestored += _ConnectionRestored;
                

                LoggerQueue.Debug(string.Format(">>>>>> ALL REDIS CONNECTED (0) >>>>>>", sw.Elapsed));
            
            catch (Exception ex)
            
                LoggerQueue.Error(">>>>>> REDIS CONNECT FAILURE >>>>>>", ex);

                if (throwException == true)
                
                    throw;
                
                else
                
                    // internal reconnect, the reconnect has failed so might as well clean everything and try again
                    _KillTimers();
                    _KillRedisClient();

                    // faster than usual reconnect if failure
                    _ReconnectTimer(1000);
                
            
            finally
            
                // finished connection attempt, notify everything and remove lock
                m_Connecting = false;
                Monitor.Exit(m_ConnectionLocker);
            
        

        private ConfigurationOptions _CreateRedisConfiguration(CommandMap commandMap, string password, List<RedisConnection> connections)
        
            ConfigurationOptions connection = new ConfigurationOptions
            
                CommandMap = commandMap,
                AbortOnConnectFail = true,
                AllowAdmin = true,
                ConnectTimeout = m_ConnectTimeout,
                SyncTimeout = m_Timeout,
                ServiceName = "master",
                TieBreaker = string.Empty,
                Password = password,
            ;

            connections.ForEach(s =>
            
                connection.EndPoints.Add(s.Address, s.Port);
            );

            return connection;
        

        private void _OutputConfigurationFromSentinel()
        
            m_SentinelServers.ForEach(s =>
            
                try
                
                    IServer server = m_Sentinel.GetServer(s.Address, s.Port);
                    if (server.IsConnected == true)
                    
                        try
                        
                            IPEndPoint master = server.SentinelGetMasterAddressByName("master") as IPEndPoint;
                            var slaves = server.SentinelSlaves("master");

                            StringBuilder sb = new StringBuilder();
                            sb.Append(">>>>>> _OutputConfigurationFromSentinel Server ");
                            sb.Append(s.Address);
                            sb.Append(" thinks that master is ");
                            sb.Append(master);
                            sb.Append(" and slaves are ");

                            foreach (var slave in slaves)
                            
                                string name = slave.Where(i => i.Key == "name").Single().Value;
                                bool up = slave.Where(i => i.Key == "flags").Single().Value.Contains("disconnected") == false;

                                sb.Append(name);
                                sb.Append("(");
                                sb.Append(up == true ? "connected" : "down");
                                sb.Append(") ");
                            
                            sb.Append(">>>>>>");
                            LoggerQueue.Debug(sb.ToString());
                        
                        catch (Exception ex)
                        
                            LoggerQueue.Error(string.Format(">>>>>> _OutputConfigurationFromSentinel Could not get configuration from sentinel server (0) >>>>>>", s.Address), ex);
                        
                    
                    else
                    
                        LoggerQueue.Error(string.Format(">>>>>> _OutputConfigurationFromSentinel Sentinel server 0 was not connected", s.Address));
                    
                
                catch (Exception ex)
                
                    LoggerQueue.Error(string.Format(">>>>>> _OutputConfigurationFromSentinel Could not get IServer from sentinel (0) >>>>>>", s.Address), ex);
                
            );
        

        private RedisConnection[] _GetAllRedisServersFromSentinel()
        
            // ask each sentinel server for its configuration
            List<RedisConnection> redisServers = new List<RedisConnection>();
            m_SentinelServers.ForEach(s =>
            
                try
                
                    IServer server = m_Sentinel.GetServer(s.Address, s.Port);
                    if (server.IsConnected == true)
                    
                        try
                        
                            // store master in list
                            IPEndPoint master = server.SentinelGetMasterAddressByName("master") as IPEndPoint;
                            redisServers.Add(new RedisConnection  Address = master.Address.ToString(), Port = master.Port );

                            var slaves = server.SentinelSlaves("master");
                            foreach (var slave in slaves)
                            
                                string address = slave.Where(i => i.Key == "ip").Single().Value;
                                string port = slave.Where(i => i.Key == "port").Single().Value;

                                redisServers.Add(new RedisConnection  Address = address, Port = Convert.ToInt32(port) );
                            
                        
                        catch (Exception ex)
                        
                            LoggerQueue.Error(string.Format(">>>>>> _GetAllRedisServersFromSentinel Could not get redis servers from sentinel server (0) >>>>>>", s.Address), ex);
                        
                    
                    else
                    
                        LoggerQueue.Error(string.Format(">>>>>> _GetAllRedisServersFromSentinel Sentinel server 0 was not connected", s.Address));
                    
                
                catch (Exception ex)
                
                    LoggerQueue.Error(string.Format(">>>>>> _GetAllRedisServersFromSentinel Could not get IServer from sentinel (0) >>>>>>", s.Address), ex);
                
            );

            return redisServers.Distinct().ToArray();
        

        private IServer _GetRedisMasterFromSentinel()
        
            // ask each sentinel server for its configuration
            foreach (RedisConnection sentinel in m_SentinelServers)
            
                IServer sentinelServer = _Sentinel.GetServer(sentinel.Address, sentinel.Port);
                if (sentinelServer.IsConnected == true)
                
                    try
                    
                        IPEndPoint master = sentinelServer.SentinelGetMasterAddressByName("master") as IPEndPoint;
                        return _Redis.GetServer(master);
                    
                    catch (Exception ex)
                    
                        LoggerQueue.Error(string.Format(">>>>>> Could not get redis master from sentinel server (0) >>>>>>", sentinel.Address), ex);
                    
                
            

            throw new InvalidOperationException("No sentinel server available to get master");
        

        private void _ReconnectTimer(Nullable<Int32> reconnectMilliseconds)
        
            try
            
                lock (m_ReconnectLocker)
                
                    if (m_ReconnectTimer != null)
                    
                        m_ReconnectTimer.Dispose();
                        m_ReconnectTimer = null;
                    

                    // since a reconnect will definately occur we can stop the check timers for now until reconnect succeeds (where they are recreated)
                    _KillTimers();

                    LoggerQueue.Warn(">>>>>> REDIS STARTING RECONNECT TIMER >>>>>>");

                    m_ReconnectTimer = new Timer(_Connect, false, reconnectMilliseconds.GetValueOrDefault(m_ReconnectTime), Timeout.Infinite);
                
            
            catch (Exception ex)
            
                LoggerQueue.Error("Error during _ReconnectTimer", ex);
            
        

        private void _CheckSubscriptions(object state)
        
            if (Monitor.TryEnter(m_ConnectionLocker, TimeSpan.FromSeconds(1)) == false)
            
                return;
            

            try
            
                DateTime now = DateTime.UtcNow;
                foreach (RedisSubscription subscription in m_Subscriptions.Values)
                
                    if ((now - subscription.LastUsed) > TimeSpan.FromSeconds(subscription.MaxNoReceiveSeconds))
                    
                        try
                        
                            EndPoint endpoint = m_Redis.GetSubscriber().IdentifyEndpoint(subscription.Channel);
                            EndPoint subscribedEndpoint = m_Redis.GetSubscriber().SubscribedEndpoint(subscription.Channel);

                            LoggerQueue.Warn(string.Format(">>>>>> REDIS Channel '0' has not been used for longer than 1s, IsConnected: 2, IsConnectedChannel: 3, EndPoint: 4, SubscribedEndPoint: 5, reconnecting...", subscription.Channel, subscription.MaxNoReceiveSeconds, m_Redis.GetSubscriber().IsConnected(), m_Redis.GetSubscriber().IsConnected(subscription.Channel), endpoint != null ? endpoint.ToString() : "null", subscribedEndpoint != null ? subscribedEndpoint.ToString() : "null"));
                        
                        catch (Exception ex)
                        
                            LoggerQueue.Error(string.Format(">>>>>> REDIS Error logging out details of Channel '0' reconnect", subscription.Channel), ex);
                        

                        _ReconnectTimer(null);
                        return;
                    
                
            
            catch (Exception ex)
            
                LoggerQueue.Error(">>>>>> REDIS Exception ERROR during _CheckSubscriptions", ex);
            
            finally
            
                Monitor.Exit(m_ConnectionLocker);
            
        

        private void _CheckWrite(object state)
        
            if (Monitor.TryEnter(m_ConnectionLocker, TimeSpan.FromSeconds(1)) == false)
            
                return;
            

            try
            
                this.Database.HashSet(Environment.MachineName + "SmartoddsWriteCheck", m_CheckWriteGuid.ToString(), DateTime.UtcNow.Ticks);
            
            catch (RedisConnectionNotReadyException)
            
                LoggerQueue.Warn(">>>>>> REDIS RedisConnectionNotReadyException ERROR DURING _CheckWrite");
            
            catch (RedisServerException ex)
            
                LoggerQueue.Warn(">>>>>> REDIS RedisServerException ERROR DURING _CheckWrite, reconnecting... - " + ex.Message);
                _ReconnectTimer(null);
            
            catch (RedisConnectionException ex)
            
                LoggerQueue.Warn(">>>>>> REDIS RedisConnectionException ERROR DURING _CheckWrite, reconnecting... - " + ex.Message);
                _ReconnectTimer(null);
            
            catch (TimeoutException ex)
            
                LoggerQueue.Warn(">>>>>> REDIS TimeoutException ERROR DURING _CheckWrite - " + ex.Message);
            
            catch (Exception ex)
            
                LoggerQueue.Error(">>>>>> REDIS Exception ERROR during _CheckWrite", ex);
            
            finally
            
                Monitor.Exit(m_ConnectionLocker);
            
        

        private void _ConnectionFailure(object sender, ConnectionFailedEventArgs e)
        
            LoggerQueue.Warn(string.Format(">>>>>> REDIS CONNECTION FAILURE, 0, 1, 2 >>>>>>", e.ConnectionType, e.EndPoint.ToString(), e.FailureType));
        

        private void _ConnectionRestored(object sender, ConnectionFailedEventArgs e)
        
            LoggerQueue.Warn(string.Format(">>>>>> REDIS CONNECTION RESTORED, 0, 1, 2 >>>>>>", e.ConnectionType, e.EndPoint.ToString(), e.FailureType));
        

        private void _SubscriptionHandler(string channel, RedisValue value)
        
            // get handler lookup
            RedisSubscription subscription = null;
            if (m_Subscriptions.TryGetValue(channel, out subscription) == false || subscription == null)
            
                return;
            

            // update last used
            subscription.LastUsed = DateTime.UtcNow;

            // call handler
            subscription.Handler(channel, value);
        

        public Int64 Publish(string channel, RedisValue message)
        
            try
            
                return _Redis.GetSubscriber().Publish(channel, message);
            
            catch (RedisConnectionNotReadyException)
            
                LoggerQueue.Error("REDIS RedisConnectionNotReadyException ERROR DURING Publish");
                throw;
            
            catch (RedisServerException ex)
            
                LoggerQueue.Error("REDIS RedisServerException ERROR DURING Publish - " + ex.Message);
                throw;
            
            catch (RedisConnectionException ex)
            
                LoggerQueue.Error("REDIS RedisConnectionException ERROR DURING Publish - " + ex.Message);
                throw;
            
            catch (TimeoutException ex)
            
                LoggerQueue.Error("REDIS TimeoutException ERROR DURING Publish - " + ex.Message);
                throw;
            
            catch (Exception ex)
            
                LoggerQueue.Error("REDIS Exception ERROR DURING Publish", ex);
                throw;
            
        

        public bool LockTake(RedisKey key, RedisValue value, TimeSpan expiry)
        
            return _Execute(() => this.Database.LockTake(key, value, expiry));
        

        public bool LockExtend(RedisKey key, RedisValue value, TimeSpan extension)
        
            return _Execute(() => this.Database.LockExtend(key, value, extension));
        

        public bool LockRelease(RedisKey key, RedisValue value)
        
            return _Execute(() => this.Database.LockRelease(key, value));
        

        private void _Execute(Action action)
        
            try
            
                action.Invoke();
            
            catch (RedisServerException ex)
            
                LoggerQueue.Error("REDIS RedisServerException ERROR DURING _Execute - " + ex.Message);
                throw;
            
            catch (RedisConnectionException ex)
            
                LoggerQueue.Error("REDIS RedisConnectionException ERROR DURING _Execute - " + ex.Message);
                throw;
            
            catch (TimeoutException ex)
            
                LoggerQueue.Error("REDIS TimeoutException ERROR DURING _Execute - " + ex.Message);
                throw;
            
            catch (Exception ex)
            
                LoggerQueue.Error("REDIS Exception ERROR DURING _Execute", ex);
                throw;
            
        

        private TResult _Execute<TResult>(Func<TResult> function)
        
            try
            
                return function.Invoke();
            
            catch (RedisServerException ex)
            
                LoggerQueue.Error("REDIS RedisServerException ERROR DURING _Execute - " + ex.Message);
                throw;
            
            catch (RedisConnectionException ex)
            
                LoggerQueue.Error("REDIS RedisConnectionException ERROR DURING _Execute - " + ex.Message);
                throw;
            
            catch (TimeoutException ex)
            
                LoggerQueue.Error("REDIS TimeoutException ERROR DURING _Execute - " + ex.Message);
                throw;
            
            catch (Exception ex)
            
                LoggerQueue.Error("REDIS ERROR DURING _Execute", ex);
                throw;
            
        

        public string[] GetAllKeys(string pattern)
        
            if (m_Sentinel != null)
            
                return _GetAnyRedisSlaveFromSentinel().Keys(m_DatabaseId, pattern).Select(k => (string)k).ToArray();
            
            else
            
                return _Redis.GetServer(_Redis.GetEndPoints().First()).Keys(m_DatabaseId, pattern).Select(k => (string)k).ToArray();
            
        

        private void _KillSentinelClient()
        
            try
            
                if (m_Sentinel != null)
                
                    LoggerQueue.Debug(">>>>>> KILLING SENTINEL CONNECTION >>>>>>");

                    ConnectionMultiplexer sentinel = m_Sentinel;
                    m_Sentinel = null;

                    sentinel.Close(false);
                    sentinel.Dispose();
                
            
            catch (Exception ex)
            
                LoggerQueue.Error(">>>>>> Error during _KillSentinelClient", ex);
            
        

        private void _KillRedisClient()
        
            try
            
                if (m_Redis != null)
                
                    Stopwatch sw = Stopwatch.StartNew();
                    LoggerQueue.Debug(">>>>>> KILLING REDIS CONNECTION >>>>>>");

                    ConnectionMultiplexer redis = m_Redis;
                    m_Redis = null;

                    if (this.IsSentinel == true)
                    
                        redis.ConnectionFailed -= _ConnectionFailure;
                        redis.ConnectionRestored -= _ConnectionRestored;
                    

                    redis.Close(false);
                    redis.Dispose();

                    LoggerQueue.Debug(">>>>>> KILLED REDIS CONNECTION >>>>>> " + sw.Elapsed);
                
            
            catch (Exception ex)
            
                LoggerQueue.Error(">>>>>> Error during _KillRedisClient", ex);
            
        

        private void _KillClients()
        
            lock (m_ConnectionLocker)
            
                _KillSentinelClient();
                _KillRedisClient();
            
        

        private void _KillTimers()
        
            if (m_CheckSubscriptionsTimer != null)
            
                m_CheckSubscriptionsTimer.Dispose();
                m_CheckSubscriptionsTimer = null;
            
            if (m_CheckWriteTimer != null)
            
                m_CheckWriteTimer.Dispose();
                m_CheckWriteTimer = null;
            
        

        public void Dispose()
        
            _KillClients();
            _KillTimers();
        
    

【讨论】:

自 Redis 2.8 新版本以来,这种方法是否有任何变化?收听 Sentinel pub/sub 还是不如这种方法吗?【参考方案3】:

上周我花了一些时间与 Linux 人员一起测试场景并在此实现的 C# 方面工作,并且正在使用以下方法:

从配置中读取哨兵地址并创建一个 ConnectionMultiplexer 来连接它们 订阅 +switch-master 频道 依次询问每个哨兵服务器他们认为主 redis 和从服务器是什么,比较它们以确保它们都同意 使用从 sentinel 读取的 redis 服务器地址创建一个新的 ConnectionMultiplexer 并连接,将事件处理程序添加到 ConnectionFailed 和 ConnectionRestored。 当我收到 +switch-master 消息时,我在 redis ConnectionMultiplexer 上调用 Configure() 作为一种带和大括号的方法,当连接类型为 ConnectionType.Interactive 时,我总是在收到 connectionFailed 或 connectionRestored 事件 12 秒后在 redis ConnectionMultiplexer 上调用 Configure()。

我发现通常在失去 redis 主服务器大约 5 秒后我正在工作和重新配置。在这段时间里,我不能写,但我可以读(因为你可以读一个奴隶)。 5 秒对我们来说是可以的,因为我们的数据更新得非常快,几秒钟后就变得陈旧(随后被覆盖)。

我不确定的一件事是,当实例出现故障时,我是否应该从 redis ConnectionMultiplexer 中删除 redis 服务器,或者让它继续重试连接。我决定让它重试,因为它一恢复就作为奴隶回到混音中。我做了一些性能测试,有没有重试连接,似乎没什么区别。也许有人可以澄清这是否是正确的方法。

时不时带回一个以前是 master 的实例似乎确实会引起一些混乱 - 在它恢复几秒钟后,我会收到一个写入异常 - “READONLY”表明我无法写入奴隶。这种情况很少见,但我发现我在连接状态更改后 12 秒调用 Configure() 的“包罗万象”方法遇到了这个问题。调用 Configure() 似乎很便宜,因此无论是否有必要都调用它两次似乎没问题。

现在我有了奴隶,我已经卸载了一些数据清理代码,这些代码对奴隶进行键扫描,这让我很高兴。

总而言之,我很满意,它并不完美,但对于很少发生的事情来说已经足够了。

【讨论】:

我用我们最新的策略和一些代码添加了另一个答案。我达到了字符数限制,所以我不得不删除大块代码(包括成员变量),但我希望它没有失去意义。 我们有没有机会在您调用 Configure() 的方法上达到顶峰?它们不包括在内,因此在调用该方法之前是否/如何重新配置​​ redis ConnectionMultiplexer 并不明显【参考方案4】:

我刚刚问了这个问题,发现了一个与您和我的问题类似的问题,我相信它回答了我们的代码(客户端)现在如何知道当前主服务器出现故障时哪个是新的主服务器的问题?

How to tell a Client where the new Redis master is using Sentinel

显然,您只需要订阅和收听来自 Sentinel 的事件。有道理..我只是想有一种更精简的方式。

我读到了一些关于 Linux 的 Twemproxy 的文章,它充当代理,可能为你做这件事?但我在 Windows 的 redis 上,并试图找到一个 Windows 选项。如果这是经过批准的方式,我们可能会迁移到 Linux。

【讨论】:

谢谢 Ben,我上周确实在玩这个,正要来回答我自己的问题。稍后我将更详细地分享我所做的事情以及遇到的问题 - 你所说的似乎是做到这一点的方法。

以上是关于使用 C# 的 StackExchange / Sentinel 进行 Redis 故障转移的主要内容,如果未能解决你的问题,请参考以下文章

如何从 C# StackExchange.Redis 获取多个 Redis 键的 TTL

Redis C#缓存的使用

扩展 StackExchange.Redis 支持实体

干货.NET/C#中使用Redis

StackExchange.Redis helper访问类封装

“在 C# 中使用‘部分’类的好处”? [复制]