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获取连接和连接池相关的内容。