go-redis源码分析(一)

3,141 阅读3分钟
type Client struct {
	*baseClient
	cmdable
	hooks
	ctx context.Context
}

Client用于对真实的baseClient进行封装,提供cmdable处理操作命令,hooks用于提供钩子函数,可以在处理命令前或处理后进行一些操作。 业务代码中通过如下函数创建Client对象:

func NewClient(opt *Options) *Client {
	opt.init()

	c := Client{
		baseClient: newBaseClient(opt, newConnPool(opt)),
		ctx:        context.Background(),
	}
	c.cmdable = c.Process

	return &c
}

baseClient用于封装连接池:

type baseClient struct {
	opt      *Options
	connPool pool.Pooler

	onClose func() error // hook called when client is closed
}

func newBaseClient(opt *Options, connPool pool.Pooler) *baseClient {
	return &baseClient{
		opt:      opt,
		connPool: connPool,
	}
}

最后真实的redis操作命令通过调用对应的api完成:

type baseCmd struct {
	ctx    context.Context
	args   []interface{} //命令参数 
	err    error 
	keyPos int8

	_readTimeout *time.Duration
}

type StringCmd struct {
	baseCmd

	val string
}

// Redis `GET key` command. It returns redis.Nil error when key does not exist.
func (c cmdable) Get(ctx context.Context, key string) *StringCmd {
    //根据参数组合操作命令 
	cmd := NewStringCmd(ctx, "get", key) 
    //调用c.Process进行真正的命令处理 
	_ = c(ctx, cmd)
	return cmd
}

Get在client上调用,比如:

cli := redis.NewClient(&redis.Options{
    Addr:     viper.GetString("redis.server"),
    Password: viper.GetString("redis.password"),
    DB:       0,
    MaxRetries: 3,
})
val,err := cli.Get(ctx, key).Result()

在Get中调用了c(ctx, cmd),实际调用的是c.cmdable,也就是c.Process,最终调用hooks.Process

func (hs hooks) process(
	ctx context.Context, cmd Cmder, fn func(context.Context, Cmder) error,
) error {
	if len(hs.hooks) == 0 {
        //这里执行redis命令的实际处理,调用的是c.baseClient.process
		err := hs.withContext(ctx, func() error {
			return fn(ctx, cmd)
		})
		cmd.SetErr(err)
		return err
	}

	var hookIndex int
	var retErr error

	for ; hookIndex < len(hs.hooks) && retErr == nil; hookIndex++ {
		ctx, retErr = hs.hooks[hookIndex].BeforeProcess(ctx, cmd)
		if retErr != nil {
			cmd.SetErr(retErr)
		}
	}

	if retErr == nil {
		retErr = hs.withContext(ctx, func() error {
			return fn(ctx, cmd)
		})
		cmd.SetErr(retErr)
	}

	for hookIndex--; hookIndex >= 0; hookIndex-- {
		if err := hs.hooks[hookIndex].AfterProcess(ctx, cmd); err != nil {
			retErr = err
			cmd.SetErr(retErr)
		}
	}

	return retErr
}

可以通过向hooks注入BeforeProcess和AfterProcess,在命令操作前后进行一些处理。所以Client对baseClient进行了封装,方便做一些钩子处理,最终真实调用的还是c.baseClient.process

func (c *baseClient) process(ctx context.Context, cmd Cmder) error {
	var lastErr error
	for attempt := 0; attempt <= c.opt.MaxRetries; attempt++ {
		attempt := attempt

		var retry bool
		err := internal.WithSpan(ctx, "redis.process", func(ctx context.Context, span trace.Span) error {
			if attempt > 0 {
                //重试时间间隔
				if err := internal.Sleep(ctx, c.retryBackoff(attempt)); err != nil {
					return err
				}
			}

			retryTimeout := uint32(1)
			err := c.withConn(ctx, func(ctx context.Context, cn *pool.Conn) error {
				err := cn.WithWriter(ctx, c.opt.WriteTimeout, func(wr *proto.Writer) error {
					return writeCmd(wr, cmd)
				})
				if err != nil {
					return err
				}

				err = cn.WithReader(ctx, c.cmdTimeout(cmd), cmd.readReply)
				if err != nil {
					if cmd.readTimeout() == nil {
						atomic.StoreUint32(&retryTimeout, 1)
					}
					return err
				}

				return nil
			})
			if err == nil {
				return nil
			}
            //shouldRetry根据err和retryTimeout判断是否需要重试
			retry = shouldRetry(err, atomic.LoadUint32(&retryTimeout) == 1)
			return err
		})
		if err == nil || !retry {
			return err
		}
		lastErr = err
	}
	return lastErr
}

在介绍c.baseClient.process之前,先介绍withSpan,withSpan主要用于判断是否需要对代码进行trace, span的名称redis.process

func WithSpan(ctx context.Context, name string, fn func(context.Context, trace.Span) error) error {
	if span := trace.SpanFromContext(ctx); !span.IsRecording() {
		return fn(ctx, span)
	}

	ctx, span := tracer.Start(ctx, name)
	defer span.End()

	return fn(ctx, span)
}

redis操作核心处理代码如下,在withConn时,也会trace代码,span名称redis.with_conn

err := c.withConn(ctx, func(ctx context.Context, cn *pool.Conn) error {
    //往redis连接中写入操作命令
    err := cn.WithWriter(ctx, c.opt.WriteTimeout, func(wr *proto.Writer) error {
        return writeCmd(wr, cmd)
    })
    if err != nil {
        return err
    }
   //从redis中读取响应结果 
    err = cn.WithReader(ctx, c.cmdTimeout(cmd), cmd.readReply)
    if err != nil {
        if cmd.readTimeout() == nil {
            atomic.StoreUint32(&retryTimeout, 1)
        }
        return err
    }

    return nil
})

withConn代码如下:

func (c *baseClient) withConn(
	ctx context.Context, fn func(context.Context, *pool.Conn) error,
) error {
	return internal.WithSpan(ctx, "redis.with_conn", func(ctx context.Context, span trace.Span) error {
        //从连接池获取连接
		cn, err := c.getConn(ctx)
		if err != nil {
			return err
		}

		if span.IsRecording() {
			if remoteAddr := cn.RemoteAddr(); remoteAddr != nil {
				span.SetAttributes(label.String("net.peer.ip", remoteAddr.String()))
			}
		}

		defer func() {
            //将连接放回连接池或者从连接池删除无效连接
			c.releaseConn(ctx, cn, err)
		}()
        //done是一个只读channel 
		done := ctx.Done()
      
		if done == nil {
			err = fn(ctx, cn)
			return err
		}

		errc := make(chan error, 1)
        //新启动协程进行redis读写操作
		go func() { errc <- fn(ctx, cn) }()

		select {
		case <-done:
			_ = cn.Close()
			// Wait for the goroutine to finish and send something.
			<-errc

			err = ctx.Err()
			return err
        //读写redis发生错误
		case err = <-errc:
			return err
		}
	})
}

下一篇文章再介绍c.getConn获取连接和连接池相关的内容。