go-redis源码解析:如何实现sentinel高可用

go-redis里,sentinel只用来获取master和从节点的ip地址,在获取master和replica节点ip时,如果sentinel不可用,那么会换其他的sentinel重试,并将可用的sentinel换到第一个

1. 用于获取master节点

  1. 先通过读锁获取c.sentinel,使用c.sentinel获取主节点信息

  2. 如果上面sentinel为空或者出错未获取到主节点,那么可能在此中间有地方重新设置了c.sentinel,继续用读写锁,使用c.sentinel获取主节点信息

    1. 分读锁和读写锁这两步的原因是,提高并发性能
  3. 如果以上两步的sentinel都有问题,那么遍历所有的sentinelAddr,创建sentinel进行处理

  4. 最后如果所有的sentinel都不可用,则报错

1.1. 核心高可用代码

1.2. 方法内所有处理逻辑

javascript 复制代码
func (c *sentinelFailover) replicaAddrs(ctx context.Context, useDisconnected bool) ([]string, error) {
	// 1. 先通过读锁获取c.sentinel,使用c.sentinel获取主节点信息
	c.mu.RLock()
	sentinel := c.sentinel
	c.mu.RUnlock()

	if sentinel != nil {
		// 通过哨兵查询从节点
		addrs, err := c.getReplicaAddrs(ctx, sentinel)
		if err != nil {
			if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
				return nil, err
			}
			// Continue on other errors
			internal.Logger.Printf(ctx, "sentinel: Replicas name=%q failed: %s",
				c.opt.MasterName, err)
		} else if len(addrs) > 0 {
			return addrs, nil
		}
	}

	// 2. 如果上面sentinel为空或者出错未获取到主节点,那么可能在此中间有地方重新设置了c.sentinel,继续用读写锁,使用c.sentinel获取主节点信息
	// 分读锁和读写锁这两步的原因是,提高并发性能
	c.mu.Lock()
	defer c.mu.Unlock()

	if c.sentinel != nil {
		addrs, err := c.getReplicaAddrs(ctx, c.sentinel)
		if err != nil {
			_ = c.closeSentinel()
			if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
				return nil, err
			}
			// Continue on other errors
			internal.Logger.Printf(ctx, "sentinel: Replicas name=%q failed: %s",
				c.opt.MasterName, err)
		} else if len(addrs) > 0 {
			return addrs, nil
		} else {
			// No error and no replicas.
			_ = c.closeSentinel()
		}
	}

	// 3. 如果以上两步的sentinel都有问题,那么遍历所有的sentinelAddr,创建sentinel进行处理
	var sentinelReachable bool

	for i, sentinelAddr := range c.sentinelAddrs {
		sentinel := NewSentinelClient(c.opt.sentinelOptions(sentinelAddr))

		replicas, err := sentinel.Replicas(ctx, c.opt.MasterName, c.opt.ServerVersion).Result()
		if err != nil {
			_ = sentinel.Close()
			if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
				return nil, err
			}
			internal.Logger.Printf(ctx, "sentinel: Replicas master=%q failed: %s",
				c.opt.MasterName, err)
			continue
		}
		sentinelReachable = true
		addrs := parseReplicaAddrs(replicas, useDisconnected)
		if len(addrs) == 0 {
			continue
		}
		// Push working sentinel to the top.
		c.sentinelAddrs[0], c.sentinelAddrs[i] = c.sentinelAddrs[i], c.sentinelAddrs[0]
		c.setSentinel(ctx, sentinel)

		return addrs, nil
	}

  
	if sentinelReachable {
		return []string{}, nil
	}
  // 4. 最后如果所有的sentinel都不可用,则报错
	return []string{}, errors.New("redis: all sentinels specified in configuration are unreachable")
}

2. 用于获取replica从节点

和获取主节点方法处理逻辑类似

  1. 先通过读锁获取c.sentinel,使用c.sentinel获取从节点信息

  2. 如果上面sentinel为空或者出错未获取到从节点,那么可能在此中间有地方重新设置了c.sentinel,继续用读写锁,使用c.sentinel获取从节点信息

    1. 分读锁和读写锁这两步的原因是,提高并发性能
  3. 如果以上两步的sentinel都有问题,那么遍历所有的sentinelAddr,创建sentinel进行处理

  4. 最后如果所有的sentinel都不可用,则报错

javascript 复制代码
func (c *sentinelFailover) replicaAddrs(ctx context.Context, useDisconnected bool) ([]string, error) {
	c.mu.RLock()
	sentinel := c.sentinel
	c.mu.RUnlock()

	if sentinel != nil {
		// 通过哨兵查询从节点
		addrs, err := c.getReplicaAddrs(ctx, sentinel)
		if err != nil {
			if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
				return nil, err
			}
			// Continue on other errors
			internal.Logger.Printf(ctx, "sentinel: Replicas name=%q failed: %s",
				c.opt.MasterName, err)
		} else if len(addrs) > 0 {
			return addrs, nil
		}
	}

	c.mu.Lock()
	defer c.mu.Unlock()

	if c.sentinel != nil {
		addrs, err := c.getReplicaAddrs(ctx, c.sentinel)
		if err != nil {
			_ = c.closeSentinel()
			if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
				return nil, err
			}
			// Continue on other errors
			internal.Logger.Printf(ctx, "sentinel: Replicas name=%q failed: %s",
				c.opt.MasterName, err)
		} else if len(addrs) > 0 {
			return addrs, nil
		} else {
			// No error and no replicas.
			_ = c.closeSentinel()
		}
	}

	var sentinelReachable bool

	for i, sentinelAddr := range c.sentinelAddrs {
		sentinel := NewSentinelClient(c.opt.sentinelOptions(sentinelAddr))

		replicas, err := sentinel.Replicas(ctx, c.opt.MasterName, c.opt.ServerVersion).Result()
		if err != nil {
			_ = sentinel.Close()
			if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
				return nil, err
			}
			internal.Logger.Printf(ctx, "sentinel: Replicas master=%q failed: %s",
				c.opt.MasterName, err)
			continue
		}
		sentinelReachable = true
		addrs := parseReplicaAddrs(replicas, useDisconnected)
		if len(addrs) == 0 {
			continue
		}
		// Push working sentinel to the top.
		c.sentinelAddrs[0], c.sentinelAddrs[i] = c.sentinelAddrs[i], c.sentinelAddrs[0]
		c.setSentinel(ctx, sentinel)

		return addrs, nil
	}

	if sentinelReachable {
		return []string{}, nil
	}
	return []string{}, errors.New("redis: all sentinels specified in configuration are unreachable")
}

2.1. 使用sentinel获取主节点和从节点的大部分代码都类似,为什么不合并成一个方法

2.1.1. 语义清晰

对于外部调用方来讲,并不关心内部实现是否类似,要的只是获取主节点和从节点信息,分开两个方法,语义很清晰

2.1.2. 方便改动

如果底层合并成一个方法,很容易改动获取master节点的方法,影响到获取从节点的逻辑,造成bug。这也给我们一个启示,不要害怕代码重复,要更关注代码可改动性。如果看着代码类似就合并,很容易造成后面代码改不动,改一个地方,影响其他很多地方

相关推荐
roman_日积跬步-终至千里35 分钟前
【Go语言基础【20】】Go的包与工程
开发语言·后端·golang
让我上个超影吧2 小时前
黑马点评【基于redis实现共享session登录】
java·redis
懒羊羊大王呀5 小时前
Ubuntu20.04中 Redis 的安装和配置
linux·redis
John Song7 小时前
Redis 集群批量删除key报错 CROSSSLOT Keys in request don‘t hash to the same slot
数据库·redis·哈希算法
海奥华29 小时前
go中的接口返回设计思想
开发语言·后端·golang
飞川撸码13 小时前
【LeetCode 热题100】网格路径类 DP 系列题:不同路径 & 最小路径和(力扣62 / 64 )(Go语言版)
算法·leetcode·golang·动态规划
Zfox_15 小时前
Redis:Hash数据类型
服务器·数据库·redis·缓存·微服务·哈希算法
呼拉拉呼拉15 小时前
Redis内存淘汰策略
redis·缓存
咖啡啡不加糖20 小时前
Redis大key产生、排查与优化实践
java·数据库·redis·后端·缓存