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。这也给我们一个启示,不要害怕代码重复,要更关注代码可改动性。如果看着代码类似就合并,很容易造成后面代码改不动,改一个地方,影响其他很多地方

相关推荐
我不会起名字32215 小时前
Redis 从 0 到 1(三十三):机器断电 Redis 起不来——AOF/RDB 损坏的 4 步抢救流程
redis·数据恢复·aof·rdb·排查
JWASX15 小时前
【Go】map 源码解析 - group
后端·golang·map·源码分析
csdn_aspnet17 小时前
Redis 入门:Windows、Mac安装指南
数据库·windows·redis·缓存·mac
IT规划师18 小时前
Redis 数据类型全解 - HyperLogLog:12 KB 估出亿级 UV(八)
redis·uv·hyperloglog·.net10
aiks_yuan1 天前
Token 放 Redis 还是用 JWT:Java 实测验签和查 Redis 耗时,退出登录、改密码踢下线怎么做(jjwt 0.12 + Redis 7.2)
redis
露天赏雪1 天前
第3篇:Sentinel熔断降级实战:大模型接口抖动时的服务兜底方案
java·开发语言·人工智能·spring boot·spring·servlet·sentinel
联犀1 天前
纯 Go 的 CAD 开源库 go-cad:直接解析 DWG/DXF,现在也能导出 PDF
开发语言·golang·pdf
FungLeo2 天前
成为全栈·Go 后端篇·三轮代码审阅之后:把可读性和证据做成工程规则
golang·go·ast·静态检查·成为全栈·代码审阅
我不会起名字3222 天前
Redis 从 0 到 1(二十九):CPU 突然 100%——commandstats 定位法与 6 步排查链路
redis·cpu·慢查询·排查·commandstats